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    /// Unlimited-OCR: the DASH router and page state (`uocr::Dash`).
150    pub dash: Option<Box<crate::uocr::Dash>>,
151    /// Its prompt chunks ride the batch graph until that declines.
152    dash_batch_on: bool,
153    /// The decode token's mHC streams, `[streams · hidden]`.
154    mhc_r: Vec<f32>,
155    /// Interleaved-MRoPE sections and the prompt rows' (t, h, w) positions
156    /// for the prefill of an image prompt (SenseNova-Loop); cleared when
157    /// the prefill ends.
158    pub mrope_sections: Option<[usize; 3]>,
159    pub prompt_mrope: Option<Vec<[i64; 3]>>,
160    pub vocab_size: usize,
161    pub rms_eps: f64,
162    pub rope_base: f32,
163    pub norm_style: NormStyle,
164    /// RoPE dims actually rotated (≤ head_dim; Qwen3.5 uses head_dim/4).
165    pub rotary_dim: usize,
166    /// Optional Q-head count override for each attention layer (Laguna).
167    pub attention_heads_per_layer: Option<Vec<usize>>,
168    /// Optional KV-head count of each PHYSICAL attention layer (MiMo-V2:
169    /// 4 on full-attention layers, 8 on sliding ones). Set only through
170    /// [`Pipeline::set_attn_geometry`], which also reshapes the layer
171    /// caches. None = every layer has `num_kv_heads`.
172    pub kv_heads_per_layer: Option<Vec<usize>>,
173    /// Width of each V head when it is narrower than `head_dim` (MiMo-V2:
174    /// 128 against 192). V is zero-padded to `head_dim` inside the cache
175    /// and the attention output is compacted back to nh·v_head_dim before
176    /// o_proj (see `QwenAttnCfg::v_head_dim`). None = `head_dim`.
177    pub v_head_dim: Option<usize>,
178    /// `CMF_LAYER_DUMP=<dir>` (read once at construction; tests set it
179    /// directly): the hidden state after every layer, for every position,
180    /// as raw little-endian f32 files `p{pos:06}_l{li:02}.f32` of
181    /// `hidden_size` floats each. Written by the CPU layer walks — the
182    /// batched prefill (`prefill_batch_span`) and the single-token forward
183    /// (`forward_layers_span`) — so both prompt ingest and decode can be
184    /// diffed layer by layer against an external oracle (tools/mimo_ref).
185    /// Layers a device graph runs (wgpu token/batch graph, Metal chunk or
186    /// block graphs) and the own-stack families (DeepSeek-V4/V4.1,
187    /// Qwen3.8-Flash-Next, Gemma-3n) are not dumped: run with CMF_GPU=0
188    /// for a complete set. `li` is the virtual layer index. Final logits:
189    /// `CMF_LOGIT_DUMP=<file>` (hidden + logits of the first decode step).
190    pub layer_dump: Option<std::path::PathBuf>,
191    /// GPU-graph declines already logged for this pipeline, as (graph
192    /// site, reason) — one line each, see `graph_attn_decline_reason`.
193    graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
194    /// MiMo-V2 expert placement (prefix / dynamic bank / hybrid), decided
195    /// on the first forward — see `crate::mimo_moe`.
196    pub(crate) mimo_moe: crate::mimo_moe::Slot,
197    /// Opt-in MiniCPM q4tp expert cache for a graph-produced CPU tail.
198    /// It is deliberately separate from MiMo's placement policy because it
199    /// serves only the stock MiniCPM top-8 geometry and otherwise falls back
200    /// to the ordinary exact host MoE path.
201    pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
202    /// Linear-core geometry (present when the model has linear layers).
203    pub vmf_cfg: Option<VmfPhaseCfg>,
204    /// GatedDeltaNet geometry (faithful vendor operator).
205    pub gdn_cfg: Option<GdnCfg>,
206    /// MiniCPM-class logit scale (tied lm_head → cannot fold into weights).
207    pub logit_multiplier: Option<f32>,
208    /// Cooperative cancel: set from any thread (FFI `cortiq_cancel`,
209    /// a dropped server connection); the generate loop checks it at
210    /// every prefill chunk and decode step and finishes with
211    /// `finish_reason: "cancelled"`. Auto-cleared when honoured.
212    pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
213    /// A GPU graph failure is distinct from a user/request cancellation.
214    /// Graph code sets this before raising the cooperative cancel flag so the
215    /// generation API can return an error instead of reporting a successful
216    /// `finish_reason: cancelled` result.
217    graph_failed: std::sync::atomic::AtomicBool,
218    /// Resident-graph MoE expert banks, built once per layer
219    /// (`GraphMoeBank`). `CMF_GRAPH_MOE_BANK=0` rebuilds them every token.
220    graph_moe_banks: std::sync::Mutex<std::collections::HashMap<GraphMoeKey, GraphMoeBank>>,
221    /// Token ids currently materialized in the KV cache (the forwarded
222    /// prompt + all generated tokens except the last, which is sampled
223    /// but not yet forwarded). Lets the next generate call prefill only
224    /// the suffix when a chat app resends the whole history.
225    pub kv_history: Vec<u32>,
226    /// Owner tag of `kv_history`: the resident device graph held that
227    /// sequence (true) or the host did. Reuse continues only on the owner.
228    pub kv_history_device: bool,
229    /// KDA geometry (Kimi Linear / Kimi-K3) — shared by every Kda layer.
230    pub kda_cfg: Option<crate::linear_core::KdaCfg>,
231    /// Gemma-3n stack (AltUp/LAuReL/PLE/KV-sharing): its own forward —
232    /// weights.layers stays empty, the KV caches are the shared ones.
233    pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
234    /// Gemma-4 E-series stack (dual-geometry gemma4 + PLE + shared KV):
235    /// its own per-token forward, same contract as `g3n`.
236    pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
237    /// DeepSeek-V4 runs its own stack too: its hidden state is `hc_mult`
238    /// copies of a vector, so no loop written for a single residual
239    /// stream can carry it.
240    pub dsv4: Option<
241        Box<(
242            crate::dsv4::Dsv4Globals,
243            Vec<crate::dsv4::Dsv4Layer>,
244            crate::dsv4::Dsv4Cfg,
245            crate::dsv4::Dsv4State,
246        )>,
247    >,
248    /// DeepSeek-V4.1 owns the shared CED/CSA2 attention state, raw Engram
249    /// lookup and four-stream mHC handoff. It cannot use the V4 cache
250    /// layout, so it has a dedicated executor and state tuple.
251    /// Dory: a looped normalized GPT, its weights and KV state (`dory`).
252    pub dory: Option<Box<(crate::dory::Dory, crate::dory::DoryState)>>,
253    pub dsv41: Option<
254        Box<(
255            crate::dsv41::Dsv41Globals,
256            Vec<crate::dsv41::Dsv41Layer>,
257            crate::dsv41::Dsv41Cfg,
258            crate::dsv41::Dsv41State,
259        )>,
260    >,
261    /// Optional V4.1 vision tower. Text-only files leave this unset.
262    pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
263    /// Prepared image rows consumed by the next V4.1 prefill.
264    dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
265    /// Qwen3.8-Flash-Next owns four residual streams plus QSA/PLE state;
266    /// the generic single-residual layer loop cannot represent it.
267    /// Block-diffusion generation (Clover): preview weights and the
268    /// decoding controls; `None` on every causal model.
269    pub(crate) clover: Option<std::sync::Arc<clover::Clover>>,
270    clover_stats: clover::CloverStats,
271    /// The causal decode's response penalties (`clover_causal_logits`).
272    clover_pen: clover::Penalties,
273    pub qwen4_exp: Option<
274        Box<(
275            crate::qwen4_exp::Globals,
276            Vec<crate::qwen4_exp::Layer>,
277            crate::qwen4_exp::Cfg,
278            crate::qwen4_exp::State,
279        )>,
280    >,
281    /// DeepSeek-V4's own speculation stack: three draft modules, each a full
282    /// layer, plus a confidence head on the last. Empty when the file has
283    /// none, which is the only signal the decode path needs.
284    pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
285    /// The draft's per-sequence state (KV rings, captured trunk hidden).
286    pub dspark: Option<crate::dsv4::DsparkState>,
287    /// Drafts awaiting their verdict: (position, proposals, still matching,
288    /// accepted so far).
289    pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
290    /// Accepted prefix length of every graded draft.
291    pub dspark_hist: Vec<usize>,
292    /// The real tokens the drafts were graded against — a degenerate,
293    /// repeating output would make any acceptance number meaningless, and
294    /// the cheapest guard against believing one is to count them.
295    pub dspark_real: Vec<u32>,
296    /// The trunk's expert picks for the last few tokens, per layer. The
297    /// union over a window of them is what a batched verify would have to
298    /// read, and the ratio to the pick count is all it could save.
299    pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
300    /// (unique, total) expert picks per draft, trunk side and draft side.
301    pub dspark_exp: Vec<(usize, usize, usize, usize)>,
302    /// Wall time spent in the deliberately out-of-core draft. Kept separate
303    /// from trunk decode so block batching can be judged without conflating
304    /// it with GPU chain variance.
305    pub dspark_draft_ns: u128,
306    /// Per-layer winners of the last pooled route — the admission predictor.
307    m3_winners: Vec<Vec<u32>>,
308    /// Route frequency per (layer, expert) — the bank's prewarm profile.
309    m3_counts: Vec<u32>,
310    m3_n_exp: usize,
311    m3_tokens: u64,
312    m3_prewarmed: bool,
313    /// LFM2 short-convolution geometry (present when the model has
314    /// `ShortConv` mixer layers).
315    pub short_conv_cfg: Option<ShortConvCfg>,
316    /// Multi-token-prediction head (None = absent).
317    pub mtp: Option<MtpModule>,
318    /// MiMo-V2's draft stack (three chained MTP layers from the
319    /// `<stem>.mtp.cmf` sidecar); None = absent. Speculative greedy decode
320    /// uses it unless `CMF_MTP=0` / `CMF_MIMO_MTP=0`.
321    pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
322    /// Set while the MiMo speculative verify runs `prefill_batch`: its MoE
323    /// layers take `moe_ffn_rows_exact` (each row bit-identical to decode).
324    verify_exact_moe: bool,
325    /// Speculative decode via MTP (greedy only; `CMF_MTP=0` disables).
326    pub speculative: bool,
327    /// Keep generating past end-of-sequence ids (the llama-bench contract
328    /// for a timed run). A loop flag, deliberately NOT a sampler
329    /// suppression: suppressed ids count as a penalty and switch the
330    /// speculative round and the greedy burst off, so a benchmark that
331    /// suppressed EOS never measured either.
332    pub ignore_eos: bool,
333    /// Draft-head shortlist guard: tokens left during which the draft
334    /// uses the FULL head because a recently committed id lay past the
335    /// `CMF_DRAFT_VOCAB` cut (Cyrillic and CJK ids sit above 131072 in
336    /// Qwen's table, so a prefix shortlist would draft nothing usable
337    /// there — measured on Russian prose: 2.9 → 1.6 accepted a round).
338    pub draft_full_streak: u32,
339    /// Adaptive draft depth for the speculative round (None until the
340    /// first round): grows while nearly every draft is accepted, shrinks
341    /// when fewer than half are. The verify's cost climbs with the rows on
342    /// a discrete card (RTX PRO 4000: 52 ms at 2 rows, 74 at 5, 80 at 6),
343    /// so prose wants k≈3 and code or the repetitive bench k≈5 — measured
344    /// 33.6 vs 27.6 tok/s on an essay at k=3 vs 5, 45.6 vs 38 on code.
345    /// `CMF_GRAPH_SPEC_K` pins it.
346    pub spec_k_adapt: Option<usize>,
347    /// EWMA of the accepted fraction that drives `spec_k_adapt`.
348    pub spec_acc_ewma: f32,
349    /// Native Metal over a q4tp trunk: how often draft `j` holds once the
350    /// one before it held, a running average (see `metal_spec_depth`).
351    pub spec_hold: [f32; 7],
352    /// wgpu: what a round of k drafts costs on this device, a running
353    /// average of its wall in ms (0 until a depth has run twice), the rounds
354    /// each depth has run, and the rounds the depth still stays where the
355    /// last change put it (`wgpu_spec_depth`).
356    spec_wall: [f32; 7],
357    spec_wall_n: [u8; 7],
358    spec_k_dwell: u8,
359    /// wgpu: a plain token's time as a speculation trial last measured it,
360    /// and the position it was measured at. A later request's trial decides
361    /// from it instead of decoding eight plain tokens again.
362    spec_plain_ms: (f64, usize),
363    /// The same for the DFlash drafter's rounds, and whether the last round
364    /// was one of them.
365    pub dflash_hold: [f32; 7],
366    dflash_last: bool,
367    /// Rounds the DFlash block still sits out, the wait its next lost turn
368    /// costs, and the rounds of its turn so far.
369    dflash_wait: u32,
370    dflash_back: u32,
371    dflash_turn: u32,
372    /// The tokens the turn's rounds landed and what they cost, in plain tokens.
373    dflash_turn_sum: (f32, f32),
374    /// The sidecar's router: how far the target's last rows lean to the
375    /// block's rounds (a running average of `Route::margin`).
376    dflash_lean: f32,
377    /// The depth of the block's rounds on a card whose verify grows with its
378    /// rows, and the running share of drafts they keep (`spec_k_adapt`'s twin).
379    dflash_k_adapt: Option<usize>,
380    dflash_acc_ewma: f32,
381    /// Prompt lookup on native Metal: the drafts it added to a round that
382    /// held, a running average, and the rounds it sits out after that fell
383    /// under `LOOKUP_PAYS`.
384    pub spec_lookup_gain: f32,
385    /// CUDA: the context lookup may fill a 16-row verify window (after a
386    /// lookup round whose drafts all held), else 8 rows (one price there).
387    pub spec_lookup_wide: bool,
388    pub spec_lookup_rest: u32,
389    rng: SplitMix64,
390    sampler_scratch: SamplerScratch,
391    /// Speculative SAMPLING state (graph_spec_step, temperature > 0): the
392    /// correction token a rejected draft produced — committed by the loop
393    /// top in place of a fresh draw — and the per-round draft
394    /// distributions / target scratch, reused so a round allocates
395    /// nothing at the vocab size.
396    spec_forced: Option<u32>,
397    spec_q: Vec<Vec<f32>>,
398    spec_p: Vec<f32>,
399    spec_res: Vec<f32>,
400    /// The same three for the sparse chain (top-k configs).
401    spec_qs: Vec<sampler::Sparse>,
402    spec_ps: sampler::Sparse,
403    spec_ress: sampler::Sparse,
404    /// Which arm the MTP draft block runs on this generation: Some(true)
405    /// = the whole-token graph (device attention, one submit a step),
406    /// Some(false) = the per-op path; None = not decided yet. Decided
407    /// on the first draft and held, because the two arms keep the MTP
408    /// KV in different places (device mirror vs the CPU cache) and a
409    /// mid-run switch would read the wrong one.
410    mtp_graph_mode: Option<bool>,
411    /// The DFlash block drafter of the model's sidecar (`CMF_DFLASH=1`): it
412    /// drafts a round in one block from the trunk's tapped layer outputs.
413    pub dflash: Option<Box<crate::dflash::Drafter>>,
414    /// The tapped rows of the verify in flight, joined a row at a time, and
415    /// their first position: the accepted ones become the drafter's context.
416    dflash_rows: Option<(usize, Vec<f32>)>,
417    /// Rows the wgpu graphs kept for the drafter, `(first position, rows as
418    /// [row][layer][hidden])` in the order the graphs ran: a prompt's and a
419    /// plain token's become context at the next round, a verify's are taken
420    /// by the round that ran it.
421    dflash_taps: std::sync::Mutex<Vec<(usize, Vec<f32>)>>,
422    /// The sidecar's block is the trunk's only drafter (the model has no MTP
423    /// block): the wgpu graphs keep its rows without being asked to.
424    pub(crate) dflash_solo: bool,
425    /// The first position whose tapped rows a DFlash drafter can still
426    /// read once the prompt is in (its layers' widest window back from the
427    /// prompt's end): the prompt's chunks before it keep none.
428    dflash_tap_from: usize,
429    /// The Metal verify graph of the round in flight, between its sync
430    /// (logits read) and the commit that replays the accepted prefix.
431    #[cfg(target_os = "macos")]
432    metal_verify: Option<MetalVerifyPending>,
433    /// Precomputed RoPE inverse frequencies [head_dim/2]. Arc: the
434    /// forward path clones a handle to escape the &mut self borrow —
435    /// cloning the table itself was a per-forward allocation.
436    pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
437    /// Reusable norm buffers for the decode hot path (roadmap §3 P0:
438    /// steady-state forward should not heap-allocate). Disjoint field
439    /// from `weights`/`kv_cache`, so split borrows keep working.
440    ws: ForwardScratch,
441    /// Persistent worker pool (None = serial; see CMF_THREADS).
442    pool: Option<std::sync::Arc<Pool>>,
443    // ── Dynamic per-token skill routing (spec §9, claim 14/16) ──
444    /// Source model, retained so a skill switch can re-resolve the
445    /// touched layers' FFN tensors (Mapped = mmap pointers, cheap).
446    pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
447    /// Masks present → weights are dequantized f32 (rebuild path).
448    pub(crate) dyn_force_f32: bool,
449    /// Per-skill FFN layers actually replaced (derived from tensors, not
450    /// the meta `layers` field — ru2 replaces down_proj in 0..23 while
451    /// its meta says [20..23]). None = skill touches non-FFN tensors →
452    /// ineligible for cheap dynamic switching (honest refusal).
453    pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
454    /// Currently overlaid skill (index into model.header.skills); None =
455    /// backbone. Set at load time to the statically-overlaid skill so
456    /// `set_active_skill(None)` correctly reverts it (else a static
457    /// skill would silently persist — the union-diff assumes dyn_active
458    /// always mirrors the live overlay). Switched by `set_active_skill`.
459    pub(crate) dyn_active: Option<usize>,
460    /// Pipeline was loaded with a soft blend (materialized working
461    /// tensors, not a single skill index) → dynamic routing refuses:
462    /// there is no single index to revert the blend from.
463    pub(crate) dyn_blend_loaded: bool,
464    /// Layer whose post-residual hidden feeds the router φ (shared by
465    /// swarm skills). None = φ capture off.
466    pub(crate) dyn_phi_layer: Option<usize>,
467    /// EMA of φ at `dyn_phi_layer` over the decode window (on-policy).
468    dyn_phi_ema: Vec<f32>,
469    dyn_phi_seen: usize,
470    /// Hysteresis router driving per-token skill switches during decode
471    /// (None = static/no dynamic routing). Taken out during generation.
472    pub dyn_router: Option<crate::swarm::DynRouter>,
473    /// O(1) Nyström attention setting (CLI/env/header-hint resolved by
474    /// the caller; None = plain cache attention everywhere).
475    o1_cfg: Option<crate::nystrom::O1Cfg>,
476    /// A turbo file: the far field on long prompts only (`cortiq far-turbo`).
477    pub(crate) o1_turbo: Option<O1Turbo>,
478    /// Over a far field the draft head starts at this prompt row, and counts from it.
479    mtp_pos_base: usize,
480    /// A whole prompt's tail rows read by the far field's prefix (the draft
481    /// head is warmed over them).
482    o1_prefix_tail: Vec<f32>,
483    /// The CUDA hybrid arm's chat snapshot: the prompt ids before the last
484    /// turn's assistant header, whose GDN states the card keeps a copy of
485    /// (`gpu::gdn_snapshot`). The next turn that starts with them goes back
486    /// there and reads only what follows.
487    gdn_snap: Option<Vec<u32>>,
488    /// The draft head's base position when the last turn warmed it over
489    /// its whole prompt on the batched path (None otherwise): its rows from
490    /// there to that prompt's end are prompt rows a chat snapshot keeps.
491    mtp_warm_base: Option<usize>,
492    /// Bumped once per collecting→sealed transition — the GPU state mirror
493    /// re-uploads when it sees a new epoch (each fresh sealed state).
494    o1_epoch: u64,
495    /// Per-layer o1 flags derived from `o1_cfg` (Full layers only).
496    o1_flags: Vec<bool>,
497    /// Emit a structured per-token trace (B4 telemetry channel). Off by
498    /// default — the runtime is silent unless observation is requested.
499    trace: bool,
500    /// Confidence-calibration temperature (B1): reported probability is
501    /// softmax(logits / calib_temp). 1.0 = raw. Set from header.calibration.
502    calib_temp: f32,
503    /// Process-unique id keying this pipeline's device KV mirrors.
504    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
505    graph_kv_id: u64,
506    /// Decode asks the token graph to also run final-norm + lm_head on
507    /// the device (drops the separate per-op lm_head round trip).
508    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
509    graph_want_logits: bool,
510    /// NLL quality gates require the graph's fused head rather than silently
511    /// accepting a CPU head fallback. Generation keeps the historical
512    /// best-effort `graph_want_logits` behavior.
513    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
514    graph_head_required: bool,
515    /// Logits the graph produced for the token just forwarded (taken by
516    /// the decode loop; None = compute on the CPU path).
517    graph_logits: Option<Vec<f32>>,
518    /// Packed Embryo graph model, built lazily on the first eligible
519    /// resident call so other architectures pay no packing cost.
520    embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
521    /// The wide token path refused this file: a matrix is not exactly f16.
522    embryo_wide_refused: bool,
523    /// This pipeline's token graph refused for a STRUCTURAL reason (its
524    /// own weights / layer kinds): not retried. Per pipeline, never
525    /// process-wide: a skill lane whose pack does not build must not flip
526    /// the backbone slot mid-sequence onto the host path (and a new lane
527    /// must not clear the backbone's verdict) — R4/NF-2.
528    graph_refused: std::sync::atomic::AtomicBool,
529    /// Token embeddings are multiplied by this at input (Gemma: √hidden).
530    pub embed_multiplier: f32,
531    /// Attention score scale (1/√head_dim unless the arch overrides —
532    /// Gemma's query_pre_attn_scalar).
533    pub attn_scale: f32,
534    /// Sliding-window attention: (window, every-Nth-layer-is-global
535    /// pattern) — Gemma-3.
536    pub swa: Option<(usize, usize)>,
537    /// Sliding-window tail trimming, `(slack, align)` for
538    /// `LayerKvCache::trim_window`: every layer with a window keeps only
539    /// the rows its window can still read (Spark-X2.5: 576..639 rows per
540    /// 512-window layer instead of the whole context — 27 of the 4B's 36
541    /// layers). Set at load for Spark-X2.5 only (`CMF_SWA_TRIM=0` turns
542    /// it off); None keeps every row, as every other model always has.
543    pub swa_trim: Option<(usize, usize)>,
544    /// Prompt end of the current turn: the tail trim keeps every row a
545    /// rollback to it still reads (`LayerKvCache::trim_window_anchored`),
546    /// so the next chat turn can drop this turn's answer.
547    swa_trim_anchor: usize,
548    /// Fewest prompt rows the Metal chunk prefill graph takes; a shorter
549    /// chunk walks the layers on the host. 32 since the graph landed
550    /// (measured on Qwen2.5-0.5B); Spark-X2.5/X2.6 take 8 (`loader`).
551    pub chunk_min_rows: usize,
552    /// A far field's exact prefix rides the Metal chunk graph, which then also
553    /// keeps the collecting layers' queries (`o1_prefix_on_metal`).
554    o1_chunk_prefix: bool,
555    /// The batched wgpu graph declined this dense stack's prompt: later
556    /// prompts read chunked on the host walk (`graph_prefill_preferred`).
557    dense_batch_declined: std::sync::atomic::AtomicBool,
558    /// Explicit local/global schedule for architectures that cannot be
559    /// represented by Gemma's every-Nth-global convention.
560    pub sliding_layers: Option<Vec<bool>>,
561    /// Per-layer flag: this layer runs WITHOUT RoPE. Recursive Flint applies
562    /// position encoding only on its sliding-window layers; its 14
563    /// full-attention layers have none. The rotary dim of such a layer is 0,
564    /// which is what the attention kernel reads.
565    pub no_rope_layers: Option<Vec<bool>>,
566    /// Ripple's value residual: per layer, the blend `sigmoid(value_res_lambda)` of the lerp
567    /// that feeds V, `lerp(embedding, normed, blend)`. None = V reads the normed input.
568    pub value_blend: Option<Vec<f32>>,
569    /// The scaled embedding rows of the positions in flight (one, a pair's two, a chunk's).
570    vres_e0: Vec<f32>,
571    /// The rows V projects in the layer being run.
572    vres_in: Vec<f32>,
573    /// Natively bounded anchor record (`arch.anchor_core`): the file's
574    /// operator, installed once at load. `Some` = the model is
575    /// bounded-native — `--o1`/`CMF_O1*` are refused, prefix reuse and
576    /// the penalty window are bounded, and no anchor layer stores
577    /// anything per position.
578    pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
579    /// The `[W][rd/2]` relative-rotation table every bounded layer shares
580    /// (built from `inv_freq` once the RoPE setup is final).
581    bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
582    /// Bounded prefix-reuse key (length + rolling hash + tail) of a
583    /// bounded-native model; `kv_history` stays empty there so nothing
584    /// in the pipeline grows with the dialogue.
585    pub kv_prefix: KvPrefix,
586    /// Prompt positions the last `generate*` call actually forwarded
587    /// (prefix reuse subtracts what the cache already held).
588    pub last_prefill_tokens: usize,
589    /// RoPE table of the sliding (local) layers, when they use their
590    /// own base frequency (Gemma-3: 10k local vs 1M global).
591    pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
592    pub rotary_dim_local: Option<usize>,
593    pub rope_scale: f32,
594    pub rope_scale_local: f32,
595    /// Gemma-4: global layers run their own geometry — (head_dim,
596    /// num_kv_heads); sliding layers keep the base fields.
597    pub global_attn: Option<(usize, usize)>,
598    /// Gemma-4: the global layers' proportional RoPE table (len
599    /// global_head_dim/2, zero-padded tail = identity rotation).
600    pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
601    /// Scale-less RMS normalization of V heads before caching (Gemma-4).
602    pub attn_v_norm: bool,
603    /// HunYuan dense: per-head q/k norm runs after RoPE (see the arch flag).
604    pub qk_norm_after_rope: bool,
605    /// The per-head `self_attn.g_proj` output gate uses sigmoid
606    /// (Spark-X2.5) instead of softplus (Laguna).
607    pub proj_gate_sigmoid: bool,
608    /// Final-logit soft-capping C: logits = C·tanh(logits/C) (Gemma-4).
609    pub final_softcap: Option<f32>,
610    /// Cortiq Embryo hierarchical head: cluster matrix [C, hidden]. The
611    /// flat logits h·Eᵀ are turned into the two-level log-probabilities
612    /// log softmax_c(h·Cᵀ)[c(v)] + log softmax_{s∈c(v)}(h·E_c(v)ᵀ)[v].
613    pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
614    /// Gemma-2 attention-logit soft-capping (0.0 = off).
615    pub attn_softcap: f32,
616    /// Compute per-token confidence (a full-vocab softmax each
617    /// token). On by default; `bench --core` turns it off to match
618    /// llama-bench's core timing.
619    confidence_on: bool,
620    /// Test-only one-shot forward failure, scoped to this pipeline so
621    /// parallel scoring tests cannot consume one another's injection.
622    #[cfg(test)]
623    nll_test_fail_at: Option<usize>,
624    /// Test-only route override; avoids mutating the process-wide
625    /// `CMF_PREFILL` environment variable while forcing the serial path.
626    #[cfg(test)]
627    nll_test_force_serial: bool,
628}
629
630#[cfg(target_os = "macos")]
631impl Drop for Pipeline {
632    fn drop(&mut self) {
633        // the async replay writes into `kv_cache` Vecs about to be freed
634        let _ = crate::gpu_metal::wait_replay();
635        crate::gpu::kv_mirror_drop(self.graph_kv_id);
636    }
637}
638
639#[cfg(not(target_os = "macos"))]
640impl Drop for Pipeline {
641    fn drop(&mut self) {
642        // The wgpu resident Embryo graph owns recurrent/KV buffers keyed by
643        // this pipeline's sequence id.  Release that sequence image when a
644        // pooled pipeline is dropped; model weights stay cached for reuse.
645        crate::gpu::graph_kv_reset(self.graph_kv_id);
646    }
647}
648
649/// Model weights. Matrices are `QTensor` (owned f32 for small models
650/// and tests — bit-identical to the historical paths — or quantized
651/// bytes zero-copy from the CMF mmap for big models). 1-D norms are
652/// always small and stay f32.
653pub struct PipelineWeights {
654    /// Embedding table: [vocab_size, hidden_size]
655    pub embed_tokens: QTensor,
656    /// Per-layer weights
657    pub layers: Vec<LayerWeights>,
658    /// LM head: [vocab_size, hidden_size]
659    pub lm_head: QTensor,
660    /// Final norm: [hidden_size]
661    pub final_norm: Vec<f32>,
662}
663
664/// One transformer layer: shared norms + MLP, attention by kind.
665pub struct LayerWeights {
666    pub input_norm: Vec<f32>,
667    /// The pre-FFN norm (`post_attention_layernorm` classically;
668    /// `pre_feedforward_layernorm` on Gemma-2/3 sandwich layers).
669    pub post_norm: Vec<f32>,
670    /// Gemma-2/3 sandwich: norm applied to the ATTENTION OUTPUT before
671    /// its residual add (`post_attention_layernorm` there).
672    pub attn_out_norm: Option<Vec<f32>>,
673    /// Gemma-4: the whole layer output is multiplied by this scalar.
674    pub layer_scale: Option<f32>,
675    /// Gemma-2/3 sandwich: norm applied to the FFN OUTPUT before its
676    /// residual add (`post_feedforward_layernorm`).
677    pub ffn_out_norm: Option<Vec<f32>>,
678    pub ffn: FfnKind,
679    pub attn: AttnKind,
680}
681
682/// FFN gate activation: SiLU (SwiGLU family), tanh-GELU (Gemma's GeGLU),
683/// or a source-specific gated contract. A property of the model, carried on
684/// every FFN triple.
685#[derive(Clone, Copy, PartialEq, Debug, Default)]
686pub enum Act {
687    #[default]
688    Silu,
689    GeluTanh,
690    /// Exact erf GELU (HF `hidden_act = "gelu"`: Spark-X2.5).
691    Gelu,
692    /// Kimi-K3 SituAndMul: BOTH halves transform —
693    /// a = β·tanh(g/β)·σ(g), up' = linβ·tanh(u/linβ) (linβ>0), out = a·up'.
694    Situ {
695        beta: f32,
696        linear_beta: f32,
697    },
698    /// MiniMax M3's SwiGLU-OAI: upper-clamped gate, symmetrically clamped
699    /// up branch, `sigmoid(alpha * gate)`, and a `+1` up-branch offset.
700    /// It deliberately has no generic GPU graph arm until a matching kernel
701    /// exists; CPU uses the exact scalar reference in `inference`.
702    SwiGluOai {
703        alpha: f32,
704        limit: f32,
705    },
706    /// Ripple's clamped SwiGLU (`ModelArch::ffn_clamp`): the gate is cut from above and the up
707    /// branch on both sides at `limit`, then `silu(gate)·up`. The contract of the
708    /// `swiglu_limit` kernels.
709    SiluClamp {
710        limit: f32,
711    },
712}
713
714impl Act {
715    pub fn from_arch(name: &str) -> Self {
716        if name == "gelu_tanh" {
717            Self::GeluTanh
718        } else if name == "gelu" {
719            Self::Gelu
720        } else {
721            Self::Silu
722        }
723    }
724
725    /// Arch-driven constructor (activation name + situ betas).
726    pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
727        match arch.hidden_act.as_str() {
728            "situ" => Self::Situ {
729                beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
730                linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
731            },
732            // `swigluoai` alone is deliberately not a generic activation
733            // alias.  Its alpha/limit are part of MiniMax M3's checked
734            // source contract, and accepting that string on an unrelated
735            // architecture would silently impose M3 math on its FFN.
736            "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
737                alpha: 1.702,
738                limit: 7.0,
739            },
740            "silu" if arch.ffn_clamp.is_some_and(|l| l > 0.0) => Self::SiluClamp {
741                limit: arch.ffn_clamp.unwrap_or(0.0),
742            },
743            other => Self::from_arch(other),
744        }
745    }
746
747    /// The clamp of a dense FFN's pre-activations (0 = none).
748    #[inline]
749    pub fn clamp_limit(self) -> f32 {
750        match self {
751            Self::SiluClamp { limit } => limit,
752            _ => 0.0,
753        }
754    }
755
756    /// The wgpu graphs' dense FFN: an arm and the clamp its kernels take beside it. Only the
757    /// builders that hand the clamp on may ask; every other door reads `graph_act`, which has
758    /// no arm for a clamped SiLU.
759    pub(crate) fn dense_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
760        match self {
761            Self::SiluClamp { limit } => Some((crate::gpu::GraphAct::Silu, limit)),
762            // The M3 arm carries its frozen clamp inside its code.
763            _ => self.wgpu_graph_act().map(|a| (a, 0.0)),
764        }
765    }
766
767    /// Construct the MiniMax M3 SwiGLU-OAI activation from a checked model
768    /// contract. This is intentionally explicit rather than recognizing an
769    /// arbitrary `hidden_act` string: callers must validate `alpha` and
770    /// `limit` against their source architecture before enabling it.
771    #[inline]
772    pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
773        Self::SwiGluOai { alpha, limit }
774    }
775
776    #[inline]
777    pub fn apply(self, x: f32) -> f32 {
778        match self {
779            Self::Silu => inference::silu(x),
780            Self::GeluTanh => inference::gelu_tanh(x),
781            Self::Gelu => inference::gelu_erf(x),
782            Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
783            Self::SwiGluOai { alpha, limit } => {
784                let gate = x.min(limit);
785                gate * (1.0 / (1.0 + (-alpha * gate).exp()))
786            }
787            Self::SiluClamp { limit } => inference::silu(x.min(limit)),
788        }
789    }
790
791    /// Gated combine — the FFN contract. Situ transforms the UP half
792    /// too, so callers must use this instead of apply(g)·u.
793    #[inline]
794    pub fn combine(self, g: f32, u: f32) -> f32 {
795        match self {
796            Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
797                self.apply(g) * (linear_beta * (u / linear_beta).tanh())
798            }
799            Self::SwiGluOai { alpha, limit } => {
800                inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
801            }
802            Self::SiluClamp { limit } => self.apply(g) * u.clamp(-limit, limit),
803            _ => self.apply(g) * u,
804        }
805    }
806
807    /// The wgpu graphs' arm for this activation; None = no device kernel
808    /// computes it, and the graph builder must refuse the layer (the dense
809    /// graph FFN once computed SiLU for whatever the model asked for).
810    pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
811        match self {
812            Self::Silu => Some(crate::gpu::GraphAct::Silu),
813            Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
814            Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
815            Self::Situ { .. } | Self::SwiGluOai { .. } | Self::SiluClamp { .. } => None,
816        }
817    }
818
819    /// The WGPU graph arm.  Keep this deliberately separate from
820    /// `graph_act`: OAI needs its dedicated code rather than a generic SiLU
821    /// fallback.  Only the frozen MiniMax-M3 source constants get a device
822    /// code; a future OAI-like architecture with different constants must add
823    /// and test its own kernel contract.
824    pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
825        // A clamped SiLU is an arm AND a limit: a door that reads the arm alone would run it
826        // unclamped, so it gets none here (`dense_graph_contract` hands both on).
827        if matches!(self, Self::SiluClamp { .. }) {
828            return None;
829        }
830        self.moe_job_graph_contract().map(|(act, _)| act)
831    }
832
833    /// The exact device contract for a gate/up/down MoE job.  OAI is not a
834    /// generic "clamped SiLU": code 3 commits a backend to MiniMax M3's
835    /// alpha, asymmetric up transform, and frozen clamp.  Keep the clamp
836    /// alongside the marker so a backend cannot accidentally run code 3 with
837    /// a nearby model's limit.
838    pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
839        match self {
840            Self::SwiGluOai { alpha, limit }
841                if alpha.to_bits() == 1.702f32.to_bits()
842                    && limit.to_bits() == 7.0f32.to_bits() =>
843            {
844                Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
845            }
846            // The `swiglu_limit` kernels' own contract: gate from above, up on both sides.
847            Self::SiluClamp { limit } => Some((crate::gpu::GraphAct::Silu, limit)),
848            _ => self.graph_act().map(|act| (act, 0.0)),
849        }
850    }
851}
852
853/// Dense gated triple — the FFN of a dense layer or of one expert.
854pub struct DenseFfn {
855    pub gate_proj: QTensor,
856    pub up_proj: QTensor,
857    pub down_proj: QTensor,
858    /// Gate activation (SiLU default; Gemma: tanh-GELU).
859    pub act: Act,
860    /// `down_proj` stored transposed (`[inter, hidden]`), when the file
861    /// carries it. Only the per-token sparse path reads it: a neuron's
862    /// down weights are a contiguous ROW there, so the token's chosen
863    /// neurons are the only bytes touched. `None` = the ordinary layout,
864    /// and the sparse path stays off.
865    pub down_t: Option<QTensor>,
866    /// Task tubes (spec: defragged task-conditional width). The three
867    /// matrices above are the CORE — the neurons every task computes;
868    /// each tube is an independently quantized slice of the SAME layer
869    /// holding the neurons only some tasks need. A tube is a normal
870    /// tensor triple, so every kernel runs it unchanged, and the bytes
871    /// of an inactive tube are never read. Empty = ordinary dense FFN.
872    pub segs: Vec<FfnSeg>,
873}
874
875/// One task tube: a contiguous slice of a layer's FFN neurons, stored
876/// as its own `[w, hidden]` / `[hidden, w]` triple. `start` is the
877/// neuron's index in the layer's FULL space (core first, then tubes in
878/// order) — the bit a task mask sets to switch this tube on.
879pub struct FfnSeg {
880    pub gate: QTensor,
881    pub up: QTensor,
882    pub down: QTensor,
883    pub start: usize,
884    pub width: usize,
885}
886
887/// FFN operator of a layer, decided by tensor presence at load time
888/// (router `mlp.gate.weight` in the directory = MoE layer).
889pub enum FfnKind {
890    Dense(DenseFfn),
891    /// Mixture-of-Experts (Qwen2-MoE / Qwen3-MoE): softmax over ALL
892    /// expert logits → top-k, optional renorm; experts stay quantized
893    /// in mmap — only the selected ones are touched per token.
894    Moe(MoeFfn),
895    /// Gemma-4 MoE: a dense MLP branch AND a routed-expert branch in
896    /// the SAME layer, each with its own norm sandwich. The dense
897    /// branch reads the pre-FFN-normed input; the expert branch (and
898    /// the router) read the RAW residual through `pre_norm_2`:
899    ///   d = post_norm_1(dense(x̂));  m = post_norm_2(Σwₑ·FFNₑ(pre_norm_2(h)))
900    ///   ffn_out = d + m   (the caller's ffn_out_norm + residual follow)
901    DenseMoe(Box<DenseMoeFfn>),
902}
903
904/// Gemma-4 dual-branch FFN (see `FfnKind::DenseMoe`).
905pub struct DenseMoeFfn {
906    pub dense: DenseFfn,
907    pub moe: MoeFfn,
908    /// post_feedforward_layernorm_1 — dense-branch output norm.
909    pub post_norm_1: Vec<f32>,
910    /// pre_feedforward_layernorm_2 — expert-branch input norm (applied
911    /// to the RAW residual, not the pre-FFN-normed activation).
912    pub pre_norm_2: Vec<f32>,
913    /// post_feedforward_layernorm_2 — expert-branch output norm.
914    pub post_norm_2: Vec<f32>,
915}
916
917pub struct MoeFfn {
918    /// Router `mlp.gate.weight` [num_experts, hidden].
919    pub router: QTensor,
920    pub experts: Vec<DenseFfn>,
921    pub top_k: usize,
922    pub norm_topk_prob: bool,
923    /// Router scores per-expert with a sigmoid (LFM2-MoE / DeepSeek-V3
924    /// `noaux_tc`) instead of a softmax over all experts (Qwen).
925    pub router_sigmoid: bool,
926    /// Addend in the selected-score denominator of a sigmoid router.
927    /// Most supported checkpoints use the historical `1e-6` floor; Flint's
928    /// reference routing uses `1e-20`; MiniMax M3.1 preserves the same FP32
929    /// `sum + 1e-20` law. This is per-MoE rather than a global family switch
930    /// because the same sigmoid routing law has several contracts.
931    pub sigmoid_denom_eps: f32,
932    /// Per-expert selection bias `mlp.expert_bias` [num_experts]
933    /// (LFM2-MoE): added to the sigmoid scores for the top-k CHOICE only;
934    /// the gathered weights use the unbiased scores. None = no bias.
935    pub expert_bias: Option<Vec<f32>>,
936    /// Top-k weights are multiplied by this after the optional renorm
937    /// (LFM2-MoE `routed_scaling_factor`; 1.0 = off).
938    pub routed_scaling: f32,
939    /// Adaptive routing (CMF_MOE_TAU, opt-in): keep the smallest
940    /// prefix of the top-k whose renormalized mass reaches τ —
941    /// confident tokens touch 1–2 experts, flat ones keep all k.
942    /// MoE decode is memory-bound, so skipped experts are skipped
943    /// weight traffic. None = classic fixed top-k (bit-identical).
944    pub route_tau: Option<f32>,
945    /// Always-on shared expert. Qwen2-MoE carries an additional sigmoid
946    /// gate; Laguna adds the shared expert unconditionally (`None`).
947    pub shared: Option<(DenseFfn, Option<QTensor>)>,
948    /// Expert-selection counters (truncated Fisher B-field of claim 12:
949    /// routing frequency during calibration). Filled by every forward,
950    /// read by the CLI via CMF_MOE_STATS. RefCell: decode is single-threaded.
951    pub stats: std::cell::RefCell<Vec<u64>>,
952    /// Per-CHANNEL sum of squares of this FFN's input, accumulated over a
953    /// calibration run (`CMF_RMS_TRACE`). These are the RMS activation
954    /// traces AWNP needs: raw weight magnitude says every channel matters
955    /// equally, and the question AWNP asks is whether the ACTIVATIONS
956    /// disagree. Off unless the env var is set — an f64 add per channel
957    /// per token is cheap, but not free.
958    pub act_sq: std::cell::RefCell<Vec<f64>>,
959    /// Raw FFN-input rows captured for the layers named by `CMF_ACT_DUMP`
960    /// (`"9,19"`). AWNP is nullspace PROJECTION: after dropping channels the
961    /// survivors are refitted to absorb what was removed, and how much they
962    /// can absorb depends on the activation COVARIANCE, not on per-channel
963    /// RMS. Per-channel numbers can only bound the cost from above.
964    pub act_rows: std::cell::RefCell<Vec<f32>>,
965    /// Task mask over routed experts (DTG-MA over MoE, claim-12 B-field
966    /// applied): `false` experts are excluded from selection, the
967    /// softmax renormalizes over the allowed set. Built by the loader
968    /// from CMF_MOE_MASK=<stats.json> + CMF_MOE_MASK_COVER. None = all.
969    pub mask: Option<Vec<bool>>,
970    /// Gemma-4: per-expert weight scale applied AFTER the top-k renorm
971    /// (`router.per_expert_scale`). None = 1.0 everywhere.
972    pub per_expert_scale: Option<Vec<f32>>,
973    /// Gemma-4: the router reads a SCALE-LESS rms-norm of its input
974    /// (the constant gain router.scale·√hidden is folded into the
975    /// router weights at convert time).
976    pub router_input_norm: bool,
977    /// Cortiq Embryo: resonance routing (P1) — the "logits" are
978    /// bias_e − ‖(x−μ_e) − U_eᵀU_e(x−μ_e)‖², argmax = the expert whose
979    /// descriptor reconstructs the input best. `router` is a placeholder.
980    pub resonance: Option<Resonance>,
981    /// Growth records (`kind = "expert_append"`, spec §9.5.1) mounted
982    /// behind the trunk experts, one entry per grown expert IN THE ORDER
983    /// they sit in `experts` (the tail `experts[experts.len() - grown.len()..]`).
984    /// The trunk keeps `experts.len() - grown.len()` experts. Empty on a
985    /// gated MoE, on a file without records and under `CMF_GROWTH=off`.
986    pub grown: Vec<GrownExpert>,
987}
988
989/// One grown expert of an `expert_append` record as the loader mounted it.
990#[derive(Debug, Clone, PartialEq, Eq)]
991pub struct GrownExpert {
992    /// The record's skill id.
993    pub record: String,
994    /// Position of the record in `header.skills`.
995    pub record_index: usize,
996    pub layer: usize,
997    /// The index the tensor name declares (`experts.{e}` — the chain rule
998    /// of the format); the executed position may be smaller when an
999    /// earlier record of the layer is not mounted.
1000    pub expert: usize,
1001}
1002
1003/// Per-expert resonance descriptors of one MoE layer (`mlp.desc.*`).
1004pub struct Resonance {
1005    /// [E, hidden]
1006    pub mu: Vec<f32>,
1007    /// [E, k, hidden] orthonormal directions (k may be 0)
1008    pub u: Vec<f32>,
1009    pub k: usize,
1010    /// [E] selection bias (loss-free balancing, trained online)
1011    pub bias: Vec<f32>,
1012    /// [E] reconstruction-error shell: an expert whose error
1013    /// `‖(x−μ)⊥U‖² = d² − proj` exceeds its shell scores `−∞` (it never
1014    /// wins). `+inf` = no shell — every trunk expert; a grown expert
1015    /// (`expert_append`) carries the finite `desc.shell` its record stores.
1016    /// Empty = no shell anywhere (legacy constructors).
1017    pub shell: Vec<f32>,
1018}
1019
1020/// `CMF_GROWTH_SHELL` state: 0 = not yet read from the environment, 1 = on,
1021/// 2 = off. Process-wide, like the environment it mirrors.
1022static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
1023
1024/// Is the growth shell applied (`Resonance::scores` −∞ rule, the resident
1025/// graph's packed shell)? `CMF_GROWTH_SHELL=off` disables it for
1026/// measurement; [`set_growth_shell`] overrides the environment in-process
1027/// (`growth-eval --shell`). Default: on.
1028pub fn growth_shell_enabled() -> bool {
1029    use std::sync::atomic::Ordering;
1030    match GROWTH_SHELL.load(Ordering::Relaxed) {
1031        1 => true,
1032        2 => false,
1033        _ => {
1034            let off = std::env::var("CMF_GROWTH_SHELL")
1035                .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
1036                .unwrap_or(false);
1037            GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
1038            !off
1039        }
1040    }
1041}
1042
1043/// Switch the growth shell on/off for this process (`None` = re-read
1044/// `CMF_GROWTH_SHELL` on the next query). A pipeline packed into the
1045/// resident graph BEFORE the switch keeps the shell it was packed with —
1046/// build a new pipeline after switching.
1047pub fn set_growth_shell(on: Option<bool>) {
1048    GROWTH_SHELL.store(
1049        match on {
1050            Some(true) => 1,
1051            Some(false) => 2,
1052            None => 0,
1053        },
1054        std::sync::atomic::Ordering::Relaxed,
1055    );
1056}
1057
1058impl Resonance {
1059    /// Does any expert carry a finite shell (a mounted growth record)?
1060    pub fn has_shell(&self) -> bool {
1061        self.shell.iter().any(|s| s.is_finite())
1062    }
1063
1064    /// The shell the runtime applies right now: the stored one, or all
1065    /// `+inf` when the shell is switched off (`CMF_GROWTH_SHELL=off`).
1066    pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
1067        let mut out = vec![f32::INFINITY; ne];
1068        if growth_shell_enabled() {
1069            for (o, s) in out.iter_mut().zip(&self.shell) {
1070                *o = *s;
1071            }
1072        }
1073        out
1074    }
1075
1076    /// Routing scores for one input row (higher = better). A grown
1077    /// expert whose reconstruction error lies outside its shell gets
1078    /// `−∞` (unless the shell is switched off); trunk rows are the exact
1079    /// bit pattern they were before growth.
1080    pub fn scores(&self, x: &[f32], out: &mut [f32]) {
1081        let h = x.len();
1082        let ne = out.len();
1083        let shell_on = growth_shell_enabled() && !self.shell.is_empty();
1084        for e in 0..ne {
1085            let mu = &self.mu[e * h..(e + 1) * h];
1086            let mut d2 = 0.0f32;
1087            for j in 0..h {
1088                let d = x[j] - mu[j];
1089                d2 += d * d;
1090            }
1091            let mut proj = 0.0f32;
1092            for i in 0..self.k {
1093                let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
1094                let mut p = 0.0f32;
1095                for j in 0..h {
1096                    p += (x[j] - mu[j]) * u[j];
1097                }
1098                proj += p * p;
1099            }
1100            let err = d2 - proj;
1101            out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
1102            if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
1103                out[e] = f32::NEG_INFINITY;
1104            }
1105        }
1106    }
1107}
1108
1109/// Attention operator of a layer. Extension point: new operators are
1110/// new variants here + a forward in their own module.
1111pub enum AttnKind {
1112    /// GQA softmax attention (+ optional Qwen3.5 qk-norm / output gate).
1113    Full {
1114        wq: QTensor,
1115        wk: QTensor,
1116        wv: QTensor,
1117        wo: QTensor,
1118        q_norm: Option<Vec<f32>>,
1119        k_norm: Option<Vec<f32>>,
1120        output_gate: bool,
1121        /// Laguna: a separate softplus projection applied to the attention
1122        /// output before O. The bool means one scalar per head (broadcast
1123        /// across head_dim); false means one scalar per element.
1124        softplus_gate: Option<(QTensor, bool)>,
1125        /// Qwen2-family projection biases (q, k, v).
1126        bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
1127    },
1128    /// Canonical linear core (VMF phase attention).
1129    Linear(VmfPhaseWeights),
1130    /// Faithful vendor linear operator (Qwen3.5 GatedDeltaNet).
1131    LinearGdn(GdnWeights),
1132    /// LFM2 gated short-convolution mixer (no KV cache; conv ring state
1133    /// lives in the layer's `linear_state`).
1134    ShortConv(ShortConvWeights),
1135    /// DeepSeek-V2 Multi-head Latent Attention. v1 executes it as
1136    /// expand-to-MHA: the latent is projected per token, K/V expand to
1137    /// every head and live in the ordinary cache (K head layout
1138    /// [rope | nope] so the standard partial rotary covers the shared
1139    /// rope key; V rows are zero-padded to the K head_dim and the pad
1140    /// is sliced off before O). Latent-resident cache is a later
1141    /// optimization, not a semantic change.
1142    Mla(Box<MlaWeights>),
1143    /// Kimi Delta Attention (Kimi Linear / Kimi-K3): per-channel decayed
1144    /// delta rule, separate q/k/v short convs, sigmoid-gated output norm.
1145    /// State lives in the layer's `linear_state` (no KV cache).
1146    Kda(Box<crate::linear_core::KdaWeights>),
1147    /// Natively bounded softmax anchor `swa_sink_v1` (Embryo-O1): ring of
1148    /// the last W raw keys with relative RoPE + trained NoPE sinks, one
1149    /// softmax. State is the fixed-size ring in `LayerKvCache::bounded`;
1150    /// nothing is stored per position (see `crate::bounded`).
1151    Bounded(Box<crate::bounded::BoundedWeights>),
1152}
1153
1154/// DeepSeek-V2 MLA projections (see `AttnKind::Mla`).
1155pub struct MlaWeights {
1156    /// `[nh·(rope+nope), hidden]` (or `[…, q_lora]` when compressed) —
1157    /// the converter permutes each head rope-first so rotary_dim =
1158    /// qk_rope works unchanged.
1159    pub q_proj: QTensor,
1160    /// Compressed q (K3/V3 class): x → q_a `[q_lora, hidden]` →
1161    /// rms(q_a_norm) → q_proj (= q_b). None = direct q (V2-Lite).
1162    pub q_a: Option<QTensor>,
1163    pub q_a_norm: Option<Vec<f32>>,
1164    /// `kv_a_proj_with_mqa` `[lora + rope, hidden]` (latent first).
1165    pub kv_a: QTensor,
1166    /// RMS-norm weights over the latent (`kv_a_layernorm`, [lora]).
1167    pub kv_a_norm: Vec<f32>,
1168    /// `[nh·(nope+v), lora]` — per head [k_nope | v].
1169    pub kv_b: QTensor,
1170    /// `[hidden, nh·v]`.
1171    pub o_proj: QTensor,
1172    pub nh: usize,
1173    pub qk_rope: usize,
1174    pub qk_nope: usize,
1175    pub v_dim: usize,
1176    pub lora: usize,
1177    /// Softmax scale (1/√(rope+nope), YaRN-mscale-corrected at load).
1178    pub scale: f32,
1179    /// Kimi Linear NoPE: skip the rotary entirely (layout unchanged).
1180    pub nope: bool,
1181}
1182
1183/// Multi-token-prediction head (DeepSeek/Qwen style, spec §2.1):
1184/// `x = eh_proj·[enorm(embed(next)); hnorm(hidden)]` → one transformer
1185/// block over its own KV → shared lm_head. Drafts the token after next;
1186/// the main model verifies, so output is exact — MTP only buys speed.
1187pub struct MtpModule {
1188    pub enorm: Vec<f32>,
1189    pub hnorm: Vec<f32>,
1190    /// [hidden, 2·hidden]
1191    pub eh_proj: QTensor,
1192    pub layer: LayerWeights,
1193    pub final_norm: Vec<f32>,
1194    pub kv: crate::kv_cache::LayerKvCache,
1195}
1196
1197/// Teacher-forced activation statistics for one MTP draft FFN.
1198///
1199/// `mass[i]` is `Σ_rows |activation_i|`; `squared_mass[i]` is
1200/// `Σ_rows activation_i²`, where `activation = act(gate) · up` immediately
1201/// before the draft layer's down projection. `observed_positions` is the
1202/// number of teacher-forced rows that contributed to both arrays.
1203///
1204/// Rows of FFN input the canonical draft probe measures in one batch.
1205const MTP_PROBE_FFN_ROWS: usize = 128;
1206
1207/// [`Pipeline::probe_mtp_ffn`] returns one of these for each actual draft
1208/// layer.  It intentionally has a separate type/API from
1209/// [`Pipeline::probe_ffn_mass`] so a compactor cannot accidentally substitute
1210/// trunk statistics for an MTP sidecar whose FFN has a different width.
1211#[derive(Clone, Debug, PartialEq)]
1212pub struct MtpFfnProbe {
1213    pub mass: Vec<f64>,
1214    pub squared_mass: Vec<f64>,
1215    pub observed_positions: usize,
1216}
1217
1218/// A Metal verify graph after its sync: what the commit needs — the
1219/// graph (per-layer replay scratch), the GDN layers in encode order (their
1220/// CPU states receive the replay), and the attention layers with the CPU
1221/// row count they were encoded against (the accepted rows are pulled from
1222/// the mirror from there).
1223/// One item of the Metal rows-graph plan.
1224#[cfg(target_os = "macos")]
1225enum MetalRowsItem<'a> {
1226    Gdn {
1227        run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1228        first: usize,
1229    },
1230    Attn {
1231        l: crate::gpu_metal::AttnGpuLayer<'a>,
1232        li: usize,
1233        q_norm: Option<&'a [f32]>,
1234        k_norm: Option<&'a [f32]>,
1235        output_gate: bool,
1236        /// Spark-X2.6's per-head gate `[nh × hidden]`.
1237        head_gate: Option<&'a [f32]>,
1238    },
1239}
1240
1241#[cfg(target_os = "macos")]
1242struct MetalVerifyPending {
1243    graph: crate::gpu_metal::VerifyGraph,
1244    gdn_layers: Vec<usize>,
1245    attn_layers: Vec<(usize, usize)>,
1246}
1247
1248/// A round's batched MTP warm-up, submitted but not yet waited
1249/// (`mtp_warm_batch_submit` → `mtp_warm_batch_finish`): the trunk commit's
1250/// GDN replay is queued between the two.
1251#[cfg(target_os = "macos")]
1252struct MetalWarmPending {
1253    graph: crate::gpu_metal::VerifyGraph,
1254    cpu_stored: usize,
1255    b: usize,
1256}
1257
1258#[cfg(target_os = "macos")]
1259enum MetalRowsRun {
1260    /// Capability/preflight refusal before a command buffer was committed.
1261    Declined,
1262    /// A graph was admitted and then failed; callers must clear the sequence
1263    /// rather than replaying it through CPU/serial state.
1264    Failed,
1265    Completed(MetalVerifyPending),
1266}
1267
1268#[cfg(target_os = "macos")]
1269enum MetalPrefillOutcome {
1270    Declined,
1271    Failed,
1272    Completed(Vec<f32>),
1273}
1274
1275#[cfg(target_os = "macos")]
1276enum MetalBatchNllOutcome {
1277    Declined,
1278    Failed(String),
1279    Completed(f64, usize),
1280}
1281
1282/// The speculation trial's phases (see the decode loop): up to five timed
1283/// speculative rounds, eight timed plain tokens, then the faster arm until a
1284/// re-check. Two consecutive rounds with no accepted draft skip directly to
1285/// the plain measurement: another speculative round cannot pay when it emits
1286/// exactly the one token the plain loop would have produced.
1287#[derive(Clone, Copy)]
1288enum SpecTrial {
1289    Spec {
1290        t0: std::time::Instant,
1291        gen0: usize,
1292        rounds: usize,
1293    },
1294    Plain {
1295        t0: std::time::Instant,
1296        gen0: usize,
1297    },
1298    Decided {
1299        spec: bool,
1300        recheck_at: usize,
1301    },
1302}
1303
1304/// `CMF_GRAPH_SPEC_TIME`: 0 = off, 1 = one line per speculative round
1305/// plus the host stamps of any OUTLIER round (wall > 1.4× the running
1306/// median), 2 = the host stamps of every round.
1307pub(crate) fn spec_time_level() -> u8 {
1308    static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1309    *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1310        Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1311        Err(_) => 0,
1312    })
1313}
1314
1315/// `GraphMoeBank` reuse across tokens (`CMF_GRAPH_MOE_BANK=0` rebuilds
1316/// every layer's bank on every token, the route before the cache).
1317fn graph_moe_bank_on() -> bool {
1318    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1319    *ON.get_or_init(|| std::env::var("CMF_GRAPH_MOE_BANK").as_deref() != Ok("0"))
1320}
1321
1322/// Decide whether the batched speculative verifier is admitted by the
1323/// user/backend policy, before capability checks.  Native Metal's row-batch
1324/// verifier is deliberately fail-closed by default: it has produced a
1325/// different *emitted* greedy token than the serial path on a real model.
1326/// It remains available only through the documented explicit experiment
1327/// spelling, `CMF_GRAPH_SPEC=1`; other backends retain their historical
1328/// nonzero override behaviour.
1329fn graph_spec_wanted_by_policy(
1330    spec_env: Option<&str>,
1331    spec_default_ok: bool,
1332    penalized: bool,
1333    metal_wgpu: bool,
1334    native_metal: bool,
1335) -> bool {
1336    match spec_env {
1337        Some("0") => false,
1338        Some("1") => true,
1339        // Off native Metal any other value still forces the drafts on, as it
1340        // always did; on native Metal it means the default below.
1341        Some(_) if !native_metal => true,
1342        // Native Metal shares the default since the batched verify passed the
1343        // exact-ID gate over q4tp trunks (`spec_default_ok`): MacBook Air M4,
1344        // Qwen3.8-27B Turbo and OrcaCyber-Nano-1.0, five prompts each, the
1345        // same token ids with drafts and without, 1.45-2.3x the plain decode.
1346        _ => spec_default_ok && !penalized && !metal_wgpu,
1347    }
1348}
1349
1350/// The round's host stamps: `spec_stamp(name)` records the time since
1351/// the previous stamp (the section that just ended) — from anywhere on
1352/// the round's call chain (the Metal verify, the draft step, the commit),
1353/// no plumbing. Off (a single atomic load) unless `CMF_GRAPH_SPEC_TIME`
1354/// is set; one decode thread at a time is assumed (diagnostics).
1355struct SpecStampLog {
1356    t_last: std::time::Instant,
1357    items: Vec<(&'static str, f32)>,
1358}
1359
1360static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1361
1362pub(crate) fn spec_stamp(name: &'static str) {
1363    if spec_time_level() == 0 {
1364        return;
1365    }
1366    if let Ok(mut g) = SPEC_STAMPS.lock() {
1367        if let Some(log) = g.as_mut() {
1368            let now = std::time::Instant::now();
1369            log.items
1370                .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1371            log.t_last = now;
1372        }
1373    }
1374}
1375
1376fn spec_stamps_begin() {
1377    if spec_time_level() == 0 {
1378        return;
1379    }
1380    if let Ok(mut g) = SPEC_STAMPS.lock() {
1381        *g = Some(SpecStampLog {
1382            t_last: std::time::Instant::now(),
1383            items: Vec::with_capacity(64),
1384        });
1385    }
1386}
1387
1388fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1389    SPEC_STAMPS
1390        .lock()
1391        .ok()
1392        .and_then(|mut g| g.take())
1393        .map(|l| l.items)
1394        .unwrap_or_default()
1395}
1396
1397/// One line: every stamp name in first-seen order with its total over the
1398/// round and, when it fired more than once (the draft steps), the count.
1399fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1400    let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1401    for &(n, ms) in items {
1402        match agg.iter_mut().find(|e| e.0 == n) {
1403            Some(e) => {
1404                e.1 += ms;
1405                e.2 += 1;
1406            }
1407            None => agg.push((n, ms, 1)),
1408        }
1409    }
1410    let mut s = String::with_capacity(agg.len() * 16);
1411    for (n, ms, k) in agg {
1412        if k > 1 {
1413            s.push_str(&format!("{n} {ms:.1}/{k} "));
1414        } else {
1415            s.push_str(&format!("{n} {ms:.1} "));
1416        }
1417    }
1418    s
1419}
1420
1421/// The speculation monitor: exponential averages of a round's wall time
1422/// and of the tokens it produced, and the plain token's wall time — the
1423/// three numbers the keep/stop rule needs. A round pays when
1424/// `tokens_per_round · plain_ms > round_ms · 1.03`. The one-shot trial
1425/// (four rounds against eight tokens) mis-called prose: the first rounds
1426/// after a prompt are formulaic and accept well, the body does not (an
1427/// essay measured 39 against a plain 44.8 with the trial saying
1428/// "speculate"), so the rule now runs on EVERY round and stops after four
1429/// consecutive losing rounds; a stopped speculation is retried 128 tokens
1430/// later.
1431///
1432/// Native Metal (`metal: true`) does not pay the eight plain tokens up
1433/// front: on the 27B a plain token is ~150 ms, so the trial alone cost
1434/// ~1.2 s of every answer. There the plain phase is (a) skipped while the
1435/// rounds land at least `SPEC_PROXY_TOKENS` tokens each — a k=7 round on
1436/// Metal costs ~1.9 plain tokens (286 against 148 ms measured on the M4),
1437/// so 3.5 tokens/round cannot lose on any Metal round/plain ratio seen —
1438/// and (b) otherwise bounded to the fewest tokens that time it: two, or
1439/// as many as fit in `SPEC_PLAIN_MIN_MS` (a 150-ms token measures itself;
1440/// a 10-ms one needs the eight). The keep/stop rule itself is unchanged:
1441/// the moment a plain rate exists, it decides.
1442#[derive(Default, Clone, Copy)]
1443struct SpecMon {
1444    round_ms: f64,
1445    tokens: f64,
1446    plain_ms: f64,
1447    n: u32,
1448    fails: u32,
1449    /// Consecutive rounds that produced only their verifier-tip token (zero
1450    /// accepted MTP drafts). This is deliberately counted before the warm-up
1451    /// round is excluded from timing: `SPEC_ZERO_ROUNDS` real zero-acceptance
1452    /// rounds stop charging an obviously losing speculative arm.
1453    zero_accept_rounds: u8,
1454    metal: bool,
1455    /// The CUDA hybrid arm: a round costs about 1.25 plain tokens (a verify
1456    /// of five rows 1.1x a token, the drafts a tenth more), so rounds that
1457    /// land `SPEC_PROXY_TOKENS_CUDA` pay without timing plain tokens.
1458    cuda: bool,
1459}
1460
1461/// What a speculative round of k drafts costs on native Metal over a q4tp
1462/// trunk, in plain tokens. One and two drafts verify on the small-batch
1463/// matvec (`q4tp_matvec_b2` / `_b3`), three and more on the narrow GEMM at
1464/// one flat price. MacBook Air M4, Qwen3.8-27B: a plain token 160 ms, a
1465/// round of one draft 176, of two 204, of three 246, of seven 281.
1466const METAL_ROUND_COST: [f32; 7] = [1.10, 1.275, 1.54, 1.59, 1.65, 1.70, 1.76];
1467/// The same on the CUDA hybrid arm, verify and commit of a round of k
1468/// drafts: RTX 3090, Qwen3.8-27B q4tp, a plain token 20.4 ms, a round of
1469/// four chain drafts 24.3 ms past its drafts, of seven block drafts 25.1.
1470const CUDA_ROUND_COST: [f32; 7] = [1.12, 1.15, 1.17, 1.19, 1.21, 1.22, 1.24];
1471/// A DFlash block on the CUDA arm with its context rows and head (3.9 ms),
1472/// and a step of the MTP chain (0.62 ms), in plain tokens.
1473const CUDA_BLOCK_DRAFT: f32 = 0.19;
1474const CUDA_DRAFT_STEP: f32 = 0.03;
1475/// How often a draft holds once the one before it held, before any round has
1476/// shown it: the 27B's chain on code and on prose holds its first draft in
1477/// four rounds of five, the second in three of four after that, the third in
1478/// one of two.
1479const SPEC_HOLD_PRIOR: [f32; 7] = [0.80, 0.75, 0.50, 0.67, 0.65, 0.69, 0.67];
1480
1481/// The draft depth that lands the most tokens in a unit of time: a round of
1482/// k drafts gives its tip and every draft the chain held, for the cost of
1483/// that depth's verify.
1484fn metal_spec_depth(hold: &[f32; 7]) -> usize {
1485    metal_spec_plan(hold, 0.0).0
1486}
1487
1488/// `metal_spec_depth` for a drafter whose drafts cost `draft` plain tokens a
1489/// round whatever the depth (a DFlash block; an MTP chain's steps are small
1490/// change beside the verify and count as nothing): the depth, and the tokens
1491/// it lands in the time of a plain one.
1492fn metal_spec_plan(hold: &[f32; 7], draft: f32) -> (usize, f32) {
1493    spec_plan_at(&METAL_ROUND_COST, hold, draft, 0.0)
1494}
1495
1496/// `metal_spec_plan` over any device's price list: `cost[k - 1]` plain tokens
1497/// for a round of k drafts, `draft` a round and `step` a draft on top.
1498fn spec_plan_at(cost: &[f32; 7], hold: &[f32; 7], draft: f32, step: f32) -> (usize, f32) {
1499    let mut best = (1usize, 0f32);
1500    let (mut tokens, mut reach) = (1.0f32, 1.0f32);
1501    for k in 1..=7 {
1502        reach *= hold[k - 1];
1503        tokens += reach;
1504        let rate = tokens / (cost[k - 1] + draft + step * k as f32);
1505        // a longer chain has to win by more than the averages' noise
1506        if rate > best.1 * 1.04 {
1507            best = (k, rate);
1508        }
1509    }
1510    best
1511}
1512
1513/// The depth of the MTP chain's rounds on wgpu, from what a round costs on
1514/// this device: `wall[k - 1]` is the measured wall of a round of k drafts in
1515/// ms (0: not timed yet), and a depth between or past the timed ones is
1516/// priced on the line through the two nearest. A verify's price climbs with
1517/// its rows by a card's own slope (Qwen3.8-27B Turbo, a round of 3 / 4 / 5
1518/// drafts: RTX 2000 Ada 111 / 126 / 145 ms against a plain token's 75), so
1519/// the acceptance alone does not say where a deeper round stops paying.
1520/// `None` until two depths are timed: the acceptance rule moves the depth
1521/// until then. The depth in turn stays unless another lands 4 % more tokens
1522/// in a unit of time.
1523fn wgpu_spec_depth(hold: &[f32; 7], wall: &[f32; 7], k_now: usize, k_min: usize, k_max: usize) -> Option<usize> {
1524    let mut timed = (1..=7usize).filter(|&k| wall[k - 1] > 0.0);
1525    let (first, second) = (timed.next()?, timed.next()?);
1526    let price = |k: usize| -> f32 {
1527        if wall[k - 1] > 0.0 {
1528            return wall[k - 1];
1529        }
1530        // the two timed depths nearest to k
1531        let (mut a, mut b) = (first, second);
1532        for j in (1..=7usize).filter(|&j| wall[j - 1] > 0.0 && j != first && j != second) {
1533            let far = if a.abs_diff(k) >= b.abs_diff(k) { &mut a } else { &mut b };
1534            if j.abs_diff(k) < far.abs_diff(k) {
1535                *far = j;
1536            }
1537        }
1538        let (a, b) = (a.min(b), a.max(b));
1539        let slope = ((wall[b - 1] - wall[a - 1]) / (b - a) as f32).max(0.0);
1540        (wall[a - 1] + slope * (k as f32 - a as f32)).max(wall[a - 1] * 0.5)
1541    };
1542    let (k_min, k_max) = (k_min.clamp(1, 7), k_max.clamp(1, 7));
1543    let (mut tokens, mut reach) = (1.0f32, 1.0f32);
1544    let mut rate = [0f32; 7];
1545    for k in 1..=7 {
1546        reach *= hold[k - 1];
1547        tokens += reach;
1548        rate[k - 1] = tokens / price(k);
1549    }
1550    let k_now = k_now.clamp(k_min, k_max);
1551    let best = (k_min..=k_max).max_by(|&a, &b| rate[a - 1].total_cmp(&rate[b - 1]))?;
1552    Some(if rate[best - 1] > rate[k_now - 1] * 1.04 { best } else { k_now })
1553}
1554
1555/// A DFlash block with its head and selector on native Metal, in plain
1556/// tokens: one price a round, every draft of the block at once. MacBook Air
1557/// M4, the DFlash 2 drafter of Qwen3.8-27B as q4tp: 70 ms against a plain
1558/// token's 160.
1559const DFLASH_ROUND_DRAFT: f32 = 0.44;
1560/// How often a DFlash draft holds once the one before it held, before any
1561/// round has shown it: the 27B's drafter keeps 3.4 drafts of seven from every
1562/// start of mixed text.
1563const DFLASH_HOLD_PRIOR: [f32; 7] = [0.85, 0.82, 0.80, 0.80, 0.80, 0.80, 0.80];
1564/// Rounds the DFlash block sits out after a turn that the MTP chain took
1565/// back, and the most it ever sits out: a turn of fewer than `DFLASH_TURN`
1566/// rounds doubles the wait, a longer one starts it over. A turn that landed
1567/// more tokens over all its rounds than the chain was landing lost them to a
1568/// hard stretch (the block holds a whole round or none of it, so a few empty
1569/// rounds in a row say little): it sits out `DFLASH_WAIT_DIP` rounds only.
1570const DFLASH_WAIT: u32 = 48;
1571const DFLASH_WAIT_MOST: u32 = 768;
1572/// The router's margin of a round enters the running average with this
1573/// weight, and the drafter in turn keeps the rounds while the average is
1574/// inside this band (the margin is a difference of squared errors of unit
1575/// rows).
1576pub const DFLASH_LEAN_STEP: f32 = 0.5;
1577pub const DFLASH_LEAN_BAND: f32 = 0.0;
1578/// A stretch being copied from the context is the chain's whatever the router
1579/// says: this many of the last tokens repeating an earlier stretch (see
1580/// `tail_repeat`). Its short round and the lookup after it draft such text
1581/// for less than a block (edits and quotes: 8 keeps the code's rounds, where
1582/// three or four tokens repeat all the time).
1583pub const DFLASH_COPY_MIN: usize = 8;
1584/// The running gain of the chain's lookups (`spec_lookup_gain`) past which a
1585/// CUDA round stays the chain's: an edit of 512 tokens lost 15 % to the
1586/// block's rounds between its copies.
1587const DFLASH_LOOKUP_KEEPS: f32 = 5.0;
1588const DFLASH_WAIT_DIP: u32 = 8;
1589const DFLASH_TURN: u32 = 16;
1590
1591/// One logged start of a sequence, for `dflash_round_values`.
1592pub struct DflashStart<'a> {
1593    /// The committed stream through the round's first token.
1594    pub seq: &'a [u32],
1595    /// What the trunk went on to say after it (seven tokens or more).
1596    pub after: &'a [u32],
1597    /// The block's seven drafts there.
1598    pub block: &'a [u32],
1599    /// The chain's seven.
1600    pub chain: &'a [u32],
1601}
1602
1603/// For `cortiq dflash-route-fit`: what a round at each of a sequence's logged
1604/// starts lands and what it costs in plain tokens, `(tokens, cost)` under the
1605/// block and under the chain, each at the depth its own rule picks from the
1606/// starts before it, a prompt lookup going on after the drafts as it does in
1607/// a round (`METAL_ROUND_COST`, `DFLASH_ROUND_DRAFT`: the price of a round on
1608/// a MacBook Air M4).
1609pub fn dflash_round_values(starts: &[DflashStart<'_>]) -> Vec<((f32, f32), (f32, f32))> {
1610    dflash_round_values_at(starts, &METAL_ROUND_COST, DFLASH_ROUND_DRAFT, 0.0)
1611}
1612
1613/// `dflash_round_values` at another device's prices: `cost[k - 1]` plain
1614/// tokens for the verify and commit of a round of k drafts, `block_draft`
1615/// for a block, `chain_step` for each draft of the chain.
1616pub fn dflash_round_values_at(
1617    starts: &[DflashStart<'_>],
1618    cost: &[f32; 7],
1619    block_draft: f32,
1620    chain_step: f32,
1621) -> Vec<((f32, f32), (f32, f32))> {
1622    // a drafter's account of what holds, and what its lookups have paid
1623    struct Side {
1624        hold: [f32; 7],
1625        gain: f32,
1626        rest: u32,
1627        draft: f32,
1628        step: f32,
1629    }
1630    let mut seq: Vec<u32> = Vec::new();
1631    let mut round = |side: &mut Side, s: &DflashStart<'_>, own: &[u32]| -> (f32, f32) {
1632        let k = spec_plan_at(cost, &side.hold, side.draft, side.step).0.min(own.len()).max(1);
1633        let mut drafts = own[..k.min(own.len())].to_vec();
1634        if k < 7 && drafts.len() == k {
1635            if side.rest > 0 {
1636                side.rest -= 1;
1637            } else {
1638                seq.clear();
1639                seq.extend_from_slice(s.seq);
1640                seq.extend_from_slice(&drafts);
1641                let more = lookup_continuation(&seq, k + LOOKUP_MIN_REAL, 7 - k).to_vec();
1642                drafts.extend(more);
1643            }
1644        }
1645        let kept = drafts.iter().zip(s.after).take_while(|(d, t)| d == t).count();
1646        if drafts.len() > k {
1647            side.gain += 0.2 * (kept.saturating_sub(k) as f32 - side.gain);
1648            if side.gain < LOOKUP_PAYS {
1649                (side.rest, side.gain) = (24, 1.6);
1650            }
1651        }
1652        spec_hold_update(&mut side.hold, k, kept.min(k));
1653        ((1 + kept) as f32, cost[drafts.len().clamp(1, 7) - 1] + side.draft + side.step * k as f32)
1654    };
1655    let mut block = Side { hold: DFLASH_HOLD_PRIOR, gain: 2.0, rest: 0, draft: block_draft, step: 0.0 };
1656    let mut chain = Side { hold: SPEC_HOLD_PRIOR, gain: 2.0, rest: 0, draft: 0.0, step: chain_step };
1657    starts.iter().map(|s| (round(&mut block, s, s.block), round(&mut chain, s, s.chain))).collect()
1658}
1659
1660/// `CMF_DFLASH_ROWS=FILE`: every round of the DFlash block is written there
1661/// for `cortiq dflash-route-fit`, and the MTP chain drafts the same round
1662/// beside it, unverified. A record is 24 u32 and the target's row at the
1663/// tip (hidden f32), little-endian: the position of the round's first
1664/// token, that token, drafts verified, drafts accepted, the block's seven
1665/// drafts, the chain's seven, the lookup's count and its five after the
1666/// chain's first two (`u32::MAX` where there is none). `FILE.ids` holds the
1667/// stream through the first logged round's first token (u32).
1668fn dflash_rows_log() -> Option<&'static std::sync::Mutex<std::fs::File>> {
1669    static LOG: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> = std::sync::OnceLock::new();
1670    LOG.get_or_init(|| {
1671        let path = std::env::var("CMF_DFLASH_ROWS").ok()?;
1672        match std::fs::OpenOptions::new().create(true).append(true).open(&path) {
1673            Ok(f) => Some(std::sync::Mutex::new(f)),
1674            Err(e) => {
1675                tracing::warn!("CMF_DFLASH_ROWS: {path}: {e}");
1676                None
1677            }
1678        }
1679    })
1680    .as_ref()
1681}
1682
1683/// Rows of the trunk's tapped layers that may wait for the drafter
1684/// (`dflash_taps`): 100 KB a row on five layers of 5120.
1685const DFLASH_TAPS_ROWS: usize = 16384;
1686
1687/// `CMF_SPEC_SCRIPT=FILE`: token ids (the whole sequence, prompt first) that a
1688/// round of a trunk without a draft block takes its drafts from, in place of
1689/// a drafter. For tests of the verify and the rewind: with the plain greedy
1690/// sequence in the file every draft holds, and an id changed there is a
1691/// draft that does not.
1692#[cfg(feature = "gpu")]
1693fn solo_script() -> Option<&'static [u32]> {
1694    static IDS: std::sync::OnceLock<Option<Vec<u32>>> = std::sync::OnceLock::new();
1695    IDS.get_or_init(|| {
1696        let path = std::env::var("CMF_SPEC_SCRIPT").ok()?;
1697        match std::fs::read_to_string(&path) {
1698            Ok(s) => Some(s.split_whitespace().filter_map(|t| t.parse().ok()).collect()),
1699            Err(e) => {
1700                tracing::warn!("CMF_SPEC_SCRIPT: {path}: {e}");
1701                None
1702            }
1703        }
1704    })
1705    .as_deref()
1706}
1707
1708/// Little-endian bytes of f32 values (`Pipeline::dflash_dump`).
1709fn f32_bytes(v: &[f32]) -> Vec<u8> {
1710    v.iter().flat_map(|x| x.to_le_bytes()).collect()
1711}
1712
1713/// What a round of a trunk without a draft block leaves the decode loop
1714/// (`Pipeline::solo_spec_round`): the drafts that held, the trunk's row and
1715/// logits after the last of them.
1716#[cfg(feature = "gpu")]
1717struct SoloRound {
1718    accepted: Vec<u32>,
1719    drafted: usize,
1720    hidden: Vec<f32>,
1721    logits: Vec<f32>,
1722}
1723
1724/// The decode loop's account of those rounds: how deep the next one drafts,
1725/// and whether rounds land tokens faster than plain steps do.
1726#[cfg(feature = "gpu")]
1727struct SoloSpec {
1728    k: usize,
1729    /// Plain tokens to go before the next round, and the wait a losing
1730    /// stretch of rounds gets next time.
1731    rest: u32,
1732    back: u32,
1733    /// A plain token's time and a round's time per token it lands, ms
1734    /// (the first plain token of a sequence pays the warm-up: not a sample).
1735    plain_ms: f32,
1736    plain_n: u32,
1737    round_ms: f32,
1738    rounds: u32,
1739    /// The judging window: the rounds' summed wall and summed tokens (a
1740    /// ratio of sums — a mean of per-round ratios misrates a drafter).
1741    win_ms: f32,
1742    win_tok: u32,
1743    /// The step in flight: its start, and the tokens it lands (0: a plain one).
1744    last: Option<(std::time::Instant, usize)>,
1745    /// The sequence's first round is still to come: it pays the drafter's
1746    /// read of the prompt (and, on a device block, the kernels' warm-up),
1747    /// so like the first plain token it is not a sample. Before this the
1748    /// 700 ms of that round sat in the EWMA for a dozen rounds and sent a
1749    /// drafter that was winning 40 against 30 tok/s on code to rest.
1750    first: bool,
1751}
1752
1753/// The deepest round of a trunk without a draft block (the tip and seven
1754/// drafts: the batch graph's narrow kernels take eight rows, and a ninth
1755/// row costs a plane-wide GEMM a layer — 660 ms a verify against 120 on
1756/// the A40), the rounds a stretch is judged over, and the waits of a
1757/// stretch that lost.
1758#[cfg(feature = "gpu")]
1759const SOLO_K_MOST: usize = 7;
1760/// The deepest round the pay-off monitor picks on its own. The verify's
1761/// int8 batch kernels grow with the rows (eight rows 108 ms, five 70 on
1762/// Ripple-65B / RTX PRO 6000), so seven drafts at 80 % kept gave 36 tok/s
1763/// where four gave 47; `CMF_GRAPH_SPEC_K` still pins any depth to 7.
1764#[cfg(feature = "gpu")]
1765const SOLO_K_AUTO: usize = 5;
1766#[cfg(feature = "gpu")]
1767const SOLO_TRIAL: u32 = 6;
1768/// The rounds a judging window holds before its first verdict.
1769#[cfg(feature = "gpu")]
1770const SOLO_JUDGE: u32 = 3;
1771#[cfg(feature = "gpu")]
1772const SOLO_REST: u32 = 32;
1773#[cfg(feature = "gpu")]
1774const SOLO_REST_MOST: u32 = 512;
1775
1776/// Which drafter takes rounds when a DFlash sidecar sits beside the model's
1777/// own MTP block: `CMF_DFLASH_PICK=dflash` / `mtp` pins one, anything else
1778/// leaves it to what each of them holds.
1779fn dflash_pick() -> Option<bool> {
1780    static PICK: std::sync::OnceLock<Option<bool>> = std::sync::OnceLock::new();
1781    *PICK.get_or_init(|| match std::env::var("CMF_DFLASH_PICK").as_deref() {
1782        Ok("dflash") => Some(true),
1783        Ok("mtp") => Some(false),
1784        _ => None,
1785    })
1786}
1787
1788/// A round of `k` drafts held its first `accepted`: move every depth the
1789/// round put to the test. Depths past `k` keep their last value, except
1790/// while the deepest one shown holds in nine rounds of ten: text that
1791/// predictable is worth a longer chain now and then, so they drift towards
1792/// it until a deeper round measures them.
1793fn spec_hold_update(hold: &mut [f32; 7], k: usize, accepted: usize) {
1794    let k = k.clamp(1, 7);
1795    for j in 1..=k {
1796        if accepted + 1 >= j {
1797            let hit = if accepted >= j { 1.0 } else { 0.0 };
1798            hold[j - 1] += 0.1 * (hit - hold[j - 1]);
1799        }
1800    }
1801    if k < 7 && hold[k - 1] >= 0.9 {
1802        for j in k + 1..=7 {
1803            hold[j - 1] += 0.005 * (hold[k - 1] - hold[j - 1]);
1804        }
1805    }
1806}
1807
1808/// Real tokens before the head's drafts that have to repeat with them for a
1809/// lookup to continue the chain.
1810const LOOKUP_MIN_REAL: usize = 3;
1811/// Drafts a lookup has to add to a round on average to pay for the wider
1812/// verify (a round of two drafts against one of seven on the M4: 204 and
1813/// 281 ms).
1814const LOOKUP_PAYS: f32 = 1.0;
1815
1816/// Prompt lookup: where the tail of `seq`, its last `min_len` tokens or
1817/// more, repeats an earlier stretch, the tokens that followed that stretch
1818/// there, at most `want` of them. The longest repeat wins, the latest of
1819/// equal ones.
1820fn lookup_continuation(seq: &[u32], min_len: usize, want: usize) -> &[u32] {
1821    const CAP: usize = 32;
1822    let n = seq.len();
1823    if want == 0 || min_len == 0 || n <= min_len {
1824        return &[];
1825    }
1826    let last = seq[n - 1];
1827    let mut best = (0usize, 0usize);
1828    for p in (0..n - 1).rev() {
1829        if seq[p] != last {
1830            continue;
1831        }
1832        let mut l = 1;
1833        while l < CAP && l <= p && seq[p - l] == seq[n - 1 - l] {
1834            l += 1;
1835        }
1836        if l > best.0 {
1837            best = (l, p);
1838            if l == CAP {
1839                break;
1840            }
1841        }
1842    }
1843    if best.0 < min_len {
1844        return &[];
1845    }
1846    let from = best.1 + 1;
1847    &seq[from..(from + want).min(n)]
1848}
1849
1850/// How many of the last tokens of `seq` repeat an earlier stretch of it (the
1851/// longest such repeat, 32 at most): text that is being copied from the
1852/// context. A lookup drafts such a stretch for nothing after the chain's
1853/// short round, so a round there is not worth a DFlash block.
1854pub fn tail_repeat(seq: &[u32]) -> usize {
1855    const CAP: usize = 32;
1856    let n = seq.len();
1857    if n < 2 {
1858        return 0;
1859    }
1860    let last = seq[n - 1];
1861    let mut best = 0usize;
1862    for p in (0..n - 1).rev() {
1863        if seq[p] != last {
1864            continue;
1865        }
1866        let mut l = 1;
1867        while l < CAP && l <= p && seq[p - l] == seq[n - 1 - l] {
1868            l += 1;
1869        }
1870        if l > best {
1871            best = l;
1872            if l == CAP {
1873                break;
1874            }
1875        }
1876    }
1877    best
1878}
1879
1880/// Zero-acceptance rounds in a row that end a speculation trial early. Two
1881/// was too few: a good head meets two empty rounds in a row now and then
1882/// (OrcaCyber-Nano-1.0 on an RTX 2000 Ada, 95 pinned rounds of code and a
1883/// review: four such pairs, no triple), and at the start of a chat turn a
1884/// pair sent the whole 200-token turn to plain decode, 13.4 tok/s against
1885/// 25.9 with the drafts. A head that never holds costs one more round.
1886const SPEC_ZERO_ROUNDS: u8 = 3;
1887/// Tokens a plain decision taken on thin evidence (fewer than five timed
1888/// rounds) stands before the rounds are tried again; one taken on a full
1889/// trial stands for `SPEC_RECHECK_PLAIN`.
1890const SPEC_RECHECK_THIN: usize = 32;
1891const SPEC_RECHECK_PLAIN: usize = 128;
1892
1893/// Tokens per round at or above which a Metal round pays without a plain
1894/// measurement (see `SpecMon`).
1895const SPEC_PROXY_TOKENS: f64 = 3.5;
1896/// The CUDA hybrid arm's proxy (see `SpecMon::cuda`): RTX 3090, Qwen3.8-27B,
1897/// a round 24.5-26.8 ms against a plain token's 20.4.
1898const SPEC_PROXY_TOKENS_CUDA: f64 = 2.0;
1899/// The Metal plain phase: at least two tokens, and more until this much
1900/// wall time has been timed (up to the eight the other backends time).
1901const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1902
1903impl SpecMon {
1904    fn round(&mut self, dt_ms: f64, produced: usize) {
1905        if produced <= 1 {
1906            self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1907        } else {
1908            self.zero_accept_rounds = 0;
1909        }
1910        self.n += 1;
1911        if self.n == 1 {
1912            return; // round 1 pays the batch scratch and the draft mirror
1913        }
1914        let a = if self.n == 2 { 1.0 } else { 0.3 };
1915        self.round_ms += a * (dt_ms - self.round_ms);
1916        self.tokens += a * (produced as f64 - self.tokens);
1917    }
1918    fn pays(&self) -> bool {
1919        if self.plain_ms > 0.0 {
1920            self.tokens * self.plain_ms > self.round_ms * 1.03
1921        } else {
1922            (self.metal && self.tokens >= SPEC_PROXY_TOKENS) || (self.cuda && self.tokens >= SPEC_PROXY_TOKENS_CUDA)
1923        }
1924    }
1925    fn repeated_zero_acceptance(&self) -> bool {
1926        self.zero_accept_rounds >= SPEC_ZERO_ROUNDS
1927    }
1928    /// When a plain decision is tried again: soon if the rounds behind it
1929    /// were few (round 1 is not timed, so `n` counts one more).
1930    fn recheck_plain_at(&self, generated: usize) -> usize {
1931        generated + if self.n < 6 { SPEC_RECHECK_THIN } else { SPEC_RECHECK_PLAIN }
1932    }
1933    /// Has the plain phase timed enough tokens to decide?
1934    fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1935        let n = generated.saturating_sub(gen0);
1936        if n >= 8 {
1937            return true;
1938        }
1939        self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1940    }
1941}
1942
1943/// Ids of the consumed prefix the bounded reuse key remembers literally
1944/// (the rest is covered by the rolling hash).
1945pub const KV_PREFIX_TAIL: usize = 128;
1946
1947/// Bounded prefix-reuse key: how many ids the cache holds, a rolling
1948/// hash of ALL of them and the last [`KV_PREFIX_TAIL`] ids literally.
1949/// Answers "does this prompt strictly extend what is cached" exactly
1950/// (hash over the whole consumed prefix + literal tail) without keeping
1951/// the dialogue — the record is a fixed size whatever the session length.
1952#[derive(Debug, Clone, Default)]
1953pub struct KvPrefix {
1954    len: usize,
1955    hash: u64,
1956    tail: Vec<u32>,
1957    /// Owner of the state this key describes: the resident device graph
1958    /// (true) or the host. A turn continues the prefix only on its owner
1959    /// (R4: a host continuation of a device sequence reads an empty host
1960    /// state; a device continuation of a host sequence has no image).
1961    device: bool,
1962}
1963
1964impl KvPrefix {
1965    #[inline]
1966    fn fold(mut h: u64, ids: &[u32]) -> u64 {
1967        for &id in ids {
1968            h ^= id as u64;
1969            h = h.wrapping_mul(0x100000001b3);
1970            h ^= h >> 29;
1971        }
1972        h
1973    }
1974
1975    pub fn clear(&mut self) {
1976        self.len = 0;
1977        self.hash = 0xcbf29ce484222325;
1978        self.tail.clear();
1979        self.device = false;
1980    }
1981
1982    /// Was the prefix built on the resident device graph?
1983    pub fn on_device(&self) -> bool {
1984        self.device
1985    }
1986
1987    /// Tag the owner of the recorded prefix.
1988    pub fn set_on_device(&mut self, device: bool) {
1989        self.device = device;
1990    }
1991
1992    /// Ids the cache holds (the forwarded prefix).
1993    pub fn len(&self) -> usize {
1994        self.len
1995    }
1996
1997    pub fn is_empty(&self) -> bool {
1998        self.len == 0
1999    }
2000
2001    /// Literal tail currently kept (≤ `KV_PREFIX_TAIL`).
2002    pub fn tail_len(&self) -> usize {
2003        self.tail.len()
2004    }
2005
2006    /// Replace the key with `ids` (a fresh sequence).
2007    pub fn set(&mut self, ids: &[u32]) {
2008        self.clear();
2009        self.extend(ids);
2010    }
2011
2012    /// Append `more` to the consumed prefix (an extension-only turn).
2013    pub fn extend(&mut self, more: &[u32]) {
2014        if self.len == 0 && self.hash == 0 {
2015            self.hash = 0xcbf29ce484222325;
2016        }
2017        self.hash = Self::fold(self.hash, more);
2018        self.len += more.len();
2019        if more.len() >= KV_PREFIX_TAIL {
2020            self.tail.clear();
2021            self.tail
2022                .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
2023        } else {
2024            let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
2025            self.tail.drain(..drop);
2026            self.tail.extend_from_slice(more);
2027        }
2028    }
2029
2030    /// Cached positions when `ids` strictly extends the consumed prefix,
2031    /// 0 otherwise. The tail is compared literally first (cheap), then
2032    /// the hash over the whole prefix must agree.
2033    pub fn extension(&self, ids: &[u32]) -> usize {
2034        if self.len == 0 || ids.len() <= self.len {
2035            return 0;
2036        }
2037        let t = self.tail.len();
2038        if ids[self.len - t..self.len] != self.tail[..] {
2039            return 0;
2040        }
2041        if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
2042            return 0;
2043        }
2044        self.len
2045    }
2046}
2047
2048/// Result of a generation call.
2049pub struct GenerateResult {
2050    pub text: String,
2051    pub token_ids: Vec<u32>,
2052    pub prompt_tokens: usize,
2053    pub tokens_generated: usize,
2054    pub finish_reason: String,
2055    /// Speculative-decode stats (0/0 when MTP is absent or inactive).
2056    pub mtp_drafted: usize,
2057    pub mtp_accepted: usize,
2058    /// Per-generated-token confidence = softmax probability of the token
2059    /// that was actually emitted (softmax probability on the chosen state). High =
2060    /// the model was sure; low = it was guessing. Same length as the
2061    /// generated slice of `token_ids`.
2062    pub token_confidence: Vec<f32>,
2063    /// Structured per-token telemetry (B4 channel). Empty unless
2064    /// `set_trace(true)`; otherwise same length as the generated slice.
2065    pub traces: Vec<TokenTrace>,
2066}
2067
2068/// One row of the structured telemetry trace (B4): the model's internal
2069/// routing state at the moment a token was emitted. Every field is a
2070/// quantity the runtime already computes — nothing is inferred or
2071/// estimated (anti-principle: only measured bytes).
2072#[derive(Clone, Debug)]
2073pub struct TokenTrace {
2074    /// 0-based index within the generated slice.
2075    pub t: usize,
2076    /// The emitted token id.
2077    pub token_id: u32,
2078    /// Softmax probability on the emitted token — how sure the model was.
2079    pub confidence: f32,
2080    /// Skill in force while this token was generated (None = backbone).
2081    pub active_skill: Option<String>,
2082    /// Recon error E = ‖r−BBᵀr‖²/‖φ‖² at the last routing eval — coherence
2083    /// with the active skill's subspace (low = coherent). None = no router
2084    /// or not yet evaluated.
2085    pub recon: Option<f32>,
2086    /// The router changed the active skill right after this token (a
2087    /// domain boundary crossed under the hysteresis barrier).
2088    pub switched: bool,
2089}
2090
2091/// Calibrated softmax probability of `id` under `logits` (the confidence on
2092/// the emitted token) — the confidence signal, cheap from logits already
2093/// computed for sampling. `temp` is the calibration temperature (B1):
2094/// softmax(logits / temp); 1.0 = raw.
2095#[cfg_attr(not(test), allow(dead_code))]
2096fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
2097    let t = if temp > 1e-3 { temp } else { 1.0 };
2098    let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2099    let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
2100    if sum > 0.0 {
2101        (((logits[id as usize] - max) / t).exp()) / sum
2102    } else {
2103        0.0
2104    }
2105}
2106
2107/// prefill-GEMM enabled? (CMF_PREFILL=seq — emergency fallback to the
2108/// sequential path.)
2109fn prefill_batched() -> bool {
2110    std::env::var("CMF_PREFILL")
2111        .map(|v| v != "seq")
2112        .unwrap_or(true)
2113}
2114
2115/// Decide the graph NLL route without conflating graph quality with the
2116/// optional native-Metal fused head. A hidden-state graph remains a valid
2117/// quality route on Vulkan/Wgpu; only native Metal requires graph logits.
2118#[inline]
2119/// ‖a − b‖ / ‖b‖ (infinite when the lengths differ).
2120fn rel_l2(a: &[f32], b: &[f32]) -> f64 {
2121    if a.len() != b.len() {
2122        return f64::INFINITY;
2123    }
2124    let (mut d, mut r) = (0f64, 0f64);
2125    for (&x, &y) in a.iter().zip(b) {
2126        let e = (x - y) as f64;
2127        d += e * e;
2128        r += y as f64 * y as f64;
2129    }
2130    (d / r.max(1e-30)).sqrt()
2131}
2132
2133fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
2134    let graph_quality = unmasked && prefer_graph;
2135    let fused_head_quality = graph_quality && native_metal;
2136    (graph_quality, fused_head_quality)
2137}
2138
2139/// Input to the layer-major batched span walk: token ids (embeds itself,
2140/// full-stack and coordinator prefill) or ready boundary hiddens (the
2141/// network worker's side of a split).
2142#[derive(Clone, Copy)]
2143enum PrefillIn<'a> {
2144    Ids(&'a [u32]),
2145    Hidden(&'a [f32]),
2146}
2147
2148/// The batched prefill walks `weights.layers`. Architectures that load
2149/// their own stack (gemma-3n's AltUp replicas, DeepSeek-V4's hyper-
2150/// connections) leave that empty and must go position by position — asking
2151/// otherwise indexes an empty vector, which is a panic rather than a
2152/// fallback. Every call site goes through here so the next such
2153/// architecture is one line, not four.
2154/// What a turbo file's header asks for.
2155#[derive(Clone)]
2156pub(crate) struct O1Turbo {
2157    pub(crate) cfg: crate::nystrom::O1Cfg,
2158    /// A prompt this long goes over to the far field; a shorter one stays exact.
2159    pub(crate) seal: usize,
2160    pub(crate) exact: crate::dory::ExactRows,
2161}
2162
2163/// Rows of a prompt read exactly on the card before the first seal, at most. Past them
2164/// the rows leave the card as they are read.
2165const O1_PREFIX_MAX: usize = 8192;
2166
2167/// Where a turbo file seals on this card. Exact rows take about 190 KB each on Qwen3.8-27B,
2168/// and the state, the scratch and the draft head about 1.8 GB beside the weights: a 16 GB card
2169/// has no room for 8192 rows, and gives up a little prompt speed to keep the ceiling.
2170/// `CMF_O1_PREFIX_MAX` chooses by hand.
2171pub(crate) fn o1_seal_for_card(seal: usize, weight_bytes: u64) -> usize {
2172    o1_seal_for_card_rows(seal, weight_bytes, 190 << 10)
2173}
2174
2175/// `o1_seal_for_card` for rows of `row_bytes` on the card (Dory's full layers: 54 KB).
2176pub(crate) fn o1_seal_for_card_rows(seal: usize, weight_bytes: u64, row_bytes: u64) -> usize {
2177    if let Some(s) = std::env::var("CMF_O1_PREFIX_MAX").ok().and_then(|v| v.parse().ok()) {
2178        return s;
2179    }
2180    let mut budget = crate::gpu::vram_budget();
2181    // Unified memory: what the system lets the GPU keep, and a row there lives twice, in the
2182    // host's cache and in the mirror.
2183    let mut row = row_bytes.max(1);
2184    #[cfg(target_os = "macos")]
2185    if budget == u64::MAX {
2186        budget = crate::gpu_metal::working_set_bytes();
2187        row *= 2;
2188    }
2189    if budget == 0 || budget == u64::MAX {
2190        return seal;
2191    }
2192    let room = budget.saturating_sub(weight_bytes + (1800 << 20));
2193    seal.min(((room / row) as usize).max(O1_PREFIX_MAX / 2))
2194}
2195
2196/// Rows of an archive as they come in: in RAM, or in a file (`CMF_O1_SPILL=DIR`) when
2197/// RAM is short and a disk is not.
2198pub(crate) enum ArchStore {
2199    Ram(Vec<f32>),
2200    Disk { path: std::path::PathBuf, file: std::io::BufWriter<std::fs::File> },
2201}
2202
2203impl ArchStore {
2204    pub(crate) fn new(dir: Option<&std::path::Path>, name: String) -> Result<Self, String> {
2205        let Some(dir) = dir else { return Ok(Self::Ram(Vec::new())) };
2206        std::fs::create_dir_all(dir).map_err(|e| format!("O(1) spill {}: {e}", dir.display()))?;
2207        let path = dir.join(format!("cortiq-o1-{}-{name}.rows", std::process::id()));
2208        let file = std::fs::File::create(&path).map_err(|e| format!("O(1) spill {}: {e}", path.display()))?;
2209        Ok(Self::Disk { path, file: std::io::BufWriter::with_capacity(1 << 22, file) })
2210    }
2211
2212    pub(crate) fn push(&mut self, rows: &[f32]) -> Result<(), String> {
2213        match self {
2214            Self::Ram(v) => {
2215                v.extend_from_slice(rows);
2216                Ok(())
2217            }
2218            Self::Disk { path, file } => {
2219                use std::io::Write;
2220                // SAFETY: an `f32` slice is valid as bytes of four times its length.
2221                let bytes = unsafe { std::slice::from_raw_parts(rows.as_ptr().cast::<u8>(), rows.len() * 4) };
2222                file.write_all(bytes).map_err(|e| format!("O(1) spill {}: {e}", path.display()))
2223            }
2224        }
2225    }
2226
2227    pub(crate) fn freeze(mut self) -> Result<ArchRows, String> {
2228        match self {
2229            Self::Ram(ref mut v) => Ok(ArchRows::Ram(std::mem::take(v))),
2230            Self::Disk { ref mut path, ref mut file } => {
2231                use std::io::Write;
2232                file.flush().map_err(|e| format!("O(1) spill {}: {e}", path.display()))?;
2233                let f = std::fs::File::open(&*path).map_err(|e| format!("O(1) spill {}: {e}", path.display()))?;
2234                // SAFETY: the file is this process's own and no one writes it any more.
2235                let map = unsafe { memmap2::Mmap::map(&f) }.map_err(|e| format!("O(1) spill {}: {e}", path.display()))?;
2236                Ok(ArchRows::Disk { map: Some(map), path: std::mem::take(path) })
2237            }
2238        }
2239    }
2240}
2241
2242impl Drop for ArchStore {
2243    fn drop(&mut self) {
2244        if let Self::Disk { path, .. } = self {
2245            // A store that was frozen hands its file over; the file is gone only here if not.
2246            if !path.as_os_str().is_empty() {
2247                let _ = std::fs::remove_file(&*path);
2248            }
2249        }
2250    }
2251}
2252
2253pub(crate) enum ArchRows {
2254    Ram(Vec<f32>),
2255    Disk { map: Option<memmap2::Mmap>, path: std::path::PathBuf },
2256}
2257
2258impl ArchRows {
2259    pub(crate) fn as_slice(&self) -> &[f32] {
2260        match self {
2261            Self::Ram(v) => v,
2262            Self::Disk { map, .. } => {
2263                let b = map.as_ref().map_or(&[][..], |m| &m[..]);
2264                // SAFETY: a page-aligned map of what `push` wrote, whole `f32`s.
2265                unsafe { std::slice::from_raw_parts(b.as_ptr().cast::<f32>(), b.len() / 4) }
2266            }
2267        }
2268    }
2269}
2270
2271impl Drop for ArchRows {
2272    fn drop(&mut self) {
2273        if let Self::Disk { map, path } = self {
2274            *map = None;
2275            let _ = std::fs::remove_file(&*path);
2276        }
2277    }
2278}
2279
2280/// The rows of a prompt past its first seal, off the card until the far field is built
2281/// anew over all of them.
2282struct O1Archive {
2283    dir: Option<std::path::PathBuf>,
2284    layers: Vec<O1ArchLayer>,
2285    rows: usize,
2286    stride: usize,
2287    nh: usize,
2288    nk: usize,
2289    hd: usize,
2290}
2291
2292impl O1Archive {
2293    fn new() -> Self {
2294        Self { dir: std::env::var_os("CMF_O1_SPILL").map(Into::into), layers: Vec::new(), rows: 0, stride: 1, nh: 0, nk: 0, hd: 0 }
2295    }
2296}
2297
2298struct O1ArchLayer {
2299    li: usize,
2300    k: ArchStore,
2301    v: ArchStore,
2302    q: ArchStore,
2303}
2304
2305impl Pipeline {
2306    pub(crate) fn can_prefill_batched(&self) -> bool {
2307        #[cfg(test)]
2308        let force_serial = self.nll_test_force_serial;
2309        #[cfg(not(test))]
2310        let force_serial = false;
2311        prefill_batched() && !force_serial && !self.weights.layers.is_empty()
2312    }
2313
2314    /// The backend's automatic capacity split for a mapped transformer.
2315    /// Kept as a method so prefill and decode use the exact same boundary.
2316    fn automatic_gpu_prefix(&self) -> Option<usize> {
2317        let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
2318        crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
2319    }
2320
2321    /// Positions per batched pass of the layer-stack prefill for THIS
2322    /// model on THIS backend (see [`prefill_chunk_rule`]). Pub: the network
2323    /// split must chunk exactly like the local path to reproduce it.
2324    pub fn prefill_chunk(&self) -> usize {
2325        let env = env_prefill_chunk();
2326        if env.is_some() || ChunkHost::here() != ChunkHost::Other {
2327            return prefill_chunk_rule(env, ChunkHost::here(), false);
2328        }
2329        let facts = self.chunk_stack_facts();
2330        if facts.dense_on_cpu() {
2331            return CPU_DENSE_PREFILL_CHUNK;
2332        }
2333        prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete())
2334    }
2335
2336    /// Prompt lookup in a speculative round (`CMF_SPEC_LOOKUP=0` leaves the
2337    /// chain to the draft head alone).
2338    fn spec_lookup_on() -> bool {
2339        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2340        *ON.get_or_init(|| std::env::var("CMF_SPEC_LOOKUP").as_deref() != Ok("0"))
2341    }
2342
2343    /// Whether a verify of two or three rows runs on the small-batch matvec:
2344    /// native Metal, a dense q4tp FFN in the trunk, the kernel not switched
2345    /// off (`CMF_METAL_MVS=0`).
2346    fn metal_small_batch_verify(&self) -> bool {
2347        #[cfg(target_os = "macos")]
2348        {
2349            crate::gpu::q1_force()
2350                && crate::gpu_metal::small_batch_mv()
2351                && self.weights.layers.first().is_some_and(|l| match &l.ffn {
2352                    FfnKind::Dense(d) => d.gate_proj.mapped_q4tp().is_some(),
2353                    _ => false,
2354                })
2355        }
2356        #[cfg(not(target_os = "macos"))]
2357        {
2358            false
2359        }
2360    }
2361
2362    /// Architectures that run a dedicated per-token stack (gemma-3n's
2363    /// AltUp flow, the gemma-4 E-series PLE/shared-KV body): batched
2364    /// prefill and the graphs stay off for them.
2365    fn per_token_stack(&self) -> bool {
2366        self.g3n.is_some() || self.g4e.is_some()
2367    }
2368
2369    fn chunk_stack_facts(&self) -> ChunkStackFacts {
2370        let plain_dense = !self.weights.layers.is_empty()
2371            && !self.per_token_stack()
2372            && self.dsv4.is_none()
2373            && self.dsv41.is_none()
2374            && self.dory.is_none()
2375            && self.qwen4_exp.is_none()
2376            && self.weights.layers.iter().all(|lw| {
2377                // LFM2/LFM3's short-conv mixer is two projection GEMMs and
2378                // an elementwise conv: the host-driven chunk walk starves
2379                // the card the same way (LFM3-1.7B q8_2f on an A40, 2048
2380                // positions: 137 tok/s at 48, 460-640 at 512).
2381                matches!(lw.attn, AttnKind::Full { .. } | AttnKind::ShortConv(_))
2382                    && matches!(lw.ffn, FfnKind::Dense(_))
2383            });
2384        let gpu_on = crate::gpu::enabled();
2385        ChunkStackFacts {
2386            plain_dense,
2387            discrete: gpu_on && crate::gpu::discrete(),
2388            gpu_on,
2389            // Only asked when the rest already qualifies: it opens the
2390            // backend's capacity plan.
2391            capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
2392                || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
2393            multi_gpu: self.gpu_plan.is_some(),
2394            o1: self.o1_active(),
2395        }
2396    }
2397}
2398
2399/// Prefill chunk (positions per batched pass), model-agnostic form. On
2400/// macOS the AMX GEMM path wants tall panels — M=48 starves the matrix
2401/// units (ggml uses ubatch 512); elsewhere the historical 48 stays.
2402/// CMF_PREFILL_CHUNK overrides. The architectures with their own stacks
2403/// (DeepSeek-V4/V4.1) chunk with this; the layer-stack prefill asks
2404/// [`Pipeline::prefill_chunk`], which also knows the model and the card.
2405/// A different chunk is a different (equally valid) generation: panel
2406/// width reorders float accumulation.
2407pub fn prefill_chunk() -> usize {
2408    prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
2409}
2410
2411fn env_prefill_chunk() -> Option<usize> {
2412    std::env::var("CMF_PREFILL_CHUNK")
2413        .ok()
2414        .and_then(|v| v.parse::<usize>().ok())
2415}
2416
2417/// The host classes the chunk width distinguishes.
2418#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2419enum ChunkHost {
2420    Macos,
2421    /// Linux/Android aarch64 (phones, SBCs).
2422    Aarch64,
2423    /// Everything else: x86-64 Linux/Windows, CPU or Vulkan/DX12.
2424    Other,
2425}
2426
2427impl ChunkHost {
2428    fn here() -> Self {
2429        if cfg!(target_os = "macos") {
2430            ChunkHost::Macos
2431        } else if cfg!(target_arch = "aarch64") {
2432            ChunkHost::Aarch64
2433        } else {
2434            ChunkHost::Other
2435        }
2436    }
2437}
2438
2439/// Chunk for a plain dense stack whose every layer lives on a discrete
2440/// card. On x86 the layer-stack prefill is host-driven: each GEMM and the
2441/// chunk attention (which re-uploads the whole KV prefix per layer) is a
2442/// separate submit + readback, so 48 positions a pass left the card idle
2443/// between them. Measured in-process on an RTX 3090 (Vulkan), 2048-token
2444/// prompt — see CHANGELOG 0.7.6 for the table.
2445const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
2446
2447/// Chunk for a plain dense stack with no GPU at all on x86-64: the AVX2
2448/// q4tp batch kernel and the micro-GEMM chunk attend feed on wider
2449/// panels. Spark-X2.6-4B q4tp, 1000-token prompt on 8 Zen 2 cores: 20.7
2450/// tok/s at 48, 24.7 at 128, 25.0 at 256.
2451const CPU_DENSE_PREFILL_CHUNK: usize = 128;
2452
2453/// The chunk-width rule. `dense_on_discrete` is true only for a plain
2454/// dense transformer (full attention, dense FFN, no special stack) that
2455/// is entirely resident on one discrete card — the one case measured
2456/// here. GDN hybrids, MoE, DeepSeek stacks, capacity-split and CPU-only
2457/// runs keep the width they were tuned with.
2458fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
2459    if let Some(n) = env {
2460        return n.max(1);
2461    }
2462    match host {
2463        ChunkHost::Macos => 512,
2464        // Mobile: big enough to feed the batched attend (gate b ≥ 32)
2465        // and the blocked SDOT GEMM without the memory of 512.
2466        ChunkHost::Aarch64 => 256,
2467        ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
2468        ChunkHost::Other => 48,
2469    }
2470}
2471
2472/// What the chunk rule needs to know about a loaded stack.
2473#[derive(Clone, Copy, Debug, Default)]
2474struct ChunkStackFacts {
2475    /// Every layer is `AttnKind::Full` or `AttnKind::ShortConv` +
2476    /// `FfnKind::Dense`, and no architecture-owned stack (g3n,
2477    /// DeepSeek-V4/V4.1, qwen4-exp) is set.
2478    plain_dense: bool,
2479    /// The active GPU backend is a discrete card.
2480    discrete: bool,
2481    /// The backend is up and not paused.
2482    gpu_on: bool,
2483    /// A capacity-derived device prefix: some layers run on the host.
2484    capacity_split: bool,
2485    /// An in-process multi-GPU plan is set.
2486    multi_gpu: bool,
2487    /// O(1) layers (their Q trace is recorded by the prefill).
2488    o1: bool,
2489}
2490
2491impl ChunkStackFacts {
2492    /// A plain dense stack on an x86-64 host with no GPU backend up
2493    /// (O(1) stacks keep the width their trace was measured with).
2494    fn dense_on_cpu(self) -> bool {
2495        cfg!(target_arch = "x86_64")
2496            && self.plain_dense
2497            && !self.gpu_on
2498            && !self.multi_gpu
2499            && !self.o1
2500    }
2501
2502    fn dense_on_discrete(self) -> bool {
2503        self.plain_dense
2504            && self.discrete
2505            && self.gpu_on
2506            && !self.capacity_split
2507            && !self.multi_gpu
2508            && !self.o1
2509    }
2510}
2511
2512/// Number of prompt rows that have a real teacher-forced next-token pair in a
2513/// prefill span.  The final prompt row has no successor token, so it must not
2514/// be handed to the MTP warm-up.  Keeping this arithmetic in one helper makes
2515/// the full-chunk and tail-chunk boundaries explicit for both the graph and
2516/// CPU implementations.
2517#[inline]
2518fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
2519    if end <= start || start >= input_len {
2520        return 0;
2521    }
2522    let rows = (end.min(input_len) - start).min(input_len - start);
2523    if end < input_len {
2524        rows
2525    } else {
2526        rows.saturating_sub(1)
2527    }
2528}
2529
2530/// Callback for streaming tokens. Return `false` to cancel.
2531pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
2532
2533/// One layer's cache ownership at a cross-turn KV reuse boundary.
2534#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2535pub(crate) struct ReuseLayer {
2536    /// Exact-attention layer (rows in `LayerKvCache`); otherwise a
2537    /// recurrent / latent mixer whose state cannot be rewound.
2538    pub full: bool,
2539    /// Positions the host owner cache reaches (`pos_len`: a trimmed
2540    /// sliding tail stores only the newest of them).
2541    pub host_rows: usize,
2542    /// Rows the wgpu token graph's device mirror holds (None: no mirror).
2543    pub device_rows: Option<usize>,
2544    /// A recurrent state lives on the device (advanced past the host copy).
2545    pub device_state: bool,
2546}
2547
2548/// What a reused turn must do before its tail prefill runs on the HOST.
2549#[derive(Debug, Clone, PartialEq, Eq)]
2550pub(crate) enum ReusePlan {
2551    /// Host caches already hold exactly the reused prefix.
2552    Ready,
2553    /// Copy device mirror rows `[from..to)` into the host cache of each
2554    /// listed layer (the rows decode wrote on the device only).
2555    Pull(Vec<(usize, usize, usize)>),
2556    /// The prefix cannot be continued on the host exactly: start fresh.
2557    Fresh,
2558}
2559
2560/// The wgpu whole-token graph decodes into a DEVICE K/V mirror and never
2561/// writes those rows back to the host cache, while the chunked prefill of a
2562/// pure-attention model reads (and appends to) the host cache. A reused turn
2563/// therefore found its host cache ending at the previous PROMPT, not at the
2564/// previous answer: the tail prefill attended without the model's own
2565/// answer and appended its rows at the wrong index (MiniCPM5 on Vulkan
2566/// repeated its tool call instead of reading the tool result). Every layer
2567/// must hold exactly `reuse_from` host rows before the host continues; rows
2568/// that exist only on the device are pulled back, anything else is fresh.
2569pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
2570    let mut pulls = Vec::new();
2571    for (li, l) in layers.iter().enumerate() {
2572        if !l.full {
2573            if l.device_state {
2574                return ReusePlan::Fresh;
2575            }
2576            continue;
2577        }
2578        if l.host_rows == reuse_from {
2579            continue;
2580        }
2581        if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
2582            pulls.push((li, l.host_rows, reuse_from));
2583            continue;
2584        }
2585        return ReusePlan::Fresh;
2586    }
2587    if pulls.is_empty() {
2588        ReusePlan::Ready
2589    } else {
2590        ReusePlan::Pull(pulls)
2591    }
2592}
2593
2594impl Pipeline {
2595    /// Reset state whose meaning is local to a single MTP draft sequence.
2596    /// Unlike ordinary KV reuse, MTP is always rebuilt from the prompt.
2597    fn reset_fresh_mtp_draft_state(&mut self) {
2598        self.draft_full_streak = 0;
2599    }
2600
2601    /// Clear all per-sequence state, including backend device mirrors.
2602    ///
2603    /// The host KV/history buffers are only half of the request lifecycle on
2604    /// wgpu: GDN/O(1) state and cached graph bind groups are keyed by the
2605    /// pipeline id and otherwise survive a pooled request.  Keep every fresh
2606    /// sequence entry point on this one reset path so a new request cannot
2607    /// inherit the prior request's device state.
2608    fn clear_sequence_state(&mut self) {
2609        // a replay still writing the GDN owners must land before they are
2610        // cleared or reallocated (the device holds raw pointers to them)
2611        #[cfg(target_os = "macos")]
2612        let _ = crate::gpu_metal::wait_replay();
2613        self.kv_cache.clear();
2614        // Both reuse keys (the legacy `kv_history` and the bounded
2615        // `kv_prefix`) describe the state being dropped here.
2616        self.clear_history();
2617        self.graph_logits = None;
2618        if let Some(d) = &mut self.dflash {
2619            d.reset();
2620        }
2621        self.dflash_rows = None;
2622        self.dflash_taps.lock().unwrap().clear();
2623        // a new sequence owes the DFlash block nothing of the last one's turns
2624        self.dflash_hold = DFLASH_HOLD_PRIOR;
2625        (self.dflash_last, self.dflash_wait, self.dflash_back, self.dflash_turn) = (false, 0, DFLASH_WAIT, 0);
2626        self.dflash_turn_sum = (0.0, 0.0);
2627        self.dflash_lean = 0.0;
2628        if let Some(b) = &mut self.dsv41 {
2629            b.3.clear();
2630        }
2631        if let Some(b) = &mut self.dory {
2632            b.1.clear();
2633            crate::gpu::dory_reset(self.graph_kv_id);
2634        }
2635        crate::gpu::graph_kv_reset(self.graph_kv_id);
2636        // the card's GDN snapshot went with the sequence's states
2637        self.gdn_snap = None;
2638        self.mtp_warm_base = None;
2639        // MTP is detached from `self` for the duration of generation, so its
2640        // device mirror is not covered by the trunk reset above.  Reset the
2641        // derived id as well: a failed/aborted warm-up must never leave a
2642        // mirror that a later request can mistake for a current MTP cache.
2643        crate::gpu::graph_kv_reset(self.mtp_kv_id());
2644    }
2645
2646    /// Make the host caches own exactly the reused prefix `[0..reuse_from)`
2647    /// before a reused turn's tail prefill runs on the host (see
2648    /// [`kv_reuse_plan`]). Returns false when the prefix cannot be continued
2649    /// exactly — the caller then starts a fresh sequence. A model whose
2650    /// prefill runs through the token graph keeps its device state as the
2651    /// authority and is left untouched.
2652    fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
2653        if self.dory.is_some() || self.graph_prefill_preferred() {
2654            return true;
2655        }
2656        let kv_id = self.graph_kv_id;
2657        let layers: Vec<ReuseLayer> = (0..self.num_layers)
2658            .map(|li| {
2659                let full = matches!(
2660                    self.weights.layers[self.phys_layer(li)].attn,
2661                    AttnKind::Full { .. }
2662                );
2663                ReuseLayer {
2664                    full,
2665                    // Absolute depth: a trimmed sliding tail stores fewer
2666                    // rows than the positions it has seen.
2667                    host_rows: self.kv_cache.layers[li].pos_len(),
2668                    device_rows: crate::gpu::graph_kv_stored(kv_id, li),
2669                    device_state: crate::gpu::graph_state_resident(kv_id, li),
2670                }
2671            })
2672            .collect();
2673        // No wgpu device state at all (CPU, Metal — whose graph appends every
2674        // decoded row to the owner cache itself): the host is the owner and
2675        // the extension check already proved the prefix.
2676        if layers
2677            .iter()
2678            .all(|l| l.device_rows.is_none() && !l.device_state)
2679        {
2680            return true;
2681        }
2682        let plan = kv_reuse_plan(reuse_from, &layers);
2683        let (what, rows, n) = match &plan {
2684            ReusePlan::Ready => ("host ready", 0, 0),
2685            ReusePlan::Fresh => ("fresh", 0, 0),
2686            ReusePlan::Pull(p) => (
2687                "pull",
2688                p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
2689                p.len(),
2690            ),
2691        };
2692        let t0 = std::time::Instant::now();
2693        let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
2694        if std::env::var("CMF_PREFILL_PROF").is_ok() {
2695            eprintln!(
2696                "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
2697                if ok { "" } else { " (failed → fresh)" },
2698                t0.elapsed().as_secs_f64() * 1e3
2699            );
2700        }
2701        ok
2702    }
2703
2704    fn apply_kv_reuse_plan(
2705        &mut self,
2706        reuse_from: usize,
2707        plan: ReusePlan,
2708        layers: &[ReuseLayer],
2709    ) -> bool {
2710        let kv_id = self.graph_kv_id;
2711        match plan {
2712            ReusePlan::Fresh => return false,
2713            ReusePlan::Ready => {}
2714            ReusePlan::Pull(pulls) => {
2715                // Mirrors of one uniform geometry: one batched read serves
2716                // every layer. Per-layer geometry (MiMo-V2: 4/8 KV heads,
2717                // narrow V, sliding rings) is read layer by layer in the
2718                // host layout instead.
2719                let (nkv, hd) = {
2720                    let c = &self.kv_cache.layers[pulls[0].0];
2721                    (c.num_kv_heads, c.head_dim)
2722                };
2723                let uniform = pulls.iter().all(|&(li, _, _)| {
2724                    let c = &self.kv_cache.layers[li];
2725                    (c.num_kv_heads, c.head_dim) == (nkv, hd)
2726                });
2727                let batched = if uniform {
2728                    crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
2729                } else {
2730                    None
2731                };
2732                let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
2733                    Some(rows) => rows,
2734                    None => {
2735                        let mut rows = Vec::with_capacity(pulls.len());
2736                        for &(li, from, to) in &pulls {
2737                            let (lnkv, lhd) = {
2738                                let c = &self.kv_cache.layers[li];
2739                                (c.num_kv_heads, c.head_dim)
2740                            };
2741                            let Some((k, v, first_valid)) =
2742                                crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
2743                            else {
2744                                return false;
2745                            };
2746                            // The host continues at `to`: a sliding layer
2747                            // reads back only its last window, a full one
2748                            // every row it lacks.
2749                            let need_from = match self.layer_window(li) {
2750                                Some(w) => from.max((to + 1).saturating_sub(w)),
2751                                None => from,
2752                            };
2753                            if first_valid > need_from {
2754                                return false;
2755                            }
2756                            rows.push((k, v));
2757                        }
2758                        rows
2759                    }
2760                };
2761                for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
2762                    let cache = &mut self.kv_cache.layers[li];
2763                    let row = cache.num_kv_heads * cache.head_dim;
2764                    for p in 0..to - from {
2765                        cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
2766                    }
2767                    if cache.pos_len() != to {
2768                        return false;
2769                    }
2770                }
2771            }
2772        }
2773        // A mirror past the prefix (a greedy burst that ran beyond the stop)
2774        // holds rows of the OLD continuation: rewind it so the next graph
2775        // token re-syncs those positions from the host.
2776        for (li, l) in layers.iter().enumerate() {
2777            if l.full
2778                && l.device_rows.is_some_and(|d| d > reuse_from)
2779                && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
2780            {
2781                return false;
2782            }
2783        }
2784        true
2785    }
2786
2787    /// Finish a generation lifecycle after the MTP/router owners were
2788    /// detached.  Every terminal path must put those owners back before the
2789    /// pooled pipeline can serve another request.  Graph side channels and
2790    /// device mirrors are cleared on errors and cancellations; a successful
2791    /// generation keeps its decode-ready host cache for KV reuse.
2792    fn finish_generation(
2793        &mut self,
2794        mtp: &mut Option<MtpModule>,
2795        router: &mut Option<crate::swarm::DynRouter>,
2796        clear_sequence: bool,
2797    ) {
2798        // A dynamic route may have switched the overlay before the terminal
2799        // path. Restore the backbone while the detached router is still
2800        // available, because set_active_skill also owns the overlay reset.
2801        if router.is_some() {
2802            let _ = self.set_active_skill(None);
2803        }
2804        // The last speculative round's replay may still be in flight on
2805        // the second queue: whoever reads the host cache after generate()
2806        // returns (session export, the network split's KV wire, a KV
2807        // reuse) must see the final states.
2808        // A replay that failed leaves the GDN owners half-written: fail
2809        // closed and drop the sequence instead of handing the cache on.
2810        #[cfg(target_os = "macos")]
2811        let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
2812        if clear_sequence {
2813            self.clear_sequence_state();
2814            if let Some(m) = mtp.as_mut() {
2815                // The MTP owner is detached while generation runs, so the
2816                // trunk reset above cannot clear its host cache.  Drop its
2817                // partial rows before reattaching it to the pooled pipeline;
2818                // the next request must start from the same empty anchor on
2819                // CPU and on the device mirror.
2820                m.kv.clear();
2821            }
2822            if let Some(m) = self.mtp.as_mut() {
2823                // A non-speculative request leaves the configured MTP owner
2824                // attached.  Clear that dormant cache too when a shared
2825                // generation failure/cancellation resets the sequence.
2826                m.kv.clear();
2827            }
2828        }
2829        self.graph_want_logits = false;
2830        self.graph_head_required = false;
2831        self.graph_logits = None;
2832        self.graph_failed
2833            .store(false, std::sync::atomic::Ordering::Relaxed);
2834        self.cancel
2835            .store(false, std::sync::atomic::Ordering::Relaxed);
2836        self.dyn_router = router.take().or(self.dyn_router.take());
2837        self.mtp = mtp.take().or(self.mtp.take());
2838        self.mtp_graph_mode = None;
2839        self.spec_forced = None;
2840    }
2841
2842    /// Consume a graph failure reported by a forward that returns only a
2843    /// hidden vector.  `forward_ids` is a public Result API, so it must not
2844    /// turn the graph's zero hidden sentinel into a valid lm_head result.
2845    fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2846        if self
2847            .graph_failed
2848            .swap(false, std::sync::atomic::Ordering::Relaxed)
2849        {
2850            self.cancel
2851                .store(false, std::sync::atomic::Ordering::Relaxed);
2852            self.clear_sequence_state();
2853            self.graph_logits = None;
2854            self.graph_want_logits = false;
2855            self.graph_head_required = false;
2856            return Err(format!("GPU graph failed during {phase} at position {pos}"));
2857        }
2858        Ok(())
2859    }
2860
2861    #[cfg(target_os = "macos")]
2862    fn fail_metal_graph(&mut self, reason: &str) {
2863        crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2864        self.clear_sequence_state();
2865        self.graph_logits = None;
2866        self.graph_failed
2867            .store(true, std::sync::atomic::Ordering::Relaxed);
2868        self.cancel
2869            .store(true, std::sync::atomic::Ordering::Relaxed);
2870        tracing::error!("native Metal TokenGraph failed closed: {reason}");
2871    }
2872
2873    /// Start an NLL/PPL request with all graph side channels in a known
2874    /// state.  A graph failure also raises the cooperative cancel bit; it is
2875    /// consumed here and that graph-induced bit is cleared so an independent
2876    /// request can be reused.  A caller-owned cancellation remains intact.
2877    fn nll_begin(&mut self) -> Result<(), String> {
2878        if self
2879            .graph_failed
2880            .swap(false, std::sync::atomic::Ordering::Relaxed)
2881        {
2882            self.cancel
2883                .store(false, std::sync::atomic::Ordering::Relaxed);
2884            self.clear_sequence_state();
2885            self.graph_logits = None;
2886            self.graph_want_logits = false;
2887            self.graph_head_required = false;
2888            return Err("GPU graph failed before NLL scoring".to_string());
2889        }
2890        self.clear_sequence_state();
2891        self.graph_logits = None;
2892        self.graph_want_logits = false;
2893        self.graph_head_required = false;
2894        Ok(())
2895    }
2896
2897    /// End an NLL/PPL request, including the side channels that are not part
2898    /// of the host KV cache.  This is intentionally explicit instead of
2899    /// relying on a tuple/sentinel return: callers must see every failure.
2900    fn nll_end(&mut self) {
2901        self.clear_sequence_state();
2902        self.graph_logits = None;
2903        self.graph_want_logits = false;
2904        self.graph_head_required = false;
2905        self.graph_failed
2906            .store(false, std::sync::atomic::Ordering::Relaxed);
2907    }
2908
2909    /// Check the graph failure channel at a scoring boundary and leave the
2910    /// pipeline reusable when the device path failed.
2911    fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2912        #[cfg(test)]
2913        if self.nll_test_fail_at == Some(pos) {
2914            self.nll_test_fail_at = None;
2915            self.graph_failed
2916                .store(true, std::sync::atomic::Ordering::Relaxed);
2917            self.cancel
2918                .store(true, std::sync::atomic::Ordering::Relaxed);
2919        }
2920        if self
2921            .graph_failed
2922            .swap(false, std::sync::atomic::Ordering::Relaxed)
2923        {
2924            self.cancel
2925                .store(false, std::sync::atomic::Ordering::Relaxed);
2926            self.clear_sequence_state();
2927            self.graph_logits = None;
2928            self.graph_want_logits = false;
2929            return Err(format!(
2930                "GPU graph failed during NLL {phase} at position {pos}"
2931            ));
2932        }
2933        Ok(())
2934    }
2935
2936    /// Map a virtual layer index to its physical weight index.
2937    /// Looped Transformer (Nanbeige 4.2): 22 physical layers × 2 loops = 44 virtual;
2938    /// virtual layer 23 maps back to physical layer 1 (23 % 22 = 1).
2939    #[inline]
2940    pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2941        match &self.layer_map {
2942            Some(m) => m[virtual_idx],
2943            None => virtual_idx % self.physical_layers,
2944        }
2945    }
2946
2947    /// Mage-VL (`llava_onevision2`) and MiniCPM-OCR prompt ingest through the batched wgpu
2948    /// graph (`CMF_GRAPH_PREFILL=0` keeps the chunked host prefill).
2949    fn onevision2_prefill(&self) -> bool {
2950        self.model.as_ref().is_some_and(|m| matches!(m.header.arch.arch_name.as_str(), "llava_onevision2" | "minicpm_ocr"))
2951            && std::env::var("CMF_GRAPH_PREFILL").as_deref() != Ok("0")
2952    }
2953
2954    /// SenseNova-Loop: whether virtual layer `li` runs inside the mHC
2955    /// streams (the device graphs decline such layers).
2956    #[inline]
2957    pub fn mhc_layer(&self, li: usize) -> bool {
2958        self.mhc.as_ref().is_some_and(|m| m.on(self.phys_layer(li)))
2959    }
2960
2961    /// mHC before a sub-layer of the decode token: route its streams into
2962    /// `h` (entering the range copies `h` into every stream first).
2963    fn mhc_pre(&mut self, li: usize, ffn: bool, h: &mut [f32]) -> crate::sensenova::MhcCoef {
2964        let p = self.phys_layer(li);
2965        let m = self.mhc.as_deref().expect("mhc layer");
2966        let n = m.s.streams * m.dim;
2967        if self.mhc_r.len() != n {
2968            self.mhc_r.resize(n, 0.0);
2969        }
2970        if !ffn && m.enters(p) {
2971            m.enter(&mut self.mhc_r, h);
2972        }
2973        m.route(p, ffn, &self.mhc_r, h)
2974    }
2975
2976    /// mHC after a sub-layer: fold its output `y` into the streams; after
2977    /// the range's last FFN, `h` becomes the streams' mean.
2978    fn mhc_post(&mut self, li: usize, ffn: bool, y: &[f32], c: &crate::sensenova::MhcCoef, h: &mut [f32]) {
2979        let p = self.phys_layer(li);
2980        let m = self.mhc.as_deref().expect("mhc layer");
2981        m.merge(&mut self.mhc_r, y, c);
2982        if ffn && m.leaves(p) {
2983            m.leave(&self.mhc_r, h);
2984        }
2985    }
2986
2987    /// True when `virtual_idx` is the last layer of a loop iteration
2988    /// (used for loop_final_norm insertion).
2989    #[inline]
2990    pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2991        self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2992    }
2993
2994    /// Build a pipeline from parts (used by the loader and tests).
2995    #[allow(clippy::too_many_arguments)]
2996
2997    /// Whole-block q1 token graph on the GPU (macOS/Metal): the run of
2998    /// consecutive q1 layers — GDN *and* full attention — starting at
2999    /// `start` executes as few command buffers as the CPU truly needs.
3000    /// Hidden stays device-resident across every layer; the only syncs
3001    /// are before each CPU attend (it needs q/k/v and owns the KV
3002    /// cache) and the final hidden readback. Recurrent states
3003    /// round-trip through shared memory (the CPU stays their owner, so
3004    /// every other path remains coherent). Returns the first layer
3005    /// index NOT covered (== `start` → refused, caller falls through
3006    /// to the per-layer CPU path).
3007    /// Should prefill run position-by-position through the GPU token
3008    /// graph instead of the batched CPU chunk-GEMM? True for q1 GDN
3009    /// hybrids on native Metal: their chunk prefill is walled by the
3010    /// sequential scalar recurrence, so the graph's decode rate wins.
3011    /// NOT for Looped Transformers, despite the per-chunk loop_final_norm
3012    /// sync: the chunk-GEMM amortizes each weight over the whole chunk,
3013    /// which the per-position graph cannot (Nanbeige 4.2 on M4, 512-token
3014    /// prompt: 85 tok/s chunked vs 14 through the graph).
3015    #[cfg(target_os = "macos")]
3016    fn graph_prefill_preferred(&self) -> bool {
3017        // The resident Embryo graph owns the phase state and the anchor KV on
3018        // the device: the prompt must seed them there (wgpu over Metal).
3019        if crate::gpu::enabled_here()
3020            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
3021            && matches!(std::env::var("CMF_EMBRYO_RESIDENT").as_deref(), Ok("1") | Ok("parallel"))
3022            && self.embryo_resident_eligible()
3023        {
3024            return true;
3025        }
3026        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
3027        if !crate::gpu::enabled_here()
3028            || !graph_force
3029            || std::env::var("CMF_GPU_BLOCK")
3030                .map(|v| v == "0")
3031                .unwrap_or(false)
3032            // CMF_PREFILL_GRAPH=0: the chunked prefill (GEMM projections,
3033            // CPU recurrence) instead of the per-position token graph.
3034            || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
3035        {
3036            return false;
3037        }
3038        self.weights
3039            .layers
3040            .iter()
3041            .any(|lw| {
3042                matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
3043            })
3044    }
3045
3046    /// Prompt ingest through the batched wgpu graph in device-prefix mode:
3047    /// a MoE stack that does not fit the card runs each chunk's leading
3048    /// layers on the device (experts resident) and the rest on the host's
3049    /// batched walk. On by default for models with per-layer attention
3050    /// geometry (MiMo-V2 — its measured default); `CMF_BATCH_PREFIX=1`
3051    /// opts any other MoE model in, `=0` keeps the chunked host prefill.
3052    #[cfg(not(target_os = "macos"))]
3053    fn batch_prefix_prefill(&self) -> bool {
3054        let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
3055            Ok("0") => return false,
3056            Ok("1") => true,
3057            _ => false,
3058        };
3059        crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
3060            && crate::gpu::enabled_here()
3061            && !self.graph_refused()
3062            && (forced || self.graph_attn_decline_reason().is_some())
3063            && self.wgpu_graph_attn_decline().is_none()
3064            && self.attn_softcap == 0.0
3065            && self
3066                .weights
3067                .layers
3068                .iter()
3069                .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
3070            && self.automatic_gpu_prefix().is_some()
3071    }
3072
3073    /// Gemma-4's prompt through the batched wgpu graph (`gemma_batch`): a
3074    /// dense stack wholly on a discrete card, whose token graph already
3075    /// decodes it. Not `graph_prefill_preferred` — the host stays the KV
3076    /// owner for reuse and scoring; only generation's ingest changes.
3077    /// `CMF_G4_BATCH=0` keeps the chunked host prefill.
3078    fn g4_batch_prefill(&self) -> bool {
3079        #[cfg(target_os = "macos")]
3080        {
3081            false
3082        }
3083        #[cfg(not(target_os = "macos"))]
3084        {
3085            // An EMPTY layer stack (`all()` is vacuously true) plus a
3086            // gemma4 geometry flag is exactly the gemma-4 E-series
3087            // per-token stack — it has no batch graph rows to ingest.
3088            !self.weights.layers.is_empty()
3089                && !self.per_token_stack()
3090                // Gemma-4, and Ripple: a dense stack with per-layer geometry
3091                // whose host walk attends 80 heads a row on the CPU.
3092                && (self.global_attn.is_some() || self.attn_v_norm || self.value_blend.is_some())
3093                && crate::gpu::g4_batch_on()
3094                && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
3095                && crate::gpu::enabled_here()
3096                && crate::gpu::discrete()
3097                && !self.graph_refused()
3098                && !self.o1_active()
3099                && self.attn_softcap == 0.0
3100                && self.wgpu_graph_attn_decline().is_none()
3101                && self.weights.layers.iter().all(|lw| {
3102                    matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
3103                })
3104                && self.automatic_gpu_prefix().is_none()
3105        }
3106    }
3107
3108    /// Gemma-4's batched prefill of `ids` at positions `pos0..`
3109    /// (`g4_batch_prefill`): chunks of `generation_batch_k` rows through the
3110    /// batch graph. Ok((n, rows)): `n` positions ingested; `rows` = the last
3111    /// one's hidden, or every row's (`keep_all`). A refusal stops early
3112    /// (n < ids.len()) after pulling the device rows the host walk needs.
3113    /// Err: a failure after admission — the device state is undefined.
3114    #[cfg(not(target_os = "macos"))]
3115    fn g4_batch_ingest(
3116        &mut self,
3117        ids: &[u32],
3118        pos0: usize,
3119        keep_all: bool,
3120    ) -> Result<(usize, Vec<f32>), String> {
3121        let hs = self.hidden_size;
3122        let chunk = self.generation_batch_k().max(1);
3123        let end_all = pos0 + ids.len();
3124        let mut pos = pos0;
3125        let mut out = Vec::new();
3126        while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3127            let end = (pos + chunk).min(end_all);
3128            let bk = end - pos;
3129            let mut hiddens = vec![0f32; bk * hs];
3130            for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
3131                hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
3132            }
3133            let positions: Vec<usize> = (pos..end).collect();
3134            let t_chunk = std::time::Instant::now();
3135            let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
3136            if std::env::var("CMF_GRAPH_PROF").is_ok() {
3137                let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
3138                eprintln!(
3139                    "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
3140                    bk as f64 / (ms / 1000.0)
3141                );
3142            }
3143            match outcome {
3144                crate::gpu::BatchGraphOutcome::Completed => {
3145                    if keep_all {
3146                        out.extend_from_slice(&hiddens);
3147                    } else {
3148                        out = hiddens[(bk - 1) * hs..].to_vec();
3149                    }
3150                    pos = end;
3151                }
3152                crate::gpu::BatchGraphOutcome::Failed => {
3153                    return Err("Gemma-4 batched prefill failed after admission".into());
3154                }
3155                crate::gpu::BatchGraphOutcome::Declined => {
3156                    #[cfg(feature = "gpu")]
3157                    if pos > pos0 {
3158                        self.pull_lagging_host_kv(0, self.num_layers, pos);
3159                    }
3160                    break;
3161                }
3162            }
3163        }
3164        Ok((pos - pos0, out))
3165    }
3166
3167    /// Hidden rows of every position of `ids` through Gemma-4's batched
3168    /// prefill from a fresh sequence (the prefill's teacher-forced logits
3169    /// for parity gates: `logits_from_hidden` per row). None when the model
3170    /// or the card does not take that route, or it refused part way.
3171    pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
3172        #[cfg(not(target_os = "macos"))]
3173        {
3174            if ids.is_empty() || !self.g4_batch_prefill() {
3175                return None;
3176            }
3177            self.clear_sequence_state();
3178            match self.g4_batch_ingest(ids, 0, true) {
3179                Ok((n, rows)) if n == ids.len() => Some(rows),
3180                _ => {
3181                    self.clear_sequence_state();
3182                    None
3183                }
3184            }
3185        }
3186        #[cfg(target_os = "macos")]
3187        {
3188            let _ = ids;
3189            None
3190        }
3191    }
3192
3193    #[cfg(not(target_os = "macos"))]
3194    fn graph_prefill_preferred(&self) -> bool {
3195        // Discrete-GPU wgpu whole-token graph: GDN layers carry recurrent state
3196        // (conv ring + delta-rule S) resident on the GPU. A batched CPU prefill
3197        // builds that state on the CPU only, leaving the GPU buffers zeroed at
3198        // decode → garbage. Route GDN-hybrid prefill through the graph one
3199        // position at a time so the resident state is seeded exactly as decode
3200        // will read it. Fully resident pure-attention models take the batched
3201        // graph as well (see the end of this function).
3202        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
3203        if !graph_on || !crate::gpu::enabled_here() {
3204            return false;
3205        }
3206        // Embryo's phase state is device-owned by the resident graph during
3207        // prefill; the batched CPU path would leave decode seeing a zeroed
3208        // device recurrence.  Route the prompt position-by-position too.
3209        if self.embryo_resident_eligible() {
3210            // The resident graph owns the phase state and anchor KV on the
3211            // device.  A prefill-only graph would leave decode on the host
3212            // with no way to import that state, so keep the whole sequence
3213            // on one owner (or use the ordinary CPU prefill/decode pair).
3214            return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
3215        }
3216        // The descriptor-aware Prism graph now carries both the FWHT/affine
3217        // transforms and resident GDN state, so it is also the exact prefill
3218        // path for this model.  Keeping it here (rather than falling through
3219        // to the CPU chunk walk) is required for a long prompt to seed the
3220        // same device state that decode consumes.
3221        // O(1) needs the CPU prefill: the q-trace that seals the Nyström
3222        // skeleton is recorded there and nowhere else. The GDN half of
3223        // the hybrid loses nothing — the graph's first decode creates
3224        // its (ring, S) entries seeded from `cpu_state`, the same
3225        // handoff every graph run relies on when the entry is fresh.
3226        // Without this line the two designs collide on hybrids and o1
3227        // never becomes graph-portable: prefill through the graph
3228        // records no trace, so views stay None forever.
3229        if self.o1_active() {
3230            return false;
3231        }
3232        // A model the wgpu graphs decline outright would walk its prompt
3233        // one position at a time through a graph that never runs: the
3234        // batched CPU chunk prefill is the right ingest for it.
3235        if self.wgpu_graph_attn_decline().is_some() {
3236            return false;
3237        }
3238        if self
3239            .weights
3240            .layers
3241            .iter()
3242            .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
3243            // Mage-VL: its screenshot prompts (~2k rows) go through the
3244            // batched graph, 4× the chunked host ingest on an A40 (2048
3245            // tokens: 806 tok/s at 128 rows, 893 at 256, against 196).
3246            || self.onevision2_prefill()
3247        {
3248            return true;
3249        }
3250        // MoE models too: the chunked CPU prefill runs every expert on the
3251        // host (Hy-MT2-30B-A3B on a Xeon: 8 tok/s of ingest against 53 of
3252        // graph decode), while the token graph — and the batched graph under
3253        // CMF_BATCH_K — keep the experts resident. Full attention in the
3254        // graph writes the KV mirror that decode reads, exactly as it does
3255        // for the hybrids' attention layers. Only when the whole stack is
3256        // resident: with a device prefix the per-position walk finishes
3257        // every token on the host, and the chunked prefill (GEMMs on the
3258        // card, the expert loop batched on the host) is the faster ingest
3259        // (the 8 GB ladder point: 7 tok/s chunked against ~1 walked).
3260        // Dense stacks too: the chunked walk paid a submit, a wait and a
3261        // readback per projection and per attend, so a resident 2B read its
3262        // prompt at the host's pace (CherryPit-1.2-2B on an RTX 5090, 2048
3263        // tokens: 10.6 s chunked, 1.2 s through the batched graph, the same
3264        // greedy text). `CMF_DENSE_GRAPH_PREFILL=0` keeps the chunked walk.
3265        // A dense stack the batched graph declined once (Spark-X2.6) keeps the
3266        // chunked walk: the per-position graph read its prompt at decode pace,
3267        // 65 tok/s against 350 on an RTX 2000 Ada.
3268        (self
3269            .weights
3270            .layers
3271            .iter()
3272            .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
3273            || (std::env::var("CMF_DENSE_GRAPH_PREFILL").as_deref() != Ok("0")
3274                && !self.dense_batch_declined.load(std::sync::atomic::Ordering::Relaxed)
3275                // Sliding-window stacks take the graph where its whole-run
3276                // attend is tiled: Spark-X2.6 q4tp on an RTX 2000 Ada,
3277                // 16 000 tokens, 182 tok/s chunked, 135 through the per-token
3278                // batched arm, 390+ tiled. `CMF_DENSE_GRAPH_PREFILL=1` sends
3279                // them through the graph regardless.
3280                && ((self.sliding_layers.is_none() && self.swa.is_none())
3281                    || crate::gpu::batched_attend_tiled()
3282                    || std::env::var("CMF_DENSE_GRAPH_PREFILL").as_deref() == Ok("1"))))
3283            && self.automatic_gpu_prefix().is_none()
3284    }
3285
3286    #[cfg(target_os = "macos")]
3287    fn q1_graph_gpu(
3288        &mut self,
3289        start: usize,
3290        upto: Option<usize>,
3291        position: usize,
3292        h: &mut [f32],
3293    ) -> usize {
3294        let _mt0 = std::time::Instant::now(); // CMF_METAL_HOSTPROF
3295        use crate::gpu::{
3296            AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, SconvGpuLayer, TokenGraph,
3297        };
3298        const MOE_WORKING_SET_REASON: &str =
3299            "model weights exceed Metal working set for native MoE graph";
3300        // A prior admission check rejected this pipeline. Avoid rebuilding the
3301        // entire 40-layer graph plan at every individual host layer after the
3302        // fallback begins; the per-operation path below stays eligible.
3303        if self
3304            .graph_declines
3305            .borrow()
3306            .iter()
3307            .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
3308        {
3309            return start;
3310        }
3311        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
3312        if self.attn_softcap > 0.0 // capped scores: no graph kernel — CPU path
3313            // V with rows of its own (Ripple's value residual): no Metal kernel blends them.
3314            || self.value_blend.is_some()
3315            || !self.metal_rope_scale_ok()
3316            || !crate::gpu::enabled_here()
3317            || !graph_force
3318            || std::env::var("CMF_GPU_BLOCK")
3319                .map(|v| v == "0")
3320                .unwrap_or(false)
3321        {
3322            if std::env::var("CMF_GRAPH_DBG").is_ok() {
3323                eprintln!(
3324                    "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
3325                    self.attn_softcap > 0.0,
3326                    crate::gpu::enabled_here(),
3327                    graph_force,
3328                );
3329            }
3330            if self.graph_head_required {
3331                self.fail_metal_graph("native graph front gate refused");
3332            }
3333            return start;
3334        }
3335        // The graph encodes SiLU or exact-GELU FFNs and attention with an
3336        // explicit model scale. Sliding-window layers with their own RoPE
3337        // table / rotary width ride it too (`metal_graph_swa`): both the
3338        // device attend and the sandwich's CPU attend read each layer's
3339        // window and table. Sandwich norms and other activations fall back
3340        // to the CPU path.
3341        // Gemma-4 (`metal_gemma4`): its sliding layers ride the same door,
3342        // its global layers, V norm, sandwich norms, layer scalar and
3343        // tanh-GELU the `gpu_metal::gemma4` kernels.
3344        let gemma4 = self.metal_gemma4();
3345        let swa_graph = self.metal_graph_swa() || gemma4;
3346        // The normal native GQA kernel carries a learned, value-less
3347        // softmax column.  It also takes the exact per-layer sliding
3348        // window, but its split-K twin has no sink column.  Thus a
3349        // sink-bearing model may use this door only when the ordinary
3350        // sliding geometry has passed `metal_graph_swa` (or when it has no
3351        // sliding layers at all).
3352        let graph_attn_reason = self.graph_attn_decline_reason();
3353        let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
3354            && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
3355        // Plain-weight (x̂·w) sandwich norms (LFM3) take the generic norm
3356        // kernels, as on the wgpu graph; Gemma-2/3's (1+w) norms and the
3357        // layer scalar stay Gemma-4's.
3358        let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
3359        if (self.swa.is_some() && !swa_graph)
3360            || (self.global_attn.is_some() && !gemma4)
3361            || self.attention_heads_per_layer.is_some()
3362            || (self.attn_v_norm && !gemma4)
3363            // Per-layer KV heads / narrow V stay outside this graph.  A
3364            // learned sink is admitted only through native_sink_attend.
3365            || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
3366            || self.weights.layers.iter().any(|lw| {
3367                ((lw.layer_scale.is_some()
3368                    || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some())))
3369                    && !gemma4)
3370                    || matches!(&lw.ffn, FfnKind::Dense(d)
3371                        if !(matches!(d.act, Act::Silu | Act::Gelu)
3372                            || (gemma4 && d.act == Act::GeluTanh)))
3373            })
3374        {
3375            // The Metal graphs' device attend has no per-layer attention
3376            // geometry (the wgpu graphs do): say so once, by name.
3377            if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
3378                self.note_graph_decline("metal block graph", reason);
3379            }
3380            if std::env::var("CMF_GRAPH_DBG").is_ok() {
3381                eprintln!(
3382                    "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
3383                    self.swa.is_some(),
3384                    self.global_attn.is_some(),
3385                    self.attention_heads_per_layer.is_some(),
3386                    self.attn_v_norm,
3387                    (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
3388                );
3389            }
3390            if self.graph_head_required {
3391                self.fail_metal_graph("native graph architecture gate refused");
3392            }
3393            return start;
3394        }
3395        // Looped Transformer: the graph covers ALL loop iterations;
3396        // encode_loop_norm is inserted on-device at each boundary.
3397        let limit = upto
3398            .map(|u| u + 1)
3399            .unwrap_or(self.num_layers)
3400            .min(self.num_layers);
3401
3402        enum Item<'a> {
3403            Gdn {
3404                run: Vec<GdnGpuLayer<'a>>,
3405                first: usize,
3406            },
3407            /// A run of LFM2/LFM3 short-conv layers (conv ring on device).
3408            Sconv {
3409                run: Vec<SconvGpuLayer<'a>>,
3410                first: usize,
3411            },
3412            Attn {
3413                l: AttnGpuLayer<'a>,
3414                li: usize,
3415                q_norm: Option<&'a [f32]>,
3416                k_norm: Option<&'a [f32]>,
3417                output_gate: bool,
3418                /// `self_attn.g_proj` output gate (per-head flag): the
3419                /// sandwich projects the device-normed input on the host
3420                /// and gates the attend's output before O.
3421                proj_gate: Option<(&'a QTensor, bool)>,
3422                /// The same gate's f32 rows `[nh × hidden]` when the device
3423                /// attend can apply it (per-head sigmoid, f32 in RAM).
3424                head_gate_w: Option<&'a [f32]>,
3425                bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
3426                /// Attend on the device too (no sync): F32 KV, no
3427                /// o1/bias, dims inside the kernels' contract.
3428                full_gpu: bool,
3429            },
3430        }
3431
3432        // Device-attend KERNEL contract, shared by every Full layer. The
3433        // hd>128 default-off POLICY is applied after the scan: it was
3434        // measured on dense models, and a MoE plan inverts it — with the
3435        // experts on device each CPU-attend sandwich costs a
3436        // commit+wait, ~30 submits/token (W2 on M4: 14.7 tok/s
3437        // sandwiched vs 27.1 device-attend vs 18.8 pure CPU).
3438        let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
3439        let attend_contract = attend_mode != "0"
3440            && attend_mode != "off"
3441            && self.head_dim % 4 == 0
3442            && self.head_dim <= 256
3443            // Flint has explicit no-RoPE layers.  The device RoPE kernel
3444            // accepts rd=0 (zero rotation lanes) and binds a sentinel table;
3445            // retain the historical lane-pair requirement for nonzero rd.
3446            && (self.rotary_dim == 0
3447                || (self.rotary_dim >= 2
3448                    && self.rotary_dim <= self.head_dim
3449                    && (self.rotary_dim / 2) % 32 == 0))
3450            && self.num_kv_heads > 0
3451            && self.num_heads % self.num_kv_heads == 0;
3452
3453        let mut plan: Vec<Item> = Vec::new();
3454        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
3455        // Break-reason diagnostics ride the same env as the plan summary.
3456        let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
3457        let mut scan = start;
3458        while scan < limit {
3459            // SenseNova-Loop: MRoPE prompt rows rotate on the host; an mHC
3460            // layer rides the graph when its kernels compiled (two streams,
3461            // dense FFN, device attend — checked below).
3462            if self.prompt_mrope.is_some()
3463                || (self.mhc_layer(scan)
3464                    && (!crate::gpu_metal::mhc::available()
3465                        || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
3466                        || self.mhc.as_ref().is_some_and(|m| m.s.streams != 2 || m.s.fixed_identity)
3467                        || !matches!(self.weights.layers[self.phys_layer(scan)].ffn, FfnKind::Dense(_))))
3468            {
3469                break;
3470            }
3471            let lw = &self.weights.layers[self.phys_layer(scan)];
3472            let ffn = match &lw.ffn {
3473                FfnKind::Dense(d) if d.segs.is_empty() => {
3474                    let (Some(g), Some(u), Some(dn)) = (
3475                        d.gate_proj.metal_graph_parts(),
3476                        d.up_proj.metal_graph_parts(),
3477                        d.down_proj.metal_graph_parts(),
3478                    ) else {
3479                        if block_diag {
3480                            eprintln!(
3481                                "block-graph: L{scan} FFN trio not graph-mappable — run ends"
3482                            );
3483                        }
3484                        break;
3485                    };
3486                    MetalFfn::Dense {
3487                        gate: g,
3488                        up: u,
3489                        down: dn,
3490                        // The front gate admitted SiLU, exact GELU and
3491                        // (Gemma-4) tanh-GELU only.
3492                        act: d.act.graph_act().map_or(0, |a| a.code() as u8),
3493                    }
3494                }
3495                FfnKind::Moe(m) => {
3496                    // Flint's detached Q4 shared expert is intentionally
3497                    // tried before the generic uniform-bank graph builder.
3498                    // The latter correctly refuses its ragged Q2/Q4 widths;
3499                    // this narrow descriptor keeps the router and selected
3500                    // routed experts device-resident inside a bounded page.
3501                    let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
3502                    {
3503                        MetalFfn::FlintMoe(flint)
3504                    } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
3505                        MetalFfn::Moe(moe)
3506                    } else {
3507                        if block_diag {
3508                            eprintln!(
3509                                "block-graph: L{scan} MoE outside the graph contract — run ends"
3510                            );
3511                        }
3512                        break;
3513                    };
3514                    if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
3515                        model_ref.get_or_insert_with(|| model.clone());
3516                    }
3517                    ffn
3518                }
3519                _ => {
3520                    if block_diag {
3521                        eprintln!("block-graph: L{scan} non-graph FFN — run ends");
3522                    }
3523                    break;
3524                }
3525            };
3526            match &lw.attn {
3527                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
3528                    let parts = (
3529                        w.in_proj_qkv.metal_graph_parts(),
3530                        w.in_proj_z.metal_graph_parts(),
3531                        w.in_proj_a.f32_parts(),
3532                        w.in_proj_b.f32_parts(),
3533                        w.out_proj.metal_graph_parts(),
3534                    );
3535                    let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
3536                        if block_diag {
3537                            eprintln!(
3538                                "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
3539                                w.in_proj_qkv.metal_graph_parts().is_some(),
3540                                w.in_proj_z.metal_graph_parts().is_some(),
3541                                w.in_proj_a.f32_parts().is_some(),
3542                                w.in_proj_b.f32_parts().is_some(),
3543                                w.out_proj.metal_graph_parts().is_some(),
3544                            );
3545                        }
3546                        break;
3547                    };
3548                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
3549                        model_ref.get_or_insert_with(|| model.clone());
3550                    }
3551                    let gl = GdnGpuLayer {
3552                        mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(scan)),
3553                        attn_norm: &lw.input_norm,
3554                        post_norm: &lw.post_norm,
3555                        qkv,
3556                        z,
3557                        a,
3558                        b,
3559                        out,
3560                        ffn,
3561                        conv1d: &w.conv1d,
3562                        a_log: &w.a_log,
3563                        dt_bias: &w.dt_bias,
3564                        gnorm: &w.norm,
3565                    };
3566                    // a run ends at a layer a DFlash drafter reads: its output is kept there
3567                    let cut = scan > 0 && self.dflash.as_ref().is_some_and(|d| d.g.taps.contains(&(scan - 1)));
3568                    match plan.last_mut() {
3569                        Some(Item::Gdn { run, .. }) if !cut => run.push(gl),
3570                        _ => plan.push(Item::Gdn {
3571                            run: vec![gl],
3572                            first: scan,
3573                        }),
3574                    }
3575                }
3576                AttnKind::ShortConv(w) if self.short_conv_cfg.is_some() => {
3577                    let (Some(inp), Some(out)) =
3578                        (w.in_proj.metal_graph_parts(), w.out_proj.metal_graph_parts())
3579                    else {
3580                        if block_diag {
3581                            eprintln!("block-graph: L{scan} short-conv parts refused");
3582                        }
3583                        break;
3584                    };
3585                    if let QTensor::Mapped { model, .. } = &w.in_proj {
3586                        model_ref.get_or_insert_with(|| model.clone());
3587                    }
3588                    let sl = SconvGpuLayer {
3589                        attn_norm: &lw.input_norm,
3590                        post_norm: &lw.post_norm,
3591                        in_proj: inp,
3592                        taps: &w.conv,
3593                        out,
3594                        attn_out_norm: lw.attn_out_norm.as_deref(),
3595                        ffn_out_norm: lw.ffn_out_norm.as_deref(),
3596                        ffn,
3597                    };
3598                    match plan.last_mut() {
3599                        Some(Item::Sconv { run, .. }) => run.push(sl),
3600                        _ => plan.push(Item::Sconv {
3601                            run: vec![sl],
3602                            first: scan,
3603                        }),
3604                    }
3605                }
3606                AttnKind::Full {
3607                    wq,
3608                    wk,
3609                    wv,
3610                    wo,
3611                    q_norm,
3612                    k_norm,
3613                    output_gate,
3614                    softplus_gate,
3615                    bias,
3616                } if (!self.kv_cache.layers[scan].o1_sealed()
3617                    // Sealed o1 stays plannable when the Metal o1 port
3618                    // is on: full_gpu attends through the device state,
3619                    // and any refusal falls to the sandwich, whose CPU
3620                    // core routes sealed layers through the nystrom step.
3621                    || std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
3622                    || self.o1_on_graph())
3623                    // A projected output gate: Spark-X2.5's per-head
3624                    // sigmoid form only (Laguna's softplus gate is
3625                    // unmeasured on these paths and keeps the CPU walk).
3626                    && softplus_gate
3627                        .as_ref()
3628                        .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
3629                {
3630                    let parts = (
3631                        wq.metal_graph_parts(),
3632                        wk.metal_graph_parts(),
3633                        wv.metal_graph_parts(),
3634                        wo.metal_graph_parts(),
3635                    );
3636                    let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
3637                        break;
3638                    };
3639                    if let QTensor::Mapped { model, .. } = wq {
3640                        model_ref.get_or_insert_with(|| model.clone());
3641                    }
3642                    let cache = &self.kv_cache.layers[scan];
3643                    // O(1) layer on Metal: the device attends through the
3644                    // sealed Nystrom state (opt-in while the port proves
3645                    // itself). Unsealed -> sandwich path = the CPU o1 step.
3646                    let o1_metal = cache.o1.is_some()
3647                        && (std::env::var("CMF_O1_METAL").as_deref() == Ok("1") || self.o1_on_graph())
3648                        && cache.o1_views().is_some();
3649                    // The device attend takes the layer's window and RoPE
3650                    // table, and the per-head gate from f32 rows; a gate
3651                    // held otherwise sandwiches.
3652                    let head_gate_w = softplus_gate
3653                        .as_ref()
3654                        .and_then(|(g, _)| g.f32_parts())
3655                        .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
3656                        .map(|(d, _, _)| d);
3657                    // Gemma-4's global layers are 512 wide over one KV head
3658                    // (the `gemma4` 16-slot kernels, `CMF_METAL_GEMMA4_W16`).
3659                    let (nkv_g, hd_g) = if gemma4 {
3660                        let (a, b, _) = self.layer_geom(scan);
3661                        (a, b)
3662                    } else {
3663                        (self.num_kv_heads, self.head_dim)
3664                    };
3665                    let full_gpu = attend_contract
3666                        && softplus_gate.is_none() == head_gate_w.is_none()
3667                        && cache.mode == crate::kv_cache::KvMode::F32
3668                        && (cache.o1.is_none() || o1_metal)
3669                        && bias.is_none()
3670                        && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
3671                        && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
3672                        && pk.1 == nkv_g * hd_g
3673                        && pv.1 == nkv_g * hd_g
3674                        && po.2 == self.num_heads * hd_g;
3675                    // An mHC layer needs the device attend (the CPU-attend
3676                    // sandwich has no stream hand-off), and so does a DASH
3677                    // layer: its router state lives on the device.
3678                    if !full_gpu && (self.mhc_layer(scan) || self.dash.is_some()) {
3679                        break;
3680                    }
3681                    plan.push(Item::Attn {
3682                        l: AttnGpuLayer {
3683                            mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(scan)),
3684                            attn_norm: &lw.input_norm,
3685                            post_norm: &lw.post_norm,
3686                            wq: pq,
3687                            wk: pk,
3688                            wv: pv,
3689                            wo: po,
3690                            ffn,
3691                            attn_out_norm: lw.attn_out_norm.as_deref(),
3692                            ffn_out_norm: lw.ffn_out_norm.as_deref(),
3693                            layer_scale: lw.layer_scale,
3694                        },
3695                        li: scan,
3696                        q_norm: q_norm.as_deref(),
3697                        k_norm: k_norm.as_deref(),
3698                        output_gate: *output_gate,
3699                        proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
3700                        head_gate_w,
3701                        bias: bias
3702                            .as_ref()
3703                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3704                        full_gpu,
3705                    });
3706                }
3707                _ => break,
3708            }
3709            scan += 1;
3710        }
3711        // SenseNova-Loop: the mHC streams live on the device only inside one
3712        // graph, so a graph may neither enter a range midway nor stop
3713        // inside one (the host walks such a stretch with its own streams).
3714        if let Some(m) = self.mhc.as_deref() {
3715            let inside = |li: usize| m.on(self.phys_layer(li));
3716            let starts_mid = inside(start) && !m.enters(self.phys_layer(start));
3717            let stops_mid = scan > start && inside(scan - 1) && !m.leaves(self.phys_layer(scan - 1));
3718            if starts_mid || stops_mid {
3719                if std::env::var("CMF_GRAPH_DBG").is_ok() {
3720                    eprintln!("q1-graph: [{start}, {scan}) would split an mHC range — host path");
3721                }
3722                return start;
3723            }
3724        }
3725        // Unlimited-OCR: all of it or none of it, the host's router state and
3726        // the device's are not interchangeable (`CMF_GPU=wgpu` keeps the host's).
3727        if self.dash.is_some()
3728            && (start != 0 || scan != self.num_layers || crate::gpu::backend_name() != "metal")
3729        {
3730            return start;
3731        }
3732        let Some(model) = model_ref else {
3733            if std::env::var("CMF_GRAPH_DBG").is_ok() {
3734                eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
3735            }
3736            if self.graph_head_required {
3737                self.fail_metal_graph("native graph has no mapped model reference");
3738            }
3739            return start;
3740        };
3741        if plan.is_empty() {
3742            if std::env::var("CMF_GRAPH_DBG").is_ok() {
3743                eprintln!("q1-graph: empty plan at layer {start}");
3744            }
3745            if self.graph_head_required {
3746                self.fail_metal_graph("native graph plan is empty");
3747            }
3748            return start;
3749        }
3750        let has_moe = plan.iter().any(|it| match it {
3751            Item::Gdn { run, .. } => run
3752                .iter()
3753                .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
3754            Item::Sconv { run, .. } => run
3755                .iter()
3756                .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
3757            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
3758        });
3759        let has_flint_moe = plan.iter().any(|it| match it {
3760            Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
3761            Item::Sconv { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
3762            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
3763        });
3764        let metal_working_set = crate::gpu_metal::working_set_bytes();
3765        let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
3766        let model_bytes = model.primary_bytes().len() as u64;
3767        // Flint q2 can sit below recommendedMaxWorkingSetSize on a 24 GB
3768        // M4 yet still exceed maxBufferLength. In that case a normal graph
3769        // would manufacture overlapping whole-file WeightArena windows;
3770        // treat it as the same bounded-page requirement rather than letting
3771        // the generic graph reintroduce broad residency.
3772        let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
3773            || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
3774        // An oversized MoE mmap normally has to decline: the jobs kernels
3775        // bind all WeightArena windows.  The opt-in pager takes a different,
3776        // bounded route: a *complete sequence of logical layers* is put in
3777        // one transient no-copy page, graph-finished, dropped, then the outer
3778        // layer loop calls us again for the next page.  That preserves exact
3779        // hidden/GDN/KV handoff while replacing ~40 host-routed expert waits
3780        // with a few page-boundary syncs.  Never use it for a fused-head NLL
3781        // request: that contract explicitly requires a single whole graph.
3782        let pager_cap = if !has_moe {
3783            None
3784        } else if has_flint_moe {
3785            // Do not let generic `CMF_METAL_MOE_PAGER` turn the experimental
3786            // full-bank Flint graph on. Its direct selected-view pager stays
3787            // independently automatic; this path needs its own opt-in.
3788            crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
3789        } else {
3790            crate::gpu_metal::moe_pager_cap_bytes()
3791        };
3792        let pager_eligible =
3793            pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
3794        let mut pager_span: Option<(usize, usize)> = None;
3795        if pager_eligible {
3796            let cap = pager_cap.expect("checked above");
3797            // When this page reaches the true model end, include the fused
3798            // lm_head too.  Otherwise `lm_head_ok` would correctly refuse a
3799            // binding outside the page after the layers had already run.
3800            let tail_span = (self.graph_want_logits
3801                && upto.is_none()
3802                && scan == self.num_layers
3803                && std::env::var("CMF_GPU_LMHEAD")
3804                    .map(|v| v != "0")
3805                    .unwrap_or(true))
3806            .then(|| self.weights.lm_head.metal_graph_parts())
3807            .flatten()
3808            .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
3809            let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
3810                let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
3811                Some(match span {
3812                    Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
3813                    None => (lo, hi),
3814                })
3815            };
3816            let page_span = |span: (usize, usize), include_tail: bool| {
3817                let (mut lo, mut hi) = span;
3818                if include_tail {
3819                    if let Some((tail_lo, tail_hi)) = tail_span {
3820                        lo = lo.min(tail_lo);
3821                        hi = hi.max(tail_hi);
3822                    }
3823                }
3824                crate::gpu_metal::page_round_span(lo, hi)
3825            };
3826            let mut selected = 0usize;
3827            let mut selected_span: Option<(usize, usize)> = None;
3828            // `Item::Gdn` is built as a maximal run so the normal resident
3829            // graph can batch its recurrent state work.  A physical CMF can
3830            // nevertheless place that whole run across a page cap while each
3831            // logical layer fits.  Split *only the final admitted run* at a
3832            // layer boundary: the next outer-loop call rebuilds the remaining
3833            // suffix, preserving hidden/state handoff and keeping the normal
3834            // long-run batching within every page.
3835            let mut gdn_prefix: Option<(usize, usize)> = None;
3836            'items: for (i, item) in plan.iter().enumerate() {
3837                match item {
3838                    // Short-conv runs are not paged (no oversized
3839                    // short-conv MoE has been measured): the page ends.
3840                    Item::Sconv { .. } => break,
3841                    Item::Attn { li, .. } => {
3842                        let Some(span) = add_layer_span(selected_span, *li) else {
3843                            break;
3844                        };
3845                        let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
3846                            break;
3847                        };
3848                        if page_hi - page_lo > cap {
3849                            break;
3850                        }
3851                        selected = i + 1;
3852                        selected_span = Some((page_lo, page_hi));
3853                    }
3854                    Item::Gdn { first, run } => {
3855                        // Preserve a complete recurrent run whenever it can
3856                        // occupy a page on its own.  If the preceding items
3857                        // merely used the remaining cap, close that page and
3858                        // let the next q1 invocation retain the full run.
3859                        // Prefix-splitting is only for a run that genuinely
3860                        // cannot fit an empty page.
3861                        let mut whole = None;
3862                        for take in 1..=run.len() {
3863                            whole = add_layer_span(whole, *first + take - 1);
3864                            if whole.is_none() {
3865                                break;
3866                            }
3867                        }
3868                        let whole_fits_empty_page = whole
3869                            .and_then(|span| page_span(span, false))
3870                            .is_some_and(|(lo, hi)| hi - lo <= cap);
3871                        if whole_fits_empty_page {
3872                            let mut span = selected_span;
3873                            for take in 1..=run.len() {
3874                                let Some(next) = add_layer_span(span, *first + take - 1) else {
3875                                    break 'items;
3876                                };
3877                                span = Some(next);
3878                            }
3879                            let Some(span) = span else {
3880                                break;
3881                            };
3882                            let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
3883                            else {
3884                                break;
3885                            };
3886                            if page_hi - page_lo > cap {
3887                                break;
3888                            }
3889                            selected = i + 1;
3890                            selected_span = Some((page_lo, page_hi));
3891                            continue;
3892                        }
3893                        if selected != 0 {
3894                            break;
3895                        }
3896                        let mut span = None;
3897                        let mut best: Option<(usize, (usize, usize))> = None;
3898                        for take in 1..=run.len() {
3899                            let Some(next) = add_layer_span(span, *first + take - 1) else {
3900                                break;
3901                            };
3902                            // A partial final run cannot encode the lm_head;
3903                            // only its complete form above may include it.
3904                            let Some((page_lo, page_hi)) = page_span(next, false) else {
3905                                break;
3906                            };
3907                            if page_hi - page_lo > cap {
3908                                break;
3909                            }
3910                            span = Some((page_lo, page_hi));
3911                            best = Some((take, (page_lo, page_hi)));
3912                        }
3913                        let Some((take, page)) = best else {
3914                            break;
3915                        };
3916                        selected = i + 1;
3917                        selected_span = Some(page);
3918                        if take != run.len() {
3919                            gdn_prefix = Some((i, take));
3920                            break 'items;
3921                        }
3922                    }
3923                }
3924            }
3925            if selected != 0 {
3926                if let Some((item, take)) = gdn_prefix {
3927                    match &mut plan[item] {
3928                        Item::Gdn { run, .. } => run.truncate(take),
3929                        Item::Sconv { .. } | Item::Attn { .. } => {
3930                            unreachable!("only a GDN run is splittable")
3931                        }
3932                    }
3933                }
3934                let (page_lo, page_hi) = selected_span.expect("selected page span");
3935                tracing::info!(
3936                    page_start = page_lo,
3937                    page_end = page_hi,
3938                    page_mb = (page_hi - page_lo) / (1024 * 1024),
3939                    cap_mb = cap / (1024 * 1024),
3940                    graph_items = selected,
3941                    "native Metal MoE graph uses transient bounded weight page"
3942                );
3943                plan.truncate(selected);
3944                pager_span = Some((page_lo, page_hi));
3945            } else {
3946                tracing::warn!(
3947                    cap_mb = cap / (1024 * 1024),
3948                    "native Metal MoE pager declined: one graph item does not fit its safe page cap"
3949                );
3950            }
3951        }
3952        // A native MoE graph binds every WeightArena window to its jobs
3953        // kernels. On UMA, an mmap that already exceeds the driver's
3954        // recommended working set will therefore fail after admission, when
3955        // some recurrent/KV scratch has already been touched. Decline before
3956        // allocating/encoding the graph instead; the ordinary per-operation
3957        // route remains available and only materializes the needed window.
3958        // Do not apply this to dense graphs: they bind one projection at a
3959        // time and do not have the MoE jobs' all-window lifetime.
3960        if has_moe && needs_bounded_pager && pager_span.is_none() {
3961            tracing::info!(
3962                model_bytes,
3963                metal_working_set,
3964                "native Metal MoE graph declined before allocation"
3965            );
3966            self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
3967            if self.graph_head_required {
3968                self.fail_metal_graph(MOE_WORKING_SET_REASON);
3969            }
3970            return start;
3971        }
3972        let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
3973        let dev_attend = attend_contract
3974            && (self.head_dim <= 128
3975                || has_moe
3976                // A GDN hybrid attends on a quarter of its layers: the
3977                // hd>128 caution was measured on pure-dense models where
3978                // gqa_attend dominates, and on Qwen3.8-27B (hd 256, 48
3979                // GDN + 16 attn) the sandwich costs 2x the whole decode
3980                // (1.2 vs 2.21 tok/s measured before the arena fix).
3981                || (self.head_dim <= 256 && has_gdn)
3982                // Sliding-window layers bound most attends by the window:
3983                // Spark-X2.5-1.7B q4tp on the M4 decodes 67 tok/s
3984                // device-attended against 23 sandwiched (28 syncs/token).
3985                || (self.head_dim <= 256 && swa_graph)
3986                || attend_mode == "force"
3987                || attend_mode == "256");
3988        if !dev_attend {
3989            for it in &mut plan {
3990                if let Item::Attn { li, full_gpu, .. } = it {
3991                    // The hd>128 policy is about gqa_attend; an o1 layer
3992                    // attends through its own kernel set.
3993                    let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
3994                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
3995                    if !keep_o1 {
3996                        *full_gpu = false;
3997                    }
3998                }
3999            }
4000        }
4001        if std::env::var("CMF_GRAPH_DBG").is_ok() {
4002            use std::sync::atomic::{AtomicBool, Ordering};
4003            static SAID: AtomicBool = AtomicBool::new(false);
4004            if !SAID.swap(true, Ordering::Relaxed) {
4005                let fg = plan
4006                    .iter()
4007                    .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
4008                    .count();
4009                let att = plan
4010                    .iter()
4011                    .filter(|it| matches!(it, Item::Attn { .. }))
4012                    .count();
4013                eprintln!(
4014                    "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
4015                    plan.len(),
4016                    self.head_dim,
4017                    self.rotary_dim,
4018                    self.num_kv_heads,
4019                    self.num_heads,
4020                );
4021            }
4022        }
4023        let dims = GraphDims {
4024            hidden: self.hidden_size,
4025            eps: self.rms_eps as f32,
4026            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
4027        };
4028        // The outer `forward_layers_span` scope is one logical token.  This
4029        // guard starts at q1_graph_gpu entry and ends after all page readback,
4030        // so a multi-page token cannot be reported as several whole tokens.
4031        let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
4032            crate::gpu_metal::MoePagerPageScope::new(
4033                _mt0,
4034                position,
4035                start,
4036                page_lo,
4037                page_hi,
4038                // `pager_span` is selected only after a cap was admitted;
4039                // keep the profiler observational if that invariant is ever
4040                // loosened rather than introducing a new inference panic.
4041                pager_cap.unwrap_or(0),
4042            )
4043        });
4044        let graph_new = match pager_span {
4045            Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
4046            None => TokenGraph::new(&model, dims, h),
4047        };
4048        let Some(mut graph) = graph_new else {
4049            if self.graph_head_required {
4050                self.fail_metal_graph("native TokenGraph allocation refused");
4051            }
4052            return start;
4053        };
4054        if swa_graph && self.head_dim > 128 {
4055            // Spark-X2.5 (hd 256, 4 Q heads per KV head): the GQA-shared
4056            // split-K attend reads each K/V row once for the group instead
4057            // of once per head plus the importance re-read — at depth 1000
4058            // on the M4, 59.5 tok/s against 50.0 with the per-head kernel
4059            // up to the default 512 (every sliding layer sits at <= 512).
4060            // From the second position on and in 32-position blocks (more
4061            // threadgroups in flight for a latency-bound walk): Spark-X2.6
4062            // q4tp decode 0.86x the GPU time short and 0.88x at depth 1000,
4063            // q8_2f 0.91x / 0.92x (in-process A/B, M4).
4064            graph.set_attend_blk(1, 32);
4065        }
4066        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
4067            nv: cfg.num_v_heads,
4068            nk: cfg.num_k_heads,
4069            dk: cfg.key_head_dim,
4070            dv: cfg.value_head_dim,
4071            kk: cfg.conv_kernel,
4072            hidden: self.hidden_size,
4073            inter: self.intermediate_size,
4074            c_dim: cfg.conv_dim(),
4075            eps: cfg.rms_eps as f32,
4076            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
4077        });
4078        // Validate the whole plan BEFORE encoding anything: after the
4079        // first sync a refused layer would leave the token
4080        // half-executed, so truncate to the provably encodable prefix.
4081        let mut valid = 0usize;
4082        let mut end = start;
4083        crate::gpu::stageprof(1, _mt0.elapsed()); // конец планирования
4084        if std::env::var("CMF_PLAN_DUMP").is_ok() {
4085            static ONCE: std::sync::Once = std::sync::Once::new();
4086            ONCE.call_once(|| {
4087                for it in &plan {
4088                    match it {
4089                        Item::Gdn { first, run } => {
4090                            eprintln!("plan: Gdn first={first} len={}", run.len())
4091                        }
4092                        Item::Sconv { first, run } => {
4093                            eprintln!("plan: Sconv first={first} len={}", run.len())
4094                        }
4095                        Item::Attn { li, full_gpu, .. } => {
4096                            eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
4097                        }
4098                    }
4099                }
4100            });
4101        }
4102        for item in &plan {
4103            let ok = match item {
4104                Item::Gdn { run, .. } => gcfg
4105                    .as_ref()
4106                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
4107                    .unwrap_or(false),
4108                Item::Sconv { run, .. } => self
4109                    .short_conv_cfg
4110                    .is_some_and(|sc| run.iter().all(|l| graph.sconv_ok(l, sc.kernel))),
4111                Item::Attn { l, .. } => graph.attn_ok(l),
4112            };
4113            if !ok {
4114                if block_diag {
4115                    eprintln!(
4116                        "block-graph: plan item {} ({}) failed graph preflight",
4117                        valid,
4118                        match item {
4119                            Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
4120                            Item::Sconv { run, first } => {
4121                                format!("short-conv run L{first}+{}", run.len())
4122                            }
4123                            Item::Attn { li, .. } => format!("Attn L{li}"),
4124                        }
4125                    );
4126                }
4127                break;
4128            }
4129            valid += 1;
4130            end += match item {
4131                Item::Gdn { run, .. } => run.len(),
4132                Item::Sconv { run, .. } => run.len(),
4133                Item::Attn { .. } => 1,
4134            };
4135        }
4136        plan.truncate(valid);
4137        if plan.is_empty() {
4138            if self.graph_head_required {
4139                self.fail_metal_graph("native graph preflight produced no valid items");
4140            }
4141            return start;
4142        }
4143
4144        if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
4145            self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
4146            return start;
4147        }
4148
4149        // Plain dense decode (every item a device-attended full-attention
4150        // layer with a dense FFN, no O(1) state): the only plan shape the
4151        // masked-nibble q4tp matvec and the concurrent layer encoder were
4152        // measured on (MiniCPM5-2B, Qwen3-0.6B on the M4). Hybrids, MoE and
4153        // o1 layers keep the historical serial path bit for bit.
4154        // Every projection must be ONE dispatch (q1t adds an overlay pass,
4155        // Prism q2tp a transform pass — dependent pairs a concurrent
4156        // encoder would race).
4157        let one_pass = |t: (usize, usize, usize)| {
4158            use cortiq_core::TensorDtype as D;
4159            matches!(
4160                model.tensors[t.0].dtype,
4161                D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
4162            )
4163        };
4164        let dense_fast = plan.iter().all(|it| match it {
4165            Item::Attn {
4166                l, li, full_gpu, ..
4167            } => {
4168                *full_gpu
4169                    && self.kv_cache.layers[*li].o1.is_none()
4170                    && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
4171                    && match l.ffn {
4172                        MetalFfn::Dense { gate, up, down, .. } => {
4173                            one_pass(gate) && one_pass(up) && one_pass(down)
4174                        }
4175                        _ => false,
4176                    }
4177            }
4178            // A short-conv run encodes serially in its own encoder: its
4179            // one-pass projections take the same matvec levers.
4180            Item::Sconv { run, .. } => run.iter().all(|l| {
4181                one_pass(l.in_proj)
4182                    && one_pass(l.out)
4183                    && match l.ffn {
4184                        MetalFfn::Dense { gate, up, down, .. } => {
4185                            one_pass(gate) && one_pass(up) && one_pass(down)
4186                        }
4187                        _ => false,
4188                    }
4189            }),
4190            Item::Gdn { .. } => false,
4191        });
4192        // MoE decode with every layer device-attended (Mellum2.1): the
4193        // routed experts' jobs ladder and the q8_2f attention projections
4194        // take their measured levers (`MOE_*`, `DENSE_Q8R4`).
4195        let moe_plan = !dense_fast
4196            && plan.iter().all(|it| match it {
4197                Item::Attn {
4198                    l, li, full_gpu, ..
4199                } => {
4200                    *full_gpu
4201                        && self.kv_cache.layers[*li].o1.is_none()
4202                        && (matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
4203                            || self.dash.is_some())
4204                }
4205                Item::Gdn { .. } | Item::Sconv { .. } => false,
4206            });
4207        let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
4208        if block_diag {
4209            use std::sync::atomic::{AtomicBool, Ordering};
4210            static SAID_AB: AtomicBool = AtomicBool::new(false);
4211            if !SAID_AB.swap(true, Ordering::Relaxed) {
4212                eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
4213            }
4214        }
4215        let _mv_fast = match ab {
4216            Some((bits, _)) => {
4217                graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
4218                crate::gpu_metal::MvFastGuard::set_raw(bits)
4219            }
4220            None if moe_plan => {
4221                graph.set_dense_concurrent(
4222                    crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
4223                );
4224                crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
4225            }
4226            None => {
4227                graph.set_dense_concurrent(dense_fast);
4228                // The four-row q8_2f matvec (same per-row sum order, text
4229                // unchanged): in-process A/B on the M4, token time 0.81x on
4230                // MiniCPM5-2B, 0.78x on Minico-M1, 0.85x on MNN-Pocket-0.3B;
4231                // Spark-X2.5 1.7B 40.5 against 27.7 tok/s. `CMF_Q8_R4=0` closes it.
4232                let _ = swa_graph;
4233                crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
4234                    crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | crate::gpu_metal::DENSE_Q8R4
4235                } else {
4236                    0
4237                })
4238            }
4239        };
4240
4241        // RoPE table and rotary width are per layer (`layer_inv_freq`,
4242        // `layer_geom`) inside the loop below.
4243        let pool = self.pool.clone();
4244        let (nh, nkv, hd, hs, eps) = (
4245            self.num_heads,
4246            self.num_kv_heads,
4247            self.head_dim,
4248            self.hidden_size,
4249            self.rms_eps,
4250        );
4251        let norm_style = self.norm_style;
4252        let gemma = norm_style == cortiq_core::NormStyle::Gemma;
4253        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
4254        let kv_id = self.graph_kv_id;
4255        // GDN runs whose states await readback after the next sync
4256        // (device-attended layers add no sync, so several may stack).
4257        let mut pending: Vec<(usize, usize)> = Vec::new();
4258        // Device-attended layers: their K/V/imp are pulled from the
4259        // mirror after the final sync.
4260        let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
4261        // The layers a DFlash drafter reads, kept as the token passes them
4262        // (a whole token in one graph only).
4263        let taps: Vec<usize> = match &self.dflash {
4264            Some(d) if start == 0 && end == self.num_layers => d.g.taps.clone(),
4265            _ => Vec::new(),
4266        };
4267        let mut tapped = 0usize;
4268        for item in &plan {
4269            let _xt0 = std::time::Instant::now();
4270            let _xkind: u32 = match item {
4271                Item::Gdn { .. } | Item::Sconv { .. } => 2,
4272                Item::Attn { .. } => 3,
4273            };
4274            // Looped Transformer: insert on-device norm at loop boundaries.
4275            if self.loop_final_norm {
4276                let item_start = match item {
4277                    Item::Gdn { first, .. } | Item::Sconv { first, .. } => *first,
4278                    Item::Attn { li, .. } => *li,
4279                };
4280                if item_start > start && self.is_loop_end(item_start - 1) {
4281                    graph.encode_loop_norm(&self.weights.final_norm);
4282                }
4283            }
4284            match item {
4285                Item::Gdn { run, first } => {
4286                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
4287                        if l.linear_state.len() != want {
4288                            l.linear_state = vec![0f32; want];
4289                        }
4290                    }
4291                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
4292                        .iter()
4293                        .map(|l| l.linear_state.as_slice())
4294                        .collect();
4295                    let _ig = std::time::Instant::now();
4296                    if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
4297                        // Unreachable: the plan was validated above.
4298                        tracing::error!("q1 graph: GDN run refused after validation");
4299                        return start;
4300                    }
4301                    if let Some(slot) = taps.iter().position(|t| *t + 1 == *first + run.len()) {
4302                        tapped += graph.tap(slot, taps.len()) as usize;
4303                    }
4304                    // Early commit: the GPU starts the run while the
4305                    // CPU encodes the next layer (nothing to wait on).
4306                    graph.commit_kind = 2;
4307                    graph.commit();
4308                    crate::gpu::stageprof(0, _ig.elapsed());
4309                    pending.push((*first, run.len()));
4310                }
4311                Item::Sconv { run, first } => {
4312                    let sc = self.short_conv_cfg.expect("validated short-conv run");
4313                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
4314                        if l.linear_state.len() != sc.state_len() {
4315                            l.linear_state = vec![0f32; sc.state_len()];
4316                        }
4317                    }
4318                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
4319                        .iter()
4320                        .map(|l| l.linear_state.as_slice())
4321                        .collect();
4322                    if !graph.encode_sconv_run(run, &ro, sc.kernel) {
4323                        tracing::error!("q1 graph: short-conv run refused after validation");
4324                        return start;
4325                    }
4326                    graph.commit_kind = 2;
4327                    graph.commit();
4328                    pending.push((*first, run.len()));
4329                }
4330                Item::Attn {
4331                    l,
4332                    li,
4333                    q_norm,
4334                    k_norm,
4335                    output_gate,
4336                    proj_gate,
4337                    head_gate_w,
4338                    bias,
4339                    full_gpu,
4340                } => {
4341                    let _ia = std::time::Instant::now();
4342                    // This layer's attention geometry: its window, RoPE
4343                    // table and rotary width (Spark-X2.5 interleaves
4344                    // 512-window layers rotating all dims at θ 1e4 with
4345                    // full layers rotating a quarter at θ 5e6). Every model
4346                    // without sliding layers reads the global ones here.
4347                    let inv_freq_l = self.layer_inv_freq(*li);
4348                    let rd_l = self.layer_geom(*li).2;
4349                    let window_l = self.layer_window(*li);
4350                    let head_gate_w = *head_gate_w;
4351                    // This layer's KV heads and head width: Gemma-4's global
4352                    // layers (one 512-wide KV head) differ from the base.
4353                    let (nkv, hd) = if gemma4 {
4354                        let (a, b, _) = self.layer_geom(*li);
4355                        (a, b)
4356                    } else {
4357                        (nkv, hd)
4358                    };
4359                    // ── Fully device-resident attention: no sync at all.
4360                    if *full_gpu {
4361                        let cache = &self.kv_cache.layers[*li];
4362                        let o1p = if cache.o1.is_some() {
4363                            match cache.o1_views() {
4364                                Some(views) => Some(crate::gpu::O1AttnParams {
4365                                    views,
4366                                    epoch: self.o1_epoch,
4367                                }),
4368                                // Sealed state gone mid-run: sandwich.
4369                                None => None,
4370                            }
4371                        } else {
4372                            None
4373                        };
4374                        let o1_layer = cache.o1.is_some();
4375                        if o1_layer && o1p.is_none() {
4376                            // fall to the sandwich (CPU o1 step)
4377                        }
4378                        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
4379                        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
4380                        let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
4381                        // A trimmed tail always holds the window the
4382                        // device attend reads (`first = rows + 1 − w`).
4383                        debug_assert!(
4384                            cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
4385                        );
4386                        let dash_l = self.graph_dash(*li);
4387                        // A DASH row carries its own angles: position 1 over
4388                        // them is the rotation (`uocr::rope_angles`).
4389                        let (rope_at, rope_invf, rope_sc): (usize, &[f32], f32) = match self.dash.as_deref() {
4390                            Some(d) => (1, &d.angles, 1.0),
4391                            None => (position, &inv_freq_l, self.layer_rope_scale(*li)),
4392                        };
4393                        let p = crate::gpu::AttnDeviceParams {
4394                            kv_id,
4395                            layer: *li,
4396                            nh,
4397                            nkv,
4398                            hd,
4399                            rd: rd_l,
4400                            position: rope_at,
4401                            scale: self.attn_scale,
4402                            eps: eps as f32,
4403                            gemma,
4404                            late_qk_norm: self.qk_norm_after_rope,
4405                            output_gate: *output_gate,
4406                            q_norm: *q_norm,
4407                            k_norm: *k_norm,
4408                            inv_freq: rope_invf,
4409                            rope_scale: rope_sc,
4410                            cpu_k,
4411                            cpu_v,
4412                            cpu_stored,
4413                            cpu_gen: cache.generation(),
4414                            o1: o1p,
4415                            window: window_l,
4416                            softmax_sink: cache.sinks.as_deref(),
4417                            head_gate: head_gate_w,
4418                            v_norm: self.attn_v_norm,
4419                            dash: dash_l.as_ref(),
4420                        };
4421                        let o1_bad = o1_layer && p.o1.is_none();
4422                        if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
4423                        {
4424                            // o1 layers leave no mirror row to pull.
4425                            if p.o1.is_none() {
4426                                dev_attn.push((*li, nkv, hd));
4427                            }
4428                            if let Some(slot) = taps.iter().position(|t| t == li) {
4429                                tapped += graph.tap(slot, taps.len()) as usize;
4430                            }
4431                            graph.commit_kind = 3;
4432                            graph.commit();
4433                            // The footer below is skipped by `continue`:
4434                            // account the device-attn item here or its
4435                            // cost hides from the stage profile entirely.
4436                            crate::gpu::stageprof(_xkind, _xt0.elapsed());
4437                            continue;
4438                        }
4439                        // Mirror refused (nothing encoded) → sandwich.
4440                    }
4441                    graph.encode_attn_prefix(l);
4442                    if let Err(err) = graph.sync_checked() {
4443                        self.fail_metal_graph(&err);
4444                        return start;
4445                    }
4446                    if !pending.is_empty() {
4447                        let idxs: Vec<usize> =
4448                            pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
4449                        let mut outs: Vec<&mut [f32]> = self
4450                            .kv_cache
4451                            .layers
4452                            .iter_mut()
4453                            .enumerate()
4454                            .filter(|(i, _)| idxs.binary_search(i).is_ok())
4455                            .map(|(_, s)| s.linear_state.as_mut_slice())
4456                            .collect();
4457                        graph.read_states(&mut outs);
4458                    }
4459                    let mut q_raw = attention::take_buf(l.wq.1);
4460                    let mut k = attention::take_buf(l.wk.1);
4461                    let mut v = attention::take_buf(l.wv.1);
4462                    graph.read_qkv(&mut q_raw, &mut k, &mut v);
4463                    // Projected output gate: g = G·norm(h) on the host, off
4464                    // the normed input the prefix left on the device.
4465                    let mut gate_raw = proj_gate.map(|(gp, _)| {
4466                        let mut normed = attention::take_buf(hs);
4467                        graph.read_normed(&mut normed);
4468                        let mut raw = attention::take_buf(gp.rows());
4469                        gp.matvec(&normed, &mut raw, pool.as_deref());
4470                        attention::recycle_buf(&mut normed);
4471                        raw
4472                    });
4473                    let cfg = QwenAttnCfg {
4474                        mrope: None,
4475                        num_heads: nh,
4476                        num_kv_heads: nkv,
4477                        head_dim: hd,
4478                        hidden_size: hs,
4479                        position,
4480                        inv_freq: &inv_freq_l,
4481                        rotary_dim: rd_l,
4482                        scale: self.attn_scale,
4483                        softcap: self.attn_softcap,
4484                        window: window_l,
4485                        // Gemma-4 only (the front gate refuses V norm
4486                        // otherwise).
4487                        v_norm: self.attn_v_norm,
4488                        qk_norm_after_rope: self.qk_norm_after_rope,
4489                        gate_sigmoid: self.proj_gate_sigmoid,
4490                        q_norm: *q_norm,
4491                        k_norm: *k_norm,
4492                        output_gate: *output_gate,
4493                        softplus_gate: None,
4494                        rope_scale: self.layer_rope_scale(*li),
4495                        bias: *bias,
4496                        rms_eps: eps,
4497                        norm_style,
4498                        pool: pool.as_deref(),
4499                        v_head_dim: hd,
4500                        value_in: None,
4501                    };
4502                    // CMF_ATTN_ORACLE=1: diff the device attend against
4503                    // this CPU attend on identical inputs (bring-up).
4504                    let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
4505                        || std::env::var("CMF_ATTN_DUMP").is_ok();
4506                    let _ = full_gpu;
4507                    let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
4508                    let mut ao = attention::qwen_attention_core(
4509                        q_raw,
4510                        k,
4511                        v,
4512                        &mut self.kv_cache.layers[*li],
4513                        &cfg,
4514                    );
4515                    // CMF_ATTN_DUMP=<dir>: this token's rope'd Q and the layer's whole
4516                    // K/V cache as raw f32 (offline attention-statistics probes:
4517                    // block bounds, mass concentration). Needs CMF_GPU_ATTEND=0.
4518                    if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
4519                        if let Some((qr0, k0, v0)) = oracle_in.clone() {
4520                            let (cq, _cg, _ck, _cv) =
4521                                attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
4522                            let cache = &self.kv_cache.layers[*li];
4523                            let n = cache.head_keys(0).len() / hd;
4524                            let mut bytes: Vec<u8> = Vec::new();
4525                            for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
4526                                bytes.extend_from_slice(&v.to_le_bytes());
4527                            }
4528                            for v in &cq {
4529                                bytes.extend_from_slice(&v.to_le_bytes());
4530                            }
4531                            for g in 0..nkv {
4532                                for v in cache.head_keys(g) {
4533                                    bytes.extend_from_slice(&v.to_le_bytes());
4534                                }
4535                            }
4536                            for g in 0..nkv {
4537                                for v in cache.head_values(g) {
4538                                    bytes.extend_from_slice(&v.to_le_bytes());
4539                                }
4540                            }
4541                            let _ =
4542                                std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
4543                        }
4544                    }
4545                    if let Some((qr0, k0, v0)) =
4546                        oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
4547                    {
4548                        let (cq, _cg, ck, cv) =
4549                            attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
4550                        let mut h_now = vec![0f32; hs];
4551                        graph.read_h(&mut h_now);
4552                        let cache = &self.kv_cache.layers[*li];
4553                        let n_after = cache.head_keys(0).len() / hd;
4554                        // A sealed O(1) cache may have no dense current-row
4555                        // entry. The oracle is a debug probe, so let it see
4556                        // zero stored exact rows instead of underflowing.
4557                        let stored = n_after.saturating_sub(1);
4558                        let cpu_k: Vec<&[f32]> = (0..nkv)
4559                            .map(|g| &cache.head_keys(g)[..stored * hd])
4560                            .collect();
4561                        let cpu_v: Vec<&[f32]> = (0..nkv)
4562                            .map(|g| &cache.head_values(g)[..stored * hd])
4563                            .collect();
4564                        let p = crate::gpu::AttnDeviceParams {
4565                            kv_id,
4566                            layer: *li,
4567                            nh,
4568                            nkv,
4569                            hd,
4570                            // The layer's own geometry, as the CPU attend
4571                            // above used it (the projected gate is applied
4572                            // after this probe on both sides).
4573                            rd: rd_l,
4574                            position,
4575                            scale: self.attn_scale,
4576                            eps: eps as f32,
4577                            gemma,
4578                            late_qk_norm: self.qk_norm_after_rope,
4579                            output_gate: *output_gate,
4580                            q_norm: *q_norm,
4581                            k_norm: *k_norm,
4582                            inv_freq: &inv_freq_l,
4583                            rope_scale: self.layer_rope_scale(*li),
4584                            cpu_k,
4585                            cpu_v,
4586                            cpu_stored: stored,
4587                            cpu_gen: cache.generation(),
4588                            o1: None,
4589                            window: window_l,
4590                            softmax_sink: cache.sinks.as_deref(),
4591                            head_gate: None,
4592                            v_norm: self.attn_v_norm,
4593                            dash: None,
4594                        };
4595                        if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
4596                            let md = |a: &[f32], b: &[f32]| {
4597                                a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
4598                            };
4599                            let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
4600                            eprintln!(
4601                                "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}",
4602                                nn(&cq),
4603                                md(&cq, &dq),
4604                                nn(&ck),
4605                                md(&ck, &dk),
4606                                nn(&cv),
4607                                md(&cv, &dv),
4608                                nn(&ao),
4609                                md(&ao, &dao)
4610                            );
4611                        } else {
4612                            eprintln!("attn-oracle L{li}: device probe declined");
4613                        }
4614                    }
4615                    if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
4616                        // V is as wide as the head (the front gate), so ao
4617                        // is nh·hd here, as in `qwen_attention`.
4618                        attention::apply_projected_gate(
4619                            &mut ao,
4620                            raw,
4621                            per_head,
4622                            hd,
4623                            self.proj_gate_sigmoid,
4624                        );
4625                    }
4626                    if let Some(mut raw) = gate_raw.take() {
4627                        attention::recycle_buf(&mut raw);
4628                    }
4629                    graph.encode_attn_suffix(l, &ao);
4630                    if let Some(slot) = taps.iter().position(|t| t == li) {
4631                        tapped += graph.tap(slot, taps.len()) as usize;
4632                    }
4633                    // Early commit: the GPU starts O+FFN while the CPU
4634                    // encodes the following GDN run / attention prefix.
4635                    graph.commit();
4636                    attention::recycle_buf(&mut ao);
4637                }
4638            }
4639
4640            crate::gpu::stageprof(_xkind, _xt0.elapsed());
4641        }
4642        // Ride the final norm + lm_head in the same command buffer when
4643        // this run reaches the model's end and the caller wants logits:
4644        // the separate per-op lm_head submit (a full round trip) folds
4645        // into the sync that already happens here.
4646        let mut lm_rows = None;
4647        if self.graph_want_logits
4648            && upto.is_none()
4649            && end == self.num_layers
4650            && std::env::var("CMF_GPU_LMHEAD")
4651                .map(|v| v != "0")
4652                .unwrap_or(true)
4653        {
4654            if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
4655                if graph.lm_head_ok(lm) {
4656                    graph.encode_lm_head(&self.weights.final_norm, lm);
4657                    lm_rows = Some(lm.1);
4658                }
4659            }
4660        }
4661        if self.graph_head_required && lm_rows.is_none() {
4662            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4663            self.fail_metal_graph("fused graph head was requested but not encodable");
4664            return start;
4665        }
4666        let _sy0 = std::time::Instant::now();
4667        if let Err(err) = graph.sync_checked() {
4668            if let Some(profile) = pager_profile_page.as_mut() {
4669                profile.finish(start, graph.pager_page_stats(), "sync-error");
4670            }
4671            self.fail_metal_graph(&err);
4672            return start;
4673        }
4674        let _rs0 = std::time::Instant::now();
4675        if !pending.is_empty() {
4676            let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
4677            let mut outs: Vec<&mut [f32]> = self
4678                .kv_cache
4679                .layers
4680                .iter_mut()
4681                .enumerate()
4682                .filter(|(i, _)| idxs.binary_search(i).is_ok())
4683                .map(|(_, s)| s.linear_state.as_mut_slice())
4684                .collect();
4685            graph.read_states(&mut outs);
4686        }
4687        if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
4688            use std::sync::atomic::{AtomicU64, Ordering};
4689            static SY: AtomicU64 = AtomicU64::new(0);
4690            static RS: AtomicU64 = AtomicU64::new(0);
4691            static N: AtomicU64 = AtomicU64::new(0);
4692            SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
4693            RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
4694            let n = N.fetch_add(1, Ordering::Relaxed) + 1;
4695            if n % 100 == 0 {
4696                eprintln!(
4697                    "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
4698                    SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
4699                    RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
4700                );
4701            }
4702        }
4703        if let Some(rows) = lm_rows {
4704            crate::gpu::hostprof_encode_done(_mt0);
4705            let mut lg = attention::take_buf(rows.min(self.vocab_size));
4706            graph.read_logits(&mut lg);
4707            crate::gpu::hostprof_total(_mt0);
4708            lg.resize(self.vocab_size, 0.0);
4709            if let Some(c) = self.final_softcap {
4710                softcap_in_place(&mut lg, c, self.pool.as_deref());
4711            }
4712            self.graph_logits = Some(lg);
4713        }
4714        graph.read_h(h);
4715        // the token's row of the drafter's context
4716        if !taps.is_empty() && tapped == taps.len() {
4717            let mut row = vec![0f32; taps.len() * self.hidden_size];
4718            if graph.read_taps(&mut row) {
4719                if let Some(d) = self.dflash.as_mut() {
4720                    d.queue_context(position, &row, self.pool.as_deref());
4721                }
4722            }
4723        }
4724        if self.graph_head_required && self.graph_logits.is_none() {
4725            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4726            self.fail_metal_graph("fused graph head completed without logits readback");
4727            return start;
4728        }
4729        METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4730        METAL_GRAPH_LAYERS.fetch_add(
4731            end.saturating_sub(start) as u64,
4732            std::sync::atomic::Ordering::Relaxed,
4733        );
4734        if self.graph_head_required {
4735            METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4736        }
4737        // Device-attended layers: replay the CPU bookkeeping — append
4738        // the mirror's new K/V row (rope'd on the GPU) into the owner
4739        // cache, then bank this token's attention-importance mass.
4740        for (li, nkv, hd) in dev_attn {
4741            let mut krow = attention::take_buf(nkv * hd);
4742            let mut vrow = attention::take_buf(nkv * hd);
4743            if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
4744                let cache = &mut self.kv_cache.layers[li];
4745                cache.append(&krow, &vrow, &[]);
4746                let n = cache.seq_len;
4747                let mut imp = attention::take_buf(n);
4748                crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
4749                cache.accumulate_imp(&imp);
4750                attention::recycle_buf(&mut imp);
4751            }
4752            attention::recycle_buf(&mut krow);
4753            attention::recycle_buf(&mut vrow);
4754        }
4755        if let Some(profile) = pager_profile_page.as_mut() {
4756            profile.finish(end, graph.pager_page_stats(), "page-complete");
4757        }
4758        if let Some((_, arm)) = ab {
4759            crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
4760        }
4761        end
4762    }
4763
4764    pub fn new(
4765        tokenizer: Tokenizer,
4766        weights: PipelineWeights,
4767        hidden_size: usize,
4768        intermediate_size: usize,
4769        num_heads: usize,
4770        num_kv_heads: usize,
4771        head_dim: usize,
4772        num_layers: usize,
4773        physical_layers: usize,
4774        loop_final_norm: bool,
4775        vocab_size: usize,
4776        rms_eps: f64,
4777        rope_base: f32,
4778        norm_style: NormStyle,
4779        max_seq_len: usize,
4780        sampler_config: SamplerConfig,
4781    ) -> Self {
4782        let rng = match sampler_config.seed {
4783            Some(s) => SplitMix64::new(s),
4784            None => SplitMix64::from_entropy(),
4785        };
4786        let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
4787        let pool = Pool::from_env();
4788        if let Some(p) = &pool {
4789            tracing::info!("worker pool: {} threads", p.n_workers());
4790            // Keep the workers on the socket that holds the weights.
4791            if let Some(model) = weights
4792                .lm_head
4793                .model_arc()
4794                .or_else(|| weights.embed_tokens.model_arc())
4795            {
4796                let regions: Vec<&[u8]> =
4797                    model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
4798                p.bind_numa(&regions);
4799            }
4800        }
4801        Self {
4802            gpu_plan: None,
4803            tokenizer: std::sync::Arc::new(tokenizer),
4804            kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
4805            sampler_config,
4806            weights,
4807            hidden_size,
4808            intermediate_size,
4809            num_heads,
4810            num_kv_heads,
4811            head_dim,
4812            num_layers,
4813            physical_layers,
4814            loop_final_norm,
4815            layer_map: None,
4816            dash: None,
4817            dash_batch_on: true,
4818            mhc: None,
4819            mhc_r: Vec::new(),
4820            mrope_sections: None,
4821            prompt_mrope: None,
4822            vocab_size,
4823            rms_eps,
4824            rope_base,
4825            norm_style,
4826            rotary_dim: head_dim,
4827            attention_heads_per_layer: None,
4828            kv_heads_per_layer: None,
4829            v_head_dim: None,
4830            layer_dump: std::env::var_os("CMF_LAYER_DUMP")
4831                .filter(|v| !v.is_empty())
4832                .map(std::path::PathBuf::from),
4833            graph_declines: std::cell::RefCell::new(Vec::new()),
4834            mimo_moe: Default::default(),
4835            minicpm_tail: Default::default(),
4836            vmf_cfg: None,
4837            gdn_cfg: None,
4838            kda_cfg: None,
4839            g3n: None,
4840            g4e: None,
4841            dsv4: None,
4842            dory: None,
4843            dsv41: None,
4844            dsv41_vision: None,
4845            dsv41_prefill: None,
4846            qwen4_exp: None,
4847            clover: None,
4848            clover_stats: clover::CloverStats::default(),
4849            clover_pen: Default::default(),
4850            dsv4_mtp: Vec::new(),
4851            dspark: None,
4852            dspark_pending: Vec::new(),
4853            dspark_hist: Vec::new(),
4854            dspark_real: Vec::new(),
4855            dspark_trunk_picks: Vec::new(),
4856            dspark_exp: Vec::new(),
4857            dspark_draft_ns: 0,
4858            m3_winners: Vec::new(),
4859            m3_counts: Vec::new(),
4860            m3_n_exp: 0,
4861            m3_tokens: 0,
4862            m3_prewarmed: false,
4863            logit_multiplier: None,
4864            cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
4865            graph_failed: std::sync::atomic::AtomicBool::new(false),
4866            graph_moe_banks: std::sync::Mutex::new(std::collections::HashMap::new()),
4867            kv_history: Vec::new(),
4868            kv_history_device: false,
4869            short_conv_cfg: None,
4870            mtp: None,
4871            mimo_mtp: None,
4872            verify_exact_moe: false,
4873            speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
4874            ignore_eos: false,
4875            draft_full_streak: 0,
4876            spec_k_adapt: None,
4877            spec_acc_ewma: 0.7,
4878            spec_hold: SPEC_HOLD_PRIOR,
4879            spec_wall: [0.0; 7],
4880            spec_wall_n: [0; 7],
4881            spec_k_dwell: 0,
4882            spec_plain_ms: (0.0, 0),
4883            dflash_hold: DFLASH_HOLD_PRIOR,
4884            dflash_last: false,
4885            dflash_wait: 0,
4886            dflash_back: DFLASH_WAIT,
4887            dflash_turn: 0,
4888            dflash_turn_sum: (0.0, 0.0),
4889            dflash_lean: 0.0,
4890            dflash_k_adapt: None,
4891            dflash_acc_ewma: 0.6,
4892            spec_lookup_gain: 2.0,
4893            spec_lookup_wide: false,
4894            spec_lookup_rest: 0,
4895            rng,
4896            sampler_scratch: SamplerScratch::default(),
4897            spec_forced: None,
4898            spec_q: Vec::new(),
4899            spec_p: Vec::new(),
4900            spec_res: Vec::new(),
4901            spec_qs: Vec::new(),
4902            spec_ps: Vec::new(),
4903            spec_ress: Vec::new(),
4904            mtp_graph_mode: None,
4905            dflash: None,
4906            dflash_rows: None,
4907            dflash_taps: std::sync::Mutex::new(Vec::new()),
4908            dflash_solo: false,
4909            dflash_tap_from: 0,
4910            #[cfg(target_os = "macos")]
4911            metal_verify: None,
4912            inv_freq,
4913            ws: ForwardScratch::new(hidden_size),
4914            pool,
4915            model: None,
4916            dyn_force_f32: false,
4917            dyn_skill_layers: Vec::new(),
4918            dyn_active: None,
4919            dyn_blend_loaded: false,
4920            dyn_phi_layer: None,
4921            dyn_phi_ema: Vec::new(),
4922            dyn_phi_seen: 0,
4923            dyn_router: None,
4924            o1_cfg: None,
4925            o1_turbo: None,
4926            mtp_pos_base: 0,
4927            o1_prefix_tail: Vec::new(),
4928            gdn_snap: None,
4929            mtp_warm_base: None,
4930            o1_epoch: 0,
4931            o1_flags: Vec::new(),
4932            trace: false,
4933            calib_temp: 1.0,
4934            confidence_on: true,
4935            embed_multiplier: 1.0,
4936            attn_scale: 1.0 / (head_dim as f32).sqrt(),
4937            swa: None,
4938            swa_trim: None,
4939            swa_trim_anchor: usize::MAX,
4940            chunk_min_rows: 32,
4941            o1_chunk_prefix: false,
4942            dense_batch_declined: std::sync::atomic::AtomicBool::new(false),
4943            sliding_layers: None,
4944            no_rope_layers: None,
4945            value_blend: None,
4946            vres_e0: Vec::new(),
4947            vres_in: Vec::new(),
4948            anchor_core: None,
4949            bounded_rope: None,
4950            kv_prefix: KvPrefix::default(),
4951            last_prefill_tokens: 0,
4952            inv_freq_local: None,
4953            rotary_dim_local: None,
4954            rope_scale: 1.0,
4955            rope_scale_local: 1.0,
4956            global_attn: None,
4957            inv_freq_global: None,
4958            attn_v_norm: false,
4959            qk_norm_after_rope: false,
4960            proj_gate_sigmoid: false,
4961            final_softcap: None,
4962            head_clusters: None,
4963            attn_softcap: 0.0,
4964            graph_want_logits: false,
4965            graph_head_required: false,
4966            graph_logits: None,
4967            embryo_graph: None,
4968            embryo_wide_refused: false,
4969            graph_refused: std::sync::atomic::AtomicBool::new(false),
4970            graph_kv_id: {
4971                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
4972                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
4973            },
4974            #[cfg(test)]
4975            nll_test_fail_at: None,
4976            #[cfg(test)]
4977            nll_test_force_serial: false,
4978        }
4979    }
4980
4981    /// Enable/disable per-layer O(1) Nyström attention. Only Full
4982    /// layers are eligible (a linear layer keeps its own operator).
4983    /// Applies to generation (`generate*`/`forward_ids`): the prompt
4984    /// pass stays exact, then the state seals after prefill or at the
4985    /// deferred skeleton-safe boundary for short prompts; decode runs on
4986    /// the O(1) state. Teacher-forced scoring (`ppl_ids`) intentionally
4987    /// stays exact.
4988    pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
4989        if let Err(e) = self.try_set_o1(cfg) {
4990            tracing::error!("{e}");
4991        }
4992    }
4993
4994    /// True when the file carries a native bounded anchor
4995    /// (`arch.anchor_core`): its state is a fixed record the header
4996    /// fixes, and the post-hoc O(1) override is meaningless on it.
4997    pub fn bounded_native(&self) -> bool {
4998        self.anchor_core.is_some()
4999    }
5000
5001    /// Bytes the resident device graph holds for this pipeline's sequence:
5002    /// `(recurrent state, anchor KV/ring)`; None on the host path.
5003    pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
5004        if self.dory.is_some() {
5005            return crate::gpu::dory_kv_bytes(self.graph_kv_id).map(|b| (0, b));
5006        }
5007        crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
5008    }
5009
5010    /// Why an O(1) override is refused on this pipeline, if it is.
5011    pub fn o1_refusal(&self) -> Option<String> {
5012        self.anchor_core.as_ref().map(|ac| {
5013            format!(
5014                "--o1 / CMF_O1 refused: the anchor is native bounded \
5015                 (anchor_core kind={} window={} sink={}); the file's operator \
5016                 is executed as-is and no post-hoc Nyström overlay applies",
5017                ac.kind, ac.window, ac.sink
5018            )
5019        })
5020    }
5021
5022    /// `set_o1` that reports the refusal instead of logging it.
5023    pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
5024        // Dory runs its own stack: the far field goes on its full layers.
5025        if let Some(b) = &mut self.dory {
5026            b.0.o1 = cfg.map(|c| crate::dory::DoryO1 { m: c.m, w: c.w, sink: c.sink, seal: (c.w + c.sink + 16).max(8192) });
5027            return Ok(());
5028        }
5029        if let Some(c) = &cfg {
5030            if let Some(why) = self.o1_refusal() {
5031                self.o1_flags = Vec::new();
5032                self.o1_cfg = None;
5033                return Err(why);
5034            }
5035            if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
5036                self.o1_flags.clear();
5037                self.o1_cfg = None;
5038                return Err(format!(
5039                    "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
5040                    c.w, c.sink
5041                ));
5042            }
5043        }
5044        self.o1_flags = match &cfg {
5045            Some(c) => {
5046                let mut flags = c.layer_flags(self.num_layers);
5047                for (li, f) in flags.iter_mut().enumerate() {
5048                    // The Nyström state replaces a full-context plain
5049                    // softmax: a sliding window or a learned sink is not
5050                    // something it can represent, and a V narrower than
5051                    // the head is not what its streaming state stores.
5052                    // Those layers keep exact cache attention.
5053                    if *f
5054                        && (!matches!(
5055                            self.weights.layers[self.phys_layer(li)].attn,
5056                            AttnKind::Full { .. }
5057                        ) || self.layer_window(li).is_some()
5058                            || self.kv_cache.layers[li].sinks.is_some()
5059                            || self.layer_v_dim(li) != self.layer_geom(li).1)
5060                    {
5061                        *f = false;
5062                    }
5063                }
5064                flags
5065            }
5066            None => Vec::new(),
5067        };
5068        if let Some(c) = &cfg {
5069            let n = self.o1_flags.iter().filter(|&&f| f).count();
5070            tracing::info!(
5071                "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
5072                self.num_layers,
5073                c.m,
5074                c.w,
5075                c.sink,
5076                c.rect
5077            );
5078        }
5079        self.o1_cfg = cfg;
5080        Ok(())
5081    }
5082
5083    /// Install the file's bounded anchor: one fixed-size ring per
5084    /// `AttnKind::Bounded` layer (from the header, not per prompt) and
5085    /// the shared relative-rotation table. Must run after the RoPE setup
5086    /// (`set_rotary`, YaRN) so the table is built from the final
5087    /// `inv_freq`.
5088    pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
5089        if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
5090            return Err(format!(
5091                "anchor_core kind '{}' is not executable by this runtime",
5092                cfg.kind
5093            ));
5094        }
5095        if cfg.window == 0 {
5096            return Err("anchor_core.window must be >= 1".into());
5097        }
5098        let mut n = 0usize;
5099        for li in 0..self.num_layers {
5100            let pl = self.phys_layer(li);
5101            if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
5102                if w.window != cfg.window
5103                    || w.sink != cfg.sink
5104                    || w.group_windows != cfg.group_windows
5105                {
5106                    return Err(format!(
5107                        "layer {li}: bounded weights (window {} sink {} groups {:?}) disagree \
5108                         with anchor_core (window {} sink {} groups {:?})",
5109                        w.window, w.sink, w.group_windows, cfg.window, cfg.sink, cfg.group_windows
5110                    ));
5111                }
5112                self.kv_cache.layers[li].install_bounded(cfg.window);
5113                n += 1;
5114            }
5115        }
5116        if n == 0 {
5117            return Err("anchor_core is present but no layer executes it".into());
5118        }
5119        let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
5120        self.bounded_rope = Some(std::sync::Arc::new(rope));
5121        self.anchor_core = Some(cfg.clone());
5122        self.embryo_graph = None;
5123        tracing::info!(
5124            "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
5125            cfg.kind,
5126            cfg.window,
5127            cfg.sink,
5128            self.kv_cache
5129                .layers
5130                .iter()
5131                .map(|l| l.bounded_state_bytes())
5132                .max()
5133                .unwrap_or(0)
5134        );
5135        Ok(())
5136    }
5137
5138    /// Tag every layer's cache with its wire record kind and the model's
5139    /// operator identity (hash64 of `linear_core_identity` JSON) so the
5140    /// versioned state wire refuses a peer holding another operator.
5141    pub fn install_wire_identity(&mut self, identity: u64) {
5142        for li in 0..self.kv_cache.layers.len() {
5143            let pl = self.phys_layer(li);
5144            let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
5145                Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
5146                Some(AttnKind::Linear(_))
5147                | Some(AttnKind::LinearGdn(_))
5148                | Some(AttnKind::ShortConv(_))
5149                | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
5150                _ => crate::kv_cache::WireKind::Full,
5151            };
5152            let l = &mut self.kv_cache.layers[li];
5153            l.wire_kind = kind;
5154            l.wire_identity = identity;
5155            // A per-layer geometry (`set_attn_geometry`, Gemma's global
5156            // heads) rebuilds a layer cache with index 0: re-tag it.
5157            l.wire_layer = li as u32;
5158        }
5159    }
5160
5161    /// Forget the reuse keys (legacy `kv_history` and the bounded prefix).
5162    pub fn clear_history(&mut self) {
5163        self.kv_history.clear();
5164        self.kv_history_device = false;
5165        self.kv_prefix.clear();
5166    }
5167
5168    /// Did THIS pipeline's token graph refuse for a structural reason?
5169    /// (Per pipeline: another lane's refusal, or a new pipeline of the
5170    /// same model, never changes it.)
5171    pub fn graph_refused(&self) -> bool {
5172        self.graph_refused
5173            .load(std::sync::atomic::Ordering::Relaxed)
5174    }
5175
5176    /// Remember a structural refusal of this pipeline's token graph.
5177    pub fn mark_graph_refused(&self) {
5178        if !self
5179            .graph_refused
5180            .swap(true, std::sync::atomic::Ordering::Relaxed)
5181        {
5182            tracing::info!(
5183                "token graph: unsupported for this pipeline (seq {}) — not retrying",
5184                self.graph_kv_id
5185            );
5186        }
5187    }
5188
5189    /// Position the resident Embryo graph holds for this pipeline's
5190    /// sequence (`Some(next position)`), `None` when the device holds no
5191    /// image of it (host-owned sequence, or none started).
5192    pub fn device_sequence_position(&self) -> Option<usize> {
5193        crate::gpu::embryo_device_next_position(self.graph_kv_id)
5194    }
5195
5196    /// Is the sequence this pipeline's reuse key describes owned by the
5197    /// device path it would take now? A prefix recorded on the resident
5198    /// graph continues only there (at exactly `n`), a host prefix only on
5199    /// the host; any mismatch means the next turn re-prefills from zero.
5200    fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
5201        let dev = self.device_sequence_position();
5202        if recorded_on_device {
5203            dev == Some(n) && self.embryo_resident_wanted()
5204        } else {
5205            dev.is_none()
5206        }
5207    }
5208
5209    /// The weights under the sequence changed (a real skill switch): every
5210    /// cached state was computed by other weights. Clear the host KV /
5211    /// ring / recurrent state AND the reuse keys — a surviving `kv_prefix`
5212    /// would let the next turn "extend" a prefix the new weights never
5213    /// saw — drop the packed resident graph (it holds the old FFN
5214    /// tensors; the next build packs the live ones under a fresh id) and
5215    /// reset its device sequence.
5216    pub(crate) fn invalidate_for_weight_change(&mut self) {
5217        self.clear_sequence_state();
5218        self.embryo_graph = None;
5219    }
5220
5221    /// The CUDA hybrid arm keeps a chat snapshot (GDN states copied on the
5222    /// card): a GDN stack with exact attention, no far field, reuse on.
5223    fn gdn_snap_ok(&self) -> bool {
5224        crate::gpu::cuda_active()
5225            && self.gdn_cfg.is_some()
5226            && self.o1_cfg.is_none()
5227            && self.dory.is_none()
5228            && self.dsv41.is_none()
5229            && self.qwen4_exp.is_none()
5230            && std::env::var("CMF_KV_REUSE").as_deref() != Ok("0")
5231    }
5232
5233    /// Where this prompt's snapshot goes: its last `<|im_start|>`, the
5234    /// assistant header the next turn renders differently (a previous
5235    /// answer loses its think block there), past `from` and before the
5236    /// prompt's last row.
5237    fn gdn_snap_target(&self, input_ids: &[u32], from: usize) -> Option<usize> {
5238        if !self.gdn_snap_ok() {
5239            return None;
5240        }
5241        let id = self.tokenizer.token_to_id("<|im_start|>")?;
5242        let s = input_ids.iter().rposition(|&t| t == id)?;
5243        (s > from && s + 1 < input_ids.len()).then_some(s)
5244    }
5245
5246    /// Positions a turn takes back from the chat snapshot (0: none). The
5247    /// card's states go back to it here, all layers or none.
5248    fn gdn_snap_reuse(&mut self, input_ids: &[u32]) -> usize {
5249        let n = match &self.gdn_snap {
5250            Some(s) if !s.is_empty() && s.len() < input_ids.len() && input_ids[..s.len()] == s[..] => s.len(),
5251            _ => return 0,
5252        };
5253        if !self.gdn_snap_ok() || !crate::gpu::gdn_snapshot_restore(self.graph_kv_id, n) {
5254            self.gdn_snap = None;
5255            return 0;
5256        }
5257        n
5258    }
5259
5260    /// Prompt positions the cache already holds when `input_ids`
5261    /// strictly EXTENDS the consumed prefix (0 otherwise). Bounded-native
5262    /// models answer from the fixed-size `kv_prefix` record; everything
5263    /// else from the legacy `kv_history` vector (which the network split
5264    /// also reads and writes).
5265    fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
5266        let (n, on_device) = if self.bounded_native() {
5267            (
5268                self.kv_prefix.extension(input_ids),
5269                self.kv_prefix.on_device(),
5270            )
5271        } else {
5272            let h = &self.kv_history;
5273            if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
5274                (h.len(), self.kv_history_device)
5275            } else if !h.is_empty() && !self.kv_history_device {
5276                (self.rollback_prefix_len(input_ids), false)
5277            } else {
5278                (0, false)
5279            }
5280        };
5281        // The owner tag: the state the key describes must live where this
5282        // turn will continue it (R4) — else a fresh sequence.
5283        if n > 0 && !self.prefix_owner_matches(n, on_device) {
5284            tracing::warn!(
5285                "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
5286                 the device now holds {:?} — re-prefilling from zero",
5287                if on_device { "resident device" } else { "host" },
5288                self.device_sequence_position()
5289            );
5290            return 0;
5291        }
5292        n
5293    }
5294
5295    /// Positions a turn whose ids DIVERGE from the consumed prefix keeps:
5296    /// their longest common prefix, when every layer can drop its rows past
5297    /// it exactly — full attention whose f32 rows the host owns (no wgpu
5298    /// mirror ahead of the host, no recurrent, O(1) or bounded state) and
5299    /// every trimmed sliding tail still holding the window the query at the
5300    /// cut reads. A chat app re-renders the previous answer through the
5301    /// template (a thinking model's history drops its `<think>` block), so a
5302    /// conversation almost never extends token for token: extension-only
5303    /// reuse re-prefilled the whole history every turn. One id at least is
5304    /// left to prefill. `CMF_KV_ROLLBACK=0` keeps extension-only reuse.
5305    fn rollback_prefix_len(&self, input_ids: &[u32]) -> usize {
5306        if std::env::var("CMF_KV_ROLLBACK").as_deref() == Ok("0")
5307            || self.device_sequence_position().is_some()
5308        {
5309            return 0;
5310        }
5311        let h = &self.kv_history;
5312        let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
5313        let n = lcp.min(input_ids.len().saturating_sub(1));
5314        // Stacks that keep their own layers (qwen4, DSV4) leave the generic
5315        // ones empty: nothing here can roll back, the prompt is read again.
5316        if n == 0
5317            || self.kv_cache.layers.len() < self.num_layers
5318            || self.weights.layers.len() < self.num_layers
5319        {
5320            return 0;
5321        }
5322        let kv_id = self.graph_kv_id;
5323        for li in 0..self.num_layers {
5324            let l = &self.kv_cache.layers[li];
5325            let tail_ok = l.base() == 0
5326                || self
5327                    .layer_window(li)
5328                    .is_some_and(|w| l.base() + w <= n + 1);
5329            if !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. })
5330                || l.o1.is_some()
5331                || l.bounded.is_some()
5332                || !l.linear_state.is_empty()
5333                || l.mode != crate::kv_cache::KvMode::F32
5334                || l.pos_len() != h.len()
5335                || !tail_ok
5336                || crate::gpu::graph_kv_stored(kv_id, li).is_some()
5337                || crate::gpu::graph_state_resident(kv_id, li)
5338            {
5339                return 0;
5340            }
5341        }
5342        n
5343    }
5344
5345    /// Public view of the prefix-reuse decision for `input_ids` (positions
5346    /// the next `generate*` would take from the cache; 0 = fresh sequence).
5347    pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
5348        self.cached_prefix_len(input_ids)
5349    }
5350
5351    /// Record the forwarded prefix as the next turn's reuse key. A
5352    /// bounded-native model extends the rolling record (`reused` = the
5353    /// positions this turn found cached); others keep the literal vector.
5354    /// Either way the key carries its OWNER: the resident device graph
5355    /// (it holds an image of this sequence) or the host.
5356    fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
5357        let on_device = self.device_sequence_position().is_some();
5358        if self.bounded_native() {
5359            let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
5360            let prev_device = self.kv_prefix.on_device();
5361            self.kv_history.clear();
5362            self.kv_history_device = false;
5363            if keep && prev_device == on_device {
5364                self.kv_prefix.extend(&consumed[reused..]);
5365            } else {
5366                self.kv_prefix.set(consumed);
5367            }
5368            self.kv_prefix.set_on_device(on_device);
5369        } else {
5370            self.kv_history = consumed.to_vec();
5371            self.kv_history_device = on_device;
5372        }
5373    }
5374
5375    /// True when at least one layer runs the O(1) kernel.
5376    pub fn o1_active(&self) -> bool {
5377        self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
5378    }
5379
5380    /// Whether generation's prompt ingest is routed through the whole-token
5381    /// graph.  The bench uses this to label the measured generation prefill
5382    /// honestly; keep the predicate in Pipeline so CLI labels cannot drift
5383    /// from the production route.
5384    /// Positions per batched-graph submit for the prompt: `CMF_BATCH_K`
5385    /// when set (0 = one position at a time through the token graph),
5386    /// otherwise 128 (32 for a MoE stack) on a discrete card whose prompt
5387    /// takes the graph route.
5388    /// The batched graph read a 2048-token prompt at 53 tok/s against 28.5
5389    /// one position at a time on an RTX PRO 4000 (Qwen3.8-27B q4tp: TTFT
5390    /// 39 s against 72), and its states are the speculative verify's,
5391    /// measured identical to the plain path. macOS keeps its own arm.
5392    /// 128, not 32: the q4tp prefill GEMM tiles 64 batch rows, so a
5393    /// 32-row chunk left half of every tile idle. Qwen3.8-27B q4tp on an
5394    /// RTX 3090, 1000-token prompt: 51 tok/s at 32, 82 at 64, 95 at 128,
5395    /// 97 at 256; greedy output byte-identical to the 32-row chunks
5396    /// (every GEMM row and the k-looped recurrences are per position).
5397    /// A dense sliding-window stack whose batched prefill attends with the
5398    /// query-tiled kernels (the Spark-X2.6 class on a discrete wgpu card).
5399    #[cfg(not(target_os = "macos"))]
5400    fn tiled_sliding_batch(&self) -> bool {
5401        (self.sliding_layers.is_some() || self.swa.is_some())
5402            && !self.weights.layers.iter().any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
5403            && crate::gpu::batched_attend_tiled()
5404    }
5405
5406    pub fn generation_batch_k(&self) -> usize {
5407        if let Some(k) = std::env::var("CMF_BATCH_K")
5408            .ok()
5409            .and_then(|v| v.parse::<usize>().ok())
5410        {
5411            return k;
5412        }
5413        // Gemma-4 (`g4_batch_prefill`): 128-row chunks, the GEMM tile's
5414        // two 64-row halves full.
5415        if self.g4_batch_prefill() {
5416            return 128;
5417        }
5418        #[cfg(target_os = "macos")]
5419        if self.o1_active() && self.o1_on_graph() && crate::gpu::q1_force() && crate::gpu::enabled_here() {
5420            return if crate::gpu_metal::ane::wanted() { 512 } else { 128 };
5421        }
5422        // The sealed O(1) suffix of a bounded prompt (`CMF_O1_PREFILL`) on
5423        // the device route rides the batch graph, whose O(1) kernels take
5424        // the chunk in 64-row sub-chunks (`o1_batch`); 128 as the exact
5425        // prompt below. Without it the suffix went one position at a time.
5426        #[cfg(not(target_os = "macos"))]
5427        if self.o1_active()
5428            && self.o1_on_graph()
5429            && crate::gpu::enabled_here()
5430            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
5431        {
5432            if let Some(k) = crate::gpu::cuda_batch_rows() {
5433                return k;
5434            }
5435            return if self.tiled_sliding_batch() { 512 } else { 128 };
5436        }
5437        #[cfg(not(target_os = "macos"))]
5438        if self.graph_prefill_preferred() && !self.o1_active() {
5439            // The CUDA batch graph expands each weight once a chunk and
5440            // multiplies with cuBLAS: its own, larger chunk (`CMF_CUDA_BATCH`).
5441            if let Some(k) = crate::gpu::cuda_batch_rows() {
5442                return k;
5443            }
5444            // A dense sliding-window stack on the tiled whole-run attend:
5445            // 512 rows a submit (Spark-X2.6 q4tp on an RTX 2000 Ada, 4096
5446            // tokens: 288 tok/s at 128, 317 at 512, the same greedy text;
5447            // the tile holds 16 tokens, and a chunk is read against its K/V
5448            // once per sub-run of whole tiles).
5449            if self.tiled_sliding_batch() {
5450                return 512;
5451            }
5452            // 128 is measured on the dense hybrids; MoE stacks other than
5453            // the two expert codecs below keep the 32 their expert-grouped
5454            // prefill was measured at.
5455            let moe = self
5456                .weights
5457                .layers
5458                .iter()
5459                .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
5460            // The dense GDN hybrids (Qwen3.5 / 3.6 / 3.8) on the tiled
5461            // attend: 512 rows a submit, each weight unpacked to its f16
5462            // plane once a chunk. Qwen3.5-4B q4tp on an RTX A5000, prompt
5463            // tok/s at 4 000 / 16 000 / 32 000 tokens: 1223 / 1122 / 977
5464            // at 128 rows, 1669 / 1550 / - at 256, 1833 / 1897 / 1539 at 512.
5465            if !moe && self.graph_gdn_layer_count() > 0 && crate::gpu::batched_attend_tiled() {
5466                return 512;
5467            }
5468            // q8_2f experts (moe_q82's grouped prefill) read each expert
5469            // once per group of up to 16 of its entries, so the chunk that
5470            // fills those groups is the faster ingest: Mellum2.1 q8_2f on an
5471            // RTX 3090, 4000-token prompt, 503 tok/s at 64 rows, 542 at 128
5472            // (and 542 at 256).
5473            let q82 = crate::gpu::moe_q82_graph_on()
5474                && self.weights.layers.iter().any(|lw| {
5475                    matches!(&lw.ffn, FfnKind::Moe(m)
5476                        if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
5477                });
5478            // q4tp experts: the grouped GEMM reads every routed expert once
5479            // per chunk, and 32 rows (256 entries over 256 experts, about
5480            // one row each) re-read nearly the whole expert set every 32
5481            // tokens. Apex-35B q4tp on an A40, 4096-token prompt, MTP off:
5482            // 295 tok/s at 32 rows, 357 at 64, 385 at 128, 401 at 256,
5483            // 396 at 512.
5484            let q4tp_moe = self.weights.layers.iter().any(|lw| {
5485                matches!(&lw.ffn, FfnKind::Moe(m)
5486                    if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q4tp().is_some()))
5487            });
5488            return if q82 {
5489                128
5490            } else if q4tp_moe || self.onevision2_prefill() {
5491                256
5492            } else if moe {
5493                32
5494            } else {
5495                128
5496            };
5497        }
5498        0
5499    }
5500
5501    pub fn generation_graph_prefill(&self) -> bool {
5502        let graph = self.graph_prefill_preferred();
5503        // On wgpu, an active MTP head now consumes the trunk's graph batches
5504        // and warms its own block from those returned rows.  The selected
5505        // generation measurement is therefore the batched path, even though
5506        // the underlying GDN model still satisfies the graph-prefill
5507        // predicate.  Keep the CLI label tied to the actual route.  Native
5508        // Metal has a separate prefill-batch arm and retains its historical
5509        // label here.
5510        // A batched prompt (`generation_batch_k` > 0) is the batched graph
5511        // for every model on the graph route, not only those with an MTP
5512        // head — the label follows the route.
5513        #[cfg(not(target_os = "macos"))]
5514        if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
5515            return false;
5516        }
5517        graph
5518    }
5519
5520    /// Device-side O(1) mirrors currently uploaded for this pipeline's
5521    /// sequence.  The count/bytes are zero before seal or after a fresh
5522    /// reset; callers use this to distinguish logical host state from the
5523    /// GPU allocation that actually serves decode.
5524    pub fn o1_device_stats(&self) -> (usize, u64) {
5525        crate::gpu::o1_device_stats(self.graph_kv_id)
5526    }
5527
5528    /// Arm query collection on the o1 layers (fresh prompt pass).
5529    /// Reset the o1 layers to Collecting for a fresh sequence. Pub for the
5530    /// network split: each side runs the o1 lifecycle over ITS OWN layers
5531    /// (begin before prefill, seal at the prefill barrier).
5532    pub fn o1_begin(&mut self) {
5533        self.o1_begin_with_prefix(None);
5534    }
5535
5536    /// Arm collection and optionally request a positive calibration prefix.
5537    /// The effective barrier is always at least the skeleton-safe floor, so
5538    /// a short requested prefix cannot create an exact-only runtime state.
5539    pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
5540        if let Some(c) = &self.o1_cfg {
5541            let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
5542            let boundary = requested_prefix.map(|p| {
5543                p.max(
5544                    crate::nystrom::o1_deferred_boundary(w, sink)
5545                        .expect("o1 config boundary validated in set_o1"),
5546                )
5547            });
5548            // Exact rows are chosen across every far-field layer at the boundary:
5549            // a layer that reached it first must not seal without them.
5550            let await_keep = boundary.is_some() && self.o1_exact_cfg().is_some();
5551            for (li, &f) in self.o1_flags.iter().enumerate() {
5552                if f {
5553                    let cache = &mut self.kv_cache.layers[li];
5554                    cache.o1_begin_with_boundary(m, w, sink, rect, boundary);
5555                    cache.o1_await_keep = await_keep;
5556                }
5557            }
5558        }
5559    }
5560
5561    /// Effective deferred boundary for a positive prefix request.
5562    fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
5563        self.o1_cfg.as_ref().and_then(|c| {
5564            crate::nystrom::o1_deferred_boundary(c.w, c.sink)
5565                .map(|floor| requested_prefix.max(floor))
5566        })
5567    }
5568
5569    fn o1_note_transition(&mut self) {
5570        // Drain every layer's one-shot bit before publishing one pipeline
5571        // epoch. `any()` would short-circuit on the first layer and leak the
5572        // remaining bits into later forwards, causing one epoch per layer.
5573        let mut transitioned = false;
5574        for (li, &flagged) in self.o1_flags.iter().enumerate() {
5575            if flagged {
5576                transitioned |= self.kv_cache.layers[li].take_o1_transition();
5577            }
5578        }
5579        if transitioned {
5580            self.o1_epoch = self.o1_epoch.wrapping_add(1);
5581        }
5582    }
5583
5584    fn o1_pending(&self) -> bool {
5585        self.o1_flags.iter().enumerate().any(|(li, &f)| {
5586            f && self.kv_cache.layers[li].seq_len > 0
5587                && self.kv_cache.layers[li].o1_pending_boundary().is_some()
5588        })
5589    }
5590
5591    fn o1_fail(&mut self, err: String) {
5592        tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
5593        self.clear_sequence_state();
5594        self.graph_failed
5595            .store(true, std::sync::atomic::Ordering::Relaxed);
5596        self.cancel
5597            .store(true, std::sync::atomic::Ordering::Relaxed);
5598    }
5599
5600    /// Seal participating layers while retaining the exact state when the
5601    /// prompt is below the deferred boundary. A split worker may have
5602    /// collecting layers outside its owned span; zero-depth layers remain
5603    /// armed and are intentionally skipped until their peer runs them.
5604    pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
5605        if self.o1_cfg.is_none() {
5606            return Ok(false);
5607        }
5608        self.o1_choose_exact_rows();
5609        let (t_seal, mut sealed_now, epoch) = (std::time::Instant::now(), 0usize, self.o1_epoch);
5610        let mut todo: Vec<(usize, usize)> = Vec::new();
5611        for li in 0..self.num_layers {
5612            if !self.o1_flags.get(li).copied().unwrap_or(false) {
5613                continue;
5614            }
5615            if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
5616                return Err(err);
5617            }
5618            if self.kv_cache.layers[li].seq_len == 0 {
5619                continue;
5620            }
5621            todo.push((li, self.layer_num_heads(li)));
5622        }
5623        let participating = !todo.is_empty();
5624        // A layer's seal depends on that layer's rows alone, and its groups
5625        // already seal on their own threads: a few layers go at a time, as
5626        // many as the cores carry (`CMF_O1_SEAL_PAR=N` sets the number). Each
5627        // holds its queries per group while it seals, so not all at once —
5628        // and one after another on a Mac, where that memory is the model's
5629        // too.
5630        let par = std::env::var("CMF_O1_SEAL_PAR")
5631            .ok()
5632            .and_then(|v| v.parse::<usize>().ok())
5633            .unwrap_or_else(|| {
5634                if cfg!(target_os = "macos") {
5635                    return 1;
5636                }
5637                let cores = std::thread::available_parallelism().map_or(1, |n| n.get());
5638                let groups = todo.first().map_or(1, |&(li, _)| self.kv_cache.layers[li].num_kv_heads.max(1));
5639                cores.div_ceil(groups).clamp(1, 3)
5640            })
5641            .max(1);
5642        let mut slots: Vec<Option<&mut crate::kv_cache::LayerKvCache>> = self.kv_cache.layers.iter_mut().map(Some).collect();
5643        let mut jobs: Vec<(&mut crate::kv_cache::LayerKvCache, usize)> =
5644            todo.iter().filter_map(|&(li, nh)| slots[li].take().map(|c| (c, nh))).collect();
5645        for run in jobs.chunks_mut(par) {
5646            let done: Vec<Result<bool, String>> = if run.len() == 1 {
5647                let (c, nh) = &mut run[0];
5648                let was = c.o1_sealed();
5649                vec![c.o1_seal_checked(*nh).map(|ok| ok && !was)]
5650            } else {
5651                std::thread::scope(|s| {
5652                    let hs: Vec<_> = run
5653                        .iter_mut()
5654                        .map(|(c, nh)| {
5655                            let nh = *nh;
5656                            s.spawn(move || {
5657                                let was = c.o1_sealed();
5658                                c.o1_seal_checked(nh).map(|ok| ok && !was)
5659                            })
5660                        })
5661                        .collect();
5662                    hs.into_iter()
5663                        .map(|h| h.join().unwrap_or_else(|_| Err("o1 seal: a layer's thread panicked".into())))
5664                        .collect()
5665                })
5666            };
5667            for r in done {
5668                sealed_now += r? as usize;
5669            }
5670        }
5671        drop(jobs);
5672        drop(slots);
5673        self.o1_note_transition();
5674        if self.o1_epoch != epoch && sealed_now > 0 {
5675            tracing::info!("o1: {sealed_now} layer(s) sealed in {:.2} s", t_seal.elapsed().as_secs_f64());
5676        }
5677        for li in 0..self.num_layers {
5678            if self.o1_flags.get(li).copied().unwrap_or(false) {
5679                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
5680                    return Err(err);
5681                }
5682            }
5683        }
5684        Ok(participating
5685            && (0..self.num_layers).all(|li| {
5686                !self.o1_flags.get(li).copied().unwrap_or(false)
5687                    || self.kv_cache.layers[li].seq_len == 0
5688                    || self.kv_cache.layers[li].o1_sealed()
5689            }))
5690    }
5691
5692    /// A sealed chunk's rows join the archive.
5693    fn o1_archive_chunk(&self, a: &mut O1Archive, pos: usize, bk: usize) -> Result<(), String> {
5694        let got = crate::gpu::o1_acap_take().ok_or("O(1) archive: readback failed")?;
5695        let qrow = a.nh * a.hd;
5696        let stride = a.stride;
5697        // a backend may hand back the kept query rows alone (CUDA)
5698        let kept = (pos..pos + bk).filter(|p| p % stride == 0).count();
5699        for (gl, q, k, v) in got {
5700            let Some(l) = a.layers.iter_mut().find(|l| l.li == gl) else { continue };
5701            l.k.push(&k)?;
5702            l.v.push(&v)?;
5703            if q.len() == kept * qrow && kept != bk {
5704                l.q.push(&q)?;
5705                continue;
5706            }
5707            if q.len() != bk * qrow {
5708                return Err(format!("O(1) archive: layer {gl} handed back {} query values", q.len()));
5709            }
5710            for p in pos..pos + bk {
5711                if p % stride == 0 {
5712                    l.q.push(&q[(p - pos) * qrow..(p - pos + 1) * qrow])?;
5713                }
5714            }
5715        }
5716        a.rows = pos + bk;
5717        Ok(())
5718    }
5719
5720    /// The far field built anew over all the rows read so far, the chosen ones out of it.
5721    /// One layer at a time, so RAM holds one layer's rows.
5722    fn o1_seal_archive(&mut self, a: O1Archive) -> Result<bool, String> {
5723        let t0 = std::time::Instant::now();
5724        let (rows, stride, nk, hd) = (a.rows, a.stride, a.nk, a.hd);
5725        let mut layers = Vec::with_capacity(a.layers.len());
5726        for l in a.layers {
5727            layers.push((l.li, l.k.freeze()?, l.v.freeze()?, l.q.freeze()?));
5728        }
5729        let keep = match (self.o1_exact_cfg().cloned(), self.o1_cfg.clone()) {
5730            (Some(x), Some(c)) => {
5731                let views: Vec<crate::dory::FlatRows> = layers.iter().map(|l| crate::dory::FlatRows { k: l.1.as_slice(), v: l.2.as_slice(), nk, hd }).collect();
5732                let refs: Vec<&dyn crate::dory::LayerRows> = views.iter().map(|v| v as &dyn crate::dory::LayerRows).collect();
5733                crate::dory::pick_exact_rows(&x, &refs, rows, nk, hd, c.m, c.sink, rows.saturating_sub(c.w))
5734            }
5735            _ => Vec::new(),
5736        };
5737        let t_pick = t0.elapsed().as_secs_f64();
5738        let mut all = !layers.is_empty();
5739        let prof = std::env::var("CMF_PREFILL_PROF").is_ok();
5740        let t_layers = std::time::Instant::now();
5741        // a few layers at a time (`CMF_O1_SEAL_PAR`), each on its own thread: a
5742        // layer's rows into its cache, then its seal (its groups on threads of
5743        // their own); RAM holds those layers' rows twice meanwhile
5744        let par = std::env::var("CMF_O1_SEAL_PAR")
5745            .ok()
5746            .and_then(|v| v.parse::<usize>().ok())
5747            .unwrap_or_else(|| std::thread::available_parallelism().map_or(1, |p| p.get()).div_ceil(8).clamp(1, 4));
5748        let heads: Vec<usize> = (0..self.kv_cache.layers.len()).map(|li| self.layer_num_heads(li)).collect();
5749        let mut by_li: Vec<Option<_>> = (0..self.kv_cache.layers.len()).map(|_| None).collect();
5750        for (li, k, v, q) in layers {
5751            if li < by_li.len() {
5752                by_li[li] = Some((k, v, q));
5753            }
5754        }
5755        let mut jobs: Vec<(usize, &mut crate::kv_cache::LayerKvCache, _)> = Vec::new();
5756        for (li, cache) in self.kv_cache.layers.iter_mut().enumerate() {
5757            if let Some(d) = by_li[li].take() {
5758                jobs.push((li, cache, d));
5759            }
5760        }
5761        let keep_ref = &keep;
5762        let heads = &heads;
5763        let results: Vec<Result<bool, String>> = std::thread::scope(|sc| {
5764            let mut out = Vec::new();
5765            let mut jobs = jobs.into_iter();
5766            loop {
5767                let wave: Vec<_> = jobs.by_ref().take(par.max(1)).collect();
5768                if wave.is_empty() {
5769                    break;
5770                }
5771                let hs: Vec<_> = wave
5772                    .into_iter()
5773                    .map(|(li, cache, (k, v, q))| {
5774                        sc.spawn(move || -> Result<bool, String> {
5775                            if !cache.o1_recollect(k.as_slice(), v.as_slice(), q.as_slice().to_vec(), stride, rows) {
5776                                return Err(format!("O(1) archive: layer {li} is not sealed"));
5777                            }
5778                            drop((k, v, q));
5779                            cache.o1_keep = keep_ref.clone();
5780                            cache.o1_seal_checked(heads[li])
5781                        })
5782                    })
5783                    .collect();
5784                for h in hs {
5785                    out.push(h.join().unwrap_or_else(|_| Err("O(1) archive: a layer's seal panicked".into())));
5786                }
5787            }
5788            out
5789        });
5790        for r in results {
5791            all &= r?;
5792        }
5793        if prof {
5794            eprintln!("o1 archive seal: {} layers {par} at a time in {:.2} s", by_li.len(), t_layers.elapsed().as_secs_f64());
5795        }
5796        self.o1_note_transition();
5797        tracing::info!(
5798            "o1: far field rebuilt over {rows} rows, {} kept exact, chosen in {t_pick:.2} s, {:.2} s in all",
5799            keep.len(),
5800            t0.elapsed().as_secs_f64()
5801        );
5802        Ok(all)
5803    }
5804
5805    /// A sealed prompt chunk on the device: wgpu's batch graph, or Metal's rows graph.
5806    fn o1_prompt_chunk(&mut self, hiddens: &mut Vec<f32>, positions: &[usize], bk: usize) -> crate::gpu::BatchGraphOutcome {
5807        #[cfg(target_os = "macos")]
5808        if crate::gpu::q1_force() && self.o1_active() {
5809            return match self.prefill_hiddens_metal(hiddens.clone(), positions[0], None) {
5810                MetalPrefillOutcome::Completed(h) => {
5811                    *hiddens = h;
5812                    crate::gpu::BatchGraphOutcome::Completed
5813                }
5814                MetalPrefillOutcome::Declined => crate::gpu::BatchGraphOutcome::Declined,
5815                MetalPrefillOutcome::Failed => crate::gpu::BatchGraphOutcome::Failed,
5816            };
5817        }
5818        self.try_batch_graph_wgpu(hiddens, positions, bk, None)
5819    }
5820
5821    /// `o1_prefix_on_graph` on Metal: the prompt's exact part on the rows graph, which leaves its
5822    /// rows in the host cache and the mirror. Queries come from the graph, one row in `QT_STRIDE`.
5823    #[cfg(target_os = "macos")]
5824    fn o1_prefix_on_metal(
5825        &mut self,
5826        ids: &[u32],
5827        chunk: usize,
5828        mut arch: Option<&mut O1Archive>,
5829    ) -> Result<Option<Vec<f32>>, String> {
5830        const QT_STRIDE: usize = 8;
5831        let o1_layers: Vec<usize> = (0..self.num_layers)
5832            .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
5833            .collect();
5834        let Some(&first) = o1_layers.first() else {
5835            return Ok(None);
5836        };
5837        let (nkv, hd) = {
5838            let c = &self.kv_cache.layers[first];
5839            (c.num_kv_heads, c.head_dim)
5840        };
5841        let nh = self.num_heads;
5842        if chunk == 0
5843            || o1_layers.iter().any(|&li| {
5844                let c = &self.kv_cache.layers[li];
5845                self.layer_num_heads(li) != nh
5846                    || (c.num_kv_heads, c.head_dim) != (nkv, hd)
5847                    || c.seq_len != 0
5848                    || c.o1_pending_boundary().is_none()
5849            })
5850        {
5851            return Ok(None);
5852        }
5853        let hs = self.hidden_size;
5854        let n = ids.len();
5855        let mut last = vec![0f32; hs];
5856        let qrow = nh * hd;
5857        let mut qs: Vec<Vec<f32>> = vec![Vec::new(); o1_layers.len()];
5858        crate::gpu_metal::o1c::qcap_begin(&o1_layers);
5859        for &li in &o1_layers {
5860            self.kv_cache.layers[li].o1_set_q_stride(QT_STRIDE);
5861        }
5862        let mut pos = 0;
5863        // Layers the rows graph does not hold (Spark-X2.6: GELU, per-head
5864        // gates, sliding windows) read the prefix on the chunk graph, which
5865        // keeps the collecting layers' queries too. Without it the prefix
5866        // went to the host walk: 14 000 tokens in 155 s against 53 s exact.
5867        let mut on_chunks = false;
5868        let mut step = chunk;
5869        while pos < n {
5870            let end = (pos + step).min(n);
5871            let bk = end - pos;
5872            let mut hiddens = vec![0f32; bk * hs];
5873            for (j, &id) in ids[pos..end].iter().enumerate() {
5874                hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5875            }
5876            let rows = if on_chunks {
5877                MetalPrefillOutcome::Declined
5878            } else {
5879                self.prefill_hiddens_metal(hiddens.clone(), pos, None)
5880            };
5881            match rows {
5882                MetalPrefillOutcome::Completed(h) => last.copy_from_slice(&h[(bk - 1) * hs..]),
5883                MetalPrefillOutcome::Declined if pos == 0 || on_chunks => {
5884                    if !on_chunks {
5885                        on_chunks = true;
5886                        // The chunk graph's own prompt chunk.
5887                        let want = self.prefill_chunk().max(chunk);
5888                        if want != step {
5889                            step = want;
5890                            continue;
5891                        }
5892                    }
5893                    self.o1_chunk_prefix = true;
5894                    let got = self.chunk_run_gpu(0, &mut hiddens, bk, pos, None, self.num_layers);
5895                    self.o1_chunk_prefix = false;
5896                    if got != self.num_layers {
5897                        crate::gpu_metal::o1c::cap_end();
5898                        if pos == 0 && got == 0 {
5899                            for &li in &o1_layers {
5900                                self.kv_cache.layers[li].o1_set_q_stride(1);
5901                            }
5902                            return Ok(None);
5903                        }
5904                        return Err("O(1) prefix: the Metal chunk graph stopped after admission".into());
5905                    }
5906                    // Every layer appended the chunk: sliding layers keep their tails only.
5907                    self.swa_trim_tails();
5908                    last.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5909                }
5910                _ => {
5911                    crate::gpu_metal::o1c::cap_end();
5912                    return Err("O(1) prefix: the Metal rows graph stopped after admission".into());
5913                }
5914            }
5915            for (li, q, _, _) in crate::gpu_metal::o1c::cap_take() {
5916                let Some(i) = o1_layers.iter().position(|&l| l == li) else { continue };
5917                if q.len() != bk * qrow {
5918                    crate::gpu_metal::o1c::cap_end();
5919                    return Err(format!("O(1) prefix: {} query values, expected {}", q.len(), bk * qrow));
5920                }
5921                for p in pos..end {
5922                    if p % QT_STRIDE == 0 {
5923                        qs[i].extend_from_slice(&q[(p - pos) * qrow..(p - pos + 1) * qrow]);
5924                    }
5925                }
5926            }
5927            pos = end;
5928        }
5929        crate::gpu_metal::o1c::cap_end();
5930        let row = nkv * hd;
5931        for (&li, q) in o1_layers.iter().zip(qs) {
5932            if q.len() != n.div_ceil(QT_STRIDE) * qrow {
5933                return Err(format!("O(1) prefix: layer {li} kept {} query values", q.len()));
5934            }
5935            if let Some(a) = arch.as_deref_mut() {
5936                let (mut k, mut v) = (vec![0f32; n * row], vec![0f32; n * row]);
5937                if !crate::gpu_metal::kv_mirror_read_rows(self.graph_kv_id, li, nkv, hd, 0, n, &mut k, &mut v) {
5938                    return Err("O(1) prefix: K/V readback failed".into());
5939                }
5940                let dir = a.dir.clone();
5941                let mut layer = O1ArchLayer {
5942                    li,
5943                    k: ArchStore::new(dir.as_deref(), format!("{li}-k"))?,
5944                    v: ArchStore::new(dir.as_deref(), format!("{li}-v"))?,
5945                    q: ArchStore::new(dir.as_deref(), format!("{li}-q"))?,
5946                };
5947                layer.k.push(&k)?;
5948                layer.v.push(&v)?;
5949                layer.q.push(&q)?;
5950                a.layers.push(layer);
5951                a.rows = n;
5952                a.stride = QT_STRIDE;
5953                a.nh = nh;
5954                a.nk = nkv;
5955                a.hd = hd;
5956            }
5957            self.kv_cache.layers[li].o1_set_q_trace(q);
5958        }
5959        Ok(Some(last))
5960    }
5961
5962    fn o1_exact_cfg(&self) -> Option<&crate::dory::ExactRows> {
5963        static EXACT: std::sync::OnceLock<Option<crate::dory::ExactRows>> = std::sync::OnceLock::new();
5964        EXACT
5965            .get_or_init(|| crate::dory::exact_from_var("CMF_O1_EXACT"))
5966            .as_ref()
5967            .or(self.o1_turbo.as_ref().map(|t| &t.exact))
5968    }
5969
5970    pub fn is_far_turbo(&self) -> bool {
5971        self.o1_turbo.is_some() && self.o1_graph_backend()
5972    }
5973
5974    /// A backend with the far field's prompt path and stages.
5975    fn o1_graph_backend(&self) -> bool {
5976        crate::gpu::wgpu_active() || crate::gpu::cuda_active() || crate::gpu::backend_name() == "metal"
5977    }
5978
5979    /// The whole prompt is read exact on the card, and one seal after it
5980    /// chooses among all of its rows, with the question's queries in the
5981    /// trace. The CUDA graph's way where it has no sealed-chunk kernels (the
5982    /// dense arm); its hybrid arm reads a prompt past the first seal in chunks
5983    /// as the wgpu graph does, so memory stays at the seal's
5984    /// (`CMF_O1_CHUNKS=0` reads it whole there too). `CMF_O1_WHOLE=1` takes
5985    /// either graph the whole way, so the two can be compared row for row.
5986    #[cfg(not(target_os = "macos"))]
5987    fn o1_whole_prompt(&self) -> bool {
5988        if std::env::var("CMF_O1_WHOLE").as_deref() == Ok("1") {
5989            return true;
5990        }
5991        crate::gpu::cuda_active() && (self.graph_gdn_layer_count() == 0 || std::env::var("CMF_O1_CHUNKS").as_deref() == Ok("0"))
5992    }
5993
5994    pub fn far_turbo_seal(&self) -> usize {
5995        self.o1_turbo.as_ref().map_or(usize::MAX, |t| t.seal)
5996    }
5997
5998    /// The draft head on top of a far field: a verify's rows attend, and the state moves
5999    /// over the accepted ones only. On the wgpu graph; `CMF_O1_SPEC=0` turns it off.
6000    fn o1_spec(&self) -> bool {
6001        self.o1_active()
6002            && self.o1_on_graph()
6003            && self.o1_graph_backend()
6004            && (self.o1_turbo.is_some() || std::env::var("CMF_O1_SPEC").as_deref() == Ok("1"))
6005            && std::env::var("CMF_O1_SPEC").as_deref() != Ok("0")
6006    }
6007
6008    /// The far field rides the device graph: asked for, or a turbo file's way.
6009    fn o1_on_graph(&self) -> bool {
6010        self.o1_turbo.is_some() || std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6011    }
6012
6013    /// A turbo file takes the far field for a long prompt and leaves it for a short one,
6014    /// where exact attention and speculation are the faster way.
6015    fn o1_turbo_gate(&mut self, prompt_rows: usize) {
6016        // Off the wgpu and Metal graphs the far field would run on the host, so a turbo file
6017        // runs as the plain file it holds.
6018        if !self.is_far_turbo() {
6019            return;
6020        }
6021        let Some(t) = self.o1_turbo.clone() else { return };
6022        let want = prompt_rows >= t.seal;
6023        if want != self.o1_active() {
6024            self.clear_sequence_state();
6025            if let Err(e) = self.try_set_o1(want.then(|| t.cfg.clone())) {
6026                tracing::warn!("turbo file: the far field is off: {e}");
6027                self.o1_turbo = None;
6028            }
6029        }
6030    }
6031
6032    /// The rows this seal keeps exact (`CMF_O1_EXACT=FILE:rows`), the same in every layer.
6033    fn o1_choose_exact_rows(&mut self) {
6034        let Some(x) = self.o1_exact_cfg().cloned() else { return };
6035        let Some(c) = self.o1_cfg.clone() else { return };
6036        let layers: Vec<usize> = (0..self.num_layers)
6037            .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false) && self.kv_cache.layers[li].seq_len > 0 && self.kv_cache.layers[li].o1_is_collecting())
6038            .collect();
6039        let Some(&first) = layers.first() else { return };
6040        let rows = self.kv_cache.layers[first].seq_len;
6041        let floor = crate::nystrom::o1_deferred_boundary(c.w, c.sink).unwrap_or(usize::MAX);
6042        let target = self.kv_cache.layers[first].o1_pending_boundary().unwrap_or(floor).max(floor);
6043        if rows < target || layers.iter().any(|&li| self.kv_cache.layers[li].seq_len != rows) {
6044            return;
6045        }
6046        let t0 = std::time::Instant::now();
6047        let (nk, hd) = (self.kv_cache.layers[first].num_kv_heads, self.kv_cache.layers[first].head_dim);
6048        let keep = {
6049            let views: Vec<_> = layers.iter().map(|&li| self.kv_cache.layers[li].group_rows()).collect();
6050            let refs: Vec<&dyn crate::dory::LayerRows> = views.iter().map(|v| v as &dyn crate::dory::LayerRows).collect();
6051            crate::dory::pick_exact_rows(&x, &refs, rows, nk, hd, c.m, c.sink, rows.saturating_sub(c.w))
6052        };
6053        tracing::info!("o1: {} exact rows of {rows} chosen in {:.2} s", keep.len(), t0.elapsed().as_secs_f64());
6054        for &li in &layers {
6055            self.kv_cache.layers[li].o1_keep = keep.clone();
6056        }
6057    }
6058
6059    /// Complete a deferred boundary after a full position/span forward.
6060    /// This is the pipeline owner for epoch publication and failure cleanup.
6061    fn o1_progress(&mut self) {
6062        if !self.o1_active() {
6063            return;
6064        }
6065        for li in 0..self.num_layers {
6066            if self.o1_flags.get(li).copied().unwrap_or(false) {
6067                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
6068                    self.o1_fail(err);
6069                    return;
6070                }
6071            }
6072        }
6073        // A qwen_attention row can seal in the middle of a complete layer
6074        // walk. Consume its transition even though the pending boundary has
6075        // already disappeared from the cache.
6076        self.o1_note_transition();
6077        if !self.o1_pending() {
6078            return;
6079        }
6080        if let Err(err) = self.o1_seal_checked() {
6081            self.o1_fail(err);
6082        }
6083    }
6084
6085    /// Turn a deferred O(1) failure raised by a hidden-only forward into the
6086    /// Result error its public batch/span caller must return. The failure
6087    /// path already cleared host/device sequence state; consume only the
6088    /// side-channel marker here and leave the pipeline reusable.
6089    fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
6090        if self
6091            .graph_failed
6092            .swap(false, std::sync::atomic::Ordering::Relaxed)
6093        {
6094            self.cancel
6095                .store(false, std::sync::atomic::Ordering::Relaxed);
6096            self.clear_sequence_state();
6097            return Err(format!("{phase}: deferred O(1) transition failed"));
6098        }
6099        Ok(())
6100    }
6101
6102    /// Freeze landmarks + skeleton state after the prompt pass and drop
6103    /// the o1 layers' full KV; decode then runs `step()` per token.
6104    /// Pub for the network split (see `o1_begin`).
6105    pub fn o1_seal(&mut self) {
6106        if let Err(err) = self.o1_seal_checked() {
6107            self.o1_fail(err);
6108        }
6109    }
6110
6111    /// Run a bounded O(1) prefix (`ids`, positions 0..) through the batch
6112    /// graph instead of the host prefill: the same exact attention, with the
6113    /// O(1) layers' rotated queries captured on the device and their K/V rows
6114    /// read back from the mirror, so each layer's collector holds what the
6115    /// host walk would have given its seal. The recurrent and exact layers'
6116    /// state stays on the device for the sealed suffix batches.
6117    /// `Ok(None)`: declined before any device state moved — the caller runs
6118    /// the host prefill. `Ok(Some(h))`: the last prefix row's hidden.
6119    fn o1_prefix_on_graph(
6120        &mut self,
6121        ids: &[u32],
6122        chunk: usize,
6123        mut arch: Option<&mut O1Archive>,
6124    ) -> Result<Option<Vec<f32>>, String> {
6125        let o1_layers: Vec<usize> = (0..self.num_layers)
6126            .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
6127            .collect();
6128        let Some(&first) = o1_layers.first() else {
6129            return Ok(None);
6130        };
6131        let (nkv, hd) = {
6132            let c = &self.kv_cache.layers[first];
6133            (c.num_kv_heads, c.head_dim)
6134        };
6135        let nh = self.num_heads;
6136        if chunk == 0
6137            || o1_layers.iter().any(|&li| {
6138                let c = &self.kv_cache.layers[li];
6139                self.layer_num_heads(li) != nh
6140                    || (c.num_kv_heads, c.head_dim) != (nkv, hd)
6141                    || c.seq_len != 0
6142                    || c.o1_pending_boundary().is_none()
6143            })
6144            || !crate::gpu::o1_qcap_begin(&o1_layers)
6145        {
6146            return Ok(None);
6147        }
6148        let hs = self.hidden_size;
6149        let n = ids.len();
6150        let mut last = vec![0f32; hs];
6151        let mut pos = 0;
6152        self.o1_prefix_tail.clear();
6153        // `CMF_PREFILL_PROF`: where the prefix's time goes (the chunks, the
6154        // queries off the card, the K/V rows off the card, their way into the cache).
6155        let prof = std::env::var("CMF_PREFILL_PROF").is_ok();
6156        let (mut t_chunks, mut t_q) = (0f64, 0f64);
6157        // The seal's queries leave the card chunk by chunk, one row in `QT_STRIDE`: its
6158        // landmarks are means over long segments.
6159        const QT_STRIDE: usize = 8;
6160        let qrow = nh * hd;
6161        // A backend may hand back the kept rows alone (CUDA gathers them on the card).
6162        let strided = crate::gpu::o1_qcap_stride(QT_STRIDE);
6163        let mut qs: Vec<Vec<f32>> = vec![Vec::new(); o1_layers.len()];
6164        while pos < n {
6165            let end = (pos + chunk).min(n);
6166            let bk = end - pos;
6167            let mut hiddens = vec![0f32; bk * hs];
6168            for (j, &id) in ids[pos..end].iter().enumerate() {
6169                hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
6170            }
6171            let positions: Vec<usize> = (pos..end).collect();
6172            let t_c = std::time::Instant::now();
6173            let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6174            t_chunks += t_c.elapsed().as_secs_f64();
6175            let t_d = std::time::Instant::now();
6176            match outcome {
6177                crate::gpu::BatchGraphOutcome::Completed => {
6178                    last.copy_from_slice(&hiddens[(bk - 1) * hs..]);
6179                    // the prompt's tail for the draft head over the far field
6180                    let tail_from = n.saturating_sub(crate::dory::EXACT_TAIL);
6181                    if end > tail_from {
6182                        let from = pos.max(tail_from);
6183                        self.o1_prefix_tail.extend_from_slice(&hiddens[(from - pos) * hs..]);
6184                    }
6185                }
6186                crate::gpu::BatchGraphOutcome::Declined if pos == 0 => {
6187                    let _ = crate::gpu::o1_qcap_take();
6188                    return Ok(None);
6189                }
6190                _ => {
6191                    let _ = crate::gpu::o1_qcap_take();
6192                    return Err("O(1) prefix batch graph stopped after admission".into());
6193                }
6194            }
6195            let Some(drained) = crate::gpu::o1_qcap_drain() else {
6196                let _ = crate::gpu::o1_qcap_take();
6197                return Err("O(1) prefix: query readback failed".into());
6198            };
6199            for (i, (_, rows)) in drained.iter().enumerate() {
6200                if strided {
6201                    let kept = (pos..end).filter(|p| p % QT_STRIDE == 0).count();
6202                    if rows.len() != kept * qrow {
6203                        let _ = crate::gpu::o1_qcap_take();
6204                        return Err(format!("O(1) prefix: {} query values, expected {}", rows.len(), kept * qrow));
6205                    }
6206                    qs[i].extend_from_slice(rows);
6207                    continue;
6208                }
6209                if rows.len() != bk * qrow {
6210                    let _ = crate::gpu::o1_qcap_take();
6211                    return Err(format!("O(1) prefix: {} query values, expected {}", rows.len(), bk * qrow));
6212                }
6213                for p in pos..end {
6214                    if p % QT_STRIDE == 0 {
6215                        qs[i].extend_from_slice(&rows[(p - pos) * qrow..(p - pos + 1) * qrow]);
6216                    }
6217                }
6218            }
6219            t_q += t_d.elapsed().as_secs_f64();
6220            pos = end;
6221        }
6222        let _ = crate::gpu::o1_qcap_take();
6223        let t_kv = std::time::Instant::now();
6224        let pulls: Vec<(usize, usize, usize)> = o1_layers.iter().map(|&li| (li, 0, n)).collect();
6225        let kv = crate::gpu::graph_kv_read_rows(self.graph_kv_id, &pulls, nkv, hd)
6226            .ok_or("O(1) prefix: K/V readback failed")?;
6227        let (t_kv, t_app) = (t_kv.elapsed().as_secs_f64(), std::time::Instant::now());
6228        let row = nkv * hd;
6229        if arch.is_none() {
6230            // without an archive each layer's rows go into its own cache: the
6231            // layers side by side
6232            for ((&li, q), (k, v)) in o1_layers.iter().zip(&qs).zip(&kv) {
6233                if k.len() != n * row || v.len() != n * row || q.len() != n.div_ceil(QT_STRIDE) * qrow {
6234                    return Err(format!(
6235                        "O(1) prefix: layer {li} read back {}/{}/{} values, expected {}/{}/{}",
6236                        k.len(),
6237                        v.len(),
6238                        q.len(),
6239                        n * row,
6240                        n * row,
6241                        n.div_ceil(QT_STRIDE) * qrow
6242                    ));
6243                }
6244            }
6245            let mut work: Vec<(&mut crate::kv_cache::LayerKvCache, Vec<f32>, (Vec<f32>, Vec<f32>))> = Vec::new();
6246            let mut by_layer: Vec<Option<(Vec<f32>, (Vec<f32>, Vec<f32>))>> = (0..self.kv_cache.layers.len()).map(|_| None).collect();
6247            for ((&li, q), kvp) in o1_layers.iter().zip(qs).zip(kv) {
6248                by_layer[li] = Some((q, kvp));
6249            }
6250            for (cache, slot) in self.kv_cache.layers.iter_mut().zip(by_layer) {
6251                if let Some((q, kvp)) = slot {
6252                    work.push((cache, q, kvp));
6253                }
6254            }
6255            let par = std::thread::available_parallelism().map_or(1, |p| p.get()).clamp(1, 8);
6256            let per = work.len().div_ceil(par).max(1);
6257            std::thread::scope(|sc| {
6258                for part in work.chunks_mut(per) {
6259                    sc.spawn(move || {
6260                        for (cache, q, (k, v)) in part.iter_mut() {
6261                            cache.o1_set_q_stride(QT_STRIDE);
6262                            cache.reserve_rows(n);
6263                            for p in 0..n {
6264                                cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
6265                            }
6266                            cache.o1_set_q_trace(std::mem::take(q));
6267                        }
6268                    });
6269                }
6270            });
6271            if prof {
6272                eprintln!(
6273                    "o1 prefix: {n} rows, chunks {t_chunks:.2} s, queries off the card {t_q:.2} s, K/V off the card {t_kv:.2} s, into the cache {:.2} s",
6274                    t_app.elapsed().as_secs_f64()
6275                );
6276            }
6277            return Ok(Some(last));
6278        }
6279        for ((&li, q), (k, v)) in o1_layers.iter().zip(qs).zip(kv) {
6280            if k.len() != n * row || v.len() != n * row || q.len() != n.div_ceil(QT_STRIDE) * qrow {
6281                return Err(format!(
6282                    "O(1) prefix: layer {li} read back {}/{}/{} values, expected {}/{}/{}",
6283                    k.len(),
6284                    v.len(),
6285                    q.len(),
6286                    n * row,
6287                    n * row,
6288                    n.div_ceil(QT_STRIDE) * qrow
6289                ));
6290            }
6291            let cache = &mut self.kv_cache.layers[li];
6292            cache.o1_set_q_stride(QT_STRIDE);
6293            cache.reserve_rows(n);
6294            for p in 0..n {
6295                cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
6296            }
6297            if let Some(a) = arch.as_deref_mut() {
6298                let dir = a.dir.clone();
6299                let mut layer = O1ArchLayer {
6300                    li,
6301                    k: ArchStore::new(dir.as_deref(), format!("{li}-k"))?,
6302                    v: ArchStore::new(dir.as_deref(), format!("{li}-v"))?,
6303                    q: ArchStore::new(dir.as_deref(), format!("{li}-q"))?,
6304                };
6305                layer.k.push(&k)?;
6306                layer.v.push(&v)?;
6307                layer.q.push(&q)?;
6308                a.layers.push(layer);
6309                a.rows = n;
6310                a.stride = QT_STRIDE;
6311                a.nh = nh;
6312                a.nk = nkv;
6313                a.hd = hd;
6314            }
6315            cache.o1_set_q_trace(q);
6316        }
6317        if prof {
6318            eprintln!(
6319                "o1 prefix: {n} rows, chunks {t_chunks:.2} s, queries off the card {t_q:.2} s, K/V off the card {t_kv:.2} s, into the cache {:.2} s",
6320                t_app.elapsed().as_secs_f64()
6321            );
6322        }
6323        Ok(Some(last))
6324    }
6325
6326    /// Enable/disable the structured per-token telemetry trace (B4).
6327    pub fn set_trace(&mut self, on: bool) {
6328        self.trace = on;
6329    }
6330
6331    /// Replace all request-scoped sampler options and reset the random stream.
6332    /// This is required for deterministic `seed` semantics in pooled servers.
6333    pub fn set_sampler_config(&mut self, config: SamplerConfig) {
6334        self.rng = match config.seed {
6335            Some(seed) => SplitMix64::new(seed),
6336            None => SplitMix64::from_entropy(),
6337        };
6338        self.sampler_config = config;
6339    }
6340
6341    /// Toggle the per-token confidence reduction (a full-vocab
6342    /// softmax each token). `bench --core` turns it off so the timed
6343    /// loop matches llama-bench's core contract; the result's
6344    /// `confidence` vec is empty while off.
6345    pub fn set_confidence(&mut self, on: bool) {
6346        self.confidence_on = on;
6347    }
6348
6349    /// Set the confidence-calibration temperature (B1). Values ≤0 are
6350    /// clamped to raw (1.0).
6351    pub fn set_calib_temp(&mut self, t: f32) {
6352        self.calib_temp = if t > 1e-3 { t } else { 1.0 };
6353    }
6354
6355    /// The active calibration temperature (1.0 = raw probability).
6356    pub fn calib_temp(&self) -> f32 {
6357        self.calib_temp
6358    }
6359
6360    /// The primary (full-attention) RoPE inverse-frequency table selected
6361    /// from the CMF architecture.  This is read-only diagnostic state for
6362    /// parity checks; local sliding layers may use `inv_freq_local` instead.
6363    pub fn rope_inv_freq(&self) -> &[f32] {
6364        self.inv_freq.as_slice()
6365    }
6366
6367    /// Partial rotary (Qwen3.5): rotate only the first `rotary_dim` dims;
6368    /// the frequency table is rebuilt over the rotary dims.
6369    pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
6370        self.rotary_dim = rotary_dim.min(self.head_dim);
6371        self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
6372        // The packed resident graph owns its own inverse-frequency plane;
6373        // changing RoPE after it was built must not leave a stale device
6374        // model behind the exact host configuration.
6375        self.embryo_graph = None;
6376    }
6377
6378    fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
6379        QwenAttnCfg {
6380            mrope: None,
6381            num_heads: self.num_heads,
6382            num_kv_heads: self.num_kv_heads,
6383            head_dim: self.head_dim,
6384            hidden_size: self.hidden_size,
6385            position,
6386            inv_freq: &self.inv_freq,
6387            rotary_dim: self.rotary_dim,
6388            scale: self.attn_scale,
6389            softcap: self.attn_softcap,
6390            window: None,
6391            v_norm: false,
6392            qk_norm_after_rope: self.qk_norm_after_rope,
6393            gate_sigmoid: self.proj_gate_sigmoid,
6394            q_norm: None,
6395            k_norm: None,
6396            output_gate: false,
6397            softplus_gate: None,
6398            rope_scale: self.rope_scale,
6399            bias: None,
6400            rms_eps: self.rms_eps,
6401            norm_style: self.norm_style,
6402            pool: self.pool.as_deref(),
6403            v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
6404            value_in: None,
6405        }
6406    }
6407
6408    /// Generate text from a plain-text prompt. Streams tokens via `on_token`.
6409    pub fn generate(
6410        &mut self,
6411        prompt: &str,
6412        max_tokens: usize,
6413        task_mask: Option<&TaskMask>,
6414        on_token: Option<TokenCallback>,
6415    ) -> Result<GenerateResult, String> {
6416        let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
6417        self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
6418    }
6419
6420    /// Generate from a V4.1 multimodal prompt prepared by the vision module.
6421    /// Vision rows are encoded once and fed through the same bounded token walk as text.
6422    pub fn generate_from_vl(
6423        &mut self,
6424        input: &crate::dsv41_vision::PreparedVlInputs,
6425        max_tokens: usize,
6426        task_mask: Option<&TaskMask>,
6427        on_token: Option<TokenCallback>,
6428    ) -> Result<GenerateResult, String> {
6429        let Some(dsv41) = &self.dsv41 else {
6430            return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
6431        };
6432        if input.token_ids.is_empty() {
6433            return Err("empty V4.1 multimodal prompt".into());
6434        }
6435        if input.token_types.len() != input.token_ids.len() {
6436            return Err(format!(
6437                "V4.1 token type count {} != token count {}",
6438                input.token_types.len(),
6439                input.token_ids.len()
6440            ));
6441        }
6442        let dim = dsv41.2.dim;
6443        let mut embeddings = vec![None; input.token_ids.len()];
6444        let mut participates = vec![true; input.token_ids.len()];
6445        if !input.images.is_empty() {
6446            let vision = self
6447                .dsv41_vision
6448                .as_ref()
6449                .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
6450            for image in &input.images {
6451                let end = image.start.saturating_add(image.types.len());
6452                if end > input.token_ids.len() {
6453                    return Err(format!(
6454                        "V4.1 image span {}..{} exceeds prompt length {}",
6455                        image.start,
6456                        end,
6457                        input.token_ids.len()
6458                    ));
6459                }
6460                let mut span = vec![0.0f32; image.types.len() * dim];
6461                vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
6462                for (offset, &kind) in image.types.iter().enumerate() {
6463                    let pos = image.start + offset;
6464                    if input.token_types[pos] != kind {
6465                        return Err(format!(
6466                            "V4.1 image type mismatch at position {pos}: {} != {kind}",
6467                            input.token_types[pos]
6468                        ));
6469                    }
6470                    embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
6471                    participates[pos] = false;
6472                }
6473            }
6474        }
6475        for (pos, &kind) in input.token_types.iter().enumerate() {
6476            if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
6477                return Err(format!("V4.1 text position {pos} has an image embedding"));
6478            }
6479            if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
6480                return Err(format!("V4.1 image position {pos} has no image embedding"));
6481            }
6482        }
6483        self.dsv41_prefill = Some((embeddings, participates));
6484        let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
6485        self.dsv41_prefill = None;
6486        result
6487    }
6488
6489    /// `None` when the mask forbids nothing (see `TaskMask::fully_open`).
6490    fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
6491        m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
6492    }
6493
6494    /// Generate from prepared token ids (e.g. a chat template).
6495    ///
6496    /// With an MTP head, greedy generation without a task mask takes the
6497    /// speculative path: the MTP module drafts the token after next and
6498    /// the main model verifies both in one fused two-position forward
6499    /// (weights streamed once). The output is EXACTLY the vanilla greedy
6500    /// sequence — a rejected draft is rolled back — MTP only buys speed.
6501    pub fn generate_from_ids(
6502        &mut self,
6503        input_ids: &[u32],
6504        max_tokens: usize,
6505        task_mask: Option<&TaskMask>,
6506        on_token: Option<TokenCallback>,
6507    ) -> Result<GenerateResult, String> {
6508        self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
6509    }
6510
6511    /// Generate from complete prompt embeddings [token_count, hidden_size].
6512    /// Text rows can be obtained with `embed_id`; media rows replace only
6513    /// their expanded placeholder positions. Rows are already scaled and
6514    /// enter `PrefillIn::Hidden`, so a device graph must not re-embed them.
6515    /// Token-only KV reuse is disabled both into and out of this request.
6516    pub fn generate_from_embeds(
6517        &mut self,
6518        input_ids: &[u32],
6519        prompt_rows: &[f32],
6520        max_tokens: usize,
6521        task_mask: Option<&TaskMask>,
6522        on_token: Option<TokenCallback>,
6523    ) -> Result<GenerateResult, String> {
6524        if input_ids.is_empty()
6525            || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
6526        {
6527            return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
6528        }
6529        if prompt_rows.iter().any(|x| !x.is_finite()) {
6530            return Err("embedded prompt contains non-finite values".into());
6531        }
6532        if !self.can_prefill_batched()
6533            || self.dyn_router.is_some()
6534            || self.o1_active()
6535            || self.mtp.is_some()
6536            || self.gpu_plan.is_some()
6537        {
6538            return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
6539        }
6540        self.generate_with_prompt_rows(
6541            input_ids,
6542            Some(prompt_rows),
6543            max_tokens,
6544            task_mask,
6545            on_token,
6546        )
6547    }
6548
6549    fn generate_with_prompt_rows(
6550        &mut self,
6551        input_ids: &[u32],
6552        prompt_rows: Option<&[f32]>,
6553        max_tokens: usize,
6554        task_mask: Option<&TaskMask>,
6555        mut on_token: Option<TokenCallback>,
6556    ) -> Result<GenerateResult, String> {
6557        #[cfg(target_os = "macos")]
6558        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
6559        if std::env::var("CMF_TRACE_H").is_ok() {
6560            eprintln!("input_ids: {input_ids:?}");
6561        }
6562        if input_ids.is_empty() {
6563            return Err("empty prompt: nothing to generate from".to_string());
6564        }
6565        // A prior graph failure is terminal for that sequence but must not
6566        // poison the next independent request.  Keep this flag separate from
6567        // the externally-owned cooperative cancel bit.
6568        self.graph_failed
6569            .store(false, std::sync::atomic::Ordering::Relaxed);
6570        // A mask that forbids nothing still costs every fused path and
6571        // whole-token graph, all of which are gated on `is_none()`. A
6572        // narrowed file whose one segment is always on carries exactly
6573        // such a mask — drop it here rather than pay 5x for a no-op.
6574        self.o1_turbo_gate(input_ids.len());
6575        self.mtp_pos_base = 0;
6576        let prev_mtp_warm = self.mtp_warm_base.take();
6577        // the draft head's rows a chat snapshot keeps (counted from its base)
6578        let mut mtp_keep: Option<usize> = None;
6579        let task_mask = self.drop_open_mask(task_mask);
6580        // Unlimited-OCR: the page coordinates of this prompt.
6581        if self.dash.is_some() {
6582            if prompt_rows.is_none() {
6583                return Err("Unlimited-OCR reads images: pass at least one".into());
6584            }
6585            let eos = self.tokenizer.eos_token_id.unwrap_or(1);
6586            if let Some(d) = self.dash.as_mut() {
6587                d.begin(input_ids, eos)?;
6588            }
6589        }
6590        // A block-diffusion model generates whole blocks by denoising
6591        // previews, unless the request asks for the causal decode.
6592        if self.clover.is_some() && !self.clover_causal_request() {
6593            if prompt_rows.is_some() || task_mask.is_some() {
6594                return Err("block-diffusion generation takes token prompts without task masks".into());
6595            }
6596            return self.clover_generate(input_ids, max_tokens, on_token);
6597        }
6598
6599        // Cross-turn KV reuse: a chat app resends the whole history
6600        // every turn; when the new ids strictly EXTEND what the cache
6601        // already holds, prefill only the tail — turn latency stays
6602        // proportional to the new text instead of the whole session.
6603        // Extension-only (no rollback), so it is exact for every layer
6604        // kind including recurrent state; MTP/o1/task-mask runs keep
6605        // the fresh-sequence path. CMF_KV_REUSE=0 disables.
6606        let mut reuse_from = if self.dory.is_some() {
6607            self.dory_cached_prefix(input_ids)
6608        } else {
6609            let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
6610            if on
6611                && prompt_rows.is_none()
6612                && task_mask.is_none()
6613                && self.mtp.is_none()
6614                && !(self.mimo_mtp.is_some() && self.speculative)
6615                && self.o1_cfg.is_none()
6616                && self.dsv41.is_none()
6617            {
6618                self.cached_prefix_len(input_ids)
6619            } else {
6620                0
6621            }
6622        };
6623        // The device may own rows the host tail prefill needs (wgpu decode
6624        // writes only its mirror): hand them to the host, or start fresh.
6625        if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
6626            reuse_from = 0;
6627        }
6628        // The CUDA hybrid arm: a chat turn that starts with the ids before
6629        // the last turn's assistant header goes back to the GDN snapshot the
6630        // card took there; the attention mirrors rewind with it. The rows
6631        // before it are prompt rows no decode wrote, so this is exact after
6632        // any speculative round, and the draft head reads from there on.
6633        if reuse_from == 0 && prompt_rows.is_none() && task_mask.is_none() {
6634            let n = self.gdn_snap_reuse(input_ids);
6635            if n > 0 {
6636                reuse_from = n;
6637                self.kv_history.truncate(n);
6638                for l in &mut self.kv_cache.layers {
6639                    let d = l.pos_len().saturating_sub(n);
6640                    if d > 0 {
6641                        l.truncate_last(d);
6642                    }
6643                }
6644                self.graph_logits = None;
6645                // The draft head's rows before `n` are the last prompt's
6646                // rows when it was warmed over that whole prompt: they stay,
6647                // and it reads the conversation, not just the new turn.
6648                if self.mtp.is_some() {
6649                    match prev_mtp_warm {
6650                        Some(b) if b <= n => {
6651                            self.mtp_pos_base = b;
6652                            mtp_keep = Some(n - b);
6653                        }
6654                        _ => self.mtp_pos_base = n,
6655                    }
6656                }
6657                if std::env::var("CMF_PREFILL_PROF").is_ok() {
6658                    eprintln!("kv-reuse: GDN snapshot at {n} of {} prompt positions", input_ids.len());
6659                }
6660            }
6661        }
6662        // A diverging turn (`rollback_prefix_len`): drop every row past the
6663        // common prefix, so the tail prefill appends at `reuse_from`.
6664        if reuse_from > 0 && self.dory.is_some() && reuse_from < self.kv_history.len() {
6665            // The device's rings are addressed by position, so going back there
6666            // is only this: the next rows overwrite the rolled-back ones.
6667            if let Some(b) = &mut self.dory {
6668                let kv_row = b.0.cfg.nk * b.0.cfg.hd;
6669                b.1.rollback(reuse_from, kv_row);
6670            }
6671            self.kv_history.truncate(reuse_from);
6672            self.graph_logits = None;
6673        }
6674        if reuse_from > 0 && self.dory.is_none() && !self.bounded_native() && reuse_from < self.kv_history.len() {
6675            for l in &mut self.kv_cache.layers {
6676                let d = l.pos_len() - reuse_from;
6677                l.truncate_last(d);
6678            }
6679            if std::env::var("CMF_PREFILL_PROF").is_ok() {
6680                eprintln!(
6681                    "kv-reuse: rolled back {} answer position(s) to the common prefix",
6682                    self.kv_history.len() - reuse_from
6683                );
6684            }
6685            self.kv_history.truncate(reuse_from);
6686            self.graph_logits = None;
6687        }
6688        self.swa_trim_anchor = input_ids.len();
6689        self.dflash_tap_from = match &self.dflash {
6690            Some(d) if (0..d.g.layers).all(|l| d.g.window_of(l) > 0) => {
6691                let w = (0..d.g.layers).map(|l| d.g.window_of(l)).max().unwrap_or(0);
6692                input_ids.len().saturating_sub(w + crate::dflash::BLOCK_SLACK)
6693            }
6694            _ => 0,
6695        };
6696        self.last_prefill_tokens = input_ids.len() - reuse_from;
6697        let bounded_native = self.bounded_native();
6698        if reuse_from == 0 {
6699            // Fresh sequence — the cache holds absolute positions.
6700            // MTP never participates in cross-turn KV reuse, so a
6701            // high-vocabulary token from the previous request must not keep
6702            // this request on its expensive full draft head.
6703            self.reset_fresh_mtp_draft_state();
6704            self.clear_sequence_state();
6705        } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
6706            eprintln!(
6707                "kv-reuse: {} of {} prompt positions already cached",
6708                reuse_from,
6709                input_ids.len()
6710            );
6711        }
6712        crate::gpu::graph_race_begin_generation();
6713        // Optional bounded calibration prefix. Keep the requested value
6714        // even when it is longer than the prompt; the collecting layer will
6715        // defer at the effective boundary and remain exact for short input.
6716        let o1_prefill = if self.o1_active() && task_mask.is_none() {
6717            std::env::var("CMF_O1_PREFILL")
6718                .ok()
6719                .and_then(|v| v.parse::<usize>().ok())
6720                .filter(|&p| p > 0)
6721                // Exact rows are chosen before the prompt's last rows, which then read them.
6722                .or_else(|| {
6723                    self.o1_exact_cfg()
6724                        .map(|_| {
6725                            let max = std::env::var("CMF_O1_PREFIX_MAX")
6726                                .ok()
6727                                .and_then(|v| v.parse().ok())
6728                                .or(self.o1_turbo.as_ref().map(|t| t.seal))
6729                                .unwrap_or(O1_PREFIX_MAX);
6730                            input_ids.len().saturating_sub(crate::dory::EXACT_TAIL).min(max)
6731                        })
6732                        .filter(|&p| p > 0)
6733                })
6734        } else {
6735            None
6736        };
6737        if task_mask.is_none() {
6738            self.o1_begin_with_prefix(o1_prefill);
6739        }
6740        if let Ok(f) = std::env::var("CMF_O1_TOKENS_DUMP") {
6741            let b: Vec<u8> = input_ids.iter().flat_map(|v| v.to_le_bytes()).collect();
6742            let _ = std::fs::write(f, b);
6743        }
6744
6745        // Speculative decode is off under o1: a rejected draft can't be
6746        // rolled back out of the far accumulators / ring window (the
6747        // Nyström insertion is irreversible by design).
6748        // The wgpu token graph owns a device K/V mirror that speculative
6749        // rollback would desync — the two are mutually exclusive.
6750        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
6751        // Graph speculative decode (`CMF_GRAPH_SPEC=1`): the MTP head
6752        // drafts, ONE batched graph submit verifies the whole chain.
6753        //
6754        // It now PAYS on Qwen3.6-27B / RTX 5090 — 51.1 tok/s against a
6755        // plain 49.4 at k=3, medians of three, 89% of drafts accepted,
6756        // and the greedy continuation is byte-identical to the plain
6757        // path. That took the batch matvec sharing its nibble unpack
6758        // across the batch (`CMF_MV_BK=2`); before it, the same round
6759        // measured 43.6, an 11% LOSS, which is what the earlier note
6760        // here described.
6761        //
6762        // Still opt-in. One model's win is not a default: the verify
6763        // rides `gdn_spec_restore` and a batched frame whose numerics
6764        // are the batch kernels', and that has to be shown on more than
6765        // one architecture before every greedy decode takes it.
6766        // Greedy (with or without penalties) verifies by argmax equality.
6767        // Sampling (temperature > 0) can go through speculative SAMPLING —
6768        // draft from the MTP head's own post-chain distribution, accept
6769        // with min(1, p/q), correct from max(0, p − q); the emitted stream
6770        // is distributed exactly as the plain sampler's — but it is
6771        // OPT-IN (`CMF_GRAPH_SPEC_SAMPLE=1`): measured on Qwen3.8-27B /
6772        // RTX 5090 at the instruct row (0.7 / 0.80 / 20 / presence 1.5)
6773        // it decoded 19-22 tok/s against a plain 40 — nine post-chain
6774        // distributions a round plus a lower acceptance than greedy's,
6775        // against a verify that costs 2.7 single tokens. The greedy arms
6776        // pay +10%; the sampling arm needs a cheaper verify first.
6777        // Native Metal has a fast row-batch verify, but it is NOT admitted
6778        // by default: a real-model exactness check found an emitted greedy
6779        // divergence from the serial path.  Every native-Metal speculative
6780        // arm therefore requires the explicit `CMF_GRAPH_SPEC=1` experiment
6781        // gate until the batch kernels are numerically qualified.  If it is
6782        // explicitly enabled, its sampling arm is useful only for a config
6783        // the SPARSE chain serves (a top-k within `sparse_ok`): without it a
6784        // round builds nine 248k-float distributions on the host, which is
6785        // the 5090's measured loss and not a cost the round-token proxy
6786        // below can see. A top-k-less sampling config keeps the plain path
6787        // unless asked for by name.
6788        #[cfg(target_os = "macos")]
6789        let metal_graph = crate::gpu::q1_force()
6790            && crate::gpu::enabled_here()
6791            && std::env::var("CMF_GPU_BLOCK")
6792                .map(|v| v != "0")
6793                .unwrap_or(true);
6794        // `CMF_GPU_BLOCK=0` declines the ordinary native block route but
6795        // does not change the backend identity: a separately requested wgpu
6796        // graph can still reach `graph_spec_step`, whose native branch uses
6797        // VerifyGraph. Keep the quality policy tied to that identity, not to
6798        // the particular block-route admission above.
6799        #[cfg(target_os = "macos")]
6800        let native_metal_backend = crate::gpu::q1_force();
6801        #[cfg(not(target_os = "macos"))]
6802        let metal_graph = false;
6803        #[cfg(not(target_os = "macos"))]
6804        let native_metal_backend = false;
6805        let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
6806        // A round whose cost is the MEASURED one: greedy (argmax rows), or
6807        // sampling through the sparse chain. Anything else pays the dense
6808        // chain's host time, which no proxy can price.
6809        let spec_cheap_round =
6810            self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
6811        let spec_sampling_ok = self.sampler_config.temperature < 1e-6
6812            || match spec_sample_env.as_deref() {
6813                Some("1") => true,
6814                Some(_) => false,
6815                None => metal_graph && spec_cheap_round,
6816            };
6817        // ON by default for greedy on the wgpu graph: with the draft on
6818        // the graph and the verify bit-exact, it measured 58.7 tok/s
6819        // against a plain 48.1 on Qwen3.8-27B q4tp / RTX 5090 (k=4) and
6820        // 51.1 against 49.4 on Qwen3.6-27B, and a round that stops
6821        // paying turns itself off below (acceptance watchdog).
6822        // `CMF_GRAPH_SPEC=0` disables; `=1` is the explicit opt-in spelling.
6823        // …but only where the batched verify has its register-blocked
6824        // kernel: q4tp dense FFNs (graph kind 6). q4t and q8_2f verify
6825        // through tile GEMMs today and measured a LOSS (q8_2f 22 against
6826        // 29 tok/s), the 2-bit plane the same; those stay opt-in
6827        // (`CMF_GRAPH_SPEC=1`).
6828        // …at least in nine dense FFNs of ten: a healed file carries its
6829        // last two layers at q8_2f, and two tile-GEMM verifies among 64 do
6830        // not change the arithmetic (measured: the healed q4tp file
6831        // decodes at the plain file's rate and would otherwise sit out).
6832        let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
6833        for lw in &self.weights.layers {
6834            if let FfnKind::Dense(d) = &lw.ffn {
6835                dense_n += 1;
6836                if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
6837                    && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
6838                    && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
6839                {
6840                    dense_q4tp += 1;
6841                }
6842            }
6843        }
6844        // …and only where the MTP block itself runs on the graph. A MoE
6845        // draft block (Qwen3.6-35B-A3B, Apex-35B: 256 experts in the MTP
6846        // layer) fails `mtp_graph_ok`, so it drafts — and warms its cache
6847        // over the whole prompt — op by op on the host. Apex-35B q4tp on an
6848        // A40 + Xeon Gold 6342: greedy 51.8 against 65.6 tok/s plain (300
6849        // tokens, 57 % accepted), and 2.5 ms per prompt position (a
6850        // 4096-token ingest 171 against 295 tok/s); the published
6851        // Qwen3.6-35B-A3B 79.7 against 86.6. `CMF_GRAPH_SPEC=1` opts in.
6852        // Native Metal has no wgpu graph: its draft step is `mtp_step_metal`,
6853        // which takes the same block shape.
6854        #[cfg(feature = "gpu")]
6855        let mtp_on_graph = self.mtp.as_ref().is_none_or(|m| {
6856            if native_metal_backend {
6857                self.mtp_block_shape_ok(m)
6858            } else {
6859                self.mtp_graph_ok(m)
6860            }
6861        });
6862        #[cfg(not(feature = "gpu"))]
6863        let mtp_on_graph = self.mtp.as_ref().is_none_or(|m| !native_metal_backend || self.mtp_block_shape_ok(m));
6864        let spec_default_ok = (dense_n == 0 || dense_q4tp * 10 >= dense_n * 9) && mtp_on_graph;
6865        // Penalties break the draft head's agreement with the trunk (a
6866        // 1.1 repetition penalty measured 2 of 16 accepted): not by
6867        // default there either — off Metal that rule is untouched, and
6868        // suppressed ids keep counting as a penalty there, because no
6869        // measurement on a discrete card says otherwise.
6870        //
6871        // On native Metal the penalized arms DO pay: the draft applies
6872        // the same penalty and the verify scores the penalized rows
6873        // exactly (`greedy_pen`, the plain loop's arithmetic), so the
6874        // text is the plain path's and only the round's shape changes.
6875        // Measured on this M4 — see the report for the interleaved run.
6876        //
6877        // On the CUDA hybrid arm a suppress list alone (a no-think chat
6878        // masks `<think>`) does not count: it reads no history, the draft
6879        // and the verify rows mask the same ids, and the verify rows are
6880        // the plain rows bit for bit, so the round (`greedy_pen`) keeps the
6881        // plain text. Qwen3.8-27B q4tp, RTX 3090, a no-think chat over
6882        // `cortiq serve`: 44.8 tok/s plain before this.
6883        let suppress_only_ok = crate::gpu::cuda_active()
6884            && self.sampler_config.repetition_penalty == 1.0
6885            && self.sampler_config.presence_penalty == 0.0;
6886        let penalized = !metal_graph
6887            && (self.sampler_config.repetition_penalty != 1.0
6888                || self.sampler_config.presence_penalty != 0.0
6889                || (!self.sampler_config.suppress_tokens.is_empty() && !suppress_only_ok));
6890        // …and not on wgpu-over-Metal: the batched verify graph there
6891        // returned 0 accepted drafts and garbage text on a GDN hybrid
6892        // (16.08, Qwen3.5-0.8B) while Vulkan is bit-exact; the Mac's
6893        // default backend is native Metal without a batch graph anyway.
6894        #[cfg(feature = "gpu")]
6895        let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
6896        #[cfg(not(feature = "gpu"))]
6897        let metal_wgpu = false;
6898        let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
6899        let spec_wanted = graph_spec_wanted_by_policy(
6900            spec_env.as_deref(),
6901            spec_default_ok,
6902            penalized,
6903            metal_wgpu,
6904            native_metal_backend,
6905        );
6906        if spec_wanted && metal_wgpu {
6907            tracing::warn!(
6908                "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
6909                 verified on this backend (garbage measured on Qwen3.5-0.8B)"
6910            );
6911        }
6912        // Native Metal: the b-row verify graph (`try_batch_graph_metal`)
6913        // stands where the wgpu batch graph stands on discrete cards
6914        // (`metal_graph`, above).
6915        let graph_spec = self.speculative
6916            && (graph_on || metal_graph)
6917            && self.mtp.is_some()
6918            && task_mask.is_none()
6919            && (!self.o1_active() || self.o1_spec())
6920            && spec_sampling_ok
6921            && spec_wanted;
6922        // A trunk without a draft block of its own (Ripple): a DFlash sidecar
6923        // drafts a round in one block and the batch graph verifies it, the
6924        // rows of drafts that did not hold let go. Plain greedy only, so the
6925        // text is the plain path's. `CMF_GRAPH_SPEC=0` keeps plain tokens.
6926        #[cfg(feature = "gpu")]
6927        let solo_spec = self.speculative
6928            && graph_on
6929            && !metal_wgpu
6930            && self.mtp.is_none()
6931            && task_mask.is_none()
6932            && (!self.o1_active() || self.o1_spec())
6933            && self.sampler_config.temperature < 1e-6
6934            && !penalized
6935            && spec_env.as_deref() != Some("0")
6936            && (self.dflash.is_some() || solo_script().is_some());
6937        #[cfg(feature = "gpu")]
6938        let solo_pin: Option<usize> =
6939            std::env::var("CMF_GRAPH_SPEC_K").ok().and_then(|v| v.parse().ok()).filter(|k| (1..=SOLO_K_MOST).contains(k));
6940        #[cfg(feature = "gpu")]
6941        let mut solo =
6942            SoloSpec { k: solo_pin.unwrap_or(4), rest: 3, back: SOLO_REST, plain_ms: 0.0, plain_n: 0, round_ms: 0.0, rounds: 0, win_ms: 0.0, win_tok: 0, last: None, first: true };
6943        // The host fused pair is an optimization, not a verifier. On real
6944        // q4tp Qwen3.5 a near-tied row produced a different greedy argmax
6945        // from two serial target forwards. That makes a seemingly accepted
6946        // draft change text, which is worse than losing speculative speed.
6947        // Keep it explicitly experimental until a model/backend has passed
6948        // a long exact-ID gate; the native Metal batch verifier has its own
6949        // separate graph admission and is not controlled by this switch.
6950        //
6951        // GDN hybrids are also excluded by default: their recurrence is
6952        // sequential, so the pair lane cannot parallelize and measured 2x
6953        // slower on the 35B. `CMF_MTP_PAIR=1` is deliberately the only
6954        // opt-in for the host fused-pair experiment (not `CMF_MTP=1`).
6955        let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
6956        let pair_pays = host_pair_opt_in
6957            && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
6958        // Native Metal: say the route ONCE (RUST_LOG=info), including why
6959        // the drafts are off when they are (`graph_spec_wanted_by_policy`).
6960        #[cfg(target_os = "macos")]
6961        if metal_graph {
6962            static SAID: std::sync::Once = std::sync::Once::new();
6963            SAID.call_once(|| {
6964                let spec = if graph_spec {
6965                    let k = std::env::var("CMF_GRAPH_SPEC_K")
6966                        .ok()
6967                        .and_then(|v| v.parse::<usize>().ok())
6968                        .filter(|&v| (1..=8).contains(&v))
6969                        .map(|v| v.to_string())
6970                        .unwrap_or_else(|| {
6971                            if self.metal_small_batch_verify() {
6972                                "auto".to_string()
6973                            } else {
6974                                "7".to_string()
6975                            }
6976                        });
6977                    let arm = if self.sampler_config.temperature < 1e-6 {
6978                        "greedy"
6979                    } else {
6980                        "sampling"
6981                    };
6982                    format!(
6983                        "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
6984                        Self::draft_vocab_rows(usize::MAX)
6985                    )
6986                } else if !self.speculative {
6987                    "spec off (CMF_MTP=0)".to_string()
6988                } else if self.mtp.is_none() {
6989                    "spec off (no MTP head)".to_string()
6990                } else if !spec_sampling_ok {
6991                    if spec_cheap_round {
6992                        "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
6993                    } else {
6994                        "spec off (sampling without a top-k: the dense chain \
6995                         costs more than it saves)"
6996                            .to_string()
6997                    }
6998                } else if !spec_wanted {
6999                    if spec_env.as_deref() == Some("0") {
7000                        "spec off (CMF_GRAPH_SPEC=0)".to_string()
7001                    } else {
7002                        "spec off (drafts by default only over a q4tp trunk; CMF_GRAPH_SPEC=1 asks for them)".to_string()
7003                    }
7004                } else if task_mask.is_some() {
7005                    "spec off (task mask)".to_string()
7006                } else if !graph_spec && !pair_pays {
7007                    "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
7008                        .to_string()
7009                } else {
7010                    "spec off (O(1) attention)".to_string()
7011                };
7012                let on = |var: &str| {
7013                    if std::env::var(var).as_deref() == Ok("0") {
7014                        "off"
7015                    } else {
7016                        "on"
7017                    }
7018                };
7019                tracing::info!(
7020                    "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
7021                     MTP graph {}, attend {}, probe {}",
7022                    if crate::gpu_metal::state4_on() {
7023                        "on"
7024                    } else {
7025                        "off"
7026                    },
7027                    if crate::gpu_metal::async_replay_on() {
7028                        "on"
7029                    } else {
7030                        "off"
7031                    },
7032                    on("CMF_METAL_PREFILL"),
7033                    on("CMF_MTP_GRAPH"),
7034                    std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
7035                    if crate::gpu::probe_enabled() {
7036                        "bypassed (q1 force)"
7037                    } else {
7038                        "off"
7039                    },
7040                );
7041            });
7042        }
7043        let spec_active = self.speculative
7044            && self.mtp.is_some()
7045            && task_mask.is_none()
7046            && (!self.o1_active() || self.o1_spec())
7047            && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
7048        // The MTP module is detached during generation so its mutable
7049        // state does not fight the borrow on `self`.
7050        let mut mtp = if spec_active { self.mtp.take() } else { None };
7051        if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
7052            eprintln!(
7053                "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
7054                mtp.is_some(),
7055                self.speculative,
7056                self.sampler_config.temperature < 1e-6,
7057            );
7058        }
7059        if let Some(m) = &mut mtp {
7060            let kept = mtp_keep.is_some_and(|rows| {
7061                crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, rows)
7062            });
7063            if kept {
7064                let rows = mtp_keep.unwrap_or(0);
7065                let d = m.kv.pos_len().saturating_sub(rows);
7066                if d > 0 {
7067                    m.kv.truncate_last(d);
7068                }
7069            } else {
7070                m.kv.clear();
7071                // The MTP block's own device mirror starts over with its cache.
7072                crate::gpu::graph_kv_reset(self.mtp_kv_id());
7073                if mtp_keep.is_some() {
7074                    // nothing kept: the head reads from the snapshot on
7075                    self.mtp_pos_base = reuse_from;
7076                }
7077            }
7078            self.mtp_graph_mode = None;
7079        }
7080        // MiMo-V2's draft stack: greedy rounds (draft K with the chained
7081        // MTP layers, verify K+1 rows in one batched forward). Sampling
7082        // decodes plain; `CMF_MTP=0` / `CMF_MIMO_MTP=0` turn it off.
7083        let mimo_spec = self.speculative
7084            && self.mimo_mtp.is_some()
7085            && task_mask.is_none()
7086            && !self.o1_active()
7087            && self.dyn_router.is_none()
7088            && self.sampler_config.temperature < 1e-6
7089            && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
7090        if let Some(st) = self.mimo_mtp.as_mut() {
7091            st.reset();
7092            if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
7093                Self::mimo_mtp_hist_cap(st, input_ids.len());
7094            }
7095        }
7096        // Dynamic router detached during decode (same borrow trick as MTP).
7097        // Speculative decode and dynamic routing are mutually exclusive
7098        // for now — the fused-pair path doesn't carry per-token φ.
7099        let mut router = if mtp.is_none() {
7100            self.dyn_router.take()
7101        } else {
7102            None
7103        };
7104        let mut reuse_from = reuse_from;
7105        if let Some(r) = &mut router {
7106            r.reset(); // active=backbone, matching a fresh overlay
7107            self.dyn_phi_seen = 0; // fresh φ EMA per generation
7108            if self.dyn_active.is_some() {
7109                // A real switch back to the backbone invalidates the
7110                // cache the reuse key was computed against.
7111                let _ = self.set_active_skill(None);
7112                reuse_from = 0;
7113                self.last_prefill_tokens = input_ids.len();
7114            }
7115        }
7116
7117        let mut all_ids = input_ids.to_vec();
7118        // A block-diffusion model's response penalties count the response
7119        // only (`CloverPenaltyState`), never the prompt.
7120        let penalty_from = if self.clover.is_some() { input_ids.len() } else { 0 };
7121        let mut generated = 0usize;
7122        let mut finish_reason = "max_tokens".to_string();
7123        let mut drafted = 0usize;
7124        let mut accepted = 0usize;
7125        // DeepSeek-V4's draft quality is strongly content-dependent.  Two
7126        // consecutive paid rounds with no extra token put it on a bounded
7127        // cooldown; predictable text keeps batching, ordinary prose falls
7128        // back to the exact walk instead of paying a slow draft forever.
7129        // Local to one generation so one difficult request cannot poison the
7130        // next one, and deliberately automatic — this is not a user knob.
7131        let mut dsv4_spec_bad = 0usize;
7132        let mut dsv4_spec_retry_at = 0usize;
7133        let mut confidence: Vec<f32> = Vec::new();
7134        let trace_on = self.trace;
7135        let calib_temp = self.calib_temp;
7136        let mut traces: Vec<TokenTrace> = Vec::new();
7137
7138        // ── Prefill: forward each prompt token once, KEEP the last hidden.
7139        //    Dense prefill runs in fused pairs (weights streamed once per
7140        //    two positions — bit-identical to sequential, proven by the
7141        //    pair tests). With MTP: warm the draft head on
7142        //    (hidden_p, token_{p+1}) pairs.
7143        let mut hidden = vec![0.0f32; self.hidden_size];
7144        let mut pos = reuse_from;
7145        // lm_head-in-graph is only sound when the very next logits
7146        // consumer is this loop's own (MTP and skill routing interleave
7147        // other forwards / can swap lm_head between forward and sample).
7148        // CMF_GPU_LMHEAD=0 keeps lm_head off the graph: the token reads back
7149        // the 8 KB hidden instead of ~1 MB of logits, and the head runs on
7150        // the host. A probe for how much of the graph's fixed per-token cost
7151        // is the logits readback (the layer sweep puts that fixed part at
7152        // 3.88 ms of an 18.5 ms frame).
7153        let fuse_lm = mtp.is_none()
7154            && router.is_none()
7155            && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
7156        self.graph_logits = None;
7157        self.graph_want_logits = false;
7158        let _tpf = std::time::Instant::now();
7159        let batch_k = self.generation_batch_k();
7160        if let Some(rows) = prompt_rows {
7161            let hs = self.hidden_size;
7162            // The batched graph's row count when the prompt rides it.
7163            let chunk = if batch_k > 0 { batch_k } else { self.prefill_chunk().max(1) };
7164            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
7165                let end = (pos + chunk).min(input_ids.len());
7166                let hb = match self.prefill_input_rows(
7167                    PrefillIn::Hidden(&rows[pos * hs..end * hs]),
7168                    pos,
7169                    task_mask,
7170                ) {
7171                    Ok(hb) => hb,
7172                    Err(err) => {
7173                        self.prompt_mrope = None;
7174                        self.finish_generation(&mut mtp, &mut router, true);
7175                        return Err(err);
7176                    }
7177                };
7178                if mimo_spec {
7179                    self.mimo_note_rows(&hb, pos);
7180                }
7181                hidden.copy_from_slice(&hb[hb.len() - hs..]);
7182                pos = end;
7183            }
7184        }
7185        // The prompt's MRoPE rows are in the cache; decode positions are
7186        // plain (`sensenova::mrope_positions` shifted them so).
7187        self.prompt_mrope = None;
7188        // DeepSeek-V4 owns a separate hyper-connection stack. Route it
7189        // before the generic prefill choices: those correctly reject an
7190        // empty `weights.layers`, but their final per-position fallback used
7191        // to consume the whole prompt before `dsv4::forward_chunk` could see
7192        // it. The batch implementation therefore existed without a live
7193        // production entry point.
7194        //
7195        // Bounded chunks preserve cancellation responsiveness. Only the
7196        // prompt's final chunk asks for logits; every earlier head projection
7197        // would produce 129 280 values that no caller reads.
7198        while self.qwen4_exp.is_some()
7199            && mtp.is_none()
7200            && pos < input_ids.len()
7201            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
7202        {
7203            // The device path takes several prompt tokens per layer frame;
7204            // the host path runs them one by one inside the same call.
7205            let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
7206            let want_logits = end == input_ids.len();
7207            let mut lg = Vec::new();
7208            if let Some(b) = &mut self.qwen4_exp {
7209                crate::qwen4_exp::forward_tokens(
7210                    &b.0,
7211                    &b.1,
7212                    &b.2,
7213                    &mut b.3,
7214                    &input_ids[pos..end],
7215                    pos,
7216                    &self.inv_freq,
7217                    self.pool.as_deref(),
7218                    &mut lg,
7219                    want_logits,
7220                );
7221            }
7222            if want_logits {
7223                self.graph_logits = Some(lg);
7224            }
7225            pos = end;
7226            hidden.fill(0.0);
7227        }
7228        while self.dsv4.is_some()
7229            && mtp.is_none()
7230            && pos < input_ids.len()
7231            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
7232        {
7233            let end = (pos + prefill_chunk()).min(input_ids.len());
7234            let ids: Vec<u32> = input_ids[pos..end].to_vec();
7235            let mut lg = Vec::new();
7236            if let Some(b) = &mut self.dsv4 {
7237                let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
7238                crate::dsv4::forward_chunk(
7239                    g,
7240                    layers,
7241                    &cfg,
7242                    st,
7243                    &ids,
7244                    pos,
7245                    &self.inv_freq,
7246                    self.pool.as_deref(),
7247                    &mut lg,
7248                    end == input_ids.len(),
7249                );
7250            }
7251            if end == input_ids.len() {
7252                self.graph_logits = Some(lg);
7253            }
7254            pos = end;
7255            hidden = vec![0.0; self.hidden_size];
7256        }
7257        if self.dory.is_some() && mtp.is_none() && pos < input_ids.len() {
7258            let lg = self.dory_forward(&input_ids[pos..], pos, crate::dory::Want::Last);
7259            self.graph_logits = Some(lg);
7260            pos = input_ids.len();
7261            hidden = vec![0.0; self.hidden_size];
7262        }
7263        let dsv41_prefill = self.dsv41_prefill.take();
7264        while self.dsv41.is_some()
7265            && mtp.is_none()
7266            && pos < input_ids.len()
7267            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
7268        {
7269            let end = (pos + prefill_chunk()).min(input_ids.len());
7270            let ids: Vec<u32> = input_ids[pos..end].to_vec();
7271            let mut lg = Vec::new();
7272            if let Some(b) = &mut self.dsv41 {
7273                let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
7274                if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
7275                    crate::dsv41::forward_chunk_masked_with_embeddings(
7276                        g,
7277                        layers,
7278                        cfg,
7279                        st,
7280                        &ids,
7281                        pos,
7282                        &embeddings[pos..end],
7283                        &participates[pos..end],
7284                        self.pool.as_deref(),
7285                        &mut lg,
7286                    );
7287                } else {
7288                    crate::dsv41::forward_chunk(
7289                        g,
7290                        layers,
7291                        cfg,
7292                        st,
7293                        &ids,
7294                        pos,
7295                        self.pool.as_deref(),
7296                        &mut lg,
7297                    );
7298                }
7299            }
7300            if end == input_ids.len() {
7301                self.graph_logits = Some(lg);
7302            }
7303            pos = end;
7304            hidden = vec![0.0; self.hidden_size];
7305        }
7306        // With dynamic routing, prefill sequentially so the φ hook fires
7307        // over the PROMPT — the router enters decode with a warm φ (the
7308        // fused-pair path skips the per-layer φ capture). o1 layers
7309        // collect their query trace in both the single and pair paths.
7310        let dyn_prefill = router.is_some();
7311        // Optional bounded calibration prefix for generation.  The normal
7312        // O(1) path seals after the full prompt; this explicit knob instead
7313        // runs only the requested prefix through exact attention, seals the
7314        // Nyström state, and streams the rest of the prompt through the same
7315        // O(1) step used by decode.  It keeps the O(1) layers' Q trace and
7316        // temporary full KV bounded by the prefix while leaving the default
7317        // full-prompt quality profile untouched.
7318        let o1_prefill_limit = o1_prefill
7319            .and_then(|requested| self.o1_effective_boundary(requested))
7320            .map(|boundary| boundary.min(input_ids.len()));
7321        let mut o1_sealed = false;
7322        let mut o1_arch: Option<O1Archive> = None;
7323        if let Some(limit) = o1_prefill_limit {
7324            // On the device route the prefix takes the batch graph and its
7325            // sealed suffix the O(1) batches after it (`o1_prefix_on_graph`):
7326            // the host walk read it at 16 tok/s on Qwen3.8-27B / A40, 33 s
7327            // of a 72 s TTFT at 4k. Only when a suffix follows — a prompt
7328            // inside the boundary stays exact on the host as before.
7329            // `CMF_O1_PREFIX_GPU=0` keeps the host prefill.
7330            if pos == 0
7331                && limit < input_ids.len()
7332                && batch_k > 0
7333                && task_mask.is_none()
7334                && (mtp.is_none() || self.o1_spec())
7335                && !dyn_prefill
7336                && self.o1_on_graph()
7337                && std::env::var("CMF_O1_PREFIX_GPU").as_deref() != Ok("0")
7338                && (0..self.num_layers).all(|li| self.phys_layer(li) == li)
7339            {
7340                let t_px = std::time::Instant::now();
7341                // More of the prompt than the first seal covers: its rows stay in host
7342                // memory, and the far field is built anew before the prompt's last rows.
7343                let tail_from = input_ids.len().saturating_sub(crate::dory::EXACT_TAIL);
7344                if limit < tail_from && self.o1_exact_cfg().is_some() {
7345                    o1_arch = Some(O1Archive::new());
7346                }
7347                // Without a Metal rows graph (Spark-X2.6: GELU, per-head gates, sliding
7348                // windows) the sealed rows would go one position at a time: 14 000 tokens
7349                // on the M4 took 219 s. The chunk graph reads the whole prompt exact
7350                // instead, and the seal after it chooses among all of its rows, with the
7351                // question's queries in the trace.
7352                #[cfg(target_os = "macos")]
7353                let px_len = if crate::gpu::q1_force() && o1_arch.is_some() && self.metal_rows_plan().is_none() {
7354                    o1_arch = None;
7355                    let n = input_ids.len();
7356                    for li in 0..self.num_layers {
7357                        if self.o1_flags.get(li).copied().unwrap_or(false) {
7358                            self.kv_cache.layers[li].o1_set_boundary(n);
7359                        }
7360                    }
7361                    n
7362                } else {
7363                    limit
7364                };
7365                #[cfg(not(target_os = "macos"))]
7366                let px_len = if self.o1_whole_prompt() {
7367                    o1_arch = None;
7368                    let n = input_ids.len();
7369                    for li in 0..self.num_layers {
7370                        if self.o1_flags.get(li).copied().unwrap_or(false) {
7371                            self.kv_cache.layers[li].o1_set_boundary(n);
7372                        }
7373                    }
7374                    n
7375                } else {
7376                    limit
7377                };
7378                #[cfg(target_os = "macos")]
7379                let prefix = if crate::gpu::q1_force() {
7380                    self.o1_prefix_on_metal(&input_ids[..px_len], batch_k, o1_arch.as_mut())
7381                } else {
7382                    self.o1_prefix_on_graph(&input_ids[..px_len], batch_k, o1_arch.as_mut())
7383                };
7384                #[cfg(not(target_os = "macos"))]
7385                let prefix = {
7386                    // the CUDA mirrors take the whole prompt at the first chunk
7387                    let hint = crate::gpu::cuda_active();
7388                    if hint {
7389                        crate::gpu::set_batch_kv_hint(px_len + 1);
7390                    }
7391                    let r = self.o1_prefix_on_graph(&input_ids[..px_len], batch_k, o1_arch.as_mut());
7392                    if hint {
7393                        crate::gpu::set_batch_kv_hint(0);
7394                    }
7395                    r
7396                };
7397                match prefix {
7398                    Ok(Some(last)) => {
7399                        hidden.copy_from_slice(&last);
7400                        pos = px_len;
7401                        // the whole prompt went by the prefix (CUDA): the draft head
7402                        // reads its tail, as over the wgpu path's far field
7403                        let tail = std::mem::take(&mut self.o1_prefix_tail);
7404                        if px_len == input_ids.len() && mtp.is_some() && !tail.is_empty() {
7405                            let hs = self.hidden_size;
7406                            let from = px_len - tail.len() / hs;
7407                            self.mtp_pos_base = from;
7408                            let n_pairs = mtp_prefill_pair_count(from, px_len, input_ids.len());
7409                            let pairs: Vec<(&[f32], u32)> = (0..n_pairs).map(|j| (&tail[j * hs..(j + 1) * hs], input_ids[from + j + 1])).collect();
7410                            let warm_error = if let Some(m) = mtp.as_mut() { self.mtp_warm_prefill_pairs(m, &pairs, from).err() } else { None };
7411                            if let Some(err) = warm_error {
7412                                self.finish_generation(&mut mtp, &mut router, true);
7413                                return Err(err.to_string());
7414                            }
7415                            self.mtp_warm_base = Some(from);
7416                        }
7417                        if std::env::var("CMF_PREFILL_PROF").is_ok() {
7418                            eprintln!(
7419                                "o1 prefix on the batch graph: {px_len} rows in {:.1} ms",
7420                                t_px.elapsed().as_secs_f64() * 1e3
7421                            );
7422                        }
7423                    }
7424                    Ok(None) => {}
7425                    Err(err) => {
7426                        self.finish_generation(&mut mtp, &mut router, true);
7427                        return Err(err);
7428                    }
7429                }
7430            }
7431            // Without the device route (LFM2's short convolutions on Metal) the rows
7432            // past the first seal would stream into the far field unchosen. With exact
7433            // rows wanted, the host keeps the prompt exact up to its last rows and
7434            // chooses among all of them there, as the archive does on the device.
7435            let tail_from = input_ids.len().saturating_sub(crate::dory::EXACT_TAIL);
7436            let limit = if pos == 0 && limit < tail_from && self.o1_exact_cfg().is_some() {
7437                o1_arch = None;
7438                for li in 0..self.num_layers {
7439                    if self.o1_flags.get(li).copied().unwrap_or(false) {
7440                        self.kv_cache.layers[li].o1_set_boundary(tail_from);
7441                    }
7442                }
7443                tracing::info!("o1: no device prefix; exact on the host up to row {tail_from}, then the seal");
7444                tail_from
7445            } else {
7446                limit
7447            };
7448            // Reuse the exact batched prefix machinery when available; it
7449            // records the same per-position Q trace as the full prefill.
7450            if pos < limit && self.can_prefill_batched() && limit > 2 {
7451                let chunk = self.prefill_chunk();
7452                let hs = self.hidden_size;
7453                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
7454                    let end = (pos + chunk).min(limit);
7455                    let hb = self.prefill_batch(&input_ids[pos..end], pos);
7456                    hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
7457                    pos = end;
7458                }
7459            } else {
7460                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
7461                    hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
7462                    pos += 1;
7463                }
7464            }
7465            if pos >= limit {
7466                o1_sealed = match self.o1_seal_checked() {
7467                    Ok(sealed) => sealed,
7468                    Err(err) => {
7469                        self.finish_generation(&mut mtp, &mut router, true);
7470                        return Err(err);
7471                    }
7472                };
7473                // Sealed layers read no mirror again: their rows leave the memory they share
7474                // with everything else on a Mac.
7475                #[cfg(target_os = "macos")]
7476                if o1_sealed && crate::gpu::q1_force() {
7477                    crate::gpu_metal::kv_mirror_drop(self.graph_kv_id);
7478                }
7479                tracing::info!(
7480                    "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
7481                    o1_prefill.unwrap_or(0),
7482                    self.o1_effective_boundary(o1_prefill.unwrap_or(0))
7483                        .unwrap_or(limit),
7484                    limit,
7485                    input_ids.len()
7486                );
7487            }
7488        }
7489        // q1 hybrids on Metal: the per-position GPU token graph beats
7490        // the CPU chunk-GEMM (whose wall is the sequential scalar GDN
7491        // recurrence), so prefill goes position-by-position through the
7492        // same graph as decode. Pure-attention models keep the batched
7493        // path — there the chunk-GEMM amortization wins.
7494        let graph_prefill = self.graph_prefill_preferred();
7495        // Native Metal, q4tp GDN hybrids: the prompt through the b-row
7496        // rows graph — projections as GEMMs over up to 512 positions, the
7497        // GDN recurrence in registers on the device, K/V rows appended by
7498        // the chunk — instead of one token-graph submit per position (the
7499        // 27B: 8 tok/s → GEMM-bound). The MTP warm-up rows come out of one
7500        // batched run of the block per chunk. Any refusal leaves the rest
7501        // of the prompt to the sequential paths below. Pure-attention
7502        // models take it too (the plan admits their layers): CherryPit-1.2-2B
7503        // read an 8 192-token prompt on the M4 in 31 s instead of the host
7504        // walk's 58 s (q4tp; q8_2f 34 s against 55), at the same WikiText-2
7505        // ppl. `CMF_METAL_PREFILL=0` keeps the host walk.
7506        #[cfg(target_os = "macos")]
7507        if task_mask.is_none()
7508            && !dyn_prefill
7509            && !o1_sealed
7510            && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
7511            && crate::gpu::enabled_here()
7512            && !self.per_token_stack()
7513            && input_ids.len() > 8
7514            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
7515            && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
7516        {
7517            let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
7518                .ok()
7519                .and_then(|v| v.parse().ok())
7520                .filter(|&v| (16..=512).contains(&v))
7521                // The ANE's weights go once a chunk: a larger one pays them over more rows.
7522                .unwrap_or(if crate::gpu_metal::ane::wanted() { 512 } else { 256 });
7523            let hs = self.hidden_size;
7524            let _tp = std::time::Instant::now();
7525            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
7526                let end = (pos + chunk).min(input_ids.len());
7527                let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
7528                    MetalPrefillOutcome::Completed(hb) => hb,
7529                    MetalPrefillOutcome::Declined => break,
7530                    MetalPrefillOutcome::Failed => {
7531                        self.finish_generation(&mut mtp, &mut router, true);
7532                        return Err("ordinary Metal prefill failed after admission".into());
7533                    }
7534                };
7535                if let Some(m) = &mut mtp {
7536                    let n_pairs = if end < input_ids.len() {
7537                        end - pos
7538                    } else {
7539                        end - pos - 1
7540                    };
7541                    if n_pairs > 0 {
7542                        let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
7543                            .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
7544                            .collect();
7545                        if !self.mtp_warm_batch_metal(m, &pairs, pos) {
7546                            for (j, (h, t)) in pairs.iter().enumerate() {
7547                                let h = h.to_vec();
7548                                let _ = self.mtp_step(m, &h, *t, pos + j);
7549                            }
7550                        }
7551                    }
7552                }
7553                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
7554                pos = end;
7555            }
7556            if std::env::var("CMF_PREFILL_PROF").is_ok() {
7557                eprintln!(
7558                    "metal-prefill: {} of {} tokens in {:.1} ms",
7559                    pos,
7560                    input_ids.len(),
7561                    _tp.elapsed().as_secs_f64() * 1e3
7562                );
7563                // Per-stage GPU time of the rows graph (`CMF_VERIFY_CUT=1`).
7564                crate::gpu_metal::cut_report();
7565            }
7566        }
7567        // Gemma-4 on a discrete card: the prompt through the batched wgpu
7568        // graph (`g4_batch_ingest`); its K/V land in the device mirrors
7569        // decode reads. A refusal leaves the rest of the prompt to the
7570        // chunked host prefill below.
7571        #[cfg(not(target_os = "macos"))]
7572        if task_mask.is_none()
7573            && !dyn_prefill
7574            && mtp.is_none()
7575            && o1_prefill.is_none()
7576            && batch_k > 0
7577            && pos + 1 < input_ids.len()
7578            && self.g4_batch_prefill()
7579        {
7580            let t_g4 = std::time::Instant::now();
7581            match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
7582                Ok((n, last)) => {
7583                    if n > 0 {
7584                        hidden.copy_from_slice(&last);
7585                    }
7586                    if std::env::var("CMF_PREFILL_PROF").is_ok() {
7587                        eprintln!(
7588                            "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
7589                            input_ids.len() - pos,
7590                            t_g4.elapsed().as_secs_f64() * 1e3
7591                        );
7592                    }
7593                    pos += n;
7594                }
7595                Err(e) => {
7596                    self.finish_generation(&mut mtp, &mut router, true);
7597                    return Err(e);
7598                }
7599            }
7600        }
7601        self.mimo_moe_prepare();
7602        // A MoE stack larger than the card (MiMo-V2 q4tp on 96 GB): the
7603        // batched wgpu graph runs the device prefix of every chunk — its
7604        // experts resident — and the host's batched layer walk finishes
7605        // the chunk. Any refusal leaves the rest of the prompt to the
7606        // chunked prefill below.
7607        #[cfg(not(target_os = "macos"))]
7608        if task_mask.is_none()
7609            && !dyn_prefill
7610            && !graph_prefill
7611            && mtp.is_none()
7612            && o1_prefill.is_none()
7613            && !self.o1_active()
7614            && input_ids.len() > 2
7615            && self.batch_prefix_prefill()
7616        {
7617            let chunk = self.prefill_chunk().max(1);
7618            let hs = self.hidden_size;
7619            let t_bp = std::time::Instant::now();
7620            let pos0 = pos;
7621            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
7622                let end = (pos + chunk).min(input_ids.len());
7623                let bk = end - pos;
7624                let mut hiddens = vec![0f32; bk * hs];
7625                for (j, &id) in input_ids[pos..end].iter().enumerate() {
7626                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
7627                }
7628                let positions: Vec<usize> = (pos..end).collect();
7629                let mut run = 0usize;
7630                let outcome = self.try_batch_graph_wgpu_prefix(
7631                    &mut hiddens,
7632                    &positions,
7633                    bk,
7634                    None,
7635                    Some(&mut run),
7636                );
7637                match outcome {
7638                    crate::gpu::BatchGraphOutcome::Completed => {
7639                        let hb = if run < self.num_layers {
7640                            self.prefill_batch_span(
7641                                PrefillIn::Hidden(&hiddens),
7642                                pos,
7643                                None,
7644                                run,
7645                                self.num_layers,
7646                            )
7647                        } else {
7648                            hiddens
7649                        };
7650                        if mimo_spec {
7651                            self.mimo_note_rows(&hb, pos);
7652                        }
7653                        hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
7654                        pos = end;
7655                    }
7656                    crate::gpu::BatchGraphOutcome::Failed => {
7657                        self.finish_generation(&mut mtp, &mut router, true);
7658                        return Err("batched prefix prefill failed after admission".into());
7659                    }
7660                    crate::gpu::BatchGraphOutcome::Declined => {
7661                        // Earlier chunks left their prefix rows on the
7662                        // device only: the host walk below needs them.
7663                        #[cfg(feature = "gpu")]
7664                        if pos > pos0 {
7665                            self.pull_lagging_host_kv(0, self.num_layers, pos);
7666                        }
7667                        break;
7668                    }
7669                }
7670            }
7671            if std::env::var("CMF_PREFILL_PROF").is_ok() {
7672                eprintln!(
7673                    "batch-prefix prefill: {} of {} tokens in {:.1} ms",
7674                    pos - pos0,
7675                    input_ids.len(),
7676                    t_bp.elapsed().as_secs_f64() * 1e3
7677                );
7678            }
7679        }
7680        if task_mask.is_none()
7681            && !dyn_prefill
7682            && !graph_prefill
7683            && self.can_prefill_batched()
7684            && !self.per_token_stack()
7685            && o1_prefill.is_none()
7686            && input_ids.len() > 2
7687        {
7688            // Production prefill = the same chunked prefill-GEMM that
7689            // bench/PPL measure (roadmap §3 P0: generation used to warm
7690            // the prompt with the slower pair path — the published
7691            // prefill number didn't match real TTFT). MTP warm-up reads
7692            // each position's hidden straight from the chunk result.
7693            let chunk = self.prefill_chunk();
7694            let hs = self.hidden_size;
7695            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
7696                let end = (pos + chunk).min(input_ids.len());
7697                let hb = self.prefill_batch(&input_ids[pos..end], pos);
7698                if mimo_spec {
7699                    self.mimo_note_rows(&hb, pos);
7700                }
7701                if let Some(m) = &mut mtp {
7702                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
7703                        .ok()
7704                        .and_then(|v| v.parse().ok())
7705                        .unwrap_or(0);
7706                    for p in pos..end {
7707                        if p + 1 < input_ids.len() {
7708                            if probe >= 1 && p + 2 < input_ids.len() {
7709                                // Teacher-forced chain acceptance (see the
7710                                // tail loop's twin): the warm-up row stays,
7711                                // the chain's rows roll back.
7712                                let (d1, mut hx) = self.mtp_step_h(
7713                                    m,
7714                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
7715                                    input_ids[p + 1],
7716                                    p,
7717                                );
7718                                let mut ok = d1 == input_ids[p + 2];
7719                                Self::chain_probe_note(0, ok);
7720                                let mut d_prev = d1;
7721                                let mut extra = 0usize;
7722                                for j in 1..probe {
7723                                    if p + 2 + j >= input_ids.len() {
7724                                        break;
7725                                    }
7726                                    let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
7727                                    extra += 1;
7728                                    ok = ok && dj == input_ids[p + 2 + j];
7729                                    Self::chain_probe_note(j, ok);
7730                                    d_prev = dj;
7731                                    hx = hj;
7732                                }
7733                                m.kv.truncate_last(extra);
7734                            } else {
7735                                let _ = self.mtp_step(
7736                                    m,
7737                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
7738                                    input_ids[p + 1],
7739                                    p,
7740                                );
7741                            }
7742                        }
7743                    }
7744                }
7745                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
7746                pos = end;
7747            }
7748        }
7749        let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
7750        if task_mask.is_none()
7751            && !dyn_prefill
7752            && !graph_prefill
7753            && !pair_off
7754            && self.pair_supported()
7755            && o1_prefill.is_none()
7756        {
7757            while pos + 1 < input_ids.len()
7758                && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
7759            {
7760                let e1 = self.embed_single(input_ids[pos]);
7761                let e2 = self.embed_single(input_ids[pos + 1]);
7762                let (h1, h2) = self.forward_pair(&e1, &e2, pos);
7763                if mimo_spec {
7764                    self.mimo_note_rows(&h1, pos);
7765                    self.mimo_note_rows(&h2, pos + 1);
7766                }
7767                // Both prefill tokens are real → commit lane-2 states.
7768                self.commit_linear_scratch();
7769                if let Some(m) = &mut mtp {
7770                    let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
7771                    if pos + 2 < input_ids.len() {
7772                        let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
7773                            .ok()
7774                            .and_then(|v| v.parse().ok())
7775                            .unwrap_or(0);
7776                        if probe >= 1 && pos + 3 < input_ids.len() {
7777                            // Same teacher-forced chain table as the tail
7778                            // loop below, fed from the pair path that owns
7779                            // most prefill positions.
7780                            let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
7781                            let mut ok = d1 == input_ids[pos + 3];
7782                            Self::chain_probe_note(0, ok);
7783                            let mut d_prev = d1;
7784                            let mut extra = 0usize;
7785                            for j in 1..probe {
7786                                if pos + 3 + j >= input_ids.len() {
7787                                    break;
7788                                }
7789                                let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
7790                                extra += 1;
7791                                ok = ok && dj == input_ids[pos + 3 + j];
7792                                Self::chain_probe_note(j, ok);
7793                                d_prev = dj;
7794                                hx = hj;
7795                            }
7796                            m.kv.truncate_last(extra);
7797                        } else {
7798                            let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
7799                        }
7800                    }
7801                }
7802                hidden = h2;
7803                pos += 2;
7804            }
7805        }
7806        // Batched GPU prefill for the wgpu decode graph (GDN hybrids): K prompt
7807        // positions per submit — projections/FFN as GEMMs (weight once per K),
7808        // attention/GDN looped inside — instead of one whole-graph submit per
7809        // position. Falls through to the per-position graph on any refusal.
7810        // Batched prefill is opt-in (CMF_BATCH_K>0). Default 0 = per-position
7811        // graph prefill. (Steady-state decode is provably identical either way —
7812        // token-graph submit and lm_head both unchanged — so this only trades
7813        // prefill wall.)
7814        // A bounded O(1) prefix is the one post-seal prompt interval: only
7815        // admit its batch when the device O(1) route is explicitly enabled and
7816        // every sealed layer exposes a portable view. The same batch size and
7817        // refusal behavior remain the ordinary controls/comparator.
7818        let o1_batch_ready = o1_sealed
7819            && o1_prefill.is_some()
7820            && (mtp.is_none() || self.o1_spec())
7821            && self.o1_on_graph()
7822            && (0..self.num_layers).all(|li| {
7823                let cache = &self.kv_cache.layers[self.phys_layer(li)];
7824                cache.o1.is_none() || cache.o1_views().is_some()
7825            });
7826        // The ordinary graph-prefill route can share each completed trunk
7827        // chunk with an attached MTP head.  Keep chain probing on its
7828        // established per-position path: the probe deliberately needs every
7829        // teacher-forced draft row and its rollback table.
7830        let mtp_batch_prefill = mtp.is_some()
7831            && graph_prefill
7832            && task_mask.is_none()
7833            && !dyn_prefill
7834            && !self.o1_active()
7835            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
7836        // A hint a failed earlier prompt left behind must not size this one.
7837        #[cfg(not(target_os = "macos"))]
7838        crate::gpu::set_batch_kv_hint(0);
7839        if batch_k > 0
7840            && (graph_prefill || o1_batch_ready)
7841            && task_mask.is_none()
7842            && (!self.o1_active() || o1_batch_ready)
7843            && (mtp.is_none() || mtp_batch_prefill || o1_batch_ready)
7844            && !dyn_prefill
7845            && pos + 1 < input_ids.len()
7846        {
7847            let hs = self.hidden_size;
7848            let chunk = batch_k;
7849            let pos_start = pos;
7850            let tail_from = input_ids.len().saturating_sub(crate::dory::EXACT_TAIL);
7851            // The K/V mirrors take the whole prompt (and its first token) at
7852            // the first chunk instead of growing on the way; cleared below.
7853            // Not under O(1): its exact mirror must stay the tail it keeps.
7854            #[cfg(not(target_os = "macos"))]
7855            if !self.o1_active() && o1_arch.is_none() && !o1_batch_ready {
7856                crate::gpu::set_batch_kv_hint(input_ids.len() + 1);
7857            }
7858            if let Some(a) = o1_arch.as_ref() {
7859                crate::gpu::o1_acap_begin(a.stride);
7860            }
7861            if o1_batch_ready && mtp.is_some() {
7862                self.mtp_pos_base = tail_from.max(pos);
7863            }
7864            // a chunk ends at the chat snapshot's row, the card copies the
7865            // GDN states there before the next chunk runs
7866            let snap_at = if o1_arch.is_none() && !o1_batch_ready { self.gdn_snap_target(input_ids, pos) } else { None };
7867            while pos < input_ids.len() {
7868                // The prompt's last rows read the rows chosen just before them.
7869                if pos >= tail_from {
7870                    if let Some(a) = o1_arch.take() {
7871                        crate::gpu::o1_acap_end();
7872                        if let Err(err) = self.o1_seal_archive(a) {
7873                            self.finish_generation(&mut mtp, &mut router, true);
7874                            return Err(err);
7875                        }
7876                    }
7877                }
7878                let stop = if o1_arch.is_some() { tail_from } else { input_ids.len() };
7879                let end = match snap_at {
7880                    Some(s) if pos < s => (pos + chunk).min(stop).min(s),
7881                    _ => (pos + chunk).min(stop),
7882                };
7883                let bk = end - pos;
7884                let mut hiddens = vec![0f32; bk * hs];
7885                for (j, &id) in input_ids[pos..end].iter().enumerate() {
7886                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
7887                }
7888                let positions: Vec<usize> = (pos..end).collect();
7889                let t_chunk = std::time::Instant::now();
7890                // A chunk before the last whose rows nothing reads: the
7891                // graph submits it and returns, and this loop encodes the
7892                // next chunk while the card runs it.
7893                #[cfg(not(target_os = "macos"))]
7894                crate::gpu::set_batch_defer(
7895                    end < input_ids.len()
7896                        && o1_arch.is_none()
7897                        && !o1_batch_ready
7898                        && !mimo_spec
7899                        && !mtp_batch_prefill,
7900                );
7901                let outcome = self.o1_prompt_chunk(&mut hiddens, &positions, bk);
7902                #[cfg(not(target_os = "macos"))]
7903                crate::gpu::set_batch_defer(false);
7904                if let (Some(a), crate::gpu::BatchGraphOutcome::Completed) = (o1_arch.as_mut(), outcome) {
7905                    if let Err(err) = self.o1_archive_chunk(a, pos, bk) {
7906                        self.finish_generation(&mut mtp, &mut router, true);
7907                        return Err(err);
7908                    }
7909                }
7910                let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
7911                // The draft head over a far field reads the prompt's tail only.
7912                if ok_b && o1_batch_ready && end > self.mtp_pos_base && mtp.is_some() {
7913                    let first = pos.max(self.mtp_pos_base);
7914                    let n_pairs = mtp_prefill_pair_count(first, end, input_ids.len());
7915                    if n_pairs > 0 {
7916                        let rows: Vec<Vec<f32>> = (0..n_pairs).map(|j| hiddens[(first - pos + j) * hs..(first - pos + j + 1) * hs].to_vec()).collect();
7917                        let pairs: Vec<(&[f32], u32)> = rows.iter().enumerate().map(|(j, row)| (row.as_slice(), input_ids[first + j + 1])).collect();
7918                        #[cfg(target_os = "macos")]
7919                        let warmed = crate::gpu::q1_force() && {
7920                            if let Some(m) = mtp.as_mut() {
7921                                if !self.mtp_warm_batch_metal(m, &pairs, first) {
7922                                    for (j, (h, t)) in pairs.iter().enumerate() {
7923                                        let h = h.to_vec();
7924                                        let _ = self.mtp_step(m, &h, *t, first + j);
7925                                    }
7926                                }
7927                            }
7928                            true
7929                        };
7930                        #[cfg(not(target_os = "macos"))]
7931                        let warmed = false;
7932                        let warm_error = if warmed {
7933                            None
7934                        } else if let Some(m) = mtp.as_mut() {
7935                            self.mtp_warm_prefill_pairs(m, &pairs, first).err()
7936                        } else {
7937                            None
7938                        };
7939                        if let Some(err) = warm_error {
7940                            self.finish_generation(&mut mtp, &mut router, true);
7941                            return Err(err.to_string());
7942                        }
7943                    }
7944                }
7945                if std::env::var("CMF_GRAPH_PROF").is_ok() {
7946                    let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
7947                    eprintln!(
7948                        "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
7949                        if o1_batch_ready {
7950                            "o1"
7951                        } else if mtp_batch_prefill {
7952                            "ordinary_mtp"
7953                        } else {
7954                            "ordinary"
7955                        },
7956                        bk as f64 / (ms / 1000.0)
7957                    );
7958                }
7959                {
7960                    use std::sync::atomic::{AtomicBool, Ordering};
7961                    static SAID: AtomicBool = AtomicBool::new(false);
7962                    if !SAID.swap(true, Ordering::Relaxed) {
7963                        if ok_b {
7964                            tracing::info!(
7965                                "batched prefill: ACTIVE mode={} (k={bk})",
7966                                if o1_batch_ready {
7967                                    "o1"
7968                                } else if mtp_batch_prefill {
7969                                    "ordinary_mtp"
7970                                } else {
7971                                    "ordinary"
7972                                }
7973                            );
7974                        } else {
7975                            tracing::warn!("batched prefill {:?} — per-position graph", outcome);
7976                        }
7977                    }
7978                }
7979                if ok_b {
7980                    if mimo_spec {
7981                        self.mimo_note_rows(&hiddens, pos);
7982                    }
7983                    if mtp_batch_prefill {
7984                        let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
7985                        if n_pairs > 0 {
7986                            // `hiddens` is owned by this chunk, so materialize
7987                            // row slices before borrowing the detached MTP
7988                            // module.  The last prompt row has no successor;
7989                            // the helper above is the single source of that
7990                            // boundary rule.
7991                            let rows: Vec<Vec<f32>> = (0..n_pairs)
7992                                .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
7993                                .collect();
7994                            let pairs: Vec<(&[f32], u32)> = rows
7995                                .iter()
7996                                .enumerate()
7997                                .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
7998                                .collect();
7999                            if std::env::var("CMF_GRAPH_PROF").is_ok() {
8000                                eprintln!(
8001                                    "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
8002                                    pos,
8003                                    n_pairs,
8004                                    pos + n_pairs - 1,
8005                                );
8006                            }
8007                            let warm_error = if let Some(m) = mtp.as_mut() {
8008                                self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
8009                            } else {
8010                                None
8011                            };
8012                            if let Some(err) = warm_error {
8013                                // The trunk batch was already admitted.  A
8014                                // failed MTP warm-up therefore clears both
8015                                // mirrors and exits; continuing would pair a
8016                                // current trunk state with a stale MTP cache.
8017                                self.finish_generation(&mut mtp, &mut router, true);
8018                                return Err(err.to_string());
8019                            }
8020                        }
8021                    }
8022                    hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
8023                    pos = end;
8024                    if snap_at == Some(end) {
8025                        self.gdn_snap = crate::gpu::gdn_snapshot(self.graph_kv_id, end).then(|| input_ids[..end].to_vec());
8026                    }
8027                } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
8028                    // A failed batch may have advanced a device recurrent
8029                    // state (ordinary GDN or sealed O(1)). A CPU fallback
8030                    // would then observe stale accumulators, so clear the
8031                    // request state and make the failure explicit.
8032                    self.finish_generation(&mut mtp, &mut router, true);
8033                    return Err(if o1_batch_ready {
8034                        "sealed O(1) batch graph failed after admission".to_string()
8035                    } else {
8036                        "ordinary recurrent batch graph failed after admission".to_string()
8037                    });
8038                } else {
8039                    break; // unsupported → per-position graph handles the rest
8040                }
8041            }
8042            if mtp_batch_prefill && mtp.is_some() && pos == input_ids.len() {
8043                self.mtp_warm_base = Some(self.mtp_pos_base);
8044            }
8045            // the CUDA DFlash block is built once the trunk's kernels are, so
8046            // its first round does not pay it
8047            #[cfg(feature = "cuda")]
8048            if crate::gpu::cuda_active() {
8049                if let Some(d) = &self.dflash {
8050                    let _ = d.dev_ready();
8051                }
8052            }
8053            #[cfg(target_os = "macos")]
8054            if std::env::var("CMF_PREFILL_PROF").is_ok() {
8055                crate::gpu_metal::cut_report();
8056            }
8057            // A dense stack the batched graph does not take reads the prompt chunked
8058            // on the host walk (its GEMMs on the card), as before the graph took dense
8059            // prompts, instead of one position at a time.
8060            if pos == pos_start
8061                && graph_prefill
8062                && !o1_batch_ready
8063                && mtp.is_none()
8064                && !mimo_spec
8065                && self.can_prefill_batched()
8066                && !self.per_token_stack()
8067                && self.weights.layers.iter().all(|lw| {
8068                    !matches!(&lw.attn, AttnKind::LinearGdn(_)) && !matches!(&lw.ffn, FfnKind::Moe(_))
8069                })
8070            {
8071                self.dense_batch_declined.store(true, std::sync::atomic::Ordering::Relaxed);
8072                let chunk = self.prefill_chunk();
8073                while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
8074                    let end = (pos + chunk).min(input_ids.len());
8075                    let hb = self.prefill_batch(&input_ids[pos..end], pos);
8076                    hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8077                    pos = end;
8078                }
8079            }
8080        }
8081        // Resident Embryo graph: the prompt in chunks of one submit each
8082        // instead of one whole-graph submit per position; the last chunk
8083        // carries the logits exactly as the per-position walk would.
8084        if graph_prefill
8085            && task_mask.is_none()
8086            && mtp.is_none()
8087            && !dyn_prefill
8088            && pos == 0
8089            && input_ids.len() > 1
8090            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
8091        {
8092            if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
8093                self.graph_logits = Some(lg);
8094                hidden = vec![0.0; self.hidden_size];
8095                pos = input_ids.len();
8096            }
8097        }
8098        while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
8099            self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
8100            hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
8101            if mimo_spec {
8102                self.mimo_note_rows(&hidden, pos);
8103            }
8104            if let Some(m) = &mut mtp {
8105                if pos + 1 < input_ids.len() {
8106                    // `CMF_MTP_CHAIN_PROBE=k`: teacher-forced acceptance of a
8107                    // CHAINED draft — iterate the head on its own hidden k
8108                    // deep and score every depth against the prompt's real
8109                    // continuation. The economics of a k-token speculative
8110                    // round stand or fall on this table.
8111                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
8112                        .ok()
8113                        .and_then(|v| v.parse().ok())
8114                        .unwrap_or(0);
8115                    if probe >= 1 && pos + 2 < input_ids.len() {
8116                        let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
8117                        let mut ok = d1 == input_ids[pos + 2];
8118                        Self::chain_probe_note(0, ok);
8119                        let mut d_prev = d1;
8120                        let mut extra = 0usize;
8121                        for j in 1..probe {
8122                            if pos + 2 + j >= input_ids.len() {
8123                                break;
8124                            }
8125                            let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
8126                            extra += 1;
8127                            ok = ok && dj == input_ids[pos + 2 + j];
8128                            Self::chain_probe_note(j, ok);
8129                            d_prev = dj;
8130                            hx = hj;
8131                        }
8132                        // The chain's rows are speculation, not the prompt —
8133                        // keep only the warmup row the plain path would add.
8134                        m.kv.truncate_last(extra);
8135                    } else {
8136                        let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
8137                    }
8138                }
8139            }
8140            pos += 1;
8141        }
8142        #[cfg(not(target_os = "macos"))]
8143        crate::gpu::set_batch_kv_hint(0);
8144        if std::env::var("CMF_PREFILL_PROF").is_ok() {
8145            eprintln!(
8146                "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
8147                input_ids.len(),
8148                _tpf.elapsed().as_secs_f64() * 1000.0
8149            );
8150        }
8151        // The Neural Engine read the prompt; decode runs on the GPU, so its surfaces go back
8152        // to the weight map (`ane::release_idle`).
8153        #[cfg(target_os = "macos")]
8154        crate::gpu_metal::ane::release_idle();
8155        if self
8156            .graph_failed
8157            .swap(false, std::sync::atomic::Ordering::Relaxed)
8158        {
8159            // MTP is detached for speculative generation.  Restore the
8160            // module before returning the terminal graph error; otherwise a
8161            // failed request would silently remove the head from a pooled
8162            // pipeline and the next request would lose its configured route.
8163            self.finish_generation(&mut mtp, &mut router, true);
8164            return Err("GPU token graph failed during prefill".to_string());
8165        }
8166        // Cancelled mid-prefill: the cache holds a partial prompt —
8167        // drop the reuse history and return an empty generation.
8168        if self
8169            .cancel
8170            .swap(false, std::sync::atomic::Ordering::Relaxed)
8171        {
8172            // A cancelled prefill can already have advanced the device
8173            // mirror. Drop the whole partial sequence so a pooled pipeline
8174            // cannot carry that state into its next request.
8175            self.finish_generation(&mut mtp, &mut router, true);
8176            return Ok(GenerateResult {
8177                text: String::new(),
8178                token_ids: Vec::new(),
8179                prompt_tokens: input_ids.len(),
8180                tokens_generated: 0,
8181                finish_reason: "cancelled".to_string(),
8182                mtp_drafted: 0,
8183                mtp_accepted: 0,
8184                token_confidence: Vec::new(),
8185                traces: Vec::new(),
8186            });
8187        }
8188
8189        // Prompt absorbed → freeze the o1 layers' skeletons; from here
8190        // every decode step on those layers is O(W + m·dv + m²).
8191        if !o1_sealed {
8192            match self.o1_seal_checked() {
8193                Ok(_) => {}
8194                Err(err) => {
8195                    self.finish_generation(&mut mtp, &mut router, true);
8196                    return Err(err);
8197                }
8198            }
8199        }
8200
8201        // Bytes of a character the last streamed token left unfinished.
8202        let mut utf8_pending: Vec<u8> = Vec::new();
8203        // Commit one token: push, check EOS, stream. Returns false = stop.
8204        macro_rules! commit {
8205            ($id:expr) => {{
8206                all_ids.push($id);
8207                generated += 1;
8208                self.note_draft_id($id);
8209                if self.tokenizer.is_eos($id) && !self.ignore_eos {
8210                    finish_reason = "stop".to_string();
8211                    false
8212                } else {
8213                    let token_text = self.tokenizer.decode_token_stream($id, &mut utf8_pending);
8214                    let mut go = true;
8215                    if let Some(ref mut cb) = on_token {
8216                        if !cb(&token_text) {
8217                            finish_reason = "cancelled".to_string();
8218                            go = false;
8219                        }
8220                    }
8221                    if go
8222                        && self.sampler_config.loop_stop
8223                        && crate::sampler::tail_repeats(&all_ids[all_ids.len() - generated..])
8224                    {
8225                        finish_reason = "loop".to_string();
8226                        go = false;
8227                    }
8228                    go
8229                }
8230            }};
8231        }
8232
8233        // Speculation is decided by MEASUREMENT, not by an acceptance
8234        // model. A k=4 round costs ~3.8 plain tokens on the 5090 (draft
8235        // 6.6 + verify 66.6 + commit 4.8 ms against a 20.6 ms token), so it
8236        // pays only when the head lands ~2.8 of 4 — predictable text (code,
8237        // structured output) does, free prose often does not, and the
8238        // ratio at which the two cross depends on the card and the context
8239        // depth. So: four speculative rounds timed, then eight plain
8240        // tokens timed, and the faster arm runs until a re-check 256
8241        // tokens later (context growth moves the balance). The trial
8242        // costs at most a few tokens of the slower arm per 256.
8243        let mut spec_trial = SpecTrial::Spec {
8244            t0: std::time::Instant::now(),
8245            gen0: generated,
8246            rounds: 0,
8247        };
8248        // The token-count proxy prices a round at ~1.9 plain tokens. That
8249        // holds for the Metal rounds whose cost was measured — greedy and
8250        // the sparse sampling chain — so an expensive round (the dense
8251        // chain, reachable only by `CMF_GRAPH_SPEC_SAMPLE=1`) still times
8252        // the plain path before it decides.
8253        let mut spec_mon = SpecMon {
8254            metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
8255            cuda: graph_spec && crate::gpu::cuda_active() && self.sampler_config.temperature < 1e-6,
8256            ..SpecMon::default()
8257        };
8258        // wgpu: a plain token an earlier request's trial timed stands while
8259        // the context is of the same order (the trunk's weights are most of
8260        // a token's time), and this trial decides after its rounds.
8261        if !spec_mon.metal && self.spec_plain_ms.0 > 0.0 && input_ids.len() <= self.spec_plain_ms.1 * 2 + 1024 {
8262            spec_mon.plain_ms = self.spec_plain_ms.0;
8263        }
8264        let mut spec_watchdog_off = false;
8265        // CMF_GRAPH_SPEC_TIME: the round walls so far (round 1 excluded —
8266        // it pays the scratch), for the outlier test on each new one
8267        let mut spec_walls: Vec<f32> = Vec::new();
8268        // ... and the end of the last round: the host time between rounds
8269        // (token commits, streaming, the loop top) is printed at level 2
8270        let mut spec_round_end: Option<std::time::Instant> = None;
8271        if mimo_spec {
8272            if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
8273                if let Some(mut st) = self.mimo_mtp.take() {
8274                    self.mimo_mtp_probe(&mut st, input_ids, &path);
8275                    self.mimo_mtp = Some(st);
8276                }
8277            }
8278        }
8279        // ── Decode ──
8280        let mut next_pos = input_ids.len();
8281        'decode: while generated < max_tokens {
8282            if self
8283                .graph_failed
8284                .swap(false, std::sync::atomic::Ordering::Relaxed)
8285            {
8286                // Keep the detached MTP module attached after a terminal
8287                // graph error so the pipeline can be reused for a fresh
8288                // sequence.  `clear_sequence_state` only clears mirrors and
8289                // host KV; it cannot recover a module dropped here.
8290                self.finish_generation(&mut mtp, &mut router, true);
8291                return Err("GPU token graph failed during decode".to_string());
8292            }
8293            if self
8294                .cancel
8295                .swap(false, std::sync::atomic::Ordering::Relaxed)
8296            {
8297                finish_reason = "cancelled".to_string();
8298                break 'decode;
8299            }
8300            // A rejected speculative draft already drew this position's
8301            // token from the residual distribution (graph_spec_step); it
8302            // is committed as-is — sampling again from the row's logits
8303            // would bias the stream toward the target's mode.
8304            if mimo_spec && next_pos > 0 {
8305                // Every path leaves `hidden` = the backbone output at
8306                // next_pos-1; the draft layers read it (idempotent).
8307                self.mimo_note_rows(&hidden, next_pos - 1);
8308            }
8309            let forced = self.spec_forced.take();
8310            let mut logits = match (forced, self.graph_logits.take()) {
8311                (Some(_), _) => Vec::new(),
8312                (None, Some(lg)) => lg,
8313                (None, None) => {
8314                    let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
8315                    // Pooled-tail mode: the head as one layer-less device
8316                    // pass (final-norm + matvec + readback). Any decline —
8317                    // no pool, unresolvable weight, backend off — keeps the
8318                    // host head exactly as before.
8319                    #[cfg(feature = "gpu")]
8320                    let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
8321                        lg
8322                    } else {
8323                        inference::rms_norm_into(
8324                            &hidden,
8325                            &self.weights.final_norm,
8326                            self.rms_eps,
8327                            self.norm_style,
8328                            &mut self.ws.n1,
8329                        );
8330                        self.lm_head_forward(&self.ws.n1)
8331                    };
8332                    #[cfg(not(feature = "gpu"))]
8333                    let logits_out = {
8334                        inference::rms_norm_into(
8335                            &hidden,
8336                            &self.weights.final_norm,
8337                            self.rms_eps,
8338                            self.norm_style,
8339                            &mut self.ws.n1,
8340                        );
8341                        self.lm_head_forward(&self.ws.n1)
8342                    };
8343                    logits_out
8344                }
8345            };
8346            // CMF_LOGIT_DUMP=<path>: the first decode step's hidden + logits
8347            // as raw f32 (hidden first) — cross-backend numerics diffing.
8348            if generated
8349                == std::env::var("CMF_LOGIT_DUMP_STEP")
8350                    .ok()
8351                    .and_then(|v| v.parse().ok())
8352                    .unwrap_or(0)
8353            {
8354                if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
8355                    let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
8356                    for v in hidden.iter().chain(logits.iter()) {
8357                        bytes.extend_from_slice(&v.to_le_bytes());
8358                    }
8359                    if let Err(e) = std::fs::write(&path, &bytes) {
8360                        eprintln!("logit dump: failed to write {path}: {e}");
8361                        self.finish_generation(&mut mtp, &mut router, true);
8362                        return Err(format!("logit dump write failed: {e}"));
8363                    }
8364                }
8365            }
8366            // CMF_LOGIT_DUMP_ALL=<dir>: every decode step's logits as raw
8367            // f32, `<dir>/step{n:05}.f32` — step-by-step backend diffing
8368            // (a greedy run on two backends compares until they diverge).
8369            if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
8370                if !logits.is_empty() {
8371                    let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
8372                    let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
8373                    if let Err(e) =
8374                        std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
8375                    {
8376                        eprintln!("logit dump: failed to write {}: {e}", path.display());
8377                    }
8378                }
8379            }
8380            self.clover_causal_logits(&mut logits, generated, &all_ids[penalty_from..]);
8381            if let Some(g) = self.dash.as_mut().and_then(|d| d.guard.as_mut()) {
8382                g.apply(&mut logits, &all_ids[input_ids.len()..]);
8383            }
8384            let t_next = match forced.or_else(|| forced_ids_at(generated)) {
8385                Some(c) => c,
8386                None => {
8387                    let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
8388                    sampler::sample_with_scratch_pool(
8389                        &logits,
8390                        &self.sampler_config,
8391                        self.sampler_config.penalty_past(&all_ids[penalty_from..], bounded_native),
8392                        &mut self.rng,
8393                        &mut self.sampler_scratch,
8394                        self.pool.as_deref(),
8395                    )
8396                }
8397            };
8398            if self.confidence_on {
8399                confidence.push(if logits.is_empty() {
8400                    0.0
8401                } else {
8402                    sampler::top1_prob_pool(
8403                        self.pool.as_deref(),
8404                        &mut self.sampler_scratch,
8405                        &logits,
8406                        t_next,
8407                        calib_temp,
8408                    )
8409                });
8410            }
8411            if !logits.is_empty() {
8412                attention::recycle_buf(&mut logits);
8413            }
8414            if trace_on {
8415                // active_skill = the overlay in force while this token was
8416                // generated; recon/switched are filled after the post-emit
8417                // routing eval below (freshest coherence for this token).
8418                let skill = router.as_ref().and_then(|r| r.active_id());
8419                traces.push(TokenTrace {
8420                    t: generated,
8421                    token_id: t_next,
8422                    confidence: confidence.last().copied().unwrap_or(0.0),
8423                    active_skill: skill,
8424                    recon: None,
8425                    switched: false,
8426                });
8427            }
8428            if !commit!(t_next) {
8429                break 'decode;
8430            }
8431            if generated >= max_tokens {
8432                break 'decode;
8433            }
8434
8435            // Catch-all for decode paths that bypass the walks' own trim
8436            // (a graph step, a speculative round).
8437            self.swa_trim_tails();
8438            if self.dsv41.is_none() && self.dory.is_none() && self.kv_cache.needs_eviction() {
8439                // Say it ONCE, loudly: past this point the model keeps
8440                // talking but has lost half its context, and on a GDN
8441                // hybrid the graph's device state goes stale on top. The
8442                // Qwen3.8 bring-up spent a day reading this cliff as
8443                // three different model bugs.
8444                static SAID: std::sync::Once = std::sync::Once::new();
8445                SAID.call_once(|| {
8446                    tracing::warn!(
8447                        "KV cache full at {} positions — evicting half; quality \
8448                         will degrade. Raise CMF_MAX_SEQ.",
8449                        self.kv_cache.max_seq_len,
8450                    );
8451                });
8452                let keep = (self.kv_cache.max_seq_len / 2).max(1);
8453                self.kv_cache.evict(keep);
8454            }
8455
8456            // Advance the speculation trial: plain-phase accounting and
8457            // the periodic re-check happen here, on every token.
8458            if graph_spec {
8459                match spec_trial {
8460                    SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
8461                        spec_mon.plain_ms =
8462                            t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
8463                        if !spec_mon.metal {
8464                            self.spec_plain_ms = (spec_mon.plain_ms, next_pos);
8465                        }
8466                        let keep = spec_mon.pays();
8467                        tracing::info!(
8468                            "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
8469                            spec_mon.tokens,
8470                            spec_mon.round_ms,
8471                            spec_mon.plain_ms,
8472                            if keep { "speculating" } else { "plain" }
8473                        );
8474                        spec_mon.fails = 0;
8475                        spec_trial = SpecTrial::Decided {
8476                            spec: keep,
8477                            recheck_at: if keep { usize::MAX } else { spec_mon.recheck_plain_at(generated) },
8478                        };
8479                    }
8480                    SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
8481                        spec_mon.n = 0;
8482                        spec_mon.zero_accept_rounds = 0;
8483                        spec_trial = SpecTrial::Spec {
8484                            t0: std::time::Instant::now(),
8485                            gen0: generated,
8486                            rounds: 0,
8487                        };
8488                    }
8489                    _ => {}
8490                }
8491                spec_watchdog_off = matches!(
8492                    spec_trial,
8493                    SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
8494                );
8495            }
8496            // ── MiMo-V2 draft stack: draft K, verify K+1 rows in one batch ──
8497            if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
8498                let budget = max_tokens - generated - 1;
8499                if let Some(mut st) = self.mimo_mtp.take() {
8500                    let k = st.depth.min(budget);
8501                    let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
8502                    self.mimo_mtp = Some(st);
8503                    let r = match r {
8504                        Ok(r) => r,
8505                        Err(err) => {
8506                            self.finish_generation(&mut mtp, &mut router, true);
8507                            return Err(err);
8508                        }
8509                    };
8510                    if let Some(r) = r {
8511                        drafted += r.drafted;
8512                        accepted += r.accepted.len();
8513                        let mut stopped = false;
8514                        for &id in &r.accepted {
8515                            if self.confidence_on {
8516                                confidence.push(0.0);
8517                            }
8518                            if !commit!(id) {
8519                                stopped = true;
8520                                break;
8521                            }
8522                        }
8523                        if stopped {
8524                            break 'decode;
8525                        }
8526                        next_pos += r.accepted.len() + 1;
8527                        hidden = r.hidden;
8528                        // The loop top chooses the round's own token from
8529                        // these logits — the same sampler, same history.
8530                        self.graph_logits = Some(r.logits);
8531                        continue 'decode;
8532                    }
8533                }
8534            }
8535            // ── A trunk without a draft block: the sidecar's block drafts, the batch graph verifies ──
8536            #[cfg(feature = "gpu")]
8537            if solo_spec && next_pos > 0 {
8538                let now = std::time::Instant::now();
8539                // the step that just ended, on the account of its kind
8540                if let Some((t, tokens)) = solo.last.take() {
8541                    let ms = (now - t).as_secs_f32() * 1e3;
8542                    if tokens == 0 {
8543                        if solo.plain_n > 0 {
8544                            solo.plain_ms = if solo.plain_ms == 0.0 { ms } else { 0.7 * solo.plain_ms + 0.3 * ms };
8545                        }
8546                        solo.plain_n += 1;
8547                    } else if solo.first {
8548                        solo.first = false;
8549                        if spec_time_level() > 0 {
8550                            eprintln!("solo-round wall {ms:.1} ms → {tokens} tokens (the first round: not a sample)");
8551                        }
8552                    } else {
8553                        let per = ms / tokens as f32;
8554                        // a tenth a round: one round that kept nothing (85 ms for its
8555                        // one token against 20 a token before it) must not send a
8556                        // drafter keeping 80 % to rest — at a fifth it did, 36 against
8557                        // 47 tok/s on code
8558                        solo.round_ms = if solo.rounds == 0 { per } else { 0.9 * solo.round_ms + 0.1 * per };
8559                        solo.rounds += 1;
8560                        if spec_time_level() > 0 {
8561                            eprintln!(
8562                                "solo-round wall {ms:.1} ms → {tokens} tokens ({:.1} ms a token, plain {:.1})",
8563                                solo.round_ms, solo.plain_ms
8564                            );
8565                        }
8566                        // Rounds that land tokens no faster than plain steps sit out, longer
8567                        // each time. Judged on the window's summed wall over its summed
8568                        // tokens from its SOLO_JUDGE-th round on: a short answer was over
8569                        // before six rounds at a third kept had sent the drafter to rest
8570                        // (MNN-Pocket-0.3B chat: 237 against 303 tok/s plain), and one
8571                        // round that kept nothing weighs only its share of the sum.
8572                        solo.win_ms += ms;
8573                        solo.win_tok += tokens as u32;
8574                        if solo_pin.is_none() && solo.plain_ms > 0.0 && solo.rounds >= SOLO_JUDGE {
8575                            if solo.win_ms / solo.win_tok.max(1) as f32 > solo.plain_ms {
8576                                solo.rest = solo.back;
8577                                solo.back = (solo.back * 2).min(SOLO_REST_MOST);
8578                                solo.rounds = 0;
8579                                solo.win_ms = 0.0;
8580                                solo.win_tok = 0;
8581                            } else if solo.rounds >= SOLO_TRIAL {
8582                                // a winning window starts over, so the verdict follows the text
8583                                solo.rounds = 0;
8584                                solo.win_ms = 0.0;
8585                                solo.win_tok = 0;
8586                            }
8587                        }
8588                    }
8589                }
8590                if solo.rest == 0 && generated + 1 < max_tokens {
8591                    let k = solo.k.min(max_tokens - generated);
8592                    match self.solo_spec_round(t_next, next_pos, &all_ids, k) {
8593                        Some(r) => {
8594                            drafted += r.drafted;
8595                            accepted += r.accepted.len();
8596                            if solo_pin.is_none() {
8597                                // two past what held: a round that held all of its drafts goes deeper
8598                                solo.k = (r.accepted.len() + 2).clamp(2, SOLO_K_AUTO);
8599                            }
8600                            solo.last = Some((now, r.accepted.len() + 1));
8601                            let mut stopped = false;
8602                            for &id in &r.accepted {
8603                                if self.confidence_on {
8604                                    confidence.push(0.0);
8605                                }
8606                                if trace_on {
8607                                    traces.push(TokenTrace {
8608                                        t: generated,
8609                                        token_id: id,
8610                                        confidence: confidence.last().copied().unwrap_or(0.0),
8611                                        active_skill: None,
8612                                        recon: None,
8613                                        switched: false,
8614                                    });
8615                                }
8616                                if !commit!(id) {
8617                                    stopped = true;
8618                                    break;
8619                                }
8620                            }
8621                            if stopped {
8622                                break 'decode;
8623                            }
8624                            next_pos += r.accepted.len() + 1;
8625                            hidden = r.hidden;
8626                            self.graph_logits = Some(r.logits);
8627                            continue 'decode;
8628                        }
8629                        None => {
8630                            if self.graph_failed.swap(false, std::sync::atomic::Ordering::Relaxed) {
8631                                self.finish_generation(&mut mtp, &mut router, true);
8632                                return Err("GPU batch graph failed during a speculative round".to_string());
8633                            }
8634                            // no round here (the drafter's context is not at the tip, the batch graph declined)
8635                            solo.rest = 8;
8636                        }
8637                    }
8638                }
8639                solo.rest = solo.rest.saturating_sub(1);
8640                solo.last = Some((now, 0));
8641            }
8642            // ── Qwen3.8-Flash-Next draft head: k greedy drafts from the MTP
8643            //    sidecar, one batched verify window on the device ──
8644            #[cfg(feature = "gpu")]
8645            if self.speculative
8646                && self.qwen4_exp.is_some()
8647                && task_mask.is_none()
8648                && self.sampler_config.temperature < 1e-6
8649                && generated + 1 < max_tokens
8650                && next_pos > 0
8651                && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
8652            {
8653                let r = match &mut self.qwen4_exp {
8654                    Some(b) => {
8655                        // a copy frame drafts no further than the request goes
8656                        b.3.set_draft_budget(max_tokens - generated - 1);
8657                        crate::qwen4_exp::spec_round(
8658                            &b.0,
8659                            &b.1,
8660                            &b.2,
8661                            &mut b.3,
8662                            next_pos,
8663                            &all_ids,
8664                            &self.inv_freq,
8665                            self.pool.as_deref(),
8666                        )
8667                    }
8668                    None => None,
8669                };
8670                if let Some(r) = r {
8671                    drafted += r.drafted;
8672                    accepted += r.accepted.len();
8673                    let mut stopped = false;
8674                    for &id in &r.accepted {
8675                        if self.confidence_on {
8676                            confidence.push(0.0);
8677                        }
8678                        if !commit!(id) {
8679                            stopped = true;
8680                            break;
8681                        }
8682                    }
8683                    if stopped {
8684                        break 'decode;
8685                    }
8686                    next_pos += r.accepted.len() + 1;
8687                    hidden.fill(0.0);
8688                    self.graph_logits = Some(r.logits);
8689                    continue 'decode;
8690                }
8691            }
8692            match &mut mtp {
8693                // ── Graph speculation: chain-draft, batch-verify on device ──
8694                #[cfg(feature = "gpu")]
8695                Some(m)
8696                    if graph_spec
8697                        && !spec_watchdog_off
8698                        && generated + 1 < max_tokens
8699                        && next_pos > 0 =>
8700                {
8701                    let t_round = std::time::Instant::now();
8702                    if spec_time_level() >= 2 {
8703                        if let Some(t) = spec_round_end.take() {
8704                            eprintln!(
8705                                "spec-gap {:.2} ms (host between rounds)",
8706                                t.elapsed().as_secs_f64() * 1e3
8707                            );
8708                        }
8709                    }
8710                    spec_stamps_begin();
8711                    // device buffers allocated during this round: a
8712                    // first-touch Shared allocation is zero-filled inside
8713                    // the command buffer that uses it, which is what the
8714                    // long outlier rounds were
8715                    #[cfg(target_os = "macos")]
8716                    let allocs0 =
8717                        crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
8718                    #[cfg(not(target_os = "macos"))]
8719                    let allocs0 = 0u64;
8720                    if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
8721                        m,
8722                        &hidden,
8723                        t_next,
8724                        next_pos,
8725                        &mut drafted,
8726                        &mut accepted,
8727                        &mut all_ids,
8728                        max_tokens - generated,
8729                    ) {
8730                        next_pos = n_pos;
8731                        hidden = new_h;
8732                        let level = spec_time_level();
8733                        if level > 0 {
8734                            let wall = t_round.elapsed().as_secs_f32() * 1e3;
8735                            let stamps = spec_stamps_take();
8736                            // the running median of the rounds before this
8737                            // one (round 1 pays the scratch: not a sample)
8738                            let median = if spec_walls.len() >= 3 {
8739                                let mut s = spec_walls.clone();
8740                                s.sort_by(|a, b| a.partial_cmp(b).unwrap());
8741                                Some(s[s.len() / 2])
8742                            } else {
8743                                None
8744                            };
8745                            let outlier = median.is_some_and(|m| wall > 1.4 * m);
8746                            #[cfg(target_os = "macos")]
8747                            let allocs = crate::gpu_metal::IO_BUF_ALLOCS
8748                                .load(std::sync::atomic::Ordering::Relaxed)
8749                                - allocs0;
8750                            #[cfg(not(target_os = "macos"))]
8751                            let allocs = allocs0;
8752                            eprintln!(
8753                                "spec-round wall {wall:.1} ms → {} tokens{}{}",
8754                                extra.len() + 1,
8755                                if allocs > 0 {
8756                                    format!(" [{allocs} new device buffers]")
8757                                } else {
8758                                    String::new()
8759                                },
8760                                match (outlier, median) {
8761                                    (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
8762                                    _ => String::new(),
8763                                }
8764                            );
8765                            if level >= 2 || outlier {
8766                                let sum: f32 = stamps.iter().map(|s| s.1).sum();
8767                                eprintln!(
8768                                    "spec-stamps: {}| untracked {:.1}",
8769                                    spec_stamps_format(&stamps),
8770                                    wall - sum
8771                                );
8772                            }
8773                            if spec_mon.n >= 1 {
8774                                spec_walls.push(wall);
8775                            }
8776                        }
8777                        // One speculative round done: the monitor counts it
8778                        // (round 1 untimed — it pays the batch scratch and
8779                        // the draft mirror), and the trial advances.
8780                        spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
8781                        // the round's tokens land in `generated` below; the
8782                        // plain phase must start counting AFTER them
8783                        spec_trial = Self::spec_trial_round(
8784                            spec_trial,
8785                            &mut spec_mon,
8786                            generated + extra.len() + 1,
8787                        );
8788                        let mut stopped = false;
8789                        for &id in &extra {
8790                            if self.confidence_on {
8791                                confidence.push(0.0);
8792                            }
8793                            // `graph_spec_step` commits the accepted MTP
8794                            // drafts as a batch.  They do not pass through
8795                            // the ordinary loop-top sampling block below,
8796                            // which is normally where the telemetry row is
8797                            // created.  Keep the public trace contract
8798                            // (one row per emitted token) here as well.
8799                            //
8800                            // The graph-spec path does not surface a
8801                            // per-row probability for accepted extras.  It
8802                            // deliberately matches the zero placed in the
8803                            // confidence channel above instead of inventing
8804                            // one or adding a separate probability pass.
8805                            if trace_on {
8806                                traces.push(TokenTrace {
8807                                    t: generated,
8808                                    token_id: id,
8809                                    confidence: confidence.last().copied().unwrap_or(0.0),
8810                                    active_skill: None,
8811                                    recon: None,
8812                                    switched: false,
8813                                });
8814                            }
8815                            if !commit!(id) {
8816                                stopped = true;
8817                                break;
8818                            }
8819                        }
8820                        if stopped {
8821                            break 'decode;
8822                        }
8823                        if spec_time_level() >= 2 {
8824                            spec_round_end = Some(std::time::Instant::now());
8825                        }
8826                        continue 'decode;
8827                    }
8828                    if self
8829                        .graph_failed
8830                        .swap(false, std::sync::atomic::Ordering::Relaxed)
8831                    {
8832                        // `graph_spec_step` may have detached MTP while a
8833                        // warm-up was in flight.  Do not reinterpret its
8834                        // terminal device failure as a plain decode step;
8835                        // restore the head, clear both mirrors, and surface
8836                        // one explicit error to the caller.
8837                        self.finish_generation(&mut mtp, &mut router, true);
8838                        return Err("GPU MTP graph failed during speculative decode".to_string());
8839                    }
8840                    // Declined (batch graph refused): plain forward below —
8841                    // and a round that produced one token for the trial's
8842                    // ledger, so a graph that keeps refusing is measured out
8843                    // like a head that keeps missing (it was spinning
8844                    // forever on a file whose batch graph declines).
8845                    // A declined round is not a cheap one-token round — it
8846                    // is a verify that does not exist for this file (a
8847                    // healed q8_2f tail measured 760 drafts, 0 accepted, 33
8848                    // against 48.8 tok/s while the monitor called the draft
8849                    // alone "paying"). Count it as the losing streak in one.
8850                    spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
8851                    spec_mon.tokens = 0.0;
8852                    spec_mon.fails = 3;
8853                    spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
8854                    hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
8855                    next_pos += 1;
8856                    continue 'decode;
8857                }
8858                // ── Speculative: draft t+2, verify in a fused pair ──
8859                Some(m) if !graph_spec && generated + 1 < max_tokens => {
8860                    let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
8861                    drafted += 1;
8862                    let emb1 = self.embed_single(t_next);
8863                    let emb2 = self.embed_single(draft);
8864                    let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
8865
8866                    inference::rms_norm_into(
8867                        &h1,
8868                        &self.weights.final_norm,
8869                        self.rms_eps,
8870                        self.norm_style,
8871                        &mut self.ws.n1,
8872                    );
8873                    let mut logits1 = self.lm_head_forward(&self.ws.n1);
8874                    let t_after = sampler::sample_with_scratch_pool(
8875                        &logits1,
8876                        &self.sampler_config,
8877                        self.sampler_config.penalty_past(&all_ids, bounded_native),
8878                        &mut self.rng,
8879                        &mut self.sampler_scratch,
8880                        self.pool.as_deref(),
8881                    );
8882                    if self.confidence_on {
8883                        confidence.push(sampler::top1_prob_pool(
8884                            self.pool.as_deref(),
8885                            &mut self.sampler_scratch,
8886                            &logits1,
8887                            t_after,
8888                            calib_temp,
8889                        ));
8890                    }
8891                    attention::recycle_buf(&mut logits1);
8892                    if trace_on {
8893                        // Speculative decode is mutually exclusive with
8894                        // dynamic routing (router is None here) — no skill.
8895                        traces.push(TokenTrace {
8896                            t: generated,
8897                            token_id: t_after,
8898                            confidence: confidence.last().copied().unwrap_or(0.0),
8899                            active_skill: None,
8900                            recon: None,
8901                            switched: false,
8902                        });
8903                    }
8904                    let stop = !commit!(t_after);
8905
8906                    if t_after == draft {
8907                        accepted += 1;
8908                        self.commit_linear_scratch();
8909                        let _ = self.mtp_step(m, &h1, t_after, next_pos);
8910                        hidden = h2;
8911                        next_pos += 2;
8912                    } else {
8913                        // The draft lane is wrong: roll its KV entry back.
8914                        for layer in &mut self.kv_cache.layers {
8915                            layer.truncate_last(1);
8916                        }
8917                        if !stop {
8918                            let _ = self.mtp_step(m, &h1, t_after, next_pos);
8919                            hidden = self.forward_layers(
8920                                &self.embed_single(t_after),
8921                                next_pos + 1,
8922                                None,
8923                            );
8924                        }
8925                        next_pos += 2;
8926                    }
8927                    if stop {
8928                        break 'decode;
8929                    }
8930                }
8931                // ── Vanilla: forward the sampled token ──
8932                _ => {
8933                    // ── DeepSeek-V4 speculative decode (CMF_DSV4_SPEC=1):
8934                    // draft five on the card, verify batched, commit the
8935                    // accepted prefix. Greedy only; a rejected token's state
8936                    // is restored and replayed, so output equals the walk. ──
8937                    #[cfg(feature = "gpu")]
8938                    if Self::dsv4_spec_on() && self.dsv4.is_some() {
8939                        static SAID: std::sync::Once = std::sync::Once::new();
8940                        SAID.call_once(|| {
8941                            eprintln!(
8942                                "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
8943                                !self.dsv4_mtp.is_empty(),
8944                                task_mask.is_none(),
8945                                router.is_none(),
8946                                !trace_on,
8947                                self.sampler_config.temperature < 1e-6,
8948                                self.sampler_config.repetition_penalty == 1.0,
8949                            );
8950                        });
8951                    }
8952                    #[cfg(feature = "gpu")]
8953                    if Self::dsv4_spec_on()
8954                        && self.dsv4.is_some()
8955                        && !self.dsv4_mtp.is_empty()
8956                        && task_mask.is_none()
8957                        && router.is_none()
8958                        && !trace_on
8959                        && self.sampler_config.temperature < 1e-6
8960                        && self.sampler_config.repetition_penalty == 1.0
8961                        && generated + 1 < max_tokens
8962                        && all_ids.len() >= 2
8963                        && generated >= dsv4_spec_retry_at
8964                    {
8965                        let tip_token = all_ids[all_ids.len() - 2];
8966                        let drafted0 = drafted;
8967                        let round = self.dsv4_spec_step(
8968                            tip_token,
8969                            t_next,
8970                            next_pos,
8971                            max_tokens.saturating_sub(generated),
8972                            &mut drafted,
8973                            &mut accepted,
8974                        );
8975                        if drafted > drafted0 {
8976                            let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
8977                            if useful {
8978                                dsv4_spec_bad = 0;
8979                            } else {
8980                                dsv4_spec_bad += 1;
8981                                if dsv4_spec_bad >= 2 {
8982                                    dsv4_spec_bad = 0;
8983                                    dsv4_spec_retry_at = generated.saturating_add(32);
8984                                    tracing::info!(
8985                                        "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
8986                                    );
8987                                }
8988                            }
8989                        }
8990                        if let Some((extra, n_pos)) = round {
8991                            next_pos = n_pos;
8992                            let mut stopped = false;
8993                            for &id in &extra {
8994                                if self.confidence_on {
8995                                    confidence.push(0.0);
8996                                }
8997                                if !commit!(id) {
8998                                    stopped = true;
8999                                    break;
9000                                }
9001                            }
9002                            if stopped {
9003                                break 'decode;
9004                            }
9005                            continue 'decode;
9006                        }
9007                    }
9008                    self.graph_want_logits = fuse_lm;
9009                    // Greedy burst (CMF_MULTISTEP, off unless explicitly set): while
9010                    // nothing observes per-token state — pure argmax sampling,
9011                    // no router/trace/confidence/mask — decode k tokens per
9012                    // submit and commit them wholesale. The trailing normal
9013                    // forward leaves logits for the loop top, as always.
9014                    let mut t_fwd = t_next;
9015                    let pure_greedy = self.sampler_config.temperature < 1e-6
9016                        && self.sampler_config.repetition_penalty == 1.0
9017                        && self.sampler_config.presence_penalty == 0.0
9018                        && self.sampler_config.suppress_tokens.is_empty();
9019                    // Off by default: at every k the burst measured at or
9020                    // below the plain path on this graph shape (k=1 loses
9021                    // the argmax dispatches vs a 1 MB readback, k>=8 loses
9022                    // inter-step drains vs the saved sync). Experimental.
9023                    let burst_k = std::env::var("CMF_MULTISTEP")
9024                        .ok()
9025                        .and_then(|v| v.parse::<usize>().ok())
9026                        .unwrap_or(0);
9027                    if pure_greedy
9028                        && burst_k >= 1
9029                        && fuse_lm
9030                        && task_mask.is_none()
9031                        && router.is_none()
9032                        && !trace_on
9033                        && !self.confidence_on
9034                        // Diagnostics and teacher forcing observe every
9035                        // individual logit row.  A device-only burst returns
9036                        // only ids, so decline it rather than silently
9037                        // skipping requested evidence or forced ids.
9038                        && std::env::var_os("CMF_FORCE_IDS").is_none()
9039                        && std::env::var_os("CMF_LOGIT_DUMP").is_none()
9040                        && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
9041                    {
9042                        let mut stopped = false;
9043                        loop {
9044                            // The outer loop checks cancellation too, but a
9045                            // long run can stay in this burst loop for many
9046                            // submits.  Bound cancellation latency to one
9047                            // exact k-token frame.
9048                            if self
9049                                .cancel
9050                                .swap(false, std::sync::atomic::Ordering::Relaxed)
9051                            {
9052                                finish_reason = "cancelled".to_string();
9053                                stopped = true;
9054                                break;
9055                            }
9056                            let room = max_tokens.saturating_sub(generated);
9057                            if room <= 2 {
9058                                break;
9059                            }
9060                            let k = burst_k.min(room - 1);
9061                            if k < 1 {
9062                                break;
9063                            }
9064                            let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
9065                                if self
9066                                    .graph_failed
9067                                    .swap(false, std::sync::atomic::Ordering::Relaxed)
9068                                {
9069                                    self.finish_generation(&mut mtp, &mut router, true);
9070                                    return Err(
9071                                        "GPU token graph failed during greedy burst".to_string()
9072                                    );
9073                                }
9074                                break;
9075                            };
9076                            next_pos += k;
9077                            for &id in &ids {
9078                                if !commit!(id) {
9079                                    stopped = true;
9080                                    break;
9081                                }
9082                            }
9083                            if stopped {
9084                                break;
9085                            }
9086                            t_fwd = *ids.last().unwrap();
9087                        }
9088                        if stopped {
9089                            break 'decode;
9090                        }
9091                    }
9092                    // Metal: keep the draft head's cache in step through
9093                    // the trial's plain phase and a paused speculation —
9094                    // the pair (hidden, t_fwd) at next_pos−1, the step the
9095                    // round's draft 0 would take. Without it the head's
9096                    // cache lagged the trunk by every plain token for the
9097                    // rest of the generation: the batched warm-up declined
9098                    // every later round and its rows went one by one (a
9099                    // whole MTP step per accepted token), and the drafts
9100                    // attended a context with those tokens missing.
9101                    #[cfg(target_os = "macos")]
9102                    if graph_spec
9103                        && spec_watchdog_off
9104                        && next_pos > 0
9105                        && self.mtp_graph_mode == Some(true)
9106                        && crate::gpu::q1_force()
9107                    {
9108                        if let Some(m) = mtp.as_mut() {
9109                            let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
9110                        }
9111                    }
9112                    if let Some(d) = self.dash.as_mut() {
9113                        d.at_decode(t_fwd, next_pos);
9114                    }
9115                    hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
9116                    next_pos += 1;
9117                    // Dynamic routing: the forward updated φ; ask the
9118                    // router whether to switch skills before the next token.
9119                    if let Some(r) = &mut router {
9120                        let phi = self.dyn_phi_ema.clone();
9121                        let decision = r.step(&phi, generated);
9122                        if let Some(new_active) = decision {
9123                            let _ = self.set_active_skill(new_active);
9124                        }
9125                        // Backfill this token's coherence + switch flag from
9126                        // the just-run eval (freshest measured values).
9127                        if trace_on {
9128                            if let Some(last) = traces.last_mut() {
9129                                let e = r.last_best_e();
9130                                last.recon = e.is_finite().then_some(e);
9131                                last.switched = decision.is_some();
9132                            }
9133                        }
9134                    }
9135                }
9136            }
9137        }
9138
9139        let cancelled = finish_reason == "cancelled";
9140        // A generation during which the router switched weights holds no
9141        // state any single overlay would produce (each switch cleared the
9142        // cache mid-sequence), so it leaves no reuse key behind either.
9143        let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
9144        if mimo_spec {
9145            if let Some(st) = self.mimo_mtp.as_ref() {
9146                let line = st.stats.line();
9147                tracing::info!("{line}");
9148                if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
9149                    eprintln!("{line}");
9150                }
9151            }
9152        }
9153        self.finish_generation(&mut mtp, &mut router, cancelled);
9154
9155        if self.dflash_solo && spec_time_level() > 0 {
9156            if let Some(line) = crate::dflash::stage_report() {
9157                eprintln!("solo-draft: {line}");
9158            }
9159        }
9160        // `CMF_GEN_IDS_OUT=FILE`: the sequence as ids, prompt first (what
9161        // `CMF_SPEC_SCRIPT` reads back).
9162        if let Ok(path) = std::env::var("CMF_GEN_IDS_OUT") {
9163            let text = all_ids.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(" ");
9164            if let Err(e) = std::fs::write(&path, text) {
9165                tracing::warn!("CMF_GEN_IDS_OUT: {path}: {e}");
9166            }
9167        }
9168        let output_ids = &all_ids[input_ids.len()..];
9169        // Forwarded = prompt + all generated but the LAST sampled token
9170        // (emitted without being fed back). Exact only without MTP —
9171        // reuse is gated off when MTP is active.
9172        let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
9173        // A MiMo speculative round that stopped on an accepted draft (EOS,
9174        // cancel) leaves verify rows past the committed stream in the cache:
9175        // never offer that cache for reuse. Neither does a router that
9176        // switched weights mid-sequence (no single overlay produced it).
9177        let consumed = std::mem::take(&mut all_ids);
9178        if dyn_switched {
9179            self.clear_sequence_state();
9180        } else if cancelled || mimo_spec || prompt_rows.is_some() {
9181            self.clear_history();
9182        } else {
9183            self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
9184        }
9185        all_ids = consumed;
9186        let output_ids = &all_ids[input_ids.len()..];
9187        confidence.truncate(output_ids.len()); // guard against any overshoot
9188        traces.truncate(output_ids.len());
9189        // The loop monitor stopped the answer: it says the repeated block once.
9190        let output_ids = match finish_reason.as_str() {
9191            "loop" => {
9192                let excess = crate::sampler::tail_repeat_excess(output_ids).unwrap_or(0);
9193                &output_ids[..output_ids.len() - excess]
9194            }
9195            _ => output_ids,
9196        };
9197        Ok(GenerateResult {
9198            text: self.tokenizer.decode(output_ids),
9199            token_ids: output_ids.to_vec(),
9200            prompt_tokens: input_ids.len(),
9201            tokens_generated: generated,
9202            finish_reason,
9203            mtp_drafted: drafted,
9204            mtp_accepted: accepted,
9205            token_confidence: confidence,
9206            traces,
9207        })
9208    }
9209
9210    /// One MTP step: feed `(hidden_p, token_{p+1})` into the draft head,
9211    /// advance its KV cache at position `p`, return the drafted token
9212    /// for position `p+2`.
9213    fn mtp_step(
9214        &mut self,
9215        m: &mut MtpModule,
9216        hidden: &[f32],
9217        next_token: u32,
9218        position: usize,
9219    ) -> u32 {
9220        self.mtp_step_h(m, hidden, next_token, position).0
9221    }
9222
9223    /// Tally for `CMF_MTP_CHAIN_PROBE`: per depth, how often the CHAIN is
9224    /// still an exact prefix of the real continuation. Printed every 128
9225    /// depth-0 samples so a killed run still shows its table.
9226    fn chain_probe_note(depth: usize, prefix_ok: bool) {
9227        use std::sync::Mutex;
9228        static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
9229        let mut t = T.lock().unwrap();
9230        if t.len() <= depth {
9231            t.resize(depth + 1, (0, 0));
9232        }
9233        t[depth].0 += 1;
9234        t[depth].1 += prefix_ok as u64;
9235        if depth == 0 && t[0].0 % 128 == 0 {
9236            let line: Vec<String> = t
9237                .iter()
9238                .enumerate()
9239                .map(|(d, (n, k))| {
9240                    format!(
9241                        "d{}={:.0}%({n})",
9242                        d + 1,
9243                        100.0 * *k as f64 / (*n).max(1) as f64
9244                    )
9245                })
9246                .collect();
9247            eprintln!("mtp-chain: {}", line.join(" "));
9248        }
9249    }
9250
9251    /// `mtp_step` that also hands back the block's own output hidden — the
9252    /// state a CHAINED draft feeds the next step, the way a multi-token
9253    /// speculative round iterates the head on itself.
9254    /// One MTP block step from (trunk hidden, token): the head's LOGITS
9255    /// and the block's own hidden for chaining. The draft is argmax of the
9256    /// logits on the greedy path and a draw from their post-chain
9257    /// distribution on the sampling path.
9258    fn mtp_step_hl(
9259        &mut self,
9260        m: &mut MtpModule,
9261        hidden: &[f32],
9262        next_token: u32,
9263        position: usize,
9264    ) -> (Vec<f32>, Vec<f32>) {
9265        let position = position.saturating_sub(self.mtp_pos_base);
9266        // The graph arm: the MTP block as a one-layer token graph with the
9267        // head fused — device attention over the block's own KV mirror,
9268        // one submit for block + head, hidden and logits back together.
9269        // Decided once per generation (see `mtp_graph_mode`).
9270        #[cfg(target_os = "macos")]
9271        if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
9272            if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
9273                self.mtp_graph_mode = Some(true);
9274                return r;
9275            }
9276            if self.mtp_graph_mode == Some(true) {
9277                tracing::error!("mtp Metal graph failed after admission");
9278                self.clear_sequence_state();
9279                self.graph_failed
9280                    .store(true, std::sync::atomic::Ordering::Relaxed);
9281                self.cancel
9282                    .store(true, std::sync::atomic::Ordering::Relaxed);
9283                return (Vec::new(), Vec::new());
9284            }
9285            self.mtp_graph_mode = Some(false);
9286        }
9287        #[cfg(feature = "gpu")]
9288        if self.mtp_graph_mode != Some(false) {
9289            if !self.mtp_graph_ok(m) {
9290                if self.mtp_graph_mode == Some(true) {
9291                    // A mirror was already admitted, so a capability change
9292                    // cannot safely switch this request to the stale CPU
9293                    // cache.  Keep the same terminal contract as a failed
9294                    // token graph.
9295                    tracing::error!("mtp graph became unavailable after admission");
9296                    self.clear_sequence_state();
9297                    self.graph_failed
9298                        .store(true, std::sync::atomic::Ordering::Relaxed);
9299                    self.cancel
9300                        .store(true, std::sync::atomic::Ordering::Relaxed);
9301                    return (Vec::new(), Vec::new());
9302                }
9303                self.mtp_graph_mode = Some(false);
9304            } else {
9305                if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
9306                    self.mtp_graph_mode = Some(true);
9307                    return r;
9308                }
9309                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9310                    // A token graph can have admitted a persistent MTP/GDN
9311                    // mirror before its readback failed.  The CPU MTP cache
9312                    // is not a valid continuation in that state; leave the
9313                    // flag set so the generation caller returns through its
9314                    // terminal error path instead of silently switching
9315                    // arithmetic.
9316                    return (Vec::new(), Vec::new());
9317                }
9318                // `mtp_graph_ok` was true, so a None here means a refusal or
9319                // failure after graph admission.  Do not fall through to a
9320                // CPU cache whose rows may lag the device mirror.
9321                tracing::error!("mtp graph failed or declined after admission");
9322                self.clear_sequence_state();
9323                self.graph_failed
9324                    .store(true, std::sync::atomic::Ordering::Relaxed);
9325                self.cancel
9326                    .store(true, std::sync::atomic::Ordering::Relaxed);
9327                return (Vec::new(), Vec::new());
9328            }
9329        }
9330        // fc concat order is [enorm(embed); hnorm(hidden)] — EMBEDDING
9331        // FIRST. Verified by the oracle (converter/mtp_oracle.py):
9332        // [emb;hid] → 45.8% acceptance, [hid;emb] → 0.00%.
9333        let e = self.embed_single(next_token);
9334        let mut cat = vec![0.0f32; 2 * self.hidden_size];
9335        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9336        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
9337        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
9338        let mut x = vec![0.0f32; self.hidden_size];
9339        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9340
9341        // One standard transformer block over the MTP's own cache.
9342        let lw = &m.layer;
9343        inference::rms_norm_into(
9344            &x,
9345            &lw.input_norm,
9346            self.rms_eps,
9347            self.norm_style,
9348            &mut self.ws.n1,
9349        );
9350        let attn = match &lw.attn {
9351            // MLA models carry no MTP head; this path cannot see them.
9352            AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
9353            AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
9354            AttnKind::Full {
9355                wq,
9356                wk,
9357                wv,
9358                wo,
9359                q_norm,
9360                k_norm,
9361                output_gate,
9362                softplus_gate,
9363                bias,
9364            } => {
9365                let mut cfg = self.attn_cfg(position);
9366                cfg.q_norm = q_norm.as_deref();
9367                cfg.k_norm = k_norm.as_deref();
9368                cfg.output_gate = *output_gate;
9369                cfg.softplus_gate = softplus_gate
9370                    .as_ref()
9371                    .map(|(gate, per_head)| (gate, *per_head));
9372                cfg.bias = bias
9373                    .as_ref()
9374                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
9375                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
9376            }
9377            AttnKind::Linear(_)
9378            | AttnKind::LinearGdn(_)
9379            | AttnKind::ShortConv(_)
9380            | AttnKind::Bounded(_) => {
9381                unreachable!("MTP block is full attention")
9382            }
9383        };
9384        for (i, &a) in attn.iter().enumerate() {
9385            x[i] += a;
9386        }
9387        inference::rms_norm_into(
9388            &x,
9389            &lw.post_norm,
9390            self.rms_eps,
9391            self.norm_style,
9392            &mut self.ws.p1,
9393        );
9394        let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
9395        for (i, &f) in ffn.iter().enumerate() {
9396            x[i] += f;
9397        }
9398
9399        inference::rms_norm_into(
9400            &x,
9401            &m.final_norm,
9402            self.rms_eps,
9403            self.norm_style,
9404            &mut self.ws.n1,
9405        );
9406        let lg = self.lm_head_forward(&self.ws.n1);
9407        (lg, x)
9408    }
9409
9410    /// `mtp_step_hl` reduced to the greedy draft: argmax of the head.
9411    fn mtp_step_h(
9412        &mut self,
9413        m: &mut MtpModule,
9414        hidden: &[f32],
9415        next_token: u32,
9416        position: usize,
9417    ) -> (u32, Vec<f32>) {
9418        let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
9419        let draft = sampler::argmax(&lg);
9420        attention::recycle_buf(&mut lg);
9421        (draft, x)
9422    }
9423
9424    /// One speculative round for the trial: rounds 1..5 of a `Spec` phase
9425    /// advance it (the monitor already averaged this round); after five,
9426    /// the plain phase runs (once — a known plain rate decides at once);
9427    /// a decided speculation keeps re-checking the rule every round and
9428    /// stops after four losing rounds in a row.
9429    fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
9430        // CMF_GRAPH_SPEC_PIN=1 keeps the rounds on whatever they cost: a
9431        // count of how a draft head holds has to include the stretches a
9432        // trial would decode plain.
9433        static PIN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9434        if *PIN.get_or_init(|| std::env::var("CMF_GRAPH_SPEC_PIN").as_deref() == Ok("1")) {
9435            return SpecTrial::Decided {
9436                spec: true,
9437                recheck_at: usize::MAX,
9438            };
9439        }
9440        match trial {
9441            SpecTrial::Spec { t0, gen0, rounds } => {
9442                let rounds = rounds + 1;
9443                // A round that accepts no draft still has to run the MTP
9444                // chain and a K+1 verifier, but emits only its verifier tip.
9445                // After `SPEC_ZERO_ROUNDS` such real rounds the speculative
9446                // arm cannot win on any backend; time plain immediately
9447                // rather than spend the remaining exploratory rounds on the
9448                // same one-token result. A re-check soon after (the evidence
9449                // is a few rounds) gives the text a chance to re-admit it.
9450                if mon.repeated_zero_acceptance() {
9451                    tracing::info!(
9452                        "speculation trial: no accepted drafts in two rounds — timing plain"
9453                    );
9454                    return SpecTrial::Plain {
9455                        t0: std::time::Instant::now(),
9456                        gen0: generated,
9457                    };
9458                }
9459                if rounds >= 5 {
9460                    if mon.plain_ms > 0.0 {
9461                        let keep = mon.pays();
9462                        mon.fails = 0;
9463                        tracing::info!(
9464                            "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
9465                            mon.tokens,
9466                            mon.round_ms,
9467                            mon.plain_ms,
9468                            if keep { "speculating" } else { "plain" }
9469                        );
9470                        SpecTrial::Decided {
9471                            spec: keep,
9472                            recheck_at: if keep { usize::MAX } else { mon.recheck_plain_at(generated) },
9473                        }
9474                    } else if mon.pays() {
9475                        // Metal: the rounds land enough tokens each that no
9476                        // plain measurement is needed — keep speculating,
9477                        // and re-check every round (a losing streak sends
9478                        // the loop to the plain phase, below).
9479                        mon.fails = 0;
9480                        tracing::info!(
9481                            "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
9482                            mon.tokens,
9483                            mon.round_ms,
9484                        );
9485                        SpecTrial::Decided {
9486                            spec: true,
9487                            recheck_at: usize::MAX,
9488                        }
9489                    } else {
9490                        SpecTrial::Plain {
9491                            t0: std::time::Instant::now(),
9492                            gen0: generated,
9493                        }
9494                    }
9495                } else {
9496                    SpecTrial::Spec { t0, gen0, rounds }
9497                }
9498            }
9499            SpecTrial::Decided { spec: true, .. } => {
9500                if mon.pays() {
9501                    mon.fails = 0;
9502                    trial
9503                } else {
9504                    mon.fails += 1;
9505                    if mon.fails >= 4 {
9506                        if mon.plain_ms <= 0.0 {
9507                            // Metal, plain never timed: four doubtful rounds
9508                            // buy the (bounded) plain measurement, and the
9509                            // exact rule decides from it.
9510                            tracing::info!(
9511                                "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
9512                                mon.tokens,
9513                                mon.round_ms,
9514                            );
9515                            return SpecTrial::Plain {
9516                                t0: std::time::Instant::now(),
9517                                gen0: generated,
9518                            };
9519                        }
9520                        tracing::info!(
9521                            "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
9522                            mon.tokens,
9523                            mon.round_ms,
9524                            mon.plain_ms
9525                        );
9526                        SpecTrial::Decided {
9527                            spec: false,
9528                            recheck_at: generated + SPEC_RECHECK_PLAIN,
9529                        }
9530                    } else {
9531                        trial
9532                    }
9533                }
9534            }
9535            other => other,
9536        }
9537    }
9538
9539    /// The MTP block's device-mirror id: the trunk's id with a high bit,
9540    /// so the (kv_id, layer) mirror keys never collide.
9541    fn mtp_kv_id(&self) -> u64 {
9542        self.graph_kv_id | (1u64 << 40)
9543    }
9544
9545    /// The MTP block's mirror layer index: 0 — its own kv_id keeps it
9546    /// apart from the trunk, and the BATCH graph (the warm-up path) keys
9547    /// its mirrors at layer 0 with no base of its own, so the draft's
9548    /// token graph must key the same slot.
9549    const MTP_LAYER_BASE: usize = 0;
9550
9551    /// The wgpu MTP draft writes speculative rows straight into its device
9552    /// mirror while the CPU owner retains only the real prompt/decode anchor.
9553    /// After verification, move that mirror cursor back to the anchor before
9554    /// replaying accepted pairs.  The next graph append then sees the same
9555    /// contiguous position as the CPU/Metal path without uploading stale
9556    /// speculative rows.
9557    #[cfg(feature = "gpu")]
9558    fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
9559        let stored = stored.saturating_sub(self.mtp_pos_base);
9560        self.mtp_graph_mode != Some(true)
9561            || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
9562    }
9563
9564    /// A speculative verify graph appends the full `k+1` trunk rows before
9565    /// the acceptance count is known.  GDN state already has a snapshot
9566    /// restore; Full-attention mirrors need the matching logical cursor
9567    /// rewind so the next graph call does not reject an ahead-of-position KV
9568    /// cache after a partial acceptance.
9569    #[cfg(feature = "gpu")]
9570    fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
9571        let mut ok = true;
9572        let mut expected = false;
9573        for li in 0..self.num_layers {
9574            if matches!(
9575                self.weights.layers[self.phys_layer(li)].attn,
9576                AttnKind::Full { .. }
9577            ) {
9578                // A sealed layer has no mirror: its state moved by `o1_spec_commit`.
9579                if self.o1_flags.get(li).copied().unwrap_or(false) && self.kv_cache.layers[li].o1_sealed() {
9580                    continue;
9581                }
9582                expected = true;
9583                ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
9584            }
9585        }
9586        !expected || ok
9587    }
9588
9589    /// Count the recurrent layers participating in the trunk verify graph.
9590    /// Snapshot restore is all-or-nothing across that set; deriving the count
9591    /// from the model keeps the restore contract valid for looped models too.
9592    fn graph_gdn_layer_count(&self) -> usize {
9593        // stacks that keep their own layers (qwen4, DSV4) leave the generic
9594        // ones empty: no generic GDN layer, nothing to index
9595        (0..self.num_layers)
9596            .filter(|&li| {
9597                self.weights
9598                    .layers
9599                    .get(self.phys_layer(li))
9600                    .is_some_and(|l| matches!(&l.attn, AttnKind::LinearGdn(_)))
9601            })
9602            .count()
9603    }
9604
9605    /// The block's input from (trunk hidden, token): eh_proj · [enorm(e);
9606    /// hnorm(h)] — the same arithmetic the per-op path starts with.
9607    fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
9608        self.mtp_block_inputs(m, &[(hidden, next_token)])
9609    }
9610
9611    /// The block inputs of several (trunk hidden, token) pairs, `[n][hidden]`.
9612    /// On the CUDA hybrid arm eh_proj runs on the card for all of them at
9613    /// once (about 1 ms a pair on the host for Qwen3.8-27B's 27 MB matrix,
9614    /// tens of microseconds there); everywhere else one host matvec a pair.
9615    fn mtp_block_inputs(&mut self, m: &MtpModule, pairs: &[(&[f32], u32)]) -> Vec<f32> {
9616        let hs = self.hidden_size;
9617        let mut cats = vec![0.0f32; pairs.len() * 2 * hs];
9618        for (i, (hidden, tok)) in pairs.iter().enumerate() {
9619            let e = self.embed_single(*tok);
9620            let cat = &mut cats[i * 2 * hs..(i + 1) * 2 * hs];
9621            let (cat_e, cat_h) = cat.split_at_mut(hs);
9622            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
9623            inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
9624        }
9625        let mut xs = vec![0.0f32; pairs.len() * hs];
9626        #[cfg(feature = "gpu")]
9627        if crate::gpu::cuda_active() {
9628            if let Some((model, idx, kind, rs)) = m.eh_proj.graph_weight() {
9629                let gw = crate::gpu::GraphW {
9630                    idx,
9631                    kind,
9632                    row_scale: rs,
9633                    data: &[],
9634                    prism: crate::gpu::GraphPrismOp::None,
9635                    affine: false,
9636                };
9637                let model = model.clone();
9638                if crate::gpu::rows_matvec(&model, &gw, hs, 2 * hs, &cats, pairs.len(), &mut xs) {
9639                    return xs;
9640                }
9641            }
9642        }
9643        for i in 0..pairs.len() {
9644            m.eh_proj.matvec(&cats[i * 2 * hs..(i + 1) * 2 * hs], &mut xs[i * hs..(i + 1) * hs], self.pool.as_deref());
9645        }
9646        xs
9647    }
9648
9649    /// Is the MTP block graphable at all (device up, full attention
9650    /// without softplus, dense FFN)? The plan itself is built per call.
9651    #[cfg(feature = "gpu")]
9652    fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
9653        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
9654            return false;
9655        }
9656        if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode) || !crate::gpu::enabled_here() {
9657            return false;
9658        }
9659        self.mtp_block_shape_ok(m)
9660    }
9661
9662    /// The shape a device MTP step takes (the wgpu token graph and native
9663    /// Metal's `mtp_step_metal` alike): one dense block of full attention
9664    /// without a softplus gate, V as wide as K, no softcap. A MoE draft block
9665    /// drafts op by op on the host and costs more than it saves.
9666    fn mtp_block_shape_ok(&self, m: &MtpModule) -> bool {
9667        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
9668            || self.attn_softcap > 0.0
9669            || self.attention_heads_per_layer.is_some()
9670            // The block graph caches V as wide as K and feeds o_proj
9671            // nh·head_dim; a narrow-V model keeps its MTP block per-op.
9672            || self.v_head_dim.is_some()
9673        {
9674            return false;
9675        }
9676        matches!(
9677            &m.layer.attn,
9678            AttnKind::Full {
9679                softplus_gate: None,
9680                ..
9681            }
9682        ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
9683    }
9684
9685    /// Full MTP token-graph eligibility, including the fused lm-head and all
9686    /// block projection weights.  Keep this distinct from the block-only
9687    /// check: prompt warm-up does not need the head, while a draft step does.
9688    #[cfg(feature = "gpu")]
9689    fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
9690        if !self.mtp_block_graph_ok(m) {
9691            return false;
9692        }
9693        let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
9694            return false;
9695        };
9696        let FfnKind::Dense(d) = &m.layer.ffn else {
9697            return false;
9698        };
9699        d.segs.is_empty()
9700            && wq.graph_weight().is_some()
9701            && wk.graph_weight().is_some()
9702            && wv.graph_weight().is_some()
9703            && wo.graph_weight().is_some()
9704            && d.gate_proj.graph_weight().is_some()
9705            && d.up_proj.graph_weight().is_some()
9706            && d.down_proj.graph_weight().is_some()
9707            && self.weights.lm_head.graph_weight().is_some()
9708    }
9709
9710    /// One MTP block step on the wgpu token graph: block + fused head in
9711    /// one submit, the block hidden and the logits read back together.
9712    /// None = the graph cannot take this block (softplus gate, non-dense
9713    /// FFN, unquantized head, no device) — the caller keeps the per-op
9714    /// path for the whole generation.
9715    #[cfg(feature = "gpu")]
9716    fn mtp_step_graph(
9717        &mut self,
9718        m: &mut MtpModule,
9719        hidden: &[f32],
9720        next_token: u32,
9721        position: usize,
9722    ) -> Option<(Vec<f32>, Vec<f32>)> {
9723        if !self.mtp_graph_ok(m) {
9724            return None;
9725        }
9726        let lw = &m.layer;
9727        let AttnKind::Full {
9728            wq,
9729            wk,
9730            wv,
9731            wo,
9732            q_norm,
9733            k_norm,
9734            output_gate,
9735            softplus_gate,
9736            bias,
9737        } = &lw.attn
9738        else {
9739            return None;
9740        };
9741        if softplus_gate.is_some() {
9742            return None;
9743        }
9744        let FfnKind::Dense(d) = &lw.ffn else {
9745            return None;
9746        };
9747        if !d.segs.is_empty() {
9748            return None; // tube layers run on the segmented path
9749        }
9750        // The block's input first: it borrows `self` mutably (embed scratch,
9751        // pool), the plan below borrows the weights immutably.
9752        let mut x = self.mtp_block_input(m, hidden, next_token);
9753        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
9754            let (_, i, kind, rs) = t.graph_weight()?;
9755            Some(crate::gpu::GraphW {
9756                idx: i,
9757                kind,
9758                row_scale: rs,
9759                data: &[],
9760                prism: crate::gpu::GraphPrismOp::None,
9761                affine: false,
9762            })
9763        }
9764        let (model, _, _, _) = wq.graph_weight()?;
9765        let model = model.clone();
9766        let (lm_gw, lm_rows) = {
9767            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
9768            // The draft's head over the CMF_DRAFT_VOCAB shortlist (the same
9769            // cut the native Metal draft takes): 662 MB a step on Qwen3.8
9770            // becomes 170 MB at 65536; the verify keeps the full head.
9771            let rows = if kind == 6 {
9772                self.draft_head_rows(self.weights.lm_head.rows())
9773            } else {
9774                self.weights.lm_head.rows()
9775            };
9776            (
9777                crate::gpu::GraphW {
9778                    idx: i,
9779                    kind,
9780                    row_scale: rs,
9781                    data: &[],
9782                    prism: crate::gpu::GraphPrismOp::None,
9783                    affine: false,
9784                },
9785                rows,
9786            )
9787        };
9788        let layer = crate::gpu::GraphLayer {
9789            mhc: None,
9790            input_norm: &lw.input_norm,
9791            attn: crate::gpu::GraphAttn::Full {
9792                wq: gw(wq)?,
9793                wk: gw(wk)?,
9794                wv: gw(wv)?,
9795                wo: gw(wo)?,
9796                q_norm: q_norm.as_deref(),
9797                k_norm: k_norm.as_deref(),
9798                late_qk_norm: self.qk_norm_after_rope,
9799                bias: bias
9800                    .as_ref()
9801                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9802                output_gate: *output_gate,
9803                cpu_k: m.kv.k_heads(),
9804                cpu_v: m.kv.v_heads(),
9805                cpu_base: m.kv.base(),
9806                geom: None,
9807                head_gate: None,
9808            },
9809            post_norm: &lw.post_norm,
9810            attn_out_norm: None,
9811            ffn_out_norm: None,
9812            layer_scale: None,
9813            ffn: crate::gpu::GraphFfn::Dense {
9814                gate: gw(&d.gate_proj)?,
9815                up: gw(&d.up_proj)?,
9816                down: gw(&d.down_proj)?,
9817                act: d.act.graph_act()?,
9818                limit: 0.0,
9819                hot: None,
9820            },
9821        };
9822        let nh = self.num_heads;
9823        let (nkv, hd, rd) = self.layer_geom(0);
9824        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9825        let mut logits = Vec::new();
9826        let ok = crate::gpu::forward_token_graph(
9827            &model,
9828            self.mtp_kv_id(),
9829            std::slice::from_ref(&layer),
9830            &[None],
9831            self.o1_epoch,
9832            &self.inv_freq,
9833            &mut x,
9834            nh,
9835            nkv,
9836            hd,
9837            self.attn_scale,
9838            rd,
9839            self.hidden_size,
9840            // An MTP draft can have an independently compacted FFN. The
9841            // graph must use the checked local tensor width, not the trunk
9842            // arch-wide intermediate size.
9843            d.gate_proj.rows(),
9844            position,
9845            self.kv_cache.max_seq_len,
9846            gemma,
9847            self.rms_eps as f32,
9848            Some((&lm_gw, lm_rows)),
9849            &m.final_norm,
9850            &mut logits,
9851            &[],
9852            1,
9853            None,
9854            None,
9855            None,
9856            Self::MTP_LAYER_BASE,
9857            true,
9858        );
9859        match ok {
9860            crate::gpu::TokenGraphOutcome::Completed => {}
9861            crate::gpu::TokenGraphOutcome::Declined => return None,
9862            crate::gpu::TokenGraphOutcome::Failed => {
9863                // The backend has already admitted persistent state.  Keep
9864                // this distinct from a capability refusal so the caller
9865                // cannot switch to the stale CPU MTP cache.
9866                self.clear_sequence_state();
9867                self.graph_failed
9868                    .store(true, std::sync::atomic::Ordering::Relaxed);
9869                self.cancel
9870                    .store(true, std::sync::atomic::Ordering::Relaxed);
9871                return None;
9872            }
9873        }
9874        // The graph may deliberately evaluate only a prefix of the draft
9875        // head.  Do not turn every omitted token into a zero-logit candidate:
9876        // that can win an all-negative shortlist and collapses acceptance.
9877        pad_mtp_draft_logits(&mut logits, self.vocab_size);
9878        Some((logits, x))
9879    }
9880
9881    /// The round's `k` greedy drafts on the CUDA hybrid arm as ONE graph
9882    /// (`gpu_cuda::mtp_chain`): the block input, the block and the shortlist
9883    /// head k times, each step's token embedding gathered on the card from
9884    /// the argmax before it. Err(false): declined before any state changed
9885    /// (the per-step loop takes the round); Err(true): failed after the
9886    /// block's mirror moved.
9887    #[cfg(feature = "cuda")]
9888    fn mtp_draft_chain_cuda(&self, m: &MtpModule, hidden: &[f32], t_next: u32, position: usize, k: usize) -> Result<Vec<u32>, bool> {
9889        let position = position.saturating_sub(self.mtp_pos_base);
9890        if !crate::gpu::cuda_active()
9891            || !Self::mtp_chain_on()
9892            || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
9893            || self.draft_full_streak > 0
9894            || !self.mtp_graph_ok(m)
9895            || hidden.len() != self.hidden_size
9896        {
9897            return Err(false);
9898        }
9899        let lw = &m.layer;
9900        let AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, softplus_gate: None, bias } = &lw.attn else {
9901            return Err(false);
9902        };
9903        let FfnKind::Dense(d) = &lw.ffn else {
9904            return Err(false);
9905        };
9906        if !d.segs.is_empty() {
9907            return Err(false);
9908        }
9909        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
9910            let (_, i, kind, rs) = t.graph_weight()?;
9911            Some(crate::gpu::GraphW { idx: i, kind, row_scale: rs, data: &[], prism: crate::gpu::GraphPrismOp::None, affine: false })
9912        }
9913        let mk = || -> Option<_> {
9914            let (model, _, _, _) = wq.graph_weight()?;
9915            let (_, li, lkind, lrs) = self.weights.lm_head.graph_weight()?;
9916            if lkind != 6 {
9917                return None;
9918            }
9919            let rows = Self::draft_vocab_rows(self.weights.lm_head.rows());
9920            let lm = crate::gpu::GraphW { idx: li, kind: lkind, row_scale: lrs, data: &[], prism: crate::gpu::GraphPrismOp::None, affine: false };
9921            let eh = gw(&m.eh_proj)?;
9922            let layer = crate::gpu::GraphLayer {
9923                mhc: None,
9924                input_norm: &lw.input_norm,
9925                attn: crate::gpu::GraphAttn::Full {
9926                    wq: gw(wq)?,
9927                    wk: gw(wk)?,
9928                    wv: gw(wv)?,
9929                    wo: gw(wo)?,
9930                    q_norm: q_norm.as_deref(),
9931                    k_norm: k_norm.as_deref(),
9932                    late_qk_norm: self.qk_norm_after_rope,
9933                    bias: bias.as_ref().map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9934                    output_gate: *output_gate,
9935                    cpu_k: m.kv.k_heads(),
9936                    cpu_v: m.kv.v_heads(),
9937                    cpu_base: m.kv.base(),
9938                    geom: None,
9939                    head_gate: None,
9940                },
9941                post_norm: &lw.post_norm,
9942                attn_out_norm: None,
9943                ffn_out_norm: None,
9944                layer_scale: None,
9945                ffn: crate::gpu::GraphFfn::Dense {
9946                    gate: gw(&d.gate_proj)?,
9947                    up: gw(&d.up_proj)?,
9948                    down: gw(&d.down_proj)?,
9949                    act: d.act.graph_act()?,
9950                    limit: 0.0,
9951                    hot: None,
9952                },
9953            };
9954            // the embedding: q4tp in the same file (the chain gathers its rows)
9955            let (emodel, ei, ekind, ers) = self.weights.embed_tokens.graph_weight()?;
9956            if ekind != 6 || !std::sync::Arc::ptr_eq(emodel, model) || crate::prism::is_inverse_embedding(model, &model.tensors[ei].name) {
9957                return None;
9958            }
9959            let emb = crate::gpu::GraphW { idx: ei, kind: ekind, row_scale: ers, data: &[], prism: crate::gpu::GraphPrismOp::None, affine: false };
9960            Some((model.clone(), lm, rows, eh, layer, emb))
9961        };
9962        let Some((model, lm, rows, eh, layer, emb)) = mk() else {
9963            return Err(false);
9964        };
9965        let (nkv, hd, rd) = self.layer_geom(0);
9966        let e0 = self.embed_single(t_next);
9967        crate::gpu_cuda::mtp_chain(
9968            &model,
9969            self.mtp_kv_id(),
9970            &layer,
9971            &self.inv_freq,
9972            hidden,
9973            &e0,
9974            t_next,
9975            k,
9976            self.num_heads,
9977            nkv,
9978            hd,
9979            self.attn_scale,
9980            rd,
9981            self.hidden_size,
9982            position,
9983            self.kv_cache.max_seq_len,
9984            self.norm_style == cortiq_core::NormStyle::Gemma,
9985            self.rms_eps as f32,
9986            (&lm, rows),
9987            &m.final_norm,
9988            &eh,
9989            &m.enorm,
9990            &m.hnorm,
9991            &emb,
9992            Self::MTP_LAYER_BASE,
9993        )
9994    }
9995
9996    /// The warm-ups of one speculative round on the device: every accepted
9997    /// (hidden, token) pair as ONE batched graph run over the MTP block
9998    /// (no head) — its kv_append lands the pairs in the block's mirror.
9999    /// `pairs` are consecutive positions from `first_pos`.  The tri-state
10000    /// result is intentional: a refusal before admission may use the
10001    /// per-row/CPU route, while a failure after admission must terminate the
10002    /// sequence rather than fall through to a stale CPU cache.
10003    #[cfg(feature = "gpu")]
10004    fn mtp_warm_graph(
10005        &mut self,
10006        m: &mut MtpModule,
10007        pairs: &[(&[f32], u32)],
10008        first_pos: usize,
10009    ) -> crate::gpu::BatchGraphOutcome {
10010        let first_pos = first_pos.saturating_sub(self.mtp_pos_base);
10011        if pairs.is_empty() {
10012            return crate::gpu::BatchGraphOutcome::Completed;
10013        }
10014        if !self.mtp_block_graph_ok(m) {
10015            return crate::gpu::BatchGraphOutcome::Declined;
10016        }
10017        let hs = self.hidden_size;
10018        // Block inputs for every pair (eh_proj on the per-op path, one
10019        // matvec each — the plan's own prologue).
10020        let mut hiddens = self.mtp_block_inputs(m, pairs);
10021        debug_assert_eq!(hiddens.len(), pairs.len() * hs);
10022        spec_stamp("w.inp");
10023        let lw = &m.layer;
10024        let AttnKind::Full {
10025            wq,
10026            wk,
10027            wv,
10028            wo,
10029            q_norm,
10030            k_norm,
10031            output_gate,
10032            bias,
10033            ..
10034        } = &lw.attn
10035        else {
10036            return crate::gpu::BatchGraphOutcome::Declined;
10037        };
10038        let FfnKind::Dense(d) = &lw.ffn else {
10039            return crate::gpu::BatchGraphOutcome::Declined;
10040        };
10041        if !d.segs.is_empty() {
10042            return crate::gpu::BatchGraphOutcome::Declined; // tube layers run on the segmented path
10043        }
10044        // The graph has no arm for a projected output gate, and only the
10045        // activations `graph_act` names.
10046        let (
10047            AttnKind::Full {
10048                softplus_gate: None,
10049                ..
10050            },
10051            Some(gact),
10052        ) = (&lw.attn, d.act.graph_act())
10053        else {
10054            return crate::gpu::BatchGraphOutcome::Declined;
10055        };
10056        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
10057            let (_, i, kind, rs) = t.graph_weight()?;
10058            Some(crate::gpu::GraphW {
10059                idx: i,
10060                kind,
10061                row_scale: rs,
10062                data: &[],
10063                prism: crate::gpu::GraphPrismOp::None,
10064                affine: false,
10065            })
10066        }
10067        let Some((model, _, _, _)) = wq.graph_weight() else {
10068            return crate::gpu::BatchGraphOutcome::Declined;
10069        };
10070        let model = model.clone();
10071        let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
10072            gw(wq),
10073            gw(wk),
10074            gw(wv),
10075            gw(wo),
10076            gw(&d.gate_proj),
10077            gw(&d.up_proj),
10078            gw(&d.down_proj),
10079        ) else {
10080            return crate::gpu::BatchGraphOutcome::Declined;
10081        };
10082        let layer = crate::gpu::GraphLayer {
10083            mhc: None,
10084            input_norm: &lw.input_norm,
10085            attn: crate::gpu::GraphAttn::Full {
10086                wq: gwq,
10087                wk: gwk,
10088                wv: gwv,
10089                wo: gwo,
10090                q_norm: q_norm.as_deref(),
10091                k_norm: k_norm.as_deref(),
10092                late_qk_norm: self.qk_norm_after_rope,
10093                bias: bias
10094                    .as_ref()
10095                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10096                output_gate: *output_gate,
10097                cpu_k: m.kv.k_heads(),
10098                cpu_v: m.kv.v_heads(),
10099                cpu_base: m.kv.base(),
10100                geom: None,
10101                head_gate: None,
10102            },
10103            post_norm: &lw.post_norm,
10104            attn_out_norm: None,
10105            ffn_out_norm: None,
10106            layer_scale: None,
10107            ffn: crate::gpu::GraphFfn::Dense {
10108                gate: gg,
10109                up: gu,
10110                down: gd,
10111                act: gact,
10112                limit: 0.0,
10113                hot: None,
10114            },
10115        };
10116        let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
10117        let nh = self.num_heads;
10118        let (nkv, hd, rd) = self.layer_geom(0);
10119        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
10120        // nothing reads the block's rows back: the warm-up is its K/V rows
10121        // (CUDA: no readback, no wait)
10122        let defer = crate::gpu::cuda_active();
10123        if defer {
10124            crate::gpu::set_batch_defer(true);
10125        }
10126        let out = crate::gpu::forward_batch_graph(
10127            &model,
10128            self.mtp_kv_id(),
10129            std::slice::from_ref(&layer),
10130            &self.inv_freq,
10131            &mut hiddens,
10132            nh,
10133            nkv,
10134            hd,
10135            rd,
10136            hs,
10137            // See `mtp_step_graph`: draft FFN geometry is local to this
10138            // block after independent MTP compaction.
10139            d.gate_proj.rows(),
10140            &positions,
10141            self.kv_cache.max_seq_len,
10142            gemma,
10143            self.rms_eps as f32,
10144            self.attn_scale,
10145            pairs.len(),
10146            &[],
10147            0,
10148            None,
10149            None,
10150        );
10151        if defer {
10152            crate::gpu::set_batch_defer(false);
10153        }
10154        spec_stamp("w.run");
10155        out
10156    }
10157
10158    /// Complete an MTP warm-up after the batched graph has refused.  A
10159    /// graphable block is retried one row at a time; once any device row has
10160    /// been admitted, a CPU fallback would observe a stale mirror, so every
10161    /// token-graph refusal is terminal.  If the block is not graphable and no
10162    /// mirror exists yet, warming on the CPU is safe and records the CPU mode
10163    /// for the rest of the generation.
10164    #[cfg(feature = "gpu")]
10165    fn mtp_warm_graph_fallback(
10166        &mut self,
10167        m: &mut MtpModule,
10168        pairs: &[(&[f32], u32)],
10169        first_pos: usize,
10170    ) -> bool {
10171        let first_pos = first_pos.saturating_sub(self.mtp_pos_base);
10172        if pairs.is_empty() {
10173            return true;
10174        }
10175        let graphable = self.mtp_block_graph_ok(m);
10176        if !graphable {
10177            // A previously admitted mirror cannot be made coherent by
10178            // appending to the host cache.  The caller turns this into a
10179            // terminal generation error and clears both mirrors.
10180            if self.mtp_graph_mode == Some(true) {
10181                return false;
10182            }
10183            self.mtp_graph_mode = Some(false);
10184            for (j, (h, t)) in pairs.iter().enumerate() {
10185                self.mtp_warm(m, h, *t, first_pos + j);
10186            }
10187            return true;
10188        }
10189
10190        // The batch refusal is recoverable only through the same device
10191        // state.  Keep rows owned until each token graph has completed; a
10192        // None is treated as unsafe because the token-graph API deliberately
10193        // collapses its backend refusal/failure into that result.
10194        for (j, (h, t)) in pairs.iter().enumerate() {
10195            if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
10196                return false;
10197            }
10198        }
10199        self.mtp_graph_mode = Some(true);
10200        true
10201    }
10202
10203    /// Warm a contiguous set of MTP pairs using the existing graph seam, with
10204    /// an all-or-nothing error contract for callers that already admitted the
10205    /// trunk batch.  The non-GPU build keeps the same pair accounting while
10206    /// using the established CPU warm path.
10207    #[cfg(feature = "gpu")]
10208    fn mtp_warm_prefill_pairs(
10209        &mut self,
10210        m: &mut MtpModule,
10211        pairs: &[(&[f32], u32)],
10212        first_pos: usize,
10213    ) -> Result<(), &'static str> {
10214        // Keep unsupported token-graph heads on the established CPU MTP
10215        // route before admitting any block mirror.  Once a device mirror is
10216        // active, the same condition is terminal because CPU rows cannot
10217        // repair its state.
10218        if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
10219            if self.mtp_graph_mode == Some(true) {
10220                return Err("MTP token graph became unavailable after admission");
10221            }
10222            self.mtp_graph_mode = Some(false);
10223            for (j, (h, t)) in pairs.iter().enumerate() {
10224                self.mtp_warm(m, h, *t, first_pos + j);
10225            }
10226            return Ok(());
10227        }
10228        match self.mtp_warm_graph(m, pairs, first_pos) {
10229            crate::gpu::BatchGraphOutcome::Completed => {
10230                if !pairs.is_empty() {
10231                    self.mtp_graph_mode = Some(true);
10232                }
10233                Ok(())
10234            }
10235            crate::gpu::BatchGraphOutcome::Declined => {
10236                if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
10237                    Ok(())
10238                } else {
10239                    Err("MTP warm-up fallback failed after device admission")
10240                }
10241            }
10242            crate::gpu::BatchGraphOutcome::Failed => {
10243                Err("MTP warm batch graph failed after admission")
10244            }
10245        }
10246    }
10247
10248    #[cfg(not(feature = "gpu"))]
10249    fn mtp_warm_prefill_pairs(
10250        &mut self,
10251        m: &mut MtpModule,
10252        pairs: &[(&[f32], u32)],
10253        first_pos: usize,
10254    ) -> Result<(), &'static str> {
10255        for (j, (h, t)) in pairs.iter().enumerate() {
10256            self.mtp_warm(m, h, *t, first_pos + j);
10257        }
10258        Ok(())
10259    }
10260
10261    /// The MTP block alone — advance its KV with a (hidden, token) pair the
10262    /// verify just proved, without paying the head. What keeps the draft's
10263    /// attention context warm between speculative rounds.
10264    fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
10265        let position = position.saturating_sub(self.mtp_pos_base);
10266        let e = self.embed_single(next_token);
10267        let mut cat = vec![0.0f32; 2 * self.hidden_size];
10268        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
10269        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
10270        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
10271        let mut x = vec![0.0f32; self.hidden_size];
10272        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
10273        inference::rms_norm_into(
10274            &x,
10275            &m.layer.input_norm,
10276            self.rms_eps,
10277            self.norm_style,
10278            &mut self.ws.n1,
10279        );
10280        let attn = match &m.layer.attn {
10281            AttnKind::Full {
10282                wq,
10283                wk,
10284                wv,
10285                wo,
10286                q_norm,
10287                k_norm,
10288                output_gate,
10289                softplus_gate,
10290                bias,
10291            } => {
10292                let mut cfg = self.attn_cfg(position);
10293                cfg.q_norm = q_norm.as_deref();
10294                cfg.k_norm = k_norm.as_deref();
10295                cfg.output_gate = *output_gate;
10296                cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
10297                cfg.bias = bias
10298                    .as_ref()
10299                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
10300                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
10301            }
10302            _ => return,
10303        };
10304        let _ = attn;
10305    }
10306
10307    /// Speculative decode ON the wgpu whole-token graph: draft k with the
10308    /// MTP head, verify all of them plus the tip in ONE batched graph
10309    /// submit whose tail folds the head, commit the accepted prefix and
10310    /// roll the GDN state back to the last real position. Greedy only —
10311    /// output equals the plain graph's token for token, the way the DSV4
10312    /// verify equals the walk.
10313    #[cfg(feature = "gpu")]
10314    #[allow(clippy::too_many_arguments)]
10315    fn graph_spec_step(
10316        &mut self,
10317        m: &mut MtpModule,
10318        hidden: &[f32],
10319        t_next: u32,
10320        next_pos: usize,
10321        drafted: &mut usize,
10322        accepted: &mut usize,
10323        // The committed stream (prompt + generated so far, `t_next`
10324        // included): the sampler chain's penalties read it, and the
10325        // sampling arm extends it with the drafts position by position.
10326        all_ids: &mut Vec<u32>,
10327        // Tokens left before `max_tokens`. A round commits up to k
10328        // accepted drafts, and those positions are already in the cache,
10329        // so the depth is capped here — trimming the output afterwards
10330        // would leave cache rows the committed stream does not have.
10331        room: usize,
10332    ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
10333        // 3 is the measured optimum on Qwen3.6-27B / RTX 5090 (medians
10334        // of three, greedy): 51.1 tok/s against a plain 49.4, where k=2
10335        // gives 46.1, k=4 50.0, k=5 47.4, k=6 45.2. Acceptance is 89-91%
10336        // throughout — what turns the curve over is the verify, which
10337        // costs ~7.4 ms per extra position, and the draft ~3 ms a step.
10338        // 4 since the draft moved onto the graph (Qwen3.8-27B / 5090:
10339        // k=3 51.2, k=4 51.8 with the per-op draft; the graph draft
10340        // halves the draft cost, so the extra draft is cheaper still).
10341        // 5 with the int8 verify (the default: measured 76.5 against
10342        // k=4's 72-74 and k=6's 74 on the 5090), 4 with the f32 one.
10343        #[cfg(target_os = "macos")]
10344        let metal_native = crate::gpu::q1_force();
10345        #[cfg(not(target_os = "macos"))]
10346        let metal_native = false;
10347        #[cfg(feature = "gpu")]
10348        let k_default = if metal_native {
10349            // the Metal verify's GEMM tile is 8 rows wide and flat in b:
10350            // seven drafts + the tip fill it for free
10351            7
10352        } else if crate::gpu_wgpu::verify_i8_on() {
10353            5
10354        } else {
10355            4
10356        };
10357        #[cfg(not(feature = "gpu"))]
10358        let k_default = 4;
10359        let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
10360            .ok()
10361            .and_then(|v| v.parse().ok())
10362            .filter(|&v| (1..=8).contains(&v));
10363        // Adaptive depth: start below the card's flat-verify optimum and
10364        // let the accepted fraction move it — predictable text climbs to
10365        // the old default within a few rounds, prose settles at 2-3 where
10366        // the shorter verify pays.
10367        // Native Metal over a q4tp trunk: two rows verify at about a plain
10368        // token's price and three at a quarter more, so the depth follows
10369        // each depth's running acceptance instead of filling the GEMM tile.
10370        let metal_small = metal_native && self.metal_small_batch_verify();
10371        // Greedy without penalties verifies by argmax equality (bit-exact
10372        // against the plain path). Anything else is speculative SAMPLING:
10373        // each draft is a DRAW from the MTP head's post-chain distribution
10374        // q_j, kept for the accept test; the verify's rows give p_j.
10375        let cfg = self.sampler_config.clone();
10376        let penalized = !(cfg.repetition_penalty == 1.0
10377            && cfg.presence_penalty == 0.0
10378            && cfg.suppress_tokens.is_empty());
10379        // Three verify regimes: plain greedy (argmax of the raw rows),
10380        // greedy WITH penalties (argmax of the penalized rows — a single
10381        // pass each, no distributions), and sampling (draw / accept /
10382        // correct on post-chain distributions).
10383        let greedy_pen = cfg.temperature < 1e-6 && penalized;
10384        let sampling = cfg.temperature >= 1e-6;
10385        // A DFlash drafter beside the model drafts a round in one block
10386        // (plain greedy on native Metal, where the verify hands it the
10387        // trunk's rows). Its block costs the same at any depth, so its
10388        // rounds go deep where its drafts hold; the MTP chain's are short
10389        // and nearly free. Each keeps its own account of what it holds, and
10390        // the round goes to the one that lands more tokens in a unit of time
10391        // (the one in turn has to be beaten by more than the noise). The
10392        // block was trained on Latin-script text: while the committed tokens
10393        // come from the high table (`draft_full_streak`) the chain keeps the
10394        // rounds. A turn the chain takes back makes the block sit out, longer
10395        // each time it is a short one.
10396        // The wgpu graphs keep the drafter's rows on their way (`layer_taps`):
10397        // they become context here, and a round is the block's only while that
10398        // context reaches the tip. There the block is for measuring, pinned
10399        // with `CMF_DFLASH_PICK=dflash`, and the chain keeps the rounds
10400        // otherwise: the block is a matrix a submission, and on a 16 GB card
10401        // the Turbo trunk leaves its 1.3 GB no room among the resident weights
10402        // (RTX 2000 Ada: 135 ms a block against 11 ms for the chain's drafts,
10403        // and a verify of eight rows costs two plain tokens).
10404        if !metal_native {
10405            self.dflash_taps_absorb();
10406        }
10407        // The CUDA hybrid arm keeps the drafter's context at the tip every
10408        // call (its graphs copy the tapped rows out) and verifies eight rows
10409        // at about one price: the round goes to the drafter that lands more
10410        // tokens at that card's prices, as on Metal.
10411        let cuda_rate = !metal_native && crate::gpu::cuda_active();
10412        let dflash_ok = !sampling
10413            && !greedy_pen
10414            && match &self.dflash {
10415                Some(d) => metal_native || ((dflash_pick() == Some(true) || cuda_rate) && d.next_position() == next_pos),
10416                None => false,
10417            };
10418        if !metal_native && !cuda_rate && self.dflash.is_some() && dflash_pick() != Some(true) {
10419            static SAID: std::sync::Once = std::sync::Once::new();
10420            SAID.call_once(|| {
10421                tracing::info!(
10422                    "DFlash drafter: on wgpu the MTP chain keeps the rounds (CMF_DFLASH_PICK=dflash pins the block)"
10423                )
10424            });
10425        }
10426        let (k_flash, rate_flash) = if cuda_rate {
10427            spec_plan_at(&CUDA_ROUND_COST, &self.dflash_hold, CUDA_BLOCK_DRAFT, 0.0)
10428        } else {
10429            metal_spec_plan(&self.dflash_hold, DFLASH_ROUND_DRAFT)
10430        };
10431        let (_, rate_mtp) = if cuda_rate {
10432            spec_plan_at(&CUDA_ROUND_COST, &self.spec_hold, 0.0, CUDA_DRAFT_STEP)
10433        } else {
10434            metal_spec_plan(&self.spec_hold, 0.0)
10435        };
10436        // A sidecar that carries a router says which drafter the text at the
10437        // tip is for, from the target's own row there: two subspaces of such
10438        // rows, one of the rounds the block won and one of the chain's, and
10439        // the row goes to the one that holds it better (`dflash::Route`).
10440        // A few rounds are averaged, and the drafter in turn keeps a round
10441        // the average does not clearly give away.
10442        let routed = match (&self.dflash, dflash_ok) {
10443            (Some(d), true) => d.route.as_ref().map(|r| r.margin(hidden)),
10444            _ => None,
10445        };
10446        if let Some(m) = routed {
10447            self.dflash_lean += DFLASH_LEAN_STEP * (m - self.dflash_lean);
10448        }
10449        // CUDA verifies a lookup of sixteen rows at about one price: while the
10450        // lookups land (their running gain past `DFLASH_LOOKUP_KEEPS`), an
10451        // edit or a quote stays the chain's, which the router (fitted where
10452        // the lookup was eight rows) would give the block
10453        let copying = (routed.is_some() || cuda_rate)
10454            && dflash_pick().is_none()
10455            && (tail_repeat(all_ids) >= DFLASH_COPY_MIN || (cuda_rate && self.spec_lookup_gain > DFLASH_LOOKUP_KEEPS));
10456        let dflash_round = dflash_ok
10457            && match (dflash_pick(), routed) {
10458                (Some(pin), _) => pin,
10459                (None, Some(_)) if copying => false,
10460                // the block drafts over the head's shortlist: text from the
10461                // high table is the chain's (CUDA, where the router alone
10462                // sent a Danish article to the block: 55 against 73 tok/s)
10463                (None, Some(_)) if cuda_rate && self.draft_full_streak > 0 => false,
10464                (None, Some(_)) if self.dflash_last => self.dflash_lean > -DFLASH_LEAN_BAND,
10465                (None, Some(_)) => self.dflash_lean > DFLASH_LEAN_BAND,
10466                // the chain's short round with the prompt lookup copies for less
10467                (None, None) if copying => false,
10468                (None, None) if !metal_small && !cuda_rate => true,
10469                (None, None) if self.draft_full_streak > 0 => false,
10470                (None, None) if self.dflash_last => rate_flash * 1.05 >= rate_mtp,
10471                (None, None) if self.dflash_wait > 0 => {
10472                    self.dflash_wait -= 1;
10473                    false
10474                }
10475                (None, None) => rate_flash > rate_mtp * 1.05,
10476            };
10477        if dflash_ok && dflash_round != self.dflash_last {
10478            if dflash_round {
10479                (self.dflash_turn, self.dflash_turn_sum) = (0, (0.0, 0.0));
10480            } else {
10481                // the block's turn is over: it comes back to a fresh account
10482                let (tokens, cost) = self.dflash_turn_sum;
10483                if self.dflash_turn >= DFLASH_TURN && tokens >= rate_mtp * cost {
10484                    self.dflash_wait = DFLASH_WAIT_DIP;
10485                } else {
10486                    self.dflash_back = if self.dflash_turn < DFLASH_TURN {
10487                        (self.dflash_back * 2).min(DFLASH_WAIT_MOST)
10488                    } else {
10489                        DFLASH_WAIT
10490                    };
10491                    self.dflash_wait = self.dflash_back;
10492                }
10493                self.dflash_hold = DFLASH_HOLD_PRIOR;
10494            }
10495            if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
10496                eprintln!(
10497                    "spec-drafter: {} ({rate_flash:.2} against {rate_mtp:.2} tokens in a plain token's time)",
10498                    if dflash_round { "the DFlash block" } else { "the MTP chain" }
10499                );
10500            }
10501        }
10502        self.dflash_turn += u32::from(dflash_round);
10503        self.dflash_last = dflash_round;
10504        // a pinned block never hands a round to the chain: its cache may lapse
10505        let mtp_in_step = dflash_pick() != Some(true) || dflash_rows_log().is_some();
10506        let (k_start, k_max) = if dflash_round && metal_small {
10507            (k_flash, 7)
10508        } else if metal_small {
10509            (metal_spec_depth(&self.spec_hold), 7)
10510        } else if metal_native || dflash_round {
10511            // (a block costs the same at any depth: its rounds start at all seven)
10512            (7, 7)
10513        } else {
10514            (3, k_default.max(5))
10515        };
10516        let k_full: usize = k_env.unwrap_or_else(|| {
10517            if metal_small {
10518                k_start
10519            } else if dflash_round && !metal_native {
10520                self.dflash_k_adapt.unwrap_or(k_start)
10521            } else {
10522                self.spec_k_adapt.unwrap_or(k_start)
10523            }
10524        });
10525        let k_spec = k_full.min(room).max(1);
10526        // a tail round cut short by `room` says nothing about the text:
10527        // it must not move the adaptive depth the next request starts at
10528        let k_capped = k_spec < k_full;
10529        if next_pos == 0 {
10530            return None;
10531        }
10532        let t_round = std::time::Instant::now();
10533        // Submissions per phase — and they say where the round's money is.
10534        // Qwen3.6-27B on an RTX 5090, k=3:
10535        //
10536        //   draft   9.3 ms / 12 submissions   (four per MTP step)
10537        //   verify 52.8 ms /  1               (the batched graph)
10538        //   commit  5.4 ms /  6               (two per warm)
10539        //
10540        // The verify is already one submit. The draft's own work is 834 MB
10541        // a step — 0.8 ms at this card's measured 1056 GB/s — against 3.1
10542        // ms measured, so ~0.58 ms of every step is round trip, not
10543        // arithmetic, and the same holds for the warms. Eighteen round
10544        // trips a round at roughly half a millisecond each is ~11 ms of a
10545        // 68 ms round: fusing the MTP block into ONE submit the way the
10546        // trunk already is projects to ~64 tok/s against today's 50.9.
10547        // That is the largest measured item left on this path.
10548        let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
10549        let sub0 = subs();
10550        let k_spec = match &self.dflash {
10551            Some(d) if dflash_round => k_spec.min(d.g.block - 1),
10552            _ => k_spec,
10553        };
10554        // Sampling with a top-k goes through the SPARSE chain: the dense
10555        // one builds nine 248k-float distributions a round (four drafts,
10556        // five verify rows) and measured 19-22 tok/s against a plain 40 —
10557        // the host, not the card. Sparse, the same nine cost tens of
10558        // microseconds each.
10559        let sparse = sampling && sampler::sparse_ok(&cfg);
10560        let base_len = all_ids.len();
10561        if sampling && !sparse && self.spec_q.len() < k_spec {
10562            self.spec_q.resize_with(k_spec, Vec::new);
10563        }
10564        if sparse && self.spec_qs.len() < k_spec {
10565            self.spec_qs.resize_with(k_spec, Vec::new);
10566        }
10567        // Draft the chain: first from the trunk's tip hidden, then the head
10568        // iterating on itself. Rows land in the MTP KV; the chain rows past
10569        // the first are speculation over speculative state and roll back
10570        // below, replaced by verified pairs.
10571        let mut drafts = Vec::with_capacity(k_spec);
10572        let mut hx = hidden.to_vec();
10573        // CMF_SPEC_DBG=1: draft 0 through BOTH MTP arms (graph and per-op)
10574        // from the same inputs — are the arms the difference, or the inputs?
10575        let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
10576        spec_stamp("pro");
10577        if dflash_round {
10578            drafts = self.dflash_draft(t_next, next_pos, k_spec);
10579            spec_stamp("d.flash");
10580        }
10581        // the chain's drafts of a round the block has, for the rows log: its
10582        // cache is then cut and warmed as after a round of its own
10583        let mut beside: Vec<u32> = Vec::new();
10584        #[cfg(target_os = "macos")]
10585        if dflash_round && dflash_rows_log().is_some() && self.mtp_graph_mode != Some(false) {
10586            match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, 7) {
10587                Ok(ids) => {
10588                    self.mtp_graph_mode = Some(true);
10589                    beside = ids;
10590                }
10591                Err(true) => {
10592                    tracing::error!("mtp Metal draft chain failed after commit");
10593                    self.clear_sequence_state();
10594                    self.graph_failed
10595                        .store(true, std::sync::atomic::Ordering::Relaxed);
10596                    self.cancel
10597                        .store(true, std::sync::atomic::Ordering::Relaxed);
10598                    return None;
10599                }
10600                Err(false) => {}
10601            }
10602        }
10603        // Plain greedy on native Metal: the whole chain as one command
10604        // buffer (device argmax + embedding gather between the steps).
10605        // A decline before commit hands the round to the per-step loop
10606        // below; a failure after commit is terminal, like any graph
10607        // failure after admission.
10608        #[cfg(target_os = "macos")]
10609        if metal_native && !dflash_round && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
10610            match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
10611                Ok(ids) => {
10612                    self.mtp_graph_mode = Some(true);
10613                    drafts = ids;
10614                }
10615                Err(true) => {
10616                    tracing::error!("mtp Metal draft chain failed after commit");
10617                    self.clear_sequence_state();
10618                    self.graph_failed
10619                        .store(true, std::sync::atomic::Ordering::Relaxed);
10620                    self.cancel
10621                        .store(true, std::sync::atomic::Ordering::Relaxed);
10622                    return None;
10623                }
10624                Err(false) => {}
10625            }
10626        }
10627        // CUDA hybrid arm, greedy: the whole chain as one graph (device
10628        // argmax and embedding gather between the steps). A suppress list
10629        // whose ids all sit past the draft shortlist cannot change its
10630        // argmax. A decline hands the round to the per-step loop below; a
10631        // failure after admission is terminal, like any graph failure.
10632        #[cfg(feature = "cuda")]
10633        if crate::gpu::cuda_active()
10634            && !dflash_round
10635            && !sampling
10636            && !spec_dbg
10637            && drafts.is_empty()
10638            && self.mtp_graph_mode != Some(false)
10639            && (!greedy_pen
10640                || (cfg.repetition_penalty == 1.0
10641                    && cfg.presence_penalty == 0.0
10642                    && cfg.suppress_tokens.iter().all(|&t| (t as usize) >= Self::draft_vocab_rows(self.weights.lm_head.rows()))))
10643        {
10644            match self.mtp_draft_chain_cuda(m, &hx, t_next, next_pos - 1, k_spec) {
10645                Ok(ids) => {
10646                    self.mtp_graph_mode = Some(true);
10647                    drafts = ids;
10648                    spec_stamp("d.chain");
10649                }
10650                Err(true) => {
10651                    tracing::error!("cuda draft chain failed after admission");
10652                    self.clear_sequence_state();
10653                    self.graph_failed
10654                        .store(true, std::sync::atomic::Ordering::Relaxed);
10655                    self.cancel
10656                        .store(true, std::sync::atomic::Ordering::Relaxed);
10657                    return None;
10658                }
10659                Err(false) => {}
10660            }
10661        }
10662        for j in drafts.len()..k_spec {
10663            let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
10664            let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
10665            if spec_dbg {
10666                let saved = self.mtp_graph_mode;
10667                self.mtp_graph_mode = Some(false);
10668                let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
10669                self.mtp_graph_mode = saved;
10670                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10671                    return None;
10672                }
10673                m.kv.truncate_last(1);
10674                dbg_ref = Some(r);
10675            }
10676            let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
10677            if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10678                return None;
10679            }
10680            if let Some((lg_cpu, h_cpu)) = dbg_ref {
10681                let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
10682                let dl = lg
10683                    .iter()
10684                    .zip(&lg_cpu)
10685                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
10686                let dh = hj
10687                    .iter()
10688                    .zip(&h_cpu)
10689                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
10690                eprintln!(
10691                    "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 {}",
10692                    next_pos - 1 + j,
10693                    sampler::argmax(&lg_cpu),
10694                    sampler::argmax(&lg),
10695                    n(&h_cpu),
10696                    n(&hj),
10697                    m.kv.seq_len
10698                );
10699            }
10700            let dj = if sparse {
10701                let mut q = std::mem::take(&mut self.spec_qs[j]);
10702                let ok = sampler::sparse_distribution_into(
10703                    &lg,
10704                    &cfg,
10705                    all_ids,
10706                    &mut self.sampler_scratch,
10707                    self.pool.as_deref(),
10708                    &mut q,
10709                );
10710                let d = if ok {
10711                    sampler::draw_sparse(&q, &mut self.rng)
10712                } else {
10713                    // everything filtered: the dense chain's greedy fallback
10714                    let t = sampler::argmax(&lg);
10715                    q.clear();
10716                    q.push((t, 1.0));
10717                    t
10718                };
10719                self.spec_qs[j] = q;
10720                all_ids.push(d);
10721                d
10722            } else if sampling {
10723                let mut q = std::mem::take(&mut self.spec_q[j]);
10724                sampler::distribution_into(
10725                    &lg,
10726                    &cfg,
10727                    all_ids,
10728                    &mut self.sampler_scratch,
10729                    self.pool.as_deref(),
10730                    &mut q,
10731                );
10732                let d = sampler::draw(&q, &mut self.rng);
10733                self.spec_q[j] = q;
10734                all_ids.push(d); // the next draft's penalties see this one
10735                d
10736            } else if greedy_pen {
10737                let d = sampler::argmax_penalized(
10738                    &lg,
10739                    &cfg,
10740                    all_ids,
10741                    &mut self.sampler_scratch,
10742                    self.pool.as_deref(),
10743                );
10744                all_ids.push(d);
10745                d
10746            } else {
10747                sampler::argmax(&lg)
10748            };
10749            attention::recycle_buf(&mut lg);
10750            drafts.push(dj);
10751            hx = hj;
10752            spec_stamp("d.pick");
10753        }
10754        all_ids.truncate(base_len);
10755        // Prompt lookup, plain greedy on native Metal, where a verify of
10756        // four to eight rows has one price: when the tip and the head's
10757        // drafts repeat an earlier stretch of the sequence, the tokens that
10758        // followed it there go on as drafts. The head's drafts are the
10759        // test: a lookup continues only what they agree with.
10760        let k_mtp = k_spec;
10761        // steps the MTP block took this round: its own round's drafts, the
10762        // chain run beside a DFlash block, or none
10763        let mtp_steps = if dflash_round { beside.len() } else { k_mtp };
10764        // The CUDA hybrid arm verifies up to 16 rows on the tensor cores at
10765        // about one price (8 rows 1.05x a plain token, 16 about 1.6x on a
10766        // 3090): a copy may run 15 drafts deep there.
10767        let cuda_window = crate::gpu::cuda_active();
10768        let k_room = room.min(if cuda_window && self.spec_lookup_wide { 15 } else { 7 });
10769        // A suppress list alone scores a continuation the same at any
10770        // history, so the CUDA window takes lookups under it too.
10771        let history_free = cfg.repetition_penalty == 1.0 && cfg.presence_penalty == 0.0;
10772        if (metal_native || cuda_window)
10773            && !sampling
10774            && (!greedy_pen || (cuda_window && history_free))
10775            && k_mtp < k_room
10776            && drafts.len() == k_mtp
10777            && Self::spec_lookup_on()
10778        {
10779            if self.spec_lookup_rest > 0 {
10780                self.spec_lookup_rest -= 1;
10781            } else {
10782                all_ids.extend_from_slice(&drafts);
10783                let more =
10784                    lookup_continuation(all_ids, k_mtp + LOOKUP_MIN_REAL, k_room - k_mtp).to_vec();
10785                all_ids.truncate(base_len);
10786                drafts.extend(more);
10787            }
10788        }
10789        let k_spec = drafts.len();
10790        spec_stamp("d.look");
10791        *drafted += k_spec;
10792        let t_draft = t_round.elapsed();
10793        let sub_draft = subs();
10794        // Verify batch: [t_next, d1 .. d_{k-1}] at next_pos.. — every row's
10795        // logits come back from the graph's own head.
10796        let b = k_spec + 1;
10797        let mut hiddens = vec![0.0f32; b * self.hidden_size];
10798        for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
10799            let e = self.embed_single(t);
10800            hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
10801        }
10802        let positions: Vec<usize> = (next_pos..next_pos + b).collect();
10803        spec_stamp("v.emb");
10804        let (lm_gw, lm_rows) = {
10805            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
10806            (
10807                crate::gpu::GraphW {
10808                    idx: i,
10809                    kind,
10810                    row_scale: rs,
10811                    data: &[],
10812                    prism: crate::gpu::GraphPrismOp::None,
10813                    affine: false,
10814                },
10815                self.weights.lm_head.rows(),
10816            )
10817        };
10818        let mut logits = Vec::new();
10819        let final_norm = self.weights.final_norm.clone();
10820        // Plain greedy on Metal: the b argmaxes come from the device
10821        // (`argmax_rows` after the head) and the 7.9 MB logits plane is
10822        // never read back — the round's decision needs only the ids, and
10823        // the loop top takes the last verified id as `spec_forced`, which
10824        // is exactly what its argmax of the row would give. The full rows
10825        // stay for anything that reads them: sampling, penalties,
10826        // confidence, the verify oracle, the logit dump.
10827        // `CMF_METAL_DEV_ARGMAX=0` keeps the host path.
10828        #[cfg(target_os = "macos")]
10829        let greedy_dev = metal_native
10830            && !sampling
10831            && !greedy_pen
10832            && !self.confidence_on
10833            && self.final_softcap.is_none()
10834            // The host acceptance argmax scans the WHOLE head row
10835            // (`lm_rows`), the sampler's own row only `vocab_size`: they
10836            // coincide exactly when the head has no padding rows, and
10837            // only then is the device argmax (which scores `vocab_size`)
10838            // bit-identical to both.
10839            && self.vocab_size == lm_rows
10840            && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
10841            && std::env::var_os("CMF_LOGIT_DUMP").is_none()
10842            && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
10843        // The CUDA hybrid arm the same way: each verify row's argmax on the
10844        // card (the host's tie rule), 5 MB of logits not read back.
10845        // `CMF_CUDA_DEV_ARGMAX=0` keeps the host path.
10846        #[cfg(all(not(target_os = "macos"), feature = "cuda"))]
10847        let greedy_dev = crate::gpu::cuda_active()
10848            && !sampling
10849            && !greedy_pen
10850            && !self.confidence_on
10851            && self.final_softcap.is_none()
10852            && self.vocab_size == lm_rows
10853            && std::env::var_os("CMF_LOGIT_DUMP").is_none()
10854            && std::env::var("CMF_CUDA_DEV_ARGMAX").as_deref() != Ok("0");
10855        #[cfg(all(not(target_os = "macos"), not(feature = "cuda")))]
10856        let greedy_dev = false;
10857        let mut dev_ids: Vec<u32> = Vec::new();
10858        #[cfg(target_os = "macos")]
10859        let verify_outcome = if metal_native {
10860            let lm = self.weights.lm_head.q1_parts()?;
10861            let n_score = self.vocab_size.min(lm_rows);
10862            self.try_batch_graph_metal(
10863                &mut hiddens,
10864                &positions,
10865                b,
10866                Some((lm, &final_norm, &mut logits)),
10867                if greedy_dev {
10868                    Some((n_score, &mut dev_ids))
10869                } else {
10870                    None
10871                },
10872            )
10873        } else {
10874            self.try_batch_graph_wgpu(
10875                &mut hiddens,
10876                &positions,
10877                b,
10878                Some(crate::gpu::SpecTail {
10879                    lm: lm_gw,
10880                    lm_rows,
10881                    final_norm: &final_norm,
10882                    logits_out: &mut logits,
10883                    pick: None,
10884                }),
10885            )
10886        };
10887        #[cfg(all(not(target_os = "macos"), feature = "cuda"))]
10888        crate::gpu_cuda::verify_argmax_arm(greedy_dev);
10889        #[cfg(not(target_os = "macos"))]
10890        let verify_outcome = self.try_batch_graph_wgpu(
10891            &mut hiddens,
10892            &positions,
10893            b,
10894            Some(crate::gpu::SpecTail {
10895                lm: lm_gw,
10896                lm_rows,
10897                final_norm: &final_norm,
10898                logits_out: &mut logits,
10899                pick: None,
10900            }),
10901        );
10902        #[cfg(all(not(target_os = "macos"), feature = "cuda"))]
10903        if greedy_dev {
10904            if let Some(ids) = crate::gpu_cuda::verify_argmax_take() {
10905                dev_ids = ids;
10906            }
10907        }
10908        match verify_outcome {
10909            crate::gpu::BatchGraphOutcome::Completed => {}
10910            crate::gpu::BatchGraphOutcome::Declined => {
10911                // The verifier refused before admission.  Its draft MTP
10912                // rows are still device-resident, so rewind the separate
10913                // mirror before the caller takes the exact one-token path.
10914                // (A DFlash round put no rows there.)
10915                self.dflash_rows = None;
10916                m.kv.truncate_last(mtp_steps);
10917                if !metal_native && mtp_steps > 0 && !self.rewind_mtp_graph_mirror(next_pos) {
10918                    self.clear_sequence_state();
10919                    self.graph_failed
10920                        .store(true, std::sync::atomic::Ordering::Relaxed);
10921                    self.cancel
10922                        .store(true, std::sync::atomic::Ordering::Relaxed);
10923                    tracing::error!("MTP graph mirror rewind failed after verify decline");
10924                }
10925                return None;
10926            }
10927            crate::gpu::BatchGraphOutcome::Failed => {
10928                // A failed batch may have advanced trunk/GDN state.  Clear
10929                // both mirrors and preserve the terminal outcome rather than
10930                // falling through to stale CPU state.
10931                self.clear_sequence_state();
10932                self.graph_failed
10933                    .store(true, std::sync::atomic::Ordering::Relaxed);
10934                self.cancel
10935                    .store(true, std::sync::atomic::Ordering::Relaxed);
10936                tracing::error!("MTP verify batch graph failed after admission");
10937                self.dflash_rows = None;
10938                return None;
10939            }
10940        }
10941        // `CMF_METAL_VERIFY_CHECK=1`: run the same b tokens through the
10942        // plain per-token path and compare each row's argmax + logits with
10943        // the verify's — the bring-up oracle for the batched graph. The
10944        // plain forwards mutate the CPU state; it is snapshotted and put
10945        // back, and the K/V mirrors re-pointed, before the round goes on.
10946        #[cfg(target_os = "macos")]
10947        if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
10948            let snap: Vec<Vec<f32>> = self
10949                .kv_cache
10950                .layers
10951                .iter()
10952                .map(|l| l.linear_state.clone())
10953                .collect();
10954            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
10955            let toks: Vec<u32> = std::iter::once(t_next)
10956                .chain(drafts.iter().copied())
10957                .collect();
10958            let want_save = self.graph_want_logits;
10959            self.graph_want_logits = false;
10960            for (i, &t) in toks.iter().enumerate() {
10961                let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
10962                let _ = self.graph_logits.take();
10963                // CMF_SPEC_PLAIN_HIDDEN=1: the next round drafts from the
10964                // plain path's hidden instead of the verify's (an experiment
10965                // on the chain's sensitivity to the half-GEMM noise)
10966                if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
10967                    hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
10968                }
10969                let ref_lg = self.logits_from_hidden(&hi);
10970                let row = &logits[i * lm_rows..(i + 1) * lm_rows];
10971                let ra = sampler::argmax(&ref_lg);
10972                let va = sampler::argmax(row);
10973                let mut md = 0f32;
10974                let mut rms = 0f64;
10975                for j in 0..lm_rows.min(ref_lg.len()) {
10976                    let d = (ref_lg[j] - row[j]).abs();
10977                    md = md.max(d);
10978                    rms += (d as f64) * (d as f64);
10979                }
10980                let mut hd = 0f32;
10981                for j in 0..self.hidden_size {
10982                    hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
10983                }
10984                eprintln!(
10985                    "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3e} rms {:.3e} | max|dhidden| {hd:.3e}",
10986                    next_pos + i,
10987                    if ra == va { "OK" } else { "MISMATCH" },
10988                    (rms / lm_rows as f64).sqrt()
10989                );
10990            }
10991            self.graph_want_logits = want_save;
10992            // restore IN PLACE: the pending verify graph wraps these very
10993            // allocations (zero-copy) — replacing the Vec would strand it
10994            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
10995                if l.linear_state.len() == st.len() {
10996                    l.linear_state.copy_from_slice(&st);
10997                } else {
10998                    l.linear_state = st;
10999                }
11000            }
11001            for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
11002                let extra = l.seq_len.saturating_sub(n0);
11003                if extra > 0 {
11004                    l.truncate_last(extra);
11005                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
11006                }
11007            }
11008        }
11009        let t_verify = t_round.elapsed();
11010        let sub_verify = subs();
11011        // Acceptance. Greedy: row i's argmax is the trunk's token after
11012        // input i. Sampling: accept draft i with min(1, p_i/q_i), and on
11013        // the first rejection draw the correction from max(0, p_i − q_i)
11014        // — that token is committed by the loop top as-is (spec_forced).
11015        let mut a = 0usize;
11016        let mut forced: Option<u32> = None;
11017        let ids: Vec<u32> = if sparse {
11018            let mut p = std::mem::take(&mut self.spec_ps);
11019            let mut res = std::mem::take(&mut self.spec_ress);
11020            while a < k_spec {
11021                let ok = sampler::sparse_distribution_into(
11022                    &logits[a * lm_rows..(a + 1) * lm_rows],
11023                    &cfg,
11024                    all_ids,
11025                    &mut self.sampler_scratch,
11026                    self.pool.as_deref(),
11027                    &mut p,
11028                );
11029                if !ok {
11030                    let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
11031                    p.clear();
11032                    p.push((t, 1.0));
11033                }
11034                match sampler::spec_accept_or_correct_sparse(
11035                    &p,
11036                    &self.spec_qs[a],
11037                    drafts[a],
11038                    &mut self.rng,
11039                    &mut res,
11040                ) {
11041                    None => {
11042                        all_ids.push(drafts[a]);
11043                        a += 1;
11044                    }
11045                    Some(c) => {
11046                        forced = Some(c);
11047                        break;
11048                    }
11049                }
11050            }
11051            all_ids.truncate(base_len);
11052            self.spec_ps = p;
11053            self.spec_ress = res;
11054            drafts.clone()
11055        } else if sampling {
11056            let mut p = std::mem::take(&mut self.spec_p);
11057            let mut res = std::mem::take(&mut self.spec_res);
11058            while a < k_spec {
11059                sampler::distribution_into(
11060                    &logits[a * lm_rows..(a + 1) * lm_rows],
11061                    &cfg,
11062                    all_ids,
11063                    &mut self.sampler_scratch,
11064                    self.pool.as_deref(),
11065                    &mut p,
11066                );
11067                match sampler::spec_accept_or_correct(
11068                    &p,
11069                    &self.spec_q[a],
11070                    drafts[a],
11071                    &mut self.rng,
11072                    &mut res,
11073                    self.pool.as_deref(),
11074                ) {
11075                    None => {
11076                        all_ids.push(drafts[a]);
11077                        a += 1;
11078                    }
11079                    Some(c) => {
11080                        forced = Some(c);
11081                        break;
11082                    }
11083                }
11084            }
11085            all_ids.truncate(base_len);
11086            self.spec_p = p;
11087            self.spec_res = res;
11088            // the accepted drafts ARE the verified tokens after inputs 0..a
11089            drafts.clone()
11090        } else if greedy_pen {
11091            // Row i's penalized argmax, penalties over the stream that
11092            // includes the accepted drafts before it — the plain loop's
11093            // exact arithmetic, one pass per row, no working copy.
11094            let mut ids: Vec<u32> = Vec::with_capacity(b);
11095            for i in 0..b {
11096                let t = sampler::argmax_penalized(
11097                    &logits[i * lm_rows..(i + 1) * lm_rows],
11098                    &cfg,
11099                    all_ids,
11100                    &mut self.sampler_scratch,
11101                    self.pool.as_deref(),
11102                );
11103                ids.push(t);
11104                if i < k_spec && t == drafts[i] {
11105                    all_ids.push(t);
11106                } else {
11107                    break;
11108                }
11109            }
11110            all_ids.truncate(base_len);
11111            while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
11112                a += 1;
11113            }
11114            // rows past the first mismatch were never scored; the loop
11115            // top re-samples the last verified row itself.
11116            ids
11117        } else if greedy_dev && dev_ids.len() == b {
11118            let ids = std::mem::take(&mut dev_ids);
11119            while a < k_spec && ids[a] == drafts[a] {
11120                a += 1;
11121            }
11122            ids
11123        } else {
11124            if logits.len() < b * lm_rows {
11125                // the device argmax was asked for and came back short:
11126                // no rows to fall back on — terminal like a failed batch
11127                self.clear_sequence_state();
11128                self.graph_failed
11129                    .store(true, std::sync::atomic::Ordering::Relaxed);
11130                self.cancel
11131                    .store(true, std::sync::atomic::Ordering::Relaxed);
11132                tracing::error!("Metal verify returned neither logits nor argmax ids");
11133                return None;
11134            }
11135            let ids: Vec<u32> = (0..b)
11136                .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
11137                .collect();
11138            while a < k_spec && ids[a] == drafts[a] {
11139                a += 1;
11140            }
11141            ids
11142        };
11143        spec_stamp("acc");
11144        if dflash_round {
11145            if let Some(log) = dflash_rows_log() {
11146                let mut rec = [u32::MAX; 24];
11147                (rec[0], rec[1], rec[2], rec[3]) = (next_pos as u32, t_next, k_spec as u32, a as u32);
11148                for (o, &d) in rec[4..11].iter_mut().zip(&drafts) {
11149                    *o = d;
11150                }
11151                for (o, &d) in rec[11..18].iter_mut().zip(&beside) {
11152                    *o = d;
11153                }
11154                // what a round of the chain's own would have added from the context
11155                let mut more: Vec<u32> = Vec::new();
11156                if beside.len() >= 2 && Self::spec_lookup_on() {
11157                    all_ids.extend_from_slice(&beside[..2]);
11158                    more = lookup_continuation(all_ids, 2 + LOOKUP_MIN_REAL, 5).to_vec();
11159                    all_ids.truncate(base_len);
11160                }
11161                rec[18] = more.len() as u32;
11162                for (o, &d) in rec[19..24].iter_mut().zip(&more) {
11163                    *o = d;
11164                }
11165                let mut bytes: Vec<u8> = rec.iter().flat_map(|v| v.to_le_bytes()).collect();
11166                bytes.extend(hidden.iter().flat_map(|v| v.to_le_bytes()));
11167                use std::io::Write as _;
11168                // the stream through the first logged round's first token, once:
11169                // a lookup reads the prompt, and the fit has to as well
11170                static IDS: std::sync::Once = std::sync::Once::new();
11171                IDS.call_once(|| {
11172                    if let Ok(path) = std::env::var("CMF_DFLASH_ROWS") {
11173                        let ids: Vec<u8> = all_ids.iter().flat_map(|v| v.to_le_bytes()).collect();
11174                        if let Err(e) = std::fs::write(format!("{path}.ids"), ids) {
11175                            tracing::warn!("CMF_DFLASH_ROWS: {path}.ids: {e}");
11176                        }
11177                    }
11178                });
11179                if let Ok(mut f) = log.lock() {
11180                    let _ = f.write_all(&bytes);
11181                }
11182            }
11183        }
11184        if spec_dbg {
11185            eprintln!(
11186                "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
11187                drafts, ids
11188            );
11189        }
11190        // CMF_METAL_VERIFY_CHECK=2: the commit oracle — plain-forward the
11191        // a+1 accepted tokens from a snapshot, then diff the replayed GDN
11192        // states and the appended K/V rows against that.
11193        #[cfg(target_os = "macos")]
11194        let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
11195            && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
11196        {
11197            let snap: Vec<Vec<f32>> = self
11198                .kv_cache
11199                .layers
11200                .iter()
11201                .map(|l| l.linear_state.clone())
11202                .collect();
11203            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
11204            let toks: Vec<u32> = std::iter::once(t_next)
11205                .chain(drafts.iter().copied())
11206                .collect();
11207            let want_save = self.graph_want_logits;
11208            self.graph_want_logits = false;
11209            for (i, &t) in toks.iter().take(a + 1).enumerate() {
11210                let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
11211                let _ = self.graph_logits.take();
11212            }
11213            self.graph_want_logits = want_save;
11214            let plain_states: Vec<Vec<f32>> = self
11215                .kv_cache
11216                .layers
11217                .iter()
11218                .map(|l| l.linear_state.clone())
11219                .collect();
11220            let (nkv, hd) = (self.num_kv_heads, self.head_dim);
11221            let mut rows = Vec::new();
11222            for (li, (l, n0)) in self
11223                .kv_cache
11224                .layers
11225                .iter_mut()
11226                .zip(attn_lens.iter())
11227                .enumerate()
11228            {
11229                let extra = l.seq_len.saturating_sub(*n0);
11230                if extra > 0 {
11231                    let mut kk = Vec::new();
11232                    let mut vv = Vec::new();
11233                    for g in 0..nkv {
11234                        kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
11235                        vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
11236                    }
11237                    rows.push((li, kk, vv));
11238                    l.truncate_last(extra);
11239                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
11240                }
11241            }
11242            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
11243                if l.linear_state.len() == st.len() {
11244                    l.linear_state.copy_from_slice(&st);
11245                } else {
11246                    l.linear_state = st;
11247                }
11248            }
11249            Some((plain_states, rows))
11250        } else {
11251            None
11252        };
11253        let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
11254        // Metal: the MTP cache cut and the round's warm-up SUBMIT come
11255        // BEFORE the trunk commit, so the warm-up's command buffer is
11256        // queued ahead of the GDN replay (second queue) and its wait
11257        // below no longer sits behind the replay — measured: the warm-up's
11258        // wait grew with the accepted count exactly like the replay does
11259        // (8 ms at a=1, 17 ms at a=3, 25 ms at a=5 for ~2 ms of its own
11260        // work). The replay now overlaps the warm-up's readback, the
11261        // round's return and the next draft chain.
11262        #[cfg(target_os = "macos")]
11263        let mut warm_pending: Option<MetalWarmPending> = None;
11264        // The pairs the MTP cache has to hold of this round: the accepted
11265        // ones and, before them, the tip's own when the block was DFlash's
11266        // (an MTP chain's first step is that pair). The MTP block stays in
11267        // step through DFlash rounds, so either drafter can take the next.
11268        let hs_w = self.hidden_size;
11269        let warm_first = usize::from(mtp_steps == 0);
11270        let warm_n = if dflash_round && !mtp_in_step { 0 } else { a + warm_first };
11271        let warm_pos = next_pos - warm_first;
11272        let warm_pair = |j: usize| -> (&[f32], u32) {
11273            if j < warm_first {
11274                (hidden, t_next)
11275            } else {
11276                let j = j - warm_first;
11277                (&hiddens[j * hs_w..(j + 1) * hs_w], ids[j])
11278            }
11279        };
11280        #[cfg(target_os = "macos")]
11281        if metal_native {
11282            if mtp_steps > 0 {
11283                m.kv.truncate_last(mtp_steps - 1);
11284            }
11285            if self.mtp_graph_mode == Some(true) {
11286                // the mirror rows below the cut are the CPU rows: re-point,
11287                // no re-upload
11288                crate::gpu_metal::kv_mirror_set_stored(
11289                    self.mtp_kv_id(),
11290                    Self::MTP_LAYER_BASE,
11291                    m.kv.seq_len,
11292                );
11293                if !warm_off && warm_n > 0 {
11294                    let pairs: Vec<(&[f32], u32)> = (0..warm_n).map(&warm_pair).collect();
11295                    warm_pending = self.mtp_warm_batch_submit(m, &pairs, warm_pos);
11296                }
11297            }
11298            spec_stamp("c.wsub");
11299        }
11300        // a fully-accepted round needs no restore: every input was real.
11301        #[cfg(target_os = "macos")]
11302        if metal_native {
11303            // the Metal verify never wrote its states: the commit replays the
11304            // accepted prefix into the CPU owners and appends the K/V rows
11305            if !self.metal_verify_commit(a) {
11306                self.clear_sequence_state();
11307                self.graph_failed
11308                    .store(true, std::sync::atomic::Ordering::Relaxed);
11309                self.cancel
11310                    .store(true, std::sync::atomic::Ordering::Relaxed);
11311                tracing::error!("Metal verify state/KV handoff failed after admission");
11312                return None;
11313            }
11314            // the rows the round keeps are the drafter's next context
11315            if let (Some((pos0, rows)), Some(d)) = (self.dflash_rows.take(), self.dflash.as_mut()) {
11316                let tw = d.g.tap_width();
11317                d.queue_context(pos0, &rows[..(a + 1) * tw], self.pool.as_deref());
11318                spec_stamp("c.flash");
11319            }
11320            if let Some((plain_states, rows)) = commit_ref {
11321                crate::gpu_metal::queue_fence();
11322                // the commit's replay runs on the second queue: collect it
11323                // before the oracle reads the CPU owners it writes into
11324                let _ = crate::gpu_metal::wait_replay();
11325                let (nkv, hd) = (self.num_kv_heads, self.head_dim);
11326                let mut worst_s = 0f32;
11327                let mut worst_li = 0usize;
11328                for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
11329                    if l.linear_state.len() != ps.len() || ps.is_empty() {
11330                        continue;
11331                    }
11332                    let d = l
11333                        .linear_state
11334                        .iter()
11335                        .zip(ps)
11336                        .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
11337                    let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
11338                    let rel = d / n.max(1e-6);
11339                    if rel > worst_s {
11340                        worst_s = rel;
11341                        worst_li = li;
11342                    }
11343                }
11344                let mut worst_k = 0f32;
11345                for (li, kk, vv) in &rows {
11346                    let l = &self.kv_cache.layers[*li];
11347                    let n0 = l.seq_len - (kk.len() / (nkv * hd));
11348                    let mut ck = Vec::new();
11349                    let mut cv = Vec::new();
11350                    for g in 0..nkv {
11351                        ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
11352                        cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
11353                    }
11354                    if ck.len() == kk.len() {
11355                        let dk = ck
11356                            .iter()
11357                            .zip(kk)
11358                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
11359                        let dv = cv
11360                            .iter()
11361                            .zip(vv)
11362                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
11363                        worst_k = worst_k.max(dk).max(dv);
11364                    } else {
11365                        eprintln!(
11366                            "commit-check L{li}: kv row count mismatch {} vs {}",
11367                            ck.len(),
11368                            kk.len()
11369                        );
11370                    }
11371                }
11372                eprintln!(
11373                    "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}"
11374                );
11375            }
11376        }
11377        if !metal_native && a + 1 < b {
11378            let expected_gdn_layers = self.graph_gdn_layer_count();
11379            if expected_gdn_layers > 0
11380                && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
11381            {
11382                self.clear_sequence_state();
11383                self.graph_failed
11384                    .store(true, std::sync::atomic::Ordering::Relaxed);
11385                self.cancel
11386                    .store(true, std::sync::atomic::Ordering::Relaxed);
11387                tracing::error!("GDN speculative restore failed after verify");
11388                return None;
11389            }
11390        }
11391        if self.o1_active() && !crate::gpu::o1_spec_commit(self.graph_kv_id, next_pos, a + 1) {
11392            self.clear_sequence_state();
11393            self.graph_failed
11394                .store(true, std::sync::atomic::Ordering::Relaxed);
11395            self.cancel
11396                .store(true, std::sync::atomic::Ordering::Relaxed);
11397            tracing::error!("far field commit failed after verify");
11398            return None;
11399        }
11400        // the rows a wgpu verify kept for the drafter: those the round keeps
11401        // are its next context
11402        if !metal_native {
11403            let kept = self.dflash_taps.lock().unwrap().pop();
11404            if let (Some((pos0, rows)), Some(d)) = (kept, self.dflash.as_mut()) {
11405                let tw = d.g.tap_width();
11406                if pos0 == next_pos && rows.len() >= (a + 1) * tw {
11407                    d.queue_context(pos0, &rows[..(a + 1) * tw], self.pool.as_deref());
11408                }
11409            }
11410        }
11411        if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
11412            // The verify graph committed the full batch, but one of its
11413            // persistent Full-attention mirrors could not be re-pointed to
11414            // the accepted prefix.  Treat that as terminal state failure;
11415            // an exact CPU fallback would otherwise consume stale GDN/KV.
11416            self.clear_sequence_state();
11417            self.graph_failed
11418                .store(true, std::sync::atomic::Ordering::Relaxed);
11419            self.cancel
11420                .store(true, std::sync::atomic::Ordering::Relaxed);
11421            tracing::error!("trunk graph KV rewind failed after speculative verify");
11422            return None;
11423        }
11424        *accepted += a;
11425        // MTP cache: keep the first draft row (its inputs were real), drop
11426        // the chain's, then append the verified pairs the round produced.
11427        // Each of those is a whole MTP block on the per-op path and they
11428        // cost 5.8 ms of a 69 ms round at k=3 — a third of what the
11429        // round's own draft costs. PRICED, and they earn it: skipping
11430        // them (`CMF_SPEC_WARM=0`) drops acceptance from 89% to 81% at
11431        // k=3 and 85% to 74% at k=4, and the tok/s goes nowhere at k=3
11432        // (50.3 against 50.5) and backwards at k=4 (48.1 against 50.1).
11433        // The knob stays so the next person can re-price it after the
11434        // warms are batched instead of assuming either way.
11435        if !metal_native && mtp_steps > 0 {
11436            // (Metal cut its MTP cache before the trunk commit, above)
11437            m.kv.truncate_last(mtp_steps - 1);
11438        }
11439        spec_stamp("c.trunc");
11440        // (the block's round put no row there: the mirror already ends where
11441        // the warm-up starts)
11442        if !metal_native
11443            && self.mtp_graph_mode == Some(true)
11444            && (mtp_steps > 0 || warm_n > 0)
11445            && !self.rewind_mtp_graph_mirror(warm_pos)
11446        {
11447            // The graph draft was admitted, so inability to move its cursor
11448            // back to the real anchor is a state failure, not a capability
11449            // refusal.  Do not warm or continue with a stale mirror.
11450            self.clear_sequence_state();
11451            self.graph_failed
11452                .store(true, std::sync::atomic::Ordering::Relaxed);
11453            self.cancel
11454                .store(true, std::sync::atomic::Ordering::Relaxed);
11455            tracing::error!("MTP graph mirror rewind failed after verify commit");
11456            return None;
11457        }
11458        if !warm_off && warm_n > 0 {
11459            // Graph arm: all accepted pairs in ONE batched run over the
11460            // MTP block; the token graph one by one if the batch declines.
11461            let mut warmed = false;
11462            #[cfg(target_os = "macos")]
11463            if metal_native && self.mtp_graph_mode == Some(true) {
11464                // the batched warm-up was submitted before the trunk
11465                // commit: collect it here; one by one on the token graph
11466                // if it declined (or failed)
11467                warmed = match warm_pending.take() {
11468                    Some(p) => self.mtp_warm_batch_finish(m, p),
11469                    None => false,
11470                };
11471                if !warmed {
11472                    warmed = true;
11473                    for j in 0..warm_n {
11474                        let (row, tok) = warm_pair(j);
11475                        let row = row.to_vec();
11476                        if self
11477                            .mtp_step_metal(m, &row, tok, warm_pos + j, false)
11478                            .is_none()
11479                        {
11480                            warmed = false;
11481                            break;
11482                        }
11483                    }
11484                }
11485            }
11486            if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
11487                let rows: Vec<(Vec<f32>, u32)> = (0..warm_n)
11488                    .map(|j| {
11489                        let (row, tok) = warm_pair(j);
11490                        (row.to_vec(), tok)
11491                    })
11492                    .collect();
11493                let pairs: Vec<(&[f32], u32)> = rows.iter().map(|(r, t)| (r.as_slice(), *t)).collect();
11494                match self.mtp_warm_prefill_pairs(m, &pairs, warm_pos) {
11495                    Ok(()) => warmed = true,
11496                    Err(err) => {
11497                        // A warm-up failure after graph admission cannot
11498                        // fall back to `mtp_warm`: the detached CPU cache is
11499                        // not authoritative for the device mirror.  Mark it
11500                        // terminal so the generation caller clears state and
11501                        // returns instead of drafting from stale attention.
11502                        tracing::error!("{err}");
11503                        self.clear_sequence_state();
11504                        self.graph_failed
11505                            .store(true, std::sync::atomic::Ordering::Relaxed);
11506                        self.cancel
11507                            .store(true, std::sync::atomic::Ordering::Relaxed);
11508                        return None;
11509                    }
11510                }
11511            }
11512            if !warmed {
11513                for j in 0..warm_n {
11514                    let (row, tok) = warm_pair(j);
11515                    let row = row.to_vec();
11516                    self.mtp_warm(m, &row, tok, warm_pos + j);
11517                }
11518            }
11519        }
11520        // The sampler's contract: logits of the LAST verified position —
11521        // unless a rejected draft already drew the correction, in which
11522        // case the loop top commits that token and samples nothing.
11523        spec_stamp("c.warm");
11524        if let Some(c) = forced {
11525            self.spec_forced = Some(c);
11526            self.graph_logits = None;
11527        } else if greedy_dev && logits.is_empty() {
11528            // the row's argmax IS the token the loop top would pick from
11529            // it (plain greedy, no penalties): commit it as forced
11530            self.spec_forced = Some(ids[a]);
11531            self.graph_logits = None;
11532        } else {
11533            let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
11534            row.resize(self.vocab_size, 0.0);
11535            if let Some(c) = self.final_softcap {
11536                for l in row.iter_mut() {
11537                    *l = c * (*l / c).tanh();
11538                }
11539            }
11540            self.graph_logits = Some(row);
11541        }
11542        let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
11543        spec_stamp("c.row");
11544        // Three phases, not two. The round's wall clock was 4 ms longer
11545        // than draft+verify and the difference had nowhere to be seen:
11546        // the accepted prefix re-runs the MTP block once per token to
11547        // keep the draft head's attention cache warm, and the GDN state
11548        // rolls back on any rejection. Both live here, after the verify.
11549        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
11550            let end = subs();
11551            eprintln!(
11552                "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
11553                 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
11554                t_draft.as_secs_f64() * 1e3,
11555                sub_draft - sub0,
11556                (t_verify - t_draft).as_secs_f64() * 1e3,
11557                sub_verify - sub_draft,
11558                (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
11559                end - sub_verify,
11560                self.draft_full_streak,
11561            );
11562        }
11563        // Native Metal's verify tile is flat in b (eight rows for the price
11564        // of one), so a shorter round only forfeits tokens — measured on
11565        // the M4: an essay round at k=2 still verified in 260 ms. The
11566        // adaptation is for cards whose verify grows with the rows.
11567        if k_spec > k_mtp {
11568            // a lookup's round: what it added over the head's own drafts
11569            let extra = a.saturating_sub(k_mtp) as f32;
11570            self.spec_lookup_gain += 0.2 * (extra - self.spec_lookup_gain);
11571            // a copy that holds whole earns the long window; a miss goes back
11572            self.spec_lookup_wide = a == k_spec;
11573            if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
11574                eprintln!(
11575                    "spec-lookup: {} drafts from the context, {extra} held (average {:.2})",
11576                    k_spec - k_mtp,
11577                    self.spec_lookup_gain
11578                );
11579            }
11580            if self.spec_lookup_gain < LOOKUP_PAYS {
11581                self.spec_lookup_rest = 24;
11582                self.spec_lookup_gain = 1.6;
11583            }
11584        }
11585        if metal_small && !k_capped {
11586            // (the chain's account stands as its last rounds on this text
11587            // left it while the block has the rounds: that is what the block
11588            // has to beat)
11589            if dflash_round {
11590                self.dflash_turn_sum.0 += (a + 1) as f32;
11591                self.dflash_turn_sum.1 += METAL_ROUND_COST[k_spec.clamp(1, 7) - 1] + DFLASH_ROUND_DRAFT;
11592            }
11593            let hold = if dflash_round { &mut self.dflash_hold } else { &mut self.spec_hold };
11594            spec_hold_update(hold, k_mtp, a.min(k_mtp));
11595            if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
11596                let next = metal_spec_plan(hold, if dflash_round { DFLASH_ROUND_DRAFT } else { 0.0 }).0;
11597                if next != k_mtp && k_env.is_none() {
11598                    eprintln!("spec-k: {k_mtp} → {next} (a draft holds after the last {hold:.2?})");
11599                }
11600            }
11601        }
11602
11603        if k_env.is_none() && !metal_native && !k_capped {
11604            // The depth is the drafter's own: a round the context lookup
11605            // lengthened (CUDA) adapts by the chain's drafts and what of them
11606            // held, and its wall (a longer verify) is not a depth's price.
11607            let lookup_round = k_spec > k_mtp;
11608            let (k_spec, a) = (k_mtp, a.min(k_mtp));
11609            // Slow average and a wide band: a fast one oscillated 2↔3 on
11610            // an essay every other round (measured), which forfeits the
11611            // draft it just paid for. Each drafter has its own depth.
11612            if cuda_rate && dflash_round && !k_capped {
11613                self.dflash_turn_sum.0 += (a + 1) as f32;
11614                self.dflash_turn_sum.1 += CUDA_ROUND_COST[k_spec.clamp(1, 7) - 1] + CUDA_BLOCK_DRAFT;
11615                spec_hold_update(&mut self.dflash_hold, k_spec, a.min(k_spec));
11616            }
11617            let (ewma, k_min) = if dflash_round { (&mut self.dflash_acc_ewma, 3) } else { (&mut self.spec_acc_ewma, 2) };
11618            let f = a as f32 / k_spec.max(1) as f32;
11619            *ewma += 0.2 * (f - *ewma);
11620            let mut k_next = k_spec;
11621            if *ewma >= 0.75 && k_spec < k_max {
11622                k_next = k_spec + 1;
11623            } else if *ewma < 0.4 && k_spec > k_min {
11624                k_next = k_spec - 1;
11625            }
11626            // The chain's rounds are timed by depth, and once two depths
11627            // are, the depth is the one that lands the most tokens in a unit
11628            // of time at this device's prices (`wgpu_spec_depth`): the share
11629            // of drafts kept climbs to five on text that holds whatever a
11630            // fifth row costs. A depth's first round pays its scratch and
11631            // is not counted; a change stands for six rounds.
11632            let mut planned = false;
11633            if !dflash_round && !lookup_round {
11634                spec_hold_update(&mut self.spec_hold, k_spec, a);
11635                let d = k_spec.clamp(1, 7) - 1;
11636                let w = t_round.elapsed().as_secs_f32() * 1e3;
11637                match self.spec_wall_n[d] {
11638                    0 => self.spec_wall_n[d] = 1,
11639                    1 => (self.spec_wall[d], self.spec_wall_n[d]) = (w, 2),
11640                    _ => self.spec_wall[d] += 0.2 * (w - self.spec_wall[d]),
11641                }
11642                if let Some(k_plan) = wgpu_spec_depth(&self.spec_hold, &self.spec_wall, k_spec, k_min, k_max) {
11643                    planned = true;
11644                    k_next = if self.spec_k_dwell > 0 {
11645                        self.spec_k_dwell -= 1;
11646                        k_spec
11647                    } else {
11648                        k_plan
11649                    };
11650                    if k_next != k_spec {
11651                        self.spec_k_dwell = 6;
11652                    }
11653                }
11654            }
11655            if k_next != k_spec {
11656                *ewma = 0.6;
11657                if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
11658                    if planned {
11659                        eprintln!(
11660                            "spec-k: {k_spec} → {k_next} (a round costs {:.0?} ms, a draft holds after the last {:.2?})",
11661                            self.spec_wall, self.spec_hold
11662                        );
11663                    } else {
11664                        eprintln!("spec-k: {k_spec} → {k_next}");
11665                    }
11666                }
11667            }
11668            if dflash_round {
11669                self.dflash_k_adapt = Some(k_next);
11670            } else {
11671                self.spec_k_adapt = Some(k_next);
11672            }
11673        }
11674        spec_stamp("end");
11675        Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
11676    }
11677
11678    /// A round's drafts from the DFlash drafter: the block behind the anchor
11679    /// `t_next` at `next_pos`, its first `k` tokens. The block reads the
11680    /// context the trunk's rows before `next_pos` left; the tokens come from
11681    /// the trunk's own head through the drafter's selector.
11682    fn dflash_draft(&mut self, t_next: u32, next_pos: usize, k: usize) -> Vec<u32> {
11683        let Some(mut d) = self.dflash.take() else {
11684            return Vec::new();
11685        };
11686        let (hs, rows) = (self.hidden_size, d.g.block);
11687        let mut h = vec![0f32; rows * hs];
11688        h[..hs].copy_from_slice(&self.embed_single(t_next));
11689        let mask = self.embed_single(d.g.mask_token);
11690        for r in 1..rows {
11691            h[r * hs..(r + 1) * hs].copy_from_slice(&mask);
11692        }
11693        // A trunk that scales its embeddings on the way in (Ripple): the
11694        // block reads the table's rows as they are (`CMF_DFLASH_EMBED=scaled`
11695        // hands it the trunk's input rows).
11696        if self.embed_multiplier != 1.0 && std::env::var("CMF_DFLASH_EMBED").as_deref() != Ok("scaled") {
11697            let inv = 1.0 / self.embed_multiplier;
11698            h.iter_mut().for_each(|v| *v *= inv);
11699        }
11700        let pool = self.pool.as_deref();
11701        let t0 = std::time::Instant::now();
11702        d.settle(pool);
11703        spec_stamp("d.ctx");
11704        let t_ctx = t0.elapsed();
11705        // the block, the head and the selector's projection as one
11706        // submission on wgpu; the host block below where it declines
11707        #[cfg(feature = "gpu")]
11708        if let Some((logits, hp)) = d.block_dev(next_pos, &h, k.min(rows - 1), &self.weights.lm_head) {
11709            spec_stamp("d.block");
11710            let t_block = t0.elapsed();
11711            let n = k.min(rows - 1);
11712            let (mut ids, _) = d.select_with(&hp, &logits, t_next, n);
11713            ids.truncate(k);
11714            // `CMF_DFLASH_DEV_CHECK=1`: the host block on the same rows, and
11715            // how far the device's logits, selector rows and drafts are from it
11716            if std::env::var_os("CMF_DFLASH_DEV_CHECK").is_some() {
11717                let out = d.block(next_pos, h.clone(), pool);
11718                let head = &self.weights.lm_head;
11719                let mut hl = vec![0f32; n * head.rows()];
11720                head.matmat(&out[hs..(n + 1) * hs], n, &mut hl, pool);
11721                // the device may score only the head's first rows (CUDA: the shortlist)
11722                let width = logits.len() / n.max(1);
11723                let hl: Vec<f32> = (0..n).flat_map(|j| hl[j * head.rows()..j * head.rows() + width].to_vec()).collect();
11724                let (hids, _) = d.select(&out[hs..(n + 1) * hs], &hl, t_next);
11725                let (mut dmax, mut ref_max, mut dsum, mut cnt) = (0f32, 0f32, 0f64, 0usize);
11726                for (a, b) in logits.iter().zip(&hl) {
11727                    dmax = dmax.max((a - b).abs());
11728                    ref_max = ref_max.max(b.abs());
11729                    dsum += ((a - b) * (a - b)) as f64;
11730                    cnt += 1;
11731                }
11732                let arg = |v: &[f32]| (0..n).map(|j| sampler::argmax(&v[j * width..(j + 1) * width])).collect::<Vec<u32>>();
11733                eprintln!(
11734                    "dflash-dev-check: logits max|Δ| {dmax:.4} of max {ref_max:.2}, rms Δ {:.5} | argmax device {:?} host {:?} | drafts device {:?} host {:?}",
11735                    (dsum / cnt.max(1) as f64).sqrt(),
11736                    arg(&logits),
11737                    arg(&hl),
11738                    ids,
11739                    &hids[..k.min(hids.len())]
11740                );
11741            }
11742            if self.dflash_solo && spec_time_level() > 0 {
11743                eprintln!(
11744                    "solo-draft: context {:.1} ms | device block + head {:.1} ms | selector {:.1} ms",
11745                    t_ctx.as_secs_f64() * 1e3,
11746                    (t_block - t_ctx).as_secs_f64() * 1e3,
11747                    (t0.elapsed() - t_block).as_secs_f64() * 1e3
11748                );
11749            }
11750            spec_stamp("d.head");
11751            self.dflash = Some(d);
11752            return ids;
11753        }
11754        let out = d.block(next_pos, h, pool);
11755        spec_stamp("d.block");
11756        let t_block = t0.elapsed();
11757        // the head over the round's shortlist of the vocabulary, as the MTP
11758        // draft has it (the full head while `draft_full_streak` runs). The
11759        // shortlist is the low ids of Qwen's vocabulary: a trunk with another
11760        // tokenizer (`dflash_solo`) scores the whole head.
11761        let (n, head) = (rows - 1, &self.weights.lm_head);
11762        let width = if self.dflash_solo { head.rows() } else { self.draft_head_rows(head.rows()) };
11763        let mut logits = vec![0f32; n * width];
11764        // the block's rows on the narrow kernel; any other head as a batch
11765        let narrow = head.mapped_q4tp().is_some_and(|(m, idx)| {
11766            crate::gpu::q4tp_rows8_many(m, &[(idx, head.rows(), head.cols(), width)], &out[hs..], n, &mut [&mut logits[..]])
11767        });
11768        if !narrow {
11769            logits = vec![0f32; n * head.rows()];
11770            if !head.device_matmat(&out[hs..], n, &mut logits) {
11771                head.matmat(&out[hs..], n, &mut logits, pool);
11772            }
11773        }
11774        spec_stamp("d.head");
11775        let t_head = t0.elapsed();
11776        let (mut ids, _) = d.select(&out[hs..], &logits, t_next);
11777        ids.truncate(k);
11778        if self.dflash_solo && spec_time_level() > 0 {
11779            eprintln!(
11780                "solo-draft: context {:.1} ms | block {:.1} ms | head {:.1} ms | selector {:.1} ms",
11781                t_ctx.as_secs_f64() * 1e3,
11782                (t_block - t_ctx).as_secs_f64() * 1e3,
11783                (t_head - t_block).as_secs_f64() * 1e3,
11784                (t0.elapsed() - t_head).as_secs_f64() * 1e3
11785            );
11786        }
11787        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() && d.next_position() != next_pos {
11788            eprintln!("dflash: the context ends at {} for a block at {next_pos}", d.next_position());
11789        }
11790        self.dflash = Some(d);
11791        ids
11792    }
11793
11794    /// A speculative round of a trunk that has no draft block of its own.
11795    /// The drafts are the DFlash sidecar's block behind the tip `t_next` (or
11796    /// the next ids of `CMF_SPEC_SCRIPT`); the batch graph verifies them with
11797    /// the tip in one submission, and the cache keeps the rows of the drafts
11798    /// that held and of the tip. None: no round was made (plain greedy goes
11799    /// on), or the graph failed after admission (`graph_failed` is set).
11800    #[cfg(feature = "gpu")]
11801    fn solo_spec_round(&mut self, t_next: u32, next_pos: usize, all_ids: &[u32], k: usize) -> Option<SoloRound> {
11802        self.dflash_taps_absorb();
11803        let t0 = std::time::Instant::now();
11804        let drafts: Vec<u32> = match solo_script() {
11805            Some(script) => script.iter().skip(all_ids.len()).take(k).copied().collect(),
11806            None => {
11807                if self.dflash.as_ref().is_none_or(|d| d.next_position() != next_pos) {
11808                    return None;
11809                }
11810                self.dflash_draft(t_next, next_pos, k)
11811            }
11812        };
11813        if drafts.is_empty() {
11814            return None;
11815        }
11816        let t_draft = t0.elapsed();
11817        let (b, hs) = (drafts.len() + 1, self.hidden_size);
11818        let mut hiddens = vec![0f32; b * hs];
11819        for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
11820            hiddens[i * hs..(i + 1) * hs].copy_from_slice(&self.embed_single(t));
11821        }
11822        let positions: Vec<usize> = (next_pos..next_pos + b).collect();
11823        let (lm_gw, lm_rows) = {
11824            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
11825            (
11826                crate::gpu::GraphW { idx: i, kind, row_scale: rs, data: &[], prism: crate::gpu::GraphPrismOp::None, affine: false },
11827                self.weights.lm_head.rows(),
11828            )
11829        };
11830        let mut logits = Vec::new();
11831        let final_norm = self.weights.final_norm.clone();
11832        let outcome = self.try_batch_graph_wgpu(
11833            &mut hiddens,
11834            &positions,
11835            b,
11836            Some(crate::gpu::SpecTail { lm: lm_gw, lm_rows, final_norm: &final_norm, logits_out: &mut logits, pick: None }),
11837        );
11838        let fail = |p: &mut Self, what: &str| {
11839            p.clear_sequence_state();
11840            p.graph_failed.store(true, std::sync::atomic::Ordering::Relaxed);
11841            p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
11842            tracing::error!("{what}");
11843        };
11844        match outcome {
11845            crate::gpu::BatchGraphOutcome::Completed if logits.len() >= b * lm_rows => {}
11846            crate::gpu::BatchGraphOutcome::Declined => return None,
11847            _ => {
11848                fail(self, "the verify of a speculative round failed after admission");
11849                return None;
11850            }
11851        }
11852        let t_verify = t0.elapsed();
11853        // row i's argmax is the trunk's token after input i
11854        let ids: Vec<u32> = (0..b).map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows])).collect();
11855        let mut a = 0usize;
11856        while a < drafts.len() && ids[a] == drafts[a] {
11857            a += 1;
11858        }
11859        if std::env::var_os("CMF_SPEC_DBG").is_some() {
11860            eprintln!(
11861                "solo-dbg: tip {t_next} at {next_pos} | context {:?} | drafts {drafts:?} | the trunk's {ids:?} | kept {a}",
11862                self.dflash.as_ref().map(|d| d.span())
11863            );
11864        }
11865        if self.o1_active() && !crate::gpu::o1_spec_commit(self.graph_kv_id, next_pos, a + 1) {
11866            fail(self, "far field commit failed after a speculative round");
11867            return None;
11868        }
11869        // a hybrid trunk (Arete-27B, no draft block of its own): its GDN
11870        // states back to the rows the round kept, as after a chain's round
11871        if a + 1 < b {
11872            let gdn = self.graph_gdn_layer_count();
11873            if gdn > 0 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, gdn) {
11874                fail(self, "GDN restore failed after a speculative round");
11875                return None;
11876            }
11877        }
11878        // the rows the round keeps are the drafter's next context
11879        let kept = self.dflash_taps.lock().unwrap().pop();
11880        if let (Some((pos0, rows)), Some(d)) = (kept, self.dflash.as_mut()) {
11881            let tw = d.g.tap_width();
11882            if pos0 == next_pos && rows.len() >= (a + 1) * tw {
11883                d.queue_context(pos0, &rows[..(a + 1) * tw], self.pool.as_deref());
11884            }
11885        }
11886        if !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
11887            fail(self, "trunk graph KV rewind failed after a speculative round");
11888            return None;
11889        }
11890        let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
11891        row.resize(self.vocab_size, 0.0);
11892        if let Some(c) = self.final_softcap {
11893            for l in row.iter_mut() {
11894                *l = c * (*l / c).tanh();
11895            }
11896        }
11897        if spec_time_level() > 0 {
11898            eprintln!(
11899                "solo-round: draft {:.1} ms | verify {:.1} ms | commit {:.1} ms | kept {a} of {}",
11900                t_draft.as_secs_f64() * 1e3,
11901                (t_verify - t_draft).as_secs_f64() * 1e3,
11902                (t0.elapsed() - t_verify).as_secs_f64() * 1e3,
11903                drafts.len()
11904            );
11905        }
11906        Some(SoloRound { accepted: drafts[..a].to_vec(), drafted: drafts.len(), hidden: hiddens[a * hs..(a + 1) * hs].to_vec(), logits: row })
11907    }
11908
11909    /// Micro-benchmark: two single-position forwards vs one fused pair
11910    /// from the current cache state (KV rewound after each probe).
11911    /// Returns (two_singles_ms, fused_pair_ms) per probe, or the (0, 0)
11912    /// sentinel when this model has no pair path to measure — the same
11913    /// answer the o1 arm gives, and the bench prints it the same way.
11914    /// (An architecture that loads its own layers leaves `weights.layers`
11915    /// empty; walking it here was an index panic, found by `bench` on
11916    /// deepseek_v4.)
11917    pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
11918        if !self.pair_supported() {
11919            return (0.0, 0.0);
11920        }
11921        // A GDN model that needs an automatic device prefix never uses this
11922        // fused-pair path in normal generation: its recurrence is serial and
11923        // the speculative pair is disabled by default.  More importantly,
11924        // this diagnostic suppresses the token graph, so measuring it would
11925        // stream the non-resident tail through per-op Vulkan just to print a
11926        // non-production number.  Leave inference untouched and report the
11927        // established unavailable sentinel instead.
11928        if self.gdn_cfg.is_some()
11929            && self.graph_prefill_preferred()
11930            && self.automatic_gpu_prefix().is_some()
11931        {
11932            tracing::info!(
11933                "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
11934            );
11935            return (0.0, 0.0);
11936        }
11937        // This is a host-side pair micro-benchmark. It truncates the host KV
11938        // after every probe, so letting the whole-token graph participate
11939        // would leave its device GDN/KV mirror ahead of the next probe and
11940        // poison the process-wide graph verdict before the real generation
11941        // benchmark starts. Keep the existing per-op/GPU arithmetic while
11942        // suppressing only the stateful token graph for this measurement.
11943        let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
11944        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
11945        let emb1 = self.embed_single(1);
11946        let emb2 = self.embed_single(2);
11947        let pos = self.kv_cache.seq_len();
11948
11949        let t0 = std::time::Instant::now();
11950        for _ in 0..iters {
11951            let _ = self.forward_layers(&emb1, pos, None);
11952            let _ = self.forward_layers(&emb2, pos + 1, None);
11953            for l in &mut self.kv_cache.layers {
11954                l.truncate_last(2);
11955            }
11956        }
11957        let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
11958
11959        let t1 = std::time::Instant::now();
11960        for _ in 0..iters {
11961            let _ = self.forward_pair(&emb1, &emb2, pos);
11962            for l in &mut self.kv_cache.layers {
11963                l.truncate_last(2);
11964            }
11965        }
11966        let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
11967        match graph_env {
11968            Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
11969            None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
11970        }
11971        (singles_ms, pair_ms)
11972    }
11973
11974    /// Fused two-position forward: weight rows are streamed from memory
11975    /// once per layer for both positions. Full layers → fused GQA pair;
11976    /// linear layers → vmf_phase pair (lane 2 state is tentative in the
11977    /// per-layer scratch until the draft is accepted).
11978    /// Whether the fused two-position path covers every layer kind in
11979    /// this model. MLA and KDA run per position (their pair arms are
11980    /// unreachable); the seq prefill falls back to singles for them.
11981    fn pair_supported(&self) -> bool {
11982        // An EMPTY layer stack means the architecture loaded its own and
11983        // this path has nothing to walk. Checking that directly, rather
11984        // than naming each such architecture, is what makes the guard hold
11985        // for the next one: `any()` over no layers is false, so a
11986        // feature-by-feature test says "supported" for a model that has no
11987        // layers here at all.
11988        !self.weights.layers.is_empty()
11989            && !self.per_token_stack()
11990            && !self
11991                .weights
11992                .layers
11993                .iter()
11994                .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
11995    }
11996
11997    fn forward_pair(
11998        &mut self,
11999        emb1: &[f32],
12000        emb2: &[f32],
12001        position: usize,
12002    ) -> (Vec<f32>, Vec<f32>) {
12003        // A two-token prompt starts here, not in the layer walk: decide the
12004        // MiMo placement before the pair's per-op MoE uploads any expert.
12005        self.mimo_moe_prepare();
12006        let mut h1 = emb1.to_vec();
12007        let mut h2 = emb2.to_vec();
12008        if self.value_blend.is_some() {
12009            self.vres_e0.clear();
12010            self.vres_e0.extend_from_slice(emb1);
12011            self.vres_e0.extend_from_slice(emb2);
12012        }
12013        let (_nkv, _hd, hs, _rd, eps) = (
12014            self.num_kv_heads,
12015            self.head_dim,
12016            self.hidden_size,
12017            self.rotary_dim,
12018            self.rms_eps,
12019        );
12020        let pool = self.pool.clone();
12021        // `forward_pair` bypasses `forward_layers_span`, but its per-op
12022        // body must still obey the ordinary layer placement. In particular,
12023        // an over-capacity Vulkan model has a device prefix and a CPU tail;
12024        // letting the pair walk submit the tail's selected MoE experts would
12025        // stream the whole tail through the residency arena and defeat the
12026        // measured VRAM bound.
12027        let automatic_gpu_prefix = self.automatic_gpu_prefix();
12028
12029        for li in 0..self.num_layers {
12030            let _capacity_tail = automatic_gpu_prefix
12031                .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
12032                .map(|_| crate::gpu::enter_cpu_scope());
12033            // Also makes an explicit CMF_GPU_LAYERS split apply to fused
12034            // pairs, just as it does to the ordinary layer walk.
12035            crate::gpu::set_layer(li as i64);
12036            let lw = &self.weights.layers[self.phys_layer(li)];
12037            // Norms into pipeline scratch (4 allocs/layer on the MTP
12038            // decode hot path before this).
12039            inference::rms_norm_into(
12040                &h1,
12041                &lw.input_norm,
12042                self.rms_eps,
12043                self.norm_style,
12044                &mut self.ws.n1,
12045            );
12046            inference::rms_norm_into(
12047                &h2,
12048                &lw.input_norm,
12049                self.rms_eps,
12050                self.norm_style,
12051                &mut self.ws.n2,
12052            );
12053            // The two rows V projects, when the layer blends the embeddings into them.
12054            let vres = match self.layer_value_blend(li) {
12055                Some(blend) => {
12056                    let mut second = Vec::new();
12057                    vres_lerp(&self.vres_e0[..hs], &self.ws.n1, blend, &mut self.vres_in);
12058                    vres_lerp(&self.vres_e0[hs..], &self.ws.n2, blend, &mut second);
12059                    self.vres_in.extend_from_slice(&second);
12060                    true
12061                }
12062                None => false,
12063            };
12064
12065            let (a1, a2) = match &lw.attn {
12066                AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
12067                AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
12068                AttnKind::Bounded(w) => {
12069                    // Two sequential positions of the bounded operator
12070                    // (the ring's causal order is the pair's order).
12071                    let rope = self
12072                        .bounded_rope
12073                        .clone()
12074                        .expect("bounded layer without an installed rotation table");
12075                    let cfg = crate::bounded::BoundedAttnCfg {
12076                        num_heads: self.num_heads,
12077                        num_kv_heads: self.num_kv_heads,
12078                        head_dim: self.head_dim,
12079                        hidden_size: hs,
12080                        scale: self.attn_scale,
12081                        rope: &rope,
12082                        pool: pool.as_deref(),
12083                    };
12084                    let a1 = crate::bounded::bounded_attention(
12085                        &self.ws.n1,
12086                        w,
12087                        &mut self.kv_cache.layers[li],
12088                        &cfg,
12089                    );
12090                    let a2 = crate::bounded::bounded_attention(
12091                        &self.ws.n2,
12092                        w,
12093                        &mut self.kv_cache.layers[li],
12094                        &cfg,
12095                    );
12096                    (a1, a2)
12097                }
12098                AttnKind::Linear(w) => {
12099                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
12100                    let layer = &mut self.kv_cache.layers[li];
12101                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
12102                    vmf_phase_pair(
12103                        &self.ws.n1,
12104                        &self.ws.n2,
12105                        w,
12106                        &cfg,
12107                        state,
12108                        scratch,
12109                        self.pool.as_deref(),
12110                    )
12111                }
12112                AttnKind::LinearGdn(w) => {
12113                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
12114                    let layer = &mut self.kv_cache.layers[li];
12115                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
12116                    gdn_pair(
12117                        &self.ws.n1,
12118                        &self.ws.n2,
12119                        w,
12120                        &cfg,
12121                        state,
12122                        scratch,
12123                        self.pool.as_deref(),
12124                    )
12125                }
12126                AttnKind::ShortConv(w) => {
12127                    let cfg = self
12128                        .short_conv_cfg
12129                        .expect("short-conv layer without short_conv_cfg");
12130                    let layer = &mut self.kv_cache.layers[li];
12131                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
12132                    short_conv_pair(
12133                        &self.ws.n1,
12134                        &self.ws.n2,
12135                        w,
12136                        &cfg,
12137                        state,
12138                        scratch,
12139                        self.pool.as_deref(),
12140                    )
12141                }
12142                AttnKind::Full {
12143                    wq,
12144                    wk,
12145                    wv,
12146                    wo,
12147                    q_norm,
12148                    k_norm,
12149                    output_gate,
12150                    softplus_gate,
12151                    bias,
12152                } => {
12153                    let inv_freq_l = self.layer_inv_freq(li);
12154                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
12155                    let cfg = QwenAttnCfg {
12156                        mrope: None,
12157                        num_heads: self.layer_num_heads(li),
12158                        num_kv_heads: nkv_l,
12159                        head_dim: hd_l,
12160                        hidden_size: hs,
12161                        position,
12162                        inv_freq: &inv_freq_l,
12163                        rotary_dim: rd_l,
12164                        scale: self.attn_scale,
12165                        softcap: self.attn_softcap,
12166                        window: self.layer_window(li),
12167                        v_norm: self.attn_v_norm,
12168                        qk_norm_after_rope: self.qk_norm_after_rope,
12169                        gate_sigmoid: self.proj_gate_sigmoid,
12170                        q_norm: q_norm.as_deref(),
12171                        k_norm: k_norm.as_deref(),
12172                        output_gate: *output_gate,
12173                        softplus_gate: softplus_gate
12174                            .as_ref()
12175                            .map(|(gate, per_head)| (gate, *per_head)),
12176                        rope_scale: self.layer_rope_scale(li),
12177                        bias: bias
12178                            .as_ref()
12179                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12180                        rms_eps: eps,
12181                        norm_style: self.norm_style,
12182                        pool: pool.as_deref(),
12183                        v_head_dim: self.layer_v_dim(li),
12184                        value_in: vres.then(|| self.vres_in.as_slice()),
12185                    };
12186                    attention::qwen_attention_pair(
12187                        &self.ws.n1,
12188                        &self.ws.n2,
12189                        wq,
12190                        wk,
12191                        wv,
12192                        wo,
12193                        &mut self.kv_cache.layers[li],
12194                        &cfg,
12195                    )
12196                }
12197            };
12198            let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
12199                Some(w) => (
12200                    inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
12201                    inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
12202                ),
12203                None => (a1, a2),
12204            };
12205            for i in 0..self.hidden_size {
12206                h1[i] += a1[i];
12207                h2[i] += a2[i];
12208            }
12209            let (mut a1, mut a2) = (a1, a2);
12210            attention::recycle_buf(&mut a1);
12211            attention::recycle_buf(&mut a2);
12212
12213            let lw = &self.weights.layers[self.phys_layer(li)];
12214            inference::rms_norm_into(
12215                &h1,
12216                &lw.post_norm,
12217                self.rms_eps,
12218                self.norm_style,
12219                &mut self.ws.p1,
12220            );
12221            inference::rms_norm_into(
12222                &h2,
12223                &lw.post_norm,
12224                self.rms_eps,
12225                self.norm_style,
12226                &mut self.ws.p2,
12227            );
12228            let (f1, f2) = match &lw.ffn {
12229                // Dual-branch layers need the raw residuals — run the
12230                // two positions through the same fn decode uses.
12231                FfnKind::DenseMoe(dm) => (
12232                    dense_moe_ffn(
12233                        dm,
12234                        &self.ws.p1,
12235                        &h1,
12236                        self.rms_eps,
12237                        self.norm_style,
12238                        self.pool.as_deref(),
12239                    ),
12240                    dense_moe_ffn(
12241                        dm,
12242                        &self.ws.p2,
12243                        &h2,
12244                        self.rms_eps,
12245                        self.norm_style,
12246                        self.pool.as_deref(),
12247                    ),
12248                ),
12249                FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
12250                    moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
12251                    moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
12252                ),
12253                _ => ffn_forward_pair(
12254                    &lw.ffn,
12255                    &self.ws.p1,
12256                    &self.ws.p2,
12257                    self.pool.as_deref(),
12258                    None,
12259                ),
12260            };
12261            let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
12262                Some(w) => (
12263                    inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
12264                    inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
12265                ),
12266                None => (f1, f2),
12267            };
12268            for i in 0..self.hidden_size {
12269                h1[i] += f1[i];
12270                h2[i] += f2[i];
12271            }
12272            let (mut f1, mut f2) = (f1, f2);
12273            attention::recycle_buf(&mut f1);
12274            attention::recycle_buf(&mut f2);
12275            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
12276                for i in 0..self.hidden_size {
12277                    h1[i] *= sc;
12278                    h2[i] *= sc;
12279                }
12280            }
12281            // Looped Transformer: apply final norm at the end of each loop iteration.
12282            if self.is_loop_end(li) && li + 1 < self.num_layers {
12283                h1 = inference::rms_norm(
12284                    &h1,
12285                    &self.weights.final_norm,
12286                    self.rms_eps,
12287                    self.norm_style,
12288                );
12289                h2 = inference::rms_norm(
12290                    &h2,
12291                    &self.weights.final_norm,
12292                    self.rms_eps,
12293                    self.norm_style,
12294                );
12295            }
12296        }
12297        crate::gpu::set_layer(-1); // final/O(1) work is outside layer placement
12298        // Real O(1) prefill pairs may also carry tentative lane-2 recurrent
12299        // state. Commit it before publishing the transition epoch so the
12300        // next serial/device row cannot observe a new attention epoch with an
12301        // old GDN state. Speculative pairs run only when O(1) is inactive and
12302        // retain their existing caller-controlled commit/rollback semantics.
12303        if self.o1_active() {
12304            self.commit_linear_scratch();
12305        }
12306        self.o1_progress();
12307        self.swa_trim_tails();
12308        (h1, h2)
12309    }
12310
12311    /// Commit lane-2 linear states after an accepted draft.
12312    fn commit_linear_scratch(&mut self) {
12313        for layer in &mut self.kv_cache.layers {
12314            if !layer.linear_scratch.is_empty() {
12315                std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
12316                layer.linear_scratch.clear();
12317            }
12318        }
12319    }
12320
12321    /// This is the encoder counterpart to `forward_ids`: it runs a fresh
12322    /// sequence through the existing full/GDN hybrid stack and returns the
12323    /// final RMS-normalized hidden row for *every* token.  It is used by
12324    /// component pipelines whose text tower intentionally has no LM head
12325    /// (such as an image-conditioner Qwen3.5 model).  The sequence state is
12326    /// always released before returning, so a large text tower can be dropped
12327    /// immediately after conditioning a subsequent stage.
12328    pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
12329        #[cfg(target_os = "macos")]
12330        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
12331        if ids.is_empty() {
12332            return Err("empty id sequence".to_string());
12333        }
12334
12335        self.nll_begin()?;
12336        let result = (|| {
12337            let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
12338            self.nll_check_graph("encoder prefill", 0)?;
12339            let expected = ids.len() * self.hidden_size;
12340            if hidden.len() != expected {
12341                return Err(format!(
12342                    "encoder prefill returned {} values for {} rows of hidden size {}",
12343                    hidden.len(),
12344                    ids.len(),
12345                    self.hidden_size
12346                ));
12347            }
12348
12349            let mut out = vec![0.0f32; expected];
12350            for (src, dst) in hidden
12351                .chunks_exact(self.hidden_size)
12352                .zip(out.chunks_exact_mut(self.hidden_size))
12353            {
12354                inference::rms_norm_into(
12355                    src,
12356                    &self.weights.final_norm,
12357                    self.rms_eps,
12358                    self.norm_style,
12359                    dst,
12360                );
12361            }
12362            Ok(out)
12363        })();
12364        self.nll_end();
12365        result
12366    }
12367
12368    /// The last layer's hidden row (before the final norm) for every token
12369    /// of `ids`, from a fresh sequence, through the whole-stack prefill (the
12370    /// device chunk graph where there is one) — what a distillation cache of
12371    /// teacher outputs needs, at prefill speed.
12372    pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
12373        #[cfg(target_os = "macos")]
12374        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
12375        if ids.is_empty() {
12376            return Err("empty id sequence".to_string());
12377        }
12378        self.nll_begin()?;
12379        let result = (|| {
12380            let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
12381            self.nll_check_graph("hidden prefill", 0)?;
12382            if hidden.len() != ids.len() * self.hidden_size {
12383                return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
12384            }
12385            Ok(hidden)
12386        })();
12387        self.nll_end();
12388        result
12389    }
12390
12391    /// Forward a full id sequence from a fresh cache and return the
12392    /// logits after the last position (golden-parity harness, bench).
12393    pub fn forward_ids(
12394        &mut self,
12395        ids: &[u32],
12396        task_mask: Option<&TaskMask>,
12397    ) -> Result<Vec<f32>, String> {
12398        #[cfg(target_os = "macos")]
12399        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
12400        if ids.is_empty() {
12401            return Err("empty id sequence".to_string());
12402        }
12403        self.clear_sequence_state();
12404        self.check_forward_graph("forward_ids setup", 0)?;
12405        if task_mask.is_none() {
12406            self.o1_begin();
12407        }
12408        let mut hidden = vec![0.0f32; self.hidden_size];
12409        let mut pos = 0usize;
12410        if self.dory.is_some() {
12411            return Ok(self.dory_forward(ids, 0, crate::dory::Want::Last));
12412        }
12413        if let Some(b) = &mut self.dsv41 {
12414            let pool = self.pool.clone();
12415            let mut logits = Vec::new();
12416            crate::dsv41::forward_chunk(
12417                &b.0,
12418                &b.1,
12419                &b.2,
12420                &mut b.3,
12421                ids,
12422                0,
12423                pool.as_deref(),
12424                &mut logits,
12425            );
12426            if let Err(err) = self.o1_seal_checked() {
12427                self.clear_sequence_state();
12428                return Err(err);
12429            }
12430            return Ok(logits);
12431        }
12432        // Same routing predicate generation uses. Two reasons it must be
12433        // the same one: (1) a GDN hybrid's recurrent state is GPU-
12434        // resident, and a batched CPU prefill would build it on the host
12435        // only — decode then reads buffers the prefill never wrote;
12436        // (2) bench times THIS function and calls the result "prefill",
12437        // so a different path here reports a number production never
12438        // sees (W2 on 2×5090: 8.7 tok/s reported against 125 real).
12439        // Gemma-4: generation's batched-graph ingest (`g4_batch_ingest`),
12440        // so bench and the parity tools time and score the route run/serve
12441        // take; a refusal leaves the rest to the chunked host prefill.
12442        #[cfg(not(target_os = "macos"))]
12443        if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
12444            let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
12445                Ok(r) => r,
12446                Err(e) => {
12447                    self.clear_sequence_state();
12448                    return Err(e);
12449                }
12450            };
12451            if n > 0 {
12452                hidden.copy_from_slice(&last);
12453                pos = n;
12454            }
12455        }
12456        if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
12457            // prefill-GEMM in chunks; only the last position's hidden is
12458            // needed. (o1-compatible: the batch path attends per position
12459            // through qwen_attention, which carries the collection hook.)
12460            let chunk = self.prefill_chunk();
12461            let hs = self.hidden_size;
12462            while pos < ids.len() {
12463                let end = (pos + chunk).min(ids.len());
12464                let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
12465                self.check_forward_graph("forward_ids batched prefill", end - 1)?;
12466                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
12467                pos = end;
12468            }
12469        }
12470        // Same guards as generation's prefill — INCLUDING the graph one.
12471        // The CPU pair walk was intercepting positions that the resident
12472        // token graph would have run itself: on a GDN hybrid over wgpu
12473        // that is 89 ms of host forward against 7 ms of device submit,
12474        // and it made prefill look 12× slower than it is (W2 on an RTX
12475        // 5090, ctx 512: 11.2 tok/s with the walk, 136.6 without).
12476        // CMF_PAIR=0 opts out; a model whose layers live outside
12477        // `weights.layers` has no pair walk to take.
12478        if task_mask.is_none()
12479            && !self.graph_prefill_preferred()
12480            && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
12481            && self.pair_supported()
12482        {
12483            while pos + 1 < ids.len() {
12484                let e1 = self.embed_single(ids[pos]);
12485                let e2 = self.embed_single(ids[pos + 1]);
12486                let (_, h2) = self.forward_pair(&e1, &e2, pos);
12487                self.check_forward_graph("forward_ids pair", pos + 1)?;
12488                self.commit_linear_scratch();
12489                hidden = h2;
12490                pos += 2;
12491            }
12492        }
12493        // Resident Embryo graph: the prompt in chunks of one submit each
12494        // (the same device state and logits as the per-position walk).
12495        if task_mask.is_none() && pos == 0 && ids.len() > 1 {
12496            if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
12497                self.graph_logits = Some(lg);
12498                hidden = vec![0.0; self.hidden_size];
12499                pos = ids.len();
12500            }
12501        }
12502        while pos < ids.len() {
12503            hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
12504            self.check_forward_graph("forward_ids", pos)?;
12505            pos += 1;
12506        }
12507        if let Some(logits) = self.graph_logits.take() {
12508            // Resident stacks already applied final norm and their head in
12509            // the same submit; do not run a second norm/head over the zero
12510            // hidden sentinel returned by forward_layers_span.
12511            if let Err(err) = self.o1_seal_checked() {
12512                self.clear_sequence_state();
12513                return Err(err);
12514            }
12515            return Ok(logits);
12516        }
12517        // Harness contract: after forward_ids the cache is decode-ready —
12518        // under o1 that means sealed (bench measures the seal as part of
12519        // prefill, honestly).
12520        if let Err(err) = self.o1_seal_checked() {
12521            self.clear_sequence_state();
12522            return Err(err);
12523        }
12524        let normed = inference::rms_norm(
12525            &hidden,
12526            &self.weights.final_norm,
12527            self.rms_eps,
12528            self.norm_style,
12529        );
12530        Ok(self.lm_head_forward(&normed))
12531    }
12532
12533    /// Run the V4.1 stack one token at a time and retain logits for every
12534    /// position. This is a diagnostic surface for comparing a converted
12535    /// checkpoint with a tokenwise reference implementation.
12536    #[doc(hidden)]
12537    pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
12538        #[cfg(target_os = "macos")]
12539        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
12540        if ids.is_empty() {
12541            return Err("empty id sequence".to_string());
12542        }
12543        self.clear_sequence_state();
12544        self.dsv41
12545            .as_ref()
12546            .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
12547        self.o1_begin();
12548        let rows = {
12549            let pool = self.pool.clone();
12550            let b = self
12551                .dsv41
12552                .as_mut()
12553                .expect("dsv41 checked above; state cannot change during forward");
12554            let mut rows = Vec::with_capacity(ids.len());
12555            for (position, &id) in ids.iter().enumerate() {
12556                let mut logits = Vec::new();
12557                crate::dsv41::forward_token(
12558                    &b.0,
12559                    &b.1,
12560                    &b.2,
12561                    &mut b.3,
12562                    id,
12563                    position,
12564                    pool.as_deref(),
12565                    &mut logits,
12566                );
12567                rows.push(logits);
12568            }
12569            rows
12570        };
12571        self.o1_seal();
12572        Ok(rows)
12573    }
12574
12575    /// GPTQ calibration of DeepSeek-V4.1 (`cortiq quantize-moe`): the MoE
12576    /// input rows and routed expert ids of layers `lo..hi` for `ids` (a fresh
12577    /// sequence), see [`crate::dsv41::Dsv41MoeCapture`].
12578    pub fn dsv41_moe_capture(
12579        &mut self,
12580        ids: &[u32],
12581        lo: usize,
12582        hi: usize,
12583    ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
12584        #[cfg(target_os = "macos")]
12585        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
12586        if ids.is_empty() {
12587            return Err("empty id sequence".to_string());
12588        }
12589        self.dsv41
12590            .as_ref()
12591            .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
12592        self.clear_sequence_state();
12593        let res = {
12594            let pool = self.pool.clone();
12595            let b = self.dsv41.as_mut().expect("dsv41 checked above");
12596            crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
12597        };
12598        self.clear_sequence_state();
12599        res
12600    }
12601
12602    /// Teacher-forced perplexity over a token sequence (phase-C gate:
12603    /// honest quant comparisons instead of prompt vibes).
12604    ///
12605    /// Attention is EXACT even on a model whose layers are flagged for
12606    /// the O(1) kernel — scoring the backbone is the default on purpose
12607    /// (it is the yardstick). `nll_ids_o1` scores the CONVERTED model.
12608    pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
12609        let (nll, cnt) = self.nll_ids_from(ids, 0)?;
12610        Ok((nll / cnt.max(1) as f64).exp())
12611    }
12612
12613    /// GPTQ calibration forward: `ids` through layers `0..upto` only, in
12614    /// layer-major chunks of `chunk` positions, with no vocabulary head and
12615    /// no scoring. The Hessian hook (`gptq_capture`) folds every
12616    /// projection's input on the way; the layers above the captured span
12617    /// and the head would only cost time. Falls back to a scored forward
12618    /// where the batched prefill does not apply.
12619    pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
12620        if !self.can_prefill_batched() {
12621            return self.nll_ids_from(ids, 0).map(|_| ());
12622        }
12623        self.nll_begin()?;
12624        let result: Result<(), String> = (|| {
12625            let chunk = chunk.max(1);
12626            let mut pos = 0usize;
12627            while pos < ids.len() {
12628                let end = (pos + chunk).min(ids.len());
12629                let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
12630                self.nll_check_graph("calibration prefill", pos)?;
12631                pos = end;
12632            }
12633            Ok(())
12634        })();
12635        self.nll_end();
12636        result
12637    }
12638
12639    /// Layer-streamed calibration of one fresh window: layers `from..mid` run
12640    /// on `input` (the hidden state entering layer `from`; `None` = the
12641    /// token embeddings of `ids`) and their output is returned, then layers
12642    /// `mid..upto` run on that output for the capture hooks (their output is
12643    /// dropped). A caller that keeps the returned state per window runs each
12644    /// layer once over the whole conversion instead of re-running the stack
12645    /// below every layer. With `mid == from` the input is returned as is
12646    /// (`None` input: an empty vector).
12647    pub fn calib_window_span(
12648        &mut self,
12649        ids: &[u32],
12650        input: Option<&[f32]>,
12651        from: usize,
12652        mid: usize,
12653        upto: usize,
12654    ) -> Result<Vec<f32>, String> {
12655        if !self.can_prefill_batched() {
12656            return Err("calib_window_span: this architecture has no batched prefill".into());
12657        }
12658        self.nll_begin()?;
12659        let result: Result<Vec<f32>, String> = (|| {
12660            let h = if mid > from {
12661                let inp = match input {
12662                    Some(h) => PrefillIn::Hidden(h),
12663                    None => PrefillIn::Ids(ids),
12664                };
12665                let h = self.prefill_batch_span(inp, 0, None, from, mid);
12666                self.nll_check_graph("calibration span", 0)?;
12667                h
12668            } else {
12669                input.map(<[f32]>::to_vec).unwrap_or_default()
12670            };
12671            if upto > mid {
12672                let inp = if mid > from || input.is_some() {
12673                    PrefillIn::Hidden(&h)
12674                } else {
12675                    PrefillIn::Ids(ids)
12676                };
12677                let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
12678                self.nll_check_graph("calibration span", 0)?;
12679            }
12680            Ok(h)
12681        })();
12682        self.nll_end();
12683        result
12684    }
12685
12686    /// GPTQ calibration of a Flash-Next-class MoE (`qwen4_exp`): MoE input
12687    /// rows and router logits of layers `lo..hi` for `ids` (a fresh
12688    /// sequence), per layer `[ids.len()][hidden + n_experts]`.
12689    #[cfg(feature = "gpu")]
12690    pub fn qwen4_capture(
12691        &mut self,
12692        ids: &[u32],
12693        lo: usize,
12694        hi: usize,
12695    ) -> Result<Vec<Vec<f32>>, String> {
12696        let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
12697        crate::qwen4_exp::capture_window(
12698            &b.0,
12699            &b.1,
12700            &b.2,
12701            &mut b.3,
12702            ids,
12703            lo,
12704            hi,
12705            &self.inv_freq,
12706            self.pool.as_deref(),
12707        )
12708    }
12709
12710    /// GPTQ calibration capture of an ordinary MoE stack (Qwen3.5/3.6
12711    /// MoE, Clover): `ids` from position 0 through the batched device graph
12712    /// in prefill-sized chunks, the MoE input rows and router logits of
12713    /// layers `lo..hi` copied out after routing. Returns per layer
12714    /// `[ids.len()][hidden + n_experts]`; the sequence state is left fresh.
12715    #[cfg(feature = "gpu")]
12716    pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
12717        use crate::gpu_wgpu::clover as cap;
12718        let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
12719        let ne = (lo..hi)
12720            .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
12721                FfnKind::Moe(m) => Some(m.experts.len()),
12722                _ => None,
12723            })
12724            .ok_or("no MoE layer in the capture range")?;
12725        let chunk = self.prefill_chunk().max(1);
12726        self.clear_sequence_state();
12727        if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
12728            return Err("capture unavailable (no wgpu device)".into());
12729        }
12730        let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
12731        let res = (|| {
12732            for (ci, part) in ids.chunks(chunk).enumerate() {
12733                let mut hiddens = Vec::with_capacity(part.len() * hs);
12734                for &id in part {
12735                    hiddens.extend_from_slice(&self.embed_single(id));
12736                }
12737                let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
12738                match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
12739                    crate::gpu::BatchGraphOutcome::Completed => {}
12740                    _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
12741                }
12742                let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
12743                if written < hi - lo {
12744                    return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
12745                }
12746                for (o, r) in out.iter_mut().zip(rows) {
12747                    o.extend_from_slice(&r);
12748                }
12749            }
12750            Ok(())
12751        })();
12752        cap::moe_capture_end();
12753        self.clear_sequence_state();
12754        res.map(|_| out)
12755    }
12756
12757    /// Expert directory triples (gate, up, down) of an ordinary MoE layer,
12758    /// routed and shared.
12759    pub fn moe_expert_dirs(
12760        &self,
12761        li: usize,
12762    ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
12763        let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
12764            return None;
12765        };
12766        let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
12767        let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
12768        let shared = match &m.shared {
12769            Some((se, _)) => Some(dirs(se)?),
12770            None => None,
12771        };
12772        Some((routed, shared))
12773    }
12774
12775    /// Expert directory triples of a `qwen4_exp` layer (routed, shared).
12776    pub fn qwen4_expert_dirs(
12777        &self,
12778        li: usize,
12779    ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
12780        let b = self.qwen4_exp.as_ref()?;
12781        crate::qwen4_exp::expert_dirs(&b.1, li)
12782    }
12783
12784    /// Teacher-force the installed MTP draft stack and collect each draft
12785    /// FFN's own pre-down activation statistics.
12786    ///
12787    /// This is deliberately separate from [`Self::probe_ffn_mass`]: canonical
12788    /// MTP and MiMo sidecars can have independently compacted FFNs, and MiMo
12789    /// draft layers can differ in width from one another and from the trunk.
12790    /// The returned vector follows the actual draft stack (one entry for a
12791    /// canonical MTP head; one per MiMo draft layer). MiMo layer `k` replays
12792    /// the teacher-forced pairs `(hidden[j], ids[j + k + 1])`.
12793    ///
12794    /// Calibration starts from a fresh sequence and leaves the trunk, draft
12795    /// KVs, history, and device mirrors fresh on both success and error. It
12796    /// is therefore unsuitable for the middle of a conversation whose KV
12797    /// cache must be retained.
12798    pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
12799        self.reset_mtp_probe_state();
12800        // The trunk runs as one prefill (the device graph where there is
12801        // one); the draft replays on the host under a CPU scope, where its
12802        // FFN input can be measured. This guard also suppresses gate/oracle
12803        // sparsity experiments for the duration so the statistics and
12804        // admission replay always execute the full dense FFN.
12805        let _exact_calibration = ExactMtpCalibrationGuard::enter();
12806        let result = if ids.len() < 2 {
12807            Err("MTP FFN probe needs at least two token ids".into())
12808        } else if ids
12809            .iter()
12810            .any(|&id| id as usize >= self.weights.embed_tokens.rows())
12811        {
12812            Err("MTP FFN probe received a token id outside the embedding table".into())
12813        } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
12814            // A synthetic pipeline can carry both mechanisms, but merging
12815            // unlike draft semantics into one compaction plan is unsafe.
12816            Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
12817        } else if self.mimo_mtp.is_some() {
12818            let mut st = self
12819                .mimo_mtp
12820                .take()
12821                .expect("checked MiMo MTP before detaching it");
12822            let result = match self.probe_trunk_rows(ids) {
12823                Ok(trunk) => crate::gpu::cpu_scope(|| {
12824                    self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids, &trunk)
12825                }),
12826                Err(e) => Err(e),
12827            };
12828            // The owner is detached during the replay, so the common trunk
12829            // reset cannot clear its KVs. Restore it even on an error.
12830            st.reset();
12831            self.mimo_mtp = Some(st);
12832            result
12833        } else if self.mtp.is_some() {
12834            let mut mtp = self
12835                .mtp
12836                .take()
12837                .expect("checked canonical MTP before detaching it");
12838            let result = match self.probe_trunk_rows(ids) {
12839                Ok(trunk) => crate::gpu::cpu_scope(|| {
12840                    self.mtp_ffn_probe_teacher_forced(&mut mtp, ids, &trunk)
12841                }),
12842                Err(e) => Err(e),
12843            };
12844            mtp.kv.clear();
12845            self.mtp = Some(mtp);
12846            result
12847        } else {
12848            Err("MTP FFN probe: this pipeline has no draft stack".into())
12849        };
12850        // Cover every early/error arm above: no failed calibration can leak
12851        // a partially populated trunk or draft cache into the next request.
12852        self.reset_mtp_probe_state();
12853        result
12854    }
12855
12856    /// Reset every state owner touched by [`Self::probe_mtp_ffn`].
12857    fn reset_mtp_probe_state(&mut self) {
12858        self.clear_sequence_state();
12859        if let Some(mtp) = self.mtp.as_mut() {
12860            mtp.kv.clear();
12861        }
12862        if let Some(mimo) = self.mimo_mtp.as_mut() {
12863            mimo.reset();
12864        }
12865        self.mtp_graph_mode = None;
12866    }
12867
12868    /// The trunk's last hidden row (before the final norm) at every
12869    /// position a teacher-forced draft probe reads (`ids[..len-1]`), from
12870    /// the whole-stack prefill — the device graph where there is one. One
12871    /// position at a time through `forward_layers` on the host this was the
12872    /// probe's whole cost: a 14B's 96k calibration tokens ran for hours.
12873    pub(crate) fn probe_trunk_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
12874        if ids.len() < 2 {
12875            return Err("MTP FFN probe needs at least two token ids".into());
12876        }
12877        let rows = self.final_hidden_rows(&ids[..ids.len() - 1])?;
12878        self.clear_sequence_state();
12879        Ok(rows)
12880    }
12881
12882    /// Canonical one-layer MTP teacher-forced replay over the trunk rows
12883    /// of [`Self::probe_trunk_rows`]. Its cache advances for every row so
12884    /// later attention inputs match a real draft warm-up rather than a
12885    /// collection of independent one-token measurements. The FFN inputs are
12886    /// measured in batches: nothing after the FFN feeds a later row.
12887    fn mtp_ffn_probe_teacher_forced(
12888        &mut self,
12889        mtp: &mut MtpModule,
12890        ids: &[u32],
12891        trunk: &[f32],
12892    ) -> Result<Vec<MtpFfnProbe>, String> {
12893        let FfnKind::Dense(dense) = &mtp.layer.ffn else {
12894            return Err("MTP FFN probe: canonical draft FFN is not dense".into());
12895        };
12896        let inter = dense.gate_proj.rows();
12897        let (hs, n) = (self.hidden_size, ids.len() - 1);
12898        if trunk.len() != n * hs {
12899            return Err(format!(
12900                "MTP FFN probe: {} trunk values for {n} rows of width {hs}",
12901                trunk.len()
12902            ));
12903        }
12904        let mut mass = vec![0.0f64; inter];
12905        let mut squared_mass = vec![0.0f64; inter];
12906        let mut inputs = Vec::with_capacity(MTP_PROBE_FFN_ROWS.min(n) * hs);
12907        for position in 0..n {
12908            self.mtp_probe_ffn_input(
12909                mtp,
12910                &trunk[position * hs..(position + 1) * hs],
12911                ids[position + 1],
12912                position,
12913            )?;
12914            inputs.extend_from_slice(&self.ws.p1[..hs]);
12915            if inputs.len() == MTP_PROBE_FFN_ROWS * hs || position + 1 == n {
12916                let FfnKind::Dense(dense) = &mtp.layer.ffn else {
12917                    unreachable!("checked dense above");
12918                };
12919                accumulate_dense_ffn_activation_mass(
12920                    dense,
12921                    &inputs,
12922                    inputs.len() / hs,
12923                    self.pool.as_deref(),
12924                    &mut mass,
12925                    &mut squared_mass,
12926                )?;
12927                inputs.clear();
12928            }
12929        }
12930        Ok(vec![MtpFfnProbe {
12931            mass,
12932            squared_mass,
12933            observed_positions: n,
12934        }])
12935    }
12936
12937    /// CPU-only MTP block replay up to the FFN's input (the post-attention
12938    /// norm, left in `ws.p1`), where the probe measures. The ordinary
12939    /// `mtp_step_hl` device path keeps these values resident and therefore
12940    /// cannot truthfully provide a per-neuron calibration statistic. The
12941    /// FFN arm itself is not run: its output feeds only this row's logits.
12942    fn mtp_probe_ffn_input(
12943        &mut self,
12944        mtp: &mut MtpModule,
12945        hidden: &[f32],
12946        next_token: u32,
12947        position: usize,
12948    ) -> Result<(), String> {
12949        let e = self.embed_single(next_token);
12950        let mut cat = vec![0.0f32; 2 * self.hidden_size];
12951        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
12952        inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
12953        inference::rms_norm_into(
12954            hidden,
12955            &mtp.hnorm,
12956            self.rms_eps,
12957            self.norm_style,
12958            cat_h,
12959        );
12960        let mut x = vec![0.0f32; self.hidden_size];
12961        mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
12962
12963        let lw = &mtp.layer;
12964        inference::rms_norm_into(
12965            &x,
12966            &lw.input_norm,
12967            self.rms_eps,
12968            self.norm_style,
12969            &mut self.ws.n1,
12970        );
12971        let attn = match &lw.attn {
12972            AttnKind::Full {
12973                wq,
12974                wk,
12975                wv,
12976                wo,
12977                q_norm,
12978                k_norm,
12979                output_gate,
12980                softplus_gate,
12981                bias,
12982            } => {
12983                let mut cfg = self.attn_cfg(position);
12984                cfg.q_norm = q_norm.as_deref();
12985                cfg.k_norm = k_norm.as_deref();
12986                cfg.output_gate = *output_gate;
12987                cfg.softplus_gate = softplus_gate
12988                    .as_ref()
12989                    .map(|(gate, per_head)| (gate, *per_head));
12990                cfg.bias = bias
12991                    .as_ref()
12992                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
12993                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
12994            }
12995            _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
12996        };
12997        for (dst, src) in x.iter_mut().zip(attn) {
12998            *dst += src;
12999        }
13000        inference::rms_norm_into(
13001            &x,
13002            &lw.post_norm,
13003            self.rms_eps,
13004            self.norm_style,
13005            &mut self.ws.p1,
13006        );
13007        Ok(())
13008    }
13009
13010    /// DTG-MA calibration pass (Patent 2): run `ids` through the model
13011    /// (CPU path, per position) and return each layer's per-neuron
13012    /// activation mass Σ|silu(gate)·up| — the statistic the task-guided
13013    /// FFN mask is derived from.
13014    pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
13015        self.clear_sequence_state();
13016        FFN_PROBE.with(|p| {
13017            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
13018        });
13019        crate::gpu::cpu_scope(|| {
13020            for (pos, &id) in ids.iter().enumerate() {
13021                let emb = self.embed_single(id);
13022                let _ = self.forward_layers(&emb, pos, None);
13023            }
13024        });
13025        self.clear_sequence_state();
13026        FFN_PROBE
13027            .with(|p| p.borrow_mut().take())
13028            .unwrap_or_default()
13029    }
13030
13031    /// `probe_ffn_mass` over the BATCHED prefill: same accumulator, one
13032    /// sweep instead of one forward per token. What makes the statistic
13033    /// affordable on a 27B.
13034    pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
13035        if let Err(err) = self.nll_begin() {
13036            // A recorder can be left by a caller that was interrupted before
13037            // this request entered its scoring block.  Consume it even when
13038            // the preflight failure prevents initialization of a new one.
13039            let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
13040            self.nll_end();
13041            return Err(err);
13042        }
13043        FFN_PROBE.with(|p| {
13044            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
13045        });
13046        let result: Result<(), String> = (|| {
13047            for chunk in ids.chunks(256) {
13048                if chunk.len() < 2 {
13049                    continue;
13050                }
13051                self.nll_ids_masked(chunk, 0, None)?;
13052            }
13053            Ok(())
13054        })();
13055        self.nll_end();
13056        let probe = FFN_PROBE
13057            .with(|p| p.borrow_mut().take())
13058            .unwrap_or_default();
13059        match result {
13060            Ok(()) => Ok(probe),
13061            Err(err) => {
13062                drop(probe);
13063                Err(err)
13064            }
13065        }
13066    }
13067
13068    /// The FFN probe over one window of the layer-major batched walk
13069    /// (`calib_forward`): the statistic of `probe_ffn_mass`, the window's rows
13070    /// in one context, every projection a GEMM on the device where one is up.
13071    pub fn probe_ffn_mass_span(&mut self, ids: &[u32], chunk: usize) -> Result<Vec<Vec<f64>>, String> {
13072        FFN_PROBE.with(|p| {
13073            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
13074        });
13075        let result = self.calib_forward(ids, self.num_layers, chunk);
13076        let probe = FFN_PROBE.with(|p| p.borrow_mut().take()).unwrap_or_default();
13077        result.map(|()| probe)
13078    }
13079
13080    /// The hot-neuron statistic of a MoE model's routed experts over `ids`:
13081    /// per layer `[expert · inter + neuron]` of Σ (w·act)², `w` the expert's
13082    /// final routing weight (a layer without experts gives an empty row).
13083    /// Qwen-3.5/3.8 MoE models only, on the host path: the caller runs with
13084    /// `CMF_GPU=0`, since the device frames and the fused expert kernels do
13085    /// not report activations.
13086    pub fn probe_moe_mass(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
13087        let Some(b) = self.qwen4_exp.as_mut() else {
13088            return Err("hot-probe --experts: not a Qwen MoE model (the probe reads the qwen4 host path)".into());
13089        };
13090        let geom: Vec<(usize, usize)> = b.1.iter().map(crate::qwen4_exp::Layer::expert_geometry).collect();
13091        let inter = geom.iter().map(|g| g.1).max().unwrap_or(0);
13092        if inter == 0 || ids.is_empty() {
13093            return Err("hot-probe --experts: the model has no routed experts".into());
13094        }
13095        MOE_PROBE.with(|p| {
13096            *p.borrow_mut() = Some(MoeProbe {
13097                acc: geom.iter().map(|&(ne, _)| vec![0f64; ne * inter]).collect(),
13098                inter,
13099                cur: None,
13100            });
13101        });
13102        b.3.reset();
13103        let mut pos = 0usize;
13104        while pos < ids.len() {
13105            let end = (pos + crate::qwen4_exp::prefill_chunk()).min(ids.len());
13106            let mut lg = Vec::new();
13107            crate::qwen4_exp::forward_tokens(&b.0, &b.1, &b.2, &mut b.3, &ids[pos..end], pos, &self.inv_freq, self.pool.as_deref(), &mut lg, false);
13108            pos = end;
13109        }
13110        b.3.reset();
13111        let probe = MOE_PROBE.with(|p| p.borrow_mut().take()).map_or_else(Vec::new, |p| p.acc);
13112        if !probe.iter().any(|r| r.iter().any(|&v| v > 0.0)) {
13113            return Err("hot-probe --experts: the statistic is all zero (a device path hid the experts from the probe; run with CMF_GPU=0)".into());
13114        }
13115        Ok(probe)
13116    }
13117
13118    /// Teacher-forced PPL with a task mask active (sparse execution) —
13119    /// the quality gate for a DTG-MA-masked skill. Sequential per
13120    /// position: the batched prefill path is dense-only.
13121    pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
13122        self.nll_begin()?;
13123        let result: Result<f64, String> = (|| {
13124            let mut nll = 0f64;
13125            let mut cnt = 0usize;
13126            let mut hidden = vec![0f32; self.hidden_size];
13127            for (pos, &id) in ids.iter().enumerate() {
13128                if pos > 0 {
13129                    inference::rms_norm_into(
13130                        &hidden,
13131                        &self.weights.final_norm,
13132                        self.rms_eps,
13133                        self.norm_style,
13134                        &mut self.ws.n1,
13135                    );
13136                    let mut logits = self.lm_head_forward(&self.ws.n1);
13137                    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
13138                    let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
13139                    let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
13140                    nll -= p.max(1e-300).ln();
13141                    cnt += 1;
13142                    attention::recycle_buf(&mut logits);
13143                }
13144                let emb = self.embed_single(id);
13145                hidden = self.forward_layers(&emb, pos, Some(mask));
13146                self.nll_check_graph("masked serial forward", pos)?;
13147                // Consume a possible graph logits side channel before the
13148                // next row.  Masked scoring normally disables that route,
13149                // but stale channel state must never survive a request.
13150                let _ = self.graph_logits.take();
13151            }
13152            Ok((nll / cnt.max(1) as f64).exp())
13153        })();
13154        self.nll_end();
13155        result
13156    }
13157
13158    /// Teacher-forced NLL sum + scored-token count over positions
13159    /// `start..len-1`, attention EXACT. Positions below `start` still
13160    /// run — they are the context — they are just not scored, so this
13161    /// pairs with `nll_ids_o1(ids, start)` over the very same tokens.
13162    ///
13163    /// Returning (nll, cnt) rather than a ppl is what lets a windowed
13164    /// caller combine windows before the exp, so every scored token
13165    /// weighs the same regardless of how the windows are cut.
13166    /// `nll_ids_from` with a task mask held active at every position.
13167    ///
13168    /// The batched prefill path does not thread masks, so this walks the
13169    /// per-position forward — slower, but it scores the file exactly the
13170    /// way `run --task` will serve it, which is the point of the gate
13171    /// that calls it. With `None` it defers to the fast path.
13172    /// Masked scoring rides the SAME batched sweep as unmasked scoring —
13173    /// the masked-inference fast path: `prefill_batch_masked` lands the
13174    /// per-visit FFN rows on the activations inside the fused arms. The
13175    /// per-position loop below remains only as the no-batch fallback.
13176    pub fn nll_ids_masked(
13177        &mut self,
13178        ids: &[u32],
13179        start: usize,
13180        task_mask: Option<&TaskMask>,
13181    ) -> Result<(f64, usize), String> {
13182        let task_mask = self.drop_open_mask(task_mask);
13183        self.nll_ids_inner(ids, start, task_mask)
13184    }
13185
13186    pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
13187        self.nll_ids_inner(ids, start, None)
13188    }
13189
13190    /// Diagnostic (`CMF_PREFILL_PARITY=split[,k]` on `cortiq ppl`): the same
13191    /// tokens through three ingests, each from a clean sequence — H the host
13192    /// chunked prefill, G the wgpu batch graph in k-row chunks, M the host
13193    /// prefill up to `split` and the batch graph after it, seeded from the
13194    /// host state — and where they part: NLL and top-1 per 256-position bin
13195    /// from each arm's post-stack rows, every GDN layer's recurrent state
13196    /// (the host `linear_state` against the graph's) at `split` and at the
13197    /// end, and every exact-attention layer's K/V rows.
13198    pub fn prefill_parity_probe(
13199        &mut self,
13200        ids: &[u32],
13201        split: usize,
13202        bk: usize,
13203    ) -> Result<String, String> {
13204        use std::fmt::Write as _;
13205        let (n, hs) = (ids.len(), self.hidden_size);
13206        if split == 0 || split >= n || bk == 0 || !self.can_prefill_batched() {
13207            return Err(format!(
13208                "prefill parity: needs 0 < split < {n} tokens, k > 0 and a batched host prefill"
13209            ));
13210        }
13211        // 1 = GDN, 2 = exact attention, 0 = anything else (not compared).
13212        let kinds: Vec<u8> = (0..self.num_layers)
13213            .map(|li| match &self.weights.layers[self.phys_layer(li)].attn {
13214                AttnKind::LinearGdn(_) => 1,
13215                AttnKind::Full { .. } => 2,
13216                _ => 0,
13217            })
13218            .collect();
13219        let kv_id = self.graph_kv_id;
13220        let host_chunk = self.prefill_chunk().max(1);
13221        let host_states = |p: &Self| -> Vec<Vec<f32>> {
13222            (0..p.num_layers)
13223                .map(|li| {
13224                    if kinds[li] == 1 {
13225                        p.kv_cache.layers[li].linear_state.clone()
13226                    } else {
13227                        Vec::new()
13228                    }
13229                })
13230                .collect()
13231        };
13232        let graph_states = |_p: &Self| -> Vec<Vec<f32>> {
13233            (0..kinds.len())
13234                .map(|li| {
13235                    if kinds[li] == 1 {
13236                        crate::gpu::graph_gdn_state_read(kv_id, li).unwrap_or_default()
13237                    } else {
13238                        Vec::new()
13239                    }
13240                })
13241                .collect()
13242        };
13243        // Position-major K/V rows [0, n) of every exact-attention layer.
13244        let host_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
13245            (0..p.num_layers)
13246                .map(|li| {
13247                    let c = &p.kv_cache.layers[li];
13248                    if kinds[li] != 2
13249                        || c.mode != crate::kv_cache::KvMode::F32
13250                        || c.base() != 0
13251                        || c.seq_len != n
13252                    {
13253                        return (Vec::new(), Vec::new());
13254                    }
13255                    let (nkv, hd) = (c.num_kv_heads, c.head_dim);
13256                    let (mut k, mut v) = (vec![0f32; n * nkv * hd], vec![0f32; n * nkv * hd]);
13257                    for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
13258                        for pos in 0..n {
13259                            let d = (pos * nkv + h) * hd;
13260                            k[d..d + hd].copy_from_slice(&kh[pos * hd..(pos + 1) * hd]);
13261                            v[d..d + hd].copy_from_slice(&vh[pos * hd..(pos + 1) * hd]);
13262                        }
13263                    }
13264                    (k, v)
13265                })
13266                .collect()
13267        };
13268        let graph_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
13269            (0..p.num_layers)
13270                .map(|li| {
13271                    let c = &p.kv_cache.layers[li];
13272                    if kinds[li] != 2 {
13273                        return (Vec::new(), Vec::new());
13274                    }
13275                    crate::gpu::graph_kv_read_rows(kv_id, &[(li, 0, n)], c.num_kv_heads, c.head_dim)
13276                        .and_then(|mut v| v.pop())
13277                        .unwrap_or_default()
13278                })
13279                .collect()
13280        };
13281        let host_walk = |p: &mut Self,
13282                         from: usize,
13283                         to: usize,
13284                         rows: &mut [f32],
13285                         at_split: &mut Option<Vec<Vec<f32>>>| {
13286            let mut pos = from;
13287            while pos < to {
13288                let mut end = (pos + host_chunk).min(to);
13289                if pos < split && end > split {
13290                    end = split;
13291                }
13292                let hb = p.prefill_batch(&ids[pos..end], pos);
13293                rows[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
13294                pos = end;
13295                if pos == split {
13296                    *at_split = Some(host_states(p));
13297                }
13298            }
13299        };
13300        let graph_walk = |p: &mut Self,
13301                          from: usize,
13302                          to: usize,
13303                          rows: &mut [f32],
13304                          at_split: &mut Option<Vec<Vec<f32>>>|
13305         -> Result<(), String> {
13306            let mut pos = from;
13307            while pos < to {
13308                let mut end = (pos + bk).min(to);
13309                if pos < split && end > split {
13310                    end = split;
13311                }
13312                let k = end - pos;
13313                let mut hid = vec![0f32; k * hs];
13314                for (j, &id) in ids[pos..end].iter().enumerate() {
13315                    hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
13316                }
13317                let positions: Vec<usize> = (pos..end).collect();
13318                let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
13319                if outcome != crate::gpu::BatchGraphOutcome::Completed {
13320                    return Err(format!(
13321                        "prefill parity: batch graph {outcome:?} at position {pos}"
13322                    ));
13323                }
13324                rows[pos * hs..end * hs].copy_from_slice(&hid);
13325                pos = end;
13326                if pos == split {
13327                    *at_split = Some(graph_states(p));
13328                }
13329            }
13330            Ok(())
13331        };
13332        let score = |p: &Self, rows: &[f32], from: usize| p.parity_score(ids, rows, from);
13333
13334        let t = std::time::Instant::now();
13335        self.clear_sequence_state();
13336        let (mut hh, mut h_split) = (vec![0f32; n * hs], None);
13337        host_walk(self, 0, n, &mut hh, &mut h_split);
13338        let (h_end, h_kv) = (host_states(self), host_kv(self));
13339        let t_h = t.elapsed().as_secs_f64();
13340
13341        let t = std::time::Instant::now();
13342        self.clear_sequence_state();
13343        let (mut hg, mut g_split) = (vec![0f32; n * hs], None);
13344        let g_run = graph_walk(self, 0, n, &mut hg, &mut g_split);
13345        let (g_end, g_kv) = (graph_states(self), graph_kv(self));
13346        let t_g = t.elapsed().as_secs_f64();
13347
13348        let t = std::time::Instant::now();
13349        self.clear_sequence_state();
13350        let (mut hm, mut m_split) = (vec![0f32; n * hs], None);
13351        host_walk(self, 0, split, &mut hm, &mut m_split);
13352        let m_run = graph_walk(self, split, n, &mut hm, &mut None);
13353        let (m_end, m_kv) = (graph_states(self), graph_kv(self));
13354        let t_m = t.elapsed().as_secs_f64();
13355        self.clear_sequence_state();
13356        g_run?;
13357        m_run?;
13358
13359        let (sh, sg, sm) = (
13360            score(self, &hh, 0),
13361            score(self, &hg, 0),
13362            score(self, &hm, split),
13363        );
13364        let mut r = String::new();
13365        let _ = writeln!(
13366            r,
13367            "prefill parity: {n} tokens, split {split}, host chunk {host_chunk}, graph k {bk} \
13368             (host {t_h:.1} s, graph {t_g:.1} s, host+graph {t_m:.1} s)"
13369        );
13370        let _ = writeln!(
13371            r,
13372            "  bin          | NLL host  graph  h+g   | top1 host=graph h+g=graph | hidden rel host:graph h+g:graph"
13373        );
13374        let bin = 256usize;
13375        let mut b0 = 0;
13376        while b0 + 1 < n {
13377            let b1 = (b0 + bin).min(n - 1);
13378            let mean = |s: &[(f64, usize)], off: usize| -> Option<f64> {
13379                let v: Vec<f64> = (b0.max(off)..b1).map(|p| s[p - off].0).collect();
13380                (!v.is_empty()).then(|| v.iter().sum::<f64>() / v.len() as f64)
13381            };
13382            let agree = |a: &[(f64, usize)], ao: usize, b: &[(f64, usize)], bo: usize| {
13383                let ps: Vec<usize> = (b0.max(ao).max(bo)..b1).collect();
13384                if ps.is_empty() {
13385                    return None;
13386                }
13387                let same = ps.iter().filter(|&&p| a[p - ao].1 == b[p - bo].1).count();
13388                Some(100.0 * same as f64 / ps.len() as f64)
13389            };
13390            let hid_rel = |a: &[f32], from: usize| -> Option<f64> {
13391                (b0.max(from)..b1)
13392                    .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &hg[p * hs..(p + 1) * hs]))
13393                    .reduce(f64::max)
13394            };
13395            let f =
13396                |v: Option<f64>, prec: usize| v.map_or("-".to_string(), |x| format!("{x:.prec$}"));
13397            let e = |v: Option<f64>| v.map_or("-".to_string(), |x| format!("{x:.1e}"));
13398            let _ = writeln!(
13399                r,
13400                "  {b0:5}..{b1:<5} | {:>8} {:>6} {:>6} | {:>14} {:>9} | {:>16} {:>9}",
13401                f(mean(&sh, 0), 3),
13402                f(mean(&sg, 0), 3),
13403                f(mean(&sm, split), 3),
13404                f(agree(&sh, 0, &sg, 0), 1),
13405                f(agree(&sm, split, &sg, 0), 1),
13406                e(hid_rel(&hh, 0)),
13407                e(hid_rel(&hm, split)),
13408            );
13409            b0 = b1;
13410        }
13411        // Recurrent state, layer by layer: [conv ring | S] split at the ring.
13412        let ring_len = self
13413            .gdn_cfg
13414            .map(|c| c.conv_dim() * c.conv_kernel.saturating_sub(1))
13415            .unwrap_or(0);
13416        let state_line = |r: &mut String, label: &str, a: &[Vec<f32>], b: &[Vec<f32>]| {
13417            let _ = write!(r, "  GDN {label} (ring/S rel):");
13418            let mut first = None;
13419            for li in (0..kinds.len()).filter(|&li| kinds[li] == 1) {
13420                let (x, y) = (&a[li], &b[li]);
13421                if x.len() != y.len() || x.len() <= ring_len {
13422                    let _ = write!(r, " L{li} len {}/{}", x.len(), y.len());
13423                    continue;
13424                }
13425                let (rr, rs) = (
13426                    rel_l2(&x[..ring_len], &y[..ring_len]),
13427                    rel_l2(&x[ring_len..], &y[ring_len..]),
13428                );
13429                if first.is_none() && rr.max(rs) > 1e-2 {
13430                    first = Some(li);
13431                }
13432                let _ = write!(r, " L{li} {rr:.0e}/{rs:.0e}");
13433            }
13434            let _ = writeln!(
13435                r,
13436                "\n    first GDN layer past 1e-2: {}",
13437                first.map_or("none".to_string(), |l| l.to_string())
13438            );
13439        };
13440        match (&h_split, &g_split) {
13441            (Some(a), Some(b)) => state_line(&mut r, "host vs graph at split", a, b),
13442            _ => {
13443                let _ = writeln!(r, "  GDN at split: not captured");
13444            }
13445        }
13446        if let (Some(a), Some(b)) = (&m_split, &h_split) {
13447            state_line(&mut r, "h+g host part vs host at split", a, b);
13448        }
13449        state_line(&mut r, "host vs graph at end", &h_end, &g_end);
13450        state_line(&mut r, "h+g vs graph at end", &m_end, &g_end);
13451        // Exact-attention K/V: the worst 256-row bin per layer and the first
13452        // bin past 1e-2.
13453        let kv_line = |r: &mut String, label: &str, a: &[(Vec<f32>, Vec<f32>)], from: usize| {
13454            let _ = writeln!(r, "  K/V {label} (worst bin K/V rel, first bin past 1e-2):");
13455            for li in (0..kinds.len()).filter(|&li| kinds[li] == 2) {
13456                let ((ak, av), (gk, gv)) = (&a[li], &g_kv[li]);
13457                if ak.len() != gk.len() || ak.is_empty() {
13458                    let _ = writeln!(r, "    L{li}: rows {}/{}", ak.len(), gk.len());
13459                    continue;
13460                }
13461                let row = ak.len() / n;
13462                let (mut wk, mut wv, mut first) = (0f64, 0f64, None);
13463                let mut b0 = from;
13464                while b0 < n {
13465                    let b1 = (b0 + bin).min(n);
13466                    let (s0, s1) = (b0 * row, b1 * row);
13467                    let (rk, rv) = (
13468                        rel_l2(&ak[s0..s1], &gk[s0..s1]),
13469                        rel_l2(&av[s0..s1], &gv[s0..s1]),
13470                    );
13471                    (wk, wv) = (wk.max(rk), wv.max(rv));
13472                    if first.is_none() && rk.max(rv) > 1e-2 {
13473                        first = Some(b0);
13474                    }
13475                    b0 = b1;
13476                }
13477                let _ = writeln!(
13478                    r,
13479                    "    L{li}: {wk:.1e}/{wv:.1e} first {}",
13480                    first.map_or("none".to_string(), |p| p.to_string())
13481                );
13482            }
13483        };
13484        kv_line(&mut r, "host vs graph", &h_kv, 0);
13485        kv_line(&mut r, "h+g vs graph", &m_kv, 0);
13486        Ok(r)
13487    }
13488
13489    /// (nll, top-1 id) of every position in [from, len − 1) from post-stack
13490    /// rows (`rows` indexed by absolute position), through the scorer's
13491    /// final norm and head.
13492    fn parity_score(&self, ids: &[u32], rows: &[f32], from: usize) -> Vec<(f64, usize)> {
13493        let (n, hs) = (ids.len(), self.hidden_size);
13494        let vrows = self.weights.lm_head.rows();
13495        let vs = self.vocab_size.min(vrows);
13496        let mut out = Vec::with_capacity(n.saturating_sub(from + 1));
13497        let mut pos = from;
13498        while pos + 1 < n {
13499            let end = (pos + 32).min(n - 1);
13500            let sb = end - pos;
13501            let mut normed = vec![0f32; sb * hs];
13502            for j in 0..sb {
13503                let r = inference::rms_norm(
13504                    &rows[(pos + j) * hs..(pos + j + 1) * hs],
13505                    &self.weights.final_norm,
13506                    self.rms_eps,
13507                    self.norm_style,
13508                );
13509                normed[j * hs..(j + 1) * hs].copy_from_slice(&r);
13510            }
13511            let mut logits = vec![0f32; sb * vrows];
13512            self.weights
13513                .lm_head
13514                .matmat(&normed, sb, &mut logits, self.pool.as_deref());
13515            for j in 0..sb {
13516                let lg = &mut logits[j * vrows..j * vrows + vs];
13517                if let Some(mu) = self.logit_multiplier {
13518                    lg.iter_mut().for_each(|v| *v *= mu);
13519                }
13520                if let Some(c) = self.final_softcap {
13521                    softcap_in_place(lg, c, self.pool.as_deref());
13522                }
13523                let (mut max, mut top) = (f32::NEG_INFINITY, 0usize);
13524                for (i, &v) in lg.iter().enumerate() {
13525                    if v > max {
13526                        (max, top) = (v, i);
13527                    }
13528                }
13529                let lse = lg
13530                    .iter()
13531                    .map(|&v| ((v - max) as f64).exp())
13532                    .sum::<f64>()
13533                    .ln()
13534                    + max as f64;
13535                out.push((lse - lg[ids[pos + j + 1] as usize] as f64, top));
13536            }
13537            pos = end;
13538        }
13539        out
13540    }
13541
13542    /// Diagnostic (`CMF_PREFILL_PARITY_O1=k` on `cortiq ppl --o1 …
13543    /// --o1-prefill P`): how much the O(1) seal depends on where its exact
13544    /// prefix ran. Every arm continues on the batch graph from the boundary
13545    /// in k-row chunks:
13546    /// - OH: prefix on the host chunked prefill; seal from the host's
13547    ///   queries and K/V; recurrent state seeded from the host.
13548    /// - OG: prefix on the batch graph (recurrent state stays on the
13549    ///   device); seal from OH's host queries and host K/V.
13550    /// - OD: prefix on the batch graph; seal from OH's queries and the
13551    ///   graph's own K/V rows (read back).
13552    ///
13553    /// OH vs OG isolates the recurrent-state hand-off, OG vs OD the seal's
13554    /// response to the host/graph K/V difference. X is the exact graph.
13555    pub fn o1_seal_parity_probe(
13556        &mut self,
13557        ids: &[u32],
13558        cfg: crate::nystrom::O1Cfg,
13559        requested: usize,
13560        bk: usize,
13561    ) -> Result<String, String> {
13562        use std::fmt::Write as _;
13563        let (n, hs) = (ids.len(), self.hidden_size);
13564        self.set_o1(Some(cfg));
13565        let split = self
13566            .o1_effective_boundary(requested)
13567            .ok_or("o1 parity: no O(1) configuration")?;
13568        if split + 1 >= n || bk == 0 || !self.can_prefill_batched() {
13569            return Err(format!(
13570                "o1 parity: needs the boundary {split} inside {n} tokens, k > 0 and a batched host prefill"
13571            ));
13572        }
13573        let o1_layers: Vec<usize> = (0..self.num_layers)
13574            .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
13575            .collect();
13576        let graph_rows = |p: &mut Self, from: usize, to: usize, rows: &mut [f32]| {
13577            let mut pos = from;
13578            while pos < to {
13579                let end = (pos + bk).min(to);
13580                let k = end - pos;
13581                let mut hid = vec![0f32; k * hs];
13582                for (j, &id) in ids[pos..end].iter().enumerate() {
13583                    hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
13584                }
13585                let positions: Vec<usize> = (pos..end).collect();
13586                let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
13587                if outcome != crate::gpu::BatchGraphOutcome::Completed {
13588                    return Err(format!(
13589                        "o1 parity: batch graph {outcome:?} at position {pos}"
13590                    ));
13591                }
13592                rows[pos * hs..end * hs].copy_from_slice(&hid);
13593                pos = end;
13594            }
13595            Ok(())
13596        };
13597        // Seal every O(1) layer from the given rows; `trace` = (q, k, v) per
13598        // O(1) layer, position-major.
13599        let seal_from = |p: &mut Self, trace: &[(Vec<f32>, Vec<f32>, Vec<f32>)]| {
13600            for (&li, (q, k, v)) in o1_layers.iter().zip(trace) {
13601                let c = &mut p.kv_cache.layers[li];
13602                let (row, qrow) = (k.len() / split, q.len() / split);
13603                for pos in 0..split {
13604                    c.append(
13605                        &k[pos * row..(pos + 1) * row],
13606                        &v[pos * row..(pos + 1) * row],
13607                        &[],
13608                    );
13609                    c.o1_push_q(&q[pos * qrow..(pos + 1) * qrow]);
13610                }
13611            }
13612            p.o1_seal_checked()
13613        };
13614        // Greedy continuation (`CMF_PARITY_GREEDY` tokens, default 40) from
13615        // the last prompt row, one batch-graph row per step.
13616        let steps: usize = std::env::var("CMF_PARITY_GREEDY")
13617            .ok()
13618            .and_then(|v| v.parse().ok())
13619            .unwrap_or(40);
13620        // `CMF_PARITY_DECODE=token`: the steps through `decode_step_logits`
13621        // (the token graph generation decodes on) instead of the batch graph.
13622        let token_decode = std::env::var("CMF_PARITY_DECODE").as_deref() == Ok("token");
13623        let greedy = |p: &mut Self, rows: &[f32]| -> Result<Vec<u32>, String> {
13624            let argmax = |lg: &[f32], vs: usize| {
13625                (0..vs.min(lg.len())).fold(0, |b, i| if lg[i] > lg[b] { i } else { b }) as u32
13626            };
13627            let normed = inference::rms_norm(
13628                &rows[(n - 1) * hs..n * hs],
13629                &p.weights.final_norm,
13630                p.rms_eps,
13631                p.norm_style,
13632            );
13633            let mut out = vec![argmax(&p.lm_head_forward(&normed), p.vocab_size)];
13634            for s in 1..steps {
13635                let (tok, at) = (out[s - 1], n + s - 1);
13636                let lg = if token_decode {
13637                    p.decode_step_logits(tok, at)
13638                } else {
13639                    let mut hid = p.embed_single(tok);
13640                    let outcome = p.try_batch_graph_wgpu(&mut hid, &[at], 1, None);
13641                    if outcome != crate::gpu::BatchGraphOutcome::Completed {
13642                        return Err(format!("o1 parity: greedy step {s} {outcome:?}"));
13643                    }
13644                    let normed =
13645                        inference::rms_norm(&hid, &p.weights.final_norm, p.rms_eps, p.norm_style);
13646                    p.lm_head_forward(&normed)
13647                };
13648                out.push(argmax(&lg, p.vocab_size));
13649            }
13650            Ok(out)
13651        };
13652
13653        // OW first, in a fresh process as `run` starts: the host prefix
13654        // exactly as generation runs it — boundary at `split`, the
13655        // pipeline's prefill chunk, the layers sealing after the chunk that
13656        // reaches the boundary (`CMF_O1_WALK_BOUNDARY=1`: that chunk walks
13657        // its rows one at a time, each layer sealing after the boundary row).
13658        self.clear_sequence_state();
13659        self.o1_begin_with_prefix(Some(requested));
13660        let walk_chunk = self.prefill_chunk().max(1);
13661        let walk_from = (split - 1) / walk_chunk * walk_chunk;
13662        let mut ow = vec![0f32; n * hs];
13663        let mut pos = 0;
13664        while pos < split {
13665            let end = (pos + walk_chunk).min(split);
13666            let hb = self.prefill_batch(&ids[pos..end], pos);
13667            ow[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
13668            pos = end;
13669        }
13670        self.o1_seal_checked()?;
13671        let ow_run = graph_rows(self, split, n, &mut ow);
13672        let ow_text = ow_run.clone().and_then(|_| greedy(self, &ow));
13673
13674        // OH. Collect one row past the boundary's natural barrier so the
13675        // host walk does not seal inside the last chunk, copy the trace,
13676        // then seal at `split` exactly.
13677        let t = std::time::Instant::now();
13678        self.clear_sequence_state();
13679        self.o1_begin_with_prefix(Some(split + 1));
13680        let mut oh = vec![0f32; n * hs];
13681        let mut pos = 0;
13682        while pos < split {
13683            let end = (pos + 128).min(split);
13684            let hb = self.prefill_batch(&ids[pos..end], pos);
13685            oh[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
13686            pos = end;
13687        }
13688        let mut host = Vec::with_capacity(o1_layers.len());
13689        for &li in &o1_layers {
13690            let c = &self.kv_cache.layers[li];
13691            let q = c
13692                .o1_q_trace()
13693                .ok_or("o1 parity: host walk sealed early")?
13694                .to_vec();
13695            let (nkv, hd) = (c.num_kv_heads, c.head_dim);
13696            let (mut k, mut v) = (vec![0f32; split * nkv * hd], vec![0f32; split * nkv * hd]);
13697            for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
13698                for p in 0..split {
13699                    let d = (p * nkv + h) * hd;
13700                    k[d..d + hd].copy_from_slice(&kh[p * hd..(p + 1) * hd]);
13701                    v[d..d + hd].copy_from_slice(&vh[p * hd..(p + 1) * hd]);
13702                }
13703            }
13704            host.push((q, k, v));
13705        }
13706        for &li in &o1_layers {
13707            self.kv_cache.layers[li].o1_set_boundary(split);
13708        }
13709        self.o1_seal_checked()?;
13710        let oh_run = graph_rows(self, split, n, &mut oh);
13711        let t_oh = t.elapsed().as_secs_f64();
13712        let oh_text = oh_run.clone().and_then(|_| greedy(self, &oh));
13713
13714        // OG: graph prefix, host seal inputs. `CMF_PARITY_PERTURB=eps`
13715        // scales every host K/V value by (1 ± eps), signs from a fixed
13716        // xorshift stream: the seal's sensitivity to rounding-size noise.
13717        self.clear_sequence_state();
13718        self.o1_begin_with_prefix(Some(requested));
13719        let mut og = vec![0f32; n * hs];
13720        graph_rows(self, 0, split, &mut og)?;
13721        let perturb: f32 = std::env::var("CMF_PARITY_PERTURB")
13722            .ok()
13723            .and_then(|v| v.parse().ok())
13724            .unwrap_or(0.0);
13725        if perturb > 0.0 {
13726            let mut seed = 0x9e37_79b9_7f4a_7c15u64;
13727            let noisy: Vec<_> = host
13728                .iter()
13729                .map(|(q, k, v)| {
13730                    let mut jitter = |x: &f32| {
13731                        seed ^= seed << 13;
13732                        seed ^= seed >> 7;
13733                        seed ^= seed << 17;
13734                        x * if seed & 1 == 0 {
13735                            1.0 + perturb
13736                        } else {
13737                            1.0 - perturb
13738                        }
13739                    };
13740                    let k: Vec<f32> = k.iter().map(&mut jitter).collect();
13741                    let v: Vec<f32> = v.iter().map(&mut jitter).collect();
13742                    (q.clone(), k, v)
13743                })
13744                .collect();
13745            seal_from(self, &noisy)?;
13746        } else {
13747            seal_from(self, &host)?;
13748        }
13749        let og_run = graph_rows(self, split, n, &mut og);
13750        let og_text = og_run.clone().and_then(|_| greedy(self, &og));
13751
13752        // OD: graph prefix, the graph's own K/V with the host queries.
13753        self.clear_sequence_state();
13754        self.o1_begin_with_prefix(Some(requested));
13755        let mut od = vec![0f32; n * hs];
13756        graph_rows(self, 0, split, &mut od)?;
13757        let mut dev = Vec::with_capacity(o1_layers.len());
13758        let mut kv_rel = Vec::with_capacity(o1_layers.len());
13759        for (&li, (q, hk, hv)) in o1_layers.iter().zip(&host) {
13760            let c = &self.kv_cache.layers[li];
13761            let (k, v) = crate::gpu::graph_kv_read_rows(
13762                self.graph_kv_id,
13763                &[(li, 0, split)],
13764                c.num_kv_heads,
13765                c.head_dim,
13766            )
13767            .and_then(|mut rows| rows.pop())
13768            .ok_or_else(|| format!("o1 parity: no graph K/V rows for layer {li}"))?;
13769            kv_rel.push((li, rel_l2(hk, &k), rel_l2(hv, &v)));
13770            dev.push((q.clone(), k, v));
13771        }
13772        seal_from(self, &dev)?;
13773        let od_run = graph_rows(self, split, n, &mut od);
13774        let od_text = od_run.clone().and_then(|_| greedy(self, &od));
13775
13776        // X: exact batch graph.
13777        self.clear_sequence_state();
13778        self.set_o1(None);
13779        let mut xx = vec![0f32; n * hs];
13780        let x_run = graph_rows(self, 0, n, &mut xx);
13781        let x_text = x_run.clone().and_then(|_| greedy(self, &xx));
13782        self.clear_sequence_state();
13783        oh_run?;
13784        ow_run?;
13785        og_run?;
13786        od_run?;
13787        x_run?;
13788
13789        let sx = self.parity_score(ids, &xx, split);
13790        let arms = [
13791            ("OH", self.parity_score(ids, &oh, split)),
13792            ("OG", self.parity_score(ids, &og, split)),
13793            ("OD", self.parity_score(ids, &od, split)),
13794            ("OW", self.parity_score(ids, &ow, split)),
13795        ];
13796        let mut r = String::new();
13797        let _ = writeln!(
13798            r,
13799            "o1 seal parity: {n} tokens, boundary {split}, graph k {bk} (OH {t_oh:.1} s), \
13800             OW chunk {walk_chunk}, boundary chunk {walk_from}..{split}"
13801        );
13802        let _ = write!(r, "  seal K/V rel host vs graph:");
13803        for (li, rk, rv) in &kv_rel {
13804            let _ = write!(r, " L{li} {rk:.0e}/{rv:.0e}");
13805        }
13806        // Post-stack prefix rows against the exact graph: the batched rows,
13807        // then the rows OW walks one at a time.
13808        let worst = |a: &[f32], p0: usize, p1: usize| {
13809            (p0..p1)
13810                .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &xx[p * hs..(p + 1) * hs]))
13811                .fold(0f64, f64::max)
13812        };
13813        let _ = writeln!(
13814            r,
13815            "\n  prefix rows rel vs exact graph, [0,{walk_from}) / [{walk_from},{split}): \
13816             OH {:.1e} / {:.1e}, OW {:.1e} / {:.1e}",
13817            worst(&oh, 0, walk_from),
13818            worst(&oh, walk_from, split),
13819            worst(&ow, 0, walk_from),
13820            worst(&ow, walk_from, split),
13821        );
13822        let _ = writeln!(
13823            r,
13824            "  bin          | NLL exact    OH     OG     OD     OW | top1=exact  OH    OG    OD    OW"
13825        );
13826        let mean = |s: &[(f64, usize)], b0: usize, b1: usize| {
13827            (b0..b1).map(|p| s[p - split].0).sum::<f64>() / (b1 - b0).max(1) as f64
13828        };
13829        let agree = |s: &[(f64, usize)], b0: usize, b1: usize| {
13830            let same = (b0..b1)
13831                .filter(|&p| s[p - split].1 == sx[p - split].1)
13832                .count();
13833            100.0 * same as f64 / (b1 - b0).max(1) as f64
13834        };
13835        let mut b0 = split;
13836        let mut total = [0f64; 5];
13837        while b0 + 1 < n {
13838            let b1 = ((b0 / 256 + 1) * 256).min(n - 1);
13839            let _ = writeln!(
13840                r,
13841                "  {b0:5}..{b1:<5} | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | {:>12.1} {:>5.1} {:>5.1} {:>5.1}",
13842                mean(&sx, b0, b1),
13843                mean(&arms[0].1, b0, b1),
13844                mean(&arms[1].1, b0, b1),
13845                mean(&arms[2].1, b0, b1),
13846                mean(&arms[3].1, b0, b1),
13847                agree(&arms[0].1, b0, b1),
13848                agree(&arms[1].1, b0, b1),
13849                agree(&arms[2].1, b0, b1),
13850                agree(&arms[3].1, b0, b1),
13851            );
13852            b0 = b1;
13853        }
13854        for (i, s) in std::iter::once(&sx)
13855            .chain(arms.iter().map(|a| &a.1))
13856            .enumerate()
13857        {
13858            total[i] = s.iter().map(|x| x.0).sum::<f64>() / s.len().max(1) as f64;
13859        }
13860        let _ = writeln!(
13861            r,
13862            "  all          | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | ppl ratio vs exact OH ×{:.3} OG ×{:.3} OD ×{:.3} OW ×{:.3}",
13863            total[0],
13864            total[1],
13865            total[2],
13866            total[3],
13867            total[4],
13868            (total[1] - total[0]).exp(),
13869            (total[2] - total[0]).exp(),
13870            (total[3] - total[0]).exp(),
13871            (total[4] - total[0]).exp(),
13872        );
13873        for (name, text) in [
13874            ("X ", x_text),
13875            ("OH", oh_text),
13876            ("OG", og_text),
13877            ("OD", od_text),
13878            ("OW", ow_text),
13879        ] {
13880            let s = text.map_or_else(|e| e, |t| self.tokenizer.decode(&t));
13881            let _ = writeln!(r, "  greedy {name}: {:?}", s);
13882        }
13883        Ok(r)
13884    }
13885
13886    /// The positions of `input_ids` a Dory sequence already holds: the common
13887    /// prefix, unless going back to it passes what a sliding ring still keeps.
13888    fn dory_cached_prefix(&self, input_ids: &[u32]) -> usize {
13889        if std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0") {
13890            return 0;
13891        }
13892        let h = &self.kv_history;
13893        let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
13894        let n = lcp.min(input_ids.len().saturating_sub(1));
13895        if n == 0 || h.len() - n > crate::dory::REWIND {
13896            return 0;
13897        }
13898        // Rows past the prefix have gone through the far field: a sealed sequence can
13899        // only be extended, and a turn that goes back starts over.
13900        let sealed = crate::gpu::dory_sealed(self.graph_kv_id)
13901            || self.dory.as_ref().is_some_and(|b| b.1.slots.iter().flatten().any(|s| !s.far.is_empty()));
13902        if n < h.len() && sealed {
13903            return 0;
13904        }
13905        n
13906    }
13907
13908    /// Dory's score: chunks through its own stack, logits of every row.
13909    fn dory_nll(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
13910        self.clear_sequence_state();
13911        let chunk: usize = std::env::var("CMF_NLL_CHUNK")
13912            .ok()
13913            .and_then(|v| v.parse().ok())
13914            .filter(|&c: &usize| c > 0)
13915            .unwrap_or(128);
13916        let vocab = self.vocab_size;
13917        let n = ids.len().saturating_sub(1);
13918        // `CMF_NLL_SKIP`: a prompt of so many rows goes in as a prompt does.
13919        let skip: usize = std::env::var("CMF_NLL_SKIP").ok().and_then(|v| v.parse().ok()).unwrap_or(0);
13920        let start = start.max(skip.min(n));
13921        let (mut nll, mut cnt, mut pos) = (0f64, 0usize, 0usize);
13922        while pos < n {
13923            if pos < skip {
13924                let end = (pos + 64).min(skip).min(n);
13925                self.dory_forward(&ids[pos..end], pos, crate::dory::Want::None);
13926                pos = end;
13927                continue;
13928            }
13929            let end = (pos + chunk).min(n);
13930            let lg = self.dory_forward(&ids[pos..end], pos, crate::dory::Want::All);
13931            for (r, row) in lg.chunks_exact(vocab).enumerate() {
13932                if pos + r < start {
13933                    continue;
13934                }
13935                let target = ids[pos + r + 1] as usize;
13936                let best = row.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b)) as f64;
13937                let den: f64 = row.iter().map(|&x| ((x as f64) - best).exp()).sum();
13938                nll += den.ln() - (row[target] as f64 - best);
13939                cnt += 1;
13940            }
13941            pos = end;
13942        }
13943        Ok((nll, cnt))
13944    }
13945
13946    /// Dory's forward over a chunk: the device path when it takes the file.
13947    fn dory_forward(&mut self, ids: &[u32], pos: usize, want: crate::dory::Want) -> Vec<f32> {
13948        let pool = self.pool.clone();
13949        let id = self.graph_kv_id;
13950        let b = self.dory.as_mut().expect("a Dory file");
13951        let (m, st) = (&b.0, &mut b.1);
13952        let mut logits = Vec::new();
13953        let loops = m.cfg.loops;
13954        // GPTQ's Hessian hook sees a matrix's input only on the host stack.
13955        let host = crate::gptq_capture::capturing();
13956        let file = m.file.clone();
13957        if host || !crate::gpu::dory_forward(id, file.as_ref(), m, ids, pos, loops, want, &mut logits) {
13958            logits.clear();
13959            crate::dory::forward_host(m, st, ids, pos, loops, want, pool.as_deref(), None, &mut logits);
13960        }
13961        if m.o1.is_some() && std::env::var_os("CMF_DORY_KV_REPORT").is_some() {
13962            let (e, f) = crate::dory::host_kv_bytes(st);
13963            eprintln!("dory kv at {}: exact {:.1} MB, far {:.1} MB", pos + ids.len(), e as f64 / 1e6, f as f64 / 1e6);
13964        }
13965        if let Ok(dir) = std::env::var("CMF_DORY_DUMP") {
13966            let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
13967            let _ = std::fs::write(format!("{dir}/{pos}.bin"), bytes);
13968        }
13969        logits
13970    }
13971
13972    fn nll_ids_inner(
13973        &mut self,
13974        ids: &[u32],
13975        start: usize,
13976        task_mask: Option<&TaskMask>,
13977    ) -> Result<(f64, usize), String> {
13978        if self.dory.is_some() && task_mask.is_none() {
13979            return self.dory_nll(ids, start);
13980        }
13981        self.nll_begin()?;
13982        // Block diffusion on Apple silicon: the Metal executor's causal path.
13983        #[cfg(target_os = "macos")]
13984        if task_mask.is_none() && self.is_block_diffusion() {
13985            if let Some(r) = self.clover_metal_nll(ids, start) {
13986                self.nll_end();
13987                return r;
13988            }
13989        }
13990        let result: Result<(f64, usize), String> = (|| {
13991            let mut nll = 0f64;
13992            let mut cnt = 0usize;
13993            // An unmasked quality run with the resident wgpu graph must score
13994            // the same stateful path used by generation.  The layer-major
13995            // GEMM prefill below is a valid CPU/GEMM oracle, but it seeds
13996            // neither the graph's device GDN state nor its device KV mirrors;
13997            // using it here would silently score a different execution.  Keep
13998            // masked scoring on the exact per-position path as before, and
13999            // let the serial arm below drive the graph-aware scorer.
14000            // Only native Metal has a fused graph lm_head contract.  Vulkan
14001            // and other graph backends may expose hidden state without the
14002            // optional logits side channel; preserve their established CPU
14003            // norm/head fallback instead of turning that valid route into a
14004            // hard missing-logits error.
14005            let (graph_quality, fused_head_quality) = nll_graph_policy(
14006                task_mask.is_none(),
14007                self.graph_prefill_preferred(),
14008                crate::gpu::q1_force(),
14009            );
14010            self.graph_head_required = fused_head_quality;
14011            self.graph_want_logits = fused_head_quality;
14012            #[cfg(target_os = "macos")]
14013            if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
14014                match self.nll_batch_metal(ids, start) {
14015                    MetalBatchNllOutcome::Completed(nll, count) => {
14016                        return Ok((nll, count));
14017                    }
14018                    MetalBatchNllOutcome::Declined => {}
14019                    MetalBatchNllOutcome::Failed(err) => return Err(err),
14020                }
14021            }
14022            // CMF_NLL_SERIAL=1 scores position by position through the
14023            // decode path (the wgpu token graph where it admits the model)
14024            // — the perplexity gate of a decode kernel, which the batched
14025            // prefill arm below never runs.
14026            let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
14027            if self.can_prefill_batched() && !graph_quality && !force_serial {
14028                // prefill-GEMM: layer-major position chunks, lm_head batched
14029                // (254MB lm_head read once per chunk, not per position).
14030                // The layer chunk is large (grouping positions by MoE experts
14031                // wins with size), lm_head in sub-blocks (logit buffer
14032                // 32×vocab ≈ 32MB instead of 128×).
14033                // CMF_NLL_CHUNK: positions per prefill step (default 128).
14034                // A device scorer pays one round trip per projection per
14035                // step, so a 512-position step quarters them.
14036                let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
14037                    .ok()
14038                    .and_then(|v| v.parse().ok())
14039                    .filter(|&c: &usize| c > 0)
14040                    .unwrap_or(128);
14041                const LM_SUB: usize = 32;
14042                let n = ids.len().saturating_sub(1);
14043                let hs = self.hidden_size;
14044                let rows = self.weights.lm_head.rows();
14045                let mut pos = 0usize;
14046                let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
14047                while pos < n {
14048                    let end = (pos + chunk_len).min(n);
14049                    let bsz = end - pos;
14050                    let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
14051                    self.nll_check_graph("batched prefill", pos)?;
14052                    if state_trace && end % 256 == 0 {
14053                        self.trace_recurrent_state(end);
14054                    }
14055                    let mut k0 = 0usize;
14056                    while k0 < bsz {
14057                        let k1 = (k0 + LM_SUB).min(bsz);
14058                        let sb = k1 - k0;
14059                        // Sub-block entirely below the scored range: the KV
14060                        // it just built is all this pass needed from it.
14061                        if pos + k1 <= start {
14062                            k0 = k1;
14063                            continue;
14064                        }
14065                        let mut normed = vec![0.0f32; sb * hs];
14066                        for k in 0..sb {
14067                            let r = inference::rms_norm(
14068                                &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
14069                                &self.weights.final_norm,
14070                                self.rms_eps,
14071                                self.norm_style,
14072                            );
14073                            normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
14074                        }
14075                        let mut logits = vec![0.0f32; sb * rows];
14076                        self.weights
14077                            .lm_head
14078                            .matmat(&normed, sb, &mut logits, self.pool.as_deref());
14079                        for k in 0..sb {
14080                            if pos + k0 + k < start {
14081                                continue;
14082                            }
14083                            self.nll_check_graph("batched score row", pos + k0 + k)?;
14084                            let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
14085                            if let Some(mu) = self.logit_multiplier {
14086                                for v in lg.iter_mut() {
14087                                    *v *= mu;
14088                                }
14089                            }
14090                            // Gemma-class final-logit soft-capping: the
14091                            // decode paths apply it; scoring must too, or
14092                            // the uncapped softmax misprices every token.
14093                            if let Some(c) = self.final_softcap {
14094                                softcap_in_place(lg, c, self.pool.as_deref());
14095                            }
14096                            // Cortiq Embryo hierarchical head: same correction
14097                            // the decode path applies (lm_head_forward).
14098                            if let Some(cm) = self.head_clusters.clone() {
14099                                self.hierarchical_head_logprobs(
14100                                    &normed[k * hs..(k + 1) * hs],
14101                                    &cm,
14102                                    lg,
14103                                );
14104                            }
14105                            let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
14106                            let target = ids[pos + k0 + k + 1] as usize;
14107                            let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14108                            let lse: f64 = lg
14109                                .iter()
14110                                .map(|&v| ((v - max) as f64).exp())
14111                                .sum::<f64>()
14112                                .ln()
14113                                + max as f64;
14114                            nll += lse - lg[target] as f64;
14115                            cnt += 1;
14116                            if std::env::var("CMF_PPL_TRACE").is_ok() {
14117                                let top = lg
14118                                    .iter()
14119                                    .enumerate()
14120                                    .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14121                                    .map(|(i, _)| i)
14122                                    .unwrap_or(0);
14123                                eprintln!(
14124                                    "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
14125                                    pos + k0 + k,
14126                                    target,
14127                                    lse - lg[target] as f64,
14128                                    top,
14129                                    lg[target],
14130                                    lg[top]
14131                                );
14132                            }
14133                        }
14134                        k0 = k1;
14135                    }
14136                    pos = end;
14137                }
14138                return Ok((nll, cnt));
14139            }
14140            for pos in 0..ids.len().saturating_sub(1) {
14141                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
14142                self.nll_check_graph("serial forward", pos)?;
14143                // Architectures whose head lives inside their own stack return
14144                // the logits out of band and a zero hidden — DeepSeek-V4 folds
14145                // its hyper-connection copies between the last layer and the
14146                // norm, so it cannot hand back a vector this loop could use.
14147                // Scoring the zeros gave a perplexity of exactly the vocabulary
14148                // size, which is a uniform distribution reported as a
14149                // measurement. `generate` already reads this channel.
14150                let out_of_band = self.graph_logits.take();
14151                if self.graph_head_required && out_of_band.is_none() {
14152                    METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14153                    return Err(format!(
14154                        "fused Metal graph head did not complete at NLL position {pos}"
14155                    ));
14156                }
14157                if pos < start {
14158                    continue;
14159                }
14160                let logits = match out_of_band {
14161                    Some(lg) => lg,
14162                    None => {
14163                        let normed = inference::rms_norm(
14164                            &hidden,
14165                            &self.weights.final_norm,
14166                            self.rms_eps,
14167                            self.norm_style,
14168                        );
14169                        // lm_head_forward applies the final-logit softcap itself
14170                        // — capping again here double-squashed gemma-class
14171                        // logits (tanh∘tanh) and reported a flattered ppl.
14172                        self.lm_head_forward(&normed)
14173                    }
14174                };
14175                let target = ids[pos + 1] as usize;
14176                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14177                let lse: f64 = logits
14178                    .iter()
14179                    .map(|&v| ((v - max) as f64).exp())
14180                    .sum::<f64>()
14181                    .ln()
14182                    + max as f64;
14183                let tok_nll = lse - logits[target] as f64;
14184                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
14185                    let top = logits
14186                        .iter()
14187                        .enumerate()
14188                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14189                        .map(|(i, _)| i)
14190                        .unwrap_or(0);
14191                    eprintln!(
14192                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
14193                        logits[target], logits[top]
14194                    );
14195                }
14196                nll += tok_nll;
14197                cnt += 1;
14198            }
14199            Ok((nll, cnt))
14200        })();
14201        self.nll_end();
14202        result
14203    }
14204
14205    /// Score one post-layer hidden with the same final norm/head path used by
14206    /// decode. Keeping this in one helper is important for the production
14207    /// batch scorer: its rows stop before the final norm, just like the
14208    /// per-position O(1) path below.
14209    fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
14210        let normed = inference::rms_norm(
14211            hidden,
14212            &self.weights.final_norm,
14213            self.rms_eps,
14214            self.norm_style,
14215        );
14216        // lm_head_forward applies the final-logit softcap itself — capping
14217        // again here double-squashed gemma-class logits in earlier scorers.
14218        let mut logits = self.lm_head_forward(&normed);
14219        let target = target as usize;
14220        let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14221        let lse: f64 = logits
14222            .iter()
14223            .map(|&v| ((v - max) as f64).exp())
14224            .sum::<f64>()
14225            .ln()
14226            + max as f64;
14227        let tok_nll = lse - logits[target] as f64;
14228        if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
14229            let top = logits
14230                .iter()
14231                .enumerate()
14232                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14233                .map(|(i, _)| i)
14234                .unwrap_or(0);
14235            eprintln!(
14236                "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
14237                logits[target], logits[top]
14238            );
14239        }
14240        attention::recycle_buf(&mut logits);
14241        tok_nll
14242    }
14243
14244    /// `CMF_STATE_TRACE`: per-layer magnitude of the recurrent record at
14245    /// position `pos` — the whole `linear_state` (vmf: S then the conv
14246    /// ring; GDN: conv ring then S), its recurrent S part alone, the
14247    /// bounded ring, and the last per-position KV row count.  The tool
14248    /// that separated the ~4k perplexity cliff of the 500-step exports
14249    /// (a state that keeps climbing past the trained window) from a
14250    /// runtime boundary; one line per layer, `STATE pos=… layer=…`.
14251    fn trace_recurrent_state(&self, pos: usize) {
14252        let stats = |v: &[f32]| -> (f64, f64) {
14253            if v.is_empty() {
14254                return (0.0, 0.0);
14255            }
14256            let (mut ss, mut mx) = (0f64, 0f64);
14257            for &x in v {
14258                ss += (x as f64) * (x as f64);
14259                mx = mx.max((x as f64).abs());
14260            }
14261            ((ss / v.len() as f64).sqrt(), mx)
14262        };
14263        for (li, l) in self.kv_cache.layers.iter().enumerate() {
14264            let lw = &self.weights.layers[self.phys_layer(li)];
14265            let (kind, s_len) = match &lw.attn {
14266                AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
14267                AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
14268                AttnKind::Bounded(_) => ("bounded", 0),
14269                AttnKind::Full { .. } => ("full", 0),
14270                _ => ("other", 0),
14271            };
14272            let (rms, max) = stats(&l.linear_state);
14273            let s_part = if kind == "vmf" {
14274                &l.linear_state[..s_len.min(l.linear_state.len())]
14275            } else if kind == "gdn" {
14276                let ring = l
14277                    .linear_state
14278                    .len()
14279                    .saturating_sub(s_len)
14280                    .min(l.linear_state.len());
14281                &l.linear_state[ring..]
14282            } else {
14283                &l.linear_state[..0]
14284            };
14285            let (s_rms, s_max) = stats(s_part);
14286            let (ring_rms, ring_len) = match &l.bounded {
14287                Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
14288                None => (0.0, 0),
14289            };
14290            eprintln!(
14291                "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
14292                 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
14293                l.linear_state.len(),
14294                l.seq_len
14295            );
14296        }
14297    }
14298
14299    /// Teacher-forced NLL of the CONVERTED model: the O(1) Nyström path
14300    /// is ACTIVE over the scored positions. Returns `Ok((nll sum, scored
14301    /// count))` over `prefill..len-1` and surfaces a post-mutation batch
14302    /// failure instead of returning a partial score.
14303    ///
14304    /// Runtime discipline, deliberately NOT the matrix probe's: the
14305    /// requested prefix plus any required deferred lead-in run the exact
14306    /// prompt pass — that pass is what freezes the landmarks and M — and
14307    /// every post-seal scored position goes through `NystromState::step()`,
14308    /// the same code decode runs.
14309    /// So the landmarks are PREFILL-frozen (what ships), not
14310    /// full-sequence oracles (what the published probe measured). When the
14311    /// requested prefix is shorter than the bounded transition, rows in the
14312    /// exact lead-in are still scored so the shifted target range is stable.
14313    ///
14314    /// Pair with `nll_ids_from(ids, prefill)` for the exact baseline
14315    /// over the identical token set — that ratio is the honest one.
14316    pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
14317        // This scorer consumes host hiddens, so never request the optional
14318        // token-graph lm_head side channel. `nll_begin` also consumes a
14319        // prior graph failure and clears only the cancel bit that failure
14320        // raised, leaving a caller-owned cancellation observable.
14321        self.nll_begin()?;
14322        let requested_prefix = (prefill > 0).then_some(prefill);
14323        self.o1_begin_with_prefix(requested_prefix);
14324        let n = ids.len().saturating_sub(1);
14325        let requested_start = prefill.min(n);
14326        // The exact prefix must reach the deferred boundary before a
14327        // collecting layer can convert. Rows between the requested start and
14328        // that boundary remain part of the public NLL range and are scored
14329        // from the same hidden pass below.
14330        let exact_end = if self.o1_active() {
14331            match requested_prefix {
14332                Some(requested) => self.o1_effective_boundary(requested),
14333                None => self
14334                    .o1_cfg
14335                    .as_ref()
14336                    .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
14337            }
14338            .unwrap_or(requested_start)
14339            .min(n)
14340        } else {
14341            requested_start
14342        };
14343        let mut nll = 0f64;
14344        let mut cnt = 0usize;
14345
14346        // Exact prompt pass over ids[..exact_end]: the seal consumes its
14347        // q/k/v. Rows at or after requested_start are scored here when the
14348        // bounded lead-in is longer than the caller's requested prefix.
14349        let mut pos = 0usize;
14350        if self.can_prefill_batched() {
14351            const CHUNK: usize = 128;
14352            while pos < exact_end {
14353                let end = (pos + CHUNK).min(exact_end);
14354                let hiddens = self.prefill_batch(&ids[pos..end], pos);
14355                if self
14356                    .graph_failed
14357                    .swap(false, std::sync::atomic::Ordering::Relaxed)
14358                {
14359                    self.cancel
14360                        .store(false, std::sync::atomic::Ordering::Relaxed);
14361                    self.nll_end();
14362                    return Err("GPU graph failed during O(1) NLL prefix".into());
14363                }
14364                for row in 0..end - pos {
14365                    let score_pos = pos + row;
14366                    if score_pos >= requested_start && score_pos < n {
14367                        nll += self.nll_from_hidden(
14368                            &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
14369                            ids[score_pos + 1],
14370                            score_pos,
14371                        );
14372                        cnt += 1;
14373                    }
14374                }
14375                pos = end;
14376            }
14377        } else {
14378            while pos < exact_end {
14379                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
14380                if self
14381                    .graph_failed
14382                    .swap(false, std::sync::atomic::Ordering::Relaxed)
14383                {
14384                    self.cancel
14385                        .store(false, std::sync::atomic::Ordering::Relaxed);
14386                    self.nll_end();
14387                    return Err("GPU graph failed during O(1) NLL prefix".into());
14388                }
14389                if pos >= requested_start && pos < n {
14390                    nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
14391                    cnt += 1;
14392                }
14393                pos += 1;
14394            }
14395        }
14396        self.o1_seal_checked().map_err(|err| {
14397            self.nll_end();
14398            err
14399        })?;
14400
14401        // Reuse the production whole-token batch graph for the post-seal
14402        // suffix when the caller explicitly enabled both routes. This is a
14403        // teacher-forced scorer, so every row is ids[pos] and its target is
14404        // ids[pos + 1]; no speculative tail or rollback state is involved.
14405        // A first Declined is safe to handle with the established serial O(1)
14406        // path. Once a chunk completes, however, the device recurrent state
14407        // owns the sequence and a later decline must be terminal rather than
14408        // falling back to stale CPU state.
14409        let batch_k = std::env::var("CMF_BATCH_K")
14410            .ok()
14411            .and_then(|v| v.parse::<usize>().ok())
14412            .unwrap_or(0);
14413        let batch_admitted = batch_k > 0
14414            && self.can_prefill_batched()
14415            && self.o1_active()
14416            && self.o1_on_graph()
14417            && (0..self.num_layers).all(|li| {
14418                let cache = &self.kv_cache.layers[self.phys_layer(li)];
14419                cache.o1.is_none() || cache.o1_views().is_some()
14420            });
14421        if std::env::var("CMF_GRAPH_PROF").is_ok() {
14422            eprintln!(
14423                "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
14424                batch_admitted,
14425                batch_k,
14426                n.saturating_sub(exact_end),
14427            );
14428        }
14429        let mut batch_completed = false;
14430        if batch_admitted && exact_end < n {
14431            let hs = self.hidden_size;
14432            let mut batch_pos = exact_end;
14433            while batch_pos < n {
14434                let end = (batch_pos + batch_k).min(n);
14435                let bk = end - batch_pos;
14436                let mut hiddens = vec![0.0f32; bk * hs];
14437                for (row, &id) in ids[batch_pos..end].iter().enumerate() {
14438                    hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
14439                }
14440                let positions: Vec<usize> = (batch_pos..end).collect();
14441                let t_batch = std::time::Instant::now();
14442                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
14443                if std::env::var("CMF_GRAPH_PROF").is_ok() {
14444                    let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
14445                    eprintln!(
14446                        "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
14447                        batch_pos,
14448                        end.saturating_sub(1),
14449                        bk as f64 / (ms / 1000.0),
14450                    );
14451                }
14452                if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
14453                    self.nll_end();
14454                    return Err(err);
14455                }
14456                match outcome {
14457                    crate::gpu::BatchGraphOutcome::Completed => {
14458                        batch_completed = true;
14459                        for row in 0..bk {
14460                            nll += self.nll_from_hidden(
14461                                &hiddens[row * hs..(row + 1) * hs],
14462                                ids[batch_pos + row + 1],
14463                                batch_pos + row,
14464                            );
14465                            cnt += 1;
14466                        }
14467                        batch_pos = end;
14468                    }
14469                    crate::gpu::BatchGraphOutcome::Declined => {
14470                        if batch_completed {
14471                            self.nll_end();
14472                            return Err(format!(
14473                                "O(1) NLL batch declined after completed chunk at position {batch_pos}"
14474                            ));
14475                        }
14476                        break;
14477                    }
14478                    crate::gpu::BatchGraphOutcome::Failed => {
14479                        self.nll_end();
14480                        return Err(format!(
14481                            "O(1) NLL batch graph failed after admission at position {batch_pos}"
14482                        ));
14483                    }
14484                }
14485            }
14486            if batch_completed && cnt == n.saturating_sub(requested_start) {
14487                self.nll_end();
14488                return Ok((nll, cnt));
14489            }
14490        }
14491
14492        // Serial O(1) fallback/reference. It is intentionally retained when
14493        // batch admission declines before mutation; callers must label this
14494        // CMF_BATCH_K=0/per-position path separately from the production
14495        // whole-token batch route.
14496        for pos in exact_end..n {
14497            let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
14498            if self
14499                .graph_failed
14500                .swap(false, std::sync::atomic::Ordering::Relaxed)
14501            {
14502                self.cancel
14503                    .store(false, std::sync::atomic::Ordering::Relaxed);
14504                self.nll_end();
14505                return Err(format!(
14506                    "GPU graph failed during O(1) NLL serial scoring at position {pos}"
14507                ));
14508            }
14509            nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
14510            cnt += 1;
14511        }
14512        self.nll_end();
14513        Ok((nll, cnt))
14514    }
14515
14516    /// Teacher-forced calibration data (B1): for each position, whether the
14517    /// argmax equals the actual next token, and the top-1 softmax prob
14518    /// (top-1 probability) under EACH temperature in `temps` — all from ONE forward
14519    /// pass (argmax/correctness are temperature-invariant; only p_max
14520    /// reshapes). Feeds `cortiq calibrate` (reliability/ECE + temperature
14521    /// fit): is the model's confidence a true property, or does it need a
14522    /// measured scaling?
14523    pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
14524        self.clear_sequence_state();
14525        let n = ids.len().saturating_sub(1);
14526        let mut correct = Vec::with_capacity(n);
14527        let mut pmax = Vec::with_capacity(n);
14528        for pos in 0..n {
14529            let emb = self.embed_single(ids[pos]);
14530            let hidden = self.forward_layers(&emb, pos, None);
14531            let logits = if let Some(logits) = self.graph_logits.take() {
14532                logits
14533            } else {
14534                let normed = inference::rms_norm(
14535                    &hidden,
14536                    &self.weights.final_norm,
14537                    self.rms_eps,
14538                    self.norm_style,
14539                );
14540                // lm_head_forward applies the final-logit softcap itself —
14541                // capping again here double-squashed gemma-class logits
14542                // (tanh∘tanh) and reported a flattered ppl.
14543                self.lm_head_forward(&normed)
14544            };
14545            let target = ids[pos + 1] as usize;
14546            let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
14547            for (i, &v) in logits.iter().enumerate() {
14548                if v > mval {
14549                    mval = v;
14550                    amax = i;
14551                }
14552            }
14553            correct.push(amax == target);
14554            let row: Vec<f32> = temps
14555                .iter()
14556                .map(|&t| {
14557                    let tt = t.max(1e-3);
14558                    let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
14559                    1.0 / s.max(1e-12) // numerator at the max is exp(0)=1
14560                })
14561                .collect();
14562            pmax.push(row);
14563        }
14564        self.clear_sequence_state();
14565        (correct, pmax)
14566    }
14567
14568    /// Teacher-forced PPL with the dynamic router driving per-window
14569    /// skill switches (VMF experiment №2 measurement). Sequential (φ
14570    /// must update per token), returns (ppl, switch_count). The router
14571    /// must be enabled (`enable_dynamic_routing`); else this equals
14572    /// plain `ppl_ids`. The active skill when scoring token t shapes the
14573    /// logits for t+1 — on-policy over the held-out text itself.
14574    pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
14575        if self.dyn_router.is_none() {
14576            return Ok((self.ppl_ids(ids)?, 0));
14577        }
14578        self.nll_begin()?;
14579        let saved_active = self.dyn_active;
14580        let mut router = self
14581            .dyn_router
14582            .take()
14583            .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
14584        router.reset();
14585        self.dyn_phi_seen = 0;
14586        let _ = self.set_active_skill(None);
14587
14588        let result: Result<(f64, usize), String> = (|| {
14589            let mut nll = 0f64;
14590            let mut cnt = 0usize;
14591            for pos in 0..ids.len().saturating_sub(1) {
14592                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
14593                self.nll_check_graph("dynamic serial forward", pos)?;
14594                let out_of_band = self.graph_logits.take();
14595                let mut logits = match out_of_band {
14596                    Some(lg) => lg,
14597                    None => {
14598                        let normed = inference::rms_norm(
14599                            &hidden,
14600                            &self.weights.final_norm,
14601                            self.rms_eps,
14602                            self.norm_style,
14603                        );
14604                        // lm_head_forward applies the final-logit softcap itself —
14605                        // capping again here double-squashed gemma-class logits
14606                        // and reported a flattered ppl.
14607                        self.lm_head_forward(&normed)
14608                    }
14609                };
14610                let target = ids[pos + 1] as usize;
14611                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14612                let lse: f64 = logits
14613                    .iter()
14614                    .map(|&v| ((v - max) as f64).exp())
14615                    .sum::<f64>()
14616                    .ln()
14617                    + max as f64;
14618                let tok_nll = lse - logits[target] as f64;
14619                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
14620                    let top = logits
14621                        .iter()
14622                        .enumerate()
14623                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14624                        .map(|(i, _)| i)
14625                        .unwrap_or(0);
14626                    eprintln!(
14627                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
14628                        logits[target], logits[top]
14629                    );
14630                }
14631                nll += tok_nll;
14632                cnt += 1;
14633                attention::recycle_buf(&mut logits);
14634                // Route on the evolving phi (drives the NEXT token's skill).
14635                let phi = self.dyn_phi_ema.clone();
14636                if let Some(new_active) = router.step(&phi, pos) {
14637                    let _ = self.set_active_skill(new_active);
14638                }
14639            }
14640            Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
14641        })();
14642
14643        // Restore the detached router and the active overlay on both success
14644        // and failure. The scoring state is cleared independently below.
14645        let _ = self.set_active_skill(saved_active);
14646        self.dyn_router = Some(router);
14647        self.nll_end();
14648        result
14649    }
14650
14651    /// Routing probe φ (spec §9): mean-pooled hidden after `layer`.
14652    pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
14653        self.clear_sequence_state();
14654        let mut acc = vec![0f32; self.hidden_size];
14655        for (pos, &id) in ids.iter().enumerate() {
14656            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
14657            for (a, v) in acc.iter_mut().zip(&h) {
14658                *a += v;
14659            }
14660        }
14661        let n = ids.len().max(1) as f32;
14662        for a in acc.iter_mut() {
14663            *a /= n;
14664        }
14665        self.clear_sequence_state();
14666        acc
14667    }
14668
14669    /// Every position's hidden AFTER `layer` (n × hidden, not normalized): the
14670    /// walk of [`Self::probe_phi_span`] without the pooling, for `phi-dump`.
14671    pub fn probe_hidden_rows(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
14672        #[cfg(target_os = "macos")]
14673        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
14674        self.clear_sequence_state();
14675        let mut rows = Vec::with_capacity(ids.len() * self.hidden_size);
14676        for (pos, &id) in ids.iter().enumerate() {
14677            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
14678            rows.extend_from_slice(&h);
14679        }
14680        self.clear_sequence_state();
14681        rows
14682    }
14683
14684    /// Router-v2 φ probe (spec §9.4, `phi.pool = "span_mean"`): the hidden
14685    /// AFTER `layer` — the same per-position walk and the same quantity as
14686    /// [`Self::probe_phi`] — averaged over the positions in `span` only
14687    /// (the user text between the template's prefix and suffix ids), NOT
14688    /// unit-normalized (the decision normalizes). The walk stops at
14689    /// `span.end`: causality makes the later positions irrelevant, so the
14690    /// result is bit-identical to probing `ids[..span.end]`. An empty span
14691    /// gives the zero vector, which the decision treats as degenerate.
14692    ///
14693    /// Every sequence state is reset before and after — the host KV/ring/
14694    /// recurrent state, the reuse keys (`kv_history`, `kv_prefix`) and the
14695    /// device graph's sequence — so run it on a pipeline that does not
14696    /// also serve a conversation (its prefix reuse would be lost).
14697    pub fn probe_phi_span(
14698        &mut self,
14699        ids: &[u32],
14700        layer: usize,
14701        span: std::ops::Range<usize>,
14702    ) -> Vec<f32> {
14703        #[cfg(target_os = "macos")]
14704        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
14705        let end = span.end.min(ids.len());
14706        let start = span.start.min(end);
14707        let reset = |p: &mut Self| p.clear_sequence_state();
14708        reset(self);
14709        let mut acc = vec![0f32; self.hidden_size];
14710        for (pos, &id) in ids[..end].iter().enumerate() {
14711            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
14712            if pos >= start {
14713                for (a, v) in acc.iter_mut().zip(&h) {
14714                    *a += v;
14715                }
14716            }
14717        }
14718        let n = end - start;
14719        if n > 0 {
14720            let n = n as f32;
14721            for a in acc.iter_mut() {
14722                *a /= n;
14723            }
14724        }
14725        reset(self);
14726        acc
14727    }
14728
14729    /// One decode step of the current sequence: forward `token` at
14730    /// `position` (the cache holds positions `[0, position)`, e.g. after
14731    /// [`Self::forward_ids`]) and return the next-token logits — the same
14732    /// forward and head the generation loop runs (resident-graph logits
14733    /// when the graph ran, final norm + lm_head otherwise). The logit-dump
14734    /// tools drive greedy decoding with it so every position is observable.
14735    pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
14736        #[cfg(target_os = "macos")]
14737        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
14738        self.graph_logits = None;
14739        let hidden = self.forward_layers(&self.embed_single(token), position, None);
14740        if let Some(logits) = self.graph_logits.take() {
14741            return logits;
14742        }
14743        inference::rms_norm_into(
14744            &hidden,
14745            &self.weights.final_norm,
14746            self.rms_eps,
14747            self.norm_style,
14748            &mut self.ws.n1,
14749        );
14750        self.lm_head_forward(&self.ws.n1)
14751    }
14752
14753    /// Layer-major batched prefill (prefill-GEMM): full-attention —
14754    /// per-position with the existing operators (KV grows naturally,
14755    /// causality preserved), GDN projections / FFN / MoE — batched
14756    /// (a weight row is read from DRAM once per chunk, not per
14757    /// position). Returns the hidden of all positions [b × hidden].
14758    fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
14759        self.prefill_batch_masked(ids, start_pos, None)
14760    }
14761
14762    /// `prefill_batch` with a task mask honored on the dense-FFN panels
14763    /// (the masked-inference fast path: full fused compute, mask lands on
14764    /// the activations). The whole-chunk GPU graph is skipped for masked
14765    /// layers by the callers' arms; the per-GEMM device paths stay in
14766    /// play because the zeroing happens on the host between them.
14767    fn prefill_batch_masked(
14768        &mut self,
14769        ids: &[u32],
14770        start_pos: usize,
14771        task_mask: Option<&TaskMask>,
14772    ) -> Vec<f32> {
14773        self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
14774    }
14775
14776    /// One prompt chunk through the whole stack, post-stack rows out (no
14777    /// final norm) — the ingest generation uses, shared by scoring and
14778    /// `forward_ids` so they measure the same execution: the batched wgpu
14779    /// graph's device prefix plus the host's batched walk for the rest when
14780    /// `batch_prefix_prefill` holds and the graph admits the chunk, else
14781    /// the host's chunked prefill. Err only when a graph that had mutated
14782    /// device state failed.
14783    fn prefill_rows(
14784        &mut self,
14785        ids: &[u32],
14786        pos: usize,
14787        task_mask: Option<&TaskMask>,
14788    ) -> Result<Vec<f32>, String> {
14789        self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
14790    }
14791
14792    fn prefill_input_rows(
14793        &mut self,
14794        input: PrefillIn<'_>,
14795        pos: usize,
14796        task_mask: Option<&TaskMask>,
14797    ) -> Result<Vec<f32>, String> {
14798        if self.dash.is_some() {
14799            let PrefillIn::Hidden(rows) = input else {
14800                return Err("Unlimited-OCR prompts carry image rows".into());
14801            };
14802            return Ok(self.dash_prefill_rows(rows, pos));
14803        }
14804        self.mimo_moe_prepare();
14805        let hs = self.hidden_size;
14806        let bk = match input {
14807            PrefillIn::Ids(ids) => ids.len(),
14808            PrefillIn::Hidden(rows) => rows.len() / hs,
14809        };
14810        // MiniCPM-OCR's vision rows on native Metal: the rows graph, as an id
14811        // prompt's chunks take it in the generation loop.
14812        #[cfg(target_os = "macos")]
14813        if let PrefillIn::Hidden(rows) = input
14814            && task_mask.is_none()
14815            && self.onevision2_prefill()
14816            && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
14817            && crate::gpu::enabled_here()
14818            && self.gdn_cfg.is_some()
14819            && !self.per_token_stack()
14820            && bk > 8
14821            && bk <= 512
14822            && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
14823        {
14824            match self.prefill_hiddens_metal(rows.to_vec(), pos, None) {
14825                MetalPrefillOutcome::Completed(h) => return Ok(h),
14826                MetalPrefillOutcome::Failed => {
14827                    return Err("Metal prefill of the prompt rows failed after admission".into());
14828                }
14829                MetalPrefillOutcome::Declined => {}
14830            }
14831        }
14832        #[cfg(not(target_os = "macos"))]
14833        if task_mask.is_none()
14834            && !self.o1_active()
14835            && bk > 1
14836            && (self.batch_prefix_prefill()
14837                // Ripple: scoring reads its chunks through the batch graph
14838                // too (the host walk attends 80 heads a row on the CPU).
14839                || (self.value_blend.is_some() && self.g4_batch_prefill())
14840                // An embedded prompt (vision rows) on the graph route takes
14841                // the batched graph like an id prompt does.
14842                || (matches!(input, PrefillIn::Hidden(_)) && self.generation_batch_k() > 0)
14843                || (self.verify_exact_moe
14844                    && crate::gpu::enabled_here()
14845                    && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
14846        {
14847            let mut hiddens = match input {
14848                PrefillIn::Hidden(rows) => rows.to_vec(),
14849                PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
14850            };
14851            let positions: Vec<usize> = (pos..pos + bk).collect();
14852            let mut run = 0usize;
14853            match self.try_batch_graph_wgpu_prefix(
14854                &mut hiddens,
14855                &positions,
14856                bk,
14857                None,
14858                Some(&mut run),
14859            ) {
14860                crate::gpu::BatchGraphOutcome::Completed => {
14861                    let out = if run < self.num_layers {
14862                        self.prefill_batch_span(
14863                            PrefillIn::Hidden(&hiddens),
14864                            pos,
14865                            None,
14866                            run,
14867                            self.num_layers,
14868                        )
14869                    } else {
14870                        hiddens
14871                    };
14872                    return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
14873                        Err("MiMo attention graph failed after admission".into())
14874                    } else {
14875                        Ok(out)
14876                    };
14877                }
14878                crate::gpu::BatchGraphOutcome::Failed => {
14879                    return Err("batched prefix prefill failed after admission".into());
14880                }
14881                crate::gpu::BatchGraphOutcome::Declined => {
14882                    // Rows an earlier chunk left on the device only.
14883                    #[cfg(feature = "gpu")]
14884                    self.pull_lagging_host_kv(0, self.num_layers, pos);
14885                }
14886            }
14887        }
14888        let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
14889        if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
14890            Err("batch tail graph failed after admission".into())
14891        } else {
14892            Ok(out)
14893        }
14894    }
14895
14896    /// Unlimited-OCR's prompt rows, one position at a time: each query has its
14897    /// own coordinates and page candidates.
14898    fn dash_prefill_rows(&mut self, rows: &[f32], pos: usize) -> Vec<f32> {
14899        let hs = self.hidden_size;
14900        let n = rows.len() / hs;
14901        let mut last = vec![0f32; hs];
14902        let mut i = 0usize;
14903        while i < n && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
14904            #[cfg(not(target_os = "macos"))]
14905            if self.dash_batch_on {
14906                let k = (n - i).min(crate::gpu::DASH_CHUNK_MAX);
14907                if let Some(d) = self.dash.as_mut() {
14908                    d.at_prompt_chunk(pos + i, k);
14909                }
14910                let mut h = rows[i * hs..(i + k) * hs].to_vec();
14911                let positions: Vec<usize> = (pos + i..pos + i + k).collect();
14912                match self.try_batch_graph_wgpu_prefix(&mut h, &positions, k, None, None) {
14913                    crate::gpu::BatchGraphOutcome::Completed => {
14914                        last.copy_from_slice(&h[(k - 1) * hs..]);
14915                        i += k;
14916                        continue;
14917                    }
14918                    crate::gpu::BatchGraphOutcome::Failed => {
14919                        self.graph_failed.store(true, std::sync::atomic::Ordering::Relaxed);
14920                        return last;
14921                    }
14922                    crate::gpu::BatchGraphOutcome::Declined => self.dash_batch_on = false,
14923                }
14924            }
14925            #[cfg(target_os = "macos")]
14926            if self.dash_batch_on && n - i >= 8 && crate::gpu::backend_name() == "metal" {
14927                let k = (n - i).min(crate::gpu::DASH_CHUNK_MAX);
14928                if let Some(d) = self.dash.as_mut() {
14929                    d.at_prompt_chunk(pos + i, k);
14930                }
14931                let mut h = rows[i * hs..(i + k) * hs].to_vec();
14932                if self.chunk_run_gpu(0, &mut h, k, pos + i, None, self.num_layers) == self.num_layers {
14933                    last.copy_from_slice(&h[(k - 1) * hs..]);
14934                    i += k;
14935                    continue;
14936                }
14937                self.dash_batch_on = false;
14938            }
14939            if let Some(d) = self.dash.as_mut() {
14940                d.at_prompt(pos + i);
14941            }
14942            last = self.forward_layers(&rows[i * hs..(i + 1) * hs], pos + i, None);
14943            i += 1;
14944        }
14945        last
14946    }
14947
14948    /// The layer-major batched walk over a layer span [from..upto_excl):
14949    /// the whole prefill machinery (chunk graph, batched attends, GEMM
14950    /// panels) for a PARTIAL stack — the network split's prefill rides
14951    /// the same canon as the local one. Input is token ids (embeds
14952    /// itself, coordinator side) or ready boundary hiddens (worker side).
14953    fn prefill_batch_span(
14954        &mut self,
14955        input: PrefillIn<'_>,
14956        start_pos: usize,
14957        task_mask: Option<&TaskMask>,
14958        from: usize,
14959        upto_excl: usize,
14960    ) -> Vec<f32> {
14961        let hs = self.hidden_size;
14962        let b = match input {
14963            PrefillIn::Ids(ids) => ids.len(),
14964            PrefillIn::Hidden(hb) => hb.len() / hs,
14965        };
14966        let upto_excl = upto_excl.min(self.num_layers);
14967        // The CPU embed is deferred: when the chunk graph takes the run
14968        // from layer 0 it gathers the embeddings on the device instead.
14969        // A hidden input is ready by definition.
14970        let mut h: Vec<f32>;
14971        let mut h_ready;
14972        match input {
14973            PrefillIn::Ids(_) => {
14974                h = vec![0.0; b * hs];
14975                h_ready = false;
14976            }
14977            PrefillIn::Hidden(hb) => {
14978                h = hb.to_vec();
14979                h_ready = true;
14980            }
14981        }
14982        // Ripple's value residual reads the chunk's embedding rows in every layer.
14983        if self.value_blend.is_some() {
14984            self.vres_e0.clear();
14985            match input {
14986                PrefillIn::Ids(ids) => {
14987                    for &id in ids {
14988                        let e = self.embed_single(id);
14989                        self.vres_e0.extend_from_slice(&e);
14990                    }
14991                }
14992                PrefillIn::Hidden(hb) => {
14993                    assert!(from == 0, "value residual: a layer span past the first layer has no embedding rows");
14994                    self.vres_e0.extend_from_slice(hb);
14995                }
14996            }
14997        }
14998        let fill_h = |h: &mut Vec<f32>, me: &Self| {
14999            if let PrefillIn::Ids(ids) = input {
15000                for (bi, &id) in ids.iter().enumerate() {
15001                    let e = me.embed_single(id);
15002                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
15003                }
15004                if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
15005                    if let Ok(t) = tp.parse::<usize>() {
15006                        if t >= start_pos && t < start_pos + ids.len() {
15007                            let bi = t - start_pos;
15008                            let row = &h[bi * hs..(bi + 1) * hs];
15009                            let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
15010                            eprintln!(
15011                                "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
15012                                ids[bi],
15013                                row[0],
15014                                row[1],
15015                                ids.len(),
15016                                &ids[..ids.len().min(8)]
15017                            );
15018                        }
15019                    }
15020                }
15021            }
15022        };
15023        let (_nkv, _hd, _rd, eps) = (
15024            self.num_kv_heads,
15025            self.head_dim,
15026            self.rotary_dim,
15027            self.rms_eps,
15028        );
15029        let pool = self.pool.clone();
15030        let norm_style = self.norm_style;
15031        self.mimo_moe_prepare();
15032        let automatic_gpu_prefix = self.automatic_gpu_prefix();
15033
15034        #[cfg(target_os = "macos")]
15035        let mut chunk_skip_until = 0usize;
15036        // SenseNova-Loop: the chunk rows' mHC streams.
15037        let mut mhc_rs: Vec<f32> = Vec::new();
15038        for li in from..upto_excl {
15039            let _capacity_tail = automatic_gpu_prefix
15040                .filter(|&prefix| {
15041                    li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
15042                })
15043                .map(|_| crate::gpu::enter_cpu_scope());
15044            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU
15045            // Spark-X2.5: the layer's int8 matrix-unit GEMMs take the
15046            // `zi_mm` tile, bit-identical to the 64x64 kernel and about
15047            // three times its rate (`gpu_wgpu::zi_gemm_route`).
15048            #[cfg(feature = "gpu")]
15049            let _fast_gemm = self
15050                .proj_gate_sigmoid
15051                .then(crate::gpu::enter_prefill_fast_gemm);
15052            // GPU chunk graph (default-on under CMF_GPU=1): a run of
15053            // consecutive eligible layers for the whole chunk in ONE
15054            // Metal submission — norm, QKV, RoPE with fused mirror
15055            // append, causal attend, O, FFN, hidden device-resident
15056            // across the run. Any refusal falls through to the CPU path.
15057            #[cfg(target_os = "macos")]
15058            if task_mask.is_none() {
15059                if li < chunk_skip_until {
15060                    continue;
15061                }
15062                // Device-side embedding needs a q8_row embedding matrix;
15063                // with any other layout the CPU fills `h` first and the
15064                // graph starts from a ready hidden (refusing the whole
15065                // run over the embedding alone kept q4t models — the
15066                // whole Nanbeige/Bonsai class — on the CPU prefill).
15067                if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
15068                    fill_h(&mut h, self);
15069                    h_ready = true;
15070                }
15071                let ids_for_embed = match input {
15072                    PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
15073                    PrefillIn::Hidden(_) => None,
15074                };
15075                let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
15076                if end > li {
15077                    h_ready = true;
15078                    chunk_skip_until = end;
15079                    // Looped Transformer: the graph stopped at a loop
15080                    // boundary — apply final norm before the next iteration.
15081                    if self.is_loop_end(end - 1) && end < self.num_layers {
15082                        for bi in 0..b {
15083                            let normed = inference::rms_norm(
15084                                &h[bi * hs..(bi + 1) * hs],
15085                                &self.weights.final_norm,
15086                                eps,
15087                                norm_style,
15088                            );
15089                            h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
15090                        }
15091                    }
15092                    continue;
15093                }
15094            }
15095            if !h_ready {
15096                fill_h(&mut h, self);
15097                h_ready = true;
15098            }
15099            if task_mask.is_none() && self.verify_exact_moe {
15100                let positions: Vec<_> = (start_pos..start_pos + b).collect();
15101                match self.mimo_graph_layer_rows(li, &mut h, &positions) {
15102                    crate::gpu::BatchGraphOutcome::Completed => continue,
15103                    crate::gpu::BatchGraphOutcome::Failed => return h,
15104                    crate::gpu::BatchGraphOutcome::Declined => {}
15105                }
15106            }
15107            #[cfg(feature = "gpu")]
15108            self.pull_lagging_host_kv(li, li + 1, start_pos);
15109            // SenseNova-Loop mHC: every row's streams route into `h`; the
15110            // attention below adds onto that branch, so its output is the
15111            // difference from the copy kept here.
15112            let mhc_attn = self.mhc_layer(li).then(|| {
15113                let m = self.mhc.as_deref().expect("mhc layer");
15114                let p = self.phys_layer(li);
15115                let n = m.s.streams * hs;
15116                mhc_rs.resize(b * n, 0.0);
15117                let coefs: Vec<_> = (0..b)
15118                    .map(|bi| {
15119                        let r = &mut mhc_rs[bi * n..(bi + 1) * n];
15120                        if m.enters(p) {
15121                            m.enter(r, &h[bi * hs..(bi + 1) * hs]);
15122                        }
15123                        m.route(p, false, r, &mut h[bi * hs..(bi + 1) * hs])
15124                    })
15125                    .collect();
15126                (h.clone(), coefs)
15127            });
15128            let lw = &self.weights.layers[self.phys_layer(li)];
15129            let t_attn = std::time::Instant::now();
15130            // ── attention ──
15131            match &lw.attn {
15132                AttnKind::Kda(w) => {
15133                    // Projections batched, recurrence sequential.
15134                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
15135                    let mut normed = vec![0.0f32; b * hs];
15136                    for bi in 0..b {
15137                        inference::rms_norm_into(
15138                            &h[bi * hs..(bi + 1) * hs],
15139                            &lw.input_norm,
15140                            eps,
15141                            norm_style,
15142                            &mut normed[bi * hs..(bi + 1) * hs],
15143                        );
15144                    }
15145                    let attn = crate::linear_core::kda_forward_batch(
15146                        &normed,
15147                        b,
15148                        w,
15149                        &cfg,
15150                        &mut self.kv_cache.layers[li].linear_state,
15151                        pool.as_deref(),
15152                    );
15153                    for (dst, &a) in h.iter_mut().zip(&attn) {
15154                        *dst += a;
15155                    }
15156                }
15157                AttnKind::LinearGdn(w) => {
15158                    // Projections batched, recurrence sequential.
15159                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
15160                    let mut normed = vec![0.0f32; b * hs];
15161                    for bi in 0..b {
15162                        let r = inference::rms_norm(
15163                            &h[bi * hs..(bi + 1) * hs],
15164                            &lw.input_norm,
15165                            eps,
15166                            norm_style,
15167                        );
15168                        normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
15169                    }
15170                    let attn = crate::linear_core::gdn_forward_batch(
15171                        &normed,
15172                        b,
15173                        w,
15174                        &cfg,
15175                        &mut self.kv_cache.layers[li].linear_state,
15176                        pool.as_deref(),
15177                    );
15178                    for (dst, &a) in h.iter_mut().zip(&attn) {
15179                        *dst += a;
15180                    }
15181                }
15182                AttnKind::ShortConv(w) => {
15183                    // Projections batched over the chunk; the conv walks the
15184                    // contiguous positions in order (same ring as decode).
15185                    let cfg = self
15186                        .short_conv_cfg
15187                        .expect("short-conv layer without short_conv_cfg");
15188                    let mut normed = vec![0.0f32; b * hs];
15189                    for bi in 0..b {
15190                        inference::rms_norm_into(
15191                            &h[bi * hs..(bi + 1) * hs],
15192                            &lw.input_norm,
15193                            eps,
15194                            norm_style,
15195                            &mut normed[bi * hs..(bi + 1) * hs],
15196                        );
15197                    }
15198                    let mut attn = short_conv_forward_batch(
15199                        &normed,
15200                        b,
15201                        w,
15202                        &cfg,
15203                        &mut self.kv_cache.layers[li].linear_state,
15204                        pool.as_deref(),
15205                    );
15206                    // LFM3 sandwich: the mixer output is normalized before
15207                    // the residual, as on the decode path.
15208                    if let Some(wn) = &lw.attn_out_norm {
15209                        for bi in 0..b {
15210                            inference::rms_norm_into(
15211                                &attn[bi * hs..(bi + 1) * hs],
15212                                wn,
15213                                eps,
15214                                norm_style,
15215                                &mut normed[bi * hs..(bi + 1) * hs],
15216                            );
15217                        }
15218                        attn.copy_from_slice(&normed);
15219                    }
15220                    for (dst, &a) in h.iter_mut().zip(&attn) {
15221                        *dst += a;
15222                    }
15223                }
15224                AttnKind::Mla(w) => {
15225                    // Per-position prefill (correctness first; latent
15226                    // batching is a later optimization).
15227                    let inv_freq_l = self.layer_inv_freq(li);
15228                    let rs = self.layer_rope_scale(li);
15229                    let mut normed = vec![0.0f32; hs];
15230                    for bi in 0..b {
15231                        inference::rms_norm_into(
15232                            &h[bi * hs..(bi + 1) * hs],
15233                            &lw.input_norm,
15234                            eps,
15235                            norm_style,
15236                            &mut normed,
15237                        );
15238                        let ao = mla_attention(
15239                            w,
15240                            &normed,
15241                            &mut self.kv_cache.layers[li],
15242                            start_pos + bi,
15243                            &inv_freq_l,
15244                            rs,
15245                            eps,
15246                            pool.as_deref(),
15247                        );
15248                        for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
15249                            *dst += a;
15250                        }
15251                    }
15252                }
15253                AttnKind::Full {
15254                    wq,
15255                    wk,
15256                    wv,
15257                    wo,
15258                    q_norm,
15259                    k_norm,
15260                    output_gate,
15261                    softplus_gate,
15262                    bias,
15263                } => {
15264                    // Chunk-GEMM QKV/O; per-position causal attention
15265                    // inside (roadmap §3 P0 — full-attention prefill no
15266                    // longer re-reads the projection weights b times).
15267                    let mut normed = vec![0.0f32; b * hs];
15268                    for bi in 0..b {
15269                        inference::rms_norm_into(
15270                            &h[bi * hs..(bi + 1) * hs],
15271                            &lw.input_norm,
15272                            eps,
15273                            norm_style,
15274                            &mut normed[bi * hs..(bi + 1) * hs],
15275                        );
15276                    }
15277                    // The rows V projects, when the layer blends the embeddings into them.
15278                    let vres = match self.layer_value_blend(li) {
15279                        Some(blend) => {
15280                            vres_lerp(&self.vres_e0, &normed, blend, &mut self.vres_in);
15281                            true
15282                        }
15283                        None => false,
15284                    };
15285                    let inv_freq_l = self.layer_inv_freq(li);
15286                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
15287                    let cfg = QwenAttnCfg {
15288                        mrope: self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| attention::MropeView { base: 0, pos, sections }),
15289                        num_heads: self.layer_num_heads(li),
15290                        num_kv_heads: nkv_l,
15291                        head_dim: hd_l,
15292                        hidden_size: hs,
15293                        position: start_pos,
15294                        inv_freq: &inv_freq_l,
15295                        rotary_dim: rd_l,
15296                        scale: self.attn_scale,
15297                        softcap: self.attn_softcap,
15298                        window: self.layer_window(li),
15299                        v_norm: self.attn_v_norm,
15300                        qk_norm_after_rope: self.qk_norm_after_rope,
15301                        gate_sigmoid: self.proj_gate_sigmoid,
15302                        q_norm: q_norm.as_deref(),
15303                        k_norm: k_norm.as_deref(),
15304                        output_gate: *output_gate,
15305                        softplus_gate: softplus_gate
15306                            .as_ref()
15307                            .map(|(gate, per_head)| (gate, *per_head)),
15308                        rope_scale: self.layer_rope_scale(li),
15309                        bias: bias
15310                            .as_ref()
15311                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15312                        rms_eps: eps,
15313                        norm_style,
15314                        pool: pool.as_deref(),
15315                        v_head_dim: self.layer_v_dim(li),
15316                        value_in: vres.then(|| self.vres_in.as_slice()),
15317                    };
15318                    #[cfg(feature = "gpu")]
15319                    let _mirror = self
15320                        .prefill_mirror_target(li)
15321                        .map(crate::gpu::enter_prefill_mirror);
15322                    let mut attn = attention::qwen_attention_batch(
15323                        &normed,
15324                        b,
15325                        wq,
15326                        wk,
15327                        wv,
15328                        wo,
15329                        &mut self.kv_cache.layers[li],
15330                        &cfg,
15331                    );
15332                    if let Some(w) = &lw.attn_out_norm {
15333                        for bi in 0..b {
15334                            inference::rms_norm_into(
15335                                &attn[bi * hs..(bi + 1) * hs],
15336                                w,
15337                                eps,
15338                                norm_style,
15339                                &mut normed[bi * hs..(bi + 1) * hs],
15340                            );
15341                        }
15342                        attn.copy_from_slice(&normed);
15343                    }
15344                    for (dst, &a) in h.iter_mut().zip(&attn) {
15345                        *dst += a;
15346                    }
15347                }
15348                AttnKind::Bounded(w) => {
15349                    // Chunk-GEMM projections, the bounded operator per
15350                    // position over ring + chunk — never a growing KV.
15351                    let mut normed = vec![0.0f32; b * hs];
15352                    for bi in 0..b {
15353                        inference::rms_norm_into(
15354                            &h[bi * hs..(bi + 1) * hs],
15355                            &lw.input_norm,
15356                            eps,
15357                            norm_style,
15358                            &mut normed[bi * hs..(bi + 1) * hs],
15359                        );
15360                    }
15361                    let rope = self
15362                        .bounded_rope
15363                        .clone()
15364                        .expect("bounded layer without an installed rotation table");
15365                    let cfg = crate::bounded::BoundedAttnCfg {
15366                        num_heads: self.num_heads,
15367                        num_kv_heads: self.num_kv_heads,
15368                        head_dim: self.head_dim,
15369                        hidden_size: hs,
15370                        scale: self.attn_scale,
15371                        rope: &rope,
15372                        pool: pool.as_deref(),
15373                    };
15374                    let mut attn = crate::bounded::bounded_attention_batch(
15375                        &normed,
15376                        b,
15377                        w,
15378                        &mut self.kv_cache.layers[li],
15379                        &cfg,
15380                    );
15381                    if let Some(wn) = &lw.attn_out_norm {
15382                        for bi in 0..b {
15383                            inference::rms_norm_into(
15384                                &attn[bi * hs..(bi + 1) * hs],
15385                                wn,
15386                                eps,
15387                                norm_style,
15388                                &mut normed[bi * hs..(bi + 1) * hs],
15389                            );
15390                        }
15391                        attn.copy_from_slice(&normed);
15392                    }
15393                    for (dst, &a) in h.iter_mut().zip(&attn) {
15394                        *dst += a;
15395                    }
15396                    attention::recycle_buf(&mut attn);
15397                }
15398                AttnKind::Linear(w) => {
15399                    for bi in 0..b {
15400                        let normed = inference::rms_norm(
15401                            &h[bi * hs..(bi + 1) * hs],
15402                            &lw.input_norm,
15403                            eps,
15404                            norm_style,
15405                        );
15406                        vmf_phase_forward(
15407                            &normed,
15408                            w,
15409                            &self.vmf_cfg.expect("linear layer without vmf_cfg"),
15410                            &mut self.kv_cache.layers[li].linear_state,
15411                            pool.as_deref(),
15412                        )
15413                        .iter()
15414                        .enumerate()
15415                        .for_each(|(i, &a)| h[bi * hs + i] += a);
15416                    }
15417                }
15418            }
15419
15420            // mHC: fold the attention output into the streams, route the
15421            // FFN's branch.
15422            let mhc_ffn = mhc_attn.map(|(branch, coefs)| {
15423                let m = self.mhc.as_deref().expect("mhc layer");
15424                let p = self.phys_layer(li);
15425                let n = m.s.streams * hs;
15426                let mut y = vec![0f32; hs];
15427                (0..b)
15428                    .map(|bi| {
15429                        for (e, v) in y.iter_mut().enumerate() {
15430                            *v = h[bi * hs + e] - branch[bi * hs + e];
15431                        }
15432                        let r = &mut mhc_rs[bi * n..(bi + 1) * n];
15433                        m.merge(r, &y, &coefs[bi]);
15434                        m.route(p, true, r, &mut h[bi * hs..(bi + 1) * hs])
15435                    })
15436                    .collect::<Vec<_>>()
15437            });
15438            // ── FFN batched ──
15439            let lw = &self.weights.layers[self.phys_layer(li)];
15440            let mut post = vec![0.0f32; b * hs];
15441            for bi in 0..b {
15442                let r =
15443                    inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
15444                post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
15445            }
15446            // A restrictive per-visit FFN row lands on the activations
15447            // inside the dense arm; an all-open row costs nothing.
15448            let mask_row = task_mask
15449                .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
15450                .and_then(|m| m.ffn_masks.get(li))
15451                .map(|v| v.as_slice());
15452            let attn_ns = t_attn.elapsed().as_nanos() as u64;
15453            let t_ffn = std::time::Instant::now();
15454            let mut ffn = match &lw.ffn {
15455                FfnKind::Dense(d) if !d.segs.is_empty() => {
15456                    tube_ffn(d, &post, b, pool.as_deref(), mask_row)
15457                }
15458                FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
15459                FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
15460                    moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
15461                }
15462                FfnKind::Moe(m) if self.verify_exact_moe => {
15463                    moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
15464                }
15465                // Keep prompt expert panels off the projection arena and
15466                // use their routes to prime the model-wide bank.
15467                FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
15468                    let before = m.stats.borrow().clone();
15469                    let out = crate::gpu::cpu_scope(|| {
15470                        moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
15471                    });
15472                    self.mimo_moe.prime(li, m, &before);
15473                    out
15474                }
15475                FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
15476                // Dual-branch layers run per position (the expert branch
15477                // reads the raw residual — nothing to batch yet).
15478                FfnKind::DenseMoe(dm) => {
15479                    let mut out = vec![0.0f32; b * hs];
15480                    for bi in 0..b {
15481                        let r = dense_moe_ffn(
15482                            dm,
15483                            &post[bi * hs..(bi + 1) * hs],
15484                            &h[bi * hs..(bi + 1) * hs],
15485                            eps,
15486                            norm_style,
15487                            pool.as_deref(),
15488                        );
15489                        out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
15490                    }
15491                    out
15492                }
15493            };
15494            if prefill_prof_on() {
15495                PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
15496                PREFILL_SPLIT[1].fetch_add(
15497                    t_ffn.elapsed().as_nanos() as u64,
15498                    std::sync::atomic::Ordering::Relaxed,
15499                );
15500            }
15501            if let Some(w) = &lw.ffn_out_norm {
15502                for bi in 0..b {
15503                    inference::rms_norm_into(
15504                        &ffn[bi * hs..(bi + 1) * hs],
15505                        w,
15506                        eps,
15507                        norm_style,
15508                        &mut post[bi * hs..(bi + 1) * hs],
15509                    );
15510                }
15511                ffn.copy_from_slice(&post);
15512            }
15513            if let Some(coefs) = mhc_ffn {
15514                // mHC: fold the FFN output in; the range's last layer hands
15515                // the streams' mean on as the hidden.
15516                let m = self.mhc.as_deref().expect("mhc layer");
15517                let p = self.phys_layer(li);
15518                let n = m.s.streams * hs;
15519                for bi in 0..b {
15520                    let r = &mut mhc_rs[bi * n..(bi + 1) * n];
15521                    m.merge(r, &ffn[bi * hs..(bi + 1) * hs], &coefs[bi]);
15522                    if m.leaves(p) {
15523                        m.leave(r, &mut h[bi * hs..(bi + 1) * hs]);
15524                    }
15525                }
15526            } else {
15527                for (dst, &f) in h.iter_mut().zip(&ffn) {
15528                    *dst += f;
15529                }
15530            }
15531            if let Some(sc) = lw.layer_scale {
15532                for v in h.iter_mut() {
15533                    *v *= sc;
15534                }
15535            }
15536            // CMF_LAYER_DUMP: every position's hidden after layer li.
15537            if self.layer_dump.is_some() {
15538                for bi in 0..b {
15539                    self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
15540                }
15541            }
15542            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
15543                if let Ok(t) = tp.parse::<usize>() {
15544                    if t >= start_pos && t < start_pos + b {
15545                        let bi = t - start_pos;
15546                        let row = &h[bi * hs..(bi + 1) * hs];
15547                        let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
15548                        eprintln!(
15549                            "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
15550                            row[0], row[1]
15551                        );
15552                    }
15553                }
15554            }
15555            // CMF_DEBUG_LAYERS=1: per-layer hidden-state health of the
15556            // LAST prompt position — the knife for "which layer type
15557            // breaks first" on a new architecture.
15558            if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
15559                let row = &h[(b - 1) * hs..b * hs];
15560                let rms =
15561                    (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
15562                let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
15563                eprintln!(
15564                    "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
15565                    match &self.weights.layers[self.phys_layer(li)].attn {
15566                        AttnKind::LinearGdn(_) => "gdn",
15567                        AttnKind::Linear(_) => "vmf",
15568                        AttnKind::ShortConv(_) => "conv",
15569                        _ => "attn",
15570                    },
15571                    match &lw.ffn {
15572                        FfnKind::Moe(_) => "moe",
15573                        FfnKind::Dense(_) => "dense",
15574                        FfnKind::DenseMoe(_) => "dense+moe",
15575                    },
15576                );
15577            }
15578            // Looped Transformer: apply final norm at the end of each loop iteration.
15579            if self.is_loop_end(li) && li + 1 < self.num_layers {
15580                for bi in 0..b {
15581                    let normed = inference::rms_norm(
15582                        &h[bi * hs..(bi + 1) * hs],
15583                        &self.weights.final_norm,
15584                        eps,
15585                        norm_style,
15586                    );
15587                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
15588                }
15589            }
15590            if std::env::var("CMF_TRACE_H").is_ok() {
15591                let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
15592                let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
15593                eprintln!(
15594                    "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
15595                    lw.layer_scale
15596                );
15597            }
15598        }
15599        crate::gpu::set_layer(-1); // lm_head/final ops outside layer-split
15600        if prefill_prof_on() {
15601            let ms = |a: &std::sync::atomic::AtomicU64| {
15602                a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
15603            };
15604            let sp = &attention::ATTN_SPLIT;
15605            let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
15606            eprintln!(
15607                "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
15608                 host gate {:.1}], host loop {:.1}, attend {:.1} \
15609                 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
15610                 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
15611                ms(&PREFILL_SPLIT[0]),
15612                ms(&sp[0]),
15613                qkv_enq,
15614                qkv_wait,
15615                ms(&sp[6]),
15616                ms(&sp[1]),
15617                ms(&sp[2]),
15618                ms(&sp[4]),
15619                ms(&sp[5]),
15620                ca_wait,
15621                ms(&sp[3]),
15622                ms(&PREFILL_SPLIT[1]),
15623            );
15624            let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
15625            eprintln!(
15626                "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
15627                 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
15628                 fallbacks (cumulative)",
15629                attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
15630            );
15631        }
15632        // A batched span owns a complete set of positions. Publish any
15633        // collecting→sealed transition only after every layer has finished;
15634        // callers that cross into serial/device work must see the new epoch
15635        // before this function returns.
15636        self.o1_progress();
15637        // Every layer of the chunk has appended and attended its rows.
15638        self.swa_trim_tails();
15639        h
15640    }
15641
15642    /// Embed a single token.
15643    fn embed_single(&self, id: u32) -> Vec<f32> {
15644        let mut out = vec![0.0f32; self.hidden_size];
15645        if (id as usize) < self.weights.embed_tokens.rows() {
15646            self.weights.embed_tokens.row_f32(id as usize, &mut out);
15647        }
15648        if self.embed_multiplier != 1.0 {
15649            for v in out.iter_mut() {
15650                *v *= self.embed_multiplier;
15651            }
15652        }
15653        // DeepSeek-V4's hash layers route by TOKEN ID, so the id has to
15654        // reach the forward. It rides in slot 0 (the forward re-reads the
15655        // real embedding itself from the table).
15656        if self.dsv4.is_some() || self.dsv41.is_some() || self.dory.is_some() || self.qwen4_exp.is_some() {
15657            let mut v = vec![0.0f32; self.hidden_size.max(1)];
15658            v[0] = id as f32;
15659            return v;
15660        }
15661        // Gemma-3n: the per-layer-embedding half needs the token ID, so
15662        // it rides appended to the embedding; the g3n forward splits it.
15663        // Gemma-4 E-series PLE: same contract, its own table.
15664        if let Some(b) = &self.g4e {
15665            return b.0.extend_embedding(id, &out, self.pool.as_deref());
15666        }
15667        if let Some(b) = &self.g3n {
15668            return b.0.extend_embedding(id, &out, self.pool.as_deref());
15669        }
15670        out
15671    }
15672
15673    /// A run of consecutive prefill layers on the GPU for the whole
15674    /// chunk (default-on under CMF_GPU=1; CMF_GPU_CHUNK=0 disables).
15675    /// Eligibility per layer: q8_row weights, plain full attention
15676    /// (no output gate), F32 KV, no o1/masks/gemma extras. Returns the
15677    /// first layer index NOT processed (== `li0` when the run is empty).
15678    #[cfg(target_os = "macos")]
15679    fn chunk_run_gpu(
15680        &mut self,
15681        li0: usize,
15682        h: &mut [f32],
15683        b: usize,
15684        pos0: usize,
15685        embed_ids: Option<&[u32]>,
15686        cap: usize,
15687    ) -> usize {
15688        // (The old streaming attend needed a depth bound at ~1k; the
15689        // GEMM attention scales like the CPU path and lifted it.)
15690        // CMF_GPU_CHUNK=0 disables the graph.
15691        // Gemma-4 (`metal_gemma4`) brings its own kernels for the global
15692        // layers, V norm, attention scale and sandwich (`ChunkGemma4`).
15693        // `CMF_METAL_GEMMA4_CHUNK=0` keeps its prompt on the host walk.
15694        let gemma4 =
15695            self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
15696        if !crate::gpu::enabled_here()
15697            || std::env::var("CMF_GPU_CHUNK")
15698                .map(|v| v == "0")
15699                .unwrap_or(false)
15700            || (b < self.chunk_min_rows && self.dash.is_none() && !self.o1_chunk_prefix)
15701            // MRoPE rows rotate on the host (`attention::MropeView`).
15702            || self.prompt_mrope.is_some()
15703            // V with rows of its own (Ripple's value residual) stays on the host walk.
15704            || self.value_blend.is_some()
15705            // Sliding windows with per-layer RoPE ride the chunk graph
15706            // (`metal_graph_swa`: causal_softmax_win, per-layer tables).
15707            || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
15708            || (self.global_attn.is_some() && !gemma4)
15709            // Chunk attention has no value-less learned-sink column. q1
15710            // decode can carry it through ordinary `gqa_attend`, but prompt
15711            // chunks must stay on the exact host attention path.
15712            || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
15713            // Per-layer KV heads and narrow V remain outside this chunk
15714            // graph (the q1 sliding-native door checks the same geometry).
15715            || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
15716            // Collection owns the exact Q trace and boundary conversion;
15717            // this chunk graph appends dense KV without feeding that trace,
15718            // but for a far field's exact prefix, where it keeps the queries.
15719            || (self.o1_active() && !self.o1_chunk_prefix)
15720            || (self.attn_v_norm && !gemma4)
15721            || !self.metal_rope_scale_ok()
15722            || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
15723        {
15724            return li0;
15725        }
15726        let Some(model) = self.model.clone() else {
15727            return li0;
15728        };
15729        let (nh, nkv, hd, hs) = (
15730            self.num_heads,
15731            self.num_kv_heads,
15732            self.head_dim,
15733            self.hidden_size,
15734        );
15735        // Collect the longest run of consecutive eligible layers.
15736        // Looped Transformer: stop at the loop boundary so the CPU can
15737        // apply loop_final_norm between iterations.
15738        let loop_end = if self.loop_final_norm {
15739            ((li0 / self.physical_layers) + 1) * self.physical_layers
15740        } else {
15741            self.num_layers
15742        };
15743        let run_end = self.num_layers.min(loop_end).min(cap);
15744        // Each layer's own RoPE table (Spark-X2.5: sliding and full layers
15745        // differ); the global one for every model without sliding layers.
15746        // Declared before `layers`, which borrows it (and drops a routing
15747        // closure on MoE layers).
15748        let tables: Vec<std::sync::Arc<Vec<f32>>> =
15749            (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
15750        let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
15751        let mut stored_at: Vec<usize> = Vec::new();
15752        for li in li0..run_end {
15753            if self.mhc_layer(li) {
15754                break;
15755            }
15756            let lw = &self.weights.layers[self.phys_layer(li)];
15757            if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
15758                && !gemma4
15759            {
15760                break;
15761            }
15762            let AttnKind::Full {
15763                wq,
15764                wk,
15765                wv,
15766                wo,
15767                q_norm,
15768                k_norm,
15769                output_gate: false,
15770                softplus_gate,
15771                bias,
15772            } = &lw.attn
15773            else {
15774                break;
15775            };
15776            // Spark-X2.5's per-head sigmoid g_proj gate, from f32 rows.
15777            let head_gate = match softplus_gate {
15778                None => None,
15779                Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
15780                    Some((d, r, c)) if r == nh && c == hs => Some(d),
15781                    _ => break,
15782                },
15783                Some(_) => break,
15784            };
15785            // The FFN: a dense SiLU/GELU trio, or routed q4tp experts
15786            // (Mellum2.1: softmax router, no shared expert) whose first
15787            // trio stands in for the shape checks.
15788            let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
15789                FfnKind::Dense(d)
15790                    if (matches!(d.act, Act::Silu | Act::Gelu)
15791                        || (gemma4 && d.act == Act::GeluTanh))
15792                        && d.segs.is_empty() =>
15793                {
15794                    (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
15795                }
15796                FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
15797                    Some((router, experts, q8)) => {
15798                        let e0 = &m.experts[0];
15799                        let route = Box::new(move |lg: &[f32]| {
15800                            // `moe_ffn_batch`'s routing and weights, row by row.
15801                            let (idx, p, wsum) = moe_route(lg, m, None);
15802                            let mut st = m.stats.borrow_mut();
15803                            if st.len() < m.experts.len() {
15804                                st.resize(m.experts.len(), 0);
15805                            }
15806                            for &e in &idx {
15807                                st[e] += 1;
15808                            }
15809                            let w = idx.iter().map(|&e| p[e] / wsum).collect();
15810                            (idx, w)
15811                        });
15812                        let detached = m.shared.as_ref().map(|(s, _)| {
15813                            let t = |x: &QTensor| {
15814                                x.q8_2f_parts().map(|p| p.0).or_else(|| x.q4tp_parts().map(|p| p.0)).unwrap_or(usize::MAX)
15815                            };
15816                            ([t(&s.gate_proj), t(&s.up_proj), t(&s.down_proj)], s.gate_proj.rows())
15817                        });
15818                        (
15819                            &e0.gate_proj,
15820                            &e0.up_proj,
15821                            &e0.down_proj,
15822                            Act::Silu,
15823                            Some(crate::gpu_metal::ChunkMoe {
15824                                router,
15825                                experts,
15826                                q8,
15827                                route,
15828                                detached,
15829                            }),
15830                        )
15831                    }
15832                    None => break,
15833                },
15834                _ => break,
15835            };
15836            // q8_row (row_scale populated), q8_2f (plus its input field),
15837            // or q4_tiled / q4tp (row_scale empty — their scales are in the
15838            // payload). Mixing across the seven projections of one layer is
15839            // fine; the encoder branches per weight on the tensor's dtype.
15840            // Anything else refuses.
15841            fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
15842                t.q8_row_parts()
15843                    .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
15844                    .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
15845                    .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
15846            }
15847            fn cf(t: &QTensor) -> &[f32] {
15848                t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
15849            }
15850            let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
15851            let ffn_col = [cf(gate_t), cf(up_t), cf(down_t)];
15852            // q8_2f projections ride the chunk graph on the MoE layers it
15853            // was measured on (Mellum2.1) and on the dense exact-GELU FFN
15854            // (Spark-X2.6-4B q8_2f on the M4: the gate/up inputs staged as
15855            // x·col, the down field applied after the activation —
15856            // `ChunkLayer::ffn_col`). A dense SiLU q8_2f stack keeps the host
15857            // walk: its down GEMM fuses the activation into the X-tile load
15858            // and has no input field (0.8.14 refused every q8_2f weight here).
15859            // Gemma-4 reads every q8_2f weight through the blob GEMM with its
15860            // own field (`ChunkGemma4`), so its dense q8_2f stack rides.
15861            if moe.is_none()
15862                && !gemma4
15863                && self.dash.is_none()
15864                && ffn_act != Act::Gelu
15865                && (col.iter().any(|c| !c.is_empty()) || ffn_col.iter().any(|c| !c.is_empty()))
15866            {
15867                break;
15868            }
15869            let parts = (
15870                cw(wq),
15871                cw(wk),
15872                cw(wv),
15873                cw(wo),
15874                cw(gate_t),
15875                cw(up_t),
15876                cw(down_t),
15877            );
15878            let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
15879            else {
15880                break;
15881            };
15882            let layer = &self.kv_cache.layers[li];
15883            if layer.mode != crate::kv_cache::KvMode::F32
15884                || (layer.o1.is_some() && !(self.o1_chunk_prefix && layer.o1_is_collecting()))
15885            {
15886                break;
15887            }
15888            // A trimmed tail holds every row the chunk's first query reads.
15889            debug_assert!(
15890                layer.base() == 0
15891                    || self
15892                        .layer_window(li)
15893                        .is_some_and(|w| layer.head_len(0) + 1 >= w)
15894            );
15895            stored_at.push(layer.head_len(0));
15896            // Per-layer geometry for Gemma-4 (512-wide global layers over
15897            // one KV head); every other model keeps the pipeline-wide one.
15898            let (nkv, hd) = if gemma4 {
15899                let (a, bb, _) = self.layer_geom(li);
15900                (a, bb)
15901            } else {
15902                (nkv, hd)
15903            };
15904            layers.push(crate::gpu_metal::ChunkLayer {
15905                model: &model,
15906                kv_id: self.graph_kv_id,
15907                layer: li,
15908                wq: pq,
15909                wk: pk,
15910                wv: pv,
15911                wo: po,
15912                gate: pg,
15913                up: pu,
15914                down: pd,
15915                input_norm: &lw.input_norm,
15916                post_norm: &lw.post_norm,
15917                bias: bias
15918                    .as_ref()
15919                    .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
15920                q_norm: q_norm.as_deref(),
15921                k_norm: k_norm.as_deref(),
15922                inv_freq: &tables[li - li0],
15923                rope_scale: self.layer_rope_scale(li),
15924                rd: self.layer_geom(li).2,
15925                nh,
15926                nkv,
15927                hd,
15928                hs,
15929                inter: gate_t.rows(),
15930                gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
15931                late_qk_norm: self.qk_norm_after_rope,
15932                eps: self.rms_eps as f32,
15933                window: self.layer_window(li),
15934                head_gate,
15935                act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
15936                g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
15937                    v_norm: self.attn_v_norm,
15938                    scale: self.attn_scale,
15939                    attn_out_norm: lw.attn_out_norm.as_deref(),
15940                    ffn_out_norm: lw.ffn_out_norm.as_deref(),
15941                    layer_scale: lw.layer_scale,
15942                }),
15943                col,
15944                // The plain dense branch alone reads it: routed experts and
15945                // Gemma-4 carry their own fields.
15946                ffn_col: if moe.is_none() && !gemma4 {
15947                    ffn_col
15948                } else {
15949                    [&[][..]; 3]
15950                },
15951                moe,
15952                dash: self.graph_dash(li),
15953            });
15954        }
15955        if layers.is_empty() {
15956            return li0;
15957        }
15958        // Unlimited-OCR: all of it or none of it (the router state).
15959        if self.dash.is_some() && (li0 != 0 || layers.len() != self.num_layers) {
15960            return li0;
15961        }
15962        // KV row width per layer (uniform but for Gemma-4).
15963        let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
15964        let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
15965            .iter()
15966            .zip(&rows_kv)
15967            .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
15968            .collect();
15969        let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
15970        for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
15971            let li = layers[i].layer;
15972            let layer = &self.kv_cache.layers[li];
15973            let nkv = rows_kv[i].0;
15974            io.push(crate::gpu_metal::ChunkIo {
15975                cpu_stored: stored_at[i],
15976                cpu_gen: layer.generation(),
15977                cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
15978                cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
15979                out_k: ok,
15980                out_v: ov,
15981                imp: oi,
15982            });
15983        }
15984        let n_run = layers.len();
15985        let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
15986        // Device-side embedding when the run starts the model and the
15987        // embedding matrix is q8_row-mapped.
15988        let ep = embed_ids.and_then(|ids| {
15989            self.weights
15990                .embed_tokens
15991                .q8_row_parts()
15992                .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
15993                    idx,
15994                    rows,
15995                    row_scale: rs,
15996                    ids,
15997                    mult: self.embed_multiplier,
15998                })
15999        });
16000        if embed_ids.is_some() && ep.is_none() {
16001            return li0;
16002        }
16003        if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
16004            return li0;
16005        }
16006        drop(io);
16007        drop(layers);
16008        // CPU caches stay the owners of record: append the chunk rows
16009        // and bank the importance masses per layer.
16010        for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
16011            let li = li0 + i;
16012            let row = rows_kv[i].1;
16013            let layer = &mut self.kv_cache.layers[li];
16014            for bi in 0..b {
16015                layer.append(
16016                    &ok[bi * row..(bi + 1) * row],
16017                    &ov[bi * row..(bi + 1) * row],
16018                    &[],
16019                );
16020            }
16021            layer.accumulate_imp(oi);
16022        }
16023        last
16024    }
16025
16026    /// Is layer `li` a sliding-window (local-RoPE) layer? Gemma-3:
16027    /// every `pattern`-th layer is global, the rest are local.
16028    fn layer_is_local(&self, li: usize) -> bool {
16029        if let Some(layers) = &self.sliding_layers {
16030            return layers.get(li).copied().unwrap_or(false);
16031        }
16032        match self.swa {
16033            Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
16034            None => false,
16035        }
16036    }
16037
16038    /// The RoPE table for layer `li` (local layers may have their own;
16039    /// Gemma-4 global layers use the proportional padded table).
16040    fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
16041        if self.layer_is_local(li) {
16042            if let Some(f) = &self.inv_freq_local {
16043                return f.clone();
16044            }
16045        } else if let Some(f) = &self.inv_freq_global {
16046            return f.clone();
16047        }
16048        self.inv_freq.clone()
16049    }
16050
16051    /// Layer `li`'s value-residual blend (None = V reads the normed input).
16052    fn layer_value_blend(&self, li: usize) -> Option<f32> {
16053        self.value_blend
16054            .as_ref()
16055            .and_then(|v| v.get(self.phys_layer(li)).copied())
16056    }
16057
16058    /// The attend window for layer `li` (None = full context).
16059    fn layer_window(&self, li: usize) -> Option<usize> {
16060        self.swa
16061            .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
16062    }
16063
16064    /// Drop every sliding layer's rows its window can no longer read
16065    /// (`LayerKvCache::trim_window`). Call only at a safe point — after a
16066    /// complete position / pair / chunk walk, never between a layer's
16067    /// appends and its attend. One comparison per layer below the trigger.
16068    ///
16069    /// Off with an MTP head: its verify oracles and rollbacks snapshot and
16070    /// compare per-layer row counts as positions (`CMF_METAL_VERIFY_CHECK`,
16071    /// the MTP caches), which a trimmed tail would break.
16072    fn swa_trim_tails(&mut self) {
16073        let Some((slack, align)) = self.swa_trim else {
16074            return;
16075        };
16076        if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
16077            return;
16078        }
16079        for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
16080            if let Some(w) = self.layer_window(li) {
16081                self.kv_cache.layers[li].trim_window_anchored(w, slack, align, self.swa_trim_anchor);
16082            }
16083        }
16084    }
16085
16086    fn layer_num_heads(&self, li: usize) -> usize {
16087        self.attention_heads_per_layer
16088            .as_ref()
16089            .and_then(|v| v.get(li).copied())
16090            .unwrap_or(self.num_heads)
16091    }
16092
16093    fn layer_rope_scale(&self, li: usize) -> f32 {
16094        if self.layer_is_local(li) {
16095            self.rope_scale_local
16096        } else {
16097            self.rope_scale
16098        }
16099    }
16100
16101    /// Attention geometry of layer `li`: (num_kv_heads, head_dim,
16102    /// rotary_dim). Gemma-4 global layers override all three.
16103    fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
16104        if !self.layer_is_local(li) {
16105            if let Some((ghd, gkv)) = self.global_attn {
16106                return (gkv, ghd, ghd);
16107            }
16108        }
16109        let rd = if self.layer_is_local(li) {
16110            self.rotary_dim_local.unwrap_or(self.rotary_dim)
16111        } else {
16112            self.rotary_dim
16113        };
16114        // A layer without positional encoding rotates nothing.
16115        let rd = if self
16116            .no_rope_layers
16117            .as_ref()
16118            .and_then(|v| v.get(li))
16119            .copied()
16120            == Some(true)
16121        {
16122            0
16123        } else {
16124            rd
16125        };
16126        (self.layer_num_kv_heads(li), self.head_dim, rd)
16127    }
16128
16129    /// KV heads of layer `li` (virtual index): the per-layer count when the
16130    /// model has one (MiMo-V2), else the uniform `num_kv_heads`.
16131    fn layer_num_kv_heads(&self, li: usize) -> usize {
16132        self.kv_heads_per_layer
16133            .as_ref()
16134            .and_then(|v| v.get(self.phys_layer(li)).copied())
16135            .unwrap_or(self.num_kv_heads)
16136    }
16137
16138    /// V head width of layer `li` (≤ its head_dim).
16139    fn layer_v_dim(&self, li: usize) -> usize {
16140        let (_, hd, _) = self.layer_geom(li);
16141        self.v_head_dim.unwrap_or(hd).min(hd)
16142    }
16143
16144    /// Install a per-layer KV geometry: KV heads per PHYSICAL layer and/or
16145    /// a V head width narrower than `head_dim` (MiMo-V2). Validates it and
16146    /// reshapes the caches of every layer whose KV head count differs from
16147    /// `num_kv_heads`. The loader and the tests share this one path, so a
16148    /// hand-built pipeline cannot hold a geometry the loader would refuse.
16149    /// Call before the first forward (it drops cached rows of reshaped
16150    /// layers). Refuses combinations whose paths would read it wrong:
16151    /// Gemma-4 global layers and MLA carry their own geometry.
16152    pub fn set_attn_geometry(
16153        &mut self,
16154        kv_heads_per_layer: Option<Vec<usize>>,
16155        v_head_dim: Option<usize>,
16156    ) -> Result<(), String> {
16157        if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
16158            if self.global_attn.is_some() {
16159                return Err(
16160                    "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
16161                     attention geometry"
16162                        .into(),
16163                );
16164            }
16165            if self
16166                .weights
16167                .layers
16168                .iter()
16169                .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
16170            {
16171                return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
16172            }
16173        }
16174        if let Some(vd) = v_head_dim {
16175            if vd == 0 || vd > self.head_dim {
16176                return Err(format!(
16177                    "v_head_dim {vd} must be in 1..={} (head_dim)",
16178                    self.head_dim
16179                ));
16180            }
16181        }
16182        if let Some(v) = &kv_heads_per_layer {
16183            if v.len() != self.physical_layers {
16184                return Err(format!(
16185                    "kv_heads_per_layer has {} entries, expected {} layers",
16186                    v.len(),
16187                    self.physical_layers
16188                ));
16189            }
16190            for (li, &nkv) in v.iter().enumerate() {
16191                let is_attn = matches!(
16192                    self.weights.layers.get(li).map(|lw| &lw.attn),
16193                    Some(AttnKind::Full { .. }) | None
16194                );
16195                if !is_attn {
16196                    continue;
16197                }
16198                let nh = self
16199                    .attention_heads_per_layer
16200                    .as_ref()
16201                    .and_then(|h| h.get(li).copied())
16202                    .unwrap_or(self.num_heads);
16203                if nkv == 0 || nh % nkv != 0 {
16204                    return Err(format!(
16205                        "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
16206                    ));
16207                }
16208            }
16209        }
16210        self.kv_heads_per_layer = kv_heads_per_layer;
16211        self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
16212        if self.kv_heads_per_layer.is_some() {
16213            for li in 0..self.kv_cache.layers.len() {
16214                let full = matches!(
16215                    self.weights
16216                        .layers
16217                        .get(self.phys_layer(li))
16218                        .map(|lw| &lw.attn),
16219                    Some(AttnKind::Full { .. })
16220                );
16221                let nkv = self.layer_num_kv_heads(li);
16222                let cache = &self.kv_cache.layers[li];
16223                if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
16224                    let sinks = cache.sinks.clone();
16225                    self.kv_cache.layers[li] =
16226                        crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
16227                    self.kv_cache.layers[li].sinks = sinks;
16228                }
16229            }
16230        }
16231        Ok(())
16232    }
16233
16234    /// Attach learned attention-sink logits (one per Q head) to PHYSICAL
16235    /// layer `phys` — every virtual layer that runs it. The loader calls
16236    /// this for each `model.layers.N.self_attn.sinks` tensor.
16237    pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
16238        let Some(lw) = self.weights.layers.get(phys) else {
16239            return Err(format!("sinks for layer {phys}: no such layer"));
16240        };
16241        if !matches!(lw.attn, AttnKind::Full { .. }) {
16242            return Err(format!(
16243                "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
16244            ));
16245        }
16246        let nh = self
16247            .attention_heads_per_layer
16248            .as_ref()
16249            .and_then(|h| h.get(phys).copied())
16250            .unwrap_or(self.num_heads);
16251        if sinks.len() != nh {
16252            return Err(format!(
16253                "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
16254                sinks.len()
16255            ));
16256        }
16257        if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
16258            return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
16259        }
16260        for li in 0..self.kv_cache.layers.len() {
16261            if self.phys_layer(li) == phys {
16262                self.kv_cache.layers[li].sinks = Some(sinks.clone());
16263            }
16264        }
16265        Ok(())
16266    }
16267
16268    /// Why the GPU attention graphs cannot serve this model, if they
16269    /// cannot: the wgpu whole-token and batched graphs, the greedy
16270    /// multi-burst, the q1 attention dropin and the Metal block/chunk/rows
16271    /// graphs all assume ONE (num_kv_heads, head_dim) geometry, V heads as
16272    /// wide as K, a single RoPE table, full-context attention and a plain
16273    /// softmax. A model outside that contract runs on the CPU layer walk
16274    /// (and the per-op GPU matvecs) until a graph learns it — never on a
16275    /// graph that would read it wrong. None = no attention-level reason
16276    /// (the graph builders still check weights and layer kinds).
16277    pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
16278        if self.kv_heads_per_layer.is_some() {
16279            return Some("per-layer KV head counts");
16280        }
16281        if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
16282            return Some("V heads narrower than Q/K heads");
16283        }
16284        if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
16285            return Some("learned attention sinks");
16286        }
16287        if self.swa.is_some() || self.sliding_layers.is_some() {
16288            return Some("sliding-window layers");
16289        }
16290        None
16291    }
16292
16293    /// Can the Metal kernels apply this model's RoPE amplitude? Every
16294    /// rotation kernel (`attn_rope_qkn[_b]`, `chunk_rope_kv`) multiplies the
16295    /// rotated pair by the layer's `rope_scale` (YaRN `attention_factor`,
16296    /// Mellum2.1's global layers) exactly where the CPU's
16297    /// `rope_rotate_scaled` does. With the Q/K norm AFTER RoPE the kernels'
16298    /// shared sum of squares would be the unscaled one, so that pairing
16299    /// (no known model) declines.
16300    #[cfg(target_os = "macos")]
16301    fn metal_rope_scale_ok(&self) -> bool {
16302        !(self.qk_norm_after_rope
16303            && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
16304    }
16305
16306    /// Can the Metal block graph carry this model's sliding-window layers?
16307    /// Both of its attention forms take a layer's own window, RoPE table
16308    /// and rotary width: the device attend (`AttnDeviceParams::window`, the
16309    /// layer's `inv_freq`/`rd`) and the sandwich's host attend (exactly the
16310    /// CPU path's attention). True when the sliding window is the model's
16311    /// only attention-level decline (Spark-X2.5). Learned, value-less
16312    /// sinks may accompany that geometry: q1's ordinary device GQA
16313    /// includes the sink in its softmax denominator and disables split-K.
16314    /// Per-layer KV heads, narrow V and Gemma-4 global geometry still
16315    /// decline; a YaRN amplitude per layer rides along
16316    /// (`metal_rope_scale_ok`).
16317    ///
16318    /// Per-layer Q heads (Laguna) and capped scores (Gemma-2) decline here
16319    /// too, with V norm (Gemma-4): the chunk prefill's own gate checks
16320    /// neither of the first two, and before this door opened its `swa`
16321    /// refusal kept every sliding model with them off the chunk graph.
16322    #[cfg(target_os = "macos")]
16323    fn metal_graph_swa(&self) -> bool {
16324        (self.swa.is_some() || self.sliding_layers.is_some())
16325            && self.attention_heads_per_layer.is_none()
16326            && !self.attn_v_norm
16327            && self.attn_softcap == 0.0
16328            && self.kv_heads_per_layer.is_none()
16329            && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
16330            && self.global_attn.is_none()
16331            && self.inv_freq_global.is_none()
16332            && self.metal_rope_scale_ok()
16333            && !(0..self.num_layers).any(|li| {
16334                self.layer_is_local(li)
16335                    && self.inv_freq_local.is_none()
16336                    && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
16337            })
16338    }
16339
16340    /// Can the Metal graphs carry this Gemma-4 model (`gpu_metal::gemma4`)?
16341    /// Its sliding layers (window, local RoPE table, 256-wide heads)
16342    /// device-attend like Spark-X2.5's; the 512-wide global layers (one KV
16343    /// head, proportional RoPE in the zero-padded table) take the 16-slot
16344    /// kernels; the V norm, sandwich norms, layer scalar and tanh-GELU come
16345    /// from the Gemma-4 library. Dense FFNs only (no Gemma-4 MoE measured).
16346    /// `CMF_METAL_GEMMA4=0` declines it as before (CPU attention).
16347    #[cfg(target_os = "macos")]
16348    fn metal_gemma4(&self) -> bool {
16349        crate::gpu_metal::gemma4::enabled()
16350            && (self.global_attn.is_some() || self.attn_v_norm)
16351            && self.attention_heads_per_layer.is_none()
16352            && self.kv_heads_per_layer.is_none()
16353            && self.v_head_dim.is_none()
16354            && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
16355            && self.attn_softcap == 0.0
16356            && !self.qk_norm_after_rope
16357            && self.metal_rope_scale_ok()
16358            && self
16359                .weights
16360                .layers
16361                .iter()
16362                .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
16363            && !(0..self.num_layers).any(|li| {
16364                self.layer_is_local(li)
16365                    && self.inv_freq_local.is_none()
16366                    && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
16367            })
16368    }
16369
16370    /// Why the WGPU graphs (whole-token, batched prefill, greedy burst)
16371    /// cannot run this model's attention, if they cannot. Per-layer KV
16372    /// heads, V narrower than K, learned sinks and sliding windows ride
16373    /// their per-layer geometry (`GraphAttnGeom`, the ATTEND_X kernels);
16374    /// what that geometry does not express keeps the decline, by name.
16375    /// None for every model with one attention geometry.
16376    pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
16377        if self.value_blend.is_some() && !crate::gpu::vres_graph_on() {
16378            return Some("value residual");
16379        }
16380        // Keep the ordinary (uniform) fast arms off YaRN until they carry a
16381        // scale field too. A non-uniform geometry gets a `GraphAttnGeom` and
16382        // is handled below by the generic graph kernels.
16383        let geometry_reason = self.graph_attn_decline_reason();
16384        let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
16385        if has_scaled_rope && geometry_reason.is_none() {
16386            return Some("scaled RoPE without per-layer geometry");
16387        }
16388        // For Q/K norm after partial RoPE, the scale changes only the rotary
16389        // slice's RMS denominator. The generic graph deliberately declines
16390        // that uncommon combination until it carries a separate rotary sum;
16391        // Mellum has q/k norm before RoPE, so it remains on this path.
16392        if has_scaled_rope && self.qk_norm_after_rope {
16393            return Some("scaled RoPE with post-RoPE Q/K norm");
16394        }
16395        geometry_reason?;
16396        // Gemma-4: global layers 512 wide over 256-wide sliding ones
16397        // (`GraphAttnGeom::hd`), the scale-less V norm (`v_norm`), and the
16398        // sandwich norms + layer scalar (`GraphLayer`) ride the TOKEN graph.
16399        // The batch graph has none of the three and refuses them itself
16400        // (`batch_graph_gemma_decline`).
16401        if self.attention_heads_per_layer.is_some() {
16402            return Some("per-layer Q head counts with per-layer geometry");
16403        }
16404        // Sandwich norms are measured on Gemma-4 (the humanizer port) and
16405        // LFM3, both with plain x̂·w weights, against HF transformers;
16406        // Gemma-2/3's (1+w) norms keep the per-op path until they are
16407        // measured the same way, and a layer scale stays Gemma-4's.
16408        let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
16409        let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
16410        if !gemma4
16411            && self.weights.layers.iter().any(|lw| {
16412                lw.layer_scale.is_some()
16413                    || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some()))
16414            })
16415        {
16416            return Some("sandwich norms / layer scale with per-layer geometry");
16417        }
16418        // `GraphAttnGeom::rope_scale` carries YaRN's post-rotation
16419        // attention-amplitude factor to the WGPU RoPE kernel. It is not an
16420        // angular position scale, so it must not be folded into `invf`.
16421        if self.weights.layers.iter().any(|lw| {
16422            matches!(
16423                &lw.attn,
16424                AttnKind::Full {
16425                    output_gate: true,
16426                    ..
16427                }
16428            )
16429        }) && self.v_head_dim.is_some()
16430        {
16431            return Some("gated attention with V narrower than K");
16432        }
16433        if (0..self.num_layers).any(|li| {
16434            self.layer_is_local(li)
16435                && self.inv_freq_local.is_none()
16436                && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
16437        }) {
16438            return Some("local rotary width without a local RoPE table");
16439        }
16440        None
16441    }
16442
16443    /// What the wgpu BATCH graph (and the attention-only singleton graphs)
16444    /// cannot run of Gemma-4, which the token graph does run: per-layer
16445    /// head widths, the V norm, sandwich norms and the layer scalar.
16446    fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
16447        // Gemma-4 rides `gemma_batch` (per-layer head width, V norm,
16448        // sandwich norms, layer scalar) unless CMF_G4_BATCH=0.
16449        if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
16450            return None;
16451        }
16452        if self.global_attn.is_some() {
16453            return Some("per-layer head width (Gemma-4 global layers)");
16454        }
16455        if self.attn_v_norm {
16456            return Some("V norm");
16457        }
16458        // Sandwich norms alone ride the batch graph's plain tails (Ripple).
16459        if self.weights.layers.iter().any(|lw| lw.layer_scale.is_some()) {
16460            return Some("layer scale");
16461        }
16462        if self.value_blend.is_none()
16463            && self
16464                .weights
16465                .layers
16466                .iter()
16467                .any(|lw| lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some())
16468        {
16469            return Some("sandwich norms");
16470        }
16471        None
16472    }
16473
16474    /// The wgpu graphs' attention geometry for layer `li` (virtual index):
16475    /// Some only for a model whose layers do not share one (MiMo-V2) — KV
16476    /// heads, V width, rotary width and RoPE table, window and sinks of
16477    /// THIS layer, exactly what the CPU attention reads for it.
16478    /// Unlimited-OCR: layer `li`'s router for the device graphs.
16479    fn graph_dash(&self, li: usize) -> Option<crate::gpu::GraphDash<'_>> {
16480        self.dash.as_deref().map(|d| {
16481            let l = &d.layers[li];
16482            crate::gpu::GraphDash {
16483                qk_proj: &l.qk_proj,
16484                q_norm: &l.q_norm,
16485                k_norm: &l.k_norm,
16486                head_cls: &l.head_cls,
16487                idx_scale: l.route.idx_scale,
16488                floor: l.route.floor,
16489                sigma: l.route.sigma,
16490                k: l.route.k,
16491                block: d.block,
16492                local_blocks: d.local_blocks,
16493                nblk: d.allow.len(),
16494                cands: &d.cands,
16495                coff: &d.coff,
16496                angles: &d.angles,
16497            }
16498        })
16499    }
16500
16501    fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
16502        if self.dash.is_none() {
16503            self.graph_attn_decline_reason()?;
16504        }
16505        let (nkv, hd, rd) = self.layer_geom(li);
16506        let dash = self.graph_dash(li);
16507        let invf: &[f32] = if let Some(d) = self.dash.as_deref() {
16508            &d.angles
16509        } else if self.layer_is_local(li) {
16510            match &self.inv_freq_local {
16511                Some(f) => f.as_slice(),
16512                None => self.inv_freq.as_slice(),
16513            }
16514        } else {
16515            match &self.inv_freq_global {
16516                Some(f) => f.as_slice(),
16517                None => self.inv_freq.as_slice(),
16518            }
16519        };
16520        Some(crate::gpu::GraphAttnGeom {
16521            nkv,
16522            hd,
16523            dv: self.layer_v_dim(li),
16524            rd,
16525            invf,
16526            rope_scale: self.layer_rope_scale(li),
16527            window: self.layer_window(li),
16528            sink: self.kv_cache.layers[li].sinks.as_deref(),
16529            v_norm: self.attn_v_norm,
16530            dash,
16531            vres: None,
16532        })
16533    }
16534
16535    /// `graph_attn_geom` with Ripple's value residual: the layer's blend and
16536    /// `e0`, the embedding rows of the graph call.
16537    fn graph_attn_geom_vres<'a>(&'a self, li: usize, e0: &'a [f32]) -> Option<crate::gpu::GraphAttnGeom<'a>> {
16538        let mut g = self.graph_attn_geom(li)?;
16539        g.vres = self.layer_value_blend(li).map(|b| (b, e0));
16540        Some(g)
16541    }
16542
16543    /// The decode graph's device K/V mirror that layer `li`'s batched
16544    /// prefill appends its chunk to and attends against (wgpu), instead
16545    /// of uploading the layer's whole K/V prefix every chunk and the whole
16546    /// cache again at the first decode token. Only where the token graph
16547    /// will read that very mirror: the decode graph on, not refused, and
16548    /// this layer's geometry what it requests (`graph_attn_geom`: KV heads,
16549    /// V as wide as K, the window as a ring, no sink).
16550    ///
16551    /// Spark-X2.5 only (`proj_gate_sigmoid`), the one architecture whose
16552    /// output was measured identical on it. `CMF_PREFILL_MIRROR=0` keeps
16553    /// the host upload (A/B).
16554    #[cfg(feature = "gpu")]
16555    fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
16556        if !self.proj_gate_sigmoid
16557            || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
16558            || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16559            || self.graph_refused()
16560            || self.o1_active()
16561            || self.attn_softcap > 0.0
16562            || self.wgpu_graph_attn_decline().is_some()
16563        {
16564            return None;
16565        }
16566        let g = self.graph_attn_geom(li)?;
16567        let (nkv, hd, _) = self.layer_geom(li);
16568        // The token graph passes the call-wide head width (layer 0's).
16569        if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
16570            return None;
16571        }
16572        Some(crate::gpu::PrefillMirror {
16573            kv_id: self.graph_kv_id,
16574            layer: li,
16575            limit: self.kv_cache.max_seq_len,
16576        })
16577    }
16578
16579    /// Bring the host KV cache of every Full-attention layer in
16580    /// `[from, upto)` up to `position` rows from the wgpu mirrors, where a
16581    /// device graph advanced a layer that the host is about to run: a
16582    /// device prefix that shrank since the prompt (or a batched prefill
16583    /// prefix longer than the decode one). A layer whose mirror does not
16584    /// hold the missing rows is left alone. Rows a sliding layer's ring
16585    /// no longer holds come back as zeros — outside every window that
16586    /// will read them.
16587    #[cfg(feature = "gpu")]
16588    fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
16589        let kv_id = self.graph_kv_id;
16590        for li in from..upto.min(self.num_layers) {
16591            if !matches!(
16592                self.weights.layers[self.phys_layer(li)].attn,
16593                AttnKind::Full { .. }
16594            ) {
16595                continue;
16596            }
16597            // Absolute: a trimmed sliding tail holds positions base.. only.
16598            let host = self.kv_cache.layers[li].pos_len();
16599            if host >= position {
16600                continue;
16601            }
16602            let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
16603                continue;
16604            };
16605            let to = dev.min(position);
16606            if to <= host {
16607                continue;
16608            }
16609            let (nkv, hd) = {
16610                let c = &self.kv_cache.layers[li];
16611                (c.num_kv_heads, c.head_dim)
16612            };
16613            let Some((k, v, first_valid)) =
16614                crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
16615            else {
16616                continue;
16617            };
16618            // A sliding layer only ever reads its last `window` rows; a
16619            // full-context layer needs every row it did not have.
16620            let need_from = match self.layer_window(li) {
16621                Some(w) => host.max((position + 1).saturating_sub(w)),
16622                None => host,
16623            };
16624            if first_valid > need_from {
16625                tracing::warn!(
16626                    "layer {li}: device KV rows {host}..{to} no longer resident \
16627                     (from {first_valid}); host attention will miss them"
16628                );
16629            }
16630            let row = nkv * hd;
16631            let cache = &mut self.kv_cache.layers[li];
16632            for p in 0..to - host {
16633                cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
16634            }
16635        }
16636    }
16637
16638    /// Log (once per graph site and pipeline) that `site` declined for
16639    /// `reason`. The lines are kept so a caller or a test can read them.
16640    fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
16641        let mut seen = self.graph_declines.borrow_mut();
16642        if !seen.iter().any(|&(s, r)| s == site && r == reason) {
16643            tracing::warn!("{site} declined: {reason} (CPU attention path)");
16644            seen.push((site, reason));
16645        }
16646    }
16647
16648    /// The GPU-graph declines this pipeline has logged so far, as the
16649    /// logged lines.
16650    pub fn graph_declines(&self) -> Vec<String> {
16651        self.graph_declines
16652            .borrow()
16653            .iter()
16654            .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
16655            .collect()
16656    }
16657
16658    /// Does layer `li` have the plain attention geometry the historical
16659    /// head-masked f32 path (`multi_head_attention`) assumes — pipeline-wide
16660    /// KV heads / head_dim / RoPE table, full context, no sink, V as wide
16661    /// as K? Anything else runs the dense `qwen_attention` instead.
16662    /// `CMF_LAYER_DUMP` writer (see `Pipeline::layer_dump`): one position's
16663    /// hidden after layer `li` as raw little-endian f32 into
16664    /// `<dir>/p{pos:06}_l{li:02}.f32`. A failed write is reported once and
16665    /// never stops the forward — the dump is a diagnostic.
16666    fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
16667        let Some(dir) = &self.layer_dump else {
16668            return;
16669        };
16670        let mut bytes = Vec::with_capacity(row.len() * 4);
16671        for v in row {
16672            bytes.extend_from_slice(&v.to_le_bytes());
16673        }
16674        let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
16675        if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
16676            use std::sync::atomic::{AtomicBool, Ordering};
16677            static SAID: AtomicBool = AtomicBool::new(false);
16678            if !SAID.swap(true, Ordering::Relaxed) {
16679                tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
16680            }
16681        }
16682    }
16683
16684    /// Decide the MiMo-V2 expert placement once (`crate::mimo_moe`). Any
16685    /// other model turns the slot off on the first call.
16686    fn mimo_moe_prepare(&mut self) {
16687        if !self.mimo_moe.is_undecided() {
16688            return;
16689        }
16690        let slot = {
16691            let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
16692                .filter_map(
16693                    |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
16694                        FfnKind::Moe(m) => Some((li, m)),
16695                        _ => None,
16696                    },
16697                )
16698                .collect();
16699            // One bank lives on one device: an in-process multi-GPU split
16700            // keeps the whole-layer path.
16701            if layers.is_empty()
16702                || self.physical_layers != self.num_layers
16703                || self.gpu_plan.is_some()
16704            {
16705                crate::mimo_moe::Slot::Off
16706            } else {
16707                // Whether a whole-token graph could run this model's layers
16708                // (then a whole-layer prefix is one submit, not per-layer
16709                // fences).
16710                let graph_prefix =
16711                    self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
16712                crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
16713            }
16714        };
16715        self.mimo_moe = slot;
16716    }
16717
16718    #[cfg(test)]
16719    pub(crate) fn test_graph_kv_id(&self) -> u64 {
16720        self.graph_kv_id
16721    }
16722
16723    /// Dynamic MiMo layer: one device attention graph, followed by a bank
16724    /// frame. Both decode and short verification use this same attention
16725    /// path and absolute layer key; the host KV may intentionally lag.
16726    pub(crate) fn mimo_graph_layer_rows(
16727        &mut self,
16728        li: usize,
16729        h: &mut [f32],
16730        positions: &[usize],
16731    ) -> crate::gpu::BatchGraphOutcome {
16732        use crate::gpu::BatchGraphOutcome as Out;
16733        let b = positions.len();
16734        if !(1..=4).contains(&b)
16735            || h.len() != b * self.hidden_size
16736            || !self.mimo_moe.is_dynamic(li, true)
16737            || !crate::gpu::enabled_here()
16738            || !crate::gpu::wgpu_active()
16739            || self.o1_active()
16740            || self.physical_layers != self.num_layers
16741            // The pair-fusion diagnostic (and an explicit graph-off run)
16742            // rewinds only host KV. A hidden singleton attention graph here
16743            // would leave device mirrors ahead of the next host position.
16744            || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
16745            || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
16746            || self.wgpu_graph_attn_decline().is_some()
16747        {
16748            return Out::Declined;
16749        }
16750        let attn_started = std::time::Instant::now();
16751        let outcome = {
16752            let lw = &self.weights.layers[li];
16753            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
16754                return Out::Declined;
16755            }
16756            let FfnKind::Moe(m) = &lw.ffn else {
16757                return Out::Declined;
16758            };
16759            let AttnKind::Full {
16760                wq,
16761                wk,
16762                wv,
16763                wo,
16764                q_norm,
16765                k_norm,
16766                output_gate,
16767                softplus_gate,
16768                bias,
16769            } = &lw.attn
16770            else {
16771                return Out::Declined;
16772            };
16773            if *output_gate || softplus_gate.is_some() {
16774                return Out::Declined;
16775            }
16776            let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
16777                m.experts
16778                    .first()?
16779                    .gate_proj
16780                    .mapped_q4tp()
16781                    .map(|(m, _)| m.clone())
16782            }) else {
16783                return Out::Declined;
16784            };
16785            fn gw<'a>(
16786                t: &'a QTensor,
16787                owner: &std::sync::Arc<cortiq_core::CmfModel>,
16788            ) -> Option<crate::gpu::GraphW<'a>> {
16789                if let Some((m, idx, kind, rs)) = t.graph_weight() {
16790                    if m.uid() != owner.uid() || t.has_prism_contract() {
16791                        return None;
16792                    }
16793                    return Some(crate::gpu::GraphW {
16794                        idx,
16795                        kind,
16796                        row_scale: rs,
16797                        data: &[],
16798                        prism: crate::gpu::GraphPrismOp::None,
16799                        affine: false,
16800                    });
16801                }
16802                t.as_f32().map(|data| crate::gpu::GraphW {
16803                    idx: 0,
16804                    kind: 4,
16805                    row_scale: &[],
16806                    data,
16807                    prism: crate::gpu::GraphPrismOp::None,
16808                    affine: false,
16809                })
16810            }
16811            let (Some(q), Some(k), Some(v), Some(o)) = (
16812                gw(wq, &model),
16813                gw(wk, &model),
16814                gw(wv, &model),
16815                gw(wo, &model),
16816            ) else {
16817                return Out::Declined;
16818            };
16819            let layer = crate::gpu::GraphLayer {
16820                mhc: None,
16821                input_norm: &lw.input_norm,
16822                post_norm: &lw.post_norm,
16823                attn_out_norm: None,
16824                ffn_out_norm: None,
16825                layer_scale: None,
16826                ffn: crate::gpu::GraphFfn::AttentionOnly,
16827                attn: crate::gpu::GraphAttn::Full {
16828                    wq: q,
16829                    wk: k,
16830                    wv: v,
16831                    wo: o,
16832                    q_norm: q_norm.as_deref(),
16833                    k_norm: k_norm.as_deref(),
16834                    late_qk_norm: self.qk_norm_after_rope,
16835                    bias: bias
16836                        .as_ref()
16837                        .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
16838                    output_gate: false,
16839                    cpu_k: self.kv_cache.layers[li].k_heads(),
16840                    cpu_v: self.kv_cache.layers[li].v_heads(),
16841                    cpu_base: self.kv_cache.layers[li].base(),
16842                    geom: self.graph_attn_geom(li),
16843                    head_gate: None,
16844                },
16845            };
16846            let (nkv, hd, rd) = self.layer_geom(li);
16847            crate::gpu::forward_batch_graph_at(
16848                &model,
16849                self.graph_kv_id,
16850                li,
16851                &[layer],
16852                &self.inv_freq,
16853                h,
16854                self.layer_num_heads(li),
16855                nkv,
16856                hd,
16857                rd,
16858                self.hidden_size,
16859                1,
16860                positions,
16861                self.kv_cache.max_seq_len,
16862                self.norm_style == cortiq_core::NormStyle::Gemma,
16863                self.rms_eps as f32,
16864                self.attn_scale,
16865                b,
16866                &[],
16867                self.o1_epoch,
16868                None,
16869                None,
16870            )
16871        };
16872        match outcome {
16873            Out::Completed => {}
16874            Out::Declined => return Out::Declined,
16875            Out::Failed => {
16876                self.graph_failed
16877                    .store(true, std::sync::atomic::Ordering::Relaxed);
16878                return Out::Failed;
16879            }
16880        }
16881        let attn_ns = attn_started.elapsed().as_nanos() as u64;
16882        let hs = self.hidden_size;
16883        let lw = &self.weights.layers[li];
16884        let FfnKind::Moe(m) = &lw.ffn else {
16885            unreachable!()
16886        };
16887        let mut post = vec![0.0; h.len()];
16888        for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
16889            inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
16890        }
16891        let mut ffn = if b == 1 {
16892            moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
16893        } else {
16894            moe_ffn_banked_rows(
16895                &mut self.mimo_moe,
16896                li,
16897                m,
16898                &post,
16899                b,
16900                hs,
16901                self.pool.as_deref(),
16902            )
16903        };
16904        for (x, &f) in h.iter_mut().zip(&ffn) {
16905            *x += f;
16906        }
16907        attention::recycle_buf(&mut ffn);
16908        if self.layer_dump.is_some() {
16909            for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
16910                self.dump_layer_row(pos, li, row);
16911            }
16912        }
16913        crate::mimo_moe::note_attention_graph(b, attn_ns);
16914        Out::Completed
16915    }
16916
16917    fn layer_attn_plain(&self, li: usize) -> bool {
16918        self.kv_heads_per_layer.is_none()
16919            && self.v_head_dim.is_none()
16920            && self.global_attn.is_none()
16921            && self.layer_window(li).is_none()
16922            && self.kv_cache.layers[li].sinks.is_none()
16923    }
16924
16925    /// Forward one position through all layers (hybrid dispatch).
16926    fn forward_layers(
16927        &mut self,
16928        hidden: &[f32],
16929        position: usize,
16930        task_mask: Option<&TaskMask>,
16931    ) -> Vec<f32> {
16932        let out = self.forward_layers_upto(hidden, position, task_mask, None);
16933        self.o1_progress();
16934        self.swa_trim_tails();
16935        out
16936    }
16937
16938    // ── Network pipeline-split building blocks (coordinator/worker) ──
16939    // A remote worker owns layers [from ..= upto] and their KV; the
16940    // coordinator owns the rest plus embed / final norm / head. Attention
16941    // causality is per-layer, so a whole prompt's boundary hiddens ship
16942    // as one batch and decode ships one vector per token.
16943
16944    /// Embed one token id (embed multiplier applied).
16945    pub fn embed_id(&self, id: u32) -> Vec<f32> {
16946        self.embed_single(id)
16947    }
16948
16949    /// Refuse the archs/modes whose forward cannot be cut at a layer
16950    /// boundary. Loud by design: a split that silently changed the math
16951    /// would be a chimera.
16952    pub fn split_supported(&self) -> Result<(), String> {
16953        if self.dsv4.is_some() {
16954            return Err(
16955                "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
16956            );
16957        }
16958        if self.dory.is_some() {
16959            return Err("network split: Dory's looped stack is not splittable".into());
16960        }
16961        if self.dsv41.is_some() {
16962            return Err(
16963                "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
16964                    .into(),
16965            );
16966        }
16967        if self.qwen4_exp.is_some() {
16968            return Err(
16969                "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
16970            );
16971        }
16972        if self.per_token_stack() {
16973            return Err(
16974                "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
16975            );
16976        }
16977        if self.value_blend.is_some() {
16978            return Err(
16979                "network split: a value-residual stack (Ripple) reads the embedding row in every layer".into(),
16980            );
16981        }
16982        Ok(())
16983    }
16984
16985    /// Forward `hidden` through layers [from ..= upto] at `position`,
16986    /// appending those layers' KV/state. Both split sides call this
16987    /// over their own range; a task mask applies to the span's own
16988    /// layers (each side masks what it runs).
16989    pub fn forward_span(
16990        &mut self,
16991        hidden: &[f32],
16992        position: usize,
16993        from: usize,
16994        upto: usize,
16995        task_mask: Option<&TaskMask>,
16996    ) -> Result<Vec<f32>, String> {
16997        self.split_supported()?;
16998        if from > upto || upto >= self.num_layers {
16999            return Err(format!(
17000                "forward_span: layer range {from}..={upto} outside 0..{}",
17001                self.num_layers
17002            ));
17003        }
17004        if hidden.len() != self.hidden_size {
17005            return Err(format!(
17006                "forward_span: hidden len {} ≠ hidden_size {}",
17007                hidden.len(),
17008                self.hidden_size
17009            ));
17010        }
17011        let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
17012        self.o1_progress();
17013        self.swa_trim_tails();
17014        if self
17015            .graph_failed
17016            .swap(false, std::sync::atomic::Ordering::Relaxed)
17017        {
17018            self.cancel
17019                .store(false, std::sync::atomic::Ordering::Relaxed);
17020            self.clear_sequence_state();
17021            return Err("forward_span: deferred O(1) transition failed".into());
17022        }
17023        Ok(out)
17024    }
17025
17026    /// Final norm + lm_head over a boundary hidden (the final-logit
17027    /// softcap is applied by lm_head_forward itself).
17028    pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
17029        let normed = inference::rms_norm(
17030            hidden,
17031            &self.weights.final_norm,
17032            self.rms_eps,
17033            self.norm_style,
17034        );
17035        self.lm_head_forward(&normed)
17036    }
17037
17038    /// Sample the next token with this pipeline's sampler state.
17039    pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
17040        sampler::sample_with_scratch(
17041            logits,
17042            &self.sampler_config,
17043            past_tokens,
17044            &mut self.rng,
17045            &mut self.sampler_scratch,
17046        )
17047    }
17048
17049    /// Fresh sequence: clear KV, reuse history and device mirrors.
17050    pub fn reset_session(&mut self) {
17051        self.clear_sequence_state();
17052    }
17053
17054    /// Batched span prefill from token ids (coordinator side): embed +
17055    /// layers [0 ..= upto]; returns the boundary hiddens of ALL positions
17056    /// (ids.len() × hidden). Rides the same layer-major machinery as the
17057    /// local prefill; falls back to the per-position walk under
17058    /// CMF_PREFILL=seq.
17059    pub fn prefill_span_ids(
17060        &mut self,
17061        ids: &[u32],
17062        start_pos: usize,
17063        upto: usize,
17064        task_mask: Option<&TaskMask>,
17065    ) -> Result<Vec<f32>, String> {
17066        self.split_supported()?;
17067        if upto >= self.num_layers {
17068            return Err(format!(
17069                "prefill_span_ids: upto {upto} outside 0..{}",
17070                self.num_layers
17071            ));
17072        }
17073        // Same predicate as the whole-stack prefill: a span whose GDN
17074        // state lives on the device must walk positions through the
17075        // graph, not through the batched CPU span.
17076        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
17077            let out =
17078                self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
17079            self.check_o1_progress_failure("prefill_span_ids")?;
17080            Ok(out)
17081        } else {
17082            let hs = self.hidden_size;
17083            let mut out = Vec::with_capacity(ids.len() * hs);
17084            for (i, &id) in ids.iter().enumerate() {
17085                let emb = self.embed_id(id);
17086                out.extend_from_slice(&self.forward_span(
17087                    &emb,
17088                    start_pos + i,
17089                    0,
17090                    upto,
17091                    task_mask,
17092                )?);
17093            }
17094            Ok(out)
17095        }
17096    }
17097
17098    /// Batched span prefill from boundary hiddens (worker side): layers
17099    /// [from ..= upto] for every position in the batch; returns the batch.
17100    pub fn prefill_span_hidden(
17101        &mut self,
17102        hidden: &[f32],
17103        start_pos: usize,
17104        from: usize,
17105        upto: usize,
17106        task_mask: Option<&TaskMask>,
17107    ) -> Result<Vec<f32>, String> {
17108        self.split_supported()?;
17109        let hs = self.hidden_size;
17110        if hidden.is_empty() || hidden.len() % hs != 0 {
17111            return Err(format!(
17112                "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
17113                hidden.len()
17114            ));
17115        }
17116        if from > upto || upto >= self.num_layers {
17117            return Err(format!(
17118                "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
17119                self.num_layers
17120            ));
17121        }
17122        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
17123            let out = self.prefill_batch_span(
17124                PrefillIn::Hidden(hidden),
17125                start_pos,
17126                task_mask,
17127                from,
17128                upto + 1,
17129            );
17130            self.check_o1_progress_failure("prefill_span_hidden")?;
17131            Ok(out)
17132        } else {
17133            let b = hidden.len() / hs;
17134            let mut out = Vec::with_capacity(hidden.len());
17135            for i in 0..b {
17136                let h = self.forward_span(
17137                    &hidden[i * hs..(i + 1) * hs],
17138                    start_pos + i,
17139                    from,
17140                    upto,
17141                    task_mask,
17142                )?;
17143                out.extend_from_slice(&h);
17144            }
17145            Ok(out)
17146        }
17147    }
17148
17149    /// Build the whole-token wgpu graph for a pure-attention q1 model (every
17150    /// layer Full q1 + dense q1 FFN, no gate/bias). Returns the post-stack
17151    /// hidden (caller does final norm + lm_head), or None to fall back.
17152    fn try_token_graph_wgpu(
17153        &self,
17154        hidden: &[f32],
17155        position: usize,
17156        logits_out: &mut Vec<f32>,
17157        layers_run: &mut usize,
17158    ) -> Option<Result<Vec<f32>, ()>> {
17159        self.try_token_graph_wgpu_steps(
17160            hidden,
17161            position,
17162            logits_out,
17163            1,
17164            None,
17165            Some(layers_run),
17166            0,
17167            self.num_layers,
17168        )
17169    }
17170
17171    /// The span twin (network split): the graph covers [from..upto_excl)
17172    /// — one submit per SEGMENT per token. lm_head folds in only when
17173    /// the span reaches the last layer.
17174    fn try_token_graph_wgpu_span(
17175        &self,
17176        hidden: &[f32],
17177        position: usize,
17178        logits_out: &mut Vec<f32>,
17179        from: usize,
17180        upto_excl: usize,
17181        layers_run: &mut usize,
17182    ) -> Option<Result<Vec<f32>, ()>> {
17183        self.try_token_graph_wgpu_steps(
17184            hidden,
17185            position,
17186            logits_out,
17187            1,
17188            None,
17189            Some(layers_run),
17190            from,
17191            upto_excl,
17192        )
17193    }
17194
17195    /// A device token graph (wgpu or CUDA) decodes this pipeline: the head
17196    /// of a row the graph did not run — the first token after a prompt —
17197    /// takes the graph's head pass as well (`CMF_HEAD_GRAPH=0`: the host
17198    /// head, as before).
17199    ///
17200    /// The host head of a q8 file on a GPU run is a hybrid: half of the
17201    /// vocabulary's rows on the card in f32, the other half on the CPU with
17202    /// integer activations (`gpu_split_frac`). Every later token's logits
17203    /// come from the graph's fused head, so that one row alone carried the
17204    /// integer half. Spark-X2.6-4B q8_2f on an A40 over Vulkan, against the
17205    /// host with float activations: first-token logits 0.29 off (and a
17206    /// near-tie token flipped on one prompt in four), every other token
17207    /// 0.015; through the graph's head the first token is 0.014 off too.
17208    /// On wgpu only those two codecs take it (`m3_device_logits`): a q4tp
17209    /// head measured the same either way.
17210    #[cfg(feature = "gpu")]
17211    fn graph_head_after_prompt(&self) -> bool {
17212        (crate::gpu::wgpu_active() || crate::gpu::cuda_active())
17213            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17214            && !self.graph_refused()
17215            && std::env::var("CMF_HEAD_GRAPH").as_deref() != Ok("0")
17216    }
17217
17218    /// The pooled tail's device lm_head: final-norm + head as one
17219    /// layer-less graph pass, so decode stops paying the double-digit
17220    /// millisecond host matvec every token. `None` keeps the host head.
17221    /// Also the head of the first token after a prompt wherever a device
17222    /// graph decodes (`graph_head_after_prompt`).
17223    #[cfg(feature = "gpu")]
17224    fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
17225        let pooled = crate::minicpm_tail::minimax_pool_on();
17226        if !(pooled || self.graph_head_after_prompt())
17227            || !crate::gpu::enabled_here()
17228            || self.head_clusters.is_some()
17229            || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
17230            || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
17231        {
17232            return None;
17233        }
17234        let (model, i, kind, rs) = self
17235            .weights
17236            .lm_head
17237            .graph_weight()
17238            .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
17239        // After a prompt on wgpu: the codecs whose host head is the hybrid
17240        // (q8 by rows, q8_2f). CUDA has no host GPU head at all.
17241        if !pooled && !crate::gpu::cuda_active() && !matches!(kind, 0 | 7) {
17242            return None;
17243        }
17244        let name = &model.tensors[i].name;
17245        let prism = if crate::prism::is_inverse_embedding(&model, name) {
17246            crate::gpu::GraphPrismOp::InverseEmbedding
17247        } else if crate::prism::is_forward_weight(&model, name) {
17248            crate::gpu::GraphPrismOp::Forward
17249        } else {
17250            crate::gpu::GraphPrismOp::None
17251        };
17252        let gw = crate::gpu::GraphW {
17253            idx: i,
17254            kind,
17255            row_scale: rs,
17256            data: &[],
17257            prism,
17258            affine: crate::prism::is_affine_target(&model, name),
17259        };
17260        let rows = self.weights.lm_head.rows();
17261        let mut h = hidden.to_vec();
17262        let mut lg = Vec::new();
17263        let mut layers_run = 0usize;
17264        let outcome = crate::gpu::forward_token_graph(
17265            &model,
17266            self.graph_kv_id,
17267            &[],
17268            &[],
17269            self.o1_epoch,
17270            &self.inv_freq,
17271            &mut h,
17272            self.num_heads,
17273            self.num_kv_heads,
17274            self.head_dim,
17275            self.attn_scale,
17276            self.head_dim,
17277            self.hidden_size,
17278            self.intermediate_size,
17279            0,
17280            self.kv_cache.max_seq_len,
17281            self.norm_style == cortiq_core::NormStyle::Gemma,
17282            self.rms_eps as f32,
17283            Some((&gw, rows)),
17284            &self.weights.final_norm,
17285            &mut lg,
17286            &[],
17287            1,
17288            None,
17289            None,
17290            Some(&mut layers_run),
17291            0,
17292            false,
17293        );
17294        if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
17295            return None;
17296        }
17297        // The same post-steps the host head applies — keep sampling
17298        // identical whichever arm produced the logits.
17299        lg.resize(self.vocab_size, 0.0);
17300        if let Some(m) = self.logit_multiplier {
17301            for l in lg.iter_mut() {
17302                *l *= m;
17303            }
17304        }
17305        if let Some(c) = self.final_softcap {
17306            softcap_in_place(&mut lg, c, self.pool.as_deref());
17307        }
17308        Some(lg)
17309    }
17310
17311    /// Record one token-layer's winners into the route-frequency profile and
17312    /// write it out every 16 tokens when `CMF_M3_PROFILE_SAVE=<path>` is set.
17313    fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
17314        if winners.is_empty() {
17315            return;
17316        }
17317        if self.m3_n_exp == 0 {
17318            self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
17319            self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
17320        }
17321        for &e in winners {
17322            let e = e as usize;
17323            if e < self.m3_n_exp && li < self.num_layers {
17324                self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
17325            }
17326        }
17327        if li + 1 == self.num_layers {
17328            self.m3_tokens += 1;
17329            if self.m3_tokens % 16 == 0 {
17330                if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
17331                    let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
17332                    let _ = std::fs::write(&path, bytes);
17333                }
17334            }
17335        }
17336    }
17337
17338    /// Admit the most frequent (layer, expert) pairs of a saved profile into
17339    /// the bank before the first token: `CMF_M3_PROFILE=<path>` names the
17340    /// file, `CMF_M3_PREWARM_N` (default 300) bounds how many experts go in.
17341    /// The bank's own eviction stays in charge; prewarm only chooses order.
17342    fn m3_prewarm(&mut self) {
17343        let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
17344            return;
17345        };
17346        let Ok(bytes) = std::fs::read(&path) else {
17347            tracing::warn!("m3 prewarm: cannot read profile {path}");
17348            return;
17349        };
17350        let counts: Vec<u32> = bytes
17351            .chunks_exact(4)
17352            .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
17353            .collect();
17354        let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
17355        if counts.len() != self.num_layers * n_exp {
17356            tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
17357            return;
17358        }
17359        let limit: usize = std::env::var("CMF_M3_PREWARM_N")
17360            .ok()
17361            .and_then(|v| v.parse().ok())
17362            .unwrap_or(300);
17363        let mut ranked: Vec<(u32, usize, usize)> = counts
17364            .iter()
17365            .enumerate()
17366            .filter(|(_, c)| **c > 0)
17367            .map(|(i, &c)| (c, i / n_exp, i % n_exp))
17368            .collect();
17369        ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
17370        ranked.truncate(limit);
17371        let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
17372        for (_, li, e) in ranked {
17373            per_layer[li].push(e);
17374        }
17375        let mut admitted = 0usize;
17376        for (li, ids) in per_layer.iter().enumerate() {
17377            if ids.is_empty() {
17378                continue;
17379            }
17380            let lw = &self.weights.layers[self.phys_layer(li)];
17381            let FfnKind::Moe(m) = &lw.ffn else {
17382                continue;
17383            };
17384            // Arming installs the layer's layout; nothing predicted yet.
17385            let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
17386            if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
17387                admitted += ids.len();
17388            }
17389        }
17390        tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
17391    }
17392
17393    /// One MiniMax tail layer through the pooled token-graph arm: attention,
17394    /// the pre-FFN norm, device routing and the descriptor-indexed expert
17395    /// bank in ONE submit (one sync instead of host attention + a MoE
17396    /// frame), with cold experts finished on the host against the read-back
17397    /// FFN input and admitted for the next token.
17398    ///
17399    /// `Ok(true)` — the layer is done (hidden replaced). `Ok(false)` —
17400    /// decline before any device mutation; the ordinary host path owns this
17401    /// position. `Err(())` — the device failed after admission; the caller
17402    /// must stop the sequence exactly like a failed span run.
17403    #[cfg(feature = "gpu")]
17404    fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
17405        if self.o1_active() || self.attn_softcap > 0.0 {
17406            return Ok(false);
17407        }
17408        if !self.m3_prewarmed {
17409            self.m3_prewarmed = true;
17410            self.m3_prewarm();
17411        }
17412        let frame = {
17413            let lw = &self.weights.layers[self.phys_layer(li)];
17414            let FfnKind::Moe(m) = &lw.ffn else {
17415                return Ok(false);
17416            };
17417            if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
17418                return Ok(false);
17419            }
17420            let t_arm = std::time::Instant::now();
17421            if self.m3_winners.len() != self.num_layers {
17422                self.m3_winners = vec![Vec::new(); self.num_layers];
17423            }
17424            let pred = std::mem::take(&mut self.m3_winners[li]);
17425            let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
17426            if armed.is_none() {
17427                self.m3_winners[li] = pred; // declined before anything ran
17428            }
17429            M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
17430            match armed {
17431                Some(frame) => frame,
17432                // First-call refusal (geometry/bank): the host layer still
17433                // runs; nothing was mutated.
17434                None => return Ok(false),
17435            }
17436        };
17437        crate::gpu_wgpu::pooled_frame_enter(frame);
17438        // The pooled call must not fold lm_head: its cold experts have to
17439        // land before any final norm. The decode loop re-arms this flag for
17440        // the next position anyway.
17441        let want_logits = self.graph_want_logits;
17442        self.graph_want_logits = false;
17443        let mut lg = Vec::new();
17444        let mut gl = 0usize;
17445        let t_span = std::time::Instant::now();
17446        let span = crate::gpu::gpu_scope(|| {
17447            self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
17448        });
17449        self.graph_want_logits = want_logits;
17450        M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
17451        let hh = match span {
17452            Some(Ok(hh)) => hh,
17453            Some(Err(())) => return Err(()),
17454            None => {
17455                // Declined before submission (seed/resolve/refusal): the
17456                // device mirror has not moved, so the host path is exact.
17457                return Ok(false);
17458            }
17459        };
17460        if gl != 1 {
17461            // A single-layer run that reports no layer mutated nothing we
17462            // may build on; anything else is a contract break.
17463            return if gl == 0 { Ok(false) } else { Err(()) };
17464        }
17465        *h = hh;
17466        let winners = crate::gpu_wgpu::pooled_frame_take_winners();
17467        self.m3_note_route(li, &winners);
17468        if self.m3_winners.len() != self.num_layers {
17469            self.m3_winners = vec![Vec::new(); self.num_layers];
17470        }
17471        self.m3_winners[li] = winners;
17472        let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
17473        // A non-finite parsed weight would poison the residual for the rest
17474        // of the sequence — drop such entries loudly instead.
17475        cold.retain(|&(_, w)| {
17476            if w.is_finite() {
17477                true
17478            } else {
17479                tracing::error!("pooled tail: non-finite cold weight from expert route");
17480                false
17481            }
17482        });
17483        if cold.is_empty() {
17484            return Ok(true);
17485        }
17486        let x = crate::gpu_wgpu::pooled_frame_take_input();
17487        if x.len() != self.hidden_size {
17488            return Err(());
17489        }
17490        // Cold experts: on the device first (admit, then one prepared frame
17491        // over exactly those slots — one submit for the layer). The host
17492        // computes them only when the device declines, and then they are
17493        // admitted for the next token. `CMF_M3_COLD_CPU=1` forces the host
17494        // arm for an A/B measurement.
17495        let lw = &self.weights.layers[self.phys_layer(li)];
17496        let FfnKind::Moe(m) = &lw.ffn else {
17497            return Err(());
17498        };
17499        let t_cold = std::time::Instant::now();
17500        let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
17501        let mut out = crate::attention::take_buf(self.hidden_size);
17502        let on_device = !force_host
17503            && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
17504        if !on_device {
17505            crate::attention::recycle_buf(&mut out);
17506            let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
17507                .iter()
17508                .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
17509                .collect::<Result<_, ()>>()?;
17510            out = crate::gpu::cpu_scope(|| {
17511                crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
17512            });
17513            let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
17514            let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
17515        }
17516        M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
17517        M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
17518        if out.len() != h.len() {
17519            crate::attention::recycle_buf(&mut out);
17520            return Err(());
17521        }
17522        for (dst, src) in h.iter_mut().zip(out.iter()) {
17523            *dst += src;
17524        }
17525        crate::attention::recycle_buf(&mut out);
17526        if on_device {
17527            M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
17528        }
17529        Ok(true)
17530    }
17531
17532    #[cfg(not(feature = "gpu"))]
17533    fn m3_pooled_layer(
17534        &mut self,
17535        _li: usize,
17536        _position: usize,
17537        _h: &mut Vec<f32>,
17538    ) -> Result<bool, ()> {
17539        Ok(false)
17540    }
17541
17542    /// Greedy burst: forward `t_next` and let the device pick + re-embed
17543    /// the next k−1 tokens — k frames, ONE submit, k ids back. The ZML
17544    /// trade, on wgpu. None ⇒ caller keeps the per-token path.
17545    fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
17546        if self.o1_active() || self.attn_softcap > 0.0 {
17547            return None;
17548        }
17549        // The burst builds the whole-token graph; attention the graph's
17550        // per-layer geometry cannot express keeps the per-token path.
17551        if let Some(reason) = self.wgpu_graph_attn_decline() {
17552            self.note_graph_decline("wgpu multi-burst", reason);
17553            return None;
17554        }
17555        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
17556        if !graph_on || self.graph_refused() {
17557            // Same memo as the decode site: this path builds the very
17558            // same graph, so a model it cannot build for must not be
17559            // walked again here either. Missing this guard was worth
17560            // 2.5x on an Adreno — 0.361 tok/s against 0.905 — because
17561            // the burst retried per token what decode had already given
17562            // up on.
17563            return None;
17564        }
17565        let emb = self.embed_single(t_next);
17566        let mut lg = Vec::new();
17567        let mut ids = Vec::new();
17568        match self.try_token_graph_wgpu_steps(
17569            &emb,
17570            position,
17571            &mut lg,
17572            k,
17573            Some(&mut ids),
17574            None,
17575            0,
17576            self.num_layers,
17577        ) {
17578            Some(Ok(_)) => {}
17579            Some(Err(())) => {
17580                // Preserve the backend's post-admission failure through the
17581                // Option-based burst API.  The decode caller consumes this
17582                // flag and clears the sequence instead of falling through
17583                // to a stale CPU recurrent state.
17584                self.graph_failed
17585                    .store(true, std::sync::atomic::Ordering::Relaxed);
17586                return None;
17587            }
17588            None => return None,
17589        }
17590        (ids.len() == k).then_some(ids)
17591    }
17592
17593    /// The sequence may keep f16 K/V mirrors on the wgpu graphs (`kv16`):
17594    /// nothing it runs reads f32 mirror rows — no MTP head, no drafter, no
17595    /// O(1) seal, no block preview.
17596    fn kv16_allowed(&self) -> bool {
17597        self.mtp.is_none()
17598            && self.dflash.is_none()
17599            && !self.o1_active()
17600            && !self.is_block_diffusion()
17601            && self.clover.is_none()
17602    }
17603
17604    /// Multi-step greedy: k whole frames in ONE submit, argmax and re-embed
17605    /// on the device. `ids_out` receives the k winner ids; the hidden/logits
17606    /// outputs are NOT produced in that mode.
17607    fn try_token_graph_wgpu_steps(
17608        &self,
17609        hidden: &[f32],
17610        position: usize,
17611        logits_out: &mut Vec<f32>,
17612        steps: usize,
17613        ids_out: Option<&mut Vec<u32>>,
17614        layers_run: Option<&mut usize>,
17615        from: usize,
17616        upto_excl: usize,
17617    ) -> Option<Result<Vec<f32>, ()>> {
17618        crate::gpu::graph_kv16_note(self.graph_kv_id, self.kv16_allowed());
17619        // SenseNova-Loop: MRoPE prompt rows rotate on the host; the graph's
17620        // mHC kernels take two streams only, and a graph must not enter an
17621        // mHC range midway (its streams would be the host's).
17622        if self.prompt_mrope.is_some()
17623            || self.mhc.as_deref().is_some_and(|m| {
17624                from < self.num_layers && m.on(self.phys_layer(from)) && !m.enters(self.phys_layer(from))
17625            })
17626            || self.mhc.as_ref().is_some_and(|m| m.s.streams != 2 || m.s.fixed_identity)
17627            || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
17628                && (from..upto_excl.min(self.num_layers)).any(|li| self.mhc_layer(li))
17629        {
17630            return None;
17631        }
17632        // The bank has already reserved its VRAM. Never build a second
17633        // all-expert arena across bank-owned layers (including bursts).
17634        let upto_excl = match self.mimo_moe.graph_prefix_end() {
17635            Some(end) if end < upto_excl => {
17636                if steps != 1 || layers_run.is_none() || from >= end {
17637                    return None;
17638                }
17639                end
17640            }
17641            _ => upto_excl,
17642        };
17643        // O(1) Nyström decode runs off the sealed state, not the KV cache the
17644        // graph mirrors — never take the graph while o1 is active.
17645        let o1_gpu = self.o1_on_graph();
17646        if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
17647            // Softcapped scores have no graph kernel yet — CPU owns them.
17648            // o1 rides the graph only behind CMF_O1_GPU=1 while the port
17649            // proves itself; without it the CPU path owns o1 as before.
17650            return None;
17651        }
17652        // Per-layer KV heads, narrow V, sinks and sliding windows ride
17653        // `GraphAttn::Full::geom` (the ATTEND_X kernels). Anything that
17654        // geometry cannot express declines here, by name — before the
17655        // per-layer gate existed a sliding/sink model ran the graph as
17656        // full-context attention, fluent and wrong. The caller memoizes
17657        // the refusal.
17658        if let Some(reason) = self.wgpu_graph_attn_decline() {
17659            self.note_graph_decline("wgpu token graph", reason);
17660            return None;
17661        }
17662        // Per-layer sealed o1 state for the graph. During prefill the
17663        // state is still Collecting -> views are None -> the graph
17664        // refuses below and the CPU prefill records the q trace and
17665        // seals, exactly as the o1 design requires.
17666        let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
17667            .map(|li| {
17668                if !o1_gpu {
17669                    return None;
17670                }
17671                self.kv_cache.layers[self.phys_layer(li)].o1_views()
17672            })
17673            .collect();
17674        if self.o1_active() && o1_gpu {
17675            // Any o1 layer not sealed (or degenerate exact-only) keeps the
17676            // whole token on the CPU: half-graph forwards would desync.
17677            let want: usize = (from..upto_excl)
17678                .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
17679                .count();
17680            let have = o1_views.iter().filter(|v| v.is_some()).count();
17681            if want == 0 || have != want {
17682                // The silent twin of the gpu-side o1 gates, found the
17683                // same way: a 15x decode drop with an empty log. Views
17684                // stay None until the layer's state SEALS, so `have`
17685                // lagging `want` early in a run is the o1 design working
17686                // — but it must say so, or the next reader spends a
17687                // night proving the kernels innocent.
17688                // On CHANGE, not once: the first decline is the legal
17689                // unsealed prefill, and a once-print buries the state
17690                // that matters — what the count reads AFTER the seal.
17691                use std::sync::atomic::{AtomicUsize, Ordering};
17692                static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
17693                let code = have * 1000 + want;
17694                if LAST.swap(code, Ordering::Relaxed) != code {
17695                    tracing::warn!(
17696                        "o1 graph: {have} of {want} layers sealed — per-op until all seal"
17697                    );
17698                }
17699                return None;
17700            }
17701        }
17702        let nh = self.num_heads;
17703        let (nkv, hd, rd) = self.layer_geom(0);
17704        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
17705        // Ripple's value residual: the embedding row of this position. A
17706        // span past the first layer reads the one `forward_layers_span` kept.
17707        let vres_e0: &[f32] = if from == 0 { hidden } else { &self.vres_e0 };
17708        let mut layers = Vec::with_capacity(upto_excl - from);
17709        let mut model = None;
17710        let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
17711        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
17712            // `blr` rides as GRAPH_KIND_BLR (two stage dispatches).
17713            if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
17714                let name = &m.tensors[i].name;
17715                let prism = if crate::prism::is_inverse_embedding(m, name) {
17716                    crate::gpu::GraphPrismOp::InverseEmbedding
17717                } else if crate::prism::is_forward_weight(m, name) {
17718                    crate::gpu::GraphPrismOp::Forward
17719                } else {
17720                    crate::gpu::GraphPrismOp::None
17721                };
17722                return Some(crate::gpu::GraphW {
17723                    idx: i,
17724                    kind,
17725                    row_scale: rs,
17726                    data: &[],
17727                    prism,
17728                    affine: crate::prism::is_affine_target(m, name),
17729                });
17730            }
17731            // Small unquantized projections (GDN in_proj_a/b) stay f32.
17732            match t.as_f32() {
17733                Some(d) => Some(crate::gpu::GraphW {
17734                    idx: 0,
17735                    kind: 4,
17736                    row_scale: &[],
17737                    data: d,
17738                    prism: crate::gpu::GraphPrismOp::None,
17739                    affine: false,
17740                }),
17741                None => {
17742                    if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
17743                        eprintln!("batch graph: weight has no graph/f32 representation");
17744                    }
17745                    None
17746                }
17747            }
17748        }
17749        for li in from..upto_excl {
17750            let lw = &self.weights.layers[self.phys_layer(li)];
17751            if dbg {
17752                let ak = match &lw.attn {
17753                    AttnKind::Mla(_) => "Mla".into(),
17754                    AttnKind::Full {
17755                        output_gate, bias, ..
17756                    } => format!("Full gate={output_gate} bias={}", bias.is_some()),
17757                    AttnKind::LinearGdn(_) => "LinearGdn".into(),
17758                    AttnKind::Kda(_) => "Kda".into(),
17759                    AttnKind::Linear(_) => "Linear".into(),
17760                    AttnKind::ShortConv(_) => "ShortConv".into(),
17761                    AttnKind::Bounded(_) => "Bounded".into(),
17762                };
17763                let fk = match &lw.ffn {
17764                    FfnKind::Dense(_) => "Dense",
17765                    FfnKind::Moe(_) => "Moe",
17766                    FfnKind::DenseMoe(_) => "DenseMoe",
17767                };
17768                eprintln!("graph L{li}: attn={ak} ffn={fk}");
17769            }
17770            let gffn = match &lw.ffn {
17771                FfnKind::DenseMoe(_) => return None, // dual branch: CPU path
17772                // A tube layer is several matrices, not one. The graph has
17773                // a shape for the core with one more triple that is always
17774                // on (a hot-neuron file); task tubes keep the segmented path.
17775                FfnKind::Dense(d) if d.segs.len() > 1 => return None,
17776                FfnKind::Dense(d) => {
17777                    // An activation the graph has no kernel arm for keeps
17778                    // the CPU/per-op owner, by name — the dense graph FFN
17779                    // used to compute SiLU for whatever the model asked.
17780                    let Some((act, limit)) = d.act.dense_graph_contract() else {
17781                        self.note_graph_decline(
17782                            "wgpu token graph",
17783                            "dense FFN activation without a graph kernel",
17784                        );
17785                        return None;
17786                    };
17787                    crate::gpu::GraphFfn::Dense {
17788                        gate: gw(&d.gate_proj)?,
17789                        up: gw(&d.up_proj)?,
17790                        down: gw(&d.down_proj)?,
17791                        act,
17792                        limit,
17793                        hot: match d.segs.first() {
17794                            Some(seg) => Some(crate::gpu::GraphMoeDetachedShared {
17795                                gate: gw(&seg.gate)?,
17796                                up: gw(&seg.up)?,
17797                                down: gw(&seg.down)?,
17798                                width: seg.width,
17799                            }),
17800                            None => None,
17801                        },
17802                    }
17803                }
17804                FfnKind::Moe(m) => {
17805                    // Adaptive τ and expert masks keep the CPU path, where
17806                    // they are implemented. Sigmoid routing with a selection
17807                    // bias (LFM2-MoE / DeepSeek noaux_tc), a routed scale ≠ 1
17808                    // and an UNGATED shared expert (HunYuan hy_v3: ×2.826 on
17809                    // the routed mix, the shared expert at weight 1) are all
17810                    // graphed — before, every such token fell to the per-op
17811                    // path whole (145 submits/token on Hy-MT2-30B-A3B).
17812                    if m.route_tau.is_some() || m.mask.is_some() {
17813                        return None;
17814                    }
17815                    let shared = m.shared.as_ref();
17816                    // The resident MoE kernels do not yet carry the
17817                    // descriptor-aware transform through router/shared-gate
17818                    // selection.  Refuse the complete layer instead of
17819                    // scoring with an untransformed Prism plane (the dense
17820                    // path has an explicit FWHT boundary below).
17821                    let inter = m.experts.first()?.gate_proj.rows();
17822                    // Flint's shared expert is a 1024-wide SwiGLU while its
17823                    // routed bank is 512-wide.  The resident MoE shader
17824                    // indexes a uniform-width packed bank, so keep that
17825                    // expert out of the routed slots and add it through the
17826                    // dense q4tp path after routed accumulation.  This is
17827                    // also the safe route for a mixed q2-routed/q4-shared
17828                    // artifact: neither layout nor row stride is faked.
17829                    let detached_shared = match shared {
17830                        Some((se, None))
17831                            if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
17832                        {
17833                            let q4 = se.gate_proj.mapped_q4tp().is_some()
17834                                && se.up_proj.mapped_q4tp().is_some()
17835                                && se.down_proj.mapped_q4tp().is_some();
17836                            let q8 = se.gate_proj.mapped_q8_2f().is_some()
17837                                && se.up_proj.mapped_q8_2f().is_some()
17838                                && se.down_proj.mapped_q8_2f().is_some();
17839                            if !matches!(se.act, Act::Silu) || !(q4 || q8) {
17840                                return None;
17841                            }
17842                            Some(crate::gpu::GraphMoeDetachedShared {
17843                                gate: gw(&se.gate_proj)?,
17844                                up: gw(&se.up_proj)?,
17845                                down: gw(&se.down_proj)?,
17846                                width: se.gate_proj.rows(),
17847                            })
17848                        }
17849                        // A separately shaped gated shared expert needs a
17850                        // second sigmoid gate path.  Do not turn it into an
17851                        // ungated one merely to make the graph run.
17852                        Some((se, Some(_)))
17853                            if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
17854                        {
17855                            return None;
17856                        }
17857                        _ => None,
17858                    };
17859                    let has_shared = shared.is_some() && detached_shared.is_none();
17860                    let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
17861                    let sgate = match shared {
17862                        Some((_, Some(sg))) if has_shared => gw(sg)?,
17863                        // No gate, no shared expert, or a detached shared
17864                        // expert: the router only keeps the binding total;
17865                        // the selection shader does not read it.
17866                        _ => gw(&m.router)?,
17867                    };
17868                    let router = gw(&m.router)?;
17869                    // The resident MoE kernels do not yet carry the
17870                    // descriptor-aware transform through router/shared-gate
17871                    // selection.  Refuse the complete layer instead of
17872                    // scoring with an untransformed Prism plane (the dense
17873                    // path has an explicit FWHT boundary below).
17874                    if router.prism != crate::gpu::GraphPrismOp::None
17875                        || sgate.prism != crate::gpu::GraphPrismOp::None
17876                        || router.affine
17877                        || sgate.affine
17878                    {
17879                        tracing::warn!(
17880                            "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
17881                        );
17882                        return None;
17883                    }
17884                    let in_bank_shared = shared.filter(|_| has_shared).map(|(se, _)| se);
17885                    let edge = |t: &QTensor| {
17886                        t.graph_weight()
17887                            .or_else(|| t.graph_weight_descriptor())
17888                            .map(|(mm, i, _, _)| (std::sync::Arc::as_ptr(mm) as usize, i))
17889                    };
17890                    let bank_key: Option<GraphMoeKey> = match (
17891                        m.experts.first().and_then(|e| edge(&e.gate_proj)),
17892                        m.experts.last().and_then(|e| edge(&e.down_proj)),
17893                    ) {
17894                        (Some((fm, fi)), Some((_, di))) if graph_moe_bank_on() => Some((
17895                            self.phys_layer(li),
17896                            m.experts.as_ptr() as usize,
17897                            m.experts.len(),
17898                            in_bank_shared.map_or(0, |se| std::ptr::from_ref(se) as usize),
17899                            fm,
17900                            fi,
17901                            di,
17902                        )),
17903                        _ => None,
17904                    };
17905                    let cached =
17906                        bank_key.and_then(|k| self.graph_moe_banks.lock().unwrap().get(&k).cloned());
17907                    let bank = if let Some(b) = cached {
17908                        b
17909                    } else {
17910                    let mut experts = Vec::with_capacity(m.experts.len() + 1);
17911                    let mut bank_model: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
17912                    // q4t or q4tp, but not both in one layer — the kernels
17913                    // are picked per layer, not per expert.
17914                    let mut q4tp: Option<bool> = None;
17915                    // The mixed 2-bit profile: q2tp gate/up over a q4tp
17916                    // down. Uniform across the layer, like `q4tp` itself.
17917                    let mut gu_q2: Option<bool> = None;
17918                    // q8_2f experts (gate, up and down): their own kernels.
17919                    let mut q82: Option<bool> = None;
17920                    // One MoE gate/up kernel serves the complete packed bank.
17921                    // Bind an activation only when every routed (and in-bank
17922                    // shared) expert owns the same exact WGPU contract.
17923                    let mut moe_act = None;
17924                    for e in m.experts.iter().chain(in_bank_shared.into_iter()) {
17925                        let Some(e_act) = e.act.wgpu_graph_act() else {
17926                            return None;
17927                        };
17928                        match moe_act {
17929                            Some(existing) if existing != e_act => return None,
17930                            None => moe_act = Some(e_act),
17931                            _ => {}
17932                        }
17933                        if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
17934                            return None;
17935                        }
17936                        // Expert tensors are packed into one resident buffer
17937                        // and the MoE kernels have no transform slot per
17938                        // expert.  Keep the CPU/per-op owner for Prism or
17939                        // affine experts rather than silently using raw bytes.
17940                        for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
17941                            let Some((em, ei, _, _)) = expert_weight
17942                                .graph_weight()
17943                                .or_else(|| expert_weight.graph_weight_descriptor())
17944                            else {
17945                                return None;
17946                            };
17947                            let name = &em.tensors[ei].name;
17948                            if crate::prism::is_forward_weight(em, name)
17949                                || crate::prism::is_inverse_embedding(em, name)
17950                                || crate::prism::is_affine_target(em, name)
17951                            {
17952                                tracing::warn!(
17953                                    "resident MoE declined: expert Prism/affine transform is not implemented"
17954                                );
17955                                return None;
17956                            }
17957                        }
17958                        let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
17959                            Some((mm, gi)) => (
17960                                mm,
17961                                gi,
17962                                e.up_proj.mapped_q4t()?.1,
17963                                e.down_proj.mapped_q4t()?.1,
17964                                false,
17965                                false,
17966                                false,
17967                            ),
17968                            None => match e.gate_proj.mapped_q2tp() {
17969                                Some((mm, gi)) => (
17970                                    mm,
17971                                    gi,
17972                                    e.up_proj.mapped_q2tp()?.1,
17973                                    e.down_proj.mapped_q4tp()?.1,
17974                                    true,
17975                                    true,
17976                                    false,
17977                                ),
17978                                None => match e.gate_proj.mapped_q4tp() {
17979                                    Some((mm, gi)) => (
17980                                        mm,
17981                                        gi,
17982                                        e.up_proj.mapped_q4tp()?.1,
17983                                        e.down_proj.mapped_q4tp()?.1,
17984                                        true,
17985                                        false,
17986                                        false,
17987                                    ),
17988                                    // q8_2f experts: all three projections,
17989                                    // each with its own column field.
17990                                    None => {
17991                                        if !crate::gpu::moe_q82_graph_on() {
17992                                            return None;
17993                                        }
17994                                        let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
17995                                        (
17996                                            mm,
17997                                            gi,
17998                                            e.up_proj.mapped_q8_2f()?.1,
17999                                            e.down_proj.mapped_q8_2f()?.1,
18000                                            false,
18001                                            false,
18002                                            true,
18003                                        )
18004                                    }
18005                                },
18006                            },
18007                        };
18008                        if *q4tp.get_or_insert(is_p) != is_p
18009                            || *gu_q2.get_or_insert(is_q2) != is_q2
18010                            || *q82.get_or_insert(is_8) != is_8
18011                        {
18012                            // The shared expert rides in the same packed
18013                            // buffer as the routed ones, so a layer that
18014                            // mixes layouts cannot be indexed by one stride.
18015                            // Say so: the symptom is a whole model quietly
18016                            // running its MoE on the CPU.
18017                            tracing::warn!(
18018                                "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."
18019                            );
18020                            return None;
18021                        }
18022                        bank_model.get_or_insert_with(|| mm.clone());
18023                        experts.push((gi, ui, di));
18024                    }
18025                    let b = GraphMoeBank {
18026                        experts,
18027                        q4tp,
18028                        gu_q2,
18029                        q82,
18030                        act: moe_act,
18031                        model: bank_model,
18032                    };
18033                    if let Some(k) = bank_key {
18034                        self.graph_moe_banks.lock().unwrap().insert(k, b.clone());
18035                    }
18036                    b
18037                    };
18038                    if let Some(mm) = &bank.model {
18039                        model.get_or_insert_with(|| mm.clone());
18040                    }
18041                    crate::gpu::GraphFfn::Moe {
18042                        router,
18043                        shared_gate: sgate,
18044                        experts: bank.experts,
18045                        n_exp: m.experts.len(),
18046                        // CMF_TOPK_PROBE: timing probe only — output is WRONG.
18047                        // Fewer experts shrink the MoE arithmetic while the
18048                        // dispatch count stays identical, which is the only
18049                        // clean way to tell a launch-bound decode from a
18050                        // compute-bound one.
18051                        top_k: std::env::var("CMF_TOPK_PROBE")
18052                            .ok()
18053                            .and_then(|v| v.parse::<usize>().ok())
18054                            .filter(|k| *k > 0 && *k <= m.top_k)
18055                            .unwrap_or(m.top_k),
18056                        inter,
18057                        act: bank.act?,
18058                        norm_topk: m.norm_topk_prob,
18059                        q4tp: bank.q4tp?,
18060                        gu_q2: bank.gu_q2.unwrap_or(false),
18061                        q82: bank.q82.unwrap_or(false),
18062                        sigmoid: m.router_sigmoid,
18063                        sigmoid_denom_eps: m.sigmoid_denom_eps,
18064                        bias: m.expert_bias.as_deref(),
18065                        has_shared,
18066                        detached_shared,
18067                        shared_gated,
18068                        route_scale: m.routed_scaling,
18069                    }
18070                }
18071            };
18072            let attn = match &lw.attn {
18073                AttnKind::Full {
18074                    wq,
18075                    wk,
18076                    wv,
18077                    wo,
18078                    q_norm,
18079                    k_norm,
18080                    output_gate,
18081                    softplus_gate,
18082                    bias,
18083                } => {
18084                    if self.attention_heads_per_layer.is_some() {
18085                        return None;
18086                    }
18087                    // A projected output gate rides the graph in one form:
18088                    // Spark-X2.5's head-wise sigmoid (one g_proj row per Q
18089                    // head). Laguna's softplus gate keeps the CPU path.
18090                    let head_gate = match softplus_gate {
18091                        None => None,
18092                        Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
18093                        Some(_) => {
18094                            self.note_graph_decline(
18095                                "wgpu token graph",
18096                                "projected softplus / per-element output gate",
18097                            );
18098                            return None;
18099                        }
18100                    };
18101                    let (m, _, _, _) = wq.graph_weight_or_blr()?;
18102                    model = Some(m.clone());
18103                    crate::gpu::GraphAttn::Full {
18104                        wq: gw(wq)?,
18105                        wk: gw(wk)?,
18106                        wv: gw(wv)?,
18107                        wo: gw(wo)?,
18108                        q_norm: q_norm.as_deref(),
18109                        k_norm: k_norm.as_deref(),
18110                        late_qk_norm: self.qk_norm_after_rope,
18111                        bias: bias
18112                            .as_ref()
18113                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18114                        output_gate: *output_gate,
18115                        cpu_k: self.kv_cache.layers[li].k_heads(),
18116                        cpu_v: self.kv_cache.layers[li].v_heads(),
18117                        cpu_base: self.kv_cache.layers[li].base(),
18118                        geom: self.graph_attn_geom_vres(li, vres_e0),
18119                        head_gate,
18120                    }
18121                }
18122                AttnKind::LinearGdn(w) => {
18123                    let cfg = self.gdn_cfg?;
18124                    let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
18125                    model = Some(m.clone());
18126                    crate::gpu::GraphAttn::Gdn {
18127                        qkv: gw(&w.in_proj_qkv)?,
18128                        z: gw(&w.in_proj_z)?,
18129                        a: gw(&w.in_proj_a)?,
18130                        b: gw(&w.in_proj_b)?,
18131                        out: gw(&w.out_proj)?,
18132                        conv1d: &w.conv1d,
18133                        a_log: &w.a_log,
18134                        dt_bias: &w.dt_bias,
18135                        norm: &w.norm,
18136                        nv: cfg.num_v_heads,
18137                        nk: cfg.num_k_heads,
18138                        dk: cfg.key_head_dim,
18139                        dv: cfg.value_head_dim,
18140                        kk: cfg.conv_kernel,
18141                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
18142                        preview: None,
18143                    }
18144                }
18145                AttnKind::ShortConv(w) => {
18146                    let cfg = self.short_conv_cfg?;
18147                    let (m, _, _, _) = w
18148                        .in_proj
18149                        .graph_weight()
18150                        .or_else(|| w.in_proj.graph_weight_descriptor())?;
18151                    model = Some(m.clone());
18152                    crate::gpu::GraphAttn::ShortConv {
18153                        inp: gw(&w.in_proj)?,
18154                        out: gw(&w.out_proj)?,
18155                        taps: &w.conv,
18156                        kernel: cfg.kernel,
18157                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
18158                    }
18159                }
18160                _ => return None,
18161            };
18162            layers.push(crate::gpu::GraphLayer {
18163                mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(li)),
18164                input_norm: &lw.input_norm,
18165                attn,
18166                post_norm: &lw.post_norm,
18167                attn_out_norm: lw.attn_out_norm.as_deref(),
18168                ffn_out_norm: lw.ffn_out_norm.as_deref(),
18169                layer_scale: lw.layer_scale,
18170                ffn: gffn,
18171            });
18172        }
18173        let model = model?;
18174        // Fold final-norm + lm_head into the graph when this call wants logits
18175        // and the lm_head is a graphable (quantized) weight — the graph then
18176        // reads back logits (into logits_out) instead of the hidden, dropping
18177        // the separate CPU/GPU lm_head op + its sync. Never the f32 fallback:
18178        // an unquantized lm_head is vocab·hidden and must not be uploaded.
18179        let lm_gw = if upto_excl == self.num_layers
18180            && self.graph_want_logits
18181            && std::env::var("CMF_GPU_LMHEAD")
18182                .map(|v| v != "0")
18183                .unwrap_or(true)
18184        {
18185            self.weights
18186                .lm_head
18187                .graph_weight()
18188                .or_else(|| self.weights.lm_head.graph_weight_descriptor())
18189                .map(|(m, i, kind, rs)| {
18190                    let name = &m.tensors[i].name;
18191                    let prism = if crate::prism::is_inverse_embedding(m, name) {
18192                        crate::gpu::GraphPrismOp::InverseEmbedding
18193                    } else if crate::prism::is_forward_weight(m, name) {
18194                        crate::gpu::GraphPrismOp::Forward
18195                    } else {
18196                        crate::gpu::GraphPrismOp::None
18197                    };
18198                    (
18199                        crate::gpu::GraphW {
18200                            idx: i,
18201                            kind,
18202                            row_scale: rs,
18203                            data: &[],
18204                            prism,
18205                            affine: crate::prism::is_affine_target(m, name),
18206                        },
18207                        self.weights.lm_head.rows(),
18208                    )
18209                })
18210        } else {
18211            None
18212        };
18213        let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
18214        // Multi-step re-embeds the winner on the device.
18215        let emb_gw = if steps > 1 {
18216            self.weights
18217                .embed_tokens
18218                .graph_weight()
18219                .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
18220                .map(|(m, i, kind, rs)| {
18221                    let name = &m.tensors[i].name;
18222                    let prism = if crate::prism::is_inverse_embedding(m, name) {
18223                        crate::gpu::GraphPrismOp::InverseEmbedding
18224                    } else if crate::prism::is_forward_weight(m, name) {
18225                        crate::gpu::GraphPrismOp::Forward
18226                    } else {
18227                        crate::gpu::GraphPrismOp::None
18228                    };
18229                    (
18230                        crate::gpu::GraphW {
18231                            idx: i,
18232                            kind,
18233                            row_scale: rs,
18234                            data: &[],
18235                            prism,
18236                            affine: crate::prism::is_affine_target(m, name),
18237                        },
18238                        self.weights.embed_tokens.rows(),
18239                        self.embed_multiplier,
18240                    )
18241                })
18242        } else {
18243            None
18244        };
18245
18246        // Loop boundaries: virtual layer indices after which final_norm is
18247        // applied (mid-stack only; the GLOBAL last layer's norm folds into
18248        // lm_head). Span-relative — the executor compares its enumerate
18249        // index. A span ending mid-stack keeps its boundary norm even when
18250        // it is the span's own last layer.
18251        let loop_norm_at: Vec<usize> = if self.loop_final_norm {
18252            (from..upto_excl.min(self.num_layers - 1))
18253                .filter(|&li| (li + 1) % self.physical_layers == 0)
18254                .map(|li| li - from)
18255                .collect()
18256        } else {
18257            Vec::new()
18258        };
18259        let mut h = hidden.to_vec();
18260        // The normal decode path only needs the fused lm-head logits.  A
18261        // CMF_LOGIT_DUMP diagnostic, however, promises a prompt-boundary
18262        // post-stack hidden alongside those logits; request the existing
18263        // second readback only for that explicit probe instead of dumping
18264        // the input copy left in `h` by a folded-head graph.
18265        let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
18266        let taps = if steps == 1 { self.dflash_taps_wanted(from, upto_excl) } else { Vec::new() };
18267        crate::gpu::layer_taps_arm(&taps);
18268        let outcome = crate::gpu::forward_token_graph(
18269            &model,
18270            self.graph_kv_id,
18271            &layers,
18272            &o1_views,
18273            self.o1_epoch,
18274            &self.inv_freq,
18275            &mut h,
18276            nh,
18277            nkv,
18278            hd,
18279            self.attn_scale,
18280            rd,
18281            self.hidden_size,
18282            self.intermediate_size,
18283            position,
18284            self.kv_cache.max_seq_len,
18285            gemma,
18286            self.rms_eps as f32,
18287            lm,
18288            &self.weights.final_norm,
18289            logits_out,
18290            &loop_norm_at,
18291            steps,
18292            emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
18293            ids_out,
18294            layers_run,
18295            from,
18296            dump_hidden,
18297        );
18298        self.dflash_taps_keep(&taps, position, 1, outcome == crate::gpu::TokenGraphOutcome::Completed);
18299        match outcome {
18300            crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
18301            crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
18302            crate::gpu::TokenGraphOutcome::Declined => None,
18303        }
18304    }
18305
18306    /// Batched prefill: k contiguous prompt positions through the whole wgpu
18307    /// graph in ONE submit (projections/FFN as GEMMs). `hiddens` is [k·hidden]
18308    /// in/out (embeddings in, layer output out); KV mirror / GDN state advance.
18309    /// false ⇒ unsupported → caller keeps the per-position graph.
18310    /// The b-row Metal graph plan for the whole model: every layer as a
18311    /// GDN run or a full-attention item, all-or-nothing (a layer outside the
18312    /// graph's contract → None, the caller runs plain). Shared by the
18313    /// speculative verify and the batched prefill.
18314    #[cfg(target_os = "macos")]
18315    #[allow(clippy::type_complexity)]
18316    fn metal_rows_plan(
18317        &self,
18318    ) -> Option<(
18319        Vec<MetalRowsItem<'_>>,
18320        std::sync::Arc<cortiq_core::CmfModel>,
18321        Option<crate::gpu_metal::GdnGpuCfg>,
18322    )> {
18323        use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
18324        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
18325        if !graph_force
18326            || !crate::gpu::enabled_here()
18327            || std::env::var("CMF_GPU_BLOCK")
18328                .map(|v| v == "0")
18329                .unwrap_or(false)
18330            || self.attn_softcap > 0.0
18331            || (self.o1_active() && !self.o1_on_graph())
18332            // Sliding windows with per-layer RoPE (Spark-X2.6) ride the rows graph
18333            // as they ride the chunk graph (`metal_graph_swa`).
18334            || (self.swa.is_some() && !self.metal_graph_swa())
18335            || self.global_attn.is_some()
18336            || self.attention_heads_per_layer.is_some()
18337            // per-layer KV heads, narrow V, learned sinks (MiMo-V2)
18338            || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa())
18339            || self.attn_v_norm
18340            || self.loop_final_norm
18341            || !self.metal_rope_scale_ok()
18342        {
18343            return None;
18344        }
18345        let attend_contract = self.head_dim % 4 == 0
18346            && self.head_dim <= 256
18347            && self.rotary_dim >= 2
18348            && self.rotary_dim <= self.head_dim
18349            && (self.rotary_dim / 2) % 32 == 0
18350            && self.num_kv_heads > 0
18351            && self.num_heads % self.num_kv_heads == 0;
18352        if !attend_contract {
18353            return None;
18354        }
18355        let mut plan: Vec<MetalRowsItem> = Vec::new();
18356        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
18357        for li in 0..self.num_layers {
18358            let lw = &self.weights.layers[self.phys_layer(li)];
18359            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
18360                return None;
18361            }
18362            let ffn = match &lw.ffn {
18363                FfnKind::Dense(d) if matches!(d.act, Act::Silu | Act::Gelu) && d.segs.is_empty() => {
18364                    let (Some(g), Some(u), Some(dn)) = (
18365                        d.gate_proj.metal_graph_parts(),
18366                        d.up_proj.metal_graph_parts(),
18367                        d.down_proj.metal_graph_parts(),
18368                    ) else {
18369                        return None;
18370                    };
18371                    MetalFfn::Dense {
18372                        gate: g,
18373                        up: u,
18374                        down: dn,
18375                        // SiLU, or the exact GELU of Spark-X2.6 (`gelu_rows`).
18376                        act: if d.act == Act::Gelu { 1 } else { 0 },
18377                    }
18378                }
18379                _ => return None,
18380            };
18381            match &lw.attn {
18382                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
18383                    let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
18384                        w.in_proj_qkv.metal_graph_parts(),
18385                        w.in_proj_z.metal_graph_parts(),
18386                        w.in_proj_a.f32_parts(),
18387                        w.in_proj_b.f32_parts(),
18388                        w.out_proj.metal_graph_parts(),
18389                    ) else {
18390                        return None;
18391                    };
18392                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
18393                        model_ref.get_or_insert_with(|| model.clone());
18394                    }
18395                    let gl = GdnGpuLayer {
18396                        mhc: None,
18397                        attn_norm: &lw.input_norm,
18398                        post_norm: &lw.post_norm,
18399                        qkv,
18400                        z,
18401                        a,
18402                        b: bb,
18403                        out,
18404                        ffn,
18405                        conv1d: &w.conv1d,
18406                        a_log: &w.a_log,
18407                        dt_bias: &w.dt_bias,
18408                        gnorm: &w.norm,
18409                    };
18410                    match plan.last_mut() {
18411                        Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
18412                        _ => plan.push(MetalRowsItem::Gdn {
18413                            run: vec![gl],
18414                            first: li,
18415                        }),
18416                    }
18417                }
18418                AttnKind::Full {
18419                    wq,
18420                    wk,
18421                    wv,
18422                    wo,
18423                    q_norm,
18424                    k_norm,
18425                    output_gate,
18426                    softplus_gate,
18427                    bias: None,
18428                } => {
18429                    // Spark-X2.6's per-head sigmoid g_proj gate, from f32 rows.
18430                    let head_gate = match softplus_gate {
18431                        None => None,
18432                        Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => match g.f32_parts() {
18433                            Some((d, r, c)) if r == self.num_heads && c == self.hidden_size => Some(d),
18434                            _ => return None,
18435                        },
18436                        Some(_) => return None,
18437                    };
18438                    let (Some(pq), Some(pk), Some(pv), Some(po)) = (
18439                        wq.metal_graph_parts(),
18440                        wk.metal_graph_parts(),
18441                        wv.metal_graph_parts(),
18442                        wo.metal_graph_parts(),
18443                    ) else {
18444                        return None;
18445                    };
18446                    if let QTensor::Mapped { model, .. } = wq {
18447                        model_ref.get_or_insert_with(|| model.clone());
18448                    }
18449                    let cache = &self.kv_cache.layers[li];
18450                    if cache.mode != crate::kv_cache::KvMode::F32 || (cache.o1.is_some() && !self.o1_on_graph()) {
18451                        return None;
18452                    }
18453                    plan.push(MetalRowsItem::Attn {
18454                        l: AttnGpuLayer {
18455                            mhc: None,
18456                            attn_norm: &lw.input_norm,
18457                            post_norm: &lw.post_norm,
18458                            wq: pq,
18459                            wk: pk,
18460                            wv: pv,
18461                            wo: po,
18462                            ffn,
18463                            attn_out_norm: None,
18464                            ffn_out_norm: None,
18465                            layer_scale: None,
18466                        },
18467                        li,
18468                        q_norm: q_norm.as_deref(),
18469                        k_norm: k_norm.as_deref(),
18470                        output_gate: *output_gate,
18471                        head_gate,
18472                    });
18473                }
18474                _ => return None,
18475            }
18476        }
18477        let model = model_ref?;
18478        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
18479            nv: cfg.num_v_heads,
18480            nk: cfg.num_k_heads,
18481            dk: cfg.key_head_dim,
18482            dv: cfg.value_head_dim,
18483            kk: cfg.conv_kernel,
18484            hidden: self.hidden_size,
18485            inter: self.intermediate_size,
18486            c_dim: cfg.conv_dim(),
18487            eps: cfg.rms_eps as f32,
18488            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
18489        });
18490        Some((plan, model, gcfg))
18491    }
18492
18493    /// `AttnDeviceParams` for a plan item over the CPU cache as it stands.
18494    #[cfg(target_os = "macos")]
18495    #[allow(clippy::too_many_arguments)]
18496    fn metal_attn_params<'a>(
18497        li: usize,
18498        cache: &'a crate::kv_cache::LayerKvCache,
18499        q_norm: Option<&'a [f32]>,
18500        k_norm: Option<&'a [f32]>,
18501        output_gate: bool,
18502        inv_freq: &'a [f32],
18503        rope_scale: f32,
18504        geom: (usize, usize, usize, usize),
18505        pos0: usize,
18506        kv_id: u64,
18507        scale: f32,
18508        eps: f32,
18509        gemma: bool,
18510        late_qk_norm: bool,
18511        o1_epoch: u64,
18512        head_gate: Option<&'a [f32]>,
18513        window: Option<usize>,
18514    ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
18515        let (nh, nkv, hd, rd) = geom;
18516        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
18517        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
18518        let cpu_stored = cpu_k[0].len() / hd;
18519        (
18520            crate::gpu_metal::AttnDeviceParams {
18521                kv_id,
18522                layer: li,
18523                nh,
18524                nkv,
18525                hd,
18526                rd,
18527                position: pos0,
18528                scale,
18529                eps,
18530                gemma,
18531                late_qk_norm,
18532                output_gate,
18533                q_norm,
18534                k_norm,
18535                inv_freq,
18536                rope_scale,
18537                cpu_k,
18538                cpu_v,
18539                cpu_stored,
18540                cpu_gen: cache.generation(),
18541                // A layer still collecting has no views, and its rows go through the mirror.
18542                o1: cache.o1_views().map(|views| crate::gpu_metal::O1AttnParams { views, epoch: o1_epoch }),
18543                window,
18544                softmax_sink: cache.sinks.as_deref(),
18545                head_gate,
18546                v_norm: false,
18547                dash: None,
18548            },
18549            cpu_stored,
18550        )
18551    }
18552
18553    /// Run the rows plan over `hiddens` (b rows at `pos0..`): validate,
18554    /// encode every item, optionally the head, sync. Returns the graph
18555    /// (for the commit / state finish) plus the GDN layer indices and the
18556    /// attention layers with the row count they were encoded against.
18557    #[cfg(target_os = "macos")]
18558    #[allow(clippy::type_complexity)]
18559    fn metal_rows_run(
18560        &mut self,
18561        hiddens: &mut [f32],
18562        pos0: usize,
18563        b: usize,
18564        prefill: bool,
18565        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
18566        // Greedy verify: (row length scored, the b argmax ids out) — the
18567        // head's argmax runs on the device and the logits plane is NOT
18568        // read back (`spec.2` stays empty).
18569        mut argmax_out: Option<(usize, &mut Vec<u32>)>,
18570    ) -> MetalRowsRun {
18571        use crate::gpu_metal::{GraphDims, VerifyGraph};
18572        // The previous round's commit may still be replaying into the
18573        // trunk GDN owners on the second queue: this graph reads them
18574        // (zero-copy wraps) and may reallocate them below — collect the
18575        // replay first. Normally already complete (the draft chain ran
18576        // in between); a failed replay is terminal like a failed commit.
18577        if !crate::gpu_metal::wait_replay() {
18578            tracing::error!("Metal rows graph: the pending async replay failed");
18579            return MetalRowsRun::Failed;
18580        }
18581        spec_stamp("v.wait");
18582        // Seed the GDN recurrent records the rows graph reads — GDN layers
18583        // ONLY (the record the CPU path would allocate anyway).  Sizing
18584        // every layer here planted a zero GDN-sized record on a bounded
18585        // anchor of a natively bounded file this path then refused, and
18586        // that record counted as recurrent state on macOS.
18587        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
18588        if want > 0 {
18589            let phys = self.physical_layers.max(1);
18590            for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
18591                let is_gdn = self
18592                    .weights
18593                    .layers
18594                    .get(li % phys)
18595                    .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
18596                if is_gdn && l.linear_state.len() != want {
18597                    l.linear_state = vec![0f32; want];
18598                }
18599            }
18600        }
18601        let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
18602            return MetalRowsRun::Declined;
18603        };
18604        spec_stamp("v.plan");
18605        let dims = GraphDims {
18606            hidden: self.hidden_size,
18607            eps: self.rms_eps as f32,
18608            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
18609        };
18610        // A prompt chunk for the Neural Engine goes as two halves, a thread each: one is on the
18611        // ANE while the GPU has the other.
18612        let hs = self.hidden_size;
18613        let relay = prefill
18614            && spec.is_none()
18615            && b >= 128
18616            && crate::gpu_metal::ane::wanted()
18617            && std::env::var("CMF_ANE_RELAY").as_deref() != Ok("0");
18618        let cuts: Vec<(usize, usize)> = if relay { vec![(0, b / 2), (b / 2, b - b / 2)] } else { vec![(0, b)] };
18619        let gate = crate::gpu_metal::LaneGate::new();
18620        let mut lanes: Vec<VerifyGraph> = Vec::with_capacity(cuts.len());
18621        for (ln, &(r0, n)) in cuts.iter().enumerate() {
18622            let h = &hiddens[r0 * hs..(r0 + n) * hs];
18623            let g = if relay {
18624                VerifyGraph::new_prefill_lane(&model, dims, h, n, ln, b - r0 - n, &gate)
18625            } else if prefill {
18626                VerifyGraph::new_prefill(&model, dims, h, n)
18627            } else {
18628                VerifyGraph::new(&model, dims, h, n)
18629            };
18630            let Some(g) = g else {
18631                return MetalRowsRun::Declined;
18632            };
18633            lanes.push(g);
18634        }
18635        if prefill {
18636            use crate::gpu_metal::{AneWants, MetalFfn};
18637            // SiLU or the exact GELU (Spark-X2.6), one activation for the file's FFNs.
18638            let mut ane_act = None;
18639            let ffn = |f: &MetalFfn<'_>, ane_act: &mut Option<u8>| match f {
18640                MetalFfn::Dense { gate, up, down, act } if *act <= 1 && ane_act.is_none_or(|a| a == *act) => {
18641                    *ane_act = Some(*act);
18642                    Some([*gate, *up, *down])
18643                }
18644                _ => None,
18645            };
18646            let mut wants = Vec::new();
18647            for item in &plan {
18648                match item {
18649                    MetalRowsItem::Gdn { run, .. } => {
18650                        wants.extend(run.iter().map(|l| AneWants {
18651                            ffn: ffn(&l.ffn, &mut ane_act),
18652                            mix: if crate::gpu_metal::ane_takes_qkv() { vec![l.qkv, l.z] } else { vec![l.z] },
18653                        }))
18654                    }
18655                    MetalRowsItem::Attn { l, .. } => wants.push(AneWants {
18656                        ffn: ffn(&l.ffn, &mut ane_act),
18657                        mix: if crate::gpu_metal::ane_takes_qkv() {
18658                            vec![l.wk, l.wv, l.wq]
18659                        } else if crate::gpu_metal::ane_takes_q() {
18660                            vec![l.wq]
18661                        } else {
18662                            Vec::new()
18663                        },
18664                    }),
18665                }
18666            }
18667            crate::gpu_metal::ane::set_act(ane_act.unwrap_or(0));
18668            for g in lanes.iter_mut() {
18669                g.ane_attach(&wants);
18670            }
18671            lanes[0].ane_begin(lanes.len());
18672        }
18673        // With the Neural Engine a layer's command is committed as it ends, so a refusal past
18674        // the first one leaves states half written.
18675        let hard = relay || lanes[0].has_ane();
18676        let geom = (
18677            self.num_heads,
18678            self.num_kv_heads,
18679            self.head_dim,
18680            self.rotary_dim,
18681        );
18682        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
18683        let eps = self.rms_eps as f32;
18684        let kv_id = self.graph_kv_id;
18685        // Each layer's own RoPE table and width (Spark-X2.6: sliding and full layers differ).
18686        let tables: Vec<std::sync::Arc<Vec<f32>>> = (0..self.num_layers).map(|li| self.layer_inv_freq(li)).collect();
18687        let lgeom = |li: usize| (geom.0, geom.1, geom.2, self.layer_geom(li).2);
18688        for item in &plan {
18689            let ok = match item {
18690                MetalRowsItem::Gdn { run, .. } => gcfg
18691                    .as_ref()
18692                    .map(|gc| lanes.iter().all(|g| run.iter().all(|l| g.gdn_ok(l, gc))))
18693                    .unwrap_or(false),
18694                MetalRowsItem::Attn {
18695                    l,
18696                    li,
18697                    q_norm,
18698                    k_norm,
18699                    output_gate,
18700                    head_gate,
18701                } => {
18702                    let (p, _) = Self::metal_attn_params(
18703                        *li,
18704                        &self.kv_cache.layers[*li],
18705                        *q_norm,
18706                        *k_norm,
18707                        *output_gate,
18708                        &tables[*li],
18709                        self.layer_rope_scale(*li),
18710                        lgeom(*li),
18711                        pos0,
18712                        kv_id,
18713                        self.attn_scale,
18714                        eps,
18715                        gemma,
18716                        self.qk_norm_after_rope,
18717                        self.o1_epoch,
18718                        *head_gate,
18719                        self.layer_window(*li),
18720                    );
18721                    lanes.iter().all(|g| g.attn_ok(l, &p))
18722                }
18723            };
18724            if !ok {
18725                use std::sync::atomic::{AtomicBool, Ordering};
18726                static SAID: AtomicBool = AtomicBool::new(false);
18727                if !SAID.swap(true, Ordering::Relaxed) {
18728                    tracing::warn!("metal rows graph: a layer failed preflight — declining");
18729                }
18730                return MetalRowsRun::Declined;
18731            }
18732        }
18733        let lm = match &spec {
18734            Some((lm, _, _)) => {
18735                if !lanes[0].lm_head_ok(*lm) {
18736                    return MetalRowsRun::Declined;
18737                }
18738                Some(*lm)
18739            }
18740            None => None,
18741        };
18742        // What each half encodes, layer by layer: nothing here changes while they run.
18743        enum Step<'a> {
18744            Gdn(&'a crate::gpu_metal::GdnGpuLayer<'a>, &'a [f32]),
18745            Attn(&'a crate::gpu_metal::AttnGpuLayer<'a>, crate::gpu_metal::AttnDeviceParams<'a>),
18746        }
18747        let mut gdn_layers = Vec::new();
18748        let mut attn_layers = Vec::new();
18749        let mut steps: Vec<Vec<Step<'_>>> = cuts.iter().map(|_| Vec::new()).collect();
18750        // the layer each step is, for the rows a DFlash drafter reads
18751        let mut step_layer: Vec<usize> = Vec::new();
18752        for item in &plan {
18753            match item {
18754                MetalRowsItem::Gdn { run, first } => {
18755                    for (l, kv) in run.iter().zip(&self.kv_cache.layers[*first..*first + run.len()]) {
18756                        for st in steps.iter_mut() {
18757                            st.push(Step::Gdn(l, kv.linear_state.as_slice()));
18758                        }
18759                    }
18760                    gdn_layers.extend(*first..*first + run.len());
18761                    step_layer.extend(*first..*first + run.len());
18762                }
18763                MetalRowsItem::Attn {
18764                    l,
18765                    li,
18766                    q_norm,
18767                    k_norm,
18768                    output_gate,
18769                    head_gate,
18770                } => {
18771                    for (ln, st) in steps.iter_mut().enumerate() {
18772                        let (mut p, cpu_stored) = Self::metal_attn_params(
18773                            *li,
18774                            &self.kv_cache.layers[*li],
18775                            *q_norm,
18776                            *k_norm,
18777                            *output_gate,
18778                            &tables[*li],
18779                            self.layer_rope_scale(*li),
18780                            lgeom(*li),
18781                            pos0 + cuts[ln].0,
18782                            kv_id,
18783                            self.attn_scale,
18784                            eps,
18785                            gemma,
18786                            self.qk_norm_after_rope,
18787                            self.o1_epoch,
18788                            *head_gate,
18789                            self.layer_window(*li),
18790                        );
18791                        // The half before leaves its rows in the mirror, not yet in the host's.
18792                        p.cpu_stored += cuts[ln].0;
18793                        // A sealed layer leaves no mirror row to pull.
18794                        if ln == 0 && p.o1.is_none() {
18795                            attn_layers.push((*li, cpu_stored));
18796                        }
18797                        st.push(Step::Attn(l, p));
18798                    }
18799                    step_layer.push(*li);
18800                }
18801            }
18802        }
18803        // The outputs of the layers a DFlash drafter reads are kept on the way
18804        // (one graph only: a chunk in halves through the Neural Engine keeps none).
18805        let taps: Vec<usize> = match &self.dflash {
18806            Some(d) if !relay => d.g.taps.clone(),
18807            _ => Vec::new(),
18808        };
18809        let tap_slot: Vec<Option<usize>> = step_layer.iter().map(|l| taps.iter().position(|t| t == l)).collect();
18810        let tapped = std::sync::atomic::AtomicUsize::new(0);
18811        // One half's layers in order; `Err` tells whether a command was already committed.
18812        let gc = gcfg.as_ref();
18813        let walk = |g: &mut VerifyGraph, steps: &[Step<'_>]| -> Result<(), bool> {
18814            for (i, step) in steps.iter().enumerate() {
18815                let ok = match step {
18816                    Step::Gdn(l, state) => match gc {
18817                        Some(gc) => g.encode_gdn_run_b(std::slice::from_ref(*l), &[*state], gc),
18818                        None => false,
18819                    },
18820                    Step::Attn(l, p) => g.encode_attn_b(l, p),
18821                };
18822                if !ok {
18823                    return Err(i > 0);
18824                }
18825                if let Some(slot) = tap_slot.get(i).copied().flatten() {
18826                    if g.tap(slot, taps.len()) {
18827                        tapped.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
18828                    }
18829                }
18830            }
18831            Ok(())
18832        };
18833        let refused = |started: bool| if hard && started { MetalRowsRun::Failed } else { MetalRowsRun::Declined };
18834        if relay {
18835            let (first, second) = lanes.split_at_mut(1);
18836            let (sa, sb) = (&steps[0], &steps[1]);
18837            let (ra, rb) = std::thread::scope(|sc| {
18838                let other = sc.spawn(|| {
18839                    let r = walk(&mut second[0], sb);
18840                    if r.is_err() {
18841                        // The first half must not wait for this one's turn on the ANE.
18842                        crate::gpu_metal::ane::retire("a half of the chunk stopped");
18843                    }
18844                    r
18845                });
18846                let r = walk(&mut first[0], sa);
18847                if r.is_err() {
18848                    gate.fail();
18849                }
18850                (r, other.join().unwrap_or(Err(true)))
18851            });
18852            if ra.is_err() || rb.is_err() {
18853                return MetalRowsRun::Failed;
18854            }
18855        } else if let Err(started) = walk(&mut lanes[0], &steps[0]) {
18856            return refused(started);
18857        }
18858        drop(steps);
18859        if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
18860            if !lanes[0].encode_lm_head_b(final_norm, lm) {
18861                return refused(true);
18862            }
18863            // The device argmax is an OPTIMISATION, never a reason to
18864            // decline the round: if it will not encode, drop it and read
18865            // the logits plane back the old way (the head is encoded
18866            // either way, so the rows are there).
18867            if let Some((n, _)) = argmax_out.as_ref() {
18868                if !lanes[0].encode_argmax_b(*n) {
18869                    argmax_out = None;
18870                }
18871            }
18872        }
18873        spec_stamp("v.enc");
18874        for g in lanes.iter_mut() {
18875            if !g.sync() {
18876                return MetalRowsRun::Failed;
18877            }
18878        }
18879        spec_stamp("v.gpu");
18880        match (spec, argmax_out) {
18881            (Some(_), Some((_, ids))) => {
18882                ids.resize(b, 0);
18883                if !lanes[0].read_argmax(ids) {
18884                    return MetalRowsRun::Failed;
18885                }
18886                spec_stamp("v.am");
18887            }
18888            (Some((lm, _, logits)), None) => {
18889                logits.resize(b * lm.1, 0.0);
18890                if !lanes[0].read_logits(logits) {
18891                    return MetalRowsRun::Failed;
18892                }
18893                spec_stamp("v.lg");
18894            }
18895            (None, _) => {}
18896        }
18897        for (g, &(r0, n)) in lanes.iter().zip(&cuts) {
18898            if !g.read_hidden(&mut hiddens[r0 * hs..(r0 + n) * hs]) {
18899                return MetalRowsRun::Failed;
18900            }
18901        }
18902        spec_stamp("v.hid");
18903        // The tapped rows, a row's layers joined: a prompt's rows are context at
18904        // once, a verify's wait for the round to say which of them it keeps.
18905        self.dflash_rows = None;
18906        if !taps.is_empty() && tapped.load(std::sync::atomic::Ordering::Relaxed) == taps.len() && lanes.len() == 1 {
18907            let nt = taps.len();
18908            let mut raw = vec![0f32; nt * b * hs];
18909            if lanes[0].read_taps(&mut raw) {
18910                let mut joined = vec![0f32; b * nt * hs];
18911                for s in 0..nt {
18912                    for r in 0..b {
18913                        joined[(r * nt + s) * hs..][..hs].copy_from_slice(&raw[(s * b + r) * hs..][..hs]);
18914                    }
18915                }
18916                if prefill {
18917                    if let Some(d) = self.dflash.as_mut() {
18918                        d.queue_context(pos0, &joined, self.pool.as_deref());
18919                    }
18920                } else {
18921                    self.dflash_rows = Some((pos0, joined));
18922                }
18923                spec_stamp("v.taps");
18924            }
18925        }
18926        // The last half's graph: the layers' states are the chunk's.
18927        let graph = lanes.pop().unwrap();
18928        MetalRowsRun::Completed(MetalVerifyPending {
18929            graph,
18930            gdn_layers,
18931            attn_layers,
18932        })
18933    }
18934
18935    /// Native-Metal twin of `try_batch_graph_wgpu`: the b rows through the
18936    /// whole model on the `VerifyGraph` (one submit), the head folded in
18937    /// when `spec` asks; `hiddens` come back as the last layer's output
18938    /// rows, `spec.2` as `[b][lm_rows]` logits. The graph is parked in
18939    /// `metal_verify` for `metal_verify_commit`.
18940    #[cfg(target_os = "macos")]
18941    fn try_batch_graph_metal(
18942        &mut self,
18943        hiddens: &mut [f32],
18944        positions: &[usize],
18945        b: usize,
18946        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
18947        argmax_out: Option<(usize, &mut Vec<u32>)>,
18948    ) -> crate::gpu::BatchGraphOutcome {
18949        let _t0 = std::time::Instant::now();
18950        if positions.len() != b
18951            || self.mhc.is_some()
18952            || self.prompt_mrope.is_some()
18953            || self.value_blend.is_some()
18954            || positions.windows(2).any(|w| w[1] != w[0] + 1)
18955            || hiddens.len() != b * self.hidden_size
18956        {
18957            return crate::gpu::BatchGraphOutcome::Declined;
18958        }
18959        let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
18960            MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
18961            MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
18962            MetalRowsRun::Completed(pending) => pending,
18963        };
18964        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
18965            eprintln!(
18966                "metal-verify: {:.1} ms | b={b}",
18967                _t0.elapsed().as_secs_f64() * 1e3
18968            );
18969        }
18970        self.metal_verify = Some(pending);
18971        crate::gpu::BatchGraphOutcome::Completed
18972    }
18973
18974    /// Batched prefill on the Metal rows graph: `ids` (≤ 512) at
18975    /// `start_pos..`, states written in place, K/V rows appended to the
18976    /// CPU caches; optional final norm/head logits are returned in `spec`.
18977    /// Declined means no command buffer was admitted; Failed is terminal.
18978    #[cfg(target_os = "macos")]
18979    fn prefill_rows_metal(
18980        &mut self,
18981        ids: &[u32],
18982        start_pos: usize,
18983        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
18984    ) -> MetalPrefillOutcome {
18985        let b = ids.len();
18986        if b == 0 || b > 512 {
18987            return MetalPrefillOutcome::Declined;
18988        }
18989        let hs = self.hidden_size;
18990        let mut hiddens = vec![0f32; b * hs];
18991        for (j, &id) in ids.iter().enumerate() {
18992            let e = self.embed_single(id);
18993            hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
18994        }
18995        self.prefill_hiddens_metal(hiddens, start_pos, spec)
18996    }
18997
18998    /// Ready prompt rows (`b <= 512`) through the rows graph.
18999    #[cfg(target_os = "macos")]
19000    fn prefill_hiddens_metal(
19001        &mut self,
19002        mut hiddens: Vec<f32>,
19003        start_pos: usize,
19004        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
19005    ) -> MetalPrefillOutcome {
19006        let b = hiddens.len() / self.hidden_size;
19007        METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
19008        let with_head = spec.is_some();
19009        let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
19010            MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
19011            MetalRowsRun::Failed => {
19012                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
19013                return MetalPrefillOutcome::Failed;
19014            }
19015            MetalRowsRun::Completed(pending) => pending,
19016        };
19017        // states are final: copy them to the owners
19018        let idxs = pending.gdn_layers.clone();
19019        let mut outs: Vec<&mut [f32]> = self
19020            .kv_cache
19021            .layers
19022            .iter_mut()
19023            .enumerate()
19024            .filter(|(i, _)| idxs.binary_search(i).is_ok())
19025            .map(|(_, l)| l.linear_state.as_mut_slice())
19026            .collect();
19027        if !pending.graph.finish_states(&mut outs) {
19028            METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
19029            return MetalPrefillOutcome::Failed;
19030        }
19031        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
19032        // Read every layer before mutating any CPU cache.  A missing mirror
19033        // row is a terminal graph failure, not a reason to append a partial
19034        // prefix and replay the remainder serially.
19035        let mut rows = Vec::with_capacity(pending.attn_layers.len());
19036        for (li, cpu_stored) in &pending.attn_layers {
19037            let mut kbuf = vec![0f32; b * nkv * hd];
19038            let mut vbuf = vec![0f32; b * nkv * hd];
19039            if !crate::gpu_metal::kv_mirror_read_rows(
19040                self.graph_kv_id,
19041                *li,
19042                nkv,
19043                hd,
19044                *cpu_stored,
19045                b,
19046                &mut kbuf,
19047                &mut vbuf,
19048            ) {
19049                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
19050                return MetalPrefillOutcome::Failed;
19051            }
19052            rows.push((*li, *cpu_stored, kbuf, vbuf));
19053        }
19054        for (li, cpu_stored, kbuf, vbuf) in rows {
19055            let cache = &mut self.kv_cache.layers[li];
19056            for r in 0..b {
19057                cache.append(
19058                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
19059                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
19060                    &[],
19061                );
19062            }
19063            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
19064        }
19065        METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
19066        if with_head {
19067            METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
19068        }
19069        // Every layer appended the chunk: sliding layers keep their tails only.
19070        self.swa_trim_tails();
19071        MetalPrefillOutcome::Completed(hiddens)
19072    }
19073
19074    #[cfg(target_os = "macos")]
19075    fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
19076        self.prefill_rows_metal(ids, start_pos, None)
19077    }
19078
19079    /// Exact teacher-forced NLL through the ordinary Metal rows graph.  This
19080    /// is intentionally separate from the serial TokenGraph scorer: every
19081    /// chunk owns a real b-row graph/head completion and the recurrent/KV
19082    /// handoff is committed before the next chunk begins.
19083    #[cfg(target_os = "macos")]
19084    fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
19085        if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
19086            return MetalBatchNllOutcome::Declined;
19087        }
19088        let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
19089            return MetalBatchNllOutcome::Declined;
19090        };
19091        let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
19092            .ok()
19093            .and_then(|v| v.parse::<usize>().ok())
19094            .filter(|&v| (1..=512).contains(&v))
19095            .unwrap_or(32);
19096        let final_norm = self.weights.final_norm.clone();
19097        let mut nll = 0.0f64;
19098        let mut count = 0usize;
19099        let mut pos = 0usize;
19100        let mut completed = 0usize;
19101        while pos < ids.len() {
19102            let end = (pos + chunk).min(ids.len());
19103            let mut logits = Vec::new();
19104            let outcome =
19105                self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
19106            match outcome {
19107                MetalPrefillOutcome::Declined => {
19108                    return if completed == 0 {
19109                        MetalBatchNllOutcome::Declined
19110                    } else {
19111                        MetalBatchNllOutcome::Failed(format!(
19112                            "ordinary Metal NLL batch declined after {completed} chunks"
19113                        ))
19114                    };
19115                }
19116                MetalPrefillOutcome::Failed => {
19117                    return MetalBatchNllOutcome::Failed(
19118                        "ordinary Metal NLL batch failed after admission".to_string(),
19119                    );
19120                }
19121                MetalPrefillOutcome::Completed(_) => {}
19122            }
19123            completed += 1;
19124            let vocab = self.vocab_size.min(lm.1);
19125            if logits.len() != (end - pos) * lm.1 || vocab == 0 {
19126                return MetalBatchNllOutcome::Failed(
19127                    "ordinary Metal NLL head returned an invalid shape".to_string(),
19128                );
19129            }
19130            for row in 0..(end - pos) {
19131                let absolute = pos + row;
19132                if absolute < start || absolute + 1 >= ids.len() {
19133                    continue;
19134                }
19135                let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
19136                if let Some(mu) = self.logit_multiplier {
19137                    for v in lg.iter_mut() {
19138                        *v *= mu;
19139                    }
19140                }
19141                if let Some(c) = self.final_softcap {
19142                    softcap_in_place(lg, c, self.pool.as_deref());
19143                }
19144                let target = ids[absolute + 1] as usize;
19145                if target >= vocab {
19146                    return MetalBatchNllOutcome::Failed(format!(
19147                        "target token {target} exceeds Metal head rows {vocab}"
19148                    ));
19149                }
19150                let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
19151                let lse: f64 = lg
19152                    .iter()
19153                    .map(|&v| ((v - max) as f64).exp())
19154                    .sum::<f64>()
19155                    .ln()
19156                    + max as f64;
19157                nll += lse - lg[target] as f64;
19158                count += 1;
19159            }
19160            pos = end;
19161        }
19162        MetalBatchNllOutcome::Completed(nll, count)
19163    }
19164
19165    /// Commit a Metal verify round: replay the GDN recurrences over the
19166    /// `a + 1` accepted positions into the CPU states, append the accepted
19167    /// K/V rows from the mirrors to the CPU caches, re-point the mirrors.
19168    #[cfg(target_os = "macos")]
19169    fn metal_verify_commit(&mut self, a: usize) -> bool {
19170        let Some(mut pending) = self.metal_verify.take() else {
19171            return false;
19172        };
19173        let n = a + 1;
19174        // encode order == ascending layer order (the plan walks 0..layers)
19175        let idxs = pending.gdn_layers.clone();
19176        let mut outs: Vec<&mut [f32]> = self
19177            .kv_cache
19178            .layers
19179            .iter_mut()
19180            .enumerate()
19181            .filter(|(i, _)| idxs.binary_search(i).is_ok())
19182            .map(|(_, l)| l.linear_state.as_mut_slice())
19183            .collect();
19184        if !pending.graph.commit(n, &mut outs) {
19185            return false;
19186        }
19187        spec_stamp("c.replay");
19188        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
19189        // Read every layer before mutating any CPU cache.  Missing rows are
19190        // terminal after the replay has executed; never append a partial KV
19191        // prefix and continue on a serial path.
19192        let mut rows = Vec::with_capacity(pending.attn_layers.len());
19193        for (li, cpu_stored) in &pending.attn_layers {
19194            let mut kbuf = vec![0f32; n * nkv * hd];
19195            let mut vbuf = vec![0f32; n * nkv * hd];
19196            if !crate::gpu_metal::kv_mirror_read_rows(
19197                self.graph_kv_id,
19198                *li,
19199                nkv,
19200                hd,
19201                *cpu_stored,
19202                n,
19203                &mut kbuf,
19204                &mut vbuf,
19205            ) {
19206                return false;
19207            }
19208            rows.push((*li, *cpu_stored, kbuf, vbuf));
19209        }
19210        for (li, cpu_stored, kbuf, vbuf) in rows {
19211            let cache = &mut self.kv_cache.layers[li];
19212            for r in 0..n {
19213                cache.append(
19214                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
19215                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
19216                    &[],
19217                );
19218            }
19219            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
19220        }
19221        spec_stamp("c.kv");
19222        true
19223    }
19224
19225    /// The round's warm-ups as ONE b-row graph run over the MTP block on
19226    /// Metal: `pairs` = (trunk hidden, next token) at consecutive positions
19227    /// from `first_pos`; the block's input projection is folded in. This
19228    /// half encodes and SUBMITS (no wait); `mtp_warm_batch_finish` waits
19229    /// and pulls the appended K/V rows into the CPU MTP cache. None = the
19230    /// graph declined (nothing submitted, nothing appended).
19231    #[cfg(target_os = "macos")]
19232    fn mtp_warm_batch_submit(
19233        &mut self,
19234        m: &mut MtpModule,
19235        pairs: &[(&[f32], u32)],
19236        first_pos: usize,
19237    ) -> Option<MetalWarmPending> {
19238        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
19239        let first_pos = first_pos.saturating_sub(self.mtp_pos_base);
19240        let b = pairs.len();
19241        if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
19242            return None;
19243        }
19244        let AttnKind::Full {
19245            wq,
19246            wk,
19247            wv,
19248            wo,
19249            q_norm,
19250            k_norm,
19251            output_gate,
19252            softplus_gate: None,
19253            bias: None,
19254        } = &m.layer.attn
19255        else {
19256            return None;
19257        };
19258        let FfnKind::Dense(d) = &m.layer.ffn else {
19259            return None;
19260        };
19261        if !d.segs.is_empty() {
19262            return None;
19263        }
19264        let (Some(pq), Some(pk), Some(pv), Some(po)) =
19265            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
19266        else {
19267            return None;
19268        };
19269        let (Some(g), Some(u), Some(dn)) = (
19270            d.gate_proj.q1_parts(),
19271            d.up_proj.q1_parts(),
19272            d.down_proj.q1_parts(),
19273        ) else {
19274            return None;
19275        };
19276        let Some(eh) = m.eh_proj.q1_parts() else {
19277            return None;
19278        };
19279        let QTensor::Mapped { model, .. } = wq else {
19280            return None;
19281        };
19282        let model = model.clone();
19283        let hs = self.hidden_size;
19284        // [enorm(embed(tok)); hnorm(hidden)] rows
19285        let mut cat = vec![0f32; b * 2 * hs];
19286        for (j, (h, tok)) in pairs.iter().enumerate() {
19287            let e = self.embed_single(*tok);
19288            let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
19289            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
19290            inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
19291        }
19292        let dims = GraphDims {
19293            hidden: hs,
19294            eps: self.rms_eps as f32,
19295            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
19296        };
19297        spec_stamp("w.cat");
19298        let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
19299            return None;
19300        };
19301        spec_stamp("w.new");
19302        let l = AttnGpuLayer {
19303            mhc: None,
19304            attn_norm: &m.layer.input_norm,
19305            post_norm: &m.layer.post_norm,
19306            wq: pq,
19307            wk: pk,
19308            wv: pv,
19309            wo: po,
19310            ffn: MetalFfn::Dense {
19311                gate: g,
19312                up: u,
19313                down: dn,
19314                act: (d.act == Act::Gelu) as u8,
19315            },
19316            attn_out_norm: None,
19317            ffn_out_norm: None,
19318            layer_scale: None,
19319        };
19320        let (nh, nkv, hd, rd) = (
19321            self.num_heads,
19322            self.num_kv_heads,
19323            self.head_dim,
19324            self.rotary_dim,
19325        );
19326        let inv_freq = self.inv_freq.clone();
19327        let cpu_stored;
19328        {
19329            let cache = &m.kv;
19330            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
19331            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
19332            cpu_stored = cpu_k[0].len() / hd;
19333            // The cache may LAG the position (rows nobody warmed): the
19334            // pairs land at cpu_stored.. with their true RoPE positions
19335            // first_pos.., exactly what the one-by-one warm does. A cache
19336            // AHEAD of the position is a real inconsistency.
19337            if cpu_stored > first_pos {
19338                spec_stamp("w.decl");
19339                return None;
19340            }
19341            let p = AttnDeviceParams {
19342                kv_id: self.mtp_kv_id(),
19343                layer: Self::MTP_LAYER_BASE,
19344                nh,
19345                nkv,
19346                hd,
19347                rd,
19348                position: first_pos,
19349                scale: self.attn_scale,
19350                eps: self.rms_eps as f32,
19351                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
19352                late_qk_norm: self.qk_norm_after_rope,
19353                output_gate: *output_gate,
19354                q_norm: q_norm.as_deref(),
19355                k_norm: k_norm.as_deref(),
19356                inv_freq: &inv_freq,
19357                rope_scale: self.rope_scale,
19358                cpu_k,
19359                cpu_v,
19360                cpu_stored,
19361                cpu_gen: cache.generation(),
19362                o1: None,
19363                window: None,
19364                softmax_sink: None,
19365                head_gate: None,
19366                v_norm: false,
19367                dash: None,
19368            };
19369            if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
19370                return None;
19371            }
19372        }
19373        spec_stamp("w.enc");
19374        if !graph.submit() {
19375            return None;
19376        }
19377        spec_stamp("w.sub");
19378        Some(MetalWarmPending {
19379            graph,
19380            cpu_stored,
19381            b,
19382        })
19383    }
19384
19385    /// Submit and finish in one call (the prefill's MTP warm-up, where
19386    /// nothing runs in between).
19387    #[cfg(target_os = "macos")]
19388    fn mtp_warm_batch_metal(
19389        &mut self,
19390        m: &mut MtpModule,
19391        pairs: &[(&[f32], u32)],
19392        first_pos: usize,
19393    ) -> bool {
19394        match self.mtp_warm_batch_submit(m, pairs, first_pos) {
19395            Some(p) => self.mtp_warm_batch_finish(m, p),
19396            None => false,
19397        }
19398    }
19399
19400    /// Second half of the batched warm-up: wait for the submitted graph,
19401    /// pull its b appended K/V rows into the CPU MTP cache, re-point the
19402    /// mirror. False = the command buffer failed or the rows are missing
19403    /// (nothing appended; the caller falls back to the one-by-one warm).
19404    #[cfg(target_os = "macos")]
19405    fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
19406        let MetalWarmPending {
19407            mut graph,
19408            cpu_stored,
19409            b,
19410        } = pending;
19411        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
19412        if !graph.sync() {
19413            return false;
19414        }
19415        spec_stamp("w.gpu");
19416        let mut kbuf = vec![0f32; b * nkv * hd];
19417        let mut vbuf = vec![0f32; b * nkv * hd];
19418        if !crate::gpu_metal::kv_mirror_read_rows(
19419            self.mtp_kv_id(),
19420            Self::MTP_LAYER_BASE,
19421            nkv,
19422            hd,
19423            cpu_stored,
19424            b,
19425            &mut kbuf,
19426            &mut vbuf,
19427        ) {
19428            return false;
19429        }
19430        for r in 0..b {
19431            m.kv.append(
19432                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
19433                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
19434                &[],
19435            );
19436        }
19437        crate::gpu_metal::kv_mirror_set_stored(
19438            self.mtp_kv_id(),
19439            Self::MTP_LAYER_BASE,
19440            cpu_stored + b,
19441        );
19442        spec_stamp("w.kv");
19443        true
19444    }
19445
19446    /// A committed token id from the high table (Cyrillic, CJK and the
19447    /// like sit above 131072 in Qwen's vocabulary; Latin subwords past
19448    /// the 65536 cut are rare enough to lose as rejected drafts) switches
19449    /// the draft to the full head for the next 16 tokens; other ids count
19450    /// down. On an M4 the full 660 MB head costs 5.5 ms a draft step
19451    /// against 1.4 for the shortlist, so the streak is kept short.
19452    pub(crate) fn note_draft_id(&mut self, id: u32) {
19453        let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
19454        if (id as usize) >= cut {
19455            self.draft_full_streak = 16;
19456        } else {
19457            self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
19458        }
19459    }
19460
19461    /// The draft head's rows for the next step: the shortlist, or the full
19462    /// head while `draft_full_streak` runs.
19463    fn draft_head_rows(&self, head_rows: usize) -> usize {
19464        if self.draft_full_streak > 0 {
19465            head_rows
19466        } else {
19467            Self::draft_vocab_rows(head_rows)
19468        }
19469    }
19470
19471    /// Draft-head shortlist size: `CMF_DRAFT_VOCAB` rows (default 65536,
19472    /// capped at the head; 0 = full head).
19473    fn draft_vocab_rows(head_rows: usize) -> usize {
19474        static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19475        let n = *N.get_or_init(|| {
19476            std::env::var("CMF_DRAFT_VOCAB")
19477                .ok()
19478                .and_then(|v| v.parse().ok())
19479                .unwrap_or(65536)
19480        });
19481        if n == 0 { head_rows } else { n.min(head_rows) }
19482    }
19483
19484    /// One MTP block step on the native Metal token graph: block input on
19485    /// the host, the attention layer + FFN device-resident over the MTP
19486    /// mirror, the head folded in when `want_logits`. The appended K/V row
19487    /// is pulled into the CPU MTP cache (owner of record) after the sync.
19488    #[cfg(target_os = "macos")]
19489    fn mtp_step_metal(
19490        &mut self,
19491        m: &mut MtpModule,
19492        hidden: &[f32],
19493        next_token: u32,
19494        position: usize,
19495        want_logits: bool,
19496    ) -> Option<(Vec<f32>, Vec<f32>)> {
19497        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
19498        let position = position.saturating_sub(self.mtp_pos_base);
19499        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
19500            || !crate::gpu::q1_force()
19501            || !crate::gpu::enabled_here()
19502            || self.attn_softcap > 0.0
19503            || self.attention_heads_per_layer.is_some()
19504            || m.kv.mode != crate::kv_cache::KvMode::F32
19505            || m.kv.o1.is_some()
19506        {
19507            return None;
19508        }
19509        let AttnKind::Full {
19510            wq,
19511            wk,
19512            wv,
19513            wo,
19514            q_norm,
19515            k_norm,
19516            output_gate,
19517            softplus_gate: None,
19518            bias: None,
19519        } = &m.layer.attn
19520        else {
19521            return None;
19522        };
19523        let FfnKind::Dense(d) = &m.layer.ffn else {
19524            return None;
19525        };
19526        if d.act != Act::Silu || !d.segs.is_empty() {
19527            return None;
19528        }
19529        let (pq, pk, pv, po) = (
19530            wq.q1_parts()?,
19531            wk.q1_parts()?,
19532            wv.q1_parts()?,
19533            wo.q1_parts()?,
19534        );
19535        let (g, u, dn) = (
19536            d.gate_proj.q1_parts()?,
19537            d.up_proj.q1_parts()?,
19538            d.down_proj.q1_parts()?,
19539        );
19540        let QTensor::Mapped { model, .. } = wq else {
19541            return None;
19542        };
19543        let model = model.clone();
19544        let lm = if want_logits {
19545            Some(self.weights.lm_head.q1_parts()?)
19546        } else {
19547            None
19548        };
19549        let dims = GraphDims {
19550            hidden: self.hidden_size,
19551            eps: self.rms_eps as f32,
19552            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
19553        };
19554        // The block input `eh_proj · [enorm(e); hnorm(h)]` rides in the
19555        // graph (one submit a step); the host per-op matvec if it cannot.
19556        let hs = self.hidden_size;
19557        let mut x = vec![0f32; hs];
19558        // Let an eligible q4tp MTP draft use the measured masked-nibble
19559        // matvec.  Unlike fused epilogues, this reduces the draft's
19560        // projection work without changing its residual-update schedule;
19561        // the target verifier remains the authority over emitted tokens.
19562        // The guard keeps `CMF_METAL_MVFAST=0` as an immediate rollback
19563        // switch and restores the caller's setting on every early return.
19564        let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
19565            crate::gpu_metal::DENSE_MV,
19566        );
19567        let mut graph = TokenGraph::new(&model, dims, &x)?;
19568        let mut folded = false;
19569        if let Some(eh) = m.eh_proj.q1_parts() {
19570            let e = self.embed_single(next_token);
19571            let mut cat = vec![0.0f32; 2 * hs];
19572            let (cat_e, cat_h) = cat.split_at_mut(hs);
19573            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
19574            inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
19575            folded = graph.encode_input_proj(eh, &cat);
19576        }
19577        if !folded {
19578            x = self.mtp_block_input(m, hidden, next_token);
19579            graph = TokenGraph::new(&model, dims, &x)?;
19580        }
19581        spec_stamp("d.in");
19582        let l = AttnGpuLayer {
19583            mhc: None,
19584            attn_norm: &m.layer.input_norm,
19585            post_norm: &m.layer.post_norm,
19586            wq: pq,
19587            wk: pk,
19588            wv: pv,
19589            wo: po,
19590            ffn: MetalFfn::Dense {
19591                gate: g,
19592                up: u,
19593                down: dn,
19594                act: (d.act == Act::Gelu) as u8,
19595            },
19596            attn_out_norm: None,
19597            ffn_out_norm: None,
19598            layer_scale: None,
19599        };
19600        let (nh, nkv, hd, rd) = (
19601            self.num_heads,
19602            self.num_kv_heads,
19603            self.head_dim,
19604            self.rotary_dim,
19605        );
19606        let inv_freq = self.inv_freq.clone();
19607        {
19608            let cache = &m.kv;
19609            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
19610            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
19611            let cpu_stored = cpu_k[0].len() / hd;
19612            let p = AttnDeviceParams {
19613                kv_id: self.mtp_kv_id(),
19614                layer: Self::MTP_LAYER_BASE,
19615                nh,
19616                nkv,
19617                hd,
19618                rd,
19619                position,
19620                scale: self.attn_scale,
19621                eps: self.rms_eps as f32,
19622                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
19623                late_qk_norm: self.qk_norm_after_rope,
19624                output_gate: *output_gate,
19625                q_norm: q_norm.as_deref(),
19626                k_norm: k_norm.as_deref(),
19627                inv_freq: &inv_freq,
19628                rope_scale: self.rope_scale,
19629                cpu_k,
19630                cpu_v,
19631                cpu_stored,
19632                cpu_gen: cache.generation(),
19633                o1: None,
19634                window: None,
19635                softmax_sink: None,
19636                head_gate: None,
19637                v_norm: false,
19638                dash: None,
19639            };
19640            if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
19641                return None;
19642            }
19643        }
19644        // The draft's head over a vocabulary SHORTLIST (the first
19645        // CMF_DRAFT_VOCAB rows — BPE ids run roughly by merge rank, so the
19646        // low ids carry the mass): the verify keeps the full head, so a true
19647        // token past the cut is only a rejected draft, never a wrong token.
19648        // 662 MB a step on Qwen3.8 becomes 170 MB at 65536.
19649        let draft_rows = if let Some(lm) = lm {
19650            self.draft_head_rows(lm.1)
19651        } else {
19652            0
19653        };
19654        if let Some(lm) = lm {
19655            if !graph.lm_head_ok(lm) {
19656                return None;
19657            }
19658            if draft_rows < lm.1 {
19659                if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
19660                    return None;
19661                }
19662            } else {
19663                graph.encode_lm_head(&m.final_norm, lm);
19664            }
19665        }
19666        spec_stamp("d.enc");
19667        if graph.sync_checked().is_err() {
19668            return None;
19669        }
19670        spec_stamp("d.gpu");
19671        let mut logits = Vec::new();
19672        if let Some(lm) = lm {
19673            let n_read = draft_rows.min(lm.1).min(self.vocab_size);
19674            logits = attention::take_buf(n_read);
19675            graph.read_logits(&mut logits);
19676            // ids past the shortlist: never drafted (−∞ in every chain)
19677            pad_mtp_draft_logits(&mut logits, self.vocab_size);
19678        }
19679        graph.finish(&mut x);
19680        let mut krow = attention::take_buf(nkv * hd);
19681        let mut vrow = attention::take_buf(nkv * hd);
19682        if crate::gpu_metal::kv_mirror_read_last(
19683            self.mtp_kv_id(),
19684            Self::MTP_LAYER_BASE,
19685            nkv,
19686            hd,
19687            &mut krow,
19688            &mut vrow,
19689        ) {
19690            m.kv.append(&krow, &vrow, &[]);
19691        }
19692        attention::recycle_buf(&mut krow);
19693        attention::recycle_buf(&mut vrow);
19694        spec_stamp("d.rd");
19695        Some((logits, x))
19696    }
19697
19698    /// `CMF_MTP_CHAIN=0` keeps the per-step draft (one submit and one
19699    /// host round trip per MTP step); the default drafts the whole chain
19700    /// in one command buffer when the round is plain greedy.
19701    ///
19702    /// Measured on an M4 (24 GB), Qwen3.8-27B q4tp, P3 at 160 tokens,
19703    /// k=7, six runs per arm alternating inside one lock window — the
19704    /// round's draft phase (median over the 34 rounds of a run) is
19705    /// 34.5 ms per round old against 30.1 new, i.e. 4.93 → 4.31 ms per
19706    /// draft step. That is the whole prize: the 7 submits cost ~0.6 ms
19707    /// each in host and submit latency and nothing else changes —
19708    /// acceptance (3.41 of 7) and tokens per round (4.41) are identical,
19709    /// and the round is 289 → 285 ms, decode 13.8 → 14.0 tok/s.
19710    fn mtp_chain_on() -> bool {
19711        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19712        *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
19713    }
19714
19715    /// The round's k greedy drafts as ONE command buffer on Metal: the MTP
19716    /// block k times back to back, each step's token embedding gathered
19717    /// on the device from the argmax the step before it wrote, the head
19718    /// over the round's shortlist (or the full head during a full-head
19719    /// streak — decided once, before the chain, exactly as the per-step
19720    /// path decides it per step, since `draft_full_streak` only moves on
19721    /// a commit). One wait, then the k ids and the k appended K/V rows
19722    /// come back; the CPU MTP cache ends where k `mtp_step_metal` calls
19723    /// would have left it. `Err(false)` = declined before anything was
19724    /// committed (the per-step path takes the round); `Err(true)` = the
19725    /// command buffer failed after commit.
19726    #[cfg(target_os = "macos")]
19727    fn mtp_draft_chain_metal(
19728        &mut self,
19729        m: &mut MtpModule,
19730        hidden: &[f32],
19731        t_next: u32,
19732        position: usize,
19733        k: usize,
19734    ) -> Result<Vec<u32>, bool> {
19735        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
19736        let position = position.saturating_sub(self.mtp_pos_base);
19737        if k == 0
19738            || k > 64
19739            || !Self::mtp_chain_on()
19740            || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
19741            || !crate::gpu::q1_force()
19742            || !crate::gpu::enabled_here()
19743            || self.attn_softcap > 0.0
19744            || self.attention_heads_per_layer.is_some()
19745            || m.kv.mode != crate::kv_cache::KvMode::F32
19746            || m.kv.o1.is_some()
19747            // the chain gathers embeddings itself: only the plain table
19748            || self.dsv4.is_some()
19749            || self.dsv41.is_some()
19750            || self.dory.is_some()
19751            || self.qwen4_exp.is_some()
19752            || self.per_token_stack()
19753        {
19754            return Err(false);
19755        }
19756        let AttnKind::Full {
19757            wq,
19758            wk,
19759            wv,
19760            wo,
19761            q_norm,
19762            k_norm,
19763            output_gate,
19764            softplus_gate: None,
19765            bias: None,
19766        } = &m.layer.attn
19767        else {
19768            return Err(false);
19769        };
19770        let FfnKind::Dense(d) = &m.layer.ffn else {
19771            return Err(false);
19772        };
19773        if d.act != Act::Silu || !d.segs.is_empty() {
19774            return Err(false);
19775        }
19776        let (Some(pq), Some(pk), Some(pv), Some(po)) =
19777            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
19778        else {
19779            return Err(false);
19780        };
19781        let (Some(g), Some(u), Some(dn)) = (
19782            d.gate_proj.q1_parts(),
19783            d.up_proj.q1_parts(),
19784            d.down_proj.q1_parts(),
19785        ) else {
19786            return Err(false);
19787        };
19788        let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
19789            return Err(false);
19790        };
19791        let QTensor::Mapped { model, .. } = wq else {
19792            return Err(false);
19793        };
19794        let model = model.clone();
19795        // the embedding table: a q4tp tensor of the SAME blob, no Prism
19796        // inverse-embedding post-pass
19797        let QTensor::Mapped {
19798            model: em,
19799            idx: eidx,
19800            dtype: cortiq_core::TensorDtype::Q4TiledP,
19801            ..
19802        } = &self.weights.embed_tokens
19803        else {
19804            return Err(false);
19805        };
19806        if !std::sync::Arc::ptr_eq(em, &model)
19807            || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
19808        {
19809            return Err(false);
19810        }
19811        let embed = (
19812            *eidx,
19813            self.weights.embed_tokens.rows(),
19814            self.weights.embed_tokens.cols(),
19815        );
19816        if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
19817            return Err(false);
19818        }
19819        let dims = GraphDims {
19820            hidden: self.hidden_size,
19821            eps: self.rms_eps as f32,
19822            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
19823        };
19824        // Keep the chained MTP draft on the masked-nibble Metal matvec
19825        // path.  This scope covers every encode in the chain; the guard
19826        // restores its predecessor when this round returns or declines.
19827        let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
19828            crate::gpu_metal::DENSE_MV,
19829        );
19830        let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
19831            return Err(false);
19832        };
19833        if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
19834            return Err(false);
19835        }
19836        let l = AttnGpuLayer {
19837            mhc: None,
19838            attn_norm: &m.layer.input_norm,
19839            post_norm: &m.layer.post_norm,
19840            wq: pq,
19841            wk: pk,
19842            wv: pv,
19843            wo: po,
19844            ffn: MetalFfn::Dense {
19845                gate: g,
19846                up: u,
19847                down: dn,
19848                act: (d.act == Act::Gelu) as u8,
19849            },
19850            attn_out_norm: None,
19851            ffn_out_norm: None,
19852            layer_scale: None,
19853        };
19854        let (nh, nkv, hd, rd) = (
19855            self.num_heads,
19856            self.num_kv_heads,
19857            self.head_dim,
19858            self.rotary_dim,
19859        );
19860        let inv_freq = self.inv_freq.clone();
19861        let draft_rows = self.draft_head_rows(lm.1);
19862        let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
19863        if n_arg == 0 {
19864            return Err(false);
19865        }
19866        // `CMF_MTP_CHAIN_SPLIT=1` commits each step as it is encoded, so
19867        // the GPU starts on step 0 while the host is still encoding step
19868        // 1 — a probe for whether the host encode is on the critical
19869        // path. It is not: three runs each, draft 30.0 ms per round split
19870        // against 30.1 whole, and the whole chain's host encode measures
19871        // 0.3 ms against a 29.7 ms wait. Kept as a probe, off by default.
19872        let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
19873        // The input stage, the embedding and the block's input projection, goes as a command
19874        // of its own. The weights are one buffer of 13.6 GB on a Mac, the embedding table lies
19875        // 1.5 GB into it and the draft block 13.1 GB: in one command with the block the driver
19876        // held the command 150 to 170 ms before the GPU ran its 9 ms, in every round until the
19877        // first one without a warm-up. MacBook Air M4, Qwen3.8-27B, a round of two drafts:
19878        // 362 → 204 ms. `CMF_MTP_CHAIN_JOIN=1` keeps one command.
19879        let join = std::env::var("CMF_MTP_CHAIN_JOIN").as_deref() == Ok("1");
19880        let t_chain = std::time::Instant::now();
19881        graph.chain_ids_init(t_next, k);
19882        let cpu_stored;
19883        {
19884            let cache = &m.kv;
19885            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
19886            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
19887            cpu_stored = cpu_k[0].len() / hd;
19888            for j in 0..k {
19889                if !graph.encode_chain_input(
19890                    embed,
19891                    j as u32,
19892                    &m.enorm,
19893                    &m.hnorm,
19894                    self.embed_multiplier,
19895                    eh,
19896                ) {
19897                    return Err(false);
19898                }
19899                // step j's mirror row: the mirror is re-pointed at the CPU
19900                // rows before step 0 and advances by one per step; its
19901                // resync (never taken past step 0) reads the CPU rows
19902                let p = AttnDeviceParams {
19903                    kv_id: self.mtp_kv_id(),
19904                    layer: Self::MTP_LAYER_BASE,
19905                    nh,
19906                    nkv,
19907                    hd,
19908                    rd,
19909                    position: position + j,
19910                    scale: self.attn_scale,
19911                    eps: self.rms_eps as f32,
19912                    gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
19913                    late_qk_norm: self.qk_norm_after_rope,
19914                    output_gate: *output_gate,
19915                    q_norm: q_norm.as_deref(),
19916                    k_norm: k_norm.as_deref(),
19917                    inv_freq: &inv_freq,
19918                    rope_scale: self.rope_scale,
19919                    cpu_k: cpu_k.clone(),
19920                    cpu_v: cpu_v.clone(),
19921                    cpu_stored: cpu_stored + j,
19922                    cpu_gen: cache.generation(),
19923                    o1: None,
19924                    window: None,
19925                    softmax_sink: None,
19926                    head_gate: None,
19927                    v_norm: false,
19928                    dash: None,
19929                };
19930                if !join {
19931                    graph.commit();
19932                }
19933                if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
19934                    return Err(false);
19935                }
19936                if draft_rows < lm.1 {
19937                    if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
19938                        return Err(false);
19939                    }
19940                } else {
19941                    graph.encode_lm_head(&m.final_norm, lm);
19942                }
19943                if !graph.encode_argmax(n_arg, j as u32 + 1) {
19944                    return Err(false);
19945                }
19946                if split {
19947                    // CMF_MTP_CHAIN_SPLIT=1: commit every step so the GPU
19948                    // starts on step 0 while the host encodes the rest
19949                    graph.commit();
19950                }
19951            }
19952        }
19953        let t_enc = t_chain.elapsed();
19954        if graph.sync_checked().is_err() {
19955            return Err(true);
19956        }
19957        if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
19958            eprintln!(
19959                "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
19960                t_enc.as_secs_f64() * 1e3,
19961                (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
19962                if split { ", split" } else { "" }
19963            );
19964            if std::env::var("CMF_METAL_CMD_TIMES").as_deref() == Ok("1") {
19965                eprintln!("metal-set: {}", crate::gpu_metal::working_set_report());
19966            }
19967        }
19968        let mut ids = vec![0u32; k];
19969        if !graph.chain_ids_read(&mut ids) {
19970            return Err(true);
19971        }
19972        let mut kbuf = vec![0f32; k * nkv * hd];
19973        let mut vbuf = vec![0f32; k * nkv * hd];
19974        if !crate::gpu_metal::kv_mirror_read_rows(
19975            self.mtp_kv_id(),
19976            Self::MTP_LAYER_BASE,
19977            nkv,
19978            hd,
19979            cpu_stored,
19980            k,
19981            &mut kbuf,
19982            &mut vbuf,
19983        ) {
19984            return Err(true);
19985        }
19986        for r in 0..k {
19987            m.kv.append(
19988                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
19989                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
19990                &[],
19991            );
19992        }
19993        Ok(ids)
19994    }
19995
19996    fn try_batch_graph_wgpu(
19997        &self,
19998        hiddens: &mut [f32],
19999        positions: &[usize],
20000        k: usize,
20001        spec: Option<crate::gpu::SpecTail<'_>>,
20002    ) -> crate::gpu::BatchGraphOutcome {
20003        self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
20004    }
20005
20006    /// `try_batch_graph_wgpu` with the device-prefix mode: `layers_run`
20007    /// Some lets a stack that does not fit run its leading layers (the
20008    /// token graph's prefix rule) and reports how many; `hiddens` then
20009    /// holds the boundary rows and the caller runs the rest on the host.
20010    fn try_batch_graph_wgpu_prefix(
20011        &self,
20012        hiddens: &mut [f32],
20013        positions: &[usize],
20014        k: usize,
20015        spec: Option<crate::gpu::SpecTail<'_>>,
20016        layers_run: Option<&mut usize>,
20017    ) -> crate::gpu::BatchGraphOutcome {
20018        crate::gpu::graph_kv16_note(self.graph_kv_id, self.kv16_allowed());
20019        // SenseNova-Loop: the mHC kernels take two streams (the
20020        // CMF_MHC_GRAPH=0 knob keeps the host); MRoPE prompt rows ride the
20021        // per-row attention loop with their own angle tables.
20022        if self.mhc.as_ref().is_some_and(|m| {
20023                m.s.streams != 2 || m.s.fixed_identity || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
20024            })
20025        {
20026            return crate::gpu::BatchGraphOutcome::Declined;
20027        }
20028        let graph_end = match self.mimo_moe.graph_prefix_end() {
20029            Some(end) if end < self.num_layers => {
20030                if layers_run.is_none() || spec.is_some() || end == 0 {
20031                    return crate::gpu::BatchGraphOutcome::Declined;
20032                }
20033                end
20034            }
20035            _ => self.num_layers,
20036        };
20037        let _tb = std::time::Instant::now();
20038        let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
20039        if self.attn_softcap > 0.0 {
20040            return crate::gpu::BatchGraphOutcome::Declined; // capped scores: no graph kernel — CPU path
20041        }
20042        // Same attention contract as the token graph: per-layer geometry
20043        // rides `geom`, anything it cannot express declines by name.
20044        if let Some(reason) = self
20045            .wgpu_graph_attn_decline()
20046            .or_else(|| self.batch_graph_gemma_decline())
20047        {
20048            self.note_graph_decline("wgpu batch graph", reason);
20049            return crate::gpu::BatchGraphOutcome::Declined;
20050        }
20051        let nh = self.num_heads;
20052        let (nkv, hd, rd) = self.layer_geom(0);
20053        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
20054        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
20055            if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
20056            {
20057                let name = &m.tensors[i].name;
20058                let prism = if crate::prism::is_inverse_embedding(m, name) {
20059                    crate::gpu::GraphPrismOp::InverseEmbedding
20060                } else if crate::prism::is_forward_weight(m, name) {
20061                    crate::gpu::GraphPrismOp::Forward
20062                } else {
20063                    crate::gpu::GraphPrismOp::None
20064                };
20065                return Some(crate::gpu::GraphW {
20066                    idx: i,
20067                    kind,
20068                    row_scale: rs,
20069                    data: &[],
20070                    prism,
20071                    affine: crate::prism::is_affine_target(m, name),
20072                });
20073            }
20074            if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
20075                eprintln!(
20076                    "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
20077                    t.rows(),
20078                    t.cols()
20079                );
20080            }
20081            t.as_f32().map(|d| crate::gpu::GraphW {
20082                idx: 0,
20083                kind: 4,
20084                row_scale: &[],
20085                data: d,
20086                prism: crate::gpu::GraphPrismOp::None,
20087                affine: false,
20088            })
20089        }
20090        // Ripple's value residual: the chunk's embedding rows, as they enter
20091        // the first layer (the graph overwrites `hiddens`).
20092        let vres_e0: Vec<f32> = if self.value_blend.is_some() {
20093            hiddens[..k * self.hidden_size].to_vec()
20094        } else {
20095            Vec::new()
20096        };
20097        let built: Option<(
20098            Vec<crate::gpu::GraphLayer<'_>>,
20099            std::sync::Arc<cortiq_core::CmfModel>,
20100        )> = (|| {
20101            let mut layers = Vec::with_capacity(graph_end);
20102            let mut model = None;
20103            for li in 0..graph_end {
20104                let lw = &self.weights.layers[self.phys_layer(li)];
20105                // MoE routes per token, so its experts are encoded token by
20106                // token inside the batched submit while attention and the
20107                // projections stay GEMMs. Refusing MoE here is what left
20108                // prefill running one position at a time: 33 tok/s against
20109                // 54 on decode, i.e. reading the prompt was slower than
20110                // writing the answer.
20111                let gffn = match &lw.ffn {
20112                    FfnKind::Dense(d) if d.segs.len() > 1 => {
20113                        if batch_debug {
20114                            eprintln!("batch graph: dense segmented FFN at layer {li}");
20115                        }
20116                        return None;
20117                    }
20118                    FfnKind::Dense(d) => {
20119                        let Some((act, limit)) = d.act.dense_graph_contract() else {
20120                            if batch_debug {
20121                                eprintln!(
20122                                    "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
20123                                    d.act
20124                                );
20125                            }
20126                            return None;
20127                        };
20128                        crate::gpu::GraphFfn::Dense {
20129                            gate: gw(&d.gate_proj)?,
20130                            up: gw(&d.up_proj)?,
20131                            down: gw(&d.down_proj)?,
20132                            act,
20133                            limit,
20134                            // The layer's one tube (hot neurons), always on.
20135                            hot: match d.segs.first() {
20136                                Some(seg) => Some(crate::gpu::GraphMoeDetachedShared {
20137                                    gate: gw(&seg.gate)?,
20138                                    up: gw(&seg.up)?,
20139                                    down: gw(&seg.down)?,
20140                                    width: seg.width,
20141                                }),
20142                                None => None,
20143                            },
20144                        }
20145                    }
20146                    FfnKind::Moe(m) => {
20147                        // Adaptive τ and expert masks stay on the CPU path.
20148                        // Sigmoid scores, the selection bias, a routed scale
20149                        // ≠ 1 and an ungated shared expert (hy_v3) ride the
20150                        // same flags word as the token graph — before, this
20151                        // refusal sent every Hy-MT2-30B prompt to the chunked
20152                        // fallback (8 tok/s of ingest against 53 of decode).
20153                        if m.route_tau.is_some() || m.mask.is_some() {
20154                            return None;
20155                        }
20156                        // A shared expert rides as slot top_k (gated or
20157                        // not is a flag on the select kernel); without one
20158                        // (MiMo-V2, LFM2-MoE) the kernels run top_k slots.
20159                        let inter0 = m.experts.first()?.gate_proj.rows();
20160                        // A wider shared expert (Unlimited-OCR) stays out of
20161                        // the packed bank, as on the token graph.
20162                        let detached_shared = match m.shared.as_ref() {
20163                            Some((se, None)) if se.gate_proj.rows() != inter0 || se.up_proj.rows() != inter0 => {
20164                                if !matches!(se.act, Act::Silu) {
20165                                    return None;
20166                                }
20167                                Some(crate::gpu::GraphMoeDetachedShared {
20168                                    gate: gw(&se.gate_proj)?,
20169                                    up: gw(&se.up_proj)?,
20170                                    down: gw(&se.down_proj)?,
20171                                    width: se.gate_proj.rows(),
20172                                })
20173                            }
20174                            _ => None,
20175                        };
20176                        let shared = m.shared.as_ref().filter(|_| detached_shared.is_none());
20177                        let has_shared = shared.is_some();
20178                        let shared_gated = matches!(shared, Some((_, Some(_))));
20179                        let sgate = match shared {
20180                            Some((_, Some(sg))) => gw(sg)?,
20181                            // Ungated or absent: the router plane stands in
20182                            // so the plumbing stays total; the kernel pins
20183                            // weight 1 or never reads it.
20184                            _ => gw(&m.router)?,
20185                        };
20186                        let router = gw(&m.router)?;
20187                        // The batch MoE kernels still consume raw per-token
20188                        // rows and do not carry the descriptor-aware Prism
20189                        // transform/affine bit for router or shared-gate
20190                        // planes.  Refuse rather than route an untransformed
20191                        // source activation.
20192                        if router.prism != crate::gpu::GraphPrismOp::None
20193                            || router.affine
20194                            || sgate.prism != crate::gpu::GraphPrismOp::None
20195                            || sgate.affine
20196                        {
20197                            return None;
20198                        }
20199                        let inter = m.experts.first()?.gate_proj.rows();
20200                        let mut experts = Vec::with_capacity(m.experts.len() + 1);
20201                        let mut q4tp: Option<bool> = None;
20202                        let mut gu_q2: Option<bool> = None;
20203                        let mut q82: Option<bool> = None;
20204                        // The packed MoE graph has one gate/up program per
20205                        // layer, so accept only one exact activation contract
20206                        // across routed and in-bank shared experts.
20207                        let mut moe_act = None;
20208                        for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
20209                            let Some(e_act) = e.act.wgpu_graph_act() else {
20210                                return None;
20211                            };
20212                            match moe_act {
20213                                Some(existing) if existing != e_act => return None,
20214                                None => moe_act = Some(e_act),
20215                                _ => {}
20216                            }
20217                            if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
20218                                return None;
20219                            }
20220                            // Same ladder as the token graph: q4t → q2tp
20221                            // (mixed profile: 2-bit gate/up over a q4tp
20222                            // down) → q4tp → q8_2f. Uniform across the layer.
20223                            let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
20224                            {
20225                                Some((mm, gi)) => (
20226                                    mm,
20227                                    gi,
20228                                    e.up_proj.mapped_q4t()?.1,
20229                                    e.down_proj.mapped_q4t()?.1,
20230                                    false,
20231                                    false,
20232                                    false,
20233                                ),
20234                                None => match e.gate_proj.mapped_q2tp() {
20235                                    Some((mm, gi)) => (
20236                                        mm,
20237                                        gi,
20238                                        e.up_proj.mapped_q2tp()?.1,
20239                                        e.down_proj.mapped_q4tp()?.1,
20240                                        true,
20241                                        true,
20242                                        false,
20243                                    ),
20244                                    None => match e.gate_proj.mapped_q4tp() {
20245                                        Some((mm, gi)) => (
20246                                            mm,
20247                                            gi,
20248                                            e.up_proj.mapped_q4tp()?.1,
20249                                            e.down_proj.mapped_q4tp()?.1,
20250                                            true,
20251                                            false,
20252                                            false,
20253                                        ),
20254                                        None => {
20255                                            if !crate::gpu::moe_q82_graph_on() {
20256                                                return None;
20257                                            }
20258                                            let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
20259                                            (
20260                                                mm,
20261                                                gi,
20262                                                e.up_proj.mapped_q8_2f()?.1,
20263                                                e.down_proj.mapped_q8_2f()?.1,
20264                                                false,
20265                                                false,
20266                                                true,
20267                                            )
20268                                        }
20269                                    },
20270                                },
20271                            };
20272                            if *q4tp.get_or_insert(is_p) != is_p
20273                                || *gu_q2.get_or_insert(is_q2) != is_q2
20274                                || *q82.get_or_insert(is_8) != is_8
20275                            {
20276                                return None;
20277                            }
20278                            if [gi, ui, di].into_iter().any(|idx| {
20279                                mm.tensors.get(idx).is_some_and(|t| {
20280                                    crate::prism::is_forward_weight(mm, &t.name)
20281                                        || crate::prism::is_affine_target(mm, &t.name)
20282                                })
20283                            }) {
20284                                return None;
20285                            }
20286                            model.get_or_insert_with(|| mm.clone());
20287                            experts.push((gi, ui, di));
20288                        }
20289                        crate::gpu::GraphFfn::Moe {
20290                            router,
20291                            shared_gate: sgate,
20292                            experts,
20293                            n_exp: m.experts.len(),
20294                            top_k: m.top_k,
20295                            inter,
20296                            act: moe_act?,
20297                            norm_topk: m.norm_topk_prob,
20298                            q4tp: q4tp?,
20299                            gu_q2: gu_q2.unwrap_or(false),
20300                            q82: q82.unwrap_or(false),
20301                            sigmoid: m.router_sigmoid,
20302                            sigmoid_denom_eps: m.sigmoid_denom_eps,
20303                            bias: m.expert_bias.as_deref(),
20304                            has_shared,
20305                            detached_shared,
20306                            shared_gated,
20307                            route_scale: m.routed_scaling,
20308                        }
20309                    }
20310                    _ => return None,
20311                };
20312                let attn = match &lw.attn {
20313                    AttnKind::Full {
20314                        wq,
20315                        wk,
20316                        wv,
20317                        wo,
20318                        q_norm,
20319                        k_norm,
20320                        output_gate,
20321                        softplus_gate,
20322                        bias,
20323                    } => {
20324                        // Spark-X2.6's head-wise sigmoid gate rides the batch graph
20325                        // (one g_proj row per Q head); Laguna's softplus gate does not.
20326                        let head_gate = match softplus_gate {
20327                            Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
20328                            None => None,
20329                            Some(_) => {
20330                                if batch_debug {
20331                                    eprintln!("batch graph: unsupported Full attention gate at layer {li}");
20332                                }
20333                                return None;
20334                            }
20335                        };
20336                        if self.attention_heads_per_layer.is_some() {
20337                            if batch_debug {
20338                                eprintln!("batch graph: per-layer Q heads at layer {li}");
20339                            }
20340                            return None;
20341                        }
20342                        let (m, _, _, _) =
20343                            wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
20344                        model = Some(m.clone());
20345                        crate::gpu::GraphAttn::Full {
20346                            wq: gw(wq)?,
20347                            wk: gw(wk)?,
20348                            wv: gw(wv)?,
20349                            wo: gw(wo)?,
20350                            q_norm: q_norm.as_deref(),
20351                            k_norm: k_norm.as_deref(),
20352                            late_qk_norm: self.qk_norm_after_rope,
20353                            bias: bias
20354                                .as_ref()
20355                                .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
20356                            output_gate: *output_gate,
20357                            cpu_k: self.kv_cache.layers[li].k_heads(),
20358                            cpu_v: self.kv_cache.layers[li].v_heads(),
20359                            cpu_base: self.kv_cache.layers[li].base(),
20360                            geom: self.graph_attn_geom_vres(li, &vres_e0),
20361                            head_gate,
20362                        }
20363                    }
20364                    AttnKind::LinearGdn(w) => {
20365                        let Some(cfg) = self.gdn_cfg else {
20366                            if batch_debug {
20367                                eprintln!("batch graph: no GDN config at layer {li}");
20368                            }
20369                            return None;
20370                        };
20371                        let (m, _, _, _) = w
20372                            .in_proj_qkv
20373                            .graph_weight()
20374                            .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
20375                        model = Some(m.clone());
20376                        crate::gpu::GraphAttn::Gdn {
20377                            qkv: gw(&w.in_proj_qkv)?,
20378                            z: gw(&w.in_proj_z)?,
20379                            a: gw(&w.in_proj_a)?,
20380                            b: gw(&w.in_proj_b)?,
20381                            out: gw(&w.out_proj)?,
20382                            conv1d: &w.conv1d,
20383                            a_log: &w.a_log,
20384                            dt_bias: &w.dt_bias,
20385                            norm: &w.norm,
20386                            nv: cfg.num_v_heads,
20387                            nk: cfg.num_k_heads,
20388                            dk: cfg.key_head_dim,
20389                            dv: cfg.value_head_dim,
20390                            kk: cfg.conv_kernel,
20391                            cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
20392                            preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
20393                        }
20394                    }
20395                    _ => return None,
20396                };
20397                // Gemma-4's sandwich norms and layer scalar (`gemma_batch`),
20398                // Ripple's sandwich norms (the plain tails); every other
20399                // model hands the graph none, as before.
20400                let g4 = self.global_attn.is_some() || self.attn_v_norm;
20401                let sandwich = g4 || self.value_blend.is_some();
20402                layers.push(crate::gpu::GraphLayer {
20403                    mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(li)),
20404                    input_norm: &lw.input_norm,
20405                    attn,
20406                    post_norm: &lw.post_norm,
20407                    attn_out_norm: if sandwich {
20408                        lw.attn_out_norm.as_deref()
20409                    } else {
20410                        None
20411                    },
20412                    ffn_out_norm: if sandwich { lw.ffn_out_norm.as_deref() } else { None },
20413                    layer_scale: if g4 { lw.layer_scale } else { None },
20414                    ffn: gffn,
20415                });
20416            }
20417            Some((layers, model?))
20418        })();
20419        let Some((layers, model)) = built else {
20420            {
20421                use std::sync::atomic::{AtomicBool, Ordering};
20422                static SAID: AtomicBool = AtomicBool::new(false);
20423                if !SAID.swap(true, Ordering::Relaxed) {
20424                    tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
20425                }
20426            }
20427            return crate::gpu::BatchGraphOutcome::Declined;
20428        };
20429        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
20430            eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
20431        }
20432        // SenseNova-Loop and Mage-VL: their q8_2f prompt chunks take the
20433        // batched int8 GEMM.
20434        #[cfg(feature = "gpu")]
20435        crate::gpu_wgpu::set_batch_q82_mm(self.mhc.is_some() || self.onevision2_prefill());
20436        // SenseNova-Loop image prompt: each row's absolute MRoPE angles.
20437        #[cfg(feature = "gpu")]
20438        let _mrope = self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| {
20439            let half = rd / 2;
20440            let mut tab = vec![0f32; k * 64];
20441            for (i, &p) in positions.iter().enumerate() {
20442                for j in 0..half.min(64) {
20443                    let a = pos.get(p).map_or(p as f32, |q| q[crate::sensenova::mrope_axis(j, sections)] as f32);
20444                    tab[i * 64 + j] = a * self.inv_freq[j];
20445                }
20446            }
20447            crate::gpu_wgpu::set_batch_mrope(Some(tab));
20448            struct Clear;
20449            impl Drop for Clear {
20450                fn drop(&mut self) {
20451                    crate::gpu_wgpu::set_batch_mrope(None);
20452                }
20453            }
20454            Clear
20455        });
20456        // a prompt chunk wholly before what the drafter's windows reach keeps none
20457        let taps = if positions.last().is_some_and(|&p| p < self.dflash_tap_from) { Vec::new() } else { self.dflash_taps_wanted(0, graph_end) };
20458        crate::gpu::layer_taps_arm(&taps);
20459        let outcome = crate::gpu::forward_batch_graph(
20460            &model,
20461            self.graph_kv_id,
20462            &layers,
20463            &self.inv_freq,
20464            hiddens,
20465            nh,
20466            nkv,
20467            hd,
20468            rd,
20469            self.hidden_size,
20470            self.intermediate_size,
20471            positions,
20472            self.kv_cache.max_seq_len,
20473            gemma,
20474            self.rms_eps as f32,
20475            self.attn_scale,
20476            k,
20477            &(0..graph_end)
20478                .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
20479                .collect::<Vec<_>>(),
20480            self.o1_epoch,
20481            spec,
20482            layers_run,
20483        );
20484        self.dflash_taps_keep(&taps, positions[0], k, outcome == crate::gpu::BatchGraphOutcome::Completed);
20485        outcome
20486    }
20487
20488    /// The layers a DFlash drafter reads, when a wgpu graph over layers
20489    /// `from..upto_excl` is the whole stack (nothing otherwise).
20490    fn dflash_taps_wanted(&self, from: usize, upto_excl: usize) -> Vec<usize> {
20491        match &self.dflash {
20492            Some(d) if (self.dflash_solo || dflash_pick() == Some(true) || crate::gpu::cuda_active()) && from == 0 && upto_excl >= self.num_layers => d.g.taps.clone(),
20493            _ => Vec::new(),
20494        }
20495    }
20496
20497    /// Collect what the armed graph kept for `rows` rows from `pos0`
20498    /// (`ok`: it completed) into `dflash_taps`, a row's layers joined.
20499    fn dflash_taps_keep(&self, taps: &[usize], pos0: usize, rows: usize, ok: bool) {
20500        if taps.is_empty() {
20501            return;
20502        }
20503        let (nt, hs) = (taps.len(), self.hidden_size);
20504        let mut raw = Vec::new();
20505        if !crate::gpu::layer_taps_take(&mut raw) || !ok || raw.len() != nt * rows * hs {
20506            return;
20507        }
20508        let mut joined = vec![0f32; rows * nt * hs];
20509        for s in 0..nt {
20510            for r in 0..rows {
20511                joined[(r * nt + s) * hs..][..hs].copy_from_slice(&raw[(s * rows + r) * hs..][..hs]);
20512            }
20513        }
20514        if std::env::var_os("CMF_SPEC_DBG").is_some() {
20515            let rms = (joined.iter().map(|v| (*v as f64) * (*v as f64)).sum::<f64>() / joined.len().max(1) as f64).sqrt();
20516            eprintln!("solo-dbg: {rows} rows of {nt} layers kept from position {pos0} (rms {rms:.3})");
20517        }
20518        if let Some(dir) = std::env::var_os("CMF_DFLASH_DUMP") {
20519            self.dflash_dump(std::path::Path::new(&dir), pos0, rows, &joined);
20520        }
20521        let mut kept = self.dflash_taps.lock().unwrap();
20522        kept.push((pos0, joined));
20523        // a long prompt's rows wait here until the first round: past
20524        // `DFLASH_TAPS_ROWS` the oldest chunks go, and the drafter's context
20525        // starts at what is left
20526        let mut rows: usize = kept.iter().map(|(_, r)| r.len()).sum::<usize>() / (nt * hs);
20527        let cap = match &self.dflash {
20528            Some(d) if (0..d.g.layers).all(|l| d.g.window_of(l) > 0) => {
20529                DFLASH_TAPS_ROWS.min((0..d.g.layers).map(|l| d.g.window_of(l)).max().unwrap_or(0) + crate::dflash::BLOCK_SLACK)
20530            }
20531            _ => DFLASH_TAPS_ROWS,
20532        };
20533        while rows > cap && kept.len() > 1 {
20534            rows -= kept.remove(0).1.len() / (nt * hs);
20535        }
20536    }
20537
20538    /// `CMF_DFLASH_DUMP=DIR`: what a reference run of the published drafter
20539    /// reads (`scripts/ripple/ripple_dflash_ref.py`). `taps.bin` grows by a
20540    /// record a kept chunk (position and row count as u64, then the rows'
20541    /// f32, a row's layers joined); the embedding table and the head are
20542    /// written once as f32, the table without the trunk's input scale.
20543    fn dflash_dump(&self, dir: &std::path::Path, pos0: usize, rows: usize, joined: &[f32]) {
20544        use std::io::Write;
20545        let _ = std::fs::create_dir_all(dir);
20546        let table = |name: &str, t: &QTensor| {
20547            let p = dir.join(name);
20548            if p.exists() {
20549                return;
20550            }
20551            let Ok(f) = std::fs::File::create(&p) else { return };
20552            let mut w = std::io::BufWriter::new(f);
20553            let mut row = vec![0f32; t.cols()];
20554            for r in 0..t.rows() {
20555                t.row_f32(r, &mut row);
20556                let _ = w.write_all(&f32_bytes(&row));
20557            }
20558        };
20559        table("embed.f32", &self.weights.embed_tokens);
20560        table("head.f32", &self.weights.lm_head);
20561        let meta = dir.join("meta.json");
20562        if !meta.exists() {
20563            let _ = std::fs::write(
20564                &meta,
20565                format!(
20566                    "{{\"hidden\": {}, \"vocab\": {}, \"embed_multiplier\": {}}}\n",
20567                    self.hidden_size,
20568                    self.weights.lm_head.rows(),
20569                    self.embed_multiplier
20570                ),
20571            );
20572        }
20573        if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(dir.join("taps.bin")) {
20574            let _ = f.write_all(&(pos0 as u64).to_le_bytes());
20575            let _ = f.write_all(&(rows as u64).to_le_bytes());
20576            let _ = f.write_all(&f32_bytes(joined));
20577        }
20578    }
20579
20580    /// Rows the wgpu graphs kept become the drafter's context, oldest first.
20581    fn dflash_taps_absorb(&mut self) {
20582        let kept = std::mem::take(&mut *self.dflash_taps.lock().unwrap());
20583        if let Some(d) = self.dflash.as_mut() {
20584            for (pos0, rows) in kept {
20585                d.queue_context(pos0, &rows, self.pool.as_deref());
20586            }
20587        }
20588    }
20589
20590    /// Same, stopping after layer `upto` inclusive (routing probe φ).
20591    /// `CMF_DSV4_DRAFT_PROBE=1` — grade the draft against what the trunk goes on
20592    /// to produce. Off by default; it runs a whole draft per decoded token.
20593    fn draft_probe() -> bool {
20594        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20595        *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
20596    }
20597
20598    /// `CMF_DSV4_DRAFT_PROBE=1`: measure how much of the draft the trunk
20599    /// would have agreed with, WITHOUT verifying or rolling anything back.
20600    ///
20601    /// The number this produces decides the whole speculation design — at
20602    /// acceptance a, a block of B positions yields 1 + a + a² + ... tokens
20603    /// per trunk pass — so it is worth measuring before any of the machinery
20604    /// that would exploit it exists. Each draft is parked with the position
20605    /// it was made at, and graded as the real tokens arrive.
20606    /// `CMF_DSV4_SPEC=1` — the DeepSeek-V4 speculative decode: draft five
20607    /// on the card, verify them in one batched trunk pass, commit the
20608    /// accepted prefix, roll the rest back.
20609    #[cfg(feature = "gpu")]
20610    fn dsv4_spec_on() -> bool {
20611        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20612        *ON.get_or_init(|| {
20613            // Test-only runtime gate: model loading still performs the same
20614            // reservation and trunk packing, which gives rollback parity a
20615            // topology-identical non-speculative control arm.
20616            if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
20617                return v != "0";
20618            }
20619            // An explicit value is a diagnostic force/escape hatch.  With no
20620            // knob, speculation is eligible only when model loading reserved
20621            // its bounded pack.  On small q4tp cards the geometric reserve
20622            // gate deliberately leaves this at zero: trying to build DSpark
20623            // after the exact trunk filled VRAM is both slower and a device
20624            // OOM (measured on A40).
20625            std::env::var("CMF_DSV4_SPEC")
20626                .map(|v| v != "0")
20627                .unwrap_or_else(|_| {
20628                    crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
20629                })
20630        })
20631    }
20632
20633    /// One speculative round at the decode tip. `t_next` is the token the
20634    /// sampler just committed for `next_pos`. Returns the EXTRA accepted
20635    /// tokens (possibly none) and the new position, with `graph_logits`
20636    /// left holding the last accepted position's logits — exactly what the
20637    /// loop top expects. `None` means "speculate not this round": nothing
20638    /// was committed, the caller forwards normally.
20639    #[cfg(feature = "gpu")]
20640    fn dsv4_spec_step(
20641        &mut self,
20642        tip_token: u32,
20643        t_next: u32,
20644        next_pos: usize,
20645        max_extra: usize,
20646        drafted: &mut usize,
20647        accepted_ctr: &mut usize,
20648    ) -> Option<(Vec<u32>, usize)> {
20649        let t_all = std::time::Instant::now();
20650        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
20651            thread_local! {
20652                static LAST: std::cell::Cell<Option<std::time::Instant>> =
20653                    const { std::cell::Cell::new(None) };
20654            }
20655            LAST.with(|l| {
20656                if let Some(prev) = l.get() {
20657                    eprintln!(
20658                        "between rounds {:.1} ms",
20659                        prev.elapsed().as_secs_f64() * 1e3
20660                    );
20661                }
20662                l.set(Some(std::time::Instant::now()));
20663            });
20664        }
20665        if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
20666            eprintln!("spec_step: enter pos={next_pos}");
20667        }
20668        let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
20669        let cfg = self.dsv4.as_ref().map(|b| b.2)?;
20670        // The draft state and its capture, armed exactly as the probe does.
20671        if self.dspark.is_none() {
20672            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
20673            if t.is_empty() {
20674                return None;
20675            }
20676            crate::dsv4::dspark_arm(&t, cfg.dim);
20677            self.dspark = Some(crate::dsv4::DsparkState::new(
20678                self.dsv4_mtp.len(),
20679                &cfg,
20680                t.len(),
20681            ));
20682        }
20683        let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
20684        let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
20685        if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
20686            eprintln!("spec_step: the pack was not built (targets {targets:?})");
20687        }
20688        let pack = pack?;
20689        let block = crate::dsv4::dspark_block();
20690        let b_box = self.dsv4.as_mut()?;
20691        let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
20692        let ds = self.dspark.as_mut()?;
20693        // The tip's captures: either this token ran on a normal path that
20694        // filled the thread-local, or the previous spec round left them.
20695        let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
20696        if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
20697            if dbg {
20698                eprintln!("spec_step: no capture");
20699            }
20700            return None;
20701        }
20702        ds.have_hidden = true;
20703        let tip_pos = next_pos.checked_sub(1)?;
20704        let draft_started = std::time::Instant::now();
20705        let mut conf = Vec::new();
20706        let props = crate::dsv4::dspark_draft_gpu(
20707            g,
20708            &self.dsv4_mtp,
20709            &cfg,
20710            ds,
20711            pack,
20712            st.kv_id,
20713            tip_token,
20714            tip_pos,
20715            self.pool.as_deref(),
20716            &mut conf,
20717        );
20718        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
20719        *drafted += block;
20720        if props.is_empty() || props[0] != t_next {
20721            if dbg {
20722                eprintln!(
20723                    "spec_step: draft {} (props0={:?} t_next={t_next})",
20724                    if props.is_empty() {
20725                        "empty"
20726                    } else {
20727                        "miss"
20728                    },
20729                    props.first()
20730                );
20731            }
20732            return None;
20733        }
20734        // `fed[0]` is `t_next`, which the outer loop has already committed;
20735        // only `fed[1..]` become additional output tokens. Cap the verify
20736        // transaction itself to the caller's remaining output budget instead
20737        // of merely truncating the returned vector: otherwise the KV/state
20738        // would advance past `max_tokens` and a 64-token request could return
20739        // 66 tokens (and poison a reused session with two invisible steps).
20740        let mut k_verify = crate::dsv4::dspark_verify_k()
20741            .min(props.len())
20742            .min(max_extra.saturating_add(1));
20743        // Adaptive depth: positions the draft itself doubts are paid for on
20744        // every verify and delivered almost never (natural-text survival
20745        // [.67 .50 .29 .08 .04]). `CMF_DSPARK_CONF_MIN=p` trims the fed
20746        // prefix at the first proposal whose confidence drops below p; on
20747        // predictable text the confidences stay high and nothing changes.
20748        let conf_min = {
20749            static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
20750            *M.get_or_init(|| {
20751                std::env::var("CMF_DSPARK_CONF_MIN")
20752                    .ok()
20753                    .and_then(|v| v.parse().ok())
20754                    .unwrap_or(0.0)
20755            })
20756        };
20757        if conf_min > 0.0 && conf.len() >= props.len() {
20758            let mut keep = 1usize;
20759            while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
20760                keep += 1;
20761            }
20762            k_verify = k_verify.min(keep.max(2));
20763        }
20764        if k_verify < 2 {
20765            return None;
20766        }
20767        let mut fed = Vec::with_capacity(k_verify);
20768        fed.push(t_next);
20769        fed.extend_from_slice(&props[1..k_verify]);
20770        let mut argmax = Vec::new();
20771        let mut logits_all = Vec::new();
20772        let mut walked = Vec::new();
20773        let txn = crate::dsv4::dsv4_verify_chunk(
20774            g,
20775            layers,
20776            &cfg,
20777            st,
20778            &fed,
20779            next_pos,
20780            &self.inv_freq,
20781            self.pool.as_deref(),
20782            &targets,
20783            &mut argmax,
20784            &mut logits_all,
20785            &mut walked,
20786        );
20787        if txn.is_none() && dbg {
20788            eprintln!("spec_step: verify declined");
20789        }
20790        let txn = txn?;
20791        let spec_gpu_end = txn.gpu_end;
20792        let b = fed.len();
20793        let mut accepted = 1usize;
20794        while accepted < b && fed[accepted] == argmax[accepted - 1] {
20795            accepted += 1;
20796        }
20797        // `CMF_DSV4_SPEC_FORCE_REJECT=1` — accept nothing beyond the known
20798        // token, every round: the pure rollback exerciser. The output must
20799        // stay byte-identical to the plain walk; anything else is a
20800        // transaction bug, isolated from the acceptance logic.
20801        if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
20802            accepted = 1;
20803        }
20804        if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
20805            eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
20806        }
20807        let t_fin = std::time::Instant::now();
20808        if !crate::dsv4::dsv4_spec_finish(
20809            g,
20810            layers,
20811            &cfg,
20812            st,
20813            txn,
20814            accepted,
20815            &fed,
20816            &self.inv_freq,
20817            self.pool.as_deref(),
20818        ) {
20819            tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
20820            return None;
20821        }
20822        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
20823            eprintln!(
20824                "finish(k={accepted}): {:.1} ms",
20825                t_fin.elapsed().as_secs_f64() * 1e3
20826            );
20827        }
20828        *accepted_ctr += accepted - 1;
20829        // Captures per accepted token: device targets photographed by the
20830        // batch, host targets from the verify's own walk. The last one
20831        // becomes the new tip's draft input; every one owes the ring an
20832        // entry for its position.
20833        let (hc, dim) = (cfg.hc_mult, cfg.dim);
20834        // Complete-chain layers are photographed by the fused submission;
20835        // partial device layers overwrite that slot after exact host cold-
20836        // expert correction.  Thus every target in the contiguous device
20837        // prefix has a valid per-token capture.
20838        let dev_caps: Vec<usize> = targets
20839            .iter()
20840            .copied()
20841            .filter(|&t| t < spec_gpu_end)
20842            .collect();
20843        let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
20844        if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
20845            return None;
20846        }
20847        for t in 0..accepted {
20848            let tip = t + 1 == accepted;
20849            for (slot, &tl) in targets.iter().enumerate() {
20850                if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
20851                    let lo = (di * b + t) * hc * dim;
20852                    crate::dsv4::dspark_capture(
20853                        &caps_all[lo..lo + hc * dim],
20854                        &cfg,
20855                        slot,
20856                        &mut ds.main_hidden,
20857                    );
20858                } else if tip
20859                    && crate::dsv4::dspark_peek_slot(slot, dim, {
20860                        let lo = slot * dim;
20861                        &mut ds.main_hidden[lo..lo + dim]
20862                    })
20863                {
20864                    // The tip's host-layer captures are the walk's own
20865                    // per-layer notes — exact. (The walk that ran last ended
20866                    // on exactly this token, on both the accept-all and the
20867                    // rollback path.)
20868                } else {
20869                    // Intermediate tokens: the post-tail state stands in for
20870                    // the per-layer capture on host targets below the last
20871                    // layer. Ring-entry quality only; the tip is exact.
20872                    crate::dsv4::dspark_capture(
20873                        &walked[t * hc * dim..(t + 1) * hc * dim],
20874                        &cfg,
20875                        slot,
20876                        &mut ds.main_hidden,
20877                    );
20878                }
20879            }
20880            crate::dsv4::dspark_ring_append(
20881                g,
20882                &self.dsv4_mtp,
20883                &cfg,
20884                ds,
20885                next_pos + t,
20886                self.pool.as_deref(),
20887            );
20888        }
20889        let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
20890        self.graph_logits = Some(row);
20891        // The speculative loop never runs the probe, so the trunk tally has
20892        // no other place to cycle. Armed only when someone asked for the
20893        // dump; the host tail is the only tallying path here, which is
20894        // precisely the population a partial pack would serve.
20895        if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
20896            crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
20897            crate::dsv4::pick_tally_arm();
20898        }
20899        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
20900            eprintln!(
20901                "spec_step total {:.1} ms (k={accepted})",
20902                t_all.elapsed().as_secs_f64() * 1e3
20903            );
20904        }
20905        Some((fed[1..accepted].to_vec(), next_pos + accepted))
20906    }
20907
20908    fn dspark_probe(&mut self, position: usize, token_id: u32) {
20909        if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
20910            return;
20911        }
20912        // What the trunk just routed to, for this token.
20913        let trunk_now = crate::dsv4::pick_tally_take();
20914        crate::dsv4::trunk_freq_note(&trunk_now);
20915        if !trunk_now.is_empty() {
20916            self.dspark_trunk_picks.push(trunk_now);
20917            let keep = crate::dsv4::dspark_block();
20918            if self.dspark_trunk_picks.len() > keep {
20919                self.dspark_trunk_picks.remove(0);
20920            }
20921        }
20922        // Grade whatever is waiting: the token just decoded sits at
20923        // `position`, so it answers the draft made at `position - 1 - i`.
20924        for p in std::mem::take(&mut self.dspark_pending) {
20925            let Some(i) = position.checked_sub(p.0 + 1) else {
20926                continue;
20927            };
20928            let mut p = p;
20929            if i < p.1.len() {
20930                if p.2 && p.1[i] == token_id {
20931                    p.3 = i + 1;
20932                } else {
20933                    p.2 = false;
20934                }
20935                if i + 1 < p.1.len() {
20936                    self.dspark_pending.push(p);
20937                    continue;
20938                }
20939            }
20940            self.dspark_hist.push(p.3);
20941            self.dspark_real.push(token_id);
20942        }
20943        let Some(b) = &mut self.dsv4 else { return };
20944        let (g, layers, cfg) = (&b.0, &b.1, b.2);
20945        let n_layers = layers.len();
20946        if self.dspark.is_none() {
20947            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
20948            if t.is_empty() {
20949                return;
20950            }
20951            eprintln!(
20952                "DSpark: capture from layers {t:?}, block {}",
20953                crate::dsv4::dspark_block()
20954            );
20955            crate::dsv4::dspark_arm(&t, cfg.dim);
20956            self.dspark = Some(crate::dsv4::DsparkState::new(
20957                self.dsv4_mtp.len(),
20958                &cfg,
20959                t.len(),
20960            ));
20961        }
20962        let ds = self.dspark.as_mut().unwrap();
20963        if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
20964            return; // this token ran on a path that captures nothing
20965        }
20966        let mut conf = Vec::new();
20967        crate::dsv4::pick_tally_arm();
20968        // The trunk has already consumed the adaptive VRAM budget. Until the
20969        // draft owns an explicit bounded device pack, its tensors are an
20970        // out-of-core CPU/disk tier by contract: never let per-op probes try
20971        // to squeeze another multi-gigabyte MTP expert cache onto the card.
20972        let draft_started = std::time::Instant::now();
20973        #[cfg(feature = "gpu")]
20974        let gpu_draft = crate::dsv4::dspark_gpu_on();
20975        #[cfg(not(feature = "gpu"))]
20976        let gpu_draft = false;
20977        let props = if gpu_draft {
20978            #[cfg(feature = "gpu")]
20979            {
20980                let kv_id = b.3.kv_id;
20981                match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
20982                    Some(pk) => crate::dsv4::dspark_draft_gpu(
20983                        g,
20984                        &self.dsv4_mtp,
20985                        &cfg,
20986                        ds,
20987                        pk,
20988                        kv_id,
20989                        token_id,
20990                        position,
20991                        self.pool.as_deref(),
20992                        &mut conf,
20993                    ),
20994                    None => Vec::new(),
20995                }
20996            }
20997            #[cfg(not(feature = "gpu"))]
20998            Vec::new()
20999        } else {
21000            crate::gpu::cpu_scope(|| {
21001                crate::dsv4::dspark_draft(
21002                    g,
21003                    &self.dsv4_mtp,
21004                    &cfg,
21005                    ds,
21006                    token_id,
21007                    position,
21008                    self.pool.as_deref(),
21009                    &mut conf,
21010                )
21011            })
21012        };
21013        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
21014        let draft_picks = crate::dsv4::pick_tally_take();
21015        crate::dsv4::dspark_freq_note(&draft_picks);
21016        // Re-arm for the NEXT trunk token; the probe runs after the forward,
21017        // so this is the only place that can.
21018        crate::dsv4::pick_tally_arm();
21019        if !props.is_empty() {
21020            // Two ratios, side by side: what a batched verify over the trunk
21021            // would read against what it asks for, and the same for the
21022            // draft's three stages. Near 1.0 means a batch amortises nothing.
21023            let (tu, tt) = {
21024                let flat: Vec<(usize, Vec<usize>)> = self
21025                    .dspark_trunk_picks
21026                    .iter()
21027                    .flat_map(|v| v.iter().cloned())
21028                    .collect();
21029                // Per layer, across the window of tokens.
21030                let mut per: std::collections::HashMap<usize, Vec<usize>> =
21031                    std::collections::HashMap::new();
21032                for (li, picks) in flat {
21033                    per.entry(li).or_default().extend(picks);
21034                }
21035                let n = per.len().max(1);
21036                let mut u = 0usize;
21037                let mut t = 0usize;
21038                for (_, v) in per {
21039                    t += v.len();
21040                    u += v.iter().collect::<std::collections::HashSet<_>>().len();
21041                }
21042                (u / n, t / n)
21043            };
21044            let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
21045            self.dspark_exp.push((tu, tt, du, dt));
21046            self.dspark_pending.push((position, props, true, 0));
21047        }
21048        if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
21049            let n = self.dspark_hist.len() as f32;
21050            let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
21051            let block = crate::dsv4::dspark_block();
21052            let mut at = vec![0usize; block + 1];
21053            for &k in &self.dspark_hist {
21054                at[k] += 1;
21055            }
21056            // Prefix survival: S_i = P(the first i positions all held).
21057            let mut surv = Vec::with_capacity(block);
21058            for i in 1..=block {
21059                let k = at[i..].iter().sum::<usize>() as f32 / n;
21060                surv.push(format!("{k:.2}"));
21061            }
21062            let distinct = self
21063                .dspark_real
21064                .iter()
21065                .collect::<std::collections::HashSet<_>>()
21066                .len();
21067            let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
21068                (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
21069            });
21070            let m = self.dspark_exp.len().max(1);
21071            eprintln!(
21072                "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
21073                 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
21074                self.dspark_hist.len(),
21075                mean + 1.0,
21076                surv.join(" ")
21077            );
21078            eprintln!(
21079                "DSpark: {distinct} distinct tokens of {} (degeneracy), \
21080                 trunk experts {}/{} per layer over {block} tokens, \
21081                 draft {}/{} per block, draft {:.2} ms/block",
21082                self.dspark_real.len(),
21083                tu / m,
21084                tt / m,
21085                du / m,
21086                dt / m,
21087                self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
21088            );
21089        }
21090    }
21091
21092    fn forward_layers_upto(
21093        &mut self,
21094        hidden: &[f32],
21095        position: usize,
21096        task_mask: Option<&TaskMask>,
21097        upto: Option<usize>,
21098    ) -> Vec<f32> {
21099        // In-process multi-GPU: each segment runs pinned to its card,
21100        // and the only thing crossing the boundary is one hidden vector
21101        // that never leaves this address space. Same layer split the
21102        // network mode does, minus the second process, the socket, the
21103        // serialization and the dir_hash handshake.
21104        if let Some(plan) = self.gpu_plan.clone() {
21105            if upto.is_none() && plan.len() > 1 {
21106                let mut h = hidden.to_vec();
21107                for &(dev, from, upto_incl) in plan.iter() {
21108                    h = crate::gpu::with_device(dev, || {
21109                        self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
21110                    });
21111                }
21112                return h;
21113            }
21114        }
21115        self.forward_layers_span(hidden, position, task_mask, 0, upto)
21116    }
21117
21118    /// Split this pipeline's layer stack across local GPUs: segment i
21119    /// runs on `devices[i]`. Contiguous and even by layer count — the
21120    /// VRAM-weighted planner is the next step, and an uneven card pair
21121    /// is why it will be needed. `None` clears the plan.
21122    pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
21123        self.set_gpu_plan_at(devices, None)
21124    }
21125
21126    /// The same, with an explicit first boundary (`--peer-split`): card
21127    /// 0 takes layers `[0..at)`, the rest split what remains. Uneven
21128    /// cards, or an attention-heavy head, are why this knob exists.
21129    pub fn set_gpu_plan_at(
21130        &mut self,
21131        devices: Option<&[usize]>,
21132        at: Option<usize>,
21133    ) -> Result<(), String> {
21134        let Some(devs) = devices.filter(|d| d.len() > 1) else {
21135            self.gpu_plan = None;
21136            return Ok(());
21137        };
21138        self.split_supported()?;
21139        let n = self.num_layers;
21140        if devs.len() > n {
21141            return Err(format!("{} devices for {n} layers", devs.len()));
21142        }
21143        if let Some(k) = at {
21144            if k == 0 || k >= n {
21145                return Err(format!("split at {k}: the model has {n} layers"));
21146            }
21147            if devs.len() == 2 {
21148                self.gpu_plan = Some(std::sync::Arc::new(vec![
21149                    (devs[0], 0, k - 1),
21150                    (devs[1], k, n - 1),
21151                ]));
21152                return Ok(());
21153            }
21154            return Err(format!(
21155                "an explicit split point takes exactly 2 devices, got {}",
21156                devs.len()
21157            ));
21158        }
21159        let per = n.div_ceil(devs.len());
21160        let mut plan = Vec::with_capacity(devs.len());
21161        let mut from = 0usize;
21162        for &d in devs {
21163            if from >= n {
21164                break;
21165            }
21166            let upto = (from + per - 1).min(n - 1);
21167            plan.push((d, from, upto));
21168            from = upto + 1;
21169        }
21170        self.gpu_plan = Some(std::sync::Arc::new(plan));
21171        Ok(())
21172    }
21173
21174    /// The active in-process split, if any: (device, first layer, last).
21175    pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
21176        self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
21177    }
21178
21179    /// Layer span [from ..= upto] (upto None = last layer): the building
21180    /// block the network pipeline-split rides on. `from > 0` skips the
21181    /// arch escape hatches (the pub `forward_span` refuses those archs
21182    /// first) and the whole-token graph — the plain per-layer loop is
21183    /// the canonical executor for a partial stack.
21184    fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
21185        if let Some(x) = t.as_f32() {
21186            return x.to_vec();
21187        }
21188        let mut out = vec![0.0; t.rows() * t.cols()];
21189        for r in 0..t.rows() {
21190            t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
21191        }
21192        out
21193    }
21194
21195    fn embryo_resident_eligible(&self) -> bool {
21196        fn served(t: &QTensor) -> bool {
21197            t.as_f32().is_some() || t.q8_2f_codes().is_some()
21198        }
21199        // One mixer family per file: vmf_phase (kind 0/1) or
21200        // gated_delta_net (kind 4); the anchors are full (2) or bounded (3).
21201        if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
21202            || self.num_layers != self.physical_layers
21203            || self.loop_final_norm
21204            || self.weights.layers.len() != self.num_layers
21205            || self.head_clusters.is_none()
21206            || self.final_softcap.is_some()
21207            || self.logit_multiplier.is_some()
21208            || self.attn_softcap != 0.0
21209            || self.mtp.is_some()
21210            || self.per_token_stack()
21211            || self.dsv4.is_some()
21212            || self.dsv41.is_some()
21213            || self.dory.is_some()
21214            || self.qwen4_exp.is_some()
21215            // Dynamic routing swaps FFN weights mid-sequence under the
21216            // packed graph; a blend has no single overlay. A STATIC skill
21217            // (`from_model_with_skill`) is fine: the pack reads the live
21218            // `weights.layers[*].ffn`, i.e. the skill's tensors, and a
21219            // later `set_active_skill` change drops the pack
21220            // (`invalidate_for_weight_change`).
21221            || self.dyn_router.is_some()
21222            || self.dyn_phi_layer.is_some()
21223            || self.dyn_blend_loaded
21224            || self.o1_cfg.is_some()
21225            || self.swa.is_some()
21226            || self.sliding_layers.is_some()
21227            || self.global_attn.is_some()
21228            || self.attention_heads_per_layer.is_some()
21229            || self.attn_v_norm
21230            || self
21231                .kv_cache
21232                .layers
21233                .iter()
21234                .any(|l| l.mode != crate::kv_cache::KvMode::F32)
21235            || self.rope_scale != 1.0
21236            || self.rope_scale_local != 1.0
21237            || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
21238            || self.hidden_size == 0
21239            || self.hidden_size > 1024
21240            || self.intermediate_size > 1024
21241            || self.num_heads == 0
21242            || self.num_kv_heads == 0
21243            || self.num_heads % self.num_kv_heads != 0
21244            || self.num_heads.saturating_mul(self.head_dim) > 1024
21245            || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
21246            || self.vocab_size == 0
21247            || self.kv_cache.max_seq_len == 0
21248            || self.rotary_dim == 0
21249            || self.rotary_dim > self.head_dim
21250            || self.rotary_dim % 2 != 0
21251            || self.inv_freq.len() < self.rotary_dim / 2
21252        {
21253            return false;
21254        }
21255        // The resident shaders read f32 or f16 values, or q8_2f codes through
21256        // the same formula as the CPU path. Other quantized files keep the
21257        // ordinary executor: dequantizing a Q4 tensor into the packed buffer
21258        // would silently change the operator relative to the CPU quantized
21259        // path. Measured consequence (RTX PRO 4000, S4 bounded export requantized
21260        // with `cortiq requant --quant q4tp-quantize`): `eligible=false`,
21261        // the generic wgpu whole-token graph refuses too, and the per-op
21262        // path decodes at ~73 tok/s against ~200 tok/s on the CPU q4tp path.
21263        if self.weights.lm_head.q8_2f_codes().is_some() && !self.embryo_wide_candidate(true) {
21264            return false;
21265        }
21266        if !served(&self.weights.lm_head)
21267            || !served(&self.weights.embed_tokens)
21268            || self.weights.lm_head.rows() < self.vocab_size
21269            || self.weights.lm_head.cols() != self.hidden_size
21270            || self.weights.embed_tokens.rows() < self.vocab_size
21271            || self.weights.embed_tokens.cols() != self.hidden_size
21272            || self.weights.final_norm.len() != self.hidden_size
21273        {
21274            return false;
21275        }
21276        if let Some(cfg) = self.vmf_cfg {
21277            if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
21278                || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
21279                || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
21280                || cfg.state_len() == 0
21281            {
21282                return false;
21283            }
21284        }
21285        if let Some(g) = self.gdn_cfg {
21286            // The resident GDN kernels (gpu_wgpu.rs `embryo_core_gdn_*`):
21287            // fused projection ≤ 2048 rows, nv·dv ≤ 1024, dk ≤ 128 lanes,
21288            // dv ≤ 256 lanes in vec4 rows, SiLU output gate (the Embryo
21289            // export), same rms eps as the stack.
21290            if g.num_v_heads == 0
21291                || g.num_k_heads == 0
21292                || g.num_v_heads % g.num_k_heads != 0
21293                || g.key_head_dim == 0
21294                || g.key_head_dim > 128
21295                || g.value_head_dim == 0
21296                || g.value_head_dim > 256
21297                || g.value_head_dim % 4 != 0
21298                || g.conv_kernel == 0
21299                || g.num_v_heads > 512
21300                || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
21301                || g.conv_dim() > 2048
21302                || g.conv_dim() % 4 != 0
21303                || g.hidden_size != self.hidden_size
21304                || g.output_gate_sigmoid
21305                || g.rms_eps != self.rms_eps
21306                || g.state_len() == 0
21307            {
21308                return false;
21309            }
21310        }
21311        let mut full_seen = false;
21312        for lw in &self.weights.layers {
21313            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
21314                return false;
21315            }
21316            match &lw.attn {
21317                AttnKind::LinearGdn(w) => {
21318                    let Some(g) = self.gdn_cfg else {
21319                        return false;
21320                    };
21321                    let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
21322                    if w.in_proj_qkv.rows() != g.conv_dim()
21323                        || w.in_proj_qkv.cols() != self.hidden_size
21324                        || !served(&w.in_proj_qkv)
21325                        || w.in_proj_z.rows() != nv * dv
21326                        || w.in_proj_z.cols() != self.hidden_size
21327                        || !served(&w.in_proj_z)
21328                        || w.in_proj_a.rows() != nv
21329                        || w.in_proj_a.cols() != self.hidden_size
21330                        || !served(&w.in_proj_a)
21331                        || w.in_proj_b.rows() != nv
21332                        || w.in_proj_b.cols() != self.hidden_size
21333                        || !served(&w.in_proj_b)
21334                        || w.conv1d.len() != g.conv_dim() * kk
21335                        || w.a_log.len() != nv
21336                        || w.dt_bias.len() != nv
21337                        || w.norm.len() != dv
21338                        || w.out_proj.rows() != self.hidden_size
21339                        || w.out_proj.cols() != nv * dv
21340                        || !served(&w.out_proj)
21341                    {
21342                        return false;
21343                    }
21344                }
21345                AttnKind::Linear(w) => {
21346                    let Some(cfg) = self.vmf_cfg else {
21347                        return false;
21348                    };
21349                    if w.thq.rows() != cfg.num_heads * cfg.nphase
21350                        || w.thq.cols() != self.hidden_size
21351                        || !served(&w.thq)
21352                        || w.thk.rows() != cfg.num_heads * cfg.nphase
21353                        || w.thk.cols() != self.hidden_size
21354                        || !served(&w.thk)
21355                        || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
21356                        || w.v_proj.cols() != self.hidden_size
21357                        || !served(&w.v_proj)
21358                        || w.out_proj.rows() != self.hidden_size
21359                        || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
21360                        || !served(&w.out_proj)
21361                        || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
21362                    {
21363                        return false;
21364                    }
21365                    if let Some((kg, kb)) = &w.k_gate {
21366                        if kg.rows() != cfg.num_heads
21367                            || kg.cols() != self.hidden_size
21368                            || !served(&kg)
21369                            || kb.len() != cfg.num_heads
21370                        {
21371                            return false;
21372                        }
21373                    }
21374                    if let Some(conv) = &w.conv {
21375                        if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
21376                            return false;
21377                        }
21378                    }
21379                }
21380                AttnKind::Full {
21381                    wq,
21382                    wk,
21383                    wv,
21384                    wo,
21385                    q_norm,
21386                    k_norm,
21387                    output_gate,
21388                    softplus_gate,
21389                    bias,
21390                } => {
21391                    if full_seen
21392                        || q_norm.is_some()
21393                        || k_norm.is_some()
21394                        || *output_gate
21395                        || softplus_gate.is_some()
21396                        || bias.is_some()
21397                        || !served(&wq)
21398                        || !served(&wk)
21399                        || !served(&wv)
21400                        || !served(&wo)
21401                        || wq.rows() != self.num_heads * self.head_dim
21402                        || wk.rows() != self.num_kv_heads * self.head_dim
21403                        || wv.rows() != self.num_kv_heads * self.head_dim
21404                        || wq.cols() != self.hidden_size
21405                        || wk.cols() != self.hidden_size
21406                        || wv.cols() != self.hidden_size
21407                        || wo.rows() != self.hidden_size
21408                        || wo.cols() != self.num_heads * self.head_dim
21409                    {
21410                        return false;
21411                    }
21412                    full_seen = true;
21413                }
21414                AttnKind::Bounded(w) => {
21415                    // The resident bounded attend walks S + W keys in
21416                    // 256-lane tiles (online softmax), any window the format
21417                    // allows; per-group windows come from header word 22.
21418                    let Some(ac) = self.anchor_core.as_ref() else {
21419                        return false;
21420                    };
21421                    if self.bounded_rope.is_none()
21422                        || w.window != ac.window
21423                        || w.sink != ac.sink
21424                        || w.group_windows != ac.group_windows
21425                        || w.window == 0
21426                        || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
21427                        || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
21428                        || !served(&w.wq)
21429                        || !served(&w.wk)
21430                        || !served(&w.wv)
21431                        || !served(&w.wo)
21432                        || w.wq.rows() != self.num_heads * self.head_dim
21433                        || w.wk.rows() != self.num_kv_heads * self.head_dim
21434                        || w.wv.rows() != self.num_kv_heads * self.head_dim
21435                        || w.wq.cols() != self.hidden_size
21436                        || w.wk.cols() != self.hidden_size
21437                        || w.wv.cols() != self.hidden_size
21438                        || w.wo.rows() != self.hidden_size
21439                        || w.wo.cols() != self.num_heads * self.head_dim
21440                    {
21441                        return false;
21442                    }
21443                }
21444                _ => return false,
21445            }
21446            match &lw.ffn {
21447                FfnKind::Dense(d) => {
21448                    if d.act != Act::Silu
21449                        || !d.segs.is_empty()
21450                        || !served(&d.gate_proj)
21451                        || !served(&d.up_proj)
21452                        || !served(&d.down_proj)
21453                        || d.gate_proj.rows() != self.intermediate_size
21454                        || d.gate_proj.cols() != self.hidden_size
21455                        || d.up_proj.rows() != self.intermediate_size
21456                        || d.up_proj.cols() != self.hidden_size
21457                        || d.down_proj.rows() != self.hidden_size
21458                        || d.down_proj.cols() != self.intermediate_size
21459                    {
21460                        return false;
21461                    }
21462                }
21463                FfnKind::Moe(m) => {
21464                    if m.resonance.is_none()
21465                        || m.top_k != 1
21466                        || m.router_sigmoid
21467                        || !m.norm_topk_prob
21468                        || m.expert_bias.is_some()
21469                        || m.routed_scaling != 1.0
21470                        || m.route_tau.is_some()
21471                        || m.shared.is_none()
21472                        || m.mask.is_some()
21473                        || m.per_expert_scale.is_some()
21474                        || m.router_input_norm
21475                        || m.experts.is_empty()
21476                        || m.experts.len() > 8
21477                    {
21478                        return false;
21479                    }
21480                    let r = m.resonance.as_ref().unwrap();
21481                    if r.mu.len() != m.experts.len() * self.hidden_size
21482                        || r.bias.len() != m.experts.len()
21483                        || r.u.len() != m.experts.len() * r.k * self.hidden_size
21484                        || r.k > 128
21485                    {
21486                        return false;
21487                    }
21488                    let Some((shared, gate)) = &m.shared else {
21489                        return false;
21490                    };
21491                    if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
21492                        return false;
21493                    }
21494                    if !served(&shared.gate_proj)
21495                        || !served(&shared.up_proj)
21496                        || !served(&shared.down_proj)
21497                        || shared.gate_proj.rows() != self.intermediate_size
21498                        || shared.gate_proj.cols() != self.hidden_size
21499                        || shared.up_proj.rows() != self.intermediate_size
21500                        || shared.up_proj.cols() != self.hidden_size
21501                        || shared.down_proj.rows() != self.hidden_size
21502                        || shared.down_proj.cols() != self.intermediate_size
21503                    {
21504                        return false;
21505                    }
21506                    for e in &m.experts {
21507                        if e.act != Act::Silu
21508                            || !e.segs.is_empty()
21509                            || !served(&e.gate_proj)
21510                            || !served(&e.up_proj)
21511                            || !served(&e.down_proj)
21512                            || e.gate_proj.rows() != self.intermediate_size
21513                            || e.gate_proj.cols() != self.hidden_size
21514                            || e.up_proj.rows() != self.intermediate_size
21515                            || e.up_proj.cols() != self.hidden_size
21516                            || e.down_proj.rows() != self.hidden_size
21517                            || e.down_proj.cols() != self.intermediate_size
21518                        {
21519                            return false;
21520                        }
21521                    }
21522                }
21523                FfnKind::DenseMoe(_) => return false,
21524            }
21525            if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
21526                return false;
21527            }
21528        }
21529        if full_seen && self.anchor_core.is_some() {
21530            return false;
21531        }
21532        full_seen || self.num_layers > 0
21533    }
21534
21535    /// The resident Embryo graph is the owner of this pipeline's forward:
21536    /// the same gate `forward_layers_span` applies before handing a token
21537    /// to `forward_embryo_graph` (both graph phases on, the explicit
21538    /// opt-in, a wgpu device, no earlier refusal, an eligible stack).
21539    fn embryo_resident_wanted(&self) -> bool {
21540        crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
21541            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
21542            && matches!(
21543                std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
21544                Ok("1") | Ok("parallel")
21545            )
21546            && crate::gpu::enabled_here()
21547            && !self.graph_refused()
21548            && self.embryo_resident_eligible()
21549    }
21550
21551    /// Chunked prefill on the resident graph: `ids` from `start` in
21552    /// chunks of `EMBRYO_CHUNK_MAX`, one submit each, projections/FFN as
21553    /// chunk GEMMs and the recurrent layers walked in time on the device.
21554    /// Returns the last position's logits when the whole span ran there.
21555    /// `None` = refused before any device work (the per-position path
21556    /// takes the span).  `CMF_EMBRYO_CHUNK=0` keeps the per-position
21557    /// prefill (A/B and the parity reference).
21558    fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
21559        if ids.len() < 2
21560            || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
21561            || !self.embryo_resident_wanted()
21562        {
21563            return None;
21564        }
21565        let model = self.ensure_embryo_graph()?;
21566        let cmax = std::env::var("CMF_EMBRYO_CHUNK")
21567            .ok()
21568            .and_then(|v| v.parse::<usize>().ok())
21569            .filter(|&v| v >= 1)
21570            .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
21571            .min(crate::gpu::EMBRYO_CHUNK_MAX);
21572        let hs = self.hidden_size;
21573        let n = ids.len();
21574        let mut pos = start;
21575        let mut last = None;
21576        let mut rows = Vec::with_capacity(cmax * hs);
21577        while pos < n {
21578            let end = (pos + cmax).min(n);
21579            rows.clear();
21580            for &id in &ids[pos..end] {
21581                rows.extend_from_slice(&self.embed_single(id));
21582            }
21583            let mut lg = Vec::new();
21584            if !crate::gpu::forward_embryo_graph_chunk(
21585                &model,
21586                self.graph_kv_id,
21587                &rows,
21588                pos,
21589                end - pos,
21590                &mut lg,
21591            ) {
21592                if pos == start {
21593                    if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
21594                        eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
21595                    }
21596                    return None;
21597                }
21598                // The device sequence advanced through the earlier chunks;
21599                // a host continuation would mix two owners of the state.
21600                // Fail this sequence alone, leaving no stale state or key.
21601                self.kv_cache.clear();
21602                self.clear_history();
21603                crate::gpu::graph_kv_reset(self.graph_kv_id);
21604                panic!(
21605                    "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
21606                );
21607            }
21608            last = Some(lg);
21609            pos = end;
21610        }
21611        if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
21612            eprintln!(
21613                "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
21614                n - start,
21615                (n - start).div_ceil(cmax)
21616            );
21617        }
21618        last
21619    }
21620
21621    /// Can this file run the wide token path? GDN mixers, bounded anchors, and
21622    /// rows and columns that fit the kernels' four-row, eight-column grid.
21623    fn embryo_wide_candidate(&self, q8: bool) -> bool {
21624        if self.embryo_wide_refused
21625            || std::env::var("CMF_EMBRYO_WIDE").as_deref() == Ok("0")
21626            || !crate::gpu::embryo_wide_ready(q8)
21627        {
21628            return false;
21629        }
21630        let Some(g) = self.gdn_cfg else {
21631            return false;
21632        };
21633        let (hidden, inter, hd) = (self.hidden_size, self.intermediate_size, self.head_dim);
21634        let (nv, nk, dk, dv) = (g.num_v_heads, g.num_k_heads, g.key_head_dim, g.value_head_dim);
21635        let (q, kv) = (self.num_heads, self.num_kv_heads);
21636        // A q8_2f row is read 16 codes at a time, an f16 row 8 values at a time.
21637        let step = if q8 { 16 } else { 8 };
21638        let geometry = hidden % step == 0
21639            && hidden <= 1024
21640            && inter % step == 0
21641            && inter <= 1024
21642            && self.vocab_size % 4 == 0
21643            && self.head_clusters.as_ref().is_some_and(|c| {
21644                let nc = c.len() / hidden.max(1);
21645                // A workgroup holds a cluster's rows.
21646                nc > 0 && nc <= 256 && self.vocab_size % nc == 0 && self.vocab_size / nc <= 256
21647            })
21648            && dk % 4 == 0
21649            && dv % 4 == 0
21650            && (nv * dv) % step == 0
21651            && nv * dv <= 1024
21652            && nv <= 508
21653            && nk > 0
21654            && nv % nk == 0
21655            && g.conv_dim() % 4 == 0
21656            && g.conv_dim() <= 2048
21657            && hd % 4 == 0
21658            && (q * hd) % step == 0
21659            && q * hd <= 1024
21660            && kv > 0
21661            && q % kv == 0
21662            && (kv * hd) % 4 == 0
21663            && (self.rotary_dim / 2) % 4 == 0;
21664        if !geometry {
21665            return false;
21666        }
21667        self.weights.layers.iter().all(|lw| {
21668            matches!(lw.attn, AttnKind::Bounded(_) | AttnKind::LinearGdn(_))
21669                && match &lw.ffn {
21670                    FfnKind::Moe(m) => m.resonance.as_ref().is_some_and(|r| m.experts.len() * (r.k + 1) <= 256) && m.shared.is_some(),
21671                    FfnKind::Dense(_) => true,
21672                    FfnKind::DenseMoe(_) => false,
21673                }
21674        })
21675    }
21676
21677    fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
21678        if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
21679            const UMAX: u32 = u32::MAX;
21680            const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
21681            const REC: usize = 64;
21682            struct Pack {
21683                data: Vec<f32>,
21684                /// The wide token path: matrices in their packed form in `mats`.
21685                wide: bool,
21686                q8: bool,
21687                mats: Vec<u8>,
21688                /// q8_2f: (codes, row scales, column field) of each matrix; its
21689                /// word in the layer record is its index in the meta after the layers.
21690                table: Vec<u32>,
21691                table_base: u32,
21692                refused: bool,
21693            }
21694            impl Pack {
21695                fn put(&mut self, x: &[f32]) -> u32 {
21696                    if x.is_empty() {
21697                        return u32::MAX;
21698                    }
21699                    // The wide kernels read the small tensors as vec4
21700                    if self.wide {
21701                        self.data.resize(self.data.len().next_multiple_of(4), 0.0);
21702                    }
21703                    let off = self.data.len();
21704                    self.data.extend_from_slice(x);
21705                    off as u32
21706                }
21707                /// A matrix a token reads in full.
21708                fn mat(&mut self, t: &QTensor) -> u32 {
21709                    if !self.wide {
21710                        return self.put(&Pipeline::embryo_qtensor_f32(t));
21711                    }
21712                    if self.q8 {
21713                        let Some((q, rows, cols)) = t.q8_2f_codes() else {
21714                            self.refused = true;
21715                            return 0;
21716                        };
21717                        self.mats.resize(self.mats.len().next_multiple_of(16), 0);
21718                        let off = self.mats.len() as u32;
21719                        self.mats.extend_from_slice(&q[..t.rows() * t.cols()]);
21720                        let (rs, cs) = (self.put(rows), self.put(cols));
21721                        let e = self.table_base + self.table.len() as u32;
21722                        self.table.extend_from_slice(&[off, rs, cs]);
21723                        return e;
21724                    }
21725                    self.mats.resize(self.mats.len().next_multiple_of(16), 0);
21726                    let off = self.mats.len() / 2;
21727                    for v in Pipeline::embryo_qtensor_f32(t) {
21728                        let h = cortiq_core::quant::f32_to_f16(v);
21729                        self.refused |= cortiq_core::quant::f16_to_f32(h).to_bits() != v.to_bits();
21730                        self.mats.extend_from_slice(&h.to_le_bytes());
21731                    }
21732                    off as u32
21733                }
21734            }
21735            // The mixer family of the file: vmf_phase geometry fills the
21736            // phase header words, gated_delta_net fills words 24..29.  A
21737            // file has exactly one linear core, so at most one is live.
21738            let vmf = self.vmf_cfg;
21739            let gdn = self.gdn_cfg;
21740            // A file of q8_2f matrices or of matrices exact in f16 runs the wide path.
21741            let q8 = self.weights.lm_head.q8_2f_codes().is_some();
21742            let wide = self.embryo_wide_candidate(q8);
21743            let mut pack = Pack {
21744                data: Vec::new(),
21745                wide,
21746                q8,
21747                mats: Vec::new(),
21748                table: Vec::new(),
21749                table_base: (HEADER + self.num_layers * REC) as u32,
21750                refused: false,
21751            };
21752            let mut meta = vec![0u32; HEADER];
21753            meta[0] = self.hidden_size as u32;
21754            meta[1] = self.intermediate_size as u32;
21755            meta[2] = self.vocab_size as u32;
21756            meta[3] = self.num_layers as u32;
21757            meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
21758            meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
21759            meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
21760            if let Some(g) = gdn {
21761                meta[24] = g.num_v_heads as u32;
21762                meta[25] = g.num_k_heads as u32;
21763                meta[26] = g.key_head_dim as u32;
21764                meta[27] = g.value_head_dim as u32;
21765                meta[28] = g.conv_kernel as u32;
21766                meta[29] = g.conv_dim() as u32;
21767            }
21768            meta[7] = self.num_heads as u32;
21769            meta[8] = self.num_kv_heads as u32;
21770            meta[9] = self.head_dim as u32;
21771            meta[10] = self.kv_cache.max_seq_len as u32;
21772            let clusters = self.head_clusters.as_ref().unwrap();
21773            let cluster_count = clusters.len() / self.hidden_size;
21774            if clusters.len() % self.hidden_size != 0
21775                || cluster_count == 0
21776                || cluster_count > 1024
21777                || self.vocab_size % cluster_count != 0
21778                || self.weights.lm_head.rows() < self.vocab_size
21779                || self.weights.final_norm.len() != self.hidden_size
21780            {
21781                return None;
21782            }
21783            meta[11] = cluster_count as u32;
21784            meta[12] = (self.vocab_size / cluster_count) as u32;
21785            meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
21786            meta[16] = self.rotary_dim as u32;
21787            meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
21788            meta[19] = (self.rms_eps as f32).to_bits();
21789            let max_conv = self
21790                .weights
21791                .layers
21792                .iter()
21793                .filter_map(|lw| match &lw.attn {
21794                    AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
21795                    _ => None,
21796                })
21797                .max()
21798                .unwrap_or(1);
21799            // One state slot per recurrent layer: the phase state plus its
21800            // hidden-wide conv ring, or the GDN record `[conv ring | S]`
21801            // (`GdnCfg::state_len`).  A file carries one mixer family, so
21802            // the stride is exactly that family's record and the device
21803            // state buffer equals the header's recurrent bytes.
21804            let phase_stride = vmf
21805                .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
21806                .unwrap_or(0);
21807            let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
21808            let state_stride = phase_stride.max(gdn_stride);
21809            // Bounded genome: the KV plane of an anchor is its ring
21810            // `[kvh][W][hd]` K + V, and only anchors own one.  Legacy full
21811            // anchors keep the `max_seq` planes indexed by layer.
21812            let bounded = self.anchor_core.clone();
21813            let (anchor_window, anchor_sink) = bounded
21814                .as_ref()
21815                .map(|ac| (ac.window, ac.sink))
21816                .unwrap_or((0, 0));
21817            let kv_stride = if bounded.is_some() {
21818                2usize
21819                    .saturating_mul(self.num_kv_heads)
21820                    .saturating_mul(anchor_window)
21821                    .saturating_mul(self.head_dim)
21822            } else {
21823                2usize
21824                    .saturating_mul(self.num_kv_heads)
21825                    .saturating_mul(self.kv_cache.max_seq_len)
21826                    .saturating_mul(self.head_dim)
21827            };
21828            meta[14] = state_stride as u32;
21829            meta[15] = kv_stride as u32;
21830            meta[18] = anchor_window as u32;
21831            meta[20] = anchor_sink as u32;
21832            // Scores, weights and the key list of each head fit the wide scratch plane.
21833            pack.refused |= pack.wide
21834                && 3 * self.num_heads * (anchor_window + anchor_sink + 3) + 256 > 65_536;
21835            meta[21] = match &self.bounded_rope {
21836                // The wide path rotates by the absolute position: no table.
21837                _ if pack.wide => UMAX,
21838                Some(rope) => {
21839                    // [W][half] cos then [W][half] sin, one contiguous table.
21840                    let off = pack.put(&rope.cos);
21841                    let _ = pack.put(&rope.sin);
21842                    off
21843                }
21844                None => UMAX,
21845            };
21846            // swa_sink_v2: one window per KV group (f32 in the pack; exact
21847            // for any window the format allows); UMAX = every group `W`.
21848            meta[22] = match bounded.as_ref().filter(|ac| !ac.group_windows.is_empty()) {
21849                Some(ac) => {
21850                    let gw: Vec<f32> = ac.group_windows.iter().map(|&w| w as f32).collect();
21851                    pack.put(&gw)
21852                }
21853                None => UMAX,
21854            };
21855            let mut full_seen = false;
21856            let mut bounded_seen = 0usize;
21857            // Recurrent state slots belong to mixer layers only (phase or
21858            // GDN): an anchor owns a ring, not a state stride, so the
21859            // device state buffer is exactly the header's recurrent bytes.
21860            let mut phase_seen = 0usize;
21861            let mut gdn_seen = 0usize;
21862            for (li, lw) in self.weights.layers.iter().enumerate() {
21863                let base = meta.len();
21864                meta.resize(base + REC, UMAX);
21865                meta[base] = match &lw.attn {
21866                    AttnKind::Linear(w) if w.phase_delta => 1,
21867                    AttnKind::Linear(_) => 0,
21868                    AttnKind::Full { .. } => 2,
21869                    AttnKind::Bounded(_) => 3,
21870                    AttnKind::LinearGdn(_) => 4,
21871                    _ => UMAX,
21872                };
21873                meta[base + 1] = pack.put(&lw.input_norm);
21874                meta[base + 2] = pack.put(&lw.post_norm);
21875                meta[base + 25] = match &lw.attn {
21876                    AttnKind::Linear(_) => {
21877                        let off = ((phase_seen + gdn_seen) * state_stride) as u32;
21878                        phase_seen += 1;
21879                        off
21880                    }
21881                    AttnKind::LinearGdn(_) => {
21882                        let off = ((phase_seen + gdn_seen) * state_stride) as u32;
21883                        gdn_seen += 1;
21884                        off
21885                    }
21886                    _ => UMAX,
21887                };
21888                match &lw.attn {
21889                    AttnKind::LinearGdn(w) => {
21890                        // Layer record words 56..63 + 29, as the resident
21891                        // kernels read them (gpu_wgpu.rs `embryo_core_gdn_*`).
21892                        meta[base + 56] = pack.mat(&w.in_proj_qkv);
21893                        meta[base + 57] = pack.mat(&w.in_proj_z);
21894                        meta[base + 58] = pack.mat(&w.in_proj_a);
21895                        meta[base + 59] = pack.mat(&w.in_proj_b);
21896                        meta[base + 60] = pack.put(&w.conv1d);
21897                        meta[base + 61] = pack.put(&w.a_log);
21898                        meta[base + 62] = pack.put(&w.dt_bias);
21899                        meta[base + 63] = pack.put(&w.norm);
21900                        meta[base + 29] = pack.mat(&w.out_proj);
21901                        meta[base + 24] = 0;
21902                    }
21903                    AttnKind::Linear(w) => {
21904                        meta[base + 3] = pack.mat(&w.thq);
21905                        meta[base + 4] = pack.mat(&w.thk);
21906                        meta[base + 5] = pack.mat(&w.v_proj);
21907                        meta[base + 6] = pack.mat(&w.out_proj);
21908                        let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
21909                        meta[base + 7] = pack.put(&decay);
21910                        if let Some((kg, kb)) = &w.k_gate {
21911                            meta[base + 8] = pack.mat(&kg);
21912                            meta[base + 9] = pack.put(kb);
21913                        }
21914                        if let Some(conv) = &w.conv {
21915                            meta[base + 10] = pack.put(conv);
21916                            meta[base + 24] = (conv.len() / self.hidden_size) as u32;
21917                        } else {
21918                            meta[base + 24] = 0;
21919                        }
21920                    }
21921                    AttnKind::Full { wq, wk, wv, wo, .. } => {
21922                        full_seen = true;
21923                        meta[base + 11] = pack.mat(&wq);
21924                        meta[base + 12] = pack.mat(&wk);
21925                        meta[base + 13] = pack.mat(&wv);
21926                        meta[base + 14] = pack.mat(&wo);
21927                        meta[base + 26] = (li * kv_stride) as u32;
21928                    }
21929                    AttnKind::Bounded(w) => {
21930                        meta[base + 11] = pack.mat(&w.wq);
21931                        meta[base + 12] = pack.mat(&w.wk);
21932                        meta[base + 13] = pack.mat(&w.wv);
21933                        meta[base + 14] = pack.mat(&w.wo);
21934                        // Ring slot of this anchor (anchors only, packed).
21935                        meta[base + 26] = (bounded_seen * kv_stride) as u32;
21936                        meta[base + 27] = pack.put(&w.sink_k);
21937                        meta[base + 28] = pack.put(&w.sink_v);
21938                        bounded_seen += 1;
21939                    }
21940                    _ => return None,
21941                }
21942                match &lw.ffn {
21943                    FfnKind::Dense(d) => {
21944                        meta[base + 15] = 0;
21945                        meta[base + 16] = 0;
21946                        meta[base + 21] = pack.mat(&d.gate_proj);
21947                        meta[base + 22] = pack.mat(&d.up_proj);
21948                        meta[base + 23] = pack.mat(&d.down_proj);
21949                    }
21950                    FfnKind::Moe(m) => {
21951                        let r = m.resonance.as_ref().unwrap();
21952                        let (shared, _) = m.shared.as_ref().unwrap();
21953                        meta[base + 15] = 1;
21954                        meta[base + 16] = m.experts.len() as u32;
21955                        meta[base + 17] = pack.put(&r.mu);
21956                        meta[base + 18] = pack.put(&r.u);
21957                        meta[base + 19] = pack.put(&r.bias);
21958                        meta[base + 20] = r.k as u32;
21959                        // Word 30: the growth shell as the runtime applies
21960                        // it now (`+inf` on trunk rows, the stored finite
21961                        // shell on grown rows, all `+inf` under
21962                        // `CMF_GROWTH_SHELL=off`), followed by one `−∞`
21963                        // sentinel at index E the kernel writes as the
21964                        // score of an expert outside its shell — WGSL has
21965                        // no infinity literal, so the value travels as
21966                        // data (`embryo_core_route_finalize`).
21967                        let mut shell = r.effective_shell(m.experts.len());
21968                        shell.push(f32::NEG_INFINITY);
21969                        meta[base + 30] = pack.put(&shell);
21970                        meta[base + 21] = pack.mat(&shared.gate_proj);
21971                        meta[base + 22] = pack.mat(&shared.up_proj);
21972                        meta[base + 23] = pack.mat(&shared.down_proj);
21973                        for (e, ex) in m.experts.iter().enumerate() {
21974                            meta[base + 32 + e * 3] =
21975                                pack.mat(&ex.gate_proj);
21976                            meta[base + 33 + e * 3] =
21977                                pack.mat(&ex.up_proj);
21978                            meta[base + 34 + e * 3] =
21979                                pack.mat(&ex.down_proj);
21980                        }
21981                    }
21982                    FfnKind::DenseMoe(_) => return None,
21983                }
21984            }
21985            if !full_seen && self.num_layers == 0 {
21986                return None;
21987            }
21988            let mut lm_head = Vec::new();
21989            let mut lm_head_packed = Vec::new();
21990            if wide && q8 {
21991                let parts = self.weights.lm_head.q8_2f_codes();
21992                if let Some((q, rows, cols)) = parts {
21993                    lm_head_packed = q[..self.weights.lm_head.rows() * self.weights.lm_head.cols()].to_vec();
21994                    let (rs, cs) = (pack.put(rows), pack.put(cols));
21995                    meta[30] = pack.table_base + pack.table.len() as u32;
21996                    pack.table.extend_from_slice(&[0, rs, cs]);
21997                }
21998                meta.extend_from_slice(&pack.table);
21999            } else if wide {
22000                for v in Self::embryo_qtensor_f32(&self.weights.lm_head) {
22001                    let h = cortiq_core::quant::f32_to_f16(v);
22002                    pack.refused |= cortiq_core::quant::f16_to_f32(h).to_bits() != v.to_bits();
22003                    lm_head_packed.extend_from_slice(&h.to_le_bytes());
22004                }
22005            } else {
22006                lm_head = Self::embryo_qtensor_f32(&self.weights.lm_head);
22007            }
22008            if pack.refused {
22009                self.embryo_wide_refused = true;
22010                return self.ensure_embryo_graph();
22011            }
22012            let id = {
22013                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
22014                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
22015            };
22016            let model = crate::gpu::EmbryoGraphModel {
22017                id,
22018                hidden: self.hidden_size,
22019                intermediate: self.intermediate_size,
22020                vocab: self.vocab_size,
22021                layers: self.num_layers,
22022                phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
22023                nphase: vmf.map(|c| c.nphase).unwrap_or(0),
22024                phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
22025                anchor_q_heads: self.num_heads,
22026                anchor_kv_heads: self.num_kv_heads,
22027                anchor_head_dim: self.head_dim,
22028                rotary_dim: self.rotary_dim,
22029                rope_freq: if wide { self.inv_freq[..self.rotary_dim / 2].to_vec() } else { Vec::new() },
22030                max_seq: self.kv_cache.max_seq_len,
22031                cluster_count,
22032                cluster_size: self.vocab_size / cluster_count,
22033                phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
22034                state_stride,
22035                kv_stride,
22036                norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
22037                phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
22038                weights: pack.data,
22039                meta,
22040                lm_head,
22041                wide,
22042                wide_q8: wide && q8,
22043                mats: pack.mats,
22044                lm_head_packed,
22045                clusters: clusters.as_ref().clone(),
22046                final_norm: self.weights.final_norm.clone(),
22047                inv_freq: self.inv_freq.as_ref().clone(),
22048                bounded: bounded.is_some(),
22049                kv_layers: if bounded.is_some() {
22050                    bounded_seen
22051                } else {
22052                    self.num_layers
22053                },
22054                state_layers: phase_seen + gdn_seen,
22055                anchor_window,
22056                anchor_sink,
22057                phase_layers: phase_seen,
22058                gdn_layers: gdn_seen,
22059                gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
22060                gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
22061                gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
22062                gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
22063                gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
22064            };
22065            self.embryo_graph = Some(std::sync::Arc::new(model));
22066        }
22067        self.embryo_graph.clone()
22068    }
22069
22070    fn forward_layers_span(
22071        &mut self,
22072        hidden: &[f32],
22073        position: usize,
22074        task_mask: Option<&TaskMask>,
22075        from: usize,
22076        upto: Option<usize>,
22077    ) -> Vec<f32> {
22078        #[cfg(target_os = "macos")]
22079        let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
22080            position,
22081            from,
22082            upto.map_or(self.num_layers, |u| u.saturating_add(1)),
22083        );
22084        debug_assert!(
22085            from == 0
22086                || (self.dsv4.is_none()
22087                    && self.dsv41.is_none()
22088                    && self.qwen4_exp.is_none()
22089                    && !self.per_token_stack())
22090        );
22091        // Every plain forward — the whole-token Metal graph (`q1_graph_gpu`
22092        // wraps the GDN owners zero-copy and reallocates them on a size
22093        // change) and the CPU layer loop (reads/swaps `linear_state`) —
22094        // must see the previous speculative commit's asynchronous replay
22095        // complete. One mutex probe when nothing is pending.
22096        #[cfg(target_os = "macos")]
22097        if !crate::gpu_metal::wait_replay() {
22098            self.fail_metal_graph("the pending async replay failed before a plain forward");
22099            return vec![0.0; self.hidden_size];
22100        }
22101        if let Some(b) = &mut self.qwen4_exp {
22102            let _ = (task_mask, upto);
22103            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
22104            let mut logits = Vec::new();
22105            crate::qwen4_exp::forward_token(
22106                &b.0,
22107                &b.1,
22108                &b.2,
22109                &mut b.3,
22110                token_id,
22111                position,
22112                &self.inv_freq,
22113                self.pool.as_deref(),
22114                &mut logits,
22115                true,
22116            );
22117            self.graph_logits = Some(logits);
22118            return vec![0.0; self.hidden_size];
22119        }
22120        // DeepSeek-V4 runs its own stack: the state is hc_mult copies, and
22121        // the forward returns LOGITS, not a hidden — the head is inside it
22122        // (the final fold sits between the last layer and the norm). The
22123        // token id rides in `hidden[0]`, written by embed_single, because
22124        // the hash layers route by id rather than by content.
22125        if let Some(b) = &mut self.dsv4 {
22126            let _ = (task_mask, upto);
22127            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
22128            let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
22129            st.pos = position;
22130            let mut logits = Vec::new();
22131            crate::dsv4::forward_token(
22132                g,
22133                layers,
22134                &cfg,
22135                st,
22136                token_id,
22137                &self.inv_freq,
22138                self.pool.as_deref(),
22139                &mut logits,
22140            );
22141            self.graph_logits = Some(logits);
22142            self.dspark_probe(position, token_id);
22143            // The caller expects a hidden; the logits went out of band, as
22144            // with the fused lm_head path.
22145            return vec![0.0; self.hidden_size];
22146        }
22147        // Dory owns its complete stack and emits logits out of band.
22148        if self.dory.is_some() {
22149            let _ = (task_mask, upto);
22150            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
22151            let lg = self.dory_forward(&[token_id], position, crate::dory::Want::Last);
22152            self.graph_logits = Some(lg);
22153            return vec![0.0; self.hidden_size];
22154        }
22155        // DeepSeek-V4.1 owns its complete stack and emits logits out of band.
22156        if let Some(b) = &mut self.dsv41 {
22157            let _ = (task_mask, upto);
22158            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
22159            let mut logits = Vec::new();
22160            crate::dsv41::forward_token(
22161                &b.0,
22162                &b.1,
22163                &b.2,
22164                &mut b.3,
22165                token_id,
22166                position,
22167                self.pool.as_deref(),
22168                &mut logits,
22169            );
22170            self.graph_logits = Some(logits);
22171            return vec![0.0; self.hidden_size];
22172        }
22173        // Gemma-3n runs its own stack (4 AltUp replicas don't fit this
22174        // loop); `hidden` is the extended embedding from embed_single.
22175        // Gemma-4 E-series: same hand-off — its dual-geometry attention
22176        // with shared-KV tail layers and the PLE injection live in
22177        // `g4e_forward`.
22178        if let Some(b) = &self.g4e {
22179            let _ = (task_mask, upto);
22180            return crate::g4e::g4e_forward(
22181                &b.0,
22182                &b.1,
22183                hidden,
22184                position,
22185                &mut self.kv_cache.layers,
22186                self.num_heads,
22187                self.num_kv_heads,
22188                self.pool.as_deref(),
22189            );
22190        }
22191        if let Some(b) = &self.g3n {
22192            let _ = (task_mask, upto);
22193            return crate::g3n::g3n_forward(
22194                &b.0,
22195                &b.1,
22196                hidden,
22197                position,
22198                &mut self.kv_cache.layers,
22199                self.num_heads,
22200                self.num_kv_heads,
22201                self.head_dim,
22202                self.pool.as_deref(),
22203            );
22204        }
22205        // Cortiq Embryo owns a separate resident graph: phase recurrent
22206        // state, resonance routing, the GQA anchor KV and hierarchical head
22207        // all execute in one Vulkan submit. It is limited to a complete
22208        // unmasked stack; spans and task masks retain the exact host path.
22209        // `CMF_EMBRYO_DBG=1` names the gate that keeps a token off the
22210        // resident graph — every refusal below is otherwise silent.
22211        if from == 0
22212            && upto.is_none()
22213            && task_mask.is_none()
22214            && self.anchor_core.is_some()
22215            && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
22216        {
22217            static ONCE: std::sync::Once = std::sync::Once::new();
22218            ONCE.call_once(|| {
22219                eprintln!(
22220                    "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
22221                     unsupported={} eligible={}",
22222                    crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
22223                    crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
22224                    std::env::var("CMF_EMBRYO_RESIDENT").ok(),
22225                    crate::gpu::enabled_here(),
22226                    self.graph_refused(),
22227                    self.embryo_resident_eligible(),
22228                );
22229            });
22230        }
22231        if from == 0
22232            && upto.is_none()
22233            && task_mask.is_none()
22234            // Embryo's recurrent/KV state has no host import path.  Do not
22235            // seed it for a prefill-only graph and then silently decode from
22236            // an empty CPU cache; both phases must select the resident owner.
22237            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
22238            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
22239            // This whole-token Embryo path remains explicitly opt-in.
22240            // `CMF_GPU_WGPU_GRAPH=1` still enables the mature generic graph,
22241            // but must not silently select this model-specific resident path.
22242            && matches!(
22243                std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
22244                Ok("1") | Ok("parallel")
22245            )
22246            && crate::gpu::enabled_here()
22247            && !self.graph_refused()
22248            // Sequence owner: past position zero the device continues only
22249            // a sequence it holds. A host-owned sequence (the graph refused
22250            // at its start, or its prefix was prefilled on the host) keeps
22251            // the host path to its end — never a device attempt at p > 0
22252            // over an empty device image.
22253            && (position == 0 || self.device_sequence_position().is_some())
22254            && self.embryo_resident_eligible()
22255            && let Some(model) = self.ensure_embryo_graph()
22256        {
22257            let mut lg = Vec::new();
22258            if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
22259            {
22260                self.graph_logits = Some(lg);
22261                return vec![0.0; self.hidden_size];
22262            }
22263            if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
22264                eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
22265            }
22266            // The refusal is THIS pipeline's: falling through once is safe
22267            // at position zero (the host owns the sequence from here),
22268            // while a refusal at a later position of a device-owned
22269            // sequence would mix a host KV/state path with a partial
22270            // device sequence.
22271            self.mark_graph_refused();
22272            if position != 0 {
22273                // Fail this sequence alone and leave nothing stale behind:
22274                // no reuse key, no host or device state for the next
22275                // request on this slot to "extend".
22276                self.kv_cache.clear();
22277                self.clear_history();
22278                crate::gpu::graph_kv_reset(self.graph_kv_id);
22279                panic!(
22280                    "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
22281                );
22282            }
22283        }
22284        let mut h = hidden.to_vec();
22285        // Ripple's value residual reads this position's embedding row in every layer.
22286        // A later span of the same position (a stack split across cards)
22287        // keeps the row its first span stored.
22288        if self.value_blend.is_some() {
22289            if from == 0 {
22290                self.vres_e0.clear();
22291                self.vres_e0.extend_from_slice(hidden);
22292            }
22293            assert!(
22294                self.vres_e0.len() == hidden.len(),
22295                "value residual: a layer span past the first layer has no embedding row"
22296            );
22297        }
22298        // MiMo-V2 expert placement: decided before the graph or the per-op
22299        // arena can claim the budget the expert bank needs.
22300        self.mimo_moe_prepare();
22301        let _mimo_q8 = self
22302            .mimo_moe
22303            .is_on()
22304            .then(crate::qtensor::enter_full_gpu_q8_scope);
22305        // Split borrows: copy scalars / clone handles so the per-layer
22306        // cfg does not hold `&self` while the KV cache is `&mut`.
22307        let (nh, _nkv, _hd, hs, _rd, eps) = (
22308            self.num_heads,
22309            self.num_kv_heads,
22310            self.head_dim,
22311            self.hidden_size,
22312            self.rotary_dim,
22313            self.rms_eps,
22314        );
22315        let pool = self.pool.clone();
22316        // Opt-in wgpu token-graph attention (discrete Vulkan/DX12): the whole
22317        // attention sub-block runs resident in one submit. Off by default.
22318        // Whole-token wgpu graph: eligibility + arbitration.
22319        //  - explicit CMF_GPU_WGPU_GRAPH forces it on/off;
22320        //  - discrete adapters (4090: decode 76 -> 137 tok/s) and GDN
22321        //    hybrids (recurrent state device-resident, no CPU twin to
22322        //    race) TRUST it;
22323        //  - integrated/mobile adapters RACE it against the normal path
22324        //    at generation granularity (gpu::graph_race_*) — tiled
22325        //    mobile GPUs can turn the ~300-dispatch graph into seconds
22326        //    per token, while a fast phone GPU keeps its win.
22327        let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
22328        let graph_on = match graph_env.as_deref() {
22329            Some("0") => false,
22330            Some("prefill") => false, // decode keeps the per-op path
22331            Some(_) => true,
22332            // Unset: same discrete-only default as every other graph
22333            // site. "Is the GPU on" used to stand in here — which made
22334            // the 0.2 tok/s whole-token graph race-eligible on mobile
22335            // adapters and cost 12-14× on first tokens (cmfmobile
22336            // TUNING.md); integrated GPUs keep the per-op probe path.
22337            None => crate::gpu::wgpu_graph_default(),
22338        };
22339        let graph_trusted =
22340            graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
22341        let race_eligible =
22342            graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
22343        let mut tail_start = 0usize;
22344        if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
22345            let t_graph = std::time::Instant::now();
22346            let mut lg = Vec::new();
22347            let mut gl = 0usize;
22348            let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
22349            let declined = built.is_none();
22350            let built = match built {
22351                Some(Ok(hh)) => Some(hh),
22352                Some(Err(())) => {
22353                    // O(1) state was admitted before the device failure; the
22354                    // CPU mirrors are stale by construction.  Clear the whole
22355                    // sequence and stop rather than walking that stale state.
22356                    self.clear_sequence_state();
22357                    self.graph_failed
22358                        .store(true, std::sync::atomic::Ordering::Relaxed);
22359                    self.cancel
22360                        .store(true, std::sync::atomic::Ordering::Relaxed);
22361                    tracing::error!("token graph failed after admission; sequence state cleared");
22362                    return vec![0.0; self.hidden_size];
22363                }
22364                None => None,
22365            };
22366            // Past the transient guards (o1 still collecting, a softcap)
22367            // a refusal is about the weights and will never change —
22368            // remember it instead of walking every layer again next
22369            // token.
22370            if declined && !self.o1_active() && self.attn_softcap == 0.0 {
22371                self.mark_graph_refused();
22372            }
22373            graph_note(built.is_some(), gl, self.num_layers);
22374            if let Some(hh) = built {
22375                let dur = t_graph.elapsed();
22376                if std::env::var("CMF_GRAPH_PROF").is_ok() {
22377                    eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
22378                }
22379                if gl > 0 && gl < self.num_layers {
22380                    // Device prefix: the graph ran layers 0..gl and handed
22381                    // back the boundary hidden — the loop below owns the
22382                    // tail. The prefix layers' KV/state advanced on the
22383                    // device; the tail's advances on the host below. One
22384                    // boundary crossing per token.
22385                    h = hh;
22386                    tail_start = gl;
22387                } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
22388                    if !graph_trusted {
22389                        crate::gpu::graph_race_record(true, dur);
22390                    }
22391                    if !lg.is_empty() {
22392                        // Graph produced logits (final-norm + lm_head folded in) —
22393                        // pad/cap to vocab and hand them to the sampler directly.
22394                        lg.resize(self.vocab_size, 0.0);
22395                        if let Some(c) = self.final_softcap {
22396                            softcap_in_place(&mut lg, c, self.pool.as_deref());
22397                        }
22398                        self.graph_logits = Some(lg);
22399                    }
22400                    return hh;
22401                }
22402                // Hopeless first graph token: discard it and fall through
22403                // to the normal path. Safe exactly here — the prompt KV is
22404                // still CPU-owned (chunked prefill), so recomputing this
22405                // position is exact; the mirror's extra row is never read
22406                // (the race just settled on the normal path).
22407            }
22408        }
22409        // KIMI-LINEAR HAS NO SPLIT BUG. The 2.6× reported from the
22410        // model rotation (12.2 tok/s on one card against 4.6 on two)
22411        // was a single measurement of a model whose arm arbitration is
22412        // borderline, and it did not survive repetition. Three runs an
22413        // arm, same binary, back to back:
22414        //   probe on : 1 GPU 9.5 / 5.7 / 5.9   2 GPU 7.8 / 13.0 / 13.3
22415        //   pinned   : 1 GPU 5.6 / 5.3 / 5.2   2 GPU 3.5 / 4.2 / 3.4
22416        // With the arms pinned the split costs about 1.45×, which is
22417        // what a layer split costs. With the probe free, TWO CARDS RUN
22418        // FASTER — because for this model the CPU arm wins some op
22419        // classes and the probe finds that.
22420        //
22421        // Two things do stand, and both are measured. The token graph
22422        // builds NOTHING here (`covered 0 of 14 layers [0..14)`), so
22423        // every layer walks per-op on either arm — that is where the
22424        // headroom is, not in the split. And this model's benchmark is
22425        // unusable without `CMF_GPU_PROBE=0`: the arbitration alone
22426        // moves it by more than 2×.
22427        //
22428        // Span runs (network split): the graph covers exactly [from..=upto]
22429        // — one submit per SEGMENT per token. No race: its state is global
22430        // and calibrated on full stacks, so spans take the graph only where
22431        // it is trusted by default (discrete adapters / CMF_GPU_WGPU_GRAPH).
22432        let span = from > 0 || upto.is_some();
22433        if span && graph_on && task_mask.is_none() && graph_trusted {
22434            let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
22435            let mut lg = Vec::new();
22436            let mut gl = 0usize;
22437            let span_res =
22438                self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
22439            let span_res = match span_res {
22440                Some(Ok(hh)) => Some(hh),
22441                Some(Err(())) => {
22442                    self.clear_sequence_state();
22443                    self.graph_failed
22444                        .store(true, std::sync::atomic::Ordering::Relaxed);
22445                    self.cancel
22446                        .store(true, std::sync::atomic::Ordering::Relaxed);
22447                    tracing::error!(
22448                        "span token graph failed after admission; sequence state cleared"
22449                    );
22450                    return vec![0.0; self.hidden_size];
22451                }
22452                None => None,
22453            };
22454            graph_note(span_res.is_some(), gl, upto_excl - from);
22455            if std::env::var("CMF_GPU_DEBUG").is_ok() {
22456                // How much of the span the graph actually covered. A
22457                // prefix of nothing means every layer walks per-op and
22458                // the split's extra cost is elsewhere.
22459                static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
22460                if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
22461                    eprintln!(
22462                        "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
22463                        upto_excl - from,
22464                        span_res.is_some()
22465                    );
22466                }
22467            }
22468            if let Some(hh) = span_res {
22469                if gl == upto_excl - from {
22470                    if !lg.is_empty() {
22471                        lg.resize(self.vocab_size, 0.0);
22472                        if let Some(c) = self.final_softcap {
22473                            softcap_in_place(&mut lg, c, self.pool.as_deref());
22474                        }
22475                        self.graph_logits = Some(lg);
22476                    }
22477                    crate::gpu::set_layer(-1);
22478                    return hh;
22479                }
22480                // Partial device prefix of the span: CPU owns the tail.
22481                h = hh;
22482                tail_start = from + gl;
22483            }
22484        }
22485        // Layers the host is about to run whose device mirror moved ahead
22486        // of the host cache (a device prefix that shrank since the prompt,
22487        // a batched-prefill prefix longer than this token's): bring their
22488        // rows over first. One comparison per layer when nothing lags.
22489        let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
22490
22491        // A partial graph is an explicit GPU-prefix / CPU-tail split. Keep
22492        // the tail PURE host-side: letting its QTensor hooks re-enter the
22493        // residency arena streams every omitted layer through Vulkan and the
22494        // driver's freed-allocation cache can grow to the full model size
22495        // (25.4 GiB observed with a 14 GiB budget on Granite 30B Q8_2F).
22496        // With a MiMo expert bank the tail is not a whole-layer host
22497        // stream: its experts run from the bank (never the arena) and its
22498        // projections stay per-op on the device, which the bank's placement
22499        // left room for.
22500        let host_tail = tail_start > from;
22501        let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
22502        let automatic_gpu_prefix = self.automatic_gpu_prefix();
22503
22504        let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
22505        #[cfg(target_os = "macos")]
22506        let mut gpu_skip_until = 0usize;
22507        for li in tail_start.max(from)..self.num_layers {
22508            let _capacity_tail = automatic_gpu_prefix
22509                .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
22510                .map(|_| crate::gpu::enter_cpu_scope());
22511            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU (CMF_GPU_LAYERS)
22512            if let Some(u) = upto {
22513                if li > u {
22514                    break;
22515                }
22516            }
22517            if let Some(mask) = task_mask {
22518                if !mask.layer_alive(li) {
22519                    continue; // dead layer: residual pass-through
22520                }
22521            }
22522            // MiniMax's dynamic tail: attention + pooled MoE of this layer as
22523            // ONE device frame (single-layer graph call), cold experts on
22524            // the host. Any decline falls through to the ordinary host
22525            // layer; a post-admission device failure stops the sequence.
22526            if host_tail && task_mask.is_none() {
22527                match self.m3_pooled_layer(li, position, &mut h) {
22528                    Ok(true) => {
22529                        crate::gpu::set_layer(-1);
22530                        continue;
22531                    }
22532                    Ok(false) => {}
22533                    Err(()) => {
22534                        self.clear_sequence_state();
22535                        self.graph_failed
22536                            .store(true, std::sync::atomic::Ordering::Relaxed);
22537                        self.cancel
22538                            .store(true, std::sync::atomic::Ordering::Relaxed);
22539                        tracing::error!(
22540                            "pooled tail token graph failed after admission; sequence state cleared"
22541                        );
22542                        return vec![0.0; self.hidden_size];
22543                    }
22544                }
22545            }
22546            // Whole-block q1 token graph: a run of consecutive q1
22547            // layers — GDN and full attention — executes with one sync
22548            // per CPU attend instead of per op (macOS/Metal).
22549            #[cfg(target_os = "macos")]
22550            {
22551                if li < gpu_skip_until {
22552                    continue;
22553                }
22554                if task_mask.is_none() {
22555                    let end = self.q1_graph_gpu(li, upto, position, &mut h);
22556                    if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
22557                        // The graph may have mutated device state before a
22558                        // command-buffer error. Never continue with a CPU
22559                        // tail or read a stale host mirror after admission.
22560                        return vec![0.0; self.hidden_size];
22561                    }
22562                    if end > li {
22563                        gpu_skip_until = end;
22564                        // Looped Transformer: the graph stopped at a loop
22565                        // boundary — apply final norm before the next iteration.
22566                        if self.is_loop_end(end - 1) && end < self.num_layers {
22567                            h = inference::rms_norm(
22568                                &h,
22569                                &self.weights.final_norm,
22570                                self.rms_eps,
22571                                self.norm_style,
22572                            );
22573                        }
22574                        continue;
22575                    }
22576                }
22577            }
22578
22579            if task_mask.is_none() {
22580                match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
22581                    crate::gpu::BatchGraphOutcome::Completed => continue,
22582                    crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
22583                    crate::gpu::BatchGraphOutcome::Declined => {}
22584                }
22585            }
22586            #[cfg(feature = "gpu")]
22587            self.pull_lagging_host_kv(li, li + 1, position);
22588            // SenseNova-Loop mHC: the attention reads the routed branch of
22589            // the streams.
22590            let mhc_attn = self.mhc_layer(li).then(|| self.mhc_pre(li, false, &mut h));
22591            let lw = &self.weights.layers[self.phys_layer(li)];
22592            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
22593                if tp.parse::<usize>().ok() == Some(position) {
22594                    let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
22595                    eprintln!(
22596                        "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
22597                        h[0], h[1]
22598                    );
22599                }
22600            }
22601            // Norm into the pipeline scratch — the returning rms_norm
22602            // allocated twice per layer per token (roadmap §3 P0).
22603            let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
22604            inference::rms_norm_into(
22605                &h,
22606                &lw.input_norm,
22607                self.rms_eps,
22608                self.norm_style,
22609                &mut self.ws.n1,
22610            );
22611            drop(prof);
22612            // The row V projects, when the layer blends the embedding into it.
22613            let vres = match self.layer_value_blend(li) {
22614                Some(blend) => {
22615                    vres_lerp(&self.vres_e0, &self.ws.n1, blend, &mut self.vres_in);
22616                    true
22617                }
22618                None => false,
22619            };
22620
22621            let attn_out = match &lw.attn {
22622                AttnKind::Mla(w) => {
22623                    let inv_freq_l = self.layer_inv_freq(li);
22624                    let rs = self.layer_rope_scale(li);
22625                    let eps = self.rms_eps;
22626                    let pool = self.pool.clone();
22627                    mla_attention(
22628                        w,
22629                        &self.ws.n1,
22630                        &mut self.kv_cache.layers[li],
22631                        position,
22632                        &inv_freq_l,
22633                        rs,
22634                        eps,
22635                        pool.as_deref(),
22636                    )
22637                }
22638                AttnKind::Linear(w) => {
22639                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
22640                    vmf_phase_forward(
22641                        &self.ws.n1,
22642                        w,
22643                        &cfg,
22644                        &mut self.kv_cache.layers[li].linear_state,
22645                        self.pool.as_deref(),
22646                    )
22647                }
22648                AttnKind::Kda(w) => {
22649                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
22650                    crate::linear_core::kda_forward(
22651                        &self.ws.n1,
22652                        w,
22653                        &cfg,
22654                        &mut self.kv_cache.layers[li].linear_state,
22655                        self.pool.as_deref(),
22656                    )
22657                }
22658                AttnKind::LinearGdn(w) => {
22659                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
22660                    gdn_forward(
22661                        &self.ws.n1,
22662                        w,
22663                        &cfg,
22664                        &mut self.kv_cache.layers[li].linear_state,
22665                        self.pool.as_deref(),
22666                    )
22667                }
22668                AttnKind::ShortConv(w) => {
22669                    let cfg = self
22670                        .short_conv_cfg
22671                        .expect("short-conv layer without short_conv_cfg");
22672                    short_conv_forward(
22673                        &self.ws.n1,
22674                        w,
22675                        &cfg,
22676                        &mut self.kv_cache.layers[li].linear_state,
22677                        self.pool.as_deref(),
22678                    )
22679                }
22680                AttnKind::Bounded(w) => {
22681                    // Natively bounded anchor: insert into the ring, attend
22682                    // over sinks ∪ window. No position, nothing appended.
22683                    let rope = self
22684                        .bounded_rope
22685                        .clone()
22686                        .expect("bounded layer without an installed rotation table");
22687                    let cfg = crate::bounded::BoundedAttnCfg {
22688                        num_heads: self.num_heads,
22689                        num_kv_heads: self.num_kv_heads,
22690                        head_dim: self.head_dim,
22691                        hidden_size: hs,
22692                        scale: self.attn_scale,
22693                        rope: &rope,
22694                        pool: pool.as_deref(),
22695                    };
22696                    crate::bounded::bounded_attention(
22697                        &self.ws.n1,
22698                        w,
22699                        &mut self.kv_cache.layers[li],
22700                        &cfg,
22701                    )
22702                }
22703                AttnKind::Full { wq, wk, wv, wo, .. } if self.dash.is_some() => {
22704                    let (nh_l, hd_l) = (self.num_heads, self.head_dim);
22705                    let mut ao = vec![0f32; nh_l * hd_l];
22706                    let dash = self.dash.as_mut().expect("guarded");
22707                    crate::uocr::attention_host(
22708                        dash,
22709                        li,
22710                        &self.ws.n1,
22711                        wq,
22712                        wk,
22713                        wv,
22714                        &mut self.kv_cache.layers[li],
22715                        position,
22716                        nh_l,
22717                        hd_l,
22718                        self.rms_eps as f32,
22719                        pool.as_deref(),
22720                        &mut ao,
22721                    );
22722                    let mut out = vec![0f32; hs];
22723                    wo.matvec(&ao, &mut out, pool.as_deref());
22724                    out
22725                }
22726                AttnKind::Full {
22727                    wq,
22728                    wk,
22729                    wv,
22730                    wo,
22731                    q_norm,
22732                    k_norm,
22733                    output_gate,
22734                    softplus_gate,
22735                    bias,
22736                } if self.kv_cache.layers[li].o1_sealed() => {
22737                    // O(1) override: decode on the sealed Nyström state
22738                    // instead of the growing KV cache.
22739                    let inv_freq_l = self.layer_inv_freq(li);
22740                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
22741                    let cfg = QwenAttnCfg {
22742                        mrope: None,
22743                        num_heads: self.layer_num_heads(li),
22744                        num_kv_heads: nkv_l,
22745                        head_dim: hd_l,
22746                        hidden_size: hs,
22747                        position,
22748                        inv_freq: &inv_freq_l,
22749                        rotary_dim: rd_l,
22750                        scale: self.attn_scale,
22751                        softcap: self.attn_softcap,
22752                        window: None,
22753                        v_norm: self.attn_v_norm,
22754                        qk_norm_after_rope: self.qk_norm_after_rope,
22755                        gate_sigmoid: self.proj_gate_sigmoid,
22756                        q_norm: q_norm.as_deref(),
22757                        k_norm: k_norm.as_deref(),
22758                        output_gate: *output_gate,
22759                        softplus_gate: softplus_gate
22760                            .as_ref()
22761                            .map(|(gate, per_head)| (gate, *per_head)),
22762                        rope_scale: self.layer_rope_scale(li),
22763                        bias: bias
22764                            .as_ref()
22765                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
22766                        rms_eps: eps,
22767                        norm_style: self.norm_style,
22768                        pool: pool.as_deref(),
22769                        v_head_dim: self.layer_v_dim(li),
22770                        value_in: vres.then(|| self.vres_in.as_slice()),
22771                    };
22772                    attention::qwen_attention_nystrom(
22773                        &self.ws.n1,
22774                        wq,
22775                        wk,
22776                        wv,
22777                        wo,
22778                        &mut self.kv_cache.layers[li],
22779                        &cfg,
22780                    )
22781                }
22782                AttnKind::Full {
22783                    wq,
22784                    wk,
22785                    wv,
22786                    wo,
22787                    q_norm,
22788                    k_norm,
22789                    output_gate,
22790                    softplus_gate,
22791                    bias,
22792                } => 'attn: {
22793                    // wgpu token-graph attention (opt-in): whole sub-block in
22794                    // one submit, device K/V mirror. q1 only, no gate/bias/mask.
22795                    // Its kernel has no window, sink or narrow-V slot and
22796                    // one mirror geometry: such models stay on the CPU attend.
22797                    let dropin_reason =
22798                        graph_on.then(|| self.graph_attn_decline_reason()).flatten();
22799                    if let Some(reason) = dropin_reason {
22800                        self.note_graph_decline("wgpu attn dropin", reason);
22801                    }
22802                    if graph_on
22803                        && dropin_reason.is_none()
22804                        && !*output_gate
22805                        && softplus_gate.is_none()
22806                        && self.attention_heads_per_layer.is_none()
22807                        && bias.is_none()
22808                        && task_mask.is_none()
22809                    {
22810                        let inv_freq_l = self.layer_inv_freq(li);
22811                        let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
22812                        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
22813                        if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
22814                            wq.mapped_q1(),
22815                            wk.mapped_q1(),
22816                            wv.mapped_q1(),
22817                            wo.mapped_q1(),
22818                        ) {
22819                            let gm = gm.clone();
22820                            let mut out = vec![0f32; hs];
22821                            let cache = &self.kv_cache.layers[li];
22822                            if crate::gpu::attn_dropin(
22823                                &gm,
22824                                self.graph_kv_id,
22825                                li,
22826                                &self.ws.n1,
22827                                qi,
22828                                ki,
22829                                vi,
22830                                oi,
22831                                q_norm.as_deref(),
22832                                k_norm.as_deref(),
22833                                self.qk_norm_after_rope,
22834                                &inv_freq_l,
22835                                nh,
22836                                nkv_l,
22837                                hd_l,
22838                                rd_l,
22839                                hs,
22840                                position,
22841                                self.kv_cache.max_seq_len,
22842                                gemma,
22843                                eps as f32,
22844                                cache.k_heads(),
22845                                cache.v_heads(),
22846                                &mut out,
22847                            ) {
22848                                break 'attn out;
22849                            }
22850                        }
22851                    }
22852                    let masked = task_mask
22853                        .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
22854                        .unwrap_or(false);
22855                    let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
22856                    // The masked kernel knows one pipeline-wide geometry and
22857                    // RoPE table, no window and no sink.
22858                    let plain = self.layer_attn_plain(li);
22859                    match (masked, f32_view) {
22860                        // Historical masked path (f32 slices; the loader
22861                        // keeps masked models in f32).
22862                        (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
22863                            let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
22864                            attention::multi_head_attention(
22865                                &self.ws.n1,
22866                                q,
22867                                k,
22868                                v,
22869                                o,
22870                                &mut self.kv_cache.layers[li],
22871                                self.num_heads,
22872                                self.num_kv_heads,
22873                                self.head_dim,
22874                                self.hidden_size,
22875                                position,
22876                                &active_heads,
22877                                &self.inv_freq,
22878                            )
22879                        }
22880                        (masked, _) => {
22881                            if masked {
22882                                tracing::warn!(
22883                                    "layer {li}: head mask on quantized weights or on a \
22884                                     window/sink/per-layer-geometry layer not supported \
22885                                     yet — executing dense"
22886                                );
22887                            }
22888                            let inv_freq_l = self.layer_inv_freq(li);
22889                            let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
22890                            let cfg = QwenAttnCfg {
22891                                mrope: self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| attention::MropeView { base: 0, pos, sections }),
22892                                num_heads: self.layer_num_heads(li),
22893                                num_kv_heads: nkv_l,
22894                                head_dim: hd_l,
22895                                hidden_size: hs,
22896                                position,
22897                                inv_freq: &inv_freq_l,
22898                                rotary_dim: rd_l,
22899                                scale: self.attn_scale,
22900                                softcap: self.attn_softcap,
22901                                window: self.layer_window(li),
22902                                v_norm: self.attn_v_norm,
22903                                qk_norm_after_rope: self.qk_norm_after_rope,
22904                                gate_sigmoid: self.proj_gate_sigmoid,
22905                                q_norm: q_norm.as_deref(),
22906                                k_norm: k_norm.as_deref(),
22907                                output_gate: *output_gate,
22908                                softplus_gate: softplus_gate
22909                                    .as_ref()
22910                                    .map(|(gate, per_head)| (gate, *per_head)),
22911                                rope_scale: self.layer_rope_scale(li),
22912                                bias: bias
22913                                    .as_ref()
22914                                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
22915                                rms_eps: eps,
22916                                norm_style: self.norm_style,
22917                                pool: pool.as_deref(),
22918                                v_head_dim: self.layer_v_dim(li),
22919                                value_in: vres.then(|| self.vres_in.as_slice()),
22920                            };
22921                            attention::qwen_attention(
22922                                &self.ws.n1,
22923                                wq,
22924                                wk,
22925                                wv,
22926                                wo,
22927                                &mut self.kv_cache.layers[li],
22928                                &cfg,
22929                            )
22930                        }
22931                    }
22932                }
22933            };
22934            // Gemma sandwich norm: normalize the attention branch before
22935            // it joins the residual stream.
22936            let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
22937                Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
22938                None => attn_out,
22939            };
22940            let mhc_ffn = match mhc_attn {
22941                Some(c) => {
22942                    // mHC: fold the attention output in, route the FFN's
22943                    // branch, and norm that.
22944                    self.mhc_post(li, false, &attn_out, &c, &mut h);
22945                    let cf = self.mhc_pre(li, true, &mut h);
22946                    let lw = &self.weights.layers[self.phys_layer(li)];
22947                    inference::rms_norm_into(&h, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
22948                    Some(cf)
22949                }
22950                None => {
22951                    let lw = &self.weights.layers[self.phys_layer(li)];
22952                    let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
22953                    inference::add_rmsnorm_fused_into(
22954                        &mut h,
22955                        &attn_out,
22956                        &lw.post_norm,
22957                        self.rms_eps,
22958                        self.norm_style,
22959                        &mut self.ws.p1,
22960                    );
22961                    drop(prof);
22962                    None
22963                }
22964            };
22965            let lw = &self.weights.layers[self.phys_layer(li)];
22966            let mut attn_out = attn_out;
22967            attention::recycle_buf(&mut attn_out);
22968            let post_normed = &self.ws.p1;
22969
22970            let ffn_masked = task_mask
22971                .map(|m| m.ffn_active_count(li) < self.intermediate_size)
22972                .unwrap_or(false);
22973            // One masked dense CONTRACT, dispatched by cost. The
22974            // activation-zeroing arm (the batched sweep's, validated
22975            // against the replica to 0.8%) computes the FULL fused FFN
22976            // and zeroes the dead — right whenever most neurons live.
22977            // The sparse arm reads ONLY active rows and down columns —
22978            // per-row dots are slower per element than the fused kernel,
22979            // so it pays only once the mask is deep enough. The 0.5
22980            // crossover is first-principles (fused kernels run ~2x the
22981            // per-row dot throughput); a shallow specialist (95% alive)
22982            // stays fused, a --target-sparsity bake flips arms on its
22983            // own weight.
22984            let ffn_out = match (ffn_masked, &lw.ffn) {
22985                // A defragged tube layer answers its own mask: the core
22986                // always runs, each tube runs when its bit is on, and
22987                // the tubes that are off are never read from the mmap.
22988                (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
22989                    let row = task_mask
22990                        .and_then(|tm| tm.ffn_masks.get(li))
22991                        .map(|v| v.as_slice());
22992                    tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
22993                }
22994                (true, FfnKind::Dense(d)) => {
22995                    let tm = task_mask.unwrap();
22996                    let alive = tm.ffn_active_count(li);
22997                    let deep = alive * 2 <= self.intermediate_size;
22998                    if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
22999                        let active = tm.ffn_active_indices(li);
23000                        sparse_ffn_quant(
23001                            d,
23002                            post_normed,
23003                            &active,
23004                            self.hidden_size,
23005                            self.pool.as_deref(),
23006                        )
23007                    } else if deep
23008                        && let (Some(g), Some(u), Some(dn)) = (
23009                            d.gate_proj.as_f32(),
23010                            d.up_proj.as_f32(),
23011                            d.down_proj.as_f32(),
23012                        )
23013                    {
23014                        let active = tm.ffn_active_indices(li);
23015                        inference::sparse_ffn_forward(
23016                            post_normed,
23017                            g,
23018                            u,
23019                            dn,
23020                            self.hidden_size,
23021                            self.intermediate_size,
23022                            &active,
23023                            self.pool.as_deref(),
23024                        )
23025                    } else {
23026                        let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
23027                        dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
23028                    }
23029                }
23030                (true, FfnKind::Moe(m)) => {
23031                    // MoE is sparse by expert selection; a task mask
23032                    // narrows the ROUTABLE set via its expert fields
23033                    // (spec §5) when it carries them.
23034                    let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
23035                    ffn_forward(
23036                        &lw.ffn,
23037                        post_normed,
23038                        self.pool.as_deref(),
23039                        allowed.as_deref(),
23040                    )
23041                }
23042                (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
23043                    dm,
23044                    post_normed,
23045                    &h,
23046                    self.rms_eps,
23047                    self.norm_style,
23048                    self.pool.as_deref(),
23049                ),
23050                (false, _) => match &lw.ffn {
23051                    FfnKind::DenseMoe(dm) => dense_moe_ffn(
23052                        dm,
23053                        post_normed,
23054                        &h,
23055                        self.rms_eps,
23056                        self.norm_style,
23057                        self.pool.as_deref(),
23058                    ),
23059                    FfnKind::Moe(m)
23060                        if task_mask.is_none()
23061                            && host_tail
23062                            && li >= tail_start
23063                            && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
23064                    {
23065                        moe_ffn_minicpm_tail(
23066                            &mut self.minicpm_tail,
23067                            li,
23068                            self.num_layers,
23069                            m,
23070                            post_normed,
23071                            self.pool.as_deref(),
23072                        )
23073                    }
23074                    FfnKind::Moe(m)
23075                        if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
23076                    {
23077                        moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
23078                    }
23079                    _ => {
23080                        let allowed = match (&lw.ffn, task_mask) {
23081                            (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
23082                            _ => None,
23083                        };
23084                        ffn_forward(
23085                            &lw.ffn,
23086                            post_normed,
23087                            self.pool.as_deref(),
23088                            allowed.as_deref(),
23089                        )
23090                    }
23091                },
23092            };
23093            let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
23094                Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
23095                None => ffn_out,
23096            };
23097            match mhc_ffn {
23098                Some(c) => self.mhc_post(li, true, &ffn_out, &c, &mut h),
23099                None => {
23100                    for (i, &f) in ffn_out.iter().enumerate() {
23101                        h[i] += f;
23102                    }
23103                }
23104            }
23105            let mut ffn_out = ffn_out;
23106            attention::recycle_buf(&mut ffn_out);
23107
23108            // Gemma-4: the layer output is scaled by a learned scalar.
23109            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
23110                for v in h.iter_mut() {
23111                    *v *= sc;
23112                }
23113            }
23114            // CMF_LAYER_DUMP: this position's hidden after layer li.
23115            if self.layer_dump.is_some() {
23116                self.dump_layer_row(position, li, &h);
23117            }
23118
23119            // Looped Transformer: apply final norm at the end of each loop iteration.
23120            // Nanbeige 4.2: after layer 21 (virtual), apply norm before looping back to layer 0.
23121            if self.is_loop_end(li) && li + 1 < self.num_layers {
23122                h = inference::rms_norm(
23123                    &h,
23124                    &self.weights.final_norm,
23125                    self.rms_eps,
23126                    self.norm_style,
23127                );
23128            }
23129
23130            // Dynamic routing φ capture (on-policy): the
23131            // EMA of the post-residual hidden at the router's phi_layer,
23132            // updated as the context evolves during decode.
23133            if self.dyn_phi_layer == Some(li) {
23134                self.update_dyn_phi(&h);
23135            }
23136        }
23137        crate::gpu::set_layer(-1); // layers done — lm_head outside layer-split
23138        if let Some(t) = t_race_cpu {
23139            crate::gpu::graph_race_record(false, t.elapsed());
23140        }
23141
23142        h
23143    }
23144
23145    /// EMA of φ at the router layer (rolling, weight 0.2 = ~5-token
23146    /// horizon). First observation seeds it exactly.
23147    fn update_dyn_phi(&mut self, h: &[f32]) {
23148        const A: f32 = 0.2;
23149        if self.dyn_phi_ema.len() != h.len() {
23150            self.dyn_phi_ema = vec![0.0; h.len()];
23151            self.dyn_phi_seen = 0;
23152        }
23153        if self.dyn_phi_seen == 0 {
23154            self.dyn_phi_ema.copy_from_slice(h);
23155        } else {
23156            for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
23157                *e = (1.0 - A) * *e + A * v;
23158            }
23159        }
23160        self.dyn_phi_seen += 1;
23161    }
23162
23163    /// Current router φ (EMA at phi_layer); empty until first capture.
23164    pub fn dyn_phi(&self) -> &[f32] {
23165        &self.dyn_phi_ema
23166    }
23167
23168    /// Enable/disable φ capture at the router layer, reset the EMA.
23169    pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
23170        self.dyn_phi_layer = layer;
23171        self.dyn_phi_ema.clear();
23172        self.dyn_phi_seen = 0;
23173    }
23174
23175    /// Skills eligible for dynamic switching: (index, id, phi_layer).
23176    pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
23177        let Some(model) = &self.model else {
23178            return Vec::new();
23179        };
23180        model
23181            .header
23182            .skills
23183            .iter()
23184            .enumerate()
23185            .filter_map(|(i, sk)| {
23186                // A v2 record routes only through the request-level
23187                // backbone-gated decision (its status and gate are
23188                // checked there), never per token.
23189                if sk.is_v2() {
23190                    return None;
23191                }
23192                let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
23193                let sel = sk.selection.as_ref()?;
23194                (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
23195            })
23196            .collect()
23197    }
23198
23199    /// Index of the currently overlaid skill (None = backbone).
23200    pub fn active_skill(&self) -> Option<usize> {
23201        self.dyn_active
23202    }
23203
23204    /// Enable dynamic per-token skill routing: build the hysteresis
23205    /// router from the container's routable skills, start φ capture at
23206    /// their (shared) phi_layer. Returns the number of routable skills
23207    /// (0 = nothing to route; router stays off). Idempotent.
23208    pub fn enable_dynamic_routing(&mut self) -> usize {
23209        use crate::swarm::{DynRouter, RoutableSkill};
23210        let Some(model) = self.model.clone() else {
23211            return 0;
23212        };
23213        // Router policy v2 (spec §9.4) routes per REQUEST: the backbone is
23214        // the default and only the backbone-gated decision may pick a
23215        // skill. A per-token switch would bypass that gate (and change
23216        // the O(1) state mid-sequence), so the hysteresis router never
23217        // runs on such a file; the caller keeps the request-level
23218        // decision.
23219        if let Some(r) = &model.header.router {
23220            tracing::warn!(
23221                "dynamic routing disabled: this file declares router policy '{}' with \
23222                 granularity \"{}\" — the request-level decision applies instead",
23223                r.policy,
23224                r.granularity
23225            );
23226            return 0;
23227        }
23228        // Format-v2 skill records (bit SKILLS_V2) without a router policy:
23229        // their status/gate contract ("auto-routing requires active +
23230        // measured") lives in the backbone-gated decision only — the
23231        // hysteresis router would switch into a quarantined record
23232        // (fail-open). Refuse the whole file, not just its v2 records.
23233        if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
23234            || model.header.skills.iter().any(|s| s.is_v2())
23235        {
23236            tracing::warn!(
23237                "dynamic routing disabled: this file carries format-v2 skill records \
23238                 (SKILLS_V2) — they route per request through a router policy only"
23239            );
23240            return 0;
23241        }
23242        // A blend materialized f32 working tensors into the layers; there
23243        // is no single skill index to revert from → refuse (honest).
23244        if self.dyn_blend_loaded {
23245            tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
23246            return 0;
23247        }
23248        // A statically-overlaid skill that is NOT FFN-eligible can't be
23249        // cheaply reverted at generation start → refuse rather than
23250        // silently keep it overlaid.
23251        if let Some(a) = self.dyn_active {
23252            if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
23253                tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
23254                return 0;
23255            }
23256        }
23257        let hidden = self.hidden_size;
23258        let mut skills = Vec::new();
23259        for (idx, id, _phi) in self.dynamic_skills() {
23260            if let Some(sel) = model.header.skills[idx].selection.as_ref() {
23261                if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
23262                    skills.push(rs);
23263                }
23264            }
23265        }
23266        if skills.is_empty() {
23267            return 0;
23268        }
23269        // Skills should share a phi_layer; warn (not fail) if they don't.
23270        let phi = skills[0].phi_layer;
23271        if skills.iter().any(|s| s.phi_layer != phi) {
23272            tracing::warn!("routable skills disagree on phi_layer; using {phi}");
23273        }
23274        let n = skills.len();
23275        self.set_dyn_phi_layer(Some(phi));
23276        self.dyn_router = Some(DynRouter::new(skills));
23277        n
23278    }
23279
23280    /// Human-readable switch log from the last dynamic-routed generation.
23281    pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
23282        self.dyn_router
23283            .as_ref()
23284            .map(|r| r.switches.clone())
23285            .unwrap_or_default()
23286    }
23287
23288    /// LM head: hidden → logits [vocab_size]. The dominant matvec of
23289    /// every decode step — row-parallel on the worker pool.
23290    fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
23291        let _mimo_q8 = self
23292            .mimo_moe
23293            .is_on()
23294            .then(crate::qtensor::enter_full_gpu_q8_scope);
23295        let rows = self.weights.lm_head.rows();
23296        let mut logits = attention::take_buf(rows.min(self.vocab_size));
23297        // Banked MiMo uses the same exact projection family for the
23298        // plain/draft head and the batched verification head. Read both
23299        // scale planes in-place instead of preparing per-op scale buffers.
23300        let served = self.mimo_moe.is_on()
23301            && crate::gpu::mimo_q8_short_enabled()
23302            && rows == self.vocab_size
23303            && !self.weights.lm_head.has_prism_contract()
23304            && self
23305                .weights
23306                .lm_head
23307                .graph_weight()
23308                .is_some_and(|(model, idx, kind, _)| {
23309                    kind == 7
23310                        && crate::gpu::q82_short_rows(
23311                            model,
23312                            idx,
23313                            hidden,
23314                            1,
23315                            rows,
23316                            self.hidden_size,
23317                            &mut logits,
23318                        )
23319                });
23320        if !served {
23321            self.weights
23322                .lm_head
23323                .matvec(hidden, &mut logits, self.pool.as_deref());
23324        }
23325        logits.resize(self.vocab_size, 0.0);
23326        if let Some(m) = self.logit_multiplier {
23327            for l in logits.iter_mut() {
23328                *l *= m;
23329            }
23330        }
23331        if let Some(c) = self.final_softcap {
23332            softcap_in_place(&mut logits, c, self.pool.as_deref());
23333        }
23334        if let Some(cm) = self.head_clusters.as_ref() {
23335            self.hierarchical_head_logprobs(hidden, cm, &mut logits);
23336        }
23337        logits
23338    }
23339
23340    /// Two-level head (Cortiq Embryo): in place, logits[v] ← log p(v) =
23341    /// (lc[c] − lse(lc)) + (logit[v] − lse over v's cluster block), c = v / S.
23342    fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
23343        let h = hidden.len();
23344        let ncl = cm.len() / h.max(1);
23345        if ncl == 0 || logits.len() % ncl != 0 {
23346            return;
23347        }
23348        let cs = logits.len() / ncl;
23349        // cluster logits + log-softmax
23350        let mut lc = vec![0.0f32; ncl];
23351        for c in 0..ncl {
23352            let row = &cm[c * h..(c + 1) * h];
23353            let mut s = 0.0f32;
23354            for j in 0..h {
23355                s += row[j] * hidden[j];
23356            }
23357            lc[c] = s;
23358        }
23359        let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
23360        let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
23361        for c in 0..ncl {
23362            let blk = &mut logits[c * cs..(c + 1) * cs];
23363            let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
23364            let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
23365            let add = lc[c] - lse - bl;
23366            for v in blk.iter_mut() {
23367                *v += add;
23368            }
23369        }
23370    }
23371
23372    /// Prefill `ids` and return the next-token logits — what the model
23373    /// would predict next, WITHOUT committing to generation (introspection
23374    /// for `cortiq explain`). Clears and repopulates the KV cache; leaves
23375    /// the active overlay untouched.
23376    pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
23377        #[cfg(target_os = "macos")]
23378        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
23379        self.clear_sequence_state();
23380        // This helper is used by the pooled classification endpoint, where
23381        // every request is a fresh sequence. The shared reset also clears the
23382        // wgpu token graph's device-side recurrent state.
23383        crate::gpu::graph_race_begin_generation();
23384        if task_mask.is_none() {
23385            self.o1_begin();
23386        }
23387        let mut hidden = vec![0.0f32; self.hidden_size];
23388        for (pos, &id) in ids.iter().enumerate() {
23389            let emb = self.embed_single(id);
23390            hidden = self.forward_layers(&emb, pos, task_mask);
23391        }
23392        if let Err(err) = self.o1_seal_checked() {
23393            self.o1_fail(err);
23394        }
23395        // Stacks that own their head (V4, V4.1, Qwen3.8-Flash-Next, GLM-5)
23396        // return a zero hidden and hand the logits out of band.
23397        if let Some(logits) = self.graph_logits.take() {
23398            return logits;
23399        }
23400        inference::rms_norm_into(
23401            &hidden,
23402            &self.weights.final_norm,
23403            self.rms_eps,
23404            self.norm_style,
23405            &mut self.ws.n1,
23406        );
23407        self.lm_head_forward(&self.ws.n1)
23408    }
23409}
23410
23411/// Convenience: deterministic tiny pipeline for tests.
23412pub fn create_test_pipeline(
23413    hidden_size: usize,
23414    intermediate_size: usize,
23415    num_heads: usize,
23416    num_kv_heads: usize,
23417    head_dim: usize,
23418    num_layers: usize,
23419    vocab_size: usize,
23420) -> Pipeline {
23421    // Small pseudo-random weights: constant weights make attention
23422    // degenerate and hide indexing bugs.
23423    let synth = |n: usize, salt: usize| -> Vec<f32> {
23424        (0..n)
23425            .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
23426            .collect()
23427    };
23428    let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
23429        QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23430    };
23431    let layer_weights: Vec<LayerWeights> = (0..num_layers)
23432        .map(|li| LayerWeights {
23433            input_norm: vec![1.0; hidden_size],
23434            post_norm: vec![1.0; hidden_size],
23435            attn_out_norm: None,
23436            ffn_out_norm: None,
23437            layer_scale: None,
23438            ffn: FfnKind::Dense(DenseFfn {
23439                gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
23440                up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
23441                down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
23442                act: Act::Silu,
23443                down_t: None,
23444                segs: Vec::new(),
23445            }),
23446            attn: AttnKind::Full {
23447                bias: None,
23448                wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
23449                wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
23450                wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
23451                wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
23452                q_norm: None,
23453                k_norm: None,
23454                output_gate: false,
23455                softplus_gate: None,
23456            },
23457        })
23458        .collect();
23459
23460    Pipeline::new(
23461        Tokenizer::byte_level(),
23462        PipelineWeights {
23463            embed_tokens: qt(vocab_size, hidden_size, 100),
23464            layers: layer_weights,
23465            lm_head: qt(vocab_size, hidden_size, 200),
23466            final_norm: vec![1.0; hidden_size],
23467        },
23468        hidden_size,
23469        intermediate_size,
23470        num_heads,
23471        num_kv_heads,
23472        head_dim,
23473        num_layers,
23474        num_layers, // physical_layers = num_layers (non-looped)
23475        false,      // loop_final_norm
23476        vocab_size,
23477        1e-6,
23478        10_000.0,
23479        NormStyle::Qwen,
23480        4096,
23481        SamplerConfig {
23482            seed: Some(42),
23483            ..Default::default()
23484        },
23485    )
23486}
23487
23488/// Batched dense-FFN: gate/up/down via matmat (element-wise the same
23489/// math as b × dense_ffn — the same dot kernels).
23490/// One mask bit, LSB-first per byte — `TaskMask::ffn_active_indices`'s
23491/// convention.
23492#[inline]
23493fn mask_bit(row: &[u8], j: usize) -> bool {
23494    (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
23495}
23496
23497/// Zero the CLOSED neurons' activations in a [rows × inter] panel — the
23498/// masked-inference fast path's whole trick: full fused quant compute,
23499/// then the mask lands on the ACTIVATIONS, which is arithmetically the
23500/// pruned network without touching a quantized weight byte. Whole open
23501/// bytes (0xFF = 8 open neurons) skip in one test.
23502/// `CMF_FFN_MASK_GAIN` — Patent 12 FIG. 4, variance-preserving
23503/// rescaling: truncation removes a share of the layer's output energy,
23504/// so the survivors are scaled up to put the variance back where the
23505/// downstream norm expects it. A scalar here; per layer it is
23506/// `sqrt(total energy / kept energy)`.
23507fn mask_gain() -> f32 {
23508    static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
23509    *G.get_or_init(|| {
23510        std::env::var("CMF_FFN_MASK_GAIN")
23511            .ok()
23512            .and_then(|v| v.parse().ok())
23513            .unwrap_or(1.0)
23514    })
23515}
23516
23517fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
23518    // With CMF_FFN_MEANFILL a closed neuron contributes its average
23519    // instead of nothing — same bytes read, one constant restored.
23520    let fill = meanfill().and_then(|(i, v)| {
23521        let li = crate::gpu::cur_layer();
23522        (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
23523    });
23524    for r in 0..rows {
23525        let base = r * inter;
23526        for (bi, &byte) in row.iter().enumerate() {
23527            if byte == 0xFF {
23528                continue;
23529            }
23530            let j0 = bi * 8;
23531            for bit in 0..8 {
23532                let j = j0 + bit;
23533                if j < inter && byte & (1 << bit) == 0 {
23534                    g[base + j] = fill.map_or(0.0, |f| f[j]);
23535                }
23536            }
23537        }
23538    }
23539    let gain = mask_gain();
23540    if gain != 1.0 {
23541        for v in g[..rows * inter].iter_mut() {
23542            *v *= gain;
23543        }
23544    }
23545}
23546
23547/// True when neuron `i`'s bit is set (no mask = everything runs).
23548#[inline]
23549fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
23550    row.is_none_or(|r| mask_bit(r, i))
23551}
23552
23553/// Every bit below `n` set — the common case for a tube file's CORE,
23554/// where only the tube bits vary per task.
23555fn all_bits_on(row: &[u8], n: usize) -> bool {
23556    (0..n).all(|i| mask_bit(row, i))
23557}
23558
23559/// `CMF_TUBE_TOPK` — how many tubes a TOKEN may open (0 = the task mask
23560/// decides alone). This is the dense FFN read as a mixture: the tubes
23561/// are the experts a k-means over `gate_proj` rows found, and the token
23562/// picks among them. `CMF_TUBE_SCORE=gate` scores a tube by its own
23563/// gate (realizable: only `up`/`down` of the losers go unread),
23564/// `=oracle` scores by the true `silu(gate)·up` mass (the ceiling —
23565/// only `down` is saved, and the selection has read what it predicts).
23566fn tube_topk() -> usize {
23567    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
23568    *K.get_or_init(|| {
23569        std::env::var("CMF_TUBE_TOPK")
23570            .ok()
23571            .and_then(|v| v.parse().ok())
23572            .unwrap_or(0)
23573    })
23574}
23575
23576fn tube_score_oracle() -> bool {
23577    static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
23578    *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
23579}
23580
23581/// The routed arm of `tube_ffn`: a token opens only its best `k` tubes.
23582/// At `b == 1` (decode) the losers are genuinely never read — that is
23583/// the speed. At `b > 1` (the scoring sweep) every tube is computed and
23584/// the losers' activations are zeroed instead: same arithmetic, so the
23585/// perplexity is the routed model's, measured without a per-token
23586/// gather in the middle of a GEMM.
23587fn tube_ffn_routed(
23588    d: &DenseFfn,
23589    xs: &[f32],
23590    b: usize,
23591    pool: Option<&Pool>,
23592    mask_row: Option<&[u8]>,
23593    k: usize,
23594) -> Vec<f32> {
23595    let hidden = d.down_proj.rows();
23596    let core = d.gate_proj.rows();
23597    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
23598    let mut out = match (b, core_full, mask_row) {
23599        (1, true, _) => dense_ffn(d, xs, pool),
23600        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
23601        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
23602        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
23603    };
23604    let cand: Vec<usize> = (0..d.segs.len())
23605        .filter(|&i| tube_bit(mask_row, d.segs[i].start))
23606        .collect();
23607    if cand.is_empty() {
23608        return out;
23609    }
23610    // gate (and, where the score or the batch needs it, up) per tube.
23611    // The SCORE is taken at the point the serving path could take it:
23612    // off the gate alone, or off the finished activation for the oracle.
23613    let oracle = tube_score_oracle();
23614    let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
23615    let mut scores = vec![0f32; b * cand.len()];
23616    for (ci, &i) in cand.iter().enumerate() {
23617        let seg = &d.segs[i];
23618        let w = seg.width;
23619        let mut g = vec![0.0f32; b * w];
23620        if b == 1 {
23621            seg.gate.matvec(xs, &mut g, pool);
23622        } else {
23623            seg.gate.matmat(xs, b, &mut g, pool);
23624        }
23625        for v in g.iter_mut() {
23626            *v = Act::Silu.combine(*v, 1.0);
23627        }
23628        if !oracle {
23629            for t in 0..b {
23630                scores[t * cand.len() + ci] =
23631                    g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
23632            }
23633        }
23634        if oracle || b > 1 {
23635            let mut u = vec![0.0f32; b * w];
23636            if b == 1 {
23637                seg.up.matvec(xs, &mut u, pool);
23638            } else {
23639                seg.up.matmat(xs, b, &mut u, pool);
23640            }
23641            for (a, &v) in g.iter_mut().zip(u.iter()) {
23642                *a *= v;
23643            }
23644            if oracle {
23645                for t in 0..b {
23646                    scores[t * cand.len() + ci] =
23647                        g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
23648                }
23649            }
23650        }
23651        acts.push(g);
23652    }
23653    // per-token scores and the winners
23654    let keep = k.min(cand.len());
23655    let mut scratch: Vec<f32> = Vec::new();
23656    for t in 0..b {
23657        let mut sc: Vec<(f32, usize)> = (0..cand.len())
23658            .map(|ci| (scores[t * cand.len() + ci], ci))
23659            .collect();
23660        sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
23661        let mut alive = vec![false; cand.len()];
23662        for &(_, ci) in sc.iter().take(keep) {
23663            alive[ci] = true;
23664        }
23665        if b > 1 {
23666            for (ci, a) in acts.iter_mut().enumerate() {
23667                if !alive[ci] {
23668                    let w = d.segs[cand[ci]].width;
23669                    a[t * w..(t + 1) * w].fill(0.0);
23670                }
23671            }
23672        } else {
23673            // decode: finish only the winners — the losers' up/down
23674            // (and, with the gate score, everything but their gate)
23675            // are never touched.
23676            for (ci, &i) in cand.iter().enumerate() {
23677                if !alive[ci] {
23678                    continue;
23679                }
23680                let seg = &d.segs[i];
23681                let w = seg.width;
23682                let g = &mut acts[ci];
23683                if !tube_score_oracle() {
23684                    scratch.clear();
23685                    scratch.resize(w, 0.0);
23686                    seg.up.matvec(xs, &mut scratch, pool);
23687                    for (a, &v) in g.iter_mut().zip(scratch.iter()) {
23688                        *a *= v;
23689                    }
23690                }
23691                let mut acc = vec![0.0f32; hidden];
23692                seg.down.matvec(g, &mut acc, pool);
23693                for (o, a) in out.iter_mut().zip(&acc) {
23694                    *o += *a;
23695                }
23696            }
23697        }
23698    }
23699    if b > 1 {
23700        for (ci, &i) in cand.iter().enumerate() {
23701            let seg = &d.segs[i];
23702            let mut acc = vec![0.0f32; b * hidden];
23703            seg.down.matmat(&acts[ci], b, &mut acc, pool);
23704            for (o, a) in out.iter_mut().zip(&acc) {
23705                *o += *a;
23706            }
23707        }
23708    }
23709    out
23710}
23711
23712/// FFN of a defragged tube layer: the always-on core plus the tubes the
23713/// task mask switches on. Each tube is a normal tensor triple, so the
23714/// same kernels run it and an inactive tube's bytes are never read —
23715/// that is the whole point of the defrag (a scattered mask cannot skip
23716/// bytes; a contiguous one is just a smaller matrix).
23717fn tube_ffn(
23718    d: &DenseFfn,
23719    xs: &[f32],
23720    b: usize,
23721    pool: Option<&Pool>,
23722    mask_row: Option<&[u8]>,
23723) -> Vec<f32> {
23724    if tube_topk() > 0 {
23725        return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
23726    }
23727    let hidden = d.down_proj.rows();
23728    let core = d.gate_proj.rows();
23729    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
23730    let mut out = match (b, core_full, mask_row) {
23731        (1, true, _) => dense_ffn(d, xs, pool),
23732        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
23733        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
23734        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
23735    };
23736    TUBE_SCRATCH.with(|sc| {
23737        let mut sc = sc.borrow_mut();
23738        let [g, u, acc] = &mut *sc;
23739        for seg in &d.segs {
23740            if !tube_bit(mask_row, seg.start) {
23741                continue;
23742            }
23743            let w = seg.width;
23744            g.resize(b * w, 0.0);
23745            if b == 1
23746                && d.act == Act::Silu
23747                && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
23748            {
23749                // g holds silu(gate)·up.
23750            } else {
23751                u.resize(b * w, 0.0);
23752                if b == 1 {
23753                    QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
23754                } else {
23755                    seg.gate.matmat(xs, b, g, pool);
23756                    seg.up.matmat(xs, b, u, pool);
23757                }
23758                for i in 0..b * w {
23759                    g[i] = d.act.combine(g[i], u[i]);
23760                }
23761            }
23762            acc.resize(b * hidden, 0.0);
23763            acc.fill(0.0);
23764            if b == 1 {
23765                seg.down.matvec(g, acc, pool);
23766            } else {
23767                seg.down.matmat(g, b, acc, pool);
23768            }
23769            for (o, a) in out.iter_mut().zip(acc.iter()) {
23770                *o += *a;
23771            }
23772        }
23773        out
23774    })
23775}
23776
23777thread_local! {
23778    /// gate / up / down-accumulator scratch for the tube loop — a tube
23779    /// runs once per layer per token, and a fresh Vec each time is a
23780    /// malloc per tube per layer per token.
23781    static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
23782        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
23783}
23784
23785/// An expert's tubes (`mlp.experts.{e}.*_proj.tube1.weight`: its hot neurons at eight
23786/// bits, their rows zero in the expert's own triple) added to `out` with the expert's
23787/// routing weight. The MoE paths compute the triple themselves; this is the rest.
23788pub(crate) fn expert_tubes_add(d: &DenseFfn, x: &[f32], weight: f32, out: &mut [f32], pool: Option<&Pool>) {
23789    if d.segs.is_empty() {
23790        return;
23791    }
23792    TUBE_SCRATCH.with(|sc| {
23793        let mut sc = sc.borrow_mut();
23794        let [g, u, acc] = &mut *sc;
23795        for seg in &d.segs {
23796            let w = seg.width;
23797            g.resize(w, 0.0);
23798            if !(d.act == Act::Silu && QTensor::matvec_silu_mul(&seg.gate, &seg.up, x, g, pool)) {
23799                u.resize(w, 0.0);
23800                QTensor::matvec_many([&seg.gate, &seg.up], x, [g, u], pool);
23801                for i in 0..w {
23802                    g[i] = d.act.combine(g[i], u[i]);
23803                }
23804            }
23805            acc.resize(out.len(), 0.0);
23806            acc.fill(0.0);
23807            seg.down.matvec(g, acc, pool);
23808            for (o, a) in out.iter_mut().zip(acc.iter()) {
23809                *o += weight * *a;
23810            }
23811        }
23812    });
23813}
23814
23815/// The shared expert's weight on one token: σ of its gate, or 1 without one.
23816pub(crate) fn shared_expert_weight(gate: Option<&QTensor>, x: &[f32], pool: Option<&Pool>) -> f32 {
23817    gate.map_or(1.0, |gate| {
23818        let mut gl = [0.0f32; 1];
23819        gate.matvec(x, &mut gl, pool);
23820        1.0 / (1.0 + (-gl[0]).exp())
23821    })
23822}
23823
23824/// The shared expert's tubes (`mlp.shared_expert.*_proj.tube1.weight`, `hot-bake
23825/// --shared-q8`: the whole expert at eight bits) added to `out` with its weight.
23826pub(crate) fn shared_tubes_add(m: &MoeFfn, x: &[f32], out: &mut [f32], pool: Option<&Pool>) {
23827    if let Some((se, gate)) = &m.shared
23828        && !se.segs.is_empty()
23829    {
23830        let g = shared_expert_weight(gate.as_ref(), x, pool);
23831        expert_tubes_add(se, x, g, out, pool);
23832    }
23833}
23834
23835/// CMF_PREFILL_PROF: cumulative ns of the batched walk's attention and
23836/// FFN halves (all layers, all chunks).
23837static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
23838    std::sync::atomic::AtomicU64::new(0),
23839    std::sync::atomic::AtomicU64::new(0),
23840];
23841
23842/// The wgpu side of `CMF_PREFILL_PROF`: q|k|v GEMMs' enqueue and readback
23843/// wait, the chunk attend's readback wait (ms, cumulative) and the int8
23844/// GEMMs the `zi_mm` route took.
23845fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
23846    #[cfg(feature = "gpu")]
23847    {
23848        use std::sync::atomic::Ordering::Relaxed;
23849        let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
23850        (
23851            ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
23852            ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
23853            ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
23854            crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
23855        )
23856    }
23857    #[cfg(not(feature = "gpu"))]
23858    (0.0, 0.0, 0.0, 0)
23859}
23860
23861/// The wgpu prefill mirror attend's outcome counters
23862/// (`gpu_wgpu::MIRROR_EVENTS`).
23863fn wgpu_mirror_counters() -> [u64; 6] {
23864    #[cfg(feature = "gpu")]
23865    {
23866        crate::gpu_wgpu::MIRROR_EVENTS
23867            .each_ref()
23868            .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
23869    }
23870    #[cfg(not(feature = "gpu"))]
23871    [0; 6]
23872}
23873
23874fn prefill_prof_on() -> bool {
23875    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
23876    *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
23877}
23878
23879/// Accumulate the dense FFN activations which feed the down projection.
23880///
23881/// MTP calibration intentionally calls this independently of the trunk's
23882/// `FFN_PROBE` TLS recorder: an MTP sidecar can use a different FFN width,
23883/// and borrowing the trunk's layer-numbered accumulator would silently turn
23884/// draft statistics into trunk proxies.  The caller owns the execution scope
23885/// (the public MTP probe uses `gpu::cpu_scope`) and supplies one accumulator
23886/// per actual MTP FFN channel.
23887pub(crate) fn accumulate_dense_ffn_activation_mass(
23888    d: &DenseFfn,
23889    xs: &[f32],
23890    b: usize,
23891    pool: Option<&Pool>,
23892    mass: &mut [f64],
23893    squared_mass: &mut [f64],
23894) -> Result<(), String> {
23895    let inter = d.gate_proj.rows();
23896    let hidden = d.gate_proj.cols();
23897    if mass.len() != inter || squared_mass.len() != inter {
23898        return Err(format!(
23899            "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
23900            mass.len(),
23901            squared_mass.len()
23902        ));
23903    }
23904    if d.up_proj.rows() != inter
23905        || d.up_proj.cols() != hidden
23906        || d.down_proj.cols() != inter
23907        || d.down_proj.rows() != hidden
23908    {
23909        return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
23910    }
23911    if !d.segs.is_empty() {
23912        return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
23913    }
23914    if xs.len() != b.saturating_mul(hidden) {
23915        return Err(format!(
23916            "MTP FFN probe: {} input values for {b} × {hidden} rows",
23917            xs.len()
23918        ));
23919    }
23920    if b == 0 {
23921        return Ok(());
23922    }
23923    let mut gate = vec![0.0f32; b * inter];
23924    let mut up = vec![0.0f32; b * inter];
23925    d.gate_proj.matmat(xs, b, &mut gate, pool);
23926    d.up_proj.matmat(xs, b, &mut up, pool);
23927    for row in 0..b {
23928        for i in 0..inter {
23929            let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
23930            let a = a as f64;
23931            mass[i] += a.abs();
23932            squared_mass[i] += a * a;
23933        }
23934    }
23935    Ok(())
23936}
23937
23938fn dense_ffn_batch(
23939    d: &DenseFfn,
23940    xs: &[f32],
23941    b: usize,
23942    pool: Option<&Pool>,
23943    mask_row: Option<&[u8]>,
23944) -> Vec<f32> {
23945    let inter = d.gate_proj.rows();
23946    let hidden = d.down_proj.rows();
23947    // Fused on-device SwiGLU when the device is in play: three separate
23948    // `matmat` calls are three round trips per layer, and the gate/up
23949    // panels (b × inter — 22 MB each at a 512-token chunk) cross the bus
23950    // twice for nothing. The kernel already existed for the image DiT;
23951    // the LLM prefill was simply never wired to it. A task mask needs the
23952    // activations on the host between the halves, so it keeps the CPU
23953    // arm below.
23954    // SiLU/GELU have device arms everywhere they are admitted.  MiniMax
23955    // M3's frozen OAI contract has an additional WGPU-only arm; callers on
23956    // native Metal still get an honest decline from `q4_ffn_act` rather than
23957    // an accidental SiLU evaluation.
23958    let fused_act = d.act.wgpu_graph_act();
23959    if mask_row.is_none()
23960        && fused_act.is_some()
23961        && b >= 32
23962        && crate::gpu::enabled_here()
23963        && !crate::gpu::mm_killed()
23964        // The refit pass needs this layer's activations on the host; the
23965        // fused chain keeps them on the device. Refusing it here costs
23966        // one round trip and keeps every GEMM on the card — the
23967        // alternative was running the whole calibration on the CPU.
23968        && refit_dir().is_none()
23969        // Same for the mass/hit probes. The accumulator at the bottom of
23970        // this function only sees `g` when `g` came back to the host, so
23971        // a fused batch would leave it summing nothing — a probe that
23972        // reports zeros rather than failing, which is worse.
23973        && !ffn_probe_active()
23974        // GPTQ calibration folds every projection's input on the host
23975        // (`QTensor::matmat`); the fused chain would hand gate/up/down
23976        // their inputs on the card and leave the FFN uncalibrated.
23977        && !crate::gptq_capture::capturing()
23978    {
23979        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
23980            d.gate_proj.mapped_q4t(),
23981            d.up_proj.mapped_q4t(),
23982            d.down_proj.mapped_q4t(),
23983        ) {
23984            let mut out = vec![0.0f32; b * hidden];
23985            let act = fused_act.expect("checked above");
23986            if crate::gpu::q4_ffn_act(
23987                model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
23988            ) {
23989                return out;
23990            }
23991        }
23992        // The q4tp twin (same kernel family, scale from the row ladder) —
23993        // the DiT has run it in production since the pipeline containers;
23994        // the LLM prefill was simply never wired to it, so a q4tp model's
23995        // prefill panels stayed on the CPU.
23996        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
23997            d.gate_proj.mapped_q4tp(),
23998            d.up_proj.mapped_q4tp(),
23999            d.down_proj.mapped_q4tp(),
24000        ) {
24001            let mut out = vec![0.0f32; b * hidden];
24002            let act = fused_act.expect("checked above");
24003            if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
24004            {
24005                return out;
24006            }
24007        }
24008        // Every other device codec — int8, or gate/up and down in different
24009        // codecs: the three GEMMs with the panels kept on the card. Without
24010        // it a q8_2f prefill read both b·inter panels home, folded them on
24011        // one host thread, and sent the result back for down.
24012        // CMF_FFN_KEEP=0 keeps the per-GEMM path (A/B).
24013        if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
24014            std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
24015            d.gate_proj.mapped_device_gemm(),
24016            d.up_proj.mapped_device_gemm(),
24017            d.down_proj.mapped_device_gemm(),
24018        ) {
24019            let mut out = vec![0.0f32; b * hidden];
24020            let act = fused_act.expect("checked above");
24021            if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
24022                return out;
24023            }
24024        }
24025    }
24026    let mut g = vec![0.0f32; b * inter];
24027    d.gate_proj.matmat(xs, b, &mut g, pool);
24028    let mut u = vec![0.0f32; b * inter];
24029    d.up_proj.matmat(xs, b, &mut u, pool);
24030    if gate_topk() > 0 && d.act == Act::Silu {
24031        for t in 0..b {
24032            let row = &mut g[t * inter..(t + 1) * inter];
24033            for v in row.iter_mut() {
24034                *v = Act::Silu.combine(*v, 1.0);
24035            }
24036            keep_top_k(row, gate_topk());
24037        }
24038        for i in 0..b * inter {
24039            g[i] *= u[i];
24040        }
24041    } else {
24042        combine_pooled(d.act, &mut g[..b * inter], &u[..b * inter], pool);
24043    }
24044    if let Some(row) = mask_row {
24045        zero_masked_cols(&mut g, b, inter, row);
24046    }
24047    if oracle_topk() > 0 {
24048        for t in 0..b {
24049            keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
24050        }
24051    }
24052    let mut out = vec![0.0f32; b * hidden];
24053    d.down_proj.matmat(&g, b, &mut out, pool);
24054    if refit_dir().is_some() {
24055        let li = crate::gpu::cur_layer();
24056        if li >= 0 {
24057            refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
24058        }
24059    }
24060    // The DTG-MA probe, on the batched path: one prefill sweep gives the
24061    // same per-neuron statistic the per-position probe does, and on a 27B
24062    // that is minutes instead of hours.
24063    FFN_PROBE.with(|pr| {
24064        if let Some(acc) = pr.borrow_mut().as_mut() {
24065            let li = crate::gpu::cur_layer();
24066            if li < 0 {
24067                return;
24068            }
24069            let Some(row) = acc.get_mut(li as usize) else {
24070                return;
24071            };
24072            let (sq, topk) = (probe_sq(), probe_topk().min(inter));
24073            let mut mag: Vec<f32> = Vec::new();
24074            for t in 0..b {
24075                let gr = &g[t * inter..(t + 1) * inter];
24076                if topk > 0 {
24077                    // The hit count of the per-position probe: a position's
24078                    // `topk` loudest neurons each get one.
24079                    mag.clear();
24080                    mag.extend(gr.iter().map(|v| v.abs()));
24081                    let (_, kth, _) = mag.select_nth_unstable_by(topk - 1, |a, b| {
24082                        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
24083                    });
24084                    let thr = *kth;
24085                    for (a, &v) in row.iter_mut().zip(gr) {
24086                        if v.abs() >= thr {
24087                            *a += 1.0;
24088                        }
24089                    }
24090                    continue;
24091                }
24092                for (a, &v) in row.iter_mut().zip(gr) {
24093                    *a += if sq {
24094                        (v as f64) * (v as f64)
24095                    } else {
24096                        (v as f64).abs()
24097                    };
24098                }
24099            }
24100        }
24101    });
24102    out
24103}
24104
24105/// Batched MoE-FFN: router batched, positions are GROUPED by expert —
24106/// an expert's weights are read once for all its positions in the chunk
24107/// (the main prefill-GEMM win on MoE: 960MB/token of 35B experts).
24108/// Accumulate per-channel activation energy for `CMF_RMS_TRACE`.
24109fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
24110    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
24111    static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
24112    let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
24113    let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
24114    if (!on && !dump) || b == 0 {
24115        return;
24116    }
24117    let hidden = xs.len() / b;
24118    if on {
24119        let mut acc = m.act_sq.borrow_mut();
24120        if acc.len() < hidden {
24121            acc.resize(hidden, 0.0);
24122        }
24123        for t in 0..b {
24124            let row = &xs[t * hidden..(t + 1) * hidden];
24125            for (a, &v) in acc.iter_mut().zip(row) {
24126                *a += (v as f64) * (v as f64);
24127            }
24128        }
24129    }
24130    if dump {
24131        // Cap the capture: the covariance needs a few thousand rows, and a
24132        // whole prefill of every layer would be gigabytes for no extra rank.
24133        let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
24134            .ok()
24135            .and_then(|v| v.parse().ok())
24136            .unwrap_or(4096);
24137        let mut rows = m.act_rows.borrow_mut();
24138        if rows.len() < cap * hidden {
24139            let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
24140            rows.extend_from_slice(&xs[..take * hidden]);
24141        }
24142    }
24143}
24144
24145/// Send-able cursor over a Vec-of-Vecs: each pool worker writes only its
24146/// own slots (disjoint by construction in the caller).
24147#[derive(Clone, Copy)]
24148struct SendVecs(*mut Vec<f32>);
24149unsafe impl Send for SendVecs {}
24150unsafe impl Sync for SendVecs {}
24151impl SendVecs {
24152    #[inline]
24153    fn at(self, i: usize) -> *mut Vec<f32> {
24154        unsafe { self.0.add(i) }
24155    }
24156}
24157
24158fn moe_ffn_batch(
24159    m: &MoeFfn,
24160    xs: &[f32],
24161    b: usize,
24162    hidden: usize,
24163    pool: Option<&Pool>,
24164    allowed: Option<&[bool]>,
24165) -> Vec<f32> {
24166    accumulate_act(m, xs, b);
24167    let ne = m.experts.len();
24168    let mut logits = vec![0.0f32; b * ne];
24169    match &m.resonance {
24170        Some(r) => {
24171            let hdim = xs.len() / b.max(1);
24172            for bi in 0..b {
24173                r.scores(
24174                    &xs[bi * hdim..(bi + 1) * hdim],
24175                    &mut logits[bi * ne..(bi + 1) * ne],
24176                );
24177            }
24178        }
24179        None => m.router.matmat(xs, b, &mut logits, pool),
24180    }
24181
24182    // Assignments: expert → [(position, weight)] — same routing as
24183    // moe_ffn, per position (see `moe_route`).
24184    let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
24185    {
24186        let mut st = m.stats.borrow_mut();
24187        if st.len() < ne {
24188            st.resize(ne, 0);
24189        }
24190        for bi in 0..b {
24191            let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
24192            for &e in &idx {
24193                st[e] += 1;
24194                assign[e].push((bi, p[e] / wsum));
24195            }
24196        }
24197    }
24198
24199    let mut out = vec![0.0f32; b * hidden];
24200    let cols = m.experts[0].gate_proj.cols();
24201    let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
24202        let sb = list.len();
24203        let mut sub = vec![0.0f32; sb * cols];
24204        for (k, &(bi, _)) in list.iter().enumerate() {
24205            sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
24206        }
24207        let eo = dense_ffn_batch(d, &sub, sb, pool, None);
24208        for (k, &(bi, w)) in list.iter().enumerate() {
24209            for i in 0..hidden {
24210                out[bi * hidden + i] += w * eo[k * hidden + i];
24211            }
24212        }
24213    };
24214    // Routed experts: the panels are TINY (b·top_k spread over every
24215    // expert — a few positions each), so a pool dispatch per expert is
24216    // pure barrier cost. Invert the parallelism: workers take WHOLE
24217    // experts (serial math inside), then one deterministic scatter in
24218    // expert order — the exact accumulation order the serial loop had.
24219    let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
24220    if pool.is_some() && active.len() >= 8 {
24221        let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
24222        {
24223            let panel_ptr = SendVecs(panels.as_mut_ptr());
24224            // Capture only the expert table: `m` itself carries RefCell
24225            // stats and must not cross the pool boundary.
24226            let experts = &m.experts;
24227            let (active_r, assign_r) = (&active, &assign);
24228            let inherit_cpu = crate::gpu::inherit_cpu_scope();
24229            let run = |start: usize, end: usize| {
24230                let _cpu_scope = inherit_cpu();
24231                // Native Metal: the per-op device helpers (`q4tp_ffn`,
24232                // `q8_matmat`, …) stage through size-keyed shared io
24233                // buffers and are not re-entrant — two workers whose experts
24234                // drew the same row count wrote one buffer at once, so a
24235                // Mellum2.1 prompt came out different on every run. The
24236                // workers keep whole experts on the CPU there; the device
24237                // prefill is the MoE chunk graph.
24238                #[cfg(target_os = "macos")]
24239                let _metal_cpu = crate::gpu::enter_cpu_scope();
24240                for ai in start..end {
24241                    let e = active_r[ai];
24242                    let list = &assign_r[e];
24243                    let sb = list.len();
24244                    let mut sub = vec![0.0f32; sb * cols];
24245                    for (k, &(bi, _)) in list.iter().enumerate() {
24246                        sub[k * cols..(k + 1) * cols]
24247                            .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
24248                    }
24249                    // SAFETY: each worker owns a disjoint panels[ai].
24250                    unsafe {
24251                        *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
24252                    }
24253                }
24254            };
24255            match pool {
24256                Some(p) => p.run_rows(active.len(), &run),
24257                None => run(0, active.len()),
24258            }
24259        }
24260        for (ai, &e) in active.iter().enumerate() {
24261            for (k, &(bi, w)) in assign[e].iter().enumerate() {
24262                let eo = &panels[ai][k * hidden..(k + 1) * hidden];
24263                for i in 0..hidden {
24264                    out[bi * hidden + i] += w * eo[i];
24265                }
24266            }
24267        }
24268    } else {
24269        for &e in &active {
24270            run_expert(&m.experts[e], &assign[e], &mut out);
24271        }
24272    }
24273    if let Some((se, gate)) = &m.shared {
24274        let all: Vec<(usize, f32)> = if let Some(gate) = gate {
24275            let mut gl = vec![0.0f32; b];
24276            gate.matmat(xs, b, &mut gl, pool);
24277            (0..b)
24278                .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
24279                .collect()
24280        } else {
24281            (0..b).map(|bi| (bi, 1.0)).collect()
24282        };
24283        run_expert(se, &all, &mut out);
24284    }
24285    out
24286}
24287
24288/// Decode-exact multi-token MoE — the MiMo speculative verify's FFN. Row
24289/// `r` of the result is bit-identical to `moe_ffn(m, x_r)` on the CPU
24290/// (`moe_ffn_cpu` → `moe_ffn_cpu_batched`): router matvec per row, the same
24291/// routing, the same int8 gate/up/SiLU and down terms
24292/// (`QTensor::moe_gate_up_rows` / `moe_down_rows`), and the row's experts
24293/// summed in ITS route order from 0. What the rows share is the weight
24294/// traffic: each routed expert is read once for every row that picked it.
24295/// (`moe_ffn_batch`, the prompt path, groups the same way but sums in
24296/// expert-index order and runs blocked kernels on wide groups — close, not
24297/// bit-equal to decode.) Any layer the kernels do not cover, or a device
24298/// that could answer `moe_ffn` itself, walks `moe_ffn` row by row.
24299fn moe_ffn_rows_exact(
24300    m: &MoeFfn,
24301    xs: &[f32],
24302    b: usize,
24303    hidden: usize,
24304    pool: Option<&Pool>,
24305) -> Vec<f32> {
24306    let mut out = vec![0.0f32; b * hidden];
24307    let per_row = |out: &mut [f32]| {
24308        for r in 0..b {
24309            let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
24310            out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
24311        }
24312    };
24313    let covered = !crate::gpu::enabled_here()
24314        && moe_batch_enabled()
24315        && m.shared.is_none()
24316        && m.resonance.is_none()
24317        && FFN_PROBE.with(|pr| pr.borrow().is_none())
24318        && m.experts.iter().all(|d| d.act == Act::Silu);
24319    if !covered {
24320        per_row(&mut out);
24321        return out;
24322    }
24323    let ne = m.experts.len();
24324    // Routing, row by row, exactly as `moe_ffn`.
24325    let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
24326    for r in 0..b {
24327        let x = &xs[r * hidden..(r + 1) * hidden];
24328        accumulate_act(m, x, 1);
24329        let mut logits = vec![0.0f32; ne];
24330        m.router.matvec(x, &mut logits, pool);
24331        let (idx, p, wsum) = moe_route(&logits, m, None);
24332        {
24333            let mut st = m.stats.borrow_mut();
24334            if st.len() < ne {
24335                st.resize(ne, 0);
24336            }
24337            for &e in &idx {
24338                st[e] += 1;
24339            }
24340        }
24341        let w: Vec<f32> = idx
24342            .iter()
24343            .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
24344            .collect();
24345        routes.push((idx, w));
24346    }
24347    if routes.iter().any(|(idx, _)| idx.is_empty()) {
24348        per_row(&mut out);
24349        return out;
24350    }
24351    // Group the (row, expert) picks by expert, in first-seen order.
24352    let mut experts: Vec<usize> = Vec::new();
24353    let mut groups: Vec<Vec<usize>> = Vec::new();
24354    for (r, (idx, _)) in routes.iter().enumerate() {
24355        for &e in idx {
24356            match experts.iter().position(|&x| x == e) {
24357                Some(g) => groups[g].push(r),
24358                None => {
24359                    experts.push(e);
24360                    groups.push(vec![r]);
24361                }
24362            }
24363        }
24364    }
24365    let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
24366    let inter = m.experts[experts[0]].gate_proj.rows();
24367    let pairs: Vec<(&QTensor, &QTensor)> = experts
24368        .iter()
24369        .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
24370        .collect();
24371    let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
24372    if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
24373        per_row(&mut out);
24374        return out;
24375    }
24376    let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
24377    let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
24378    let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
24379    if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
24380        per_row(&mut out);
24381        return out;
24382    }
24383    // Where each (row, expert) term landed in the flat pair list.
24384    let mut slot = std::collections::HashMap::with_capacity(n_pairs);
24385    let mut p = 0usize;
24386    for (g, &e) in experts.iter().enumerate() {
24387        for &r in &groups[g] {
24388            slot.insert((r, e), p);
24389            p += 1;
24390        }
24391    }
24392    for (r, (idx, w)) in routes.iter().enumerate() {
24393        let terms: Vec<(&[f32], f32)> = idx
24394            .iter()
24395            .zip(w)
24396            .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
24397            .collect();
24398        let row = &mut out[r * hidden..(r + 1) * hidden];
24399        for (i, dst) in row.iter_mut().enumerate() {
24400            // `moe_down_many`'s per-row sum: from 0, in route order.
24401            let mut acc = 0f32;
24402            for (d, we) in &terms {
24403                acc += we * d[i];
24404            }
24405            *dst = acc;
24406        }
24407    }
24408    out
24409}
24410
24411/// Gemma's final-logit soft-capping, `c·tanh(x/c)`, over a whole vocab row —
24412/// element for element the serial expression, split across the pool: at a
24413/// 262k vocab the serial `tanh` pass was ~5 ms of a 36 ms decode token.
24414/// Ripple's value residual: the rows V projects, `torch.lerp(e0, normed, blend)` term for term
24415/// (the two forms torch picks between at a weight of one half).
24416pub(crate) fn vres_lerp(e0: &[f32], normed: &[f32], blend: f32, out: &mut Vec<f32>) {
24417    debug_assert_eq!(e0.len(), normed.len());
24418    out.clear();
24419    if blend < 0.5 {
24420        out.extend(e0.iter().zip(normed).map(|(&e, &n)| e + blend * (n - e)));
24421    } else {
24422        out.extend(e0.iter().zip(normed).map(|(&e, &n)| n - (n - e) * (1.0 - blend)));
24423    }
24424}
24425
24426pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
24427    match pool {
24428        Some(p) if v.len() >= 1 << 15 => {
24429            let base = v.as_mut_ptr() as usize;
24430            p.run_rows(v.len(), &|s, e| {
24431                // SAFETY: run_rows hands out disjoint [s, e) ranges of `v`,
24432                // which outlives the (blocking) dispatch.
24433                let sl =
24434                    unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
24435                for x in sl {
24436                    *x = c * (*x / c).tanh();
24437                }
24438            });
24439        }
24440        _ => {
24441            for x in v.iter_mut() {
24442                *x = c * (*x / c).tanh();
24443            }
24444        }
24445    }
24446}
24447
24448thread_local! {
24449    /// gate/up activation scratch for the dense FFN paths (single uses
24450    /// two slots, the fused pair all four) — these were fresh
24451    /// intermediate-size Vecs on every layer of every token.
24452    static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
24453        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
24454}
24455
24456/// Dense SwiGLU FFN through QTensor matvecs (any storage).
24457fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
24458    // Per-token sparsity, when the file was built for it: gate first,
24459    // then only the chosen neurons' up/down rows leave the mmap.
24460    if gate_topk() > 0
24461        && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
24462    {
24463        return out;
24464    }
24465    // Whole-FFN GPU submit (этап 4.2 increment): gate → silu·up → down
24466    // chained in ONE command buffer with the intermediate activations
24467    // resident on the device — 3 per-op polls become 1 per layer. The
24468    // moe_block backend already implements exactly this chain; a dense
24469    // FFN is one expert with weight 1. Runtime probe: the chain still
24470    // pays one submit+poll per layer — alternate it against the pure-CPU
24471    // FFN and keep whichever is faster on this machine.
24472    // q1 FFNs offload at any practical size: the q1 CPU kernel is
24473    // compute-bound, so the UMA threshold logic does not apply — the
24474    // probe measures and decides either way.
24475    // The fused GPU block has no descriptor-aware Prism path: it would either
24476    // consume an unrotated activation or decline after inspecting the mixed
24477    // q2tp/q4tp tensors.  Do not let that structural refusal enter the FFN
24478    // probe's CPU_ONLY scope; the ordinary body below dispatches each matrix
24479    // through QTensor::matvec, which owns the signed FWHT + affine q2tp route.
24480    let prism_body = d.gate_proj.has_prism_contract()
24481        || d.up_proj.has_prism_contract()
24482        || d.down_proj.has_prism_contract();
24483    if !prism_body
24484        && crate::gpu::enabled_here()
24485        && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
24486    {
24487        let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
24488            crate::gpu::ProbeArm::Gpu
24489        } else {
24490            crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
24491        };
24492        match arm {
24493            crate::gpu::ProbeArm::Gpu => {
24494                let t0 = std::time::Instant::now();
24495                if let Some(out) = dense_ffn_gpu(d, x, pool) {
24496                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
24497                    return out;
24498                }
24499                // Declined: no timing exists, so say so. Silence here is
24500                // what left `ffn` undecided for 9000 calls and cost a
24501                // failed device attempt on half of them.
24502                crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
24503            }
24504            crate::gpu::ProbeArm::CpuTimed => {
24505                let t0 = std::time::Instant::now();
24506                let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
24507                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
24508                return out;
24509            }
24510            crate::gpu::ProbeArm::Cpu => {
24511                return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
24512            }
24513        }
24514    }
24515    dense_ffn_cpu(d, x, pool)
24516}
24517
24518/// `g[i] = act.combine(g[i], u[i])`, spread over the pool from 8192
24519/// elements: elementwise, so the same bits as the serial loop. An exact
24520/// GELU is one `erf` an element — 1.3 M a layer for a 128-row chunk of
24521/// Spark-X2.6, 10 240 a decode step — and the main thread paid them alone
24522/// while the workers waited for the down projection.
24523fn combine_pooled(act: Act, g: &mut [f32], u: &[f32], pool: Option<&Pool>) {
24524    debug_assert_eq!(g.len(), u.len());
24525    match pool {
24526        Some(p) if g.len() >= 8192 => {
24527            let gp = crate::pool::SendMut::new(g.as_mut_ptr());
24528            p.run_rows(g.len(), &|start, end| {
24529                for i in start..end {
24530                    // SAFETY: each worker owns elements [start, end).
24531                    unsafe {
24532                        let v = gp.at(i);
24533                        *v = act.combine(*v, u[i]);
24534                    }
24535                }
24536            });
24537        }
24538        _ => {
24539            for (gv, &uv) in g.iter_mut().zip(u) {
24540                *gv = act.combine(*gv, uv);
24541            }
24542        }
24543    }
24544}
24545
24546/// The pure-CPU dense-FFN body (also the fallback of every GPU refusal).
24547fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
24548    let inter = d.gate_proj.rows();
24549    FFN_SCRATCH.with(|s| {
24550        let mut s = s.borrow_mut();
24551        let [g, u, ..] = &mut *s;
24552        g.resize(inter, 0.0);
24553        // Fused gate+up+silu: one dispatch, no separate silu pass.
24554        // Falls back to matvec_many + silu loop for unsupported dtypes.
24555        if gate_topk() > 0 {
24556            // Gate first, select, and only then pay for `up`: the
24557            // measurement arm computes both and zeroes the losers, which
24558            // is the same arithmetic.
24559            u.resize(inter, 0.0);
24560            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
24561            for i in 0..inter {
24562                g[i] = Act::Silu.combine(g[i], 1.0);
24563            }
24564            keep_top_k(g, gate_topk());
24565            for i in 0..inter {
24566                g[i] *= u[i];
24567            }
24568        } else if d.act == Act::Silu && {
24569            let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
24570            QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
24571        } {
24572            // g now holds silu(gate)·up directly.
24573        } else if d.act.clamp_limit() > 0.0 && {
24574            let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
24575            QTensor::matvec_silu_mul_limited(&d.gate_proj, &d.up_proj, x, g, d.act.clamp_limit(), pool)
24576        } {
24577            // g holds silu(min(gate, limit))·clamp(up).
24578        } else {
24579            u.resize(inter, 0.0);
24580            // Multi-matrix job: gate+up under one pool dispatch.
24581            let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
24582            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
24583            let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
24584            combine_pooled(d.act, &mut g[..inter], &u[..inter], pool);
24585        }
24586        // DTG-MA bake probe (Patent 2): accumulate this layer's
24587        // per-neuron activation mass while a probe pass is active.
24588        // `CMF_FFN_PROBE_TOPK=k` switches the statistic from mass to a
24589        // HIT COUNT — how many tokens rank the neuron in their own top
24590        // k. Mass asks "how loud is this neuron overall", the count
24591        // asks "how often does this task actually need it", and the two
24592        // rank neurons differently whenever a few tokens are loud.
24593        FFN_PROBE.with(|pr| {
24594            if let Some(acc) = pr.borrow_mut().as_mut() {
24595                let li = crate::gpu::cur_layer();
24596                if li >= 0 {
24597                    if let Some(row) = acc.get_mut(li as usize) {
24598                        match probe_topk() {
24599                            0 if probe_sq() => {
24600                                for (a, &v) in row.iter_mut().zip(g.iter()) {
24601                                    *a += (v as f64) * (v as f64);
24602                                }
24603                            }
24604                            0 if probe_signed() => {
24605                                for (a, &v) in row.iter_mut().zip(g.iter()) {
24606                                    *a += v as f64;
24607                                }
24608                            }
24609                            0 => {
24610                                for (a, &v) in row.iter_mut().zip(g.iter()) {
24611                                    *a += (v as f64).abs();
24612                                }
24613                            }
24614                            k => {
24615                                let n = g.len();
24616                                let k = k.min(n);
24617                                let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
24618                                let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
24619                                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
24620                                });
24621                                let thr = *kth;
24622                                for (a, &v) in row.iter_mut().zip(g.iter()) {
24623                                    if v.abs() >= thr {
24624                                        *a += 1.0;
24625                                    }
24626                                }
24627                            }
24628                        }
24629                    }
24630                }
24631            }
24632        });
24633        MOE_PROBE.with(|pr| {
24634            if let Some(pr) = pr.borrow_mut().as_mut()
24635                && let Some((e, w)) = pr.cur
24636            {
24637                let li = crate::gpu::cur_layer();
24638                if li >= 0
24639                    && let Some(row) = pr.acc.get_mut(li as usize)
24640                    && let Some(cell) = row.get_mut(e * pr.inter..(e + 1) * pr.inter)
24641                {
24642                    for (a, &v) in cell.iter_mut().zip(g.iter()) {
24643                        let s = (w * v) as f64;
24644                        *a += s * s;
24645                    }
24646                }
24647            }
24648        });
24649        if oracle_topk() > 0 {
24650            keep_top_k(g, oracle_topk());
24651        }
24652        {
24653            let li = crate::gpu::cur_layer();
24654            if li >= 0 {
24655                adump_row(li as usize, g);
24656            }
24657        }
24658        let mut out = attention::take_buf(d.down_proj.rows());
24659        let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
24660        d.down_proj.matvec(g, &mut out, pool);
24661        out
24662    })
24663}
24664
24665/// Online accumulators for the AWNP refit of a narrowed FFN.
24666///
24667/// The refit needs `Gss = A_SᵀA_S` and `YA = YᵀA_S` per layer, where `A_S`
24668/// are the calibration activations of the KEPT neurons and `Y` the full
24669/// FFN output. Both are small enough to hold; the thing that is not is
24670/// the activations they are built from — a 27B layer would dump a
24671/// gigabyte per thousand tokens. So they are accumulated as the
24672/// calibration runs and written once at the end.
24673///
24674/// `CMF_FFN_REFIT=<dir>` holds `support.<L>.u32` (a u32 count then the
24675/// kept indices) for every layer to accumulate; `CMF_FFN_REFIT_FROM/TO`
24676/// bound the layer span so the accumulators fit in RAM.
24677pub struct RefitAcc {
24678    pub support: Vec<u32>,
24679    pub gss: Vec<f32>,
24680    pub ya: Vec<f32>,
24681    pub hidden: usize,
24682    pub tokens: u64,
24683    /// Activations staged transposed ([ns, t] and [hidden, t]) until the
24684    /// batch is worth a GEMM. The product costs `ns²` to move and add
24685    /// REGARDLESS of how many tokens went into it, so folding 16 chunks
24686    /// into one call cuts that cost 16× — it was 15 TB of traffic per
24687    /// calibration pass at one call per 256 tokens.
24688    pub buf_g: Vec<f32>,
24689    pub buf_o: Vec<f32>,
24690    pub buf_t: usize,
24691}
24692
24693/// The product buffer is SHARED across layers — one 473 MB allocation,
24694/// not one per layer (that was 30 GB of nothing on a 64-layer model).
24695/// It lives under the same lock as the accumulators.
24696type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
24697
24698static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
24699    std::sync::OnceLock::new();
24700
24701/// Is an FFN probe accumulator installed on this thread? The fused GPU
24702/// FFN must decline while one is, or the probe silently measures zero.
24703fn ffn_probe_active() -> bool {
24704    FFN_PROBE.with(|p| p.borrow().is_some())
24705}
24706
24707fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
24708    REFIT
24709        .get_or_init(|| {
24710            std::env::var("CMF_FFN_REFIT").ok().map(|d| {
24711                (
24712                    d,
24713                    std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
24714                )
24715            })
24716        })
24717        .as_ref()
24718}
24719
24720/// Accumulate one prefill panel into the layer's refit statistics.
24721fn refit_accumulate(
24722    li: usize,
24723    g: &[f32],
24724    b: usize,
24725    inter: usize,
24726    out: &[f32],
24727    hidden: usize,
24728    pool: Option<&Pool>,
24729) {
24730    let Some((dir, map)) = refit_dir() else {
24731        return;
24732    };
24733    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
24734    let (from, to) = *SPAN.get_or_init(|| {
24735        let g = |k: &str, d: usize| {
24736            std::env::var(k)
24737                .ok()
24738                .and_then(|v| v.parse().ok())
24739                .unwrap_or(d)
24740        };
24741        (
24742            g("CMF_FFN_REFIT_FROM", 0),
24743            g("CMF_FFN_REFIT_TO", usize::MAX),
24744        )
24745    });
24746    if li < from || li > to {
24747        return;
24748    }
24749    let mut guard = map.lock().unwrap();
24750    let (map, shared) = &mut *guard;
24751    let acc = match map.entry(li) {
24752        std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
24753        std::collections::hash_map::Entry::Vacant(e) => {
24754            let path = format!("{dir}/support.{li}.u32");
24755            let Ok(bytes) = std::fs::read(&path) else {
24756                eprintln!("refit: no {path} — layer {li} skipped");
24757                return;
24758            };
24759            let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
24760            let support: Vec<u32> = bytes[4..4 + n * 4]
24761                .chunks_exact(4)
24762                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
24763                .collect();
24764            eprintln!(
24765                "refit: layer {li} support {n} ({:.0} MB of accumulator)",
24766                (n * n + hidden * n) as f64 * 4.0 / 1e6
24767            );
24768            e.insert(RefitAcc {
24769                gss: vec![0.0; n * n],
24770                ya: vec![0.0; hidden * n],
24771                buf_g: Vec::new(),
24772                buf_o: Vec::new(),
24773                buf_t: 0,
24774                support,
24775                hidden,
24776                tokens: 0,
24777            })
24778        }
24779    };
24780    let ns = acc.support.len();
24781    // Stage this chunk transposed; the GEMM fires once the batch is full.
24782    let cap = refit_batch();
24783    if acc.buf_g.is_empty() {
24784        acc.buf_g = vec![0.0; ns * cap];
24785        acc.buf_o = vec![0.0; hidden * cap];
24786    }
24787    let take = b.min(cap - acc.buf_t);
24788    for t in 0..take {
24789        let col = acc.buf_t + t;
24790        for (j, &n) in acc.support.iter().enumerate() {
24791            acc.buf_g[j * cap + col] = g[t * inter + n as usize];
24792        }
24793        for h in 0..hidden {
24794            acc.buf_o[h * cap + col] = out[t * hidden + h];
24795        }
24796    }
24797    acc.buf_t += take;
24798    acc.tokens += take as u64;
24799    if acc.buf_t < cap {
24800        return;
24801    }
24802    let bt = acc.buf_t;
24803    acc.buf_t = 0;
24804    // The GEMM WRITES its C (it zeroes the accumulators it uses), so the
24805    // chunk product lands in scratch and is added on — the one thing that
24806    // silently turns a Gram over 13 000 tokens into a Gram over 256.
24807    // Both products are `C[n, m] += X[n, b] · Yᵀ[b, m]` with X and Y
24808    // stored row-major [·, b] — exactly `gemm_nt_f32`'s shape, so the
24809    // card does them when it is up (this is the whole calibration's
24810    // cost: O(|S|²) per token, 2.9 PFLOP for a 27B pass). The tiled CPU
24811    // loop stays as the fallback. Neither accumulates, so the product
24812    // lands in scratch and is added on.
24813    let RefitAcc {
24814        gss,
24815        ya,
24816        buf_g,
24817        buf_o,
24818        ..
24819    } = acc;
24820    let need = (ns * ns).max(hidden * ns);
24821    if shared.len() < need {
24822        shared.resize(need, 0.0);
24823    }
24824    let scratch = &mut shared[..];
24825    let _ = bt;
24826    if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
24827        add_into(gss, &scratch[..ns * ns], pool);
24828        if crate::gpu::gemm_nt_f32_transient(
24829            buf_o,
24830            buf_g,
24831            &mut scratch[..hidden * ns],
24832            hidden,
24833            cap,
24834            ns,
24835        ) {
24836            add_into(ya, &scratch[..hidden * ns], pool);
24837        } else {
24838            accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
24839        }
24840    } else {
24841        accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
24842        accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
24843    }
24844    // No zeroing: the batch is always filled exactly (cap is a multiple
24845    // of the prefill chunk), and a memset of 178 MB a layer would cost
24846    // more than the GEMM.
24847}
24848
24849/// `CMF_FFN_REFIT_BATCH` — tokens staged before each GEMM (default 4096).
24850fn refit_batch() -> usize {
24851    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
24852    *B.get_or_init(|| {
24853        std::env::var("CMF_FFN_REFIT_BATCH")
24854            .ok()
24855            .and_then(|v| v.parse().ok())
24856            .unwrap_or(4096)
24857    })
24858}
24859
24860/// `c[m, n] += Σ_t left[m, t]·right[n, t]` — both operands transposed,
24861/// the CPU fallback for the staged batch.
24862fn accum_outer_t(
24863    c: &mut [f32],
24864    m: usize,
24865    n: usize,
24866    b: usize,
24867    left: &[f32],
24868    right: &[f32],
24869    pool: Option<&Pool>,
24870) {
24871    let ptr = SendMut(c.as_mut_ptr());
24872    let body = |i: usize| {
24873        let ptr = &ptr;
24874        let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
24875        for t in 0..b {
24876            let a = left[i * b + t];
24877            if a == 0.0 {
24878                continue;
24879            }
24880            for (j, o) in row.iter_mut().enumerate() {
24881                *o += a * right[j * b + t];
24882            }
24883        }
24884    };
24885    match pool {
24886        Some(p) if m > 1 => p.run_rows(m, &|s, e| {
24887            for i in s..e {
24888                body(i);
24889            }
24890        }),
24891        _ => {
24892            for i in 0..m {
24893                body(i);
24894            }
24895        }
24896    }
24897}
24898
24899/// `dst += src`, spread over the pool — at 118 M floats a layer this is
24900/// not a loop to leave on one core.
24901fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
24902    let n = dst.len().min(src.len());
24903    match pool {
24904        Some(p) if n >= 1 << 16 => {
24905            let ptr = SendMut(dst.as_mut_ptr());
24906            let f = |s: usize, e: usize| {
24907                let ptr = &ptr;
24908                for blk in s..e {
24909                    let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
24910                    for i in a..b {
24911                        unsafe { *ptr.0.add(i) += src[i] };
24912                    }
24913                }
24914            };
24915            p.run_rows(n.div_ceil(4096), &f);
24916        }
24917        _ => {
24918            for (d, v) in dst.iter_mut().zip(&src[..n]) {
24919                *d += *v;
24920            }
24921        }
24922    }
24923}
24924
24925/// `c[m, n] += Σ_t left[t, m]·right[t, n]`, with `left` stored [m, t] and
24926/// `right` [t, n]. Tiled over the rows of `c` so a tile stays in cache
24927/// while each token's `right` row streams past it once, and parallel
24928/// over tiles.
24929fn accum_outer(
24930    c: &mut [f32],
24931    m: usize,
24932    n: usize,
24933    b: usize,
24934    left: &[f32],
24935    right: &[f32],
24936    pool: Option<&Pool>,
24937) {
24938    const TILE: usize = 32;
24939    let tiles = m.div_ceil(TILE);
24940    let cp = SendMut(c.as_mut_ptr());
24941    let body = |ti: usize| {
24942        let cp = &cp;
24943        let i0 = ti * TILE;
24944        let i1 = (i0 + TILE).min(m);
24945        for t in 0..b {
24946            let r = &right[t * n..t * n + n];
24947            for i in i0..i1 {
24948                let a = left[i * b + t];
24949                if a == 0.0 {
24950                    continue;
24951                }
24952                // SAFETY: tiles partition c's rows; workers never overlap.
24953                let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
24954                for (o, v) in row.iter_mut().zip(r) {
24955                    *o += a * *v;
24956                }
24957            }
24958        }
24959    };
24960    match pool {
24961        Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
24962            for ti in s..e {
24963                body(ti);
24964            }
24965        }),
24966        _ => {
24967            for ti in 0..tiles {
24968                body(ti);
24969            }
24970        }
24971    }
24972}
24973
24974/// Write what the calibration accumulated: `gss.<L>.f32` and `ya.<L>.f32`.
24975pub fn refit_flush() -> usize {
24976    let Some((dir, map)) = refit_dir() else {
24977        return 0;
24978    };
24979    let guard = map.lock().unwrap();
24980    let mut n = 0;
24981    for (li, acc) in guard.0.iter() {
24982        // A silently truncated write here is a Gram that reshapes to
24983        // nothing an hour later — say it out loud instead.
24984        let w = |name: &str, v: &[f32]| {
24985            let path = format!("{dir}/{name}.{li}.f32");
24986            let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
24987            match std::fs::write(&path, &bytes) {
24988                Ok(()) => {}
24989                Err(e) => eprintln!(
24990                    "refit: FAILED to write {path} ({} MB): {e}",
24991                    bytes.len() / 1_000_000
24992                ),
24993            }
24994        };
24995        w("gss", &acc.gss);
24996        w("ya", &acc.ya);
24997        println!(
24998            "refit L{li}: {} support, {} tokens, hidden {}",
24999            acc.support.len(),
25000            acc.tokens,
25001            acc.hidden
25002        );
25003        n += 1;
25004    }
25005    n
25006}
25007
25008/// `CMF_FFN_ADUMP=<prefix>` — append every probed token's FFN activation
25009/// row to `<prefix>.<layer>.f16`. The co-activation record: which
25010/// neurons fire together, which is what a tube has to group if a token
25011/// is ever going to open one tube instead of sixteen.
25012fn adump_row(li: usize, g: &[f32]) {
25013    use std::io::Write as _;
25014    static FILES: std::sync::OnceLock<
25015        Option<(
25016            String,
25017            std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
25018        )>,
25019    > = std::sync::OnceLock::new();
25020    let Some((prefix, map)) = FILES
25021        .get_or_init(|| {
25022            std::env::var("CMF_FFN_ADUMP")
25023                .ok()
25024                .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
25025        })
25026        .as_ref()
25027    else {
25028        return;
25029    };
25030    // `CMF_FFN_ADUMP_FROM/_TO` narrow the dump to a layer span, so a big
25031    // calibration run fits on disk in a few passes instead of one.
25032    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
25033    let (from, to) = *SPAN.get_or_init(|| {
25034        let g = |k: &str, d: usize| {
25035            std::env::var(k)
25036                .ok()
25037                .and_then(|v| v.parse().ok())
25038                .unwrap_or(d)
25039        };
25040        (
25041            g("CMF_FFN_ADUMP_FROM", 0),
25042            g("CMF_FFN_ADUMP_TO", usize::MAX),
25043        )
25044    });
25045    if li < from || li > to {
25046        return;
25047    }
25048    let mut map = map.lock().unwrap();
25049    let f = map.entry(li).or_insert_with(|| {
25050        std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
25051    });
25052    let mut bytes = Vec::with_capacity(g.len() * 2);
25053    for v in g {
25054        bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
25055    }
25056    let _ = f.write_all(&bytes);
25057}
25058
25059/// `CMF_FFN_ORACLE_TOPK` — keep only the k largest |silu(g)·u| of each
25060/// token and zero the rest. Not a serving mode: it is the CEILING of
25061/// contextual sparsity — what a per-token router would be chasing —
25062/// measured by cheating, since the selection reads the very activations
25063/// it would have to predict.
25064fn oracle_topk() -> usize {
25065    if exact_mtp_calibration_active() {
25066        return 0;
25067    }
25068    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
25069    *K.get_or_init(|| {
25070        std::env::var("CMF_FFN_ORACLE_TOPK")
25071            .ok()
25072            .and_then(|v| v.parse().ok())
25073            .unwrap_or(0)
25074    })
25075}
25076
25077/// `CMF_FFN_GATE_TOPK` — the REALIZABLE cousin of the oracle: rank the
25078/// neurons by their gate alone (which the kernel has computed anyway
25079/// before it reads `up`), keep the k best, and drop the rest. Every
25080/// dropped neuron's `up` row and `down` column stay unread, so this is
25081/// the sparsity a serving path can actually take without a router.
25082fn gate_topk() -> usize {
25083    if exact_mtp_calibration_active() {
25084        return 0;
25085    }
25086    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
25087    *K.get_or_init(|| {
25088        std::env::var("CMF_FFN_GATE_TOPK")
25089            .ok()
25090            .and_then(|v| v.parse().ok())
25091            .unwrap_or(0)
25092    })
25093}
25094
25095/// `CMF_FFN_GATE_BLOCK` — select in blocks of B neurons instead of one
25096/// by one. A scattered per-neuron choice cannot be read efficiently (a
25097/// row at a time, no prefetch runway); a block of 32 is a contiguous
25098/// 32-row slab of `up` and of the transposed `down`, which the ordinary
25099/// kernels stream. The question the measurement answers is what the
25100/// block costs in quality.
25101fn gate_block() -> usize {
25102    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
25103    *B.get_or_init(|| {
25104        std::env::var("CMF_FFN_GATE_BLOCK")
25105            .ok()
25106            .and_then(|v| v.parse().ok())
25107            .unwrap_or(1)
25108    })
25109}
25110
25111/// Zero all but the `k` largest BLOCKS (by summed square) of a row.
25112fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
25113    let n = g.len();
25114    let nb = n.div_ceil(block);
25115    let kb = (keep_n.div_ceil(block)).clamp(1, nb);
25116    if kb >= nb {
25117        return;
25118    }
25119    let mut score: Vec<f32> = (0..nb)
25120        .map(|b| {
25121            g[b * block..((b + 1) * block).min(n)]
25122                .iter()
25123                .map(|v| v * v)
25124                .sum::<f32>()
25125        })
25126        .collect();
25127    let mut ord = score.clone();
25128    let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
25129        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
25130    });
25131    let thr = *kth;
25132    for b in 0..nb {
25133        if score[b] < thr {
25134            g[b * block..((b + 1) * block).min(n)].fill(0.0);
25135        }
25136    }
25137    score.clear();
25138}
25139
25140/// Zero all but the `k` largest magnitudes of one token's activation row.
25141fn keep_top_k(g: &mut [f32], k: usize) {
25142    if gate_block() > 1 {
25143        return keep_top_blocks(g, k, gate_block());
25144    }
25145    let n = g.len();
25146    if k == 0 || k >= n {
25147        return;
25148    }
25149    let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
25150    let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
25151        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
25152    });
25153    let thr = *kth;
25154    for v in g.iter_mut() {
25155        if v.abs() < thr {
25156            *v = 0.0;
25157        }
25158    }
25159}
25160
25161/// `CMF_FFN_PROBE_SQ` — accumulate Σa², so the dump divided by the token
25162/// count and square-rooted is the RMS activation trace Patent 12 weights
25163/// its matrices by.
25164fn probe_sq() -> bool {
25165    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
25166    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
25167}
25168
25169/// `CMF_FFN_PROBE_SIGNED` — accumulate the SIGNED activation sum
25170/// instead of its magnitude: what a dropped neuron contributes ON
25171/// AVERAGE, which is the bias a narrowed FFN can add back for free.
25172fn probe_signed() -> bool {
25173    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
25174    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
25175}
25176
25177/// `CMF_FFN_MEANFILL=<file>` — a masked-out neuron contributes its MEAN
25178/// activation instead of zero (`u32 layers, u32 inter, f32[…]`, the mass
25179/// dump layout, holding per-neuron means). Dropping a neuron outright
25180/// also drops its average contribution, which shifts the layer output by
25181/// a constant; filling the mean back is one add per layer and costs no
25182/// bytes off the bus. This is the measurement arm — in a tube file the
25183/// same correction ships as a per-task bias vector.
25184fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
25185    static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
25186    M.get_or_init(|| {
25187        let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
25188        let b = std::fs::read(&p).ok()?;
25189        let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
25190        let vals: Vec<f32> = b[8..]
25191            .chunks_exact(4)
25192            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
25193            .collect();
25194        eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
25195        Some((inter, vals))
25196    })
25197    .as_ref()
25198}
25199
25200/// `CMF_FFN_PROBE_TOPK` — 0 (default) = accumulate mass, k>0 = count
25201/// how often a neuron lands in a token's top k.
25202fn probe_topk() -> usize {
25203    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
25204    *K.get_or_init(|| {
25205        std::env::var("CMF_FFN_PROBE_TOPK")
25206            .ok()
25207            .and_then(|v| v.parse().ok())
25208            .unwrap_or(0)
25209    })
25210}
25211
25212thread_local! {
25213    /// DTG-MA activation probe: per-layer per-neuron Σ|silu(g)·u|
25214    /// accumulator, alive only during `Pipeline::probe_ffn_mass`.
25215    static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
25216        const { std::cell::RefCell::new(None) };
25217}
25218
25219/// Hot-neuron probe of a MoE layer's routed experts (`cortiq hot-probe --experts`):
25220/// per layer, per expert and neuron, Σ (w·act)² over the tokens the router sent
25221/// there, `w` the expert's final weight — what the neuron's down column sees.
25222pub(crate) struct MoeProbe {
25223    /// `[layer][expert · inter + neuron]`; a layer without experts keeps an empty row.
25224    acc: Vec<Vec<f64>>,
25225    inter: usize,
25226    /// The routed expert `dense_ffn` is running for, and its weight; `None`
25227    /// outside the routed loop (the shared expert is not probed).
25228    cur: Option<(usize, f32)>,
25229}
25230
25231thread_local! {
25232    /// Alive only during `Pipeline::probe_moe_mass`.
25233    static MOE_PROBE: std::cell::RefCell<Option<MoeProbe>> = const { std::cell::RefCell::new(None) };
25234}
25235
25236fn moe_probe_active() -> bool {
25237    MOE_PROBE.with(|p| p.borrow().is_some())
25238}
25239
25240/// Per-token structured sparsity, paid for in bytes.
25241///
25242/// The gate is the cheapest third of an FFN and it already says which
25243/// neurons matter: `silu(gate)` near zero means the neuron contributes
25244/// nothing whatever `up` says. So compute every gate, keep the `k`
25245/// loudest, and read ONLY those neurons' `up` rows and `down` rows —
25246/// the latter needs `down_proj` stored transposed, otherwise a neuron's
25247/// down weights are a strided column and "reading only those" costs a
25248/// full cache line each.
25249///
25250/// Returns `None` when the file has no transposed `down` (the caller
25251/// then runs the ordinary dense path).
25252fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
25253    // The scatter path reads individual rows/columns and cannot express the
25254    // per-matrix signed FWHT boundary.  Let the descriptor-aware dense path
25255    // handle Prism files rather than silently running an unrotated sparse
25256    // approximation.
25257    if d.gate_proj.has_prism_contract()
25258        || d.up_proj.has_prism_contract()
25259        || d.down_proj.has_prism_contract()
25260    {
25261        return None;
25262    }
25263    let dt = d.down_t.as_ref()?;
25264    let inter = d.gate_proj.rows();
25265    let hidden = dt.cols();
25266    if k == 0 || k >= inter || d.act != Act::Silu {
25267        return None;
25268    }
25269    DYN_SCRATCH.with(|sc| {
25270        let mut sc = sc.borrow_mut();
25271        let DynScratch {
25272            g,
25273            mag,
25274            live,
25275            parts,
25276        } = &mut *sc;
25277        g.resize(inter, 0.0);
25278        d.gate_proj.matvec(x, g, pool);
25279        for v in g.iter_mut() {
25280            *v = inference::silu(*v);
25281        }
25282        // The k-th largest |silu(gate)| is the threshold; ties keep more,
25283        // which is the safe side.
25284        mag.clear();
25285        mag.extend(g.iter().map(|v| v.abs()));
25286        let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
25287            b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
25288        });
25289        let thr = *kth;
25290        live.clear();
25291        live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
25292        let mut out = vec![0.0f32; hidden];
25293        match pool {
25294            Some(p) if live.len() >= 64 => {
25295                let nw = p.n_workers() + 1;
25296                parts.clear();
25297                parts.resize(nw * hidden, 0.0);
25298                let ptr = SendMut(parts.as_mut_ptr());
25299                let n = live.len();
25300                let live_ref: &[u32] = live;
25301                let g_ref: &[f32] = g;
25302                p.run(&|w, workers| {
25303                    let chunk = n.div_ceil(workers);
25304                    let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
25305                    if s >= e {
25306                        return;
25307                    }
25308                    WORKER_SCRATCH.with(|ws| {
25309                        let mut ws = ws.borrow_mut();
25310                        let [scratch, acc] = &mut *ws;
25311                        scratch.resize(hidden.max(x.len()), 0.0);
25312                        acc.clear();
25313                        acc.resize(hidden, 0.0);
25314                        for (o, &nrm) in live_ref[s..e].iter().enumerate() {
25315                            // One neuron of runway: the next row's lines
25316                            // start moving while this one is multiplied.
25317                            if let Some(&nx) = live_ref[s..e].get(o + 1) {
25318                                d.up_proj.prefetch_row(nx as usize);
25319                                dt.prefetch_row(nx as usize);
25320                            }
25321                            let idx = nrm as usize;
25322                            let up = d.up_proj.row_dot(idx, x, scratch);
25323                            let a = g_ref[idx] * up;
25324                            if a != 0.0 {
25325                                dt.add_row_scaled(idx, a, acc, scratch);
25326                            }
25327                        }
25328                        for (j, v) in acc.iter().enumerate() {
25329                            unsafe { *ptr.at(w * hidden + j) = *v };
25330                        }
25331                    });
25332                });
25333                for w in 0..nw {
25334                    for (j, o) in out.iter_mut().enumerate() {
25335                        *o += parts[w * hidden + j];
25336                    }
25337                }
25338            }
25339            _ => {
25340                WORKER_SCRATCH.with(|ws| {
25341                    let mut ws = ws.borrow_mut();
25342                    let [scratch, _acc] = &mut *ws;
25343                    scratch.resize(hidden.max(x.len()), 0.0);
25344                    for &nrm in live.iter() {
25345                        let idx = nrm as usize;
25346                        let up = d.up_proj.row_dot(idx, x, scratch);
25347                        let a = g[idx] * up;
25348                        if a != 0.0 {
25349                            dt.add_row_scaled(idx, a, &mut out, scratch);
25350                        }
25351                    }
25352                });
25353            }
25354        }
25355        Some(out)
25356    })
25357}
25358
25359/// Caller-side scratch of the dynamic path — one allocation per thread,
25360/// not one per layer per token (that alone cost a third of the decode).
25361struct DynScratch {
25362    g: Vec<f32>,
25363    mag: Vec<f32>,
25364    live: Vec<u32>,
25365    parts: Vec<f32>,
25366}
25367
25368thread_local! {
25369    static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
25370        std::cell::RefCell::new(DynScratch {
25371            g: Vec::new(),
25372            mag: Vec::new(),
25373            live: Vec::new(),
25374            parts: Vec::new(),
25375        })
25376    };
25377    /// Pool-worker scratch: the row buffer and this worker's partial sum.
25378    static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
25379        const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
25380}
25381
25382/// `dense_ffn_cpu` with a per-visit mask landing on the activations —
25383/// the masked-inference fast path's decode arm. Full fused quant
25384/// compute, closed neurons zeroed before down: arithmetically the
25385/// pruned network, no dequant, no weight bytes touched.
25386fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
25387    let inter = d.gate_proj.rows();
25388    FFN_SCRATCH.with(|s| {
25389        let mut s = s.borrow_mut();
25390        let [g, u, ..] = &mut *s;
25391        g.resize(inter, 0.0);
25392        if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
25393            // g holds silu(gate)·up.
25394        } else {
25395            u.resize(inter, 0.0);
25396            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
25397            for i in 0..inter {
25398                g[i] = d.act.combine(g[i], u[i]);
25399            }
25400        }
25401        zero_masked_cols(g, 1, inter, mask_row);
25402        let mut out = attention::take_buf(d.down_proj.rows());
25403        d.down_proj.matvec(g, &mut out, pool);
25404        out
25405    })
25406}
25407
25408/// Dense FFN as one GPU submission via the MoE block path (single
25409/// expert, weight 1.0): gate → silu·up → down chained in one command
25410/// buffer, intermediate activations device-resident. None → weights
25411/// not q8-mapped in the primary shard / over the VRAM budget / backend
25412/// refusal → honest CPU path.
25413fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
25414    if d.gate_proj.has_prism_contract()
25415        || d.up_proj.has_prism_contract()
25416        || d.down_proj.has_prism_contract()
25417    {
25418        return None;
25419    }
25420    // The GPU block applies the standard graph activations plus the exact
25421    // MiniMax-M3 OAI contract; any other activation stays on the CPU.
25422    d.act.moe_job_graph_contract()?;
25423    // Threshold: tiny FFNs are not worth a submission (q1 excepted —
25424    // see the caller's gate).
25425    if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
25426        return None;
25427    }
25428    let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
25429    let mut model_ref = None;
25430    moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
25431    let model = model_ref?;
25432    let hidden = jobs[0].down.1;
25433    let mut out = attention::take_buf(hidden);
25434    if crate::gpu::moe_block(&model, &jobs, &mut out) {
25435        Some(out)
25436    } else {
25437        let mut out = out;
25438        attention::recycle_buf(&mut out);
25439        None
25440    }
25441}
25442
25443/// q8-mapped primary-shard tensor parts for a GPU job: q8_2f carries
25444/// its column field, q8_row runs with empty col slices (the backend
25445/// skips the multiply). Shared by the MoE block and the dense-FFN
25446/// single-job path.
25447#[allow(clippy::type_complexity)]
25448#[allow(clippy::type_complexity)]
25449pub(crate) fn moe_parts(
25450    t: &QTensor,
25451) -> Option<(
25452    &std::sync::Arc<cortiq_core::CmfModel>,
25453    usize,
25454    usize,
25455    usize,
25456    &[f32],
25457    &[f32],
25458    bool,
25459    bool,
25460    bool,
25461)> {
25462    match t {
25463        QTensor::Mapped {
25464            model,
25465            idx,
25466            dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
25467            rows,
25468            cols,
25469            row_scale,
25470            col_field,
25471            ..
25472        } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
25473            model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
25474        )),
25475        // q1: tile-embedded scales — empty rs/col slices, raw xs.
25476        QTensor::Mapped {
25477            model,
25478            idx,
25479            dtype: cortiq_core::TensorDtype::Q1,
25480            rows,
25481            cols,
25482            ..
25483        } => Some((
25484            model,
25485            *idx,
25486            *rows,
25487            *cols,
25488            &[][..],
25489            &[][..],
25490            true,
25491            false,
25492            false,
25493        )),
25494        // q4_tiled: 18-byte tiles with embedded f16 scales — raw xs.
25495        QTensor::Mapped {
25496            model,
25497            idx,
25498            dtype: cortiq_core::TensorDtype::Q4Tiled,
25499            rows,
25500            cols,
25501            ..
25502        } => Some((
25503            model,
25504            *idx,
25505            *rows,
25506            *cols,
25507            &[][..],
25508            &[][..],
25509            false,
25510            true,
25511            false,
25512        )),
25513        // q4tp: same raw-xs contract, different stride and scale plane.
25514        QTensor::Mapped {
25515            model,
25516            idx,
25517            dtype: cortiq_core::TensorDtype::Q4TiledP,
25518            rows,
25519            cols,
25520            ..
25521        } => Some((
25522            model,
25523            *idx,
25524            *rows,
25525            *cols,
25526            &[][..],
25527            &[][..],
25528            false,
25529            true,
25530            false,
25531        )),
25532        // q2tp: the 2-bit expert plane of the mixed profile — q4 family
25533        // for stride bookkeeping, flagged q2 so the trio validation can
25534        // demand a q4tp down.
25535        QTensor::Mapped {
25536            model,
25537            idx,
25538            dtype: cortiq_core::TensorDtype::Q2TiledP,
25539            rows,
25540            cols,
25541            ..
25542        } => Some((
25543            model,
25544            *idx,
25545            *rows,
25546            *cols,
25547            &[][..],
25548            &[][..],
25549            false,
25550            true,
25551            true,
25552        )),
25553        _ => None,
25554    }
25555}
25556
25557/// The routed experts of a MoE layer as the Metal chunk prefill takes
25558/// them: f32 router rows and every expert's q4tp (gate, up, down) tensor
25559/// indices, uniform SiLU shapes. Only the plain routed form — no shared
25560/// expert, no per-expert scale, no resonance or τ router, no router input
25561/// norm; the batched host walk keeps the rest. The routing itself stays the
25562/// host's (`moe_route`), so mask and selection bias need nothing here.
25563#[cfg(target_os = "macos")]
25564#[allow(clippy::type_complexity)]
25565fn chunk_moe_parts(
25566    m: &MoeFfn,
25567    hidden: usize,
25568) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
25569    // Only a shared expert of its own width, without a gate: it adds to the
25570    // routed mix (`ChunkMoe::detached`).
25571    if m.shared.as_ref().is_some_and(|(s, g)| {
25572        g.is_some()
25573            || m.experts.is_empty()
25574            || s.gate_proj.rows() == m.experts[0].gate_proj.rows()
25575            || !(s.gate_proj.q8_2f_parts().is_some() || s.gate_proj.q4tp_parts().is_some())
25576            || !(s.up_proj.q8_2f_parts().is_some() || s.up_proj.q4tp_parts().is_some())
25577            || !(s.down_proj.q8_2f_parts().is_some() || s.down_proj.q4tp_parts().is_some())
25578    }) || m.per_expert_scale.is_some()
25579        || m.resonance.is_some()
25580        || m.route_tau.is_some()
25581        || m.router_input_norm
25582        || m.experts.is_empty()
25583        || m.top_k == 0
25584        || std::env::var("CMF_RMS_TRACE").is_ok()
25585        || std::env::var("CMF_ACT_DUMP").is_ok()
25586        || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
25587    {
25588        return None;
25589    }
25590    let (rf, rr, rc) = m.router.f32_parts()?;
25591    if rr != m.experts.len() || rc != hidden {
25592        return None;
25593    }
25594    let inter = m.experts[0].gate_proj.rows();
25595    // All-q8_2f experts (Mellum2.1 q8_2f) ride the `moe_q8` GEMMs;
25596    // `CMF_METAL_MOE_Q8=0` keeps them on the host walk.
25597    let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
25598    let experts = m
25599        .experts
25600        .iter()
25601        .map(|e| {
25602            if e.act != Act::Silu
25603                || !e.segs.is_empty()
25604                || e.gate_proj.rows() != inter
25605                || e.up_proj.rows() != inter
25606                || e.down_proj.cols() != inter
25607                || e.gate_proj.cols() != hidden
25608                || e.down_proj.rows() != hidden
25609            {
25610                return None;
25611            }
25612            if q8 {
25613                let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
25614                return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
25615            }
25616            Some((
25617                e.gate_proj.mapped_q4tp()?.1,
25618                e.up_proj.mapped_q4tp()?.1,
25619                e.down_proj.mapped_q4tp()?.1,
25620            ))
25621        })
25622        .collect::<Option<Vec<_>>>()?;
25623    Some((rf, experts, q8))
25624}
25625
25626/// Map a MoE onto the Metal token graph's contract: f32 router, a
25627/// shared expert (gated — Qwen — or ungated at weight 1 — DeepSeek-V3 /
25628/// HunYuan hy_v3), softmax or sigmoid scores with an optional selection
25629/// bias and routed scale, experts uniformly q4tp (or the mixed profile:
25630/// q2tp gate/up over a q4tp down). τ routers, masks, per-expert scales
25631/// and Gemma's router-input norm refuse here — those semantics stay on
25632/// the CPU path.
25633/// Map Flint's released ragged MoE into the native page graph.  This stays
25634/// separate from `metal_moe_graph_parts`: its six routed slots are Q2TP and
25635/// 512-wide, while its ungated shared SwiGLU is Q4TP and 1024-wide.  Feeding
25636/// the shared tuple into the uniform jobs descriptor would make the shader
25637/// use the routed stride on a different payload.
25638#[cfg(target_os = "macos")]
25639fn metal_flint_moe_graph_parts(
25640    m: &MoeFfn,
25641    hidden: usize,
25642    scratch_key: usize,
25643) -> Option<crate::gpu::GpuFlintMoe<'_>> {
25644    use std::sync::Arc;
25645
25646    const HIDDEN: usize = 2_048;
25647    const ROUTED_INTER: usize = 512;
25648    const SHARED_INTER: usize = 1_024;
25649    const EXPERTS: usize = 256;
25650    const TOP_K: usize = 6;
25651
25652    if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
25653        return None;
25654    }
25655    let routed0 = m.experts.first()?;
25656    let (model, _) = routed0.gate_proj.mapped_q2tp()?;
25657    let (router, router_rows, router_cols) = m.router.f32_parts()?;
25658    if router_rows != EXPERTS || router_cols != HIDDEN {
25659        return None;
25660    }
25661    if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
25662        return None;
25663    }
25664    let same_q2 = |t: &QTensor| -> Option<usize> {
25665        let (owner, idx) = t.mapped_q2tp()?;
25666        Arc::ptr_eq(&owner, &model).then_some(idx)
25667    };
25668    let same_q4 = |t: &QTensor| -> Option<usize> {
25669        let (owner, idx) = t.mapped_q4tp()?;
25670        Arc::ptr_eq(&owner, &model).then_some(idx)
25671    };
25672    let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
25673        if e.act != Act::Silu
25674            || !e.segs.is_empty()
25675            || e.gate_proj.rows() != ROUTED_INTER
25676            || e.gate_proj.cols() != HIDDEN
25677            || e.up_proj.rows() != ROUTED_INTER
25678            || e.up_proj.cols() != HIDDEN
25679            || e.down_proj.rows() != HIDDEN
25680            || e.down_proj.cols() != ROUTED_INTER
25681        {
25682            return None;
25683        }
25684        Some((
25685            same_q2(&e.gate_proj)?,
25686            same_q2(&e.up_proj)?,
25687            same_q4(&e.down_proj)?,
25688        ))
25689    };
25690    let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
25691        if e.act != Act::Silu
25692            || !e.segs.is_empty()
25693            || e.gate_proj.rows() != SHARED_INTER
25694            || e.gate_proj.cols() != HIDDEN
25695            || e.up_proj.rows() != SHARED_INTER
25696            || e.up_proj.cols() != HIDDEN
25697            || e.down_proj.rows() != HIDDEN
25698            || e.down_proj.cols() != SHARED_INTER
25699        {
25700            return None;
25701        }
25702        Some((
25703            same_q4(&e.gate_proj)?,
25704            same_q4(&e.up_proj)?,
25705            same_q4(&e.down_proj)?,
25706        ))
25707    };
25708    let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
25709    if experts.len() != EXPERTS {
25710        return None;
25711    }
25712    let (shared_ffn, shared_gate) = m.shared.as_ref()?;
25713    if shared_gate.is_some() {
25714        return None;
25715    }
25716    Some(crate::gpu::GpuFlintMoe {
25717        router,
25718        bias: m.expert_bias.as_deref(),
25719        experts,
25720        scratch_key,
25721        shared: shared(shared_ffn)?,
25722        top_k: m.top_k,
25723        norm_topk: m.norm_topk_prob,
25724        route_scale: m.routed_scaling,
25725        sigmoid: m.router_sigmoid,
25726        sigmoid_denom_eps: m.sigmoid_denom_eps,
25727    })
25728}
25729
25730#[cfg(target_os = "macos")]
25731fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
25732    if m.router_input_norm
25733        || m.route_tau.is_some()
25734        || m.mask.is_some()
25735        || m.per_expert_scale.is_some()
25736        || m.experts.is_empty()
25737        || m.top_k == 0
25738        || m.resonance.is_some()
25739    {
25740        return None;
25741    }
25742    // An ungated shared expert of its own width stays out of the jobs ladder:
25743    // it runs after the routed mix (`GpuMoe::detached`).
25744    let detached = m.shared.as_ref().filter(|(s, g)| {
25745        g.is_none() && !m.router_sigmoid && s.gate_proj.rows() != m.experts[0].gate_proj.rows()
25746    });
25747    // Without a shared expert the select kernel fills the k routed jobs
25748    // only (flag 32). Measured on Mellum2.1's softmax router; the sigmoid /
25749    // biased routers without one (LFM2-MoE) keep the CPU path until they
25750    // are measured too.
25751    let (sh, sg) = match &m.shared {
25752        Some((sh, sg)) if detached.is_none() => (Some(sh), sg.as_ref()),
25753        _ if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
25754        _ => return None,
25755    };
25756    let (rf, rr, rc) = m.router.f32_parts()?;
25757    if rr != m.experts.len() || rc != hidden {
25758        return None;
25759    }
25760    let shared_gated = sg.is_some();
25761    let sf = match sg {
25762        Some(sg) => {
25763            let (sf, sr, sc) = sg.f32_parts()?;
25764            if sr * sc != hidden {
25765                return None;
25766            }
25767            sf
25768        }
25769        // Ungated: the router's first row stands in for the gate matvec
25770        // (its logit is never read — the kernel pins weight 1).
25771        None => &rf[..hidden],
25772    };
25773    if let Some(b) = &m.expert_bias {
25774        if b.len() != m.experts.len() {
25775            return None;
25776        }
25777    }
25778    let inter = m.experts[0].gate_proj.rows();
25779    // The first expert's gate decides the profile; every trio (shared
25780    // included) must agree — the jobs ladder flips ONE kernel for all.
25781    let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
25782    // All-q8_2f routed experts (Mellum2.1 q8_2f: no shared expert, the
25783    // contract they were measured on) take the `moe_q8` kernels;
25784    // `CMF_METAL_MOE_Q8=0` keeps them off the graph (per-op path).
25785    let q8 = crate::gpu_metal::moe_q8::enabled()
25786        && (m.shared.is_none() || detached.is_some())
25787        && m.experts[0].gate_proj.q8_2f_parts().is_some();
25788    let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
25789        if e.act != Act::Silu
25790            || e.gate_proj.rows() != inter
25791            || e.gate_proj.cols() != hidden
25792            || e.up_proj.rows() != inter
25793            || e.up_proj.cols() != hidden
25794            || e.down_proj.rows() != hidden
25795            || e.down_proj.cols() != inter
25796        {
25797            return None;
25798        }
25799        if q8 {
25800            let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
25801            return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
25802        }
25803        let pick = |t: &QTensor| -> Option<usize> {
25804            if gu_q2 {
25805                t.mapped_q2tp().map(|(_, i)| i)
25806            } else {
25807                t.mapped_q4tp().map(|(_, i)| i)
25808            }
25809        };
25810        Some((
25811            pick(&e.gate_proj)?,
25812            pick(&e.up_proj)?,
25813            e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
25814        ))
25815    };
25816    let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
25817    let shared = match sh {
25818        Some(sh) => Some(trio(sh)?),
25819        None => None,
25820    };
25821    let detached = match detached {
25822        Some((s, _)) => Some([
25823            s.gate_proj.metal_graph_parts()?,
25824            s.up_proj.metal_graph_parts()?,
25825            s.down_proj.metal_graph_parts()?,
25826        ]),
25827        None => None,
25828    };
25829    Some(crate::gpu::GpuMoe {
25830        router: rf,
25831        sgate: sf,
25832        experts,
25833        shared,
25834        n_exp: m.experts.len(),
25835        top_k: m.top_k,
25836        inter,
25837        norm_topk: m.norm_topk_prob,
25838        route_scale: m.routed_scaling,
25839        gu_q2,
25840        q8,
25841        sigmoid: m.router_sigmoid,
25842        sigmoid_denom_eps: m.sigmoid_denom_eps,
25843        bias: m.expert_bias.as_deref(),
25844        shared_gated,
25845        detached,
25846    })
25847}
25848
25849/// `CMF_FORCE_IDS=<file>` (diagnostics): decode step n commits the n-th
25850/// whitespace-separated token id of the file instead of sampling, while the
25851/// logits are still computed (and dumped by `CMF_LOGIT_DUMP_ALL`) — one
25852/// backend teacher-forced along another's greedy sequence, so top-1
25853/// agreement can be counted past a near-tie divergence.
25854fn forced_ids_at(step: usize) -> Option<u32> {
25855    static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
25856    IDS.get_or_init(|| {
25857        std::env::var("CMF_FORCE_IDS")
25858            .ok()
25859            .and_then(|p| std::fs::read_to_string(p).ok())
25860            .map(|s| {
25861                s.split_whitespace()
25862                    .filter_map(|t| t.parse().ok())
25863                    .collect()
25864            })
25865            .unwrap_or_default()
25866    })
25867    .get(step)
25868    .copied()
25869}
25870
25871/// Build one gate/up/down GPU job from three tensors. `moe_push_job` is the
25872/// DenseFfn-shaped caller; architectures that keep their experts in their own
25873/// structs (DeepSeek-V4) come here directly.
25874pub(crate) fn moe_push_job_parts<'a>(
25875    gate: &'a QTensor,
25876    up: &'a QTensor,
25877    down: &'a QTensor,
25878    x: &[f32],
25879    w: f32,
25880    swiglu_limit: f32,
25881    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
25882    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
25883) -> Option<()> {
25884    use crate::qtensor::prescale;
25885    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
25886    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
25887    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
25888    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
25889        return None; // mixed-dtype trio — honest CPU path
25890    }
25891    // The 2-bit profile is gate/up q2tp over a PLAIN q4tp down; any other
25892    // 2-bit arrangement stays on the CPU.
25893    if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
25894        return None;
25895    }
25896    if !gq2 && dq2 {
25897        return None;
25898    }
25899    model_ref.get_or_insert_with(|| gm.clone());
25900    let dt = |cf: &[f32]| {
25901        if cf.is_empty() {
25902            cortiq_core::TensorDtype::Q8Row
25903        } else {
25904            cortiq_core::TensorDtype::Q8_2f
25905        }
25906    };
25907    jobs.push(crate::gpu::MoeJob {
25908        gate: (gi, gr, gc, grs),
25909        up: (ui, ur, uc, urs),
25910        down: (di, dr, dc, drs),
25911        xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
25912        xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
25913        down_col: dcf,
25914        w,
25915        q1: gq1,
25916        q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
25917        q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
25918        gu_q2: gq2,
25919        swiglu_limit,
25920        act: 0,
25921    });
25922    Some(())
25923}
25924
25925/// Build one gate/up/down GPU job (see `moe_parts`).
25926fn moe_push_job<'a>(
25927    d: &'a DenseFfn,
25928    x: &[f32],
25929    w: f32,
25930    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
25931    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
25932) -> Option<()> {
25933    use crate::qtensor::prescale;
25934    // The block's act(gate)·up pass takes an exact device contract.  For
25935    // ordinary SiLU/GELU this is a zero clamp; MiniMax M3 contributes its
25936    // dedicated OAI marker and frozen 7.0 source limit.  Anything without a
25937    // device contract stays on the CPU.
25938    let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
25939    let act = graph_act.code();
25940    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
25941    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
25942    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
25943    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
25944        return None; // mixed-dtype trio — honest CPU path
25945    }
25946    if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
25947        return None;
25948    }
25949    if !gq2 && dq2 {
25950        return None;
25951    }
25952    model_ref.get_or_insert_with(|| gm.clone());
25953    let gdt = if gcf.is_empty() {
25954        cortiq_core::TensorDtype::Q8Row
25955    } else {
25956        cortiq_core::TensorDtype::Q8_2f
25957    };
25958    let udt = if ucf.is_empty() {
25959        cortiq_core::TensorDtype::Q8Row
25960    } else {
25961        cortiq_core::TensorDtype::Q8_2f
25962    };
25963    jobs.push(crate::gpu::MoeJob {
25964        gate: (gi, gr, gc, grs),
25965        up: (ui, ur, uc, urs),
25966        down: (di, dr, dc, drs),
25967        xs_gate: prescale(x, gcf, gdt).into_owned(),
25968        xs_up: prescale(x, ucf, udt).into_owned(),
25969        down_col: dcf,
25970        w,
25971        q1: gq1,
25972        q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
25973        q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
25974        gu_q2: gq2,
25975        swiglu_limit,
25976        act,
25977    });
25978    Some(())
25979}
25980
25981/// Sparse dense-FFN directly on QUANTIZED weights (mask × mmap): reads
25982/// ONLY the active neurons' gate/up rows and down columns from the mmap
25983/// — no full-matrix dequant, no f32 model copy. This is what lets a
25984/// masked big model run at quantized RSS (the historical mask path
25985/// forced the whole model to f32). Semantics identical to the f32
25986/// sparse path within quant tolerance.
25987fn sparse_ffn_quant(
25988    d: &DenseFfn,
25989    x: &[f32],
25990    active: &[u16],
25991    hidden: usize,
25992    pool: Option<&Pool>,
25993) -> Vec<f32> {
25994    let n = active.len();
25995    let inter = d.gate_proj.rows();
25996    let mut act = vec![0.0f32; n];
25997    // Scratch is needed if EITHER projection is group-packed (q4/vbit);
25998    // gate/up normally share a dtype but sizing on both is robust.
25999    let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
26000    let compute = |ai: usize| -> f32 {
26001        let idx = active[ai] as usize;
26002        if idx >= inter {
26003            return 0.0; // defensive parity with the f32 sparse path
26004        }
26005        let mut s = if need_scratch {
26006            vec![0.0f32; hidden]
26007        } else {
26008            Vec::new()
26009        };
26010        let gate = d.gate_proj.row_dot(idx, x, &mut s);
26011        let up = d.up_proj.row_dot(idx, x, &mut s);
26012        d.act.combine(gate, up)
26013    };
26014    match pool {
26015        Some(p) if n >= 256 => {
26016            let ptr = SendMut(act.as_mut_ptr());
26017            p.run(&|widx, nw| {
26018                let chunk = n.div_ceil(nw);
26019                let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
26020                for ai in s..e {
26021                    unsafe { *ptr.at(ai) = compute(ai) };
26022                }
26023            });
26024        }
26025        _ => {
26026            for (ai, a) in act.iter_mut().enumerate() {
26027                *a = compute(ai);
26028            }
26029        }
26030    }
26031    // Scatter through active down columns (reads only those columns).
26032    let mut out = vec![0.0f32; hidden];
26033    for (ai, &idx) in active.iter().enumerate() {
26034        let w = act[ai];
26035        if w.abs() >= 1e-12 && (idx as usize) < inter {
26036            d.down_proj.add_col_scaled(idx as usize, w, &mut out);
26037        }
26038    }
26039    out
26040}
26041
26042/// Test-only re-export of the private sparse-quant FFN (mask × mmap gate).
26043#[doc(hidden)]
26044pub fn sparse_ffn_quant_for_test(
26045    d: &DenseFfn,
26046    x: &[f32],
26047    active: &[u16],
26048    hidden: usize,
26049) -> Vec<f32> {
26050    sparse_ffn_quant(d, x, active, hidden, None)
26051}
26052
26053/// Dequantize a DenseFfn's three matrices to f32 (transient; only the
26054/// q4/vbit-masked fallback uses it — the memory-lean path is
26055/// sparse_ffn_quant). Reuses row_f32 row-by-row.
26056fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
26057    let deq = |t: &QTensor| -> Vec<f32> {
26058        let (rows, cols) = (t.rows(), t.cols());
26059        let mut out = vec![0.0f32; rows * cols];
26060        for r in 0..rows {
26061            t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
26062        }
26063        out
26064    };
26065    (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
26066}
26067
26068/// Pointer wrapper for the worker-pool scatter (same pattern as qtensor).
26069struct SendMut(*mut f32);
26070unsafe impl Send for SendMut {}
26071unsafe impl Sync for SendMut {}
26072impl SendMut {
26073    #[inline]
26074    // Deliberate unsynchronized scatter: pool workers write disjoint indices
26075    // in parallel, so returning `&mut` from `&self` is intentional here.
26076    #[allow(clippy::mut_from_ref)]
26077    unsafe fn at(&self, i: usize) -> &mut f32 {
26078        unsafe { &mut *self.0.add(i) }
26079    }
26080}
26081
26082/// Router → (selected experts in torch.topk order, per-expert score
26083/// vector, normalizer). The final weight of expert `e` is `p[e] / wsum`.
26084///
26085/// Two regimes share this. Qwen: softmax over ALL experts, top-k of the
26086/// probabilities, optional renorm — `router_sigmoid=false`, no bias,
26087/// scale 1 → bit-identical to the historical path. LFM2-MoE /
26088/// DeepSeek-V3 `noaux_tc`: per-expert sigmoid scores, an optional
26089/// selection bias (top-k CHOICE only; weights stay unbiased), an
26090/// architecture-specified renorm floor and a routed scale.  The ordinary
26091/// loader contract is `1e-6`; Flint and MiniMax M3.1 preserve their
26092/// reference runtime's selected-score `sum + 1e-20`.
26093pub(crate) fn moe_route(
26094    logits: &[f32],
26095    m: &MoeFfn,
26096    allowed: Option<&[bool]>,
26097) -> (Vec<usize>, Vec<f32>, f32) {
26098    moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
26099}
26100
26101/// Router implementation with an explicit sigmoid renormalization floor.
26102///
26103/// Flint and MiniMax M3.1 reference runtimes compute
26104/// `sum(selected_scores) + 1e-20` in FP32. Using the generic 1e-6 floor is
26105/// not a harmless tolerance difference when all logits are very negative: it
26106/// collapses the routed branch toward zero instead of normalizing the selected
26107/// experts. Keeping the epsilon parameter here avoids changing the established
26108/// Qwen/LFM2 contract while allowing each architecture to preserve its own
26109/// numerical semantics.
26110pub(crate) fn moe_route_with_eps(
26111    logits: &[f32],
26112    m: &MoeFfn,
26113    allowed: Option<&[bool]>,
26114    sigmoid_denom_eps: f32,
26115) -> (Vec<usize>, Vec<f32>, f32) {
26116    let ne = logits.len();
26117    // Expert restriction: the static env mask (CMF_MOE_MASK) AND the
26118    // active task mask's expert fields (spec §5) both narrow the
26119    // candidate set; selection happens over the admitted experts only.
26120    // With norm_topk the kept weights renormalize below; without it
26121    // the excluded mass is honestly dropped.
26122    let admit = |e: usize| {
26123        m.mask.as_ref().is_none_or(|mk| mk[e])
26124            && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
26125    };
26126    // The resonance router (spec §9.5.1) selects by the RAW score: the
26127    // trainer (`resonance_winner`) and the resident graph
26128    // (`embryo_core_route_pick`) take the first maximum of the scores
26129    // and run the winner with weight 1.0. Selecting through the softmax
26130    // instead is not the same decision: `exp(l − max)` rounds two scores
26131    // closer than 2^-25 (possible below |score| 0.25) to the same 1.0, and
26132    // the lower index would take a token whose score is strictly smaller
26133    // — the trainer's trace and the graph would disagree with this path.
26134    // `−∞` (outside the shell) never wins; with no finite admitted expert
26135    // the generic path below degrades to uniform. The winner's
26136    // probability is 1.0 by construction (a one-hot `p`), so its
26137    // renormalized weight is `routed_scaling` on both norm_topk settings.
26138    if m.resonance.is_some() && m.top_k == 1 {
26139        let mut best: Option<usize> = None;
26140        for e in (0..ne).filter(|&e| admit(e)) {
26141            let l = logits[e];
26142            if l == f32::NEG_INFINITY || l.is_nan() {
26143                continue;
26144            }
26145            if best.is_none_or(|b| l > logits[b]) {
26146                best = Some(e);
26147            }
26148        }
26149        if let Some(b) = best {
26150            let mut p = vec![0.0f32; ne];
26151            p[b] = 1.0;
26152            return (vec![b], p, 1.0 / m.routed_scaling);
26153        }
26154    }
26155    // A `−∞` logit (a grown expert outside its shell, `Resonance::scores`)
26156    // takes probability 0 on both paths: sigmoid(−∞) = 0, exp(−∞ − max) = 0
26157    // — top-1 is the best FINITE expert, its renormalized weight exactly
26158    // 1.0. Every expert at −∞ cannot happen (trunk experts have no shell);
26159    // should it, the softmax would be NaN, so it degrades to uniform.
26160    let p: Vec<f32> = if m.router_sigmoid {
26161        logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
26162    } else {
26163        let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
26164        if mx == f32::NEG_INFINITY {
26165            vec![1.0 / ne.max(1) as f32; ne]
26166        } else {
26167            let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
26168            let s: f32 = e.iter().sum();
26169            for v in &mut e {
26170                *v /= s;
26171            }
26172            e
26173        }
26174    };
26175    let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
26176    // Descending by selection score, lower index wins ties (torch.topk).
26177    match &m.expert_bias {
26178        Some(b) => idx.sort_unstable_by(|&x, &y| {
26179            (p[y] + b[y])
26180                .partial_cmp(&(p[x] + b[x]))
26181                .unwrap()
26182                .then(x.cmp(&y))
26183        }),
26184        None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
26185    }
26186    idx.truncate(m.top_k);
26187    // Adaptive τ-routing: trim the tail experts once the kept mass is
26188    // enough. wsum below renormalizes over the KEPT set, so the output
26189    // stays a proper weighted average.
26190    if let Some(tau) = m.route_tau {
26191        let total: f32 = idx.iter().map(|&e| p[e]).sum();
26192        if total > 0.0 {
26193            let mut acc = 0.0f32;
26194            let mut keep = idx.len();
26195            for (i, &e) in idx.iter().enumerate() {
26196                acc += p[e];
26197                if acc >= tau * total {
26198                    keep = i + 1;
26199                    break;
26200                }
26201            }
26202            idx.truncate(keep);
26203        }
26204    }
26205    let wsum: f32 = if m.norm_topk_prob {
26206        let s: f32 = idx.iter().map(|&e| p[e]).sum();
26207        // Sigmoid routers use their architecture's reference floor; the
26208        // softmax path's probs already sum near 1, so it stays exactly as
26209        // before.
26210        (if m.router_sigmoid {
26211            s + sigmoid_denom_eps
26212        } else {
26213            s
26214        }) / m.routed_scaling
26215    } else {
26216        1.0 / m.routed_scaling
26217    };
26218    (idx, p, wsum)
26219}
26220
26221/// See the call site: one `layer:e1,e2,…` line per routed token.
26222fn moe_trace(idx: &[usize]) {
26223    moe_trace_at(crate::gpu::cur_layer() as i32, idx)
26224}
26225
26226/// The same, for callers that know their layer (DSV4 owns its layers and
26227/// never sets the pipeline's current-layer marker).
26228pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
26229    use std::io::Write;
26230    static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
26231        std::sync::OnceLock::new();
26232    let Some(f) = F.get_or_init(|| {
26233        let p = std::env::var("CMF_MOE_TRACE").ok()?;
26234        Some(std::sync::Mutex::new(
26235            std::fs::OpenOptions::new()
26236                .create(true)
26237                .append(true)
26238                .open(p)
26239                .ok()?,
26240        ))
26241    }) else {
26242        return;
26243    };
26244    let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
26245    let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
26246}
26247
26248/// MoE FFN: router → top-k experts (see `moe_route`). Only selected
26249/// experts' pages are touched in mmap.
26250pub(crate) fn moe_ffn(
26251    m: &MoeFfn,
26252    x: &[f32],
26253    pool: Option<&Pool>,
26254    allowed: Option<&[bool]>,
26255) -> Vec<f32> {
26256    let r = moe_ffn_route(m, x, pool, allowed);
26257    moe_ffn_experts(m, x, &r, pool)
26258}
26259
26260/// One token's host route through a MoE layer: the chosen experts in
26261/// selection order, the per-expert scores and the normalizer (see
26262/// `moe_route`), plus the raw router logits.
26263pub(crate) struct MoeRoute {
26264    pub idx: Vec<usize>,
26265    pub p: Vec<f32>,
26266    pub wsum: f32,
26267    pub logits: Vec<f32>,
26268}
26269
26270/// The routing half of `moe_ffn`, shared by every executor of the chosen
26271/// experts (the host/per-op path below and the MiMo dynamic device cache,
26272/// `crate::mimo_moe`): activation accounting, router logits, `moe_route`,
26273/// the selection statistics and the `CMF_MOE_TRACE` line — so switching
26274/// executors can never change which experts a token gets.
26275pub(crate) fn moe_ffn_route(
26276    m: &MoeFfn,
26277    x: &[f32],
26278    pool: Option<&Pool>,
26279    allowed: Option<&[bool]>,
26280) -> MoeRoute {
26281    accumulate_act(m, x, 1);
26282    let ne = m.experts.len();
26283    let mut logits = vec![0.0f32; ne];
26284    match &m.resonance {
26285        Some(r) => r.scores(x, &mut logits),
26286        None => m.router.matvec(x, &mut logits, pool),
26287    }
26288    let (idx, p, wsum) = moe_route(&logits, m, allowed);
26289    {
26290        let mut st = m.stats.borrow_mut();
26291        if st.len() < ne {
26292            st.resize(ne, 0);
26293        }
26294        for &e in &idx {
26295            st[e] += 1;
26296        }
26297    }
26298    // `CMF_MOE_TRACE=<file>`: append one line per (layer, token) with the
26299    // selected expert ids. The cumulative `stats` above answer "which
26300    // experts are popular"; a residency design needs the question they
26301    // cannot answer — whether CONSECUTIVE tokens reuse experts (the
26302    // temporal locality an LRU cache lives on, FreeToken §4).
26303    moe_trace(&idx);
26304    MoeRoute {
26305        idx,
26306        p,
26307        wsum,
26308        logits,
26309    }
26310}
26311
26312/// Flint's released mixed MoE profile has a native-Metal selected-expert
26313/// pager with a different numerical contract from the generic CPU probe:
26314/// the pager keeps activations in f32, whereas the fast host arm normally
26315/// uses its A8W8 activation path.  Timing those two approximations against
26316/// one another can make the probe reject the exact pager even when it is the
26317/// faster exact-f32 implementation.  Keep this gate deliberately narrower
26318/// than the pager itself; the pager revalidates every selected tuple before
26319/// it binds a no-copy view and an honest refusal still takes the CPU path.
26320#[cfg(target_os = "macos")]
26321fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
26322    const HIDDEN: usize = 2_048;
26323    const ROUTED_INTER: usize = 512;
26324    const SHARED_INTER: usize = 1_024;
26325    const EXPERTS: usize = 256;
26326    const TOP_K: usize = 6;
26327
26328    let Some(routed) = m.experts.first() else {
26329        return false;
26330    };
26331    let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
26332        return false;
26333    };
26334    let Some((shared, shared_gate)) = &m.shared else {
26335        return false;
26336    };
26337    if shared_gate.is_some()
26338        || model.header.arch.arch_name != "flint"
26339        || model.header.arch.prism_hadamard.is_some()
26340        || model.header.arch.hidden_size != HIDDEN
26341        || m.experts.len() != EXPERTS
26342        || m.top_k != TOP_K
26343        || !m.router_sigmoid
26344        || !m.norm_topk_prob
26345        || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
26346        || m.routed_scaling.to_bits() != 1.0f32.to_bits()
26347        || m.route_tau.is_some()
26348        || m.mask.is_some()
26349        || m.per_expert_scale.is_some()
26350        || m.router_input_norm
26351        || m.resonance.is_some()
26352        || !m.grown.is_empty()
26353    {
26354        return false;
26355    }
26356
26357    let routed_q2 = routed.act == Act::Silu
26358        && routed.segs.is_empty()
26359        && routed.gate_proj.rows() == ROUTED_INTER
26360        && routed.gate_proj.cols() == HIDDEN
26361        && routed.up_proj.rows() == ROUTED_INTER
26362        && routed.up_proj.cols() == HIDDEN
26363        && routed.down_proj.rows() == HIDDEN
26364        && routed.down_proj.cols() == ROUTED_INTER
26365        && routed.up_proj.mapped_q2tp().is_some()
26366        && routed.down_proj.mapped_q4tp().is_some();
26367    let shared_q4 = shared.act == Act::Silu
26368        && shared.segs.is_empty()
26369        && shared.gate_proj.rows() == SHARED_INTER
26370        && shared.gate_proj.cols() == HIDDEN
26371        && shared.up_proj.rows() == SHARED_INTER
26372        && shared.up_proj.cols() == HIDDEN
26373        && shared.down_proj.rows() == HIDDEN
26374        && shared.down_proj.cols() == SHARED_INTER
26375        && shared.gate_proj.mapped_q4tp().is_some()
26376        && shared.up_proj.mapped_q4tp().is_some()
26377        && shared.down_proj.mapped_q4tp().is_some();
26378    routed_q2 && shared_q4
26379}
26380
26381/// The expert half of `moe_ffn`: run a route's experts on the per-op GPU
26382/// block or the host.
26383pub(crate) fn moe_ffn_experts(
26384    m: &MoeFfn,
26385    x: &[f32],
26386    r: &MoeRoute,
26387    pool: Option<&Pool>,
26388) -> Vec<f32> {
26389    let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
26390    // D5: the whole layer MoE block in one GPU command buffer (experts — the
26391    // same mmap via a no-copy buffer; intermediate activations on the GPU).
26392    // MoE owns a separate probe class from dense FFN: its selected-span pager
26393    // has materially different residency and latency costs.
26394    if crate::gpu::enabled_here() {
26395        #[cfg(target_os = "macos")]
26396        if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
26397            // Do not compare the pager's exact-f32 result with the CPU's
26398            // optional A8W8 activation approximation.  The strict pager is
26399            // the validated Flint path; if a directory/view/device check
26400            // declines, preserve the existing CPU fallback rather than
26401            // leaving the probe in a half-sampled state.
26402            return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
26403                crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
26404            });
26405        }
26406        match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
26407            crate::gpu::ProbeArm::Gpu => {
26408                let t0 = std::time::Instant::now();
26409                if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
26410                    crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
26411                    return out;
26412                }
26413                crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
26414            }
26415            crate::gpu::ProbeArm::CpuTimed => {
26416                let t0 = std::time::Instant::now();
26417                let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
26418                crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
26419                return out;
26420            }
26421            crate::gpu::ProbeArm::Cpu => {
26422                return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
26423            }
26424        }
26425    }
26426    moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
26427}
26428
26429/// One MiniCPM CPU-tail MoE through its opt-in dynamic bank, or through the
26430/// same-route host fallback.  The route is formed exactly once so statistics,
26431/// tracing, and model semantics remain owned by `moe_ffn_route`.
26432fn moe_ffn_minicpm_tail(
26433    slot: &mut crate::minicpm_tail::MiniCpmTail,
26434    li: usize,
26435    total_layers: usize,
26436    m: &MoeFfn,
26437    x: &[f32],
26438    pool: Option<&Pool>,
26439) -> Vec<f32> {
26440    let route = moe_ffn_route(m, x, pool, None);
26441    match slot.forward(li, m, x, &route, pool, total_layers) {
26442        Some(out) => out,
26443        // A cache refusal must be observationally identical to the generic
26444        // CPU-tail path. The surrounding host-tail scope already disables the
26445        // per-op arena, so do not introduce a float-activation policy change
26446        // merely because this explicit opt-in cache declined a model/layout.
26447        None => moe_ffn_experts(m, x, &route, pool),
26448    }
26449}
26450
26451/// One MoE token through the MiMo expert bank (`crate::mimo_moe`), or —
26452/// when the bank does not serve it — through the host path with the SAME
26453/// route, so the routing statistics and `CMF_MOE_TRACE` see it once.
26454fn moe_ffn_banked(
26455    slot: &mut crate::mimo_moe::Slot,
26456    li: usize,
26457    m: &MoeFfn,
26458    x: &[f32],
26459    pool: Option<&Pool>,
26460) -> Vec<f32> {
26461    let t0 = std::time::Instant::now();
26462    let r = moe_ffn_route(m, x, pool, None);
26463    slot.note_route(t0.elapsed().as_nanos() as u64);
26464    match slot.forward(li, m, x, &r, pool) {
26465        Some(out) => out,
26466        None => crate::qtensor::float_activations_scope(|| {
26467            crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
26468        }),
26469    }
26470}
26471
26472/// Verify rows share a bank frame; routing and fallback are decode's.
26473fn moe_ffn_banked_rows(
26474    slot: &mut crate::mimo_moe::Slot,
26475    li: usize,
26476    m: &MoeFfn,
26477    xs: &[f32],
26478    b: usize,
26479    hidden: usize,
26480    pool: Option<&Pool>,
26481) -> Vec<f32> {
26482    let t0 = std::time::Instant::now();
26483    let routes: Vec<_> = xs
26484        .chunks_exact(hidden)
26485        .map(|x| moe_ffn_route(m, x, pool, None))
26486        .collect();
26487    slot.note_route(t0.elapsed().as_nanos() as u64);
26488    if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
26489        return out;
26490    }
26491    let mut out = Vec::with_capacity(b * hidden);
26492    for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
26493        let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
26494            // A failed bank must not stream missing experts into the arena.
26495            crate::qtensor::float_activations_scope(|| {
26496                crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
26497            })
26498        });
26499        out.extend(row);
26500    }
26501    out
26502}
26503
26504/// One-shot report of whether the whole-token wgpu graph actually formed.
26505/// A refusal silently reverts to the per-op path, which is how a model can
26506/// look "GPU-accelerated" while every layer walks the host.  A device prefix
26507/// is tracked separately because it still pays a host boundary for the tail.
26508fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
26509    use std::sync::atomic::{AtomicBool, Ordering};
26510    if built {
26511        GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
26512        if total_layers > 0 && layers_run < total_layers {
26513            GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
26514        } else {
26515            GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
26516        }
26517    } else {
26518        GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
26519    }
26520    static SAID: AtomicBool = AtomicBool::new(false);
26521    if !SAID.swap(true, Ordering::Relaxed) {
26522        if built {
26523            tracing::info!("wgpu whole-token graph: ACTIVE");
26524        } else {
26525            tracing::warn!("wgpu whole-token graph refused — per-op path");
26526        }
26527    }
26528}
26529
26530/// Whole-token graph outcomes, process-wide: a benchmark that claims a
26531/// GPU number while MISS climbs is measuring the CPU — the honest-bench
26532/// contract makes that an error, not a footnote.
26533pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26534pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26535/// Graph calls that returned a hidden after running only a leading device
26536/// prefix.  These are valid hybrid executions but must not be reported as a
26537/// full GPU graph in benchmark evidence.
26538pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26539/// Graph calls that covered the complete requested layer span.
26540pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26541
26542/// Native Metal TokenGraph completion counters. These are incremented only
26543/// after checked command-buffer completion and successful readback, so a
26544/// fused-head NLL report can prove the route rather than infer it from env.
26545pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26546pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26547pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
26548    std::sync::atomic::AtomicU64::new(0);
26549pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26550pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26551/// Ordinary native-Metal rows-prefill admissions and completed rows.  These
26552/// counters are separate from TokenGraph token/head counts so a batch NLL
26553/// receipt cannot accidentally claim serial execution as batched.
26554pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
26555    std::sync::atomic::AtomicU64::new(0);
26556pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26557pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
26558    std::sync::atomic::AtomicU64::new(0);
26559pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
26560    std::sync::atomic::AtomicU64::new(0);
26561
26562/// `CMF_MOE_BATCH=0` restores the per-expert serial loop — the A/B lever
26563/// for the batched kernel, and how its bit-identity is checked.
26564fn moe_batch_enabled() -> bool {
26565    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
26566    *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
26567}
26568
26569/// Two-dispatch CPU MoE: every routed expert (and the shared one) fused
26570/// into one gate/up/SiLU dispatch and one down dispatch, instead of two
26571/// pool barriers per expert. Bit-identical to the serial loop below —
26572/// see `moe_gate_up_many` / `moe_down_many`. `None` = the batched kernel
26573/// does not cover this layer, walk the serial path.
26574fn moe_ffn_cpu_batched(
26575    m: &MoeFfn,
26576    x: &[f32],
26577    idx: &[usize],
26578    p: &[f32],
26579    wsum: f32,
26580    pool: Option<&Pool>,
26581) -> Option<Vec<f32>> {
26582    if idx.is_empty() || !moe_batch_enabled() {
26583        return None;
26584    }
26585    // The bake probe reads per-neuron activation mass out of the
26586    // single-expert path; batching would skip it. Rare and offline —
26587    // hand those runs to the serial loop.
26588    if FFN_PROBE.with(|pr| pr.borrow().is_some()) || moe_probe_active() {
26589        return None;
26590    }
26591    let n = idx.len() + usize::from(m.shared.is_some());
26592    let mut pairs = Vec::with_capacity(n);
26593    let mut downs = Vec::with_capacity(n);
26594    let mut ws = Vec::with_capacity(n);
26595    for &e in idx {
26596        let d = &m.experts[e];
26597        if d.act != Act::Silu {
26598            return None;
26599        }
26600        pairs.push((&d.gate_proj, &d.up_proj));
26601        downs.push(&d.down_proj);
26602        ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
26603    }
26604    // The shared expert goes last, matching the serial loop's order —
26605    // the f32 accumulation order is part of the bit-identity claim.
26606    if let Some((se, gate)) = &m.shared {
26607        if se.act != Act::Silu {
26608            return None;
26609        }
26610        let g = gate.as_ref().map_or(1.0, |gate| {
26611            let mut gl = [0.0f32; 1];
26612            gate.matvec(x, &mut gl, pool);
26613            1.0 / (1.0 + (-gl[0]).exp())
26614        });
26615        pairs.push((&se.gate_proj, &se.up_proj));
26616        downs.push(&se.down_proj);
26617        ws.push(g);
26618    }
26619    let inter = pairs[0].0.rows();
26620    let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
26621    if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
26622        return None;
26623    }
26624    let mut out = attention::take_buf(x.len());
26625    if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
26626        attention::recycle_buf(&mut out);
26627        return None;
26628    }
26629    Some(out)
26630}
26631
26632/// Exact CPU completion for the routed experts a dynamic device cache did
26633/// not contain. The weights are already the router's final normalized mix.
26634/// Keeping this independent of `MoeFfn` makes the job `Sync`: its routing
26635/// statistics live in a `RefCell`, while the immutable expert tensors can be
26636/// evaluated safely in parallel with the GPU's resident subset.
26637/// Pooled-tail stage timers, reported per token by the bench JSON
26638/// (`m3_span_ms_per_tok`, `m3_cold_ms_per_tok`, `m3_arm_ms_per_tok`).
26639pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26640pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26641pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26642pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26643pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26644
26645pub(crate) fn moe_cold_experts_cpu(
26646    experts: &[(&DenseFfn, f32)],
26647    x: &[f32],
26648    pool: Option<&Pool>,
26649) -> Vec<f32> {
26650    moe_cold_experts_cpu_with(experts, x, pool, true)
26651}
26652
26653/// [`moe_cold_experts_cpu`] with the experts' tubes (`expert_tubes_add`) optional: the
26654/// Flash-Next device path computes every winner's tube on the card.
26655pub(crate) fn moe_cold_experts_cpu_with(
26656    experts: &[(&DenseFfn, f32)],
26657    x: &[f32],
26658    pool: Option<&Pool>,
26659    tubes: bool,
26660) -> Vec<f32> {
26661    let mut out = attention::take_buf(x.len());
26662    if experts.is_empty() {
26663        return out;
26664    }
26665    let pairs: Vec<_> = experts
26666        .iter()
26667        .map(|(e, _)| (&e.gate_proj, &e.up_proj))
26668        .collect();
26669    let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
26670    let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
26671    let inter = experts[0].0.gate_proj.rows();
26672    let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
26673    // The batched gate/up kernels implement PLAIN SiLU only (their legacy
26674    // helper passes limit 0 and no activation code). MiniMax's SwiGLU-OAI
26675    // (alpha 1.702, shifted up) must take the per-expert path that reads
26676    // each tensor's own activation — otherwise every cold expert computes
26677    // silu and the residual walks away from the reference (measured: the
26678    // pooled tail shipped cos 0.047 hidden while the resident path was
26679    // bit-close).
26680    let silu_only = experts
26681        .iter()
26682        .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
26683    if silu_only
26684        && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
26685        && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
26686    {
26687        if tubes {
26688            for &(expert, weight) in experts {
26689                expert_tubes_add(expert, x, weight, &mut out, pool);
26690            }
26691        }
26692        return out;
26693    }
26694    out.fill(0.0);
26695    for &(expert, weight) in experts {
26696        if tubes {
26697            expert_tubes_add(expert, x, weight, &mut out, pool);
26698        }
26699        let mut one = dense_ffn(expert, x, pool);
26700        for (o, v) in out.iter_mut().zip(&one) {
26701            *o += weight * v;
26702        }
26703        attention::recycle_buf(&mut one);
26704    }
26705    out
26706}
26707
26708/// Cold part of a short bank batch. Share each expert's weight stream
26709/// across its tokens, but reduce contributions in each token's route order.
26710/// On an unsupported CPU/layout, retain the single-token cold kernels.
26711pub(crate) fn moe_cold_experts_rows_cpu(
26712    jobs: &[Vec<(&DenseFfn, f32)>],
26713    xs: &[f32],
26714    hidden: usize,
26715    pool: Option<&Pool>,
26716) -> Vec<f32> {
26717    let mut out = vec![0.0; xs.len()];
26718    let mut experts: Vec<&DenseFfn> = Vec::new();
26719    let mut groups: Vec<Vec<usize>> = Vec::new();
26720    let mut terms = vec![Vec::new(); jobs.len()];
26721    for (r, row) in jobs.iter().enumerate() {
26722        for &(e, w) in row {
26723            let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
26724                Some(g) => g,
26725                None => {
26726                    experts.push(e);
26727                    groups.push(Vec::new());
26728                    groups.len() - 1
26729                }
26730            };
26731            terms[r].push((g, groups[g].len(), w));
26732            groups[g].push(r);
26733        }
26734    }
26735    if experts.is_empty() {
26736        return out;
26737    }
26738    let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
26739    let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
26740    let lens: Vec<_> = groups.iter().map(Vec::len).collect();
26741    let count: usize = lens.iter().sum();
26742    let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
26743    let mut ds = vec![vec![0.0; hidden]; count];
26744    if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
26745        && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
26746    {
26747        let mut offset = 0;
26748        let offsets: Vec<_> = lens
26749            .iter()
26750            .map(|&n| {
26751                let start = offset;
26752                offset += n;
26753                start
26754            })
26755            .collect();
26756        for (r, terms) in terms.iter().enumerate() {
26757            for &(g, slot, w) in terms {
26758                for (o, &v) in out[r * hidden..(r + 1) * hidden]
26759                    .iter_mut()
26760                    .zip(&ds[offsets[g] + slot])
26761                {
26762                    *o += w * v;
26763                }
26764            }
26765        }
26766    } else {
26767        for (r, jobs) in jobs.iter().enumerate() {
26768            if !jobs.is_empty() {
26769                let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
26770                out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
26771                attention::recycle_buf(&mut row);
26772            }
26773        }
26774    }
26775    out
26776}
26777
26778/// The pure-CPU MoE expert loop (also the fallback of every GPU refusal).
26779fn moe_ffn_cpu(
26780    m: &MoeFfn,
26781    x: &[f32],
26782    idx: &[usize],
26783    p: &[f32],
26784    wsum: f32,
26785    pool: Option<&Pool>,
26786) -> Vec<f32> {
26787    if let Some(mut out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
26788        for &e in idx {
26789            let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
26790            expert_tubes_add(&m.experts[e], x, w, &mut out, pool);
26791        }
26792        shared_tubes_add(m, x, &mut out, pool);
26793        return out;
26794    }
26795    let mut out = attention::take_buf(x.len());
26796    for &e in idx {
26797        let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
26798        MOE_PROBE.with(|pr| {
26799            if let Some(pr) = pr.borrow_mut().as_mut() {
26800                pr.cur = Some((e, w));
26801            }
26802        });
26803        let mut eo = dense_ffn(&m.experts[e], x, pool);
26804        MOE_PROBE.with(|pr| {
26805            if let Some(pr) = pr.borrow_mut().as_mut() {
26806                pr.cur = None;
26807            }
26808        });
26809        for i in 0..out.len() {
26810            out[i] += w * eo[i];
26811        }
26812        attention::recycle_buf(&mut eo);
26813        expert_tubes_add(&m.experts[e], x, w, &mut out, pool);
26814    }
26815    if let Some((se, gate)) = &m.shared {
26816        let mut so = dense_ffn(se, x, pool);
26817        let g = gate.as_ref().map_or(1.0, |gate| {
26818            let mut gl = [0.0f32; 1];
26819            gate.matvec(x, &mut gl, pool);
26820            1.0 / (1.0 + (-gl[0]).exp())
26821        });
26822        for i in 0..out.len() {
26823            out[i] += g * so[i];
26824        }
26825        attention::recycle_buf(&mut so);
26826        expert_tubes_add(se, x, g, &mut out, pool);
26827    }
26828    out
26829}
26830
26831/// DeepSeek-V2 MLA forward, expand-to-MHA form (see `AttnKind::Mla`):
26832/// per token the latent expands to every head's K/V and the ordinary
26833/// cache + grouped attend do the rest. K head layout is [rope | nope]
26834/// (rotary_dim = qk_rope rotates the shared rope key and each q head's
26835/// prefix); V rows are zero-padded to the K head_dim inside the cache
26836/// and the pad is sliced off before O. Attention importance is not
26837/// accumulated for MLA yet (no eviction interplay).
26838#[allow(clippy::too_many_arguments)]
26839pub(crate) fn mla_attention(
26840    w: &MlaWeights,
26841    normed: &[f32],
26842    cache: &mut crate::kv_cache::LayerKvCache,
26843    position: usize,
26844    inv_freq: &[f32],
26845    rope_scale: f32,
26846    eps: f64,
26847    pool: Option<&Pool>,
26848) -> Vec<f32> {
26849    let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
26850    let hd = dr + dn;
26851    let mut q = vec![0.0f32; nh * hd];
26852    match (&w.q_a, &w.q_a_norm) {
26853        (Some(qa), Some(qn)) => {
26854            let mut t = vec![0.0f32; qa.rows()];
26855            qa.matvec(normed, &mut t, pool);
26856            let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
26857            w.q_proj.matvec(&tn, &mut q, pool);
26858        }
26859        _ => w.q_proj.matvec(normed, &mut q, pool),
26860    }
26861    let mut ca = vec![0.0f32; lora + dr];
26862    w.kv_a.matvec(normed, &mut ca, pool);
26863    let (c_lat, k_rope) = ca.split_at_mut(lora);
26864    let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
26865    let mut kvb = vec![0.0f32; nh * (dn + dv)];
26866    w.kv_b.matvec(&latn, &mut kvb, pool);
26867    if !w.nope {
26868        attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
26869    }
26870    for h in 0..nh {
26871        if !w.nope {
26872            attention::rope_rotate_scaled(
26873                &mut q[h * hd..h * hd + dr],
26874                position,
26875                inv_freq,
26876                rope_scale,
26877            );
26878        }
26879    }
26880    let mut k = vec![0.0f32; nh * hd];
26881    let mut v = vec![0.0f32; nh * hd];
26882    for h in 0..nh {
26883        k[h * hd..h * hd + dr].copy_from_slice(k_rope);
26884        k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
26885        v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
26886    }
26887    cache.append(&k, &v, &vec![true; nh]);
26888    let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
26889    attention::recycle_buf(&mut imp);
26890    let mut ov = vec![0.0f32; nh * dv];
26891    for h in 0..nh {
26892        ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
26893    }
26894    let mut out = vec![0.0f32; w.o_proj.rows()];
26895    w.o_proj.matvec(&ov, &mut out, pool);
26896    out
26897}
26898
26899/// Gemma-4 dual-branch FFN (spec: see `FfnKind::DenseMoe`). The dense
26900/// branch reads the pre-FFN-normed activation; the router and the
26901/// expert branch read the RAW residual — the router through a
26902/// scale-less rms norm (its constant gain is folded into the weights),
26903/// the experts through `pre_norm_2`. CPU path; GPU graphs refuse the
26904/// layer kind honestly.
26905fn dense_moe_ffn(
26906    dm: &DenseMoeFfn,
26907    x_normed: &[f32],
26908    h_raw: &[f32],
26909    eps: f64,
26910    norm_style: NormStyle,
26911    pool: Option<&Pool>,
26912) -> Vec<f32> {
26913    let mut d = dense_ffn(&dm.dense, x_normed, pool);
26914    d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
26915    let m = &dm.moe;
26916    let ne = m.experts.len();
26917    let mut logits = vec![0.0f32; ne];
26918    if m.router_input_norm {
26919        let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
26920        let inv = 1.0 / (ss + eps as f32).sqrt();
26921        let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
26922        m.router.matvec(&xr, &mut logits, pool);
26923    } else {
26924        m.router.matvec(h_raw, &mut logits, pool);
26925    }
26926    let (idx, p, wsum) = moe_route(&logits, m, None);
26927    {
26928        let mut st = m.stats.borrow_mut();
26929        if st.len() < ne {
26930            st.resize(ne, 0);
26931        }
26932        for &e in &idx {
26933            st[e] += 1;
26934        }
26935    }
26936    let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
26937    let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
26938    let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
26939    for (di, mi) in d.iter_mut().zip(&mo) {
26940        *di += mi;
26941    }
26942    d
26943}
26944
26945/// Building the MoE-layer GPU jobs: all selected experts (+shared) must
26946/// be q8_2f-Mapped from the primary mapping; otherwise None → CPU path.
26947/// One-shot report of why the MoE GPU block refused. A silent `?` here
26948/// sends every expert to the CPU with nothing in the logs to say so —
26949/// which is exactly how a q4tp MoE model looked "GPU-accelerated" while
26950/// running entirely on the host.
26951fn moe_gpu_refused(why: &'static str) {
26952    use std::sync::atomic::{AtomicBool, Ordering};
26953    static SAID: AtomicBool = AtomicBool::new(false);
26954    if !SAID.swap(true, Ordering::Relaxed) {
26955        tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
26956    }
26957}
26958
26959fn moe_ffn_gpu(
26960    m: &MoeFfn,
26961    x: &[f32],
26962    idx: &[usize],
26963    p: &[f32],
26964    wsum: f32,
26965    pool: Option<&Pool>,
26966) -> Option<Vec<f32>> {
26967    use crate::gpu::MoeJob;
26968
26969    let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
26970    let mut model_ref = None;
26971    for &e in idx {
26972        if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
26973            moe_gpu_refused("push_job(expert)");
26974            return None;
26975        }
26976    }
26977    if let Some((se, gate)) = &m.shared {
26978        let g = gate.as_ref().map_or(1.0, |gate| {
26979            let mut gl = [0.0f32; 1];
26980            gate.matvec(x, &mut gl, pool);
26981            1.0 / (1.0 + (-gl[0]).exp())
26982        });
26983        if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
26984            moe_gpu_refused("push_job(shared)");
26985            return None;
26986        }
26987    }
26988    let Some(model) = model_ref else {
26989        moe_gpu_refused("no model_ref");
26990        return None;
26991    };
26992    let hidden = jobs[0].down.1;
26993    let mut out = vec![0.0f32; hidden];
26994    if crate::gpu::moe_block(&model, &jobs, &mut out) {
26995        // The block knows no tubes (`hot-bake --experts`): the winners' and the
26996        // shared expert's come here, as the CPU paths add them.
26997        for &e in idx {
26998            expert_tubes_add(&m.experts[e], x, p[e] / wsum, &mut out, pool);
26999        }
27000        shared_tubes_add(m, x, &mut out, pool);
27001        Some(out)
27002    } else {
27003        moe_gpu_refused("gpu::moe_block");
27004        None
27005    }
27006}
27007
27008/// Single-position FFN dispatch.
27009fn ffn_forward(
27010    ffn: &FfnKind,
27011    x: &[f32],
27012    pool: Option<&Pool>,
27013    experts_allowed: Option<&[bool]>,
27014) -> Vec<f32> {
27015    match ffn {
27016        FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
27017        FfnKind::Dense(d) => dense_ffn(d, x, pool),
27018        FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
27019        // Dual-branch layers need the raw residual — their callers
27020        // dispatch dense_moe_ffn directly; the auxiliary paths that land
27021        // here (MTP draft, o1 replay) do not co-occur with gemma-4 MoE.
27022        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
27023    }
27024}
27025
27026/// Fused two-position FFN: gate/up/down streamed once (dense). MoE
27027/// falls back to two singles — expert sets differ per position, there
27028/// is nothing to fuse.
27029fn ffn_forward_pair(
27030    ffn: &FfnKind,
27031    x1: &[f32],
27032    x2: &[f32],
27033    pool: Option<&Pool>,
27034    experts_allowed: Option<&[bool]>,
27035) -> (Vec<f32>, Vec<f32>) {
27036    let d = match ffn {
27037        // A tube layer has nothing to fuse across the pair — the tubes
27038        // are separate matrices; two singles are the honest path.
27039        FfnKind::Dense(d) if !d.segs.is_empty() => {
27040            return (
27041                tube_ffn(d, x1, 1, pool, None),
27042                tube_ffn(d, x2, 1, pool, None),
27043            );
27044        }
27045        FfnKind::Dense(d) => d,
27046        FfnKind::Moe(m) => {
27047            return (
27048                moe_ffn(m, x1, pool, experts_allowed),
27049                moe_ffn(m, x2, pool, experts_allowed),
27050            );
27051        }
27052        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
27053    };
27054    let inter = d.gate_proj.rows();
27055    FFN_SCRATCH.with(|s| {
27056        let mut s = s.borrow_mut();
27057        let [g1, g2, u1, u2] = &mut *s;
27058        g1.resize(inter, 0.0);
27059        g2.resize(inter, 0.0);
27060        u1.resize(inter, 0.0);
27061        u2.resize(inter, 0.0);
27062        // Multi-matrix pair job: gate+up under one pool dispatch
27063        // (o1s = lane-1 outputs across tensors, o2s = lane-2).
27064        QTensor::matvec2_many(
27065            [&d.gate_proj, &d.up_proj],
27066            x1,
27067            x2,
27068            [g1.as_mut_slice(), u1.as_mut_slice()],
27069            [g2.as_mut_slice(), u2.as_mut_slice()],
27070            pool,
27071        );
27072        for i in 0..inter {
27073            g1[i] = d.act.combine(g1[i], u1[i]);
27074            g2[i] = d.act.combine(g2[i], u2[i]);
27075        }
27076        let mut o1 = attention::take_buf(d.down_proj.rows());
27077        let mut o2 = attention::take_buf(d.down_proj.rows());
27078        d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
27079        (o1, o2)
27080    })
27081}
27082
27083#[cfg(test)]
27084mod tests {
27085    use std::ffi::OsString;
27086    use std::sync::Mutex;
27087
27088    // Environment switches are process-global. Keep the speculative-path
27089    // tests serialized and restore every prior value even during unwinding.
27090    static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
27091
27092    struct ScopedTestEnv {
27093        previous: Vec<(&'static str, Option<OsString>)>,
27094    }
27095
27096    impl ScopedTestEnv {
27097        fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
27098            let previous = vars
27099                .iter()
27100                .map(|(name, _)| (*name, std::env::var_os(name)))
27101                .collect();
27102            for &(name, value) in vars {
27103                unsafe {
27104                    match value {
27105                        Some(value) => std::env::set_var(name, value),
27106                        None => std::env::remove_var(name),
27107                    }
27108                }
27109            }
27110            Self { previous }
27111        }
27112    }
27113
27114    impl Drop for ScopedTestEnv {
27115        fn drop(&mut self) {
27116            for (name, value) in self.previous.drain(..).rev() {
27117                unsafe {
27118                    match value {
27119                        Some(value) => std::env::set_var(name, value),
27120                        None => std::env::remove_var(name),
27121                    }
27122                }
27123            }
27124        }
27125    }
27126
27127    #[test]
27128    fn tail_repeat_counts_the_copied_stretch() {
27129        assert_eq!(tail_repeat(&[]), 0);
27130        assert_eq!(tail_repeat(&[5]), 0);
27131        assert_eq!(tail_repeat(&[1, 2, 3, 4]), 0);
27132        // the last three repeat the first three
27133        assert_eq!(tail_repeat(&[1, 2, 3, 9, 1, 2, 3]), 3);
27134        // the longest repeat wins over a later, shorter one
27135        assert_eq!(tail_repeat(&[1, 2, 3, 4, 8, 3, 4, 7, 1, 2, 3, 4]), 4);
27136        // a repeat reaching the start of the sequence
27137        assert_eq!(tail_repeat(&[1, 2, 1, 2]), 2);
27138        let long: Vec<u32> = (0..40).chain(0..40).collect();
27139        assert_eq!(tail_repeat(&long), 32);
27140    }
27141
27142    /// The chain's depth on wgpu follows the device's own round prices: a
27143    /// card whose verify climbs with its rows comes back from five drafts
27144    /// to three on the same acceptance that keeps a flat card at five.
27145    #[test]
27146    fn wgpu_spec_depth_follows_the_round_prices() {
27147        // RTX 2000 Ada, Qwen3.8-27B Turbo: rounds of 2..5 drafts, ms
27148        let steep = [0.0, 100.5, 111.3, 126.3, 144.6, 0.0, 0.0];
27149        let hold = [0.90, 0.85, 0.80, 0.60, 0.60, 0.60, 0.60];
27150        assert_eq!(wgpu_spec_depth(&hold, &steep, 5, 2, 5), Some(3));
27151        // the depth in turn stays while no other is 4 % better
27152        assert_eq!(wgpu_spec_depth(&hold, &steep, 4, 2, 5), Some(4));
27153        let flat = [0.0, 0.0, 60.0, 62.0, 64.0, 0.0, 0.0];
27154        assert_eq!(wgpu_spec_depth(&[0.9; 7], &flat, 3, 2, 5), Some(5));
27155        // an untimed depth is priced on the line through the two nearest
27156        let two = [0.0, 0.0, 111.3, 126.3, 0.0, 0.0, 0.0];
27157        assert_eq!(wgpu_spec_depth(&[0.5; 7], &two, 4, 2, 5), Some(2));
27158        // one timed depth says nothing of the slope
27159        assert_eq!(wgpu_spec_depth(&hold, &[0.0, 0.0, 111.3, 0.0, 0.0, 0.0, 0.0], 3, 2, 5), None);
27160    }
27161
27162    #[test]
27163    fn dflash_round_values_price_a_round_of_each_drafter() {
27164        // the block holds all seven of its drafts, the chain its first only
27165        let after: Vec<u32> = (10..30).collect();
27166        let seq: Vec<u32> = (100..140).collect();
27167        let block: Vec<u32> = (10..17).collect();
27168        let chain: Vec<u32> = vec![10, 99, 99, 99, 99, 99, 99];
27169        let starts: Vec<DflashStart<'_>> =
27170            (0..40).map(|_| DflashStart { seq: &seq, after: &after, block: &block, chain: &chain }).collect();
27171        let v = dflash_round_values(&starts);
27172        let (b, c) = v[39];
27173        // a block seven deep: eight tokens for a round of seven drafts and the block's own price
27174        assert_eq!(b, (8.0, METAL_ROUND_COST[6] + DFLASH_ROUND_DRAFT));
27175        // the chain has settled at one draft, which holds
27176        assert_eq!(c, (2.0, METAL_ROUND_COST[0]));
27177        assert!(b.0 / b.1 > c.0 / c.1);
27178    }
27179
27180    #[test]
27181    fn lookup_continues_a_repeated_stretch() {
27182        // 1 2 3 4 5 6 7 | 9 9 | 2 3 4 → the stretch 2 3 4 was followed by 5 6 7
27183        let seq = [1, 2, 3, 4, 5, 6, 7, 9, 9, 2, 3, 4];
27184        assert_eq!(lookup_continuation(&seq, 3, 3), &[5, 6, 7]);
27185        assert_eq!(lookup_continuation(&seq, 3, 2), &[5, 6]);
27186        // a repeat shorter than asked gives nothing
27187        assert!(lookup_continuation(&seq, 4, 3).is_empty());
27188        // the longest repeat wins over a later, shorter one
27189        let seq = [5, 2, 3, 4, 8, 0, 3, 4, 7, 0, 5, 2, 3, 4];
27190        assert_eq!(lookup_continuation(&seq, 2, 1), &[8]);
27191        // of equal ones the latest
27192        let seq = [2, 3, 4, 8, 0, 2, 3, 4, 7, 0, 2, 3, 4];
27193        assert_eq!(lookup_continuation(&seq, 2, 1), &[7]);
27194        // a period continues through the tail itself
27195        let seq = [1, 2, 1, 2, 1, 2];
27196        assert_eq!(lookup_continuation(&seq, 3, 4), &[1, 2]);
27197        assert!(lookup_continuation(&[1, 2, 3], 3, 2).is_empty());
27198        assert!(lookup_continuation(&[], 1, 2).is_empty());
27199    }
27200
27201    #[test]
27202    fn metal_spec_depth_follows_what_the_chain_holds() {
27203        // before any round: two drafts
27204        assert_eq!(metal_spec_depth(&SPEC_HOLD_PRIOR), 2);
27205        // a second draft that seldom holds is not worth its verify
27206        assert_eq!(metal_spec_depth(&[0.6, 0.15, 0.5, 0.5, 0.5, 0.5, 0.5]), 1);
27207        // predictable text fills the GEMM tile
27208        assert_eq!(metal_spec_depth(&[0.97; 7]), 7);
27209        // rounds of two drafts that both hold reach for a longer chain
27210        let mut h = SPEC_HOLD_PRIOR;
27211        let mut k = metal_spec_depth(&h);
27212        let mut rounds = 0;
27213        while k == 2 && rounds < 200 {
27214            spec_hold_update(&mut h, k, 2);
27215            k = metal_spec_depth(&h);
27216            rounds += 1;
27217        }
27218        assert!(k > 2 && rounds < 60, "depth {k} after {rounds} rounds");
27219        // ... and come back once the longer chain shows that a third never holds
27220        let mut back = 0;
27221        while k > 2 && back < 200 {
27222            spec_hold_update(&mut h, k, 2);
27223            k = metal_spec_depth(&h);
27224            back += 1;
27225        }
27226        assert!(k == 2 && back < 10, "depth {k} after {back} rounds");
27227        // most rounds of such text stay at two drafts
27228        let mut deep = 0;
27229        for _ in 0..400 {
27230            let k = metal_spec_depth(&h);
27231            deep += (k > 2) as usize;
27232            spec_hold_update(&mut h, k, 2);
27233        }
27234        assert!(deep < 40, "{deep} of 400 rounds went deeper");
27235        // a chain that holds one draft in two rounds of three, two in one of three: never deeper
27236        let mut h = SPEC_HOLD_PRIOR;
27237        for i in 0..300 {
27238            let k = metal_spec_depth(&h);
27239            assert!(k <= 2, "depth {k} at round {i}");
27240            spec_hold_update(&mut h, k, (i % 3).min(k));
27241        }
27242    }
27243
27244    #[test]
27245    fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
27246        use super::pad_mtp_draft_logits;
27247
27248        let mut logits = vec![-3.0, -1.0, -2.0];
27249        pad_mtp_draft_logits(&mut logits, 6);
27250        assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
27251        assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
27252        assert_eq!(
27253            logits
27254                .iter()
27255                .enumerate()
27256                .max_by(|(_, a), (_, b)| a.total_cmp(b))
27257                .map(|(i, _)| i),
27258            Some(1),
27259            "an unevaluated row cannot win an all-negative shortlist"
27260        );
27261    }
27262
27263    #[test]
27264    fn speculative_trial_fast_exits_after_three_zero_acceptance_rounds() {
27265        use super::{Pipeline, SpecMon, SpecTrial};
27266
27267        let mut mon = SpecMon::default();
27268        let start = std::time::Instant::now();
27269        mon.round(1.0, 1);
27270        let first = Pipeline::spec_trial_round(
27271            SpecTrial::Spec {
27272                t0: start,
27273                gen0: 0,
27274                rounds: 0,
27275            },
27276            &mut mon,
27277            1,
27278        );
27279        assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
27280
27281        // Two empty rounds in a row happen to a good head: the trial goes on.
27282        mon.round(1.0, 1);
27283        let second = Pipeline::spec_trial_round(first, &mut mon, 2);
27284        assert!(
27285            matches!(second, SpecTrial::Spec { rounds: 2, .. }),
27286            "two empty rounds are not evidence enough to leave the drafts"
27287        );
27288        mon.round(1.0, 1);
27289        let third = Pipeline::spec_trial_round(second, &mut mon, 3);
27290        assert!(
27291            matches!(third, SpecTrial::Plain { gen0: 3, .. }),
27292            "three verifier-tip-only rounds must stop the losing MTP trial"
27293        );
27294        // A plain decision on three rounds is tried again soon.
27295        assert_eq!(mon.recheck_plain_at(3), 3 + super::SPEC_RECHECK_THIN);
27296
27297        // One successful draft resets the streak, so a viable head retains
27298        // the normal five-round measurement budget.
27299        let mut viable = SpecMon::default();
27300        viable.round(1.0, 1);
27301        viable.round(1.0, 2);
27302        let continued = Pipeline::spec_trial_round(
27303            SpecTrial::Spec {
27304                t0: start,
27305                gen0: 0,
27306                rounds: 1,
27307            },
27308            &mut viable,
27309            3,
27310        );
27311        assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
27312    }
27313
27314    #[test]
27315    fn fresh_mtp_request_does_not_inherit_full_head_streak() {
27316        let mut p = spark_test_pipeline(None);
27317        let head_rows = 248_070;
27318        p.draft_full_streak = 7;
27319        assert_eq!(p.draft_head_rows(head_rows), head_rows);
27320
27321        p.reset_fresh_mtp_draft_state();
27322        assert_eq!(
27323            p.draft_head_rows(head_rows),
27324            super::Pipeline::draft_vocab_rows(head_rows),
27325            "a fresh request returns to its configured shortlist"
27326        );
27327
27328        // A high-id first token of the NEW request still restores the
27329        // full-head protection immediately.
27330        p.note_draft_id(u32::MAX);
27331        assert_eq!(p.draft_head_rows(head_rows), head_rows);
27332    }
27333
27334    #[test]
27335    fn native_metal_batch_spec_defaults_on_over_a_q4tp_trunk() {
27336        use super::graph_spec_wanted_by_policy;
27337
27338        // A default-eligible (q4tp trunk) native Metal model drafts unless
27339        // told not to; any value other than 0/1 means the default there.
27340        assert!(graph_spec_wanted_by_policy(None, true, false, false, true));
27341        assert!(graph_spec_wanted_by_policy(
27342            Some("anything-but-1"),
27343            true,
27344            false,
27345            false,
27346            true,
27347        ));
27348        // A trunk outside the default (not q4tp) stays plain until asked.
27349        assert!(!graph_spec_wanted_by_policy(None, false, false, false, true));
27350        assert!(!graph_spec_wanted_by_policy(
27351            Some("anything-but-1"),
27352            false,
27353            false,
27354            false,
27355            true,
27356        ));
27357        assert!(graph_spec_wanted_by_policy(
27358            Some("1"),
27359            false,
27360            true,
27361            false,
27362            true,
27363        ));
27364        assert!(!graph_spec_wanted_by_policy(
27365            Some("0"),
27366            true,
27367            false,
27368            false,
27369            true,
27370        ));
27371
27372        // Do not regress the established discrete/wgpu policy while closing
27373        // the native-Metal quality gate.
27374        assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
27375        assert!(graph_spec_wanted_by_policy(
27376            Some("legacy-force"),
27377            false,
27378            true,
27379            true,
27380            false,
27381        ));
27382    }
27383
27384    #[test]
27385    fn encoder_hidden_states_are_normalized_and_reusable() {
27386        let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
27387        let ids = [1, 7, 11, 19];
27388
27389        let first = p.encode_hidden_states(&ids).expect("encoder forward");
27390        let second = p
27391            .encode_hidden_states(&ids)
27392            .expect("reusable encoder forward");
27393        assert_eq!(first.len(), ids.len() * 8);
27394        assert!(first.iter().all(|v| v.is_finite()));
27395        assert_eq!(
27396            first, second,
27397            "encoder forward must start from a fresh state"
27398        );
27399
27400        // An encoder call must not leave stale KV/GDN state behind for the
27401        // regular generative API sharing the same Pipeline implementation.
27402        assert!(p.forward_ids(&ids, None).is_ok());
27403    }
27404
27405    /// The 0.7.6 prefill-chunk rule: a plain dense stack wholly on a
27406    /// discrete card reads the prompt in wide chunks on x86; every other
27407    /// case keeps the width it had (the GDN-hybrid, MoE and DeepSeek paths
27408    /// were tuned on hardware not measured for this change).
27409    #[test]
27410    fn prefill_chunk_rule_widens_only_dense_on_discrete() {
27411        use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
27412        let dense_card = ChunkStackFacts {
27413            plain_dense: true,
27414            discrete: true,
27415            gpu_on: true,
27416            ..Default::default()
27417        };
27418        assert!(dense_card.dense_on_discrete());
27419        // The bug: a dense Llama on a Vulkan RTX 3090 got 48.
27420        assert_eq!(
27421            prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
27422            DISCRETE_DENSE_PREFILL_CHUNK
27423        );
27424        assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
27425        for (label, facts) in [
27426            (
27427                "GDN hybrid / MoE / DeepSeek stack",
27428                ChunkStackFacts {
27429                    plain_dense: false,
27430                    ..dense_card
27431                },
27432            ),
27433            (
27434                "integrated GPU",
27435                ChunkStackFacts {
27436                    discrete: false,
27437                    ..dense_card
27438                },
27439            ),
27440            (
27441                "CPU only",
27442                ChunkStackFacts {
27443                    gpu_on: false,
27444                    discrete: false,
27445                    ..dense_card
27446                },
27447            ),
27448            (
27449                "capacity split",
27450                ChunkStackFacts {
27451                    capacity_split: true,
27452                    ..dense_card
27453                },
27454            ),
27455            (
27456                "multi-GPU plan",
27457                ChunkStackFacts {
27458                    multi_gpu: true,
27459                    ..dense_card
27460                },
27461            ),
27462            (
27463                "O(1) layers",
27464                ChunkStackFacts {
27465                    o1: true,
27466                    ..dense_card
27467                },
27468            ),
27469        ] {
27470            assert!(!facts.dense_on_discrete(), "{label}");
27471            assert_eq!(
27472                prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
27473                48,
27474                "{label} keeps the historical x86 chunk"
27475            );
27476        }
27477        // Other hosts are untouched whatever the model.
27478        for dense in [false, true] {
27479            assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
27480            assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
27481        }
27482        // CMF_PREFILL_CHUNK still wins everywhere (and is clamped to ≥ 1).
27483        for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
27484            for dense in [false, true] {
27485                assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
27486                assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
27487            }
27488        }
27489    }
27490
27491    #[test]
27492    fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
27493        use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
27494        let full = |host_rows, device_rows| ReuseLayer {
27495            full: true,
27496            host_rows,
27497            device_rows,
27498            device_state: false,
27499        };
27500        // Turn 1: 300-token prompt prefilled on the host, 40 tokens decoded
27501        // by the wgpu graph into the device mirror only. Turn 2 reuses 339.
27502        assert_eq!(
27503            kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
27504            ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
27505        );
27506        // CPU / Metal: the host owner already holds every forwarded row.
27507        assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
27508        // A mirror past the prefix is fine for the host (it gets rewound).
27509        assert_eq!(
27510            kv_reuse_plan(339, &[full(339, Some(345))]),
27511            ReusePlan::Ready
27512        );
27513        // GPU prefix / CPU tail: only the device layers lag.
27514        assert_eq!(
27515            kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
27516            ReusePlan::Pull(vec![(0, 300, 339)])
27517        );
27518        // The device cannot supply the missing rows: never continue.
27519        assert_eq!(
27520            kv_reuse_plan(339, &[full(300, Some(320))]),
27521            ReusePlan::Fresh
27522        );
27523        assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
27524        assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
27525        // A recurrent state advanced on the device cannot be handed to a
27526        // host prefill (it is not rewindable and the host copy is stale).
27527        let conv = |device_state| ReuseLayer {
27528            full: false,
27529            host_rows: 0,
27530            device_rows: None,
27531            device_state,
27532        };
27533        assert_eq!(
27534            kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
27535            ReusePlan::Fresh
27536        );
27537        assert_eq!(
27538            kv_reuse_plan(339, &[conv(false), full(339, None)]),
27539            ReusePlan::Ready
27540        );
27541    }
27542
27543    #[test]
27544    fn nll_graph_policy_scopes_only_the_fused_head() {
27545        for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
27546            // A Vulkan/Wgpu hidden-only graph remains the quality route.
27547            ("vulkan graph", true, true, false, true, false),
27548            // Native Metal adds the strict fused graph-head contract.
27549            ("native Metal graph", true, true, true, true, true),
27550            // Masked NLL and the explicit non-graph fallback remain unchanged.
27551            ("masked", false, true, false, false, false),
27552            ("graph disabled", true, false, true, false, false),
27553        ] {
27554            let (graph_quality, graph_head_required) =
27555                super::nll_graph_policy(unmasked, prefer_graph, native_metal);
27556            assert_eq!(graph_quality, want_graph, "{label}: graph quality");
27557            assert_eq!(graph_head_required, want_head, "{label}: fused head");
27558        }
27559    }
27560
27561    #[test]
27562    fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
27563        assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
27564        assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
27565        assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
27566        assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
27567        assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
27568    }
27569
27570    #[test]
27571    fn cancel_flag_stops_generation() {
27572        let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
27573        // Set before the call: the prefill loops honour it, the run
27574        // returns immediately with the cancelled reason and no tokens.
27575        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
27576        let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
27577        assert_eq!(r.finish_reason, "cancelled");
27578        assert!(
27579            r.token_ids.is_empty(),
27580            "no tokens after cancel: {:?}",
27581            r.token_ids
27582        );
27583        assert_eq!(p.kv_cache.seq_len(), 0);
27584        assert!(p.kv_history.is_empty());
27585        assert!(!p.graph_want_logits);
27586        assert!(p.graph_logits.is_none());
27587        // Flag auto-cleared: the next call generates normally.
27588        let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
27589        assert_ne!(r2.finish_reason, "cancelled");
27590    }
27591    use super::*;
27592
27593    #[test]
27594    fn minimax_m3_arch_selects_checked_oai_activation_only() {
27595        // Keep this fixture minimal: `Act::from_arch_full` needs only the
27596        // architecture id and activation name, while serde supplies stable
27597        // defaults for unrelated CMF fields.
27598        let arch = |arch_name: &str, hidden_act: &str| {
27599            serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
27600                "arch_name": arch_name,
27601                "hidden_size": 8,
27602                "intermediate_size": 16,
27603                "num_layers": 1,
27604                "num_attention_heads": 1,
27605                "num_kv_heads": 1,
27606                "head_dim": 8,
27607                "vocab_size": 16,
27608                "layer_types": ["FullAttention"],
27609                "rms_norm_eps": 1e-6,
27610                "max_position_embeddings": 16,
27611                "hidden_act": hidden_act
27612            }))
27613            .expect("minimal architecture")
27614        };
27615        let minimax = arch("minimax_m3_vl", "swigluoai");
27616        assert_eq!(
27617            Act::from_arch_full(&minimax),
27618            Act::SwiGluOai {
27619                alpha: 1.702,
27620                limit: 7.0,
27621            }
27622        );
27623
27624        // The same spelling must not turn into MiniMax math on a generic
27625        // model.  Only the source-validated M3 architecture owns it.
27626        let unrelated = arch("qwen2", "swigluoai");
27627        assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
27628
27629        // Conversely, an M3-tagged header with a normal activation keeps the
27630        // normal path; this prevents the architecture id from overriding the
27631        // explicit serialized contract.
27632        let non_oai = arch("minimax_m3_vl", "gelu");
27633        assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
27634    }
27635
27636    #[test]
27637    fn minimax_swiglu_oai_act_matches_scalar_contract() {
27638        let act = Act::swiglu_oai(1.702, 7.0);
27639        let got = act.combine(10.0, -10.0);
27640        let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
27641        assert!((got - expected).abs() < 1e-6);
27642        assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
27643        assert_eq!(
27644            act.wgpu_graph_act(),
27645            Some(crate::gpu::GraphAct::SwiGluOaiM3),
27646            "the WGPU-only arm accepts only the frozen M3 constants"
27647        );
27648        assert_eq!(
27649            Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
27650            None,
27651            "nearby OAI constants must not borrow MiniMax's kernel"
27652        );
27653        assert_eq!(
27654            act.moe_job_graph_contract(),
27655            Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
27656            "the direct MoE path carries the frozen clamp with the marker"
27657        );
27658        assert_eq!(
27659            Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
27660            None,
27661            "a nearby clamp must not borrow MiniMax's device contract"
27662        );
27663
27664        // `apply` is the gate half only; `combine` owns the up clamp and
27665        // +1 offset. The two must not accidentally collapse to ordinary SiLU.
27666        let gate_only = act.apply(-10.0);
27667        let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
27668        assert!((gate_only - expected_gate).abs() < 1e-10);
27669    }
27670
27671    #[test]
27672    fn explicit_sliding_schedule_selects_local_rope_profile() {
27673        // Mellum alternates three local 1,024-token layers with one global
27674        // YaRN layer.  An explicit schedule deliberately overrides the
27675        // cadence sentinel in `swa`, so test both RoPE table and scale
27676        // dispatch rather than merely checking that the metadata loaded.
27677        let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
27678        p.swa = Some((1024, usize::MAX));
27679        p.sliding_layers = Some(vec![true, true, true, false]);
27680        p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
27681        p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
27682        p.rope_scale = 1.277_258_9;
27683        p.rope_scale_local = 1.0;
27684
27685        for li in 0..3 {
27686            assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
27687            assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
27688            assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
27689        }
27690        assert_eq!(p.layer_window(3), None);
27691        assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
27692        assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
27693    }
27694
27695    /// sparse_ffn_quant must equal a dense FFN where inactive neurons are
27696    /// zeroed (mask × mmap correctness). On F32 tensors this is EXACT —
27697    /// it validates the row_dot / add_col_scaled / scatter indexing, the
27698    /// bug-prone part. The q8 branches reuse the golden-tested linear
27699    /// The per-token sparse path reads a transposed `down`; it must
27700    /// agree with the arm that computes everything and zeroes the
27701    /// losers, or the speed measurement is measuring a different model.
27702    #[test]
27703    fn dynamic_ffn_equals_the_zeroing_arm() {
27704        let (hidden, inter) = (8usize, 32usize);
27705        let synth = |n: usize, salt: usize| -> Vec<f32> {
27706            (0..n)
27707                .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
27708                .collect()
27709        };
27710        let down = synth(hidden * inter, 3);
27711        let mut down_t = vec![0.0f32; inter * hidden];
27712        for r in 0..hidden {
27713            for c in 0..inter {
27714                down_t[c * hidden + r] = down[r * inter + c];
27715            }
27716        }
27717        let d = DenseFfn {
27718            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
27719            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
27720            down_proj: QTensor::from_f32(down.clone(), hidden, inter),
27721            act: Act::Silu,
27722            down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
27723            segs: Vec::new(),
27724        };
27725        let x = synth(hidden, 11);
27726        let k = 12usize;
27727        let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
27728        // Reference: full compute, keep the k loudest |silu(gate)|.
27729        let mut g = vec![0.0f32; inter];
27730        d.gate_proj.matvec(&x, &mut g, None);
27731        let mut u = vec![0.0f32; inter];
27732        d.up_proj.matvec(&x, &mut u, None);
27733        for v in g.iter_mut() {
27734            *v = inference::silu(*v);
27735        }
27736        keep_top_k(&mut g, k);
27737        for i in 0..inter {
27738            g[i] *= u[i];
27739        }
27740        let mut want = vec![0.0f32; hidden];
27741        d.down_proj.matvec(&g, &mut want, None);
27742        for (a, b) in want.iter().zip(&got) {
27743            assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
27744        }
27745    }
27746
27747    /// A tube layer is the same layer, re-cut. With every tube open the
27748    /// answer must equal the dense FFN over the concatenated neurons
27749    /// (the permutation is an identity on the layer's function); with a
27750    /// tube closed it must equal the dense FFN with those neurons
27751    /// zeroed — the mask semantics, now paid for in bytes not read.
27752    #[test]
27753    fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
27754        let (hidden, core, tube) = (8usize, 12usize, 8usize);
27755        let inter = core + tube;
27756        let synth = |n: usize, salt: usize| -> Vec<f32> {
27757            (0..n)
27758                .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
27759                .collect()
27760        };
27761        let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
27762        let d_all = synth(hidden * inter, 3);
27763        // The dense layer, and the same weights cut into core + tube.
27764        let dense = DenseFfn {
27765            gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
27766            up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
27767            down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
27768            act: Act::Silu,
27769            down_t: None,
27770            segs: Vec::new(),
27771        };
27772        let rows =
27773            |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
27774        let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
27775            let mut o = Vec::with_capacity(hidden * (b - a));
27776            for r in 0..hidden {
27777                o.extend_from_slice(&v[r * inter + a..r * inter + b]);
27778            }
27779            o
27780        };
27781        let tubed = DenseFfn {
27782            down_t: None,
27783            gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
27784            up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
27785            down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
27786            act: Act::Silu,
27787            segs: vec![FfnSeg {
27788                gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
27789                up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
27790                down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
27791                start: core,
27792                width: tube,
27793            }],
27794        };
27795        let x = synth(hidden, 7);
27796        let want = dense_ffn(&dense, &x, None);
27797        let got = tube_ffn(&tubed, &x, 1, None, None);
27798        for (a, b) in want.iter().zip(&got) {
27799            assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
27800        }
27801        // Closed tube: bits on for the core, off for the tube.
27802        let mut bits = vec![0u8; inter.div_ceil(8)];
27803        for n in 0..core {
27804            bits[n / 8] |= 1 << (n % 8);
27805        }
27806        let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
27807        let masked = dense_ffn_masked(&dense, &x, None, &bits);
27808        for (a, b) in masked.iter().zip(&closed) {
27809            assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
27810        }
27811        // The batched arm must agree with the single-position one.
27812        let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
27813        for (a, b) in closed.iter().zip(&batch) {
27814            assert_eq!(a, b, "batch arm disagrees with decode arm");
27815        }
27816    }
27817
27818    /// scale, structurally identical to the matvec kernels.
27819    #[test]
27820    fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
27821        let (hidden, inter) = (16usize, 40usize);
27822        let synth = |n: usize, salt: usize| -> Vec<f32> {
27823            (0..n)
27824                .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
27825                .collect()
27826        };
27827        let d = DenseFfn {
27828            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
27829            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
27830            down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
27831            act: Act::Silu,
27832            down_t: None,
27833            segs: Vec::new(),
27834        };
27835        let x = synth(hidden, 9);
27836        // Active = every 3rd neuron.
27837        let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
27838
27839        let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
27840
27841        // Reference: full dense FFN but g[i]=0 for inactive neurons.
27842        let mut g = vec![0.0f32; inter];
27843        d.gate_proj.matvec(&x, &mut g, None);
27844        let mut u = vec![0.0f32; inter];
27845        d.up_proj.matvec(&x, &mut u, None);
27846        let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
27847        for i in 0..inter {
27848            g[i] = if act_set.contains(&(i as u16)) {
27849                inference::silu(g[i]) * u[i]
27850            } else {
27851                0.0
27852            };
27853        }
27854        let mut reference = vec![0.0f32; hidden];
27855        d.down_proj.matvec(&g, &mut reference, None);
27856
27857        let max_d = sparse
27858            .iter()
27859            .zip(&reference)
27860            .map(|(a, b)| (a - b).abs())
27861            .fold(0.0f32, f32::max);
27862        assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
27863    }
27864
27865    /// Attach a synthetic MTP head. `inter` deliberately stays independent
27866    /// from the trunk width so calibration tests can catch a trunk-proxy bug.
27867    fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
27868        let (h, inter, heads, kv, hd) = (
27869            p.hidden_size,
27870            inter,
27871            p.num_heads,
27872            p.num_kv_heads,
27873            p.head_dim,
27874        );
27875        let synth = |n: usize, salt: usize| -> Vec<f32> {
27876            (0..n)
27877                .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
27878                .collect()
27879        };
27880        let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
27881            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
27882        };
27883        p.mtp = Some(MtpModule {
27884            enorm: vec![1.0; h],
27885            hnorm: vec![1.0; h],
27886            eh_proj: qt(h, 2 * h, 301),
27887            layer: LayerWeights {
27888                input_norm: vec![1.0; h],
27889                post_norm: vec![1.0; h],
27890                attn_out_norm: None,
27891                ffn_out_norm: None,
27892                layer_scale: None,
27893                ffn: FfnKind::Dense(DenseFfn {
27894                    gate_proj: qt(inter, h, 315),
27895                    up_proj: qt(inter, h, 316),
27896                    down_proj: qt(h, inter, 317),
27897                    act: Act::Silu,
27898                    down_t: None,
27899                    segs: Vec::new(),
27900                }),
27901                attn: AttnKind::Full {
27902                    bias: None,
27903                    wq: qt(heads * hd, h, 311),
27904                    wk: qt(kv * hd, h, 312),
27905                    wv: qt(kv * hd, h, 313),
27906                    wo: qt(h, heads * hd, 314),
27907                    q_norm: None,
27908                    k_norm: None,
27909                    output_gate: false,
27910                    softplus_gate: None,
27911                },
27912            },
27913            final_norm: vec![1.0; h],
27914            kv: crate::kv_cache::LayerKvCache::new(kv, hd),
27915        });
27916    }
27917
27918    /// Attach a synthetic MTP head with the trunk's conventional width.
27919    fn attach_test_mtp(p: &mut Pipeline) {
27920        attach_test_mtp_width(p, p.intermediate_size);
27921    }
27922
27923    #[test]
27924    fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
27925        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
27926        // This differs from the trunk FFN width (16). A result shaped like
27927        // the trunk probe would be a silent, dangerous compaction bug.
27928        attach_test_mtp_width(&mut p, 11);
27929        let ids = [3, 9, 4, 12, 7, 2];
27930        let trunk = p.probe_ffn_mass(&ids);
27931        assert_eq!(trunk[0].len(), p.intermediate_size);
27932
27933        let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
27934        assert_eq!(got.len(), 1);
27935        assert_eq!(got[0].mass.len(), 11);
27936        assert_eq!(got[0].squared_mass.len(), 11);
27937        assert_eq!(got[0].observed_positions, ids.len() - 1);
27938        assert!(got[0].mass.iter().any(|&v| v > 0.0));
27939        assert!(got[0]
27940            .mass
27941            .iter()
27942            .zip(&got[0].squared_mass)
27943            .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
27944        assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
27945        assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
27946
27947        // Force an error *after* the MTP attention has appended a draft KV
27948        // row. This proves the error path clears detached draft state, not
27949        // merely the easy invalid-token preflight branch.
27950        let h = p.hidden_size;
27951        let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
27952            panic!("synthetic MTP must be dense")
27953        };
27954        dense.segs.push(FfnSeg {
27955            gate: QTensor::from_f32(vec![0.0; h], 1, h),
27956            up: QTensor::from_f32(vec![0.0; h], 1, h),
27957            down: QTensor::from_f32(vec![0.0; h], h, 1),
27958            start: dense.gate_proj.rows(),
27959            width: 1,
27960        });
27961        let err = p.probe_mtp_ffn(&ids);
27962        assert!(err
27963            .unwrap_err()
27964            .contains("tube-segmented draft FFNs are not supported"));
27965        assert_eq!(p.kv_cache.seq_len(), 0);
27966        assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
27967    }
27968
27969    #[test]
27970    fn speculative_equals_vanilla_greedy() {
27971        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
27972        // Exercise the experimental host pair deterministically; the native
27973        // and wgpu batch graphs have independent coverage.
27974        let _env = ScopedTestEnv::set(&[
27975            ("CMF_GPU", Some("0")),
27976            ("CMF_GPU_WGPU_GRAPH", Some("0")),
27977            ("CMF_MTP_PAIR", Some("1")),
27978        ]);
27979        let run = |spec: bool| {
27980            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
27981            p.sampler_config.temperature = 0.0;
27982            attach_test_mtp(&mut p);
27983            p.speculative = spec;
27984            let r = p.generate("abcdef", 12, None, None).unwrap();
27985            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
27986        };
27987        let (vanilla, d0, _) = run(false);
27988        let (spec, d1, a1) = run(true);
27989        assert_eq!(d0, 0, "vanilla path must not draft");
27990        assert!(d1 > 0, "speculative path must draft");
27991        assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
27992        assert_eq!(
27993            vanilla, spec,
27994            "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
27995        );
27996    }
27997
27998    #[test]
27999    fn speculative_accepts_constant_oracle() {
28000        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
28001        let _env = ScopedTestEnv::set(&[
28002            ("CMF_GPU", Some("0")),
28003            ("CMF_GPU_WGPU_GRAPH", Some("0")),
28004            ("CMF_MTP_PAIR", Some("1")),
28005        ]);
28006        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
28007        p.sampler_config.temperature = 0.0;
28008        p.sampler_config.repetition_penalty = 1.0;
28009        // Constant lm_head → every logit equal → both the main model and
28010        // the draft head argmax to token 0: acceptance must be 100%.
28011        p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
28012        attach_test_mtp(&mut p);
28013        p.speculative = true;
28014        let r = p.generate("abcd", 10, None, None).unwrap();
28015        assert!(r.mtp_drafted > 0);
28016        assert_eq!(
28017            r.mtp_accepted, r.mtp_drafted,
28018            "constant logits → every draft accepted"
28019        );
28020        // Ties resolve to the same token in both the main and draft
28021        // heads — the sequence is one repeated token.
28022        assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
28023    }
28024
28025    #[test]
28026    fn host_fused_mtp_pair_is_opt_in() {
28027        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
28028        let _env = ScopedTestEnv::set(&[
28029            ("CMF_GPU", Some("0")),
28030            ("CMF_GPU_WGPU_GRAPH", Some("0")),
28031            ("CMF_MTP_PAIR", None),
28032        ]);
28033        let run = |spec: bool| {
28034            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
28035            p.sampler_config.temperature = 0.0;
28036            attach_test_mtp(&mut p);
28037            p.speculative = spec;
28038            let r = p.generate("abcdef", 12, None, None).unwrap();
28039            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
28040        };
28041        let (plain, d0, _) = run(false);
28042        let (guarded, d1, a1) = run(true);
28043        assert_eq!(d0, 0);
28044        assert_eq!(d1, 0, "host MTP pair must remain opt-in");
28045        assert_eq!(a1, 0);
28046        assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
28047    }
28048
28049    #[test]
28050    fn empty_prompt_is_an_error_not_a_panic() {
28051        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
28052        let r = p.generate("", 4, None, None);
28053        assert!(r.is_err(), "empty prompt must be a clean error");
28054    }
28055
28056    #[test]
28057    fn every_token_enters_kv_exactly_once() {
28058        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
28059        // Greedy so no RNG variance; byte tokenizer → 3 prompt tokens.
28060        p.sampler_config.temperature = 0.0;
28061        let r = p.generate("abc", 2, None, None).unwrap();
28062        assert_eq!(r.prompt_tokens, 3);
28063        // prompt(3) + first sampled token forwarded before second logits:
28064        // step0 samples from prefill hidden (no extra forward), then
28065        // forwards t1 → cache 4; step1 samples, loop ends (max_tokens).
28066        assert_eq!(
28067            p.kv_cache.seq_len(),
28068            3 + r.tokens_generated - 1,
28069            "each token must be cached exactly once (v1 cached the last prompt token twice)"
28070        );
28071    }
28072
28073    #[test]
28074    fn generation_is_reproducible_with_seed() {
28075        let run = || {
28076            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
28077            p.generate("hello", 8, None, None).unwrap().token_ids
28078        };
28079        assert_eq!(run(), run());
28080    }
28081
28082    #[test]
28083    fn resetting_sampler_restarts_the_seeded_stream() {
28084        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
28085        let config = SamplerConfig {
28086            seed: Some(1234),
28087            ..SamplerConfig::default()
28088        };
28089        p.set_sampler_config(config.clone());
28090        let first = p.generate("hello", 8, None, None).unwrap().token_ids;
28091        p.set_sampler_config(config);
28092        let second = p.generate("hello", 8, None, None).unwrap().token_ids;
28093        assert_eq!(first, second);
28094    }
28095
28096    #[test]
28097    fn eviction_bounds_the_cache() {
28098        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
28099        p.kv_cache.max_seq_len = 6;
28100        p.sampler_config.temperature = 0.0;
28101        let _ = p.generate("abcd", 12, None, None).unwrap();
28102        assert!(
28103            p.kv_cache.seq_len() <= 6 + 1,
28104            "cache must stay bounded by max_seq_len (got {})",
28105            p.kv_cache.seq_len()
28106        );
28107    }
28108
28109    #[test]
28110    fn confidence_matches_tokens_and_is_a_probability() {
28111        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
28112        p.sampler_config.temperature = 0.0;
28113        p.sampler_config.repetition_penalty = 1.0;
28114        let r = p.generate("abcd", 10, None, None).unwrap();
28115        assert_eq!(
28116            r.token_confidence.len(),
28117            r.token_ids.len(),
28118            "one confidence per emitted token"
28119        );
28120        for &c in &r.token_confidence {
28121            assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
28122        }
28123        // top1_prob is a valid softmax probability.
28124        let logits = [1.0f32, 3.0, 0.5, 3.0];
28125        let p0 = top1_prob_t(&logits, 1, 1.0);
28126        let p1 = top1_prob_t(&logits, 3, 1.0);
28127        assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
28128        assert!(p0 > 0.0 && p0 < 1.0);
28129        // Calibration temperature > 1 softens an over-confident peak.
28130        let sharp = top1_prob_t(&logits, 1, 1.0);
28131        let soft = top1_prob_t(&logits, 1, 2.0);
28132        assert!(soft < sharp, "higher temperature lowers peak confidence");
28133    }
28134
28135    #[test]
28136    fn trace_is_opt_in_and_parallels_the_output() {
28137        // Off by default: the runtime is silent unless observation asked.
28138        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
28139        p.sampler_config.temperature = 0.0;
28140        p.sampler_config.repetition_penalty = 1.0;
28141        let r = p.generate("abcd", 10, None, None).unwrap();
28142        assert!(r.traces.is_empty(), "trace must be empty unless enabled");
28143
28144        // On: exactly one row per emitted token, aligned with the output.
28145        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
28146        p.sampler_config.temperature = 0.0;
28147        p.sampler_config.repetition_penalty = 1.0;
28148        p.set_trace(true);
28149        let r = p.generate("abcd", 10, None, None).unwrap();
28150        assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
28151        for (i, tr) in r.traces.iter().enumerate() {
28152            assert_eq!(tr.t, i, "trace index is sequential");
28153            assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
28154            assert_eq!(
28155                tr.confidence, r.token_confidence[i],
28156                "trace confidence matches the confidence channel"
28157            );
28158            // No dynamic router in this pipeline → no skill, no coherence.
28159            assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
28160        }
28161    }
28162
28163    #[test]
28164    fn explain_prefill_logits_match_greedy_first_token() {
28165        // `cortiq explain` shows the next-token distribution from
28166        // prefill_next_logits; its argmax must equal what greedy generate
28167        // actually emits first — otherwise explain would lie.
28168        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
28169        p.sampler_config.temperature = 0.0;
28170        p.sampler_config.repetition_penalty = 1.0;
28171        let ids = p.tokenizer.encode("abcd");
28172        let logits = p.prefill_next_logits(&ids, None);
28173        let argmax = logits
28174            .iter()
28175            .enumerate()
28176            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
28177            .unwrap()
28178            .0 as u32;
28179        let r = p.generate("abcd", 1, None, None).unwrap();
28180        assert_eq!(
28181            argmax, r.token_ids[0],
28182            "explain preview must match greedy emit"
28183        );
28184    }
28185
28186    #[test]
28187    fn laguna_shared_expert_is_unconditionally_added() {
28188        let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
28189        let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
28190        let zero_dense = || DenseFfn {
28191            gate_proj: matrix(vec![0.0; 4]),
28192            up_proj: matrix(vec![0.0; 4]),
28193            down_proj: matrix(vec![0.0; 4]),
28194            act: Act::Silu,
28195            down_t: None,
28196            segs: Vec::new(),
28197        };
28198        let shared = DenseFfn {
28199            gate_proj: identity(),
28200            up_proj: identity(),
28201            down_proj: identity(),
28202            act: Act::Silu,
28203            down_t: None,
28204            segs: Vec::new(),
28205        };
28206        let x = [1.0, 2.0];
28207        let expected = dense_ffn(&shared, &x, None);
28208        let moe = MoeFfn {
28209            router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
28210            experts: vec![zero_dense()],
28211            top_k: 1,
28212            norm_topk_prob: true,
28213            router_sigmoid: true,
28214            sigmoid_denom_eps: 1e-6,
28215            expert_bias: None,
28216            routed_scaling: 1.0,
28217            route_tau: None,
28218            shared: Some((shared, None)),
28219            stats: std::cell::RefCell::new(Vec::new()),
28220            act_sq: std::cell::RefCell::new(Vec::new()),
28221            act_rows: std::cell::RefCell::new(Vec::new()),
28222            mask: None,
28223            per_expert_scale: None,
28224            router_input_norm: false,
28225            resonance: None,
28226            grown: Vec::new(),
28227        };
28228        let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
28229        for (actual, expected) in actual.iter().zip(expected) {
28230            assert!((actual - expected).abs() < 1e-6);
28231        }
28232    }
28233
28234    /// A tiny MiMo-V2-shaped stack (the M3 fixture): layers [full, sliding,
28235    /// sliding, full]; 4 Q heads over 1 (full) / 2 (sliding) KV heads;
28236    /// head_dim 8 with 4-wide V heads; partial rotary 4 at θ 1e7 (full) /
28237    /// 1e4 (sliding); window 3; learned sinks on the sliding layers; layer
28238    /// 0 a dense FFN, layers 1..3 sigmoid-routed MoE with a selection bias
28239    /// (4 experts, top-2, renormalized, no shared expert). Geometry and
28240    /// sinks go through the same `set_attn_geometry` / `set_layer_sinks`
28241    /// the loader calls.
28242    fn mimo_test_pipeline() -> Pipeline {
28243        let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
28244        let kvh = [1usize, 2, 2, 1];
28245        let synth = |n: usize, salt: usize| -> Vec<f32> {
28246            (0..n)
28247                .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
28248                .collect()
28249        };
28250        let qt = |rows: usize, cols: usize, salt: usize| {
28251            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
28252        };
28253        let dense = |inter: usize, salt: usize| DenseFfn {
28254            gate_proj: qt(inter, hs, salt),
28255            up_proj: qt(inter, hs, salt + 1),
28256            down_proj: qt(hs, inter, salt + 2),
28257            act: Act::Silu,
28258            down_t: None,
28259            segs: Vec::new(),
28260        };
28261        let layers: Vec<LayerWeights> = (0..4)
28262            .map(|li| LayerWeights {
28263                input_norm: vec![1.0; hs],
28264                post_norm: vec![1.0; hs],
28265                attn_out_norm: None,
28266                ffn_out_norm: None,
28267                layer_scale: None,
28268                ffn: if li == 0 {
28269                    FfnKind::Dense(dense(inter, 50))
28270                } else {
28271                    FfnKind::Moe(MoeFfn {
28272                        router: qt(4, hs, 60 + li),
28273                        experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
28274                        top_k: 2,
28275                        norm_topk_prob: true,
28276                        router_sigmoid: true,
28277                        sigmoid_denom_eps: 1e-6,
28278                        expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
28279                        routed_scaling: 1.0,
28280                        route_tau: None,
28281                        shared: None,
28282                        stats: std::cell::RefCell::new(Vec::new()),
28283                        act_sq: std::cell::RefCell::new(Vec::new()),
28284                        act_rows: std::cell::RefCell::new(Vec::new()),
28285                        mask: None,
28286                        per_expert_scale: None,
28287                        router_input_norm: false,
28288                        resonance: None,
28289                        grown: Vec::new(),
28290                    })
28291                },
28292                attn: AttnKind::Full {
28293                    wq: qt(nh * hd, hs, li * 10 + 1),
28294                    wk: qt(kvh[li] * hd, hs, li * 10 + 2),
28295                    wv: qt(kvh[li] * vd, hs, li * 10 + 3),
28296                    wo: qt(hs, nh * vd, li * 10 + 4),
28297                    q_norm: None,
28298                    k_norm: None,
28299                    output_gate: false,
28300                    softplus_gate: None,
28301                    bias: None,
28302                },
28303            })
28304            .collect();
28305        let mut p = Pipeline::new(
28306            Tokenizer::byte_level(),
28307            PipelineWeights {
28308                embed_tokens: qt(vocab, hs, 100),
28309                layers,
28310                lm_head: qt(vocab, hs, 200),
28311                final_norm: vec![1.0; hs],
28312            },
28313            hs,
28314            inter,
28315            nh,
28316            1, // header num_kv_heads (the full layers')
28317            hd,
28318            4,
28319            4,
28320            false,
28321            vocab,
28322            1e-6,
28323            1e7,
28324            NormStyle::Qwen,
28325            4096,
28326            SamplerConfig {
28327                seed: Some(7),
28328                ..Default::default()
28329            },
28330        );
28331        // Diagnostics stay off whatever the test environment exports.
28332        p.layer_dump = None;
28333        p.set_rotary(4, 1e7);
28334        p.sliding_layers = Some(vec![false, true, true, false]);
28335        p.swa = Some((3, usize::MAX));
28336        p.rotary_dim_local = Some(4);
28337        p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
28338        p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
28339        p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
28340        p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
28341        p
28342    }
28343
28344    #[test]
28345    fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
28346        let mut p = mimo_test_pipeline();
28347        p.speculative = false;
28348        p.ignore_eos = true;
28349        p.sampler_config.temperature = 0.0;
28350        p.sampler_config.repetition_penalty = 1.0;
28351        let a = vec![3, 5, 7, 9, 11, 13];
28352        let b = vec![4, 8, 12, 16, 20, 24];
28353        let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
28354        let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
28355        // Same placeholder IDs as an earlier request are not a cache key
28356        // for different media. The actual rows, not a re-embedding of a,
28357        // must determine the continuation.
28358        let actual = p
28359            .generate_from_embeds(&a, &rows, 8, None, None)
28360            .unwrap()
28361            .token_ids;
28362        assert_eq!(actual, expected);
28363        assert!(p.kv_history.is_empty());
28364        let mut extended = a.clone();
28365        extended.push(17);
28366        let after_media = p
28367            .generate_from_ids(&extended, 8, None, None)
28368            .unwrap()
28369            .token_ids;
28370        p.reset_session();
28371        let fresh = p
28372            .generate_from_ids(&extended, 8, None, None)
28373            .unwrap()
28374            .token_ids;
28375        assert_eq!(after_media, fresh);
28376        assert!(
28377            p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
28378                .is_err()
28379        );
28380        // Force a real token-prefix reuse opportunity into the media call.
28381        // Those labels are unchanged, but their embeddings now describe a
28382        // different source sequence and every KV row must be rebuilt.
28383        p.reset_session();
28384        p.generate_from_ids(&a, 1, None, None).unwrap();
28385        let mut media_ids = p.kv_history.clone();
28386        assert!(!media_ids.is_empty());
28387        media_ids.extend_from_slice(&[19, 21, 23]);
28388        let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
28389        let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
28390        let mut oracle = mimo_test_pipeline();
28391        oracle.speculative = false;
28392        oracle.ignore_eos = true;
28393        oracle.sampler_config.temperature = 0.0;
28394        oracle.sampler_config.repetition_penalty = 1.0;
28395        let expected = oracle
28396            .generate_from_ids(&source_ids, 8, None, None)
28397            .unwrap()
28398            .token_ids;
28399        assert_eq!(
28400            p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
28401                .unwrap()
28402                .token_ids,
28403            expected
28404        );
28405        assert!(p.kv_history.is_empty());
28406        let mut bad = rows;
28407        bad[0] = f32::NAN;
28408        assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
28409    }
28410
28411    fn f32_bits(v: &[f32]) -> Vec<u32> {
28412        v.iter().map(|x| x.to_bits()).collect()
28413    }
28414
28415    /// M3 acceptance: on the MiMo-shaped stack the decode walk (one
28416    /// position at a time through `forward_layers`) and the batched
28417    /// prefill (`prefill_batch_span`, whole prompt and split in two
28418    /// chunks) give bit-identical logits at all 12 positions — per-layer
28419    /// KV heads, narrow V, sinks, the window and the biased sigmoid MoE all
28420    /// agree across the two walks.
28421    #[test]
28422    fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
28423        let mut p = mimo_test_pipeline();
28424        let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
28425        assert_eq!(kv, vec![1, 2, 2, 1]);
28426        assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
28427        assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
28428        let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
28429        let hs = p.hidden_size;
28430        let mut decode = Vec::new();
28431        for (pos, &id) in ids.iter().enumerate() {
28432            let e = p.embed_single(id);
28433            let h = p.forward_layers(&e, pos, None);
28434            decode.push(p.logits_from_hidden(&h));
28435        }
28436        for l in &p.kv_cache.layers {
28437            assert_eq!(l.seq_len, 12);
28438            // V rows are padded to head_dim inside the cache.
28439            assert_eq!(l.head_values(0).len(), 12 * 8);
28440        }
28441        assert!(decode.iter().flatten().all(|v| v.is_finite()));
28442
28443        p.clear_sequence_state();
28444        let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
28445        for pos in 0..ids.len() {
28446            let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
28447            assert_eq!(
28448                f32_bits(&decode[pos]),
28449                f32_bits(&lg),
28450                "whole prompt, pos {pos}"
28451            );
28452        }
28453
28454        p.clear_sequence_state();
28455        let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
28456        let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
28457        for pos in 0..ids.len() {
28458            let row = if pos < 5 {
28459                &a[pos * hs..(pos + 1) * hs]
28460            } else {
28461                &b[(pos - 5) * hs..(pos - 4) * hs]
28462            };
28463            let lg = p.logits_from_hidden(row);
28464            assert_eq!(
28465                f32_bits(&decode[pos]),
28466                f32_bits(&lg),
28467                "two chunks, pos {pos}"
28468            );
28469        }
28470
28471        // The fixture is not degenerate: the sinks and the window each
28472        // change the answer.
28473        let last = |p: &mut Pipeline| {
28474            p.clear_sequence_state();
28475            let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
28476            p.logits_from_hidden(&hb[11 * hs..12 * hs])
28477        };
28478        let base = last(&mut p);
28479        let mut no_sinks = mimo_test_pipeline();
28480        for l in &mut no_sinks.kv_cache.layers {
28481            l.sinks = None;
28482        }
28483        assert_ne!(
28484            f32_bits(&last(&mut no_sinks)),
28485            f32_bits(&base),
28486            "sinks are live"
28487        );
28488        let mut wide = mimo_test_pipeline();
28489        wide.swa = Some((64, usize::MAX));
28490        assert_ne!(
28491            f32_bits(&last(&mut wide)),
28492            f32_bits(&base),
28493            "window is live"
28494        );
28495
28496        // Generation runs end to end on the same stack.
28497        p.clear_sequence_state();
28498        p.ignore_eos = true;
28499        let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
28500        assert_eq!(r.token_ids.len(), 4);
28501    }
28502
28503    /// A Spark-X2.5-shaped stack: layers 0..3 slide (window 6), layer 3
28504    /// is global, every attention carries the head-wise sigmoid g_proj gate.
28505    fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
28506        let (hs, nh) = (16usize, 4usize);
28507        let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
28508        p.layer_dump = None;
28509        p.swa = Some((6, 4));
28510        p.proj_gate_sigmoid = true;
28511        for (li, lw) in p.weights.layers.iter_mut().enumerate() {
28512            if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
28513                let g: Vec<f32> = (0..nh * hs)
28514                    .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
28515                    .collect();
28516                *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
28517            }
28518        }
28519        p.swa_trim = trim;
28520        p
28521    }
28522
28523    /// Trimming the sliding tails changes nothing but memory: the same
28524    /// prompt in uneven chunks, a decode walk, and fused pairs with a
28525    /// 2-row rollback give bit-identical hiddens with and without it,
28526    /// while the trimmed sliding layers stay bounded and the global layer
28527    /// keeps every row.
28528    #[test]
28529    fn swa_trim_pipeline_matches_untrimmed_bitwise() {
28530        let mut a = spark_test_pipeline(None);
28531        let mut b = spark_test_pipeline(Some((2, 4)));
28532        assert_eq!(
28533            (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
28534            vec![Some(6), Some(6), Some(6), None]
28535        );
28536        let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
28537        let mut pos = 0usize;
28538        for &n in [5usize, 13, 7, 16].iter().cycle() {
28539            if pos >= ids.len() {
28540                break;
28541            }
28542            let end = (pos + n).min(ids.len());
28543            let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
28544            let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
28545            assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
28546            pos = end;
28547        }
28548        for _ in 0..23 {
28549            let e = a.embed_single(((pos * 7) % 64) as u32);
28550            let ha = a.forward_layers(&e, pos, None);
28551            let hb = b.forward_layers(&e, pos, None);
28552            assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
28553            for li in 0..3 {
28554                assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
28555            }
28556            pos += 1;
28557        }
28558        for round in 0..9 {
28559            let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
28560            let (a1, a2) = a.forward_pair(&e1, &e2, pos);
28561            let (b1, b2) = b.forward_pair(&e1, &e2, pos);
28562            assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
28563            assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
28564            if round % 2 == 1 {
28565                // A rejected draft: both lanes roll back.
28566                for p in [&mut a, &mut b] {
28567                    for l in &mut p.kv_cache.layers {
28568                        l.truncate_last(2);
28569                    }
28570                }
28571            } else {
28572                pos += 2;
28573            }
28574        }
28575        let e = a.embed_single(9);
28576        let (ha, hb) = (
28577            a.forward_layers(&e, pos, None),
28578            b.forward_layers(&e, pos, None),
28579        );
28580        assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
28581        for li in 0..3 {
28582            let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
28583            assert!(lb.base() > 0, "layer {li} trimmed");
28584            assert_eq!(la.base(), 0);
28585            assert_eq!(lb.pos_len(), la.seq_len);
28586            assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
28587        }
28588        let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
28589        assert_eq!(
28590            (gb.base(), gb.seq_len),
28591            (0, ga.seq_len),
28592            "the global layer keeps all"
28593        );
28594        assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
28595        assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
28596    }
28597
28598    /// The network split's KvFetch hand-off on a trimmed stack: every
28599    /// layer shipped over the wire mid-sequence (the sliding ones as
28600    /// `FullTail`) into a fresh pipeline that never ran a position, which
28601    /// then decodes on — bit for bit what the untrimmed pipeline decodes.
28602    #[test]
28603    fn swa_trim_wire_handoff_continues_bitwise() {
28604        let mut a = spark_test_pipeline(None);
28605        let mut b = spark_test_pipeline(Some((2, 4)));
28606        let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
28607        let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
28608        let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
28609        assert_eq!(f32_bits(&ha), f32_bits(&hb));
28610        let mut pos = ids.len();
28611        for _ in 0..5 {
28612            let e = a.embed_single(((pos * 5) % 64) as u32);
28613            assert_eq!(
28614                f32_bits(&a.forward_layers(&e, pos, None)),
28615                f32_bits(&b.forward_layers(&e, pos, None))
28616            );
28617            pos += 1;
28618        }
28619        let mut c = spark_test_pipeline(Some((2, 4)));
28620        for li in 0..4 {
28621            let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
28622            c.kv_cache.layers[li].import_wire(&bytes).unwrap();
28623        }
28624        assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
28625        assert_eq!(c.kv_cache.seq_len(), pos);
28626        for step in 0..17 {
28627            let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
28628            let ha = a.forward_layers(&e, pos, None);
28629            let hc = c.forward_layers(&e, pos, None);
28630            assert_eq!(
28631                f32_bits(&ha),
28632                f32_bits(&hc),
28633                "after the hand-off, step {step}"
28634            );
28635            pos += 1;
28636        }
28637        assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
28638    }
28639
28640    /// A synthetic MiMo draft stack for `mimo_test_pipeline` (the SWA
28641    /// geometry of its sliding layers: 2 KV heads, head 8 / V 4).  Widths
28642    /// are per-draft-layer so the MTP collector cannot accidentally borrow
28643    /// the trunk's `intermediate_size`.
28644    fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
28645        let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
28646        let synth = |len: usize, salt: usize| -> Vec<f32> {
28647            (0..len)
28648                .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
28649                .collect()
28650        };
28651        let qt = |rows: usize, cols: usize, salt: usize| {
28652            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
28653        };
28654        let layers = widths
28655            .iter()
28656            .copied()
28657            .enumerate()
28658            .map(|(k, inter)| {
28659                let s = 500 + k * 40;
28660                let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
28661                kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
28662                MtpModule {
28663                    enorm: vec![1.0; hs],
28664                    hnorm: vec![1.0; hs],
28665                    eh_proj: qt(hs, 2 * hs, s),
28666                    layer: LayerWeights {
28667                        input_norm: vec![1.0; hs],
28668                        post_norm: vec![1.0; hs],
28669                        attn_out_norm: None,
28670                        ffn_out_norm: None,
28671                        layer_scale: None,
28672                        attn: AttnKind::Full {
28673                            wq: qt(nh * hd, hs, s + 1),
28674                            wk: qt(nkv * hd, hs, s + 2),
28675                            wv: qt(nkv * vd, hs, s + 3),
28676                            wo: qt(hs, nh * vd, s + 4),
28677                            q_norm: None,
28678                            k_norm: None,
28679                            output_gate: false,
28680                            softplus_gate: None,
28681                            bias: None,
28682                        },
28683                        ffn: FfnKind::Dense(DenseFfn {
28684                            gate_proj: qt(inter, hs, s + 5),
28685                            up_proj: qt(inter, hs, s + 6),
28686                            down_proj: qt(hs, inter, s + 7),
28687                            act: Act::Silu,
28688                            down_t: None,
28689                            segs: Vec::new(),
28690                        }),
28691                    },
28692                    final_norm: vec![1.0; hs],
28693                    kv,
28694                }
28695            })
28696            .collect();
28697        mimo_mtp::MimoMtp::from_layers(layers)
28698    }
28699
28700    fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
28701        mimo_test_mtp_widths(&vec![24; n], gain)
28702    }
28703
28704    fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
28705        p.clear_sequence_state();
28706        p.speculative = spec;
28707        p.ignore_eos = true;
28708        p.sampler_config.temperature = 0.0;
28709        p.generate_from_ids(ids, n, None, None).unwrap()
28710    }
28711
28712    #[test]
28713    fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
28714        let mut p = mimo_test_pipeline();
28715        let widths = [7usize, 13, 29];
28716        assert_ne!(widths[0], p.intermediate_size);
28717        assert_ne!(widths[1], p.intermediate_size);
28718        assert_ne!(widths[2], p.intermediate_size);
28719        p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
28720        let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
28721
28722        let trunk = p.probe_ffn_mass(&ids);
28723        assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
28724        let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
28725        assert_eq!(got.len(), widths.len());
28726        for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
28727            assert_eq!(stat.mass.len(), width, "draft layer {k}");
28728            assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
28729            assert_eq!(stat.observed_positions, ids.len() - k - 1);
28730            assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
28731            assert!(stat
28732                .mass
28733                .iter()
28734                .zip(&stat.squared_mass)
28735                .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
28736        }
28737        assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
28738        let st = p.mimo_mtp.as_ref().unwrap();
28739        assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
28740
28741        // As above, reject only after a MiMo draft KV has been appended.
28742        // The detached multi-layer owner must still be reset on that error.
28743        let h = p.hidden_size;
28744        let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
28745            panic!("synthetic MiMo MTP must be dense")
28746        };
28747        dense.segs.push(FfnSeg {
28748            gate: QTensor::from_f32(vec![0.0; h], 1, h),
28749            up: QTensor::from_f32(vec![0.0; h], 1, h),
28750            down: QTensor::from_f32(vec![0.0; h], h, 1),
28751            start: dense.gate_proj.rows(),
28752            width: 1,
28753        });
28754        assert!(p
28755            .probe_mtp_ffn(&ids)
28756            .unwrap_err()
28757            .contains("tube-segmented draft FFNs are not supported"));
28758        assert_eq!(p.kv_cache.seq_len(), 0);
28759        assert!(p
28760            .mimo_mtp
28761            .as_ref()
28762            .unwrap()
28763            .layers
28764            .iter()
28765            .all(|m| m.kv.seq_len == 0));
28766    }
28767
28768    #[test]
28769    fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
28770        fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
28771            p.clear_sequence_state();
28772            let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
28773            let _exact = ExactMtpCalibrationGuard::enter();
28774            let got = crate::gpu::cpu_scope(|| {
28775                let trunk = p.probe_trunk_rows(ids)?;
28776                p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, &trunk, rows)
28777            })
28778            .expect("teacher-forced MiMo probe");
28779            st.reset();
28780            p.mimo_mtp = Some(st);
28781            p.reset_mtp_probe_state();
28782            got
28783        }
28784
28785        for post_norm_hidden in [false, true] {
28786            let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
28787            let make = || {
28788                let mut p = mimo_test_pipeline();
28789                let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
28790                st.post_norm_hidden = post_norm_hidden;
28791                p.mimo_mtp = Some(st);
28792                p
28793            };
28794            let mut full = make();
28795            let expected = collect(&mut full, &ids, usize::MAX);
28796            for rows in [1usize, 7, 128] {
28797                let mut chunked = make();
28798                let got = collect(&mut chunked, &ids, rows);
28799                assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
28800                for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
28801                    assert_eq!(
28802                        got.observed_positions, expected.observed_positions,
28803                        "post={post_norm_hidden}, rows={rows}, layer={layer}"
28804                    );
28805                    assert_eq!(
28806                        got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
28807                        expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
28808                        "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
28809                    );
28810                    assert_eq!(
28811                        got.squared_mass
28812                            .iter()
28813                            .map(|v| v.to_bits())
28814                            .collect::<Vec<_>>(),
28815                        expected
28816                            .squared_mass
28817                            .iter()
28818                            .map(|v| v.to_bits())
28819                            .collect::<Vec<_>>(),
28820                        "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
28821                    );
28822                }
28823            }
28824        }
28825    }
28826
28827    /// The draft stack's incremental rounds (a few rows per layer, last
28828    /// round's provisional rows dropped) give exactly the teacher-forced
28829    /// table of one causal pass per layer over the whole sequence — the
28830    /// table `tools/mimo_ref.py mtp` computes for variant A: layer k, row
28831    /// j reads (x[j+k+1], norm(h_j)) at RoPE position j.
28832    #[test]
28833    fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
28834        // Both readings of the backbone hidden: pre-final-norm (default)
28835        // and post-final-norm (`CMF_MIMO_MTP_HIDDEN=post`).
28836        for post in [false, true] {
28837            let mut p = mimo_test_pipeline();
28838            // A non-trivial final norm, so the two readings differ.
28839            p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
28840            let mut st0 = mimo_test_mtp(3, 1.0);
28841            st0.post_norm_hidden = post;
28842            p.mimo_mtp = Some(st0);
28843            let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
28844            let hs = p.hidden_size;
28845            let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
28846            p.mimo_note_rows(&hb, 0);
28847            let mut st = p.mimo_mtp.take().unwrap();
28848            // Incremental: one round per t through the decode path (later
28849            // tokens from `ids`, the probe's teacher forcing).
28850            let k = 3;
28851            let mut inc = Vec::new();
28852            for t in 0..ids.len() - k - 1 {
28853                inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
28854            }
28855            // Reference: per layer, ONE batched causal pass over all rows
28856            // with fresh caches.
28857            let s = ids.len();
28858            let mut reference = vec![vec![0u32; k]; s - k - 1];
28859            let mut fresh = mimo_test_mtp(3, 1.0);
28860            for (layer, m) in fresh.layers.iter_mut().enumerate() {
28861                let n = s - layer - 1;
28862                let mut cats = vec![0.0f32; n * 2 * hs];
28863                for j in 0..n {
28864                    let e = p.embed_single(ids[j + layer + 1]);
28865                    let raw = &hb[j * hs..(j + 1) * hs];
28866                    let g = if post {
28867                        inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
28868                    } else {
28869                        raw.to_vec()
28870                    };
28871                    let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
28872                    inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
28873                    inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
28874                }
28875                let mut x = vec![0.0f32; n * hs];
28876                m.eh_proj.matmat(&cats, n, &mut x, None);
28877                p.mimo_mtp_block(m, &mut x, n, 0);
28878                for (t, row) in reference.iter_mut().enumerate() {
28879                    let y = inference::rms_norm(
28880                        &x[t * hs..(t + 1) * hs],
28881                        &m.final_norm,
28882                        p.rms_eps,
28883                        p.norm_style,
28884                    );
28885                    row[layer] = sampler::argmax(&p.lm_head_forward(&y));
28886                }
28887            }
28888            assert_eq!(inc, reference, "post_norm_hidden = {post}");
28889            // Not a degenerate table: the drafts vary.
28890            let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
28891            assert!(distinct.len() > 3, "{inc:?}");
28892            // Each layer's cache ends holding rows up to the last round start.
28893            let last_t = ids.len() - k - 2;
28894            for m in &st.layers {
28895                assert_eq!(m.kv.seq_len, last_t + 1);
28896            }
28897        }
28898    }
28899
28900    /// Greedy with the MiMo draft stack is the plain greedy stream, token
28901    /// for token — with the real draft layers (low acceptance) and with a
28902    /// drafter that is right most of the time (exercises accepted prefixes
28903    /// of every length, the KV truncation of the rejected rows and the
28904    /// logits hand-off to the loop top), under the default repetition
28905    /// penalty.
28906    #[test]
28907    fn mimo_speculative_greedy_equals_plain_greedy() {
28908        let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
28909        let _env = ScopedTestEnv::set(&[
28910            ("CMF_GPU", Some("0")),
28911            ("CMF_GPU_WGPU_GRAPH", Some("0")),
28912        ]);
28913        let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
28914        let n = 24;
28915        let mut p = mimo_test_pipeline();
28916        let plain = mimo_greedy(&mut p, &ids, n, false);
28917        assert_eq!(plain.mtp_drafted, 0);
28918        assert_eq!(plain.token_ids.len(), n);
28919        let plain_kv = p.kv_cache.layers[0].seq_len;
28920
28921        // Real draft layers.
28922        p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
28923        let spec = mimo_greedy(&mut p, &ids, n, true);
28924        assert!(spec.mtp_drafted > 0, "the round must draft");
28925        assert_eq!(spec.token_ids, plain.token_ids);
28926        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
28927
28928        // A drafter reading the true continuation with every fifth token
28929        // wrong: accepted prefixes of 0..=3 all occur.
28930        let mut truth: Vec<u32> = ids.clone();
28931        truth.extend(&plain.token_ids);
28932        let mut noisy = truth.clone();
28933        for (i, t) in noisy.iter_mut().enumerate() {
28934            if i % 5 == 0 {
28935                *t = (*t + 1) % 64;
28936            }
28937        }
28938        let mut st = mimo_test_mtp(3, 1.0);
28939        st.draft_override = Some(noisy);
28940        p.mimo_mtp = Some(st);
28941        let spec = mimo_greedy(&mut p, &ids, n, true);
28942        assert_eq!(spec.token_ids, plain.token_ids);
28943        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
28944        let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
28945        assert_eq!(stats.accepted as usize, spec.mtp_accepted);
28946        assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
28947        assert!(
28948            stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
28949            "{:?}",
28950            stats.accept_hist
28951        );
28952        assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
28953
28954        // A perfect drafter: every draft accepted, rounds of K+1 tokens,
28955        // and the budget is never overrun.
28956        let mut st = mimo_test_mtp(3, 1.0);
28957        st.draft_override = Some(truth);
28958        p.mimo_mtp = Some(st);
28959        let spec = mimo_greedy(&mut p, &ids, n, true);
28960        assert_eq!(spec.token_ids, plain.token_ids);
28961        assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
28962        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
28963
28964        // CMF_MTP=0 path: the stack is attached but idle.
28965        let off = mimo_greedy(&mut p, &ids, n, false);
28966        assert_eq!(off.token_ids, plain.token_ids);
28967        assert_eq!(off.mtp_drafted, 0);
28968    }
28969
28970    /// The wgpu graphs carry MiMo-V2's attention per layer (KV heads,
28971    /// narrow V, sinks, windows, two RoPE tables): no attention-level
28972    /// decline for it any more, and the geometry each layer hands the
28973    /// graph is exactly what the CPU attention reads for that layer. The
28974    /// descriptive reasons stay (the Metal graphs and the q1 dropin still
28975    /// decline on them), and what the per-layer geometry cannot express
28976    /// keeps a named wgpu decline.
28977
28978    #[test]
28979    fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
28980        let p = mimo_test_pipeline();
28981        assert_eq!(
28982            p.graph_attn_decline_reason(),
28983            Some("per-layer KV head counts")
28984        );
28985        assert_eq!(p.wgpu_graph_attn_decline(), None);
28986        let g0 = p.graph_attn_geom(0).expect("full layer geometry");
28987        assert_eq!(
28988            (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
28989            (1, 4, 4, None, false)
28990        );
28991        assert_eq!(g0.invf, p.inv_freq.as_slice());
28992        let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
28993        assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
28994        assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
28995        assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
28996        assert_ne!(g0.invf, g1.invf, "two RoPE tables");
28997        let g3 = p.graph_attn_geom(3).expect("full layer geometry");
28998        assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
28999
29000        // No wgpu device in this process: the builders run and decline on
29001        // the (f32, unmapped) experts — never with an attention line.
29002        let emb = p.embed_single(3);
29003        let mut lg = Vec::new();
29004        assert!(
29005            p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
29006                .is_none()
29007        );
29008        let mut hid = emb.clone();
29009        assert_eq!(
29010            p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
29011            crate::gpu::BatchGraphOutcome::Declined
29012        );
29013        assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
29014        assert!(p.try_multi_burst(3, 0, 4).is_none());
29015        assert!(
29016            p.graph_declines().is_empty(),
29017            "no attention decline logged: {:?}",
29018            p.graph_declines()
29019        );
29020        // (No assertion on graph_prefill_preferred: with no attention
29021        // decline it follows the device — a test process that brought a
29022        // wgpu adapter up routes this resident MoE through the graph.)
29023
29024        let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
29025        assert_eq!(plain().graph_attn_decline_reason(), None);
29026        assert_eq!(plain().wgpu_graph_attn_decline(), None);
29027        assert!(
29028            plain().graph_attn_geom(0).is_none(),
29029            "uniform models keep the historical arms"
29030        );
29031        let mut q = plain();
29032        q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
29033        assert_eq!(
29034            q.graph_attn_decline_reason(),
29035            Some("learned attention sinks")
29036        );
29037        assert_eq!(
29038            q.graph_attn_geom(1).unwrap().sink,
29039            Some(&[0.25f32, -0.25][..])
29040        );
29041        let mut q = plain();
29042        q.set_attn_geometry(None, Some(2)).unwrap();
29043        assert_eq!(
29044            q.graph_attn_decline_reason(),
29045            Some("V heads narrower than Q/K heads")
29046        );
29047        assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
29048        let mut q = plain();
29049        q.sliding_layers = Some(vec![true, false]);
29050        q.swa = Some((4, usize::MAX));
29051        assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
29052        assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
29053        assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
29054
29055        // YaRN's post-rotation amplitude factor rides the per-layer graph
29056        // geometry. It is intentionally not an angular scale folded into
29057        // `invf`: the WGPU RoPE kernel receives it separately.
29058        let mut q = mimo_test_pipeline();
29059        q.rope_scale = 2.0;
29060        q.rope_scale_local = 1.25;
29061        assert_eq!(q.wgpu_graph_attn_decline(), None);
29062        assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
29063        assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
29064
29065        // A uniform YaRN model cannot yet pass the factor through the fused
29066        // RKV arm; decline safely instead of producing scale-1 output.
29067        let mut q = plain();
29068        q.rope_scale = 2.0;
29069        assert_eq!(
29070            q.wgpu_graph_attn_decline(),
29071            Some("scaled RoPE without per-layer geometry")
29072        );
29073    }
29074
29075    /// The native q1 attention path has a narrower contract than the WGPU
29076    /// per-layer geometry graph, but it must not discard the exact Flint
29077    /// combination of uniform GQA + sliding windows + learned sinks.  The
29078    /// actual Metal kernel is covered independently with a direct sink
29079    /// softmax test; this is the pure admission test, with no model run.
29080    #[cfg(target_os = "macos")]
29081    #[test]
29082    fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
29083        // rd=64 is in the native rope kernel's lane-pair contract; the
29084        // synthetic stack otherwise has the same uniform K/V geometry that
29085        // Flint needs for q1 (unlike the deliberately ragged MiMo fixture).
29086        let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
29087        p.swa = Some((16, usize::MAX));
29088        p.sliding_layers = Some(vec![true, true]);
29089        p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
29090        p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
29091
29092        assert_eq!(
29093            p.graph_attn_decline_reason(),
29094            Some("learned attention sinks")
29095        );
29096        let swa_graph = p.metal_graph_swa();
29097        assert!(swa_graph, "uniform sliding sink geometry must reach q1");
29098        let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
29099            && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
29100        assert!(
29101            native_sink_attend,
29102            "the q1 sink admission must follow the SWA door"
29103        );
29104        // The batched chunk softmax has no value-less sink column, so its
29105        // separate admission gate must keep this prompt on the host path.
29106        assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
29107    }
29108
29109    #[test]
29110    fn mimo_verify_rewind_preserves_lagging_host_caches() {
29111        let mut p = mimo_test_pipeline();
29112        for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
29113            let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
29114            for _ in 0..if li == 0 { 2 } else { 12 } {
29115                layer.append(&row, &row, &[]);
29116            }
29117        }
29118        p.mimo_verify_rewind(9).unwrap();
29119        assert_eq!(p.kv_cache.layers[0].seq_len, 2);
29120        for layer in &p.kv_cache.layers[1..] {
29121            assert_eq!(layer.seq_len, 9);
29122        }
29123    }
29124
29125    /// CMF_LAYER_DUMP: the decode walk and the batched prefill both write
29126    /// every (position, layer) hidden, the two sets agree byte for byte,
29127    /// and the last layer's file is the stack output.
29128    #[test]
29129    fn layer_dump_covers_every_position_and_layer_on_both_walks() {
29130        let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
29131        let _ = std::fs::remove_dir_all(&dir);
29132        let mut p = mimo_test_pipeline();
29133        let hs = p.hidden_size;
29134        let ids = [5u32, 9, 11, 2, 40];
29135        p.layer_dump = Some(dir.join("decode"));
29136        for (pos, &id) in ids.iter().enumerate() {
29137            let e = p.embed_single(id);
29138            let _ = p.forward_layers(&e, pos, None);
29139        }
29140        p.clear_sequence_state();
29141        p.layer_dump = Some(dir.join("prefill"));
29142        let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
29143        for pos in 0..ids.len() {
29144            for li in 0..p.num_layers {
29145                let name = format!("p{pos:06}_l{li:02}.f32");
29146                let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
29147                let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
29148                assert_eq!(a.len(), hs * 4, "{name}");
29149                assert_eq!(a, b, "{name}");
29150            }
29151        }
29152        let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
29153        let vals: Vec<f32> = last
29154            .chunks(4)
29155            .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
29156            .collect();
29157        assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
29158        let _ = std::fs::remove_dir_all(&dir);
29159    }
29160
29161    #[test]
29162    fn attn_geometry_and_sinks_are_validated() {
29163        let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
29164        assert!(
29165            p.set_attn_geometry(Some(vec![2]), None).is_err(),
29166            "one entry per layer"
29167        );
29168        assert!(
29169            p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
29170            "3 does not divide 4"
29171        );
29172        assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
29173        assert!(p.set_attn_geometry(None, Some(0)).is_err());
29174        assert!(
29175            p.set_attn_geometry(None, Some(5)).is_err(),
29176            "V wider than the head"
29177        );
29178        p.set_attn_geometry(None, Some(4)).unwrap();
29179        assert_eq!(
29180            p.v_head_dim, None,
29181            "v_head_dim == head_dim is the uniform case"
29182        );
29183        p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
29184        p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
29185        assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
29186        assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
29187        assert!(
29188            p.kv_cache.layers[1].sinks.is_some(),
29189            "a reshape keeps the layer's sinks"
29190        );
29191        assert_eq!(p.layer_geom(1).0, 4);
29192        assert!(
29193            p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
29194            "one sink per Q head"
29195        );
29196        assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
29197        assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
29198    }
29199
29200    /// The O(1) Nyström state replaces a plain full-context softmax; it
29201    /// must never be armed on a sliding, sink or narrow-V layer.
29202    #[test]
29203    fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
29204        let cfg = || {
29205            Some(crate::nystrom::O1Cfg {
29206                layers: crate::nystrom::O1Layers::All,
29207                m: 4,
29208                w: 8,
29209                sink: 2,
29210                rect: crate::nystrom::O1Rect::Aggregate,
29211            })
29212        };
29213        let mut p = mimo_test_pipeline();
29214        p.set_o1(cfg());
29215        assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
29216        let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
29217        q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
29218        q.sliding_layers = Some(vec![false, false, true]);
29219        q.swa = Some((4, usize::MAX));
29220        q.set_o1(cfg());
29221        assert_eq!(q.o1_flags, vec![true, false, false]);
29222    }
29223
29224    #[test]
29225    fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
29226        const B: usize = 19;
29227        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
29228        p.set_o1(Some(crate::nystrom::O1Cfg {
29229            layers: crate::nystrom::O1Layers::All,
29230            m: 4,
29231            w: 8,
29232            sink: 2,
29233            rect: crate::nystrom::O1Rect::Aggregate,
29234        }));
29235        p.o1_begin_with_prefix(Some(B));
29236        let ids: Vec<u32> = (0..B as u32).collect();
29237        let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
29238
29239        assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
29240        assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
29241        let next = p.embed_single(B as u32);
29242        let _ = p.forward_layers(&next, B, None);
29243        assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
29244    }
29245
29246    /// A turbo file's exact rows are chosen across every far-field layer at the
29247    /// boundary. A batched chunk ending on it used to seal each layer as it
29248    /// finished, before the pipeline could choose, so no row stayed exact on the
29249    /// host path (LFM2 on Metal).
29250    #[test]
29251    fn o1_exact_rows_are_chosen_before_a_batched_boundary_seals() {
29252        const B: usize = 19;
29253        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
29254        let cfg = crate::nystrom::O1Cfg {
29255            layers: crate::nystrom::O1Layers::All,
29256            m: 4,
29257            w: 8,
29258            sink: 2,
29259            rect: crate::nystrom::O1Rect::Aggregate,
29260        };
29261        p.set_o1(Some(cfg.clone()));
29262        p.o1_turbo = Some(O1Turbo {
29263            cfg,
29264            seal: B,
29265            exact: crate::dory::ExactRows { weights: std::sync::Arc::new(vec![1.0; 2 * 2]), budget: 3 },
29266        });
29267        p.o1_begin_with_prefix(Some(B));
29268        let ids: Vec<u32> = (0..B as u32).collect();
29269        let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
29270        // The span's own completion chooses the rows and seals every layer.
29271        assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
29272        let keep = p.kv_cache.layers[0].o1_keep.clone();
29273        assert_eq!(keep.len(), 3, "the budget of exact rows is spent");
29274        assert_eq!(keep, p.kv_cache.layers[1].o1_keep, "the same rows in every layer");
29275        assert!(keep.iter().all(|&r| (2..B - 8).contains(&r)), "never a sink or a window row");
29276    }
29277
29278    #[test]
29279    fn o1_pair_transition_commits_scratch_before_epoch_publication() {
29280        const B: usize = 19;
29281        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
29282        // Keep a real recurrent layer ahead of the Full O(1) layer so the
29283        // pair test observes the GDN lane-2 scratch swap at the same
29284        // boundary, rather than only exercising an artificial scratch vec.
29285        let gdn_cfg = crate::linear_core::GdnCfg {
29286            num_v_heads: 2,
29287            num_k_heads: 1,
29288            key_head_dim: 2,
29289            value_head_dim: 4,
29290            conv_kernel: 3,
29291            hidden_size: 8,
29292            rms_eps: 1e-6,
29293            output_gate_sigmoid: false,
29294        };
29295        let synth = |n: usize, salt: usize| -> Vec<f32> {
29296            (0..n)
29297                .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
29298                .collect()
29299        };
29300        let qt = |rows: usize, cols: usize, salt: usize| {
29301            crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
29302        };
29303        let c_dim = gdn_cfg.conv_dim();
29304        let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
29305        p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
29306            in_proj_qkv: qt(c_dim, 8, 1),
29307            in_proj_z: qt(vd, 8, 2),
29308            in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
29309            in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
29310            conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
29311            a_log: vec![0.2, 0.5],
29312            dt_bias: synth(gdn_cfg.num_v_heads, 6),
29313            norm: vec![1.0; gdn_cfg.value_head_dim],
29314            out_proj: qt(8, vd, 7),
29315        });
29316        p.gdn_cfg = Some(gdn_cfg);
29317        p.set_o1(Some(crate::nystrom::O1Cfg {
29318            layers: crate::nystrom::O1Layers::All,
29319            m: 4,
29320            w: 8,
29321            sink: 2,
29322            rect: crate::nystrom::O1Rect::Aggregate,
29323        }));
29324        p.o1_begin_with_prefix(Some(B));
29325        for pos in 0..B - 2 {
29326            let emb = p.embed_single(pos as u32);
29327            let _ = p.forward_layers(&emb, pos, None);
29328        }
29329        let lane1_state = p.kv_cache.layers[0].linear_state.clone();
29330
29331        let e1 = p.embed_single((B - 2) as u32);
29332        let e2 = p.embed_single((B - 1) as u32);
29333        let _ = p.forward_pair(&e1, &e2, B - 2);
29334
29335        assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
29336        assert!(
29337            p.kv_cache
29338                .layers
29339                .iter()
29340                .enumerate()
29341                .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
29342        );
29343        assert!(!p.kv_cache.layers[0].linear_state.is_empty());
29344        assert_ne!(
29345            p.kv_cache.layers[0].linear_state, lane1_state,
29346            "real pair must commit GDN lane 2 before returning"
29347        );
29348        assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
29349        let next = p.embed_single(B as u32);
29350        let _ = p.forward_layers(&next, B, None);
29351        assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
29352    }
29353
29354    #[test]
29355    fn o1_error_observation_stays_terminal_until_reset() {
29356        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29357        p.set_o1(Some(crate::nystrom::O1Cfg {
29358            layers: crate::nystrom::O1Layers::All,
29359            m: 4,
29360            w: 8,
29361            sink: 2,
29362            rect: crate::nystrom::O1Rect::Aggregate,
29363        }));
29364        p.o1_begin();
29365        p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
29366
29367        assert!(p.o1_seal_checked().is_err());
29368        assert!(
29369            p.o1_seal_checked().is_err(),
29370            "retry must see the sticky error"
29371        );
29372        let k = vec![0.2f32; 4];
29373        let v = vec![0.3f32; 4];
29374        p.kv_cache.layers[0].append(&k, &v, &[]);
29375        assert_eq!(p.kv_cache.layers[0].seq_len, 0);
29376
29377        p.reset_session();
29378        p.o1_begin();
29379        p.kv_cache.layers[0].append(&k, &v, &[]);
29380        assert_eq!(p.kv_cache.layers[0].seq_len, 1);
29381    }
29382
29383    #[test]
29384    fn nll_graph_failure_is_terminal_and_request_is_reusable() {
29385        let ids = vec![1u32, 2, 3, 4, 5, 6];
29386        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29387        p.graph_logits = Some(vec![123.0]);
29388        p.graph_want_logits = true;
29389        p.graph_failed
29390            .store(true, std::sync::atomic::Ordering::Relaxed);
29391        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
29392        let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
29393        assert!(err.contains("before NLL"));
29394        assert!(p.graph_logits.is_none());
29395        assert!(!p.graph_want_logits);
29396        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
29397        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
29398
29399        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29400        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
29401        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
29402        assert_eq!(actual.1, expected.1);
29403        assert!((actual.0 - expected.0).abs() < 1e-9);
29404    }
29405
29406    #[test]
29407    fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
29408        let ids = vec![1u32, 2, 3, 4, 5, 6];
29409        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29410        p.nll_test_fail_at = Some(1);
29411        let err = p
29412            .nll_ids_from(&ids, 0)
29413            .expect_err("one-shot forward failure");
29414        assert!(err.contains("forward") || err.contains("score row"));
29415        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
29416        assert!(!p.graph_want_logits);
29417        assert!(p.graph_logits.is_none());
29418        assert!(p.kv_history.is_empty());
29419
29420        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29421        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
29422        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
29423        assert_eq!(actual.1, expected.1);
29424        assert!((actual.0 - expected.0).abs() < 1e-9);
29425    }
29426
29427    #[test]
29428    fn nll_serial_failure_before_first_row_is_reported() {
29429        let ids = vec![1u32, 2, 3, 4];
29430        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29431        p.nll_test_force_serial = true;
29432        p.nll_test_fail_at = Some(0);
29433        let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
29434        assert!(err.contains("serial forward"));
29435        assert!(p.kv_history.is_empty());
29436        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
29437        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
29438    }
29439
29440    #[test]
29441    fn ffn_probe_failure_discards_recorder_and_state() {
29442        let ids = vec![1u32, 2, 3, 4];
29443        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29444        p.nll_test_fail_at = Some(0);
29445        let err = p
29446            .probe_ffn_mass_batch(&ids)
29447            .expect_err("probe forward failure");
29448        assert!(err.contains("NLL"));
29449        assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
29450        assert!(p.kv_history.is_empty());
29451        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
29452    }
29453
29454    #[test]
29455    fn nll_test_controls_are_pipeline_scoped() {
29456        let ids = vec![1u32, 2, 3, 4];
29457        let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29458        let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29459        failing.nll_test_force_serial = true;
29460        failing.nll_test_fail_at = Some(0);
29461
29462        assert!(!failing.can_prefill_batched());
29463        assert!(unaffected.can_prefill_batched());
29464        let expected = unaffected
29465            .nll_ids_from(&ids, 0)
29466            .expect("unaffected pipeline remains usable");
29467        let err = failing
29468            .nll_ids_from(&ids, 0)
29469            .expect_err("failure injection belongs to failing pipeline");
29470        assert!(err.contains("serial forward"));
29471        assert!(failing.nll_test_fail_at.is_none());
29472        assert!(unaffected.can_prefill_batched());
29473        let actual = unaffected
29474            .nll_ids_from(&ids, 0)
29475            .expect("unaffected pipeline remains reusable");
29476        assert_eq!(actual.1, expected.1);
29477        assert!((actual.0 - expected.0).abs() < 1e-9);
29478    }
29479
29480    #[test]
29481    fn forward_ids_failure_channel_is_terminal_and_reusable() {
29482        let ids = vec![1u32, 2, 3, 4, 5, 6];
29483        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29484        p.graph_logits = Some(vec![123.0]);
29485        p.graph_want_logits = true;
29486        p.graph_failed
29487            .store(true, std::sync::atomic::Ordering::Relaxed);
29488        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
29489
29490        let err = p
29491            .forward_ids(&ids, None)
29492            .expect_err("a failed forward must not become a valid head result");
29493        assert!(err.contains("forward_ids setup"));
29494        assert!(p.graph_logits.is_none());
29495        assert!(!p.graph_want_logits);
29496        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
29497        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
29498        assert_eq!(p.kv_cache.seq_len(), 0);
29499
29500        let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
29501            .forward_ids(&ids, None)
29502            .expect("fresh forward_ids");
29503        let actual = p
29504            .forward_ids(&ids, None)
29505            .expect("pipeline remains reusable after a failed forward");
29506        assert_eq!(actual.len(), expected.len());
29507        assert!(
29508            actual
29509                .iter()
29510                .zip(expected)
29511                .all(|(a, b)| (a - b).abs() < 1e-9)
29512        );
29513        assert_eq!(p.kv_cache.seq_len(), ids.len());
29514    }
29515
29516    #[test]
29517    fn sigmoid_router_floor_is_explicit_per_architecture() {
29518        // Flint and MiniMax M3.1 reference runtimes use +1e-20, while the
29519        // generic LFM2-compatible path uses +1e-6. At low (but representable)
29520        // sigmoid scores, silently sharing the latter changes expert weights by
29521        // orders of magnitude and can make a routed layer look coherent while
29522        // discarding its expert contribution.
29523        let zero = || DenseFfn {
29524            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29525            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29526            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29527            act: Act::Silu,
29528            down_t: None,
29529            segs: Vec::new(),
29530        };
29531        let m = MoeFfn {
29532            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
29533            experts: vec![zero(), zero()],
29534            top_k: 1,
29535            norm_topk_prob: true,
29536            router_sigmoid: true,
29537            sigmoid_denom_eps: 1e-20,
29538            expert_bias: None,
29539            routed_scaling: 2.5,
29540            route_tau: None,
29541            shared: None,
29542            stats: std::cell::RefCell::new(Vec::new()),
29543            act_sq: std::cell::RefCell::new(Vec::new()),
29544            act_rows: std::cell::RefCell::new(Vec::new()),
29545            mask: None,
29546            per_expert_scale: None,
29547            router_input_norm: false,
29548            resonance: None,
29549            grown: Vec::new(),
29550        };
29551        let logits = [-20.0f32, -20.0];
29552        let (_, p, flint_wsum) = moe_route(&logits, &m, None);
29553        let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
29554        let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
29555        let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
29556        assert!((flint_wsum - reference_expected).abs() < 1e-15);
29557        assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
29558        assert!(legacy_wsum > minimax_wsum * 100.0);
29559    }
29560
29561    #[test]
29562    fn resonance_scores_match_formula_and_stable_tie() {
29563        let r = Resonance {
29564            // Three descriptors, hidden=2, one projection row each.
29565            mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
29566            u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
29567            k: 1,
29568            bias: vec![1.5, 0.5, 0.0],
29569            shell: Vec::new(),
29570        };
29571        let x = [1.0f32, 1.0];
29572        let mut got = vec![0.0; 3];
29573        r.scores(&x, &mut got);
29574        // Expert 0 and 1 are an exact score tie; the CPU top-1 contract uses
29575        // the lower index.  The values also check d² - (U·d)², not just tie
29576        // ordering.
29577        assert!((got[0] - 0.5).abs() < 1e-6);
29578        assert!((got[1] - 0.5).abs() < 1e-6);
29579        assert!(got[2].abs() < 1e-6);
29580        let best = got
29581            .iter()
29582            .enumerate()
29583            .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
29584            .map(|(i, _)| i);
29585        assert_eq!(best, Some(0));
29586        assert!(got.iter().all(|v| v.is_finite()));
29587    }
29588
29589    /// The growth shell (spec §2): a grown expert whose reconstruction
29590    /// error lies outside its shell scores −∞, one inside keeps the exact
29591    /// resonance score, trunk rows (`+inf` shell) are bit-identical to the
29592    /// shell-less computation; the process-wide switch disables it.
29593    #[test]
29594    fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
29595        // hidden = 2, rank 1. Experts 0/1 = trunk (shell +inf); 2 and 3 =
29596        // grown, the same descriptor (μ = (0, 1), u = (1, 1)) with shells
29597        // 6.0 and 0.25. At x' = (3, 0): d = (3, −1), d² = 10, proj =
29598        // (3 − 1)² = 4, err = 6 exactly — on the boundary of expert 2's
29599        // shell (kept: the rule is strict `>`), outside expert 3's.
29600        let plain = Resonance {
29601            mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
29602            u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
29603            k: 1,
29604            bias: vec![1.5, 0.5, 0.0, 0.0],
29605            shell: Vec::new(),
29606        };
29607        let shelled = Resonance {
29608            mu: plain.mu.clone(),
29609            u: plain.u.clone(),
29610            k: 1,
29611            bias: plain.bias.clone(),
29612            shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
29613        };
29614        assert!(!plain.has_shell());
29615        assert!(shelled.has_shell());
29616        let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
29617        let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
29618        set_growth_shell(Some(true));
29619        assert!(growth_shell_enabled());
29620        // x = (1, 1): the grown experts reconstruct it exactly (err 0):
29621        // inside both shells, every row the shell-less bits.
29622        let x = [1.0f32, 1.0];
29623        plain.scores(&x, &mut a);
29624        shelled.scores(&x, &mut b);
29625        assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
29626        assert!(a[2] == 0.0 && a[3] == 0.0);
29627        // x' = (3, 0): expert 3 → −∞, expert 2 (err == shell) and the
29628        // trunk rows keep their exact bits.
29629        let xo = [3.0f32, 0.0];
29630        plain.scores(&xo, &mut a);
29631        shelled.scores(&xo, &mut b);
29632        assert_eq!(a[2], -6.0);
29633        assert_eq!(a[3], -6.0);
29634        assert_eq!(
29635            bits(&a[..3]),
29636            bits(&b[..3]),
29637            "trunk rows + the boundary row"
29638        );
29639        assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
29640        assert_eq!(shelled.effective_shell(4), shelled.shell);
29641        // The switch (`CMF_GROWTH_SHELL=off` / `growth-eval --shell off`):
29642        // all +inf, the shell-less bits everywhere.
29643        set_growth_shell(Some(false));
29644        assert!(!growth_shell_enabled());
29645        assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
29646        shelled.scores(&xo, &mut b);
29647        assert_eq!(bits(&a), bits(&b));
29648        set_growth_shell(None);
29649        // A shell vector shorter than the expert count masks nothing
29650        // beyond it (a legacy layer whose tail has no shell).
29651        let short = Resonance {
29652            shell: vec![f32::INFINITY, f32::INFINITY],
29653            ..shelled
29654        };
29655        set_growth_shell(Some(true));
29656        short.scores(&xo, &mut b);
29657        assert_eq!(bits(&a), bits(&b));
29658        set_growth_shell(None);
29659    }
29660
29661    /// `moe_route` with −∞ logits (a grown expert outside its shell):
29662    /// top-1 is the best finite expert with weight exactly 1.0 on both
29663    /// the softmax and the sigmoid path; all −∞ degrades to uniform.
29664    #[test]
29665    fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
29666        let zero = || DenseFfn {
29667            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29668            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29669            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29670            act: Act::Silu,
29671            down_t: None,
29672            segs: Vec::new(),
29673        };
29674        let moe = |sigmoid: bool| MoeFfn {
29675            router: QTensor::from_f32(vec![0.0; 8], 4, 2),
29676            experts: vec![zero(), zero(), zero(), zero()],
29677            top_k: 1,
29678            norm_topk_prob: true,
29679            router_sigmoid: sigmoid,
29680            sigmoid_denom_eps: 1e-6,
29681            expert_bias: None,
29682            routed_scaling: 1.0,
29683            route_tau: None,
29684            shared: None,
29685            stats: std::cell::RefCell::new(Vec::new()),
29686            act_sq: std::cell::RefCell::new(Vec::new()),
29687            act_rows: std::cell::RefCell::new(Vec::new()),
29688            mask: None,
29689            per_expert_scale: None,
29690            router_input_norm: false,
29691            resonance: None,
29692            grown: Vec::new(),
29693        };
29694        let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
29695        for sigmoid in [false, true] {
29696            let m = moe(sigmoid);
29697            let (idx, p, wsum) = moe_route(&logits, &m, None);
29698            assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
29699            assert_eq!(p[1], 0.0);
29700            assert_eq!(p[3], 0.0);
29701            assert!(p[2] > p[0] && p[0] > 0.0);
29702            assert!(p.iter().all(|v| v.is_finite()));
29703            let w = p[2] / wsum;
29704            if sigmoid {
29705                // The sigmoid renorm keeps its reference floor (+1e-6).
29706                assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
29707            } else {
29708                assert_eq!(
29709                    w, 1.0,
29710                    "softmax: the renormalized top-1 weight is exactly 1"
29711                );
29712            }
29713            // Masked experts stay masked even when they are the only ones
29714            // "admitted" by an allow-list that covers everything.
29715            let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
29716            assert_eq!(idx, vec![2]);
29717        }
29718        // A finite expert always beats −∞ whatever the bias / order.
29719        let m = moe(false);
29720        let (idx, _, _) = moe_route(
29721            &[
29722                f32::NEG_INFINITY,
29723                f32::NEG_INFINITY,
29724                f32::NEG_INFINITY,
29725                -9.0,
29726            ],
29727            &m,
29728            None,
29729        );
29730        assert_eq!(idx, vec![3]);
29731        // Every expert at −∞ (cannot happen on a grown file — trunk rows
29732        // have no shell): uniform, finite, lowest index.
29733        let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
29734        assert_eq!(idx, vec![0]);
29735        assert!(p.iter().all(|&v| v == 0.25));
29736        assert!(wsum.is_finite() && wsum > 0.0);
29737    }
29738
29739    /// MiniMax-M3.1's DEVICE route (the pooled token-graph arm's exact
29740    /// flags: sigmoid scoring via n's high bit, +1e-20 floor,
29741    /// routed_scaling 2.0, selection bias on the CHOICE only) must match
29742    /// the host reference in experts, ORDER and weights — the check that
29743    /// caught the flags/slot collision.
29744    #[cfg(feature = "gpu")]
29745    #[test]
29746    fn minimax_device_route_matches_host_sigmoid_contract() {
29747        match crate::gpu_wgpu::selected_and_up() {
29748            None => {
29749                crate::gpu_wgpu::skip_or_fail(module_path!());
29750                return;
29751            }
29752            // Sibling unit tests in THIS process select wgpu by writing
29753            // `CMF_GPU` (the gpu_wgpu test module does it ~70 times), so on a
29754            // headless runner the variable is already set by the time this
29755            // test reads it — "requested" here is test pollution, not an
29756            // operator's ask, and the adapter genuinely does not exist in
29757            // this process. Same treatment as the gpu_wgpu tests themselves:
29758            // a visible skip, never a silent pass.
29759            Some(false) => {
29760                eprintln!(
29761                    "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
29762                    module_path!()
29763                );
29764                return;
29765            }
29766            Some(true) => {}
29767        }
29768        let n = 128usize;
29769        let top_k = 4usize;
29770        let logits: Vec<f32> = (0..n)
29771            .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
29772            .collect();
29773        let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
29774        let m = MoeFfn {
29775            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
29776            experts: Vec::new(),
29777            top_k,
29778            norm_topk_prob: true,
29779            router_sigmoid: true,
29780            sigmoid_denom_eps: 1e-20,
29781            expert_bias: Some(bias.clone()),
29782            routed_scaling: 2.0,
29783            route_tau: None,
29784            shared: None,
29785            stats: std::cell::RefCell::new(Vec::new()),
29786            act_sq: std::cell::RefCell::new(Vec::new()),
29787            act_rows: std::cell::RefCell::new(Vec::new()),
29788            mask: None,
29789            per_expert_scale: None,
29790            router_input_norm: false,
29791            resonance: None,
29792            grown: Vec::new(),
29793        };
29794        let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
29795        let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
29796
29797        for shared in [Some(n as u32), None] {
29798            let mut gi = Vec::new();
29799            let mut gw = Vec::new();
29800            assert!(
29801                crate::gpu_wgpu::minimax_moe_route_for_test(
29802                    &logits,
29803                    Some(&bias),
29804                    top_k,
29805                    2.0,
29806                    shared,
29807                    &mut gi,
29808                    &mut gw,
29809                ),
29810                "the device did not come up — the test checked nothing"
29811            );
29812            let routed = if shared.is_some() { top_k } else { gi.len() };
29813            assert_eq!(
29814                &gi[..routed],
29815                &ci[..],
29816                "different experts or order (shared={shared:?})"
29817            );
29818            let d = cw
29819                .iter()
29820                .zip(&gw[..routed])
29821                .map(|(a, b)| (a - b).abs())
29822                .fold(0.0f32, f32::max);
29823            println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
29824            assert!(d < 1e-6, "the weights diverged by {d:.3e}");
29825            if let Some(s) = shared {
29826                assert_eq!(gi[top_k], s as usize, "shared pin slot");
29827                assert_eq!(gw[top_k], 1.0, "shared weight");
29828            }
29829        }
29830    }
29831
29832    /// The pooled arm's COLD hand-back: `(expert, weight)` pairs for the
29833    /// picks the bank could not serve must carry the host's EXACT final
29834    /// weights — the device ships its resident sum, the host adds this on
29835    /// top of the same residual, so a weight drift compounds per layer.
29836    #[cfg(feature = "gpu")]
29837    #[test]
29838    fn minimax_device_cold_weights_match_host_contract() {
29839        match crate::gpu_wgpu::selected_and_up() {
29840            None => {
29841                crate::gpu_wgpu::skip_or_fail(module_path!());
29842                return;
29843            }
29844            // Sibling unit tests in THIS process select wgpu by writing
29845            // `CMF_GPU` (the gpu_wgpu test module does it ~70 times), so on a
29846            // headless runner the variable is already set by the time this
29847            // test reads it — "requested" here is test pollution, not an
29848            // operator's ask, and the adapter genuinely does not exist in
29849            // this process. Same treatment as the gpu_wgpu tests themselves:
29850            // a visible skip, never a silent pass.
29851            Some(false) => {
29852                eprintln!(
29853                    "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
29854                    module_path!()
29855                );
29856                return;
29857            }
29858            Some(true) => {}
29859        }
29860        let n = 128usize;
29861        let top_k = 4usize;
29862        let logits: Vec<f32> = (0..n)
29863            .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
29864            .collect();
29865        let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
29866        let m = MoeFfn {
29867            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
29868            experts: Vec::new(),
29869            top_k,
29870            norm_topk_prob: true,
29871            router_sigmoid: true,
29872            sigmoid_denom_eps: 1e-20,
29873            expert_bias: Some(bias.clone()),
29874            routed_scaling: 2.0,
29875            route_tau: None,
29876            shared: None,
29877            stats: std::cell::RefCell::new(Vec::new()),
29878            act_sq: std::cell::RefCell::new(Vec::new()),
29879            act_rows: std::cell::RefCell::new(Vec::new()),
29880            mask: None,
29881            per_expert_scale: None,
29882            router_input_norm: false,
29883            resonance: None,
29884            grown: Vec::new(),
29885        };
29886        let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
29887        assert_eq!(ci.len(), top_k);
29888
29889        // Every pick resident → no cold pairs at all.
29890        let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
29891        let mut cold = Vec::new();
29892        let mut idx = Vec::new();
29893        let mut dw = Vec::new();
29894        assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
29895            &logits,
29896            Some(&bias),
29897            top_k,
29898            2.0,
29899            &resident,
29900            n as u32,
29901            &mut cold,
29902            &mut idx,
29903            &mut dw,
29904        ));
29905        assert!(cold.is_empty(), "fully resident bank: {cold:?}");
29906        // With subset+pin together: routed slots carry the FLAT remap value
29907        // (here identity = the expert id), weights the host contract, and
29908        // the pinned shared keeps weight 1 — this exact combination is what
29909        // the pooled arm runs with.
29910        for (r, &e) in ci.iter().enumerate() {
29911            assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
29912            let host = p[e] / wsum;
29913            let d = (dw[r] - host).abs();
29914            assert!(
29915                d <= 1e-6 * host.abs().max(1.0),
29916                "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
29917                dw[r]
29918            );
29919        }
29920        assert_eq!(idx[top_k] as usize, n, "shared pin slot");
29921        assert_eq!(dw[top_k], 1.0, "shared weight");
29922
29923        // Make exactly two of the CHOSEN picks cold.
29924        let mut remap = resident.clone();
29925        remap[ci[0]] = u32::MAX;
29926        remap[ci[2]] = u32::MAX;
29927        let mut cold = Vec::new();
29928        let mut idx2 = Vec::new();
29929        let mut dw2 = Vec::new();
29930        assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
29931            &logits,
29932            Some(&bias),
29933            top_k,
29934            2.0,
29935            &remap,
29936            n as u32,
29937            &mut cold,
29938            &mut idx2,
29939            &mut dw2,
29940        ));
29941        // The resident picks of the partial bank still carry exact weights.
29942        // A COLD rank is a placeholder the device writes as slot 0 / weight
29943        // 0.0 — its real (id, weight) pair rides in `cold` — so weight zero
29944        // is what marks it, not the slot value: reading the placeholder as
29945        // a resident expert compared 0.0 against p[0]/wsum and failed with
29946        // Δ=2.552e-1. The test logits keep every sigmoid score strictly
29947        // positive, so a resident pick can never weigh exactly zero.
29948        let mut resident_weight_checked = 0usize;
29949        for (r, &slot) in idx2.iter().enumerate().take(top_k) {
29950            if dw2[r] == 0.0 {
29951                continue; // cold rank: placeholder, not an expert id
29952            }
29953            let e = slot as usize;
29954            assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
29955            let host = p[e] / wsum;
29956            let d = (dw2[r] - host).abs();
29957            assert!(
29958                d <= 1e-6 * host.abs().max(1.0),
29959                "partial-bank resident weight expert {e}: Δ={d:.3e}"
29960            );
29961            resident_weight_checked += 1;
29962        }
29963        assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
29964        println!("partial bank: checked {resident_weight_checked} resident weights");
29965        assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
29966        let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
29967        ids.sort_unstable();
29968        let mut want = vec![ci[0], ci[2]];
29969        want.sort_unstable();
29970        assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
29971        for &(e, w) in &cold {
29972            let host = p[e] / wsum;
29973            let d = (w - host).abs();
29974            println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
29975            assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
29976        }
29977    }
29978
29979    /// The resonance router (top-1) selects by the raw score as the
29980    /// trainer and the graph do — not by softmax probabilities, where two
29981    /// scores closer than 2^-25 collapse to the same `exp(l − max) = 1.0`
29982    /// and the LOWER index wins a token whose score is strictly smaller.
29983    #[test]
29984    fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
29985        let zero = || DenseFfn {
29986            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29987            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29988            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29989            act: Act::Silu,
29990            down_t: None,
29991            segs: Vec::new(),
29992        };
29993        let moe = |resonant: bool, norm_topk: bool| MoeFfn {
29994            router: QTensor::from_f32(vec![0.0; 6], 3, 2),
29995            experts: vec![zero(), zero(), zero()],
29996            top_k: 1,
29997            norm_topk_prob: norm_topk,
29998            router_sigmoid: false,
29999            sigmoid_denom_eps: 1e-6,
30000            expert_bias: None,
30001            routed_scaling: 1.0,
30002            route_tau: None,
30003            shared: None,
30004            stats: std::cell::RefCell::new(Vec::new()),
30005            act_sq: std::cell::RefCell::new(Vec::new()),
30006            act_rows: std::cell::RefCell::new(Vec::new()),
30007            mask: None,
30008            per_expert_scale: None,
30009            router_input_norm: false,
30010            resonance: resonant.then(|| Resonance {
30011                mu: vec![0.0; 6],
30012                u: Vec::new(),
30013                k: 0,
30014                bias: vec![0.0; 3],
30015                shell: Vec::new(),
30016            }),
30017            grown: Vec::new(),
30018        };
30019        // lo = −0.1, hi = the next f32 towards zero: hi − lo = 2^-27 <
30020        // 2^-25, so exp(lo − hi) rounds to exactly 1.0 — a softmax tie.
30021        let lo = -0.1f32;
30022        let hi = f32::from_bits(lo.to_bits() - 1);
30023        assert!(hi > lo && hi - lo < 2f32.powi(-25));
30024        assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
30025        // The gated MoE (softmax) path: the tie hands the token to index 0.
30026        let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
30027        assert_eq!(
30028            idx,
30029            vec![0],
30030            "softmax tie → lower index (the gated contract)"
30031        );
30032        // The resonance path: the strictly larger raw score wins, weight
30033        // exactly 1.0 with and without norm_topk.
30034        for norm in [true, false] {
30035            let m = moe(true, norm);
30036            let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
30037            assert_eq!(idx, vec![1], "norm_topk {norm}");
30038            assert_eq!(p, vec![0.0, 1.0, 0.0]);
30039            assert_eq!(p[1] / wsum, 1.0);
30040            // An exact tie: the first maximum (as `resonance_winner` and
30041            // `embryo_core_route_pick`).
30042            let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
30043            assert_eq!(idx, vec![0]);
30044            // `−∞` never wins; the admitted set is honoured.
30045            let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
30046            assert_eq!(idx, vec![2]);
30047            let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
30048            assert_eq!(idx, vec![0]);
30049            assert_eq!(p[0] / wsum, 1.0);
30050            // Every admitted expert at −∞: the generic path's uniform
30051            // fallback (lowest index, finite weights).
30052            let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
30053            assert_eq!(idx, vec![0]);
30054            assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
30055        }
30056    }
30057}