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;
16use crate::kv_cache::KvCache;
17use crate::linear_core::{
18    GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
19    gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
20    vmf_phase_pair,
21};
22use crate::pool::Pool;
23use crate::qtensor::QTensor;
24use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
25use crate::tokenizer::Tokenizer;
26use cortiq_core::mask::TaskMask;
27use cortiq_core::types::NormStyle;
28
29pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
30    std::sync::atomic::AtomicBool::new(false);
31
32/// Reusable per-pipeline forward scratch: the four norm outputs the
33/// decode paths recompute every layer (single: n1/p1; pair: all four).
34/// Plain buffers, resized once — steady-state decode reuses them.
35struct ForwardScratch {
36    n1: Vec<f32>,
37    n2: Vec<f32>,
38    p1: Vec<f32>,
39    p2: Vec<f32>,
40}
41
42impl ForwardScratch {
43    fn new(hidden: usize) -> Self {
44        Self {
45            n1: vec![0.0; hidden],
46            n2: vec![0.0; hidden],
47            p1: vec![0.0; hidden],
48            p2: vec![0.0; hidden],
49        }
50    }
51}
52
53/// Complete inference pipeline state.
54pub struct Pipeline {
55    /// In-process layer split across local GPUs: (device, first layer,
56    /// last layer) per segment, in execution order. `None` = one device.
57    /// Arc so cloning the plan out of `&mut self` does not fight the
58    /// borrow checker on the hot path.
59    gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
60    /// Arc: the server shares one tokenizer handle across request
61    /// handlers without borrowing a pipeline slot.
62    pub tokenizer: std::sync::Arc<Tokenizer>,
63    pub kv_cache: KvCache,
64    pub sampler_config: SamplerConfig,
65    pub weights: PipelineWeights,
66    pub hidden_size: usize,
67    pub intermediate_size: usize,
68    pub num_heads: usize,
69    pub num_kv_heads: usize,
70    pub head_dim: usize,
71    /// Total virtual layers (num_layers × num_loops for looped models).
72    pub num_layers: usize,
73    /// Physical layers in weights.layers (≤ num_layers for looped models).
74    pub physical_layers: usize,
75    /// Looped Transformer: apply final norm after each loop iteration.
76    pub loop_final_norm: bool,
77    pub vocab_size: usize,
78    pub rms_eps: f64,
79    pub rope_base: f32,
80    pub norm_style: NormStyle,
81    /// RoPE dims actually rotated (≤ head_dim; Qwen3.5 uses head_dim/4).
82    pub rotary_dim: usize,
83    /// Optional Q-head count override for each attention layer (Laguna).
84    pub attention_heads_per_layer: Option<Vec<usize>>,
85    /// Optional KV-head count of each PHYSICAL attention layer (MiMo-V2:
86    /// 4 on full-attention layers, 8 on sliding ones). Set only through
87    /// [`Pipeline::set_attn_geometry`], which also reshapes the layer
88    /// caches. None = every layer has `num_kv_heads`.
89    pub kv_heads_per_layer: Option<Vec<usize>>,
90    /// Width of each V head when it is narrower than `head_dim` (MiMo-V2:
91    /// 128 against 192). V is zero-padded to `head_dim` inside the cache
92    /// and the attention output is compacted back to nh·v_head_dim before
93    /// o_proj (see `QwenAttnCfg::v_head_dim`). None = `head_dim`.
94    pub v_head_dim: Option<usize>,
95    /// `CMF_LAYER_DUMP=<dir>` (read once at construction; tests set it
96    /// directly): the hidden state after every layer, for every position,
97    /// as raw little-endian f32 files `p{pos:06}_l{li:02}.f32` of
98    /// `hidden_size` floats each. Written by the CPU layer walks — the
99    /// batched prefill (`prefill_batch_span`) and the single-token forward
100    /// (`forward_layers_span`) — so both prompt ingest and decode can be
101    /// diffed layer by layer against an external oracle (tools/mimo_ref).
102    /// Layers a device graph runs (wgpu token/batch graph, Metal chunk or
103    /// block graphs) and the own-stack families (DeepSeek-V4/V4.1,
104    /// Qwen3.8-Flash-Next, Gemma-3n) are not dumped: run with CMF_GPU=0
105    /// for a complete set. `li` is the virtual layer index. Final logits:
106    /// `CMF_LOGIT_DUMP=<file>` (hidden + logits of the first decode step).
107    pub layer_dump: Option<std::path::PathBuf>,
108    /// GPU-graph declines already logged for this pipeline, as (graph
109    /// site, reason) — one line each, see `graph_attn_decline_reason`.
110    graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
111    /// MiMo-V2 expert placement (prefix / dynamic bank / hybrid), decided
112    /// on the first forward — see `crate::mimo_moe`.
113    pub(crate) mimo_moe: crate::mimo_moe::Slot,
114    /// Opt-in MiniCPM q4tp expert cache for a graph-produced CPU tail.
115    /// It is deliberately separate from MiMo's placement policy because it
116    /// serves only the stock MiniCPM top-8 geometry and otherwise falls back
117    /// to the ordinary exact host MoE path.
118    pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
119    /// Linear-core geometry (present when the model has linear layers).
120    pub vmf_cfg: Option<VmfPhaseCfg>,
121    /// GatedDeltaNet geometry (faithful vendor operator).
122    pub gdn_cfg: Option<GdnCfg>,
123    /// MiniCPM-class logit scale (tied lm_head → cannot fold into weights).
124    pub logit_multiplier: Option<f32>,
125    /// Cooperative cancel: set from any thread (FFI `cortiq_cancel`,
126    /// a dropped server connection); the generate loop checks it at
127    /// every prefill chunk and decode step and finishes with
128    /// `finish_reason: "cancelled"`. Auto-cleared when honoured.
129    pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
130    /// A GPU graph failure is distinct from a user/request cancellation.
131    /// Graph code sets this before raising the cooperative cancel flag so the
132    /// generation API can return an error instead of reporting a successful
133    /// `finish_reason: cancelled` result.
134    graph_failed: std::sync::atomic::AtomicBool,
135    /// Token ids currently materialized in the KV cache (the forwarded
136    /// prompt + all generated tokens except the last, which is sampled
137    /// but not yet forwarded). Lets the next generate call prefill only
138    /// the suffix when a chat app resends the whole history.
139    pub kv_history: Vec<u32>,
140    /// Owner tag of `kv_history`: the resident device graph held that
141    /// sequence (true) or the host did. Reuse continues only on the owner.
142    pub kv_history_device: bool,
143    /// KDA geometry (Kimi Linear / Kimi-K3) — shared by every Kda layer.
144    pub kda_cfg: Option<crate::linear_core::KdaCfg>,
145    /// Gemma-3n stack (AltUp/LAuReL/PLE/KV-sharing): its own forward —
146    /// weights.layers stays empty, the KV caches are the shared ones.
147    pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
148    /// DeepSeek-V4 runs its own stack too: its hidden state is `hc_mult`
149    /// copies of a vector, so no loop written for a single residual
150    /// stream can carry it.
151    pub dsv4: Option<
152        Box<(
153            crate::dsv4::Dsv4Globals,
154            Vec<crate::dsv4::Dsv4Layer>,
155            crate::dsv4::Dsv4Cfg,
156            crate::dsv4::Dsv4State,
157        )>,
158    >,
159    /// DeepSeek-V4.1 owns the shared CED/CSA2 attention state, raw Engram
160    /// lookup and four-stream mHC handoff. It cannot use the V4 cache
161    /// layout, so it has a dedicated executor and state tuple.
162    pub dsv41: Option<
163        Box<(
164            crate::dsv41::Dsv41Globals,
165            Vec<crate::dsv41::Dsv41Layer>,
166            crate::dsv41::Dsv41Cfg,
167            crate::dsv41::Dsv41State,
168        )>,
169    >,
170    /// Optional V4.1 vision tower. Text-only files leave this unset.
171    pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
172    /// Prepared image rows consumed by the next V4.1 prefill.
173    dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
174    /// Qwen3.8-Flash-Next owns four residual streams plus QSA/PLE state;
175    /// the generic single-residual layer loop cannot represent it.
176    pub qwen4_exp: Option<
177        Box<(
178            crate::qwen4_exp::Globals,
179            Vec<crate::qwen4_exp::Layer>,
180            crate::qwen4_exp::Cfg,
181            crate::qwen4_exp::State,
182        )>,
183    >,
184    /// DeepSeek-V4's own speculation stack: three draft modules, each a full
185    /// layer, plus a confidence head on the last. Empty when the file has
186    /// none, which is the only signal the decode path needs.
187    pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
188    /// The draft's per-sequence state (KV rings, captured trunk hidden).
189    pub dspark: Option<crate::dsv4::DsparkState>,
190    /// Drafts awaiting their verdict: (position, proposals, still matching,
191    /// accepted so far).
192    pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
193    /// Accepted prefix length of every graded draft.
194    pub dspark_hist: Vec<usize>,
195    /// The real tokens the drafts were graded against — a degenerate,
196    /// repeating output would make any acceptance number meaningless, and
197    /// the cheapest guard against believing one is to count them.
198    pub dspark_real: Vec<u32>,
199    /// The trunk's expert picks for the last few tokens, per layer. The
200    /// union over a window of them is what a batched verify would have to
201    /// read, and the ratio to the pick count is all it could save.
202    pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
203    /// (unique, total) expert picks per draft, trunk side and draft side.
204    pub dspark_exp: Vec<(usize, usize, usize, usize)>,
205    /// Wall time spent in the deliberately out-of-core draft. Kept separate
206    /// from trunk decode so block batching can be judged without conflating
207    /// it with GPU chain variance.
208    pub dspark_draft_ns: u128,
209    /// LFM2 short-convolution geometry (present when the model has
210    /// `ShortConv` mixer layers).
211    pub short_conv_cfg: Option<ShortConvCfg>,
212    /// Multi-token-prediction head (None = absent).
213    pub mtp: Option<MtpModule>,
214    /// MiMo-V2's draft stack (three chained MTP layers from the
215    /// `<stem>.mtp.cmf` sidecar); None = absent. Speculative greedy decode
216    /// uses it unless `CMF_MTP=0` / `CMF_MIMO_MTP=0`.
217    pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
218    /// Set while the MiMo speculative verify runs `prefill_batch`: its MoE
219    /// layers take `moe_ffn_rows_exact` (each row bit-identical to decode).
220    verify_exact_moe: bool,
221    /// Speculative decode via MTP (greedy only; `CMF_MTP=0` disables).
222    pub speculative: bool,
223    /// Keep generating past end-of-sequence ids (the llama-bench contract
224    /// for a timed run). A loop flag, deliberately NOT a sampler
225    /// suppression: suppressed ids count as a penalty and switch the
226    /// speculative round and the greedy burst off, so a benchmark that
227    /// suppressed EOS never measured either.
228    pub ignore_eos: bool,
229    /// Draft-head shortlist guard: tokens left during which the draft
230    /// uses the FULL head because a recently committed id lay past the
231    /// `CMF_DRAFT_VOCAB` cut (Cyrillic and CJK ids sit above 131072 in
232    /// Qwen's table, so a prefix shortlist would draft nothing usable
233    /// there — measured on Russian prose: 2.9 → 1.6 accepted a round).
234    pub draft_full_streak: u32,
235    /// Adaptive draft depth for the speculative round (None until the
236    /// first round): grows while nearly every draft is accepted, shrinks
237    /// when fewer than half are. The verify's cost climbs with the rows on
238    /// a discrete card (RTX PRO 4000: 52 ms at 2 rows, 74 at 5, 80 at 6),
239    /// so prose wants k≈3 and code or the repetitive bench k≈5 — measured
240    /// 33.6 vs 27.6 tok/s on an essay at k=3 vs 5, 45.6 vs 38 on code.
241    /// `CMF_GRAPH_SPEC_K` pins it.
242    pub spec_k_adapt: Option<usize>,
243    /// EWMA of the accepted fraction that drives `spec_k_adapt`.
244    pub spec_acc_ewma: f32,
245    rng: SplitMix64,
246    sampler_scratch: SamplerScratch,
247    /// Speculative SAMPLING state (graph_spec_step, temperature > 0): the
248    /// correction token a rejected draft produced — committed by the loop
249    /// top in place of a fresh draw — and the per-round draft
250    /// distributions / target scratch, reused so a round allocates
251    /// nothing at the vocab size.
252    spec_forced: Option<u32>,
253    spec_q: Vec<Vec<f32>>,
254    spec_p: Vec<f32>,
255    spec_res: Vec<f32>,
256    /// The same three for the sparse chain (top-k configs).
257    spec_qs: Vec<sampler::Sparse>,
258    spec_ps: sampler::Sparse,
259    spec_ress: sampler::Sparse,
260    /// Which arm the MTP draft block runs on this generation: Some(true)
261    /// = the whole-token graph (device attention, one submit a step),
262    /// Some(false) = the per-op path; None = not decided yet. Decided
263    /// on the first draft and held, because the two arms keep the MTP
264    /// KV in different places (device mirror vs the CPU cache) and a
265    /// mid-run switch would read the wrong one.
266    mtp_graph_mode: Option<bool>,
267    /// The Metal verify graph of the round in flight, between its sync
268    /// (logits read) and the commit that replays the accepted prefix.
269    #[cfg(target_os = "macos")]
270    metal_verify: Option<MetalVerifyPending>,
271    /// Precomputed RoPE inverse frequencies [head_dim/2]. Arc: the
272    /// forward path clones a handle to escape the &mut self borrow —
273    /// cloning the table itself was a per-forward allocation.
274    pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
275    /// Reusable norm buffers for the decode hot path (roadmap §3 P0:
276    /// steady-state forward should not heap-allocate). Disjoint field
277    /// from `weights`/`kv_cache`, so split borrows keep working.
278    ws: ForwardScratch,
279    /// Persistent worker pool (None = serial; see CMF_THREADS).
280    pool: Option<std::sync::Arc<Pool>>,
281    // ── Dynamic per-token skill routing (spec §9, claim 14/16) ──
282    /// Source model, retained so a skill switch can re-resolve the
283    /// touched layers' FFN tensors (Mapped = mmap pointers, cheap).
284    pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
285    /// Masks present → weights are dequantized f32 (rebuild path).
286    pub(crate) dyn_force_f32: bool,
287    /// Per-skill FFN layers actually replaced (derived from tensors, not
288    /// the meta `layers` field — ru2 replaces down_proj in 0..23 while
289    /// its meta says [20..23]). None = skill touches non-FFN tensors →
290    /// ineligible for cheap dynamic switching (honest refusal).
291    pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
292    /// Currently overlaid skill (index into model.header.skills); None =
293    /// backbone. Set at load time to the statically-overlaid skill so
294    /// `set_active_skill(None)` correctly reverts it (else a static
295    /// skill would silently persist — the union-diff assumes dyn_active
296    /// always mirrors the live overlay). Switched by `set_active_skill`.
297    pub(crate) dyn_active: Option<usize>,
298    /// Pipeline was loaded with a soft blend (materialized working
299    /// tensors, not a single skill index) → dynamic routing refuses:
300    /// there is no single index to revert the blend from.
301    pub(crate) dyn_blend_loaded: bool,
302    /// Layer whose post-residual hidden feeds the router φ (shared by
303    /// swarm skills). None = φ capture off.
304    pub(crate) dyn_phi_layer: Option<usize>,
305    /// EMA of φ at `dyn_phi_layer` over the decode window (on-policy).
306    dyn_phi_ema: Vec<f32>,
307    dyn_phi_seen: usize,
308    /// Hysteresis router driving per-token skill switches during decode
309    /// (None = static/no dynamic routing). Taken out during generation.
310    pub dyn_router: Option<crate::swarm::DynRouter>,
311    /// O(1) Nyström attention setting (CLI/env/header-hint resolved by
312    /// the caller; None = plain cache attention everywhere).
313    o1_cfg: Option<crate::nystrom::O1Cfg>,
314    /// Bumped once per collecting→sealed transition — the GPU state mirror
315    /// re-uploads when it sees a new epoch (each fresh sealed state).
316    o1_epoch: u64,
317    /// Per-layer o1 flags derived from `o1_cfg` (Full layers only).
318    o1_flags: Vec<bool>,
319    /// Emit a structured per-token trace (B4 telemetry channel). Off by
320    /// default — the runtime is silent unless observation is requested.
321    trace: bool,
322    /// Confidence-calibration temperature (B1): reported probability is
323    /// softmax(logits / calib_temp). 1.0 = raw. Set from header.calibration.
324    calib_temp: f32,
325    /// Process-unique id keying this pipeline's device KV mirrors.
326    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
327    graph_kv_id: u64,
328    /// Decode asks the token graph to also run final-norm + lm_head on
329    /// the device (drops the separate per-op lm_head round trip).
330    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
331    graph_want_logits: bool,
332    /// NLL quality gates require the graph's fused head rather than silently
333    /// accepting a CPU head fallback. Generation keeps the historical
334    /// best-effort `graph_want_logits` behavior.
335    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
336    graph_head_required: bool,
337    /// Logits the graph produced for the token just forwarded (taken by
338    /// the decode loop; None = compute on the CPU path).
339    graph_logits: Option<Vec<f32>>,
340    /// Packed Embryo graph model, built lazily on the first eligible
341    /// resident call so other architectures pay no packing cost.
342    embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
343    /// This pipeline's token graph refused for a STRUCTURAL reason (its
344    /// own weights / layer kinds): not retried. Per pipeline, never
345    /// process-wide: a skill lane whose pack does not build must not flip
346    /// the backbone slot mid-sequence onto the host path (and a new lane
347    /// must not clear the backbone's verdict) — R4/NF-2.
348    graph_refused: std::sync::atomic::AtomicBool,
349    /// Token embeddings are multiplied by this at input (Gemma: √hidden).
350    pub embed_multiplier: f32,
351    /// Attention score scale (1/√head_dim unless the arch overrides —
352    /// Gemma's query_pre_attn_scalar).
353    pub attn_scale: f32,
354    /// Sliding-window attention: (window, every-Nth-layer-is-global
355    /// pattern) — Gemma-3.
356    pub swa: Option<(usize, usize)>,
357    /// Sliding-window tail trimming, `(slack, align)` for
358    /// `LayerKvCache::trim_window`: every layer with a window keeps only
359    /// the rows its window can still read (Spark-X2.5: 576..639 rows per
360    /// 512-window layer instead of the whole context — 27 of the 4B's 36
361    /// layers). Set at load for Spark-X2.5 only (`CMF_SWA_TRIM=0` turns
362    /// it off); None keeps every row, as every other model always has.
363    pub swa_trim: Option<(usize, usize)>,
364    /// Explicit local/global schedule for architectures that cannot be
365    /// represented by Gemma's every-Nth-global convention.
366    pub sliding_layers: Option<Vec<bool>>,
367    /// Per-layer flag: this layer runs WITHOUT RoPE. Recursive Flint applies
368    /// position encoding only on its sliding-window layers; its 14
369    /// full-attention layers have none. The rotary dim of such a layer is 0,
370    /// which is what the attention kernel reads.
371    pub no_rope_layers: Option<Vec<bool>>,
372    /// Natively bounded anchor record (`arch.anchor_core`): the file's
373    /// operator, installed once at load. `Some` = the model is
374    /// bounded-native — `--o1`/`CMF_O1*` are refused, prefix reuse and
375    /// the penalty window are bounded, and no anchor layer stores
376    /// anything per position.
377    pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
378    /// The `[W][rd/2]` relative-rotation table every bounded layer shares
379    /// (built from `inv_freq` once the RoPE setup is final).
380    bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
381    /// Bounded prefix-reuse key (length + rolling hash + tail) of a
382    /// bounded-native model; `kv_history` stays empty there so nothing
383    /// in the pipeline grows with the dialogue.
384    pub kv_prefix: KvPrefix,
385    /// Prompt positions the last `generate*` call actually forwarded
386    /// (prefix reuse subtracts what the cache already held).
387    pub last_prefill_tokens: usize,
388    /// RoPE table of the sliding (local) layers, when they use their
389    /// own base frequency (Gemma-3: 10k local vs 1M global).
390    pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
391    pub rotary_dim_local: Option<usize>,
392    pub rope_scale: f32,
393    pub rope_scale_local: f32,
394    /// Gemma-4: global layers run their own geometry — (head_dim,
395    /// num_kv_heads); sliding layers keep the base fields.
396    pub global_attn: Option<(usize, usize)>,
397    /// Gemma-4: the global layers' proportional RoPE table (len
398    /// global_head_dim/2, zero-padded tail = identity rotation).
399    pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
400    /// Scale-less RMS normalization of V heads before caching (Gemma-4).
401    pub attn_v_norm: bool,
402    /// HunYuan dense: per-head q/k norm runs after RoPE (see the arch flag).
403    pub qk_norm_after_rope: bool,
404    /// The per-head `self_attn.g_proj` output gate uses sigmoid
405    /// (Spark-X2.5) instead of softplus (Laguna).
406    pub proj_gate_sigmoid: bool,
407    /// Final-logit soft-capping C: logits = C·tanh(logits/C) (Gemma-4).
408    pub final_softcap: Option<f32>,
409    /// Cortiq Embryo hierarchical head: cluster matrix [C, hidden]. The
410    /// flat logits h·Eᵀ are turned into the two-level log-probabilities
411    /// log softmax_c(h·Cᵀ)[c(v)] + log softmax_{s∈c(v)}(h·E_c(v)ᵀ)[v].
412    pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
413    /// Gemma-2 attention-logit soft-capping (0.0 = off).
414    pub attn_softcap: f32,
415    /// Compute per-token confidence (a full-vocab softmax each
416    /// token). On by default; `bench --core` turns it off to match
417    /// llama-bench's core timing.
418    confidence_on: bool,
419    /// Test-only one-shot forward failure, scoped to this pipeline so
420    /// parallel scoring tests cannot consume one another's injection.
421    #[cfg(test)]
422    nll_test_fail_at: Option<usize>,
423    /// Test-only route override; avoids mutating the process-wide
424    /// `CMF_PREFILL` environment variable while forcing the serial path.
425    #[cfg(test)]
426    nll_test_force_serial: bool,
427}
428
429#[cfg(target_os = "macos")]
430impl Drop for Pipeline {
431    fn drop(&mut self) {
432        // the async replay writes into `kv_cache` Vecs about to be freed
433        let _ = crate::gpu_metal::wait_replay();
434        crate::gpu::kv_mirror_drop(self.graph_kv_id);
435    }
436}
437
438#[cfg(not(target_os = "macos"))]
439impl Drop for Pipeline {
440    fn drop(&mut self) {
441        // The wgpu resident Embryo graph owns recurrent/KV buffers keyed by
442        // this pipeline's sequence id.  Release that sequence image when a
443        // pooled pipeline is dropped; model weights stay cached for reuse.
444        crate::gpu::graph_kv_reset(self.graph_kv_id);
445    }
446}
447
448/// Model weights. Matrices are `QTensor` (owned f32 for small models
449/// and tests — bit-identical to the historical paths — or quantized
450/// bytes zero-copy from the CMF mmap for big models). 1-D norms are
451/// always small and stay f32.
452pub struct PipelineWeights {
453    /// Embedding table: [vocab_size, hidden_size]
454    pub embed_tokens: QTensor,
455    /// Per-layer weights
456    pub layers: Vec<LayerWeights>,
457    /// LM head: [vocab_size, hidden_size]
458    pub lm_head: QTensor,
459    /// Final norm: [hidden_size]
460    pub final_norm: Vec<f32>,
461}
462
463/// One transformer layer: shared norms + MLP, attention by kind.
464pub struct LayerWeights {
465    pub input_norm: Vec<f32>,
466    /// The pre-FFN norm (`post_attention_layernorm` classically;
467    /// `pre_feedforward_layernorm` on Gemma-2/3 sandwich layers).
468    pub post_norm: Vec<f32>,
469    /// Gemma-2/3 sandwich: norm applied to the ATTENTION OUTPUT before
470    /// its residual add (`post_attention_layernorm` there).
471    pub attn_out_norm: Option<Vec<f32>>,
472    /// Gemma-4: the whole layer output is multiplied by this scalar.
473    pub layer_scale: Option<f32>,
474    /// Gemma-2/3 sandwich: norm applied to the FFN OUTPUT before its
475    /// residual add (`post_feedforward_layernorm`).
476    pub ffn_out_norm: Option<Vec<f32>>,
477    pub ffn: FfnKind,
478    pub attn: AttnKind,
479}
480
481/// FFN gate activation: SiLU (SwiGLU family) or tanh-GELU (Gemma's
482/// GeGLU). A property of the model, carried on every FFN triple.
483#[derive(Clone, Copy, PartialEq, Debug, Default)]
484pub enum Act {
485    #[default]
486    Silu,
487    GeluTanh,
488    /// Exact erf GELU (HF `hidden_act = "gelu"`: Spark-X2.5).
489    Gelu,
490    /// Kimi-K3 SituAndMul: BOTH halves transform —
491    /// a = β·tanh(g/β)·σ(g), up' = linβ·tanh(u/linβ) (linβ>0), out = a·up'.
492    Situ {
493        beta: f32,
494        linear_beta: f32,
495    },
496}
497
498impl Act {
499    pub fn from_arch(name: &str) -> Self {
500        if name == "gelu_tanh" {
501            Self::GeluTanh
502        } else if name == "gelu" {
503            Self::Gelu
504        } else {
505            Self::Silu
506        }
507    }
508
509    /// Arch-driven constructor (activation name + situ betas).
510    pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
511        match arch.hidden_act.as_str() {
512            "situ" => Self::Situ {
513                beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
514                linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
515            },
516            other => Self::from_arch(other),
517        }
518    }
519
520    #[inline]
521    pub fn apply(self, x: f32) -> f32 {
522        match self {
523            Self::Silu => inference::silu(x),
524            Self::GeluTanh => inference::gelu_tanh(x),
525            Self::Gelu => inference::gelu_erf(x),
526            Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
527        }
528    }
529
530    /// Gated combine — the FFN contract. Situ transforms the UP half
531    /// too, so callers must use this instead of apply(g)·u.
532    #[inline]
533    pub fn combine(self, g: f32, u: f32) -> f32 {
534        match self {
535            Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
536                self.apply(g) * (linear_beta * (u / linear_beta).tanh())
537            }
538            _ => self.apply(g) * u,
539        }
540    }
541
542    /// The wgpu graphs' arm for this activation; None = no device kernel
543    /// computes it, and the graph builder must refuse the layer (the dense
544    /// graph FFN once computed SiLU for whatever the model asked for).
545    pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
546        match self {
547            Self::Silu => Some(crate::gpu::GraphAct::Silu),
548            Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
549            Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
550            Self::Situ { .. } => None,
551        }
552    }
553}
554
555/// Dense gated triple — the FFN of a dense layer or of one expert.
556pub struct DenseFfn {
557    pub gate_proj: QTensor,
558    pub up_proj: QTensor,
559    pub down_proj: QTensor,
560    /// Gate activation (SiLU default; Gemma: tanh-GELU).
561    pub act: Act,
562    /// `down_proj` stored transposed (`[inter, hidden]`), when the file
563    /// carries it. Only the per-token sparse path reads it: a neuron's
564    /// down weights are a contiguous ROW there, so the token's chosen
565    /// neurons are the only bytes touched. `None` = the ordinary layout,
566    /// and the sparse path stays off.
567    pub down_t: Option<QTensor>,
568    /// Task tubes (spec: defragged task-conditional width). The three
569    /// matrices above are the CORE — the neurons every task computes;
570    /// each tube is an independently quantized slice of the SAME layer
571    /// holding the neurons only some tasks need. A tube is a normal
572    /// tensor triple, so every kernel runs it unchanged, and the bytes
573    /// of an inactive tube are never read. Empty = ordinary dense FFN.
574    pub segs: Vec<FfnSeg>,
575}
576
577/// One task tube: a contiguous slice of a layer's FFN neurons, stored
578/// as its own `[w, hidden]` / `[hidden, w]` triple. `start` is the
579/// neuron's index in the layer's FULL space (core first, then tubes in
580/// order) — the bit a task mask sets to switch this tube on.
581pub struct FfnSeg {
582    pub gate: QTensor,
583    pub up: QTensor,
584    pub down: QTensor,
585    pub start: usize,
586    pub width: usize,
587}
588
589/// FFN operator of a layer, decided by tensor presence at load time
590/// (router `mlp.gate.weight` in the directory = MoE layer).
591pub enum FfnKind {
592    Dense(DenseFfn),
593    /// Mixture-of-Experts (Qwen2-MoE / Qwen3-MoE): softmax over ALL
594    /// expert logits → top-k, optional renorm; experts stay quantized
595    /// in mmap — only the selected ones are touched per token.
596    Moe(MoeFfn),
597    /// Gemma-4 MoE: a dense MLP branch AND a routed-expert branch in
598    /// the SAME layer, each with its own norm sandwich. The dense
599    /// branch reads the pre-FFN-normed input; the expert branch (and
600    /// the router) read the RAW residual through `pre_norm_2`:
601    ///   d = post_norm_1(dense(x̂));  m = post_norm_2(Σwₑ·FFNₑ(pre_norm_2(h)))
602    ///   ffn_out = d + m   (the caller's ffn_out_norm + residual follow)
603    DenseMoe(Box<DenseMoeFfn>),
604}
605
606/// Gemma-4 dual-branch FFN (see `FfnKind::DenseMoe`).
607pub struct DenseMoeFfn {
608    pub dense: DenseFfn,
609    pub moe: MoeFfn,
610    /// post_feedforward_layernorm_1 — dense-branch output norm.
611    pub post_norm_1: Vec<f32>,
612    /// pre_feedforward_layernorm_2 — expert-branch input norm (applied
613    /// to the RAW residual, not the pre-FFN-normed activation).
614    pub pre_norm_2: Vec<f32>,
615    /// post_feedforward_layernorm_2 — expert-branch output norm.
616    pub post_norm_2: Vec<f32>,
617}
618
619pub struct MoeFfn {
620    /// Router `mlp.gate.weight` [num_experts, hidden].
621    pub router: QTensor,
622    pub experts: Vec<DenseFfn>,
623    pub top_k: usize,
624    pub norm_topk_prob: bool,
625    /// Router scores per-expert with a sigmoid (LFM2-MoE / DeepSeek-V3
626    /// `noaux_tc`) instead of a softmax over all experts (Qwen).
627    pub router_sigmoid: bool,
628    /// Addend in the selected-score denominator of a sigmoid router.
629    /// Most supported checkpoints use the historical `1e-6` floor; Flint's
630    /// reference routing uses `1e-20`.  This is per-MoE rather than a global
631    /// family switch because the same sigmoid routing law has both contracts.
632    pub sigmoid_denom_eps: f32,
633    /// Per-expert selection bias `mlp.expert_bias` [num_experts]
634    /// (LFM2-MoE): added to the sigmoid scores for the top-k CHOICE only;
635    /// the gathered weights use the unbiased scores. None = no bias.
636    pub expert_bias: Option<Vec<f32>>,
637    /// Top-k weights are multiplied by this after the optional renorm
638    /// (LFM2-MoE `routed_scaling_factor`; 1.0 = off).
639    pub routed_scaling: f32,
640    /// Adaptive routing (CMF_MOE_TAU, opt-in): keep the smallest
641    /// prefix of the top-k whose renormalized mass reaches τ —
642    /// confident tokens touch 1–2 experts, flat ones keep all k.
643    /// MoE decode is memory-bound, so skipped experts are skipped
644    /// weight traffic. None = classic fixed top-k (bit-identical).
645    pub route_tau: Option<f32>,
646    /// Always-on shared expert. Qwen2-MoE carries an additional sigmoid
647    /// gate; Laguna adds the shared expert unconditionally (`None`).
648    pub shared: Option<(DenseFfn, Option<QTensor>)>,
649    /// Expert-selection counters (truncated Fisher B-field of claim 12:
650    /// routing frequency during calibration). Filled by every forward,
651    /// read by the CLI via CMF_MOE_STATS. RefCell: decode is single-threaded.
652    pub stats: std::cell::RefCell<Vec<u64>>,
653    /// Per-CHANNEL sum of squares of this FFN's input, accumulated over a
654    /// calibration run (`CMF_RMS_TRACE`). These are the RMS activation
655    /// traces AWNP needs: raw weight magnitude says every channel matters
656    /// equally, and the question AWNP asks is whether the ACTIVATIONS
657    /// disagree. Off unless the env var is set — an f64 add per channel
658    /// per token is cheap, but not free.
659    pub act_sq: std::cell::RefCell<Vec<f64>>,
660    /// Raw FFN-input rows captured for the layers named by `CMF_ACT_DUMP`
661    /// (`"9,19"`). AWNP is nullspace PROJECTION: after dropping channels the
662    /// survivors are refitted to absorb what was removed, and how much they
663    /// can absorb depends on the activation COVARIANCE, not on per-channel
664    /// RMS. Per-channel numbers can only bound the cost from above.
665    pub act_rows: std::cell::RefCell<Vec<f32>>,
666    /// Task mask over routed experts (DTG-MA over MoE, claim-12 B-field
667    /// applied): `false` experts are excluded from selection, the
668    /// softmax renormalizes over the allowed set. Built by the loader
669    /// from CMF_MOE_MASK=<stats.json> + CMF_MOE_MASK_COVER. None = all.
670    pub mask: Option<Vec<bool>>,
671    /// Gemma-4: per-expert weight scale applied AFTER the top-k renorm
672    /// (`router.per_expert_scale`). None = 1.0 everywhere.
673    pub per_expert_scale: Option<Vec<f32>>,
674    /// Gemma-4: the router reads a SCALE-LESS rms-norm of its input
675    /// (the constant gain router.scale·√hidden is folded into the
676    /// router weights at convert time).
677    pub router_input_norm: bool,
678    /// Cortiq Embryo: resonance routing (P1) — the "logits" are
679    /// bias_e − ‖(x−μ_e) − U_eᵀU_e(x−μ_e)‖², argmax = the expert whose
680    /// descriptor reconstructs the input best. `router` is a placeholder.
681    pub resonance: Option<Resonance>,
682    /// Growth records (`kind = "expert_append"`, spec §9.5.1) mounted
683    /// behind the trunk experts, one entry per grown expert IN THE ORDER
684    /// they sit in `experts` (the tail `experts[experts.len() - grown.len()..]`).
685    /// The trunk keeps `experts.len() - grown.len()` experts. Empty on a
686    /// gated MoE, on a file without records and under `CMF_GROWTH=off`.
687    pub grown: Vec<GrownExpert>,
688}
689
690/// One grown expert of an `expert_append` record as the loader mounted it.
691#[derive(Debug, Clone, PartialEq, Eq)]
692pub struct GrownExpert {
693    /// The record's skill id.
694    pub record: String,
695    /// Position of the record in `header.skills`.
696    pub record_index: usize,
697    pub layer: usize,
698    /// The index the tensor name declares (`experts.{e}` — the chain rule
699    /// of the format); the executed position may be smaller when an
700    /// earlier record of the layer is not mounted.
701    pub expert: usize,
702}
703
704/// Per-expert resonance descriptors of one MoE layer (`mlp.desc.*`).
705pub struct Resonance {
706    /// [E, hidden]
707    pub mu: Vec<f32>,
708    /// [E, k, hidden] orthonormal directions (k may be 0)
709    pub u: Vec<f32>,
710    pub k: usize,
711    /// [E] selection bias (loss-free balancing, trained online)
712    pub bias: Vec<f32>,
713    /// [E] reconstruction-error shell: an expert whose error
714    /// `‖(x−μ)⊥U‖² = d² − proj` exceeds its shell scores `−∞` (it never
715    /// wins). `+inf` = no shell — every trunk expert; a grown expert
716    /// (`expert_append`) carries the finite `desc.shell` its record stores.
717    /// Empty = no shell anywhere (legacy constructors).
718    pub shell: Vec<f32>,
719}
720
721/// `CMF_GROWTH_SHELL` state: 0 = not yet read from the environment, 1 = on,
722/// 2 = off. Process-wide, like the environment it mirrors.
723static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
724
725/// Is the growth shell applied (`Resonance::scores` −∞ rule, the resident
726/// graph's packed shell)? `CMF_GROWTH_SHELL=off` disables it for
727/// measurement; [`set_growth_shell`] overrides the environment in-process
728/// (`growth-eval --shell`). Default: on.
729pub fn growth_shell_enabled() -> bool {
730    use std::sync::atomic::Ordering;
731    match GROWTH_SHELL.load(Ordering::Relaxed) {
732        1 => true,
733        2 => false,
734        _ => {
735            let off = std::env::var("CMF_GROWTH_SHELL")
736                .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
737                .unwrap_or(false);
738            GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
739            !off
740        }
741    }
742}
743
744/// Switch the growth shell on/off for this process (`None` = re-read
745/// `CMF_GROWTH_SHELL` on the next query). A pipeline packed into the
746/// resident graph BEFORE the switch keeps the shell it was packed with —
747/// build a new pipeline after switching.
748pub fn set_growth_shell(on: Option<bool>) {
749    GROWTH_SHELL.store(
750        match on {
751            Some(true) => 1,
752            Some(false) => 2,
753            None => 0,
754        },
755        std::sync::atomic::Ordering::Relaxed,
756    );
757}
758
759impl Resonance {
760    /// Does any expert carry a finite shell (a mounted growth record)?
761    pub fn has_shell(&self) -> bool {
762        self.shell.iter().any(|s| s.is_finite())
763    }
764
765    /// The shell the runtime applies right now: the stored one, or all
766    /// `+inf` when the shell is switched off (`CMF_GROWTH_SHELL=off`).
767    pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
768        let mut out = vec![f32::INFINITY; ne];
769        if growth_shell_enabled() {
770            for (o, s) in out.iter_mut().zip(&self.shell) {
771                *o = *s;
772            }
773        }
774        out
775    }
776
777    /// Routing scores for one input row (higher = better). A grown
778    /// expert whose reconstruction error lies outside its shell gets
779    /// `−∞` (unless the shell is switched off); trunk rows are the exact
780    /// bit pattern they were before growth.
781    pub fn scores(&self, x: &[f32], out: &mut [f32]) {
782        let h = x.len();
783        let ne = out.len();
784        let shell_on = growth_shell_enabled() && !self.shell.is_empty();
785        for e in 0..ne {
786            let mu = &self.mu[e * h..(e + 1) * h];
787            let mut d2 = 0.0f32;
788            for j in 0..h {
789                let d = x[j] - mu[j];
790                d2 += d * d;
791            }
792            let mut proj = 0.0f32;
793            for i in 0..self.k {
794                let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
795                let mut p = 0.0f32;
796                for j in 0..h {
797                    p += (x[j] - mu[j]) * u[j];
798                }
799                proj += p * p;
800            }
801            let err = d2 - proj;
802            out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
803            if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
804                out[e] = f32::NEG_INFINITY;
805            }
806        }
807    }
808}
809
810/// Attention operator of a layer. Extension point: new operators are
811/// new variants here + a forward in their own module.
812pub enum AttnKind {
813    /// GQA softmax attention (+ optional Qwen3.5 qk-norm / output gate).
814    Full {
815        wq: QTensor,
816        wk: QTensor,
817        wv: QTensor,
818        wo: QTensor,
819        q_norm: Option<Vec<f32>>,
820        k_norm: Option<Vec<f32>>,
821        output_gate: bool,
822        /// Laguna: a separate softplus projection applied to the attention
823        /// output before O. The bool means one scalar per head (broadcast
824        /// across head_dim); false means one scalar per element.
825        softplus_gate: Option<(QTensor, bool)>,
826        /// Qwen2-family projection biases (q, k, v).
827        bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
828    },
829    /// Canonical linear core (VMF phase attention).
830    Linear(VmfPhaseWeights),
831    /// Faithful vendor linear operator (Qwen3.5 GatedDeltaNet).
832    LinearGdn(GdnWeights),
833    /// LFM2 gated short-convolution mixer (no KV cache; conv ring state
834    /// lives in the layer's `linear_state`).
835    ShortConv(ShortConvWeights),
836    /// DeepSeek-V2 Multi-head Latent Attention. v1 executes it as
837    /// expand-to-MHA: the latent is projected per token, K/V expand to
838    /// every head and live in the ordinary cache (K head layout
839    /// [rope | nope] so the standard partial rotary covers the shared
840    /// rope key; V rows are zero-padded to the K head_dim and the pad
841    /// is sliced off before O). Latent-resident cache is a later
842    /// optimization, not a semantic change.
843    Mla(Box<MlaWeights>),
844    /// Kimi Delta Attention (Kimi Linear / Kimi-K3): per-channel decayed
845    /// delta rule, separate q/k/v short convs, sigmoid-gated output norm.
846    /// State lives in the layer's `linear_state` (no KV cache).
847    Kda(Box<crate::linear_core::KdaWeights>),
848    /// Natively bounded softmax anchor `swa_sink_v1` (Embryo-O1): ring of
849    /// the last W raw keys with relative RoPE + trained NoPE sinks, one
850    /// softmax. State is the fixed-size ring in `LayerKvCache::bounded`;
851    /// nothing is stored per position (see `crate::bounded`).
852    Bounded(Box<crate::bounded::BoundedWeights>),
853}
854
855/// DeepSeek-V2 MLA projections (see `AttnKind::Mla`).
856pub struct MlaWeights {
857    /// `[nh·(rope+nope), hidden]` (or `[…, q_lora]` when compressed) —
858    /// the converter permutes each head rope-first so rotary_dim =
859    /// qk_rope works unchanged.
860    pub q_proj: QTensor,
861    /// Compressed q (K3/V3 class): x → q_a `[q_lora, hidden]` →
862    /// rms(q_a_norm) → q_proj (= q_b). None = direct q (V2-Lite).
863    pub q_a: Option<QTensor>,
864    pub q_a_norm: Option<Vec<f32>>,
865    /// `kv_a_proj_with_mqa` `[lora + rope, hidden]` (latent first).
866    pub kv_a: QTensor,
867    /// RMS-norm weights over the latent (`kv_a_layernorm`, [lora]).
868    pub kv_a_norm: Vec<f32>,
869    /// `[nh·(nope+v), lora]` — per head [k_nope | v].
870    pub kv_b: QTensor,
871    /// `[hidden, nh·v]`.
872    pub o_proj: QTensor,
873    pub nh: usize,
874    pub qk_rope: usize,
875    pub qk_nope: usize,
876    pub v_dim: usize,
877    pub lora: usize,
878    /// Softmax scale (1/√(rope+nope), YaRN-mscale-corrected at load).
879    pub scale: f32,
880    /// Kimi Linear NoPE: skip the rotary entirely (layout unchanged).
881    pub nope: bool,
882}
883
884/// Multi-token-prediction head (DeepSeek/Qwen style, spec §2.1):
885/// `x = eh_proj·[enorm(embed(next)); hnorm(hidden)]` → one transformer
886/// block over its own KV → shared lm_head. Drafts the token after next;
887/// the main model verifies, so output is exact — MTP only buys speed.
888pub struct MtpModule {
889    pub enorm: Vec<f32>,
890    pub hnorm: Vec<f32>,
891    /// [hidden, 2·hidden]
892    pub eh_proj: QTensor,
893    pub layer: LayerWeights,
894    pub final_norm: Vec<f32>,
895    pub kv: crate::kv_cache::LayerKvCache,
896}
897
898/// A Metal verify graph after its sync: what the commit needs — the
899/// graph (per-layer replay scratch), the GDN layers in encode order (their
900/// CPU states receive the replay), and the attention layers with the CPU
901/// row count they were encoded against (the accepted rows are pulled from
902/// the mirror from there).
903/// One item of the Metal rows-graph plan.
904#[cfg(target_os = "macos")]
905enum MetalRowsItem<'a> {
906    Gdn {
907        run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
908        first: usize,
909    },
910    Attn {
911        l: crate::gpu_metal::AttnGpuLayer<'a>,
912        li: usize,
913        q_norm: Option<&'a [f32]>,
914        k_norm: Option<&'a [f32]>,
915        output_gate: bool,
916    },
917}
918
919#[cfg(target_os = "macos")]
920struct MetalVerifyPending {
921    graph: crate::gpu_metal::VerifyGraph,
922    gdn_layers: Vec<usize>,
923    attn_layers: Vec<(usize, usize)>,
924}
925
926/// A round's batched MTP warm-up, submitted but not yet waited
927/// (`mtp_warm_batch_submit` → `mtp_warm_batch_finish`): the trunk commit's
928/// GDN replay is queued between the two.
929#[cfg(target_os = "macos")]
930struct MetalWarmPending {
931    graph: crate::gpu_metal::VerifyGraph,
932    cpu_stored: usize,
933    b: usize,
934}
935
936#[cfg(target_os = "macos")]
937enum MetalRowsRun {
938    /// Capability/preflight refusal before a command buffer was committed.
939    Declined,
940    /// A graph was admitted and then failed; callers must clear the sequence
941    /// rather than replaying it through CPU/serial state.
942    Failed,
943    Completed(MetalVerifyPending),
944}
945
946#[cfg(target_os = "macos")]
947enum MetalPrefillOutcome {
948    Declined,
949    Failed,
950    Completed(Vec<f32>),
951}
952
953#[cfg(target_os = "macos")]
954enum MetalBatchNllOutcome {
955    Declined,
956    Failed(String),
957    Completed(f64, usize),
958}
959
960/// The speculation trial's phases (see the decode loop): four timed
961/// speculative rounds, eight timed plain tokens, then the faster arm
962/// until a re-check.
963#[derive(Clone, Copy)]
964enum SpecTrial {
965    Spec {
966        t0: std::time::Instant,
967        gen0: usize,
968        rounds: usize,
969    },
970    Plain {
971        t0: std::time::Instant,
972        gen0: usize,
973    },
974    Decided {
975        spec: bool,
976        recheck_at: usize,
977    },
978}
979
980/// `CMF_GRAPH_SPEC_TIME`: 0 = off, 1 = one line per speculative round
981/// plus the host stamps of any OUTLIER round (wall > 1.4× the running
982/// median), 2 = the host stamps of every round.
983pub(crate) fn spec_time_level() -> u8 {
984    static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
985    *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
986        Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
987        Err(_) => 0,
988    })
989}
990
991/// The round's host stamps: `spec_stamp(name)` records the time since
992/// the previous stamp (the section that just ended) — from anywhere on
993/// the round's call chain (the Metal verify, the draft step, the commit),
994/// no plumbing. Off (a single atomic load) unless `CMF_GRAPH_SPEC_TIME`
995/// is set; one decode thread at a time is assumed (diagnostics).
996struct SpecStampLog {
997    t_last: std::time::Instant,
998    items: Vec<(&'static str, f32)>,
999}
1000
1001static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1002
1003pub(crate) fn spec_stamp(name: &'static str) {
1004    if spec_time_level() == 0 {
1005        return;
1006    }
1007    if let Ok(mut g) = SPEC_STAMPS.lock() {
1008        if let Some(log) = g.as_mut() {
1009            let now = std::time::Instant::now();
1010            log.items
1011                .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1012            log.t_last = now;
1013        }
1014    }
1015}
1016
1017fn spec_stamps_begin() {
1018    if spec_time_level() == 0 {
1019        return;
1020    }
1021    if let Ok(mut g) = SPEC_STAMPS.lock() {
1022        *g = Some(SpecStampLog {
1023            t_last: std::time::Instant::now(),
1024            items: Vec::with_capacity(64),
1025        });
1026    }
1027}
1028
1029fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1030    SPEC_STAMPS
1031        .lock()
1032        .ok()
1033        .and_then(|mut g| g.take())
1034        .map(|l| l.items)
1035        .unwrap_or_default()
1036}
1037
1038/// One line: every stamp name in first-seen order with its total over the
1039/// round and, when it fired more than once (the draft steps), the count.
1040fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1041    let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1042    for &(n, ms) in items {
1043        match agg.iter_mut().find(|e| e.0 == n) {
1044            Some(e) => {
1045                e.1 += ms;
1046                e.2 += 1;
1047            }
1048            None => agg.push((n, ms, 1)),
1049        }
1050    }
1051    let mut s = String::with_capacity(agg.len() * 16);
1052    for (n, ms, k) in agg {
1053        if k > 1 {
1054            s.push_str(&format!("{n} {ms:.1}/{k} "));
1055        } else {
1056            s.push_str(&format!("{n} {ms:.1} "));
1057        }
1058    }
1059    s
1060}
1061
1062/// The speculation monitor: exponential averages of a round's wall time
1063/// and of the tokens it produced, and the plain token's wall time — the
1064/// three numbers the keep/stop rule needs. A round pays when
1065/// `tokens_per_round · plain_ms > round_ms · 1.03`. The one-shot trial
1066/// (four rounds against eight tokens) mis-called prose: the first rounds
1067/// after a prompt are formulaic and accept well, the body does not (an
1068/// essay measured 39 against a plain 44.8 with the trial saying
1069/// "speculate"), so the rule now runs on EVERY round and stops after four
1070/// consecutive losing rounds; a stopped speculation is retried 128 tokens
1071/// later.
1072///
1073/// Native Metal (`metal: true`) does not pay the eight plain tokens up
1074/// front: on the 27B a plain token is ~150 ms, so the trial alone cost
1075/// ~1.2 s of every answer. There the plain phase is (a) skipped while the
1076/// rounds land at least `SPEC_PROXY_TOKENS` tokens each — a k=7 round on
1077/// Metal costs ~1.9 plain tokens (286 against 148 ms measured on the M4),
1078/// so 3.5 tokens/round cannot lose on any Metal round/plain ratio seen —
1079/// and (b) otherwise bounded to the fewest tokens that time it: two, or
1080/// as many as fit in `SPEC_PLAIN_MIN_MS` (a 150-ms token measures itself;
1081/// a 10-ms one needs the eight). The keep/stop rule itself is unchanged:
1082/// the moment a plain rate exists, it decides.
1083#[derive(Default, Clone, Copy)]
1084struct SpecMon {
1085    round_ms: f64,
1086    tokens: f64,
1087    plain_ms: f64,
1088    n: u32,
1089    fails: u32,
1090    metal: bool,
1091}
1092
1093/// Tokens per round at or above which a Metal round pays without a plain
1094/// measurement (see `SpecMon`).
1095const SPEC_PROXY_TOKENS: f64 = 3.5;
1096/// The Metal plain phase: at least two tokens, and more until this much
1097/// wall time has been timed (up to the eight the other backends time).
1098const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1099
1100impl SpecMon {
1101    fn round(&mut self, dt_ms: f64, produced: usize) {
1102        self.n += 1;
1103        if self.n == 1 {
1104            return; // round 1 pays the batch scratch and the draft mirror
1105        }
1106        let a = if self.n == 2 { 1.0 } else { 0.3 };
1107        self.round_ms += a * (dt_ms - self.round_ms);
1108        self.tokens += a * (produced as f64 - self.tokens);
1109    }
1110    fn pays(&self) -> bool {
1111        if self.plain_ms > 0.0 {
1112            self.tokens * self.plain_ms > self.round_ms * 1.03
1113        } else {
1114            self.metal && self.tokens >= SPEC_PROXY_TOKENS
1115        }
1116    }
1117    /// Has the plain phase timed enough tokens to decide?
1118    fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1119        let n = generated.saturating_sub(gen0);
1120        if n >= 8 {
1121            return true;
1122        }
1123        self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1124    }
1125}
1126
1127/// Ids of the consumed prefix the bounded reuse key remembers literally
1128/// (the rest is covered by the rolling hash).
1129pub const KV_PREFIX_TAIL: usize = 128;
1130
1131/// Bounded prefix-reuse key: how many ids the cache holds, a rolling
1132/// hash of ALL of them and the last [`KV_PREFIX_TAIL`] ids literally.
1133/// Answers "does this prompt strictly extend what is cached" exactly
1134/// (hash over the whole consumed prefix + literal tail) without keeping
1135/// the dialogue — the record is a fixed size whatever the session length.
1136#[derive(Debug, Clone, Default)]
1137pub struct KvPrefix {
1138    len: usize,
1139    hash: u64,
1140    tail: Vec<u32>,
1141    /// Owner of the state this key describes: the resident device graph
1142    /// (true) or the host. A turn continues the prefix only on its owner
1143    /// (R4: a host continuation of a device sequence reads an empty host
1144    /// state; a device continuation of a host sequence has no image).
1145    device: bool,
1146}
1147
1148impl KvPrefix {
1149    #[inline]
1150    fn fold(mut h: u64, ids: &[u32]) -> u64 {
1151        for &id in ids {
1152            h ^= id as u64;
1153            h = h.wrapping_mul(0x100000001b3);
1154            h ^= h >> 29;
1155        }
1156        h
1157    }
1158
1159    pub fn clear(&mut self) {
1160        self.len = 0;
1161        self.hash = 0xcbf29ce484222325;
1162        self.tail.clear();
1163        self.device = false;
1164    }
1165
1166    /// Was the prefix built on the resident device graph?
1167    pub fn on_device(&self) -> bool {
1168        self.device
1169    }
1170
1171    /// Tag the owner of the recorded prefix.
1172    pub fn set_on_device(&mut self, device: bool) {
1173        self.device = device;
1174    }
1175
1176    /// Ids the cache holds (the forwarded prefix).
1177    pub fn len(&self) -> usize {
1178        self.len
1179    }
1180
1181    pub fn is_empty(&self) -> bool {
1182        self.len == 0
1183    }
1184
1185    /// Literal tail currently kept (≤ `KV_PREFIX_TAIL`).
1186    pub fn tail_len(&self) -> usize {
1187        self.tail.len()
1188    }
1189
1190    /// Replace the key with `ids` (a fresh sequence).
1191    pub fn set(&mut self, ids: &[u32]) {
1192        self.clear();
1193        self.extend(ids);
1194    }
1195
1196    /// Append `more` to the consumed prefix (an extension-only turn).
1197    pub fn extend(&mut self, more: &[u32]) {
1198        if self.len == 0 && self.hash == 0 {
1199            self.hash = 0xcbf29ce484222325;
1200        }
1201        self.hash = Self::fold(self.hash, more);
1202        self.len += more.len();
1203        if more.len() >= KV_PREFIX_TAIL {
1204            self.tail.clear();
1205            self.tail
1206                .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1207        } else {
1208            let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1209            self.tail.drain(..drop);
1210            self.tail.extend_from_slice(more);
1211        }
1212    }
1213
1214    /// Cached positions when `ids` strictly extends the consumed prefix,
1215    /// 0 otherwise. The tail is compared literally first (cheap), then
1216    /// the hash over the whole prefix must agree.
1217    pub fn extension(&self, ids: &[u32]) -> usize {
1218        if self.len == 0 || ids.len() <= self.len {
1219            return 0;
1220        }
1221        let t = self.tail.len();
1222        if ids[self.len - t..self.len] != self.tail[..] {
1223            return 0;
1224        }
1225        if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1226            return 0;
1227        }
1228        self.len
1229    }
1230}
1231
1232/// Result of a generation call.
1233pub struct GenerateResult {
1234    pub text: String,
1235    pub token_ids: Vec<u32>,
1236    pub prompt_tokens: usize,
1237    pub tokens_generated: usize,
1238    pub finish_reason: String,
1239    /// Speculative-decode stats (0/0 when MTP is absent or inactive).
1240    pub mtp_drafted: usize,
1241    pub mtp_accepted: usize,
1242    /// Per-generated-token confidence = softmax probability of the token
1243    /// that was actually emitted (softmax probability on the chosen state). High =
1244    /// the model was sure; low = it was guessing. Same length as the
1245    /// generated slice of `token_ids`.
1246    pub token_confidence: Vec<f32>,
1247    /// Structured per-token telemetry (B4 channel). Empty unless
1248    /// `set_trace(true)`; otherwise same length as the generated slice.
1249    pub traces: Vec<TokenTrace>,
1250}
1251
1252/// One row of the structured telemetry trace (B4): the model's internal
1253/// routing state at the moment a token was emitted. Every field is a
1254/// quantity the runtime already computes — nothing is inferred or
1255/// estimated (anti-principle: only measured bytes).
1256#[derive(Clone, Debug)]
1257pub struct TokenTrace {
1258    /// 0-based index within the generated slice.
1259    pub t: usize,
1260    /// The emitted token id.
1261    pub token_id: u32,
1262    /// Softmax probability on the emitted token — how sure the model was.
1263    pub confidence: f32,
1264    /// Skill in force while this token was generated (None = backbone).
1265    pub active_skill: Option<String>,
1266    /// Recon error E = ‖r−BBᵀr‖²/‖φ‖² at the last routing eval — coherence
1267    /// with the active skill's subspace (low = coherent). None = no router
1268    /// or not yet evaluated.
1269    pub recon: Option<f32>,
1270    /// The router changed the active skill right after this token (a
1271    /// domain boundary crossed under the hysteresis barrier).
1272    pub switched: bool,
1273}
1274
1275/// Calibrated softmax probability of `id` under `logits` (the confidence on
1276/// the emitted token) — the confidence signal, cheap from logits already
1277/// computed for sampling. `temp` is the calibration temperature (B1):
1278/// softmax(logits / temp); 1.0 = raw.
1279#[cfg_attr(not(test), allow(dead_code))]
1280fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1281    let t = if temp > 1e-3 { temp } else { 1.0 };
1282    let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1283    let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1284    if sum > 0.0 {
1285        (((logits[id as usize] - max) / t).exp()) / sum
1286    } else {
1287        0.0
1288    }
1289}
1290
1291/// prefill-GEMM enabled? (CMF_PREFILL=seq — emergency fallback to the
1292/// sequential path.)
1293fn prefill_batched() -> bool {
1294    std::env::var("CMF_PREFILL")
1295        .map(|v| v != "seq")
1296        .unwrap_or(true)
1297}
1298
1299/// Decide the graph NLL route without conflating graph quality with the
1300/// optional native-Metal fused head. A hidden-state graph remains a valid
1301/// quality route on Vulkan/Wgpu; only native Metal requires graph logits.
1302#[inline]
1303fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1304    let graph_quality = unmasked && prefer_graph;
1305    let fused_head_quality = graph_quality && native_metal;
1306    (graph_quality, fused_head_quality)
1307}
1308
1309/// Input to the layer-major batched span walk: token ids (embeds itself,
1310/// full-stack and coordinator prefill) or ready boundary hiddens (the
1311/// network worker's side of a split).
1312#[derive(Clone, Copy)]
1313enum PrefillIn<'a> {
1314    Ids(&'a [u32]),
1315    Hidden(&'a [f32]),
1316}
1317
1318/// The batched prefill walks `weights.layers`. Architectures that load
1319/// their own stack (gemma-3n's AltUp replicas, DeepSeek-V4's hyper-
1320/// connections) leave that empty and must go position by position — asking
1321/// otherwise indexes an empty vector, which is a panic rather than a
1322/// fallback. Every call site goes through here so the next such
1323/// architecture is one line, not four.
1324impl Pipeline {
1325    fn can_prefill_batched(&self) -> bool {
1326        #[cfg(test)]
1327        let force_serial = self.nll_test_force_serial;
1328        #[cfg(not(test))]
1329        let force_serial = false;
1330        prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1331    }
1332
1333    /// The backend's automatic capacity split for a mapped transformer.
1334    /// Kept as a method so prefill and decode use the exact same boundary.
1335    fn automatic_gpu_prefix(&self) -> Option<usize> {
1336        let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1337        crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1338    }
1339
1340    /// Positions per batched pass of the layer-stack prefill for THIS
1341    /// model on THIS backend (see [`prefill_chunk_rule`]). Pub: the network
1342    /// split must chunk exactly like the local path to reproduce it.
1343    pub fn prefill_chunk(&self) -> usize {
1344        let env = env_prefill_chunk();
1345        if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1346            return prefill_chunk_rule(env, ChunkHost::here(), false);
1347        }
1348        prefill_chunk_rule(
1349            None,
1350            ChunkHost::Other,
1351            self.chunk_stack_facts().dense_on_discrete(),
1352        )
1353    }
1354
1355    fn chunk_stack_facts(&self) -> ChunkStackFacts {
1356        let plain_dense = !self.weights.layers.is_empty()
1357            && self.g3n.is_none()
1358            && self.dsv4.is_none()
1359            && self.dsv41.is_none()
1360            && self.qwen4_exp.is_none()
1361            && self.weights.layers.iter().all(|lw| {
1362                matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1363            });
1364        let gpu_on = crate::gpu::enabled();
1365        ChunkStackFacts {
1366            plain_dense,
1367            discrete: gpu_on && crate::gpu::discrete(),
1368            gpu_on,
1369            // Only asked when the rest already qualifies: it opens the
1370            // backend's capacity plan.
1371            capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1372                || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1373            multi_gpu: self.gpu_plan.is_some(),
1374            o1: self.o1_active(),
1375        }
1376    }
1377}
1378
1379/// Prefill chunk (positions per batched pass), model-agnostic form. On
1380/// macOS the AMX GEMM path wants tall panels — M=48 starves the matrix
1381/// units (ggml uses ubatch 512); elsewhere the historical 48 stays.
1382/// CMF_PREFILL_CHUNK overrides. The architectures with their own stacks
1383/// (DeepSeek-V4/V4.1) chunk with this; the layer-stack prefill asks
1384/// [`Pipeline::prefill_chunk`], which also knows the model and the card.
1385/// A different chunk is a different (equally valid) generation: panel
1386/// width reorders float accumulation.
1387pub fn prefill_chunk() -> usize {
1388    prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1389}
1390
1391fn env_prefill_chunk() -> Option<usize> {
1392    std::env::var("CMF_PREFILL_CHUNK")
1393        .ok()
1394        .and_then(|v| v.parse::<usize>().ok())
1395}
1396
1397/// The host classes the chunk width distinguishes.
1398#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1399enum ChunkHost {
1400    Macos,
1401    /// Linux/Android aarch64 (phones, SBCs).
1402    Aarch64,
1403    /// Everything else: x86-64 Linux/Windows, CPU or Vulkan/DX12.
1404    Other,
1405}
1406
1407impl ChunkHost {
1408    fn here() -> Self {
1409        if cfg!(target_os = "macos") {
1410            ChunkHost::Macos
1411        } else if cfg!(target_arch = "aarch64") {
1412            ChunkHost::Aarch64
1413        } else {
1414            ChunkHost::Other
1415        }
1416    }
1417}
1418
1419/// Chunk for a plain dense stack whose every layer lives on a discrete
1420/// card. On x86 the layer-stack prefill is host-driven: each GEMM and the
1421/// chunk attention (which re-uploads the whole KV prefix per layer) is a
1422/// separate submit + readback, so 48 positions a pass left the card idle
1423/// between them. Measured in-process on an RTX 3090 (Vulkan), 2048-token
1424/// prompt — see CHANGELOG 0.7.6 for the table.
1425const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1426
1427/// The chunk-width rule. `dense_on_discrete` is true only for a plain
1428/// dense transformer (full attention, dense FFN, no special stack) that
1429/// is entirely resident on one discrete card — the one case measured
1430/// here. GDN hybrids, MoE, DeepSeek stacks, capacity-split and CPU-only
1431/// runs keep the width they were tuned with.
1432fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1433    if let Some(n) = env {
1434        return n.max(1);
1435    }
1436    match host {
1437        ChunkHost::Macos => 512,
1438        // Mobile: big enough to feed the batched attend (gate b ≥ 32)
1439        // and the blocked SDOT GEMM without the memory of 512.
1440        ChunkHost::Aarch64 => 256,
1441        ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1442        ChunkHost::Other => 48,
1443    }
1444}
1445
1446/// What the chunk rule needs to know about a loaded stack.
1447#[derive(Clone, Copy, Debug, Default)]
1448struct ChunkStackFacts {
1449    /// Every layer is `AttnKind::Full` + `FfnKind::Dense`, and no
1450    /// architecture-owned stack (g3n, DeepSeek-V4/V4.1, qwen4-exp) is set.
1451    plain_dense: bool,
1452    /// The active GPU backend is a discrete card.
1453    discrete: bool,
1454    /// The backend is up and not paused.
1455    gpu_on: bool,
1456    /// A capacity-derived device prefix: some layers run on the host.
1457    capacity_split: bool,
1458    /// An in-process multi-GPU plan is set.
1459    multi_gpu: bool,
1460    /// O(1) layers (their Q trace is recorded by the prefill).
1461    o1: bool,
1462}
1463
1464impl ChunkStackFacts {
1465    fn dense_on_discrete(self) -> bool {
1466        self.plain_dense
1467            && self.discrete
1468            && self.gpu_on
1469            && !self.capacity_split
1470            && !self.multi_gpu
1471            && !self.o1
1472    }
1473}
1474
1475/// Number of prompt rows that have a real teacher-forced next-token pair in a
1476/// prefill span.  The final prompt row has no successor token, so it must not
1477/// be handed to the MTP warm-up.  Keeping this arithmetic in one helper makes
1478/// the full-chunk and tail-chunk boundaries explicit for both the graph and
1479/// CPU implementations.
1480#[inline]
1481fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1482    if end <= start || start >= input_len {
1483        return 0;
1484    }
1485    let rows = (end.min(input_len) - start).min(input_len - start);
1486    if end < input_len {
1487        rows
1488    } else {
1489        rows.saturating_sub(1)
1490    }
1491}
1492
1493/// Callback for streaming tokens. Return `false` to cancel.
1494pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1495
1496/// One layer's cache ownership at a cross-turn KV reuse boundary.
1497#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1498pub(crate) struct ReuseLayer {
1499    /// Exact-attention layer (rows in `LayerKvCache`); otherwise a
1500    /// recurrent / latent mixer whose state cannot be rewound.
1501    pub full: bool,
1502    /// Positions the host owner cache reaches (`pos_len`: a trimmed
1503    /// sliding tail stores only the newest of them).
1504    pub host_rows: usize,
1505    /// Rows the wgpu token graph's device mirror holds (None: no mirror).
1506    pub device_rows: Option<usize>,
1507    /// A recurrent state lives on the device (advanced past the host copy).
1508    pub device_state: bool,
1509}
1510
1511/// What a reused turn must do before its tail prefill runs on the HOST.
1512#[derive(Debug, Clone, PartialEq, Eq)]
1513pub(crate) enum ReusePlan {
1514    /// Host caches already hold exactly the reused prefix.
1515    Ready,
1516    /// Copy device mirror rows `[from..to)` into the host cache of each
1517    /// listed layer (the rows decode wrote on the device only).
1518    Pull(Vec<(usize, usize, usize)>),
1519    /// The prefix cannot be continued on the host exactly: start fresh.
1520    Fresh,
1521}
1522
1523/// The wgpu whole-token graph decodes into a DEVICE K/V mirror and never
1524/// writes those rows back to the host cache, while the chunked prefill of a
1525/// pure-attention model reads (and appends to) the host cache. A reused turn
1526/// therefore found its host cache ending at the previous PROMPT, not at the
1527/// previous answer: the tail prefill attended without the model's own
1528/// answer and appended its rows at the wrong index (MiniCPM5 on Vulkan
1529/// repeated its tool call instead of reading the tool result). Every layer
1530/// must hold exactly `reuse_from` host rows before the host continues; rows
1531/// that exist only on the device are pulled back, anything else is fresh.
1532pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1533    let mut pulls = Vec::new();
1534    for (li, l) in layers.iter().enumerate() {
1535        if !l.full {
1536            if l.device_state {
1537                return ReusePlan::Fresh;
1538            }
1539            continue;
1540        }
1541        if l.host_rows == reuse_from {
1542            continue;
1543        }
1544        if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1545            pulls.push((li, l.host_rows, reuse_from));
1546            continue;
1547        }
1548        return ReusePlan::Fresh;
1549    }
1550    if pulls.is_empty() {
1551        ReusePlan::Ready
1552    } else {
1553        ReusePlan::Pull(pulls)
1554    }
1555}
1556
1557impl Pipeline {
1558    /// Clear all per-sequence state, including backend device mirrors.
1559    ///
1560    /// The host KV/history buffers are only half of the request lifecycle on
1561    /// wgpu: GDN/O(1) state and cached graph bind groups are keyed by the
1562    /// pipeline id and otherwise survive a pooled request.  Keep every fresh
1563    /// sequence entry point on this one reset path so a new request cannot
1564    /// inherit the prior request's device state.
1565    fn clear_sequence_state(&mut self) {
1566        // a replay still writing the GDN owners must land before they are
1567        // cleared or reallocated (the device holds raw pointers to them)
1568        #[cfg(target_os = "macos")]
1569        let _ = crate::gpu_metal::wait_replay();
1570        self.kv_cache.clear();
1571        // Both reuse keys (the legacy `kv_history` and the bounded
1572        // `kv_prefix`) describe the state being dropped here.
1573        self.clear_history();
1574        self.graph_logits = None;
1575        if let Some(b) = &mut self.dsv41 {
1576            b.3.clear();
1577        }
1578        crate::gpu::graph_kv_reset(self.graph_kv_id);
1579        // MTP is detached from `self` for the duration of generation, so its
1580        // device mirror is not covered by the trunk reset above.  Reset the
1581        // derived id as well: a failed/aborted warm-up must never leave a
1582        // mirror that a later request can mistake for a current MTP cache.
1583        crate::gpu::graph_kv_reset(self.mtp_kv_id());
1584    }
1585
1586    /// Make the host caches own exactly the reused prefix `[0..reuse_from)`
1587    /// before a reused turn's tail prefill runs on the host (see
1588    /// [`kv_reuse_plan`]). Returns false when the prefix cannot be continued
1589    /// exactly — the caller then starts a fresh sequence. A model whose
1590    /// prefill runs through the token graph keeps its device state as the
1591    /// authority and is left untouched.
1592    fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1593        if self.graph_prefill_preferred() {
1594            return true;
1595        }
1596        let kv_id = self.graph_kv_id;
1597        let layers: Vec<ReuseLayer> = (0..self.num_layers)
1598            .map(|li| {
1599                let full = matches!(
1600                    self.weights.layers[self.phys_layer(li)].attn,
1601                    AttnKind::Full { .. }
1602                );
1603                ReuseLayer {
1604                    full,
1605                    // Absolute depth: a trimmed sliding tail stores fewer
1606                    // rows than the positions it has seen.
1607                    host_rows: self.kv_cache.layers[li].pos_len(),
1608                    device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1609                    device_state: crate::gpu::graph_state_resident(kv_id, li),
1610                }
1611            })
1612            .collect();
1613        // No wgpu device state at all (CPU, Metal — whose graph appends every
1614        // decoded row to the owner cache itself): the host is the owner and
1615        // the extension check already proved the prefix.
1616        if layers
1617            .iter()
1618            .all(|l| l.device_rows.is_none() && !l.device_state)
1619        {
1620            return true;
1621        }
1622        let plan = kv_reuse_plan(reuse_from, &layers);
1623        let (what, rows, n) = match &plan {
1624            ReusePlan::Ready => ("host ready", 0, 0),
1625            ReusePlan::Fresh => ("fresh", 0, 0),
1626            ReusePlan::Pull(p) => (
1627                "pull",
1628                p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1629                p.len(),
1630            ),
1631        };
1632        let t0 = std::time::Instant::now();
1633        let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1634        if std::env::var("CMF_PREFILL_PROF").is_ok() {
1635            eprintln!(
1636                "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1637                if ok { "" } else { " (failed → fresh)" },
1638                t0.elapsed().as_secs_f64() * 1e3
1639            );
1640        }
1641        ok
1642    }
1643
1644    fn apply_kv_reuse_plan(
1645        &mut self,
1646        reuse_from: usize,
1647        plan: ReusePlan,
1648        layers: &[ReuseLayer],
1649    ) -> bool {
1650        let kv_id = self.graph_kv_id;
1651        match plan {
1652            ReusePlan::Fresh => return false,
1653            ReusePlan::Ready => {}
1654            ReusePlan::Pull(pulls) => {
1655                // Mirrors of one uniform geometry: one batched read serves
1656                // every layer. Per-layer geometry (MiMo-V2: 4/8 KV heads,
1657                // narrow V, sliding rings) is read layer by layer in the
1658                // host layout instead.
1659                let (nkv, hd) = {
1660                    let c = &self.kv_cache.layers[pulls[0].0];
1661                    (c.num_kv_heads, c.head_dim)
1662                };
1663                let uniform = pulls.iter().all(|&(li, _, _)| {
1664                    let c = &self.kv_cache.layers[li];
1665                    (c.num_kv_heads, c.head_dim) == (nkv, hd)
1666                });
1667                let batched = if uniform {
1668                    crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1669                } else {
1670                    None
1671                };
1672                let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1673                    Some(rows) => rows,
1674                    None => {
1675                        let mut rows = Vec::with_capacity(pulls.len());
1676                        for &(li, from, to) in &pulls {
1677                            let (lnkv, lhd) = {
1678                                let c = &self.kv_cache.layers[li];
1679                                (c.num_kv_heads, c.head_dim)
1680                            };
1681                            let Some((k, v, first_valid)) =
1682                                crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1683                            else {
1684                                return false;
1685                            };
1686                            // The host continues at `to`: a sliding layer
1687                            // reads back only its last window, a full one
1688                            // every row it lacks.
1689                            let need_from = match self.layer_window(li) {
1690                                Some(w) => from.max((to + 1).saturating_sub(w)),
1691                                None => from,
1692                            };
1693                            if first_valid > need_from {
1694                                return false;
1695                            }
1696                            rows.push((k, v));
1697                        }
1698                        rows
1699                    }
1700                };
1701                for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1702                    let cache = &mut self.kv_cache.layers[li];
1703                    let row = cache.num_kv_heads * cache.head_dim;
1704                    for p in 0..to - from {
1705                        cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1706                    }
1707                    if cache.pos_len() != to {
1708                        return false;
1709                    }
1710                }
1711            }
1712        }
1713        // A mirror past the prefix (a greedy burst that ran beyond the stop)
1714        // holds rows of the OLD continuation: rewind it so the next graph
1715        // token re-syncs those positions from the host.
1716        for (li, l) in layers.iter().enumerate() {
1717            if l.full
1718                && l.device_rows.is_some_and(|d| d > reuse_from)
1719                && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1720            {
1721                return false;
1722            }
1723        }
1724        true
1725    }
1726
1727    /// Finish a generation lifecycle after the MTP/router owners were
1728    /// detached.  Every terminal path must put those owners back before the
1729    /// pooled pipeline can serve another request.  Graph side channels and
1730    /// device mirrors are cleared on errors and cancellations; a successful
1731    /// generation keeps its decode-ready host cache for KV reuse.
1732    fn finish_generation(
1733        &mut self,
1734        mtp: &mut Option<MtpModule>,
1735        router: &mut Option<crate::swarm::DynRouter>,
1736        clear_sequence: bool,
1737    ) {
1738        // A dynamic route may have switched the overlay before the terminal
1739        // path. Restore the backbone while the detached router is still
1740        // available, because set_active_skill also owns the overlay reset.
1741        if router.is_some() {
1742            let _ = self.set_active_skill(None);
1743        }
1744        // The last speculative round's replay may still be in flight on
1745        // the second queue: whoever reads the host cache after generate()
1746        // returns (session export, the network split's KV wire, a KV
1747        // reuse) must see the final states.
1748        // A replay that failed leaves the GDN owners half-written: fail
1749        // closed and drop the sequence instead of handing the cache on.
1750        #[cfg(target_os = "macos")]
1751        let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1752        if clear_sequence {
1753            self.clear_sequence_state();
1754            if let Some(m) = mtp.as_mut() {
1755                // The MTP owner is detached while generation runs, so the
1756                // trunk reset above cannot clear its host cache.  Drop its
1757                // partial rows before reattaching it to the pooled pipeline;
1758                // the next request must start from the same empty anchor on
1759                // CPU and on the device mirror.
1760                m.kv.clear();
1761            }
1762            if let Some(m) = self.mtp.as_mut() {
1763                // A non-speculative request leaves the configured MTP owner
1764                // attached.  Clear that dormant cache too when a shared
1765                // generation failure/cancellation resets the sequence.
1766                m.kv.clear();
1767            }
1768        }
1769        self.graph_want_logits = false;
1770        self.graph_head_required = false;
1771        self.graph_logits = None;
1772        self.graph_failed
1773            .store(false, std::sync::atomic::Ordering::Relaxed);
1774        self.cancel
1775            .store(false, std::sync::atomic::Ordering::Relaxed);
1776        self.dyn_router = router.take().or(self.dyn_router.take());
1777        self.mtp = mtp.take().or(self.mtp.take());
1778        self.mtp_graph_mode = None;
1779        self.spec_forced = None;
1780    }
1781
1782    /// Consume a graph failure reported by a forward that returns only a
1783    /// hidden vector.  `forward_ids` is a public Result API, so it must not
1784    /// turn the graph's zero hidden sentinel into a valid lm_head result.
1785    fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1786        if self
1787            .graph_failed
1788            .swap(false, std::sync::atomic::Ordering::Relaxed)
1789        {
1790            self.cancel
1791                .store(false, std::sync::atomic::Ordering::Relaxed);
1792            self.clear_sequence_state();
1793            self.graph_logits = None;
1794            self.graph_want_logits = false;
1795            self.graph_head_required = false;
1796            return Err(format!("GPU graph failed during {phase} at position {pos}"));
1797        }
1798        Ok(())
1799    }
1800
1801    #[cfg(target_os = "macos")]
1802    fn fail_metal_graph(&mut self, reason: &str) {
1803        crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1804        self.clear_sequence_state();
1805        self.graph_logits = None;
1806        self.graph_failed
1807            .store(true, std::sync::atomic::Ordering::Relaxed);
1808        self.cancel
1809            .store(true, std::sync::atomic::Ordering::Relaxed);
1810        tracing::error!("native Metal TokenGraph failed closed: {reason}");
1811    }
1812
1813    /// Start an NLL/PPL request with all graph side channels in a known
1814    /// state.  A graph failure also raises the cooperative cancel bit; it is
1815    /// consumed here and that graph-induced bit is cleared so an independent
1816    /// request can be reused.  A caller-owned cancellation remains intact.
1817    fn nll_begin(&mut self) -> Result<(), String> {
1818        if self
1819            .graph_failed
1820            .swap(false, std::sync::atomic::Ordering::Relaxed)
1821        {
1822            self.cancel
1823                .store(false, std::sync::atomic::Ordering::Relaxed);
1824            self.clear_sequence_state();
1825            self.graph_logits = None;
1826            self.graph_want_logits = false;
1827            self.graph_head_required = false;
1828            return Err("GPU graph failed before NLL scoring".to_string());
1829        }
1830        self.clear_sequence_state();
1831        self.graph_logits = None;
1832        self.graph_want_logits = false;
1833        self.graph_head_required = false;
1834        Ok(())
1835    }
1836
1837    /// End an NLL/PPL request, including the side channels that are not part
1838    /// of the host KV cache.  This is intentionally explicit instead of
1839    /// relying on a tuple/sentinel return: callers must see every failure.
1840    fn nll_end(&mut self) {
1841        self.clear_sequence_state();
1842        self.graph_logits = None;
1843        self.graph_want_logits = false;
1844        self.graph_head_required = false;
1845        self.graph_failed
1846            .store(false, std::sync::atomic::Ordering::Relaxed);
1847    }
1848
1849    /// Check the graph failure channel at a scoring boundary and leave the
1850    /// pipeline reusable when the device path failed.
1851    fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1852        #[cfg(test)]
1853        if self.nll_test_fail_at == Some(pos) {
1854            self.nll_test_fail_at = None;
1855            self.graph_failed
1856                .store(true, std::sync::atomic::Ordering::Relaxed);
1857            self.cancel
1858                .store(true, std::sync::atomic::Ordering::Relaxed);
1859        }
1860        if self
1861            .graph_failed
1862            .swap(false, std::sync::atomic::Ordering::Relaxed)
1863        {
1864            self.cancel
1865                .store(false, std::sync::atomic::Ordering::Relaxed);
1866            self.clear_sequence_state();
1867            self.graph_logits = None;
1868            self.graph_want_logits = false;
1869            return Err(format!(
1870                "GPU graph failed during NLL {phase} at position {pos}"
1871            ));
1872        }
1873        Ok(())
1874    }
1875
1876    /// Map a virtual layer index to its physical weight index.
1877    /// Looped Transformer (Nanbeige 4.2): 22 physical layers × 2 loops = 44 virtual;
1878    /// virtual layer 23 maps back to physical layer 1 (23 % 22 = 1).
1879    #[inline]
1880    pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1881        virtual_idx % self.physical_layers
1882    }
1883
1884    /// True when `virtual_idx` is the last layer of a loop iteration
1885    /// (used for loop_final_norm insertion).
1886    #[inline]
1887    pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1888        self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1889    }
1890
1891    /// Build a pipeline from parts (used by the loader and tests).
1892    #[allow(clippy::too_many_arguments)]
1893
1894    /// Whole-block q1 token graph on the GPU (macOS/Metal): the run of
1895    /// consecutive q1 layers — GDN *and* full attention — starting at
1896    /// `start` executes as few command buffers as the CPU truly needs.
1897    /// Hidden stays device-resident across every layer; the only syncs
1898    /// are before each CPU attend (it needs q/k/v and owns the KV
1899    /// cache) and the final hidden readback. Recurrent states
1900    /// round-trip through shared memory (the CPU stays their owner, so
1901    /// every other path remains coherent). Returns the first layer
1902    /// index NOT covered (== `start` → refused, caller falls through
1903    /// to the per-layer CPU path).
1904    /// Should prefill run position-by-position through the GPU token
1905    /// graph instead of the batched CPU chunk-GEMM? True for q1 GDN
1906    /// hybrids on native Metal: their chunk prefill is walled by the
1907    /// sequential scalar recurrence, so the graph's decode rate wins.
1908    /// NOT for Looped Transformers, despite the per-chunk loop_final_norm
1909    /// sync: the chunk-GEMM amortizes each weight over the whole chunk,
1910    /// which the per-position graph cannot (Nanbeige 4.2 on M4, 512-token
1911    /// prompt: 85 tok/s chunked vs 14 through the graph).
1912    #[cfg(target_os = "macos")]
1913    fn graph_prefill_preferred(&self) -> bool {
1914        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1915        if !crate::gpu::enabled_here()
1916            || !graph_force
1917            || std::env::var("CMF_GPU_BLOCK")
1918                .map(|v| v == "0")
1919                .unwrap_or(false)
1920            // CMF_PREFILL_GRAPH=0: the chunked prefill (GEMM projections,
1921            // CPU recurrence) instead of the per-position token graph.
1922            || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1923        {
1924            return false;
1925        }
1926        self.weights
1927            .layers
1928            .iter()
1929            .any(|lw| {
1930                matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1931            })
1932    }
1933
1934    /// Prompt ingest through the batched wgpu graph in device-prefix mode:
1935    /// a MoE stack that does not fit the card runs each chunk's leading
1936    /// layers on the device (experts resident) and the rest on the host's
1937    /// batched walk. On by default for models with per-layer attention
1938    /// geometry (MiMo-V2 — its measured default); `CMF_BATCH_PREFIX=1`
1939    /// opts any other MoE model in, `=0` keeps the chunked host prefill.
1940    #[cfg(not(target_os = "macos"))]
1941    fn batch_prefix_prefill(&self) -> bool {
1942        let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
1943            Ok("0") => return false,
1944            Ok("1") => true,
1945            _ => false,
1946        };
1947        crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
1948            && crate::gpu::enabled_here()
1949            && !self.graph_refused()
1950            && (forced || self.graph_attn_decline_reason().is_some())
1951            && self.wgpu_graph_attn_decline().is_none()
1952            && self.attn_softcap == 0.0
1953            && self
1954                .weights
1955                .layers
1956                .iter()
1957                .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1958            && self.automatic_gpu_prefix().is_some()
1959    }
1960
1961    /// Gemma-4's prompt through the batched wgpu graph (`gemma_batch`): a
1962    /// dense stack wholly on a discrete card, whose token graph already
1963    /// decodes it. Not `graph_prefill_preferred` — the host stays the KV
1964    /// owner for reuse and scoring; only generation's ingest changes.
1965    /// `CMF_G4_BATCH=0` keeps the chunked host prefill.
1966    fn g4_batch_prefill(&self) -> bool {
1967        #[cfg(target_os = "macos")]
1968        {
1969            false
1970        }
1971        #[cfg(not(target_os = "macos"))]
1972        {
1973            (self.global_attn.is_some() || self.attn_v_norm)
1974                && crate::gpu::g4_batch_on()
1975                && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
1976                && crate::gpu::enabled_here()
1977                && crate::gpu::discrete()
1978                && !self.graph_refused()
1979                && !self.o1_active()
1980                && self.attn_softcap == 0.0
1981                && self.wgpu_graph_attn_decline().is_none()
1982                && self.weights.layers.iter().all(|lw| {
1983                    matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1984                })
1985                && self.automatic_gpu_prefix().is_none()
1986        }
1987    }
1988
1989    /// Gemma-4's batched prefill of `ids` at positions `pos0..`
1990    /// (`g4_batch_prefill`): chunks of `generation_batch_k` rows through the
1991    /// batch graph. Ok((n, rows)): `n` positions ingested; `rows` = the last
1992    /// one's hidden, or every row's (`keep_all`). A refusal stops early
1993    /// (n < ids.len()) after pulling the device rows the host walk needs.
1994    /// Err: a failure after admission — the device state is undefined.
1995    #[cfg(not(target_os = "macos"))]
1996    fn g4_batch_ingest(
1997        &mut self,
1998        ids: &[u32],
1999        pos0: usize,
2000        keep_all: bool,
2001    ) -> Result<(usize, Vec<f32>), String> {
2002        let hs = self.hidden_size;
2003        let chunk = self.generation_batch_k().max(1);
2004        let end_all = pos0 + ids.len();
2005        let mut pos = pos0;
2006        let mut out = Vec::new();
2007        while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2008            let end = (pos + chunk).min(end_all);
2009            let bk = end - pos;
2010            let mut hiddens = vec![0f32; bk * hs];
2011            for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2012                hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2013            }
2014            let positions: Vec<usize> = (pos..end).collect();
2015            let t_chunk = std::time::Instant::now();
2016            let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2017            if std::env::var("CMF_GRAPH_PROF").is_ok() {
2018                let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2019                eprintln!(
2020                    "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2021                    bk as f64 / (ms / 1000.0)
2022                );
2023            }
2024            match outcome {
2025                crate::gpu::BatchGraphOutcome::Completed => {
2026                    if keep_all {
2027                        out.extend_from_slice(&hiddens);
2028                    } else {
2029                        out = hiddens[(bk - 1) * hs..].to_vec();
2030                    }
2031                    pos = end;
2032                }
2033                crate::gpu::BatchGraphOutcome::Failed => {
2034                    return Err("Gemma-4 batched prefill failed after admission".into());
2035                }
2036                crate::gpu::BatchGraphOutcome::Declined => {
2037                    #[cfg(feature = "gpu")]
2038                    if pos > pos0 {
2039                        self.pull_lagging_host_kv(0, self.num_layers, pos);
2040                    }
2041                    break;
2042                }
2043            }
2044        }
2045        Ok((pos - pos0, out))
2046    }
2047
2048    /// Hidden rows of every position of `ids` through Gemma-4's batched
2049    /// prefill from a fresh sequence (the prefill's teacher-forced logits
2050    /// for parity gates: `logits_from_hidden` per row). None when the model
2051    /// or the card does not take that route, or it refused part way.
2052    pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2053        #[cfg(not(target_os = "macos"))]
2054        {
2055            if ids.is_empty() || !self.g4_batch_prefill() {
2056                return None;
2057            }
2058            self.clear_sequence_state();
2059            match self.g4_batch_ingest(ids, 0, true) {
2060                Ok((n, rows)) if n == ids.len() => Some(rows),
2061                _ => {
2062                    self.clear_sequence_state();
2063                    None
2064                }
2065            }
2066        }
2067        #[cfg(target_os = "macos")]
2068        {
2069            let _ = ids;
2070            None
2071        }
2072    }
2073
2074    #[cfg(not(target_os = "macos"))]
2075    fn graph_prefill_preferred(&self) -> bool {
2076        // Discrete-GPU wgpu whole-token graph: GDN layers carry recurrent state
2077        // (conv ring + delta-rule S) resident on the GPU. A batched CPU prefill
2078        // builds that state on the CPU only, leaving the GPU buffers zeroed at
2079        // decode → garbage. Route GDN-hybrid prefill through the graph one
2080        // position at a time so the resident state is seeded exactly as decode
2081        // will read it. Pure-attention models keep the batched CPU prefill (its
2082        // KV mirror re-syncs from the CPU cache, so no seeding gap).
2083        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2084        if !graph_on || !crate::gpu::enabled_here() {
2085            return false;
2086        }
2087        // Embryo's phase state is device-owned by the resident graph during
2088        // prefill; the batched CPU path would leave decode seeing a zeroed
2089        // device recurrence.  Route the prompt position-by-position too.
2090        if self.embryo_resident_eligible() {
2091            // The resident graph owns the phase state and anchor KV on the
2092            // device.  A prefill-only graph would leave decode on the host
2093            // with no way to import that state, so keep the whole sequence
2094            // on one owner (or use the ordinary CPU prefill/decode pair).
2095            return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2096        }
2097        // The descriptor-aware Prism graph now carries both the FWHT/affine
2098        // transforms and resident GDN state, so it is also the exact prefill
2099        // path for this model.  Keeping it here (rather than falling through
2100        // to the CPU chunk walk) is required for a long prompt to seed the
2101        // same device state that decode consumes.
2102        // O(1) needs the CPU prefill: the q-trace that seals the Nyström
2103        // skeleton is recorded there and nowhere else. The GDN half of
2104        // the hybrid loses nothing — the graph's first decode creates
2105        // its (ring, S) entries seeded from `cpu_state`, the same
2106        // handoff every graph run relies on when the entry is fresh.
2107        // Without this line the two designs collide on hybrids and o1
2108        // never becomes graph-portable: prefill through the graph
2109        // records no trace, so views stay None forever.
2110        if self.o1_active() {
2111            return false;
2112        }
2113        // A model the wgpu graphs decline outright would walk its prompt
2114        // one position at a time through a graph that never runs: the
2115        // batched CPU chunk prefill is the right ingest for it.
2116        if self.wgpu_graph_attn_decline().is_some() {
2117            return false;
2118        }
2119        if self
2120            .weights
2121            .layers
2122            .iter()
2123            .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2124        {
2125            return true;
2126        }
2127        // MoE models too: the chunked CPU prefill runs every expert on the
2128        // host (Hy-MT2-30B-A3B on a Xeon: 8 tok/s of ingest against 53 of
2129        // graph decode), while the token graph — and the batched graph under
2130        // CMF_BATCH_K — keep the experts resident. Full attention in the
2131        // graph writes the KV mirror that decode reads, exactly as it does
2132        // for the hybrids' attention layers. Only when the whole stack is
2133        // resident: with a device prefix the per-position walk finishes
2134        // every token on the host, and the chunked prefill (GEMMs on the
2135        // card, the expert loop batched on the host) is the faster ingest
2136        // (the 8 GB ladder point: 7 tok/s chunked against ~1 walked).
2137        self.weights
2138            .layers
2139            .iter()
2140            .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2141            && self.automatic_gpu_prefix().is_none()
2142    }
2143
2144    #[cfg(target_os = "macos")]
2145    fn q1_graph_gpu(
2146        &mut self,
2147        start: usize,
2148        upto: Option<usize>,
2149        position: usize,
2150        h: &mut [f32],
2151    ) -> usize {
2152        let _mt0 = std::time::Instant::now(); // CMF_METAL_HOSTPROF
2153        use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2154        const MOE_WORKING_SET_REASON: &str =
2155            "model weights exceed Metal working set for native MoE graph";
2156        // A prior admission check rejected this pipeline. Avoid rebuilding the
2157        // entire 40-layer graph plan at every individual host layer after the
2158        // fallback begins; the per-operation path below stays eligible.
2159        if self
2160            .graph_declines
2161            .borrow()
2162            .iter()
2163            .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2164        {
2165            return start;
2166        }
2167        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2168        if self.attn_softcap > 0.0 // capped scores: no graph kernel — CPU path
2169            || !self.metal_rope_scale_ok()
2170            || !crate::gpu::enabled_here()
2171            || !graph_force
2172            || std::env::var("CMF_GPU_BLOCK")
2173                .map(|v| v == "0")
2174                .unwrap_or(false)
2175        {
2176            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2177                eprintln!(
2178                    "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2179                    self.attn_softcap > 0.0,
2180                    crate::gpu::enabled_here(),
2181                    graph_force,
2182                );
2183            }
2184            if self.graph_head_required {
2185                self.fail_metal_graph("native graph front gate refused");
2186            }
2187            return start;
2188        }
2189        // The graph encodes SiLU or exact-GELU FFNs and attention with an
2190        // explicit model scale. Sliding-window layers with their own RoPE
2191        // table / rotary width ride it too (`metal_graph_swa`): both the
2192        // device attend and the sandwich's CPU attend read each layer's
2193        // window and table. Sandwich norms and other activations fall back
2194        // to the CPU path.
2195        // Gemma-4 (`metal_gemma4`): its sliding layers ride the same door,
2196        // its global layers, V norm, sandwich norms, layer scalar and
2197        // tanh-GELU the `gpu_metal::gemma4` kernels.
2198        let gemma4 = self.metal_gemma4();
2199        let swa_graph = self.metal_graph_swa() || gemma4;
2200        // The normal native GQA kernel carries a learned, value-less
2201        // softmax column.  It also takes the exact per-layer sliding
2202        // window, but its split-K twin has no sink column.  Thus a
2203        // sink-bearing model may use this door only when the ordinary
2204        // sliding geometry has passed `metal_graph_swa` (or when it has no
2205        // sliding layers at all).
2206        let graph_attn_reason = self.graph_attn_decline_reason();
2207        let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2208            && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2209        if (self.swa.is_some() && !swa_graph)
2210            || (self.global_attn.is_some() && !gemma4)
2211            || self.attention_heads_per_layer.is_some()
2212            || (self.attn_v_norm && !gemma4)
2213            // Per-layer KV heads / narrow V stay outside this graph.  A
2214            // learned sink is admitted only through native_sink_attend.
2215            || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2216            || self.weights.layers.iter().any(|lw| {
2217                ((lw.attn_out_norm.is_some()
2218                    || lw.ffn_out_norm.is_some()
2219                    || lw.layer_scale.is_some())
2220                    && !gemma4)
2221                    || matches!(&lw.ffn, FfnKind::Dense(d)
2222                        if !(matches!(d.act, Act::Silu | Act::Gelu)
2223                            || (gemma4 && d.act == Act::GeluTanh)))
2224            })
2225        {
2226            // The Metal graphs' device attend has no per-layer attention
2227            // geometry (the wgpu graphs do): say so once, by name.
2228            if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2229                self.note_graph_decline("metal block graph", reason);
2230            }
2231            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2232                eprintln!(
2233                    "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2234                    self.swa.is_some(),
2235                    self.global_attn.is_some(),
2236                    self.attention_heads_per_layer.is_some(),
2237                    self.attn_v_norm,
2238                    (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2239                );
2240            }
2241            if self.graph_head_required {
2242                self.fail_metal_graph("native graph architecture gate refused");
2243            }
2244            return start;
2245        }
2246        // Looped Transformer: the graph covers ALL loop iterations;
2247        // encode_loop_norm is inserted on-device at each boundary.
2248        let limit = upto
2249            .map(|u| u + 1)
2250            .unwrap_or(self.num_layers)
2251            .min(self.num_layers);
2252
2253        enum Item<'a> {
2254            Gdn {
2255                run: Vec<GdnGpuLayer<'a>>,
2256                first: usize,
2257            },
2258            Attn {
2259                l: AttnGpuLayer<'a>,
2260                li: usize,
2261                q_norm: Option<&'a [f32]>,
2262                k_norm: Option<&'a [f32]>,
2263                output_gate: bool,
2264                /// `self_attn.g_proj` output gate (per-head flag): the
2265                /// sandwich projects the device-normed input on the host
2266                /// and gates the attend's output before O.
2267                proj_gate: Option<(&'a QTensor, bool)>,
2268                /// The same gate's f32 rows `[nh × hidden]` when the device
2269                /// attend can apply it (per-head sigmoid, f32 in RAM).
2270                head_gate_w: Option<&'a [f32]>,
2271                bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2272                /// Attend on the device too (no sync): F32 KV, no
2273                /// o1/bias, dims inside the kernels' contract.
2274                full_gpu: bool,
2275            },
2276        }
2277
2278        // Device-attend KERNEL contract, shared by every Full layer. The
2279        // hd>128 default-off POLICY is applied after the scan: it was
2280        // measured on dense models, and a MoE plan inverts it — with the
2281        // experts on device each CPU-attend sandwich costs a
2282        // commit+wait, ~30 submits/token (W2 on M4: 14.7 tok/s
2283        // sandwiched vs 27.1 device-attend vs 18.8 pure CPU).
2284        let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2285        let attend_contract = attend_mode != "0"
2286            && attend_mode != "off"
2287            && self.head_dim % 4 == 0
2288            && self.head_dim <= 256
2289            // Flint has explicit no-RoPE layers.  The device RoPE kernel
2290            // accepts rd=0 (zero rotation lanes) and binds a sentinel table;
2291            // retain the historical lane-pair requirement for nonzero rd.
2292            && (self.rotary_dim == 0
2293                || (self.rotary_dim >= 2
2294                    && self.rotary_dim <= self.head_dim
2295                    && (self.rotary_dim / 2) % 32 == 0))
2296            && self.num_kv_heads > 0
2297            && self.num_heads % self.num_kv_heads == 0;
2298
2299        let mut plan: Vec<Item> = Vec::new();
2300        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2301        // Break-reason diagnostics ride the same env as the plan summary.
2302        let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2303        let mut scan = start;
2304        while scan < limit {
2305            let lw = &self.weights.layers[self.phys_layer(scan)];
2306            let ffn = match &lw.ffn {
2307                FfnKind::Dense(d) if d.segs.is_empty() => {
2308                    let (Some(g), Some(u), Some(dn)) = (
2309                        d.gate_proj.metal_graph_parts(),
2310                        d.up_proj.metal_graph_parts(),
2311                        d.down_proj.metal_graph_parts(),
2312                    ) else {
2313                        if block_diag {
2314                            eprintln!(
2315                                "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2316                            );
2317                        }
2318                        break;
2319                    };
2320                    MetalFfn::Dense {
2321                        gate: g,
2322                        up: u,
2323                        down: dn,
2324                        // The front gate admitted SiLU, exact GELU and
2325                        // (Gemma-4) tanh-GELU only.
2326                        act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2327                    }
2328                }
2329                FfnKind::Moe(m) => {
2330                    // Flint's detached Q4 shared expert is intentionally
2331                    // tried before the generic uniform-bank graph builder.
2332                    // The latter correctly refuses its ragged Q2/Q4 widths;
2333                    // this narrow descriptor keeps the router and selected
2334                    // routed experts device-resident inside a bounded page.
2335                    let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2336                    {
2337                        MetalFfn::FlintMoe(flint)
2338                    } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2339                        MetalFfn::Moe(moe)
2340                    } else {
2341                        if block_diag {
2342                            eprintln!(
2343                                "block-graph: L{scan} MoE outside the graph contract — run ends"
2344                            );
2345                        }
2346                        break;
2347                    };
2348                    if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2349                        model_ref.get_or_insert_with(|| model.clone());
2350                    }
2351                    ffn
2352                }
2353                _ => {
2354                    if block_diag {
2355                        eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2356                    }
2357                    break;
2358                }
2359            };
2360            match &lw.attn {
2361                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2362                    let parts = (
2363                        w.in_proj_qkv.metal_graph_parts(),
2364                        w.in_proj_z.metal_graph_parts(),
2365                        w.in_proj_a.f32_parts(),
2366                        w.in_proj_b.f32_parts(),
2367                        w.out_proj.metal_graph_parts(),
2368                    );
2369                    let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2370                        if block_diag {
2371                            eprintln!(
2372                                "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2373                                w.in_proj_qkv.metal_graph_parts().is_some(),
2374                                w.in_proj_z.metal_graph_parts().is_some(),
2375                                w.in_proj_a.f32_parts().is_some(),
2376                                w.in_proj_b.f32_parts().is_some(),
2377                                w.out_proj.metal_graph_parts().is_some(),
2378                            );
2379                        }
2380                        break;
2381                    };
2382                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2383                        model_ref.get_or_insert_with(|| model.clone());
2384                    }
2385                    let gl = GdnGpuLayer {
2386                        attn_norm: &lw.input_norm,
2387                        post_norm: &lw.post_norm,
2388                        qkv,
2389                        z,
2390                        a,
2391                        b,
2392                        out,
2393                        ffn,
2394                        conv1d: &w.conv1d,
2395                        a_log: &w.a_log,
2396                        dt_bias: &w.dt_bias,
2397                        gnorm: &w.norm,
2398                    };
2399                    match plan.last_mut() {
2400                        Some(Item::Gdn { run, .. }) => run.push(gl),
2401                        _ => plan.push(Item::Gdn {
2402                            run: vec![gl],
2403                            first: scan,
2404                        }),
2405                    }
2406                }
2407                AttnKind::Full {
2408                    wq,
2409                    wk,
2410                    wv,
2411                    wo,
2412                    q_norm,
2413                    k_norm,
2414                    output_gate,
2415                    softplus_gate,
2416                    bias,
2417                } if (!self.kv_cache.layers[scan].o1_sealed()
2418                    // Sealed o1 stays plannable when the Metal o1 port
2419                    // is on: full_gpu attends through the device state,
2420                    // and any refusal falls to the sandwich, whose CPU
2421                    // core routes sealed layers through the nystrom step.
2422                    || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2423                    // A projected output gate: Spark-X2.5's per-head
2424                    // sigmoid form only (Laguna's softplus gate is
2425                    // unmeasured on these paths and keeps the CPU walk).
2426                    && softplus_gate
2427                        .as_ref()
2428                        .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2429                {
2430                    let parts = (
2431                        wq.metal_graph_parts(),
2432                        wk.metal_graph_parts(),
2433                        wv.metal_graph_parts(),
2434                        wo.metal_graph_parts(),
2435                    );
2436                    let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2437                        break;
2438                    };
2439                    if let QTensor::Mapped { model, .. } = wq {
2440                        model_ref.get_or_insert_with(|| model.clone());
2441                    }
2442                    let cache = &self.kv_cache.layers[scan];
2443                    // O(1) layer on Metal: the device attends through the
2444                    // sealed Nystrom state (opt-in while the port proves
2445                    // itself). Unsealed -> sandwich path = the CPU o1 step.
2446                    let o1_metal = cache.o1.is_some()
2447                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2448                        && cache.o1_views().is_some();
2449                    // The device attend takes the layer's window and RoPE
2450                    // table, and the per-head gate from f32 rows; a gate
2451                    // held otherwise sandwiches.
2452                    let head_gate_w = softplus_gate
2453                        .as_ref()
2454                        .and_then(|(g, _)| g.f32_parts())
2455                        .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2456                        .map(|(d, _, _)| d);
2457                    // Gemma-4's global layers are 512 wide over one KV head
2458                    // (the `gemma4` 16-slot kernels, `CMF_METAL_GEMMA4_W16`).
2459                    let (nkv_g, hd_g) = if gemma4 {
2460                        let (a, b, _) = self.layer_geom(scan);
2461                        (a, b)
2462                    } else {
2463                        (self.num_kv_heads, self.head_dim)
2464                    };
2465                    let full_gpu = attend_contract
2466                        && softplus_gate.is_none() == head_gate_w.is_none()
2467                        && cache.mode == crate::kv_cache::KvMode::F32
2468                        && (cache.o1.is_none() || o1_metal)
2469                        && bias.is_none()
2470                        && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2471                        && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2472                        && pk.1 == nkv_g * hd_g
2473                        && pv.1 == nkv_g * hd_g
2474                        && po.2 == self.num_heads * hd_g;
2475                    plan.push(Item::Attn {
2476                        l: AttnGpuLayer {
2477                            attn_norm: &lw.input_norm,
2478                            post_norm: &lw.post_norm,
2479                            wq: pq,
2480                            wk: pk,
2481                            wv: pv,
2482                            wo: po,
2483                            ffn,
2484                            attn_out_norm: lw.attn_out_norm.as_deref(),
2485                            ffn_out_norm: lw.ffn_out_norm.as_deref(),
2486                            layer_scale: lw.layer_scale,
2487                        },
2488                        li: scan,
2489                        q_norm: q_norm.as_deref(),
2490                        k_norm: k_norm.as_deref(),
2491                        output_gate: *output_gate,
2492                        proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2493                        head_gate_w,
2494                        bias: bias
2495                            .as_ref()
2496                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2497                        full_gpu,
2498                    });
2499                }
2500                _ => break,
2501            }
2502            scan += 1;
2503        }
2504        let Some(model) = model_ref else {
2505            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2506                eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2507            }
2508            if self.graph_head_required {
2509                self.fail_metal_graph("native graph has no mapped model reference");
2510            }
2511            return start;
2512        };
2513        if plan.is_empty() {
2514            if std::env::var("CMF_GRAPH_DBG").is_ok() {
2515                eprintln!("q1-graph: empty plan at layer {start}");
2516            }
2517            if self.graph_head_required {
2518                self.fail_metal_graph("native graph plan is empty");
2519            }
2520            return start;
2521        }
2522        let has_moe = plan.iter().any(|it| match it {
2523            Item::Gdn { run, .. } => run
2524                .iter()
2525                .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2526            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2527        });
2528        let has_flint_moe = plan.iter().any(|it| match it {
2529            Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2530            Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2531        });
2532        let metal_working_set = crate::gpu_metal::working_set_bytes();
2533        let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2534        let model_bytes = model.primary_bytes().len() as u64;
2535        // Flint q2 can sit below recommendedMaxWorkingSetSize on a 24 GB
2536        // M4 yet still exceed maxBufferLength. In that case a normal graph
2537        // would manufacture overlapping whole-file WeightArena windows;
2538        // treat it as the same bounded-page requirement rather than letting
2539        // the generic graph reintroduce broad residency.
2540        let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2541            || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2542        // An oversized MoE mmap normally has to decline: the jobs kernels
2543        // bind all WeightArena windows.  The opt-in pager takes a different,
2544        // bounded route: a *complete sequence of logical layers* is put in
2545        // one transient no-copy page, graph-finished, dropped, then the outer
2546        // layer loop calls us again for the next page.  That preserves exact
2547        // hidden/GDN/KV handoff while replacing ~40 host-routed expert waits
2548        // with a few page-boundary syncs.  Never use it for a fused-head NLL
2549        // request: that contract explicitly requires a single whole graph.
2550        let pager_cap = if !has_moe {
2551            None
2552        } else if has_flint_moe {
2553            // Do not let generic `CMF_METAL_MOE_PAGER` turn the experimental
2554            // full-bank Flint graph on. Its direct selected-view pager stays
2555            // independently automatic; this path needs its own opt-in.
2556            crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2557        } else {
2558            crate::gpu_metal::moe_pager_cap_bytes()
2559        };
2560        let pager_eligible =
2561            pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2562        let mut pager_span: Option<(usize, usize)> = None;
2563        if pager_eligible {
2564            let cap = pager_cap.expect("checked above");
2565            // When this page reaches the true model end, include the fused
2566            // lm_head too.  Otherwise `lm_head_ok` would correctly refuse a
2567            // binding outside the page after the layers had already run.
2568            let tail_span = (self.graph_want_logits
2569                && upto.is_none()
2570                && scan == self.num_layers
2571                && std::env::var("CMF_GPU_LMHEAD")
2572                    .map(|v| v != "0")
2573                    .unwrap_or(true))
2574            .then(|| self.weights.lm_head.metal_graph_parts())
2575            .flatten()
2576            .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2577            let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2578                let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2579                Some(match span {
2580                    Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2581                    None => (lo, hi),
2582                })
2583            };
2584            let page_span = |span: (usize, usize), include_tail: bool| {
2585                let (mut lo, mut hi) = span;
2586                if include_tail {
2587                    if let Some((tail_lo, tail_hi)) = tail_span {
2588                        lo = lo.min(tail_lo);
2589                        hi = hi.max(tail_hi);
2590                    }
2591                }
2592                crate::gpu_metal::page_round_span(lo, hi)
2593            };
2594            let mut selected = 0usize;
2595            let mut selected_span: Option<(usize, usize)> = None;
2596            // `Item::Gdn` is built as a maximal run so the normal resident
2597            // graph can batch its recurrent state work.  A physical CMF can
2598            // nevertheless place that whole run across a page cap while each
2599            // logical layer fits.  Split *only the final admitted run* at a
2600            // layer boundary: the next outer-loop call rebuilds the remaining
2601            // suffix, preserving hidden/state handoff and keeping the normal
2602            // long-run batching within every page.
2603            let mut gdn_prefix: Option<(usize, usize)> = None;
2604            'items: for (i, item) in plan.iter().enumerate() {
2605                match item {
2606                    Item::Attn { li, .. } => {
2607                        let Some(span) = add_layer_span(selected_span, *li) else {
2608                            break;
2609                        };
2610                        let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2611                            break;
2612                        };
2613                        if page_hi - page_lo > cap {
2614                            break;
2615                        }
2616                        selected = i + 1;
2617                        selected_span = Some((page_lo, page_hi));
2618                    }
2619                    Item::Gdn { first, run } => {
2620                        // Preserve a complete recurrent run whenever it can
2621                        // occupy a page on its own.  If the preceding items
2622                        // merely used the remaining cap, close that page and
2623                        // let the next q1 invocation retain the full run.
2624                        // Prefix-splitting is only for a run that genuinely
2625                        // cannot fit an empty page.
2626                        let mut whole = None;
2627                        for take in 1..=run.len() {
2628                            whole = add_layer_span(whole, *first + take - 1);
2629                            if whole.is_none() {
2630                                break;
2631                            }
2632                        }
2633                        let whole_fits_empty_page = whole
2634                            .and_then(|span| page_span(span, false))
2635                            .is_some_and(|(lo, hi)| hi - lo <= cap);
2636                        if whole_fits_empty_page {
2637                            let mut span = selected_span;
2638                            for take in 1..=run.len() {
2639                                let Some(next) = add_layer_span(span, *first + take - 1) else {
2640                                    break 'items;
2641                                };
2642                                span = Some(next);
2643                            }
2644                            let Some(span) = span else {
2645                                break;
2646                            };
2647                            let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2648                            else {
2649                                break;
2650                            };
2651                            if page_hi - page_lo > cap {
2652                                break;
2653                            }
2654                            selected = i + 1;
2655                            selected_span = Some((page_lo, page_hi));
2656                            continue;
2657                        }
2658                        if selected != 0 {
2659                            break;
2660                        }
2661                        let mut span = None;
2662                        let mut best: Option<(usize, (usize, usize))> = None;
2663                        for take in 1..=run.len() {
2664                            let Some(next) = add_layer_span(span, *first + take - 1) else {
2665                                break;
2666                            };
2667                            // A partial final run cannot encode the lm_head;
2668                            // only its complete form above may include it.
2669                            let Some((page_lo, page_hi)) = page_span(next, false) else {
2670                                break;
2671                            };
2672                            if page_hi - page_lo > cap {
2673                                break;
2674                            }
2675                            span = Some((page_lo, page_hi));
2676                            best = Some((take, (page_lo, page_hi)));
2677                        }
2678                        let Some((take, page)) = best else {
2679                            break;
2680                        };
2681                        selected = i + 1;
2682                        selected_span = Some(page);
2683                        if take != run.len() {
2684                            gdn_prefix = Some((i, take));
2685                            break 'items;
2686                        }
2687                    }
2688                }
2689            }
2690            if selected != 0 {
2691                if let Some((item, take)) = gdn_prefix {
2692                    match &mut plan[item] {
2693                        Item::Gdn { run, .. } => run.truncate(take),
2694                        Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2695                    }
2696                }
2697                let (page_lo, page_hi) = selected_span.expect("selected page span");
2698                tracing::info!(
2699                    page_start = page_lo,
2700                    page_end = page_hi,
2701                    page_mb = (page_hi - page_lo) / (1024 * 1024),
2702                    cap_mb = cap / (1024 * 1024),
2703                    graph_items = selected,
2704                    "native Metal MoE graph uses transient bounded weight page"
2705                );
2706                plan.truncate(selected);
2707                pager_span = Some((page_lo, page_hi));
2708            } else {
2709                tracing::warn!(
2710                    cap_mb = cap / (1024 * 1024),
2711                    "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2712                );
2713            }
2714        }
2715        // A native MoE graph binds every WeightArena window to its jobs
2716        // kernels. On UMA, an mmap that already exceeds the driver's
2717        // recommended working set will therefore fail after admission, when
2718        // some recurrent/KV scratch has already been touched. Decline before
2719        // allocating/encoding the graph instead; the ordinary per-operation
2720        // route remains available and only materializes the needed window.
2721        // Do not apply this to dense graphs: they bind one projection at a
2722        // time and do not have the MoE jobs' all-window lifetime.
2723        if has_moe && needs_bounded_pager && pager_span.is_none() {
2724            tracing::info!(
2725                model_bytes,
2726                metal_working_set,
2727                "native Metal MoE graph declined before allocation"
2728            );
2729            self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2730            if self.graph_head_required {
2731                self.fail_metal_graph(MOE_WORKING_SET_REASON);
2732            }
2733            return start;
2734        }
2735        let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2736        let dev_attend = attend_contract
2737            && (self.head_dim <= 128
2738                || has_moe
2739                // A GDN hybrid attends on a quarter of its layers: the
2740                // hd>128 caution was measured on pure-dense models where
2741                // gqa_attend dominates, and on Qwen3.8-27B (hd 256, 48
2742                // GDN + 16 attn) the sandwich costs 2x the whole decode
2743                // (1.2 vs 2.21 tok/s measured before the arena fix).
2744                || (self.head_dim <= 256 && has_gdn)
2745                // Sliding-window layers bound most attends by the window:
2746                // Spark-X2.5-1.7B q4tp on the M4 decodes 67 tok/s
2747                // device-attended against 23 sandwiched (28 syncs/token).
2748                || (self.head_dim <= 256 && swa_graph)
2749                || attend_mode == "force"
2750                || attend_mode == "256");
2751        if !dev_attend {
2752            for it in &mut plan {
2753                if let Item::Attn { li, full_gpu, .. } = it {
2754                    // The hd>128 policy is about gqa_attend; an o1 layer
2755                    // attends through its own kernel set.
2756                    let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2757                        && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2758                    if !keep_o1 {
2759                        *full_gpu = false;
2760                    }
2761                }
2762            }
2763        }
2764        if std::env::var("CMF_GRAPH_DBG").is_ok() {
2765            use std::sync::atomic::{AtomicBool, Ordering};
2766            static SAID: AtomicBool = AtomicBool::new(false);
2767            if !SAID.swap(true, Ordering::Relaxed) {
2768                let fg = plan
2769                    .iter()
2770                    .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2771                    .count();
2772                let att = plan
2773                    .iter()
2774                    .filter(|it| matches!(it, Item::Attn { .. }))
2775                    .count();
2776                eprintln!(
2777                    "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2778                    plan.len(),
2779                    self.head_dim,
2780                    self.rotary_dim,
2781                    self.num_kv_heads,
2782                    self.num_heads,
2783                );
2784            }
2785        }
2786        let dims = GraphDims {
2787            hidden: self.hidden_size,
2788            eps: self.rms_eps as f32,
2789            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2790        };
2791        // The outer `forward_layers_span` scope is one logical token.  This
2792        // guard starts at q1_graph_gpu entry and ends after all page readback,
2793        // so a multi-page token cannot be reported as several whole tokens.
2794        let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
2795            crate::gpu_metal::MoePagerPageScope::new(
2796                _mt0,
2797                position,
2798                start,
2799                page_lo,
2800                page_hi,
2801                // `pager_span` is selected only after a cap was admitted;
2802                // keep the profiler observational if that invariant is ever
2803                // loosened rather than introducing a new inference panic.
2804                pager_cap.unwrap_or(0),
2805            )
2806        });
2807        let graph_new = match pager_span {
2808            Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
2809            None => TokenGraph::new(&model, dims, h),
2810        };
2811        let Some(mut graph) = graph_new else {
2812            if self.graph_head_required {
2813                self.fail_metal_graph("native TokenGraph allocation refused");
2814            }
2815            return start;
2816        };
2817        if swa_graph && self.head_dim > 128 {
2818            // Spark-X2.5 (hd 256, 4 Q heads per KV head): the GQA-shared
2819            // split-K attend reads each K/V row once for the group instead
2820            // of once per head plus the importance re-read — at depth 1000
2821            // on the M4, 59.5 tok/s against 50.0 with the per-head kernel
2822            // up to the default 512 (every sliding layer sits at <= 512).
2823            graph.set_attend_blk_from(64);
2824        }
2825        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
2826            nv: cfg.num_v_heads,
2827            nk: cfg.num_k_heads,
2828            dk: cfg.key_head_dim,
2829            dv: cfg.value_head_dim,
2830            kk: cfg.conv_kernel,
2831            hidden: self.hidden_size,
2832            inter: self.intermediate_size,
2833            c_dim: cfg.conv_dim(),
2834            eps: cfg.rms_eps as f32,
2835            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2836        });
2837        // Validate the whole plan BEFORE encoding anything: after the
2838        // first sync a refused layer would leave the token
2839        // half-executed, so truncate to the provably encodable prefix.
2840        let mut valid = 0usize;
2841        let mut end = start;
2842        crate::gpu::stageprof(1, _mt0.elapsed()); // конец планирования
2843        if std::env::var("CMF_PLAN_DUMP").is_ok() {
2844            static ONCE: std::sync::Once = std::sync::Once::new();
2845            ONCE.call_once(|| {
2846                for it in &plan {
2847                    match it {
2848                        Item::Gdn { first, run } => {
2849                            eprintln!("plan: Gdn first={first} len={}", run.len())
2850                        }
2851                        Item::Attn { li, full_gpu, .. } => {
2852                            eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
2853                        }
2854                    }
2855                }
2856            });
2857        }
2858        for item in &plan {
2859            let ok = match item {
2860                Item::Gdn { run, .. } => gcfg
2861                    .as_ref()
2862                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
2863                    .unwrap_or(false),
2864                Item::Attn { l, .. } => graph.attn_ok(l),
2865            };
2866            if !ok {
2867                if block_diag {
2868                    eprintln!(
2869                        "block-graph: plan item {} ({}) failed graph preflight",
2870                        valid,
2871                        match item {
2872                            Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
2873                            Item::Attn { li, .. } => format!("Attn L{li}"),
2874                        }
2875                    );
2876                }
2877                break;
2878            }
2879            valid += 1;
2880            end += match item {
2881                Item::Gdn { run, .. } => run.len(),
2882                Item::Attn { .. } => 1,
2883            };
2884        }
2885        plan.truncate(valid);
2886        if plan.is_empty() {
2887            if self.graph_head_required {
2888                self.fail_metal_graph("native graph preflight produced no valid items");
2889            }
2890            return start;
2891        }
2892
2893        if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
2894            self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
2895            return start;
2896        }
2897
2898        // Plain dense decode (every item a device-attended full-attention
2899        // layer with a dense FFN, no O(1) state): the only plan shape the
2900        // masked-nibble q4tp matvec and the concurrent layer encoder were
2901        // measured on (MiniCPM5-2B, Qwen3-0.6B on the M4). Hybrids, MoE and
2902        // o1 layers keep the historical serial path bit for bit.
2903        // Every projection must be ONE dispatch (q1t adds an overlay pass,
2904        // Prism q2tp a transform pass — dependent pairs a concurrent
2905        // encoder would race).
2906        let one_pass = |t: (usize, usize, usize)| {
2907            use cortiq_core::TensorDtype as D;
2908            matches!(
2909                model.tensors[t.0].dtype,
2910                D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
2911            )
2912        };
2913        let dense_fast = plan.iter().all(|it| match it {
2914            Item::Attn {
2915                l, li, full_gpu, ..
2916            } => {
2917                *full_gpu
2918                    && self.kv_cache.layers[*li].o1.is_none()
2919                    && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
2920                    && match l.ffn {
2921                        MetalFfn::Dense { gate, up, down, .. } => {
2922                            one_pass(gate) && one_pass(up) && one_pass(down)
2923                        }
2924                        _ => false,
2925                    }
2926            }
2927            Item::Gdn { .. } => false,
2928        });
2929        // MoE decode with every layer device-attended (Mellum2.1): the
2930        // routed experts' jobs ladder and the q8_2f attention projections
2931        // take their measured levers (`MOE_*`, `DENSE_Q8R4`).
2932        let moe_plan = !dense_fast
2933            && plan.iter().all(|it| match it {
2934                Item::Attn {
2935                    l, li, full_gpu, ..
2936                } => {
2937                    *full_gpu
2938                        && self.kv_cache.layers[*li].o1.is_none()
2939                        && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
2940                }
2941                Item::Gdn { .. } => false,
2942            });
2943        let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
2944        if block_diag {
2945            use std::sync::atomic::{AtomicBool, Ordering};
2946            static SAID_AB: AtomicBool = AtomicBool::new(false);
2947            if !SAID_AB.swap(true, Ordering::Relaxed) {
2948                eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
2949            }
2950        }
2951        let _mv_fast = match ab {
2952            Some((bits, _)) => {
2953                graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
2954                crate::gpu_metal::MvFastGuard::set_raw(bits)
2955            }
2956            None if moe_plan => {
2957                graph.set_dense_concurrent(
2958                    crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
2959                );
2960                crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
2961            }
2962            None => {
2963                graph.set_dense_concurrent(dense_fast);
2964                // Spark-X2.5 q8_2f: the four-row q8_2f matvec decodes the
2965                // 1.7B at 40.5 tok/s on the M4 against 27.7 one-row.
2966                let q8r4 = if swa_graph {
2967                    crate::gpu_metal::DENSE_Q8R4
2968                } else {
2969                    0
2970                };
2971                crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
2972                    crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
2973                } else {
2974                    0
2975                })
2976            }
2977        };
2978
2979        // RoPE table and rotary width are per layer (`layer_inv_freq`,
2980        // `layer_geom`) inside the loop below.
2981        let pool = self.pool.clone();
2982        let (nh, nkv, hd, hs, eps) = (
2983            self.num_heads,
2984            self.num_kv_heads,
2985            self.head_dim,
2986            self.hidden_size,
2987            self.rms_eps,
2988        );
2989        let norm_style = self.norm_style;
2990        let gemma = norm_style == cortiq_core::NormStyle::Gemma;
2991        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
2992        let kv_id = self.graph_kv_id;
2993        // GDN runs whose states await readback after the next sync
2994        // (device-attended layers add no sync, so several may stack).
2995        let mut pending: Vec<(usize, usize)> = Vec::new();
2996        // Device-attended layers: their K/V/imp are pulled from the
2997        // mirror after the final sync.
2998        let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
2999        for item in &plan {
3000            let _xt0 = std::time::Instant::now();
3001            let _xkind: u32 = match item {
3002                Item::Gdn { .. } => 2,
3003                Item::Attn { .. } => 3,
3004            };
3005            // Looped Transformer: insert on-device norm at loop boundaries.
3006            if self.loop_final_norm {
3007                let item_start = match item {
3008                    Item::Gdn { first, .. } => *first,
3009                    Item::Attn { li, .. } => *li,
3010                };
3011                if item_start > start && self.is_loop_end(item_start - 1) {
3012                    graph.encode_loop_norm(&self.weights.final_norm);
3013                }
3014            }
3015            match item {
3016                Item::Gdn { run, first } => {
3017                    for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3018                        if l.linear_state.len() != want {
3019                            l.linear_state = vec![0f32; want];
3020                        }
3021                    }
3022                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3023                        .iter()
3024                        .map(|l| l.linear_state.as_slice())
3025                        .collect();
3026                    let _ig = std::time::Instant::now();
3027                    if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3028                        // Unreachable: the plan was validated above.
3029                        tracing::error!("q1 graph: GDN run refused after validation");
3030                        return start;
3031                    }
3032                    // Early commit: the GPU starts the run while the
3033                    // CPU encodes the next layer (nothing to wait on).
3034                    graph.commit_kind = 2;
3035                    graph.commit();
3036                    crate::gpu::stageprof(0, _ig.elapsed());
3037                    pending.push((*first, run.len()));
3038                }
3039                Item::Attn {
3040                    l,
3041                    li,
3042                    q_norm,
3043                    k_norm,
3044                    output_gate,
3045                    proj_gate,
3046                    head_gate_w,
3047                    bias,
3048                    full_gpu,
3049                } => {
3050                    let _ia = std::time::Instant::now();
3051                    // This layer's attention geometry: its window, RoPE
3052                    // table and rotary width (Spark-X2.5 interleaves
3053                    // 512-window layers rotating all dims at θ 1e4 with
3054                    // full layers rotating a quarter at θ 5e6). Every model
3055                    // without sliding layers reads the global ones here.
3056                    let inv_freq_l = self.layer_inv_freq(*li);
3057                    let rd_l = self.layer_geom(*li).2;
3058                    let window_l = self.layer_window(*li);
3059                    let head_gate_w = *head_gate_w;
3060                    // This layer's KV heads and head width: Gemma-4's global
3061                    // layers (one 512-wide KV head) differ from the base.
3062                    let (nkv, hd) = if gemma4 {
3063                        let (a, b, _) = self.layer_geom(*li);
3064                        (a, b)
3065                    } else {
3066                        (nkv, hd)
3067                    };
3068                    // ── Fully device-resident attention: no sync at all.
3069                    if *full_gpu {
3070                        let cache = &self.kv_cache.layers[*li];
3071                        let o1p = if cache.o1.is_some() {
3072                            match cache.o1_views() {
3073                                Some(views) => Some(crate::gpu::O1AttnParams {
3074                                    views,
3075                                    epoch: self.o1_epoch,
3076                                }),
3077                                // Sealed state gone mid-run: sandwich.
3078                                None => None,
3079                            }
3080                        } else {
3081                            None
3082                        };
3083                        let o1_layer = cache.o1.is_some();
3084                        if o1_layer && o1p.is_none() {
3085                            // fall to the sandwich (CPU o1 step)
3086                        }
3087                        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3088                        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3089                        let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3090                        // A trimmed tail always holds the window the
3091                        // device attend reads (`first = rows + 1 − w`).
3092                        debug_assert!(
3093                            cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3094                        );
3095                        let p = crate::gpu::AttnDeviceParams {
3096                            kv_id,
3097                            layer: *li,
3098                            nh,
3099                            nkv,
3100                            hd,
3101                            rd: rd_l,
3102                            position,
3103                            scale: self.attn_scale,
3104                            eps: eps as f32,
3105                            gemma,
3106                            late_qk_norm: self.qk_norm_after_rope,
3107                            output_gate: *output_gate,
3108                            q_norm: *q_norm,
3109                            k_norm: *k_norm,
3110                            inv_freq: &inv_freq_l,
3111                            rope_scale: self.layer_rope_scale(*li),
3112                            cpu_k,
3113                            cpu_v,
3114                            cpu_stored,
3115                            cpu_gen: cache.generation(),
3116                            o1: o1p,
3117                            window: window_l,
3118                            softmax_sink: cache.sinks.as_deref(),
3119                            head_gate: head_gate_w,
3120                            v_norm: self.attn_v_norm,
3121                        };
3122                        let o1_bad = o1_layer && p.o1.is_none();
3123                        if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3124                        {
3125                            // o1 layers leave no mirror row to pull.
3126                            if p.o1.is_none() {
3127                                dev_attn.push((*li, nkv, hd));
3128                            }
3129                            graph.commit_kind = 3;
3130                            graph.commit();
3131                            // The footer below is skipped by `continue`:
3132                            // account the device-attn item here or its
3133                            // cost hides from the stage profile entirely.
3134                            crate::gpu::stageprof(_xkind, _xt0.elapsed());
3135                            continue;
3136                        }
3137                        // Mirror refused (nothing encoded) → sandwich.
3138                    }
3139                    graph.encode_attn_prefix(l);
3140                    if let Err(err) = graph.sync_checked() {
3141                        self.fail_metal_graph(&err);
3142                        return start;
3143                    }
3144                    if !pending.is_empty() {
3145                        let idxs: Vec<usize> =
3146                            pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3147                        let mut outs: Vec<&mut [f32]> = self
3148                            .kv_cache
3149                            .layers
3150                            .iter_mut()
3151                            .enumerate()
3152                            .filter(|(i, _)| idxs.binary_search(i).is_ok())
3153                            .map(|(_, s)| s.linear_state.as_mut_slice())
3154                            .collect();
3155                        graph.read_states(&mut outs);
3156                    }
3157                    let mut q_raw = attention::take_buf(l.wq.1);
3158                    let mut k = attention::take_buf(l.wk.1);
3159                    let mut v = attention::take_buf(l.wv.1);
3160                    graph.read_qkv(&mut q_raw, &mut k, &mut v);
3161                    // Projected output gate: g = G·norm(h) on the host, off
3162                    // the normed input the prefix left on the device.
3163                    let mut gate_raw = proj_gate.map(|(gp, _)| {
3164                        let mut normed = attention::take_buf(hs);
3165                        graph.read_normed(&mut normed);
3166                        let mut raw = attention::take_buf(gp.rows());
3167                        gp.matvec(&normed, &mut raw, pool.as_deref());
3168                        attention::recycle_buf(&mut normed);
3169                        raw
3170                    });
3171                    let cfg = QwenAttnCfg {
3172                        num_heads: nh,
3173                        num_kv_heads: nkv,
3174                        head_dim: hd,
3175                        hidden_size: hs,
3176                        position,
3177                        inv_freq: &inv_freq_l,
3178                        rotary_dim: rd_l,
3179                        scale: self.attn_scale,
3180                        softcap: self.attn_softcap,
3181                        window: window_l,
3182                        // Gemma-4 only (the front gate refuses V norm
3183                        // otherwise).
3184                        v_norm: self.attn_v_norm,
3185                        qk_norm_after_rope: self.qk_norm_after_rope,
3186                        gate_sigmoid: self.proj_gate_sigmoid,
3187                        q_norm: *q_norm,
3188                        k_norm: *k_norm,
3189                        output_gate: *output_gate,
3190                        softplus_gate: None,
3191                        rope_scale: self.layer_rope_scale(*li),
3192                        bias: *bias,
3193                        rms_eps: eps,
3194                        norm_style,
3195                        pool: pool.as_deref(),
3196                        v_head_dim: hd,
3197                    };
3198                    // CMF_ATTN_ORACLE=1: diff the device attend against
3199                    // this CPU attend on identical inputs (bring-up).
3200                    let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3201                        || std::env::var("CMF_ATTN_DUMP").is_ok();
3202                    let _ = full_gpu;
3203                    let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3204                    let mut ao = attention::qwen_attention_core(
3205                        q_raw,
3206                        k,
3207                        v,
3208                        &mut self.kv_cache.layers[*li],
3209                        &cfg,
3210                    );
3211                    // CMF_ATTN_DUMP=<dir>: this token's rope'd Q and the layer's whole
3212                    // K/V cache as raw f32 (offline attention-statistics probes:
3213                    // block bounds, mass concentration). Needs CMF_GPU_ATTEND=0.
3214                    if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3215                        if let Some((qr0, k0, v0)) = oracle_in.clone() {
3216                            let (cq, _cg, _ck, _cv) =
3217                                attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3218                            let cache = &self.kv_cache.layers[*li];
3219                            let n = cache.head_keys(0).len() / hd;
3220                            let mut bytes: Vec<u8> = Vec::new();
3221                            for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3222                                bytes.extend_from_slice(&v.to_le_bytes());
3223                            }
3224                            for v in &cq {
3225                                bytes.extend_from_slice(&v.to_le_bytes());
3226                            }
3227                            for g in 0..nkv {
3228                                for v in cache.head_keys(g) {
3229                                    bytes.extend_from_slice(&v.to_le_bytes());
3230                                }
3231                            }
3232                            for g in 0..nkv {
3233                                for v in cache.head_values(g) {
3234                                    bytes.extend_from_slice(&v.to_le_bytes());
3235                                }
3236                            }
3237                            let _ =
3238                                std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3239                        }
3240                    }
3241                    if let Some((qr0, k0, v0)) =
3242                        oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3243                    {
3244                        let (cq, _cg, ck, cv) =
3245                            attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3246                        let mut h_now = vec![0f32; hs];
3247                        graph.read_h(&mut h_now);
3248                        let cache = &self.kv_cache.layers[*li];
3249                        let n_after = cache.head_keys(0).len() / hd;
3250                        // A sealed O(1) cache may have no dense current-row
3251                        // entry. The oracle is a debug probe, so let it see
3252                        // zero stored exact rows instead of underflowing.
3253                        let stored = n_after.saturating_sub(1);
3254                        let cpu_k: Vec<&[f32]> = (0..nkv)
3255                            .map(|g| &cache.head_keys(g)[..stored * hd])
3256                            .collect();
3257                        let cpu_v: Vec<&[f32]> = (0..nkv)
3258                            .map(|g| &cache.head_values(g)[..stored * hd])
3259                            .collect();
3260                        let p = crate::gpu::AttnDeviceParams {
3261                            kv_id,
3262                            layer: *li,
3263                            nh,
3264                            nkv,
3265                            hd,
3266                            // The layer's own geometry, as the CPU attend
3267                            // above used it (the projected gate is applied
3268                            // after this probe on both sides).
3269                            rd: rd_l,
3270                            position,
3271                            scale: self.attn_scale,
3272                            eps: eps as f32,
3273                            gemma,
3274                            late_qk_norm: self.qk_norm_after_rope,
3275                            output_gate: *output_gate,
3276                            q_norm: *q_norm,
3277                            k_norm: *k_norm,
3278                            inv_freq: &inv_freq_l,
3279                            rope_scale: self.layer_rope_scale(*li),
3280                            cpu_k,
3281                            cpu_v,
3282                            cpu_stored: stored,
3283                            cpu_gen: cache.generation(),
3284                            o1: None,
3285                            window: window_l,
3286                            softmax_sink: cache.sinks.as_deref(),
3287                            head_gate: None,
3288                            v_norm: self.attn_v_norm,
3289                        };
3290                        if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3291                            let md = |a: &[f32], b: &[f32]| {
3292                                a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3293                            };
3294                            let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3295                            eprintln!(
3296                                "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}",
3297                                nn(&cq),
3298                                md(&cq, &dq),
3299                                nn(&ck),
3300                                md(&ck, &dk),
3301                                nn(&cv),
3302                                md(&cv, &dv),
3303                                nn(&ao),
3304                                md(&ao, &dao)
3305                            );
3306                        } else {
3307                            eprintln!("attn-oracle L{li}: device probe declined");
3308                        }
3309                    }
3310                    if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3311                        // V is as wide as the head (the front gate), so ao
3312                        // is nh·hd here, as in `qwen_attention`.
3313                        attention::apply_projected_gate(
3314                            &mut ao,
3315                            raw,
3316                            per_head,
3317                            hd,
3318                            self.proj_gate_sigmoid,
3319                        );
3320                    }
3321                    if let Some(mut raw) = gate_raw.take() {
3322                        attention::recycle_buf(&mut raw);
3323                    }
3324                    graph.encode_attn_suffix(l, &ao);
3325                    // Early commit: the GPU starts O+FFN while the CPU
3326                    // encodes the following GDN run / attention prefix.
3327                    graph.commit();
3328                    attention::recycle_buf(&mut ao);
3329                }
3330            }
3331
3332            crate::gpu::stageprof(_xkind, _xt0.elapsed());
3333        }
3334        // Ride the final norm + lm_head in the same command buffer when
3335        // this run reaches the model's end and the caller wants logits:
3336        // the separate per-op lm_head submit (a full round trip) folds
3337        // into the sync that already happens here.
3338        let mut lm_rows = None;
3339        if self.graph_want_logits
3340            && upto.is_none()
3341            && end == self.num_layers
3342            && std::env::var("CMF_GPU_LMHEAD")
3343                .map(|v| v != "0")
3344                .unwrap_or(true)
3345        {
3346            if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3347                if graph.lm_head_ok(lm) {
3348                    graph.encode_lm_head(&self.weights.final_norm, lm);
3349                    lm_rows = Some(lm.1);
3350                }
3351            }
3352        }
3353        if self.graph_head_required && lm_rows.is_none() {
3354            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3355            self.fail_metal_graph("fused graph head was requested but not encodable");
3356            return start;
3357        }
3358        let _sy0 = std::time::Instant::now();
3359        if let Err(err) = graph.sync_checked() {
3360            if let Some(profile) = pager_profile_page.as_mut() {
3361                profile.finish(start, graph.pager_page_stats(), "sync-error");
3362            }
3363            self.fail_metal_graph(&err);
3364            return start;
3365        }
3366        let _rs0 = std::time::Instant::now();
3367        if !pending.is_empty() {
3368            let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3369            let mut outs: Vec<&mut [f32]> = self
3370                .kv_cache
3371                .layers
3372                .iter_mut()
3373                .enumerate()
3374                .filter(|(i, _)| idxs.binary_search(i).is_ok())
3375                .map(|(_, s)| s.linear_state.as_mut_slice())
3376                .collect();
3377            graph.read_states(&mut outs);
3378        }
3379        if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3380            use std::sync::atomic::{AtomicU64, Ordering};
3381            static SY: AtomicU64 = AtomicU64::new(0);
3382            static RS: AtomicU64 = AtomicU64::new(0);
3383            static N: AtomicU64 = AtomicU64::new(0);
3384            SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3385            RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3386            let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3387            if n % 100 == 0 {
3388                eprintln!(
3389                    "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
3390                    SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3391                    RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3392                );
3393            }
3394        }
3395        if let Some(rows) = lm_rows {
3396            crate::gpu::hostprof_encode_done(_mt0);
3397            let mut lg = attention::take_buf(rows.min(self.vocab_size));
3398            graph.read_logits(&mut lg);
3399            crate::gpu::hostprof_total(_mt0);
3400            lg.resize(self.vocab_size, 0.0);
3401            if let Some(c) = self.final_softcap {
3402                softcap_in_place(&mut lg, c, self.pool.as_deref());
3403            }
3404            self.graph_logits = Some(lg);
3405        }
3406        graph.read_h(h);
3407        if self.graph_head_required && self.graph_logits.is_none() {
3408            METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3409            self.fail_metal_graph("fused graph head completed without logits readback");
3410            return start;
3411        }
3412        METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3413        METAL_GRAPH_LAYERS.fetch_add(
3414            end.saturating_sub(start) as u64,
3415            std::sync::atomic::Ordering::Relaxed,
3416        );
3417        if self.graph_head_required {
3418            METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3419        }
3420        // Device-attended layers: replay the CPU bookkeeping — append
3421        // the mirror's new K/V row (rope'd on the GPU) into the owner
3422        // cache, then bank this token's attention-importance mass.
3423        for (li, nkv, hd) in dev_attn {
3424            let mut krow = attention::take_buf(nkv * hd);
3425            let mut vrow = attention::take_buf(nkv * hd);
3426            if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3427                let cache = &mut self.kv_cache.layers[li];
3428                cache.append(&krow, &vrow, &[]);
3429                let n = cache.seq_len;
3430                let mut imp = attention::take_buf(n);
3431                crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3432                cache.accumulate_imp(&imp);
3433                attention::recycle_buf(&mut imp);
3434            }
3435            attention::recycle_buf(&mut krow);
3436            attention::recycle_buf(&mut vrow);
3437        }
3438        if let Some(profile) = pager_profile_page.as_mut() {
3439            profile.finish(end, graph.pager_page_stats(), "page-complete");
3440        }
3441        if let Some((_, arm)) = ab {
3442            crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3443        }
3444        end
3445    }
3446
3447    pub fn new(
3448        tokenizer: Tokenizer,
3449        weights: PipelineWeights,
3450        hidden_size: usize,
3451        intermediate_size: usize,
3452        num_heads: usize,
3453        num_kv_heads: usize,
3454        head_dim: usize,
3455        num_layers: usize,
3456        physical_layers: usize,
3457        loop_final_norm: bool,
3458        vocab_size: usize,
3459        rms_eps: f64,
3460        rope_base: f32,
3461        norm_style: NormStyle,
3462        max_seq_len: usize,
3463        sampler_config: SamplerConfig,
3464    ) -> Self {
3465        let rng = match sampler_config.seed {
3466            Some(s) => SplitMix64::new(s),
3467            None => SplitMix64::from_entropy(),
3468        };
3469        let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3470        let pool = Pool::from_env();
3471        if let Some(p) = &pool {
3472            tracing::info!("worker pool: {} threads", p.n_workers());
3473            // Keep the workers on the socket that holds the weights.
3474            if let Some(model) = weights
3475                .lm_head
3476                .model_arc()
3477                .or_else(|| weights.embed_tokens.model_arc())
3478            {
3479                let regions: Vec<&[u8]> =
3480                    model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3481                p.bind_numa(&regions);
3482            }
3483        }
3484        Self {
3485            gpu_plan: None,
3486            tokenizer: std::sync::Arc::new(tokenizer),
3487            kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3488            sampler_config,
3489            weights,
3490            hidden_size,
3491            intermediate_size,
3492            num_heads,
3493            num_kv_heads,
3494            head_dim,
3495            num_layers,
3496            physical_layers,
3497            loop_final_norm,
3498            vocab_size,
3499            rms_eps,
3500            rope_base,
3501            norm_style,
3502            rotary_dim: head_dim,
3503            attention_heads_per_layer: None,
3504            kv_heads_per_layer: None,
3505            v_head_dim: None,
3506            layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3507                .filter(|v| !v.is_empty())
3508                .map(std::path::PathBuf::from),
3509            graph_declines: std::cell::RefCell::new(Vec::new()),
3510            mimo_moe: Default::default(),
3511            minicpm_tail: Default::default(),
3512            vmf_cfg: None,
3513            gdn_cfg: None,
3514            kda_cfg: None,
3515            g3n: None,
3516            dsv4: None,
3517            dsv41: None,
3518            dsv41_vision: None,
3519            dsv41_prefill: None,
3520            qwen4_exp: None,
3521            dsv4_mtp: Vec::new(),
3522            dspark: None,
3523            dspark_pending: Vec::new(),
3524            dspark_hist: Vec::new(),
3525            dspark_real: Vec::new(),
3526            dspark_trunk_picks: Vec::new(),
3527            dspark_exp: Vec::new(),
3528            dspark_draft_ns: 0,
3529            logit_multiplier: None,
3530            cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3531            graph_failed: std::sync::atomic::AtomicBool::new(false),
3532            kv_history: Vec::new(),
3533            kv_history_device: false,
3534            short_conv_cfg: None,
3535            mtp: None,
3536            mimo_mtp: None,
3537            verify_exact_moe: false,
3538            speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3539            ignore_eos: false,
3540            draft_full_streak: 0,
3541            spec_k_adapt: None,
3542            spec_acc_ewma: 0.7,
3543            rng,
3544            sampler_scratch: SamplerScratch::default(),
3545            spec_forced: None,
3546            spec_q: Vec::new(),
3547            spec_p: Vec::new(),
3548            spec_res: Vec::new(),
3549            spec_qs: Vec::new(),
3550            spec_ps: Vec::new(),
3551            spec_ress: Vec::new(),
3552            mtp_graph_mode: None,
3553            #[cfg(target_os = "macos")]
3554            metal_verify: None,
3555            inv_freq,
3556            ws: ForwardScratch::new(hidden_size),
3557            pool,
3558            model: None,
3559            dyn_force_f32: false,
3560            dyn_skill_layers: Vec::new(),
3561            dyn_active: None,
3562            dyn_blend_loaded: false,
3563            dyn_phi_layer: None,
3564            dyn_phi_ema: Vec::new(),
3565            dyn_phi_seen: 0,
3566            dyn_router: None,
3567            o1_cfg: None,
3568            o1_epoch: 0,
3569            o1_flags: Vec::new(),
3570            trace: false,
3571            calib_temp: 1.0,
3572            confidence_on: true,
3573            embed_multiplier: 1.0,
3574            attn_scale: 1.0 / (head_dim as f32).sqrt(),
3575            swa: None,
3576            swa_trim: None,
3577            sliding_layers: None,
3578            no_rope_layers: None,
3579            anchor_core: None,
3580            bounded_rope: None,
3581            kv_prefix: KvPrefix::default(),
3582            last_prefill_tokens: 0,
3583            inv_freq_local: None,
3584            rotary_dim_local: None,
3585            rope_scale: 1.0,
3586            rope_scale_local: 1.0,
3587            global_attn: None,
3588            inv_freq_global: None,
3589            attn_v_norm: false,
3590            qk_norm_after_rope: false,
3591            proj_gate_sigmoid: false,
3592            final_softcap: None,
3593            head_clusters: None,
3594            attn_softcap: 0.0,
3595            graph_want_logits: false,
3596            graph_head_required: false,
3597            graph_logits: None,
3598            embryo_graph: None,
3599            graph_refused: std::sync::atomic::AtomicBool::new(false),
3600            graph_kv_id: {
3601                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3602                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3603            },
3604            #[cfg(test)]
3605            nll_test_fail_at: None,
3606            #[cfg(test)]
3607            nll_test_force_serial: false,
3608        }
3609    }
3610
3611    /// Enable/disable per-layer O(1) Nyström attention. Only Full
3612    /// layers are eligible (a linear layer keeps its own operator).
3613    /// Applies to generation (`generate*`/`forward_ids`): the prompt
3614    /// pass stays exact, then the state seals after prefill or at the
3615    /// deferred skeleton-safe boundary for short prompts; decode runs on
3616    /// the O(1) state. Teacher-forced scoring (`ppl_ids`) intentionally
3617    /// stays exact.
3618    pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3619        if let Err(e) = self.try_set_o1(cfg) {
3620            tracing::error!("{e}");
3621        }
3622    }
3623
3624    /// True when the file carries a native bounded anchor
3625    /// (`arch.anchor_core`): its state is a fixed record the header
3626    /// fixes, and the post-hoc O(1) override is meaningless on it.
3627    pub fn bounded_native(&self) -> bool {
3628        self.anchor_core.is_some()
3629    }
3630
3631    /// Bytes the resident device graph holds for this pipeline's sequence:
3632    /// `(recurrent state, anchor KV/ring)`; None on the host path.
3633    pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3634        crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3635    }
3636
3637    /// Why an O(1) override is refused on this pipeline, if it is.
3638    pub fn o1_refusal(&self) -> Option<String> {
3639        self.anchor_core.as_ref().map(|ac| {
3640            format!(
3641                "--o1 / CMF_O1 refused: the anchor is native bounded \
3642                 (anchor_core kind={} window={} sink={}); the file's operator \
3643                 is executed as-is and no post-hoc Nyström overlay applies",
3644                ac.kind, ac.window, ac.sink
3645            )
3646        })
3647    }
3648
3649    /// `set_o1` that reports the refusal instead of logging it.
3650    pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3651        if let Some(c) = &cfg {
3652            if let Some(why) = self.o1_refusal() {
3653                self.o1_flags = Vec::new();
3654                self.o1_cfg = None;
3655                return Err(why);
3656            }
3657            if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3658                self.o1_flags.clear();
3659                self.o1_cfg = None;
3660                return Err(format!(
3661                    "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3662                    c.w, c.sink
3663                ));
3664            }
3665        }
3666        self.o1_flags = match &cfg {
3667            Some(c) => {
3668                let mut flags = c.layer_flags(self.num_layers);
3669                for (li, f) in flags.iter_mut().enumerate() {
3670                    // The Nyström state replaces a full-context plain
3671                    // softmax: a sliding window or a learned sink is not
3672                    // something it can represent, and a V narrower than
3673                    // the head is not what its streaming state stores.
3674                    // Those layers keep exact cache attention.
3675                    if *f
3676                        && (!matches!(
3677                            self.weights.layers[self.phys_layer(li)].attn,
3678                            AttnKind::Full { .. }
3679                        ) || self.layer_window(li).is_some()
3680                            || self.kv_cache.layers[li].sinks.is_some()
3681                            || self.layer_v_dim(li) != self.layer_geom(li).1)
3682                    {
3683                        *f = false;
3684                    }
3685                }
3686                flags
3687            }
3688            None => Vec::new(),
3689        };
3690        if let Some(c) = &cfg {
3691            let n = self.o1_flags.iter().filter(|&&f| f).count();
3692            tracing::info!(
3693                "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3694                self.num_layers,
3695                c.m,
3696                c.w,
3697                c.sink,
3698                c.rect
3699            );
3700        }
3701        self.o1_cfg = cfg;
3702        Ok(())
3703    }
3704
3705    /// Install the file's bounded anchor: one fixed-size ring per
3706    /// `AttnKind::Bounded` layer (from the header, not per prompt) and
3707    /// the shared relative-rotation table. Must run after the RoPE setup
3708    /// (`set_rotary`, YaRN) so the table is built from the final
3709    /// `inv_freq`.
3710    pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
3711        if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3712            return Err(format!(
3713                "anchor_core kind '{}' is not executable by this runtime",
3714                cfg.kind
3715            ));
3716        }
3717        if cfg.window == 0 {
3718            return Err("anchor_core.window must be >= 1".into());
3719        }
3720        let mut n = 0usize;
3721        for li in 0..self.num_layers {
3722            let pl = self.phys_layer(li);
3723            if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3724                if w.window != cfg.window || w.sink != cfg.sink {
3725                    return Err(format!(
3726                        "layer {li}: bounded weights (window {} sink {}) disagree with \
3727                         anchor_core (window {} sink {})",
3728                        w.window, w.sink, cfg.window, cfg.sink
3729                    ));
3730                }
3731                self.kv_cache.layers[li].install_bounded(cfg.window);
3732                n += 1;
3733            }
3734        }
3735        if n == 0 {
3736            return Err("anchor_core is present but no layer executes it".into());
3737        }
3738        let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3739        self.bounded_rope = Some(std::sync::Arc::new(rope));
3740        self.anchor_core = Some(cfg.clone());
3741        self.embryo_graph = None;
3742        tracing::info!(
3743            "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3744            cfg.kind,
3745            cfg.window,
3746            cfg.sink,
3747            self.kv_cache
3748                .layers
3749                .iter()
3750                .map(|l| l.bounded_state_bytes())
3751                .max()
3752                .unwrap_or(0)
3753        );
3754        Ok(())
3755    }
3756
3757    /// Tag every layer's cache with its wire record kind and the model's
3758    /// operator identity (hash64 of `linear_core_identity` JSON) so the
3759    /// versioned state wire refuses a peer holding another operator.
3760    pub fn install_wire_identity(&mut self, identity: u64) {
3761        for li in 0..self.kv_cache.layers.len() {
3762            let pl = self.phys_layer(li);
3763            let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3764                Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3765                Some(AttnKind::Linear(_))
3766                | Some(AttnKind::LinearGdn(_))
3767                | Some(AttnKind::ShortConv(_))
3768                | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3769                _ => crate::kv_cache::WireKind::Full,
3770            };
3771            let l = &mut self.kv_cache.layers[li];
3772            l.wire_kind = kind;
3773            l.wire_identity = identity;
3774            // A per-layer geometry (`set_attn_geometry`, Gemma's global
3775            // heads) rebuilds a layer cache with index 0: re-tag it.
3776            l.wire_layer = li as u32;
3777        }
3778    }
3779
3780    /// Forget the reuse keys (legacy `kv_history` and the bounded prefix).
3781    pub fn clear_history(&mut self) {
3782        self.kv_history.clear();
3783        self.kv_history_device = false;
3784        self.kv_prefix.clear();
3785    }
3786
3787    /// Did THIS pipeline's token graph refuse for a structural reason?
3788    /// (Per pipeline: another lane's refusal, or a new pipeline of the
3789    /// same model, never changes it.)
3790    pub fn graph_refused(&self) -> bool {
3791        self.graph_refused
3792            .load(std::sync::atomic::Ordering::Relaxed)
3793    }
3794
3795    /// Remember a structural refusal of this pipeline's token graph.
3796    pub fn mark_graph_refused(&self) {
3797        if !self
3798            .graph_refused
3799            .swap(true, std::sync::atomic::Ordering::Relaxed)
3800        {
3801            tracing::info!(
3802                "token graph: unsupported for this pipeline (seq {}) — not retrying",
3803                self.graph_kv_id
3804            );
3805        }
3806    }
3807
3808    /// Position the resident Embryo graph holds for this pipeline's
3809    /// sequence (`Some(next position)`), `None` when the device holds no
3810    /// image of it (host-owned sequence, or none started).
3811    pub fn device_sequence_position(&self) -> Option<usize> {
3812        crate::gpu::embryo_device_next_position(self.graph_kv_id)
3813    }
3814
3815    /// Is the sequence this pipeline's reuse key describes owned by the
3816    /// device path it would take now? A prefix recorded on the resident
3817    /// graph continues only there (at exactly `n`), a host prefix only on
3818    /// the host; any mismatch means the next turn re-prefills from zero.
3819    fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
3820        let dev = self.device_sequence_position();
3821        if recorded_on_device {
3822            dev == Some(n) && self.embryo_resident_wanted()
3823        } else {
3824            dev.is_none()
3825        }
3826    }
3827
3828    /// The weights under the sequence changed (a real skill switch): every
3829    /// cached state was computed by other weights. Clear the host KV /
3830    /// ring / recurrent state AND the reuse keys — a surviving `kv_prefix`
3831    /// would let the next turn "extend" a prefix the new weights never
3832    /// saw — drop the packed resident graph (it holds the old FFN
3833    /// tensors; the next build packs the live ones under a fresh id) and
3834    /// reset its device sequence.
3835    pub(crate) fn invalidate_for_weight_change(&mut self) {
3836        self.clear_sequence_state();
3837        self.embryo_graph = None;
3838    }
3839
3840    /// Prompt positions the cache already holds when `input_ids`
3841    /// strictly EXTENDS the consumed prefix (0 otherwise). Bounded-native
3842    /// models answer from the fixed-size `kv_prefix` record; everything
3843    /// else from the legacy `kv_history` vector (which the network split
3844    /// also reads and writes).
3845    fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
3846        let (n, on_device) = if self.bounded_native() {
3847            (
3848                self.kv_prefix.extension(input_ids),
3849                self.kv_prefix.on_device(),
3850            )
3851        } else {
3852            let h = &self.kv_history;
3853            if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
3854                (h.len(), self.kv_history_device)
3855            } else {
3856                (0, false)
3857            }
3858        };
3859        // The owner tag: the state the key describes must live where this
3860        // turn will continue it (R4) — else a fresh sequence.
3861        if n > 0 && !self.prefix_owner_matches(n, on_device) {
3862            tracing::warn!(
3863                "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
3864                 the device now holds {:?} — re-prefilling from zero",
3865                if on_device { "resident device" } else { "host" },
3866                self.device_sequence_position()
3867            );
3868            return 0;
3869        }
3870        n
3871    }
3872
3873    /// Public view of the prefix-reuse decision for `input_ids` (positions
3874    /// the next `generate*` would take from the cache; 0 = fresh sequence).
3875    pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
3876        self.cached_prefix_len(input_ids)
3877    }
3878
3879    /// Record the forwarded prefix as the next turn's reuse key. A
3880    /// bounded-native model extends the rolling record (`reused` = the
3881    /// positions this turn found cached); others keep the literal vector.
3882    /// Either way the key carries its OWNER: the resident device graph
3883    /// (it holds an image of this sequence) or the host.
3884    fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
3885        let on_device = self.device_sequence_position().is_some();
3886        if self.bounded_native() {
3887            let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
3888            let prev_device = self.kv_prefix.on_device();
3889            self.kv_history.clear();
3890            self.kv_history_device = false;
3891            if keep && prev_device == on_device {
3892                self.kv_prefix.extend(&consumed[reused..]);
3893            } else {
3894                self.kv_prefix.set(consumed);
3895            }
3896            self.kv_prefix.set_on_device(on_device);
3897        } else {
3898            self.kv_history = consumed.to_vec();
3899            self.kv_history_device = on_device;
3900        }
3901    }
3902
3903    /// True when at least one layer runs the O(1) kernel.
3904    pub fn o1_active(&self) -> bool {
3905        self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
3906    }
3907
3908    /// Whether generation's prompt ingest is routed through the whole-token
3909    /// graph.  The bench uses this to label the measured generation prefill
3910    /// honestly; keep the predicate in Pipeline so CLI labels cannot drift
3911    /// from the production route.
3912    /// Positions per batched-graph submit for the prompt: `CMF_BATCH_K`
3913    /// when set (0 = one position at a time through the token graph),
3914    /// otherwise 128 (32 for a MoE stack) on a discrete card whose prompt
3915    /// takes the graph route.
3916    /// The batched graph read a 2048-token prompt at 53 tok/s against 28.5
3917    /// one position at a time on an RTX PRO 4000 (Qwen3.8-27B q4tp: TTFT
3918    /// 39 s against 72), and its states are the speculative verify's,
3919    /// measured identical to the plain path. macOS keeps its own arm.
3920    /// 128, not 32: the q4tp prefill GEMM tiles 64 batch rows, so a
3921    /// 32-row chunk left half of every tile idle. Qwen3.8-27B q4tp on an
3922    /// RTX 3090, 1000-token prompt: 51 tok/s at 32, 82 at 64, 95 at 128,
3923    /// 97 at 256; greedy output byte-identical to the 32-row chunks
3924    /// (every GEMM row and the k-looped recurrences are per position).
3925    pub fn generation_batch_k(&self) -> usize {
3926        if let Some(k) = std::env::var("CMF_BATCH_K")
3927            .ok()
3928            .and_then(|v| v.parse::<usize>().ok())
3929        {
3930            return k;
3931        }
3932        // Gemma-4 (`g4_batch_prefill`): 128-row chunks, the GEMM tile's
3933        // two 64-row halves full.
3934        if self.g4_batch_prefill() {
3935            return 128;
3936        }
3937        #[cfg(not(target_os = "macos"))]
3938        if self.graph_prefill_preferred() && !self.o1_active() {
3939            // MoE stacks keep the 32 their expert-grouped prefill was
3940            // measured at; 128 is measured on the dense hybrids only.
3941            let moe = self
3942                .weights
3943                .layers
3944                .iter()
3945                .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
3946            // q8_2f experts (moe_q82's grouped prefill) read each expert
3947            // once per group of up to 16 of its entries, so the chunk that
3948            // fills those groups is the faster ingest: Mellum2.1 q8_2f on an
3949            // RTX 3090, 4000-token prompt, 503 tok/s at 64 rows, 542 at 128
3950            // (and 542 at 256).
3951            let q82 = crate::gpu::moe_q82_graph_on()
3952                && self.weights.layers.iter().any(|lw| {
3953                    matches!(&lw.ffn, FfnKind::Moe(m)
3954                        if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
3955                });
3956            return if q82 {
3957                128
3958            } else if moe {
3959                32
3960            } else {
3961                128
3962            };
3963        }
3964        0
3965    }
3966
3967    pub fn generation_graph_prefill(&self) -> bool {
3968        let graph = self.graph_prefill_preferred();
3969        // On wgpu, an active MTP head now consumes the trunk's graph batches
3970        // and warms its own block from those returned rows.  The selected
3971        // generation measurement is therefore the batched path, even though
3972        // the underlying GDN model still satisfies the graph-prefill
3973        // predicate.  Keep the CLI label tied to the actual route.  Native
3974        // Metal has a separate prefill-batch arm and retains its historical
3975        // label here.
3976        // A batched prompt (`generation_batch_k` > 0) is the batched graph
3977        // for every model on the graph route, not only those with an MTP
3978        // head — the label follows the route.
3979        #[cfg(not(target_os = "macos"))]
3980        if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
3981            return false;
3982        }
3983        graph
3984    }
3985
3986    /// Device-side O(1) mirrors currently uploaded for this pipeline's
3987    /// sequence.  The count/bytes are zero before seal or after a fresh
3988    /// reset; callers use this to distinguish logical host state from the
3989    /// GPU allocation that actually serves decode.
3990    pub fn o1_device_stats(&self) -> (usize, u64) {
3991        crate::gpu::o1_device_stats(self.graph_kv_id)
3992    }
3993
3994    /// Arm query collection on the o1 layers (fresh prompt pass).
3995    /// Reset the o1 layers to Collecting for a fresh sequence. Pub for the
3996    /// network split: each side runs the o1 lifecycle over ITS OWN layers
3997    /// (begin before prefill, seal at the prefill barrier).
3998    pub fn o1_begin(&mut self) {
3999        self.o1_begin_with_prefix(None);
4000    }
4001
4002    /// Arm collection and optionally request a positive calibration prefix.
4003    /// The effective barrier is always at least the skeleton-safe floor, so
4004    /// a short requested prefix cannot create an exact-only runtime state.
4005    pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4006        if let Some(c) = &self.o1_cfg {
4007            let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4008            let boundary = requested_prefix.map(|p| {
4009                p.max(
4010                    crate::nystrom::o1_deferred_boundary(w, sink)
4011                        .expect("o1 config boundary validated in set_o1"),
4012                )
4013            });
4014            for (li, &f) in self.o1_flags.iter().enumerate() {
4015                if f {
4016                    self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4017                }
4018            }
4019        }
4020    }
4021
4022    /// Effective deferred boundary for a positive prefix request.
4023    fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4024        self.o1_cfg.as_ref().and_then(|c| {
4025            crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4026                .map(|floor| requested_prefix.max(floor))
4027        })
4028    }
4029
4030    fn o1_note_transition(&mut self) {
4031        // Drain every layer's one-shot bit before publishing one pipeline
4032        // epoch. `any()` would short-circuit on the first layer and leak the
4033        // remaining bits into later forwards, causing one epoch per layer.
4034        let mut transitioned = false;
4035        for (li, &flagged) in self.o1_flags.iter().enumerate() {
4036            if flagged {
4037                transitioned |= self.kv_cache.layers[li].take_o1_transition();
4038            }
4039        }
4040        if transitioned {
4041            self.o1_epoch = self.o1_epoch.wrapping_add(1);
4042        }
4043    }
4044
4045    fn o1_pending(&self) -> bool {
4046        self.o1_flags.iter().enumerate().any(|(li, &f)| {
4047            f && self.kv_cache.layers[li].seq_len > 0
4048                && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4049        })
4050    }
4051
4052    fn o1_fail(&mut self, err: String) {
4053        tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4054        self.clear_sequence_state();
4055        self.graph_failed
4056            .store(true, std::sync::atomic::Ordering::Relaxed);
4057        self.cancel
4058            .store(true, std::sync::atomic::Ordering::Relaxed);
4059    }
4060
4061    /// Seal participating layers while retaining the exact state when the
4062    /// prompt is below the deferred boundary. A split worker may have
4063    /// collecting layers outside its owned span; zero-depth layers remain
4064    /// armed and are intentionally skipped until their peer runs them.
4065    pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4066        if self.o1_cfg.is_none() {
4067            return Ok(false);
4068        }
4069        let mut participating = false;
4070        for li in 0..self.num_layers {
4071            if !self.o1_flags.get(li).copied().unwrap_or(false) {
4072                continue;
4073            }
4074            if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4075                return Err(err);
4076            }
4077            if self.kv_cache.layers[li].seq_len == 0 {
4078                continue;
4079            }
4080            participating = true;
4081            let num_heads = self.layer_num_heads(li);
4082            self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4083        }
4084        self.o1_note_transition();
4085        for li in 0..self.num_layers {
4086            if self.o1_flags.get(li).copied().unwrap_or(false) {
4087                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4088                    return Err(err);
4089                }
4090            }
4091        }
4092        Ok(participating
4093            && (0..self.num_layers).all(|li| {
4094                !self.o1_flags.get(li).copied().unwrap_or(false)
4095                    || self.kv_cache.layers[li].seq_len == 0
4096                    || self.kv_cache.layers[li].o1_sealed()
4097            }))
4098    }
4099
4100    /// Complete a deferred boundary after a full position/span forward.
4101    /// This is the pipeline owner for epoch publication and failure cleanup.
4102    fn o1_progress(&mut self) {
4103        if !self.o1_active() {
4104            return;
4105        }
4106        for li in 0..self.num_layers {
4107            if self.o1_flags.get(li).copied().unwrap_or(false) {
4108                if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4109                    self.o1_fail(err);
4110                    return;
4111                }
4112            }
4113        }
4114        // A qwen_attention row can seal in the middle of a complete layer
4115        // walk. Consume its transition even though the pending boundary has
4116        // already disappeared from the cache.
4117        self.o1_note_transition();
4118        if !self.o1_pending() {
4119            return;
4120        }
4121        if let Err(err) = self.o1_seal_checked() {
4122            self.o1_fail(err);
4123        }
4124    }
4125
4126    /// Turn a deferred O(1) failure raised by a hidden-only forward into the
4127    /// Result error its public batch/span caller must return. The failure
4128    /// path already cleared host/device sequence state; consume only the
4129    /// side-channel marker here and leave the pipeline reusable.
4130    fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4131        if self
4132            .graph_failed
4133            .swap(false, std::sync::atomic::Ordering::Relaxed)
4134        {
4135            self.cancel
4136                .store(false, std::sync::atomic::Ordering::Relaxed);
4137            self.clear_sequence_state();
4138            return Err(format!("{phase}: deferred O(1) transition failed"));
4139        }
4140        Ok(())
4141    }
4142
4143    /// Freeze landmarks + skeleton state after the prompt pass and drop
4144    /// the o1 layers' full KV; decode then runs `step()` per token.
4145    /// Pub for the network split (see `o1_begin`).
4146    pub fn o1_seal(&mut self) {
4147        if let Err(err) = self.o1_seal_checked() {
4148            self.o1_fail(err);
4149        }
4150    }
4151
4152    /// Enable/disable the structured per-token telemetry trace (B4).
4153    pub fn set_trace(&mut self, on: bool) {
4154        self.trace = on;
4155    }
4156
4157    /// Replace all request-scoped sampler options and reset the random stream.
4158    /// This is required for deterministic `seed` semantics in pooled servers.
4159    pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4160        self.rng = match config.seed {
4161            Some(seed) => SplitMix64::new(seed),
4162            None => SplitMix64::from_entropy(),
4163        };
4164        self.sampler_config = config;
4165    }
4166
4167    /// Toggle the per-token confidence reduction (a full-vocab
4168    /// softmax each token). `bench --core` turns it off so the timed
4169    /// loop matches llama-bench's core contract; the result's
4170    /// `confidence` vec is empty while off.
4171    pub fn set_confidence(&mut self, on: bool) {
4172        self.confidence_on = on;
4173    }
4174
4175    /// Set the confidence-calibration temperature (B1). Values ≤0 are
4176    /// clamped to raw (1.0).
4177    pub fn set_calib_temp(&mut self, t: f32) {
4178        self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4179    }
4180
4181    /// The active calibration temperature (1.0 = raw probability).
4182    pub fn calib_temp(&self) -> f32 {
4183        self.calib_temp
4184    }
4185
4186    /// The primary (full-attention) RoPE inverse-frequency table selected
4187    /// from the CMF architecture.  This is read-only diagnostic state for
4188    /// parity checks; local sliding layers may use `inv_freq_local` instead.
4189    pub fn rope_inv_freq(&self) -> &[f32] {
4190        self.inv_freq.as_slice()
4191    }
4192
4193    /// Partial rotary (Qwen3.5): rotate only the first `rotary_dim` dims;
4194    /// the frequency table is rebuilt over the rotary dims.
4195    pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4196        self.rotary_dim = rotary_dim.min(self.head_dim);
4197        self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4198        // The packed resident graph owns its own inverse-frequency plane;
4199        // changing RoPE after it was built must not leave a stale device
4200        // model behind the exact host configuration.
4201        self.embryo_graph = None;
4202    }
4203
4204    fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4205        QwenAttnCfg {
4206            num_heads: self.num_heads,
4207            num_kv_heads: self.num_kv_heads,
4208            head_dim: self.head_dim,
4209            hidden_size: self.hidden_size,
4210            position,
4211            inv_freq: &self.inv_freq,
4212            rotary_dim: self.rotary_dim,
4213            scale: self.attn_scale,
4214            softcap: self.attn_softcap,
4215            window: None,
4216            v_norm: false,
4217            qk_norm_after_rope: self.qk_norm_after_rope,
4218            gate_sigmoid: self.proj_gate_sigmoid,
4219            q_norm: None,
4220            k_norm: None,
4221            output_gate: false,
4222            softplus_gate: None,
4223            rope_scale: self.rope_scale,
4224            bias: None,
4225            rms_eps: self.rms_eps,
4226            norm_style: self.norm_style,
4227            pool: self.pool.as_deref(),
4228            v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4229        }
4230    }
4231
4232    /// Generate text from a plain-text prompt. Streams tokens via `on_token`.
4233    pub fn generate(
4234        &mut self,
4235        prompt: &str,
4236        max_tokens: usize,
4237        task_mask: Option<&TaskMask>,
4238        on_token: Option<TokenCallback>,
4239    ) -> Result<GenerateResult, String> {
4240        let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4241        self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4242    }
4243
4244    /// Generate from a V4.1 multimodal prompt prepared by the vision module.
4245    /// Vision rows are encoded once and fed through the same bounded token walk as text.
4246    pub fn generate_from_vl(
4247        &mut self,
4248        input: &crate::dsv41_vision::PreparedVlInputs,
4249        max_tokens: usize,
4250        task_mask: Option<&TaskMask>,
4251        on_token: Option<TokenCallback>,
4252    ) -> Result<GenerateResult, String> {
4253        let Some(dsv41) = &self.dsv41 else {
4254            return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4255        };
4256        if input.token_ids.is_empty() {
4257            return Err("empty V4.1 multimodal prompt".into());
4258        }
4259        if input.token_types.len() != input.token_ids.len() {
4260            return Err(format!(
4261                "V4.1 token type count {} != token count {}",
4262                input.token_types.len(),
4263                input.token_ids.len()
4264            ));
4265        }
4266        let dim = dsv41.2.dim;
4267        let mut embeddings = vec![None; input.token_ids.len()];
4268        let mut participates = vec![true; input.token_ids.len()];
4269        if !input.images.is_empty() {
4270            let vision = self
4271                .dsv41_vision
4272                .as_ref()
4273                .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4274            for image in &input.images {
4275                let end = image.start.saturating_add(image.types.len());
4276                if end > input.token_ids.len() {
4277                    return Err(format!(
4278                        "V4.1 image span {}..{} exceeds prompt length {}",
4279                        image.start,
4280                        end,
4281                        input.token_ids.len()
4282                    ));
4283                }
4284                let mut span = vec![0.0f32; image.types.len() * dim];
4285                vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4286                for (offset, &kind) in image.types.iter().enumerate() {
4287                    let pos = image.start + offset;
4288                    if input.token_types[pos] != kind {
4289                        return Err(format!(
4290                            "V4.1 image type mismatch at position {pos}: {} != {kind}",
4291                            input.token_types[pos]
4292                        ));
4293                    }
4294                    embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4295                    participates[pos] = false;
4296                }
4297            }
4298        }
4299        for (pos, &kind) in input.token_types.iter().enumerate() {
4300            if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4301                return Err(format!("V4.1 text position {pos} has an image embedding"));
4302            }
4303            if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4304                return Err(format!("V4.1 image position {pos} has no image embedding"));
4305            }
4306        }
4307        self.dsv41_prefill = Some((embeddings, participates));
4308        let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4309        self.dsv41_prefill = None;
4310        result
4311    }
4312
4313    /// `None` when the mask forbids nothing (see `TaskMask::fully_open`).
4314    fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4315        m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4316    }
4317
4318    /// Generate from prepared token ids (e.g. a chat template).
4319    ///
4320    /// With an MTP head, greedy generation without a task mask takes the
4321    /// speculative path: the MTP module drafts the token after next and
4322    /// the main model verifies both in one fused two-position forward
4323    /// (weights streamed once). The output is EXACTLY the vanilla greedy
4324    /// sequence — a rejected draft is rolled back — MTP only buys speed.
4325    pub fn generate_from_ids(
4326        &mut self,
4327        input_ids: &[u32],
4328        max_tokens: usize,
4329        task_mask: Option<&TaskMask>,
4330        on_token: Option<TokenCallback>,
4331    ) -> Result<GenerateResult, String> {
4332        self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4333    }
4334
4335    /// Generate from complete prompt embeddings [token_count, hidden_size].
4336    /// Text rows can be obtained with `embed_id`; media rows replace only
4337    /// their expanded placeholder positions. Rows are already scaled and
4338    /// enter `PrefillIn::Hidden`, so a device graph must not re-embed them.
4339    /// Token-only KV reuse is disabled both into and out of this request.
4340    pub fn generate_from_embeds(
4341        &mut self,
4342        input_ids: &[u32],
4343        prompt_rows: &[f32],
4344        max_tokens: usize,
4345        task_mask: Option<&TaskMask>,
4346        on_token: Option<TokenCallback>,
4347    ) -> Result<GenerateResult, String> {
4348        if input_ids.is_empty()
4349            || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4350        {
4351            return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4352        }
4353        if prompt_rows.iter().any(|x| !x.is_finite()) {
4354            return Err("embedded prompt contains non-finite values".into());
4355        }
4356        if !self.can_prefill_batched()
4357            || self.dyn_router.is_some()
4358            || self.o1_active()
4359            || self.mtp.is_some()
4360            || self.gpu_plan.is_some()
4361        {
4362            return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4363        }
4364        self.generate_with_prompt_rows(
4365            input_ids,
4366            Some(prompt_rows),
4367            max_tokens,
4368            task_mask,
4369            on_token,
4370        )
4371    }
4372
4373    fn generate_with_prompt_rows(
4374        &mut self,
4375        input_ids: &[u32],
4376        prompt_rows: Option<&[f32]>,
4377        max_tokens: usize,
4378        task_mask: Option<&TaskMask>,
4379        mut on_token: Option<TokenCallback>,
4380    ) -> Result<GenerateResult, String> {
4381        #[cfg(target_os = "macos")]
4382        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4383        if std::env::var("CMF_TRACE_H").is_ok() {
4384            eprintln!("input_ids: {input_ids:?}");
4385        }
4386        if input_ids.is_empty() {
4387            return Err("empty prompt: nothing to generate from".to_string());
4388        }
4389        // A prior graph failure is terminal for that sequence but must not
4390        // poison the next independent request.  Keep this flag separate from
4391        // the externally-owned cooperative cancel bit.
4392        self.graph_failed
4393            .store(false, std::sync::atomic::Ordering::Relaxed);
4394        // A mask that forbids nothing still costs every fused path and
4395        // whole-token graph, all of which are gated on `is_none()`. A
4396        // narrowed file whose one segment is always on carries exactly
4397        // such a mask — drop it here rather than pay 5x for a no-op.
4398        let task_mask = self.drop_open_mask(task_mask);
4399
4400        // Cross-turn KV reuse: a chat app resends the whole history
4401        // every turn; when the new ids strictly EXTEND what the cache
4402        // already holds, prefill only the tail — turn latency stays
4403        // proportional to the new text instead of the whole session.
4404        // Extension-only (no rollback), so it is exact for every layer
4405        // kind including recurrent state; MTP/o1/task-mask runs keep
4406        // the fresh-sequence path. CMF_KV_REUSE=0 disables.
4407        let mut reuse_from = {
4408            let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4409            if on
4410                && prompt_rows.is_none()
4411                && task_mask.is_none()
4412                && self.mtp.is_none()
4413                && !(self.mimo_mtp.is_some() && self.speculative)
4414                && self.o1_cfg.is_none()
4415                && self.dsv41.is_none()
4416            {
4417                self.cached_prefix_len(input_ids)
4418            } else {
4419                0
4420            }
4421        };
4422        // The device may own rows the host tail prefill needs (wgpu decode
4423        // writes only its mirror): hand them to the host, or start fresh.
4424        if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4425            reuse_from = 0;
4426        }
4427        self.last_prefill_tokens = input_ids.len() - reuse_from;
4428        let bounded_native = self.bounded_native();
4429        if reuse_from == 0 {
4430            // Fresh sequence — the cache holds absolute positions.
4431            self.clear_sequence_state();
4432        } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4433            eprintln!(
4434                "kv-reuse: {} of {} prompt positions already cached",
4435                reuse_from,
4436                input_ids.len()
4437            );
4438        }
4439        crate::gpu::graph_race_begin_generation();
4440        // Optional bounded calibration prefix. Keep the requested value
4441        // even when it is longer than the prompt; the collecting layer will
4442        // defer at the effective boundary and remain exact for short input.
4443        let o1_prefill = if self.o1_active() && task_mask.is_none() {
4444            std::env::var("CMF_O1_PREFILL")
4445                .ok()
4446                .and_then(|v| v.parse::<usize>().ok())
4447                .filter(|&p| p > 0)
4448        } else {
4449            None
4450        };
4451        if task_mask.is_none() {
4452            self.o1_begin_with_prefix(o1_prefill);
4453        }
4454
4455        // Speculative decode is off under o1: a rejected draft can't be
4456        // rolled back out of the far accumulators / ring window (the
4457        // Nyström insertion is irreversible by design).
4458        // The wgpu token graph owns a device K/V mirror that speculative
4459        // rollback would desync — the two are mutually exclusive.
4460        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4461        // Graph speculative decode (`CMF_GRAPH_SPEC=1`): the MTP head
4462        // drafts, ONE batched graph submit verifies the whole chain.
4463        //
4464        // It now PAYS on Qwen3.6-27B / RTX 5090 — 51.1 tok/s against a
4465        // plain 49.4 at k=3, medians of three, 89% of drafts accepted,
4466        // and the greedy continuation is byte-identical to the plain
4467        // path. That took the batch matvec sharing its nibble unpack
4468        // across the batch (`CMF_MV_BK=2`); before it, the same round
4469        // measured 43.6, an 11% LOSS, which is what the earlier note
4470        // here described.
4471        //
4472        // Still opt-in. One model's win is not a default: the verify
4473        // rides `gdn_spec_restore` and a batched frame whose numerics
4474        // are the batch kernels', and that has to be shown on more than
4475        // one architecture before every greedy decode takes it.
4476        // Greedy (with or without penalties) verifies by argmax equality.
4477        // Sampling (temperature > 0) can go through speculative SAMPLING —
4478        // draft from the MTP head's own post-chain distribution, accept
4479        // with min(1, p/q), correct from max(0, p − q); the emitted stream
4480        // is distributed exactly as the plain sampler's — but it is
4481        // OPT-IN (`CMF_GRAPH_SPEC_SAMPLE=1`): measured on Qwen3.8-27B /
4482        // RTX 5090 at the instruct row (0.7 / 0.80 / 20 / presence 1.5)
4483        // it decoded 19-22 tok/s against a plain 40 — nine post-chain
4484        // distributions a round plus a lower acceptance than greedy's,
4485        // against a verify that costs 2.7 single tokens. The greedy arms
4486        // pay +10%; the sampling arm needs a cheaper verify first.
4487        // Native Metal HAS that verify: its eight-row tile is flat in b,
4488        // so a round costs ~1.9 plain tokens and the sampling arm pays at
4489        // 2.3 accepted per round — measured on Qwen3.8-27B q4tp / M4 at
4490        // the CLI defaults (0.7 / rep 1.1 / top-k 40, seed 42), a code
4491        // prompt: 9.0 tok/s against a plain 5.4 in the same window, and
4492        // the per-round watchdog turns it off where prose loses. So on
4493        // Metal the sampling arm is ON (`CMF_GRAPH_SPEC_SAMPLE=0` opts out)
4494        // — but only for a config the SPARSE chain serves (a top-k within
4495        // `sparse_ok`): without it a round builds nine 248k-float
4496        // distributions on the host, which is the 5090's measured loss and
4497        // not a cost the round-token proxy below can see. A top-k-less
4498        // sampling config keeps the plain path unless asked for by name.
4499        #[cfg(target_os = "macos")]
4500        let metal_graph = crate::gpu::q1_force()
4501            && crate::gpu::enabled_here()
4502            && std::env::var("CMF_GPU_BLOCK")
4503                .map(|v| v != "0")
4504                .unwrap_or(true);
4505        #[cfg(not(target_os = "macos"))]
4506        let metal_graph = false;
4507        let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4508        // A round whose cost is the MEASURED one: greedy (argmax rows), or
4509        // sampling through the sparse chain. Anything else pays the dense
4510        // chain's host time, which no proxy can price.
4511        let spec_cheap_round =
4512            self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4513        let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4514            || match spec_sample_env.as_deref() {
4515                Some("1") => true,
4516                Some(_) => false,
4517                None => metal_graph && spec_cheap_round,
4518            };
4519        // ON by default for greedy on the wgpu graph: with the draft on
4520        // the graph and the verify bit-exact, it measured 58.7 tok/s
4521        // against a plain 48.1 on Qwen3.8-27B q4tp / RTX 5090 (k=4) and
4522        // 51.1 against 49.4 on Qwen3.6-27B, and a round that stops
4523        // paying turns itself off below (acceptance watchdog).
4524        // `CMF_GRAPH_SPEC=0` disables; `=1` was the old opt-in spelling.
4525        // …but only where the batched verify has its register-blocked
4526        // kernel: q4tp dense FFNs (graph kind 6). q4t and q8_2f verify
4527        // through tile GEMMs today and measured a LOSS (q8_2f 22 against
4528        // 29 tok/s), the 2-bit plane the same; those stay opt-in
4529        // (`CMF_GRAPH_SPEC=1`).
4530        // …at least in nine dense FFNs of ten: a healed file carries its
4531        // last two layers at q8_2f, and two tile-GEMM verifies among 64 do
4532        // not change the arithmetic (measured: the healed q4tp file
4533        // decodes at the plain file's rate and would otherwise sit out).
4534        let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4535        for lw in &self.weights.layers {
4536            if let FfnKind::Dense(d) = &lw.ffn {
4537                dense_n += 1;
4538                if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4539                    && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4540                    && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4541                {
4542                    dense_q4tp += 1;
4543                }
4544            }
4545        }
4546        let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4547        // Penalties break the draft head's agreement with the trunk (a
4548        // 1.1 repetition penalty measured 2 of 16 accepted): not by
4549        // default there either — off Metal that rule is untouched, and
4550        // suppressed ids keep counting as a penalty there, because no
4551        // measurement on a discrete card says otherwise.
4552        //
4553        // On native Metal the penalized arms DO pay: the draft applies
4554        // the same penalty and the verify scores the penalized rows
4555        // exactly (`greedy_pen`, the plain loop's arithmetic), so the
4556        // text is the plain path's and only the round's shape changes.
4557        // Measured on this M4 — see the report for the interleaved run.
4558        let penalized = !metal_graph
4559            && (self.sampler_config.repetition_penalty != 1.0
4560                || self.sampler_config.presence_penalty != 0.0
4561                || !self.sampler_config.suppress_tokens.is_empty());
4562        // …and not on wgpu-over-Metal: the batched verify graph there
4563        // returned 0 accepted drafts and garbage text on a GDN hybrid
4564        // (16.08, Qwen3.5-0.8B) while Vulkan is bit-exact; the Mac's
4565        // default backend is native Metal without a batch graph anyway.
4566        #[cfg(feature = "gpu")]
4567        let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4568        #[cfg(not(feature = "gpu"))]
4569        let metal_wgpu = false;
4570        let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4571        let spec_wanted = match spec_env.as_deref() {
4572            Some("0") => false,
4573            Some(_) => {
4574                if metal_wgpu {
4575                    tracing::warn!(
4576                        "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4577                         verified on this backend (garbage measured on Qwen3.5-0.8B)"
4578                    );
4579                }
4580                true
4581            }
4582            None => spec_default_ok && !penalized && !metal_wgpu,
4583        };
4584        // Native Metal: the b-row verify graph (`try_batch_graph_metal`)
4585        // stands where the wgpu batch graph stands on discrete cards
4586        // (`metal_graph`, above).
4587        let graph_spec = self.speculative
4588            && (graph_on || metal_graph)
4589            && self.mtp.is_some()
4590            && task_mask.is_none()
4591            && !self.o1_active()
4592            && spec_sampling_ok
4593            && spec_wanted;
4594        // Native Metal: say the route ONCE (RUST_LOG=info), so a user can
4595        // confirm the fast path without setting a single flag — every
4596        // knob below defaults to the measured-best value on the M4.
4597        #[cfg(target_os = "macos")]
4598        if metal_graph {
4599            static SAID: std::sync::Once = std::sync::Once::new();
4600            SAID.call_once(|| {
4601                let spec = if graph_spec {
4602                    let k = std::env::var("CMF_GRAPH_SPEC_K")
4603                        .ok()
4604                        .and_then(|v| v.parse::<usize>().ok())
4605                        .filter(|&v| (1..=8).contains(&v))
4606                        .unwrap_or(7);
4607                    let arm = if self.sampler_config.temperature < 1e-6 {
4608                        "greedy"
4609                    } else {
4610                        "sampling"
4611                    };
4612                    format!(
4613                        "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4614                        Self::draft_vocab_rows(usize::MAX)
4615                    )
4616                } else if !self.speculative {
4617                    "spec off (CMF_MTP=0)".to_string()
4618                } else if self.mtp.is_none() {
4619                    "spec off (no MTP head)".to_string()
4620                } else if !spec_sampling_ok {
4621                    if spec_cheap_round {
4622                        "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4623                    } else {
4624                        "spec off (sampling without a top-k: the dense chain \
4625                         costs more than it saves)"
4626                            .to_string()
4627                    }
4628                } else if !spec_wanted {
4629                    "spec off (CMF_GRAPH_SPEC=0 or non-q4tp FFNs)".to_string()
4630                } else if task_mask.is_some() {
4631                    "spec off (task mask)".to_string()
4632                } else {
4633                    "spec off (O(1) attention)".to_string()
4634                };
4635                let on = |var: &str| {
4636                    if std::env::var(var).as_deref() == Ok("0") {
4637                        "off"
4638                    } else {
4639                        "on"
4640                    }
4641                };
4642                tracing::info!(
4643                    "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4644                     MTP graph {}, attend {}, probe {}",
4645                    if crate::gpu_metal::state4_on() {
4646                        "on"
4647                    } else {
4648                        "off"
4649                    },
4650                    if crate::gpu_metal::async_replay_on() {
4651                        "on"
4652                    } else {
4653                        "off"
4654                    },
4655                    on("CMF_METAL_PREFILL"),
4656                    on("CMF_MTP_GRAPH"),
4657                    std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4658                    if crate::gpu::probe_enabled() {
4659                        "bypassed (q1 force)"
4660                    } else {
4661                        "off"
4662                    },
4663                );
4664            });
4665        }
4666        // GDN hybrids sit the fused-pair speculation out by default: the
4667        // recurrence is sequential, so the pair lane cannot parallelize
4668        // (the bench's own Pair line reads fused 1.28x TWO singles on the
4669        // 35B) and the draft's full-vocab head rides on top — measured 2x
4670        // SLOWER end to end (16.1 vs 32.4 tok/s on the 48-core stand).
4671        // CMF_MTP=1 forces it back for study.
4672        let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
4673        let spec_active = self.speculative
4674            && self.mtp.is_some()
4675            && task_mask.is_none()
4676            && !self.o1_active()
4677            && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4678        // The MTP module is detached during generation so its mutable
4679        // state does not fight the borrow on `self`.
4680        let mut mtp = if spec_active { self.mtp.take() } else { None };
4681        if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4682            eprintln!(
4683                "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4684                mtp.is_some(),
4685                self.speculative,
4686                self.sampler_config.temperature < 1e-6,
4687            );
4688        }
4689        if let Some(m) = &mut mtp {
4690            m.kv.clear();
4691            // The MTP block's own device mirror starts over with its cache.
4692            crate::gpu::graph_kv_reset(self.mtp_kv_id());
4693            self.mtp_graph_mode = None;
4694        }
4695        // MiMo-V2's draft stack: greedy rounds (draft K with the chained
4696        // MTP layers, verify K+1 rows in one batched forward). Sampling
4697        // decodes plain; `CMF_MTP=0` / `CMF_MIMO_MTP=0` turn it off.
4698        let mimo_spec = self.speculative
4699            && self.mimo_mtp.is_some()
4700            && task_mask.is_none()
4701            && !self.o1_active()
4702            && self.dyn_router.is_none()
4703            && self.sampler_config.temperature < 1e-6
4704            && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
4705        if let Some(st) = self.mimo_mtp.as_mut() {
4706            st.reset();
4707            if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
4708                Self::mimo_mtp_hist_cap(st, input_ids.len());
4709            }
4710        }
4711        // Dynamic router detached during decode (same borrow trick as MTP).
4712        // Speculative decode and dynamic routing are mutually exclusive
4713        // for now — the fused-pair path doesn't carry per-token φ.
4714        let mut router = if mtp.is_none() {
4715            self.dyn_router.take()
4716        } else {
4717            None
4718        };
4719        let mut reuse_from = reuse_from;
4720        if let Some(r) = &mut router {
4721            r.reset(); // active=backbone, matching a fresh overlay
4722            self.dyn_phi_seen = 0; // fresh φ EMA per generation
4723            if self.dyn_active.is_some() {
4724                // A real switch back to the backbone invalidates the
4725                // cache the reuse key was computed against.
4726                let _ = self.set_active_skill(None);
4727                reuse_from = 0;
4728                self.last_prefill_tokens = input_ids.len();
4729            }
4730        }
4731
4732        let mut all_ids = input_ids.to_vec();
4733        let mut generated = 0usize;
4734        let mut finish_reason = "max_tokens".to_string();
4735        let mut drafted = 0usize;
4736        let mut accepted = 0usize;
4737        // DeepSeek-V4's draft quality is strongly content-dependent.  Two
4738        // consecutive paid rounds with no extra token put it on a bounded
4739        // cooldown; predictable text keeps batching, ordinary prose falls
4740        // back to the exact walk instead of paying a slow draft forever.
4741        // Local to one generation so one difficult request cannot poison the
4742        // next one, and deliberately automatic — this is not a user knob.
4743        let mut dsv4_spec_bad = 0usize;
4744        let mut dsv4_spec_retry_at = 0usize;
4745        let mut confidence: Vec<f32> = Vec::new();
4746        let trace_on = self.trace;
4747        let calib_temp = self.calib_temp;
4748        let mut traces: Vec<TokenTrace> = Vec::new();
4749
4750        // ── Prefill: forward each prompt token once, KEEP the last hidden.
4751        //    Dense prefill runs in fused pairs (weights streamed once per
4752        //    two positions — bit-identical to sequential, proven by the
4753        //    pair tests). With MTP: warm the draft head on
4754        //    (hidden_p, token_{p+1}) pairs.
4755        let mut hidden = vec![0.0f32; self.hidden_size];
4756        let mut pos = reuse_from;
4757        // lm_head-in-graph is only sound when the very next logits
4758        // consumer is this loop's own (MTP and skill routing interleave
4759        // other forwards / can swap lm_head between forward and sample).
4760        // CMF_GPU_LMHEAD=0 keeps lm_head off the graph: the token reads back
4761        // the 8 KB hidden instead of ~1 MB of logits, and the head runs on
4762        // the host. A probe for how much of the graph's fixed per-token cost
4763        // is the logits readback (the layer sweep puts that fixed part at
4764        // 3.88 ms of an 18.5 ms frame).
4765        let fuse_lm = mtp.is_none()
4766            && router.is_none()
4767            && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
4768        self.graph_logits = None;
4769        self.graph_want_logits = false;
4770        let _tpf = std::time::Instant::now();
4771        let batch_k = self.generation_batch_k();
4772        if let Some(rows) = prompt_rows {
4773            let hs = self.hidden_size;
4774            let chunk = self.prefill_chunk().max(1);
4775            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4776                let end = (pos + chunk).min(input_ids.len());
4777                let hb = match self.prefill_input_rows(
4778                    PrefillIn::Hidden(&rows[pos * hs..end * hs]),
4779                    pos,
4780                    task_mask,
4781                ) {
4782                    Ok(hb) => hb,
4783                    Err(err) => {
4784                        self.finish_generation(&mut mtp, &mut router, true);
4785                        return Err(err);
4786                    }
4787                };
4788                if mimo_spec {
4789                    self.mimo_note_rows(&hb, pos);
4790                }
4791                hidden.copy_from_slice(&hb[hb.len() - hs..]);
4792                pos = end;
4793            }
4794        }
4795        // DeepSeek-V4 owns a separate hyper-connection stack. Route it
4796        // before the generic prefill choices: those correctly reject an
4797        // empty `weights.layers`, but their final per-position fallback used
4798        // to consume the whole prompt before `dsv4::forward_chunk` could see
4799        // it. The batch implementation therefore existed without a live
4800        // production entry point.
4801        //
4802        // Bounded chunks preserve cancellation responsiveness. Only the
4803        // prompt's final chunk asks for logits; every earlier head projection
4804        // would produce 129 280 values that no caller reads.
4805        while self.qwen4_exp.is_some()
4806            && mtp.is_none()
4807            && pos < input_ids.len()
4808            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4809        {
4810            // The device path takes several prompt tokens per layer frame;
4811            // the host path runs them one by one inside the same call.
4812            let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
4813            let want_logits = end == input_ids.len();
4814            let mut lg = Vec::new();
4815            if let Some(b) = &mut self.qwen4_exp {
4816                crate::qwen4_exp::forward_tokens(
4817                    &b.0,
4818                    &b.1,
4819                    &b.2,
4820                    &mut b.3,
4821                    &input_ids[pos..end],
4822                    pos,
4823                    &self.inv_freq,
4824                    self.pool.as_deref(),
4825                    &mut lg,
4826                    want_logits,
4827                );
4828            }
4829            if want_logits {
4830                self.graph_logits = Some(lg);
4831            }
4832            pos = end;
4833            hidden.fill(0.0);
4834        }
4835        while self.dsv4.is_some()
4836            && mtp.is_none()
4837            && pos < input_ids.len()
4838            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4839        {
4840            let end = (pos + prefill_chunk()).min(input_ids.len());
4841            let ids: Vec<u32> = input_ids[pos..end].to_vec();
4842            let mut lg = Vec::new();
4843            if let Some(b) = &mut self.dsv4 {
4844                let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
4845                crate::dsv4::forward_chunk(
4846                    g,
4847                    layers,
4848                    &cfg,
4849                    st,
4850                    &ids,
4851                    pos,
4852                    &self.inv_freq,
4853                    self.pool.as_deref(),
4854                    &mut lg,
4855                    end == input_ids.len(),
4856                );
4857            }
4858            if end == input_ids.len() {
4859                self.graph_logits = Some(lg);
4860            }
4861            pos = end;
4862            hidden = vec![0.0; self.hidden_size];
4863        }
4864        let dsv41_prefill = self.dsv41_prefill.take();
4865        while self.dsv41.is_some()
4866            && mtp.is_none()
4867            && pos < input_ids.len()
4868            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4869        {
4870            let end = (pos + prefill_chunk()).min(input_ids.len());
4871            let ids: Vec<u32> = input_ids[pos..end].to_vec();
4872            let mut lg = Vec::new();
4873            if let Some(b) = &mut self.dsv41 {
4874                let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
4875                if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
4876                    crate::dsv41::forward_chunk_masked_with_embeddings(
4877                        g,
4878                        layers,
4879                        cfg,
4880                        st,
4881                        &ids,
4882                        pos,
4883                        &embeddings[pos..end],
4884                        &participates[pos..end],
4885                        self.pool.as_deref(),
4886                        &mut lg,
4887                    );
4888                } else {
4889                    crate::dsv41::forward_chunk(
4890                        g,
4891                        layers,
4892                        cfg,
4893                        st,
4894                        &ids,
4895                        pos,
4896                        self.pool.as_deref(),
4897                        &mut lg,
4898                    );
4899                }
4900            }
4901            if end == input_ids.len() {
4902                self.graph_logits = Some(lg);
4903            }
4904            pos = end;
4905            hidden = vec![0.0; self.hidden_size];
4906        }
4907        // With dynamic routing, prefill sequentially so the φ hook fires
4908        // over the PROMPT — the router enters decode with a warm φ (the
4909        // fused-pair path skips the per-layer φ capture). o1 layers
4910        // collect their query trace in both the single and pair paths.
4911        let dyn_prefill = router.is_some();
4912        // Optional bounded calibration prefix for generation.  The normal
4913        // O(1) path seals after the full prompt; this explicit knob instead
4914        // runs only the requested prefix through exact attention, seals the
4915        // Nyström state, and streams the rest of the prompt through the same
4916        // O(1) step used by decode.  It keeps the O(1) layers' Q trace and
4917        // temporary full KV bounded by the prefix while leaving the default
4918        // full-prompt quality profile untouched.
4919        let o1_prefill_limit = o1_prefill
4920            .and_then(|requested| self.o1_effective_boundary(requested))
4921            .map(|boundary| boundary.min(input_ids.len()));
4922        let mut o1_sealed = false;
4923        if let Some(limit) = o1_prefill_limit {
4924            // Reuse the exact batched prefix machinery when available; it
4925            // records the same per-position Q trace as the full prefill.
4926            if self.can_prefill_batched() && limit > 2 {
4927                let chunk = self.prefill_chunk();
4928                let hs = self.hidden_size;
4929                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4930                    let end = (pos + chunk).min(limit);
4931                    let hb = self.prefill_batch(&input_ids[pos..end], pos);
4932                    hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4933                    pos = end;
4934                }
4935            } else {
4936                while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4937                    hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
4938                    pos += 1;
4939                }
4940            }
4941            if pos >= limit {
4942                o1_sealed = match self.o1_seal_checked() {
4943                    Ok(sealed) => sealed,
4944                    Err(err) => {
4945                        self.finish_generation(&mut mtp, &mut router, true);
4946                        return Err(err);
4947                    }
4948                };
4949                tracing::info!(
4950                    "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
4951                    o1_prefill.unwrap_or(0),
4952                    self.o1_effective_boundary(o1_prefill.unwrap_or(0))
4953                        .unwrap_or(limit),
4954                    limit,
4955                    input_ids.len()
4956                );
4957            }
4958        }
4959        // q1 hybrids on Metal: the per-position GPU token graph beats
4960        // the CPU chunk-GEMM (whose wall is the sequential scalar GDN
4961        // recurrence), so prefill goes position-by-position through the
4962        // same graph as decode. Pure-attention models keep the batched
4963        // path — there the chunk-GEMM amortization wins.
4964        let graph_prefill = self.graph_prefill_preferred();
4965        // Native Metal, q4tp GDN hybrids: the prompt through the b-row
4966        // rows graph — projections as GEMMs over up to 512 positions, the
4967        // GDN recurrence in registers on the device, K/V rows appended by
4968        // the chunk — instead of one token-graph submit per position (the
4969        // 27B: 8 tok/s → GEMM-bound). The MTP warm-up rows come out of one
4970        // batched run of the block per chunk. Any refusal leaves the rest
4971        // of the prompt to the sequential paths below.
4972        #[cfg(target_os = "macos")]
4973        if task_mask.is_none()
4974            && !dyn_prefill
4975            && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
4976            && crate::gpu::enabled_here()
4977            && self.gdn_cfg.is_some()
4978            && self.g3n.is_none()
4979            && input_ids.len() > 8
4980            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
4981            && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
4982        {
4983            let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
4984                .ok()
4985                .and_then(|v| v.parse().ok())
4986                .filter(|&v| (16..=512).contains(&v))
4987                .unwrap_or(256);
4988            let hs = self.hidden_size;
4989            let _tp = std::time::Instant::now();
4990            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4991                let end = (pos + chunk).min(input_ids.len());
4992                let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
4993                    MetalPrefillOutcome::Completed(hb) => hb,
4994                    MetalPrefillOutcome::Declined => break,
4995                    MetalPrefillOutcome::Failed => {
4996                        self.finish_generation(&mut mtp, &mut router, true);
4997                        return Err("ordinary Metal prefill failed after admission".into());
4998                    }
4999                };
5000                if let Some(m) = &mut mtp {
5001                    let n_pairs = if end < input_ids.len() {
5002                        end - pos
5003                    } else {
5004                        end - pos - 1
5005                    };
5006                    if n_pairs > 0 {
5007                        let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5008                            .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5009                            .collect();
5010                        if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5011                            for (j, (h, t)) in pairs.iter().enumerate() {
5012                                let h = h.to_vec();
5013                                let _ = self.mtp_step(m, &h, *t, pos + j);
5014                            }
5015                        }
5016                    }
5017                }
5018                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5019                pos = end;
5020            }
5021            if std::env::var("CMF_PREFILL_PROF").is_ok() {
5022                eprintln!(
5023                    "metal-prefill: {} of {} tokens in {:.1} ms",
5024                    pos,
5025                    input_ids.len(),
5026                    _tp.elapsed().as_secs_f64() * 1e3
5027                );
5028            }
5029        }
5030        // Gemma-4 on a discrete card: the prompt through the batched wgpu
5031        // graph (`g4_batch_ingest`); its K/V land in the device mirrors
5032        // decode reads. A refusal leaves the rest of the prompt to the
5033        // chunked host prefill below.
5034        #[cfg(not(target_os = "macos"))]
5035        if task_mask.is_none()
5036            && !dyn_prefill
5037            && mtp.is_none()
5038            && o1_prefill.is_none()
5039            && batch_k > 0
5040            && pos + 1 < input_ids.len()
5041            && self.g4_batch_prefill()
5042        {
5043            let t_g4 = std::time::Instant::now();
5044            match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5045                Ok((n, last)) => {
5046                    if n > 0 {
5047                        hidden.copy_from_slice(&last);
5048                    }
5049                    if std::env::var("CMF_PREFILL_PROF").is_ok() {
5050                        eprintln!(
5051                            "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5052                            input_ids.len() - pos,
5053                            t_g4.elapsed().as_secs_f64() * 1e3
5054                        );
5055                    }
5056                    pos += n;
5057                }
5058                Err(e) => {
5059                    self.finish_generation(&mut mtp, &mut router, true);
5060                    return Err(e);
5061                }
5062            }
5063        }
5064        self.mimo_moe_prepare();
5065        // A MoE stack larger than the card (MiMo-V2 q4tp on 96 GB): the
5066        // batched wgpu graph runs the device prefix of every chunk — its
5067        // experts resident — and the host's batched layer walk finishes
5068        // the chunk. Any refusal leaves the rest of the prompt to the
5069        // chunked prefill below.
5070        #[cfg(not(target_os = "macos"))]
5071        if task_mask.is_none()
5072            && !dyn_prefill
5073            && !graph_prefill
5074            && mtp.is_none()
5075            && o1_prefill.is_none()
5076            && !self.o1_active()
5077            && input_ids.len() > 2
5078            && self.batch_prefix_prefill()
5079        {
5080            let chunk = self.prefill_chunk().max(1);
5081            let hs = self.hidden_size;
5082            let t_bp = std::time::Instant::now();
5083            let pos0 = pos;
5084            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5085                let end = (pos + chunk).min(input_ids.len());
5086                let bk = end - pos;
5087                let mut hiddens = vec![0f32; bk * hs];
5088                for (j, &id) in input_ids[pos..end].iter().enumerate() {
5089                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5090                }
5091                let positions: Vec<usize> = (pos..end).collect();
5092                let mut run = 0usize;
5093                let outcome = self.try_batch_graph_wgpu_prefix(
5094                    &mut hiddens,
5095                    &positions,
5096                    bk,
5097                    None,
5098                    Some(&mut run),
5099                );
5100                match outcome {
5101                    crate::gpu::BatchGraphOutcome::Completed => {
5102                        let hb = if run < self.num_layers {
5103                            self.prefill_batch_span(
5104                                PrefillIn::Hidden(&hiddens),
5105                                pos,
5106                                None,
5107                                run,
5108                                self.num_layers,
5109                            )
5110                        } else {
5111                            hiddens
5112                        };
5113                        if mimo_spec {
5114                            self.mimo_note_rows(&hb, pos);
5115                        }
5116                        hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5117                        pos = end;
5118                    }
5119                    crate::gpu::BatchGraphOutcome::Failed => {
5120                        self.finish_generation(&mut mtp, &mut router, true);
5121                        return Err("batched prefix prefill failed after admission".into());
5122                    }
5123                    crate::gpu::BatchGraphOutcome::Declined => {
5124                        // Earlier chunks left their prefix rows on the
5125                        // device only: the host walk below needs them.
5126                        #[cfg(feature = "gpu")]
5127                        if pos > pos0 {
5128                            self.pull_lagging_host_kv(0, self.num_layers, pos);
5129                        }
5130                        break;
5131                    }
5132                }
5133            }
5134            if std::env::var("CMF_PREFILL_PROF").is_ok() {
5135                eprintln!(
5136                    "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5137                    pos - pos0,
5138                    input_ids.len(),
5139                    t_bp.elapsed().as_secs_f64() * 1e3
5140                );
5141            }
5142        }
5143        if task_mask.is_none()
5144            && !dyn_prefill
5145            && !graph_prefill
5146            && self.can_prefill_batched()
5147            && self.g3n.is_none()
5148            && o1_prefill.is_none()
5149            && input_ids.len() > 2
5150        {
5151            // Production prefill = the same chunked prefill-GEMM that
5152            // bench/PPL measure (roadmap §3 P0: generation used to warm
5153            // the prompt with the slower pair path — the published
5154            // prefill number didn't match real TTFT). MTP warm-up reads
5155            // each position's hidden straight from the chunk result.
5156            let chunk = self.prefill_chunk();
5157            let hs = self.hidden_size;
5158            while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5159                let end = (pos + chunk).min(input_ids.len());
5160                let hb = self.prefill_batch(&input_ids[pos..end], pos);
5161                if mimo_spec {
5162                    self.mimo_note_rows(&hb, pos);
5163                }
5164                if let Some(m) = &mut mtp {
5165                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5166                        .ok()
5167                        .and_then(|v| v.parse().ok())
5168                        .unwrap_or(0);
5169                    for p in pos..end {
5170                        if p + 1 < input_ids.len() {
5171                            if probe >= 1 && p + 2 < input_ids.len() {
5172                                // Teacher-forced chain acceptance (see the
5173                                // tail loop's twin): the warm-up row stays,
5174                                // the chain's rows roll back.
5175                                let (d1, mut hx) = self.mtp_step_h(
5176                                    m,
5177                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
5178                                    input_ids[p + 1],
5179                                    p,
5180                                );
5181                                let mut ok = d1 == input_ids[p + 2];
5182                                Self::chain_probe_note(0, ok);
5183                                let mut d_prev = d1;
5184                                let mut extra = 0usize;
5185                                for j in 1..probe {
5186                                    if p + 2 + j >= input_ids.len() {
5187                                        break;
5188                                    }
5189                                    let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5190                                    extra += 1;
5191                                    ok = ok && dj == input_ids[p + 2 + j];
5192                                    Self::chain_probe_note(j, ok);
5193                                    d_prev = dj;
5194                                    hx = hj;
5195                                }
5196                                m.kv.truncate_last(extra);
5197                            } else {
5198                                let _ = self.mtp_step(
5199                                    m,
5200                                    &hb[(p - pos) * hs..(p - pos + 1) * hs],
5201                                    input_ids[p + 1],
5202                                    p,
5203                                );
5204                            }
5205                        }
5206                    }
5207                }
5208                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5209                pos = end;
5210            }
5211        }
5212        let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5213        if task_mask.is_none()
5214            && !dyn_prefill
5215            && !graph_prefill
5216            && !pair_off
5217            && self.pair_supported()
5218            && o1_prefill.is_none()
5219        {
5220            while pos + 1 < input_ids.len()
5221                && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5222            {
5223                let e1 = self.embed_single(input_ids[pos]);
5224                let e2 = self.embed_single(input_ids[pos + 1]);
5225                let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5226                if mimo_spec {
5227                    self.mimo_note_rows(&h1, pos);
5228                    self.mimo_note_rows(&h2, pos + 1);
5229                }
5230                // Both prefill tokens are real → commit lane-2 states.
5231                self.commit_linear_scratch();
5232                if let Some(m) = &mut mtp {
5233                    let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5234                    if pos + 2 < input_ids.len() {
5235                        let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5236                            .ok()
5237                            .and_then(|v| v.parse().ok())
5238                            .unwrap_or(0);
5239                        if probe >= 1 && pos + 3 < input_ids.len() {
5240                            // Same teacher-forced chain table as the tail
5241                            // loop below, fed from the pair path that owns
5242                            // most prefill positions.
5243                            let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5244                            let mut ok = d1 == input_ids[pos + 3];
5245                            Self::chain_probe_note(0, ok);
5246                            let mut d_prev = d1;
5247                            let mut extra = 0usize;
5248                            for j in 1..probe {
5249                                if pos + 3 + j >= input_ids.len() {
5250                                    break;
5251                                }
5252                                let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5253                                extra += 1;
5254                                ok = ok && dj == input_ids[pos + 3 + j];
5255                                Self::chain_probe_note(j, ok);
5256                                d_prev = dj;
5257                                hx = hj;
5258                            }
5259                            m.kv.truncate_last(extra);
5260                        } else {
5261                            let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5262                        }
5263                    }
5264                }
5265                hidden = h2;
5266                pos += 2;
5267            }
5268        }
5269        // Batched GPU prefill for the wgpu decode graph (GDN hybrids): K prompt
5270        // positions per submit — projections/FFN as GEMMs (weight once per K),
5271        // attention/GDN looped inside — instead of one whole-graph submit per
5272        // position. Falls through to the per-position graph on any refusal.
5273        // Batched prefill is opt-in (CMF_BATCH_K>0). Default 0 = per-position
5274        // graph prefill. (Steady-state decode is provably identical either way —
5275        // token-graph submit and lm_head both unchanged — so this only trades
5276        // prefill wall.)
5277        // A bounded O(1) prefix is the one post-seal prompt interval: only
5278        // admit its batch when the device O(1) route is explicitly enabled and
5279        // every sealed layer exposes a portable view. The same batch size and
5280        // refusal behavior remain the ordinary controls/comparator.
5281        let o1_batch_ready = o1_sealed
5282            && o1_prefill.is_some()
5283            && mtp.is_none()
5284            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5285            && (0..self.num_layers).all(|li| {
5286                let cache = &self.kv_cache.layers[self.phys_layer(li)];
5287                cache.o1.is_none() || cache.o1_views().is_some()
5288            });
5289        // The ordinary graph-prefill route can share each completed trunk
5290        // chunk with an attached MTP head.  Keep chain probing on its
5291        // established per-position path: the probe deliberately needs every
5292        // teacher-forced draft row and its rollback table.
5293        let mtp_batch_prefill = mtp.is_some()
5294            && graph_prefill
5295            && task_mask.is_none()
5296            && !dyn_prefill
5297            && !self.o1_active()
5298            && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5299        if batch_k > 0
5300            && (graph_prefill || o1_batch_ready)
5301            && task_mask.is_none()
5302            && (!self.o1_active() || o1_batch_ready)
5303            && (mtp.is_none() || mtp_batch_prefill)
5304            && !dyn_prefill
5305            && pos + 1 < input_ids.len()
5306        {
5307            let hs = self.hidden_size;
5308            let chunk = batch_k;
5309            while pos < input_ids.len() {
5310                let end = (pos + chunk).min(input_ids.len());
5311                let bk = end - pos;
5312                let mut hiddens = vec![0f32; bk * hs];
5313                for (j, &id) in input_ids[pos..end].iter().enumerate() {
5314                    hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5315                }
5316                let positions: Vec<usize> = (pos..end).collect();
5317                let t_chunk = std::time::Instant::now();
5318                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5319                let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5320                if std::env::var("CMF_GRAPH_PROF").is_ok() {
5321                    let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5322                    eprintln!(
5323                        "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5324                        if o1_batch_ready {
5325                            "o1"
5326                        } else if mtp_batch_prefill {
5327                            "ordinary_mtp"
5328                        } else {
5329                            "ordinary"
5330                        },
5331                        bk as f64 / (ms / 1000.0)
5332                    );
5333                }
5334                {
5335                    use std::sync::atomic::{AtomicBool, Ordering};
5336                    static SAID: AtomicBool = AtomicBool::new(false);
5337                    if !SAID.swap(true, Ordering::Relaxed) {
5338                        if ok_b {
5339                            tracing::info!(
5340                                "batched prefill: ACTIVE mode={} (k={bk})",
5341                                if o1_batch_ready {
5342                                    "o1"
5343                                } else if mtp_batch_prefill {
5344                                    "ordinary_mtp"
5345                                } else {
5346                                    "ordinary"
5347                                }
5348                            );
5349                        } else {
5350                            tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5351                        }
5352                    }
5353                }
5354                if ok_b {
5355                    if mimo_spec {
5356                        self.mimo_note_rows(&hiddens, pos);
5357                    }
5358                    if mtp_batch_prefill {
5359                        let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5360                        if n_pairs > 0 {
5361                            // `hiddens` is owned by this chunk, so materialize
5362                            // row slices before borrowing the detached MTP
5363                            // module.  The last prompt row has no successor;
5364                            // the helper above is the single source of that
5365                            // boundary rule.
5366                            let rows: Vec<Vec<f32>> = (0..n_pairs)
5367                                .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5368                                .collect();
5369                            let pairs: Vec<(&[f32], u32)> = rows
5370                                .iter()
5371                                .enumerate()
5372                                .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5373                                .collect();
5374                            if std::env::var("CMF_GRAPH_PROF").is_ok() {
5375                                eprintln!(
5376                                    "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5377                                    pos,
5378                                    n_pairs,
5379                                    pos + n_pairs - 1,
5380                                );
5381                            }
5382                            let warm_error = if let Some(m) = mtp.as_mut() {
5383                                self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5384                            } else {
5385                                None
5386                            };
5387                            if let Some(err) = warm_error {
5388                                // The trunk batch was already admitted.  A
5389                                // failed MTP warm-up therefore clears both
5390                                // mirrors and exits; continuing would pair a
5391                                // current trunk state with a stale MTP cache.
5392                                self.finish_generation(&mut mtp, &mut router, true);
5393                                return Err(err.to_string());
5394                            }
5395                        }
5396                    }
5397                    hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5398                    pos = end;
5399                } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5400                    // A failed batch may have advanced a device recurrent
5401                    // state (ordinary GDN or sealed O(1)). A CPU fallback
5402                    // would then observe stale accumulators, so clear the
5403                    // request state and make the failure explicit.
5404                    self.finish_generation(&mut mtp, &mut router, true);
5405                    return Err(if o1_batch_ready {
5406                        "sealed O(1) batch graph failed after admission".to_string()
5407                    } else {
5408                        "ordinary recurrent batch graph failed after admission".to_string()
5409                    });
5410                } else {
5411                    break; // unsupported → per-position graph handles the rest
5412                }
5413            }
5414        }
5415        // Resident Embryo graph: the prompt in chunks of one submit each
5416        // instead of one whole-graph submit per position; the last chunk
5417        // carries the logits exactly as the per-position walk would.
5418        if graph_prefill
5419            && task_mask.is_none()
5420            && mtp.is_none()
5421            && !dyn_prefill
5422            && pos == 0
5423            && input_ids.len() > 1
5424            && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5425        {
5426            if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5427                self.graph_logits = Some(lg);
5428                hidden = vec![0.0; self.hidden_size];
5429                pos = input_ids.len();
5430            }
5431        }
5432        while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5433            self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5434            hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5435            if mimo_spec {
5436                self.mimo_note_rows(&hidden, pos);
5437            }
5438            if let Some(m) = &mut mtp {
5439                if pos + 1 < input_ids.len() {
5440                    // `CMF_MTP_CHAIN_PROBE=k`: teacher-forced acceptance of a
5441                    // CHAINED draft — iterate the head on its own hidden k
5442                    // deep and score every depth against the prompt's real
5443                    // continuation. The economics of a k-token speculative
5444                    // round stand or fall on this table.
5445                    let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5446                        .ok()
5447                        .and_then(|v| v.parse().ok())
5448                        .unwrap_or(0);
5449                    if probe >= 1 && pos + 2 < input_ids.len() {
5450                        let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5451                        let mut ok = d1 == input_ids[pos + 2];
5452                        Self::chain_probe_note(0, ok);
5453                        let mut d_prev = d1;
5454                        let mut extra = 0usize;
5455                        for j in 1..probe {
5456                            if pos + 2 + j >= input_ids.len() {
5457                                break;
5458                            }
5459                            let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5460                            extra += 1;
5461                            ok = ok && dj == input_ids[pos + 2 + j];
5462                            Self::chain_probe_note(j, ok);
5463                            d_prev = dj;
5464                            hx = hj;
5465                        }
5466                        // The chain's rows are speculation, not the prompt —
5467                        // keep only the warmup row the plain path would add.
5468                        m.kv.truncate_last(extra);
5469                    } else {
5470                        let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5471                    }
5472                }
5473            }
5474            pos += 1;
5475        }
5476        if std::env::var("CMF_PREFILL_PROF").is_ok() {
5477            eprintln!(
5478                "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5479                input_ids.len(),
5480                _tpf.elapsed().as_secs_f64() * 1000.0
5481            );
5482        }
5483        if self
5484            .graph_failed
5485            .swap(false, std::sync::atomic::Ordering::Relaxed)
5486        {
5487            // MTP is detached for speculative generation.  Restore the
5488            // module before returning the terminal graph error; otherwise a
5489            // failed request would silently remove the head from a pooled
5490            // pipeline and the next request would lose its configured route.
5491            self.finish_generation(&mut mtp, &mut router, true);
5492            return Err("GPU token graph failed during prefill".to_string());
5493        }
5494        // Cancelled mid-prefill: the cache holds a partial prompt —
5495        // drop the reuse history and return an empty generation.
5496        if self
5497            .cancel
5498            .swap(false, std::sync::atomic::Ordering::Relaxed)
5499        {
5500            // A cancelled prefill can already have advanced the device
5501            // mirror. Drop the whole partial sequence so a pooled pipeline
5502            // cannot carry that state into its next request.
5503            self.finish_generation(&mut mtp, &mut router, true);
5504            return Ok(GenerateResult {
5505                text: String::new(),
5506                token_ids: Vec::new(),
5507                prompt_tokens: input_ids.len(),
5508                tokens_generated: 0,
5509                finish_reason: "cancelled".to_string(),
5510                mtp_drafted: 0,
5511                mtp_accepted: 0,
5512                token_confidence: Vec::new(),
5513                traces: Vec::new(),
5514            });
5515        }
5516
5517        // Prompt absorbed → freeze the o1 layers' skeletons; from here
5518        // every decode step on those layers is O(W + m·dv + m²).
5519        if !o1_sealed {
5520            match self.o1_seal_checked() {
5521                Ok(_) => {}
5522                Err(err) => {
5523                    self.finish_generation(&mut mtp, &mut router, true);
5524                    return Err(err);
5525                }
5526            }
5527        }
5528
5529        // Commit one token: push, check EOS, stream. Returns false = stop.
5530        macro_rules! commit {
5531            ($id:expr) => {{
5532                all_ids.push($id);
5533                generated += 1;
5534                self.note_draft_id($id);
5535                if self.tokenizer.is_eos($id) && !self.ignore_eos {
5536                    finish_reason = "stop".to_string();
5537                    false
5538                } else {
5539                    let token_text = self.tokenizer.decode_token($id);
5540                    let mut go = true;
5541                    if let Some(ref mut cb) = on_token {
5542                        if !cb(&token_text) {
5543                            finish_reason = "cancelled".to_string();
5544                            go = false;
5545                        }
5546                    }
5547                    go
5548                }
5549            }};
5550        }
5551
5552        // Speculation is decided by MEASUREMENT, not by an acceptance
5553        // model. A k=4 round costs ~3.8 plain tokens on the 5090 (draft
5554        // 6.6 + verify 66.6 + commit 4.8 ms against a 20.6 ms token), so it
5555        // pays only when the head lands ~2.8 of 4 — predictable text (code,
5556        // structured output) does, free prose often does not, and the
5557        // ratio at which the two cross depends on the card and the context
5558        // depth. So: four speculative rounds timed, then eight plain
5559        // tokens timed, and the faster arm runs until a re-check 256
5560        // tokens later (context growth moves the balance). The trial
5561        // costs at most a few tokens of the slower arm per 256.
5562        let mut spec_trial = SpecTrial::Spec {
5563            t0: std::time::Instant::now(),
5564            gen0: generated,
5565            rounds: 0,
5566        };
5567        // The token-count proxy prices a round at ~1.9 plain tokens. That
5568        // holds for the Metal rounds whose cost was measured — greedy and
5569        // the sparse sampling chain — so an expensive round (the dense
5570        // chain, reachable only by `CMF_GRAPH_SPEC_SAMPLE=1`) still times
5571        // the plain path before it decides.
5572        let mut spec_mon = SpecMon {
5573            metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5574            ..SpecMon::default()
5575        };
5576        let mut spec_watchdog_off = false;
5577        // CMF_GRAPH_SPEC_TIME: the round walls so far (round 1 excluded —
5578        // it pays the scratch), for the outlier test on each new one
5579        let mut spec_walls: Vec<f32> = Vec::new();
5580        // ... and the end of the last round: the host time between rounds
5581        // (token commits, streaming, the loop top) is printed at level 2
5582        let mut spec_round_end: Option<std::time::Instant> = None;
5583        if mimo_spec {
5584            if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5585                if let Some(mut st) = self.mimo_mtp.take() {
5586                    self.mimo_mtp_probe(&mut st, input_ids, &path);
5587                    self.mimo_mtp = Some(st);
5588                }
5589            }
5590        }
5591        // ── Decode ──
5592        let mut next_pos = input_ids.len();
5593        'decode: while generated < max_tokens {
5594            if self
5595                .graph_failed
5596                .swap(false, std::sync::atomic::Ordering::Relaxed)
5597            {
5598                // Keep the detached MTP module attached after a terminal
5599                // graph error so the pipeline can be reused for a fresh
5600                // sequence.  `clear_sequence_state` only clears mirrors and
5601                // host KV; it cannot recover a module dropped here.
5602                self.finish_generation(&mut mtp, &mut router, true);
5603                return Err("GPU token graph failed during decode".to_string());
5604            }
5605            if self
5606                .cancel
5607                .swap(false, std::sync::atomic::Ordering::Relaxed)
5608            {
5609                finish_reason = "cancelled".to_string();
5610                break 'decode;
5611            }
5612            // A rejected speculative draft already drew this position's
5613            // token from the residual distribution (graph_spec_step); it
5614            // is committed as-is — sampling again from the row's logits
5615            // would bias the stream toward the target's mode.
5616            if mimo_spec && next_pos > 0 {
5617                // Every path leaves `hidden` = the backbone output at
5618                // next_pos-1; the draft layers read it (idempotent).
5619                self.mimo_note_rows(&hidden, next_pos - 1);
5620            }
5621            let forced = self.spec_forced.take();
5622            let mut logits = match (forced, self.graph_logits.take()) {
5623                (Some(_), _) => Vec::new(),
5624                (None, Some(lg)) => lg,
5625                (None, None) => {
5626                    let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5627                    inference::rms_norm_into(
5628                        &hidden,
5629                        &self.weights.final_norm,
5630                        self.rms_eps,
5631                        self.norm_style,
5632                        &mut self.ws.n1,
5633                    );
5634                    self.lm_head_forward(&self.ws.n1)
5635                }
5636            };
5637            // CMF_LOGIT_DUMP=<path>: the first decode step's hidden + logits
5638            // as raw f32 (hidden first) — cross-backend numerics diffing.
5639            if generated
5640                == std::env::var("CMF_LOGIT_DUMP_STEP")
5641                    .ok()
5642                    .and_then(|v| v.parse().ok())
5643                    .unwrap_or(0)
5644            {
5645                if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5646                    let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5647                    for v in hidden.iter().chain(logits.iter()) {
5648                        bytes.extend_from_slice(&v.to_le_bytes());
5649                    }
5650                    if let Err(e) = std::fs::write(&path, &bytes) {
5651                        eprintln!("logit dump: failed to write {path}: {e}");
5652                        self.finish_generation(&mut mtp, &mut router, true);
5653                        return Err(format!("logit dump write failed: {e}"));
5654                    }
5655                }
5656            }
5657            // CMF_LOGIT_DUMP_ALL=<dir>: every decode step's logits as raw
5658            // f32, `<dir>/step{n:05}.f32` — step-by-step backend diffing
5659            // (a greedy run on two backends compares until they diverge).
5660            if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5661                if !logits.is_empty() {
5662                    let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5663                    let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5664                    if let Err(e) =
5665                        std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
5666                    {
5667                        eprintln!("logit dump: failed to write {}: {e}", path.display());
5668                    }
5669                }
5670            }
5671            let t_next = match forced.or_else(|| forced_ids_at(generated)) {
5672                Some(c) => c,
5673                None => {
5674                    let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
5675                    sampler::sample_with_scratch_pool(
5676                        &logits,
5677                        &self.sampler_config,
5678                        self.sampler_config.penalty_past(&all_ids, bounded_native),
5679                        &mut self.rng,
5680                        &mut self.sampler_scratch,
5681                        self.pool.as_deref(),
5682                    )
5683                }
5684            };
5685            if self.confidence_on {
5686                confidence.push(if logits.is_empty() {
5687                    0.0
5688                } else {
5689                    sampler::top1_prob_pool(
5690                        self.pool.as_deref(),
5691                        &mut self.sampler_scratch,
5692                        &logits,
5693                        t_next,
5694                        calib_temp,
5695                    )
5696                });
5697            }
5698            if !logits.is_empty() {
5699                attention::recycle_buf(&mut logits);
5700            }
5701            if trace_on {
5702                // active_skill = the overlay in force while this token was
5703                // generated; recon/switched are filled after the post-emit
5704                // routing eval below (freshest coherence for this token).
5705                let skill = router.as_ref().and_then(|r| r.active_id());
5706                traces.push(TokenTrace {
5707                    t: generated,
5708                    token_id: t_next,
5709                    confidence: confidence.last().copied().unwrap_or(0.0),
5710                    active_skill: skill,
5711                    recon: None,
5712                    switched: false,
5713                });
5714            }
5715            if !commit!(t_next) {
5716                break 'decode;
5717            }
5718            if generated >= max_tokens {
5719                break 'decode;
5720            }
5721
5722            // Catch-all for decode paths that bypass the walks' own trim
5723            // (a graph step, a speculative round).
5724            self.swa_trim_tails();
5725            if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
5726                // Say it ONCE, loudly: past this point the model keeps
5727                // talking but has lost half its context, and on a GDN
5728                // hybrid the graph's device state goes stale on top. The
5729                // Qwen3.8 bring-up spent a day reading this cliff as
5730                // three different model bugs.
5731                static SAID: std::sync::Once = std::sync::Once::new();
5732                SAID.call_once(|| {
5733                    tracing::warn!(
5734                        "KV cache full at {} positions — evicting half; quality \
5735                         will degrade. Raise CMF_MAX_SEQ.",
5736                        self.kv_cache.max_seq_len,
5737                    );
5738                });
5739                let keep = (self.kv_cache.max_seq_len / 2).max(1);
5740                self.kv_cache.evict(keep);
5741            }
5742
5743            // Advance the speculation trial: plain-phase accounting and
5744            // the periodic re-check happen here, on every token.
5745            if graph_spec {
5746                match spec_trial {
5747                    SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
5748                        spec_mon.plain_ms =
5749                            t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
5750                        let keep = spec_mon.pays();
5751                        tracing::info!(
5752                            "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5753                            spec_mon.tokens,
5754                            spec_mon.round_ms,
5755                            spec_mon.plain_ms,
5756                            if keep { "speculating" } else { "plain" }
5757                        );
5758                        spec_mon.fails = 0;
5759                        spec_trial = SpecTrial::Decided {
5760                            spec: keep,
5761                            recheck_at: if keep { usize::MAX } else { generated + 128 },
5762                        };
5763                    }
5764                    SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
5765                        spec_mon.n = 0;
5766                        spec_trial = SpecTrial::Spec {
5767                            t0: std::time::Instant::now(),
5768                            gen0: generated,
5769                            rounds: 0,
5770                        };
5771                    }
5772                    _ => {}
5773                }
5774                spec_watchdog_off = matches!(
5775                    spec_trial,
5776                    SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
5777                );
5778            }
5779            // ── MiMo-V2 draft stack: draft K, verify K+1 rows in one batch ──
5780            if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
5781                let budget = max_tokens - generated - 1;
5782                if let Some(mut st) = self.mimo_mtp.take() {
5783                    let k = st.depth.min(budget);
5784                    let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
5785                    self.mimo_mtp = Some(st);
5786                    let r = match r {
5787                        Ok(r) => r,
5788                        Err(err) => {
5789                            self.finish_generation(&mut mtp, &mut router, true);
5790                            return Err(err);
5791                        }
5792                    };
5793                    if let Some(r) = r {
5794                        drafted += r.drafted;
5795                        accepted += r.accepted.len();
5796                        let mut stopped = false;
5797                        for &id in &r.accepted {
5798                            if self.confidence_on {
5799                                confidence.push(0.0);
5800                            }
5801                            if !commit!(id) {
5802                                stopped = true;
5803                                break;
5804                            }
5805                        }
5806                        if stopped {
5807                            break 'decode;
5808                        }
5809                        next_pos += r.accepted.len() + 1;
5810                        hidden = r.hidden;
5811                        // The loop top chooses the round's own token from
5812                        // these logits — the same sampler, same history.
5813                        self.graph_logits = Some(r.logits);
5814                        continue 'decode;
5815                    }
5816                }
5817            }
5818            // ── Qwen3.8-Flash-Next draft head: k greedy drafts from the MTP
5819            //    sidecar, one batched verify window on the device ──
5820            #[cfg(feature = "gpu")]
5821            if self.speculative
5822                && self.qwen4_exp.is_some()
5823                && task_mask.is_none()
5824                && self.sampler_config.temperature < 1e-6
5825                && generated + 1 < max_tokens
5826                && next_pos > 0
5827                && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
5828            {
5829                let r = match &mut self.qwen4_exp {
5830                    Some(b) => crate::qwen4_exp::spec_round(
5831                        &b.0,
5832                        &b.1,
5833                        &b.2,
5834                        &mut b.3,
5835                        next_pos,
5836                        &all_ids,
5837                        &self.inv_freq,
5838                        self.pool.as_deref(),
5839                    ),
5840                    None => None,
5841                };
5842                if let Some(r) = r {
5843                    drafted += r.drafted;
5844                    accepted += r.accepted.len();
5845                    let mut stopped = false;
5846                    for &id in &r.accepted {
5847                        if self.confidence_on {
5848                            confidence.push(0.0);
5849                        }
5850                        if !commit!(id) {
5851                            stopped = true;
5852                            break;
5853                        }
5854                    }
5855                    if stopped {
5856                        break 'decode;
5857                    }
5858                    next_pos += r.accepted.len() + 1;
5859                    hidden.fill(0.0);
5860                    self.graph_logits = Some(r.logits);
5861                    continue 'decode;
5862                }
5863            }
5864            match &mut mtp {
5865                // ── Graph speculation: chain-draft, batch-verify on device ──
5866                #[cfg(feature = "gpu")]
5867                Some(m)
5868                    if graph_spec
5869                        && !spec_watchdog_off
5870                        && generated + 1 < max_tokens
5871                        && next_pos > 0 =>
5872                {
5873                    let t_round = std::time::Instant::now();
5874                    if spec_time_level() >= 2 {
5875                        if let Some(t) = spec_round_end.take() {
5876                            eprintln!(
5877                                "spec-gap {:.2} ms (host between rounds)",
5878                                t.elapsed().as_secs_f64() * 1e3
5879                            );
5880                        }
5881                    }
5882                    spec_stamps_begin();
5883                    // device buffers allocated during this round: a
5884                    // first-touch Shared allocation is zero-filled inside
5885                    // the command buffer that uses it, which is what the
5886                    // long outlier rounds were
5887                    #[cfg(target_os = "macos")]
5888                    let allocs0 =
5889                        crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
5890                    #[cfg(not(target_os = "macos"))]
5891                    let allocs0 = 0u64;
5892                    if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
5893                        m,
5894                        &hidden,
5895                        t_next,
5896                        next_pos,
5897                        &mut drafted,
5898                        &mut accepted,
5899                        &mut all_ids,
5900                        max_tokens - generated,
5901                    ) {
5902                        next_pos = n_pos;
5903                        hidden = new_h;
5904                        let level = spec_time_level();
5905                        if level > 0 {
5906                            let wall = t_round.elapsed().as_secs_f32() * 1e3;
5907                            let stamps = spec_stamps_take();
5908                            // the running median of the rounds before this
5909                            // one (round 1 pays the scratch: not a sample)
5910                            let median = if spec_walls.len() >= 3 {
5911                                let mut s = spec_walls.clone();
5912                                s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5913                                Some(s[s.len() / 2])
5914                            } else {
5915                                None
5916                            };
5917                            let outlier = median.is_some_and(|m| wall > 1.4 * m);
5918                            #[cfg(target_os = "macos")]
5919                            let allocs = crate::gpu_metal::IO_BUF_ALLOCS
5920                                .load(std::sync::atomic::Ordering::Relaxed)
5921                                - allocs0;
5922                            #[cfg(not(target_os = "macos"))]
5923                            let allocs = allocs0;
5924                            eprintln!(
5925                                "spec-round wall {wall:.1} ms → {} tokens{}{}",
5926                                extra.len() + 1,
5927                                if allocs > 0 {
5928                                    format!(" [{allocs} new device buffers]")
5929                                } else {
5930                                    String::new()
5931                                },
5932                                match (outlier, median) {
5933                                    (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
5934                                    _ => String::new(),
5935                                }
5936                            );
5937                            if level >= 2 || outlier {
5938                                let sum: f32 = stamps.iter().map(|s| s.1).sum();
5939                                eprintln!(
5940                                    "spec-stamps: {}| untracked {:.1}",
5941                                    spec_stamps_format(&stamps),
5942                                    wall - sum
5943                                );
5944                            }
5945                            if spec_mon.n >= 1 {
5946                                spec_walls.push(wall);
5947                            }
5948                        }
5949                        // One speculative round done: the monitor counts it
5950                        // (round 1 untimed — it pays the batch scratch and
5951                        // the draft mirror), and the trial advances.
5952                        spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
5953                        // the round's tokens land in `generated` below; the
5954                        // plain phase must start counting AFTER them
5955                        spec_trial = Self::spec_trial_round(
5956                            spec_trial,
5957                            &mut spec_mon,
5958                            generated + extra.len() + 1,
5959                        );
5960                        let mut stopped = false;
5961                        for &id in &extra {
5962                            if self.confidence_on {
5963                                confidence.push(0.0);
5964                            }
5965                            if !commit!(id) {
5966                                stopped = true;
5967                                break;
5968                            }
5969                        }
5970                        if stopped {
5971                            break 'decode;
5972                        }
5973                        if spec_time_level() >= 2 {
5974                            spec_round_end = Some(std::time::Instant::now());
5975                        }
5976                        continue 'decode;
5977                    }
5978                    if self
5979                        .graph_failed
5980                        .swap(false, std::sync::atomic::Ordering::Relaxed)
5981                    {
5982                        // `graph_spec_step` may have detached MTP while a
5983                        // warm-up was in flight.  Do not reinterpret its
5984                        // terminal device failure as a plain decode step;
5985                        // restore the head, clear both mirrors, and surface
5986                        // one explicit error to the caller.
5987                        self.finish_generation(&mut mtp, &mut router, true);
5988                        return Err("GPU MTP graph failed during speculative decode".to_string());
5989                    }
5990                    // Declined (batch graph refused): plain forward below —
5991                    // and a round that produced one token for the trial's
5992                    // ledger, so a graph that keeps refusing is measured out
5993                    // like a head that keeps missing (it was spinning
5994                    // forever on a file whose batch graph declines).
5995                    // A declined round is not a cheap one-token round — it
5996                    // is a verify that does not exist for this file (a
5997                    // healed q8_2f tail measured 760 drafts, 0 accepted, 33
5998                    // against 48.8 tok/s while the monitor called the draft
5999                    // alone "paying"). Count it as the losing streak in one.
6000                    spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6001                    spec_mon.tokens = 0.0;
6002                    spec_mon.fails = 3;
6003                    spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6004                    hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6005                    next_pos += 1;
6006                    continue 'decode;
6007                }
6008                // ── Speculative: draft t+2, verify in a fused pair ──
6009                Some(m) if !graph_spec && generated + 1 < max_tokens => {
6010                    let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6011                    drafted += 1;
6012                    let emb1 = self.embed_single(t_next);
6013                    let emb2 = self.embed_single(draft);
6014                    let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6015
6016                    inference::rms_norm_into(
6017                        &h1,
6018                        &self.weights.final_norm,
6019                        self.rms_eps,
6020                        self.norm_style,
6021                        &mut self.ws.n1,
6022                    );
6023                    let mut logits1 = self.lm_head_forward(&self.ws.n1);
6024                    let t_after = sampler::sample_with_scratch_pool(
6025                        &logits1,
6026                        &self.sampler_config,
6027                        self.sampler_config.penalty_past(&all_ids, bounded_native),
6028                        &mut self.rng,
6029                        &mut self.sampler_scratch,
6030                        self.pool.as_deref(),
6031                    );
6032                    if self.confidence_on {
6033                        confidence.push(sampler::top1_prob_pool(
6034                            self.pool.as_deref(),
6035                            &mut self.sampler_scratch,
6036                            &logits1,
6037                            t_after,
6038                            calib_temp,
6039                        ));
6040                    }
6041                    attention::recycle_buf(&mut logits1);
6042                    if trace_on {
6043                        // Speculative decode is mutually exclusive with
6044                        // dynamic routing (router is None here) — no skill.
6045                        traces.push(TokenTrace {
6046                            t: generated,
6047                            token_id: t_after,
6048                            confidence: confidence.last().copied().unwrap_or(0.0),
6049                            active_skill: None,
6050                            recon: None,
6051                            switched: false,
6052                        });
6053                    }
6054                    let stop = !commit!(t_after);
6055
6056                    if t_after == draft {
6057                        accepted += 1;
6058                        self.commit_linear_scratch();
6059                        let _ = self.mtp_step(m, &h1, t_after, next_pos);
6060                        hidden = h2;
6061                        next_pos += 2;
6062                    } else {
6063                        // The draft lane is wrong: roll its KV entry back.
6064                        for layer in &mut self.kv_cache.layers {
6065                            layer.truncate_last(1);
6066                        }
6067                        if !stop {
6068                            let _ = self.mtp_step(m, &h1, t_after, next_pos);
6069                            hidden = self.forward_layers(
6070                                &self.embed_single(t_after),
6071                                next_pos + 1,
6072                                None,
6073                            );
6074                        }
6075                        next_pos += 2;
6076                    }
6077                    if stop {
6078                        break 'decode;
6079                    }
6080                }
6081                // ── Vanilla: forward the sampled token ──
6082                _ => {
6083                    // ── DeepSeek-V4 speculative decode (CMF_DSV4_SPEC=1):
6084                    // draft five on the card, verify batched, commit the
6085                    // accepted prefix. Greedy only; a rejected token's state
6086                    // is restored and replayed, so output equals the walk. ──
6087                    #[cfg(feature = "gpu")]
6088                    if Self::dsv4_spec_on() && self.dsv4.is_some() {
6089                        static SAID: std::sync::Once = std::sync::Once::new();
6090                        SAID.call_once(|| {
6091                            eprintln!(
6092                                "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
6093                                !self.dsv4_mtp.is_empty(),
6094                                task_mask.is_none(),
6095                                router.is_none(),
6096                                !trace_on,
6097                                self.sampler_config.temperature < 1e-6,
6098                                self.sampler_config.repetition_penalty == 1.0,
6099                            );
6100                        });
6101                    }
6102                    #[cfg(feature = "gpu")]
6103                    if Self::dsv4_spec_on()
6104                        && self.dsv4.is_some()
6105                        && !self.dsv4_mtp.is_empty()
6106                        && task_mask.is_none()
6107                        && router.is_none()
6108                        && !trace_on
6109                        && self.sampler_config.temperature < 1e-6
6110                        && self.sampler_config.repetition_penalty == 1.0
6111                        && generated + 1 < max_tokens
6112                        && all_ids.len() >= 2
6113                        && generated >= dsv4_spec_retry_at
6114                    {
6115                        let tip_token = all_ids[all_ids.len() - 2];
6116                        let drafted0 = drafted;
6117                        let round = self.dsv4_spec_step(
6118                            tip_token,
6119                            t_next,
6120                            next_pos,
6121                            max_tokens.saturating_sub(generated),
6122                            &mut drafted,
6123                            &mut accepted,
6124                        );
6125                        if drafted > drafted0 {
6126                            let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6127                            if useful {
6128                                dsv4_spec_bad = 0;
6129                            } else {
6130                                dsv4_spec_bad += 1;
6131                                if dsv4_spec_bad >= 2 {
6132                                    dsv4_spec_bad = 0;
6133                                    dsv4_spec_retry_at = generated.saturating_add(32);
6134                                    tracing::info!(
6135                                        "dsv4: draft не окупился дважды — точный walk на 32 токена"
6136                                    );
6137                                }
6138                            }
6139                        }
6140                        if let Some((extra, n_pos)) = round {
6141                            next_pos = n_pos;
6142                            let mut stopped = false;
6143                            for &id in &extra {
6144                                if self.confidence_on {
6145                                    confidence.push(0.0);
6146                                }
6147                                if !commit!(id) {
6148                                    stopped = true;
6149                                    break;
6150                                }
6151                            }
6152                            if stopped {
6153                                break 'decode;
6154                            }
6155                            continue 'decode;
6156                        }
6157                    }
6158                    self.graph_want_logits = fuse_lm;
6159                    // Greedy burst (CMF_MULTISTEP, off unless explicitly set): while
6160                    // nothing observes per-token state — pure argmax sampling,
6161                    // no router/trace/confidence/mask — decode k tokens per
6162                    // submit and commit them wholesale. The trailing normal
6163                    // forward leaves logits for the loop top, as always.
6164                    let mut t_fwd = t_next;
6165                    let pure_greedy = self.sampler_config.temperature < 1e-6
6166                        && self.sampler_config.repetition_penalty == 1.0
6167                        && self.sampler_config.presence_penalty == 0.0
6168                        && self.sampler_config.suppress_tokens.is_empty();
6169                    // Off by default: at every k the burst measured at or
6170                    // below the plain path on this graph shape (k=1 loses
6171                    // the argmax dispatches vs a 1 MB readback, k>=8 loses
6172                    // inter-step drains vs the saved sync). Experimental.
6173                    let burst_k = std::env::var("CMF_MULTISTEP")
6174                        .ok()
6175                        .and_then(|v| v.parse::<usize>().ok())
6176                        .unwrap_or(0);
6177                    if pure_greedy
6178                        && burst_k >= 1
6179                        && fuse_lm
6180                        && task_mask.is_none()
6181                        && router.is_none()
6182                        && !trace_on
6183                        && !self.confidence_on
6184                        // Diagnostics and teacher forcing observe every
6185                        // individual logit row.  A device-only burst returns
6186                        // only ids, so decline it rather than silently
6187                        // skipping requested evidence or forced ids.
6188                        && std::env::var_os("CMF_FORCE_IDS").is_none()
6189                        && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6190                        && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6191                    {
6192                        let mut stopped = false;
6193                        loop {
6194                            // The outer loop checks cancellation too, but a
6195                            // long run can stay in this burst loop for many
6196                            // submits.  Bound cancellation latency to one
6197                            // exact k-token frame.
6198                            if self
6199                                .cancel
6200                                .swap(false, std::sync::atomic::Ordering::Relaxed)
6201                            {
6202                                finish_reason = "cancelled".to_string();
6203                                stopped = true;
6204                                break;
6205                            }
6206                            let room = max_tokens.saturating_sub(generated);
6207                            if room <= 2 {
6208                                break;
6209                            }
6210                            let k = burst_k.min(room - 1);
6211                            if k < 1 {
6212                                break;
6213                            }
6214                            let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6215                                if self
6216                                    .graph_failed
6217                                    .swap(false, std::sync::atomic::Ordering::Relaxed)
6218                                {
6219                                    self.finish_generation(&mut mtp, &mut router, true);
6220                                    return Err(
6221                                        "GPU token graph failed during greedy burst".to_string()
6222                                    );
6223                                }
6224                                break;
6225                            };
6226                            next_pos += k;
6227                            for &id in &ids {
6228                                if !commit!(id) {
6229                                    stopped = true;
6230                                    break;
6231                                }
6232                            }
6233                            if stopped {
6234                                break;
6235                            }
6236                            t_fwd = *ids.last().unwrap();
6237                        }
6238                        if stopped {
6239                            break 'decode;
6240                        }
6241                    }
6242                    // Metal: keep the draft head's cache in step through
6243                    // the trial's plain phase and a paused speculation —
6244                    // the pair (hidden, t_fwd) at next_pos−1, the step the
6245                    // round's draft 0 would take. Without it the head's
6246                    // cache lagged the trunk by every plain token for the
6247                    // rest of the generation: the batched warm-up declined
6248                    // every later round and its rows went one by one (a
6249                    // whole MTP step per accepted token), and the drafts
6250                    // attended a context with those tokens missing.
6251                    #[cfg(target_os = "macos")]
6252                    if graph_spec
6253                        && spec_watchdog_off
6254                        && next_pos > 0
6255                        && self.mtp_graph_mode == Some(true)
6256                        && crate::gpu::q1_force()
6257                    {
6258                        if let Some(m) = mtp.as_mut() {
6259                            let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6260                        }
6261                    }
6262                    hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6263                    next_pos += 1;
6264                    // Dynamic routing: the forward updated φ; ask the
6265                    // router whether to switch skills before the next token.
6266                    if let Some(r) = &mut router {
6267                        let phi = self.dyn_phi_ema.clone();
6268                        let decision = r.step(&phi, generated);
6269                        if let Some(new_active) = decision {
6270                            let _ = self.set_active_skill(new_active);
6271                        }
6272                        // Backfill this token's coherence + switch flag from
6273                        // the just-run eval (freshest measured values).
6274                        if trace_on {
6275                            if let Some(last) = traces.last_mut() {
6276                                let e = r.last_best_e();
6277                                last.recon = e.is_finite().then_some(e);
6278                                last.switched = decision.is_some();
6279                            }
6280                        }
6281                    }
6282                }
6283            }
6284        }
6285
6286        let cancelled = finish_reason == "cancelled";
6287        // A generation during which the router switched weights holds no
6288        // state any single overlay would produce (each switch cleared the
6289        // cache mid-sequence), so it leaves no reuse key behind either.
6290        let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6291        if mimo_spec {
6292            if let Some(st) = self.mimo_mtp.as_ref() {
6293                let line = st.stats.line();
6294                tracing::info!("{line}");
6295                if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6296                    eprintln!("{line}");
6297                }
6298            }
6299        }
6300        self.finish_generation(&mut mtp, &mut router, cancelled);
6301
6302        let output_ids = &all_ids[input_ids.len()..];
6303        // Forwarded = prompt + all generated but the LAST sampled token
6304        // (emitted without being fed back). Exact only without MTP —
6305        // reuse is gated off when MTP is active.
6306        let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6307        // A MiMo speculative round that stopped on an accepted draft (EOS,
6308        // cancel) leaves verify rows past the committed stream in the cache:
6309        // never offer that cache for reuse. Neither does a router that
6310        // switched weights mid-sequence (no single overlay produced it).
6311        let consumed = std::mem::take(&mut all_ids);
6312        if dyn_switched {
6313            self.clear_sequence_state();
6314        } else if cancelled || mimo_spec || prompt_rows.is_some() {
6315            self.clear_history();
6316        } else {
6317            self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6318        }
6319        all_ids = consumed;
6320        let output_ids = &all_ids[input_ids.len()..];
6321        confidence.truncate(output_ids.len()); // guard against any overshoot
6322        traces.truncate(output_ids.len());
6323        Ok(GenerateResult {
6324            text: self.tokenizer.decode(output_ids),
6325            token_ids: output_ids.to_vec(),
6326            prompt_tokens: input_ids.len(),
6327            tokens_generated: generated,
6328            finish_reason,
6329            mtp_drafted: drafted,
6330            mtp_accepted: accepted,
6331            token_confidence: confidence,
6332            traces,
6333        })
6334    }
6335
6336    /// One MTP step: feed `(hidden_p, token_{p+1})` into the draft head,
6337    /// advance its KV cache at position `p`, return the drafted token
6338    /// for position `p+2`.
6339    fn mtp_step(
6340        &mut self,
6341        m: &mut MtpModule,
6342        hidden: &[f32],
6343        next_token: u32,
6344        position: usize,
6345    ) -> u32 {
6346        self.mtp_step_h(m, hidden, next_token, position).0
6347    }
6348
6349    /// Tally for `CMF_MTP_CHAIN_PROBE`: per depth, how often the CHAIN is
6350    /// still an exact prefix of the real continuation. Printed every 128
6351    /// depth-0 samples so a killed run still shows its table.
6352    fn chain_probe_note(depth: usize, prefix_ok: bool) {
6353        use std::sync::Mutex;
6354        static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6355        let mut t = T.lock().unwrap();
6356        if t.len() <= depth {
6357            t.resize(depth + 1, (0, 0));
6358        }
6359        t[depth].0 += 1;
6360        t[depth].1 += prefix_ok as u64;
6361        if depth == 0 && t[0].0 % 128 == 0 {
6362            let line: Vec<String> = t
6363                .iter()
6364                .enumerate()
6365                .map(|(d, (n, k))| {
6366                    format!(
6367                        "d{}={:.0}%({n})",
6368                        d + 1,
6369                        100.0 * *k as f64 / (*n).max(1) as f64
6370                    )
6371                })
6372                .collect();
6373            eprintln!("mtp-chain: {}", line.join(" "));
6374        }
6375    }
6376
6377    /// `mtp_step` that also hands back the block's own output hidden — the
6378    /// state a CHAINED draft feeds the next step, the way a multi-token
6379    /// speculative round iterates the head on itself.
6380    /// One MTP block step from (trunk hidden, token): the head's LOGITS
6381    /// and the block's own hidden for chaining. The draft is argmax of the
6382    /// logits on the greedy path and a draw from their post-chain
6383    /// distribution on the sampling path.
6384    fn mtp_step_hl(
6385        &mut self,
6386        m: &mut MtpModule,
6387        hidden: &[f32],
6388        next_token: u32,
6389        position: usize,
6390    ) -> (Vec<f32>, Vec<f32>) {
6391        // The graph arm: the MTP block as a one-layer token graph with the
6392        // head fused — device attention over the block's own KV mirror,
6393        // one submit for block + head, hidden and logits back together.
6394        // Decided once per generation (see `mtp_graph_mode`).
6395        #[cfg(target_os = "macos")]
6396        if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6397            if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6398                self.mtp_graph_mode = Some(true);
6399                return r;
6400            }
6401            if self.mtp_graph_mode == Some(true) {
6402                tracing::error!("mtp Metal graph failed after admission");
6403                self.clear_sequence_state();
6404                self.graph_failed
6405                    .store(true, std::sync::atomic::Ordering::Relaxed);
6406                self.cancel
6407                    .store(true, std::sync::atomic::Ordering::Relaxed);
6408                return (Vec::new(), Vec::new());
6409            }
6410            self.mtp_graph_mode = Some(false);
6411        }
6412        #[cfg(feature = "gpu")]
6413        if self.mtp_graph_mode != Some(false) {
6414            if !self.mtp_graph_ok(m) {
6415                if self.mtp_graph_mode == Some(true) {
6416                    // A mirror was already admitted, so a capability change
6417                    // cannot safely switch this request to the stale CPU
6418                    // cache.  Keep the same terminal contract as a failed
6419                    // token graph.
6420                    tracing::error!("mtp graph became unavailable after admission");
6421                    self.clear_sequence_state();
6422                    self.graph_failed
6423                        .store(true, std::sync::atomic::Ordering::Relaxed);
6424                    self.cancel
6425                        .store(true, std::sync::atomic::Ordering::Relaxed);
6426                    return (Vec::new(), Vec::new());
6427                }
6428                self.mtp_graph_mode = Some(false);
6429            } else {
6430                if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6431                    self.mtp_graph_mode = Some(true);
6432                    return r;
6433                }
6434                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6435                    // A token graph can have admitted a persistent MTP/GDN
6436                    // mirror before its readback failed.  The CPU MTP cache
6437                    // is not a valid continuation in that state; leave the
6438                    // flag set so the generation caller returns through its
6439                    // terminal error path instead of silently switching
6440                    // arithmetic.
6441                    return (Vec::new(), Vec::new());
6442                }
6443                // `mtp_graph_ok` was true, so a None here means a refusal or
6444                // failure after graph admission.  Do not fall through to a
6445                // CPU cache whose rows may lag the device mirror.
6446                tracing::error!("mtp graph failed or declined after admission");
6447                self.clear_sequence_state();
6448                self.graph_failed
6449                    .store(true, std::sync::atomic::Ordering::Relaxed);
6450                self.cancel
6451                    .store(true, std::sync::atomic::Ordering::Relaxed);
6452                return (Vec::new(), Vec::new());
6453            }
6454        }
6455        // fc concat order is [enorm(embed); hnorm(hidden)] — EMBEDDING
6456        // FIRST. Verified by the oracle (converter/mtp_oracle.py):
6457        // [emb;hid] → 45.8% acceptance, [hid;emb] → 0.00%.
6458        let e = self.embed_single(next_token);
6459        let mut cat = vec![0.0f32; 2 * self.hidden_size];
6460        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6461        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6462        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6463        let mut x = vec![0.0f32; self.hidden_size];
6464        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6465
6466        // One standard transformer block over the MTP's own cache.
6467        let lw = &m.layer;
6468        inference::rms_norm_into(
6469            &x,
6470            &lw.input_norm,
6471            self.rms_eps,
6472            self.norm_style,
6473            &mut self.ws.n1,
6474        );
6475        let attn = match &lw.attn {
6476            // MLA models carry no MTP head; this path cannot see them.
6477            AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6478            AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6479            AttnKind::Full {
6480                wq,
6481                wk,
6482                wv,
6483                wo,
6484                q_norm,
6485                k_norm,
6486                output_gate,
6487                softplus_gate,
6488                bias,
6489            } => {
6490                let mut cfg = self.attn_cfg(position);
6491                cfg.q_norm = q_norm.as_deref();
6492                cfg.k_norm = k_norm.as_deref();
6493                cfg.output_gate = *output_gate;
6494                cfg.softplus_gate = softplus_gate
6495                    .as_ref()
6496                    .map(|(gate, per_head)| (gate, *per_head));
6497                cfg.bias = bias
6498                    .as_ref()
6499                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6500                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6501            }
6502            AttnKind::Linear(_)
6503            | AttnKind::LinearGdn(_)
6504            | AttnKind::ShortConv(_)
6505            | AttnKind::Bounded(_) => {
6506                unreachable!("MTP block is full attention")
6507            }
6508        };
6509        for (i, &a) in attn.iter().enumerate() {
6510            x[i] += a;
6511        }
6512        inference::rms_norm_into(
6513            &x,
6514            &lw.post_norm,
6515            self.rms_eps,
6516            self.norm_style,
6517            &mut self.ws.p1,
6518        );
6519        let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6520        for (i, &f) in ffn.iter().enumerate() {
6521            x[i] += f;
6522        }
6523
6524        inference::rms_norm_into(
6525            &x,
6526            &m.final_norm,
6527            self.rms_eps,
6528            self.norm_style,
6529            &mut self.ws.n1,
6530        );
6531        let lg = self.lm_head_forward(&self.ws.n1);
6532        (lg, x)
6533    }
6534
6535    /// `mtp_step_hl` reduced to the greedy draft: argmax of the head.
6536    fn mtp_step_h(
6537        &mut self,
6538        m: &mut MtpModule,
6539        hidden: &[f32],
6540        next_token: u32,
6541        position: usize,
6542    ) -> (u32, Vec<f32>) {
6543        let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6544        let draft = sampler::argmax(&lg);
6545        attention::recycle_buf(&mut lg);
6546        (draft, x)
6547    }
6548
6549    /// One speculative round for the trial: rounds 1..5 of a `Spec` phase
6550    /// advance it (the monitor already averaged this round); after five,
6551    /// the plain phase runs (once — a known plain rate decides at once);
6552    /// a decided speculation keeps re-checking the rule every round and
6553    /// stops after four losing rounds in a row.
6554    fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6555        match trial {
6556            SpecTrial::Spec { t0, gen0, rounds } => {
6557                let rounds = rounds + 1;
6558                if rounds >= 5 {
6559                    if mon.plain_ms > 0.0 {
6560                        let keep = mon.pays();
6561                        mon.fails = 0;
6562                        tracing::info!(
6563                            "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6564                            mon.tokens,
6565                            mon.round_ms,
6566                            mon.plain_ms,
6567                            if keep { "speculating" } else { "plain" }
6568                        );
6569                        SpecTrial::Decided {
6570                            spec: keep,
6571                            recheck_at: if keep { usize::MAX } else { generated + 128 },
6572                        }
6573                    } else if mon.pays() {
6574                        // Metal: the rounds land enough tokens each that no
6575                        // plain measurement is needed — keep speculating,
6576                        // and re-check every round (a losing streak sends
6577                        // the loop to the plain phase, below).
6578                        mon.fails = 0;
6579                        tracing::info!(
6580                            "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6581                            mon.tokens,
6582                            mon.round_ms,
6583                        );
6584                        SpecTrial::Decided {
6585                            spec: true,
6586                            recheck_at: usize::MAX,
6587                        }
6588                    } else {
6589                        SpecTrial::Plain {
6590                            t0: std::time::Instant::now(),
6591                            gen0: generated,
6592                        }
6593                    }
6594                } else {
6595                    SpecTrial::Spec { t0, gen0, rounds }
6596                }
6597            }
6598            SpecTrial::Decided { spec: true, .. } => {
6599                if mon.pays() {
6600                    mon.fails = 0;
6601                    trial
6602                } else {
6603                    mon.fails += 1;
6604                    if mon.fails >= 4 {
6605                        if mon.plain_ms <= 0.0 {
6606                            // Metal, plain never timed: four doubtful rounds
6607                            // buy the (bounded) plain measurement, and the
6608                            // exact rule decides from it.
6609                            tracing::info!(
6610                                "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6611                                mon.tokens,
6612                                mon.round_ms,
6613                            );
6614                            return SpecTrial::Plain {
6615                                t0: std::time::Instant::now(),
6616                                gen0: generated,
6617                            };
6618                        }
6619                        tracing::info!(
6620                            "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6621                            mon.tokens,
6622                            mon.round_ms,
6623                            mon.plain_ms
6624                        );
6625                        SpecTrial::Decided {
6626                            spec: false,
6627                            recheck_at: generated + 128,
6628                        }
6629                    } else {
6630                        trial
6631                    }
6632                }
6633            }
6634            other => other,
6635        }
6636    }
6637
6638    /// The MTP block's device-mirror id: the trunk's id with a high bit,
6639    /// so the (kv_id, layer) mirror keys never collide.
6640    fn mtp_kv_id(&self) -> u64 {
6641        self.graph_kv_id | (1u64 << 40)
6642    }
6643
6644    /// The MTP block's mirror layer index: 0 — its own kv_id keeps it
6645    /// apart from the trunk, and the BATCH graph (the warm-up path) keys
6646    /// its mirrors at layer 0 with no base of its own, so the draft's
6647    /// token graph must key the same slot.
6648    const MTP_LAYER_BASE: usize = 0;
6649
6650    /// The wgpu MTP draft writes speculative rows straight into its device
6651    /// mirror while the CPU owner retains only the real prompt/decode anchor.
6652    /// After verification, move that mirror cursor back to the anchor before
6653    /// replaying accepted pairs.  The next graph append then sees the same
6654    /// contiguous position as the CPU/Metal path without uploading stale
6655    /// speculative rows.
6656    #[cfg(feature = "gpu")]
6657    fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
6658        self.mtp_graph_mode != Some(true)
6659            || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
6660    }
6661
6662    /// A speculative verify graph appends the full `k+1` trunk rows before
6663    /// the acceptance count is known.  GDN state already has a snapshot
6664    /// restore; Full-attention mirrors need the matching logical cursor
6665    /// rewind so the next graph call does not reject an ahead-of-position KV
6666    /// cache after a partial acceptance.
6667    #[cfg(feature = "gpu")]
6668    fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
6669        let mut ok = true;
6670        let mut expected = false;
6671        for li in 0..self.num_layers {
6672            if matches!(
6673                self.weights.layers[self.phys_layer(li)].attn,
6674                AttnKind::Full { .. }
6675            ) {
6676                expected = true;
6677                ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
6678            }
6679        }
6680        !expected || ok
6681    }
6682
6683    /// Count the recurrent layers participating in the trunk verify graph.
6684    /// Snapshot restore is all-or-nothing across that set; deriving the count
6685    /// from the model keeps the restore contract valid for looped models too.
6686    fn graph_gdn_layer_count(&self) -> usize {
6687        (0..self.num_layers)
6688            .filter(|&li| {
6689                matches!(
6690                    &self.weights.layers[self.phys_layer(li)].attn,
6691                    AttnKind::LinearGdn(_)
6692                )
6693            })
6694            .count()
6695    }
6696
6697    /// The block's input from (trunk hidden, token): eh_proj · [enorm(e);
6698    /// hnorm(h)] — the same arithmetic the per-op path starts with.
6699    fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
6700        let e = self.embed_single(next_token);
6701        let mut cat = vec![0.0f32; 2 * self.hidden_size];
6702        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6703        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6704        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6705        let mut x = vec![0.0f32; self.hidden_size];
6706        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6707        x
6708    }
6709
6710    /// Is the MTP block graphable at all (device up, full attention
6711    /// without softplus, dense FFN)? The plan itself is built per call.
6712    #[cfg(feature = "gpu")]
6713    fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
6714        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
6715            return false;
6716        }
6717        if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
6718            || !crate::gpu::enabled_here()
6719            || self.attn_softcap > 0.0
6720            || self.attention_heads_per_layer.is_some()
6721            // The block graph caches V as wide as K and feeds o_proj
6722            // nh·head_dim; a narrow-V model keeps its MTP block per-op.
6723            || self.v_head_dim.is_some()
6724        {
6725            return false;
6726        }
6727        matches!(
6728            &m.layer.attn,
6729            AttnKind::Full {
6730                softplus_gate: None,
6731                ..
6732            }
6733        ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
6734    }
6735
6736    /// Full MTP token-graph eligibility, including the fused lm-head and all
6737    /// block projection weights.  Keep this distinct from the block-only
6738    /// check: prompt warm-up does not need the head, while a draft step does.
6739    #[cfg(feature = "gpu")]
6740    fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
6741        if !self.mtp_block_graph_ok(m) {
6742            return false;
6743        }
6744        let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
6745            return false;
6746        };
6747        let FfnKind::Dense(d) = &m.layer.ffn else {
6748            return false;
6749        };
6750        d.segs.is_empty()
6751            && wq.graph_weight().is_some()
6752            && wk.graph_weight().is_some()
6753            && wv.graph_weight().is_some()
6754            && wo.graph_weight().is_some()
6755            && d.gate_proj.graph_weight().is_some()
6756            && d.up_proj.graph_weight().is_some()
6757            && d.down_proj.graph_weight().is_some()
6758            && self.weights.lm_head.graph_weight().is_some()
6759    }
6760
6761    /// One MTP block step on the wgpu token graph: block + fused head in
6762    /// one submit, the block hidden and the logits read back together.
6763    /// None = the graph cannot take this block (softplus gate, non-dense
6764    /// FFN, unquantized head, no device) — the caller keeps the per-op
6765    /// path for the whole generation.
6766    #[cfg(feature = "gpu")]
6767    fn mtp_step_graph(
6768        &mut self,
6769        m: &mut MtpModule,
6770        hidden: &[f32],
6771        next_token: u32,
6772        position: usize,
6773    ) -> Option<(Vec<f32>, Vec<f32>)> {
6774        if !self.mtp_graph_ok(m) {
6775            return None;
6776        }
6777        let lw = &m.layer;
6778        let AttnKind::Full {
6779            wq,
6780            wk,
6781            wv,
6782            wo,
6783            q_norm,
6784            k_norm,
6785            output_gate,
6786            softplus_gate,
6787            bias,
6788        } = &lw.attn
6789        else {
6790            return None;
6791        };
6792        if softplus_gate.is_some() {
6793            return None;
6794        }
6795        let FfnKind::Dense(d) = &lw.ffn else {
6796            return None;
6797        };
6798        if !d.segs.is_empty() {
6799            return None; // tube layers run on the segmented path
6800        }
6801        // The block's input first: it borrows `self` mutably (embed scratch,
6802        // pool), the plan below borrows the weights immutably.
6803        let mut x = self.mtp_block_input(m, hidden, next_token);
6804        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6805            let (_, i, kind, rs) = t.graph_weight()?;
6806            Some(crate::gpu::GraphW {
6807                idx: i,
6808                kind,
6809                row_scale: rs,
6810                data: &[],
6811                prism: crate::gpu::GraphPrismOp::None,
6812                affine: false,
6813            })
6814        }
6815        let (model, _, _, _) = wq.graph_weight()?;
6816        let model = model.clone();
6817        let (lm_gw, lm_rows) = {
6818            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
6819            // The draft's head over the CMF_DRAFT_VOCAB shortlist (the same
6820            // cut the native Metal draft takes): 662 MB a step on Qwen3.8
6821            // becomes 170 MB at 65536; the verify keeps the full head.
6822            let rows = if kind == 6 {
6823                self.draft_head_rows(self.weights.lm_head.rows())
6824            } else {
6825                self.weights.lm_head.rows()
6826            };
6827            (
6828                crate::gpu::GraphW {
6829                    idx: i,
6830                    kind,
6831                    row_scale: rs,
6832                    data: &[],
6833                    prism: crate::gpu::GraphPrismOp::None,
6834                    affine: false,
6835                },
6836                rows,
6837            )
6838        };
6839        let layer = crate::gpu::GraphLayer {
6840            input_norm: &lw.input_norm,
6841            attn: crate::gpu::GraphAttn::Full {
6842                wq: gw(wq)?,
6843                wk: gw(wk)?,
6844                wv: gw(wv)?,
6845                wo: gw(wo)?,
6846                q_norm: q_norm.as_deref(),
6847                k_norm: k_norm.as_deref(),
6848                late_qk_norm: self.qk_norm_after_rope,
6849                bias: bias
6850                    .as_ref()
6851                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6852                output_gate: *output_gate,
6853                cpu_k: m.kv.k_heads(),
6854                cpu_v: m.kv.v_heads(),
6855                cpu_base: m.kv.base(),
6856                geom: None,
6857                head_gate: None,
6858            },
6859            post_norm: &lw.post_norm,
6860            attn_out_norm: None,
6861            ffn_out_norm: None,
6862            layer_scale: None,
6863            ffn: crate::gpu::GraphFfn::Dense {
6864                gate: gw(&d.gate_proj)?,
6865                up: gw(&d.up_proj)?,
6866                down: gw(&d.down_proj)?,
6867                act: d.act.graph_act()?,
6868            },
6869        };
6870        let nh = self.num_heads;
6871        let (nkv, hd, rd) = self.layer_geom(0);
6872        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6873        let mut logits = Vec::new();
6874        let ok = crate::gpu::forward_token_graph(
6875            &model,
6876            self.mtp_kv_id(),
6877            std::slice::from_ref(&layer),
6878            &[None],
6879            self.o1_epoch,
6880            &self.inv_freq,
6881            &mut x,
6882            nh,
6883            nkv,
6884            hd,
6885            self.attn_scale,
6886            rd,
6887            self.hidden_size,
6888            self.intermediate_size,
6889            position,
6890            self.kv_cache.max_seq_len,
6891            gemma,
6892            self.rms_eps as f32,
6893            Some((&lm_gw, lm_rows)),
6894            &m.final_norm,
6895            &mut logits,
6896            &[],
6897            1,
6898            None,
6899            None,
6900            None,
6901            Self::MTP_LAYER_BASE,
6902            true,
6903        );
6904        match ok {
6905            crate::gpu::TokenGraphOutcome::Completed => {}
6906            crate::gpu::TokenGraphOutcome::Declined => return None,
6907            crate::gpu::TokenGraphOutcome::Failed => {
6908                // The backend has already admitted persistent state.  Keep
6909                // this distinct from a capability refusal so the caller
6910                // cannot switch to the stale CPU MTP cache.
6911                self.clear_sequence_state();
6912                self.graph_failed
6913                    .store(true, std::sync::atomic::Ordering::Relaxed);
6914                self.cancel
6915                    .store(true, std::sync::atomic::Ordering::Relaxed);
6916                return None;
6917            }
6918        }
6919        logits.resize(self.vocab_size, 0.0);
6920        Some((logits, x))
6921    }
6922
6923    /// The warm-ups of one speculative round on the device: every accepted
6924    /// (hidden, token) pair as ONE batched graph run over the MTP block
6925    /// (no head) — its kv_append lands the pairs in the block's mirror.
6926    /// `pairs` are consecutive positions from `first_pos`.  The tri-state
6927    /// result is intentional: a refusal before admission may use the
6928    /// per-row/CPU route, while a failure after admission must terminate the
6929    /// sequence rather than fall through to a stale CPU cache.
6930    #[cfg(feature = "gpu")]
6931    fn mtp_warm_graph(
6932        &mut self,
6933        m: &mut MtpModule,
6934        pairs: &[(&[f32], u32)],
6935        first_pos: usize,
6936    ) -> crate::gpu::BatchGraphOutcome {
6937        if pairs.is_empty() {
6938            return crate::gpu::BatchGraphOutcome::Completed;
6939        }
6940        if !self.mtp_block_graph_ok(m) {
6941            return crate::gpu::BatchGraphOutcome::Declined;
6942        }
6943        let hs = self.hidden_size;
6944        // Block inputs for every pair (eh_proj on the per-op path, one
6945        // matvec each — the plan's own prologue).
6946        let mut hiddens = Vec::with_capacity(pairs.len() * hs);
6947        for (h, t) in pairs {
6948            hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
6949        }
6950        let lw = &m.layer;
6951        let AttnKind::Full {
6952            wq,
6953            wk,
6954            wv,
6955            wo,
6956            q_norm,
6957            k_norm,
6958            output_gate,
6959            bias,
6960            ..
6961        } = &lw.attn
6962        else {
6963            return crate::gpu::BatchGraphOutcome::Declined;
6964        };
6965        let FfnKind::Dense(d) = &lw.ffn else {
6966            return crate::gpu::BatchGraphOutcome::Declined;
6967        };
6968        if !d.segs.is_empty() {
6969            return crate::gpu::BatchGraphOutcome::Declined; // tube layers run on the segmented path
6970        }
6971        // The graph has no arm for a projected output gate, and only the
6972        // activations `graph_act` names.
6973        let (
6974            AttnKind::Full {
6975                softplus_gate: None,
6976                ..
6977            },
6978            Some(gact),
6979        ) = (&lw.attn, d.act.graph_act())
6980        else {
6981            return crate::gpu::BatchGraphOutcome::Declined;
6982        };
6983        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6984            let (_, i, kind, rs) = t.graph_weight()?;
6985            Some(crate::gpu::GraphW {
6986                idx: i,
6987                kind,
6988                row_scale: rs,
6989                data: &[],
6990                prism: crate::gpu::GraphPrismOp::None,
6991                affine: false,
6992            })
6993        }
6994        let Some((model, _, _, _)) = wq.graph_weight() else {
6995            return crate::gpu::BatchGraphOutcome::Declined;
6996        };
6997        let model = model.clone();
6998        let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
6999            gw(wq),
7000            gw(wk),
7001            gw(wv),
7002            gw(wo),
7003            gw(&d.gate_proj),
7004            gw(&d.up_proj),
7005            gw(&d.down_proj),
7006        ) else {
7007            return crate::gpu::BatchGraphOutcome::Declined;
7008        };
7009        let layer = crate::gpu::GraphLayer {
7010            input_norm: &lw.input_norm,
7011            attn: crate::gpu::GraphAttn::Full {
7012                wq: gwq,
7013                wk: gwk,
7014                wv: gwv,
7015                wo: gwo,
7016                q_norm: q_norm.as_deref(),
7017                k_norm: k_norm.as_deref(),
7018                late_qk_norm: self.qk_norm_after_rope,
7019                bias: bias
7020                    .as_ref()
7021                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7022                output_gate: *output_gate,
7023                cpu_k: m.kv.k_heads(),
7024                cpu_v: m.kv.v_heads(),
7025                cpu_base: m.kv.base(),
7026                geom: None,
7027                head_gate: None,
7028            },
7029            post_norm: &lw.post_norm,
7030            attn_out_norm: None,
7031            ffn_out_norm: None,
7032            layer_scale: None,
7033            ffn: crate::gpu::GraphFfn::Dense {
7034                gate: gg,
7035                up: gu,
7036                down: gd,
7037                act: gact,
7038            },
7039        };
7040        let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7041        let nh = self.num_heads;
7042        let (nkv, hd, rd) = self.layer_geom(0);
7043        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7044        crate::gpu::forward_batch_graph(
7045            &model,
7046            self.mtp_kv_id(),
7047            std::slice::from_ref(&layer),
7048            &self.inv_freq,
7049            &mut hiddens,
7050            nh,
7051            nkv,
7052            hd,
7053            rd,
7054            hs,
7055            self.intermediate_size,
7056            &positions,
7057            self.kv_cache.max_seq_len,
7058            gemma,
7059            self.rms_eps as f32,
7060            self.attn_scale,
7061            pairs.len(),
7062            &[],
7063            0,
7064            None,
7065            None,
7066        )
7067    }
7068
7069    /// Complete an MTP warm-up after the batched graph has refused.  A
7070    /// graphable block is retried one row at a time; once any device row has
7071    /// been admitted, a CPU fallback would observe a stale mirror, so every
7072    /// token-graph refusal is terminal.  If the block is not graphable and no
7073    /// mirror exists yet, warming on the CPU is safe and records the CPU mode
7074    /// for the rest of the generation.
7075    #[cfg(feature = "gpu")]
7076    fn mtp_warm_graph_fallback(
7077        &mut self,
7078        m: &mut MtpModule,
7079        pairs: &[(&[f32], u32)],
7080        first_pos: usize,
7081    ) -> bool {
7082        if pairs.is_empty() {
7083            return true;
7084        }
7085        let graphable = self.mtp_block_graph_ok(m);
7086        if !graphable {
7087            // A previously admitted mirror cannot be made coherent by
7088            // appending to the host cache.  The caller turns this into a
7089            // terminal generation error and clears both mirrors.
7090            if self.mtp_graph_mode == Some(true) {
7091                return false;
7092            }
7093            self.mtp_graph_mode = Some(false);
7094            for (j, (h, t)) in pairs.iter().enumerate() {
7095                self.mtp_warm(m, h, *t, first_pos + j);
7096            }
7097            return true;
7098        }
7099
7100        // The batch refusal is recoverable only through the same device
7101        // state.  Keep rows owned until each token graph has completed; a
7102        // None is treated as unsafe because the token-graph API deliberately
7103        // collapses its backend refusal/failure into that result.
7104        for (j, (h, t)) in pairs.iter().enumerate() {
7105            if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7106                return false;
7107            }
7108        }
7109        self.mtp_graph_mode = Some(true);
7110        true
7111    }
7112
7113    /// Warm a contiguous set of MTP pairs using the existing graph seam, with
7114    /// an all-or-nothing error contract for callers that already admitted the
7115    /// trunk batch.  The non-GPU build keeps the same pair accounting while
7116    /// using the established CPU warm path.
7117    #[cfg(feature = "gpu")]
7118    fn mtp_warm_prefill_pairs(
7119        &mut self,
7120        m: &mut MtpModule,
7121        pairs: &[(&[f32], u32)],
7122        first_pos: usize,
7123    ) -> Result<(), &'static str> {
7124        // Keep unsupported token-graph heads on the established CPU MTP
7125        // route before admitting any block mirror.  Once a device mirror is
7126        // active, the same condition is terminal because CPU rows cannot
7127        // repair its state.
7128        if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7129            if self.mtp_graph_mode == Some(true) {
7130                return Err("MTP token graph became unavailable after admission");
7131            }
7132            self.mtp_graph_mode = Some(false);
7133            for (j, (h, t)) in pairs.iter().enumerate() {
7134                self.mtp_warm(m, h, *t, first_pos + j);
7135            }
7136            return Ok(());
7137        }
7138        match self.mtp_warm_graph(m, pairs, first_pos) {
7139            crate::gpu::BatchGraphOutcome::Completed => {
7140                if !pairs.is_empty() {
7141                    self.mtp_graph_mode = Some(true);
7142                }
7143                Ok(())
7144            }
7145            crate::gpu::BatchGraphOutcome::Declined => {
7146                if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7147                    Ok(())
7148                } else {
7149                    Err("MTP warm-up fallback failed after device admission")
7150                }
7151            }
7152            crate::gpu::BatchGraphOutcome::Failed => {
7153                Err("MTP warm batch graph failed after admission")
7154            }
7155        }
7156    }
7157
7158    #[cfg(not(feature = "gpu"))]
7159    fn mtp_warm_prefill_pairs(
7160        &mut self,
7161        m: &mut MtpModule,
7162        pairs: &[(&[f32], u32)],
7163        first_pos: usize,
7164    ) -> Result<(), &'static str> {
7165        for (j, (h, t)) in pairs.iter().enumerate() {
7166            self.mtp_warm(m, h, *t, first_pos + j);
7167        }
7168        Ok(())
7169    }
7170
7171    /// The MTP block alone — advance its KV with a (hidden, token) pair the
7172    /// verify just proved, without paying the head. What keeps the draft's
7173    /// attention context warm between speculative rounds.
7174    fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7175        let e = self.embed_single(next_token);
7176        let mut cat = vec![0.0f32; 2 * self.hidden_size];
7177        let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7178        inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7179        inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7180        let mut x = vec![0.0f32; self.hidden_size];
7181        m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7182        inference::rms_norm_into(
7183            &x,
7184            &m.layer.input_norm,
7185            self.rms_eps,
7186            self.norm_style,
7187            &mut self.ws.n1,
7188        );
7189        let attn = match &m.layer.attn {
7190            AttnKind::Full {
7191                wq,
7192                wk,
7193                wv,
7194                wo,
7195                q_norm,
7196                k_norm,
7197                output_gate,
7198                softplus_gate,
7199                bias,
7200            } => {
7201                let mut cfg = self.attn_cfg(position);
7202                cfg.q_norm = q_norm.as_deref();
7203                cfg.k_norm = k_norm.as_deref();
7204                cfg.output_gate = *output_gate;
7205                cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7206                cfg.bias = bias
7207                    .as_ref()
7208                    .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7209                attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7210            }
7211            _ => return,
7212        };
7213        let _ = attn;
7214    }
7215
7216    /// Speculative decode ON the wgpu whole-token graph: draft k with the
7217    /// MTP head, verify all of them plus the tip in ONE batched graph
7218    /// submit whose tail folds the head, commit the accepted prefix and
7219    /// roll the GDN state back to the last real position. Greedy only —
7220    /// output equals the plain graph's token for token, the way the DSV4
7221    /// verify equals the walk.
7222    #[cfg(feature = "gpu")]
7223    #[allow(clippy::too_many_arguments)]
7224    fn graph_spec_step(
7225        &mut self,
7226        m: &mut MtpModule,
7227        hidden: &[f32],
7228        t_next: u32,
7229        next_pos: usize,
7230        drafted: &mut usize,
7231        accepted: &mut usize,
7232        // The committed stream (prompt + generated so far, `t_next`
7233        // included): the sampler chain's penalties read it, and the
7234        // sampling arm extends it with the drafts position by position.
7235        all_ids: &mut Vec<u32>,
7236        // Tokens left before `max_tokens`. A round commits up to k
7237        // accepted drafts, and those positions are already in the cache,
7238        // so the depth is capped here — trimming the output afterwards
7239        // would leave cache rows the committed stream does not have.
7240        room: usize,
7241    ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7242        // 3 is the measured optimum on Qwen3.6-27B / RTX 5090 (medians
7243        // of three, greedy): 51.1 tok/s against a plain 49.4, where k=2
7244        // gives 46.1, k=4 50.0, k=5 47.4, k=6 45.2. Acceptance is 89-91%
7245        // throughout — what turns the curve over is the verify, which
7246        // costs ~7.4 ms per extra position, and the draft ~3 ms a step.
7247        // 4 since the draft moved onto the graph (Qwen3.8-27B / 5090:
7248        // k=3 51.2, k=4 51.8 with the per-op draft; the graph draft
7249        // halves the draft cost, so the extra draft is cheaper still).
7250        // 5 with the int8 verify (the default: measured 76.5 against
7251        // k=4's 72-74 and k=6's 74 on the 5090), 4 with the f32 one.
7252        #[cfg(target_os = "macos")]
7253        let metal_native = crate::gpu::q1_force();
7254        #[cfg(not(target_os = "macos"))]
7255        let metal_native = false;
7256        #[cfg(feature = "gpu")]
7257        let k_default = if metal_native {
7258            // the Metal verify's GEMM tile is 8 rows wide and flat in b:
7259            // seven drafts + the tip fill it for free
7260            7
7261        } else if crate::gpu_wgpu::verify_i8_on() {
7262            5
7263        } else {
7264            4
7265        };
7266        #[cfg(not(feature = "gpu"))]
7267        let k_default = 4;
7268        let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7269            .ok()
7270            .and_then(|v| v.parse().ok())
7271            .filter(|&v| (1..=8).contains(&v));
7272        // Adaptive depth: start below the card's flat-verify optimum and
7273        // let the accepted fraction move it — predictable text climbs to
7274        // the old default within a few rounds, prose settles at 2-3 where
7275        // the shorter verify pays.
7276        let (k_start, k_max) = if metal_native {
7277            (7, 7)
7278        } else {
7279            (3, k_default.max(5))
7280        };
7281        let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7282        let k_spec = k_full.min(room).max(1);
7283        // a tail round cut short by `room` says nothing about the text:
7284        // it must not move the adaptive depth the next request starts at
7285        let k_capped = k_spec < k_full;
7286        if next_pos == 0 {
7287            return None;
7288        }
7289        let t_round = std::time::Instant::now();
7290        // Submissions per phase — and they say where the round's money is.
7291        // Qwen3.6-27B on an RTX 5090, k=3:
7292        //
7293        //   draft   9.3 ms / 12 submissions   (four per MTP step)
7294        //   verify 52.8 ms /  1               (the batched graph)
7295        //   commit  5.4 ms /  6               (two per warm)
7296        //
7297        // The verify is already one submit. The draft's own work is 834 MB
7298        // a step — 0.8 ms at this card's measured 1056 GB/s — against 3.1
7299        // ms measured, so ~0.58 ms of every step is round trip, not
7300        // arithmetic, and the same holds for the warms. Eighteen round
7301        // trips a round at roughly half a millisecond each is ~11 ms of a
7302        // 68 ms round: fusing the MTP block into ONE submit the way the
7303        // trunk already is projects to ~64 tok/s against today's 50.9.
7304        // That is the largest measured item left on this path.
7305        let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7306        let sub0 = subs();
7307        // Greedy without penalties verifies by argmax equality (bit-exact
7308        // against the plain path). Anything else is speculative SAMPLING:
7309        // each draft is a DRAW from the MTP head's post-chain distribution
7310        // q_j, kept for the accept test; the verify's rows give p_j.
7311        let cfg = self.sampler_config.clone();
7312        let penalized = !(cfg.repetition_penalty == 1.0
7313            && cfg.presence_penalty == 0.0
7314            && cfg.suppress_tokens.is_empty());
7315        // Three verify regimes: plain greedy (argmax of the raw rows),
7316        // greedy WITH penalties (argmax of the penalized rows — a single
7317        // pass each, no distributions), and sampling (draw / accept /
7318        // correct on post-chain distributions).
7319        let greedy_pen = cfg.temperature < 1e-6 && penalized;
7320        let sampling = cfg.temperature >= 1e-6;
7321        // Sampling with a top-k goes through the SPARSE chain: the dense
7322        // one builds nine 248k-float distributions a round (four drafts,
7323        // five verify rows) and measured 19-22 tok/s against a plain 40 —
7324        // the host, not the card. Sparse, the same nine cost tens of
7325        // microseconds each.
7326        let sparse = sampling && sampler::sparse_ok(&cfg);
7327        let base_len = all_ids.len();
7328        if sampling && !sparse && self.spec_q.len() < k_spec {
7329            self.spec_q.resize_with(k_spec, Vec::new);
7330        }
7331        if sparse && self.spec_qs.len() < k_spec {
7332            self.spec_qs.resize_with(k_spec, Vec::new);
7333        }
7334        // Draft the chain: first from the trunk's tip hidden, then the head
7335        // iterating on itself. Rows land in the MTP KV; the chain rows past
7336        // the first are speculation over speculative state and roll back
7337        // below, replaced by verified pairs.
7338        let mut drafts = Vec::with_capacity(k_spec);
7339        let mut hx = hidden.to_vec();
7340        // CMF_SPEC_DBG=1: draft 0 through BOTH MTP arms (graph and per-op)
7341        // from the same inputs — are the arms the difference, or the inputs?
7342        let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7343        spec_stamp("pro");
7344        // Plain greedy on native Metal: the whole chain as one command
7345        // buffer (device argmax + embedding gather between the steps).
7346        // A decline before commit hands the round to the per-step loop
7347        // below; a failure after commit is terminal, like any graph
7348        // failure after admission.
7349        #[cfg(target_os = "macos")]
7350        if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7351            match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7352                Ok(ids) => {
7353                    self.mtp_graph_mode = Some(true);
7354                    drafts = ids;
7355                }
7356                Err(true) => {
7357                    tracing::error!("mtp Metal draft chain failed after commit");
7358                    self.clear_sequence_state();
7359                    self.graph_failed
7360                        .store(true, std::sync::atomic::Ordering::Relaxed);
7361                    self.cancel
7362                        .store(true, std::sync::atomic::Ordering::Relaxed);
7363                    return None;
7364                }
7365                Err(false) => {}
7366            }
7367        }
7368        for j in drafts.len()..k_spec {
7369            let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7370            let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7371            if spec_dbg {
7372                let saved = self.mtp_graph_mode;
7373                self.mtp_graph_mode = Some(false);
7374                let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7375                self.mtp_graph_mode = saved;
7376                if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7377                    return None;
7378                }
7379                m.kv.truncate_last(1);
7380                dbg_ref = Some(r);
7381            }
7382            let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7383            if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7384                return None;
7385            }
7386            if let Some((lg_cpu, h_cpu)) = dbg_ref {
7387                let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7388                let dl = lg
7389                    .iter()
7390                    .zip(&lg_cpu)
7391                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7392                let dh = hj
7393                    .iter()
7394                    .zip(&h_cpu)
7395                    .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7396                eprintln!(
7397                    "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 {}",
7398                    next_pos - 1 + j,
7399                    sampler::argmax(&lg_cpu),
7400                    sampler::argmax(&lg),
7401                    n(&h_cpu),
7402                    n(&hj),
7403                    m.kv.seq_len
7404                );
7405            }
7406            let dj = if sparse {
7407                let mut q = std::mem::take(&mut self.spec_qs[j]);
7408                let ok = sampler::sparse_distribution_into(
7409                    &lg,
7410                    &cfg,
7411                    all_ids,
7412                    &mut self.sampler_scratch,
7413                    self.pool.as_deref(),
7414                    &mut q,
7415                );
7416                let d = if ok {
7417                    sampler::draw_sparse(&q, &mut self.rng)
7418                } else {
7419                    // everything filtered: the dense chain's greedy fallback
7420                    let t = sampler::argmax(&lg);
7421                    q.clear();
7422                    q.push((t, 1.0));
7423                    t
7424                };
7425                self.spec_qs[j] = q;
7426                all_ids.push(d);
7427                d
7428            } else if sampling {
7429                let mut q = std::mem::take(&mut self.spec_q[j]);
7430                sampler::distribution_into(
7431                    &lg,
7432                    &cfg,
7433                    all_ids,
7434                    &mut self.sampler_scratch,
7435                    self.pool.as_deref(),
7436                    &mut q,
7437                );
7438                let d = sampler::draw(&q, &mut self.rng);
7439                self.spec_q[j] = q;
7440                all_ids.push(d); // the next draft's penalties see this one
7441                d
7442            } else if greedy_pen {
7443                let d = sampler::argmax_penalized(
7444                    &lg,
7445                    &cfg,
7446                    all_ids,
7447                    &mut self.sampler_scratch,
7448                    self.pool.as_deref(),
7449                );
7450                all_ids.push(d);
7451                d
7452            } else {
7453                sampler::argmax(&lg)
7454            };
7455            attention::recycle_buf(&mut lg);
7456            drafts.push(dj);
7457            hx = hj;
7458            spec_stamp("d.pick");
7459        }
7460        all_ids.truncate(base_len);
7461        *drafted += k_spec;
7462        let t_draft = t_round.elapsed();
7463        let sub_draft = subs();
7464        // Verify batch: [t_next, d1 .. d_{k-1}] at next_pos.. — every row's
7465        // logits come back from the graph's own head.
7466        let b = k_spec + 1;
7467        let mut hiddens = vec![0.0f32; b * self.hidden_size];
7468        for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7469            let e = self.embed_single(t);
7470            hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7471        }
7472        let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7473        spec_stamp("v.emb");
7474        let (lm_gw, lm_rows) = {
7475            let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7476            (
7477                crate::gpu::GraphW {
7478                    idx: i,
7479                    kind,
7480                    row_scale: rs,
7481                    data: &[],
7482                    prism: crate::gpu::GraphPrismOp::None,
7483                    affine: false,
7484                },
7485                self.weights.lm_head.rows(),
7486            )
7487        };
7488        let mut logits = Vec::new();
7489        let final_norm = self.weights.final_norm.clone();
7490        // Plain greedy on Metal: the b argmaxes come from the device
7491        // (`argmax_rows` after the head) and the 7.9 MB logits plane is
7492        // never read back — the round's decision needs only the ids, and
7493        // the loop top takes the last verified id as `spec_forced`, which
7494        // is exactly what its argmax of the row would give. The full rows
7495        // stay for anything that reads them: sampling, penalties,
7496        // confidence, the verify oracle, the logit dump.
7497        // `CMF_METAL_DEV_ARGMAX=0` keeps the host path.
7498        #[cfg(target_os = "macos")]
7499        let greedy_dev = metal_native
7500            && !sampling
7501            && !greedy_pen
7502            && !self.confidence_on
7503            && self.final_softcap.is_none()
7504            // The host acceptance argmax scans the WHOLE head row
7505            // (`lm_rows`), the sampler's own row only `vocab_size`: they
7506            // coincide exactly when the head has no padding rows, and
7507            // only then is the device argmax (which scores `vocab_size`)
7508            // bit-identical to both.
7509            && self.vocab_size == lm_rows
7510            && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7511            && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7512            && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7513        #[cfg(not(target_os = "macos"))]
7514        let greedy_dev = false;
7515        let mut dev_ids: Vec<u32> = Vec::new();
7516        #[cfg(target_os = "macos")]
7517        let verify_outcome = if metal_native {
7518            let lm = self.weights.lm_head.q1_parts()?;
7519            let n_score = self.vocab_size.min(lm_rows);
7520            self.try_batch_graph_metal(
7521                &mut hiddens,
7522                &positions,
7523                b,
7524                Some((lm, &final_norm, &mut logits)),
7525                if greedy_dev {
7526                    Some((n_score, &mut dev_ids))
7527                } else {
7528                    None
7529                },
7530            )
7531        } else {
7532            self.try_batch_graph_wgpu(
7533                &mut hiddens,
7534                &positions,
7535                b,
7536                Some(crate::gpu::SpecTail {
7537                    lm: lm_gw,
7538                    lm_rows,
7539                    final_norm: &final_norm,
7540                    logits_out: &mut logits,
7541                }),
7542            )
7543        };
7544        #[cfg(not(target_os = "macos"))]
7545        let verify_outcome = self.try_batch_graph_wgpu(
7546            &mut hiddens,
7547            &positions,
7548            b,
7549            Some(crate::gpu::SpecTail {
7550                lm: lm_gw,
7551                lm_rows,
7552                final_norm: &final_norm,
7553                logits_out: &mut logits,
7554            }),
7555        );
7556        match verify_outcome {
7557            crate::gpu::BatchGraphOutcome::Completed => {}
7558            crate::gpu::BatchGraphOutcome::Declined => {
7559                // The verifier refused before admission.  Its draft MTP
7560                // rows are still device-resident, so rewind the separate
7561                // mirror before the caller takes the exact one-token path.
7562                m.kv.truncate_last(k_spec);
7563                if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7564                    self.clear_sequence_state();
7565                    self.graph_failed
7566                        .store(true, std::sync::atomic::Ordering::Relaxed);
7567                    self.cancel
7568                        .store(true, std::sync::atomic::Ordering::Relaxed);
7569                    tracing::error!("MTP graph mirror rewind failed after verify decline");
7570                }
7571                return None;
7572            }
7573            crate::gpu::BatchGraphOutcome::Failed => {
7574                // A failed batch may have advanced trunk/GDN state.  Clear
7575                // both mirrors and preserve the terminal outcome rather than
7576                // falling through to stale CPU state.
7577                self.clear_sequence_state();
7578                self.graph_failed
7579                    .store(true, std::sync::atomic::Ordering::Relaxed);
7580                self.cancel
7581                    .store(true, std::sync::atomic::Ordering::Relaxed);
7582                tracing::error!("MTP verify batch graph failed after admission");
7583                return None;
7584            }
7585        }
7586        // `CMF_METAL_VERIFY_CHECK=1`: run the same b tokens through the
7587        // plain per-token path and compare each row's argmax + logits with
7588        // the verify's — the bring-up oracle for the batched graph. The
7589        // plain forwards mutate the CPU state; it is snapshotted and put
7590        // back, and the K/V mirrors re-pointed, before the round goes on.
7591        #[cfg(target_os = "macos")]
7592        if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7593            let snap: Vec<Vec<f32>> = self
7594                .kv_cache
7595                .layers
7596                .iter()
7597                .map(|l| l.linear_state.clone())
7598                .collect();
7599            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7600            let toks: Vec<u32> = std::iter::once(t_next)
7601                .chain(drafts.iter().copied())
7602                .collect();
7603            let want_save = self.graph_want_logits;
7604            self.graph_want_logits = false;
7605            for (i, &t) in toks.iter().enumerate() {
7606                let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7607                let _ = self.graph_logits.take();
7608                // CMF_SPEC_PLAIN_HIDDEN=1: the next round drafts from the
7609                // plain path's hidden instead of the verify's (an experiment
7610                // on the chain's sensitivity to the half-GEMM noise)
7611                if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7612                    hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7613                }
7614                let ref_lg = self.logits_from_hidden(&hi);
7615                let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7616                let ra = sampler::argmax(&ref_lg);
7617                let va = sampler::argmax(row);
7618                let mut md = 0f32;
7619                let mut rms = 0f64;
7620                for j in 0..lm_rows.min(ref_lg.len()) {
7621                    let d = (ref_lg[j] - row[j]).abs();
7622                    md = md.max(d);
7623                    rms += (d as f64) * (d as f64);
7624                }
7625                let mut hd = 0f32;
7626                for j in 0..self.hidden_size {
7627                    hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
7628                }
7629                eprintln!(
7630                    "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
7631                    next_pos + i,
7632                    if ra == va { "OK" } else { "MISMATCH" },
7633                    (rms / lm_rows as f64).sqrt()
7634                );
7635            }
7636            self.graph_want_logits = want_save;
7637            // restore IN PLACE: the pending verify graph wraps these very
7638            // allocations (zero-copy) — replacing the Vec would strand it
7639            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7640                if l.linear_state.len() == st.len() {
7641                    l.linear_state.copy_from_slice(&st);
7642                } else {
7643                    l.linear_state = st;
7644                }
7645            }
7646            for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
7647                let extra = l.seq_len.saturating_sub(n0);
7648                if extra > 0 {
7649                    l.truncate_last(extra);
7650                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
7651                }
7652            }
7653        }
7654        let t_verify = t_round.elapsed();
7655        let sub_verify = subs();
7656        // Acceptance. Greedy: row i's argmax is the trunk's token after
7657        // input i. Sampling: accept draft i with min(1, p_i/q_i), and on
7658        // the first rejection draw the correction from max(0, p_i − q_i)
7659        // — that token is committed by the loop top as-is (spec_forced).
7660        let mut a = 0usize;
7661        let mut forced: Option<u32> = None;
7662        let ids: Vec<u32> = if sparse {
7663            let mut p = std::mem::take(&mut self.spec_ps);
7664            let mut res = std::mem::take(&mut self.spec_ress);
7665            while a < k_spec {
7666                let ok = sampler::sparse_distribution_into(
7667                    &logits[a * lm_rows..(a + 1) * lm_rows],
7668                    &cfg,
7669                    all_ids,
7670                    &mut self.sampler_scratch,
7671                    self.pool.as_deref(),
7672                    &mut p,
7673                );
7674                if !ok {
7675                    let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
7676                    p.clear();
7677                    p.push((t, 1.0));
7678                }
7679                match sampler::spec_accept_or_correct_sparse(
7680                    &p,
7681                    &self.spec_qs[a],
7682                    drafts[a],
7683                    &mut self.rng,
7684                    &mut res,
7685                ) {
7686                    None => {
7687                        all_ids.push(drafts[a]);
7688                        a += 1;
7689                    }
7690                    Some(c) => {
7691                        forced = Some(c);
7692                        break;
7693                    }
7694                }
7695            }
7696            all_ids.truncate(base_len);
7697            self.spec_ps = p;
7698            self.spec_ress = res;
7699            drafts.clone()
7700        } else if sampling {
7701            let mut p = std::mem::take(&mut self.spec_p);
7702            let mut res = std::mem::take(&mut self.spec_res);
7703            while a < k_spec {
7704                sampler::distribution_into(
7705                    &logits[a * lm_rows..(a + 1) * lm_rows],
7706                    &cfg,
7707                    all_ids,
7708                    &mut self.sampler_scratch,
7709                    self.pool.as_deref(),
7710                    &mut p,
7711                );
7712                match sampler::spec_accept_or_correct(
7713                    &p,
7714                    &self.spec_q[a],
7715                    drafts[a],
7716                    &mut self.rng,
7717                    &mut res,
7718                    self.pool.as_deref(),
7719                ) {
7720                    None => {
7721                        all_ids.push(drafts[a]);
7722                        a += 1;
7723                    }
7724                    Some(c) => {
7725                        forced = Some(c);
7726                        break;
7727                    }
7728                }
7729            }
7730            all_ids.truncate(base_len);
7731            self.spec_p = p;
7732            self.spec_res = res;
7733            // the accepted drafts ARE the verified tokens after inputs 0..a
7734            drafts.clone()
7735        } else if greedy_pen {
7736            // Row i's penalized argmax, penalties over the stream that
7737            // includes the accepted drafts before it — the plain loop's
7738            // exact arithmetic, one pass per row, no working copy.
7739            let mut ids: Vec<u32> = Vec::with_capacity(b);
7740            for i in 0..b {
7741                let t = sampler::argmax_penalized(
7742                    &logits[i * lm_rows..(i + 1) * lm_rows],
7743                    &cfg,
7744                    all_ids,
7745                    &mut self.sampler_scratch,
7746                    self.pool.as_deref(),
7747                );
7748                ids.push(t);
7749                if i < k_spec && t == drafts[i] {
7750                    all_ids.push(t);
7751                } else {
7752                    break;
7753                }
7754            }
7755            all_ids.truncate(base_len);
7756            while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
7757                a += 1;
7758            }
7759            // rows past the first mismatch were never scored; the loop
7760            // top re-samples the last verified row itself.
7761            ids
7762        } else if greedy_dev && dev_ids.len() == b {
7763            let ids = std::mem::take(&mut dev_ids);
7764            while a < k_spec && ids[a] == drafts[a] {
7765                a += 1;
7766            }
7767            ids
7768        } else {
7769            if logits.len() < b * lm_rows {
7770                // the device argmax was asked for and came back short:
7771                // no rows to fall back on — terminal like a failed batch
7772                self.clear_sequence_state();
7773                self.graph_failed
7774                    .store(true, std::sync::atomic::Ordering::Relaxed);
7775                self.cancel
7776                    .store(true, std::sync::atomic::Ordering::Relaxed);
7777                tracing::error!("Metal verify returned neither logits nor argmax ids");
7778                return None;
7779            }
7780            let ids: Vec<u32> = (0..b)
7781                .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
7782                .collect();
7783            while a < k_spec && ids[a] == drafts[a] {
7784                a += 1;
7785            }
7786            ids
7787        };
7788        spec_stamp("acc");
7789        if spec_dbg {
7790            eprintln!(
7791                "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
7792                drafts, ids
7793            );
7794        }
7795        // CMF_METAL_VERIFY_CHECK=2: the commit oracle — plain-forward the
7796        // a+1 accepted tokens from a snapshot, then diff the replayed GDN
7797        // states and the appended K/V rows against that.
7798        #[cfg(target_os = "macos")]
7799        let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
7800            && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
7801        {
7802            let snap: Vec<Vec<f32>> = self
7803                .kv_cache
7804                .layers
7805                .iter()
7806                .map(|l| l.linear_state.clone())
7807                .collect();
7808            let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7809            let toks: Vec<u32> = std::iter::once(t_next)
7810                .chain(drafts.iter().copied())
7811                .collect();
7812            let want_save = self.graph_want_logits;
7813            self.graph_want_logits = false;
7814            for (i, &t) in toks.iter().take(a + 1).enumerate() {
7815                let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7816                let _ = self.graph_logits.take();
7817            }
7818            self.graph_want_logits = want_save;
7819            let plain_states: Vec<Vec<f32>> = self
7820                .kv_cache
7821                .layers
7822                .iter()
7823                .map(|l| l.linear_state.clone())
7824                .collect();
7825            let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7826            let mut rows = Vec::new();
7827            for (li, (l, n0)) in self
7828                .kv_cache
7829                .layers
7830                .iter_mut()
7831                .zip(attn_lens.iter())
7832                .enumerate()
7833            {
7834                let extra = l.seq_len.saturating_sub(*n0);
7835                if extra > 0 {
7836                    let mut kk = Vec::new();
7837                    let mut vv = Vec::new();
7838                    for g in 0..nkv {
7839                        kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7840                        vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7841                    }
7842                    rows.push((li, kk, vv));
7843                    l.truncate_last(extra);
7844                    crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
7845                }
7846            }
7847            for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7848                if l.linear_state.len() == st.len() {
7849                    l.linear_state.copy_from_slice(&st);
7850                } else {
7851                    l.linear_state = st;
7852                }
7853            }
7854            Some((plain_states, rows))
7855        } else {
7856            None
7857        };
7858        let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
7859        // Metal: the MTP cache cut and the round's warm-up SUBMIT come
7860        // BEFORE the trunk commit, so the warm-up's command buffer is
7861        // queued ahead of the GDN replay (second queue) and its wait
7862        // below no longer sits behind the replay — measured: the warm-up's
7863        // wait grew with the accepted count exactly like the replay does
7864        // (8 ms at a=1, 17 ms at a=3, 25 ms at a=5 for ~2 ms of its own
7865        // work). The replay now overlaps the warm-up's readback, the
7866        // round's return and the next draft chain.
7867        #[cfg(target_os = "macos")]
7868        let mut warm_pending: Option<MetalWarmPending> = None;
7869        #[cfg(target_os = "macos")]
7870        if metal_native {
7871            m.kv.truncate_last(k_spec.saturating_sub(1));
7872            if self.mtp_graph_mode == Some(true) {
7873                // the mirror rows below the cut are the CPU rows: re-point,
7874                // no re-upload
7875                crate::gpu_metal::kv_mirror_set_stored(
7876                    self.mtp_kv_id(),
7877                    Self::MTP_LAYER_BASE,
7878                    m.kv.seq_len,
7879                );
7880                if !warm_off && a > 0 {
7881                    let pairs: Vec<(&[f32], u32)> = (0..a)
7882                        .map(|j| {
7883                            (
7884                                &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
7885                                ids[j],
7886                            )
7887                        })
7888                        .collect();
7889                    warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
7890                }
7891            }
7892            spec_stamp("c.wsub");
7893        }
7894        // a fully-accepted round needs no restore: every input was real.
7895        #[cfg(target_os = "macos")]
7896        if metal_native {
7897            // the Metal verify never wrote its states: the commit replays the
7898            // accepted prefix into the CPU owners and appends the K/V rows
7899            if !self.metal_verify_commit(a) {
7900                self.clear_sequence_state();
7901                self.graph_failed
7902                    .store(true, std::sync::atomic::Ordering::Relaxed);
7903                self.cancel
7904                    .store(true, std::sync::atomic::Ordering::Relaxed);
7905                tracing::error!("Metal verify state/KV handoff failed after admission");
7906                return None;
7907            }
7908            if let Some((plain_states, rows)) = commit_ref {
7909                crate::gpu_metal::queue_fence();
7910                // the commit's replay runs on the second queue: collect it
7911                // before the oracle reads the CPU owners it writes into
7912                let _ = crate::gpu_metal::wait_replay();
7913                let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7914                let mut worst_s = 0f32;
7915                let mut worst_li = 0usize;
7916                for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
7917                    if l.linear_state.len() != ps.len() || ps.is_empty() {
7918                        continue;
7919                    }
7920                    let d = l
7921                        .linear_state
7922                        .iter()
7923                        .zip(ps)
7924                        .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7925                    let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
7926                    let rel = d / n.max(1e-6);
7927                    if rel > worst_s {
7928                        worst_s = rel;
7929                        worst_li = li;
7930                    }
7931                }
7932                let mut worst_k = 0f32;
7933                for (li, kk, vv) in &rows {
7934                    let l = &self.kv_cache.layers[*li];
7935                    let n0 = l.seq_len - (kk.len() / (nkv * hd));
7936                    let mut ck = Vec::new();
7937                    let mut cv = Vec::new();
7938                    for g in 0..nkv {
7939                        ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7940                        cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7941                    }
7942                    if ck.len() == kk.len() {
7943                        let dk = ck
7944                            .iter()
7945                            .zip(kk)
7946                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7947                        let dv = cv
7948                            .iter()
7949                            .zip(vv)
7950                            .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7951                        worst_k = worst_k.max(dk).max(dv);
7952                    } else {
7953                        eprintln!(
7954                            "commit-check L{li}: kv row count mismatch {} vs {}",
7955                            ck.len(),
7956                            kk.len()
7957                        );
7958                    }
7959                }
7960                eprintln!(
7961                    "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}"
7962                );
7963            }
7964        }
7965        if !metal_native && a + 1 < b {
7966            let expected_gdn_layers = self.graph_gdn_layer_count();
7967            if expected_gdn_layers > 0
7968                && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
7969            {
7970                self.clear_sequence_state();
7971                self.graph_failed
7972                    .store(true, std::sync::atomic::Ordering::Relaxed);
7973                self.cancel
7974                    .store(true, std::sync::atomic::Ordering::Relaxed);
7975                tracing::error!("GDN speculative restore failed after verify");
7976                return None;
7977            }
7978        }
7979        if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
7980            // The verify graph committed the full batch, but one of its
7981            // persistent Full-attention mirrors could not be re-pointed to
7982            // the accepted prefix.  Treat that as terminal state failure;
7983            // an exact CPU fallback would otherwise consume stale GDN/KV.
7984            self.clear_sequence_state();
7985            self.graph_failed
7986                .store(true, std::sync::atomic::Ordering::Relaxed);
7987            self.cancel
7988                .store(true, std::sync::atomic::Ordering::Relaxed);
7989            tracing::error!("trunk graph KV rewind failed after speculative verify");
7990            return None;
7991        }
7992        *accepted += a;
7993        // MTP cache: keep the first draft row (its inputs were real), drop
7994        // the chain's, then append the verified pairs the round produced.
7995        // Each of those is a whole MTP block on the per-op path and they
7996        // cost 5.8 ms of a 69 ms round at k=3 — a third of what the
7997        // round's own draft costs. PRICED, and they earn it: skipping
7998        // them (`CMF_SPEC_WARM=0`) drops acceptance from 89% to 81% at
7999        // k=3 and 85% to 74% at k=4, and the tok/s goes nowhere at k=3
8000        // (50.3 against 50.5) and backwards at k=4 (48.1 against 50.1).
8001        // The knob stays so the next person can re-price it after the
8002        // warms are batched instead of assuming either way.
8003        if !metal_native {
8004            // (Metal cut its MTP cache before the trunk commit, above)
8005            m.kv.truncate_last(k_spec.saturating_sub(1));
8006        }
8007        spec_stamp("c.trunc");
8008        if !metal_native
8009            && self.mtp_graph_mode == Some(true)
8010            && !self.rewind_mtp_graph_mirror(next_pos)
8011        {
8012            // The graph draft was admitted, so inability to move its cursor
8013            // back to the real anchor is a state failure, not a capability
8014            // refusal.  Do not warm or continue with a stale mirror.
8015            self.clear_sequence_state();
8016            self.graph_failed
8017                .store(true, std::sync::atomic::Ordering::Relaxed);
8018            self.cancel
8019                .store(true, std::sync::atomic::Ordering::Relaxed);
8020            tracing::error!("MTP graph mirror rewind failed after verify commit");
8021            return None;
8022        }
8023        if !warm_off && a > 0 {
8024            // Graph arm: all accepted pairs in ONE batched run over the
8025            // MTP block; the token graph one by one if the batch declines.
8026            let mut warmed = false;
8027            #[cfg(target_os = "macos")]
8028            if metal_native && self.mtp_graph_mode == Some(true) {
8029                // the batched warm-up was submitted before the trunk
8030                // commit: collect it here; one by one on the token graph
8031                // if it declined (or failed)
8032                warmed = match warm_pending.take() {
8033                    Some(p) => self.mtp_warm_batch_finish(m, p),
8034                    None => false,
8035                };
8036                if !warmed {
8037                    warmed = true;
8038                    for j in 0..a {
8039                        let row =
8040                            hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8041                        if self
8042                            .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8043                            .is_none()
8044                        {
8045                            warmed = false;
8046                            break;
8047                        }
8048                    }
8049                }
8050            }
8051            if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8052                let rows: Vec<Vec<f32>> = (0..a)
8053                    .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8054                    .collect();
8055                let pairs: Vec<(&[f32], u32)> = rows
8056                    .iter()
8057                    .zip(ids.iter())
8058                    .map(|(r, &t)| (r.as_slice(), t))
8059                    .collect();
8060                match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8061                    Ok(()) => warmed = true,
8062                    Err(err) => {
8063                        // A warm-up failure after graph admission cannot
8064                        // fall back to `mtp_warm`: the detached CPU cache is
8065                        // not authoritative for the device mirror.  Mark it
8066                        // terminal so the generation caller clears state and
8067                        // returns instead of drafting from stale attention.
8068                        tracing::error!("{err}");
8069                        self.clear_sequence_state();
8070                        self.graph_failed
8071                            .store(true, std::sync::atomic::Ordering::Relaxed);
8072                        self.cancel
8073                            .store(true, std::sync::atomic::Ordering::Relaxed);
8074                        return None;
8075                    }
8076                }
8077            }
8078            if !warmed {
8079                for j in 0..a {
8080                    let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8081                    let row = row.to_vec();
8082                    self.mtp_warm(m, &row, ids[j], next_pos + j);
8083                }
8084            }
8085        }
8086        // The sampler's contract: logits of the LAST verified position —
8087        // unless a rejected draft already drew the correction, in which
8088        // case the loop top commits that token and samples nothing.
8089        spec_stamp("c.warm");
8090        if let Some(c) = forced {
8091            self.spec_forced = Some(c);
8092            self.graph_logits = None;
8093        } else if greedy_dev && logits.is_empty() {
8094            // the row's argmax IS the token the loop top would pick from
8095            // it (plain greedy, no penalties): commit it as forced
8096            self.spec_forced = Some(ids[a]);
8097            self.graph_logits = None;
8098        } else {
8099            let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8100            row.resize(self.vocab_size, 0.0);
8101            if let Some(c) = self.final_softcap {
8102                for l in row.iter_mut() {
8103                    *l = c * (*l / c).tanh();
8104                }
8105            }
8106            self.graph_logits = Some(row);
8107        }
8108        let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8109        spec_stamp("c.row");
8110        // Three phases, not two. The round's wall clock was 4 ms longer
8111        // than draft+verify and the difference had nowhere to be seen:
8112        // the accepted prefix re-runs the MTP block once per token to
8113        // keep the draft head's attention cache warm, and the GDN state
8114        // rolls back on any rejection. Both live here, after the verify.
8115        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8116            let end = subs();
8117            eprintln!(
8118                "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8119                 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8120                t_draft.as_secs_f64() * 1e3,
8121                sub_draft - sub0,
8122                (t_verify - t_draft).as_secs_f64() * 1e3,
8123                sub_verify - sub_draft,
8124                (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8125                end - sub_verify,
8126                self.draft_full_streak,
8127            );
8128        }
8129        // Native Metal's verify tile is flat in b (eight rows for the price
8130        // of one), so a shorter round only forfeits tokens — measured on
8131        // the M4: an essay round at k=2 still verified in 260 ms. The
8132        // adaptation is for cards whose verify grows with the rows.
8133        if k_env.is_none() && !metal_native && !k_capped {
8134            // Slow average and a wide band: a fast one oscillated 2↔3 on
8135            // an essay every other round (measured), which forfeits the
8136            // draft it just paid for.
8137            let f = a as f32 / k_spec.max(1) as f32;
8138            self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8139            let mut k_next = k_spec;
8140            if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8141                k_next = k_spec + 1;
8142            } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8143                k_next = k_spec - 1;
8144            }
8145            if k_next != k_spec {
8146                self.spec_acc_ewma = 0.6;
8147                if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8148                    eprintln!("spec-k: {k_spec} → {k_next}");
8149                }
8150            }
8151            self.spec_k_adapt = Some(k_next);
8152        }
8153        spec_stamp("end");
8154        Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8155    }
8156
8157    /// Micro-benchmark: two single-position forwards vs one fused pair
8158    /// from the current cache state (KV rewound after each probe).
8159    /// Returns (two_singles_ms, fused_pair_ms) per probe, or the (0, 0)
8160    /// sentinel when this model has no pair path to measure — the same
8161    /// answer the o1 arm gives, and the bench prints it the same way.
8162    /// (An architecture that loads its own layers leaves `weights.layers`
8163    /// empty; walking it here was an index panic, found by `bench` on
8164    /// deepseek_v4.)
8165    pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8166        if !self.pair_supported() {
8167            return (0.0, 0.0);
8168        }
8169        // A GDN model that needs an automatic device prefix never uses this
8170        // fused-pair path in normal generation: its recurrence is serial and
8171        // the speculative pair is disabled by default.  More importantly,
8172        // this diagnostic suppresses the token graph, so measuring it would
8173        // stream the non-resident tail through per-op Vulkan just to print a
8174        // non-production number.  Leave inference untouched and report the
8175        // established unavailable sentinel instead.
8176        if self.gdn_cfg.is_some()
8177            && self.graph_prefill_preferred()
8178            && self.automatic_gpu_prefix().is_some()
8179        {
8180            tracing::info!(
8181                "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8182            );
8183            return (0.0, 0.0);
8184        }
8185        // This is a host-side pair micro-benchmark. It truncates the host KV
8186        // after every probe, so letting the whole-token graph participate
8187        // would leave its device GDN/KV mirror ahead of the next probe and
8188        // poison the process-wide graph verdict before the real generation
8189        // benchmark starts. Keep the existing per-op/GPU arithmetic while
8190        // suppressing only the stateful token graph for this measurement.
8191        let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8192        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8193        let emb1 = self.embed_single(1);
8194        let emb2 = self.embed_single(2);
8195        let pos = self.kv_cache.seq_len();
8196
8197        let t0 = std::time::Instant::now();
8198        for _ in 0..iters {
8199            let _ = self.forward_layers(&emb1, pos, None);
8200            let _ = self.forward_layers(&emb2, pos + 1, None);
8201            for l in &mut self.kv_cache.layers {
8202                l.truncate_last(2);
8203            }
8204        }
8205        let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8206
8207        let t1 = std::time::Instant::now();
8208        for _ in 0..iters {
8209            let _ = self.forward_pair(&emb1, &emb2, pos);
8210            for l in &mut self.kv_cache.layers {
8211                l.truncate_last(2);
8212            }
8213        }
8214        let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8215        match graph_env {
8216            Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8217            None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8218        }
8219        (singles_ms, pair_ms)
8220    }
8221
8222    /// Fused two-position forward: weight rows are streamed from memory
8223    /// once per layer for both positions. Full layers → fused GQA pair;
8224    /// linear layers → vmf_phase pair (lane 2 state is tentative in the
8225    /// per-layer scratch until the draft is accepted).
8226    /// Whether the fused two-position path covers every layer kind in
8227    /// this model. MLA and KDA run per position (their pair arms are
8228    /// unreachable); the seq prefill falls back to singles for them.
8229    fn pair_supported(&self) -> bool {
8230        // An EMPTY layer stack means the architecture loaded its own and
8231        // this path has nothing to walk. Checking that directly, rather
8232        // than naming each such architecture, is what makes the guard hold
8233        // for the next one: `any()` over no layers is false, so a
8234        // feature-by-feature test says "supported" for a model that has no
8235        // layers here at all.
8236        !self.weights.layers.is_empty()
8237            && self.g3n.is_none()
8238            && !self
8239                .weights
8240                .layers
8241                .iter()
8242                .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8243    }
8244
8245    fn forward_pair(
8246        &mut self,
8247        emb1: &[f32],
8248        emb2: &[f32],
8249        position: usize,
8250    ) -> (Vec<f32>, Vec<f32>) {
8251        // A two-token prompt starts here, not in the layer walk: decide the
8252        // MiMo placement before the pair's per-op MoE uploads any expert.
8253        self.mimo_moe_prepare();
8254        let mut h1 = emb1.to_vec();
8255        let mut h2 = emb2.to_vec();
8256        let (_nkv, _hd, hs, _rd, eps) = (
8257            self.num_kv_heads,
8258            self.head_dim,
8259            self.hidden_size,
8260            self.rotary_dim,
8261            self.rms_eps,
8262        );
8263        let pool = self.pool.clone();
8264        // `forward_pair` bypasses `forward_layers_span`, but its per-op
8265        // body must still obey the ordinary layer placement. In particular,
8266        // an over-capacity Vulkan model has a device prefix and a CPU tail;
8267        // letting the pair walk submit the tail's selected MoE experts would
8268        // stream the whole tail through the residency arena and defeat the
8269        // measured VRAM bound.
8270        let automatic_gpu_prefix = self.automatic_gpu_prefix();
8271
8272        for li in 0..self.num_layers {
8273            let _capacity_tail = automatic_gpu_prefix
8274                .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8275                .map(|_| crate::gpu::enter_cpu_scope());
8276            // Also makes an explicit CMF_GPU_LAYERS split apply to fused
8277            // pairs, just as it does to the ordinary layer walk.
8278            crate::gpu::set_layer(li as i64);
8279            let lw = &self.weights.layers[self.phys_layer(li)];
8280            // Norms into pipeline scratch (4 allocs/layer on the MTP
8281            // decode hot path before this).
8282            inference::rms_norm_into(
8283                &h1,
8284                &lw.input_norm,
8285                self.rms_eps,
8286                self.norm_style,
8287                &mut self.ws.n1,
8288            );
8289            inference::rms_norm_into(
8290                &h2,
8291                &lw.input_norm,
8292                self.rms_eps,
8293                self.norm_style,
8294                &mut self.ws.n2,
8295            );
8296
8297            let (a1, a2) = match &lw.attn {
8298                AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8299                AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8300                AttnKind::Bounded(w) => {
8301                    // Two sequential positions of the bounded operator
8302                    // (the ring's causal order is the pair's order).
8303                    let rope = self
8304                        .bounded_rope
8305                        .clone()
8306                        .expect("bounded layer without an installed rotation table");
8307                    let cfg = crate::bounded::BoundedAttnCfg {
8308                        num_heads: self.num_heads,
8309                        num_kv_heads: self.num_kv_heads,
8310                        head_dim: self.head_dim,
8311                        hidden_size: hs,
8312                        scale: self.attn_scale,
8313                        rope: &rope,
8314                        pool: pool.as_deref(),
8315                    };
8316                    let a1 = crate::bounded::bounded_attention(
8317                        &self.ws.n1,
8318                        w,
8319                        &mut self.kv_cache.layers[li],
8320                        &cfg,
8321                    );
8322                    let a2 = crate::bounded::bounded_attention(
8323                        &self.ws.n2,
8324                        w,
8325                        &mut self.kv_cache.layers[li],
8326                        &cfg,
8327                    );
8328                    (a1, a2)
8329                }
8330                AttnKind::Linear(w) => {
8331                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8332                    let layer = &mut self.kv_cache.layers[li];
8333                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8334                    vmf_phase_pair(
8335                        &self.ws.n1,
8336                        &self.ws.n2,
8337                        w,
8338                        &cfg,
8339                        state,
8340                        scratch,
8341                        self.pool.as_deref(),
8342                    )
8343                }
8344                AttnKind::LinearGdn(w) => {
8345                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8346                    let layer = &mut self.kv_cache.layers[li];
8347                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8348                    gdn_pair(
8349                        &self.ws.n1,
8350                        &self.ws.n2,
8351                        w,
8352                        &cfg,
8353                        state,
8354                        scratch,
8355                        self.pool.as_deref(),
8356                    )
8357                }
8358                AttnKind::ShortConv(w) => {
8359                    let cfg = self
8360                        .short_conv_cfg
8361                        .expect("short-conv layer without short_conv_cfg");
8362                    let layer = &mut self.kv_cache.layers[li];
8363                    let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8364                    short_conv_pair(
8365                        &self.ws.n1,
8366                        &self.ws.n2,
8367                        w,
8368                        &cfg,
8369                        state,
8370                        scratch,
8371                        self.pool.as_deref(),
8372                    )
8373                }
8374                AttnKind::Full {
8375                    wq,
8376                    wk,
8377                    wv,
8378                    wo,
8379                    q_norm,
8380                    k_norm,
8381                    output_gate,
8382                    softplus_gate,
8383                    bias,
8384                } => {
8385                    let inv_freq_l = self.layer_inv_freq(li);
8386                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8387                    let cfg = QwenAttnCfg {
8388                        num_heads: self.layer_num_heads(li),
8389                        num_kv_heads: nkv_l,
8390                        head_dim: hd_l,
8391                        hidden_size: hs,
8392                        position,
8393                        inv_freq: &inv_freq_l,
8394                        rotary_dim: rd_l,
8395                        scale: self.attn_scale,
8396                        softcap: self.attn_softcap,
8397                        window: self.layer_window(li),
8398                        v_norm: self.attn_v_norm,
8399                        qk_norm_after_rope: self.qk_norm_after_rope,
8400                        gate_sigmoid: self.proj_gate_sigmoid,
8401                        q_norm: q_norm.as_deref(),
8402                        k_norm: k_norm.as_deref(),
8403                        output_gate: *output_gate,
8404                        softplus_gate: softplus_gate
8405                            .as_ref()
8406                            .map(|(gate, per_head)| (gate, *per_head)),
8407                        rope_scale: self.layer_rope_scale(li),
8408                        bias: bias
8409                            .as_ref()
8410                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8411                        rms_eps: eps,
8412                        norm_style: self.norm_style,
8413                        pool: pool.as_deref(),
8414                        v_head_dim: self.layer_v_dim(li),
8415                    };
8416                    attention::qwen_attention_pair(
8417                        &self.ws.n1,
8418                        &self.ws.n2,
8419                        wq,
8420                        wk,
8421                        wv,
8422                        wo,
8423                        &mut self.kv_cache.layers[li],
8424                        &cfg,
8425                    )
8426                }
8427            };
8428            let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8429                Some(w) => (
8430                    inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8431                    inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8432                ),
8433                None => (a1, a2),
8434            };
8435            for i in 0..self.hidden_size {
8436                h1[i] += a1[i];
8437                h2[i] += a2[i];
8438            }
8439            let (mut a1, mut a2) = (a1, a2);
8440            attention::recycle_buf(&mut a1);
8441            attention::recycle_buf(&mut a2);
8442
8443            let lw = &self.weights.layers[self.phys_layer(li)];
8444            inference::rms_norm_into(
8445                &h1,
8446                &lw.post_norm,
8447                self.rms_eps,
8448                self.norm_style,
8449                &mut self.ws.p1,
8450            );
8451            inference::rms_norm_into(
8452                &h2,
8453                &lw.post_norm,
8454                self.rms_eps,
8455                self.norm_style,
8456                &mut self.ws.p2,
8457            );
8458            let (f1, f2) = match &lw.ffn {
8459                // Dual-branch layers need the raw residuals — run the
8460                // two positions through the same fn decode uses.
8461                FfnKind::DenseMoe(dm) => (
8462                    dense_moe_ffn(
8463                        dm,
8464                        &self.ws.p1,
8465                        &h1,
8466                        self.rms_eps,
8467                        self.norm_style,
8468                        self.pool.as_deref(),
8469                    ),
8470                    dense_moe_ffn(
8471                        dm,
8472                        &self.ws.p2,
8473                        &h2,
8474                        self.rms_eps,
8475                        self.norm_style,
8476                        self.pool.as_deref(),
8477                    ),
8478                ),
8479                FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8480                    moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8481                    moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8482                ),
8483                _ => ffn_forward_pair(
8484                    &lw.ffn,
8485                    &self.ws.p1,
8486                    &self.ws.p2,
8487                    self.pool.as_deref(),
8488                    None,
8489                ),
8490            };
8491            let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8492                Some(w) => (
8493                    inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8494                    inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8495                ),
8496                None => (f1, f2),
8497            };
8498            for i in 0..self.hidden_size {
8499                h1[i] += f1[i];
8500                h2[i] += f2[i];
8501            }
8502            let (mut f1, mut f2) = (f1, f2);
8503            attention::recycle_buf(&mut f1);
8504            attention::recycle_buf(&mut f2);
8505            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8506                for i in 0..self.hidden_size {
8507                    h1[i] *= sc;
8508                    h2[i] *= sc;
8509                }
8510            }
8511            // Looped Transformer: apply final norm at the end of each loop iteration.
8512            if self.is_loop_end(li) && li + 1 < self.num_layers {
8513                h1 = inference::rms_norm(
8514                    &h1,
8515                    &self.weights.final_norm,
8516                    self.rms_eps,
8517                    self.norm_style,
8518                );
8519                h2 = inference::rms_norm(
8520                    &h2,
8521                    &self.weights.final_norm,
8522                    self.rms_eps,
8523                    self.norm_style,
8524                );
8525            }
8526        }
8527        crate::gpu::set_layer(-1); // final/O(1) work is outside layer placement
8528        // Real O(1) prefill pairs may also carry tentative lane-2 recurrent
8529        // state. Commit it before publishing the transition epoch so the
8530        // next serial/device row cannot observe a new attention epoch with an
8531        // old GDN state. Speculative pairs run only when O(1) is inactive and
8532        // retain their existing caller-controlled commit/rollback semantics.
8533        if self.o1_active() {
8534            self.commit_linear_scratch();
8535        }
8536        self.o1_progress();
8537        self.swa_trim_tails();
8538        (h1, h2)
8539    }
8540
8541    /// Commit lane-2 linear states after an accepted draft.
8542    fn commit_linear_scratch(&mut self) {
8543        for layer in &mut self.kv_cache.layers {
8544            if !layer.linear_scratch.is_empty() {
8545                std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8546                layer.linear_scratch.clear();
8547            }
8548        }
8549    }
8550
8551    /// Forward a full id sequence from a fresh cache and return the
8552    /// logits after the last position (golden-parity harness, bench).
8553    pub fn forward_ids(
8554        &mut self,
8555        ids: &[u32],
8556        task_mask: Option<&TaskMask>,
8557    ) -> Result<Vec<f32>, String> {
8558        #[cfg(target_os = "macos")]
8559        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8560        if ids.is_empty() {
8561            return Err("empty id sequence".to_string());
8562        }
8563        self.clear_sequence_state();
8564        self.check_forward_graph("forward_ids setup", 0)?;
8565        if task_mask.is_none() {
8566            self.o1_begin();
8567        }
8568        let mut hidden = vec![0.0f32; self.hidden_size];
8569        let mut pos = 0usize;
8570        if let Some(b) = &mut self.dsv41 {
8571            let pool = self.pool.clone();
8572            let mut logits = Vec::new();
8573            crate::dsv41::forward_chunk(
8574                &b.0,
8575                &b.1,
8576                &b.2,
8577                &mut b.3,
8578                ids,
8579                0,
8580                pool.as_deref(),
8581                &mut logits,
8582            );
8583            if let Err(err) = self.o1_seal_checked() {
8584                self.clear_sequence_state();
8585                return Err(err);
8586            }
8587            return Ok(logits);
8588        }
8589        // Same routing predicate generation uses. Two reasons it must be
8590        // the same one: (1) a GDN hybrid's recurrent state is GPU-
8591        // resident, and a batched CPU prefill would build it on the host
8592        // only — decode then reads buffers the prefill never wrote;
8593        // (2) bench times THIS function and calls the result "prefill",
8594        // so a different path here reports a number production never
8595        // sees (W2 on 2×5090: 8.7 tok/s reported against 125 real).
8596        // Gemma-4: generation's batched-graph ingest (`g4_batch_ingest`),
8597        // so bench and the parity tools time and score the route run/serve
8598        // take; a refusal leaves the rest to the chunked host prefill.
8599        #[cfg(not(target_os = "macos"))]
8600        if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
8601            let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
8602                Ok(r) => r,
8603                Err(e) => {
8604                    self.clear_sequence_state();
8605                    return Err(e);
8606                }
8607            };
8608            if n > 0 {
8609                hidden.copy_from_slice(&last);
8610                pos = n;
8611            }
8612        }
8613        if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
8614            // prefill-GEMM in chunks; only the last position's hidden is
8615            // needed. (o1-compatible: the batch path attends per position
8616            // through qwen_attention, which carries the collection hook.)
8617            let chunk = self.prefill_chunk();
8618            let hs = self.hidden_size;
8619            while pos < ids.len() {
8620                let end = (pos + chunk).min(ids.len());
8621                let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8622                self.check_forward_graph("forward_ids batched prefill", end - 1)?;
8623                hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8624                pos = end;
8625            }
8626        }
8627        // Same guards as generation's prefill — INCLUDING the graph one.
8628        // The CPU pair walk was intercepting positions that the resident
8629        // token graph would have run itself: on a GDN hybrid over wgpu
8630        // that is 89 ms of host forward against 7 ms of device submit,
8631        // and it made prefill look 12× slower than it is (W2 on an RTX
8632        // 5090, ctx 512: 11.2 tok/s with the walk, 136.6 without).
8633        // CMF_PAIR=0 opts out; a model whose layers live outside
8634        // `weights.layers` has no pair walk to take.
8635        if task_mask.is_none()
8636            && !self.graph_prefill_preferred()
8637            && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
8638            && self.pair_supported()
8639        {
8640            while pos + 1 < ids.len() {
8641                let e1 = self.embed_single(ids[pos]);
8642                let e2 = self.embed_single(ids[pos + 1]);
8643                let (_, h2) = self.forward_pair(&e1, &e2, pos);
8644                self.check_forward_graph("forward_ids pair", pos + 1)?;
8645                self.commit_linear_scratch();
8646                hidden = h2;
8647                pos += 2;
8648            }
8649        }
8650        // Resident Embryo graph: the prompt in chunks of one submit each
8651        // (the same device state and logits as the per-position walk).
8652        if task_mask.is_none() && pos == 0 && ids.len() > 1 {
8653            if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
8654                self.graph_logits = Some(lg);
8655                hidden = vec![0.0; self.hidden_size];
8656                pos = ids.len();
8657            }
8658        }
8659        while pos < ids.len() {
8660            hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8661            self.check_forward_graph("forward_ids", pos)?;
8662            pos += 1;
8663        }
8664        if let Some(logits) = self.graph_logits.take() {
8665            // Resident stacks already applied final norm and their head in
8666            // the same submit; do not run a second norm/head over the zero
8667            // hidden sentinel returned by forward_layers_span.
8668            if let Err(err) = self.o1_seal_checked() {
8669                self.clear_sequence_state();
8670                return Err(err);
8671            }
8672            return Ok(logits);
8673        }
8674        // Harness contract: after forward_ids the cache is decode-ready —
8675        // under o1 that means sealed (bench measures the seal as part of
8676        // prefill, honestly).
8677        if let Err(err) = self.o1_seal_checked() {
8678            self.clear_sequence_state();
8679            return Err(err);
8680        }
8681        let normed = inference::rms_norm(
8682            &hidden,
8683            &self.weights.final_norm,
8684            self.rms_eps,
8685            self.norm_style,
8686        );
8687        Ok(self.lm_head_forward(&normed))
8688    }
8689
8690    /// Run the V4.1 stack one token at a time and retain logits for every
8691    /// position. This is a diagnostic surface for comparing a converted
8692    /// checkpoint with a tokenwise reference implementation.
8693    #[doc(hidden)]
8694    pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
8695        #[cfg(target_os = "macos")]
8696        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8697        if ids.is_empty() {
8698            return Err("empty id sequence".to_string());
8699        }
8700        self.clear_sequence_state();
8701        self.dsv41
8702            .as_ref()
8703            .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
8704        self.o1_begin();
8705        let rows = {
8706            let pool = self.pool.clone();
8707            let b = self
8708                .dsv41
8709                .as_mut()
8710                .expect("dsv41 checked above; state cannot change during forward");
8711            let mut rows = Vec::with_capacity(ids.len());
8712            for (position, &id) in ids.iter().enumerate() {
8713                let mut logits = Vec::new();
8714                crate::dsv41::forward_token(
8715                    &b.0,
8716                    &b.1,
8717                    &b.2,
8718                    &mut b.3,
8719                    id,
8720                    position,
8721                    pool.as_deref(),
8722                    &mut logits,
8723                );
8724                rows.push(logits);
8725            }
8726            rows
8727        };
8728        self.o1_seal();
8729        Ok(rows)
8730    }
8731
8732    /// GPTQ calibration of DeepSeek-V4.1 (`cortiq quantize-moe`): the MoE
8733    /// input rows and routed expert ids of layers `lo..hi` for `ids` (a fresh
8734    /// sequence), see [`crate::dsv41::Dsv41MoeCapture`].
8735    pub fn dsv41_moe_capture(
8736        &mut self,
8737        ids: &[u32],
8738        lo: usize,
8739        hi: usize,
8740    ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
8741        #[cfg(target_os = "macos")]
8742        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8743        if ids.is_empty() {
8744            return Err("empty id sequence".to_string());
8745        }
8746        self.dsv41
8747            .as_ref()
8748            .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
8749        self.clear_sequence_state();
8750        let res = {
8751            let pool = self.pool.clone();
8752            let b = self.dsv41.as_mut().expect("dsv41 checked above");
8753            crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
8754        };
8755        self.clear_sequence_state();
8756        res
8757    }
8758
8759    /// Teacher-forced perplexity over a token sequence (phase-C gate:
8760    /// honest quant comparisons instead of prompt vibes).
8761    ///
8762    /// Attention is EXACT even on a model whose layers are flagged for
8763    /// the O(1) kernel — scoring the backbone is the default on purpose
8764    /// (it is the yardstick). `nll_ids_o1` scores the CONVERTED model.
8765    pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
8766        let (nll, cnt) = self.nll_ids_from(ids, 0)?;
8767        Ok((nll / cnt.max(1) as f64).exp())
8768    }
8769
8770    /// GPTQ calibration forward: `ids` through layers `0..upto` only, in
8771    /// layer-major chunks of `chunk` positions, with no vocabulary head and
8772    /// no scoring. The Hessian hook (`gptq_capture`) folds every
8773    /// projection's input on the way; the layers above the captured span
8774    /// and the head would only cost time. Falls back to a scored forward
8775    /// where the batched prefill does not apply.
8776    pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
8777        if !self.can_prefill_batched() {
8778            return self.nll_ids_from(ids, 0).map(|_| ());
8779        }
8780        self.nll_begin()?;
8781        let result: Result<(), String> = (|| {
8782            let chunk = chunk.max(1);
8783            let mut pos = 0usize;
8784            while pos < ids.len() {
8785                let end = (pos + chunk).min(ids.len());
8786                let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
8787                self.nll_check_graph("calibration prefill", pos)?;
8788                pos = end;
8789            }
8790            Ok(())
8791        })();
8792        self.nll_end();
8793        result
8794    }
8795
8796    /// Layer-streamed calibration of one fresh window: layers `from..mid` run
8797    /// on `input` (the hidden state entering layer `from`; `None` = the
8798    /// token embeddings of `ids`) and their output is returned, then layers
8799    /// `mid..upto` run on that output for the capture hooks (their output is
8800    /// dropped). A caller that keeps the returned state per window runs each
8801    /// layer once over the whole conversion instead of re-running the stack
8802    /// below every layer. With `mid == from` the input is returned as is
8803    /// (`None` input: an empty vector).
8804    pub fn calib_window_span(
8805        &mut self,
8806        ids: &[u32],
8807        input: Option<&[f32]>,
8808        from: usize,
8809        mid: usize,
8810        upto: usize,
8811    ) -> Result<Vec<f32>, String> {
8812        if !self.can_prefill_batched() {
8813            return Err("calib_window_span: this architecture has no batched prefill".into());
8814        }
8815        self.nll_begin()?;
8816        let result: Result<Vec<f32>, String> = (|| {
8817            let h = if mid > from {
8818                let inp = match input {
8819                    Some(h) => PrefillIn::Hidden(h),
8820                    None => PrefillIn::Ids(ids),
8821                };
8822                let h = self.prefill_batch_span(inp, 0, None, from, mid);
8823                self.nll_check_graph("calibration span", 0)?;
8824                h
8825            } else {
8826                input.map(<[f32]>::to_vec).unwrap_or_default()
8827            };
8828            if upto > mid {
8829                let inp = if mid > from || input.is_some() {
8830                    PrefillIn::Hidden(&h)
8831                } else {
8832                    PrefillIn::Ids(ids)
8833                };
8834                let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
8835                self.nll_check_graph("calibration span", 0)?;
8836            }
8837            Ok(h)
8838        })();
8839        self.nll_end();
8840        result
8841    }
8842
8843    /// GPTQ calibration of a Flash-Next-class MoE (`qwen4_exp`): MoE input
8844    /// rows and router logits of layers `lo..hi` for `ids` (a fresh
8845    /// sequence), per layer `[ids.len()][hidden + n_experts]`.
8846    #[cfg(feature = "gpu")]
8847    pub fn qwen4_capture(
8848        &mut self,
8849        ids: &[u32],
8850        lo: usize,
8851        hi: usize,
8852    ) -> Result<Vec<Vec<f32>>, String> {
8853        let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
8854        crate::qwen4_exp::capture_window(
8855            &b.0,
8856            &b.1,
8857            &b.2,
8858            &mut b.3,
8859            ids,
8860            lo,
8861            hi,
8862            &self.inv_freq,
8863            self.pool.as_deref(),
8864        )
8865    }
8866
8867    /// Expert directory triples of a `qwen4_exp` layer (routed, shared).
8868    pub fn qwen4_expert_dirs(
8869        &self,
8870        li: usize,
8871    ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
8872        let b = self.qwen4_exp.as_ref()?;
8873        crate::qwen4_exp::expert_dirs(&b.1, li)
8874    }
8875
8876    /// DTG-MA calibration pass (Patent 2): run `ids` through the model
8877    /// (CPU path, per position) and return each layer's per-neuron
8878    /// activation mass Σ|silu(gate)·up| — the statistic the task-guided
8879    /// FFN mask is derived from.
8880    pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
8881        self.clear_sequence_state();
8882        FFN_PROBE.with(|p| {
8883            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8884        });
8885        crate::gpu::cpu_scope(|| {
8886            for (pos, &id) in ids.iter().enumerate() {
8887                let emb = self.embed_single(id);
8888                let _ = self.forward_layers(&emb, pos, None);
8889            }
8890        });
8891        self.clear_sequence_state();
8892        FFN_PROBE
8893            .with(|p| p.borrow_mut().take())
8894            .unwrap_or_default()
8895    }
8896
8897    /// `probe_ffn_mass` over the BATCHED prefill: same accumulator, one
8898    /// sweep instead of one forward per token. What makes the statistic
8899    /// affordable on a 27B.
8900    pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
8901        if let Err(err) = self.nll_begin() {
8902            // A recorder can be left by a caller that was interrupted before
8903            // this request entered its scoring block.  Consume it even when
8904            // the preflight failure prevents initialization of a new one.
8905            let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
8906            self.nll_end();
8907            return Err(err);
8908        }
8909        FFN_PROBE.with(|p| {
8910            *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8911        });
8912        let result: Result<(), String> = (|| {
8913            for chunk in ids.chunks(256) {
8914                if chunk.len() < 2 {
8915                    continue;
8916                }
8917                self.nll_ids_masked(chunk, 0, None)?;
8918            }
8919            Ok(())
8920        })();
8921        self.nll_end();
8922        let probe = FFN_PROBE
8923            .with(|p| p.borrow_mut().take())
8924            .unwrap_or_default();
8925        match result {
8926            Ok(()) => Ok(probe),
8927            Err(err) => {
8928                drop(probe);
8929                Err(err)
8930            }
8931        }
8932    }
8933
8934    /// Teacher-forced PPL with a task mask active (sparse execution) —
8935    /// the quality gate for a DTG-MA-masked skill. Sequential per
8936    /// position: the batched prefill path is dense-only.
8937    pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
8938        self.nll_begin()?;
8939        let result: Result<f64, String> = (|| {
8940            let mut nll = 0f64;
8941            let mut cnt = 0usize;
8942            let mut hidden = vec![0f32; self.hidden_size];
8943            for (pos, &id) in ids.iter().enumerate() {
8944                if pos > 0 {
8945                    inference::rms_norm_into(
8946                        &hidden,
8947                        &self.weights.final_norm,
8948                        self.rms_eps,
8949                        self.norm_style,
8950                        &mut self.ws.n1,
8951                    );
8952                    let mut logits = self.lm_head_forward(&self.ws.n1);
8953                    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8954                    let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
8955                    let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
8956                    nll -= p.max(1e-300).ln();
8957                    cnt += 1;
8958                    attention::recycle_buf(&mut logits);
8959                }
8960                let emb = self.embed_single(id);
8961                hidden = self.forward_layers(&emb, pos, Some(mask));
8962                self.nll_check_graph("masked serial forward", pos)?;
8963                // Consume a possible graph logits side channel before the
8964                // next row.  Masked scoring normally disables that route,
8965                // but stale channel state must never survive a request.
8966                let _ = self.graph_logits.take();
8967            }
8968            Ok((nll / cnt.max(1) as f64).exp())
8969        })();
8970        self.nll_end();
8971        result
8972    }
8973
8974    /// Teacher-forced NLL sum + scored-token count over positions
8975    /// `start..len-1`, attention EXACT. Positions below `start` still
8976    /// run — they are the context — they are just not scored, so this
8977    /// pairs with `nll_ids_o1(ids, start)` over the very same tokens.
8978    ///
8979    /// Returning (nll, cnt) rather than a ppl is what lets a windowed
8980    /// caller combine windows before the exp, so every scored token
8981    /// weighs the same regardless of how the windows are cut.
8982    /// `nll_ids_from` with a task mask held active at every position.
8983    ///
8984    /// The batched prefill path does not thread masks, so this walks the
8985    /// per-position forward — slower, but it scores the file exactly the
8986    /// way `run --task` will serve it, which is the point of the gate
8987    /// that calls it. With `None` it defers to the fast path.
8988    /// Masked scoring rides the SAME batched sweep as unmasked scoring —
8989    /// the masked-inference fast path: `prefill_batch_masked` lands the
8990    /// per-visit FFN rows on the activations inside the fused arms. The
8991    /// per-position loop below remains only as the no-batch fallback.
8992    pub fn nll_ids_masked(
8993        &mut self,
8994        ids: &[u32],
8995        start: usize,
8996        task_mask: Option<&TaskMask>,
8997    ) -> Result<(f64, usize), String> {
8998        let task_mask = self.drop_open_mask(task_mask);
8999        self.nll_ids_inner(ids, start, task_mask)
9000    }
9001
9002    pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9003        self.nll_ids_inner(ids, start, None)
9004    }
9005
9006    fn nll_ids_inner(
9007        &mut self,
9008        ids: &[u32],
9009        start: usize,
9010        task_mask: Option<&TaskMask>,
9011    ) -> Result<(f64, usize), String> {
9012        self.nll_begin()?;
9013        let result: Result<(f64, usize), String> = (|| {
9014            let mut nll = 0f64;
9015            let mut cnt = 0usize;
9016            // An unmasked quality run with the resident wgpu graph must score
9017            // the same stateful path used by generation.  The layer-major
9018            // GEMM prefill below is a valid CPU/GEMM oracle, but it seeds
9019            // neither the graph's device GDN state nor its device KV mirrors;
9020            // using it here would silently score a different execution.  Keep
9021            // masked scoring on the exact per-position path as before, and
9022            // let the serial arm below drive the graph-aware scorer.
9023            // Only native Metal has a fused graph lm_head contract.  Vulkan
9024            // and other graph backends may expose hidden state without the
9025            // optional logits side channel; preserve their established CPU
9026            // norm/head fallback instead of turning that valid route into a
9027            // hard missing-logits error.
9028            let (graph_quality, fused_head_quality) = nll_graph_policy(
9029                task_mask.is_none(),
9030                self.graph_prefill_preferred(),
9031                crate::gpu::q1_force(),
9032            );
9033            self.graph_head_required = fused_head_quality;
9034            self.graph_want_logits = fused_head_quality;
9035            #[cfg(target_os = "macos")]
9036            if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
9037                match self.nll_batch_metal(ids, start) {
9038                    MetalBatchNllOutcome::Completed(nll, count) => {
9039                        return Ok((nll, count));
9040                    }
9041                    MetalBatchNllOutcome::Declined => {}
9042                    MetalBatchNllOutcome::Failed(err) => return Err(err),
9043                }
9044            }
9045            // CMF_NLL_SERIAL=1 scores position by position through the
9046            // decode path (the wgpu token graph where it admits the model)
9047            // — the perplexity gate of a decode kernel, which the batched
9048            // prefill arm below never runs.
9049            let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
9050            if self.can_prefill_batched() && !graph_quality && !force_serial {
9051                // prefill-GEMM: layer-major position chunks, lm_head batched
9052                // (254MB lm_head read once per chunk, not per position).
9053                // The layer chunk is large (grouping positions by MoE experts
9054                // wins with size), lm_head in sub-blocks (logit buffer
9055                // 32×vocab ≈ 32MB instead of 128×).
9056                // CMF_NLL_CHUNK: positions per prefill step (default 128).
9057                // A device scorer pays one round trip per projection per
9058                // step, so a 512-position step quarters them.
9059                let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
9060                    .ok()
9061                    .and_then(|v| v.parse().ok())
9062                    .filter(|&c: &usize| c > 0)
9063                    .unwrap_or(128);
9064                const LM_SUB: usize = 32;
9065                let n = ids.len().saturating_sub(1);
9066                let hs = self.hidden_size;
9067                let rows = self.weights.lm_head.rows();
9068                let mut pos = 0usize;
9069                let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
9070                while pos < n {
9071                    let end = (pos + chunk_len).min(n);
9072                    let bsz = end - pos;
9073                    let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9074                    self.nll_check_graph("batched prefill", pos)?;
9075                    if state_trace && end % 256 == 0 {
9076                        self.trace_recurrent_state(end);
9077                    }
9078                    let mut k0 = 0usize;
9079                    while k0 < bsz {
9080                        let k1 = (k0 + LM_SUB).min(bsz);
9081                        let sb = k1 - k0;
9082                        // Sub-block entirely below the scored range: the KV
9083                        // it just built is all this pass needed from it.
9084                        if pos + k1 <= start {
9085                            k0 = k1;
9086                            continue;
9087                        }
9088                        let mut normed = vec![0.0f32; sb * hs];
9089                        for k in 0..sb {
9090                            let r = inference::rms_norm(
9091                                &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
9092                                &self.weights.final_norm,
9093                                self.rms_eps,
9094                                self.norm_style,
9095                            );
9096                            normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
9097                        }
9098                        let mut logits = vec![0.0f32; sb * rows];
9099                        self.weights
9100                            .lm_head
9101                            .matmat(&normed, sb, &mut logits, self.pool.as_deref());
9102                        for k in 0..sb {
9103                            if pos + k0 + k < start {
9104                                continue;
9105                            }
9106                            self.nll_check_graph("batched score row", pos + k0 + k)?;
9107                            let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
9108                            if let Some(mu) = self.logit_multiplier {
9109                                for v in lg.iter_mut() {
9110                                    *v *= mu;
9111                                }
9112                            }
9113                            // Gemma-class final-logit soft-capping: the
9114                            // decode paths apply it; scoring must too, or
9115                            // the uncapped softmax misprices every token.
9116                            if let Some(c) = self.final_softcap {
9117                                softcap_in_place(lg, c, self.pool.as_deref());
9118                            }
9119                            // Cortiq Embryo hierarchical head: same correction
9120                            // the decode path applies (lm_head_forward).
9121                            if let Some(cm) = self.head_clusters.clone() {
9122                                self.hierarchical_head_logprobs(
9123                                    &normed[k * hs..(k + 1) * hs],
9124                                    &cm,
9125                                    lg,
9126                                );
9127                            }
9128                            let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
9129                            let target = ids[pos + k0 + k + 1] as usize;
9130                            let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9131                            let lse: f64 = lg
9132                                .iter()
9133                                .map(|&v| ((v - max) as f64).exp())
9134                                .sum::<f64>()
9135                                .ln()
9136                                + max as f64;
9137                            nll += lse - lg[target] as f64;
9138                            cnt += 1;
9139                            if std::env::var("CMF_PPL_TRACE").is_ok() {
9140                                let top = lg
9141                                    .iter()
9142                                    .enumerate()
9143                                    .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9144                                    .map(|(i, _)| i)
9145                                    .unwrap_or(0);
9146                                eprintln!(
9147                                    "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9148                                    pos + k0 + k,
9149                                    target,
9150                                    lse - lg[target] as f64,
9151                                    top,
9152                                    lg[target],
9153                                    lg[top]
9154                                );
9155                            }
9156                        }
9157                        k0 = k1;
9158                    }
9159                    pos = end;
9160                }
9161                return Ok((nll, cnt));
9162            }
9163            for pos in 0..ids.len().saturating_sub(1) {
9164                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9165                self.nll_check_graph("serial forward", pos)?;
9166                // Architectures whose head lives inside their own stack return
9167                // the logits out of band and a zero hidden — DeepSeek-V4 folds
9168                // its hyper-connection copies between the last layer and the
9169                // norm, so it cannot hand back a vector this loop could use.
9170                // Scoring the zeros gave a perplexity of exactly the vocabulary
9171                // size, which is a uniform distribution reported as a
9172                // measurement. `generate` already reads this channel.
9173                let out_of_band = self.graph_logits.take();
9174                if self.graph_head_required && out_of_band.is_none() {
9175                    METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9176                    return Err(format!(
9177                        "fused Metal graph head did not complete at NLL position {pos}"
9178                    ));
9179                }
9180                if pos < start {
9181                    continue;
9182                }
9183                let logits = match out_of_band {
9184                    Some(lg) => lg,
9185                    None => {
9186                        let normed = inference::rms_norm(
9187                            &hidden,
9188                            &self.weights.final_norm,
9189                            self.rms_eps,
9190                            self.norm_style,
9191                        );
9192                        // lm_head_forward applies the final-logit softcap itself
9193                        // — capping again here double-squashed gemma-class
9194                        // logits (tanh∘tanh) and reported a flattered ppl.
9195                        self.lm_head_forward(&normed)
9196                    }
9197                };
9198                let target = ids[pos + 1] as usize;
9199                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9200                let lse: f64 = logits
9201                    .iter()
9202                    .map(|&v| ((v - max) as f64).exp())
9203                    .sum::<f64>()
9204                    .ln()
9205                    + max as f64;
9206                let tok_nll = lse - logits[target] as f64;
9207                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9208                    let top = logits
9209                        .iter()
9210                        .enumerate()
9211                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9212                        .map(|(i, _)| i)
9213                        .unwrap_or(0);
9214                    eprintln!(
9215                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9216                        logits[target], logits[top]
9217                    );
9218                }
9219                nll += tok_nll;
9220                cnt += 1;
9221            }
9222            Ok((nll, cnt))
9223        })();
9224        self.nll_end();
9225        result
9226    }
9227
9228    /// Score one post-layer hidden with the same final norm/head path used by
9229    /// decode. Keeping this in one helper is important for the production
9230    /// batch scorer: its rows stop before the final norm, just like the
9231    /// per-position O(1) path below.
9232    fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
9233        let normed = inference::rms_norm(
9234            hidden,
9235            &self.weights.final_norm,
9236            self.rms_eps,
9237            self.norm_style,
9238        );
9239        // lm_head_forward applies the final-logit softcap itself — capping
9240        // again here double-squashed gemma-class logits in earlier scorers.
9241        let mut logits = self.lm_head_forward(&normed);
9242        let target = target as usize;
9243        let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9244        let lse: f64 = logits
9245            .iter()
9246            .map(|&v| ((v - max) as f64).exp())
9247            .sum::<f64>()
9248            .ln()
9249            + max as f64;
9250        let tok_nll = lse - logits[target] as f64;
9251        if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9252            let top = logits
9253                .iter()
9254                .enumerate()
9255                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9256                .map(|(i, _)| i)
9257                .unwrap_or(0);
9258            eprintln!(
9259                "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9260                logits[target], logits[top]
9261            );
9262        }
9263        attention::recycle_buf(&mut logits);
9264        tok_nll
9265    }
9266
9267    /// `CMF_STATE_TRACE`: per-layer magnitude of the recurrent record at
9268    /// position `pos` — the whole `linear_state` (vmf: S then the conv
9269    /// ring; GDN: conv ring then S), its recurrent S part alone, the
9270    /// bounded ring, and the last per-position KV row count.  The tool
9271    /// that separated the ~4k perplexity cliff of the 500-step exports
9272    /// (a state that keeps climbing past the trained window) from a
9273    /// runtime boundary; one line per layer, `STATE pos=… layer=…`.
9274    fn trace_recurrent_state(&self, pos: usize) {
9275        let stats = |v: &[f32]| -> (f64, f64) {
9276            if v.is_empty() {
9277                return (0.0, 0.0);
9278            }
9279            let (mut ss, mut mx) = (0f64, 0f64);
9280            for &x in v {
9281                ss += (x as f64) * (x as f64);
9282                mx = mx.max((x as f64).abs());
9283            }
9284            ((ss / v.len() as f64).sqrt(), mx)
9285        };
9286        for (li, l) in self.kv_cache.layers.iter().enumerate() {
9287            let lw = &self.weights.layers[self.phys_layer(li)];
9288            let (kind, s_len) = match &lw.attn {
9289                AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
9290                AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
9291                AttnKind::Bounded(_) => ("bounded", 0),
9292                AttnKind::Full { .. } => ("full", 0),
9293                _ => ("other", 0),
9294            };
9295            let (rms, max) = stats(&l.linear_state);
9296            let s_part = if kind == "vmf" {
9297                &l.linear_state[..s_len.min(l.linear_state.len())]
9298            } else if kind == "gdn" {
9299                let ring = l
9300                    .linear_state
9301                    .len()
9302                    .saturating_sub(s_len)
9303                    .min(l.linear_state.len());
9304                &l.linear_state[ring..]
9305            } else {
9306                &l.linear_state[..0]
9307            };
9308            let (s_rms, s_max) = stats(s_part);
9309            let (ring_rms, ring_len) = match &l.bounded {
9310                Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
9311                None => (0.0, 0),
9312            };
9313            eprintln!(
9314                "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
9315                 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
9316                l.linear_state.len(),
9317                l.seq_len
9318            );
9319        }
9320    }
9321
9322    /// Teacher-forced NLL of the CONVERTED model: the O(1) Nyström path
9323    /// is ACTIVE over the scored positions. Returns `Ok((nll sum, scored
9324    /// count))` over `prefill..len-1` and surfaces a post-mutation batch
9325    /// failure instead of returning a partial score.
9326    ///
9327    /// Runtime discipline, deliberately NOT the matrix probe's: the
9328    /// requested prefix plus any required deferred lead-in run the exact
9329    /// prompt pass — that pass is what freezes the landmarks and M — and
9330    /// every post-seal scored position goes through `NystromState::step()`,
9331    /// the same code decode runs.
9332    /// So the landmarks are PREFILL-frozen (what ships), not
9333    /// full-sequence oracles (what the published probe measured). When the
9334    /// requested prefix is shorter than the bounded transition, rows in the
9335    /// exact lead-in are still scored so the shifted target range is stable.
9336    ///
9337    /// Pair with `nll_ids_from(ids, prefill)` for the exact baseline
9338    /// over the identical token set — that ratio is the honest one.
9339    pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
9340        // This scorer consumes host hiddens, so never request the optional
9341        // token-graph lm_head side channel. `nll_begin` also consumes a
9342        // prior graph failure and clears only the cancel bit that failure
9343        // raised, leaving a caller-owned cancellation observable.
9344        self.nll_begin()?;
9345        let requested_prefix = (prefill > 0).then_some(prefill);
9346        self.o1_begin_with_prefix(requested_prefix);
9347        let n = ids.len().saturating_sub(1);
9348        let requested_start = prefill.min(n);
9349        // The exact prefix must reach the deferred boundary before a
9350        // collecting layer can convert. Rows between the requested start and
9351        // that boundary remain part of the public NLL range and are scored
9352        // from the same hidden pass below.
9353        let exact_end = if self.o1_active() {
9354            match requested_prefix {
9355                Some(requested) => self.o1_effective_boundary(requested),
9356                None => self
9357                    .o1_cfg
9358                    .as_ref()
9359                    .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
9360            }
9361            .unwrap_or(requested_start)
9362            .min(n)
9363        } else {
9364            requested_start
9365        };
9366        let mut nll = 0f64;
9367        let mut cnt = 0usize;
9368
9369        // Exact prompt pass over ids[..exact_end]: the seal consumes its
9370        // q/k/v. Rows at or after requested_start are scored here when the
9371        // bounded lead-in is longer than the caller's requested prefix.
9372        let mut pos = 0usize;
9373        if self.can_prefill_batched() {
9374            const CHUNK: usize = 128;
9375            while pos < exact_end {
9376                let end = (pos + CHUNK).min(exact_end);
9377                let hiddens = self.prefill_batch(&ids[pos..end], pos);
9378                if self
9379                    .graph_failed
9380                    .swap(false, std::sync::atomic::Ordering::Relaxed)
9381                {
9382                    self.cancel
9383                        .store(false, std::sync::atomic::Ordering::Relaxed);
9384                    self.nll_end();
9385                    return Err("GPU graph failed during O(1) NLL prefix".into());
9386                }
9387                for row in 0..end - pos {
9388                    let score_pos = pos + row;
9389                    if score_pos >= requested_start && score_pos < n {
9390                        nll += self.nll_from_hidden(
9391                            &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
9392                            ids[score_pos + 1],
9393                            score_pos,
9394                        );
9395                        cnt += 1;
9396                    }
9397                }
9398                pos = end;
9399            }
9400        } else {
9401            while pos < exact_end {
9402                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9403                if self
9404                    .graph_failed
9405                    .swap(false, std::sync::atomic::Ordering::Relaxed)
9406                {
9407                    self.cancel
9408                        .store(false, std::sync::atomic::Ordering::Relaxed);
9409                    self.nll_end();
9410                    return Err("GPU graph failed during O(1) NLL prefix".into());
9411                }
9412                if pos >= requested_start && pos < n {
9413                    nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9414                    cnt += 1;
9415                }
9416                pos += 1;
9417            }
9418        }
9419        self.o1_seal_checked().map_err(|err| {
9420            self.nll_end();
9421            err
9422        })?;
9423
9424        // Reuse the production whole-token batch graph for the post-seal
9425        // suffix when the caller explicitly enabled both routes. This is a
9426        // teacher-forced scorer, so every row is ids[pos] and its target is
9427        // ids[pos + 1]; no speculative tail or rollback state is involved.
9428        // A first Declined is safe to handle with the established serial O(1)
9429        // path. Once a chunk completes, however, the device recurrent state
9430        // owns the sequence and a later decline must be terminal rather than
9431        // falling back to stale CPU state.
9432        let batch_k = std::env::var("CMF_BATCH_K")
9433            .ok()
9434            .and_then(|v| v.parse::<usize>().ok())
9435            .unwrap_or(0);
9436        let batch_admitted = batch_k > 0
9437            && self.can_prefill_batched()
9438            && self.o1_active()
9439            && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
9440            && (0..self.num_layers).all(|li| {
9441                let cache = &self.kv_cache.layers[self.phys_layer(li)];
9442                cache.o1.is_none() || cache.o1_views().is_some()
9443            });
9444        if std::env::var("CMF_GRAPH_PROF").is_ok() {
9445            eprintln!(
9446                "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
9447                batch_admitted,
9448                batch_k,
9449                n.saturating_sub(exact_end),
9450            );
9451        }
9452        let mut batch_completed = false;
9453        if batch_admitted && exact_end < n {
9454            let hs = self.hidden_size;
9455            let mut batch_pos = exact_end;
9456            while batch_pos < n {
9457                let end = (batch_pos + batch_k).min(n);
9458                let bk = end - batch_pos;
9459                let mut hiddens = vec![0.0f32; bk * hs];
9460                for (row, &id) in ids[batch_pos..end].iter().enumerate() {
9461                    hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
9462                }
9463                let positions: Vec<usize> = (batch_pos..end).collect();
9464                let t_batch = std::time::Instant::now();
9465                let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
9466                if std::env::var("CMF_GRAPH_PROF").is_ok() {
9467                    let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
9468                    eprintln!(
9469                        "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
9470                        batch_pos,
9471                        end.saturating_sub(1),
9472                        bk as f64 / (ms / 1000.0),
9473                    );
9474                }
9475                if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
9476                    self.nll_end();
9477                    return Err(err);
9478                }
9479                match outcome {
9480                    crate::gpu::BatchGraphOutcome::Completed => {
9481                        batch_completed = true;
9482                        for row in 0..bk {
9483                            nll += self.nll_from_hidden(
9484                                &hiddens[row * hs..(row + 1) * hs],
9485                                ids[batch_pos + row + 1],
9486                                batch_pos + row,
9487                            );
9488                            cnt += 1;
9489                        }
9490                        batch_pos = end;
9491                    }
9492                    crate::gpu::BatchGraphOutcome::Declined => {
9493                        if batch_completed {
9494                            self.nll_end();
9495                            return Err(format!(
9496                                "O(1) NLL batch declined after completed chunk at position {batch_pos}"
9497                            ));
9498                        }
9499                        break;
9500                    }
9501                    crate::gpu::BatchGraphOutcome::Failed => {
9502                        self.nll_end();
9503                        return Err(format!(
9504                            "O(1) NLL batch graph failed after admission at position {batch_pos}"
9505                        ));
9506                    }
9507                }
9508            }
9509            if batch_completed && cnt == n.saturating_sub(requested_start) {
9510                self.nll_end();
9511                return Ok((nll, cnt));
9512            }
9513        }
9514
9515        // Serial O(1) fallback/reference. It is intentionally retained when
9516        // batch admission declines before mutation; callers must label this
9517        // CMF_BATCH_K=0/per-position path separately from the production
9518        // whole-token batch route.
9519        for pos in exact_end..n {
9520            let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9521            if self
9522                .graph_failed
9523                .swap(false, std::sync::atomic::Ordering::Relaxed)
9524            {
9525                self.cancel
9526                    .store(false, std::sync::atomic::Ordering::Relaxed);
9527                self.nll_end();
9528                return Err(format!(
9529                    "GPU graph failed during O(1) NLL serial scoring at position {pos}"
9530                ));
9531            }
9532            nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9533            cnt += 1;
9534        }
9535        self.nll_end();
9536        Ok((nll, cnt))
9537    }
9538
9539    /// Teacher-forced calibration data (B1): for each position, whether the
9540    /// argmax equals the actual next token, and the top-1 softmax prob
9541    /// (top-1 probability) under EACH temperature in `temps` — all from ONE forward
9542    /// pass (argmax/correctness are temperature-invariant; only p_max
9543    /// reshapes). Feeds `cortiq calibrate` (reliability/ECE + temperature
9544    /// fit): is the model's confidence a true property, or does it need a
9545    /// measured scaling?
9546    pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
9547        self.clear_sequence_state();
9548        let n = ids.len().saturating_sub(1);
9549        let mut correct = Vec::with_capacity(n);
9550        let mut pmax = Vec::with_capacity(n);
9551        for pos in 0..n {
9552            let emb = self.embed_single(ids[pos]);
9553            let hidden = self.forward_layers(&emb, pos, None);
9554            let logits = if let Some(logits) = self.graph_logits.take() {
9555                logits
9556            } else {
9557                let normed = inference::rms_norm(
9558                    &hidden,
9559                    &self.weights.final_norm,
9560                    self.rms_eps,
9561                    self.norm_style,
9562                );
9563                // lm_head_forward applies the final-logit softcap itself —
9564                // capping again here double-squashed gemma-class logits
9565                // (tanh∘tanh) and reported a flattered ppl.
9566                self.lm_head_forward(&normed)
9567            };
9568            let target = ids[pos + 1] as usize;
9569            let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
9570            for (i, &v) in logits.iter().enumerate() {
9571                if v > mval {
9572                    mval = v;
9573                    amax = i;
9574                }
9575            }
9576            correct.push(amax == target);
9577            let row: Vec<f32> = temps
9578                .iter()
9579                .map(|&t| {
9580                    let tt = t.max(1e-3);
9581                    let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
9582                    1.0 / s.max(1e-12) // numerator at the max is exp(0)=1
9583                })
9584                .collect();
9585            pmax.push(row);
9586        }
9587        self.clear_sequence_state();
9588        (correct, pmax)
9589    }
9590
9591    /// Teacher-forced PPL with the dynamic router driving per-window
9592    /// skill switches (VMF experiment №2 measurement). Sequential (φ
9593    /// must update per token), returns (ppl, switch_count). The router
9594    /// must be enabled (`enable_dynamic_routing`); else this equals
9595    /// plain `ppl_ids`. The active skill when scoring token t shapes the
9596    /// logits for t+1 — on-policy over the held-out text itself.
9597    pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
9598        if self.dyn_router.is_none() {
9599            return Ok((self.ppl_ids(ids)?, 0));
9600        }
9601        self.nll_begin()?;
9602        let saved_active = self.dyn_active;
9603        let mut router = self
9604            .dyn_router
9605            .take()
9606            .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
9607        router.reset();
9608        self.dyn_phi_seen = 0;
9609        let _ = self.set_active_skill(None);
9610
9611        let result: Result<(f64, usize), String> = (|| {
9612            let mut nll = 0f64;
9613            let mut cnt = 0usize;
9614            for pos in 0..ids.len().saturating_sub(1) {
9615                let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9616                self.nll_check_graph("dynamic serial forward", pos)?;
9617                let out_of_band = self.graph_logits.take();
9618                let mut logits = match out_of_band {
9619                    Some(lg) => lg,
9620                    None => {
9621                        let normed = inference::rms_norm(
9622                            &hidden,
9623                            &self.weights.final_norm,
9624                            self.rms_eps,
9625                            self.norm_style,
9626                        );
9627                        // lm_head_forward applies the final-logit softcap itself —
9628                        // capping again here double-squashed gemma-class logits
9629                        // and reported a flattered ppl.
9630                        self.lm_head_forward(&normed)
9631                    }
9632                };
9633                let target = ids[pos + 1] as usize;
9634                let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9635                let lse: f64 = logits
9636                    .iter()
9637                    .map(|&v| ((v - max) as f64).exp())
9638                    .sum::<f64>()
9639                    .ln()
9640                    + max as f64;
9641                let tok_nll = lse - logits[target] as f64;
9642                if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9643                    let top = logits
9644                        .iter()
9645                        .enumerate()
9646                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9647                        .map(|(i, _)| i)
9648                        .unwrap_or(0);
9649                    eprintln!(
9650                        "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9651                        logits[target], logits[top]
9652                    );
9653                }
9654                nll += tok_nll;
9655                cnt += 1;
9656                attention::recycle_buf(&mut logits);
9657                // Route on the evolving phi (drives the NEXT token's skill).
9658                let phi = self.dyn_phi_ema.clone();
9659                if let Some(new_active) = router.step(&phi, pos) {
9660                    let _ = self.set_active_skill(new_active);
9661                }
9662            }
9663            Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
9664        })();
9665
9666        // Restore the detached router and the active overlay on both success
9667        // and failure. The scoring state is cleared independently below.
9668        let _ = self.set_active_skill(saved_active);
9669        self.dyn_router = Some(router);
9670        self.nll_end();
9671        result
9672    }
9673
9674    /// Routing probe φ (spec §9): mean-pooled hidden after `layer`.
9675    pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
9676        self.clear_sequence_state();
9677        let mut acc = vec![0f32; self.hidden_size];
9678        for (pos, &id) in ids.iter().enumerate() {
9679            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9680            for (a, v) in acc.iter_mut().zip(&h) {
9681                *a += v;
9682            }
9683        }
9684        let n = ids.len().max(1) as f32;
9685        for a in acc.iter_mut() {
9686            *a /= n;
9687        }
9688        self.clear_sequence_state();
9689        acc
9690    }
9691
9692    /// Router-v2 φ probe (spec §9.4, `phi.pool = "span_mean"`): the hidden
9693    /// AFTER `layer` — the same per-position walk and the same quantity as
9694    /// [`Self::probe_phi`] — averaged over the positions in `span` only
9695    /// (the user text between the template's prefix and suffix ids), NOT
9696    /// unit-normalized (the decision normalizes). The walk stops at
9697    /// `span.end`: causality makes the later positions irrelevant, so the
9698    /// result is bit-identical to probing `ids[..span.end]`. An empty span
9699    /// gives the zero vector, which the decision treats as degenerate.
9700    ///
9701    /// Every sequence state is reset before and after — the host KV/ring/
9702    /// recurrent state, the reuse keys (`kv_history`, `kv_prefix`) and the
9703    /// device graph's sequence — so run it on a pipeline that does not
9704    /// also serve a conversation (its prefix reuse would be lost).
9705    pub fn probe_phi_span(
9706        &mut self,
9707        ids: &[u32],
9708        layer: usize,
9709        span: std::ops::Range<usize>,
9710    ) -> Vec<f32> {
9711        #[cfg(target_os = "macos")]
9712        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9713        let end = span.end.min(ids.len());
9714        let start = span.start.min(end);
9715        let reset = |p: &mut Self| p.clear_sequence_state();
9716        reset(self);
9717        let mut acc = vec![0f32; self.hidden_size];
9718        for (pos, &id) in ids[..end].iter().enumerate() {
9719            let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9720            if pos >= start {
9721                for (a, v) in acc.iter_mut().zip(&h) {
9722                    *a += v;
9723                }
9724            }
9725        }
9726        let n = end - start;
9727        if n > 0 {
9728            let n = n as f32;
9729            for a in acc.iter_mut() {
9730                *a /= n;
9731            }
9732        }
9733        reset(self);
9734        acc
9735    }
9736
9737    /// One decode step of the current sequence: forward `token` at
9738    /// `position` (the cache holds positions `[0, position)`, e.g. after
9739    /// [`Self::forward_ids`]) and return the next-token logits — the same
9740    /// forward and head the generation loop runs (resident-graph logits
9741    /// when the graph ran, final norm + lm_head otherwise). The logit-dump
9742    /// tools drive greedy decoding with it so every position is observable.
9743    pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
9744        #[cfg(target_os = "macos")]
9745        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9746        self.graph_logits = None;
9747        let hidden = self.forward_layers(&self.embed_single(token), position, None);
9748        if let Some(logits) = self.graph_logits.take() {
9749            return logits;
9750        }
9751        inference::rms_norm_into(
9752            &hidden,
9753            &self.weights.final_norm,
9754            self.rms_eps,
9755            self.norm_style,
9756            &mut self.ws.n1,
9757        );
9758        self.lm_head_forward(&self.ws.n1)
9759    }
9760
9761    /// Layer-major batched prefill (prefill-GEMM): full-attention —
9762    /// per-position with the existing operators (KV grows naturally,
9763    /// causality preserved), GDN projections / FFN / MoE — batched
9764    /// (a weight row is read from DRAM once per chunk, not per
9765    /// position). Returns the hidden of all positions [b × hidden].
9766    fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
9767        self.prefill_batch_masked(ids, start_pos, None)
9768    }
9769
9770    /// `prefill_batch` with a task mask honored on the dense-FFN panels
9771    /// (the masked-inference fast path: full fused compute, mask lands on
9772    /// the activations). The whole-chunk GPU graph is skipped for masked
9773    /// layers by the callers' arms; the per-GEMM device paths stay in
9774    /// play because the zeroing happens on the host between them.
9775    fn prefill_batch_masked(
9776        &mut self,
9777        ids: &[u32],
9778        start_pos: usize,
9779        task_mask: Option<&TaskMask>,
9780    ) -> Vec<f32> {
9781        self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
9782    }
9783
9784    /// One prompt chunk through the whole stack, post-stack rows out (no
9785    /// final norm) — the ingest generation uses, shared by scoring and
9786    /// `forward_ids` so they measure the same execution: the batched wgpu
9787    /// graph's device prefix plus the host's batched walk for the rest when
9788    /// `batch_prefix_prefill` holds and the graph admits the chunk, else
9789    /// the host's chunked prefill. Err only when a graph that had mutated
9790    /// device state failed.
9791    fn prefill_rows(
9792        &mut self,
9793        ids: &[u32],
9794        pos: usize,
9795        task_mask: Option<&TaskMask>,
9796    ) -> Result<Vec<f32>, String> {
9797        self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
9798    }
9799
9800    fn prefill_input_rows(
9801        &mut self,
9802        input: PrefillIn<'_>,
9803        pos: usize,
9804        task_mask: Option<&TaskMask>,
9805    ) -> Result<Vec<f32>, String> {
9806        self.mimo_moe_prepare();
9807        let hs = self.hidden_size;
9808        let bk = match input {
9809            PrefillIn::Ids(ids) => ids.len(),
9810            PrefillIn::Hidden(rows) => rows.len() / hs,
9811        };
9812        #[cfg(not(target_os = "macos"))]
9813        if task_mask.is_none()
9814            && !self.o1_active()
9815            && bk > 1
9816            && (self.batch_prefix_prefill()
9817                || (self.verify_exact_moe
9818                    && crate::gpu::enabled_here()
9819                    && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
9820        {
9821            let mut hiddens = match input {
9822                PrefillIn::Hidden(rows) => rows.to_vec(),
9823                PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
9824            };
9825            let positions: Vec<usize> = (pos..pos + bk).collect();
9826            let mut run = 0usize;
9827            match self.try_batch_graph_wgpu_prefix(
9828                &mut hiddens,
9829                &positions,
9830                bk,
9831                None,
9832                Some(&mut run),
9833            ) {
9834                crate::gpu::BatchGraphOutcome::Completed => {
9835                    let out = if run < self.num_layers {
9836                        self.prefill_batch_span(
9837                            PrefillIn::Hidden(&hiddens),
9838                            pos,
9839                            None,
9840                            run,
9841                            self.num_layers,
9842                        )
9843                    } else {
9844                        hiddens
9845                    };
9846                    return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9847                        Err("MiMo attention graph failed after admission".into())
9848                    } else {
9849                        Ok(out)
9850                    };
9851                }
9852                crate::gpu::BatchGraphOutcome::Failed => {
9853                    return Err("batched prefix prefill failed after admission".into());
9854                }
9855                crate::gpu::BatchGraphOutcome::Declined => {
9856                    // Rows an earlier chunk left on the device only.
9857                    #[cfg(feature = "gpu")]
9858                    self.pull_lagging_host_kv(0, self.num_layers, pos);
9859                }
9860            }
9861        }
9862        let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
9863        if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9864            Err("batch tail graph failed after admission".into())
9865        } else {
9866            Ok(out)
9867        }
9868    }
9869
9870    /// The layer-major batched walk over a layer span [from..upto_excl):
9871    /// the whole prefill machinery (chunk graph, batched attends, GEMM
9872    /// panels) for a PARTIAL stack — the network split's prefill rides
9873    /// the same canon as the local one. Input is token ids (embeds
9874    /// itself, coordinator side) or ready boundary hiddens (worker side).
9875    fn prefill_batch_span(
9876        &mut self,
9877        input: PrefillIn<'_>,
9878        start_pos: usize,
9879        task_mask: Option<&TaskMask>,
9880        from: usize,
9881        upto_excl: usize,
9882    ) -> Vec<f32> {
9883        let hs = self.hidden_size;
9884        let b = match input {
9885            PrefillIn::Ids(ids) => ids.len(),
9886            PrefillIn::Hidden(hb) => hb.len() / hs,
9887        };
9888        let upto_excl = upto_excl.min(self.num_layers);
9889        // The CPU embed is deferred: when the chunk graph takes the run
9890        // from layer 0 it gathers the embeddings on the device instead.
9891        // A hidden input is ready by definition.
9892        let mut h: Vec<f32>;
9893        let mut h_ready;
9894        match input {
9895            PrefillIn::Ids(_) => {
9896                h = vec![0.0; b * hs];
9897                h_ready = false;
9898            }
9899            PrefillIn::Hidden(hb) => {
9900                h = hb.to_vec();
9901                h_ready = true;
9902            }
9903        }
9904        let fill_h = |h: &mut Vec<f32>, me: &Self| {
9905            if let PrefillIn::Ids(ids) = input {
9906                for (bi, &id) in ids.iter().enumerate() {
9907                    let e = me.embed_single(id);
9908                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
9909                }
9910                if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9911                    if let Ok(t) = tp.parse::<usize>() {
9912                        if t >= start_pos && t < start_pos + ids.len() {
9913                            let bi = t - start_pos;
9914                            let row = &h[bi * hs..(bi + 1) * hs];
9915                            let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
9916                            eprintln!(
9917                                "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
9918                                ids[bi],
9919                                row[0],
9920                                row[1],
9921                                ids.len(),
9922                                &ids[..ids.len().min(8)]
9923                            );
9924                        }
9925                    }
9926                }
9927            }
9928        };
9929        let (_nkv, _hd, _rd, eps) = (
9930            self.num_kv_heads,
9931            self.head_dim,
9932            self.rotary_dim,
9933            self.rms_eps,
9934        );
9935        let pool = self.pool.clone();
9936        let norm_style = self.norm_style;
9937        self.mimo_moe_prepare();
9938        let automatic_gpu_prefix = self.automatic_gpu_prefix();
9939
9940        #[cfg(target_os = "macos")]
9941        let mut chunk_skip_until = 0usize;
9942        for li in from..upto_excl {
9943            let _capacity_tail = automatic_gpu_prefix
9944                .filter(|&prefix| {
9945                    li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
9946                })
9947                .map(|_| crate::gpu::enter_cpu_scope());
9948            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU
9949            // Spark-X2.5: the layer's int8 matrix-unit GEMMs take the
9950            // `zi_mm` tile, bit-identical to the 64x64 kernel and about
9951            // three times its rate (`gpu_wgpu::zi_gemm_route`).
9952            #[cfg(feature = "gpu")]
9953            let _fast_gemm = self
9954                .proj_gate_sigmoid
9955                .then(crate::gpu::enter_prefill_fast_gemm);
9956            // GPU chunk graph (default-on under CMF_GPU=1): a run of
9957            // consecutive eligible layers for the whole chunk in ONE
9958            // Metal submission — norm, QKV, RoPE with fused mirror
9959            // append, causal attend, O, FFN, hidden device-resident
9960            // across the run. Any refusal falls through to the CPU path.
9961            #[cfg(target_os = "macos")]
9962            if task_mask.is_none() {
9963                if li < chunk_skip_until {
9964                    continue;
9965                }
9966                // Device-side embedding needs a q8_row embedding matrix;
9967                // with any other layout the CPU fills `h` first and the
9968                // graph starts from a ready hidden (refusing the whole
9969                // run over the embedding alone kept q4t models — the
9970                // whole Nanbeige/Bonsai class — on the CPU prefill).
9971                if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
9972                    fill_h(&mut h, self);
9973                    h_ready = true;
9974                }
9975                let ids_for_embed = match input {
9976                    PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
9977                    PrefillIn::Hidden(_) => None,
9978                };
9979                let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
9980                if end > li {
9981                    h_ready = true;
9982                    chunk_skip_until = end;
9983                    // Looped Transformer: the graph stopped at a loop
9984                    // boundary — apply final norm before the next iteration.
9985                    if self.is_loop_end(end - 1) && end < self.num_layers {
9986                        for bi in 0..b {
9987                            let normed = inference::rms_norm(
9988                                &h[bi * hs..(bi + 1) * hs],
9989                                &self.weights.final_norm,
9990                                eps,
9991                                norm_style,
9992                            );
9993                            h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
9994                        }
9995                    }
9996                    continue;
9997                }
9998            }
9999            if !h_ready {
10000                fill_h(&mut h, self);
10001                h_ready = true;
10002            }
10003            if task_mask.is_none() && self.verify_exact_moe {
10004                let positions: Vec<_> = (start_pos..start_pos + b).collect();
10005                match self.mimo_graph_layer_rows(li, &mut h, &positions) {
10006                    crate::gpu::BatchGraphOutcome::Completed => continue,
10007                    crate::gpu::BatchGraphOutcome::Failed => return h,
10008                    crate::gpu::BatchGraphOutcome::Declined => {}
10009                }
10010            }
10011            #[cfg(feature = "gpu")]
10012            self.pull_lagging_host_kv(li, li + 1, start_pos);
10013            let lw = &self.weights.layers[self.phys_layer(li)];
10014            let t_attn = std::time::Instant::now();
10015            // ── attention ──
10016            match &lw.attn {
10017                AttnKind::Kda(w) => {
10018                    // Projections batched, recurrence sequential.
10019                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10020                    let mut normed = vec![0.0f32; b * hs];
10021                    for bi in 0..b {
10022                        inference::rms_norm_into(
10023                            &h[bi * hs..(bi + 1) * hs],
10024                            &lw.input_norm,
10025                            eps,
10026                            norm_style,
10027                            &mut normed[bi * hs..(bi + 1) * hs],
10028                        );
10029                    }
10030                    let attn = crate::linear_core::kda_forward_batch(
10031                        &normed,
10032                        b,
10033                        w,
10034                        &cfg,
10035                        &mut self.kv_cache.layers[li].linear_state,
10036                        pool.as_deref(),
10037                    );
10038                    for (dst, &a) in h.iter_mut().zip(&attn) {
10039                        *dst += a;
10040                    }
10041                }
10042                AttnKind::LinearGdn(w) => {
10043                    // Projections batched, recurrence sequential.
10044                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10045                    let mut normed = vec![0.0f32; b * hs];
10046                    for bi in 0..b {
10047                        let r = inference::rms_norm(
10048                            &h[bi * hs..(bi + 1) * hs],
10049                            &lw.input_norm,
10050                            eps,
10051                            norm_style,
10052                        );
10053                        normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10054                    }
10055                    let attn = crate::linear_core::gdn_forward_batch(
10056                        &normed,
10057                        b,
10058                        w,
10059                        &cfg,
10060                        &mut self.kv_cache.layers[li].linear_state,
10061                        pool.as_deref(),
10062                    );
10063                    for (dst, &a) in h.iter_mut().zip(&attn) {
10064                        *dst += a;
10065                    }
10066                }
10067                AttnKind::ShortConv(w) => {
10068                    // Projections batched over the chunk; the conv walks the
10069                    // contiguous positions in order (same ring as decode).
10070                    let cfg = self
10071                        .short_conv_cfg
10072                        .expect("short-conv layer without short_conv_cfg");
10073                    let mut normed = vec![0.0f32; b * hs];
10074                    for bi in 0..b {
10075                        inference::rms_norm_into(
10076                            &h[bi * hs..(bi + 1) * hs],
10077                            &lw.input_norm,
10078                            eps,
10079                            norm_style,
10080                            &mut normed[bi * hs..(bi + 1) * hs],
10081                        );
10082                    }
10083                    let attn = short_conv_forward_batch(
10084                        &normed,
10085                        b,
10086                        w,
10087                        &cfg,
10088                        &mut self.kv_cache.layers[li].linear_state,
10089                        pool.as_deref(),
10090                    );
10091                    for (dst, &a) in h.iter_mut().zip(&attn) {
10092                        *dst += a;
10093                    }
10094                }
10095                AttnKind::Mla(w) => {
10096                    // Per-position prefill (correctness first; latent
10097                    // batching is a later optimization).
10098                    let inv_freq_l = self.layer_inv_freq(li);
10099                    let rs = self.layer_rope_scale(li);
10100                    let mut normed = vec![0.0f32; hs];
10101                    for bi in 0..b {
10102                        inference::rms_norm_into(
10103                            &h[bi * hs..(bi + 1) * hs],
10104                            &lw.input_norm,
10105                            eps,
10106                            norm_style,
10107                            &mut normed,
10108                        );
10109                        let ao = mla_attention(
10110                            w,
10111                            &normed,
10112                            &mut self.kv_cache.layers[li],
10113                            start_pos + bi,
10114                            &inv_freq_l,
10115                            rs,
10116                            eps,
10117                            pool.as_deref(),
10118                        );
10119                        for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
10120                            *dst += a;
10121                        }
10122                    }
10123                }
10124                AttnKind::Full {
10125                    wq,
10126                    wk,
10127                    wv,
10128                    wo,
10129                    q_norm,
10130                    k_norm,
10131                    output_gate,
10132                    softplus_gate,
10133                    bias,
10134                } => {
10135                    // Chunk-GEMM QKV/O; per-position causal attention
10136                    // inside (roadmap §3 P0 — full-attention prefill no
10137                    // longer re-reads the projection weights b times).
10138                    let mut normed = vec![0.0f32; b * hs];
10139                    for bi in 0..b {
10140                        inference::rms_norm_into(
10141                            &h[bi * hs..(bi + 1) * hs],
10142                            &lw.input_norm,
10143                            eps,
10144                            norm_style,
10145                            &mut normed[bi * hs..(bi + 1) * hs],
10146                        );
10147                    }
10148                    let inv_freq_l = self.layer_inv_freq(li);
10149                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10150                    let cfg = QwenAttnCfg {
10151                        num_heads: self.layer_num_heads(li),
10152                        num_kv_heads: nkv_l,
10153                        head_dim: hd_l,
10154                        hidden_size: hs,
10155                        position: start_pos,
10156                        inv_freq: &inv_freq_l,
10157                        rotary_dim: rd_l,
10158                        scale: self.attn_scale,
10159                        softcap: self.attn_softcap,
10160                        window: self.layer_window(li),
10161                        v_norm: self.attn_v_norm,
10162                        qk_norm_after_rope: self.qk_norm_after_rope,
10163                        gate_sigmoid: self.proj_gate_sigmoid,
10164                        q_norm: q_norm.as_deref(),
10165                        k_norm: k_norm.as_deref(),
10166                        output_gate: *output_gate,
10167                        softplus_gate: softplus_gate
10168                            .as_ref()
10169                            .map(|(gate, per_head)| (gate, *per_head)),
10170                        rope_scale: self.layer_rope_scale(li),
10171                        bias: bias
10172                            .as_ref()
10173                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10174                        rms_eps: eps,
10175                        norm_style,
10176                        pool: pool.as_deref(),
10177                        v_head_dim: self.layer_v_dim(li),
10178                    };
10179                    #[cfg(feature = "gpu")]
10180                    let _mirror = self
10181                        .prefill_mirror_target(li)
10182                        .map(crate::gpu::enter_prefill_mirror);
10183                    let mut attn = attention::qwen_attention_batch(
10184                        &normed,
10185                        b,
10186                        wq,
10187                        wk,
10188                        wv,
10189                        wo,
10190                        &mut self.kv_cache.layers[li],
10191                        &cfg,
10192                    );
10193                    if let Some(w) = &lw.attn_out_norm {
10194                        for bi in 0..b {
10195                            inference::rms_norm_into(
10196                                &attn[bi * hs..(bi + 1) * hs],
10197                                w,
10198                                eps,
10199                                norm_style,
10200                                &mut normed[bi * hs..(bi + 1) * hs],
10201                            );
10202                        }
10203                        attn.copy_from_slice(&normed);
10204                    }
10205                    for (dst, &a) in h.iter_mut().zip(&attn) {
10206                        *dst += a;
10207                    }
10208                }
10209                AttnKind::Bounded(w) => {
10210                    // Chunk-GEMM projections, the bounded operator per
10211                    // position over ring + chunk — never a growing KV.
10212                    let mut normed = vec![0.0f32; b * hs];
10213                    for bi in 0..b {
10214                        inference::rms_norm_into(
10215                            &h[bi * hs..(bi + 1) * hs],
10216                            &lw.input_norm,
10217                            eps,
10218                            norm_style,
10219                            &mut normed[bi * hs..(bi + 1) * hs],
10220                        );
10221                    }
10222                    let rope = self
10223                        .bounded_rope
10224                        .clone()
10225                        .expect("bounded layer without an installed rotation table");
10226                    let cfg = crate::bounded::BoundedAttnCfg {
10227                        num_heads: self.num_heads,
10228                        num_kv_heads: self.num_kv_heads,
10229                        head_dim: self.head_dim,
10230                        hidden_size: hs,
10231                        scale: self.attn_scale,
10232                        rope: &rope,
10233                        pool: pool.as_deref(),
10234                    };
10235                    let mut attn = crate::bounded::bounded_attention_batch(
10236                        &normed,
10237                        b,
10238                        w,
10239                        &mut self.kv_cache.layers[li],
10240                        &cfg,
10241                    );
10242                    if let Some(wn) = &lw.attn_out_norm {
10243                        for bi in 0..b {
10244                            inference::rms_norm_into(
10245                                &attn[bi * hs..(bi + 1) * hs],
10246                                wn,
10247                                eps,
10248                                norm_style,
10249                                &mut normed[bi * hs..(bi + 1) * hs],
10250                            );
10251                        }
10252                        attn.copy_from_slice(&normed);
10253                    }
10254                    for (dst, &a) in h.iter_mut().zip(&attn) {
10255                        *dst += a;
10256                    }
10257                    attention::recycle_buf(&mut attn);
10258                }
10259                AttnKind::Linear(w) => {
10260                    for bi in 0..b {
10261                        let normed = inference::rms_norm(
10262                            &h[bi * hs..(bi + 1) * hs],
10263                            &lw.input_norm,
10264                            eps,
10265                            norm_style,
10266                        );
10267                        vmf_phase_forward(
10268                            &normed,
10269                            w,
10270                            &self.vmf_cfg.expect("linear layer without vmf_cfg"),
10271                            &mut self.kv_cache.layers[li].linear_state,
10272                            pool.as_deref(),
10273                        )
10274                        .iter()
10275                        .enumerate()
10276                        .for_each(|(i, &a)| h[bi * hs + i] += a);
10277                    }
10278                }
10279            }
10280
10281            // ── FFN batched ──
10282            let lw = &self.weights.layers[self.phys_layer(li)];
10283            let mut post = vec![0.0f32; b * hs];
10284            for bi in 0..b {
10285                let r =
10286                    inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
10287                post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10288            }
10289            // A restrictive per-visit FFN row lands on the activations
10290            // inside the dense arm; an all-open row costs nothing.
10291            let mask_row = task_mask
10292                .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
10293                .and_then(|m| m.ffn_masks.get(li))
10294                .map(|v| v.as_slice());
10295            let attn_ns = t_attn.elapsed().as_nanos() as u64;
10296            let t_ffn = std::time::Instant::now();
10297            let mut ffn = match &lw.ffn {
10298                FfnKind::Dense(d) if !d.segs.is_empty() => {
10299                    tube_ffn(d, &post, b, pool.as_deref(), mask_row)
10300                }
10301                FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
10302                FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
10303                    moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
10304                }
10305                FfnKind::Moe(m) if self.verify_exact_moe => {
10306                    moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
10307                }
10308                // Keep prompt expert panels off the projection arena and
10309                // use their routes to prime the model-wide bank.
10310                FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
10311                    let before = m.stats.borrow().clone();
10312                    let out = crate::gpu::cpu_scope(|| {
10313                        moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
10314                    });
10315                    self.mimo_moe.prime(li, m, &before);
10316                    out
10317                }
10318                FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
10319                // Dual-branch layers run per position (the expert branch
10320                // reads the raw residual — nothing to batch yet).
10321                FfnKind::DenseMoe(dm) => {
10322                    let mut out = vec![0.0f32; b * hs];
10323                    for bi in 0..b {
10324                        let r = dense_moe_ffn(
10325                            dm,
10326                            &post[bi * hs..(bi + 1) * hs],
10327                            &h[bi * hs..(bi + 1) * hs],
10328                            eps,
10329                            norm_style,
10330                            pool.as_deref(),
10331                        );
10332                        out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10333                    }
10334                    out
10335                }
10336            };
10337            if prefill_prof_on() {
10338                PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
10339                PREFILL_SPLIT[1].fetch_add(
10340                    t_ffn.elapsed().as_nanos() as u64,
10341                    std::sync::atomic::Ordering::Relaxed,
10342                );
10343            }
10344            if let Some(w) = &lw.ffn_out_norm {
10345                for bi in 0..b {
10346                    inference::rms_norm_into(
10347                        &ffn[bi * hs..(bi + 1) * hs],
10348                        w,
10349                        eps,
10350                        norm_style,
10351                        &mut post[bi * hs..(bi + 1) * hs],
10352                    );
10353                }
10354                ffn.copy_from_slice(&post);
10355            }
10356            for (dst, &f) in h.iter_mut().zip(&ffn) {
10357                *dst += f;
10358            }
10359            if let Some(sc) = lw.layer_scale {
10360                for v in h.iter_mut() {
10361                    *v *= sc;
10362                }
10363            }
10364            // CMF_LAYER_DUMP: every position's hidden after layer li.
10365            if self.layer_dump.is_some() {
10366                for bi in 0..b {
10367                    self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
10368                }
10369            }
10370            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10371                if let Ok(t) = tp.parse::<usize>() {
10372                    if t >= start_pos && t < start_pos + b {
10373                        let bi = t - start_pos;
10374                        let row = &h[bi * hs..(bi + 1) * hs];
10375                        let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10376                        eprintln!(
10377                            "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10378                            row[0], row[1]
10379                        );
10380                    }
10381                }
10382            }
10383            // CMF_DEBUG_LAYERS=1: per-layer hidden-state health of the
10384            // LAST prompt position — the knife for "which layer type
10385            // breaks first" on a new architecture.
10386            if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
10387                let row = &h[(b - 1) * hs..b * hs];
10388                let rms =
10389                    (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
10390                let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
10391                eprintln!(
10392                    "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
10393                    match &self.weights.layers[self.phys_layer(li)].attn {
10394                        AttnKind::LinearGdn(_) => "gdn",
10395                        AttnKind::Linear(_) => "vmf",
10396                        AttnKind::ShortConv(_) => "conv",
10397                        _ => "attn",
10398                    },
10399                    match &lw.ffn {
10400                        FfnKind::Moe(_) => "moe",
10401                        FfnKind::Dense(_) => "dense",
10402                        FfnKind::DenseMoe(_) => "dense+moe",
10403                    },
10404                );
10405            }
10406            // Looped Transformer: apply final norm at the end of each loop iteration.
10407            if self.is_loop_end(li) && li + 1 < self.num_layers {
10408                for bi in 0..b {
10409                    let normed = inference::rms_norm(
10410                        &h[bi * hs..(bi + 1) * hs],
10411                        &self.weights.final_norm,
10412                        eps,
10413                        norm_style,
10414                    );
10415                    h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10416                }
10417            }
10418            if std::env::var("CMF_TRACE_H").is_ok() {
10419                let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
10420                let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
10421                eprintln!(
10422                    "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
10423                    lw.layer_scale
10424                );
10425            }
10426        }
10427        crate::gpu::set_layer(-1); // lm_head/final ops outside layer-split
10428        if prefill_prof_on() {
10429            let ms = |a: &std::sync::atomic::AtomicU64| {
10430                a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
10431            };
10432            let sp = &attention::ATTN_SPLIT;
10433            let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
10434            eprintln!(
10435                "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
10436                 host gate {:.1}], host loop {:.1}, attend {:.1} \
10437                 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
10438                 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
10439                ms(&PREFILL_SPLIT[0]),
10440                ms(&sp[0]),
10441                qkv_enq,
10442                qkv_wait,
10443                ms(&sp[6]),
10444                ms(&sp[1]),
10445                ms(&sp[2]),
10446                ms(&sp[4]),
10447                ms(&sp[5]),
10448                ca_wait,
10449                ms(&sp[3]),
10450                ms(&PREFILL_SPLIT[1]),
10451            );
10452            let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
10453            eprintln!(
10454                "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
10455                 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
10456                 fallbacks (cumulative)",
10457                attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
10458            );
10459        }
10460        // A batched span owns a complete set of positions. Publish any
10461        // collecting→sealed transition only after every layer has finished;
10462        // callers that cross into serial/device work must see the new epoch
10463        // before this function returns.
10464        self.o1_progress();
10465        // Every layer of the chunk has appended and attended its rows.
10466        self.swa_trim_tails();
10467        h
10468    }
10469
10470    /// Embed a single token.
10471    fn embed_single(&self, id: u32) -> Vec<f32> {
10472        let mut out = vec![0.0f32; self.hidden_size];
10473        if (id as usize) < self.weights.embed_tokens.rows() {
10474            self.weights.embed_tokens.row_f32(id as usize, &mut out);
10475        }
10476        if self.embed_multiplier != 1.0 {
10477            for v in out.iter_mut() {
10478                *v *= self.embed_multiplier;
10479            }
10480        }
10481        // DeepSeek-V4's hash layers route by TOKEN ID, so the id has to
10482        // reach the forward. It rides in slot 0 (the forward re-reads the
10483        // real embedding itself from the table).
10484        if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
10485            let mut v = vec![0.0f32; self.hidden_size.max(1)];
10486            v[0] = id as f32;
10487            return v;
10488        }
10489        // Gemma-3n: the per-layer-embedding half needs the token ID, so
10490        // it rides appended to the embedding; the g3n forward splits it.
10491        if let Some(b) = &self.g3n {
10492            return b.0.extend_embedding(id, &out, self.pool.as_deref());
10493        }
10494        out
10495    }
10496
10497    /// A run of consecutive prefill layers on the GPU for the whole
10498    /// chunk (default-on under CMF_GPU=1; CMF_GPU_CHUNK=0 disables).
10499    /// Eligibility per layer: q8_row weights, plain full attention
10500    /// (no output gate), F32 KV, no o1/masks/gemma extras. Returns the
10501    /// first layer index NOT processed (== `li0` when the run is empty).
10502    #[cfg(target_os = "macos")]
10503    fn chunk_run_gpu(
10504        &mut self,
10505        li0: usize,
10506        h: &mut [f32],
10507        b: usize,
10508        pos0: usize,
10509        embed_ids: Option<&[u32]>,
10510        cap: usize,
10511    ) -> usize {
10512        // (The old streaming attend needed a depth bound at ~1k; the
10513        // GEMM attention scales like the CPU path and lifted it.)
10514        // CMF_GPU_CHUNK=0 disables the graph.
10515        // Gemma-4 (`metal_gemma4`) brings its own kernels for the global
10516        // layers, V norm, attention scale and sandwich (`ChunkGemma4`).
10517        // `CMF_METAL_GEMMA4_CHUNK=0` keeps its prompt on the host walk.
10518        let gemma4 =
10519            self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
10520        if !crate::gpu::enabled_here()
10521            || std::env::var("CMF_GPU_CHUNK")
10522                .map(|v| v == "0")
10523                .unwrap_or(false)
10524            || b < 32
10525            // Sliding windows with per-layer RoPE ride the chunk graph
10526            // (`metal_graph_swa`: causal_softmax_win, per-layer tables).
10527            || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
10528            || (self.global_attn.is_some() && !gemma4)
10529            // Chunk attention has no value-less learned-sink column. q1
10530            // decode can carry it through ordinary `gqa_attend`, but prompt
10531            // chunks must stay on the exact host attention path.
10532            || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10533            // Per-layer KV heads and narrow V remain outside this chunk
10534            // graph (the q1 sliding-native door checks the same geometry).
10535            || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
10536            // Collection owns the exact Q trace and boundary conversion;
10537            // this chunk graph appends dense KV without feeding that trace.
10538            || self.o1_active()
10539            || (self.attn_v_norm && !gemma4)
10540            || !self.metal_rope_scale_ok()
10541            || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
10542        {
10543            return li0;
10544        }
10545        let Some(model) = self.model.clone() else {
10546            return li0;
10547        };
10548        let (nh, nkv, hd, hs) = (
10549            self.num_heads,
10550            self.num_kv_heads,
10551            self.head_dim,
10552            self.hidden_size,
10553        );
10554        // Collect the longest run of consecutive eligible layers.
10555        // Looped Transformer: stop at the loop boundary so the CPU can
10556        // apply loop_final_norm between iterations.
10557        let loop_end = if self.loop_final_norm {
10558            ((li0 / self.physical_layers) + 1) * self.physical_layers
10559        } else {
10560            self.num_layers
10561        };
10562        let run_end = self.num_layers.min(loop_end).min(cap);
10563        // Each layer's own RoPE table (Spark-X2.5: sliding and full layers
10564        // differ); the global one for every model without sliding layers.
10565        // Declared before `layers`, which borrows it (and drops a routing
10566        // closure on MoE layers).
10567        let tables: Vec<std::sync::Arc<Vec<f32>>> =
10568            (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
10569        let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
10570        let mut stored_at: Vec<usize> = Vec::new();
10571        for li in li0..run_end {
10572            let lw = &self.weights.layers[self.phys_layer(li)];
10573            if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
10574                && !gemma4
10575            {
10576                break;
10577            }
10578            let AttnKind::Full {
10579                wq,
10580                wk,
10581                wv,
10582                wo,
10583                q_norm,
10584                k_norm,
10585                output_gate: false,
10586                softplus_gate,
10587                bias,
10588            } = &lw.attn
10589            else {
10590                break;
10591            };
10592            // Spark-X2.5's per-head sigmoid g_proj gate, from f32 rows.
10593            let head_gate = match softplus_gate {
10594                None => None,
10595                Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
10596                    Some((d, r, c)) if r == nh && c == hs => Some(d),
10597                    _ => break,
10598                },
10599                Some(_) => break,
10600            };
10601            // The FFN: a dense SiLU/GELU trio, or routed q4tp experts
10602            // (Mellum2.1: softmax router, no shared expert) whose first
10603            // trio stands in for the shape checks.
10604            let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
10605                FfnKind::Dense(d)
10606                    if (matches!(d.act, Act::Silu | Act::Gelu)
10607                        || (gemma4 && d.act == Act::GeluTanh))
10608                        && d.segs.is_empty() =>
10609                {
10610                    (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
10611                }
10612                FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
10613                    Some((router, experts, q8)) => {
10614                        let e0 = &m.experts[0];
10615                        let route = Box::new(move |lg: &[f32]| {
10616                            // `moe_ffn_batch`'s routing and weights, row by row.
10617                            let (idx, p, wsum) = moe_route(lg, m, None);
10618                            let mut st = m.stats.borrow_mut();
10619                            if st.len() < m.experts.len() {
10620                                st.resize(m.experts.len(), 0);
10621                            }
10622                            for &e in &idx {
10623                                st[e] += 1;
10624                            }
10625                            let w = idx.iter().map(|&e| p[e] / wsum).collect();
10626                            (idx, w)
10627                        });
10628                        (
10629                            &e0.gate_proj,
10630                            &e0.up_proj,
10631                            &e0.down_proj,
10632                            Act::Silu,
10633                            Some(crate::gpu_metal::ChunkMoe {
10634                                router,
10635                                experts,
10636                                q8,
10637                                route,
10638                            }),
10639                        )
10640                    }
10641                    None => break,
10642                },
10643                _ => break,
10644            };
10645            // q8_row (row_scale populated), q8_2f (plus its input field),
10646            // or q4_tiled / q4tp (row_scale empty — their scales are in the
10647            // payload). Mixing across the seven projections of one layer is
10648            // fine; the encoder branches per weight on the tensor's dtype.
10649            // Anything else refuses.
10650            fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
10651                t.q8_row_parts()
10652                    .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
10653                    .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10654                    .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10655            }
10656            fn cf(t: &QTensor) -> &[f32] {
10657                t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
10658            }
10659            let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
10660            // q8_2f projections ride the chunk graph on the MoE layers it
10661            // was measured on (Mellum2.1); dense q8_2f stacks keep the host
10662            // walk until they are measured too. That includes a q8_2f
10663            // gate/up/down behind q4/q8_row attention: the chunk FFN has no
10664            // x·col staging, so it would run those weights without their
10665            // input field (0.8.14 refused every q8_2f weight here).
10666            // Gemma-4 reads every q8_2f weight through the blob GEMM with its
10667            // own field (`ChunkGemma4`), so its dense q8_2f stack rides.
10668            if moe.is_none()
10669                && !gemma4
10670                && (col.iter().any(|c| !c.is_empty())
10671                    || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
10672            {
10673                break;
10674            }
10675            let parts = (
10676                cw(wq),
10677                cw(wk),
10678                cw(wv),
10679                cw(wo),
10680                cw(gate_t),
10681                cw(up_t),
10682                cw(down_t),
10683            );
10684            let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
10685            else {
10686                break;
10687            };
10688            let layer = &self.kv_cache.layers[li];
10689            if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
10690                break;
10691            }
10692            // A trimmed tail holds every row the chunk's first query reads.
10693            debug_assert!(
10694                layer.base() == 0
10695                    || self
10696                        .layer_window(li)
10697                        .is_some_and(|w| layer.head_len(0) + 1 >= w)
10698            );
10699            stored_at.push(layer.head_len(0));
10700            // Per-layer geometry for Gemma-4 (512-wide global layers over
10701            // one KV head); every other model keeps the pipeline-wide one.
10702            let (nkv, hd) = if gemma4 {
10703                let (a, bb, _) = self.layer_geom(li);
10704                (a, bb)
10705            } else {
10706                (nkv, hd)
10707            };
10708            layers.push(crate::gpu_metal::ChunkLayer {
10709                model: &model,
10710                kv_id: self.graph_kv_id,
10711                layer: li,
10712                wq: pq,
10713                wk: pk,
10714                wv: pv,
10715                wo: po,
10716                gate: pg,
10717                up: pu,
10718                down: pd,
10719                input_norm: &lw.input_norm,
10720                post_norm: &lw.post_norm,
10721                bias: bias
10722                    .as_ref()
10723                    .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
10724                q_norm: q_norm.as_deref(),
10725                k_norm: k_norm.as_deref(),
10726                inv_freq: &tables[li - li0],
10727                rope_scale: self.layer_rope_scale(li),
10728                rd: self.layer_geom(li).2,
10729                nh,
10730                nkv,
10731                hd,
10732                hs,
10733                inter: gate_t.rows(),
10734                gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
10735                late_qk_norm: self.qk_norm_after_rope,
10736                eps: self.rms_eps as f32,
10737                window: self.layer_window(li),
10738                head_gate,
10739                act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
10740                g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
10741                    v_norm: self.attn_v_norm,
10742                    scale: self.attn_scale,
10743                    attn_out_norm: lw.attn_out_norm.as_deref(),
10744                    ffn_out_norm: lw.ffn_out_norm.as_deref(),
10745                    layer_scale: lw.layer_scale,
10746                }),
10747                col,
10748                moe,
10749            });
10750        }
10751        if layers.is_empty() {
10752            return li0;
10753        }
10754        // KV row width per layer (uniform but for Gemma-4).
10755        let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
10756        let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
10757            .iter()
10758            .zip(&rows_kv)
10759            .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
10760            .collect();
10761        let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
10762        for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
10763            let li = layers[i].layer;
10764            let layer = &self.kv_cache.layers[li];
10765            let nkv = rows_kv[i].0;
10766            io.push(crate::gpu_metal::ChunkIo {
10767                cpu_stored: stored_at[i],
10768                cpu_gen: layer.generation(),
10769                cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
10770                cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
10771                out_k: ok,
10772                out_v: ov,
10773                imp: oi,
10774            });
10775        }
10776        let n_run = layers.len();
10777        let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
10778        // Device-side embedding when the run starts the model and the
10779        // embedding matrix is q8_row-mapped.
10780        let ep = embed_ids.and_then(|ids| {
10781            self.weights
10782                .embed_tokens
10783                .q8_row_parts()
10784                .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
10785                    idx,
10786                    rows,
10787                    row_scale: rs,
10788                    ids,
10789                    mult: self.embed_multiplier,
10790                })
10791        });
10792        if embed_ids.is_some() && ep.is_none() {
10793            return li0;
10794        }
10795        if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
10796            return li0;
10797        }
10798        drop(io);
10799        drop(layers);
10800        // CPU caches stay the owners of record: append the chunk rows
10801        // and bank the importance masses per layer.
10802        for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
10803            let li = li0 + i;
10804            let row = rows_kv[i].1;
10805            let layer = &mut self.kv_cache.layers[li];
10806            for bi in 0..b {
10807                layer.append(
10808                    &ok[bi * row..(bi + 1) * row],
10809                    &ov[bi * row..(bi + 1) * row],
10810                    &[],
10811                );
10812            }
10813            layer.accumulate_imp(oi);
10814        }
10815        last
10816    }
10817
10818    /// Is layer `li` a sliding-window (local-RoPE) layer? Gemma-3:
10819    /// every `pattern`-th layer is global, the rest are local.
10820    fn layer_is_local(&self, li: usize) -> bool {
10821        if let Some(layers) = &self.sliding_layers {
10822            return layers.get(li).copied().unwrap_or(false);
10823        }
10824        match self.swa {
10825            Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
10826            None => false,
10827        }
10828    }
10829
10830    /// The RoPE table for layer `li` (local layers may have their own;
10831    /// Gemma-4 global layers use the proportional padded table).
10832    fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
10833        if self.layer_is_local(li) {
10834            if let Some(f) = &self.inv_freq_local {
10835                return f.clone();
10836            }
10837        } else if let Some(f) = &self.inv_freq_global {
10838            return f.clone();
10839        }
10840        self.inv_freq.clone()
10841    }
10842
10843    /// The attend window for layer `li` (None = full context).
10844    fn layer_window(&self, li: usize) -> Option<usize> {
10845        self.swa
10846            .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
10847    }
10848
10849    /// Drop every sliding layer's rows its window can no longer read
10850    /// (`LayerKvCache::trim_window`). Call only at a safe point — after a
10851    /// complete position / pair / chunk walk, never between a layer's
10852    /// appends and its attend. One comparison per layer below the trigger.
10853    ///
10854    /// Off with an MTP head: its verify oracles and rollbacks snapshot and
10855    /// compare per-layer row counts as positions (`CMF_METAL_VERIFY_CHECK`,
10856    /// the MTP caches), which a trimmed tail would break.
10857    fn swa_trim_tails(&mut self) {
10858        let Some((slack, align)) = self.swa_trim else {
10859            return;
10860        };
10861        if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
10862            return;
10863        }
10864        for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
10865            if let Some(w) = self.layer_window(li) {
10866                self.kv_cache.layers[li].trim_window(w, slack, align);
10867            }
10868        }
10869    }
10870
10871    fn layer_num_heads(&self, li: usize) -> usize {
10872        self.attention_heads_per_layer
10873            .as_ref()
10874            .and_then(|v| v.get(li).copied())
10875            .unwrap_or(self.num_heads)
10876    }
10877
10878    fn layer_rope_scale(&self, li: usize) -> f32 {
10879        if self.layer_is_local(li) {
10880            self.rope_scale_local
10881        } else {
10882            self.rope_scale
10883        }
10884    }
10885
10886    /// Attention geometry of layer `li`: (num_kv_heads, head_dim,
10887    /// rotary_dim). Gemma-4 global layers override all three.
10888    fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
10889        if !self.layer_is_local(li) {
10890            if let Some((ghd, gkv)) = self.global_attn {
10891                return (gkv, ghd, ghd);
10892            }
10893        }
10894        let rd = if self.layer_is_local(li) {
10895            self.rotary_dim_local.unwrap_or(self.rotary_dim)
10896        } else {
10897            self.rotary_dim
10898        };
10899        // A layer without positional encoding rotates nothing.
10900        let rd = if self
10901            .no_rope_layers
10902            .as_ref()
10903            .and_then(|v| v.get(li))
10904            .copied()
10905            == Some(true)
10906        {
10907            0
10908        } else {
10909            rd
10910        };
10911        (self.layer_num_kv_heads(li), self.head_dim, rd)
10912    }
10913
10914    /// KV heads of layer `li` (virtual index): the per-layer count when the
10915    /// model has one (MiMo-V2), else the uniform `num_kv_heads`.
10916    fn layer_num_kv_heads(&self, li: usize) -> usize {
10917        self.kv_heads_per_layer
10918            .as_ref()
10919            .and_then(|v| v.get(self.phys_layer(li)).copied())
10920            .unwrap_or(self.num_kv_heads)
10921    }
10922
10923    /// V head width of layer `li` (≤ its head_dim).
10924    fn layer_v_dim(&self, li: usize) -> usize {
10925        let (_, hd, _) = self.layer_geom(li);
10926        self.v_head_dim.unwrap_or(hd).min(hd)
10927    }
10928
10929    /// Install a per-layer KV geometry: KV heads per PHYSICAL layer and/or
10930    /// a V head width narrower than `head_dim` (MiMo-V2). Validates it and
10931    /// reshapes the caches of every layer whose KV head count differs from
10932    /// `num_kv_heads`. The loader and the tests share this one path, so a
10933    /// hand-built pipeline cannot hold a geometry the loader would refuse.
10934    /// Call before the first forward (it drops cached rows of reshaped
10935    /// layers). Refuses combinations whose paths would read it wrong:
10936    /// Gemma-4 global layers and MLA carry their own geometry.
10937    pub fn set_attn_geometry(
10938        &mut self,
10939        kv_heads_per_layer: Option<Vec<usize>>,
10940        v_head_dim: Option<usize>,
10941    ) -> Result<(), String> {
10942        if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
10943            if self.global_attn.is_some() {
10944                return Err(
10945                    "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
10946                     attention geometry"
10947                        .into(),
10948                );
10949            }
10950            if self
10951                .weights
10952                .layers
10953                .iter()
10954                .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
10955            {
10956                return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
10957            }
10958        }
10959        if let Some(vd) = v_head_dim {
10960            if vd == 0 || vd > self.head_dim {
10961                return Err(format!(
10962                    "v_head_dim {vd} must be in 1..={} (head_dim)",
10963                    self.head_dim
10964                ));
10965            }
10966        }
10967        if let Some(v) = &kv_heads_per_layer {
10968            if v.len() != self.physical_layers {
10969                return Err(format!(
10970                    "kv_heads_per_layer has {} entries, expected {} layers",
10971                    v.len(),
10972                    self.physical_layers
10973                ));
10974            }
10975            for (li, &nkv) in v.iter().enumerate() {
10976                let is_attn = matches!(
10977                    self.weights.layers.get(li).map(|lw| &lw.attn),
10978                    Some(AttnKind::Full { .. }) | None
10979                );
10980                if !is_attn {
10981                    continue;
10982                }
10983                let nh = self
10984                    .attention_heads_per_layer
10985                    .as_ref()
10986                    .and_then(|h| h.get(li).copied())
10987                    .unwrap_or(self.num_heads);
10988                if nkv == 0 || nh % nkv != 0 {
10989                    return Err(format!(
10990                        "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
10991                    ));
10992                }
10993            }
10994        }
10995        self.kv_heads_per_layer = kv_heads_per_layer;
10996        self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
10997        if self.kv_heads_per_layer.is_some() {
10998            for li in 0..self.kv_cache.layers.len() {
10999                let full = matches!(
11000                    self.weights
11001                        .layers
11002                        .get(self.phys_layer(li))
11003                        .map(|lw| &lw.attn),
11004                    Some(AttnKind::Full { .. })
11005                );
11006                let nkv = self.layer_num_kv_heads(li);
11007                let cache = &self.kv_cache.layers[li];
11008                if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
11009                    let sinks = cache.sinks.clone();
11010                    self.kv_cache.layers[li] =
11011                        crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
11012                    self.kv_cache.layers[li].sinks = sinks;
11013                }
11014            }
11015        }
11016        Ok(())
11017    }
11018
11019    /// Attach learned attention-sink logits (one per Q head) to PHYSICAL
11020    /// layer `phys` — every virtual layer that runs it. The loader calls
11021    /// this for each `model.layers.N.self_attn.sinks` tensor.
11022    pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
11023        let Some(lw) = self.weights.layers.get(phys) else {
11024            return Err(format!("sinks for layer {phys}: no such layer"));
11025        };
11026        if !matches!(lw.attn, AttnKind::Full { .. }) {
11027            return Err(format!(
11028                "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
11029            ));
11030        }
11031        let nh = self
11032            .attention_heads_per_layer
11033            .as_ref()
11034            .and_then(|h| h.get(phys).copied())
11035            .unwrap_or(self.num_heads);
11036        if sinks.len() != nh {
11037            return Err(format!(
11038                "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
11039                sinks.len()
11040            ));
11041        }
11042        if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
11043            return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
11044        }
11045        for li in 0..self.kv_cache.layers.len() {
11046            if self.phys_layer(li) == phys {
11047                self.kv_cache.layers[li].sinks = Some(sinks.clone());
11048            }
11049        }
11050        Ok(())
11051    }
11052
11053    /// Why the GPU attention graphs cannot serve this model, if they
11054    /// cannot: the wgpu whole-token and batched graphs, the greedy
11055    /// multi-burst, the q1 attention dropin and the Metal block/chunk/rows
11056    /// graphs all assume ONE (num_kv_heads, head_dim) geometry, V heads as
11057    /// wide as K, a single RoPE table, full-context attention and a plain
11058    /// softmax. A model outside that contract runs on the CPU layer walk
11059    /// (and the per-op GPU matvecs) until a graph learns it — never on a
11060    /// graph that would read it wrong. None = no attention-level reason
11061    /// (the graph builders still check weights and layer kinds).
11062    pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
11063        if self.kv_heads_per_layer.is_some() {
11064            return Some("per-layer KV head counts");
11065        }
11066        if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
11067            return Some("V heads narrower than Q/K heads");
11068        }
11069        if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
11070            return Some("learned attention sinks");
11071        }
11072        if self.swa.is_some() || self.sliding_layers.is_some() {
11073            return Some("sliding-window layers");
11074        }
11075        None
11076    }
11077
11078    /// Can the Metal kernels apply this model's RoPE amplitude? Every
11079    /// rotation kernel (`attn_rope_qkn[_b]`, `chunk_rope_kv`) multiplies the
11080    /// rotated pair by the layer's `rope_scale` (YaRN `attention_factor`,
11081    /// Mellum2.1's global layers) exactly where the CPU's
11082    /// `rope_rotate_scaled` does. With the Q/K norm AFTER RoPE the kernels'
11083    /// shared sum of squares would be the unscaled one, so that pairing
11084    /// (no known model) declines.
11085    #[cfg(target_os = "macos")]
11086    fn metal_rope_scale_ok(&self) -> bool {
11087        !(self.qk_norm_after_rope
11088            && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
11089    }
11090
11091    /// Can the Metal block graph carry this model's sliding-window layers?
11092    /// Both of its attention forms take a layer's own window, RoPE table
11093    /// and rotary width: the device attend (`AttnDeviceParams::window`, the
11094    /// layer's `inv_freq`/`rd`) and the sandwich's host attend (exactly the
11095    /// CPU path's attention). True when the sliding window is the model's
11096    /// only attention-level decline (Spark-X2.5). Learned, value-less
11097    /// sinks may accompany that geometry: q1's ordinary device GQA
11098    /// includes the sink in its softmax denominator and disables split-K.
11099    /// Per-layer KV heads, narrow V and Gemma-4 global geometry still
11100    /// decline; a YaRN amplitude per layer rides along
11101    /// (`metal_rope_scale_ok`).
11102    ///
11103    /// Per-layer Q heads (Laguna) and capped scores (Gemma-2) decline here
11104    /// too, with V norm (Gemma-4): the chunk prefill's own gate checks
11105    /// neither of the first two, and before this door opened its `swa`
11106    /// refusal kept every sliding model with them off the chunk graph.
11107    #[cfg(target_os = "macos")]
11108    fn metal_graph_swa(&self) -> bool {
11109        (self.swa.is_some() || self.sliding_layers.is_some())
11110            && self.attention_heads_per_layer.is_none()
11111            && !self.attn_v_norm
11112            && self.attn_softcap == 0.0
11113            && self.kv_heads_per_layer.is_none()
11114            && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
11115            && self.global_attn.is_none()
11116            && self.inv_freq_global.is_none()
11117            && self.metal_rope_scale_ok()
11118            && !(0..self.num_layers).any(|li| {
11119                self.layer_is_local(li)
11120                    && self.inv_freq_local.is_none()
11121                    && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11122            })
11123    }
11124
11125    /// Can the Metal graphs carry this Gemma-4 model (`gpu_metal::gemma4`)?
11126    /// Its sliding layers (window, local RoPE table, 256-wide heads)
11127    /// device-attend like Spark-X2.5's; the 512-wide global layers (one KV
11128    /// head, proportional RoPE in the zero-padded table) take the 16-slot
11129    /// kernels; the V norm, sandwich norms, layer scalar and tanh-GELU come
11130    /// from the Gemma-4 library. Dense FFNs only (no Gemma-4 MoE measured).
11131    /// `CMF_METAL_GEMMA4=0` declines it as before (CPU attention).
11132    #[cfg(target_os = "macos")]
11133    fn metal_gemma4(&self) -> bool {
11134        crate::gpu_metal::gemma4::enabled()
11135            && (self.global_attn.is_some() || self.attn_v_norm)
11136            && self.attention_heads_per_layer.is_none()
11137            && self.kv_heads_per_layer.is_none()
11138            && self.v_head_dim.is_none()
11139            && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11140            && self.attn_softcap == 0.0
11141            && !self.qk_norm_after_rope
11142            && self.metal_rope_scale_ok()
11143            && self
11144                .weights
11145                .layers
11146                .iter()
11147                .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
11148            && !(0..self.num_layers).any(|li| {
11149                self.layer_is_local(li)
11150                    && self.inv_freq_local.is_none()
11151                    && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11152            })
11153    }
11154
11155    /// Why the WGPU graphs (whole-token, batched prefill, greedy burst)
11156    /// cannot run this model's attention, if they cannot. Per-layer KV
11157    /// heads, V narrower than K, learned sinks and sliding windows ride
11158    /// their per-layer geometry (`GraphAttnGeom`, the ATTEND_X kernels);
11159    /// what that geometry does not express keeps the decline, by name.
11160    /// None for every model with one attention geometry.
11161    pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
11162        // Keep the ordinary (uniform) fast arms off YaRN until they carry a
11163        // scale field too. A non-uniform geometry gets a `GraphAttnGeom` and
11164        // is handled below by the generic graph kernels.
11165        let geometry_reason = self.graph_attn_decline_reason();
11166        let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
11167        if has_scaled_rope && geometry_reason.is_none() {
11168            return Some("scaled RoPE without per-layer geometry");
11169        }
11170        // For Q/K norm after partial RoPE, the scale changes only the rotary
11171        // slice's RMS denominator. The generic graph deliberately declines
11172        // that uncommon combination until it carries a separate rotary sum;
11173        // Mellum has q/k norm before RoPE, so it remains on this path.
11174        if has_scaled_rope && self.qk_norm_after_rope {
11175            return Some("scaled RoPE with post-RoPE Q/K norm");
11176        }
11177        geometry_reason?;
11178        // Gemma-4: global layers 512 wide over 256-wide sliding ones
11179        // (`GraphAttnGeom::hd`), the scale-less V norm (`v_norm`), and the
11180        // sandwich norms + layer scalar (`GraphLayer`) ride the TOKEN graph.
11181        // The batch graph has none of the three and refuses them itself
11182        // (`batch_graph_gemma_decline`).
11183        if self.attention_heads_per_layer.is_some() {
11184            return Some("per-layer Q head counts with per-layer geometry");
11185        }
11186        // Sandwich norms are measured on Gemma-4 only (the humanizer port,
11187        // against HF transformers); Gemma-2/3 keep the per-op path until
11188        // they are measured the same way.
11189        let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
11190        if !gemma4
11191            && self.weights.layers.iter().any(|lw| {
11192                lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11193            })
11194        {
11195            return Some("sandwich norms / layer scale with per-layer geometry");
11196        }
11197        // `GraphAttnGeom::rope_scale` carries YaRN's post-rotation
11198        // attention-amplitude factor to the WGPU RoPE kernel. It is not an
11199        // angular position scale, so it must not be folded into `invf`.
11200        if self.weights.layers.iter().any(|lw| {
11201            matches!(
11202                &lw.attn,
11203                AttnKind::Full {
11204                    output_gate: true,
11205                    ..
11206                }
11207            )
11208        }) && self.v_head_dim.is_some()
11209        {
11210            return Some("gated attention with V narrower than K");
11211        }
11212        if (0..self.num_layers).any(|li| {
11213            self.layer_is_local(li)
11214                && self.inv_freq_local.is_none()
11215                && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11216        }) {
11217            return Some("local rotary width without a local RoPE table");
11218        }
11219        None
11220    }
11221
11222    /// What the wgpu BATCH graph (and the attention-only singleton graphs)
11223    /// cannot run of Gemma-4, which the token graph does run: per-layer
11224    /// head widths, the V norm, sandwich norms and the layer scalar.
11225    fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
11226        // Gemma-4 rides `gemma_batch` (per-layer head width, V norm,
11227        // sandwich norms, layer scalar) unless CMF_G4_BATCH=0.
11228        if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
11229            return None;
11230        }
11231        if self.global_attn.is_some() {
11232            return Some("per-layer head width (Gemma-4 global layers)");
11233        }
11234        if self.attn_v_norm {
11235            return Some("V norm");
11236        }
11237        if self.weights.layers.iter().any(|lw| {
11238            lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11239        }) {
11240            return Some("sandwich norms / layer scale");
11241        }
11242        None
11243    }
11244
11245    /// The wgpu graphs' attention geometry for layer `li` (virtual index):
11246    /// Some only for a model whose layers do not share one (MiMo-V2) — KV
11247    /// heads, V width, rotary width and RoPE table, window and sinks of
11248    /// THIS layer, exactly what the CPU attention reads for it.
11249    fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
11250        self.graph_attn_decline_reason()?;
11251        let (nkv, hd, rd) = self.layer_geom(li);
11252        let invf: &[f32] = if self.layer_is_local(li) {
11253            match &self.inv_freq_local {
11254                Some(f) => f.as_slice(),
11255                None => self.inv_freq.as_slice(),
11256            }
11257        } else {
11258            match &self.inv_freq_global {
11259                Some(f) => f.as_slice(),
11260                None => self.inv_freq.as_slice(),
11261            }
11262        };
11263        Some(crate::gpu::GraphAttnGeom {
11264            nkv,
11265            hd,
11266            dv: self.layer_v_dim(li),
11267            rd,
11268            invf,
11269            rope_scale: self.layer_rope_scale(li),
11270            window: self.layer_window(li),
11271            sink: self.kv_cache.layers[li].sinks.as_deref(),
11272            v_norm: self.attn_v_norm,
11273        })
11274    }
11275
11276    /// The decode graph's device K/V mirror that layer `li`'s batched
11277    /// prefill appends its chunk to and attends against (wgpu), instead
11278    /// of uploading the layer's whole K/V prefix every chunk and the whole
11279    /// cache again at the first decode token. Only where the token graph
11280    /// will read that very mirror: the decode graph on, not refused, and
11281    /// this layer's geometry what it requests (`graph_attn_geom`: KV heads,
11282    /// V as wide as K, the window as a ring, no sink).
11283    ///
11284    /// Spark-X2.5 only (`proj_gate_sigmoid`), the one architecture whose
11285    /// output was measured identical on it. `CMF_PREFILL_MIRROR=0` keeps
11286    /// the host upload (A/B).
11287    #[cfg(feature = "gpu")]
11288    fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
11289        if !self.proj_gate_sigmoid
11290            || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
11291            || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
11292            || self.graph_refused()
11293            || self.o1_active()
11294            || self.attn_softcap > 0.0
11295            || self.wgpu_graph_attn_decline().is_some()
11296        {
11297            return None;
11298        }
11299        let g = self.graph_attn_geom(li)?;
11300        let (nkv, hd, _) = self.layer_geom(li);
11301        // The token graph passes the call-wide head width (layer 0's).
11302        if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
11303            return None;
11304        }
11305        Some(crate::gpu::PrefillMirror {
11306            kv_id: self.graph_kv_id,
11307            layer: li,
11308            limit: self.kv_cache.max_seq_len,
11309        })
11310    }
11311
11312    /// Bring the host KV cache of every Full-attention layer in
11313    /// `[from, upto)` up to `position` rows from the wgpu mirrors, where a
11314    /// device graph advanced a layer that the host is about to run: a
11315    /// device prefix that shrank since the prompt (or a batched prefill
11316    /// prefix longer than the decode one). A layer whose mirror does not
11317    /// hold the missing rows is left alone. Rows a sliding layer's ring
11318    /// no longer holds come back as zeros — outside every window that
11319    /// will read them.
11320    #[cfg(feature = "gpu")]
11321    fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
11322        let kv_id = self.graph_kv_id;
11323        for li in from..upto.min(self.num_layers) {
11324            if !matches!(
11325                self.weights.layers[self.phys_layer(li)].attn,
11326                AttnKind::Full { .. }
11327            ) {
11328                continue;
11329            }
11330            // Absolute: a trimmed sliding tail holds positions base.. only.
11331            let host = self.kv_cache.layers[li].pos_len();
11332            if host >= position {
11333                continue;
11334            }
11335            let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
11336                continue;
11337            };
11338            let to = dev.min(position);
11339            if to <= host {
11340                continue;
11341            }
11342            let (nkv, hd) = {
11343                let c = &self.kv_cache.layers[li];
11344                (c.num_kv_heads, c.head_dim)
11345            };
11346            let Some((k, v, first_valid)) =
11347                crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
11348            else {
11349                continue;
11350            };
11351            // A sliding layer only ever reads its last `window` rows; a
11352            // full-context layer needs every row it did not have.
11353            let need_from = match self.layer_window(li) {
11354                Some(w) => host.max((position + 1).saturating_sub(w)),
11355                None => host,
11356            };
11357            if first_valid > need_from {
11358                tracing::warn!(
11359                    "layer {li}: device KV rows {host}..{to} no longer resident \
11360                     (from {first_valid}); host attention will miss them"
11361                );
11362            }
11363            let row = nkv * hd;
11364            let cache = &mut self.kv_cache.layers[li];
11365            for p in 0..to - host {
11366                cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
11367            }
11368        }
11369    }
11370
11371    /// Log (once per graph site and pipeline) that `site` declined for
11372    /// `reason`. The lines are kept so a caller or a test can read them.
11373    fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
11374        let mut seen = self.graph_declines.borrow_mut();
11375        if !seen.iter().any(|&(s, r)| s == site && r == reason) {
11376            tracing::warn!("{site} declined: {reason} (CPU attention path)");
11377            seen.push((site, reason));
11378        }
11379    }
11380
11381    /// The GPU-graph declines this pipeline has logged so far, as the
11382    /// logged lines.
11383    pub fn graph_declines(&self) -> Vec<String> {
11384        self.graph_declines
11385            .borrow()
11386            .iter()
11387            .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
11388            .collect()
11389    }
11390
11391    /// Does layer `li` have the plain attention geometry the historical
11392    /// head-masked f32 path (`multi_head_attention`) assumes — pipeline-wide
11393    /// KV heads / head_dim / RoPE table, full context, no sink, V as wide
11394    /// as K? Anything else runs the dense `qwen_attention` instead.
11395    /// `CMF_LAYER_DUMP` writer (see `Pipeline::layer_dump`): one position's
11396    /// hidden after layer `li` as raw little-endian f32 into
11397    /// `<dir>/p{pos:06}_l{li:02}.f32`. A failed write is reported once and
11398    /// never stops the forward — the dump is a diagnostic.
11399    fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
11400        let Some(dir) = &self.layer_dump else {
11401            return;
11402        };
11403        let mut bytes = Vec::with_capacity(row.len() * 4);
11404        for v in row {
11405            bytes.extend_from_slice(&v.to_le_bytes());
11406        }
11407        let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
11408        if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
11409            use std::sync::atomic::{AtomicBool, Ordering};
11410            static SAID: AtomicBool = AtomicBool::new(false);
11411            if !SAID.swap(true, Ordering::Relaxed) {
11412                tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
11413            }
11414        }
11415    }
11416
11417    /// Decide the MiMo-V2 expert placement once (`crate::mimo_moe`). Any
11418    /// other model turns the slot off on the first call.
11419    fn mimo_moe_prepare(&mut self) {
11420        if !self.mimo_moe.is_undecided() {
11421            return;
11422        }
11423        let slot = {
11424            let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
11425                .filter_map(
11426                    |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
11427                        FfnKind::Moe(m) => Some((li, m)),
11428                        _ => None,
11429                    },
11430                )
11431                .collect();
11432            // One bank lives on one device: an in-process multi-GPU split
11433            // keeps the whole-layer path.
11434            if layers.is_empty()
11435                || self.physical_layers != self.num_layers
11436                || self.gpu_plan.is_some()
11437            {
11438                crate::mimo_moe::Slot::Off
11439            } else {
11440                // Whether a whole-token graph could run this model's layers
11441                // (then a whole-layer prefix is one submit, not per-layer
11442                // fences).
11443                let graph_prefix =
11444                    self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
11445                crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
11446            }
11447        };
11448        self.mimo_moe = slot;
11449    }
11450
11451    #[cfg(test)]
11452    pub(crate) fn test_graph_kv_id(&self) -> u64 {
11453        self.graph_kv_id
11454    }
11455
11456    /// Dynamic MiMo layer: one device attention graph, followed by a bank
11457    /// frame. Both decode and short verification use this same attention
11458    /// path and absolute layer key; the host KV may intentionally lag.
11459    pub(crate) fn mimo_graph_layer_rows(
11460        &mut self,
11461        li: usize,
11462        h: &mut [f32],
11463        positions: &[usize],
11464    ) -> crate::gpu::BatchGraphOutcome {
11465        use crate::gpu::BatchGraphOutcome as Out;
11466        let b = positions.len();
11467        if !(1..=4).contains(&b)
11468            || h.len() != b * self.hidden_size
11469            || !self.mimo_moe.is_dynamic(li, true)
11470            || !crate::gpu::enabled_here()
11471            || !crate::gpu::wgpu_active()
11472            || self.o1_active()
11473            || self.physical_layers != self.num_layers
11474            // The pair-fusion diagnostic (and an explicit graph-off run)
11475            // rewinds only host KV. A hidden singleton attention graph here
11476            // would leave device mirrors ahead of the next host position.
11477            || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
11478            || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
11479            || self.wgpu_graph_attn_decline().is_some()
11480        {
11481            return Out::Declined;
11482        }
11483        let attn_started = std::time::Instant::now();
11484        let outcome = {
11485            let lw = &self.weights.layers[li];
11486            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
11487                return Out::Declined;
11488            }
11489            let FfnKind::Moe(m) = &lw.ffn else {
11490                return Out::Declined;
11491            };
11492            let AttnKind::Full {
11493                wq,
11494                wk,
11495                wv,
11496                wo,
11497                q_norm,
11498                k_norm,
11499                output_gate,
11500                softplus_gate,
11501                bias,
11502            } = &lw.attn
11503            else {
11504                return Out::Declined;
11505            };
11506            if *output_gate || softplus_gate.is_some() {
11507                return Out::Declined;
11508            }
11509            let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
11510                m.experts
11511                    .first()?
11512                    .gate_proj
11513                    .mapped_q4tp()
11514                    .map(|(m, _)| m.clone())
11515            }) else {
11516                return Out::Declined;
11517            };
11518            fn gw<'a>(
11519                t: &'a QTensor,
11520                owner: &std::sync::Arc<cortiq_core::CmfModel>,
11521            ) -> Option<crate::gpu::GraphW<'a>> {
11522                if let Some((m, idx, kind, rs)) = t.graph_weight() {
11523                    if m.uid() != owner.uid() || t.has_prism_contract() {
11524                        return None;
11525                    }
11526                    return Some(crate::gpu::GraphW {
11527                        idx,
11528                        kind,
11529                        row_scale: rs,
11530                        data: &[],
11531                        prism: crate::gpu::GraphPrismOp::None,
11532                        affine: false,
11533                    });
11534                }
11535                t.as_f32().map(|data| crate::gpu::GraphW {
11536                    idx: 0,
11537                    kind: 4,
11538                    row_scale: &[],
11539                    data,
11540                    prism: crate::gpu::GraphPrismOp::None,
11541                    affine: false,
11542                })
11543            }
11544            let (Some(q), Some(k), Some(v), Some(o)) = (
11545                gw(wq, &model),
11546                gw(wk, &model),
11547                gw(wv, &model),
11548                gw(wo, &model),
11549            ) else {
11550                return Out::Declined;
11551            };
11552            let layer = crate::gpu::GraphLayer {
11553                input_norm: &lw.input_norm,
11554                post_norm: &lw.post_norm,
11555                attn_out_norm: None,
11556                ffn_out_norm: None,
11557                layer_scale: None,
11558                ffn: crate::gpu::GraphFfn::AttentionOnly,
11559                attn: crate::gpu::GraphAttn::Full {
11560                    wq: q,
11561                    wk: k,
11562                    wv: v,
11563                    wo: o,
11564                    q_norm: q_norm.as_deref(),
11565                    k_norm: k_norm.as_deref(),
11566                    late_qk_norm: self.qk_norm_after_rope,
11567                    bias: bias
11568                        .as_ref()
11569                        .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
11570                    output_gate: false,
11571                    cpu_k: self.kv_cache.layers[li].k_heads(),
11572                    cpu_v: self.kv_cache.layers[li].v_heads(),
11573                    cpu_base: self.kv_cache.layers[li].base(),
11574                    geom: self.graph_attn_geom(li),
11575                    head_gate: None,
11576                },
11577            };
11578            let (nkv, hd, rd) = self.layer_geom(li);
11579            crate::gpu::forward_batch_graph_at(
11580                &model,
11581                self.graph_kv_id,
11582                li,
11583                &[layer],
11584                &self.inv_freq,
11585                h,
11586                self.layer_num_heads(li),
11587                nkv,
11588                hd,
11589                rd,
11590                self.hidden_size,
11591                1,
11592                positions,
11593                self.kv_cache.max_seq_len,
11594                self.norm_style == cortiq_core::NormStyle::Gemma,
11595                self.rms_eps as f32,
11596                self.attn_scale,
11597                b,
11598                &[],
11599                self.o1_epoch,
11600                None,
11601                None,
11602            )
11603        };
11604        match outcome {
11605            Out::Completed => {}
11606            Out::Declined => return Out::Declined,
11607            Out::Failed => {
11608                self.graph_failed
11609                    .store(true, std::sync::atomic::Ordering::Relaxed);
11610                return Out::Failed;
11611            }
11612        }
11613        let attn_ns = attn_started.elapsed().as_nanos() as u64;
11614        let hs = self.hidden_size;
11615        let lw = &self.weights.layers[li];
11616        let FfnKind::Moe(m) = &lw.ffn else {
11617            unreachable!()
11618        };
11619        let mut post = vec![0.0; h.len()];
11620        for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
11621            inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
11622        }
11623        let mut ffn = if b == 1 {
11624            moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
11625        } else {
11626            moe_ffn_banked_rows(
11627                &mut self.mimo_moe,
11628                li,
11629                m,
11630                &post,
11631                b,
11632                hs,
11633                self.pool.as_deref(),
11634            )
11635        };
11636        for (x, &f) in h.iter_mut().zip(&ffn) {
11637            *x += f;
11638        }
11639        attention::recycle_buf(&mut ffn);
11640        if self.layer_dump.is_some() {
11641            for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
11642                self.dump_layer_row(pos, li, row);
11643            }
11644        }
11645        crate::mimo_moe::note_attention_graph(b, attn_ns);
11646        Out::Completed
11647    }
11648
11649    fn layer_attn_plain(&self, li: usize) -> bool {
11650        self.kv_heads_per_layer.is_none()
11651            && self.v_head_dim.is_none()
11652            && self.global_attn.is_none()
11653            && self.layer_window(li).is_none()
11654            && self.kv_cache.layers[li].sinks.is_none()
11655    }
11656
11657    /// Forward one position through all layers (hybrid dispatch).
11658    fn forward_layers(
11659        &mut self,
11660        hidden: &[f32],
11661        position: usize,
11662        task_mask: Option<&TaskMask>,
11663    ) -> Vec<f32> {
11664        let out = self.forward_layers_upto(hidden, position, task_mask, None);
11665        self.o1_progress();
11666        self.swa_trim_tails();
11667        out
11668    }
11669
11670    // ── Network pipeline-split building blocks (coordinator/worker) ──
11671    // A remote worker owns layers [from ..= upto] and their KV; the
11672    // coordinator owns the rest plus embed / final norm / head. Attention
11673    // causality is per-layer, so a whole prompt's boundary hiddens ship
11674    // as one batch and decode ships one vector per token.
11675
11676    /// Embed one token id (embed multiplier applied).
11677    pub fn embed_id(&self, id: u32) -> Vec<f32> {
11678        self.embed_single(id)
11679    }
11680
11681    /// Refuse the archs/modes whose forward cannot be cut at a layer
11682    /// boundary. Loud by design: a split that silently changed the math
11683    /// would be a chimera.
11684    pub fn split_supported(&self) -> Result<(), String> {
11685        if self.dsv4.is_some() {
11686            return Err(
11687                "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
11688            );
11689        }
11690        if self.dsv41.is_some() {
11691            return Err(
11692                "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
11693                    .into(),
11694            );
11695        }
11696        if self.qwen4_exp.is_some() {
11697            return Err(
11698                "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
11699            );
11700        }
11701        if self.g3n.is_some() {
11702            return Err(
11703                "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
11704            );
11705        }
11706        Ok(())
11707    }
11708
11709    /// Forward `hidden` through layers [from ..= upto] at `position`,
11710    /// appending those layers' KV/state. Both split sides call this
11711    /// over their own range; a task mask applies to the span's own
11712    /// layers (each side masks what it runs).
11713    pub fn forward_span(
11714        &mut self,
11715        hidden: &[f32],
11716        position: usize,
11717        from: usize,
11718        upto: usize,
11719        task_mask: Option<&TaskMask>,
11720    ) -> Result<Vec<f32>, String> {
11721        self.split_supported()?;
11722        if from > upto || upto >= self.num_layers {
11723            return Err(format!(
11724                "forward_span: layer range {from}..={upto} outside 0..{}",
11725                self.num_layers
11726            ));
11727        }
11728        if hidden.len() != self.hidden_size {
11729            return Err(format!(
11730                "forward_span: hidden len {} ≠ hidden_size {}",
11731                hidden.len(),
11732                self.hidden_size
11733            ));
11734        }
11735        let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
11736        self.o1_progress();
11737        self.swa_trim_tails();
11738        if self
11739            .graph_failed
11740            .swap(false, std::sync::atomic::Ordering::Relaxed)
11741        {
11742            self.cancel
11743                .store(false, std::sync::atomic::Ordering::Relaxed);
11744            self.clear_sequence_state();
11745            return Err("forward_span: deferred O(1) transition failed".into());
11746        }
11747        Ok(out)
11748    }
11749
11750    /// Final norm + lm_head over a boundary hidden (the final-logit
11751    /// softcap is applied by lm_head_forward itself).
11752    pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
11753        let normed = inference::rms_norm(
11754            hidden,
11755            &self.weights.final_norm,
11756            self.rms_eps,
11757            self.norm_style,
11758        );
11759        self.lm_head_forward(&normed)
11760    }
11761
11762    /// Sample the next token with this pipeline's sampler state.
11763    pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
11764        sampler::sample_with_scratch(
11765            logits,
11766            &self.sampler_config,
11767            past_tokens,
11768            &mut self.rng,
11769            &mut self.sampler_scratch,
11770        )
11771    }
11772
11773    /// Fresh sequence: clear KV, reuse history and device mirrors.
11774    pub fn reset_session(&mut self) {
11775        self.clear_sequence_state();
11776    }
11777
11778    /// Batched span prefill from token ids (coordinator side): embed +
11779    /// layers [0 ..= upto]; returns the boundary hiddens of ALL positions
11780    /// (ids.len() × hidden). Rides the same layer-major machinery as the
11781    /// local prefill; falls back to the per-position walk under
11782    /// CMF_PREFILL=seq.
11783    pub fn prefill_span_ids(
11784        &mut self,
11785        ids: &[u32],
11786        start_pos: usize,
11787        upto: usize,
11788        task_mask: Option<&TaskMask>,
11789    ) -> Result<Vec<f32>, String> {
11790        self.split_supported()?;
11791        if upto >= self.num_layers {
11792            return Err(format!(
11793                "prefill_span_ids: upto {upto} outside 0..{}",
11794                self.num_layers
11795            ));
11796        }
11797        // Same predicate as the whole-stack prefill: a span whose GDN
11798        // state lives on the device must walk positions through the
11799        // graph, not through the batched CPU span.
11800        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11801            let out =
11802                self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
11803            self.check_o1_progress_failure("prefill_span_ids")?;
11804            Ok(out)
11805        } else {
11806            let hs = self.hidden_size;
11807            let mut out = Vec::with_capacity(ids.len() * hs);
11808            for (i, &id) in ids.iter().enumerate() {
11809                let emb = self.embed_id(id);
11810                out.extend_from_slice(&self.forward_span(
11811                    &emb,
11812                    start_pos + i,
11813                    0,
11814                    upto,
11815                    task_mask,
11816                )?);
11817            }
11818            Ok(out)
11819        }
11820    }
11821
11822    /// Batched span prefill from boundary hiddens (worker side): layers
11823    /// [from ..= upto] for every position in the batch; returns the batch.
11824    pub fn prefill_span_hidden(
11825        &mut self,
11826        hidden: &[f32],
11827        start_pos: usize,
11828        from: usize,
11829        upto: usize,
11830        task_mask: Option<&TaskMask>,
11831    ) -> Result<Vec<f32>, String> {
11832        self.split_supported()?;
11833        let hs = self.hidden_size;
11834        if hidden.is_empty() || hidden.len() % hs != 0 {
11835            return Err(format!(
11836                "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
11837                hidden.len()
11838            ));
11839        }
11840        if from > upto || upto >= self.num_layers {
11841            return Err(format!(
11842                "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
11843                self.num_layers
11844            ));
11845        }
11846        if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11847            let out = self.prefill_batch_span(
11848                PrefillIn::Hidden(hidden),
11849                start_pos,
11850                task_mask,
11851                from,
11852                upto + 1,
11853            );
11854            self.check_o1_progress_failure("prefill_span_hidden")?;
11855            Ok(out)
11856        } else {
11857            let b = hidden.len() / hs;
11858            let mut out = Vec::with_capacity(hidden.len());
11859            for i in 0..b {
11860                let h = self.forward_span(
11861                    &hidden[i * hs..(i + 1) * hs],
11862                    start_pos + i,
11863                    from,
11864                    upto,
11865                    task_mask,
11866                )?;
11867                out.extend_from_slice(&h);
11868            }
11869            Ok(out)
11870        }
11871    }
11872
11873    /// Build the whole-token wgpu graph for a pure-attention q1 model (every
11874    /// layer Full q1 + dense q1 FFN, no gate/bias). Returns the post-stack
11875    /// hidden (caller does final norm + lm_head), or None to fall back.
11876    fn try_token_graph_wgpu(
11877        &self,
11878        hidden: &[f32],
11879        position: usize,
11880        logits_out: &mut Vec<f32>,
11881        layers_run: &mut usize,
11882    ) -> Option<Result<Vec<f32>, ()>> {
11883        self.try_token_graph_wgpu_steps(
11884            hidden,
11885            position,
11886            logits_out,
11887            1,
11888            None,
11889            Some(layers_run),
11890            0,
11891            self.num_layers,
11892        )
11893    }
11894
11895    /// The span twin (network split): the graph covers [from..upto_excl)
11896    /// — one submit per SEGMENT per token. lm_head folds in only when
11897    /// the span reaches the last layer.
11898    fn try_token_graph_wgpu_span(
11899        &self,
11900        hidden: &[f32],
11901        position: usize,
11902        logits_out: &mut Vec<f32>,
11903        from: usize,
11904        upto_excl: usize,
11905        layers_run: &mut usize,
11906    ) -> Option<Result<Vec<f32>, ()>> {
11907        self.try_token_graph_wgpu_steps(
11908            hidden,
11909            position,
11910            logits_out,
11911            1,
11912            None,
11913            Some(layers_run),
11914            from,
11915            upto_excl,
11916        )
11917    }
11918
11919    /// Greedy burst: forward `t_next` and let the device pick + re-embed
11920    /// the next k−1 tokens — k frames, ONE submit, k ids back. The ZML
11921    /// trade, on wgpu. None ⇒ caller keeps the per-token path.
11922    fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
11923        if self.o1_active() || self.attn_softcap > 0.0 {
11924            return None;
11925        }
11926        // The burst builds the whole-token graph; attention the graph's
11927        // per-layer geometry cannot express keeps the per-token path.
11928        if let Some(reason) = self.wgpu_graph_attn_decline() {
11929            self.note_graph_decline("wgpu multi-burst", reason);
11930            return None;
11931        }
11932        let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
11933        if !graph_on || self.graph_refused() {
11934            // Same memo as the decode site: this path builds the very
11935            // same graph, so a model it cannot build for must not be
11936            // walked again here either. Missing this guard was worth
11937            // 2.5x on an Adreno — 0.361 tok/s against 0.905 — because
11938            // the burst retried per token what decode had already given
11939            // up on.
11940            return None;
11941        }
11942        let emb = self.embed_single(t_next);
11943        let mut lg = Vec::new();
11944        let mut ids = Vec::new();
11945        match self.try_token_graph_wgpu_steps(
11946            &emb,
11947            position,
11948            &mut lg,
11949            k,
11950            Some(&mut ids),
11951            None,
11952            0,
11953            self.num_layers,
11954        ) {
11955            Some(Ok(_)) => {}
11956            Some(Err(())) => {
11957                // Preserve the backend's post-admission failure through the
11958                // Option-based burst API.  The decode caller consumes this
11959                // flag and clears the sequence instead of falling through
11960                // to a stale CPU recurrent state.
11961                self.graph_failed
11962                    .store(true, std::sync::atomic::Ordering::Relaxed);
11963                return None;
11964            }
11965            None => return None,
11966        }
11967        (ids.len() == k).then_some(ids)
11968    }
11969
11970    /// Multi-step greedy: k whole frames in ONE submit, argmax and re-embed
11971    /// on the device. `ids_out` receives the k winner ids; the hidden/logits
11972    /// outputs are NOT produced in that mode.
11973    fn try_token_graph_wgpu_steps(
11974        &self,
11975        hidden: &[f32],
11976        position: usize,
11977        logits_out: &mut Vec<f32>,
11978        steps: usize,
11979        ids_out: Option<&mut Vec<u32>>,
11980        layers_run: Option<&mut usize>,
11981        from: usize,
11982        upto_excl: usize,
11983    ) -> Option<Result<Vec<f32>, ()>> {
11984        // The bank has already reserved its VRAM. Never build a second
11985        // all-expert arena across bank-owned layers (including bursts).
11986        let upto_excl = match self.mimo_moe.graph_prefix_end() {
11987            Some(end) if end < upto_excl => {
11988                if steps != 1 || layers_run.is_none() || from >= end {
11989                    return None;
11990                }
11991                end
11992            }
11993            _ => upto_excl,
11994        };
11995        // O(1) Nyström decode runs off the sealed state, not the KV cache the
11996        // graph mirrors — never take the graph while o1 is active.
11997        let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
11998        if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
11999            // Softcapped scores have no graph kernel yet — CPU owns them.
12000            // o1 rides the graph only behind CMF_O1_GPU=1 while the port
12001            // proves itself; without it the CPU path owns o1 as before.
12002            return None;
12003        }
12004        // Per-layer KV heads, narrow V, sinks and sliding windows ride
12005        // `GraphAttn::Full::geom` (the ATTEND_X kernels). Anything that
12006        // geometry cannot express declines here, by name — before the
12007        // per-layer gate existed a sliding/sink model ran the graph as
12008        // full-context attention, fluent and wrong. The caller memoizes
12009        // the refusal.
12010        if let Some(reason) = self.wgpu_graph_attn_decline() {
12011            self.note_graph_decline("wgpu token graph", reason);
12012            return None;
12013        }
12014        // Per-layer sealed o1 state for the graph. During prefill the
12015        // state is still Collecting -> views are None -> the graph
12016        // refuses below and the CPU prefill records the q trace and
12017        // seals, exactly as the o1 design requires.
12018        let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
12019            .map(|li| {
12020                if !o1_gpu {
12021                    return None;
12022                }
12023                self.kv_cache.layers[self.phys_layer(li)].o1_views()
12024            })
12025            .collect();
12026        if self.o1_active() && o1_gpu {
12027            // Any o1 layer not sealed (or degenerate exact-only) keeps the
12028            // whole token on the CPU: half-graph forwards would desync.
12029            let want: usize = (from..upto_excl)
12030                .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
12031                .count();
12032            let have = o1_views.iter().filter(|v| v.is_some()).count();
12033            if want == 0 || have != want {
12034                // The silent twin of the gpu-side o1 gates, found the
12035                // same way: a 15x decode drop with an empty log. Views
12036                // stay None until the layer's state SEALS, so `have`
12037                // lagging `want` early in a run is the o1 design working
12038                // — but it must say so, or the next reader spends a
12039                // night proving the kernels innocent.
12040                // On CHANGE, not once: the first decline is the legal
12041                // unsealed prefill, and a once-print buries the state
12042                // that matters — what the count reads AFTER the seal.
12043                use std::sync::atomic::{AtomicUsize, Ordering};
12044                static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
12045                let code = have * 1000 + want;
12046                if LAST.swap(code, Ordering::Relaxed) != code {
12047                    tracing::warn!(
12048                        "o1 graph: {have} of {want} layers sealed — per-op until all seal"
12049                    );
12050                }
12051                return None;
12052            }
12053        }
12054        let nh = self.num_heads;
12055        let (nkv, hd, rd) = self.layer_geom(0);
12056        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12057        let mut layers = Vec::with_capacity(upto_excl - from);
12058        let mut model = None;
12059        let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
12060        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
12061            // `blr` rides as GRAPH_KIND_BLR (two stage dispatches).
12062            if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
12063                let name = &m.tensors[i].name;
12064                let prism = if crate::prism::is_inverse_embedding(m, name) {
12065                    crate::gpu::GraphPrismOp::InverseEmbedding
12066                } else if crate::prism::is_forward_weight(m, name) {
12067                    crate::gpu::GraphPrismOp::Forward
12068                } else {
12069                    crate::gpu::GraphPrismOp::None
12070                };
12071                return Some(crate::gpu::GraphW {
12072                    idx: i,
12073                    kind,
12074                    row_scale: rs,
12075                    data: &[],
12076                    prism,
12077                    affine: crate::prism::is_affine_target(m, name),
12078                });
12079            }
12080            // Small unquantized projections (GDN in_proj_a/b) stay f32.
12081            match t.as_f32() {
12082                Some(d) => Some(crate::gpu::GraphW {
12083                    idx: 0,
12084                    kind: 4,
12085                    row_scale: &[],
12086                    data: d,
12087                    prism: crate::gpu::GraphPrismOp::None,
12088                    affine: false,
12089                }),
12090                None => {
12091                    if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
12092                        eprintln!("batch graph: weight has no graph/f32 representation");
12093                    }
12094                    None
12095                }
12096            }
12097        }
12098        for li in from..upto_excl {
12099            let lw = &self.weights.layers[self.phys_layer(li)];
12100            if dbg {
12101                let ak = match &lw.attn {
12102                    AttnKind::Mla(_) => "Mla".into(),
12103                    AttnKind::Full {
12104                        output_gate, bias, ..
12105                    } => format!("Full gate={output_gate} bias={}", bias.is_some()),
12106                    AttnKind::LinearGdn(_) => "LinearGdn".into(),
12107                    AttnKind::Kda(_) => "Kda".into(),
12108                    AttnKind::Linear(_) => "Linear".into(),
12109                    AttnKind::ShortConv(_) => "ShortConv".into(),
12110                    AttnKind::Bounded(_) => "Bounded".into(),
12111                };
12112                let fk = match &lw.ffn {
12113                    FfnKind::Dense(_) => "Dense",
12114                    FfnKind::Moe(_) => "Moe",
12115                    FfnKind::DenseMoe(_) => "DenseMoe",
12116                };
12117                eprintln!("graph L{li}: attn={ak} ffn={fk}");
12118            }
12119            let gffn = match &lw.ffn {
12120                FfnKind::DenseMoe(_) => return None, // dual branch: CPU path
12121                // A tube layer is several matrices, not one — the
12122                // whole-layer graph has no shape for it yet.
12123                FfnKind::Dense(d) if !d.segs.is_empty() => return None,
12124                FfnKind::Dense(d) => {
12125                    // An activation the graph has no kernel arm for keeps
12126                    // the CPU/per-op owner, by name — the dense graph FFN
12127                    // used to compute SiLU for whatever the model asked.
12128                    let Some(act) = d.act.graph_act() else {
12129                        self.note_graph_decline(
12130                            "wgpu token graph",
12131                            "dense FFN activation without a graph kernel",
12132                        );
12133                        return None;
12134                    };
12135                    crate::gpu::GraphFfn::Dense {
12136                        gate: gw(&d.gate_proj)?,
12137                        up: gw(&d.up_proj)?,
12138                        down: gw(&d.down_proj)?,
12139                        act,
12140                    }
12141                }
12142                FfnKind::Moe(m) => {
12143                    // Adaptive τ and expert masks keep the CPU path, where
12144                    // they are implemented. Sigmoid routing with a selection
12145                    // bias (LFM2-MoE / DeepSeek noaux_tc), a routed scale ≠ 1
12146                    // and an UNGATED shared expert (HunYuan hy_v3: ×2.826 on
12147                    // the routed mix, the shared expert at weight 1) are all
12148                    // graphed — before, every such token fell to the per-op
12149                    // path whole (145 submits/token on Hy-MT2-30B-A3B).
12150                    if m.route_tau.is_some() || m.mask.is_some() {
12151                        return None;
12152                    }
12153                    let shared = m.shared.as_ref();
12154                    // The resident MoE kernels do not yet carry the
12155                    // descriptor-aware transform through router/shared-gate
12156                    // selection.  Refuse the complete layer instead of
12157                    // scoring with an untransformed Prism plane (the dense
12158                    // path has an explicit FWHT boundary below).
12159                    let inter = m.experts.first()?.gate_proj.rows();
12160                    // Flint's shared expert is a 1024-wide SwiGLU while its
12161                    // routed bank is 512-wide.  The resident MoE shader
12162                    // indexes a uniform-width packed bank, so keep that
12163                    // expert out of the routed slots and add it through the
12164                    // dense q4tp path after routed accumulation.  This is
12165                    // also the safe route for a mixed q2-routed/q4-shared
12166                    // artifact: neither layout nor row stride is faked.
12167                    let detached_shared = match shared {
12168                        Some((se, None))
12169                            if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
12170                        {
12171                            if !matches!(se.act, Act::Silu)
12172                                || se.gate_proj.mapped_q4tp().is_none()
12173                                || se.up_proj.mapped_q4tp().is_none()
12174                                || se.down_proj.mapped_q4tp().is_none()
12175                            {
12176                                return None;
12177                            }
12178                            Some(crate::gpu::GraphMoeDetachedShared {
12179                                gate: gw(&se.gate_proj)?,
12180                                up: gw(&se.up_proj)?,
12181                                down: gw(&se.down_proj)?,
12182                                width: se.gate_proj.rows(),
12183                            })
12184                        }
12185                        // A separately shaped gated shared expert needs a
12186                        // second sigmoid gate path.  Do not turn it into an
12187                        // ungated one merely to make the graph run.
12188                        Some((se, Some(_)))
12189                            if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
12190                        {
12191                            return None;
12192                        }
12193                        _ => None,
12194                    };
12195                    let has_shared = shared.is_some() && detached_shared.is_none();
12196                    let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
12197                    let sgate = match shared {
12198                        Some((_, Some(sg))) if has_shared => gw(sg)?,
12199                        // No gate, no shared expert, or a detached shared
12200                        // expert: the router only keeps the binding total;
12201                        // the selection shader does not read it.
12202                        _ => gw(&m.router)?,
12203                    };
12204                    let router = gw(&m.router)?;
12205                    // The resident MoE kernels do not yet carry the
12206                    // descriptor-aware transform through router/shared-gate
12207                    // selection.  Refuse the complete layer instead of
12208                    // scoring with an untransformed Prism plane (the dense
12209                    // path has an explicit FWHT boundary below).
12210                    if router.prism != crate::gpu::GraphPrismOp::None
12211                        || sgate.prism != crate::gpu::GraphPrismOp::None
12212                        || router.affine
12213                        || sgate.affine
12214                    {
12215                        tracing::warn!(
12216                            "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
12217                        );
12218                        return None;
12219                    }
12220                    let mut experts = Vec::with_capacity(m.experts.len() + 1);
12221                    // q4t or q4tp, but not both in one layer — the kernels
12222                    // are picked per layer, not per expert.
12223                    let mut q4tp: Option<bool> = None;
12224                    // The mixed 2-bit profile: q2tp gate/up over a q4tp
12225                    // down. Uniform across the layer, like `q4tp` itself.
12226                    let mut gu_q2: Option<bool> = None;
12227                    // q8_2f experts (gate, up and down): their own kernels.
12228                    let mut q82: Option<bool> = None;
12229                    for e in m
12230                        .experts
12231                        .iter()
12232                        .chain(shared.filter(|_| has_shared).map(|(se, _)| se).into_iter())
12233                    {
12234                        if !matches!(e.act, Act::Silu)
12235                            || e.gate_proj.rows() != inter
12236                            || e.up_proj.rows() != inter
12237                        {
12238                            return None;
12239                        }
12240                        // Expert tensors are packed into one resident buffer
12241                        // and the MoE kernels have no transform slot per
12242                        // expert.  Keep the CPU/per-op owner for Prism or
12243                        // affine experts rather than silently using raw bytes.
12244                        for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
12245                            let Some((em, ei, _, _)) = expert_weight
12246                                .graph_weight()
12247                                .or_else(|| expert_weight.graph_weight_descriptor())
12248                            else {
12249                                return None;
12250                            };
12251                            let name = &em.tensors[ei].name;
12252                            if crate::prism::is_forward_weight(em, name)
12253                                || crate::prism::is_inverse_embedding(em, name)
12254                                || crate::prism::is_affine_target(em, name)
12255                            {
12256                                tracing::warn!(
12257                                    "resident MoE declined: expert Prism/affine transform is not implemented"
12258                                );
12259                                return None;
12260                            }
12261                        }
12262                        let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
12263                            Some((mm, gi)) => (
12264                                mm,
12265                                gi,
12266                                e.up_proj.mapped_q4t()?.1,
12267                                e.down_proj.mapped_q4t()?.1,
12268                                false,
12269                                false,
12270                                false,
12271                            ),
12272                            None => match e.gate_proj.mapped_q2tp() {
12273                                Some((mm, gi)) => (
12274                                    mm,
12275                                    gi,
12276                                    e.up_proj.mapped_q2tp()?.1,
12277                                    e.down_proj.mapped_q4tp()?.1,
12278                                    true,
12279                                    true,
12280                                    false,
12281                                ),
12282                                None => match e.gate_proj.mapped_q4tp() {
12283                                    Some((mm, gi)) => (
12284                                        mm,
12285                                        gi,
12286                                        e.up_proj.mapped_q4tp()?.1,
12287                                        e.down_proj.mapped_q4tp()?.1,
12288                                        true,
12289                                        false,
12290                                        false,
12291                                    ),
12292                                    // q8_2f experts: all three projections,
12293                                    // each with its own column field.
12294                                    None => {
12295                                        if !crate::gpu::moe_q82_graph_on() {
12296                                            return None;
12297                                        }
12298                                        let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
12299                                        (
12300                                            mm,
12301                                            gi,
12302                                            e.up_proj.mapped_q8_2f()?.1,
12303                                            e.down_proj.mapped_q8_2f()?.1,
12304                                            false,
12305                                            false,
12306                                            true,
12307                                        )
12308                                    }
12309                                },
12310                            },
12311                        };
12312                        if *q4tp.get_or_insert(is_p) != is_p
12313                            || *gu_q2.get_or_insert(is_q2) != is_q2
12314                            || *q82.get_or_insert(is_8) != is_8
12315                        {
12316                            // The shared expert rides in the same packed
12317                            // buffer as the routed ones, so a layer that
12318                            // mixes layouts cannot be indexed by one stride.
12319                            // Say so: the symptom is a whole model quietly
12320                            // running its MoE on the CPU.
12321                            tracing::warn!(
12322                                "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."
12323                            );
12324                            return None;
12325                        }
12326                        model.get_or_insert_with(|| mm.clone());
12327                        experts.push((gi, ui, di));
12328                    }
12329                    crate::gpu::GraphFfn::Moe {
12330                        router,
12331                        shared_gate: sgate,
12332                        experts,
12333                        n_exp: m.experts.len(),
12334                        // CMF_TOPK_PROBE: timing probe only — output is WRONG.
12335                        // Fewer experts shrink the MoE arithmetic while the
12336                        // dispatch count stays identical, which is the only
12337                        // clean way to tell a launch-bound decode from a
12338                        // compute-bound one.
12339                        top_k: std::env::var("CMF_TOPK_PROBE")
12340                            .ok()
12341                            .and_then(|v| v.parse::<usize>().ok())
12342                            .filter(|k| *k > 0 && *k <= m.top_k)
12343                            .unwrap_or(m.top_k),
12344                        inter,
12345                        norm_topk: m.norm_topk_prob,
12346                        q4tp: q4tp?,
12347                        gu_q2: gu_q2.unwrap_or(false),
12348                        q82: q82.unwrap_or(false),
12349                        sigmoid: m.router_sigmoid,
12350                        sigmoid_denom_eps: m.sigmoid_denom_eps,
12351                        bias: m.expert_bias.as_deref(),
12352                        has_shared,
12353                        detached_shared,
12354                        shared_gated,
12355                        route_scale: m.routed_scaling,
12356                    }
12357                }
12358            };
12359            let attn = match &lw.attn {
12360                AttnKind::Full {
12361                    wq,
12362                    wk,
12363                    wv,
12364                    wo,
12365                    q_norm,
12366                    k_norm,
12367                    output_gate,
12368                    softplus_gate,
12369                    bias,
12370                } => {
12371                    if self.attention_heads_per_layer.is_some() {
12372                        return None;
12373                    }
12374                    // A projected output gate rides the graph in one form:
12375                    // Spark-X2.5's head-wise sigmoid (one g_proj row per Q
12376                    // head). Laguna's softplus gate keeps the CPU path.
12377                    let head_gate = match softplus_gate {
12378                        None => None,
12379                        Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
12380                        Some(_) => {
12381                            self.note_graph_decline(
12382                                "wgpu token graph",
12383                                "projected softplus / per-element output gate",
12384                            );
12385                            return None;
12386                        }
12387                    };
12388                    let (m, _, _, _) = wq.graph_weight_or_blr()?;
12389                    model = Some(m.clone());
12390                    crate::gpu::GraphAttn::Full {
12391                        wq: gw(wq)?,
12392                        wk: gw(wk)?,
12393                        wv: gw(wv)?,
12394                        wo: gw(wo)?,
12395                        q_norm: q_norm.as_deref(),
12396                        k_norm: k_norm.as_deref(),
12397                        late_qk_norm: self.qk_norm_after_rope,
12398                        bias: bias
12399                            .as_ref()
12400                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12401                        output_gate: *output_gate,
12402                        cpu_k: self.kv_cache.layers[li].k_heads(),
12403                        cpu_v: self.kv_cache.layers[li].v_heads(),
12404                        cpu_base: self.kv_cache.layers[li].base(),
12405                        geom: self.graph_attn_geom(li),
12406                        head_gate,
12407                    }
12408                }
12409                AttnKind::LinearGdn(w) => {
12410                    let cfg = self.gdn_cfg?;
12411                    let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
12412                    model = Some(m.clone());
12413                    crate::gpu::GraphAttn::Gdn {
12414                        qkv: gw(&w.in_proj_qkv)?,
12415                        z: gw(&w.in_proj_z)?,
12416                        a: gw(&w.in_proj_a)?,
12417                        b: gw(&w.in_proj_b)?,
12418                        out: gw(&w.out_proj)?,
12419                        conv1d: &w.conv1d,
12420                        a_log: &w.a_log,
12421                        dt_bias: &w.dt_bias,
12422                        norm: &w.norm,
12423                        nv: cfg.num_v_heads,
12424                        nk: cfg.num_k_heads,
12425                        dk: cfg.key_head_dim,
12426                        dv: cfg.value_head_dim,
12427                        kk: cfg.conv_kernel,
12428                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12429                    }
12430                }
12431                AttnKind::ShortConv(w) => {
12432                    let cfg = self.short_conv_cfg?;
12433                    let (m, _, _, _) = w
12434                        .in_proj
12435                        .graph_weight()
12436                        .or_else(|| w.in_proj.graph_weight_descriptor())?;
12437                    model = Some(m.clone());
12438                    crate::gpu::GraphAttn::ShortConv {
12439                        inp: gw(&w.in_proj)?,
12440                        out: gw(&w.out_proj)?,
12441                        taps: &w.conv,
12442                        kernel: cfg.kernel,
12443                        cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12444                    }
12445                }
12446                _ => return None,
12447            };
12448            layers.push(crate::gpu::GraphLayer {
12449                input_norm: &lw.input_norm,
12450                attn,
12451                post_norm: &lw.post_norm,
12452                attn_out_norm: lw.attn_out_norm.as_deref(),
12453                ffn_out_norm: lw.ffn_out_norm.as_deref(),
12454                layer_scale: lw.layer_scale,
12455                ffn: gffn,
12456            });
12457        }
12458        let model = model?;
12459        // Fold final-norm + lm_head into the graph when this call wants logits
12460        // and the lm_head is a graphable (quantized) weight — the graph then
12461        // reads back logits (into logits_out) instead of the hidden, dropping
12462        // the separate CPU/GPU lm_head op + its sync. Never the f32 fallback:
12463        // an unquantized lm_head is vocab·hidden and must not be uploaded.
12464        let lm_gw = if upto_excl == self.num_layers
12465            && self.graph_want_logits
12466            && std::env::var("CMF_GPU_LMHEAD")
12467                .map(|v| v != "0")
12468                .unwrap_or(true)
12469        {
12470            self.weights
12471                .lm_head
12472                .graph_weight()
12473                .or_else(|| self.weights.lm_head.graph_weight_descriptor())
12474                .map(|(m, i, kind, rs)| {
12475                    let name = &m.tensors[i].name;
12476                    let prism = if crate::prism::is_inverse_embedding(m, name) {
12477                        crate::gpu::GraphPrismOp::InverseEmbedding
12478                    } else if crate::prism::is_forward_weight(m, name) {
12479                        crate::gpu::GraphPrismOp::Forward
12480                    } else {
12481                        crate::gpu::GraphPrismOp::None
12482                    };
12483                    (
12484                        crate::gpu::GraphW {
12485                            idx: i,
12486                            kind,
12487                            row_scale: rs,
12488                            data: &[],
12489                            prism,
12490                            affine: crate::prism::is_affine_target(m, name),
12491                        },
12492                        self.weights.lm_head.rows(),
12493                    )
12494                })
12495        } else {
12496            None
12497        };
12498        let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
12499        // Multi-step re-embeds the winner on the device.
12500        let emb_gw = if steps > 1 {
12501            self.weights
12502                .embed_tokens
12503                .graph_weight()
12504                .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
12505                .map(|(m, i, kind, rs)| {
12506                    let name = &m.tensors[i].name;
12507                    let prism = if crate::prism::is_inverse_embedding(m, name) {
12508                        crate::gpu::GraphPrismOp::InverseEmbedding
12509                    } else if crate::prism::is_forward_weight(m, name) {
12510                        crate::gpu::GraphPrismOp::Forward
12511                    } else {
12512                        crate::gpu::GraphPrismOp::None
12513                    };
12514                    (
12515                        crate::gpu::GraphW {
12516                            idx: i,
12517                            kind,
12518                            row_scale: rs,
12519                            data: &[],
12520                            prism,
12521                            affine: crate::prism::is_affine_target(m, name),
12522                        },
12523                        self.weights.embed_tokens.rows(),
12524                        self.embed_multiplier,
12525                    )
12526                })
12527        } else {
12528            None
12529        };
12530
12531        // Loop boundaries: virtual layer indices after which final_norm is
12532        // applied (mid-stack only; the GLOBAL last layer's norm folds into
12533        // lm_head). Span-relative — the executor compares its enumerate
12534        // index. A span ending mid-stack keeps its boundary norm even when
12535        // it is the span's own last layer.
12536        let loop_norm_at: Vec<usize> = if self.loop_final_norm {
12537            (from..upto_excl.min(self.num_layers - 1))
12538                .filter(|&li| (li + 1) % self.physical_layers == 0)
12539                .map(|li| li - from)
12540                .collect()
12541        } else {
12542            Vec::new()
12543        };
12544        let mut h = hidden.to_vec();
12545        // The normal decode path only needs the fused lm-head logits.  A
12546        // CMF_LOGIT_DUMP diagnostic, however, promises a prompt-boundary
12547        // post-stack hidden alongside those logits; request the existing
12548        // second readback only for that explicit probe instead of dumping
12549        // the input copy left in `h` by a folded-head graph.
12550        let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
12551        let outcome = crate::gpu::forward_token_graph(
12552            &model,
12553            self.graph_kv_id,
12554            &layers,
12555            &o1_views,
12556            self.o1_epoch,
12557            &self.inv_freq,
12558            &mut h,
12559            nh,
12560            nkv,
12561            hd,
12562            self.attn_scale,
12563            rd,
12564            self.hidden_size,
12565            self.intermediate_size,
12566            position,
12567            self.kv_cache.max_seq_len,
12568            gemma,
12569            self.rms_eps as f32,
12570            lm,
12571            &self.weights.final_norm,
12572            logits_out,
12573            &loop_norm_at,
12574            steps,
12575            emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
12576            ids_out,
12577            layers_run,
12578            from,
12579            dump_hidden,
12580        );
12581        match outcome {
12582            crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
12583            crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
12584            crate::gpu::TokenGraphOutcome::Declined => None,
12585        }
12586    }
12587
12588    /// Batched prefill: k contiguous prompt positions through the whole wgpu
12589    /// graph in ONE submit (projections/FFN as GEMMs). `hiddens` is [k·hidden]
12590    /// in/out (embeddings in, layer output out); KV mirror / GDN state advance.
12591    /// false ⇒ unsupported → caller keeps the per-position graph.
12592    /// The b-row Metal graph plan for the whole model: every layer as a
12593    /// GDN run or a full-attention item, all-or-nothing (a layer outside the
12594    /// graph's contract → None, the caller runs plain). Shared by the
12595    /// speculative verify and the batched prefill.
12596    #[cfg(target_os = "macos")]
12597    #[allow(clippy::type_complexity)]
12598    fn metal_rows_plan(
12599        &self,
12600    ) -> Option<(
12601        Vec<MetalRowsItem<'_>>,
12602        std::sync::Arc<cortiq_core::CmfModel>,
12603        Option<crate::gpu_metal::GdnGpuCfg>,
12604    )> {
12605        use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
12606        let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
12607        if !graph_force
12608            || !crate::gpu::enabled_here()
12609            || std::env::var("CMF_GPU_BLOCK")
12610                .map(|v| v == "0")
12611                .unwrap_or(false)
12612            || self.attn_softcap > 0.0
12613            || self.o1_active()
12614            || self.swa.is_some()
12615            || self.global_attn.is_some()
12616            || self.attention_heads_per_layer.is_some()
12617            // per-layer KV heads, narrow V, learned sinks (MiMo-V2)
12618            || self.graph_attn_decline_reason().is_some()
12619            || self.attn_v_norm
12620            || self.loop_final_norm
12621            || !self.metal_rope_scale_ok()
12622        {
12623            return None;
12624        }
12625        let attend_contract = self.head_dim % 4 == 0
12626            && self.head_dim <= 256
12627            && self.rotary_dim >= 2
12628            && self.rotary_dim <= self.head_dim
12629            && (self.rotary_dim / 2) % 32 == 0
12630            && self.num_kv_heads > 0
12631            && self.num_heads % self.num_kv_heads == 0;
12632        if !attend_contract {
12633            return None;
12634        }
12635        let mut plan: Vec<MetalRowsItem> = Vec::new();
12636        let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
12637        for li in 0..self.num_layers {
12638            let lw = &self.weights.layers[self.phys_layer(li)];
12639            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12640                return None;
12641            }
12642            let ffn = match &lw.ffn {
12643                FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
12644                    let (Some(g), Some(u), Some(dn)) = (
12645                        d.gate_proj.metal_graph_parts(),
12646                        d.up_proj.metal_graph_parts(),
12647                        d.down_proj.metal_graph_parts(),
12648                    ) else {
12649                        return None;
12650                    };
12651                    MetalFfn::Dense {
12652                        gate: g,
12653                        up: u,
12654                        down: dn,
12655                        act: 0, // SiLU only (the arm above)
12656                    }
12657                }
12658                _ => return None,
12659            };
12660            match &lw.attn {
12661                AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
12662                    let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
12663                        w.in_proj_qkv.metal_graph_parts(),
12664                        w.in_proj_z.metal_graph_parts(),
12665                        w.in_proj_a.f32_parts(),
12666                        w.in_proj_b.f32_parts(),
12667                        w.out_proj.metal_graph_parts(),
12668                    ) else {
12669                        return None;
12670                    };
12671                    if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
12672                        model_ref.get_or_insert_with(|| model.clone());
12673                    }
12674                    let gl = GdnGpuLayer {
12675                        attn_norm: &lw.input_norm,
12676                        post_norm: &lw.post_norm,
12677                        qkv,
12678                        z,
12679                        a,
12680                        b: bb,
12681                        out,
12682                        ffn,
12683                        conv1d: &w.conv1d,
12684                        a_log: &w.a_log,
12685                        dt_bias: &w.dt_bias,
12686                        gnorm: &w.norm,
12687                    };
12688                    match plan.last_mut() {
12689                        Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
12690                        _ => plan.push(MetalRowsItem::Gdn {
12691                            run: vec![gl],
12692                            first: li,
12693                        }),
12694                    }
12695                }
12696                AttnKind::Full {
12697                    wq,
12698                    wk,
12699                    wv,
12700                    wo,
12701                    q_norm,
12702                    k_norm,
12703                    output_gate,
12704                    softplus_gate: None,
12705                    bias: None,
12706                } => {
12707                    let (Some(pq), Some(pk), Some(pv), Some(po)) = (
12708                        wq.metal_graph_parts(),
12709                        wk.metal_graph_parts(),
12710                        wv.metal_graph_parts(),
12711                        wo.metal_graph_parts(),
12712                    ) else {
12713                        return None;
12714                    };
12715                    if let QTensor::Mapped { model, .. } = wq {
12716                        model_ref.get_or_insert_with(|| model.clone());
12717                    }
12718                    let cache = &self.kv_cache.layers[li];
12719                    if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
12720                        return None;
12721                    }
12722                    plan.push(MetalRowsItem::Attn {
12723                        l: AttnGpuLayer {
12724                            attn_norm: &lw.input_norm,
12725                            post_norm: &lw.post_norm,
12726                            wq: pq,
12727                            wk: pk,
12728                            wv: pv,
12729                            wo: po,
12730                            ffn,
12731                            attn_out_norm: None,
12732                            ffn_out_norm: None,
12733                            layer_scale: None,
12734                        },
12735                        li,
12736                        q_norm: q_norm.as_deref(),
12737                        k_norm: k_norm.as_deref(),
12738                        output_gate: *output_gate,
12739                    });
12740                }
12741                _ => return None,
12742            }
12743        }
12744        let model = model_ref?;
12745        let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
12746            nv: cfg.num_v_heads,
12747            nk: cfg.num_k_heads,
12748            dk: cfg.key_head_dim,
12749            dv: cfg.value_head_dim,
12750            kk: cfg.conv_kernel,
12751            hidden: self.hidden_size,
12752            inter: self.intermediate_size,
12753            c_dim: cfg.conv_dim(),
12754            eps: cfg.rms_eps as f32,
12755            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12756        });
12757        Some((plan, model, gcfg))
12758    }
12759
12760    /// `AttnDeviceParams` for a plan item over the CPU cache as it stands.
12761    #[cfg(target_os = "macos")]
12762    #[allow(clippy::too_many_arguments)]
12763    fn metal_attn_params<'a>(
12764        li: usize,
12765        cache: &'a crate::kv_cache::LayerKvCache,
12766        q_norm: Option<&'a [f32]>,
12767        k_norm: Option<&'a [f32]>,
12768        output_gate: bool,
12769        inv_freq: &'a [f32],
12770        rope_scale: f32,
12771        geom: (usize, usize, usize, usize),
12772        pos0: usize,
12773        kv_id: u64,
12774        scale: f32,
12775        eps: f32,
12776        gemma: bool,
12777        late_qk_norm: bool,
12778    ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
12779        let (nh, nkv, hd, rd) = geom;
12780        let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12781        let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12782        let cpu_stored = cpu_k[0].len() / hd;
12783        (
12784            crate::gpu_metal::AttnDeviceParams {
12785                kv_id,
12786                layer: li,
12787                nh,
12788                nkv,
12789                hd,
12790                rd,
12791                position: pos0,
12792                scale,
12793                eps,
12794                gemma,
12795                late_qk_norm,
12796                output_gate,
12797                q_norm,
12798                k_norm,
12799                inv_freq,
12800                rope_scale,
12801                cpu_k,
12802                cpu_v,
12803                cpu_stored,
12804                cpu_gen: cache.generation(),
12805                o1: None,
12806                window: None,
12807                softmax_sink: cache.sinks.as_deref(),
12808                head_gate: None,
12809                v_norm: false,
12810            },
12811            cpu_stored,
12812        )
12813    }
12814
12815    /// Run the rows plan over `hiddens` (b rows at `pos0..`): validate,
12816    /// encode every item, optionally the head, sync. Returns the graph
12817    /// (for the commit / state finish) plus the GDN layer indices and the
12818    /// attention layers with the row count they were encoded against.
12819    #[cfg(target_os = "macos")]
12820    #[allow(clippy::type_complexity)]
12821    fn metal_rows_run(
12822        &mut self,
12823        hiddens: &mut [f32],
12824        pos0: usize,
12825        b: usize,
12826        prefill: bool,
12827        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12828        // Greedy verify: (row length scored, the b argmax ids out) — the
12829        // head's argmax runs on the device and the logits plane is NOT
12830        // read back (`spec.2` stays empty).
12831        mut argmax_out: Option<(usize, &mut Vec<u32>)>,
12832    ) -> MetalRowsRun {
12833        use crate::gpu_metal::{GraphDims, VerifyGraph};
12834        // The previous round's commit may still be replaying into the
12835        // trunk GDN owners on the second queue: this graph reads them
12836        // (zero-copy wraps) and may reallocate them below — collect the
12837        // replay first. Normally already complete (the draft chain ran
12838        // in between); a failed replay is terminal like a failed commit.
12839        if !crate::gpu_metal::wait_replay() {
12840            tracing::error!("Metal rows graph: the pending async replay failed");
12841            return MetalRowsRun::Failed;
12842        }
12843        spec_stamp("v.wait");
12844        // Seed the GDN recurrent records the rows graph reads — GDN layers
12845        // ONLY (the record the CPU path would allocate anyway).  Sizing
12846        // every layer here planted a zero GDN-sized record on a bounded
12847        // anchor of a natively bounded file this path then refused, and
12848        // that record counted as recurrent state on macOS.
12849        let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
12850        if want > 0 {
12851            let phys = self.physical_layers.max(1);
12852            for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
12853                let is_gdn = self
12854                    .weights
12855                    .layers
12856                    .get(li % phys)
12857                    .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
12858                if is_gdn && l.linear_state.len() != want {
12859                    l.linear_state = vec![0f32; want];
12860                }
12861            }
12862        }
12863        let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
12864            return MetalRowsRun::Declined;
12865        };
12866        spec_stamp("v.plan");
12867        let dims = GraphDims {
12868            hidden: self.hidden_size,
12869            eps: self.rms_eps as f32,
12870            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12871        };
12872        let Some(mut graph) = (if prefill {
12873            VerifyGraph::new_prefill(&model, dims, hiddens, b)
12874        } else {
12875            VerifyGraph::new(&model, dims, hiddens, b)
12876        }) else {
12877            return MetalRowsRun::Declined;
12878        };
12879        let geom = (
12880            self.num_heads,
12881            self.num_kv_heads,
12882            self.head_dim,
12883            self.rotary_dim,
12884        );
12885        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12886        let eps = self.rms_eps as f32;
12887        let kv_id = self.graph_kv_id;
12888        let inv_freq = self.inv_freq.clone();
12889        for item in &plan {
12890            let ok = match item {
12891                MetalRowsItem::Gdn { run, .. } => gcfg
12892                    .as_ref()
12893                    .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
12894                    .unwrap_or(false),
12895                MetalRowsItem::Attn {
12896                    l,
12897                    li,
12898                    q_norm,
12899                    k_norm,
12900                    output_gate,
12901                } => {
12902                    let (p, _) = Self::metal_attn_params(
12903                        *li,
12904                        &self.kv_cache.layers[*li],
12905                        *q_norm,
12906                        *k_norm,
12907                        *output_gate,
12908                        &inv_freq,
12909                        self.layer_rope_scale(*li),
12910                        geom,
12911                        pos0,
12912                        kv_id,
12913                        self.attn_scale,
12914                        eps,
12915                        gemma,
12916                        self.qk_norm_after_rope,
12917                    );
12918                    graph.attn_ok(l, &p)
12919                }
12920            };
12921            if !ok {
12922                use std::sync::atomic::{AtomicBool, Ordering};
12923                static SAID: AtomicBool = AtomicBool::new(false);
12924                if !SAID.swap(true, Ordering::Relaxed) {
12925                    tracing::warn!("metal rows graph: a layer failed preflight — declining");
12926                }
12927                return MetalRowsRun::Declined;
12928            }
12929        }
12930        let lm = match &spec {
12931            Some((lm, _, _)) => {
12932                if !graph.lm_head_ok(*lm) {
12933                    return MetalRowsRun::Declined;
12934                }
12935                Some(*lm)
12936            }
12937            None => None,
12938        };
12939        let mut gdn_layers = Vec::new();
12940        let mut attn_layers = Vec::new();
12941        for item in &plan {
12942            match item {
12943                MetalRowsItem::Gdn { run, first } => {
12944                    let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
12945                        .iter()
12946                        .map(|l| l.linear_state.as_slice())
12947                        .collect();
12948                    if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
12949                        return MetalRowsRun::Declined;
12950                    }
12951                    gdn_layers.extend(*first..*first + run.len());
12952                }
12953                MetalRowsItem::Attn {
12954                    l,
12955                    li,
12956                    q_norm,
12957                    k_norm,
12958                    output_gate,
12959                } => {
12960                    let (p, cpu_stored) = Self::metal_attn_params(
12961                        *li,
12962                        &self.kv_cache.layers[*li],
12963                        *q_norm,
12964                        *k_norm,
12965                        *output_gate,
12966                        &inv_freq,
12967                        self.layer_rope_scale(*li),
12968                        geom,
12969                        pos0,
12970                        kv_id,
12971                        self.attn_scale,
12972                        eps,
12973                        gemma,
12974                        self.qk_norm_after_rope,
12975                    );
12976                    if !graph.encode_attn_b(l, &p) {
12977                        return MetalRowsRun::Declined;
12978                    }
12979                    attn_layers.push((*li, cpu_stored));
12980                }
12981            }
12982        }
12983        if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
12984            if !graph.encode_lm_head_b(final_norm, lm) {
12985                return MetalRowsRun::Declined;
12986            }
12987            // The device argmax is an OPTIMISATION, never a reason to
12988            // decline the round: if it will not encode, drop it and read
12989            // the logits plane back the old way (the head is encoded
12990            // either way, so the rows are there).
12991            if let Some((n, _)) = argmax_out.as_ref() {
12992                if !graph.encode_argmax_b(*n) {
12993                    argmax_out = None;
12994                }
12995            }
12996        }
12997        spec_stamp("v.enc");
12998        if !graph.sync() {
12999            return MetalRowsRun::Failed;
13000        }
13001        spec_stamp("v.gpu");
13002        match (spec, argmax_out) {
13003            (Some(_), Some((_, ids))) => {
13004                ids.resize(b, 0);
13005                if !graph.read_argmax(ids) {
13006                    return MetalRowsRun::Failed;
13007                }
13008                spec_stamp("v.am");
13009            }
13010            (Some((lm, _, logits)), None) => {
13011                logits.resize(b * lm.1, 0.0);
13012                if !graph.read_logits(logits) {
13013                    return MetalRowsRun::Failed;
13014                }
13015                spec_stamp("v.lg");
13016            }
13017            (None, _) => {}
13018        }
13019        if !graph.read_hidden(hiddens) {
13020            return MetalRowsRun::Failed;
13021        }
13022        spec_stamp("v.hid");
13023        MetalRowsRun::Completed(MetalVerifyPending {
13024            graph,
13025            gdn_layers,
13026            attn_layers,
13027        })
13028    }
13029
13030    /// Native-Metal twin of `try_batch_graph_wgpu`: the b rows through the
13031    /// whole model on the `VerifyGraph` (one submit), the head folded in
13032    /// when `spec` asks; `hiddens` come back as the last layer's output
13033    /// rows, `spec.2` as `[b][lm_rows]` logits. The graph is parked in
13034    /// `metal_verify` for `metal_verify_commit`.
13035    #[cfg(target_os = "macos")]
13036    fn try_batch_graph_metal(
13037        &mut self,
13038        hiddens: &mut [f32],
13039        positions: &[usize],
13040        b: usize,
13041        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13042        argmax_out: Option<(usize, &mut Vec<u32>)>,
13043    ) -> crate::gpu::BatchGraphOutcome {
13044        let _t0 = std::time::Instant::now();
13045        if positions.len() != b
13046            || positions.windows(2).any(|w| w[1] != w[0] + 1)
13047            || hiddens.len() != b * self.hidden_size
13048        {
13049            return crate::gpu::BatchGraphOutcome::Declined;
13050        }
13051        let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
13052            MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
13053            MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
13054            MetalRowsRun::Completed(pending) => pending,
13055        };
13056        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
13057            eprintln!(
13058                "metal-verify: {:.1} ms | b={b}",
13059                _t0.elapsed().as_secs_f64() * 1e3
13060            );
13061        }
13062        self.metal_verify = Some(pending);
13063        crate::gpu::BatchGraphOutcome::Completed
13064    }
13065
13066    /// Batched prefill on the Metal rows graph: `ids` (≤ 512) at
13067    /// `start_pos..`, states written in place, K/V rows appended to the
13068    /// CPU caches; optional final norm/head logits are returned in `spec`.
13069    /// Declined means no command buffer was admitted; Failed is terminal.
13070    #[cfg(target_os = "macos")]
13071    fn prefill_rows_metal(
13072        &mut self,
13073        ids: &[u32],
13074        start_pos: usize,
13075        spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13076    ) -> MetalPrefillOutcome {
13077        let b = ids.len();
13078        if b == 0 || b > 512 {
13079            return MetalPrefillOutcome::Declined;
13080        }
13081        METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13082        let with_head = spec.is_some();
13083        let hs = self.hidden_size;
13084        let mut hiddens = vec![0f32; b * hs];
13085        for (j, &id) in ids.iter().enumerate() {
13086            let e = self.embed_single(id);
13087            hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
13088        }
13089        let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
13090            MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
13091            MetalRowsRun::Failed => {
13092                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13093                return MetalPrefillOutcome::Failed;
13094            }
13095            MetalRowsRun::Completed(pending) => pending,
13096        };
13097        // states are final: copy them to the owners
13098        let idxs = pending.gdn_layers.clone();
13099        let mut outs: Vec<&mut [f32]> = self
13100            .kv_cache
13101            .layers
13102            .iter_mut()
13103            .enumerate()
13104            .filter(|(i, _)| idxs.binary_search(i).is_ok())
13105            .map(|(_, l)| l.linear_state.as_mut_slice())
13106            .collect();
13107        if !pending.graph.finish_states(&mut outs) {
13108            METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13109            return MetalPrefillOutcome::Failed;
13110        }
13111        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13112        // Read every layer before mutating any CPU cache.  A missing mirror
13113        // row is a terminal graph failure, not a reason to append a partial
13114        // prefix and replay the remainder serially.
13115        let mut rows = Vec::with_capacity(pending.attn_layers.len());
13116        for (li, cpu_stored) in &pending.attn_layers {
13117            let mut kbuf = vec![0f32; b * nkv * hd];
13118            let mut vbuf = vec![0f32; b * nkv * hd];
13119            if !crate::gpu_metal::kv_mirror_read_rows(
13120                self.graph_kv_id,
13121                *li,
13122                nkv,
13123                hd,
13124                *cpu_stored,
13125                b,
13126                &mut kbuf,
13127                &mut vbuf,
13128            ) {
13129                METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13130                return MetalPrefillOutcome::Failed;
13131            }
13132            rows.push((*li, *cpu_stored, kbuf, vbuf));
13133        }
13134        for (li, cpu_stored, kbuf, vbuf) in rows {
13135            let cache = &mut self.kv_cache.layers[li];
13136            for r in 0..b {
13137                cache.append(
13138                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13139                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13140                    &[],
13141                );
13142            }
13143            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
13144        }
13145        METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
13146        if with_head {
13147            METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
13148        }
13149        MetalPrefillOutcome::Completed(hiddens)
13150    }
13151
13152    #[cfg(target_os = "macos")]
13153    fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
13154        self.prefill_rows_metal(ids, start_pos, None)
13155    }
13156
13157    /// Exact teacher-forced NLL through the ordinary Metal rows graph.  This
13158    /// is intentionally separate from the serial TokenGraph scorer: every
13159    /// chunk owns a real b-row graph/head completion and the recurrent/KV
13160    /// handoff is committed before the next chunk begins.
13161    #[cfg(target_os = "macos")]
13162    fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
13163        if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
13164            return MetalBatchNllOutcome::Declined;
13165        }
13166        let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
13167            return MetalBatchNllOutcome::Declined;
13168        };
13169        let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
13170            .ok()
13171            .and_then(|v| v.parse::<usize>().ok())
13172            .filter(|&v| (1..=512).contains(&v))
13173            .unwrap_or(32);
13174        let final_norm = self.weights.final_norm.clone();
13175        let mut nll = 0.0f64;
13176        let mut count = 0usize;
13177        let mut pos = 0usize;
13178        let mut completed = 0usize;
13179        while pos < ids.len() {
13180            let end = (pos + chunk).min(ids.len());
13181            let mut logits = Vec::new();
13182            let outcome =
13183                self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
13184            match outcome {
13185                MetalPrefillOutcome::Declined => {
13186                    return if completed == 0 {
13187                        MetalBatchNllOutcome::Declined
13188                    } else {
13189                        MetalBatchNllOutcome::Failed(format!(
13190                            "ordinary Metal NLL batch declined after {completed} chunks"
13191                        ))
13192                    };
13193                }
13194                MetalPrefillOutcome::Failed => {
13195                    return MetalBatchNllOutcome::Failed(
13196                        "ordinary Metal NLL batch failed after admission".to_string(),
13197                    );
13198                }
13199                MetalPrefillOutcome::Completed(_) => {}
13200            }
13201            completed += 1;
13202            let vocab = self.vocab_size.min(lm.1);
13203            if logits.len() != (end - pos) * lm.1 || vocab == 0 {
13204                return MetalBatchNllOutcome::Failed(
13205                    "ordinary Metal NLL head returned an invalid shape".to_string(),
13206                );
13207            }
13208            for row in 0..(end - pos) {
13209                let absolute = pos + row;
13210                if absolute < start || absolute + 1 >= ids.len() {
13211                    continue;
13212                }
13213                let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
13214                if let Some(mu) = self.logit_multiplier {
13215                    for v in lg.iter_mut() {
13216                        *v *= mu;
13217                    }
13218                }
13219                if let Some(c) = self.final_softcap {
13220                    softcap_in_place(lg, c, self.pool.as_deref());
13221                }
13222                let target = ids[absolute + 1] as usize;
13223                if target >= vocab {
13224                    return MetalBatchNllOutcome::Failed(format!(
13225                        "target token {target} exceeds Metal head rows {vocab}"
13226                    ));
13227                }
13228                let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
13229                let lse: f64 = lg
13230                    .iter()
13231                    .map(|&v| ((v - max) as f64).exp())
13232                    .sum::<f64>()
13233                    .ln()
13234                    + max as f64;
13235                nll += lse - lg[target] as f64;
13236                count += 1;
13237            }
13238            pos = end;
13239        }
13240        MetalBatchNllOutcome::Completed(nll, count)
13241    }
13242
13243    /// Commit a Metal verify round: replay the GDN recurrences over the
13244    /// `a + 1` accepted positions into the CPU states, append the accepted
13245    /// K/V rows from the mirrors to the CPU caches, re-point the mirrors.
13246    #[cfg(target_os = "macos")]
13247    fn metal_verify_commit(&mut self, a: usize) -> bool {
13248        let Some(mut pending) = self.metal_verify.take() else {
13249            return false;
13250        };
13251        let n = a + 1;
13252        // encode order == ascending layer order (the plan walks 0..layers)
13253        let idxs = pending.gdn_layers.clone();
13254        let mut outs: Vec<&mut [f32]> = self
13255            .kv_cache
13256            .layers
13257            .iter_mut()
13258            .enumerate()
13259            .filter(|(i, _)| idxs.binary_search(i).is_ok())
13260            .map(|(_, l)| l.linear_state.as_mut_slice())
13261            .collect();
13262        if !pending.graph.commit(n, &mut outs) {
13263            return false;
13264        }
13265        spec_stamp("c.replay");
13266        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13267        // Read every layer before mutating any CPU cache.  Missing rows are
13268        // terminal after the replay has executed; never append a partial KV
13269        // prefix and continue on a serial path.
13270        let mut rows = Vec::with_capacity(pending.attn_layers.len());
13271        for (li, cpu_stored) in &pending.attn_layers {
13272            let mut kbuf = vec![0f32; n * nkv * hd];
13273            let mut vbuf = vec![0f32; n * nkv * hd];
13274            if !crate::gpu_metal::kv_mirror_read_rows(
13275                self.graph_kv_id,
13276                *li,
13277                nkv,
13278                hd,
13279                *cpu_stored,
13280                n,
13281                &mut kbuf,
13282                &mut vbuf,
13283            ) {
13284                return false;
13285            }
13286            rows.push((*li, *cpu_stored, kbuf, vbuf));
13287        }
13288        for (li, cpu_stored, kbuf, vbuf) in rows {
13289            let cache = &mut self.kv_cache.layers[li];
13290            for r in 0..n {
13291                cache.append(
13292                    &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13293                    &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13294                    &[],
13295                );
13296            }
13297            crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
13298        }
13299        spec_stamp("c.kv");
13300        true
13301    }
13302
13303    /// The round's warm-ups as ONE b-row graph run over the MTP block on
13304    /// Metal: `pairs` = (trunk hidden, next token) at consecutive positions
13305    /// from `first_pos`; the block's input projection is folded in. This
13306    /// half encodes and SUBMITS (no wait); `mtp_warm_batch_finish` waits
13307    /// and pulls the appended K/V rows into the CPU MTP cache. None = the
13308    /// graph declined (nothing submitted, nothing appended).
13309    #[cfg(target_os = "macos")]
13310    fn mtp_warm_batch_submit(
13311        &mut self,
13312        m: &mut MtpModule,
13313        pairs: &[(&[f32], u32)],
13314        first_pos: usize,
13315    ) -> Option<MetalWarmPending> {
13316        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
13317        let b = pairs.len();
13318        if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
13319            return None;
13320        }
13321        let AttnKind::Full {
13322            wq,
13323            wk,
13324            wv,
13325            wo,
13326            q_norm,
13327            k_norm,
13328            output_gate,
13329            softplus_gate: None,
13330            bias: None,
13331        } = &m.layer.attn
13332        else {
13333            return None;
13334        };
13335        let FfnKind::Dense(d) = &m.layer.ffn else {
13336            return None;
13337        };
13338        if !d.segs.is_empty() {
13339            return None;
13340        }
13341        let (Some(pq), Some(pk), Some(pv), Some(po)) =
13342            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13343        else {
13344            return None;
13345        };
13346        let (Some(g), Some(u), Some(dn)) = (
13347            d.gate_proj.q1_parts(),
13348            d.up_proj.q1_parts(),
13349            d.down_proj.q1_parts(),
13350        ) else {
13351            return None;
13352        };
13353        let Some(eh) = m.eh_proj.q1_parts() else {
13354            return None;
13355        };
13356        let QTensor::Mapped { model, .. } = wq else {
13357            return None;
13358        };
13359        let model = model.clone();
13360        let hs = self.hidden_size;
13361        // [enorm(embed(tok)); hnorm(hidden)] rows
13362        let mut cat = vec![0f32; b * 2 * hs];
13363        for (j, (h, tok)) in pairs.iter().enumerate() {
13364            let e = self.embed_single(*tok);
13365            let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
13366            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
13367            inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
13368        }
13369        let dims = GraphDims {
13370            hidden: hs,
13371            eps: self.rms_eps as f32,
13372            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13373        };
13374        spec_stamp("w.cat");
13375        let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
13376            return None;
13377        };
13378        spec_stamp("w.new");
13379        let l = AttnGpuLayer {
13380            attn_norm: &m.layer.input_norm,
13381            post_norm: &m.layer.post_norm,
13382            wq: pq,
13383            wk: pk,
13384            wv: pv,
13385            wo: po,
13386            ffn: MetalFfn::Dense {
13387                gate: g,
13388                up: u,
13389                down: dn,
13390                act: (d.act == Act::Gelu) as u8,
13391            },
13392            attn_out_norm: None,
13393            ffn_out_norm: None,
13394            layer_scale: None,
13395        };
13396        let (nh, nkv, hd, rd) = (
13397            self.num_heads,
13398            self.num_kv_heads,
13399            self.head_dim,
13400            self.rotary_dim,
13401        );
13402        let inv_freq = self.inv_freq.clone();
13403        let cpu_stored;
13404        {
13405            let cache = &m.kv;
13406            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13407            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13408            cpu_stored = cpu_k[0].len() / hd;
13409            // The cache may LAG the position (rows nobody warmed): the
13410            // pairs land at cpu_stored.. with their true RoPE positions
13411            // first_pos.., exactly what the one-by-one warm does. A cache
13412            // AHEAD of the position is a real inconsistency.
13413            if cpu_stored > first_pos {
13414                spec_stamp("w.decl");
13415                return None;
13416            }
13417            let p = AttnDeviceParams {
13418                kv_id: self.mtp_kv_id(),
13419                layer: Self::MTP_LAYER_BASE,
13420                nh,
13421                nkv,
13422                hd,
13423                rd,
13424                position: first_pos,
13425                scale: self.attn_scale,
13426                eps: self.rms_eps as f32,
13427                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13428                late_qk_norm: self.qk_norm_after_rope,
13429                output_gate: *output_gate,
13430                q_norm: q_norm.as_deref(),
13431                k_norm: k_norm.as_deref(),
13432                inv_freq: &inv_freq,
13433                rope_scale: self.rope_scale,
13434                cpu_k,
13435                cpu_v,
13436                cpu_stored,
13437                cpu_gen: cache.generation(),
13438                o1: None,
13439                window: None,
13440                softmax_sink: None,
13441                head_gate: None,
13442                v_norm: false,
13443            };
13444            if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
13445                return None;
13446            }
13447        }
13448        spec_stamp("w.enc");
13449        if !graph.submit() {
13450            return None;
13451        }
13452        spec_stamp("w.sub");
13453        Some(MetalWarmPending {
13454            graph,
13455            cpu_stored,
13456            b,
13457        })
13458    }
13459
13460    /// Submit and finish in one call (the prefill's MTP warm-up, where
13461    /// nothing runs in between).
13462    #[cfg(target_os = "macos")]
13463    fn mtp_warm_batch_metal(
13464        &mut self,
13465        m: &mut MtpModule,
13466        pairs: &[(&[f32], u32)],
13467        first_pos: usize,
13468    ) -> bool {
13469        match self.mtp_warm_batch_submit(m, pairs, first_pos) {
13470            Some(p) => self.mtp_warm_batch_finish(m, p),
13471            None => false,
13472        }
13473    }
13474
13475    /// Second half of the batched warm-up: wait for the submitted graph,
13476    /// pull its b appended K/V rows into the CPU MTP cache, re-point the
13477    /// mirror. False = the command buffer failed or the rows are missing
13478    /// (nothing appended; the caller falls back to the one-by-one warm).
13479    #[cfg(target_os = "macos")]
13480    fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
13481        let MetalWarmPending {
13482            mut graph,
13483            cpu_stored,
13484            b,
13485        } = pending;
13486        let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13487        if !graph.sync() {
13488            return false;
13489        }
13490        spec_stamp("w.gpu");
13491        let mut kbuf = vec![0f32; b * nkv * hd];
13492        let mut vbuf = vec![0f32; b * nkv * hd];
13493        if !crate::gpu_metal::kv_mirror_read_rows(
13494            self.mtp_kv_id(),
13495            Self::MTP_LAYER_BASE,
13496            nkv,
13497            hd,
13498            cpu_stored,
13499            b,
13500            &mut kbuf,
13501            &mut vbuf,
13502        ) {
13503            return false;
13504        }
13505        for r in 0..b {
13506            m.kv.append(
13507                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13508                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13509                &[],
13510            );
13511        }
13512        crate::gpu_metal::kv_mirror_set_stored(
13513            self.mtp_kv_id(),
13514            Self::MTP_LAYER_BASE,
13515            cpu_stored + b,
13516        );
13517        spec_stamp("w.kv");
13518        true
13519    }
13520
13521    /// A committed token id from the high table (Cyrillic, CJK and the
13522    /// like sit above 131072 in Qwen's vocabulary; Latin subwords past
13523    /// the 65536 cut are rare enough to lose as rejected drafts) switches
13524    /// the draft to the full head for the next 16 tokens; other ids count
13525    /// down. On an M4 the full 660 MB head costs 5.5 ms a draft step
13526    /// against 1.4 for the shortlist, so the streak is kept short.
13527    pub(crate) fn note_draft_id(&mut self, id: u32) {
13528        let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
13529        if (id as usize) >= cut {
13530            self.draft_full_streak = 16;
13531        } else {
13532            self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
13533        }
13534    }
13535
13536    /// The draft head's rows for the next step: the shortlist, or the full
13537    /// head while `draft_full_streak` runs.
13538    fn draft_head_rows(&self, head_rows: usize) -> usize {
13539        if self.draft_full_streak > 0 {
13540            head_rows
13541        } else {
13542            Self::draft_vocab_rows(head_rows)
13543        }
13544    }
13545
13546    /// Draft-head shortlist size: `CMF_DRAFT_VOCAB` rows (default 65536,
13547    /// capped at the head; 0 = full head).
13548    fn draft_vocab_rows(head_rows: usize) -> usize {
13549        static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13550        let n = *N.get_or_init(|| {
13551            std::env::var("CMF_DRAFT_VOCAB")
13552                .ok()
13553                .and_then(|v| v.parse().ok())
13554                .unwrap_or(65536)
13555        });
13556        if n == 0 { head_rows } else { n.min(head_rows) }
13557    }
13558
13559    /// One MTP block step on the native Metal token graph: block input on
13560    /// the host, the attention layer + FFN device-resident over the MTP
13561    /// mirror, the head folded in when `want_logits`. The appended K/V row
13562    /// is pulled into the CPU MTP cache (owner of record) after the sync.
13563    #[cfg(target_os = "macos")]
13564    fn mtp_step_metal(
13565        &mut self,
13566        m: &mut MtpModule,
13567        hidden: &[f32],
13568        next_token: u32,
13569        position: usize,
13570        want_logits: bool,
13571    ) -> Option<(Vec<f32>, Vec<f32>)> {
13572        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13573        if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13574            || !crate::gpu::q1_force()
13575            || !crate::gpu::enabled_here()
13576            || self.attn_softcap > 0.0
13577            || self.attention_heads_per_layer.is_some()
13578            || m.kv.mode != crate::kv_cache::KvMode::F32
13579            || m.kv.o1.is_some()
13580        {
13581            return None;
13582        }
13583        let AttnKind::Full {
13584            wq,
13585            wk,
13586            wv,
13587            wo,
13588            q_norm,
13589            k_norm,
13590            output_gate,
13591            softplus_gate: None,
13592            bias: None,
13593        } = &m.layer.attn
13594        else {
13595            return None;
13596        };
13597        let FfnKind::Dense(d) = &m.layer.ffn else {
13598            return None;
13599        };
13600        if d.act != Act::Silu || !d.segs.is_empty() {
13601            return None;
13602        }
13603        let (pq, pk, pv, po) = (
13604            wq.q1_parts()?,
13605            wk.q1_parts()?,
13606            wv.q1_parts()?,
13607            wo.q1_parts()?,
13608        );
13609        let (g, u, dn) = (
13610            d.gate_proj.q1_parts()?,
13611            d.up_proj.q1_parts()?,
13612            d.down_proj.q1_parts()?,
13613        );
13614        let QTensor::Mapped { model, .. } = wq else {
13615            return None;
13616        };
13617        let model = model.clone();
13618        let lm = if want_logits {
13619            Some(self.weights.lm_head.q1_parts()?)
13620        } else {
13621            None
13622        };
13623        let dims = GraphDims {
13624            hidden: self.hidden_size,
13625            eps: self.rms_eps as f32,
13626            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13627        };
13628        // The block input `eh_proj · [enorm(e); hnorm(h)]` rides in the
13629        // graph (one submit a step); the host per-op matvec if it cannot.
13630        let hs = self.hidden_size;
13631        let mut x = vec![0f32; hs];
13632        let mut graph = TokenGraph::new(&model, dims, &x)?;
13633        let mut folded = false;
13634        if let Some(eh) = m.eh_proj.q1_parts() {
13635            let e = self.embed_single(next_token);
13636            let mut cat = vec![0.0f32; 2 * hs];
13637            let (cat_e, cat_h) = cat.split_at_mut(hs);
13638            inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
13639            inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
13640            folded = graph.encode_input_proj(eh, &cat);
13641        }
13642        if !folded {
13643            x = self.mtp_block_input(m, hidden, next_token);
13644            graph = TokenGraph::new(&model, dims, &x)?;
13645        }
13646        spec_stamp("d.in");
13647        let l = AttnGpuLayer {
13648            attn_norm: &m.layer.input_norm,
13649            post_norm: &m.layer.post_norm,
13650            wq: pq,
13651            wk: pk,
13652            wv: pv,
13653            wo: po,
13654            ffn: MetalFfn::Dense {
13655                gate: g,
13656                up: u,
13657                down: dn,
13658                act: (d.act == Act::Gelu) as u8,
13659            },
13660            attn_out_norm: None,
13661            ffn_out_norm: None,
13662            layer_scale: None,
13663        };
13664        let (nh, nkv, hd, rd) = (
13665            self.num_heads,
13666            self.num_kv_heads,
13667            self.head_dim,
13668            self.rotary_dim,
13669        );
13670        let inv_freq = self.inv_freq.clone();
13671        {
13672            let cache = &m.kv;
13673            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13674            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13675            let cpu_stored = cpu_k[0].len() / hd;
13676            let p = AttnDeviceParams {
13677                kv_id: self.mtp_kv_id(),
13678                layer: Self::MTP_LAYER_BASE,
13679                nh,
13680                nkv,
13681                hd,
13682                rd,
13683                position,
13684                scale: self.attn_scale,
13685                eps: self.rms_eps as f32,
13686                gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13687                late_qk_norm: self.qk_norm_after_rope,
13688                output_gate: *output_gate,
13689                q_norm: q_norm.as_deref(),
13690                k_norm: k_norm.as_deref(),
13691                inv_freq: &inv_freq,
13692                rope_scale: self.rope_scale,
13693                cpu_k,
13694                cpu_v,
13695                cpu_stored,
13696                cpu_gen: cache.generation(),
13697                o1: None,
13698                window: None,
13699                softmax_sink: None,
13700                head_gate: None,
13701                v_norm: false,
13702            };
13703            if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13704                return None;
13705            }
13706        }
13707        // The draft's head over a vocabulary SHORTLIST (the first
13708        // CMF_DRAFT_VOCAB rows — BPE ids run roughly by merge rank, so the
13709        // low ids carry the mass): the verify keeps the full head, so a true
13710        // token past the cut is only a rejected draft, never a wrong token.
13711        // 662 MB a step on Qwen3.8 becomes 170 MB at 65536.
13712        let draft_rows = if let Some(lm) = lm {
13713            self.draft_head_rows(lm.1)
13714        } else {
13715            0
13716        };
13717        if let Some(lm) = lm {
13718            if !graph.lm_head_ok(lm) {
13719                return None;
13720            }
13721            if draft_rows < lm.1 {
13722                if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13723                    return None;
13724                }
13725            } else {
13726                graph.encode_lm_head(&m.final_norm, lm);
13727            }
13728        }
13729        spec_stamp("d.enc");
13730        if graph.sync_checked().is_err() {
13731            return None;
13732        }
13733        spec_stamp("d.gpu");
13734        let mut logits = Vec::new();
13735        if let Some(lm) = lm {
13736            let n_read = draft_rows.min(lm.1).min(self.vocab_size);
13737            logits = attention::take_buf(n_read);
13738            graph.read_logits(&mut logits);
13739            // ids past the shortlist: never drafted (−∞ in every chain)
13740            logits.resize(self.vocab_size, f32::NEG_INFINITY);
13741        }
13742        graph.finish(&mut x);
13743        let mut krow = attention::take_buf(nkv * hd);
13744        let mut vrow = attention::take_buf(nkv * hd);
13745        if crate::gpu_metal::kv_mirror_read_last(
13746            self.mtp_kv_id(),
13747            Self::MTP_LAYER_BASE,
13748            nkv,
13749            hd,
13750            &mut krow,
13751            &mut vrow,
13752        ) {
13753            m.kv.append(&krow, &vrow, &[]);
13754        }
13755        attention::recycle_buf(&mut krow);
13756        attention::recycle_buf(&mut vrow);
13757        spec_stamp("d.rd");
13758        Some((logits, x))
13759    }
13760
13761    /// `CMF_MTP_CHAIN=0` keeps the per-step draft (one submit and one
13762    /// host round trip per MTP step); the default drafts the whole chain
13763    /// in one command buffer when the round is plain greedy.
13764    ///
13765    /// Measured on an M4 (24 GB), Qwen3.8-27B q4tp, P3 at 160 tokens,
13766    /// k=7, six runs per arm alternating inside one lock window — the
13767    /// round's draft phase (median over the 34 rounds of a run) is
13768    /// 34.5 ms per round old against 30.1 new, i.e. 4.93 → 4.31 ms per
13769    /// draft step. That is the whole prize: the 7 submits cost ~0.6 ms
13770    /// each in host and submit latency and nothing else changes —
13771    /// acceptance (3.41 of 7) and tokens per round (4.41) are identical,
13772    /// and the round is 289 → 285 ms, decode 13.8 → 14.0 tok/s.
13773    fn mtp_chain_on() -> bool {
13774        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13775        *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
13776    }
13777
13778    /// The round's k greedy drafts as ONE command buffer on Metal: the MTP
13779    /// block k times back to back, each step's token embedding gathered
13780    /// on the device from the argmax the step before it wrote, the head
13781    /// over the round's shortlist (or the full head during a full-head
13782    /// streak — decided once, before the chain, exactly as the per-step
13783    /// path decides it per step, since `draft_full_streak` only moves on
13784    /// a commit). One wait, then the k ids and the k appended K/V rows
13785    /// come back; the CPU MTP cache ends where k `mtp_step_metal` calls
13786    /// would have left it. `Err(false)` = declined before anything was
13787    /// committed (the per-step path takes the round); `Err(true)` = the
13788    /// command buffer failed after commit.
13789    #[cfg(target_os = "macos")]
13790    fn mtp_draft_chain_metal(
13791        &mut self,
13792        m: &mut MtpModule,
13793        hidden: &[f32],
13794        t_next: u32,
13795        position: usize,
13796        k: usize,
13797    ) -> Result<Vec<u32>, bool> {
13798        use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13799        if k == 0
13800            || k > 64
13801            || !Self::mtp_chain_on()
13802            || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13803            || !crate::gpu::q1_force()
13804            || !crate::gpu::enabled_here()
13805            || self.attn_softcap > 0.0
13806            || self.attention_heads_per_layer.is_some()
13807            || m.kv.mode != crate::kv_cache::KvMode::F32
13808            || m.kv.o1.is_some()
13809            // the chain gathers embeddings itself: only the plain table
13810            || self.dsv4.is_some()
13811            || self.dsv41.is_some()
13812            || self.qwen4_exp.is_some()
13813            || self.g3n.is_some()
13814        {
13815            return Err(false);
13816        }
13817        let AttnKind::Full {
13818            wq,
13819            wk,
13820            wv,
13821            wo,
13822            q_norm,
13823            k_norm,
13824            output_gate,
13825            softplus_gate: None,
13826            bias: None,
13827        } = &m.layer.attn
13828        else {
13829            return Err(false);
13830        };
13831        let FfnKind::Dense(d) = &m.layer.ffn else {
13832            return Err(false);
13833        };
13834        if d.act != Act::Silu || !d.segs.is_empty() {
13835            return Err(false);
13836        }
13837        let (Some(pq), Some(pk), Some(pv), Some(po)) =
13838            (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13839        else {
13840            return Err(false);
13841        };
13842        let (Some(g), Some(u), Some(dn)) = (
13843            d.gate_proj.q1_parts(),
13844            d.up_proj.q1_parts(),
13845            d.down_proj.q1_parts(),
13846        ) else {
13847            return Err(false);
13848        };
13849        let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
13850            return Err(false);
13851        };
13852        let QTensor::Mapped { model, .. } = wq else {
13853            return Err(false);
13854        };
13855        let model = model.clone();
13856        // the embedding table: a q4tp tensor of the SAME blob, no Prism
13857        // inverse-embedding post-pass
13858        let QTensor::Mapped {
13859            model: em,
13860            idx: eidx,
13861            dtype: cortiq_core::TensorDtype::Q4TiledP,
13862            ..
13863        } = &self.weights.embed_tokens
13864        else {
13865            return Err(false);
13866        };
13867        if !std::sync::Arc::ptr_eq(em, &model)
13868            || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
13869        {
13870            return Err(false);
13871        }
13872        let embed = (
13873            *eidx,
13874            self.weights.embed_tokens.rows(),
13875            self.weights.embed_tokens.cols(),
13876        );
13877        if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
13878            return Err(false);
13879        }
13880        let dims = GraphDims {
13881            hidden: self.hidden_size,
13882            eps: self.rms_eps as f32,
13883            gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13884        };
13885        let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
13886            return Err(false);
13887        };
13888        if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
13889            return Err(false);
13890        }
13891        let l = AttnGpuLayer {
13892            attn_norm: &m.layer.input_norm,
13893            post_norm: &m.layer.post_norm,
13894            wq: pq,
13895            wk: pk,
13896            wv: pv,
13897            wo: po,
13898            ffn: MetalFfn::Dense {
13899                gate: g,
13900                up: u,
13901                down: dn,
13902                act: (d.act == Act::Gelu) as u8,
13903            },
13904            attn_out_norm: None,
13905            ffn_out_norm: None,
13906            layer_scale: None,
13907        };
13908        let (nh, nkv, hd, rd) = (
13909            self.num_heads,
13910            self.num_kv_heads,
13911            self.head_dim,
13912            self.rotary_dim,
13913        );
13914        let inv_freq = self.inv_freq.clone();
13915        let draft_rows = self.draft_head_rows(lm.1);
13916        let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
13917        if n_arg == 0 {
13918            return Err(false);
13919        }
13920        // `CMF_MTP_CHAIN_SPLIT=1` commits each step as it is encoded, so
13921        // the GPU starts on step 0 while the host is still encoding step
13922        // 1 — a probe for whether the host encode is on the critical
13923        // path. It is not: three runs each, draft 30.0 ms per round split
13924        // against 30.1 whole, and the whole chain's host encode measures
13925        // 0.3 ms against a 29.7 ms wait. Kept as a probe, off by default.
13926        let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
13927        let t_chain = std::time::Instant::now();
13928        graph.chain_ids_init(t_next, k);
13929        let cpu_stored;
13930        {
13931            let cache = &m.kv;
13932            let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13933            let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13934            cpu_stored = cpu_k[0].len() / hd;
13935            for j in 0..k {
13936                if !graph.encode_chain_input(
13937                    embed,
13938                    j as u32,
13939                    &m.enorm,
13940                    &m.hnorm,
13941                    self.embed_multiplier,
13942                    eh,
13943                ) {
13944                    return Err(false);
13945                }
13946                // step j's mirror row: the mirror is re-pointed at the CPU
13947                // rows before step 0 and advances by one per step; its
13948                // resync (never taken past step 0) reads the CPU rows
13949                let p = AttnDeviceParams {
13950                    kv_id: self.mtp_kv_id(),
13951                    layer: Self::MTP_LAYER_BASE,
13952                    nh,
13953                    nkv,
13954                    hd,
13955                    rd,
13956                    position: position + j,
13957                    scale: self.attn_scale,
13958                    eps: self.rms_eps as f32,
13959                    gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13960                    late_qk_norm: self.qk_norm_after_rope,
13961                    output_gate: *output_gate,
13962                    q_norm: q_norm.as_deref(),
13963                    k_norm: k_norm.as_deref(),
13964                    inv_freq: &inv_freq,
13965                    rope_scale: self.rope_scale,
13966                    cpu_k: cpu_k.clone(),
13967                    cpu_v: cpu_v.clone(),
13968                    cpu_stored: cpu_stored + j,
13969                    cpu_gen: cache.generation(),
13970                    o1: None,
13971                    window: None,
13972                    softmax_sink: None,
13973                    head_gate: None,
13974                    v_norm: false,
13975                };
13976                if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13977                    return Err(false);
13978                }
13979                if draft_rows < lm.1 {
13980                    if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13981                        return Err(false);
13982                    }
13983                } else {
13984                    graph.encode_lm_head(&m.final_norm, lm);
13985                }
13986                if !graph.encode_argmax(n_arg, j as u32 + 1) {
13987                    return Err(false);
13988                }
13989                if split {
13990                    // CMF_MTP_CHAIN_SPLIT=1: commit every step so the GPU
13991                    // starts on step 0 while the host encodes the rest
13992                    graph.commit();
13993                }
13994            }
13995        }
13996        let t_enc = t_chain.elapsed();
13997        if graph.sync_checked().is_err() {
13998            return Err(true);
13999        }
14000        if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
14001            eprintln!(
14002                "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
14003                t_enc.as_secs_f64() * 1e3,
14004                (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
14005                if split { ", split" } else { "" }
14006            );
14007        }
14008        let mut ids = vec![0u32; k];
14009        if !graph.chain_ids_read(&mut ids) {
14010            return Err(true);
14011        }
14012        let mut kbuf = vec![0f32; k * nkv * hd];
14013        let mut vbuf = vec![0f32; k * nkv * hd];
14014        if !crate::gpu_metal::kv_mirror_read_rows(
14015            self.mtp_kv_id(),
14016            Self::MTP_LAYER_BASE,
14017            nkv,
14018            hd,
14019            cpu_stored,
14020            k,
14021            &mut kbuf,
14022            &mut vbuf,
14023        ) {
14024            return Err(true);
14025        }
14026        for r in 0..k {
14027            m.kv.append(
14028                &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14029                &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14030                &[],
14031            );
14032        }
14033        Ok(ids)
14034    }
14035
14036    fn try_batch_graph_wgpu(
14037        &self,
14038        hiddens: &mut [f32],
14039        positions: &[usize],
14040        k: usize,
14041        spec: Option<crate::gpu::SpecTail<'_>>,
14042    ) -> crate::gpu::BatchGraphOutcome {
14043        self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
14044    }
14045
14046    /// `try_batch_graph_wgpu` with the device-prefix mode: `layers_run`
14047    /// Some lets a stack that does not fit run its leading layers (the
14048    /// token graph's prefix rule) and reports how many; `hiddens` then
14049    /// holds the boundary rows and the caller runs the rest on the host.
14050    fn try_batch_graph_wgpu_prefix(
14051        &self,
14052        hiddens: &mut [f32],
14053        positions: &[usize],
14054        k: usize,
14055        spec: Option<crate::gpu::SpecTail<'_>>,
14056        layers_run: Option<&mut usize>,
14057    ) -> crate::gpu::BatchGraphOutcome {
14058        let graph_end = match self.mimo_moe.graph_prefix_end() {
14059            Some(end) if end < self.num_layers => {
14060                if layers_run.is_none() || spec.is_some() || end == 0 {
14061                    return crate::gpu::BatchGraphOutcome::Declined;
14062                }
14063                end
14064            }
14065            _ => self.num_layers,
14066        };
14067        let _tb = std::time::Instant::now();
14068        let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
14069        if self.attn_softcap > 0.0 {
14070            return crate::gpu::BatchGraphOutcome::Declined; // capped scores: no graph kernel — CPU path
14071        }
14072        // Same attention contract as the token graph: per-layer geometry
14073        // rides `geom`, anything it cannot express declines by name.
14074        if let Some(reason) = self
14075            .wgpu_graph_attn_decline()
14076            .or_else(|| self.batch_graph_gemma_decline())
14077        {
14078            self.note_graph_decline("wgpu batch graph", reason);
14079            return crate::gpu::BatchGraphOutcome::Declined;
14080        }
14081        let nh = self.num_heads;
14082        let (nkv, hd, rd) = self.layer_geom(0);
14083        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14084        fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
14085            if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
14086            {
14087                let name = &m.tensors[i].name;
14088                let prism = if crate::prism::is_inverse_embedding(m, name) {
14089                    crate::gpu::GraphPrismOp::InverseEmbedding
14090                } else if crate::prism::is_forward_weight(m, name) {
14091                    crate::gpu::GraphPrismOp::Forward
14092                } else {
14093                    crate::gpu::GraphPrismOp::None
14094                };
14095                return Some(crate::gpu::GraphW {
14096                    idx: i,
14097                    kind,
14098                    row_scale: rs,
14099                    data: &[],
14100                    prism,
14101                    affine: crate::prism::is_affine_target(m, name),
14102                });
14103            }
14104            if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
14105                eprintln!(
14106                    "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
14107                    t.rows(),
14108                    t.cols()
14109                );
14110            }
14111            t.as_f32().map(|d| crate::gpu::GraphW {
14112                idx: 0,
14113                kind: 4,
14114                row_scale: &[],
14115                data: d,
14116                prism: crate::gpu::GraphPrismOp::None,
14117                affine: false,
14118            })
14119        }
14120        let built: Option<(
14121            Vec<crate::gpu::GraphLayer<'_>>,
14122            std::sync::Arc<cortiq_core::CmfModel>,
14123        )> = (|| {
14124            let mut layers = Vec::with_capacity(graph_end);
14125            let mut model = None;
14126            for li in 0..graph_end {
14127                let lw = &self.weights.layers[self.phys_layer(li)];
14128                // MoE routes per token, so its experts are encoded token by
14129                // token inside the batched submit while attention and the
14130                // projections stay GEMMs. Refusing MoE here is what left
14131                // prefill running one position at a time: 33 tok/s against
14132                // 54 on decode, i.e. reading the prompt was slower than
14133                // writing the answer.
14134                let gffn = match &lw.ffn {
14135                    FfnKind::Dense(d) if !d.segs.is_empty() => {
14136                        if batch_debug {
14137                            eprintln!("batch graph: dense segmented FFN at layer {li}");
14138                        }
14139                        return None;
14140                    }
14141                    FfnKind::Dense(d) => {
14142                        let Some(act) = d.act.graph_act() else {
14143                            if batch_debug {
14144                                eprintln!(
14145                                    "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
14146                                    d.act
14147                                );
14148                            }
14149                            return None;
14150                        };
14151                        crate::gpu::GraphFfn::Dense {
14152                            gate: gw(&d.gate_proj)?,
14153                            up: gw(&d.up_proj)?,
14154                            down: gw(&d.down_proj)?,
14155                            act,
14156                        }
14157                    }
14158                    FfnKind::Moe(m) => {
14159                        // Adaptive τ and expert masks stay on the CPU path.
14160                        // Sigmoid scores, the selection bias, a routed scale
14161                        // ≠ 1 and an ungated shared expert (hy_v3) ride the
14162                        // same flags word as the token graph — before, this
14163                        // refusal sent every Hy-MT2-30B prompt to the chunked
14164                        // fallback (8 tok/s of ingest against 53 of decode).
14165                        if m.route_tau.is_some() || m.mask.is_some() {
14166                            return None;
14167                        }
14168                        // A shared expert rides as slot top_k (gated or
14169                        // not is a flag on the select kernel); without one
14170                        // (MiMo-V2, LFM2-MoE) the kernels run top_k slots.
14171                        let shared = m.shared.as_ref();
14172                        let has_shared = shared.is_some();
14173                        let shared_gated = matches!(shared, Some((_, Some(_))));
14174                        let sgate = match shared {
14175                            Some((_, Some(sg))) => gw(sg)?,
14176                            // Ungated or absent: the router plane stands in
14177                            // so the plumbing stays total; the kernel pins
14178                            // weight 1 or never reads it.
14179                            _ => gw(&m.router)?,
14180                        };
14181                        let router = gw(&m.router)?;
14182                        // The batch MoE kernels still consume raw per-token
14183                        // rows and do not carry the descriptor-aware Prism
14184                        // transform/affine bit for router or shared-gate
14185                        // planes.  Refuse rather than route an untransformed
14186                        // source activation.
14187                        if router.prism != crate::gpu::GraphPrismOp::None
14188                            || router.affine
14189                            || sgate.prism != crate::gpu::GraphPrismOp::None
14190                            || sgate.affine
14191                        {
14192                            return None;
14193                        }
14194                        let inter = m.experts.first()?.gate_proj.rows();
14195                        let mut experts = Vec::with_capacity(m.experts.len() + 1);
14196                        let mut q4tp: Option<bool> = None;
14197                        let mut gu_q2: Option<bool> = None;
14198                        let mut q82: Option<bool> = None;
14199                        for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
14200                            if !matches!(e.act, Act::Silu)
14201                                || e.gate_proj.rows() != inter
14202                                || e.up_proj.rows() != inter
14203                            {
14204                                return None;
14205                            }
14206                            // Same ladder as the token graph: q4t → q2tp
14207                            // (mixed profile: 2-bit gate/up over a q4tp
14208                            // down) → q4tp → q8_2f. Uniform across the layer.
14209                            let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
14210                            {
14211                                Some((mm, gi)) => (
14212                                    mm,
14213                                    gi,
14214                                    e.up_proj.mapped_q4t()?.1,
14215                                    e.down_proj.mapped_q4t()?.1,
14216                                    false,
14217                                    false,
14218                                    false,
14219                                ),
14220                                None => match e.gate_proj.mapped_q2tp() {
14221                                    Some((mm, gi)) => (
14222                                        mm,
14223                                        gi,
14224                                        e.up_proj.mapped_q2tp()?.1,
14225                                        e.down_proj.mapped_q4tp()?.1,
14226                                        true,
14227                                        true,
14228                                        false,
14229                                    ),
14230                                    None => match e.gate_proj.mapped_q4tp() {
14231                                        Some((mm, gi)) => (
14232                                            mm,
14233                                            gi,
14234                                            e.up_proj.mapped_q4tp()?.1,
14235                                            e.down_proj.mapped_q4tp()?.1,
14236                                            true,
14237                                            false,
14238                                            false,
14239                                        ),
14240                                        None => {
14241                                            if !crate::gpu::moe_q82_graph_on() {
14242                                                return None;
14243                                            }
14244                                            let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14245                                            (
14246                                                mm,
14247                                                gi,
14248                                                e.up_proj.mapped_q8_2f()?.1,
14249                                                e.down_proj.mapped_q8_2f()?.1,
14250                                                false,
14251                                                false,
14252                                                true,
14253                                            )
14254                                        }
14255                                    },
14256                                },
14257                            };
14258                            if *q4tp.get_or_insert(is_p) != is_p
14259                                || *gu_q2.get_or_insert(is_q2) != is_q2
14260                                || *q82.get_or_insert(is_8) != is_8
14261                            {
14262                                return None;
14263                            }
14264                            if [gi, ui, di].into_iter().any(|idx| {
14265                                mm.tensors.get(idx).is_some_and(|t| {
14266                                    crate::prism::is_forward_weight(mm, &t.name)
14267                                        || crate::prism::is_affine_target(mm, &t.name)
14268                                })
14269                            }) {
14270                                return None;
14271                            }
14272                            model.get_or_insert_with(|| mm.clone());
14273                            experts.push((gi, ui, di));
14274                        }
14275                        crate::gpu::GraphFfn::Moe {
14276                            router,
14277                            shared_gate: sgate,
14278                            experts,
14279                            n_exp: m.experts.len(),
14280                            top_k: m.top_k,
14281                            inter,
14282                            norm_topk: m.norm_topk_prob,
14283                            q4tp: q4tp?,
14284                            gu_q2: gu_q2.unwrap_or(false),
14285                            q82: q82.unwrap_or(false),
14286                            sigmoid: m.router_sigmoid,
14287                            sigmoid_denom_eps: m.sigmoid_denom_eps,
14288                            bias: m.expert_bias.as_deref(),
14289                            has_shared,
14290                            detached_shared: None,
14291                            shared_gated,
14292                            route_scale: m.routed_scaling,
14293                        }
14294                    }
14295                    _ => return None,
14296                };
14297                let attn = match &lw.attn {
14298                    AttnKind::Full {
14299                        wq,
14300                        wk,
14301                        wv,
14302                        wo,
14303                        q_norm,
14304                        k_norm,
14305                        output_gate,
14306                        softplus_gate,
14307                        bias,
14308                    } => {
14309                        if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
14310                            if batch_debug {
14311                                eprintln!(
14312                                    "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
14313                                    softplus_gate.is_some(),
14314                                    self.attention_heads_per_layer.is_some()
14315                                );
14316                            }
14317                            return None;
14318                        }
14319                        let (m, _, _, _) =
14320                            wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
14321                        model = Some(m.clone());
14322                        crate::gpu::GraphAttn::Full {
14323                            wq: gw(wq)?,
14324                            wk: gw(wk)?,
14325                            wv: gw(wv)?,
14326                            wo: gw(wo)?,
14327                            q_norm: q_norm.as_deref(),
14328                            k_norm: k_norm.as_deref(),
14329                            late_qk_norm: self.qk_norm_after_rope,
14330                            bias: bias
14331                                .as_ref()
14332                                .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14333                            output_gate: *output_gate,
14334                            cpu_k: self.kv_cache.layers[li].k_heads(),
14335                            cpu_v: self.kv_cache.layers[li].v_heads(),
14336                            cpu_base: self.kv_cache.layers[li].base(),
14337                            geom: self.graph_attn_geom(li),
14338                            // The batched graph has no head-gate arm; a
14339                            // gated layer is refused above.
14340                            head_gate: None,
14341                        }
14342                    }
14343                    AttnKind::LinearGdn(w) => {
14344                        let Some(cfg) = self.gdn_cfg else {
14345                            if batch_debug {
14346                                eprintln!("batch graph: no GDN config at layer {li}");
14347                            }
14348                            return None;
14349                        };
14350                        let (m, _, _, _) = w
14351                            .in_proj_qkv
14352                            .graph_weight()
14353                            .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
14354                        model = Some(m.clone());
14355                        crate::gpu::GraphAttn::Gdn {
14356                            qkv: gw(&w.in_proj_qkv)?,
14357                            z: gw(&w.in_proj_z)?,
14358                            a: gw(&w.in_proj_a)?,
14359                            b: gw(&w.in_proj_b)?,
14360                            out: gw(&w.out_proj)?,
14361                            conv1d: &w.conv1d,
14362                            a_log: &w.a_log,
14363                            dt_bias: &w.dt_bias,
14364                            norm: &w.norm,
14365                            nv: cfg.num_v_heads,
14366                            nk: cfg.num_k_heads,
14367                            dk: cfg.key_head_dim,
14368                            dv: cfg.value_head_dim,
14369                            kk: cfg.conv_kernel,
14370                            cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14371                        }
14372                    }
14373                    _ => return None,
14374                };
14375                // Gemma-4's sandwich norms and layer scalar (`gemma_batch`);
14376                // every other model hands the graph none, as before.
14377                let g4 = self.global_attn.is_some() || self.attn_v_norm;
14378                layers.push(crate::gpu::GraphLayer {
14379                    input_norm: &lw.input_norm,
14380                    attn,
14381                    post_norm: &lw.post_norm,
14382                    attn_out_norm: if g4 {
14383                        lw.attn_out_norm.as_deref()
14384                    } else {
14385                        None
14386                    },
14387                    ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
14388                    layer_scale: if g4 { lw.layer_scale } else { None },
14389                    ffn: gffn,
14390                });
14391            }
14392            Some((layers, model?))
14393        })();
14394        let Some((layers, model)) = built else {
14395            {
14396                use std::sync::atomic::{AtomicBool, Ordering};
14397                static SAID: AtomicBool = AtomicBool::new(false);
14398                if !SAID.swap(true, Ordering::Relaxed) {
14399                    tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
14400                }
14401            }
14402            return crate::gpu::BatchGraphOutcome::Declined;
14403        };
14404        if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14405            eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
14406        }
14407        crate::gpu::forward_batch_graph(
14408            &model,
14409            self.graph_kv_id,
14410            &layers,
14411            &self.inv_freq,
14412            hiddens,
14413            nh,
14414            nkv,
14415            hd,
14416            rd,
14417            self.hidden_size,
14418            self.intermediate_size,
14419            positions,
14420            self.kv_cache.max_seq_len,
14421            gemma,
14422            self.rms_eps as f32,
14423            self.attn_scale,
14424            k,
14425            &(0..graph_end)
14426                .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
14427                .collect::<Vec<_>>(),
14428            self.o1_epoch,
14429            spec,
14430            layers_run,
14431        )
14432    }
14433
14434    /// Same, stopping after layer `upto` inclusive (routing probe φ).
14435    /// `CMF_DSV4_DRAFT_PROBE=1` — grade the draft against what the trunk goes on
14436    /// to produce. Off by default; it runs a whole draft per decoded token.
14437    fn draft_probe() -> bool {
14438        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14439        *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
14440    }
14441
14442    /// `CMF_DSV4_DRAFT_PROBE=1`: measure how much of the draft the trunk
14443    /// would have agreed with, WITHOUT verifying or rolling anything back.
14444    ///
14445    /// The number this produces decides the whole speculation design — at
14446    /// acceptance a, a block of B positions yields 1 + a + a² + ... tokens
14447    /// per trunk pass — so it is worth measuring before any of the machinery
14448    /// that would exploit it exists. Each draft is parked with the position
14449    /// it was made at, and graded as the real tokens arrive.
14450    /// `CMF_DSV4_SPEC=1` — the DeepSeek-V4 speculative decode: draft five
14451    /// on the card, verify them in one batched trunk pass, commit the
14452    /// accepted prefix, roll the rest back.
14453    #[cfg(feature = "gpu")]
14454    fn dsv4_spec_on() -> bool {
14455        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14456        *ON.get_or_init(|| {
14457            // Test-only runtime gate: model loading still performs the same
14458            // reservation and trunk packing, which gives rollback parity a
14459            // topology-identical non-speculative control arm.
14460            if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
14461                return v != "0";
14462            }
14463            // An explicit value is a diagnostic force/escape hatch.  With no
14464            // knob, speculation is eligible only when model loading reserved
14465            // its bounded pack.  On small q4tp cards the geometric reserve
14466            // gate deliberately leaves this at zero: trying to build DSpark
14467            // after the exact trunk filled VRAM is both slower and a device
14468            // OOM (measured on A40).
14469            std::env::var("CMF_DSV4_SPEC")
14470                .map(|v| v != "0")
14471                .unwrap_or_else(|_| {
14472                    crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
14473                })
14474        })
14475    }
14476
14477    /// One speculative round at the decode tip. `t_next` is the token the
14478    /// sampler just committed for `next_pos`. Returns the EXTRA accepted
14479    /// tokens (possibly none) and the new position, with `graph_logits`
14480    /// left holding the last accepted position's logits — exactly what the
14481    /// loop top expects. `None` means "speculate not this round": nothing
14482    /// was committed, the caller forwards normally.
14483    #[cfg(feature = "gpu")]
14484    fn dsv4_spec_step(
14485        &mut self,
14486        tip_token: u32,
14487        t_next: u32,
14488        next_pos: usize,
14489        max_extra: usize,
14490        drafted: &mut usize,
14491        accepted_ctr: &mut usize,
14492    ) -> Option<(Vec<u32>, usize)> {
14493        let t_all = std::time::Instant::now();
14494        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14495            thread_local! {
14496                static LAST: std::cell::Cell<Option<std::time::Instant>> =
14497                    const { std::cell::Cell::new(None) };
14498            }
14499            LAST.with(|l| {
14500                if let Some(prev) = l.get() {
14501                    eprintln!(
14502                        "между раундами {:.1} мс",
14503                        prev.elapsed().as_secs_f64() * 1e3
14504                    );
14505                }
14506                l.set(Some(std::time::Instant::now()));
14507            });
14508        }
14509        if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14510            eprintln!("spec_step: вход pos={next_pos}");
14511        }
14512        let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
14513        let cfg = self.dsv4.as_ref().map(|b| b.2)?;
14514        // The draft state and its capture, armed exactly as the probe does.
14515        if self.dspark.is_none() {
14516            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14517            if t.is_empty() {
14518                return None;
14519            }
14520            crate::dsv4::dspark_arm(&t, cfg.dim);
14521            self.dspark = Some(crate::dsv4::DsparkState::new(
14522                self.dsv4_mtp.len(),
14523                &cfg,
14524                t.len(),
14525            ));
14526        }
14527        let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14528        let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
14529        if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14530            eprintln!("spec_step: пак не построился (targets {targets:?})");
14531        }
14532        let pack = pack?;
14533        let block = crate::dsv4::dspark_block();
14534        let b_box = self.dsv4.as_mut()?;
14535        let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
14536        let ds = self.dspark.as_mut()?;
14537        // The tip's captures: either this token ran on a normal path that
14538        // filled the thread-local, or the previous spec round left them.
14539        let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
14540        if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
14541            if dbg {
14542                eprintln!("spec_step: нет захвата");
14543            }
14544            return None;
14545        }
14546        ds.have_hidden = true;
14547        let tip_pos = next_pos.checked_sub(1)?;
14548        let draft_started = std::time::Instant::now();
14549        let mut conf = Vec::new();
14550        let props = crate::dsv4::dspark_draft_gpu(
14551            g,
14552            &self.dsv4_mtp,
14553            &cfg,
14554            ds,
14555            pack,
14556            st.kv_id,
14557            tip_token,
14558            tip_pos,
14559            self.pool.as_deref(),
14560            &mut conf,
14561        );
14562        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14563        *drafted += block;
14564        if props.is_empty() || props[0] != t_next {
14565            if dbg {
14566                eprintln!(
14567                    "spec_step: черновик {} (props0={:?} t_next={t_next})",
14568                    if props.is_empty() {
14569                        "пуст"
14570                    } else {
14571                        "мимо"
14572                    },
14573                    props.first()
14574                );
14575            }
14576            return None;
14577        }
14578        // `fed[0]` is `t_next`, which the outer loop has already committed;
14579        // only `fed[1..]` become additional output tokens. Cap the verify
14580        // transaction itself to the caller's remaining output budget instead
14581        // of merely truncating the returned vector: otherwise the KV/state
14582        // would advance past `max_tokens` and a 64-token request could return
14583        // 66 tokens (and poison a reused session with two invisible steps).
14584        let mut k_verify = crate::dsv4::dspark_verify_k()
14585            .min(props.len())
14586            .min(max_extra.saturating_add(1));
14587        // Adaptive depth: positions the draft itself doubts are paid for on
14588        // every verify and delivered almost never (natural-text survival
14589        // [.67 .50 .29 .08 .04]). `CMF_DSPARK_CONF_MIN=p` trims the fed
14590        // prefix at the first proposal whose confidence drops below p; on
14591        // predictable text the confidences stay high and nothing changes.
14592        let conf_min = {
14593            static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
14594            *M.get_or_init(|| {
14595                std::env::var("CMF_DSPARK_CONF_MIN")
14596                    .ok()
14597                    .and_then(|v| v.parse().ok())
14598                    .unwrap_or(0.0)
14599            })
14600        };
14601        if conf_min > 0.0 && conf.len() >= props.len() {
14602            let mut keep = 1usize;
14603            while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
14604                keep += 1;
14605            }
14606            k_verify = k_verify.min(keep.max(2));
14607        }
14608        if k_verify < 2 {
14609            return None;
14610        }
14611        let mut fed = Vec::with_capacity(k_verify);
14612        fed.push(t_next);
14613        fed.extend_from_slice(&props[1..k_verify]);
14614        let mut argmax = Vec::new();
14615        let mut logits_all = Vec::new();
14616        let mut walked = Vec::new();
14617        let txn = crate::dsv4::dsv4_verify_chunk(
14618            g,
14619            layers,
14620            &cfg,
14621            st,
14622            &fed,
14623            next_pos,
14624            &self.inv_freq,
14625            self.pool.as_deref(),
14626            &targets,
14627            &mut argmax,
14628            &mut logits_all,
14629            &mut walked,
14630        );
14631        if txn.is_none() && dbg {
14632            eprintln!("spec_step: verify отказал");
14633        }
14634        let txn = txn?;
14635        let spec_gpu_end = txn.gpu_end;
14636        let b = fed.len();
14637        let mut accepted = 1usize;
14638        while accepted < b && fed[accepted] == argmax[accepted - 1] {
14639            accepted += 1;
14640        }
14641        // `CMF_DSV4_SPEC_FORCE_REJECT=1` — accept nothing beyond the known
14642        // token, every round: the pure rollback exerciser. The output must
14643        // stay byte-identical to the plain walk; anything else is a
14644        // transaction bug, isolated from the acceptance logic.
14645        if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
14646            accepted = 1;
14647        }
14648        if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
14649            eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
14650        }
14651        let t_fin = std::time::Instant::now();
14652        if !crate::dsv4::dsv4_spec_finish(
14653            g,
14654            layers,
14655            &cfg,
14656            st,
14657            txn,
14658            accepted,
14659            &fed,
14660            &self.inv_freq,
14661            self.pool.as_deref(),
14662        ) {
14663            tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
14664            return None;
14665        }
14666        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14667            eprintln!(
14668                "finish(k={accepted}): {:.1} мс",
14669                t_fin.elapsed().as_secs_f64() * 1e3
14670            );
14671        }
14672        *accepted_ctr += accepted - 1;
14673        // Captures per accepted token: device targets photographed by the
14674        // batch, host targets from the verify's own walk. The last one
14675        // becomes the new tip's draft input; every one owes the ring an
14676        // entry for its position.
14677        let (hc, dim) = (cfg.hc_mult, cfg.dim);
14678        // Complete-chain layers are photographed by the fused submission;
14679        // partial device layers overwrite that slot after exact host cold-
14680        // expert correction.  Thus every target in the contiguous device
14681        // prefix has a valid per-token capture.
14682        let dev_caps: Vec<usize> = targets
14683            .iter()
14684            .copied()
14685            .filter(|&t| t < spec_gpu_end)
14686            .collect();
14687        let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
14688        if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
14689            return None;
14690        }
14691        for t in 0..accepted {
14692            let tip = t + 1 == accepted;
14693            for (slot, &tl) in targets.iter().enumerate() {
14694                if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
14695                    let lo = (di * b + t) * hc * dim;
14696                    crate::dsv4::dspark_capture(
14697                        &caps_all[lo..lo + hc * dim],
14698                        &cfg,
14699                        slot,
14700                        &mut ds.main_hidden,
14701                    );
14702                } else if tip
14703                    && crate::dsv4::dspark_peek_slot(slot, dim, {
14704                        let lo = slot * dim;
14705                        &mut ds.main_hidden[lo..lo + dim]
14706                    })
14707                {
14708                    // The tip's host-layer captures are the walk's own
14709                    // per-layer notes — exact. (The walk that ran last ended
14710                    // on exactly this token, on both the accept-all and the
14711                    // rollback path.)
14712                } else {
14713                    // Intermediate tokens: the post-tail state stands in for
14714                    // the per-layer capture on host targets below the last
14715                    // layer. Ring-entry quality only; the tip is exact.
14716                    crate::dsv4::dspark_capture(
14717                        &walked[t * hc * dim..(t + 1) * hc * dim],
14718                        &cfg,
14719                        slot,
14720                        &mut ds.main_hidden,
14721                    );
14722                }
14723            }
14724            crate::dsv4::dspark_ring_append(
14725                g,
14726                &self.dsv4_mtp,
14727                &cfg,
14728                ds,
14729                next_pos + t,
14730                self.pool.as_deref(),
14731            );
14732        }
14733        let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
14734        self.graph_logits = Some(row);
14735        // The speculative loop never runs the probe, so the trunk tally has
14736        // no other place to cycle. Armed only when someone asked for the
14737        // dump; the host tail is the only tallying path here, which is
14738        // precisely the population a partial pack would serve.
14739        if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
14740            crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
14741            crate::dsv4::pick_tally_arm();
14742        }
14743        if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14744            eprintln!(
14745                "spec_step total {:.1} мс (k={accepted})",
14746                t_all.elapsed().as_secs_f64() * 1e3
14747            );
14748        }
14749        Some((fed[1..accepted].to_vec(), next_pos + accepted))
14750    }
14751
14752    fn dspark_probe(&mut self, position: usize, token_id: u32) {
14753        if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
14754            return;
14755        }
14756        // What the trunk just routed to, for this token.
14757        let trunk_now = crate::dsv4::pick_tally_take();
14758        crate::dsv4::trunk_freq_note(&trunk_now);
14759        if !trunk_now.is_empty() {
14760            self.dspark_trunk_picks.push(trunk_now);
14761            let keep = crate::dsv4::dspark_block();
14762            if self.dspark_trunk_picks.len() > keep {
14763                self.dspark_trunk_picks.remove(0);
14764            }
14765        }
14766        // Grade whatever is waiting: the token just decoded sits at
14767        // `position`, so it answers the draft made at `position - 1 - i`.
14768        for p in std::mem::take(&mut self.dspark_pending) {
14769            let Some(i) = position.checked_sub(p.0 + 1) else {
14770                continue;
14771            };
14772            let mut p = p;
14773            if i < p.1.len() {
14774                if p.2 && p.1[i] == token_id {
14775                    p.3 = i + 1;
14776                } else {
14777                    p.2 = false;
14778                }
14779                if i + 1 < p.1.len() {
14780                    self.dspark_pending.push(p);
14781                    continue;
14782                }
14783            }
14784            self.dspark_hist.push(p.3);
14785            self.dspark_real.push(token_id);
14786        }
14787        let Some(b) = &mut self.dsv4 else { return };
14788        let (g, layers, cfg) = (&b.0, &b.1, b.2);
14789        let n_layers = layers.len();
14790        if self.dspark.is_none() {
14791            let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14792            if t.is_empty() {
14793                return;
14794            }
14795            eprintln!(
14796                "DSpark: захват со слоёв {t:?}, блок {}",
14797                crate::dsv4::dspark_block()
14798            );
14799            crate::dsv4::dspark_arm(&t, cfg.dim);
14800            self.dspark = Some(crate::dsv4::DsparkState::new(
14801                self.dsv4_mtp.len(),
14802                &cfg,
14803                t.len(),
14804            ));
14805        }
14806        let ds = self.dspark.as_mut().unwrap();
14807        if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
14808            return; // this token ran on a path that captures nothing
14809        }
14810        let mut conf = Vec::new();
14811        crate::dsv4::pick_tally_arm();
14812        // The trunk has already consumed the adaptive VRAM budget. Until the
14813        // draft owns an explicit bounded device pack, its tensors are an
14814        // out-of-core CPU/disk tier by contract: never let per-op probes try
14815        // to squeeze another multi-gigabyte MTP expert cache onto the card.
14816        let draft_started = std::time::Instant::now();
14817        #[cfg(feature = "gpu")]
14818        let gpu_draft = crate::dsv4::dspark_gpu_on();
14819        #[cfg(not(feature = "gpu"))]
14820        let gpu_draft = false;
14821        let props = if gpu_draft {
14822            #[cfg(feature = "gpu")]
14823            {
14824                let kv_id = b.3.kv_id;
14825                match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
14826                    Some(pk) => crate::dsv4::dspark_draft_gpu(
14827                        g,
14828                        &self.dsv4_mtp,
14829                        &cfg,
14830                        ds,
14831                        pk,
14832                        kv_id,
14833                        token_id,
14834                        position,
14835                        self.pool.as_deref(),
14836                        &mut conf,
14837                    ),
14838                    None => Vec::new(),
14839                }
14840            }
14841            #[cfg(not(feature = "gpu"))]
14842            Vec::new()
14843        } else {
14844            crate::gpu::cpu_scope(|| {
14845                crate::dsv4::dspark_draft(
14846                    g,
14847                    &self.dsv4_mtp,
14848                    &cfg,
14849                    ds,
14850                    token_id,
14851                    position,
14852                    self.pool.as_deref(),
14853                    &mut conf,
14854                )
14855            })
14856        };
14857        self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14858        let draft_picks = crate::dsv4::pick_tally_take();
14859        crate::dsv4::dspark_freq_note(&draft_picks);
14860        // Re-arm for the NEXT trunk token; the probe runs after the forward,
14861        // so this is the only place that can.
14862        crate::dsv4::pick_tally_arm();
14863        if !props.is_empty() {
14864            // Two ratios, side by side: what a batched verify over the trunk
14865            // would read against what it asks for, and the same for the
14866            // draft's three stages. Near 1.0 means a batch amortises nothing.
14867            let (tu, tt) = {
14868                let flat: Vec<(usize, Vec<usize>)> = self
14869                    .dspark_trunk_picks
14870                    .iter()
14871                    .flat_map(|v| v.iter().cloned())
14872                    .collect();
14873                // Per layer, across the window of tokens.
14874                let mut per: std::collections::HashMap<usize, Vec<usize>> =
14875                    std::collections::HashMap::new();
14876                for (li, picks) in flat {
14877                    per.entry(li).or_default().extend(picks);
14878                }
14879                let n = per.len().max(1);
14880                let mut u = 0usize;
14881                let mut t = 0usize;
14882                for (_, v) in per {
14883                    t += v.len();
14884                    u += v.iter().collect::<std::collections::HashSet<_>>().len();
14885                }
14886                (u / n, t / n)
14887            };
14888            let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
14889            self.dspark_exp.push((tu, tt, du, dt));
14890            self.dspark_pending.push((position, props, true, 0));
14891        }
14892        if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
14893            let n = self.dspark_hist.len() as f32;
14894            let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
14895            let block = crate::dsv4::dspark_block();
14896            let mut at = vec![0usize; block + 1];
14897            for &k in &self.dspark_hist {
14898                at[k] += 1;
14899            }
14900            // Prefix survival: S_i = P(the first i positions all held).
14901            let mut surv = Vec::with_capacity(block);
14902            for i in 1..=block {
14903                let k = at[i..].iter().sum::<usize>() as f32 / n;
14904                surv.push(format!("{k:.2}"));
14905            }
14906            let distinct = self
14907                .dspark_real
14908                .iter()
14909                .collect::<std::collections::HashSet<_>>()
14910                .len();
14911            let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
14912                (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
14913            });
14914            let m = self.dspark_exp.len().max(1);
14915            eprintln!(
14916                "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
14917                 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
14918                self.dspark_hist.len(),
14919                mean + 1.0,
14920                surv.join(" ")
14921            );
14922            eprintln!(
14923                "DSpark: разных токенов {distinct} из {} (вырожденность), \
14924                 эксперты ствол {}/{} на слой за {block} токенов, \
14925                 черновик {}/{} за блок, draft {:.2} мс/блок",
14926                self.dspark_real.len(),
14927                tu / m,
14928                tt / m,
14929                du / m,
14930                dt / m,
14931                self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
14932            );
14933        }
14934    }
14935
14936    fn forward_layers_upto(
14937        &mut self,
14938        hidden: &[f32],
14939        position: usize,
14940        task_mask: Option<&TaskMask>,
14941        upto: Option<usize>,
14942    ) -> Vec<f32> {
14943        // In-process multi-GPU: each segment runs pinned to its card,
14944        // and the only thing crossing the boundary is one hidden vector
14945        // that never leaves this address space. Same layer split the
14946        // network mode does, minus the second process, the socket, the
14947        // serialization and the dir_hash handshake.
14948        if let Some(plan) = self.gpu_plan.clone() {
14949            if upto.is_none() && plan.len() > 1 {
14950                let mut h = hidden.to_vec();
14951                for &(dev, from, upto_incl) in plan.iter() {
14952                    h = crate::gpu::with_device(dev, || {
14953                        self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
14954                    });
14955                }
14956                return h;
14957            }
14958        }
14959        self.forward_layers_span(hidden, position, task_mask, 0, upto)
14960    }
14961
14962    /// Split this pipeline's layer stack across local GPUs: segment i
14963    /// runs on `devices[i]`. Contiguous and even by layer count — the
14964    /// VRAM-weighted planner is the next step, and an uneven card pair
14965    /// is why it will be needed. `None` clears the plan.
14966    pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
14967        self.set_gpu_plan_at(devices, None)
14968    }
14969
14970    /// The same, with an explicit first boundary (`--peer-split`): card
14971    /// 0 takes layers `[0..at)`, the rest split what remains. Uneven
14972    /// cards, or an attention-heavy head, are why this knob exists.
14973    pub fn set_gpu_plan_at(
14974        &mut self,
14975        devices: Option<&[usize]>,
14976        at: Option<usize>,
14977    ) -> Result<(), String> {
14978        let Some(devs) = devices.filter(|d| d.len() > 1) else {
14979            self.gpu_plan = None;
14980            return Ok(());
14981        };
14982        self.split_supported()?;
14983        let n = self.num_layers;
14984        if devs.len() > n {
14985            return Err(format!("{} devices for {n} layers", devs.len()));
14986        }
14987        if let Some(k) = at {
14988            if k == 0 || k >= n {
14989                return Err(format!("split at {k}: the model has {n} layers"));
14990            }
14991            if devs.len() == 2 {
14992                self.gpu_plan = Some(std::sync::Arc::new(vec![
14993                    (devs[0], 0, k - 1),
14994                    (devs[1], k, n - 1),
14995                ]));
14996                return Ok(());
14997            }
14998            return Err(format!(
14999                "an explicit split point takes exactly 2 devices, got {}",
15000                devs.len()
15001            ));
15002        }
15003        let per = n.div_ceil(devs.len());
15004        let mut plan = Vec::with_capacity(devs.len());
15005        let mut from = 0usize;
15006        for &d in devs {
15007            if from >= n {
15008                break;
15009            }
15010            let upto = (from + per - 1).min(n - 1);
15011            plan.push((d, from, upto));
15012            from = upto + 1;
15013        }
15014        self.gpu_plan = Some(std::sync::Arc::new(plan));
15015        Ok(())
15016    }
15017
15018    /// The active in-process split, if any: (device, first layer, last).
15019    pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
15020        self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
15021    }
15022
15023    /// Layer span [from ..= upto] (upto None = last layer): the building
15024    /// block the network pipeline-split rides on. `from > 0` skips the
15025    /// arch escape hatches (the pub `forward_span` refuses those archs
15026    /// first) and the whole-token graph — the plain per-layer loop is
15027    /// the canonical executor for a partial stack.
15028    fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
15029        if let Some(x) = t.as_f32() {
15030            return x.to_vec();
15031        }
15032        let mut out = vec![0.0; t.rows() * t.cols()];
15033        for r in 0..t.rows() {
15034            t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
15035        }
15036        out
15037    }
15038
15039    fn embryo_resident_eligible(&self) -> bool {
15040        // One mixer family per file: vmf_phase (kind 0/1) or
15041        // gated_delta_net (kind 4); the anchors are full (2) or bounded (3).
15042        if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
15043            || self.num_layers != self.physical_layers
15044            || self.loop_final_norm
15045            || self.weights.layers.len() != self.num_layers
15046            || self.head_clusters.is_none()
15047            || self.final_softcap.is_some()
15048            || self.logit_multiplier.is_some()
15049            || self.attn_softcap != 0.0
15050            || self.mtp.is_some()
15051            || self.g3n.is_some()
15052            || self.dsv4.is_some()
15053            || self.dsv41.is_some()
15054            || self.qwen4_exp.is_some()
15055            // Dynamic routing swaps FFN weights mid-sequence under the
15056            // packed graph; a blend has no single overlay. A STATIC skill
15057            // (`from_model_with_skill`) is fine: the pack reads the live
15058            // `weights.layers[*].ffn`, i.e. the skill's tensors, and a
15059            // later `set_active_skill` change drops the pack
15060            // (`invalidate_for_weight_change`).
15061            || self.dyn_router.is_some()
15062            || self.dyn_phi_layer.is_some()
15063            || self.dyn_blend_loaded
15064            || self.o1_cfg.is_some()
15065            || self.swa.is_some()
15066            || self.sliding_layers.is_some()
15067            || self.global_attn.is_some()
15068            || self.attention_heads_per_layer.is_some()
15069            || self.attn_v_norm
15070            || self
15071                .kv_cache
15072                .layers
15073                .iter()
15074                .any(|l| l.mode != crate::kv_cache::KvMode::F32)
15075            || self.rope_scale != 1.0
15076            || self.rope_scale_local != 1.0
15077            || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
15078            || self.hidden_size == 0
15079            || self.hidden_size > 1024
15080            || self.intermediate_size > 1024
15081            || self.num_heads == 0
15082            || self.num_kv_heads == 0
15083            || self.num_heads % self.num_kv_heads != 0
15084            || self.num_heads.saturating_mul(self.head_dim) > 1024
15085            || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
15086            || self.vocab_size == 0
15087            || self.kv_cache.max_seq_len == 0
15088            || self.rotary_dim == 0
15089            || self.rotary_dim > self.head_dim
15090            || self.rotary_dim % 2 != 0
15091            || self.inv_freq.len() < self.rotary_dim / 2
15092        {
15093            return false;
15094        }
15095        // The resident shader is deliberately an f32 profile.  Dequantizing
15096        // a Q4/Q8 tensor into the packed buffer would silently change the
15097        // operator relative to the CPU quantized path, so quantized CMFs
15098        // retain the exact ordinary executor instead of claiming parity.
15099        // Measured consequence (RTX PRO 4000, S4 bounded export requantized
15100        // with `cortiq requant --quant q4tp-quantize`): `eligible=false`,
15101        // the generic wgpu whole-token graph refuses too, and the per-op
15102        // path decodes at ~73 tok/s against ~200 tok/s on the CPU q4tp
15103        // path — a q4tp Embryo-O1 file is a CPU artifact today; the
15104        // resident graph serves the f32 export.
15105        if self.weights.lm_head.as_f32().is_none()
15106            || self.weights.embed_tokens.as_f32().is_none()
15107            || self.weights.lm_head.rows() < self.vocab_size
15108            || self.weights.lm_head.cols() != self.hidden_size
15109            || self.weights.embed_tokens.rows() < self.vocab_size
15110            || self.weights.embed_tokens.cols() != self.hidden_size
15111            || self.weights.final_norm.len() != self.hidden_size
15112        {
15113            return false;
15114        }
15115        if let Some(cfg) = self.vmf_cfg {
15116            if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
15117                || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
15118                || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
15119                || cfg.state_len() == 0
15120            {
15121                return false;
15122            }
15123        }
15124        if let Some(g) = self.gdn_cfg {
15125            // The resident GDN kernels (gpu_wgpu.rs `embryo_core_gdn_*`):
15126            // fused projection ≤ 2048 rows, nv·dv ≤ 1024, dk ≤ 128 lanes,
15127            // dv ≤ 256 lanes in vec4 rows, SiLU output gate (the Embryo
15128            // export), same rms eps as the stack.
15129            if g.num_v_heads == 0
15130                || g.num_k_heads == 0
15131                || g.num_v_heads % g.num_k_heads != 0
15132                || g.key_head_dim == 0
15133                || g.key_head_dim > 128
15134                || g.value_head_dim == 0
15135                || g.value_head_dim > 256
15136                || g.value_head_dim % 4 != 0
15137                || g.conv_kernel == 0
15138                || g.num_v_heads > 512
15139                || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
15140                || g.conv_dim() > 2048
15141                || g.conv_dim() % 4 != 0
15142                || g.hidden_size != self.hidden_size
15143                || g.output_gate_sigmoid
15144                || g.rms_eps != self.rms_eps
15145                || g.state_len() == 0
15146            {
15147                return false;
15148            }
15149        }
15150        let mut full_seen = false;
15151        for lw in &self.weights.layers {
15152            if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
15153                return false;
15154            }
15155            match &lw.attn {
15156                AttnKind::LinearGdn(w) => {
15157                    let Some(g) = self.gdn_cfg else {
15158                        return false;
15159                    };
15160                    let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
15161                    if w.in_proj_qkv.rows() != g.conv_dim()
15162                        || w.in_proj_qkv.cols() != self.hidden_size
15163                        || w.in_proj_qkv.as_f32().is_none()
15164                        || w.in_proj_z.rows() != nv * dv
15165                        || w.in_proj_z.cols() != self.hidden_size
15166                        || w.in_proj_z.as_f32().is_none()
15167                        || w.in_proj_a.rows() != nv
15168                        || w.in_proj_a.cols() != self.hidden_size
15169                        || w.in_proj_a.as_f32().is_none()
15170                        || w.in_proj_b.rows() != nv
15171                        || w.in_proj_b.cols() != self.hidden_size
15172                        || w.in_proj_b.as_f32().is_none()
15173                        || w.conv1d.len() != g.conv_dim() * kk
15174                        || w.a_log.len() != nv
15175                        || w.dt_bias.len() != nv
15176                        || w.norm.len() != dv
15177                        || w.out_proj.rows() != self.hidden_size
15178                        || w.out_proj.cols() != nv * dv
15179                        || w.out_proj.as_f32().is_none()
15180                    {
15181                        return false;
15182                    }
15183                }
15184                AttnKind::Linear(w) => {
15185                    let Some(cfg) = self.vmf_cfg else {
15186                        return false;
15187                    };
15188                    if w.thq.rows() != cfg.num_heads * cfg.nphase
15189                        || w.thq.cols() != self.hidden_size
15190                        || w.thq.as_f32().is_none()
15191                        || w.thk.rows() != cfg.num_heads * cfg.nphase
15192                        || w.thk.cols() != self.hidden_size
15193                        || w.thk.as_f32().is_none()
15194                        || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
15195                        || w.v_proj.cols() != self.hidden_size
15196                        || w.v_proj.as_f32().is_none()
15197                        || w.out_proj.rows() != self.hidden_size
15198                        || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
15199                        || w.out_proj.as_f32().is_none()
15200                        || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
15201                    {
15202                        return false;
15203                    }
15204                    if let Some((kg, kb)) = &w.k_gate {
15205                        if kg.rows() != cfg.num_heads
15206                            || kg.cols() != self.hidden_size
15207                            || kg.as_f32().is_none()
15208                            || kb.len() != cfg.num_heads
15209                        {
15210                            return false;
15211                        }
15212                    }
15213                    if let Some(conv) = &w.conv {
15214                        if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
15215                            return false;
15216                        }
15217                    }
15218                }
15219                AttnKind::Full {
15220                    wq,
15221                    wk,
15222                    wv,
15223                    wo,
15224                    q_norm,
15225                    k_norm,
15226                    output_gate,
15227                    softplus_gate,
15228                    bias,
15229                } => {
15230                    if full_seen
15231                        || q_norm.is_some()
15232                        || k_norm.is_some()
15233                        || *output_gate
15234                        || softplus_gate.is_some()
15235                        || bias.is_some()
15236                        || wq.as_f32().is_none()
15237                        || wk.as_f32().is_none()
15238                        || wv.as_f32().is_none()
15239                        || wo.as_f32().is_none()
15240                        || wq.rows() != self.num_heads * self.head_dim
15241                        || wk.rows() != self.num_kv_heads * self.head_dim
15242                        || wv.rows() != self.num_kv_heads * self.head_dim
15243                        || wq.cols() != self.hidden_size
15244                        || wk.cols() != self.hidden_size
15245                        || wv.cols() != self.hidden_size
15246                        || wo.rows() != self.hidden_size
15247                        || wo.cols() != self.num_heads * self.head_dim
15248                    {
15249                        return false;
15250                    }
15251                    full_seen = true;
15252                }
15253                AttnKind::Bounded(w) => {
15254                    // The resident bounded attend scores S + W lanes in one
15255                    // 256-lane chunk; the format caps S + W at 160.
15256                    let Some(ac) = self.anchor_core.as_ref() else {
15257                        return false;
15258                    };
15259                    if self.bounded_rope.is_none()
15260                        || w.window != ac.window
15261                        || w.sink != ac.sink
15262                        || w.window == 0
15263                        || w.window + w.sink > 256
15264                        || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
15265                        || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
15266                        || w.wq.as_f32().is_none()
15267                        || w.wk.as_f32().is_none()
15268                        || w.wv.as_f32().is_none()
15269                        || w.wo.as_f32().is_none()
15270                        || w.wq.rows() != self.num_heads * self.head_dim
15271                        || w.wk.rows() != self.num_kv_heads * self.head_dim
15272                        || w.wv.rows() != self.num_kv_heads * self.head_dim
15273                        || w.wq.cols() != self.hidden_size
15274                        || w.wk.cols() != self.hidden_size
15275                        || w.wv.cols() != self.hidden_size
15276                        || w.wo.rows() != self.hidden_size
15277                        || w.wo.cols() != self.num_heads * self.head_dim
15278                    {
15279                        return false;
15280                    }
15281                }
15282                _ => return false,
15283            }
15284            match &lw.ffn {
15285                FfnKind::Dense(d) => {
15286                    if d.act != Act::Silu
15287                        || !d.segs.is_empty()
15288                        || d.gate_proj.as_f32().is_none()
15289                        || d.up_proj.as_f32().is_none()
15290                        || d.down_proj.as_f32().is_none()
15291                        || d.gate_proj.rows() != self.intermediate_size
15292                        || d.gate_proj.cols() != self.hidden_size
15293                        || d.up_proj.rows() != self.intermediate_size
15294                        || d.up_proj.cols() != self.hidden_size
15295                        || d.down_proj.rows() != self.hidden_size
15296                        || d.down_proj.cols() != self.intermediate_size
15297                    {
15298                        return false;
15299                    }
15300                }
15301                FfnKind::Moe(m) => {
15302                    if m.resonance.is_none()
15303                        || m.top_k != 1
15304                        || m.router_sigmoid
15305                        || !m.norm_topk_prob
15306                        || m.expert_bias.is_some()
15307                        || m.routed_scaling != 1.0
15308                        || m.route_tau.is_some()
15309                        || m.shared.is_none()
15310                        || m.mask.is_some()
15311                        || m.per_expert_scale.is_some()
15312                        || m.router_input_norm
15313                        || m.experts.is_empty()
15314                        || m.experts.len() > 8
15315                    {
15316                        return false;
15317                    }
15318                    let r = m.resonance.as_ref().unwrap();
15319                    if r.mu.len() != m.experts.len() * self.hidden_size
15320                        || r.bias.len() != m.experts.len()
15321                        || r.u.len() != m.experts.len() * r.k * self.hidden_size
15322                        || r.k > 128
15323                    {
15324                        return false;
15325                    }
15326                    let Some((shared, gate)) = &m.shared else {
15327                        return false;
15328                    };
15329                    if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
15330                        return false;
15331                    }
15332                    if shared.gate_proj.as_f32().is_none()
15333                        || shared.up_proj.as_f32().is_none()
15334                        || shared.down_proj.as_f32().is_none()
15335                        || shared.gate_proj.rows() != self.intermediate_size
15336                        || shared.gate_proj.cols() != self.hidden_size
15337                        || shared.up_proj.rows() != self.intermediate_size
15338                        || shared.up_proj.cols() != self.hidden_size
15339                        || shared.down_proj.rows() != self.hidden_size
15340                        || shared.down_proj.cols() != self.intermediate_size
15341                    {
15342                        return false;
15343                    }
15344                    for e in &m.experts {
15345                        if e.act != Act::Silu
15346                            || !e.segs.is_empty()
15347                            || e.gate_proj.as_f32().is_none()
15348                            || e.up_proj.as_f32().is_none()
15349                            || e.down_proj.as_f32().is_none()
15350                            || e.gate_proj.rows() != self.intermediate_size
15351                            || e.gate_proj.cols() != self.hidden_size
15352                            || e.up_proj.rows() != self.intermediate_size
15353                            || e.up_proj.cols() != self.hidden_size
15354                            || e.down_proj.rows() != self.hidden_size
15355                            || e.down_proj.cols() != self.intermediate_size
15356                        {
15357                            return false;
15358                        }
15359                    }
15360                }
15361                FfnKind::DenseMoe(_) => return false,
15362            }
15363            if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
15364                return false;
15365            }
15366        }
15367        if full_seen && self.anchor_core.is_some() {
15368            return false;
15369        }
15370        full_seen || self.num_layers > 0
15371    }
15372
15373    /// The resident Embryo graph is the owner of this pipeline's forward:
15374    /// the same gate `forward_layers_span` applies before handing a token
15375    /// to `forward_embryo_graph` (both graph phases on, the explicit
15376    /// opt-in, a wgpu device, no earlier refusal, an eligible stack).
15377    fn embryo_resident_wanted(&self) -> bool {
15378        crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15379            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15380            && matches!(
15381                std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15382                Ok("1") | Ok("parallel")
15383            )
15384            && crate::gpu::enabled_here()
15385            && !self.graph_refused()
15386            && self.embryo_resident_eligible()
15387    }
15388
15389    /// Chunked prefill on the resident graph: `ids` from `start` in
15390    /// chunks of `EMBRYO_CHUNK_MAX`, one submit each, projections/FFN as
15391    /// chunk GEMMs and the recurrent layers walked in time on the device.
15392    /// Returns the last position's logits when the whole span ran there.
15393    /// `None` = refused before any device work (the per-position path
15394    /// takes the span).  `CMF_EMBRYO_CHUNK=0` keeps the per-position
15395    /// prefill (A/B and the parity reference).
15396    fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
15397        if ids.len() < 2
15398            || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
15399            || !self.embryo_resident_wanted()
15400        {
15401            return None;
15402        }
15403        let model = self.ensure_embryo_graph()?;
15404        let cmax = std::env::var("CMF_EMBRYO_CHUNK")
15405            .ok()
15406            .and_then(|v| v.parse::<usize>().ok())
15407            .filter(|&v| v >= 1)
15408            .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
15409            .min(crate::gpu::EMBRYO_CHUNK_MAX);
15410        let hs = self.hidden_size;
15411        let n = ids.len();
15412        let mut pos = start;
15413        let mut last = None;
15414        let mut rows = Vec::with_capacity(cmax * hs);
15415        while pos < n {
15416            let end = (pos + cmax).min(n);
15417            rows.clear();
15418            for &id in &ids[pos..end] {
15419                rows.extend_from_slice(&self.embed_single(id));
15420            }
15421            let mut lg = Vec::new();
15422            if !crate::gpu::forward_embryo_graph_chunk(
15423                &model,
15424                self.graph_kv_id,
15425                &rows,
15426                pos,
15427                end - pos,
15428                &mut lg,
15429            ) {
15430                if pos == start {
15431                    if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15432                        eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
15433                    }
15434                    return None;
15435                }
15436                // The device sequence advanced through the earlier chunks;
15437                // a host continuation would mix two owners of the state.
15438                // Fail this sequence alone, leaving no stale state or key.
15439                self.kv_cache.clear();
15440                self.clear_history();
15441                crate::gpu::graph_kv_reset(self.graph_kv_id);
15442                panic!(
15443                    "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
15444                );
15445            }
15446            last = Some(lg);
15447            pos = end;
15448        }
15449        if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15450            eprintln!(
15451                "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
15452                n - start,
15453                (n - start).div_ceil(cmax)
15454            );
15455        }
15456        last
15457    }
15458
15459    fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
15460        if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
15461            const UMAX: u32 = u32::MAX;
15462            const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
15463            const REC: usize = 64;
15464            struct Pack {
15465                data: Vec<f32>,
15466            }
15467            impl Pack {
15468                fn put(&mut self, x: &[f32]) -> u32 {
15469                    if x.is_empty() {
15470                        return u32::MAX;
15471                    }
15472                    let off = self.data.len();
15473                    self.data.extend_from_slice(x);
15474                    off as u32
15475                }
15476            }
15477            // The mixer family of the file: vmf_phase geometry fills the
15478            // phase header words, gated_delta_net fills words 24..29.  A
15479            // file has exactly one linear core, so at most one is live.
15480            let vmf = self.vmf_cfg;
15481            let gdn = self.gdn_cfg;
15482            let mut pack = Pack { data: Vec::new() };
15483            let mut meta = vec![0u32; HEADER];
15484            meta[0] = self.hidden_size as u32;
15485            meta[1] = self.intermediate_size as u32;
15486            meta[2] = self.vocab_size as u32;
15487            meta[3] = self.num_layers as u32;
15488            meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
15489            meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
15490            meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
15491            if let Some(g) = gdn {
15492                meta[24] = g.num_v_heads as u32;
15493                meta[25] = g.num_k_heads as u32;
15494                meta[26] = g.key_head_dim as u32;
15495                meta[27] = g.value_head_dim as u32;
15496                meta[28] = g.conv_kernel as u32;
15497                meta[29] = g.conv_dim() as u32;
15498            }
15499            meta[7] = self.num_heads as u32;
15500            meta[8] = self.num_kv_heads as u32;
15501            meta[9] = self.head_dim as u32;
15502            meta[10] = self.kv_cache.max_seq_len as u32;
15503            let clusters = self.head_clusters.as_ref().unwrap();
15504            let cluster_count = clusters.len() / self.hidden_size;
15505            if clusters.len() % self.hidden_size != 0
15506                || cluster_count == 0
15507                || cluster_count > 1024
15508                || self.vocab_size % cluster_count != 0
15509                || self.weights.lm_head.rows() < self.vocab_size
15510                || self.weights.final_norm.len() != self.hidden_size
15511            {
15512                return None;
15513            }
15514            meta[11] = cluster_count as u32;
15515            meta[12] = (self.vocab_size / cluster_count) as u32;
15516            meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
15517            meta[16] = self.rotary_dim as u32;
15518            meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
15519            meta[19] = (self.rms_eps as f32).to_bits();
15520            let max_conv = self
15521                .weights
15522                .layers
15523                .iter()
15524                .filter_map(|lw| match &lw.attn {
15525                    AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
15526                    _ => None,
15527                })
15528                .max()
15529                .unwrap_or(1);
15530            // One state slot per recurrent layer: the phase state plus its
15531            // hidden-wide conv ring, or the GDN record `[conv ring | S]`
15532            // (`GdnCfg::state_len`).  A file carries one mixer family, so
15533            // the stride is exactly that family's record and the device
15534            // state buffer equals the header's recurrent bytes.
15535            let phase_stride = vmf
15536                .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
15537                .unwrap_or(0);
15538            let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
15539            let state_stride = phase_stride.max(gdn_stride);
15540            // Bounded genome: the KV plane of an anchor is its ring
15541            // `[kvh][W][hd]` K + V, and only anchors own one.  Legacy full
15542            // anchors keep the `max_seq` planes indexed by layer.
15543            let bounded = self.anchor_core.clone();
15544            let (anchor_window, anchor_sink) = bounded
15545                .as_ref()
15546                .map(|ac| (ac.window, ac.sink))
15547                .unwrap_or((0, 0));
15548            let kv_stride = if bounded.is_some() {
15549                2usize
15550                    .saturating_mul(self.num_kv_heads)
15551                    .saturating_mul(anchor_window)
15552                    .saturating_mul(self.head_dim)
15553            } else {
15554                2usize
15555                    .saturating_mul(self.num_kv_heads)
15556                    .saturating_mul(self.kv_cache.max_seq_len)
15557                    .saturating_mul(self.head_dim)
15558            };
15559            meta[14] = state_stride as u32;
15560            meta[15] = kv_stride as u32;
15561            meta[18] = anchor_window as u32;
15562            meta[20] = anchor_sink as u32;
15563            meta[21] = match &self.bounded_rope {
15564                Some(rope) => {
15565                    // [W][half] cos then [W][half] sin, one contiguous table.
15566                    let off = pack.put(&rope.cos);
15567                    let _ = pack.put(&rope.sin);
15568                    off
15569                }
15570                None => UMAX,
15571            };
15572            let mut full_seen = false;
15573            let mut bounded_seen = 0usize;
15574            // Recurrent state slots belong to mixer layers only (phase or
15575            // GDN): an anchor owns a ring, not a state stride, so the
15576            // device state buffer is exactly the header's recurrent bytes.
15577            let mut phase_seen = 0usize;
15578            let mut gdn_seen = 0usize;
15579            for (li, lw) in self.weights.layers.iter().enumerate() {
15580                let base = meta.len();
15581                meta.resize(base + REC, UMAX);
15582                meta[base] = match &lw.attn {
15583                    AttnKind::Linear(w) if w.phase_delta => 1,
15584                    AttnKind::Linear(_) => 0,
15585                    AttnKind::Full { .. } => 2,
15586                    AttnKind::Bounded(_) => 3,
15587                    AttnKind::LinearGdn(_) => 4,
15588                    _ => UMAX,
15589                };
15590                meta[base + 1] = pack.put(&lw.input_norm);
15591                meta[base + 2] = pack.put(&lw.post_norm);
15592                meta[base + 25] = match &lw.attn {
15593                    AttnKind::Linear(_) => {
15594                        let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15595                        phase_seen += 1;
15596                        off
15597                    }
15598                    AttnKind::LinearGdn(_) => {
15599                        let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15600                        gdn_seen += 1;
15601                        off
15602                    }
15603                    _ => UMAX,
15604                };
15605                match &lw.attn {
15606                    AttnKind::LinearGdn(w) => {
15607                        // Layer record words 56..63 + 29, as the resident
15608                        // kernels read them (gpu_wgpu.rs `embryo_core_gdn_*`).
15609                        meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
15610                        meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
15611                        meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
15612                        meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
15613                        meta[base + 60] = pack.put(&w.conv1d);
15614                        meta[base + 61] = pack.put(&w.a_log);
15615                        meta[base + 62] = pack.put(&w.dt_bias);
15616                        meta[base + 63] = pack.put(&w.norm);
15617                        meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15618                        meta[base + 24] = 0;
15619                    }
15620                    AttnKind::Linear(w) => {
15621                        meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
15622                        meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
15623                        meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
15624                        meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15625                        let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
15626                        meta[base + 7] = pack.put(&decay);
15627                        if let Some((kg, kb)) = &w.k_gate {
15628                            meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
15629                            meta[base + 9] = pack.put(kb);
15630                        }
15631                        if let Some(conv) = &w.conv {
15632                            meta[base + 10] = pack.put(conv);
15633                            meta[base + 24] = (conv.len() / self.hidden_size) as u32;
15634                        } else {
15635                            meta[base + 24] = 0;
15636                        }
15637                    }
15638                    AttnKind::Full { wq, wk, wv, wo, .. } => {
15639                        full_seen = true;
15640                        meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
15641                        meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
15642                        meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
15643                        meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
15644                        meta[base + 26] = (li * kv_stride) as u32;
15645                    }
15646                    AttnKind::Bounded(w) => {
15647                        meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
15648                        meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
15649                        meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
15650                        meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
15651                        // Ring slot of this anchor (anchors only, packed).
15652                        meta[base + 26] = (bounded_seen * kv_stride) as u32;
15653                        meta[base + 27] = pack.put(&w.sink_k);
15654                        meta[base + 28] = pack.put(&w.sink_v);
15655                        bounded_seen += 1;
15656                    }
15657                    _ => return None,
15658                }
15659                match &lw.ffn {
15660                    FfnKind::Dense(d) => {
15661                        meta[base + 15] = 0;
15662                        meta[base + 16] = 0;
15663                        meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
15664                        meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
15665                        meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
15666                    }
15667                    FfnKind::Moe(m) => {
15668                        let r = m.resonance.as_ref().unwrap();
15669                        let (shared, _) = m.shared.as_ref().unwrap();
15670                        meta[base + 15] = 1;
15671                        meta[base + 16] = m.experts.len() as u32;
15672                        meta[base + 17] = pack.put(&r.mu);
15673                        meta[base + 18] = pack.put(&r.u);
15674                        meta[base + 19] = pack.put(&r.bias);
15675                        meta[base + 20] = r.k as u32;
15676                        // Word 30: the growth shell as the runtime applies
15677                        // it now (`+inf` on trunk rows, the stored finite
15678                        // shell on grown rows, all `+inf` under
15679                        // `CMF_GROWTH_SHELL=off`), followed by one `−∞`
15680                        // sentinel at index E the kernel writes as the
15681                        // score of an expert outside its shell — WGSL has
15682                        // no infinity literal, so the value travels as
15683                        // data (`embryo_core_route_finalize`).
15684                        let mut shell = r.effective_shell(m.experts.len());
15685                        shell.push(f32::NEG_INFINITY);
15686                        meta[base + 30] = pack.put(&shell);
15687                        meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
15688                        meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
15689                        meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
15690                        for (e, ex) in m.experts.iter().enumerate() {
15691                            meta[base + 32 + e * 3] =
15692                                pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
15693                            meta[base + 33 + e * 3] =
15694                                pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
15695                            meta[base + 34 + e * 3] =
15696                                pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
15697                        }
15698                    }
15699                    FfnKind::DenseMoe(_) => return None,
15700                }
15701            }
15702            if !full_seen && self.num_layers == 0 {
15703                return None;
15704            }
15705            let id = {
15706                static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
15707                NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
15708            };
15709            let model = crate::gpu::EmbryoGraphModel {
15710                id,
15711                hidden: self.hidden_size,
15712                intermediate: self.intermediate_size,
15713                vocab: self.vocab_size,
15714                layers: self.num_layers,
15715                phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
15716                nphase: vmf.map(|c| c.nphase).unwrap_or(0),
15717                phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
15718                anchor_q_heads: self.num_heads,
15719                anchor_kv_heads: self.num_kv_heads,
15720                anchor_head_dim: self.head_dim,
15721                rotary_dim: self.rotary_dim,
15722                max_seq: self.kv_cache.max_seq_len,
15723                cluster_count,
15724                cluster_size: self.vocab_size / cluster_count,
15725                phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
15726                state_stride,
15727                kv_stride,
15728                norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
15729                phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
15730                weights: pack.data,
15731                meta,
15732                lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
15733                clusters: clusters.as_ref().clone(),
15734                final_norm: self.weights.final_norm.clone(),
15735                inv_freq: self.inv_freq.as_ref().clone(),
15736                bounded: bounded.is_some(),
15737                kv_layers: if bounded.is_some() {
15738                    bounded_seen
15739                } else {
15740                    self.num_layers
15741                },
15742                state_layers: phase_seen + gdn_seen,
15743                anchor_window,
15744                anchor_sink,
15745                phase_layers: phase_seen,
15746                gdn_layers: gdn_seen,
15747                gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
15748                gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
15749                gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
15750                gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
15751                gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
15752            };
15753            self.embryo_graph = Some(std::sync::Arc::new(model));
15754        }
15755        self.embryo_graph.clone()
15756    }
15757
15758    fn forward_layers_span(
15759        &mut self,
15760        hidden: &[f32],
15761        position: usize,
15762        task_mask: Option<&TaskMask>,
15763        from: usize,
15764        upto: Option<usize>,
15765    ) -> Vec<f32> {
15766        #[cfg(target_os = "macos")]
15767        let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
15768            position,
15769            from,
15770            upto.map_or(self.num_layers, |u| u.saturating_add(1)),
15771        );
15772        debug_assert!(
15773            from == 0
15774                || (self.dsv4.is_none()
15775                    && self.dsv41.is_none()
15776                    && self.qwen4_exp.is_none()
15777                    && self.g3n.is_none())
15778        );
15779        // Every plain forward — the whole-token Metal graph (`q1_graph_gpu`
15780        // wraps the GDN owners zero-copy and reallocates them on a size
15781        // change) and the CPU layer loop (reads/swaps `linear_state`) —
15782        // must see the previous speculative commit's asynchronous replay
15783        // complete. One mutex probe when nothing is pending.
15784        #[cfg(target_os = "macos")]
15785        if !crate::gpu_metal::wait_replay() {
15786            self.fail_metal_graph("the pending async replay failed before a plain forward");
15787            return vec![0.0; self.hidden_size];
15788        }
15789        if let Some(b) = &mut self.qwen4_exp {
15790            let _ = (task_mask, upto);
15791            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15792            let mut logits = Vec::new();
15793            crate::qwen4_exp::forward_token(
15794                &b.0,
15795                &b.1,
15796                &b.2,
15797                &mut b.3,
15798                token_id,
15799                position,
15800                &self.inv_freq,
15801                self.pool.as_deref(),
15802                &mut logits,
15803                true,
15804            );
15805            self.graph_logits = Some(logits);
15806            return vec![0.0; self.hidden_size];
15807        }
15808        // DeepSeek-V4 runs its own stack: the state is hc_mult copies, and
15809        // the forward returns LOGITS, not a hidden — the head is inside it
15810        // (the final fold sits between the last layer and the norm). The
15811        // token id rides in `hidden[0]`, written by embed_single, because
15812        // the hash layers route by id rather than by content.
15813        if let Some(b) = &mut self.dsv4 {
15814            let _ = (task_mask, upto);
15815            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15816            let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
15817            st.pos = position;
15818            let mut logits = Vec::new();
15819            crate::dsv4::forward_token(
15820                g,
15821                layers,
15822                &cfg,
15823                st,
15824                token_id,
15825                &self.inv_freq,
15826                self.pool.as_deref(),
15827                &mut logits,
15828            );
15829            self.graph_logits = Some(logits);
15830            self.dspark_probe(position, token_id);
15831            // The caller expects a hidden; the logits went out of band, as
15832            // with the fused lm_head path.
15833            return vec![0.0; self.hidden_size];
15834        }
15835        // DeepSeek-V4.1 owns its complete stack and emits logits out of band.
15836        if let Some(b) = &mut self.dsv41 {
15837            let _ = (task_mask, upto);
15838            let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15839            let mut logits = Vec::new();
15840            crate::dsv41::forward_token(
15841                &b.0,
15842                &b.1,
15843                &b.2,
15844                &mut b.3,
15845                token_id,
15846                position,
15847                self.pool.as_deref(),
15848                &mut logits,
15849            );
15850            self.graph_logits = Some(logits);
15851            return vec![0.0; self.hidden_size];
15852        }
15853        // Gemma-3n runs its own stack (4 AltUp replicas don't fit this
15854        // loop); `hidden` is the extended embedding from embed_single.
15855        if let Some(b) = &self.g3n {
15856            let _ = (task_mask, upto);
15857            return crate::g3n::g3n_forward(
15858                &b.0,
15859                &b.1,
15860                hidden,
15861                position,
15862                &mut self.kv_cache.layers,
15863                self.num_heads,
15864                self.num_kv_heads,
15865                self.head_dim,
15866                self.pool.as_deref(),
15867            );
15868        }
15869        // Cortiq Embryo owns a separate resident graph: phase recurrent
15870        // state, resonance routing, the GQA anchor KV and hierarchical head
15871        // all execute in one Vulkan submit. It is limited to a complete
15872        // unmasked stack; spans and task masks retain the exact host path.
15873        // `CMF_EMBRYO_DBG=1` names the gate that keeps a token off the
15874        // resident graph — every refusal below is otherwise silent.
15875        if from == 0
15876            && upto.is_none()
15877            && task_mask.is_none()
15878            && self.anchor_core.is_some()
15879            && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
15880        {
15881            static ONCE: std::sync::Once = std::sync::Once::new();
15882            ONCE.call_once(|| {
15883                eprintln!(
15884                    "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
15885                     unsupported={} eligible={}",
15886                    crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
15887                    crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
15888                    std::env::var("CMF_EMBRYO_RESIDENT").ok(),
15889                    crate::gpu::enabled_here(),
15890                    self.graph_refused(),
15891                    self.embryo_resident_eligible(),
15892                );
15893            });
15894        }
15895        if from == 0
15896            && upto.is_none()
15897            && task_mask.is_none()
15898            // Embryo's recurrent/KV state has no host import path.  Do not
15899            // seed it for a prefill-only graph and then silently decode from
15900            // an empty CPU cache; both phases must select the resident owner.
15901            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15902            && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15903            // This whole-token Embryo path remains explicitly opt-in.
15904            // `CMF_GPU_WGPU_GRAPH=1` still enables the mature generic graph,
15905            // but must not silently select this model-specific resident path.
15906            && matches!(
15907                std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15908                Ok("1") | Ok("parallel")
15909            )
15910            && crate::gpu::enabled_here()
15911            && !self.graph_refused()
15912            // Sequence owner: past position zero the device continues only
15913            // a sequence it holds. A host-owned sequence (the graph refused
15914            // at its start, or its prefix was prefilled on the host) keeps
15915            // the host path to its end — never a device attempt at p > 0
15916            // over an empty device image.
15917            && (position == 0 || self.device_sequence_position().is_some())
15918            && self.embryo_resident_eligible()
15919            && let Some(model) = self.ensure_embryo_graph()
15920        {
15921            let mut lg = Vec::new();
15922            if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
15923            {
15924                self.graph_logits = Some(lg);
15925                return vec![0.0; self.hidden_size];
15926            }
15927            if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15928                eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
15929            }
15930            // The refusal is THIS pipeline's: falling through once is safe
15931            // at position zero (the host owns the sequence from here),
15932            // while a refusal at a later position of a device-owned
15933            // sequence would mix a host KV/state path with a partial
15934            // device sequence.
15935            self.mark_graph_refused();
15936            if position != 0 {
15937                // Fail this sequence alone and leave nothing stale behind:
15938                // no reuse key, no host or device state for the next
15939                // request on this slot to "extend".
15940                self.kv_cache.clear();
15941                self.clear_history();
15942                crate::gpu::graph_kv_reset(self.graph_kv_id);
15943                panic!(
15944                    "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
15945                );
15946            }
15947        }
15948        let mut h = hidden.to_vec();
15949        // MiMo-V2 expert placement: decided before the graph or the per-op
15950        // arena can claim the budget the expert bank needs.
15951        self.mimo_moe_prepare();
15952        let _mimo_q8 = self
15953            .mimo_moe
15954            .is_on()
15955            .then(crate::qtensor::enter_full_gpu_q8_scope);
15956        // Split borrows: copy scalars / clone handles so the per-layer
15957        // cfg does not hold `&self` while the KV cache is `&mut`.
15958        let (nh, _nkv, _hd, hs, _rd, eps) = (
15959            self.num_heads,
15960            self.num_kv_heads,
15961            self.head_dim,
15962            self.hidden_size,
15963            self.rotary_dim,
15964            self.rms_eps,
15965        );
15966        let pool = self.pool.clone();
15967        // Opt-in wgpu token-graph attention (discrete Vulkan/DX12): the whole
15968        // attention sub-block runs resident in one submit. Off by default.
15969        // Whole-token wgpu graph: eligibility + arbitration.
15970        //  - explicit CMF_GPU_WGPU_GRAPH forces it on/off;
15971        //  - discrete adapters (4090: decode 76 -> 137 tok/s) and GDN
15972        //    hybrids (recurrent state device-resident, no CPU twin to
15973        //    race) TRUST it;
15974        //  - integrated/mobile adapters RACE it against the normal path
15975        //    at generation granularity (gpu::graph_race_*) — tiled
15976        //    mobile GPUs can turn the ~300-dispatch graph into seconds
15977        //    per token, while a fast phone GPU keeps its win.
15978        let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
15979        let graph_on = match graph_env.as_deref() {
15980            Some("0") => false,
15981            Some("prefill") => false, // decode keeps the per-op path
15982            Some(_) => true,
15983            // Unset: same discrete-only default as every other graph
15984            // site. "Is the GPU on" used to stand in here — which made
15985            // the 0.2 tok/s whole-token graph race-eligible on mobile
15986            // adapters and cost 12-14× on first tokens (cmfmobile
15987            // TUNING.md); integrated GPUs keep the per-op probe path.
15988            None => crate::gpu::wgpu_graph_default(),
15989        };
15990        let graph_trusted =
15991            graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
15992        let race_eligible =
15993            graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
15994        let mut tail_start = 0usize;
15995        if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
15996            let t_graph = std::time::Instant::now();
15997            let mut lg = Vec::new();
15998            let mut gl = 0usize;
15999            let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
16000            let declined = built.is_none();
16001            let built = match built {
16002                Some(Ok(hh)) => Some(hh),
16003                Some(Err(())) => {
16004                    // O(1) state was admitted before the device failure; the
16005                    // CPU mirrors are stale by construction.  Clear the whole
16006                    // sequence and stop rather than walking that stale state.
16007                    self.clear_sequence_state();
16008                    self.graph_failed
16009                        .store(true, std::sync::atomic::Ordering::Relaxed);
16010                    self.cancel
16011                        .store(true, std::sync::atomic::Ordering::Relaxed);
16012                    tracing::error!("token graph failed after admission; sequence state cleared");
16013                    return vec![0.0; self.hidden_size];
16014                }
16015                None => None,
16016            };
16017            // Past the transient guards (o1 still collecting, a softcap)
16018            // a refusal is about the weights and will never change —
16019            // remember it instead of walking every layer again next
16020            // token.
16021            if declined && !self.o1_active() && self.attn_softcap == 0.0 {
16022                self.mark_graph_refused();
16023            }
16024            graph_note(built.is_some(), gl, self.num_layers);
16025            if let Some(hh) = built {
16026                let dur = t_graph.elapsed();
16027                if std::env::var("CMF_GRAPH_PROF").is_ok() {
16028                    eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
16029                }
16030                if gl > 0 && gl < self.num_layers {
16031                    // Device prefix: the graph ran layers 0..gl and handed
16032                    // back the boundary hidden — the loop below owns the
16033                    // tail. The prefix layers' KV/state advanced on the
16034                    // device; the tail's advances on the host below. One
16035                    // boundary crossing per token.
16036                    h = hh;
16037                    tail_start = gl;
16038                } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
16039                    if !graph_trusted {
16040                        crate::gpu::graph_race_record(true, dur);
16041                    }
16042                    if !lg.is_empty() {
16043                        // Graph produced logits (final-norm + lm_head folded in) —
16044                        // pad/cap to vocab and hand them to the sampler directly.
16045                        lg.resize(self.vocab_size, 0.0);
16046                        if let Some(c) = self.final_softcap {
16047                            softcap_in_place(&mut lg, c, self.pool.as_deref());
16048                        }
16049                        self.graph_logits = Some(lg);
16050                    }
16051                    return hh;
16052                }
16053                // Hopeless first graph token: discard it and fall through
16054                // to the normal path. Safe exactly here — the prompt KV is
16055                // still CPU-owned (chunked prefill), so recomputing this
16056                // position is exact; the mirror's extra row is never read
16057                // (the race just settled on the normal path).
16058            }
16059        }
16060        // KIMI-LINEAR HAS NO SPLIT BUG. The 2.6× reported from the
16061        // model rotation (12.2 tok/s on one card against 4.6 on two)
16062        // was a single measurement of a model whose arm arbitration is
16063        // borderline, and it did not survive repetition. Three runs an
16064        // arm, same binary, back to back:
16065        //   probe on : 1 GPU 9.5 / 5.7 / 5.9   2 GPU 7.8 / 13.0 / 13.3
16066        //   pinned   : 1 GPU 5.6 / 5.3 / 5.2   2 GPU 3.5 / 4.2 / 3.4
16067        // With the arms pinned the split costs about 1.45×, which is
16068        // what a layer split costs. With the probe free, TWO CARDS RUN
16069        // FASTER — because for this model the CPU arm wins some op
16070        // classes and the probe finds that.
16071        //
16072        // Two things do stand, and both are measured. The token graph
16073        // builds NOTHING here (`covered 0 of 14 layers [0..14)`), so
16074        // every layer walks per-op on either arm — that is where the
16075        // headroom is, not in the split. And this model's benchmark is
16076        // unusable without `CMF_GPU_PROBE=0`: the arbitration alone
16077        // moves it by more than 2×.
16078        //
16079        // Span runs (network split): the graph covers exactly [from..=upto]
16080        // — one submit per SEGMENT per token. No race: its state is global
16081        // and calibrated on full stacks, so spans take the graph only where
16082        // it is trusted by default (discrete adapters / CMF_GPU_WGPU_GRAPH).
16083        let span = from > 0 || upto.is_some();
16084        if span && graph_on && task_mask.is_none() && graph_trusted {
16085            let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
16086            let mut lg = Vec::new();
16087            let mut gl = 0usize;
16088            let span_res =
16089                self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
16090            let span_res = match span_res {
16091                Some(Ok(hh)) => Some(hh),
16092                Some(Err(())) => {
16093                    self.clear_sequence_state();
16094                    self.graph_failed
16095                        .store(true, std::sync::atomic::Ordering::Relaxed);
16096                    self.cancel
16097                        .store(true, std::sync::atomic::Ordering::Relaxed);
16098                    tracing::error!(
16099                        "span token graph failed after admission; sequence state cleared"
16100                    );
16101                    return vec![0.0; self.hidden_size];
16102                }
16103                None => None,
16104            };
16105            graph_note(span_res.is_some(), gl, upto_excl - from);
16106            if std::env::var("CMF_GPU_DEBUG").is_ok() {
16107                // How much of the span the graph actually covered. A
16108                // prefix of nothing means every layer walks per-op and
16109                // the split's extra cost is elsewhere.
16110                static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
16111                if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
16112                    eprintln!(
16113                        "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
16114                        upto_excl - from,
16115                        span_res.is_some()
16116                    );
16117                }
16118            }
16119            if let Some(hh) = span_res {
16120                if gl == upto_excl - from {
16121                    if !lg.is_empty() {
16122                        lg.resize(self.vocab_size, 0.0);
16123                        if let Some(c) = self.final_softcap {
16124                            softcap_in_place(&mut lg, c, self.pool.as_deref());
16125                        }
16126                        self.graph_logits = Some(lg);
16127                    }
16128                    crate::gpu::set_layer(-1);
16129                    return hh;
16130                }
16131                // Partial device prefix of the span: CPU owns the tail.
16132                h = hh;
16133                tail_start = from + gl;
16134            }
16135        }
16136        // Layers the host is about to run whose device mirror moved ahead
16137        // of the host cache (a device prefix that shrank since the prompt,
16138        // a batched-prefill prefix longer than this token's): bring their
16139        // rows over first. One comparison per layer when nothing lags.
16140        let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
16141
16142        // A partial graph is an explicit GPU-prefix / CPU-tail split. Keep
16143        // the tail PURE host-side: letting its QTensor hooks re-enter the
16144        // residency arena streams every omitted layer through Vulkan and the
16145        // driver's freed-allocation cache can grow to the full model size
16146        // (25.4 GiB observed with a 14 GiB budget on Granite 30B Q8_2F).
16147        // With a MiMo expert bank the tail is not a whole-layer host
16148        // stream: its experts run from the bank (never the arena) and its
16149        // projections stay per-op on the device, which the bank's placement
16150        // left room for.
16151        let host_tail = tail_start > from;
16152        let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
16153        let automatic_gpu_prefix = self.automatic_gpu_prefix();
16154
16155        let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
16156        #[cfg(target_os = "macos")]
16157        let mut gpu_skip_until = 0usize;
16158        for li in tail_start.max(from)..self.num_layers {
16159            let _capacity_tail = automatic_gpu_prefix
16160                .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
16161                .map(|_| crate::gpu::enter_cpu_scope());
16162            crate::gpu::set_layer(li as i64); // layer-split GPU/CPU (CMF_GPU_LAYERS)
16163            if let Some(u) = upto {
16164                if li > u {
16165                    break;
16166                }
16167            }
16168            if let Some(mask) = task_mask {
16169                if !mask.layer_alive(li) {
16170                    continue; // dead layer: residual pass-through
16171                }
16172            }
16173            // Whole-block q1 token graph: a run of consecutive q1
16174            // layers — GDN and full attention — executes with one sync
16175            // per CPU attend instead of per op (macOS/Metal).
16176            #[cfg(target_os = "macos")]
16177            {
16178                if li < gpu_skip_until {
16179                    continue;
16180                }
16181                if task_mask.is_none() {
16182                    let end = self.q1_graph_gpu(li, upto, position, &mut h);
16183                    if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
16184                        // The graph may have mutated device state before a
16185                        // command-buffer error. Never continue with a CPU
16186                        // tail or read a stale host mirror after admission.
16187                        return vec![0.0; self.hidden_size];
16188                    }
16189                    if end > li {
16190                        gpu_skip_until = end;
16191                        // Looped Transformer: the graph stopped at a loop
16192                        // boundary — apply final norm before the next iteration.
16193                        if self.is_loop_end(end - 1) && end < self.num_layers {
16194                            h = inference::rms_norm(
16195                                &h,
16196                                &self.weights.final_norm,
16197                                self.rms_eps,
16198                                self.norm_style,
16199                            );
16200                        }
16201                        continue;
16202                    }
16203                }
16204            }
16205
16206            if task_mask.is_none() {
16207                match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
16208                    crate::gpu::BatchGraphOutcome::Completed => continue,
16209                    crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
16210                    crate::gpu::BatchGraphOutcome::Declined => {}
16211                }
16212            }
16213            #[cfg(feature = "gpu")]
16214            self.pull_lagging_host_kv(li, li + 1, position);
16215            let lw = &self.weights.layers[self.phys_layer(li)];
16216            if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
16217                if tp.parse::<usize>().ok() == Some(position) {
16218                    let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
16219                    eprintln!(
16220                        "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
16221                        h[0], h[1]
16222                    );
16223                }
16224            }
16225            // Norm into the pipeline scratch — the returning rms_norm
16226            // allocated twice per layer per token (roadmap §3 P0).
16227            let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16228            inference::rms_norm_into(
16229                &h,
16230                &lw.input_norm,
16231                self.rms_eps,
16232                self.norm_style,
16233                &mut self.ws.n1,
16234            );
16235            drop(prof);
16236
16237            let attn_out = match &lw.attn {
16238                AttnKind::Mla(w) => {
16239                    let inv_freq_l = self.layer_inv_freq(li);
16240                    let rs = self.layer_rope_scale(li);
16241                    let eps = self.rms_eps;
16242                    let pool = self.pool.clone();
16243                    mla_attention(
16244                        w,
16245                        &self.ws.n1,
16246                        &mut self.kv_cache.layers[li],
16247                        position,
16248                        &inv_freq_l,
16249                        rs,
16250                        eps,
16251                        pool.as_deref(),
16252                    )
16253                }
16254                AttnKind::Linear(w) => {
16255                    let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
16256                    vmf_phase_forward(
16257                        &self.ws.n1,
16258                        w,
16259                        &cfg,
16260                        &mut self.kv_cache.layers[li].linear_state,
16261                        self.pool.as_deref(),
16262                    )
16263                }
16264                AttnKind::Kda(w) => {
16265                    let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
16266                    crate::linear_core::kda_forward(
16267                        &self.ws.n1,
16268                        w,
16269                        &cfg,
16270                        &mut self.kv_cache.layers[li].linear_state,
16271                        self.pool.as_deref(),
16272                    )
16273                }
16274                AttnKind::LinearGdn(w) => {
16275                    let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
16276                    gdn_forward(
16277                        &self.ws.n1,
16278                        w,
16279                        &cfg,
16280                        &mut self.kv_cache.layers[li].linear_state,
16281                        self.pool.as_deref(),
16282                    )
16283                }
16284                AttnKind::ShortConv(w) => {
16285                    let cfg = self
16286                        .short_conv_cfg
16287                        .expect("short-conv layer without short_conv_cfg");
16288                    short_conv_forward(
16289                        &self.ws.n1,
16290                        w,
16291                        &cfg,
16292                        &mut self.kv_cache.layers[li].linear_state,
16293                        self.pool.as_deref(),
16294                    )
16295                }
16296                AttnKind::Bounded(w) => {
16297                    // Natively bounded anchor: insert into the ring, attend
16298                    // over sinks ∪ window. No position, nothing appended.
16299                    let rope = self
16300                        .bounded_rope
16301                        .clone()
16302                        .expect("bounded layer without an installed rotation table");
16303                    let cfg = crate::bounded::BoundedAttnCfg {
16304                        num_heads: self.num_heads,
16305                        num_kv_heads: self.num_kv_heads,
16306                        head_dim: self.head_dim,
16307                        hidden_size: hs,
16308                        scale: self.attn_scale,
16309                        rope: &rope,
16310                        pool: pool.as_deref(),
16311                    };
16312                    crate::bounded::bounded_attention(
16313                        &self.ws.n1,
16314                        w,
16315                        &mut self.kv_cache.layers[li],
16316                        &cfg,
16317                    )
16318                }
16319                AttnKind::Full {
16320                    wq,
16321                    wk,
16322                    wv,
16323                    wo,
16324                    q_norm,
16325                    k_norm,
16326                    output_gate,
16327                    softplus_gate,
16328                    bias,
16329                } if self.kv_cache.layers[li].o1_sealed() => {
16330                    // O(1) override: decode on the sealed Nyström state
16331                    // instead of the growing KV cache.
16332                    let inv_freq_l = self.layer_inv_freq(li);
16333                    let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16334                    let cfg = QwenAttnCfg {
16335                        num_heads: self.layer_num_heads(li),
16336                        num_kv_heads: nkv_l,
16337                        head_dim: hd_l,
16338                        hidden_size: hs,
16339                        position,
16340                        inv_freq: &inv_freq_l,
16341                        rotary_dim: rd_l,
16342                        scale: self.attn_scale,
16343                        softcap: self.attn_softcap,
16344                        window: None,
16345                        v_norm: self.attn_v_norm,
16346                        qk_norm_after_rope: self.qk_norm_after_rope,
16347                        gate_sigmoid: self.proj_gate_sigmoid,
16348                        q_norm: q_norm.as_deref(),
16349                        k_norm: k_norm.as_deref(),
16350                        output_gate: *output_gate,
16351                        softplus_gate: softplus_gate
16352                            .as_ref()
16353                            .map(|(gate, per_head)| (gate, *per_head)),
16354                        rope_scale: self.layer_rope_scale(li),
16355                        bias: bias
16356                            .as_ref()
16357                            .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16358                        rms_eps: eps,
16359                        norm_style: self.norm_style,
16360                        pool: pool.as_deref(),
16361                        v_head_dim: self.layer_v_dim(li),
16362                    };
16363                    attention::qwen_attention_nystrom(
16364                        &self.ws.n1,
16365                        wq,
16366                        wk,
16367                        wv,
16368                        wo,
16369                        &mut self.kv_cache.layers[li],
16370                        &cfg,
16371                    )
16372                }
16373                AttnKind::Full {
16374                    wq,
16375                    wk,
16376                    wv,
16377                    wo,
16378                    q_norm,
16379                    k_norm,
16380                    output_gate,
16381                    softplus_gate,
16382                    bias,
16383                } => 'attn: {
16384                    // wgpu token-graph attention (opt-in): whole sub-block in
16385                    // one submit, device K/V mirror. q1 only, no gate/bias/mask.
16386                    // Its kernel has no window, sink or narrow-V slot and
16387                    // one mirror geometry: such models stay on the CPU attend.
16388                    let dropin_reason =
16389                        graph_on.then(|| self.graph_attn_decline_reason()).flatten();
16390                    if let Some(reason) = dropin_reason {
16391                        self.note_graph_decline("wgpu attn dropin", reason);
16392                    }
16393                    if graph_on
16394                        && dropin_reason.is_none()
16395                        && !*output_gate
16396                        && softplus_gate.is_none()
16397                        && self.attention_heads_per_layer.is_none()
16398                        && bias.is_none()
16399                        && task_mask.is_none()
16400                    {
16401                        let inv_freq_l = self.layer_inv_freq(li);
16402                        let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16403                        let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16404                        if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
16405                            wq.mapped_q1(),
16406                            wk.mapped_q1(),
16407                            wv.mapped_q1(),
16408                            wo.mapped_q1(),
16409                        ) {
16410                            let gm = gm.clone();
16411                            let mut out = vec![0f32; hs];
16412                            let cache = &self.kv_cache.layers[li];
16413                            if crate::gpu::attn_dropin(
16414                                &gm,
16415                                self.graph_kv_id,
16416                                li,
16417                                &self.ws.n1,
16418                                qi,
16419                                ki,
16420                                vi,
16421                                oi,
16422                                q_norm.as_deref(),
16423                                k_norm.as_deref(),
16424                                self.qk_norm_after_rope,
16425                                &inv_freq_l,
16426                                nh,
16427                                nkv_l,
16428                                hd_l,
16429                                rd_l,
16430                                hs,
16431                                position,
16432                                self.kv_cache.max_seq_len,
16433                                gemma,
16434                                eps as f32,
16435                                cache.k_heads(),
16436                                cache.v_heads(),
16437                                &mut out,
16438                            ) {
16439                                break 'attn out;
16440                            }
16441                        }
16442                    }
16443                    let masked = task_mask
16444                        .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
16445                        .unwrap_or(false);
16446                    let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
16447                    // The masked kernel knows one pipeline-wide geometry and
16448                    // RoPE table, no window and no sink.
16449                    let plain = self.layer_attn_plain(li);
16450                    match (masked, f32_view) {
16451                        // Historical masked path (f32 slices; the loader
16452                        // keeps masked models in f32).
16453                        (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
16454                            let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
16455                            attention::multi_head_attention(
16456                                &self.ws.n1,
16457                                q,
16458                                k,
16459                                v,
16460                                o,
16461                                &mut self.kv_cache.layers[li],
16462                                self.num_heads,
16463                                self.num_kv_heads,
16464                                self.head_dim,
16465                                self.hidden_size,
16466                                position,
16467                                &active_heads,
16468                                &self.inv_freq,
16469                            )
16470                        }
16471                        (masked, _) => {
16472                            if masked {
16473                                tracing::warn!(
16474                                    "layer {li}: head mask on quantized weights or on a \
16475                                     window/sink/per-layer-geometry layer not supported \
16476                                     yet — executing dense"
16477                                );
16478                            }
16479                            let inv_freq_l = self.layer_inv_freq(li);
16480                            let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16481                            let cfg = QwenAttnCfg {
16482                                num_heads: self.layer_num_heads(li),
16483                                num_kv_heads: nkv_l,
16484                                head_dim: hd_l,
16485                                hidden_size: hs,
16486                                position,
16487                                inv_freq: &inv_freq_l,
16488                                rotary_dim: rd_l,
16489                                scale: self.attn_scale,
16490                                softcap: self.attn_softcap,
16491                                window: self.layer_window(li),
16492                                v_norm: self.attn_v_norm,
16493                                qk_norm_after_rope: self.qk_norm_after_rope,
16494                                gate_sigmoid: self.proj_gate_sigmoid,
16495                                q_norm: q_norm.as_deref(),
16496                                k_norm: k_norm.as_deref(),
16497                                output_gate: *output_gate,
16498                                softplus_gate: softplus_gate
16499                                    .as_ref()
16500                                    .map(|(gate, per_head)| (gate, *per_head)),
16501                                rope_scale: self.layer_rope_scale(li),
16502                                bias: bias
16503                                    .as_ref()
16504                                    .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16505                                rms_eps: eps,
16506                                norm_style: self.norm_style,
16507                                pool: pool.as_deref(),
16508                                v_head_dim: self.layer_v_dim(li),
16509                            };
16510                            attention::qwen_attention(
16511                                &self.ws.n1,
16512                                wq,
16513                                wk,
16514                                wv,
16515                                wo,
16516                                &mut self.kv_cache.layers[li],
16517                                &cfg,
16518                            )
16519                        }
16520                    }
16521                }
16522            };
16523            // Gemma sandwich norm: normalize the attention branch before
16524            // it joins the residual stream.
16525            let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
16526                Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
16527                None => attn_out,
16528            };
16529            let lw = &self.weights.layers[self.phys_layer(li)];
16530            let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16531            inference::add_rmsnorm_fused_into(
16532                &mut h,
16533                &attn_out,
16534                &lw.post_norm,
16535                self.rms_eps,
16536                self.norm_style,
16537                &mut self.ws.p1,
16538            );
16539            drop(prof);
16540            let mut attn_out = attn_out;
16541            attention::recycle_buf(&mut attn_out);
16542            let post_normed = &self.ws.p1;
16543
16544            let ffn_masked = task_mask
16545                .map(|m| m.ffn_active_count(li) < self.intermediate_size)
16546                .unwrap_or(false);
16547            // One masked dense CONTRACT, dispatched by cost. The
16548            // activation-zeroing arm (the batched sweep's, validated
16549            // against the replica to 0.8%) computes the FULL fused FFN
16550            // and zeroes the dead — right whenever most neurons live.
16551            // The sparse arm reads ONLY active rows and down columns —
16552            // per-row dots are slower per element than the fused kernel,
16553            // so it pays only once the mask is deep enough. The 0.5
16554            // crossover is first-principles (fused kernels run ~2x the
16555            // per-row dot throughput); a shallow specialist (95% alive)
16556            // stays fused, a --target-sparsity bake flips arms on its
16557            // own weight.
16558            let ffn_out = match (ffn_masked, &lw.ffn) {
16559                // A defragged tube layer answers its own mask: the core
16560                // always runs, each tube runs when its bit is on, and
16561                // the tubes that are off are never read from the mmap.
16562                (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
16563                    let row = task_mask
16564                        .and_then(|tm| tm.ffn_masks.get(li))
16565                        .map(|v| v.as_slice());
16566                    tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
16567                }
16568                (true, FfnKind::Dense(d)) => {
16569                    let tm = task_mask.unwrap();
16570                    let alive = tm.ffn_active_count(li);
16571                    let deep = alive * 2 <= self.intermediate_size;
16572                    if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
16573                        let active = tm.ffn_active_indices(li);
16574                        sparse_ffn_quant(
16575                            d,
16576                            post_normed,
16577                            &active,
16578                            self.hidden_size,
16579                            self.pool.as_deref(),
16580                        )
16581                    } else if deep
16582                        && let (Some(g), Some(u), Some(dn)) = (
16583                            d.gate_proj.as_f32(),
16584                            d.up_proj.as_f32(),
16585                            d.down_proj.as_f32(),
16586                        )
16587                    {
16588                        let active = tm.ffn_active_indices(li);
16589                        inference::sparse_ffn_forward(
16590                            post_normed,
16591                            g,
16592                            u,
16593                            dn,
16594                            self.hidden_size,
16595                            self.intermediate_size,
16596                            &active,
16597                            self.pool.as_deref(),
16598                        )
16599                    } else {
16600                        let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
16601                        dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
16602                    }
16603                }
16604                (true, FfnKind::Moe(m)) => {
16605                    // MoE is sparse by expert selection; a task mask
16606                    // narrows the ROUTABLE set via its expert fields
16607                    // (spec §5) when it carries them.
16608                    let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
16609                    ffn_forward(
16610                        &lw.ffn,
16611                        post_normed,
16612                        self.pool.as_deref(),
16613                        allowed.as_deref(),
16614                    )
16615                }
16616                (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
16617                    dm,
16618                    post_normed,
16619                    &h,
16620                    self.rms_eps,
16621                    self.norm_style,
16622                    self.pool.as_deref(),
16623                ),
16624                (false, _) => match &lw.ffn {
16625                    FfnKind::DenseMoe(dm) => dense_moe_ffn(
16626                        dm,
16627                        post_normed,
16628                        &h,
16629                        self.rms_eps,
16630                        self.norm_style,
16631                        self.pool.as_deref(),
16632                    ),
16633                    FfnKind::Moe(m)
16634                        if task_mask.is_none()
16635                            && host_tail
16636                            && li >= tail_start
16637                            && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
16638                    {
16639                        moe_ffn_minicpm_tail(
16640                            &mut self.minicpm_tail,
16641                            li,
16642                            self.num_layers,
16643                            m,
16644                            post_normed,
16645                            self.pool.as_deref(),
16646                        )
16647                    }
16648                    FfnKind::Moe(m)
16649                        if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
16650                    {
16651                        moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
16652                    }
16653                    _ => {
16654                        let allowed = match (&lw.ffn, task_mask) {
16655                            (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
16656                            _ => None,
16657                        };
16658                        ffn_forward(
16659                            &lw.ffn,
16660                            post_normed,
16661                            self.pool.as_deref(),
16662                            allowed.as_deref(),
16663                        )
16664                    }
16665                },
16666            };
16667            let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
16668                Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
16669                None => ffn_out,
16670            };
16671            for (i, &f) in ffn_out.iter().enumerate() {
16672                h[i] += f;
16673            }
16674            let mut ffn_out = ffn_out;
16675            attention::recycle_buf(&mut ffn_out);
16676
16677            // Gemma-4: the layer output is scaled by a learned scalar.
16678            if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
16679                for v in h.iter_mut() {
16680                    *v *= sc;
16681                }
16682            }
16683            // CMF_LAYER_DUMP: this position's hidden after layer li.
16684            if self.layer_dump.is_some() {
16685                self.dump_layer_row(position, li, &h);
16686            }
16687
16688            // Looped Transformer: apply final norm at the end of each loop iteration.
16689            // Nanbeige 4.2: after layer 21 (virtual), apply norm before looping back to layer 0.
16690            if self.is_loop_end(li) && li + 1 < self.num_layers {
16691                h = inference::rms_norm(
16692                    &h,
16693                    &self.weights.final_norm,
16694                    self.rms_eps,
16695                    self.norm_style,
16696                );
16697            }
16698
16699            // Dynamic routing φ capture (on-policy): the
16700            // EMA of the post-residual hidden at the router's phi_layer,
16701            // updated as the context evolves during decode.
16702            if self.dyn_phi_layer == Some(li) {
16703                self.update_dyn_phi(&h);
16704            }
16705        }
16706        crate::gpu::set_layer(-1); // layers done — lm_head outside layer-split
16707        if let Some(t) = t_race_cpu {
16708            crate::gpu::graph_race_record(false, t.elapsed());
16709        }
16710
16711        h
16712    }
16713
16714    /// EMA of φ at the router layer (rolling, weight 0.2 = ~5-token
16715    /// horizon). First observation seeds it exactly.
16716    fn update_dyn_phi(&mut self, h: &[f32]) {
16717        const A: f32 = 0.2;
16718        if self.dyn_phi_ema.len() != h.len() {
16719            self.dyn_phi_ema = vec![0.0; h.len()];
16720            self.dyn_phi_seen = 0;
16721        }
16722        if self.dyn_phi_seen == 0 {
16723            self.dyn_phi_ema.copy_from_slice(h);
16724        } else {
16725            for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
16726                *e = (1.0 - A) * *e + A * v;
16727            }
16728        }
16729        self.dyn_phi_seen += 1;
16730    }
16731
16732    /// Current router φ (EMA at phi_layer); empty until first capture.
16733    pub fn dyn_phi(&self) -> &[f32] {
16734        &self.dyn_phi_ema
16735    }
16736
16737    /// Enable/disable φ capture at the router layer, reset the EMA.
16738    pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
16739        self.dyn_phi_layer = layer;
16740        self.dyn_phi_ema.clear();
16741        self.dyn_phi_seen = 0;
16742    }
16743
16744    /// Skills eligible for dynamic switching: (index, id, phi_layer).
16745    pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
16746        let Some(model) = &self.model else {
16747            return Vec::new();
16748        };
16749        model
16750            .header
16751            .skills
16752            .iter()
16753            .enumerate()
16754            .filter_map(|(i, sk)| {
16755                // A v2 record routes only through the request-level
16756                // backbone-gated decision (its status and gate are
16757                // checked there), never per token.
16758                if sk.is_v2() {
16759                    return None;
16760                }
16761                let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
16762                let sel = sk.selection.as_ref()?;
16763                (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
16764            })
16765            .collect()
16766    }
16767
16768    /// Index of the currently overlaid skill (None = backbone).
16769    pub fn active_skill(&self) -> Option<usize> {
16770        self.dyn_active
16771    }
16772
16773    /// Enable dynamic per-token skill routing: build the hysteresis
16774    /// router from the container's routable skills, start φ capture at
16775    /// their (shared) phi_layer. Returns the number of routable skills
16776    /// (0 = nothing to route; router stays off). Idempotent.
16777    pub fn enable_dynamic_routing(&mut self) -> usize {
16778        use crate::swarm::{DynRouter, RoutableSkill};
16779        let Some(model) = self.model.clone() else {
16780            return 0;
16781        };
16782        // Router policy v2 (spec §9.4) routes per REQUEST: the backbone is
16783        // the default and only the backbone-gated decision may pick a
16784        // skill. A per-token switch would bypass that gate (and change
16785        // the O(1) state mid-sequence), so the hysteresis router never
16786        // runs on such a file; the caller keeps the request-level
16787        // decision.
16788        if let Some(r) = &model.header.router {
16789            tracing::warn!(
16790                "dynamic routing disabled: this file declares router policy '{}' with \
16791                 granularity \"{}\" — the request-level decision applies instead",
16792                r.policy,
16793                r.granularity
16794            );
16795            return 0;
16796        }
16797        // Format-v2 skill records (bit SKILLS_V2) without a router policy:
16798        // their status/gate contract ("auto-routing requires active +
16799        // measured") lives in the backbone-gated decision only — the
16800        // hysteresis router would switch into a quarantined record
16801        // (fail-open). Refuse the whole file, not just its v2 records.
16802        if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
16803            || model.header.skills.iter().any(|s| s.is_v2())
16804        {
16805            tracing::warn!(
16806                "dynamic routing disabled: this file carries format-v2 skill records \
16807                 (SKILLS_V2) — they route per request through a router policy only"
16808            );
16809            return 0;
16810        }
16811        // A blend materialized f32 working tensors into the layers; there
16812        // is no single skill index to revert from → refuse (honest).
16813        if self.dyn_blend_loaded {
16814            tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
16815            return 0;
16816        }
16817        // A statically-overlaid skill that is NOT FFN-eligible can't be
16818        // cheaply reverted at generation start → refuse rather than
16819        // silently keep it overlaid.
16820        if let Some(a) = self.dyn_active {
16821            if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
16822                tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
16823                return 0;
16824            }
16825        }
16826        let hidden = self.hidden_size;
16827        let mut skills = Vec::new();
16828        for (idx, id, _phi) in self.dynamic_skills() {
16829            if let Some(sel) = model.header.skills[idx].selection.as_ref() {
16830                if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
16831                    skills.push(rs);
16832                }
16833            }
16834        }
16835        if skills.is_empty() {
16836            return 0;
16837        }
16838        // Skills should share a phi_layer; warn (not fail) if they don't.
16839        let phi = skills[0].phi_layer;
16840        if skills.iter().any(|s| s.phi_layer != phi) {
16841            tracing::warn!("routable skills disagree on phi_layer; using {phi}");
16842        }
16843        let n = skills.len();
16844        self.set_dyn_phi_layer(Some(phi));
16845        self.dyn_router = Some(DynRouter::new(skills));
16846        n
16847    }
16848
16849    /// Human-readable switch log from the last dynamic-routed generation.
16850    pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
16851        self.dyn_router
16852            .as_ref()
16853            .map(|r| r.switches.clone())
16854            .unwrap_or_default()
16855    }
16856
16857    /// LM head: hidden → logits [vocab_size]. The dominant matvec of
16858    /// every decode step — row-parallel on the worker pool.
16859    fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
16860        let _mimo_q8 = self
16861            .mimo_moe
16862            .is_on()
16863            .then(crate::qtensor::enter_full_gpu_q8_scope);
16864        let rows = self.weights.lm_head.rows();
16865        let mut logits = attention::take_buf(rows.min(self.vocab_size));
16866        // Banked MiMo uses the same exact projection family for the
16867        // plain/draft head and the batched verification head. Read both
16868        // scale planes in-place instead of preparing per-op scale buffers.
16869        let served = self.mimo_moe.is_on()
16870            && crate::gpu::mimo_q8_short_enabled()
16871            && rows == self.vocab_size
16872            && !self.weights.lm_head.has_prism_contract()
16873            && self
16874                .weights
16875                .lm_head
16876                .graph_weight()
16877                .is_some_and(|(model, idx, kind, _)| {
16878                    kind == 7
16879                        && crate::gpu::q82_short_rows(
16880                            model,
16881                            idx,
16882                            hidden,
16883                            1,
16884                            rows,
16885                            self.hidden_size,
16886                            &mut logits,
16887                        )
16888                });
16889        if !served {
16890            self.weights
16891                .lm_head
16892                .matvec(hidden, &mut logits, self.pool.as_deref());
16893        }
16894        logits.resize(self.vocab_size, 0.0);
16895        if let Some(m) = self.logit_multiplier {
16896            for l in logits.iter_mut() {
16897                *l *= m;
16898            }
16899        }
16900        if let Some(c) = self.final_softcap {
16901            softcap_in_place(&mut logits, c, self.pool.as_deref());
16902        }
16903        if let Some(cm) = self.head_clusters.as_ref() {
16904            self.hierarchical_head_logprobs(hidden, cm, &mut logits);
16905        }
16906        logits
16907    }
16908
16909    /// Two-level head (Cortiq Embryo): in place, logits[v] ← log p(v) =
16910    /// (lc[c] − lse(lc)) + (logit[v] − lse over v's cluster block), c = v / S.
16911    fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
16912        let h = hidden.len();
16913        let ncl = cm.len() / h.max(1);
16914        if ncl == 0 || logits.len() % ncl != 0 {
16915            return;
16916        }
16917        let cs = logits.len() / ncl;
16918        // cluster logits + log-softmax
16919        let mut lc = vec![0.0f32; ncl];
16920        for c in 0..ncl {
16921            let row = &cm[c * h..(c + 1) * h];
16922            let mut s = 0.0f32;
16923            for j in 0..h {
16924                s += row[j] * hidden[j];
16925            }
16926            lc[c] = s;
16927        }
16928        let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16929        let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
16930        for c in 0..ncl {
16931            let blk = &mut logits[c * cs..(c + 1) * cs];
16932            let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16933            let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
16934            let add = lc[c] - lse - bl;
16935            for v in blk.iter_mut() {
16936                *v += add;
16937            }
16938        }
16939    }
16940
16941    /// Prefill `ids` and return the next-token logits — what the model
16942    /// would predict next, WITHOUT committing to generation (introspection
16943    /// for `cortiq explain`). Clears and repopulates the KV cache; leaves
16944    /// the active overlay untouched.
16945    pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
16946        #[cfg(target_os = "macos")]
16947        crate::gpu_metal::set_io_namespace(self.graph_kv_id);
16948        self.clear_sequence_state();
16949        // This helper is used by the pooled classification endpoint, where
16950        // every request is a fresh sequence. The shared reset also clears the
16951        // wgpu token graph's device-side recurrent state.
16952        crate::gpu::graph_race_begin_generation();
16953        if task_mask.is_none() {
16954            self.o1_begin();
16955        }
16956        let mut hidden = vec![0.0f32; self.hidden_size];
16957        for (pos, &id) in ids.iter().enumerate() {
16958            let emb = self.embed_single(id);
16959            hidden = self.forward_layers(&emb, pos, task_mask);
16960        }
16961        if let Err(err) = self.o1_seal_checked() {
16962            self.o1_fail(err);
16963        }
16964        // Stacks that own their head (V4, V4.1, Qwen3.8-Flash-Next, GLM-5)
16965        // return a zero hidden and hand the logits out of band.
16966        if let Some(logits) = self.graph_logits.take() {
16967            return logits;
16968        }
16969        inference::rms_norm_into(
16970            &hidden,
16971            &self.weights.final_norm,
16972            self.rms_eps,
16973            self.norm_style,
16974            &mut self.ws.n1,
16975        );
16976        self.lm_head_forward(&self.ws.n1)
16977    }
16978}
16979
16980/// Convenience: deterministic tiny pipeline for tests.
16981pub fn create_test_pipeline(
16982    hidden_size: usize,
16983    intermediate_size: usize,
16984    num_heads: usize,
16985    num_kv_heads: usize,
16986    head_dim: usize,
16987    num_layers: usize,
16988    vocab_size: usize,
16989) -> Pipeline {
16990    // Small pseudo-random weights: constant weights make attention
16991    // degenerate and hide indexing bugs.
16992    let synth = |n: usize, salt: usize| -> Vec<f32> {
16993        (0..n)
16994            .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
16995            .collect()
16996    };
16997    let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
16998        QTensor::from_f32(synth(rows * cols, salt), rows, cols)
16999    };
17000    let layer_weights: Vec<LayerWeights> = (0..num_layers)
17001        .map(|li| LayerWeights {
17002            input_norm: vec![1.0; hidden_size],
17003            post_norm: vec![1.0; hidden_size],
17004            attn_out_norm: None,
17005            ffn_out_norm: None,
17006            layer_scale: None,
17007            ffn: FfnKind::Dense(DenseFfn {
17008                gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
17009                up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
17010                down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
17011                act: Act::Silu,
17012                down_t: None,
17013                segs: Vec::new(),
17014            }),
17015            attn: AttnKind::Full {
17016                bias: None,
17017                wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
17018                wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
17019                wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
17020                wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
17021                q_norm: None,
17022                k_norm: None,
17023                output_gate: false,
17024                softplus_gate: None,
17025            },
17026        })
17027        .collect();
17028
17029    Pipeline::new(
17030        Tokenizer::byte_level(),
17031        PipelineWeights {
17032            embed_tokens: qt(vocab_size, hidden_size, 100),
17033            layers: layer_weights,
17034            lm_head: qt(vocab_size, hidden_size, 200),
17035            final_norm: vec![1.0; hidden_size],
17036        },
17037        hidden_size,
17038        intermediate_size,
17039        num_heads,
17040        num_kv_heads,
17041        head_dim,
17042        num_layers,
17043        num_layers, // physical_layers = num_layers (non-looped)
17044        false,      // loop_final_norm
17045        vocab_size,
17046        1e-6,
17047        10_000.0,
17048        NormStyle::Qwen,
17049        4096,
17050        SamplerConfig {
17051            seed: Some(42),
17052            ..Default::default()
17053        },
17054    )
17055}
17056
17057/// Batched dense-FFN: gate/up/down via matmat (element-wise the same
17058/// math as b × dense_ffn — the same dot kernels).
17059/// One mask bit, LSB-first per byte — `TaskMask::ffn_active_indices`'s
17060/// convention.
17061#[inline]
17062fn mask_bit(row: &[u8], j: usize) -> bool {
17063    (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
17064}
17065
17066/// Zero the CLOSED neurons' activations in a [rows × inter] panel — the
17067/// masked-inference fast path's whole trick: full fused quant compute,
17068/// then the mask lands on the ACTIVATIONS, which is arithmetically the
17069/// pruned network without touching a quantized weight byte. Whole open
17070/// bytes (0xFF = 8 open neurons) skip in one test.
17071/// `CMF_FFN_MASK_GAIN` — Patent 12 FIG. 4, variance-preserving
17072/// rescaling: truncation removes a share of the layer's output energy,
17073/// so the survivors are scaled up to put the variance back where the
17074/// downstream norm expects it. A scalar here; per layer it is
17075/// `sqrt(total energy / kept energy)`.
17076fn mask_gain() -> f32 {
17077    static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
17078    *G.get_or_init(|| {
17079        std::env::var("CMF_FFN_MASK_GAIN")
17080            .ok()
17081            .and_then(|v| v.parse().ok())
17082            .unwrap_or(1.0)
17083    })
17084}
17085
17086fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
17087    // With CMF_FFN_MEANFILL a closed neuron contributes its average
17088    // instead of nothing — same bytes read, one constant restored.
17089    let fill = meanfill().and_then(|(i, v)| {
17090        let li = crate::gpu::cur_layer();
17091        (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
17092    });
17093    for r in 0..rows {
17094        let base = r * inter;
17095        for (bi, &byte) in row.iter().enumerate() {
17096            if byte == 0xFF {
17097                continue;
17098            }
17099            let j0 = bi * 8;
17100            for bit in 0..8 {
17101                let j = j0 + bit;
17102                if j < inter && byte & (1 << bit) == 0 {
17103                    g[base + j] = fill.map_or(0.0, |f| f[j]);
17104                }
17105            }
17106        }
17107    }
17108    let gain = mask_gain();
17109    if gain != 1.0 {
17110        for v in g[..rows * inter].iter_mut() {
17111            *v *= gain;
17112        }
17113    }
17114}
17115
17116/// True when neuron `i`'s bit is set (no mask = everything runs).
17117#[inline]
17118fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
17119    row.is_none_or(|r| mask_bit(r, i))
17120}
17121
17122/// Every bit below `n` set — the common case for a tube file's CORE,
17123/// where only the tube bits vary per task.
17124fn all_bits_on(row: &[u8], n: usize) -> bool {
17125    (0..n).all(|i| mask_bit(row, i))
17126}
17127
17128/// `CMF_TUBE_TOPK` — how many tubes a TOKEN may open (0 = the task mask
17129/// decides alone). This is the dense FFN read as a mixture: the tubes
17130/// are the experts a k-means over `gate_proj` rows found, and the token
17131/// picks among them. `CMF_TUBE_SCORE=gate` scores a tube by its own
17132/// gate (realizable: only `up`/`down` of the losers go unread),
17133/// `=oracle` scores by the true `silu(gate)·up` mass (the ceiling —
17134/// only `down` is saved, and the selection has read what it predicts).
17135fn tube_topk() -> usize {
17136    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17137    *K.get_or_init(|| {
17138        std::env::var("CMF_TUBE_TOPK")
17139            .ok()
17140            .and_then(|v| v.parse().ok())
17141            .unwrap_or(0)
17142    })
17143}
17144
17145fn tube_score_oracle() -> bool {
17146    static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17147    *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
17148}
17149
17150/// The routed arm of `tube_ffn`: a token opens only its best `k` tubes.
17151/// At `b == 1` (decode) the losers are genuinely never read — that is
17152/// the speed. At `b > 1` (the scoring sweep) every tube is computed and
17153/// the losers' activations are zeroed instead: same arithmetic, so the
17154/// perplexity is the routed model's, measured without a per-token
17155/// gather in the middle of a GEMM.
17156fn tube_ffn_routed(
17157    d: &DenseFfn,
17158    xs: &[f32],
17159    b: usize,
17160    pool: Option<&Pool>,
17161    mask_row: Option<&[u8]>,
17162    k: usize,
17163) -> Vec<f32> {
17164    let hidden = d.down_proj.rows();
17165    let core = d.gate_proj.rows();
17166    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
17167    let mut out = match (b, core_full, mask_row) {
17168        (1, true, _) => dense_ffn(d, xs, pool),
17169        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
17170        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
17171        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
17172    };
17173    let cand: Vec<usize> = (0..d.segs.len())
17174        .filter(|&i| tube_bit(mask_row, d.segs[i].start))
17175        .collect();
17176    if cand.is_empty() {
17177        return out;
17178    }
17179    // gate (and, where the score or the batch needs it, up) per tube.
17180    // The SCORE is taken at the point the serving path could take it:
17181    // off the gate alone, or off the finished activation for the oracle.
17182    let oracle = tube_score_oracle();
17183    let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
17184    let mut scores = vec![0f32; b * cand.len()];
17185    for (ci, &i) in cand.iter().enumerate() {
17186        let seg = &d.segs[i];
17187        let w = seg.width;
17188        let mut g = vec![0.0f32; b * w];
17189        if b == 1 {
17190            seg.gate.matvec(xs, &mut g, pool);
17191        } else {
17192            seg.gate.matmat(xs, b, &mut g, pool);
17193        }
17194        for v in g.iter_mut() {
17195            *v = Act::Silu.combine(*v, 1.0);
17196        }
17197        if !oracle {
17198            for t in 0..b {
17199                scores[t * cand.len() + ci] =
17200                    g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
17201            }
17202        }
17203        if oracle || b > 1 {
17204            let mut u = vec![0.0f32; b * w];
17205            if b == 1 {
17206                seg.up.matvec(xs, &mut u, pool);
17207            } else {
17208                seg.up.matmat(xs, b, &mut u, pool);
17209            }
17210            for (a, &v) in g.iter_mut().zip(u.iter()) {
17211                *a *= v;
17212            }
17213            if oracle {
17214                for t in 0..b {
17215                    scores[t * cand.len() + ci] =
17216                        g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
17217                }
17218            }
17219        }
17220        acts.push(g);
17221    }
17222    // per-token scores and the winners
17223    let keep = k.min(cand.len());
17224    let mut scratch: Vec<f32> = Vec::new();
17225    for t in 0..b {
17226        let mut sc: Vec<(f32, usize)> = (0..cand.len())
17227            .map(|ci| (scores[t * cand.len() + ci], ci))
17228            .collect();
17229        sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
17230        let mut alive = vec![false; cand.len()];
17231        for &(_, ci) in sc.iter().take(keep) {
17232            alive[ci] = true;
17233        }
17234        if b > 1 {
17235            for (ci, a) in acts.iter_mut().enumerate() {
17236                if !alive[ci] {
17237                    let w = d.segs[cand[ci]].width;
17238                    a[t * w..(t + 1) * w].fill(0.0);
17239                }
17240            }
17241        } else {
17242            // decode: finish only the winners — the losers' up/down
17243            // (and, with the gate score, everything but their gate)
17244            // are never touched.
17245            for (ci, &i) in cand.iter().enumerate() {
17246                if !alive[ci] {
17247                    continue;
17248                }
17249                let seg = &d.segs[i];
17250                let w = seg.width;
17251                let g = &mut acts[ci];
17252                if !tube_score_oracle() {
17253                    scratch.clear();
17254                    scratch.resize(w, 0.0);
17255                    seg.up.matvec(xs, &mut scratch, pool);
17256                    for (a, &v) in g.iter_mut().zip(scratch.iter()) {
17257                        *a *= v;
17258                    }
17259                }
17260                let mut acc = vec![0.0f32; hidden];
17261                seg.down.matvec(g, &mut acc, pool);
17262                for (o, a) in out.iter_mut().zip(&acc) {
17263                    *o += *a;
17264                }
17265            }
17266        }
17267    }
17268    if b > 1 {
17269        for (ci, &i) in cand.iter().enumerate() {
17270            let seg = &d.segs[i];
17271            let mut acc = vec![0.0f32; b * hidden];
17272            seg.down.matmat(&acts[ci], b, &mut acc, pool);
17273            for (o, a) in out.iter_mut().zip(&acc) {
17274                *o += *a;
17275            }
17276        }
17277    }
17278    out
17279}
17280
17281/// FFN of a defragged tube layer: the always-on core plus the tubes the
17282/// task mask switches on. Each tube is a normal tensor triple, so the
17283/// same kernels run it and an inactive tube's bytes are never read —
17284/// that is the whole point of the defrag (a scattered mask cannot skip
17285/// bytes; a contiguous one is just a smaller matrix).
17286fn tube_ffn(
17287    d: &DenseFfn,
17288    xs: &[f32],
17289    b: usize,
17290    pool: Option<&Pool>,
17291    mask_row: Option<&[u8]>,
17292) -> Vec<f32> {
17293    if tube_topk() > 0 {
17294        return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
17295    }
17296    let hidden = d.down_proj.rows();
17297    let core = d.gate_proj.rows();
17298    let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
17299    let mut out = match (b, core_full, mask_row) {
17300        (1, true, _) => dense_ffn(d, xs, pool),
17301        (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
17302        (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
17303        (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
17304    };
17305    TUBE_SCRATCH.with(|sc| {
17306        let mut sc = sc.borrow_mut();
17307        let [g, u, acc] = &mut *sc;
17308        for seg in &d.segs {
17309            if !tube_bit(mask_row, seg.start) {
17310                continue;
17311            }
17312            let w = seg.width;
17313            g.resize(b * w, 0.0);
17314            if b == 1
17315                && d.act == Act::Silu
17316                && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
17317            {
17318                // g holds silu(gate)·up.
17319            } else {
17320                u.resize(b * w, 0.0);
17321                if b == 1 {
17322                    QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
17323                } else {
17324                    seg.gate.matmat(xs, b, g, pool);
17325                    seg.up.matmat(xs, b, u, pool);
17326                }
17327                for i in 0..b * w {
17328                    g[i] = d.act.combine(g[i], u[i]);
17329                }
17330            }
17331            acc.resize(b * hidden, 0.0);
17332            acc.fill(0.0);
17333            if b == 1 {
17334                seg.down.matvec(g, acc, pool);
17335            } else {
17336                seg.down.matmat(g, b, acc, pool);
17337            }
17338            for (o, a) in out.iter_mut().zip(acc.iter()) {
17339                *o += *a;
17340            }
17341        }
17342        out
17343    })
17344}
17345
17346thread_local! {
17347    /// gate / up / down-accumulator scratch for the tube loop — a tube
17348    /// runs once per layer per token, and a fresh Vec each time is a
17349    /// malloc per tube per layer per token.
17350    static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
17351        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
17352}
17353
17354/// CMF_PREFILL_PROF: cumulative ns of the batched walk's attention and
17355/// FFN halves (all layers, all chunks).
17356static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
17357    std::sync::atomic::AtomicU64::new(0),
17358    std::sync::atomic::AtomicU64::new(0),
17359];
17360
17361/// The wgpu side of `CMF_PREFILL_PROF`: q|k|v GEMMs' enqueue and readback
17362/// wait, the chunk attend's readback wait (ms, cumulative) and the int8
17363/// GEMMs the `zi_mm` route took.
17364fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
17365    #[cfg(feature = "gpu")]
17366    {
17367        use std::sync::atomic::Ordering::Relaxed;
17368        let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
17369        (
17370            ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
17371            ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
17372            ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
17373            crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
17374        )
17375    }
17376    #[cfg(not(feature = "gpu"))]
17377    (0.0, 0.0, 0.0, 0)
17378}
17379
17380/// The wgpu prefill mirror attend's outcome counters
17381/// (`gpu_wgpu::MIRROR_EVENTS`).
17382fn wgpu_mirror_counters() -> [u64; 6] {
17383    #[cfg(feature = "gpu")]
17384    {
17385        crate::gpu_wgpu::MIRROR_EVENTS
17386            .each_ref()
17387            .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
17388    }
17389    #[cfg(not(feature = "gpu"))]
17390    [0; 6]
17391}
17392
17393fn prefill_prof_on() -> bool {
17394    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17395    *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
17396}
17397
17398fn dense_ffn_batch(
17399    d: &DenseFfn,
17400    xs: &[f32],
17401    b: usize,
17402    pool: Option<&Pool>,
17403    mask_row: Option<&[u8]>,
17404) -> Vec<f32> {
17405    let inter = d.gate_proj.rows();
17406    let hidden = d.down_proj.rows();
17407    // Fused on-device SwiGLU when the device is in play: three separate
17408    // `matmat` calls are three round trips per layer, and the gate/up
17409    // panels (b × inter — 22 MB each at a 512-token chunk) cross the bus
17410    // twice for nothing. The kernel already existed for the image DiT;
17411    // the LLM prefill was simply never wired to it. A task mask needs the
17412    // activations on the host between the halves, so it keeps the CPU
17413    // arm below.
17414    // SiLU and the exact GELU (Spark-X2.5) have device arms; `q4_ffn_act`
17415    // hands SiLU to the very entry points this used to call.
17416    let fused_act = d.act.graph_act();
17417    if mask_row.is_none()
17418        && fused_act.is_some()
17419        && b >= 32
17420        && crate::gpu::enabled_here()
17421        && !crate::gpu::mm_killed()
17422        // The refit pass needs this layer's activations on the host; the
17423        // fused chain keeps them on the device. Refusing it here costs
17424        // one round trip and keeps every GEMM on the card — the
17425        // alternative was running the whole calibration on the CPU.
17426        && refit_dir().is_none()
17427        // Same for the mass/hit probes. The accumulator at the bottom of
17428        // this function only sees `g` when `g` came back to the host, so
17429        // a fused batch would leave it summing nothing — a probe that
17430        // reports zeros rather than failing, which is worse.
17431        && !ffn_probe_active()
17432        // GPTQ calibration folds every projection's input on the host
17433        // (`QTensor::matmat`); the fused chain would hand gate/up/down
17434        // their inputs on the card and leave the FFN uncalibrated.
17435        && !crate::gptq_capture::capturing()
17436    {
17437        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17438            d.gate_proj.mapped_q4t(),
17439            d.up_proj.mapped_q4t(),
17440            d.down_proj.mapped_q4t(),
17441        ) {
17442            let mut out = vec![0.0f32; b * hidden];
17443            let act = fused_act.expect("checked above");
17444            if crate::gpu::q4_ffn_act(
17445                model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
17446            ) {
17447                return out;
17448            }
17449        }
17450        // The q4tp twin (same kernel family, scale from the row ladder) —
17451        // the DiT has run it in production since the pipeline containers;
17452        // the LLM prefill was simply never wired to it, so a q4tp model's
17453        // prefill panels stayed on the CPU.
17454        if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17455            d.gate_proj.mapped_q4tp(),
17456            d.up_proj.mapped_q4tp(),
17457            d.down_proj.mapped_q4tp(),
17458        ) {
17459            let mut out = vec![0.0f32; b * hidden];
17460            let act = fused_act.expect("checked above");
17461            if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
17462            {
17463                return out;
17464            }
17465        }
17466        // Every other device codec — int8, or gate/up and down in different
17467        // codecs: the three GEMMs with the panels kept on the card. Without
17468        // it a q8_2f prefill read both b·inter panels home, folded them on
17469        // one host thread, and sent the result back for down.
17470        // CMF_FFN_KEEP=0 keeps the per-GEMM path (A/B).
17471        if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17472            std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
17473            d.gate_proj.mapped_device_gemm(),
17474            d.up_proj.mapped_device_gemm(),
17475            d.down_proj.mapped_device_gemm(),
17476        ) {
17477            let mut out = vec![0.0f32; b * hidden];
17478            let act = fused_act.expect("checked above");
17479            if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
17480                return out;
17481            }
17482        }
17483    }
17484    let mut g = vec![0.0f32; b * inter];
17485    d.gate_proj.matmat(xs, b, &mut g, pool);
17486    let mut u = vec![0.0f32; b * inter];
17487    d.up_proj.matmat(xs, b, &mut u, pool);
17488    if gate_topk() > 0 && d.act == Act::Silu {
17489        for t in 0..b {
17490            let row = &mut g[t * inter..(t + 1) * inter];
17491            for v in row.iter_mut() {
17492                *v = Act::Silu.combine(*v, 1.0);
17493            }
17494            keep_top_k(row, gate_topk());
17495        }
17496        for i in 0..b * inter {
17497            g[i] *= u[i];
17498        }
17499    } else {
17500        for i in 0..b * inter {
17501            g[i] = d.act.combine(g[i], u[i]);
17502        }
17503    }
17504    if let Some(row) = mask_row {
17505        zero_masked_cols(&mut g, b, inter, row);
17506    }
17507    if oracle_topk() > 0 {
17508        for t in 0..b {
17509            keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
17510        }
17511    }
17512    let mut out = vec![0.0f32; b * hidden];
17513    d.down_proj.matmat(&g, b, &mut out, pool);
17514    if refit_dir().is_some() {
17515        let li = crate::gpu::cur_layer();
17516        if li >= 0 {
17517            refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
17518        }
17519    }
17520    // The DTG-MA probe, on the batched path: one prefill sweep gives the
17521    // same per-neuron statistic the per-position probe does, and on a 27B
17522    // that is minutes instead of hours.
17523    FFN_PROBE.with(|pr| {
17524        if let Some(acc) = pr.borrow_mut().as_mut() {
17525            let li = crate::gpu::cur_layer();
17526            if li < 0 {
17527                return;
17528            }
17529            let Some(row) = acc.get_mut(li as usize) else {
17530                return;
17531            };
17532            let sq = probe_sq();
17533            for t in 0..b {
17534                for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
17535                    *a += if sq {
17536                        (v as f64) * (v as f64)
17537                    } else {
17538                        (v as f64).abs()
17539                    };
17540                }
17541            }
17542        }
17543    });
17544    out
17545}
17546
17547/// Batched MoE-FFN: router batched, positions are GROUPED by expert —
17548/// an expert's weights are read once for all its positions in the chunk
17549/// (the main prefill-GEMM win on MoE: 960MB/token of 35B experts).
17550/// Accumulate per-channel activation energy for `CMF_RMS_TRACE`.
17551fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
17552    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17553    static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17554    let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
17555    let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
17556    if (!on && !dump) || b == 0 {
17557        return;
17558    }
17559    let hidden = xs.len() / b;
17560    if on {
17561        let mut acc = m.act_sq.borrow_mut();
17562        if acc.len() < hidden {
17563            acc.resize(hidden, 0.0);
17564        }
17565        for t in 0..b {
17566            let row = &xs[t * hidden..(t + 1) * hidden];
17567            for (a, &v) in acc.iter_mut().zip(row) {
17568                *a += (v as f64) * (v as f64);
17569            }
17570        }
17571    }
17572    if dump {
17573        // Cap the capture: the covariance needs a few thousand rows, and a
17574        // whole prefill of every layer would be gigabytes for no extra rank.
17575        let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
17576            .ok()
17577            .and_then(|v| v.parse().ok())
17578            .unwrap_or(4096);
17579        let mut rows = m.act_rows.borrow_mut();
17580        if rows.len() < cap * hidden {
17581            let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
17582            rows.extend_from_slice(&xs[..take * hidden]);
17583        }
17584    }
17585}
17586
17587/// Send-able cursor over a Vec-of-Vecs: each pool worker writes only its
17588/// own slots (disjoint by construction in the caller).
17589#[derive(Clone, Copy)]
17590struct SendVecs(*mut Vec<f32>);
17591unsafe impl Send for SendVecs {}
17592unsafe impl Sync for SendVecs {}
17593impl SendVecs {
17594    #[inline]
17595    fn at(self, i: usize) -> *mut Vec<f32> {
17596        unsafe { self.0.add(i) }
17597    }
17598}
17599
17600fn moe_ffn_batch(
17601    m: &MoeFfn,
17602    xs: &[f32],
17603    b: usize,
17604    hidden: usize,
17605    pool: Option<&Pool>,
17606    allowed: Option<&[bool]>,
17607) -> Vec<f32> {
17608    accumulate_act(m, xs, b);
17609    let ne = m.experts.len();
17610    let mut logits = vec![0.0f32; b * ne];
17611    match &m.resonance {
17612        Some(r) => {
17613            let hdim = xs.len() / b.max(1);
17614            for bi in 0..b {
17615                r.scores(
17616                    &xs[bi * hdim..(bi + 1) * hdim],
17617                    &mut logits[bi * ne..(bi + 1) * ne],
17618                );
17619            }
17620        }
17621        None => m.router.matmat(xs, b, &mut logits, pool),
17622    }
17623
17624    // Assignments: expert → [(position, weight)] — same routing as
17625    // moe_ffn, per position (see `moe_route`).
17626    let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
17627    {
17628        let mut st = m.stats.borrow_mut();
17629        if st.len() < ne {
17630            st.resize(ne, 0);
17631        }
17632        for bi in 0..b {
17633            let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
17634            for &e in &idx {
17635                st[e] += 1;
17636                assign[e].push((bi, p[e] / wsum));
17637            }
17638        }
17639    }
17640
17641    let mut out = vec![0.0f32; b * hidden];
17642    let cols = m.experts[0].gate_proj.cols();
17643    let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
17644        let sb = list.len();
17645        let mut sub = vec![0.0f32; sb * cols];
17646        for (k, &(bi, _)) in list.iter().enumerate() {
17647            sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17648        }
17649        let eo = dense_ffn_batch(d, &sub, sb, pool, None);
17650        for (k, &(bi, w)) in list.iter().enumerate() {
17651            for i in 0..hidden {
17652                out[bi * hidden + i] += w * eo[k * hidden + i];
17653            }
17654        }
17655    };
17656    // Routed experts: the panels are TINY (b·top_k spread over every
17657    // expert — a few positions each), so a pool dispatch per expert is
17658    // pure barrier cost. Invert the parallelism: workers take WHOLE
17659    // experts (serial math inside), then one deterministic scatter in
17660    // expert order — the exact accumulation order the serial loop had.
17661    let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
17662    if pool.is_some() && active.len() >= 8 {
17663        let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
17664        {
17665            let panel_ptr = SendVecs(panels.as_mut_ptr());
17666            // Capture only the expert table: `m` itself carries RefCell
17667            // stats and must not cross the pool boundary.
17668            let experts = &m.experts;
17669            let (active_r, assign_r) = (&active, &assign);
17670            let inherit_cpu = crate::gpu::inherit_cpu_scope();
17671            let run = |start: usize, end: usize| {
17672                let _cpu_scope = inherit_cpu();
17673                // Native Metal: the per-op device helpers (`q4tp_ffn`,
17674                // `q8_matmat`, …) stage through size-keyed shared io
17675                // buffers and are not re-entrant — two workers whose experts
17676                // drew the same row count wrote one buffer at once, so a
17677                // Mellum2.1 prompt came out different on every run. The
17678                // workers keep whole experts on the CPU there; the device
17679                // prefill is the MoE chunk graph.
17680                #[cfg(target_os = "macos")]
17681                let _metal_cpu = crate::gpu::enter_cpu_scope();
17682                for ai in start..end {
17683                    let e = active_r[ai];
17684                    let list = &assign_r[e];
17685                    let sb = list.len();
17686                    let mut sub = vec![0.0f32; sb * cols];
17687                    for (k, &(bi, _)) in list.iter().enumerate() {
17688                        sub[k * cols..(k + 1) * cols]
17689                            .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17690                    }
17691                    // SAFETY: each worker owns a disjoint panels[ai].
17692                    unsafe {
17693                        *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
17694                    }
17695                }
17696            };
17697            match pool {
17698                Some(p) => p.run_rows(active.len(), &run),
17699                None => run(0, active.len()),
17700            }
17701        }
17702        for (ai, &e) in active.iter().enumerate() {
17703            for (k, &(bi, w)) in assign[e].iter().enumerate() {
17704                let eo = &panels[ai][k * hidden..(k + 1) * hidden];
17705                for i in 0..hidden {
17706                    out[bi * hidden + i] += w * eo[i];
17707                }
17708            }
17709        }
17710    } else {
17711        for &e in &active {
17712            run_expert(&m.experts[e], &assign[e], &mut out);
17713        }
17714    }
17715    if let Some((se, gate)) = &m.shared {
17716        let all: Vec<(usize, f32)> = if let Some(gate) = gate {
17717            let mut gl = vec![0.0f32; b];
17718            gate.matmat(xs, b, &mut gl, pool);
17719            (0..b)
17720                .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
17721                .collect()
17722        } else {
17723            (0..b).map(|bi| (bi, 1.0)).collect()
17724        };
17725        run_expert(se, &all, &mut out);
17726    }
17727    out
17728}
17729
17730/// Decode-exact multi-token MoE — the MiMo speculative verify's FFN. Row
17731/// `r` of the result is bit-identical to `moe_ffn(m, x_r)` on the CPU
17732/// (`moe_ffn_cpu` → `moe_ffn_cpu_batched`): router matvec per row, the same
17733/// routing, the same int8 gate/up/SiLU and down terms
17734/// (`QTensor::moe_gate_up_rows` / `moe_down_rows`), and the row's experts
17735/// summed in ITS route order from 0. What the rows share is the weight
17736/// traffic: each routed expert is read once for every row that picked it.
17737/// (`moe_ffn_batch`, the prompt path, groups the same way but sums in
17738/// expert-index order and runs blocked kernels on wide groups — close, not
17739/// bit-equal to decode.) Any layer the kernels do not cover, or a device
17740/// that could answer `moe_ffn` itself, walks `moe_ffn` row by row.
17741fn moe_ffn_rows_exact(
17742    m: &MoeFfn,
17743    xs: &[f32],
17744    b: usize,
17745    hidden: usize,
17746    pool: Option<&Pool>,
17747) -> Vec<f32> {
17748    let mut out = vec![0.0f32; b * hidden];
17749    let per_row = |out: &mut [f32]| {
17750        for r in 0..b {
17751            let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
17752            out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
17753        }
17754    };
17755    let covered = !crate::gpu::enabled_here()
17756        && moe_batch_enabled()
17757        && m.shared.is_none()
17758        && m.resonance.is_none()
17759        && FFN_PROBE.with(|pr| pr.borrow().is_none())
17760        && m.experts.iter().all(|d| d.act == Act::Silu);
17761    if !covered {
17762        per_row(&mut out);
17763        return out;
17764    }
17765    let ne = m.experts.len();
17766    // Routing, row by row, exactly as `moe_ffn`.
17767    let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
17768    for r in 0..b {
17769        let x = &xs[r * hidden..(r + 1) * hidden];
17770        accumulate_act(m, x, 1);
17771        let mut logits = vec![0.0f32; ne];
17772        m.router.matvec(x, &mut logits, pool);
17773        let (idx, p, wsum) = moe_route(&logits, m, None);
17774        {
17775            let mut st = m.stats.borrow_mut();
17776            if st.len() < ne {
17777                st.resize(ne, 0);
17778            }
17779            for &e in &idx {
17780                st[e] += 1;
17781            }
17782        }
17783        let w: Vec<f32> = idx
17784            .iter()
17785            .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
17786            .collect();
17787        routes.push((idx, w));
17788    }
17789    if routes.iter().any(|(idx, _)| idx.is_empty()) {
17790        per_row(&mut out);
17791        return out;
17792    }
17793    // Group the (row, expert) picks by expert, in first-seen order.
17794    let mut experts: Vec<usize> = Vec::new();
17795    let mut groups: Vec<Vec<usize>> = Vec::new();
17796    for (r, (idx, _)) in routes.iter().enumerate() {
17797        for &e in idx {
17798            match experts.iter().position(|&x| x == e) {
17799                Some(g) => groups[g].push(r),
17800                None => {
17801                    experts.push(e);
17802                    groups.push(vec![r]);
17803                }
17804            }
17805        }
17806    }
17807    let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
17808    let inter = m.experts[experts[0]].gate_proj.rows();
17809    let pairs: Vec<(&QTensor, &QTensor)> = experts
17810        .iter()
17811        .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
17812        .collect();
17813    let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
17814    if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
17815        per_row(&mut out);
17816        return out;
17817    }
17818    let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
17819    let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
17820    let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
17821    if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
17822        per_row(&mut out);
17823        return out;
17824    }
17825    // Where each (row, expert) term landed in the flat pair list.
17826    let mut slot = std::collections::HashMap::with_capacity(n_pairs);
17827    let mut p = 0usize;
17828    for (g, &e) in experts.iter().enumerate() {
17829        for &r in &groups[g] {
17830            slot.insert((r, e), p);
17831            p += 1;
17832        }
17833    }
17834    for (r, (idx, w)) in routes.iter().enumerate() {
17835        let terms: Vec<(&[f32], f32)> = idx
17836            .iter()
17837            .zip(w)
17838            .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
17839            .collect();
17840        let row = &mut out[r * hidden..(r + 1) * hidden];
17841        for (i, dst) in row.iter_mut().enumerate() {
17842            // `moe_down_many`'s per-row sum: from 0, in route order.
17843            let mut acc = 0f32;
17844            for (d, we) in &terms {
17845                acc += we * d[i];
17846            }
17847            *dst = acc;
17848        }
17849    }
17850    out
17851}
17852
17853/// Gemma's final-logit soft-capping, `c·tanh(x/c)`, over a whole vocab row —
17854/// element for element the serial expression, split across the pool: at a
17855/// 262k vocab the serial `tanh` pass was ~5 ms of a 36 ms decode token.
17856pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
17857    match pool {
17858        Some(p) if v.len() >= 1 << 15 => {
17859            let base = v.as_mut_ptr() as usize;
17860            p.run_rows(v.len(), &|s, e| {
17861                // SAFETY: run_rows hands out disjoint [s, e) ranges of `v`,
17862                // which outlives the (blocking) dispatch.
17863                let sl =
17864                    unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
17865                for x in sl {
17866                    *x = c * (*x / c).tanh();
17867                }
17868            });
17869        }
17870        _ => {
17871            for x in v.iter_mut() {
17872                *x = c * (*x / c).tanh();
17873            }
17874        }
17875    }
17876}
17877
17878thread_local! {
17879    /// gate/up activation scratch for the dense FFN paths (single uses
17880    /// two slots, the fused pair all four) — these were fresh
17881    /// intermediate-size Vecs on every layer of every token.
17882    static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
17883        const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
17884}
17885
17886/// Dense SwiGLU FFN through QTensor matvecs (any storage).
17887fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17888    // Per-token sparsity, when the file was built for it: gate first,
17889    // then only the chosen neurons' up/down rows leave the mmap.
17890    if gate_topk() > 0
17891        && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
17892    {
17893        return out;
17894    }
17895    // Whole-FFN GPU submit (этап 4.2 increment): gate → silu·up → down
17896    // chained in ONE command buffer with the intermediate activations
17897    // resident on the device — 3 per-op polls become 1 per layer. The
17898    // moe_block backend already implements exactly this chain; a dense
17899    // FFN is one expert with weight 1. Runtime probe: the chain still
17900    // pays one submit+poll per layer — alternate it against the pure-CPU
17901    // FFN and keep whichever is faster on this machine.
17902    // q1 FFNs offload at any practical size: the q1 CPU kernel is
17903    // compute-bound, so the UMA threshold logic does not apply — the
17904    // probe measures and decides either way.
17905    // The fused GPU block has no descriptor-aware Prism path: it would either
17906    // consume an unrotated activation or decline after inspecting the mixed
17907    // q2tp/q4tp tensors.  Do not let that structural refusal enter the FFN
17908    // probe's CPU_ONLY scope; the ordinary body below dispatches each matrix
17909    // through QTensor::matvec, which owns the signed FWHT + affine q2tp route.
17910    let prism_body = d.gate_proj.has_prism_contract()
17911        || d.up_proj.has_prism_contract()
17912        || d.down_proj.has_prism_contract();
17913    if !prism_body
17914        && crate::gpu::enabled_here()
17915        && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
17916    {
17917        let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
17918            crate::gpu::ProbeArm::Gpu
17919        } else {
17920            crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
17921        };
17922        match arm {
17923            crate::gpu::ProbeArm::Gpu => {
17924                let t0 = std::time::Instant::now();
17925                if let Some(out) = dense_ffn_gpu(d, x, pool) {
17926                    crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
17927                    return out;
17928                }
17929                // Declined: no timing exists, so say so. Silence here is
17930                // what left `ffn` undecided for 9000 calls and cost a
17931                // failed device attempt on half of them.
17932                crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
17933            }
17934            crate::gpu::ProbeArm::CpuTimed => {
17935                let t0 = std::time::Instant::now();
17936                let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17937                crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
17938                return out;
17939            }
17940            crate::gpu::ProbeArm::Cpu => {
17941                return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17942            }
17943        }
17944    }
17945    dense_ffn_cpu(d, x, pool)
17946}
17947
17948/// The pure-CPU dense-FFN body (also the fallback of every GPU refusal).
17949fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17950    let inter = d.gate_proj.rows();
17951    FFN_SCRATCH.with(|s| {
17952        let mut s = s.borrow_mut();
17953        let [g, u, ..] = &mut *s;
17954        g.resize(inter, 0.0);
17955        // Fused gate+up+silu: one dispatch, no separate silu pass.
17956        // Falls back to matvec_many + silu loop for unsupported dtypes.
17957        if gate_topk() > 0 {
17958            // Gate first, select, and only then pay for `up`: the
17959            // measurement arm computes both and zeroes the losers, which
17960            // is the same arithmetic.
17961            u.resize(inter, 0.0);
17962            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17963            for i in 0..inter {
17964                g[i] = Act::Silu.combine(g[i], 1.0);
17965            }
17966            keep_top_k(g, gate_topk());
17967            for i in 0..inter {
17968                g[i] *= u[i];
17969            }
17970        } else if d.act == Act::Silu && {
17971            let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17972            QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
17973        } {
17974            // g now holds silu(gate)·up directly.
17975        } else {
17976            u.resize(inter, 0.0);
17977            // Multi-matrix job: gate+up under one pool dispatch.
17978            let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17979            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17980            for i in 0..inter {
17981                g[i] = d.act.combine(g[i], u[i]);
17982            }
17983        }
17984        // DTG-MA bake probe (Patent 2): accumulate this layer's
17985        // per-neuron activation mass while a probe pass is active.
17986        // `CMF_FFN_PROBE_TOPK=k` switches the statistic from mass to a
17987        // HIT COUNT — how many tokens rank the neuron in their own top
17988        // k. Mass asks "how loud is this neuron overall", the count
17989        // asks "how often does this task actually need it", and the two
17990        // rank neurons differently whenever a few tokens are loud.
17991        FFN_PROBE.with(|pr| {
17992            if let Some(acc) = pr.borrow_mut().as_mut() {
17993                let li = crate::gpu::cur_layer();
17994                if li >= 0 {
17995                    if let Some(row) = acc.get_mut(li as usize) {
17996                        match probe_topk() {
17997                            0 if probe_sq() => {
17998                                for (a, &v) in row.iter_mut().zip(g.iter()) {
17999                                    *a += (v as f64) * (v as f64);
18000                                }
18001                            }
18002                            0 if probe_signed() => {
18003                                for (a, &v) in row.iter_mut().zip(g.iter()) {
18004                                    *a += v as f64;
18005                                }
18006                            }
18007                            0 => {
18008                                for (a, &v) in row.iter_mut().zip(g.iter()) {
18009                                    *a += (v as f64).abs();
18010                                }
18011                            }
18012                            k => {
18013                                let n = g.len();
18014                                let k = k.min(n);
18015                                let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
18016                                let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18017                                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18018                                });
18019                                let thr = *kth;
18020                                for (a, &v) in row.iter_mut().zip(g.iter()) {
18021                                    if v.abs() >= thr {
18022                                        *a += 1.0;
18023                                    }
18024                                }
18025                            }
18026                        }
18027                    }
18028                }
18029            }
18030        });
18031        if oracle_topk() > 0 {
18032            keep_top_k(g, oracle_topk());
18033        }
18034        {
18035            let li = crate::gpu::cur_layer();
18036            if li >= 0 {
18037                adump_row(li as usize, g);
18038            }
18039        }
18040        let mut out = attention::take_buf(d.down_proj.rows());
18041        let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
18042        d.down_proj.matvec(g, &mut out, pool);
18043        out
18044    })
18045}
18046
18047/// Online accumulators for the AWNP refit of a narrowed FFN.
18048///
18049/// The refit needs `Gss = A_SᵀA_S` and `YA = YᵀA_S` per layer, where `A_S`
18050/// are the calibration activations of the KEPT neurons and `Y` the full
18051/// FFN output. Both are small enough to hold; the thing that is not is
18052/// the activations they are built from — a 27B layer would dump a
18053/// gigabyte per thousand tokens. So they are accumulated as the
18054/// calibration runs and written once at the end.
18055///
18056/// `CMF_FFN_REFIT=<dir>` holds `support.<L>.u32` (a u32 count then the
18057/// kept indices) for every layer to accumulate; `CMF_FFN_REFIT_FROM/TO`
18058/// bound the layer span so the accumulators fit in RAM.
18059pub struct RefitAcc {
18060    pub support: Vec<u32>,
18061    pub gss: Vec<f32>,
18062    pub ya: Vec<f32>,
18063    pub hidden: usize,
18064    pub tokens: u64,
18065    /// Activations staged transposed ([ns, t] and [hidden, t]) until the
18066    /// batch is worth a GEMM. The product costs `ns²` to move and add
18067    /// REGARDLESS of how many tokens went into it, so folding 16 chunks
18068    /// into one call cuts that cost 16× — it was 15 TB of traffic per
18069    /// calibration pass at one call per 256 tokens.
18070    pub buf_g: Vec<f32>,
18071    pub buf_o: Vec<f32>,
18072    pub buf_t: usize,
18073}
18074
18075/// The product buffer is SHARED across layers — one 473 MB allocation,
18076/// not one per layer (that was 30 GB of nothing on a 64-layer model).
18077/// It lives under the same lock as the accumulators.
18078type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
18079
18080static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
18081    std::sync::OnceLock::new();
18082
18083/// Is an FFN probe accumulator installed on this thread? The fused GPU
18084/// FFN must decline while one is, or the probe silently measures zero.
18085fn ffn_probe_active() -> bool {
18086    FFN_PROBE.with(|p| p.borrow().is_some())
18087}
18088
18089fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
18090    REFIT
18091        .get_or_init(|| {
18092            std::env::var("CMF_FFN_REFIT").ok().map(|d| {
18093                (
18094                    d,
18095                    std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
18096                )
18097            })
18098        })
18099        .as_ref()
18100}
18101
18102/// Accumulate one prefill panel into the layer's refit statistics.
18103fn refit_accumulate(
18104    li: usize,
18105    g: &[f32],
18106    b: usize,
18107    inter: usize,
18108    out: &[f32],
18109    hidden: usize,
18110    pool: Option<&Pool>,
18111) {
18112    let Some((dir, map)) = refit_dir() else {
18113        return;
18114    };
18115    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
18116    let (from, to) = *SPAN.get_or_init(|| {
18117        let g = |k: &str, d: usize| {
18118            std::env::var(k)
18119                .ok()
18120                .and_then(|v| v.parse().ok())
18121                .unwrap_or(d)
18122        };
18123        (
18124            g("CMF_FFN_REFIT_FROM", 0),
18125            g("CMF_FFN_REFIT_TO", usize::MAX),
18126        )
18127    });
18128    if li < from || li > to {
18129        return;
18130    }
18131    let mut guard = map.lock().unwrap();
18132    let (map, shared) = &mut *guard;
18133    let acc = match map.entry(li) {
18134        std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
18135        std::collections::hash_map::Entry::Vacant(e) => {
18136            let path = format!("{dir}/support.{li}.u32");
18137            let Ok(bytes) = std::fs::read(&path) else {
18138                eprintln!("refit: no {path} — layer {li} skipped");
18139                return;
18140            };
18141            let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
18142            let support: Vec<u32> = bytes[4..4 + n * 4]
18143                .chunks_exact(4)
18144                .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
18145                .collect();
18146            eprintln!(
18147                "refit: layer {li} support {n} ({:.0} MB of accumulator)",
18148                (n * n + hidden * n) as f64 * 4.0 / 1e6
18149            );
18150            e.insert(RefitAcc {
18151                gss: vec![0.0; n * n],
18152                ya: vec![0.0; hidden * n],
18153                buf_g: Vec::new(),
18154                buf_o: Vec::new(),
18155                buf_t: 0,
18156                support,
18157                hidden,
18158                tokens: 0,
18159            })
18160        }
18161    };
18162    let ns = acc.support.len();
18163    // Stage this chunk transposed; the GEMM fires once the batch is full.
18164    let cap = refit_batch();
18165    if acc.buf_g.is_empty() {
18166        acc.buf_g = vec![0.0; ns * cap];
18167        acc.buf_o = vec![0.0; hidden * cap];
18168    }
18169    let take = b.min(cap - acc.buf_t);
18170    for t in 0..take {
18171        let col = acc.buf_t + t;
18172        for (j, &n) in acc.support.iter().enumerate() {
18173            acc.buf_g[j * cap + col] = g[t * inter + n as usize];
18174        }
18175        for h in 0..hidden {
18176            acc.buf_o[h * cap + col] = out[t * hidden + h];
18177        }
18178    }
18179    acc.buf_t += take;
18180    acc.tokens += take as u64;
18181    if acc.buf_t < cap {
18182        return;
18183    }
18184    let bt = acc.buf_t;
18185    acc.buf_t = 0;
18186    // The GEMM WRITES its C (it zeroes the accumulators it uses), so the
18187    // chunk product lands in scratch and is added on — the one thing that
18188    // silently turns a Gram over 13 000 tokens into a Gram over 256.
18189    // Both products are `C[n, m] += X[n, b] · Yᵀ[b, m]` with X and Y
18190    // stored row-major [·, b] — exactly `gemm_nt_f32`'s shape, so the
18191    // card does them when it is up (this is the whole calibration's
18192    // cost: O(|S|²) per token, 2.9 PFLOP for a 27B pass). The tiled CPU
18193    // loop stays as the fallback. Neither accumulates, so the product
18194    // lands in scratch and is added on.
18195    let RefitAcc {
18196        gss,
18197        ya,
18198        buf_g,
18199        buf_o,
18200        ..
18201    } = acc;
18202    let need = (ns * ns).max(hidden * ns);
18203    if shared.len() < need {
18204        shared.resize(need, 0.0);
18205    }
18206    let scratch = &mut shared[..];
18207    let _ = bt;
18208    if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
18209        add_into(gss, &scratch[..ns * ns], pool);
18210        if crate::gpu::gemm_nt_f32_transient(
18211            buf_o,
18212            buf_g,
18213            &mut scratch[..hidden * ns],
18214            hidden,
18215            cap,
18216            ns,
18217        ) {
18218            add_into(ya, &scratch[..hidden * ns], pool);
18219        } else {
18220            accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
18221        }
18222    } else {
18223        accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
18224        accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
18225    }
18226    // No zeroing: the batch is always filled exactly (cap is a multiple
18227    // of the prefill chunk), and a memset of 178 MB a layer would cost
18228    // more than the GEMM.
18229}
18230
18231/// `CMF_FFN_REFIT_BATCH` — tokens staged before each GEMM (default 4096).
18232fn refit_batch() -> usize {
18233    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18234    *B.get_or_init(|| {
18235        std::env::var("CMF_FFN_REFIT_BATCH")
18236            .ok()
18237            .and_then(|v| v.parse().ok())
18238            .unwrap_or(4096)
18239    })
18240}
18241
18242/// `c[m, n] += Σ_t left[m, t]·right[n, t]` — both operands transposed,
18243/// the CPU fallback for the staged batch.
18244fn accum_outer_t(
18245    c: &mut [f32],
18246    m: usize,
18247    n: usize,
18248    b: usize,
18249    left: &[f32],
18250    right: &[f32],
18251    pool: Option<&Pool>,
18252) {
18253    let ptr = SendMut(c.as_mut_ptr());
18254    let body = |i: usize| {
18255        let ptr = &ptr;
18256        let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
18257        for t in 0..b {
18258            let a = left[i * b + t];
18259            if a == 0.0 {
18260                continue;
18261            }
18262            for (j, o) in row.iter_mut().enumerate() {
18263                *o += a * right[j * b + t];
18264            }
18265        }
18266    };
18267    match pool {
18268        Some(p) if m > 1 => p.run_rows(m, &|s, e| {
18269            for i in s..e {
18270                body(i);
18271            }
18272        }),
18273        _ => {
18274            for i in 0..m {
18275                body(i);
18276            }
18277        }
18278    }
18279}
18280
18281/// `dst += src`, spread over the pool — at 118 M floats a layer this is
18282/// not a loop to leave on one core.
18283fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
18284    let n = dst.len().min(src.len());
18285    match pool {
18286        Some(p) if n >= 1 << 16 => {
18287            let ptr = SendMut(dst.as_mut_ptr());
18288            let f = |s: usize, e: usize| {
18289                let ptr = &ptr;
18290                for blk in s..e {
18291                    let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
18292                    for i in a..b {
18293                        unsafe { *ptr.0.add(i) += src[i] };
18294                    }
18295                }
18296            };
18297            p.run_rows(n.div_ceil(4096), &f);
18298        }
18299        _ => {
18300            for (d, v) in dst.iter_mut().zip(&src[..n]) {
18301                *d += *v;
18302            }
18303        }
18304    }
18305}
18306
18307/// `c[m, n] += Σ_t left[t, m]·right[t, n]`, with `left` stored [m, t] and
18308/// `right` [t, n]. Tiled over the rows of `c` so a tile stays in cache
18309/// while each token's `right` row streams past it once, and parallel
18310/// over tiles.
18311fn accum_outer(
18312    c: &mut [f32],
18313    m: usize,
18314    n: usize,
18315    b: usize,
18316    left: &[f32],
18317    right: &[f32],
18318    pool: Option<&Pool>,
18319) {
18320    const TILE: usize = 32;
18321    let tiles = m.div_ceil(TILE);
18322    let cp = SendMut(c.as_mut_ptr());
18323    let body = |ti: usize| {
18324        let cp = &cp;
18325        let i0 = ti * TILE;
18326        let i1 = (i0 + TILE).min(m);
18327        for t in 0..b {
18328            let r = &right[t * n..t * n + n];
18329            for i in i0..i1 {
18330                let a = left[i * b + t];
18331                if a == 0.0 {
18332                    continue;
18333                }
18334                // SAFETY: tiles partition c's rows; workers never overlap.
18335                let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
18336                for (o, v) in row.iter_mut().zip(r) {
18337                    *o += a * *v;
18338                }
18339            }
18340        }
18341    };
18342    match pool {
18343        Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
18344            for ti in s..e {
18345                body(ti);
18346            }
18347        }),
18348        _ => {
18349            for ti in 0..tiles {
18350                body(ti);
18351            }
18352        }
18353    }
18354}
18355
18356/// Write what the calibration accumulated: `gss.<L>.f32` and `ya.<L>.f32`.
18357pub fn refit_flush() -> usize {
18358    let Some((dir, map)) = refit_dir() else {
18359        return 0;
18360    };
18361    let guard = map.lock().unwrap();
18362    let mut n = 0;
18363    for (li, acc) in guard.0.iter() {
18364        // A silently truncated write here is a Gram that reshapes to
18365        // nothing an hour later — say it out loud instead.
18366        let w = |name: &str, v: &[f32]| {
18367            let path = format!("{dir}/{name}.{li}.f32");
18368            let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
18369            match std::fs::write(&path, &bytes) {
18370                Ok(()) => {}
18371                Err(e) => eprintln!(
18372                    "refit: FAILED to write {path} ({} MB): {e}",
18373                    bytes.len() / 1_000_000
18374                ),
18375            }
18376        };
18377        w("gss", &acc.gss);
18378        w("ya", &acc.ya);
18379        println!(
18380            "refit L{li}: {} support, {} tokens, hidden {}",
18381            acc.support.len(),
18382            acc.tokens,
18383            acc.hidden
18384        );
18385        n += 1;
18386    }
18387    n
18388}
18389
18390/// `CMF_FFN_ADUMP=<prefix>` — append every probed token's FFN activation
18391/// row to `<prefix>.<layer>.f16`. The co-activation record: which
18392/// neurons fire together, which is what a tube has to group if a token
18393/// is ever going to open one tube instead of sixteen.
18394fn adump_row(li: usize, g: &[f32]) {
18395    use std::io::Write as _;
18396    static FILES: std::sync::OnceLock<
18397        Option<(
18398            String,
18399            std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
18400        )>,
18401    > = std::sync::OnceLock::new();
18402    let Some((prefix, map)) = FILES
18403        .get_or_init(|| {
18404            std::env::var("CMF_FFN_ADUMP")
18405                .ok()
18406                .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
18407        })
18408        .as_ref()
18409    else {
18410        return;
18411    };
18412    // `CMF_FFN_ADUMP_FROM/_TO` narrow the dump to a layer span, so a big
18413    // calibration run fits on disk in a few passes instead of one.
18414    static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
18415    let (from, to) = *SPAN.get_or_init(|| {
18416        let g = |k: &str, d: usize| {
18417            std::env::var(k)
18418                .ok()
18419                .and_then(|v| v.parse().ok())
18420                .unwrap_or(d)
18421        };
18422        (
18423            g("CMF_FFN_ADUMP_FROM", 0),
18424            g("CMF_FFN_ADUMP_TO", usize::MAX),
18425        )
18426    });
18427    if li < from || li > to {
18428        return;
18429    }
18430    let mut map = map.lock().unwrap();
18431    let f = map.entry(li).or_insert_with(|| {
18432        std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
18433    });
18434    let mut bytes = Vec::with_capacity(g.len() * 2);
18435    for v in g {
18436        bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
18437    }
18438    let _ = f.write_all(&bytes);
18439}
18440
18441/// `CMF_FFN_ORACLE_TOPK` — keep only the k largest |silu(g)·u| of each
18442/// token and zero the rest. Not a serving mode: it is the CEILING of
18443/// contextual sparsity — what a per-token router would be chasing —
18444/// measured by cheating, since the selection reads the very activations
18445/// it would have to predict.
18446fn oracle_topk() -> usize {
18447    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18448    *K.get_or_init(|| {
18449        std::env::var("CMF_FFN_ORACLE_TOPK")
18450            .ok()
18451            .and_then(|v| v.parse().ok())
18452            .unwrap_or(0)
18453    })
18454}
18455
18456/// `CMF_FFN_GATE_TOPK` — the REALIZABLE cousin of the oracle: rank the
18457/// neurons by their gate alone (which the kernel has computed anyway
18458/// before it reads `up`), keep the k best, and drop the rest. Every
18459/// dropped neuron's `up` row and `down` column stay unread, so this is
18460/// the sparsity a serving path can actually take without a router.
18461fn gate_topk() -> usize {
18462    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18463    *K.get_or_init(|| {
18464        std::env::var("CMF_FFN_GATE_TOPK")
18465            .ok()
18466            .and_then(|v| v.parse().ok())
18467            .unwrap_or(0)
18468    })
18469}
18470
18471/// `CMF_FFN_GATE_BLOCK` — select in blocks of B neurons instead of one
18472/// by one. A scattered per-neuron choice cannot be read efficiently (a
18473/// row at a time, no prefetch runway); a block of 32 is a contiguous
18474/// 32-row slab of `up` and of the transposed `down`, which the ordinary
18475/// kernels stream. The question the measurement answers is what the
18476/// block costs in quality.
18477fn gate_block() -> usize {
18478    static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18479    *B.get_or_init(|| {
18480        std::env::var("CMF_FFN_GATE_BLOCK")
18481            .ok()
18482            .and_then(|v| v.parse().ok())
18483            .unwrap_or(1)
18484    })
18485}
18486
18487/// Zero all but the `k` largest BLOCKS (by summed square) of a row.
18488fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
18489    let n = g.len();
18490    let nb = n.div_ceil(block);
18491    let kb = (keep_n.div_ceil(block)).clamp(1, nb);
18492    if kb >= nb {
18493        return;
18494    }
18495    let mut score: Vec<f32> = (0..nb)
18496        .map(|b| {
18497            g[b * block..((b + 1) * block).min(n)]
18498                .iter()
18499                .map(|v| v * v)
18500                .sum::<f32>()
18501        })
18502        .collect();
18503    let mut ord = score.clone();
18504    let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
18505        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18506    });
18507    let thr = *kth;
18508    for b in 0..nb {
18509        if score[b] < thr {
18510            g[b * block..((b + 1) * block).min(n)].fill(0.0);
18511        }
18512    }
18513    score.clear();
18514}
18515
18516/// Zero all but the `k` largest magnitudes of one token's activation row.
18517fn keep_top_k(g: &mut [f32], k: usize) {
18518    if gate_block() > 1 {
18519        return keep_top_blocks(g, k, gate_block());
18520    }
18521    let n = g.len();
18522    if k == 0 || k >= n {
18523        return;
18524    }
18525    let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
18526    let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18527        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18528    });
18529    let thr = *kth;
18530    for v in g.iter_mut() {
18531        if v.abs() < thr {
18532            *v = 0.0;
18533        }
18534    }
18535}
18536
18537/// `CMF_FFN_PROBE_SQ` — accumulate Σa², so the dump divided by the token
18538/// count and square-rooted is the RMS activation trace Patent 12 weights
18539/// its matrices by.
18540fn probe_sq() -> bool {
18541    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18542    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
18543}
18544
18545/// `CMF_FFN_PROBE_SIGNED` — accumulate the SIGNED activation sum
18546/// instead of its magnitude: what a dropped neuron contributes ON
18547/// AVERAGE, which is the bias a narrowed FFN can add back for free.
18548fn probe_signed() -> bool {
18549    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18550    *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
18551}
18552
18553/// `CMF_FFN_MEANFILL=<file>` — a masked-out neuron contributes its MEAN
18554/// activation instead of zero (`u32 layers, u32 inter, f32[…]`, the mass
18555/// dump layout, holding per-neuron means). Dropping a neuron outright
18556/// also drops its average contribution, which shifts the layer output by
18557/// a constant; filling the mean back is one add per layer and costs no
18558/// bytes off the bus. This is the measurement arm — in a tube file the
18559/// same correction ships as a per-task bias vector.
18560fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
18561    static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
18562    M.get_or_init(|| {
18563        let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
18564        let b = std::fs::read(&p).ok()?;
18565        let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
18566        let vals: Vec<f32> = b[8..]
18567            .chunks_exact(4)
18568            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
18569            .collect();
18570        eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
18571        Some((inter, vals))
18572    })
18573    .as_ref()
18574}
18575
18576/// `CMF_FFN_PROBE_TOPK` — 0 (default) = accumulate mass, k>0 = count
18577/// how often a neuron lands in a token's top k.
18578fn probe_topk() -> usize {
18579    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18580    *K.get_or_init(|| {
18581        std::env::var("CMF_FFN_PROBE_TOPK")
18582            .ok()
18583            .and_then(|v| v.parse().ok())
18584            .unwrap_or(0)
18585    })
18586}
18587
18588thread_local! {
18589    /// DTG-MA activation probe: per-layer per-neuron Σ|silu(g)·u|
18590    /// accumulator, alive only during `Pipeline::probe_ffn_mass`.
18591    static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
18592        const { std::cell::RefCell::new(None) };
18593}
18594
18595/// Per-token structured sparsity, paid for in bytes.
18596///
18597/// The gate is the cheapest third of an FFN and it already says which
18598/// neurons matter: `silu(gate)` near zero means the neuron contributes
18599/// nothing whatever `up` says. So compute every gate, keep the `k`
18600/// loudest, and read ONLY those neurons' `up` rows and `down` rows —
18601/// the latter needs `down_proj` stored transposed, otherwise a neuron's
18602/// down weights are a strided column and "reading only those" costs a
18603/// full cache line each.
18604///
18605/// Returns `None` when the file has no transposed `down` (the caller
18606/// then runs the ordinary dense path).
18607fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
18608    // The scatter path reads individual rows/columns and cannot express the
18609    // per-matrix signed FWHT boundary.  Let the descriptor-aware dense path
18610    // handle Prism files rather than silently running an unrotated sparse
18611    // approximation.
18612    if d.gate_proj.has_prism_contract()
18613        || d.up_proj.has_prism_contract()
18614        || d.down_proj.has_prism_contract()
18615    {
18616        return None;
18617    }
18618    let dt = d.down_t.as_ref()?;
18619    let inter = d.gate_proj.rows();
18620    let hidden = dt.cols();
18621    if k == 0 || k >= inter || d.act != Act::Silu {
18622        return None;
18623    }
18624    DYN_SCRATCH.with(|sc| {
18625        let mut sc = sc.borrow_mut();
18626        let DynScratch {
18627            g,
18628            mag,
18629            live,
18630            parts,
18631        } = &mut *sc;
18632        g.resize(inter, 0.0);
18633        d.gate_proj.matvec(x, g, pool);
18634        for v in g.iter_mut() {
18635            *v = inference::silu(*v);
18636        }
18637        // The k-th largest |silu(gate)| is the threshold; ties keep more,
18638        // which is the safe side.
18639        mag.clear();
18640        mag.extend(g.iter().map(|v| v.abs()));
18641        let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18642            b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18643        });
18644        let thr = *kth;
18645        live.clear();
18646        live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
18647        let mut out = vec![0.0f32; hidden];
18648        match pool {
18649            Some(p) if live.len() >= 64 => {
18650                let nw = p.n_workers() + 1;
18651                parts.clear();
18652                parts.resize(nw * hidden, 0.0);
18653                let ptr = SendMut(parts.as_mut_ptr());
18654                let n = live.len();
18655                let live_ref: &[u32] = live;
18656                let g_ref: &[f32] = g;
18657                p.run(&|w, workers| {
18658                    let chunk = n.div_ceil(workers);
18659                    let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
18660                    if s >= e {
18661                        return;
18662                    }
18663                    WORKER_SCRATCH.with(|ws| {
18664                        let mut ws = ws.borrow_mut();
18665                        let [scratch, acc] = &mut *ws;
18666                        scratch.resize(hidden.max(x.len()), 0.0);
18667                        acc.clear();
18668                        acc.resize(hidden, 0.0);
18669                        for (o, &nrm) in live_ref[s..e].iter().enumerate() {
18670                            // One neuron of runway: the next row's lines
18671                            // start moving while this one is multiplied.
18672                            if let Some(&nx) = live_ref[s..e].get(o + 1) {
18673                                d.up_proj.prefetch_row(nx as usize);
18674                                dt.prefetch_row(nx as usize);
18675                            }
18676                            let idx = nrm as usize;
18677                            let up = d.up_proj.row_dot(idx, x, scratch);
18678                            let a = g_ref[idx] * up;
18679                            if a != 0.0 {
18680                                dt.add_row_scaled(idx, a, acc, scratch);
18681                            }
18682                        }
18683                        for (j, v) in acc.iter().enumerate() {
18684                            unsafe { *ptr.at(w * hidden + j) = *v };
18685                        }
18686                    });
18687                });
18688                for w in 0..nw {
18689                    for (j, o) in out.iter_mut().enumerate() {
18690                        *o += parts[w * hidden + j];
18691                    }
18692                }
18693            }
18694            _ => {
18695                WORKER_SCRATCH.with(|ws| {
18696                    let mut ws = ws.borrow_mut();
18697                    let [scratch, _acc] = &mut *ws;
18698                    scratch.resize(hidden.max(x.len()), 0.0);
18699                    for &nrm in live.iter() {
18700                        let idx = nrm as usize;
18701                        let up = d.up_proj.row_dot(idx, x, scratch);
18702                        let a = g[idx] * up;
18703                        if a != 0.0 {
18704                            dt.add_row_scaled(idx, a, &mut out, scratch);
18705                        }
18706                    }
18707                });
18708            }
18709        }
18710        Some(out)
18711    })
18712}
18713
18714/// Caller-side scratch of the dynamic path — one allocation per thread,
18715/// not one per layer per token (that alone cost a third of the decode).
18716struct DynScratch {
18717    g: Vec<f32>,
18718    mag: Vec<f32>,
18719    live: Vec<u32>,
18720    parts: Vec<f32>,
18721}
18722
18723thread_local! {
18724    static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
18725        std::cell::RefCell::new(DynScratch {
18726            g: Vec::new(),
18727            mag: Vec::new(),
18728            live: Vec::new(),
18729            parts: Vec::new(),
18730        })
18731    };
18732    /// Pool-worker scratch: the row buffer and this worker's partial sum.
18733    static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
18734        const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
18735}
18736
18737/// `dense_ffn_cpu` with a per-visit mask landing on the activations —
18738/// the masked-inference fast path's decode arm. Full fused quant
18739/// compute, closed neurons zeroed before down: arithmetically the
18740/// pruned network, no dequant, no weight bytes touched.
18741fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
18742    let inter = d.gate_proj.rows();
18743    FFN_SCRATCH.with(|s| {
18744        let mut s = s.borrow_mut();
18745        let [g, u, ..] = &mut *s;
18746        g.resize(inter, 0.0);
18747        if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
18748            // g holds silu(gate)·up.
18749        } else {
18750            u.resize(inter, 0.0);
18751            QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18752            for i in 0..inter {
18753                g[i] = d.act.combine(g[i], u[i]);
18754            }
18755        }
18756        zero_masked_cols(g, 1, inter, mask_row);
18757        let mut out = attention::take_buf(d.down_proj.rows());
18758        d.down_proj.matvec(g, &mut out, pool);
18759        out
18760    })
18761}
18762
18763/// Dense FFN as one GPU submission via the MoE block path (single
18764/// expert, weight 1.0): gate → silu·up → down chained in one command
18765/// buffer, intermediate activations device-resident. None → weights
18766/// not q8-mapped in the primary shard / over the VRAM budget / backend
18767/// refusal → honest CPU path.
18768fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
18769    if d.gate_proj.has_prism_contract()
18770        || d.up_proj.has_prism_contract()
18771        || d.down_proj.has_prism_contract()
18772    {
18773        return None;
18774    }
18775    // The GPU block applies SiLU, exact GELU and tanh-GELU (Gemma); other
18776    // activations stay on the CPU.
18777    d.act.graph_act()?;
18778    // Threshold: tiny FFNs are not worth a submission (q1 excepted —
18779    // see the caller's gate).
18780    if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
18781        return None;
18782    }
18783    let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
18784    let mut model_ref = None;
18785    moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
18786    let model = model_ref?;
18787    let hidden = jobs[0].down.1;
18788    let mut out = attention::take_buf(hidden);
18789    if crate::gpu::moe_block(&model, &jobs, &mut out) {
18790        Some(out)
18791    } else {
18792        let mut out = out;
18793        attention::recycle_buf(&mut out);
18794        None
18795    }
18796}
18797
18798/// q8-mapped primary-shard tensor parts for a GPU job: q8_2f carries
18799/// its column field, q8_row runs with empty col slices (the backend
18800/// skips the multiply). Shared by the MoE block and the dense-FFN
18801/// single-job path.
18802#[allow(clippy::type_complexity)]
18803#[allow(clippy::type_complexity)]
18804pub(crate) fn moe_parts(
18805    t: &QTensor,
18806) -> Option<(
18807    &std::sync::Arc<cortiq_core::CmfModel>,
18808    usize,
18809    usize,
18810    usize,
18811    &[f32],
18812    &[f32],
18813    bool,
18814    bool,
18815    bool,
18816)> {
18817    match t {
18818        QTensor::Mapped {
18819            model,
18820            idx,
18821            dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
18822            rows,
18823            cols,
18824            row_scale,
18825            col_field,
18826            ..
18827        } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
18828            model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
18829        )),
18830        // q1: tile-embedded scales — empty rs/col slices, raw xs.
18831        QTensor::Mapped {
18832            model,
18833            idx,
18834            dtype: cortiq_core::TensorDtype::Q1,
18835            rows,
18836            cols,
18837            ..
18838        } => Some((
18839            model,
18840            *idx,
18841            *rows,
18842            *cols,
18843            &[][..],
18844            &[][..],
18845            true,
18846            false,
18847            false,
18848        )),
18849        // q4_tiled: 18-byte tiles with embedded f16 scales — raw xs.
18850        QTensor::Mapped {
18851            model,
18852            idx,
18853            dtype: cortiq_core::TensorDtype::Q4Tiled,
18854            rows,
18855            cols,
18856            ..
18857        } => Some((
18858            model,
18859            *idx,
18860            *rows,
18861            *cols,
18862            &[][..],
18863            &[][..],
18864            false,
18865            true,
18866            false,
18867        )),
18868        // q4tp: same raw-xs contract, different stride and scale plane.
18869        QTensor::Mapped {
18870            model,
18871            idx,
18872            dtype: cortiq_core::TensorDtype::Q4TiledP,
18873            rows,
18874            cols,
18875            ..
18876        } => Some((
18877            model,
18878            *idx,
18879            *rows,
18880            *cols,
18881            &[][..],
18882            &[][..],
18883            false,
18884            true,
18885            false,
18886        )),
18887        // q2tp: the 2-bit expert plane of the mixed profile — q4 family
18888        // for stride bookkeeping, flagged q2 so the trio validation can
18889        // demand a q4tp down.
18890        QTensor::Mapped {
18891            model,
18892            idx,
18893            dtype: cortiq_core::TensorDtype::Q2TiledP,
18894            rows,
18895            cols,
18896            ..
18897        } => Some((
18898            model,
18899            *idx,
18900            *rows,
18901            *cols,
18902            &[][..],
18903            &[][..],
18904            false,
18905            true,
18906            true,
18907        )),
18908        _ => None,
18909    }
18910}
18911
18912/// The routed experts of a MoE layer as the Metal chunk prefill takes
18913/// them: f32 router rows and every expert's q4tp (gate, up, down) tensor
18914/// indices, uniform SiLU shapes. Only the plain routed form — no shared
18915/// expert, no per-expert scale, no resonance or τ router, no router input
18916/// norm; the batched host walk keeps the rest. The routing itself stays the
18917/// host's (`moe_route`), so mask and selection bias need nothing here.
18918#[cfg(target_os = "macos")]
18919#[allow(clippy::type_complexity)]
18920fn chunk_moe_parts(
18921    m: &MoeFfn,
18922    hidden: usize,
18923) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
18924    if m.shared.is_some()
18925        || m.per_expert_scale.is_some()
18926        || m.resonance.is_some()
18927        || m.route_tau.is_some()
18928        || m.router_input_norm
18929        || m.experts.is_empty()
18930        || m.top_k == 0
18931        || std::env::var("CMF_RMS_TRACE").is_ok()
18932        || std::env::var("CMF_ACT_DUMP").is_ok()
18933        || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
18934    {
18935        return None;
18936    }
18937    let (rf, rr, rc) = m.router.f32_parts()?;
18938    if rr != m.experts.len() || rc != hidden {
18939        return None;
18940    }
18941    let inter = m.experts[0].gate_proj.rows();
18942    // All-q8_2f experts (Mellum2.1 q8_2f) ride the `moe_q8` GEMMs;
18943    // `CMF_METAL_MOE_Q8=0` keeps them on the host walk.
18944    let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
18945    let experts = m
18946        .experts
18947        .iter()
18948        .map(|e| {
18949            if e.act != Act::Silu
18950                || !e.segs.is_empty()
18951                || e.gate_proj.rows() != inter
18952                || e.up_proj.rows() != inter
18953                || e.down_proj.cols() != inter
18954                || e.gate_proj.cols() != hidden
18955                || e.down_proj.rows() != hidden
18956            {
18957                return None;
18958            }
18959            if q8 {
18960                let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18961                return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18962            }
18963            Some((
18964                e.gate_proj.mapped_q4tp()?.1,
18965                e.up_proj.mapped_q4tp()?.1,
18966                e.down_proj.mapped_q4tp()?.1,
18967            ))
18968        })
18969        .collect::<Option<Vec<_>>>()?;
18970    Some((rf, experts, q8))
18971}
18972
18973/// Map a MoE onto the Metal token graph's contract: f32 router, a
18974/// shared expert (gated — Qwen — or ungated at weight 1 — DeepSeek-V3 /
18975/// HunYuan hy_v3), softmax or sigmoid scores with an optional selection
18976/// bias and routed scale, experts uniformly q4tp (or the mixed profile:
18977/// q2tp gate/up over a q4tp down). τ routers, masks, per-expert scales
18978/// and Gemma's router-input norm refuse here — those semantics stay on
18979/// the CPU path.
18980/// Map Flint's released ragged MoE into the native page graph.  This stays
18981/// separate from `metal_moe_graph_parts`: its six routed slots are Q2TP and
18982/// 512-wide, while its ungated shared SwiGLU is Q4TP and 1024-wide.  Feeding
18983/// the shared tuple into the uniform jobs descriptor would make the shader
18984/// use the routed stride on a different payload.
18985#[cfg(target_os = "macos")]
18986fn metal_flint_moe_graph_parts(
18987    m: &MoeFfn,
18988    hidden: usize,
18989    scratch_key: usize,
18990) -> Option<crate::gpu::GpuFlintMoe<'_>> {
18991    use std::sync::Arc;
18992
18993    const HIDDEN: usize = 2_048;
18994    const ROUTED_INTER: usize = 512;
18995    const SHARED_INTER: usize = 1_024;
18996    const EXPERTS: usize = 256;
18997    const TOP_K: usize = 6;
18998
18999    if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
19000        return None;
19001    }
19002    let routed0 = m.experts.first()?;
19003    let (model, _) = routed0.gate_proj.mapped_q2tp()?;
19004    let (router, router_rows, router_cols) = m.router.f32_parts()?;
19005    if router_rows != EXPERTS || router_cols != HIDDEN {
19006        return None;
19007    }
19008    if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
19009        return None;
19010    }
19011    let same_q2 = |t: &QTensor| -> Option<usize> {
19012        let (owner, idx) = t.mapped_q2tp()?;
19013        Arc::ptr_eq(&owner, &model).then_some(idx)
19014    };
19015    let same_q4 = |t: &QTensor| -> Option<usize> {
19016        let (owner, idx) = t.mapped_q4tp()?;
19017        Arc::ptr_eq(&owner, &model).then_some(idx)
19018    };
19019    let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
19020        if e.act != Act::Silu
19021            || !e.segs.is_empty()
19022            || e.gate_proj.rows() != ROUTED_INTER
19023            || e.gate_proj.cols() != HIDDEN
19024            || e.up_proj.rows() != ROUTED_INTER
19025            || e.up_proj.cols() != HIDDEN
19026            || e.down_proj.rows() != HIDDEN
19027            || e.down_proj.cols() != ROUTED_INTER
19028        {
19029            return None;
19030        }
19031        Some((
19032            same_q2(&e.gate_proj)?,
19033            same_q2(&e.up_proj)?,
19034            same_q4(&e.down_proj)?,
19035        ))
19036    };
19037    let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
19038        if e.act != Act::Silu
19039            || !e.segs.is_empty()
19040            || e.gate_proj.rows() != SHARED_INTER
19041            || e.gate_proj.cols() != HIDDEN
19042            || e.up_proj.rows() != SHARED_INTER
19043            || e.up_proj.cols() != HIDDEN
19044            || e.down_proj.rows() != HIDDEN
19045            || e.down_proj.cols() != SHARED_INTER
19046        {
19047            return None;
19048        }
19049        Some((
19050            same_q4(&e.gate_proj)?,
19051            same_q4(&e.up_proj)?,
19052            same_q4(&e.down_proj)?,
19053        ))
19054    };
19055    let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
19056    if experts.len() != EXPERTS {
19057        return None;
19058    }
19059    let (shared_ffn, shared_gate) = m.shared.as_ref()?;
19060    if shared_gate.is_some() {
19061        return None;
19062    }
19063    Some(crate::gpu::GpuFlintMoe {
19064        router,
19065        bias: m.expert_bias.as_deref(),
19066        experts,
19067        scratch_key,
19068        shared: shared(shared_ffn)?,
19069        top_k: m.top_k,
19070        norm_topk: m.norm_topk_prob,
19071        route_scale: m.routed_scaling,
19072        sigmoid: m.router_sigmoid,
19073        sigmoid_denom_eps: m.sigmoid_denom_eps,
19074    })
19075}
19076
19077#[cfg(target_os = "macos")]
19078fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
19079    if m.router_input_norm
19080        || m.route_tau.is_some()
19081        || m.mask.is_some()
19082        || m.per_expert_scale.is_some()
19083        || m.experts.is_empty()
19084        || m.top_k == 0
19085        || m.resonance.is_some()
19086    {
19087        return None;
19088    }
19089    // Without a shared expert the select kernel fills the k routed jobs
19090    // only (flag 32). Measured on Mellum2.1's softmax router; the sigmoid /
19091    // biased routers without one (LFM2-MoE) keep the CPU path until they
19092    // are measured too.
19093    let (sh, sg) = match &m.shared {
19094        Some((sh, sg)) => (Some(sh), sg.as_ref()),
19095        None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
19096        None => return None,
19097    };
19098    let (rf, rr, rc) = m.router.f32_parts()?;
19099    if rr != m.experts.len() || rc != hidden {
19100        return None;
19101    }
19102    let shared_gated = sg.is_some();
19103    let sf = match sg {
19104        Some(sg) => {
19105            let (sf, sr, sc) = sg.f32_parts()?;
19106            if sr * sc != hidden {
19107                return None;
19108            }
19109            sf
19110        }
19111        // Ungated: the router's first row stands in for the gate matvec
19112        // (its logit is never read — the kernel pins weight 1).
19113        None => &rf[..hidden],
19114    };
19115    if let Some(b) = &m.expert_bias {
19116        if b.len() != m.experts.len() {
19117            return None;
19118        }
19119    }
19120    let inter = m.experts[0].gate_proj.rows();
19121    // The first expert's gate decides the profile; every trio (shared
19122    // included) must agree — the jobs ladder flips ONE kernel for all.
19123    let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
19124    // All-q8_2f routed experts (Mellum2.1 q8_2f: no shared expert, the
19125    // contract they were measured on) take the `moe_q8` kernels;
19126    // `CMF_METAL_MOE_Q8=0` keeps them off the graph (per-op path).
19127    let q8 = crate::gpu_metal::moe_q8::enabled()
19128        && m.shared.is_none()
19129        && m.experts[0].gate_proj.q8_2f_parts().is_some();
19130    let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
19131        if e.act != Act::Silu
19132            || e.gate_proj.rows() != inter
19133            || e.gate_proj.cols() != hidden
19134            || e.up_proj.rows() != inter
19135            || e.up_proj.cols() != hidden
19136            || e.down_proj.rows() != hidden
19137            || e.down_proj.cols() != inter
19138        {
19139            return None;
19140        }
19141        if q8 {
19142            let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
19143            return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
19144        }
19145        let pick = |t: &QTensor| -> Option<usize> {
19146            if gu_q2 {
19147                t.mapped_q2tp().map(|(_, i)| i)
19148            } else {
19149                t.mapped_q4tp().map(|(_, i)| i)
19150            }
19151        };
19152        Some((
19153            pick(&e.gate_proj)?,
19154            pick(&e.up_proj)?,
19155            e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
19156        ))
19157    };
19158    let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
19159    let shared = match sh {
19160        Some(sh) => Some(trio(sh)?),
19161        None => None,
19162    };
19163    Some(crate::gpu::GpuMoe {
19164        router: rf,
19165        sgate: sf,
19166        experts,
19167        shared,
19168        n_exp: m.experts.len(),
19169        top_k: m.top_k,
19170        inter,
19171        norm_topk: m.norm_topk_prob,
19172        route_scale: m.routed_scaling,
19173        gu_q2,
19174        q8,
19175        sigmoid: m.router_sigmoid,
19176        sigmoid_denom_eps: m.sigmoid_denom_eps,
19177        bias: m.expert_bias.as_deref(),
19178        shared_gated,
19179    })
19180}
19181
19182/// `CMF_FORCE_IDS=<file>` (diagnostics): decode step n commits the n-th
19183/// whitespace-separated token id of the file instead of sampling, while the
19184/// logits are still computed (and dumped by `CMF_LOGIT_DUMP_ALL`) — one
19185/// backend teacher-forced along another's greedy sequence, so top-1
19186/// agreement can be counted past a near-tie divergence.
19187fn forced_ids_at(step: usize) -> Option<u32> {
19188    static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
19189    IDS.get_or_init(|| {
19190        std::env::var("CMF_FORCE_IDS")
19191            .ok()
19192            .and_then(|p| std::fs::read_to_string(p).ok())
19193            .map(|s| {
19194                s.split_whitespace()
19195                    .filter_map(|t| t.parse().ok())
19196                    .collect()
19197            })
19198            .unwrap_or_default()
19199    })
19200    .get(step)
19201    .copied()
19202}
19203
19204/// Build one gate/up/down GPU job from three tensors. `moe_push_job` is the
19205/// DenseFfn-shaped caller; architectures that keep their experts in their own
19206/// structs (DeepSeek-V4) come here directly.
19207pub(crate) fn moe_push_job_parts<'a>(
19208    gate: &'a QTensor,
19209    up: &'a QTensor,
19210    down: &'a QTensor,
19211    x: &[f32],
19212    w: f32,
19213    swiglu_limit: f32,
19214    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
19215    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
19216) -> Option<()> {
19217    use crate::qtensor::prescale;
19218    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
19219    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
19220    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
19221    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
19222        return None; // mixed-dtype trio — honest CPU path
19223    }
19224    // The 2-bit profile is gate/up q2tp over a PLAIN q4tp down; any other
19225    // 2-bit arrangement stays on the CPU.
19226    if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
19227        return None;
19228    }
19229    if !gq2 && dq2 {
19230        return None;
19231    }
19232    model_ref.get_or_insert_with(|| gm.clone());
19233    let dt = |cf: &[f32]| {
19234        if cf.is_empty() {
19235            cortiq_core::TensorDtype::Q8Row
19236        } else {
19237            cortiq_core::TensorDtype::Q8_2f
19238        }
19239    };
19240    jobs.push(crate::gpu::MoeJob {
19241        gate: (gi, gr, gc, grs),
19242        up: (ui, ur, uc, urs),
19243        down: (di, dr, dc, drs),
19244        xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
19245        xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
19246        down_col: dcf,
19247        w,
19248        q1: gq1,
19249        q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
19250        q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
19251        gu_q2: gq2,
19252        swiglu_limit,
19253        act: 0,
19254    });
19255    Some(())
19256}
19257
19258/// Build one gate/up/down GPU job (see `moe_parts`).
19259fn moe_push_job<'a>(
19260    d: &'a DenseFfn,
19261    x: &[f32],
19262    w: f32,
19263    jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
19264    model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
19265) -> Option<()> {
19266    use crate::qtensor::prescale;
19267    // The block's act(gate)·up pass takes the activation's code (SiLU,
19268    // exact GELU, tanh-GELU); anything without a device form stays home.
19269    let act = d.act.graph_act()?.code();
19270    let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
19271    let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
19272    let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
19273    if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
19274        return None; // mixed-dtype trio — honest CPU path
19275    }
19276    if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
19277        return None;
19278    }
19279    if !gq2 && dq2 {
19280        return None;
19281    }
19282    model_ref.get_or_insert_with(|| gm.clone());
19283    let gdt = if gcf.is_empty() {
19284        cortiq_core::TensorDtype::Q8Row
19285    } else {
19286        cortiq_core::TensorDtype::Q8_2f
19287    };
19288    let udt = if ucf.is_empty() {
19289        cortiq_core::TensorDtype::Q8Row
19290    } else {
19291        cortiq_core::TensorDtype::Q8_2f
19292    };
19293    jobs.push(crate::gpu::MoeJob {
19294        gate: (gi, gr, gc, grs),
19295        up: (ui, ur, uc, urs),
19296        down: (di, dr, dc, drs),
19297        xs_gate: prescale(x, gcf, gdt).into_owned(),
19298        xs_up: prescale(x, ucf, udt).into_owned(),
19299        down_col: dcf,
19300        w,
19301        q1: gq1,
19302        q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
19303        q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
19304        gu_q2: gq2,
19305        swiglu_limit: 0.0,
19306        act,
19307    });
19308    Some(())
19309}
19310
19311/// Sparse dense-FFN directly on QUANTIZED weights (mask × mmap): reads
19312/// ONLY the active neurons' gate/up rows and down columns from the mmap
19313/// — no full-matrix dequant, no f32 model copy. This is what lets a
19314/// masked big model run at quantized RSS (the historical mask path
19315/// forced the whole model to f32). Semantics identical to the f32
19316/// sparse path within quant tolerance.
19317fn sparse_ffn_quant(
19318    d: &DenseFfn,
19319    x: &[f32],
19320    active: &[u16],
19321    hidden: usize,
19322    pool: Option<&Pool>,
19323) -> Vec<f32> {
19324    let n = active.len();
19325    let inter = d.gate_proj.rows();
19326    let mut act = vec![0.0f32; n];
19327    // Scratch is needed if EITHER projection is group-packed (q4/vbit);
19328    // gate/up normally share a dtype but sizing on both is robust.
19329    let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
19330    let compute = |ai: usize| -> f32 {
19331        let idx = active[ai] as usize;
19332        if idx >= inter {
19333            return 0.0; // defensive parity with the f32 sparse path
19334        }
19335        let mut s = if need_scratch {
19336            vec![0.0f32; hidden]
19337        } else {
19338            Vec::new()
19339        };
19340        let gate = d.gate_proj.row_dot(idx, x, &mut s);
19341        let up = d.up_proj.row_dot(idx, x, &mut s);
19342        d.act.combine(gate, up)
19343    };
19344    match pool {
19345        Some(p) if n >= 256 => {
19346            let ptr = SendMut(act.as_mut_ptr());
19347            p.run(&|widx, nw| {
19348                let chunk = n.div_ceil(nw);
19349                let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
19350                for ai in s..e {
19351                    unsafe { *ptr.at(ai) = compute(ai) };
19352                }
19353            });
19354        }
19355        _ => {
19356            for (ai, a) in act.iter_mut().enumerate() {
19357                *a = compute(ai);
19358            }
19359        }
19360    }
19361    // Scatter through active down columns (reads only those columns).
19362    let mut out = vec![0.0f32; hidden];
19363    for (ai, &idx) in active.iter().enumerate() {
19364        let w = act[ai];
19365        if w.abs() >= 1e-12 && (idx as usize) < inter {
19366            d.down_proj.add_col_scaled(idx as usize, w, &mut out);
19367        }
19368    }
19369    out
19370}
19371
19372/// Test-only re-export of the private sparse-quant FFN (mask × mmap gate).
19373#[doc(hidden)]
19374pub fn sparse_ffn_quant_for_test(
19375    d: &DenseFfn,
19376    x: &[f32],
19377    active: &[u16],
19378    hidden: usize,
19379) -> Vec<f32> {
19380    sparse_ffn_quant(d, x, active, hidden, None)
19381}
19382
19383/// Dequantize a DenseFfn's three matrices to f32 (transient; only the
19384/// q4/vbit-masked fallback uses it — the memory-lean path is
19385/// sparse_ffn_quant). Reuses row_f32 row-by-row.
19386fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
19387    let deq = |t: &QTensor| -> Vec<f32> {
19388        let (rows, cols) = (t.rows(), t.cols());
19389        let mut out = vec![0.0f32; rows * cols];
19390        for r in 0..rows {
19391            t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
19392        }
19393        out
19394    };
19395    (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
19396}
19397
19398/// Pointer wrapper for the worker-pool scatter (same pattern as qtensor).
19399struct SendMut(*mut f32);
19400unsafe impl Send for SendMut {}
19401unsafe impl Sync for SendMut {}
19402impl SendMut {
19403    #[inline]
19404    // Deliberate unsynchronized scatter: pool workers write disjoint indices
19405    // in parallel, so returning `&mut` from `&self` is intentional here.
19406    #[allow(clippy::mut_from_ref)]
19407    unsafe fn at(&self, i: usize) -> &mut f32 {
19408        unsafe { &mut *self.0.add(i) }
19409    }
19410}
19411
19412/// Router → (selected experts in torch.topk order, per-expert score
19413/// vector, normalizer). The final weight of expert `e` is `p[e] / wsum`.
19414///
19415/// Two regimes share this. Qwen: softmax over ALL experts, top-k of the
19416/// probabilities, optional renorm — `router_sigmoid=false`, no bias,
19417/// scale 1 → bit-identical to the historical path. LFM2-MoE /
19418/// DeepSeek-V3 `noaux_tc`: per-expert sigmoid scores, an optional
19419/// selection bias (top-k CHOICE only; weights stay unbiased), an
19420/// architecture-specified renorm floor and a routed scale.  The ordinary
19421/// loader contract is `1e-6`; Flint preserves its source's `1e-20`.
19422pub(crate) fn moe_route(
19423    logits: &[f32],
19424    m: &MoeFfn,
19425    allowed: Option<&[bool]>,
19426) -> (Vec<usize>, Vec<f32>, f32) {
19427    moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
19428}
19429
19430/// Router implementation with an explicit sigmoid renormalization floor.
19431///
19432/// Flint's source computes `sum(selected_scores) + 1e-20`; using the generic
19433/// 1e-6 floor there is not a harmless tolerance difference when all logits
19434/// are very negative: it collapses the routed branch toward zero instead of
19435/// normalizing the selected experts. Keeping the epsilon parameter here
19436/// avoids changing the established Qwen/LFM2 contract while allowing each
19437/// architecture to preserve its own numerical semantics.
19438pub(crate) fn moe_route_with_eps(
19439    logits: &[f32],
19440    m: &MoeFfn,
19441    allowed: Option<&[bool]>,
19442    sigmoid_denom_eps: f32,
19443) -> (Vec<usize>, Vec<f32>, f32) {
19444    let ne = logits.len();
19445    // Expert restriction: the static env mask (CMF_MOE_MASK) AND the
19446    // active task mask's expert fields (spec §5) both narrow the
19447    // candidate set; selection happens over the admitted experts only.
19448    // With norm_topk the kept weights renormalize below; without it
19449    // the excluded mass is honestly dropped.
19450    let admit = |e: usize| {
19451        m.mask.as_ref().is_none_or(|mk| mk[e])
19452            && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
19453    };
19454    // The resonance router (spec §9.5.1) selects by the RAW score: the
19455    // trainer (`resonance_winner`) and the resident graph
19456    // (`embryo_core_route_pick`) take the first maximum of the scores
19457    // and run the winner with weight 1.0. Selecting through the softmax
19458    // instead is not the same decision: `exp(l − max)` rounds two scores
19459    // closer than 2^-25 (possible below |score| 0.25) to the same 1.0, and
19460    // the lower index would take a token whose score is strictly smaller
19461    // — the trainer's trace and the graph would disagree with this path.
19462    // `−∞` (outside the shell) never wins; with no finite admitted expert
19463    // the generic path below degrades to uniform. The winner's
19464    // probability is 1.0 by construction (a one-hot `p`), so its
19465    // renormalized weight is `routed_scaling` on both norm_topk settings.
19466    if m.resonance.is_some() && m.top_k == 1 {
19467        let mut best: Option<usize> = None;
19468        for e in (0..ne).filter(|&e| admit(e)) {
19469            let l = logits[e];
19470            if l == f32::NEG_INFINITY || l.is_nan() {
19471                continue;
19472            }
19473            if best.is_none_or(|b| l > logits[b]) {
19474                best = Some(e);
19475            }
19476        }
19477        if let Some(b) = best {
19478            let mut p = vec![0.0f32; ne];
19479            p[b] = 1.0;
19480            return (vec![b], p, 1.0 / m.routed_scaling);
19481        }
19482    }
19483    // A `−∞` logit (a grown expert outside its shell, `Resonance::scores`)
19484    // takes probability 0 on both paths: sigmoid(−∞) = 0, exp(−∞ − max) = 0
19485    // — top-1 is the best FINITE expert, its renormalized weight exactly
19486    // 1.0. Every expert at −∞ cannot happen (trunk experts have no shell);
19487    // should it, the softmax would be NaN, so it degrades to uniform.
19488    let p: Vec<f32> = if m.router_sigmoid {
19489        logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
19490    } else {
19491        let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19492        if mx == f32::NEG_INFINITY {
19493            vec![1.0 / ne.max(1) as f32; ne]
19494        } else {
19495            let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
19496            let s: f32 = e.iter().sum();
19497            for v in &mut e {
19498                *v /= s;
19499            }
19500            e
19501        }
19502    };
19503    let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
19504    // Descending by selection score, lower index wins ties (torch.topk).
19505    match &m.expert_bias {
19506        Some(b) => idx.sort_unstable_by(|&x, &y| {
19507            (p[y] + b[y])
19508                .partial_cmp(&(p[x] + b[x]))
19509                .unwrap()
19510                .then(x.cmp(&y))
19511        }),
19512        None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
19513    }
19514    idx.truncate(m.top_k);
19515    // Adaptive τ-routing: trim the tail experts once the kept mass is
19516    // enough. wsum below renormalizes over the KEPT set, so the output
19517    // stays a proper weighted average.
19518    if let Some(tau) = m.route_tau {
19519        let total: f32 = idx.iter().map(|&e| p[e]).sum();
19520        if total > 0.0 {
19521            let mut acc = 0.0f32;
19522            let mut keep = idx.len();
19523            for (i, &e) in idx.iter().enumerate() {
19524                acc += p[e];
19525                if acc >= tau * total {
19526                    keep = i + 1;
19527                    break;
19528                }
19529            }
19530            idx.truncate(keep);
19531        }
19532    }
19533    let wsum: f32 = if m.norm_topk_prob {
19534        let s: f32 = idx.iter().map(|&e| p[e]).sum();
19535        // Sigmoid routers use their architecture's reference floor; the
19536        // softmax path's probs already sum near 1, so it stays exactly as
19537        // before.
19538        (if m.router_sigmoid {
19539            s + sigmoid_denom_eps
19540        } else {
19541            s
19542        }) / m.routed_scaling
19543    } else {
19544        1.0 / m.routed_scaling
19545    };
19546    (idx, p, wsum)
19547}
19548
19549/// See the call site: one `layer:e1,e2,…` line per routed token.
19550fn moe_trace(idx: &[usize]) {
19551    moe_trace_at(crate::gpu::cur_layer() as i32, idx)
19552}
19553
19554/// The same, for callers that know their layer (DSV4 owns its layers and
19555/// never sets the pipeline's current-layer marker).
19556pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
19557    use std::io::Write;
19558    static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
19559        std::sync::OnceLock::new();
19560    let Some(f) = F.get_or_init(|| {
19561        let p = std::env::var("CMF_MOE_TRACE").ok()?;
19562        Some(std::sync::Mutex::new(
19563            std::fs::OpenOptions::new()
19564                .create(true)
19565                .append(true)
19566                .open(p)
19567                .ok()?,
19568        ))
19569    }) else {
19570        return;
19571    };
19572    let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
19573    let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
19574}
19575
19576/// MoE FFN: router → top-k experts (see `moe_route`). Only selected
19577/// experts' pages are touched in mmap.
19578pub(crate) fn moe_ffn(
19579    m: &MoeFfn,
19580    x: &[f32],
19581    pool: Option<&Pool>,
19582    allowed: Option<&[bool]>,
19583) -> Vec<f32> {
19584    let r = moe_ffn_route(m, x, pool, allowed);
19585    moe_ffn_experts(m, x, &r, pool)
19586}
19587
19588/// One token's host route through a MoE layer: the chosen experts in
19589/// selection order, the per-expert scores and the normalizer (see
19590/// `moe_route`), plus the raw router logits.
19591pub(crate) struct MoeRoute {
19592    pub idx: Vec<usize>,
19593    pub p: Vec<f32>,
19594    pub wsum: f32,
19595    pub logits: Vec<f32>,
19596}
19597
19598/// The routing half of `moe_ffn`, shared by every executor of the chosen
19599/// experts (the host/per-op path below and the MiMo dynamic device cache,
19600/// `crate::mimo_moe`): activation accounting, router logits, `moe_route`,
19601/// the selection statistics and the `CMF_MOE_TRACE` line — so switching
19602/// executors can never change which experts a token gets.
19603pub(crate) fn moe_ffn_route(
19604    m: &MoeFfn,
19605    x: &[f32],
19606    pool: Option<&Pool>,
19607    allowed: Option<&[bool]>,
19608) -> MoeRoute {
19609    accumulate_act(m, x, 1);
19610    let ne = m.experts.len();
19611    let mut logits = vec![0.0f32; ne];
19612    match &m.resonance {
19613        Some(r) => r.scores(x, &mut logits),
19614        None => m.router.matvec(x, &mut logits, pool),
19615    }
19616    let (idx, p, wsum) = moe_route(&logits, m, allowed);
19617    {
19618        let mut st = m.stats.borrow_mut();
19619        if st.len() < ne {
19620            st.resize(ne, 0);
19621        }
19622        for &e in &idx {
19623            st[e] += 1;
19624        }
19625    }
19626    // `CMF_MOE_TRACE=<file>`: append one line per (layer, token) with the
19627    // selected expert ids. The cumulative `stats` above answer "which
19628    // experts are popular"; a residency design needs the question they
19629    // cannot answer — whether CONSECUTIVE tokens reuse experts (the
19630    // temporal locality an LRU cache lives on, FreeToken §4).
19631    moe_trace(&idx);
19632    MoeRoute {
19633        idx,
19634        p,
19635        wsum,
19636        logits,
19637    }
19638}
19639
19640/// Flint's released mixed MoE profile has a native-Metal selected-expert
19641/// pager with a different numerical contract from the generic CPU probe:
19642/// the pager keeps activations in f32, whereas the fast host arm normally
19643/// uses its A8W8 activation path.  Timing those two approximations against
19644/// one another can make the probe reject the exact pager even when it is the
19645/// faster exact-f32 implementation.  Keep this gate deliberately narrower
19646/// than the pager itself; the pager revalidates every selected tuple before
19647/// it binds a no-copy view and an honest refusal still takes the CPU path.
19648#[cfg(target_os = "macos")]
19649fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
19650    const HIDDEN: usize = 2_048;
19651    const ROUTED_INTER: usize = 512;
19652    const SHARED_INTER: usize = 1_024;
19653    const EXPERTS: usize = 256;
19654    const TOP_K: usize = 6;
19655
19656    let Some(routed) = m.experts.first() else {
19657        return false;
19658    };
19659    let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
19660        return false;
19661    };
19662    let Some((shared, shared_gate)) = &m.shared else {
19663        return false;
19664    };
19665    if shared_gate.is_some()
19666        || model.header.arch.arch_name != "flint"
19667        || model.header.arch.prism_hadamard.is_some()
19668        || model.header.arch.hidden_size != HIDDEN
19669        || m.experts.len() != EXPERTS
19670        || m.top_k != TOP_K
19671        || !m.router_sigmoid
19672        || !m.norm_topk_prob
19673        || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
19674        || m.routed_scaling.to_bits() != 1.0f32.to_bits()
19675        || m.route_tau.is_some()
19676        || m.mask.is_some()
19677        || m.per_expert_scale.is_some()
19678        || m.router_input_norm
19679        || m.resonance.is_some()
19680        || !m.grown.is_empty()
19681    {
19682        return false;
19683    }
19684
19685    let routed_q2 = routed.act == Act::Silu
19686        && routed.segs.is_empty()
19687        && routed.gate_proj.rows() == ROUTED_INTER
19688        && routed.gate_proj.cols() == HIDDEN
19689        && routed.up_proj.rows() == ROUTED_INTER
19690        && routed.up_proj.cols() == HIDDEN
19691        && routed.down_proj.rows() == HIDDEN
19692        && routed.down_proj.cols() == ROUTED_INTER
19693        && routed.up_proj.mapped_q2tp().is_some()
19694        && routed.down_proj.mapped_q4tp().is_some();
19695    let shared_q4 = shared.act == Act::Silu
19696        && shared.segs.is_empty()
19697        && shared.gate_proj.rows() == SHARED_INTER
19698        && shared.gate_proj.cols() == HIDDEN
19699        && shared.up_proj.rows() == SHARED_INTER
19700        && shared.up_proj.cols() == HIDDEN
19701        && shared.down_proj.rows() == HIDDEN
19702        && shared.down_proj.cols() == SHARED_INTER
19703        && shared.gate_proj.mapped_q4tp().is_some()
19704        && shared.up_proj.mapped_q4tp().is_some()
19705        && shared.down_proj.mapped_q4tp().is_some();
19706    routed_q2 && shared_q4
19707}
19708
19709/// The expert half of `moe_ffn`: run a route's experts on the per-op GPU
19710/// block or the host.
19711pub(crate) fn moe_ffn_experts(
19712    m: &MoeFfn,
19713    x: &[f32],
19714    r: &MoeRoute,
19715    pool: Option<&Pool>,
19716) -> Vec<f32> {
19717    let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
19718    // D5: the whole layer MoE block in one GPU command buffer (experts — the
19719    // same mmap via a no-copy buffer; intermediate activations on the GPU).
19720    // MoE owns a separate probe class from dense FFN: its selected-span pager
19721    // has materially different residency and latency costs.
19722    if crate::gpu::enabled_here() {
19723        #[cfg(target_os = "macos")]
19724        if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
19725            // Do not compare the pager's exact-f32 result with the CPU's
19726            // optional A8W8 activation approximation.  The strict pager is
19727            // the validated Flint path; if a directory/view/device check
19728            // declines, preserve the existing CPU fallback rather than
19729            // leaving the probe in a half-sampled state.
19730            return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
19731                crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
19732            });
19733        }
19734        match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
19735            crate::gpu::ProbeArm::Gpu => {
19736                let t0 = std::time::Instant::now();
19737                if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
19738                    crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
19739                    return out;
19740                }
19741                crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
19742            }
19743            crate::gpu::ProbeArm::CpuTimed => {
19744                let t0 = std::time::Instant::now();
19745                let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
19746                crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
19747                return out;
19748            }
19749            crate::gpu::ProbeArm::Cpu => {
19750                return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
19751            }
19752        }
19753    }
19754    moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
19755}
19756
19757/// One MiniCPM CPU-tail MoE through its opt-in dynamic bank, or through the
19758/// same-route host fallback.  The route is formed exactly once so statistics,
19759/// tracing, and model semantics remain owned by `moe_ffn_route`.
19760fn moe_ffn_minicpm_tail(
19761    slot: &mut crate::minicpm_tail::MiniCpmTail,
19762    li: usize,
19763    total_layers: usize,
19764    m: &MoeFfn,
19765    x: &[f32],
19766    pool: Option<&Pool>,
19767) -> Vec<f32> {
19768    let route = moe_ffn_route(m, x, pool, None);
19769    match slot.forward(li, m, x, &route, pool, total_layers) {
19770        Some(out) => out,
19771        // A cache refusal must be observationally identical to the generic
19772        // CPU-tail path. The surrounding host-tail scope already disables the
19773        // per-op arena, so do not introduce a float-activation policy change
19774        // merely because this explicit opt-in cache declined a model/layout.
19775        None => moe_ffn_experts(m, x, &route, pool),
19776    }
19777}
19778
19779/// One MoE token through the MiMo expert bank (`crate::mimo_moe`), or —
19780/// when the bank does not serve it — through the host path with the SAME
19781/// route, so the routing statistics and `CMF_MOE_TRACE` see it once.
19782fn moe_ffn_banked(
19783    slot: &mut crate::mimo_moe::Slot,
19784    li: usize,
19785    m: &MoeFfn,
19786    x: &[f32],
19787    pool: Option<&Pool>,
19788) -> Vec<f32> {
19789    let t0 = std::time::Instant::now();
19790    let r = moe_ffn_route(m, x, pool, None);
19791    slot.note_route(t0.elapsed().as_nanos() as u64);
19792    match slot.forward(li, m, x, &r, pool) {
19793        Some(out) => out,
19794        None => crate::qtensor::float_activations_scope(|| {
19795            crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
19796        }),
19797    }
19798}
19799
19800/// Verify rows share a bank frame; routing and fallback are decode's.
19801fn moe_ffn_banked_rows(
19802    slot: &mut crate::mimo_moe::Slot,
19803    li: usize,
19804    m: &MoeFfn,
19805    xs: &[f32],
19806    b: usize,
19807    hidden: usize,
19808    pool: Option<&Pool>,
19809) -> Vec<f32> {
19810    let t0 = std::time::Instant::now();
19811    let routes: Vec<_> = xs
19812        .chunks_exact(hidden)
19813        .map(|x| moe_ffn_route(m, x, pool, None))
19814        .collect();
19815    slot.note_route(t0.elapsed().as_nanos() as u64);
19816    if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
19817        return out;
19818    }
19819    let mut out = Vec::with_capacity(b * hidden);
19820    for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
19821        let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
19822            // A failed bank must not stream missing experts into the arena.
19823            crate::qtensor::float_activations_scope(|| {
19824                crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
19825            })
19826        });
19827        out.extend(row);
19828    }
19829    out
19830}
19831
19832/// One-shot report of whether the whole-token wgpu graph actually formed.
19833/// A refusal silently reverts to the per-op path, which is how a model can
19834/// look "GPU-accelerated" while every layer walks the host.  A device prefix
19835/// is tracked separately because it still pays a host boundary for the tail.
19836fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
19837    use std::sync::atomic::{AtomicBool, Ordering};
19838    if built {
19839        GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
19840        if total_layers > 0 && layers_run < total_layers {
19841            GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
19842        } else {
19843            GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
19844        }
19845    } else {
19846        GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
19847    }
19848    static SAID: AtomicBool = AtomicBool::new(false);
19849    if !SAID.swap(true, Ordering::Relaxed) {
19850        if built {
19851            tracing::info!("wgpu whole-token graph: ACTIVE");
19852        } else {
19853            tracing::warn!("wgpu whole-token graph refused — per-op path");
19854        }
19855    }
19856}
19857
19858/// Whole-token graph outcomes, process-wide: a benchmark that claims a
19859/// GPU number while MISS climbs is measuring the CPU — the honest-bench
19860/// contract makes that an error, not a footnote.
19861pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19862pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19863/// Graph calls that returned a hidden after running only a leading device
19864/// prefix.  These are valid hybrid executions but must not be reported as a
19865/// full GPU graph in benchmark evidence.
19866pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19867/// Graph calls that covered the complete requested layer span.
19868pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19869
19870/// Native Metal TokenGraph completion counters. These are incremented only
19871/// after checked command-buffer completion and successful readback, so a
19872/// fused-head NLL report can prove the route rather than infer it from env.
19873pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19874pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19875pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
19876    std::sync::atomic::AtomicU64::new(0);
19877pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19878pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19879/// Ordinary native-Metal rows-prefill admissions and completed rows.  These
19880/// counters are separate from TokenGraph token/head counts so a batch NLL
19881/// receipt cannot accidentally claim serial execution as batched.
19882pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
19883    std::sync::atomic::AtomicU64::new(0);
19884pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19885pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
19886    std::sync::atomic::AtomicU64::new(0);
19887pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
19888    std::sync::atomic::AtomicU64::new(0);
19889
19890/// `CMF_MOE_BATCH=0` restores the per-expert serial loop — the A/B lever
19891/// for the batched kernel, and how its bit-identity is checked.
19892fn moe_batch_enabled() -> bool {
19893    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19894    *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
19895}
19896
19897/// Two-dispatch CPU MoE: every routed expert (and the shared one) fused
19898/// into one gate/up/SiLU dispatch and one down dispatch, instead of two
19899/// pool barriers per expert. Bit-identical to the serial loop below —
19900/// see `moe_gate_up_many` / `moe_down_many`. `None` = the batched kernel
19901/// does not cover this layer, walk the serial path.
19902fn moe_ffn_cpu_batched(
19903    m: &MoeFfn,
19904    x: &[f32],
19905    idx: &[usize],
19906    p: &[f32],
19907    wsum: f32,
19908    pool: Option<&Pool>,
19909) -> Option<Vec<f32>> {
19910    if idx.is_empty() || !moe_batch_enabled() {
19911        return None;
19912    }
19913    // The bake probe reads per-neuron activation mass out of the
19914    // single-expert path; batching would skip it. Rare and offline —
19915    // hand those runs to the serial loop.
19916    if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
19917        return None;
19918    }
19919    let n = idx.len() + usize::from(m.shared.is_some());
19920    let mut pairs = Vec::with_capacity(n);
19921    let mut downs = Vec::with_capacity(n);
19922    let mut ws = Vec::with_capacity(n);
19923    for &e in idx {
19924        let d = &m.experts[e];
19925        if d.act != Act::Silu {
19926            return None;
19927        }
19928        pairs.push((&d.gate_proj, &d.up_proj));
19929        downs.push(&d.down_proj);
19930        ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
19931    }
19932    // The shared expert goes last, matching the serial loop's order —
19933    // the f32 accumulation order is part of the bit-identity claim.
19934    if let Some((se, gate)) = &m.shared {
19935        if se.act != Act::Silu {
19936            return None;
19937        }
19938        let g = gate.as_ref().map_or(1.0, |gate| {
19939            let mut gl = [0.0f32; 1];
19940            gate.matvec(x, &mut gl, pool);
19941            1.0 / (1.0 + (-gl[0]).exp())
19942        });
19943        pairs.push((&se.gate_proj, &se.up_proj));
19944        downs.push(&se.down_proj);
19945        ws.push(g);
19946    }
19947    let inter = pairs[0].0.rows();
19948    let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
19949    if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
19950        return None;
19951    }
19952    let mut out = attention::take_buf(x.len());
19953    if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
19954        attention::recycle_buf(&mut out);
19955        return None;
19956    }
19957    Some(out)
19958}
19959
19960/// Exact CPU completion for the routed experts a dynamic device cache did
19961/// not contain. The weights are already the router's final normalized mix.
19962/// Keeping this independent of `MoeFfn` makes the job `Sync`: its routing
19963/// statistics live in a `RefCell`, while the immutable expert tensors can be
19964/// evaluated safely in parallel with the GPU's resident subset.
19965pub(crate) fn moe_cold_experts_cpu(
19966    experts: &[(&DenseFfn, f32)],
19967    x: &[f32],
19968    pool: Option<&Pool>,
19969) -> Vec<f32> {
19970    let mut out = attention::take_buf(x.len());
19971    if experts.is_empty() {
19972        return out;
19973    }
19974    let pairs: Vec<_> = experts
19975        .iter()
19976        .map(|(e, _)| (&e.gate_proj, &e.up_proj))
19977        .collect();
19978    let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
19979    let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
19980    let inter = experts[0].0.gate_proj.rows();
19981    let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
19982    if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
19983        && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
19984    {
19985        return out;
19986    }
19987    out.fill(0.0);
19988    for &(expert, weight) in experts {
19989        let mut one = dense_ffn(expert, x, pool);
19990        for (o, v) in out.iter_mut().zip(&one) {
19991            *o += weight * v;
19992        }
19993        attention::recycle_buf(&mut one);
19994    }
19995    out
19996}
19997
19998/// Cold part of a short bank batch. Share each expert's weight stream
19999/// across its tokens, but reduce contributions in each token's route order.
20000/// On an unsupported CPU/layout, retain the single-token cold kernels.
20001pub(crate) fn moe_cold_experts_rows_cpu(
20002    jobs: &[Vec<(&DenseFfn, f32)>],
20003    xs: &[f32],
20004    hidden: usize,
20005    pool: Option<&Pool>,
20006) -> Vec<f32> {
20007    let mut out = vec![0.0; xs.len()];
20008    let mut experts: Vec<&DenseFfn> = Vec::new();
20009    let mut groups: Vec<Vec<usize>> = Vec::new();
20010    let mut terms = vec![Vec::new(); jobs.len()];
20011    for (r, row) in jobs.iter().enumerate() {
20012        for &(e, w) in row {
20013            let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
20014                Some(g) => g,
20015                None => {
20016                    experts.push(e);
20017                    groups.push(Vec::new());
20018                    groups.len() - 1
20019                }
20020            };
20021            terms[r].push((g, groups[g].len(), w));
20022            groups[g].push(r);
20023        }
20024    }
20025    if experts.is_empty() {
20026        return out;
20027    }
20028    let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
20029    let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
20030    let lens: Vec<_> = groups.iter().map(Vec::len).collect();
20031    let count: usize = lens.iter().sum();
20032    let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
20033    let mut ds = vec![vec![0.0; hidden]; count];
20034    if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
20035        && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
20036    {
20037        let mut offset = 0;
20038        let offsets: Vec<_> = lens
20039            .iter()
20040            .map(|&n| {
20041                let start = offset;
20042                offset += n;
20043                start
20044            })
20045            .collect();
20046        for (r, terms) in terms.iter().enumerate() {
20047            for &(g, slot, w) in terms {
20048                for (o, &v) in out[r * hidden..(r + 1) * hidden]
20049                    .iter_mut()
20050                    .zip(&ds[offsets[g] + slot])
20051                {
20052                    *o += w * v;
20053                }
20054            }
20055        }
20056    } else {
20057        for (r, jobs) in jobs.iter().enumerate() {
20058            if !jobs.is_empty() {
20059                let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
20060                out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
20061                attention::recycle_buf(&mut row);
20062            }
20063        }
20064    }
20065    out
20066}
20067
20068/// The pure-CPU MoE expert loop (also the fallback of every GPU refusal).
20069fn moe_ffn_cpu(
20070    m: &MoeFfn,
20071    x: &[f32],
20072    idx: &[usize],
20073    p: &[f32],
20074    wsum: f32,
20075    pool: Option<&Pool>,
20076) -> Vec<f32> {
20077    if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
20078        return out;
20079    }
20080    let mut out = attention::take_buf(x.len());
20081    for &e in idx {
20082        let mut eo = dense_ffn(&m.experts[e], x, pool);
20083        let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
20084        for i in 0..out.len() {
20085            out[i] += w * eo[i];
20086        }
20087        attention::recycle_buf(&mut eo);
20088    }
20089    if let Some((se, gate)) = &m.shared {
20090        let mut so = dense_ffn(se, x, pool);
20091        let g = gate.as_ref().map_or(1.0, |gate| {
20092            let mut gl = [0.0f32; 1];
20093            gate.matvec(x, &mut gl, pool);
20094            1.0 / (1.0 + (-gl[0]).exp())
20095        });
20096        for i in 0..out.len() {
20097            out[i] += g * so[i];
20098        }
20099        attention::recycle_buf(&mut so);
20100    }
20101    out
20102}
20103
20104/// DeepSeek-V2 MLA forward, expand-to-MHA form (see `AttnKind::Mla`):
20105/// per token the latent expands to every head's K/V and the ordinary
20106/// cache + grouped attend do the rest. K head layout is [rope | nope]
20107/// (rotary_dim = qk_rope rotates the shared rope key and each q head's
20108/// prefix); V rows are zero-padded to the K head_dim inside the cache
20109/// and the pad is sliced off before O. Attention importance is not
20110/// accumulated for MLA yet (no eviction interplay).
20111#[allow(clippy::too_many_arguments)]
20112pub(crate) fn mla_attention(
20113    w: &MlaWeights,
20114    normed: &[f32],
20115    cache: &mut crate::kv_cache::LayerKvCache,
20116    position: usize,
20117    inv_freq: &[f32],
20118    rope_scale: f32,
20119    eps: f64,
20120    pool: Option<&Pool>,
20121) -> Vec<f32> {
20122    let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
20123    let hd = dr + dn;
20124    let mut q = vec![0.0f32; nh * hd];
20125    match (&w.q_a, &w.q_a_norm) {
20126        (Some(qa), Some(qn)) => {
20127            let mut t = vec![0.0f32; qa.rows()];
20128            qa.matvec(normed, &mut t, pool);
20129            let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
20130            w.q_proj.matvec(&tn, &mut q, pool);
20131        }
20132        _ => w.q_proj.matvec(normed, &mut q, pool),
20133    }
20134    let mut ca = vec![0.0f32; lora + dr];
20135    w.kv_a.matvec(normed, &mut ca, pool);
20136    let (c_lat, k_rope) = ca.split_at_mut(lora);
20137    let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
20138    let mut kvb = vec![0.0f32; nh * (dn + dv)];
20139    w.kv_b.matvec(&latn, &mut kvb, pool);
20140    if !w.nope {
20141        attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
20142    }
20143    for h in 0..nh {
20144        if !w.nope {
20145            attention::rope_rotate_scaled(
20146                &mut q[h * hd..h * hd + dr],
20147                position,
20148                inv_freq,
20149                rope_scale,
20150            );
20151        }
20152    }
20153    let mut k = vec![0.0f32; nh * hd];
20154    let mut v = vec![0.0f32; nh * hd];
20155    for h in 0..nh {
20156        k[h * hd..h * hd + dr].copy_from_slice(k_rope);
20157        k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
20158        v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
20159    }
20160    cache.append(&k, &v, &vec![true; nh]);
20161    let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
20162    attention::recycle_buf(&mut imp);
20163    let mut ov = vec![0.0f32; nh * dv];
20164    for h in 0..nh {
20165        ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
20166    }
20167    let mut out = vec![0.0f32; w.o_proj.rows()];
20168    w.o_proj.matvec(&ov, &mut out, pool);
20169    out
20170}
20171
20172/// Gemma-4 dual-branch FFN (spec: see `FfnKind::DenseMoe`). The dense
20173/// branch reads the pre-FFN-normed activation; the router and the
20174/// expert branch read the RAW residual — the router through a
20175/// scale-less rms norm (its constant gain is folded into the weights),
20176/// the experts through `pre_norm_2`. CPU path; GPU graphs refuse the
20177/// layer kind honestly.
20178fn dense_moe_ffn(
20179    dm: &DenseMoeFfn,
20180    x_normed: &[f32],
20181    h_raw: &[f32],
20182    eps: f64,
20183    norm_style: NormStyle,
20184    pool: Option<&Pool>,
20185) -> Vec<f32> {
20186    let mut d = dense_ffn(&dm.dense, x_normed, pool);
20187    d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
20188    let m = &dm.moe;
20189    let ne = m.experts.len();
20190    let mut logits = vec![0.0f32; ne];
20191    if m.router_input_norm {
20192        let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
20193        let inv = 1.0 / (ss + eps as f32).sqrt();
20194        let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
20195        m.router.matvec(&xr, &mut logits, pool);
20196    } else {
20197        m.router.matvec(h_raw, &mut logits, pool);
20198    }
20199    let (idx, p, wsum) = moe_route(&logits, m, None);
20200    {
20201        let mut st = m.stats.borrow_mut();
20202        if st.len() < ne {
20203            st.resize(ne, 0);
20204        }
20205        for &e in &idx {
20206            st[e] += 1;
20207        }
20208    }
20209    let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
20210    let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
20211    let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
20212    for (di, mi) in d.iter_mut().zip(&mo) {
20213        *di += mi;
20214    }
20215    d
20216}
20217
20218/// Building the MoE-layer GPU jobs: all selected experts (+shared) must
20219/// be q8_2f-Mapped from the primary mapping; otherwise None → CPU path.
20220/// One-shot report of why the MoE GPU block refused. A silent `?` here
20221/// sends every expert to the CPU with nothing in the logs to say so —
20222/// which is exactly how a q4tp MoE model looked "GPU-accelerated" while
20223/// running entirely on the host.
20224fn moe_gpu_refused(why: &'static str) {
20225    use std::sync::atomic::{AtomicBool, Ordering};
20226    static SAID: AtomicBool = AtomicBool::new(false);
20227    if !SAID.swap(true, Ordering::Relaxed) {
20228        tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
20229    }
20230}
20231
20232fn moe_ffn_gpu(
20233    m: &MoeFfn,
20234    x: &[f32],
20235    idx: &[usize],
20236    p: &[f32],
20237    wsum: f32,
20238    pool: Option<&Pool>,
20239) -> Option<Vec<f32>> {
20240    use crate::gpu::MoeJob;
20241
20242    let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
20243    let mut model_ref = None;
20244    for &e in idx {
20245        if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
20246            moe_gpu_refused("push_job(expert)");
20247            return None;
20248        }
20249    }
20250    if let Some((se, gate)) = &m.shared {
20251        let g = gate.as_ref().map_or(1.0, |gate| {
20252            let mut gl = [0.0f32; 1];
20253            gate.matvec(x, &mut gl, pool);
20254            1.0 / (1.0 + (-gl[0]).exp())
20255        });
20256        if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
20257            moe_gpu_refused("push_job(shared)");
20258            return None;
20259        }
20260    }
20261    let Some(model) = model_ref else {
20262        moe_gpu_refused("no model_ref");
20263        return None;
20264    };
20265    let hidden = jobs[0].down.1;
20266    let mut out = vec![0.0f32; hidden];
20267    if crate::gpu::moe_block(&model, &jobs, &mut out) {
20268        Some(out)
20269    } else {
20270        moe_gpu_refused("gpu::moe_block");
20271        None
20272    }
20273}
20274
20275/// Single-position FFN dispatch.
20276fn ffn_forward(
20277    ffn: &FfnKind,
20278    x: &[f32],
20279    pool: Option<&Pool>,
20280    experts_allowed: Option<&[bool]>,
20281) -> Vec<f32> {
20282    match ffn {
20283        FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
20284        FfnKind::Dense(d) => dense_ffn(d, x, pool),
20285        FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
20286        // Dual-branch layers need the raw residual — their callers
20287        // dispatch dense_moe_ffn directly; the auxiliary paths that land
20288        // here (MTP draft, o1 replay) do not co-occur with gemma-4 MoE.
20289        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
20290    }
20291}
20292
20293/// Fused two-position FFN: gate/up/down streamed once (dense). MoE
20294/// falls back to two singles — expert sets differ per position, there
20295/// is nothing to fuse.
20296fn ffn_forward_pair(
20297    ffn: &FfnKind,
20298    x1: &[f32],
20299    x2: &[f32],
20300    pool: Option<&Pool>,
20301    experts_allowed: Option<&[bool]>,
20302) -> (Vec<f32>, Vec<f32>) {
20303    let d = match ffn {
20304        // A tube layer has nothing to fuse across the pair — the tubes
20305        // are separate matrices; two singles are the honest path.
20306        FfnKind::Dense(d) if !d.segs.is_empty() => {
20307            return (
20308                tube_ffn(d, x1, 1, pool, None),
20309                tube_ffn(d, x2, 1, pool, None),
20310            );
20311        }
20312        FfnKind::Dense(d) => d,
20313        FfnKind::Moe(m) => {
20314            return (
20315                moe_ffn(m, x1, pool, experts_allowed),
20316                moe_ffn(m, x2, pool, experts_allowed),
20317            );
20318        }
20319        FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
20320    };
20321    let inter = d.gate_proj.rows();
20322    FFN_SCRATCH.with(|s| {
20323        let mut s = s.borrow_mut();
20324        let [g1, g2, u1, u2] = &mut *s;
20325        g1.resize(inter, 0.0);
20326        g2.resize(inter, 0.0);
20327        u1.resize(inter, 0.0);
20328        u2.resize(inter, 0.0);
20329        // Multi-matrix pair job: gate+up under one pool dispatch
20330        // (o1s = lane-1 outputs across tensors, o2s = lane-2).
20331        QTensor::matvec2_many(
20332            [&d.gate_proj, &d.up_proj],
20333            x1,
20334            x2,
20335            [g1.as_mut_slice(), u1.as_mut_slice()],
20336            [g2.as_mut_slice(), u2.as_mut_slice()],
20337            pool,
20338        );
20339        for i in 0..inter {
20340            g1[i] = d.act.combine(g1[i], u1[i]);
20341            g2[i] = d.act.combine(g2[i], u2[i]);
20342        }
20343        let mut o1 = attention::take_buf(d.down_proj.rows());
20344        let mut o2 = attention::take_buf(d.down_proj.rows());
20345        d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
20346        (o1, o2)
20347    })
20348}
20349
20350#[cfg(test)]
20351mod tests {
20352
20353    /// The 0.7.6 prefill-chunk rule: a plain dense stack wholly on a
20354    /// discrete card reads the prompt in wide chunks on x86; every other
20355    /// case keeps the width it had (the GDN-hybrid, MoE and DeepSeek paths
20356    /// were tuned on hardware not measured for this change).
20357    #[test]
20358    fn prefill_chunk_rule_widens_only_dense_on_discrete() {
20359        use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
20360        let dense_card = ChunkStackFacts {
20361            plain_dense: true,
20362            discrete: true,
20363            gpu_on: true,
20364            ..Default::default()
20365        };
20366        assert!(dense_card.dense_on_discrete());
20367        // The bug: a dense Llama on a Vulkan RTX 3090 got 48.
20368        assert_eq!(
20369            prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
20370            DISCRETE_DENSE_PREFILL_CHUNK
20371        );
20372        assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
20373        for (label, facts) in [
20374            (
20375                "GDN hybrid / MoE / DeepSeek stack",
20376                ChunkStackFacts {
20377                    plain_dense: false,
20378                    ..dense_card
20379                },
20380            ),
20381            (
20382                "integrated GPU",
20383                ChunkStackFacts {
20384                    discrete: false,
20385                    ..dense_card
20386                },
20387            ),
20388            (
20389                "CPU only",
20390                ChunkStackFacts {
20391                    gpu_on: false,
20392                    discrete: false,
20393                    ..dense_card
20394                },
20395            ),
20396            (
20397                "capacity split",
20398                ChunkStackFacts {
20399                    capacity_split: true,
20400                    ..dense_card
20401                },
20402            ),
20403            (
20404                "multi-GPU plan",
20405                ChunkStackFacts {
20406                    multi_gpu: true,
20407                    ..dense_card
20408                },
20409            ),
20410            (
20411                "O(1) layers",
20412                ChunkStackFacts {
20413                    o1: true,
20414                    ..dense_card
20415                },
20416            ),
20417        ] {
20418            assert!(!facts.dense_on_discrete(), "{label}");
20419            assert_eq!(
20420                prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
20421                48,
20422                "{label} keeps the historical x86 chunk"
20423            );
20424        }
20425        // Other hosts are untouched whatever the model.
20426        for dense in [false, true] {
20427            assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
20428            assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
20429        }
20430        // CMF_PREFILL_CHUNK still wins everywhere (and is clamped to ≥ 1).
20431        for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
20432            for dense in [false, true] {
20433                assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
20434                assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
20435            }
20436        }
20437    }
20438
20439    #[test]
20440    fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
20441        use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
20442        let full = |host_rows, device_rows| ReuseLayer {
20443            full: true,
20444            host_rows,
20445            device_rows,
20446            device_state: false,
20447        };
20448        // Turn 1: 300-token prompt prefilled on the host, 40 tokens decoded
20449        // by the wgpu graph into the device mirror only. Turn 2 reuses 339.
20450        assert_eq!(
20451            kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
20452            ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
20453        );
20454        // CPU / Metal: the host owner already holds every forwarded row.
20455        assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
20456        // A mirror past the prefix is fine for the host (it gets rewound).
20457        assert_eq!(
20458            kv_reuse_plan(339, &[full(339, Some(345))]),
20459            ReusePlan::Ready
20460        );
20461        // GPU prefix / CPU tail: only the device layers lag.
20462        assert_eq!(
20463            kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
20464            ReusePlan::Pull(vec![(0, 300, 339)])
20465        );
20466        // The device cannot supply the missing rows: never continue.
20467        assert_eq!(
20468            kv_reuse_plan(339, &[full(300, Some(320))]),
20469            ReusePlan::Fresh
20470        );
20471        assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
20472        assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
20473        // A recurrent state advanced on the device cannot be handed to a
20474        // host prefill (it is not rewindable and the host copy is stale).
20475        let conv = |device_state| ReuseLayer {
20476            full: false,
20477            host_rows: 0,
20478            device_rows: None,
20479            device_state,
20480        };
20481        assert_eq!(
20482            kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
20483            ReusePlan::Fresh
20484        );
20485        assert_eq!(
20486            kv_reuse_plan(339, &[conv(false), full(339, None)]),
20487            ReusePlan::Ready
20488        );
20489    }
20490
20491    #[test]
20492    fn nll_graph_policy_scopes_only_the_fused_head() {
20493        for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
20494            // A Vulkan/Wgpu hidden-only graph remains the quality route.
20495            ("vulkan graph", true, true, false, true, false),
20496            // Native Metal adds the strict fused graph-head contract.
20497            ("native Metal graph", true, true, true, true, true),
20498            // Masked NLL and the explicit non-graph fallback remain unchanged.
20499            ("masked", false, true, false, false, false),
20500            ("graph disabled", true, false, true, false, false),
20501        ] {
20502            let (graph_quality, graph_head_required) =
20503                super::nll_graph_policy(unmasked, prefer_graph, native_metal);
20504            assert_eq!(graph_quality, want_graph, "{label}: graph quality");
20505            assert_eq!(graph_head_required, want_head, "{label}: fused head");
20506        }
20507    }
20508
20509    #[test]
20510    fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
20511        assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
20512        assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
20513        assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
20514        assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
20515        assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
20516    }
20517
20518    #[test]
20519    fn cancel_flag_stops_generation() {
20520        let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
20521        // Set before the call: the prefill loops honour it, the run
20522        // returns immediately with the cancelled reason and no tokens.
20523        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
20524        let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
20525        assert_eq!(r.finish_reason, "cancelled");
20526        assert!(
20527            r.token_ids.is_empty(),
20528            "no tokens after cancel: {:?}",
20529            r.token_ids
20530        );
20531        assert_eq!(p.kv_cache.seq_len(), 0);
20532        assert!(p.kv_history.is_empty());
20533        assert!(!p.graph_want_logits);
20534        assert!(p.graph_logits.is_none());
20535        // Flag auto-cleared: the next call generates normally.
20536        let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
20537        assert_ne!(r2.finish_reason, "cancelled");
20538    }
20539    use super::*;
20540
20541    #[test]
20542    fn explicit_sliding_schedule_selects_local_rope_profile() {
20543        // Mellum alternates three local 1,024-token layers with one global
20544        // YaRN layer.  An explicit schedule deliberately overrides the
20545        // cadence sentinel in `swa`, so test both RoPE table and scale
20546        // dispatch rather than merely checking that the metadata loaded.
20547        let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
20548        p.swa = Some((1024, usize::MAX));
20549        p.sliding_layers = Some(vec![true, true, true, false]);
20550        p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
20551        p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
20552        p.rope_scale = 1.277_258_9;
20553        p.rope_scale_local = 1.0;
20554
20555        for li in 0..3 {
20556            assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
20557            assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
20558            assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
20559        }
20560        assert_eq!(p.layer_window(3), None);
20561        assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
20562        assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
20563    }
20564
20565    /// sparse_ffn_quant must equal a dense FFN where inactive neurons are
20566    /// zeroed (mask × mmap correctness). On F32 tensors this is EXACT —
20567    /// it validates the row_dot / add_col_scaled / scatter indexing, the
20568    /// bug-prone part. The q8 branches reuse the golden-tested linear
20569    /// The per-token sparse path reads a transposed `down`; it must
20570    /// agree with the arm that computes everything and zeroes the
20571    /// losers, or the speed measurement is measuring a different model.
20572    #[test]
20573    fn dynamic_ffn_equals_the_zeroing_arm() {
20574        let (hidden, inter) = (8usize, 32usize);
20575        let synth = |n: usize, salt: usize| -> Vec<f32> {
20576            (0..n)
20577                .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
20578                .collect()
20579        };
20580        let down = synth(hidden * inter, 3);
20581        let mut down_t = vec![0.0f32; inter * hidden];
20582        for r in 0..hidden {
20583            for c in 0..inter {
20584                down_t[c * hidden + r] = down[r * inter + c];
20585            }
20586        }
20587        let d = DenseFfn {
20588            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
20589            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
20590            down_proj: QTensor::from_f32(down.clone(), hidden, inter),
20591            act: Act::Silu,
20592            down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
20593            segs: Vec::new(),
20594        };
20595        let x = synth(hidden, 11);
20596        let k = 12usize;
20597        let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
20598        // Reference: full compute, keep the k loudest |silu(gate)|.
20599        let mut g = vec![0.0f32; inter];
20600        d.gate_proj.matvec(&x, &mut g, None);
20601        let mut u = vec![0.0f32; inter];
20602        d.up_proj.matvec(&x, &mut u, None);
20603        for v in g.iter_mut() {
20604            *v = inference::silu(*v);
20605        }
20606        keep_top_k(&mut g, k);
20607        for i in 0..inter {
20608            g[i] *= u[i];
20609        }
20610        let mut want = vec![0.0f32; hidden];
20611        d.down_proj.matvec(&g, &mut want, None);
20612        for (a, b) in want.iter().zip(&got) {
20613            assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
20614        }
20615    }
20616
20617    /// A tube layer is the same layer, re-cut. With every tube open the
20618    /// answer must equal the dense FFN over the concatenated neurons
20619    /// (the permutation is an identity on the layer's function); with a
20620    /// tube closed it must equal the dense FFN with those neurons
20621    /// zeroed — the mask semantics, now paid for in bytes not read.
20622    #[test]
20623    fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
20624        let (hidden, core, tube) = (8usize, 12usize, 8usize);
20625        let inter = core + tube;
20626        let synth = |n: usize, salt: usize| -> Vec<f32> {
20627            (0..n)
20628                .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
20629                .collect()
20630        };
20631        let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
20632        let d_all = synth(hidden * inter, 3);
20633        // The dense layer, and the same weights cut into core + tube.
20634        let dense = DenseFfn {
20635            gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
20636            up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
20637            down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
20638            act: Act::Silu,
20639            down_t: None,
20640            segs: Vec::new(),
20641        };
20642        let rows =
20643            |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
20644        let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
20645            let mut o = Vec::with_capacity(hidden * (b - a));
20646            for r in 0..hidden {
20647                o.extend_from_slice(&v[r * inter + a..r * inter + b]);
20648            }
20649            o
20650        };
20651        let tubed = DenseFfn {
20652            down_t: None,
20653            gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
20654            up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
20655            down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
20656            act: Act::Silu,
20657            segs: vec![FfnSeg {
20658                gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
20659                up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
20660                down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
20661                start: core,
20662                width: tube,
20663            }],
20664        };
20665        let x = synth(hidden, 7);
20666        let want = dense_ffn(&dense, &x, None);
20667        let got = tube_ffn(&tubed, &x, 1, None, None);
20668        for (a, b) in want.iter().zip(&got) {
20669            assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
20670        }
20671        // Closed tube: bits on for the core, off for the tube.
20672        let mut bits = vec![0u8; inter.div_ceil(8)];
20673        for n in 0..core {
20674            bits[n / 8] |= 1 << (n % 8);
20675        }
20676        let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
20677        let masked = dense_ffn_masked(&dense, &x, None, &bits);
20678        for (a, b) in masked.iter().zip(&closed) {
20679            assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
20680        }
20681        // The batched arm must agree with the single-position one.
20682        let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
20683        for (a, b) in closed.iter().zip(&batch) {
20684            assert_eq!(a, b, "batch arm disagrees with decode arm");
20685        }
20686    }
20687
20688    /// scale, structurally identical to the matvec kernels.
20689    #[test]
20690    fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
20691        let (hidden, inter) = (16usize, 40usize);
20692        let synth = |n: usize, salt: usize| -> Vec<f32> {
20693            (0..n)
20694                .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
20695                .collect()
20696        };
20697        let d = DenseFfn {
20698            gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
20699            up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
20700            down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
20701            act: Act::Silu,
20702            down_t: None,
20703            segs: Vec::new(),
20704        };
20705        let x = synth(hidden, 9);
20706        // Active = every 3rd neuron.
20707        let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
20708
20709        let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
20710
20711        // Reference: full dense FFN but g[i]=0 for inactive neurons.
20712        let mut g = vec![0.0f32; inter];
20713        d.gate_proj.matvec(&x, &mut g, None);
20714        let mut u = vec![0.0f32; inter];
20715        d.up_proj.matvec(&x, &mut u, None);
20716        let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
20717        for i in 0..inter {
20718            g[i] = if act_set.contains(&(i as u16)) {
20719                inference::silu(g[i]) * u[i]
20720            } else {
20721                0.0
20722            };
20723        }
20724        let mut reference = vec![0.0f32; hidden];
20725        d.down_proj.matvec(&g, &mut reference, None);
20726
20727        let max_d = sparse
20728            .iter()
20729            .zip(&reference)
20730            .map(|(a, b)| (a - b).abs())
20731            .fold(0.0f32, f32::max);
20732        assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
20733    }
20734
20735    /// Attach a synthetic MTP head (same structure as a main layer).
20736    fn attach_test_mtp(p: &mut Pipeline) {
20737        let (h, inter, heads, kv, hd) = (
20738            p.hidden_size,
20739            p.intermediate_size,
20740            p.num_heads,
20741            p.num_kv_heads,
20742            p.head_dim,
20743        );
20744        let synth = |n: usize, salt: usize| -> Vec<f32> {
20745            (0..n)
20746                .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
20747                .collect()
20748        };
20749        let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
20750            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20751        };
20752        p.mtp = Some(MtpModule {
20753            enorm: vec![1.0; h],
20754            hnorm: vec![1.0; h],
20755            eh_proj: qt(h, 2 * h, 301),
20756            layer: LayerWeights {
20757                input_norm: vec![1.0; h],
20758                post_norm: vec![1.0; h],
20759                attn_out_norm: None,
20760                ffn_out_norm: None,
20761                layer_scale: None,
20762                ffn: FfnKind::Dense(DenseFfn {
20763                    gate_proj: qt(inter, h, 315),
20764                    up_proj: qt(inter, h, 316),
20765                    down_proj: qt(h, inter, 317),
20766                    act: Act::Silu,
20767                    down_t: None,
20768                    segs: Vec::new(),
20769                }),
20770                attn: AttnKind::Full {
20771                    bias: None,
20772                    wq: qt(heads * hd, h, 311),
20773                    wk: qt(kv * hd, h, 312),
20774                    wv: qt(kv * hd, h, 313),
20775                    wo: qt(h, heads * hd, 314),
20776                    q_norm: None,
20777                    k_norm: None,
20778                    output_gate: false,
20779                    softplus_gate: None,
20780                },
20781            },
20782            final_norm: vec![1.0; h],
20783            kv: crate::kv_cache::LayerKvCache::new(kv, hd),
20784        });
20785    }
20786
20787    #[test]
20788    fn speculative_equals_vanilla_greedy() {
20789        // Speculative decode and the wgpu token graph are mutually
20790        // exclusive; a leaked CMF_GPU=wgpu from a parallel gpu test
20791        // would silently disable drafting. Pin the graph off.
20792        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20793        let run = |spec: bool| {
20794            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20795            p.sampler_config.temperature = 0.0;
20796            attach_test_mtp(&mut p);
20797            p.speculative = spec;
20798            let r = p.generate("abcdef", 12, None, None).unwrap();
20799            (r.token_ids, r.mtp_drafted, r.mtp_accepted)
20800        };
20801        let (vanilla, d0, _) = run(false);
20802        let (spec, d1, a1) = run(true);
20803        assert_eq!(d0, 0, "vanilla path must not draft");
20804        assert!(d1 > 0, "speculative path must draft");
20805        assert_eq!(
20806            vanilla, spec,
20807            "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
20808        );
20809    }
20810
20811    #[test]
20812    fn speculative_accepts_constant_oracle() {
20813        // See speculative_equals_vanilla_greedy: pin the wgpu graph off.
20814        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20815        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20816        p.sampler_config.temperature = 0.0;
20817        p.sampler_config.repetition_penalty = 1.0;
20818        // Constant lm_head → every logit equal → both the main model and
20819        // the draft head argmax to token 0: acceptance must be 100%.
20820        p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
20821        attach_test_mtp(&mut p);
20822        p.speculative = true;
20823        let r = p.generate("abcd", 10, None, None).unwrap();
20824        assert!(r.mtp_drafted > 0);
20825        assert_eq!(
20826            r.mtp_accepted, r.mtp_drafted,
20827            "constant logits → every draft accepted"
20828        );
20829        // Ties resolve to the same token in both the main and draft
20830        // heads — the sequence is one repeated token.
20831        assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
20832    }
20833
20834    #[test]
20835    fn empty_prompt_is_an_error_not_a_panic() {
20836        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20837        let r = p.generate("", 4, None, None);
20838        assert!(r.is_err(), "empty prompt must be a clean error");
20839    }
20840
20841    #[test]
20842    fn every_token_enters_kv_exactly_once() {
20843        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20844        // Greedy so no RNG variance; byte tokenizer → 3 prompt tokens.
20845        p.sampler_config.temperature = 0.0;
20846        let r = p.generate("abc", 2, None, None).unwrap();
20847        assert_eq!(r.prompt_tokens, 3);
20848        // prompt(3) + first sampled token forwarded before second logits:
20849        // step0 samples from prefill hidden (no extra forward), then
20850        // forwards t1 → cache 4; step1 samples, loop ends (max_tokens).
20851        assert_eq!(
20852            p.kv_cache.seq_len(),
20853            3 + r.tokens_generated - 1,
20854            "each token must be cached exactly once (v1 cached the last prompt token twice)"
20855        );
20856    }
20857
20858    #[test]
20859    fn generation_is_reproducible_with_seed() {
20860        let run = || {
20861            let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20862            p.generate("hello", 8, None, None).unwrap().token_ids
20863        };
20864        assert_eq!(run(), run());
20865    }
20866
20867    #[test]
20868    fn resetting_sampler_restarts_the_seeded_stream() {
20869        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20870        let config = SamplerConfig {
20871            seed: Some(1234),
20872            ..SamplerConfig::default()
20873        };
20874        p.set_sampler_config(config.clone());
20875        let first = p.generate("hello", 8, None, None).unwrap().token_ids;
20876        p.set_sampler_config(config);
20877        let second = p.generate("hello", 8, None, None).unwrap().token_ids;
20878        assert_eq!(first, second);
20879    }
20880
20881    #[test]
20882    fn eviction_bounds_the_cache() {
20883        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20884        p.kv_cache.max_seq_len = 6;
20885        p.sampler_config.temperature = 0.0;
20886        let _ = p.generate("abcd", 12, None, None).unwrap();
20887        assert!(
20888            p.kv_cache.seq_len() <= 6 + 1,
20889            "cache must stay bounded by max_seq_len (got {})",
20890            p.kv_cache.seq_len()
20891        );
20892    }
20893
20894    #[test]
20895    fn confidence_matches_tokens_and_is_a_probability() {
20896        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20897        p.sampler_config.temperature = 0.0;
20898        p.sampler_config.repetition_penalty = 1.0;
20899        let r = p.generate("abcd", 10, None, None).unwrap();
20900        assert_eq!(
20901            r.token_confidence.len(),
20902            r.token_ids.len(),
20903            "one confidence per emitted token"
20904        );
20905        for &c in &r.token_confidence {
20906            assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
20907        }
20908        // top1_prob is a valid softmax probability.
20909        let logits = [1.0f32, 3.0, 0.5, 3.0];
20910        let p0 = top1_prob_t(&logits, 1, 1.0);
20911        let p1 = top1_prob_t(&logits, 3, 1.0);
20912        assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
20913        assert!(p0 > 0.0 && p0 < 1.0);
20914        // Calibration temperature > 1 softens an over-confident peak.
20915        let sharp = top1_prob_t(&logits, 1, 1.0);
20916        let soft = top1_prob_t(&logits, 1, 2.0);
20917        assert!(soft < sharp, "higher temperature lowers peak confidence");
20918    }
20919
20920    #[test]
20921    fn trace_is_opt_in_and_parallels_the_output() {
20922        // Off by default: the runtime is silent unless observation asked.
20923        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20924        p.sampler_config.temperature = 0.0;
20925        p.sampler_config.repetition_penalty = 1.0;
20926        let r = p.generate("abcd", 10, None, None).unwrap();
20927        assert!(r.traces.is_empty(), "trace must be empty unless enabled");
20928
20929        // On: exactly one row per emitted token, aligned with the output.
20930        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20931        p.sampler_config.temperature = 0.0;
20932        p.sampler_config.repetition_penalty = 1.0;
20933        p.set_trace(true);
20934        let r = p.generate("abcd", 10, None, None).unwrap();
20935        assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
20936        for (i, tr) in r.traces.iter().enumerate() {
20937            assert_eq!(tr.t, i, "trace index is sequential");
20938            assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
20939            assert_eq!(
20940                tr.confidence, r.token_confidence[i],
20941                "trace confidence matches the confidence channel"
20942            );
20943            // No dynamic router in this pipeline → no skill, no coherence.
20944            assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
20945        }
20946    }
20947
20948    #[test]
20949    fn explain_prefill_logits_match_greedy_first_token() {
20950        // `cortiq explain` shows the next-token distribution from
20951        // prefill_next_logits; its argmax must equal what greedy generate
20952        // actually emits first — otherwise explain would lie.
20953        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20954        p.sampler_config.temperature = 0.0;
20955        p.sampler_config.repetition_penalty = 1.0;
20956        let ids = p.tokenizer.encode("abcd");
20957        let logits = p.prefill_next_logits(&ids, None);
20958        let argmax = logits
20959            .iter()
20960            .enumerate()
20961            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
20962            .unwrap()
20963            .0 as u32;
20964        let r = p.generate("abcd", 1, None, None).unwrap();
20965        assert_eq!(
20966            argmax, r.token_ids[0],
20967            "explain preview must match greedy emit"
20968        );
20969    }
20970
20971    #[test]
20972    fn laguna_shared_expert_is_unconditionally_added() {
20973        let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
20974        let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
20975        let zero_dense = || DenseFfn {
20976            gate_proj: matrix(vec![0.0; 4]),
20977            up_proj: matrix(vec![0.0; 4]),
20978            down_proj: matrix(vec![0.0; 4]),
20979            act: Act::Silu,
20980            down_t: None,
20981            segs: Vec::new(),
20982        };
20983        let shared = DenseFfn {
20984            gate_proj: identity(),
20985            up_proj: identity(),
20986            down_proj: identity(),
20987            act: Act::Silu,
20988            down_t: None,
20989            segs: Vec::new(),
20990        };
20991        let x = [1.0, 2.0];
20992        let expected = dense_ffn(&shared, &x, None);
20993        let moe = MoeFfn {
20994            router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
20995            experts: vec![zero_dense()],
20996            top_k: 1,
20997            norm_topk_prob: true,
20998            router_sigmoid: true,
20999            sigmoid_denom_eps: 1e-6,
21000            expert_bias: None,
21001            routed_scaling: 1.0,
21002            route_tau: None,
21003            shared: Some((shared, None)),
21004            stats: std::cell::RefCell::new(Vec::new()),
21005            act_sq: std::cell::RefCell::new(Vec::new()),
21006            act_rows: std::cell::RefCell::new(Vec::new()),
21007            mask: None,
21008            per_expert_scale: None,
21009            router_input_norm: false,
21010            resonance: None,
21011            grown: Vec::new(),
21012        };
21013        let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
21014        for (actual, expected) in actual.iter().zip(expected) {
21015            assert!((actual - expected).abs() < 1e-6);
21016        }
21017    }
21018
21019    /// A tiny MiMo-V2-shaped stack (the M3 fixture): layers [full, sliding,
21020    /// sliding, full]; 4 Q heads over 1 (full) / 2 (sliding) KV heads;
21021    /// head_dim 8 with 4-wide V heads; partial rotary 4 at θ 1e7 (full) /
21022    /// 1e4 (sliding); window 3; learned sinks on the sliding layers; layer
21023    /// 0 a dense FFN, layers 1..3 sigmoid-routed MoE with a selection bias
21024    /// (4 experts, top-2, renormalized, no shared expert). Geometry and
21025    /// sinks go through the same `set_attn_geometry` / `set_layer_sinks`
21026    /// the loader calls.
21027    fn mimo_test_pipeline() -> Pipeline {
21028        let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
21029        let kvh = [1usize, 2, 2, 1];
21030        let synth = |n: usize, salt: usize| -> Vec<f32> {
21031            (0..n)
21032                .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
21033                .collect()
21034        };
21035        let qt = |rows: usize, cols: usize, salt: usize| {
21036            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21037        };
21038        let dense = |inter: usize, salt: usize| DenseFfn {
21039            gate_proj: qt(inter, hs, salt),
21040            up_proj: qt(inter, hs, salt + 1),
21041            down_proj: qt(hs, inter, salt + 2),
21042            act: Act::Silu,
21043            down_t: None,
21044            segs: Vec::new(),
21045        };
21046        let layers: Vec<LayerWeights> = (0..4)
21047            .map(|li| LayerWeights {
21048                input_norm: vec![1.0; hs],
21049                post_norm: vec![1.0; hs],
21050                attn_out_norm: None,
21051                ffn_out_norm: None,
21052                layer_scale: None,
21053                ffn: if li == 0 {
21054                    FfnKind::Dense(dense(inter, 50))
21055                } else {
21056                    FfnKind::Moe(MoeFfn {
21057                        router: qt(4, hs, 60 + li),
21058                        experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
21059                        top_k: 2,
21060                        norm_topk_prob: true,
21061                        router_sigmoid: true,
21062                        sigmoid_denom_eps: 1e-6,
21063                        expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
21064                        routed_scaling: 1.0,
21065                        route_tau: None,
21066                        shared: None,
21067                        stats: std::cell::RefCell::new(Vec::new()),
21068                        act_sq: std::cell::RefCell::new(Vec::new()),
21069                        act_rows: std::cell::RefCell::new(Vec::new()),
21070                        mask: None,
21071                        per_expert_scale: None,
21072                        router_input_norm: false,
21073                        resonance: None,
21074                        grown: Vec::new(),
21075                    })
21076                },
21077                attn: AttnKind::Full {
21078                    wq: qt(nh * hd, hs, li * 10 + 1),
21079                    wk: qt(kvh[li] * hd, hs, li * 10 + 2),
21080                    wv: qt(kvh[li] * vd, hs, li * 10 + 3),
21081                    wo: qt(hs, nh * vd, li * 10 + 4),
21082                    q_norm: None,
21083                    k_norm: None,
21084                    output_gate: false,
21085                    softplus_gate: None,
21086                    bias: None,
21087                },
21088            })
21089            .collect();
21090        let mut p = Pipeline::new(
21091            Tokenizer::byte_level(),
21092            PipelineWeights {
21093                embed_tokens: qt(vocab, hs, 100),
21094                layers,
21095                lm_head: qt(vocab, hs, 200),
21096                final_norm: vec![1.0; hs],
21097            },
21098            hs,
21099            inter,
21100            nh,
21101            1, // header num_kv_heads (the full layers')
21102            hd,
21103            4,
21104            4,
21105            false,
21106            vocab,
21107            1e-6,
21108            1e7,
21109            NormStyle::Qwen,
21110            4096,
21111            SamplerConfig {
21112                seed: Some(7),
21113                ..Default::default()
21114            },
21115        );
21116        // Diagnostics stay off whatever the test environment exports.
21117        p.layer_dump = None;
21118        p.set_rotary(4, 1e7);
21119        p.sliding_layers = Some(vec![false, true, true, false]);
21120        p.swa = Some((3, usize::MAX));
21121        p.rotary_dim_local = Some(4);
21122        p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
21123        p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
21124        p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
21125        p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
21126        p
21127    }
21128
21129    #[test]
21130    fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
21131        let mut p = mimo_test_pipeline();
21132        p.speculative = false;
21133        p.ignore_eos = true;
21134        p.sampler_config.temperature = 0.0;
21135        p.sampler_config.repetition_penalty = 1.0;
21136        let a = vec![3, 5, 7, 9, 11, 13];
21137        let b = vec![4, 8, 12, 16, 20, 24];
21138        let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
21139        let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
21140        // Same placeholder IDs as an earlier request are not a cache key
21141        // for different media. The actual rows, not a re-embedding of a,
21142        // must determine the continuation.
21143        let actual = p
21144            .generate_from_embeds(&a, &rows, 8, None, None)
21145            .unwrap()
21146            .token_ids;
21147        assert_eq!(actual, expected);
21148        assert!(p.kv_history.is_empty());
21149        let mut extended = a.clone();
21150        extended.push(17);
21151        let after_media = p
21152            .generate_from_ids(&extended, 8, None, None)
21153            .unwrap()
21154            .token_ids;
21155        p.reset_session();
21156        let fresh = p
21157            .generate_from_ids(&extended, 8, None, None)
21158            .unwrap()
21159            .token_ids;
21160        assert_eq!(after_media, fresh);
21161        assert!(
21162            p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
21163                .is_err()
21164        );
21165        // Force a real token-prefix reuse opportunity into the media call.
21166        // Those labels are unchanged, but their embeddings now describe a
21167        // different source sequence and every KV row must be rebuilt.
21168        p.reset_session();
21169        p.generate_from_ids(&a, 1, None, None).unwrap();
21170        let mut media_ids = p.kv_history.clone();
21171        assert!(!media_ids.is_empty());
21172        media_ids.extend_from_slice(&[19, 21, 23]);
21173        let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
21174        let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
21175        let mut oracle = mimo_test_pipeline();
21176        oracle.speculative = false;
21177        oracle.ignore_eos = true;
21178        oracle.sampler_config.temperature = 0.0;
21179        oracle.sampler_config.repetition_penalty = 1.0;
21180        let expected = oracle
21181            .generate_from_ids(&source_ids, 8, None, None)
21182            .unwrap()
21183            .token_ids;
21184        assert_eq!(
21185            p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
21186                .unwrap()
21187                .token_ids,
21188            expected
21189        );
21190        assert!(p.kv_history.is_empty());
21191        let mut bad = rows;
21192        bad[0] = f32::NAN;
21193        assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
21194    }
21195
21196    fn f32_bits(v: &[f32]) -> Vec<u32> {
21197        v.iter().map(|x| x.to_bits()).collect()
21198    }
21199
21200    /// M3 acceptance: on the MiMo-shaped stack the decode walk (one
21201    /// position at a time through `forward_layers`) and the batched
21202    /// prefill (`prefill_batch_span`, whole prompt and split in two
21203    /// chunks) give bit-identical logits at all 12 positions — per-layer
21204    /// KV heads, narrow V, sinks, the window and the biased sigmoid MoE all
21205    /// agree across the two walks.
21206    #[test]
21207    fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
21208        let mut p = mimo_test_pipeline();
21209        let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
21210        assert_eq!(kv, vec![1, 2, 2, 1]);
21211        assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
21212        assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
21213        let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
21214        let hs = p.hidden_size;
21215        let mut decode = Vec::new();
21216        for (pos, &id) in ids.iter().enumerate() {
21217            let e = p.embed_single(id);
21218            let h = p.forward_layers(&e, pos, None);
21219            decode.push(p.logits_from_hidden(&h));
21220        }
21221        for l in &p.kv_cache.layers {
21222            assert_eq!(l.seq_len, 12);
21223            // V rows are padded to head_dim inside the cache.
21224            assert_eq!(l.head_values(0).len(), 12 * 8);
21225        }
21226        assert!(decode.iter().flatten().all(|v| v.is_finite()));
21227
21228        p.clear_sequence_state();
21229        let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21230        for pos in 0..ids.len() {
21231            let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
21232            assert_eq!(
21233                f32_bits(&decode[pos]),
21234                f32_bits(&lg),
21235                "whole prompt, pos {pos}"
21236            );
21237        }
21238
21239        p.clear_sequence_state();
21240        let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
21241        let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
21242        for pos in 0..ids.len() {
21243            let row = if pos < 5 {
21244                &a[pos * hs..(pos + 1) * hs]
21245            } else {
21246                &b[(pos - 5) * hs..(pos - 4) * hs]
21247            };
21248            let lg = p.logits_from_hidden(row);
21249            assert_eq!(
21250                f32_bits(&decode[pos]),
21251                f32_bits(&lg),
21252                "two chunks, pos {pos}"
21253            );
21254        }
21255
21256        // The fixture is not degenerate: the sinks and the window each
21257        // change the answer.
21258        let last = |p: &mut Pipeline| {
21259            p.clear_sequence_state();
21260            let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21261            p.logits_from_hidden(&hb[11 * hs..12 * hs])
21262        };
21263        let base = last(&mut p);
21264        let mut no_sinks = mimo_test_pipeline();
21265        for l in &mut no_sinks.kv_cache.layers {
21266            l.sinks = None;
21267        }
21268        assert_ne!(
21269            f32_bits(&last(&mut no_sinks)),
21270            f32_bits(&base),
21271            "sinks are live"
21272        );
21273        let mut wide = mimo_test_pipeline();
21274        wide.swa = Some((64, usize::MAX));
21275        assert_ne!(
21276            f32_bits(&last(&mut wide)),
21277            f32_bits(&base),
21278            "window is live"
21279        );
21280
21281        // Generation runs end to end on the same stack.
21282        p.clear_sequence_state();
21283        p.ignore_eos = true;
21284        let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
21285        assert_eq!(r.token_ids.len(), 4);
21286    }
21287
21288    /// A Spark-X2.5-shaped stack: layers 0..3 slide (window 6), layer 3
21289    /// is global, every attention carries the head-wise sigmoid g_proj gate.
21290    fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
21291        let (hs, nh) = (16usize, 4usize);
21292        let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
21293        p.layer_dump = None;
21294        p.swa = Some((6, 4));
21295        p.proj_gate_sigmoid = true;
21296        for (li, lw) in p.weights.layers.iter_mut().enumerate() {
21297            if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
21298                let g: Vec<f32> = (0..nh * hs)
21299                    .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
21300                    .collect();
21301                *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
21302            }
21303        }
21304        p.swa_trim = trim;
21305        p
21306    }
21307
21308    /// Trimming the sliding tails changes nothing but memory: the same
21309    /// prompt in uneven chunks, a decode walk, and fused pairs with a
21310    /// 2-row rollback give bit-identical hiddens with and without it,
21311    /// while the trimmed sliding layers stay bounded and the global layer
21312    /// keeps every row.
21313    #[test]
21314    fn swa_trim_pipeline_matches_untrimmed_bitwise() {
21315        let mut a = spark_test_pipeline(None);
21316        let mut b = spark_test_pipeline(Some((2, 4)));
21317        assert_eq!(
21318            (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
21319            vec![Some(6), Some(6), Some(6), None]
21320        );
21321        let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
21322        let mut pos = 0usize;
21323        for &n in [5usize, 13, 7, 16].iter().cycle() {
21324            if pos >= ids.len() {
21325                break;
21326            }
21327            let end = (pos + n).min(ids.len());
21328            let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
21329            let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
21330            assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
21331            pos = end;
21332        }
21333        for _ in 0..23 {
21334            let e = a.embed_single(((pos * 7) % 64) as u32);
21335            let ha = a.forward_layers(&e, pos, None);
21336            let hb = b.forward_layers(&e, pos, None);
21337            assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
21338            for li in 0..3 {
21339                assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
21340            }
21341            pos += 1;
21342        }
21343        for round in 0..9 {
21344            let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
21345            let (a1, a2) = a.forward_pair(&e1, &e2, pos);
21346            let (b1, b2) = b.forward_pair(&e1, &e2, pos);
21347            assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
21348            assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
21349            if round % 2 == 1 {
21350                // A rejected draft: both lanes roll back.
21351                for p in [&mut a, &mut b] {
21352                    for l in &mut p.kv_cache.layers {
21353                        l.truncate_last(2);
21354                    }
21355                }
21356            } else {
21357                pos += 2;
21358            }
21359        }
21360        let e = a.embed_single(9);
21361        let (ha, hb) = (
21362            a.forward_layers(&e, pos, None),
21363            b.forward_layers(&e, pos, None),
21364        );
21365        assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
21366        for li in 0..3 {
21367            let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
21368            assert!(lb.base() > 0, "layer {li} trimmed");
21369            assert_eq!(la.base(), 0);
21370            assert_eq!(lb.pos_len(), la.seq_len);
21371            assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
21372        }
21373        let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
21374        assert_eq!(
21375            (gb.base(), gb.seq_len),
21376            (0, ga.seq_len),
21377            "the global layer keeps all"
21378        );
21379        assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
21380        assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
21381    }
21382
21383    /// The network split's KvFetch hand-off on a trimmed stack: every
21384    /// layer shipped over the wire mid-sequence (the sliding ones as
21385    /// `FullTail`) into a fresh pipeline that never ran a position, which
21386    /// then decodes on — bit for bit what the untrimmed pipeline decodes.
21387    #[test]
21388    fn swa_trim_wire_handoff_continues_bitwise() {
21389        let mut a = spark_test_pipeline(None);
21390        let mut b = spark_test_pipeline(Some((2, 4)));
21391        let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
21392        let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
21393        let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
21394        assert_eq!(f32_bits(&ha), f32_bits(&hb));
21395        let mut pos = ids.len();
21396        for _ in 0..5 {
21397            let e = a.embed_single(((pos * 5) % 64) as u32);
21398            assert_eq!(
21399                f32_bits(&a.forward_layers(&e, pos, None)),
21400                f32_bits(&b.forward_layers(&e, pos, None))
21401            );
21402            pos += 1;
21403        }
21404        let mut c = spark_test_pipeline(Some((2, 4)));
21405        for li in 0..4 {
21406            let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
21407            c.kv_cache.layers[li].import_wire(&bytes).unwrap();
21408        }
21409        assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
21410        assert_eq!(c.kv_cache.seq_len(), pos);
21411        for step in 0..17 {
21412            let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
21413            let ha = a.forward_layers(&e, pos, None);
21414            let hc = c.forward_layers(&e, pos, None);
21415            assert_eq!(
21416                f32_bits(&ha),
21417                f32_bits(&hc),
21418                "after the hand-off, step {step}"
21419            );
21420            pos += 1;
21421        }
21422        assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
21423    }
21424
21425    /// A synthetic MiMo draft stack of `n` layers for `mimo_test_pipeline`
21426    /// (the SWA geometry of its sliding layers: 2 KV heads, head 8 / V 4).
21427    fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
21428        let (hs, inter, nh, hd, vd, nkv) = (16usize, 24usize, 4usize, 8usize, 4usize, 2usize);
21429        let synth = |len: usize, salt: usize| -> Vec<f32> {
21430            (0..len)
21431                .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
21432                .collect()
21433        };
21434        let qt = |rows: usize, cols: usize, salt: usize| {
21435            QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21436        };
21437        let layers = (0..n)
21438            .map(|k| {
21439                let s = 500 + k * 40;
21440                let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
21441                kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
21442                MtpModule {
21443                    enorm: vec![1.0; hs],
21444                    hnorm: vec![1.0; hs],
21445                    eh_proj: qt(hs, 2 * hs, s),
21446                    layer: LayerWeights {
21447                        input_norm: vec![1.0; hs],
21448                        post_norm: vec![1.0; hs],
21449                        attn_out_norm: None,
21450                        ffn_out_norm: None,
21451                        layer_scale: None,
21452                        attn: AttnKind::Full {
21453                            wq: qt(nh * hd, hs, s + 1),
21454                            wk: qt(nkv * hd, hs, s + 2),
21455                            wv: qt(nkv * vd, hs, s + 3),
21456                            wo: qt(hs, nh * vd, s + 4),
21457                            q_norm: None,
21458                            k_norm: None,
21459                            output_gate: false,
21460                            softplus_gate: None,
21461                            bias: None,
21462                        },
21463                        ffn: FfnKind::Dense(DenseFfn {
21464                            gate_proj: qt(inter, hs, s + 5),
21465                            up_proj: qt(inter, hs, s + 6),
21466                            down_proj: qt(hs, inter, s + 7),
21467                            act: Act::Silu,
21468                            down_t: None,
21469                            segs: Vec::new(),
21470                        }),
21471                    },
21472                    final_norm: vec![1.0; hs],
21473                    kv,
21474                }
21475            })
21476            .collect();
21477        mimo_mtp::MimoMtp::from_layers(layers)
21478    }
21479
21480    fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
21481        p.clear_sequence_state();
21482        p.speculative = spec;
21483        p.ignore_eos = true;
21484        p.sampler_config.temperature = 0.0;
21485        p.generate_from_ids(ids, n, None, None).unwrap()
21486    }
21487
21488    /// The draft stack's incremental rounds (a few rows per layer, last
21489    /// round's provisional rows dropped) give exactly the teacher-forced
21490    /// table of one causal pass per layer over the whole sequence — the
21491    /// table `tools/mimo_ref.py mtp` computes for variant A: layer k, row
21492    /// j reads (x[j+k+1], norm(h_j)) at RoPE position j.
21493    #[test]
21494    fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
21495        // Both readings of the backbone hidden: pre-final-norm (default)
21496        // and post-final-norm (`CMF_MIMO_MTP_HIDDEN=post`).
21497        for post in [false, true] {
21498            let mut p = mimo_test_pipeline();
21499            // A non-trivial final norm, so the two readings differ.
21500            p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
21501            let mut st0 = mimo_test_mtp(3, 1.0);
21502            st0.post_norm_hidden = post;
21503            p.mimo_mtp = Some(st0);
21504            let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
21505            let hs = p.hidden_size;
21506            let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21507            p.mimo_note_rows(&hb, 0);
21508            let mut st = p.mimo_mtp.take().unwrap();
21509            // Incremental: one round per t through the decode path (later
21510            // tokens from `ids`, the probe's teacher forcing).
21511            let k = 3;
21512            let mut inc = Vec::new();
21513            for t in 0..ids.len() - k - 1 {
21514                inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
21515            }
21516            // Reference: per layer, ONE batched causal pass over all rows
21517            // with fresh caches.
21518            let s = ids.len();
21519            let mut reference = vec![vec![0u32; k]; s - k - 1];
21520            let mut fresh = mimo_test_mtp(3, 1.0);
21521            for (layer, m) in fresh.layers.iter_mut().enumerate() {
21522                let n = s - layer - 1;
21523                let mut cats = vec![0.0f32; n * 2 * hs];
21524                for j in 0..n {
21525                    let e = p.embed_single(ids[j + layer + 1]);
21526                    let raw = &hb[j * hs..(j + 1) * hs];
21527                    let g = if post {
21528                        inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
21529                    } else {
21530                        raw.to_vec()
21531                    };
21532                    let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
21533                    inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
21534                    inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
21535                }
21536                let mut x = vec![0.0f32; n * hs];
21537                m.eh_proj.matmat(&cats, n, &mut x, None);
21538                p.mimo_mtp_block(m, &mut x, n, 0);
21539                for (t, row) in reference.iter_mut().enumerate() {
21540                    let y = inference::rms_norm(
21541                        &x[t * hs..(t + 1) * hs],
21542                        &m.final_norm,
21543                        p.rms_eps,
21544                        p.norm_style,
21545                    );
21546                    row[layer] = sampler::argmax(&p.lm_head_forward(&y));
21547                }
21548            }
21549            assert_eq!(inc, reference, "post_norm_hidden = {post}");
21550            // Not a degenerate table: the drafts vary.
21551            let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
21552            assert!(distinct.len() > 3, "{inc:?}");
21553            // Each layer's cache ends holding rows up to the last round start.
21554            let last_t = ids.len() - k - 2;
21555            for m in &st.layers {
21556                assert_eq!(m.kv.seq_len, last_t + 1);
21557            }
21558        }
21559    }
21560
21561    /// Greedy with the MiMo draft stack is the plain greedy stream, token
21562    /// for token — with the real draft layers (low acceptance) and with a
21563    /// drafter that is right most of the time (exercises accepted prefixes
21564    /// of every length, the KV truncation of the rejected rows and the
21565    /// logits hand-off to the loop top), under the default repetition
21566    /// penalty.
21567    #[test]
21568    fn mimo_speculative_greedy_equals_plain_greedy() {
21569        unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
21570        let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
21571        let n = 24;
21572        let mut p = mimo_test_pipeline();
21573        let plain = mimo_greedy(&mut p, &ids, n, false);
21574        assert_eq!(plain.mtp_drafted, 0);
21575        assert_eq!(plain.token_ids.len(), n);
21576        let plain_kv = p.kv_cache.layers[0].seq_len;
21577
21578        // Real draft layers.
21579        p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
21580        let spec = mimo_greedy(&mut p, &ids, n, true);
21581        assert!(spec.mtp_drafted > 0, "the round must draft");
21582        assert_eq!(spec.token_ids, plain.token_ids);
21583        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21584
21585        // A drafter reading the true continuation with every fifth token
21586        // wrong: accepted prefixes of 0..=3 all occur.
21587        let mut truth: Vec<u32> = ids.clone();
21588        truth.extend(&plain.token_ids);
21589        let mut noisy = truth.clone();
21590        for (i, t) in noisy.iter_mut().enumerate() {
21591            if i % 5 == 0 {
21592                *t = (*t + 1) % 64;
21593            }
21594        }
21595        let mut st = mimo_test_mtp(3, 1.0);
21596        st.draft_override = Some(noisy);
21597        p.mimo_mtp = Some(st);
21598        let spec = mimo_greedy(&mut p, &ids, n, true);
21599        assert_eq!(spec.token_ids, plain.token_ids);
21600        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21601        let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
21602        assert_eq!(stats.accepted as usize, spec.mtp_accepted);
21603        assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
21604        assert!(
21605            stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
21606            "{:?}",
21607            stats.accept_hist
21608        );
21609        assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
21610
21611        // A perfect drafter: every draft accepted, rounds of K+1 tokens,
21612        // and the budget is never overrun.
21613        let mut st = mimo_test_mtp(3, 1.0);
21614        st.draft_override = Some(truth);
21615        p.mimo_mtp = Some(st);
21616        let spec = mimo_greedy(&mut p, &ids, n, true);
21617        assert_eq!(spec.token_ids, plain.token_ids);
21618        assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
21619        assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21620
21621        // CMF_MTP=0 path: the stack is attached but idle.
21622        let off = mimo_greedy(&mut p, &ids, n, false);
21623        assert_eq!(off.token_ids, plain.token_ids);
21624        assert_eq!(off.mtp_drafted, 0);
21625    }
21626
21627    /// The wgpu graphs carry MiMo-V2's attention per layer (KV heads,
21628    /// narrow V, sinks, windows, two RoPE tables): no attention-level
21629    /// decline for it any more, and the geometry each layer hands the
21630    /// graph is exactly what the CPU attention reads for that layer. The
21631    /// descriptive reasons stay (the Metal graphs and the q1 dropin still
21632    /// decline on them), and what the per-layer geometry cannot express
21633    /// keeps a named wgpu decline.
21634
21635    #[test]
21636    fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
21637        let p = mimo_test_pipeline();
21638        assert_eq!(
21639            p.graph_attn_decline_reason(),
21640            Some("per-layer KV head counts")
21641        );
21642        assert_eq!(p.wgpu_graph_attn_decline(), None);
21643        let g0 = p.graph_attn_geom(0).expect("full layer geometry");
21644        assert_eq!(
21645            (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
21646            (1, 4, 4, None, false)
21647        );
21648        assert_eq!(g0.invf, p.inv_freq.as_slice());
21649        let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
21650        assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
21651        assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
21652        assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
21653        assert_ne!(g0.invf, g1.invf, "two RoPE tables");
21654        let g3 = p.graph_attn_geom(3).expect("full layer geometry");
21655        assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
21656
21657        // No wgpu device in this process: the builders run and decline on
21658        // the (f32, unmapped) experts — never with an attention line.
21659        let emb = p.embed_single(3);
21660        let mut lg = Vec::new();
21661        assert!(
21662            p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
21663                .is_none()
21664        );
21665        let mut hid = emb.clone();
21666        assert_eq!(
21667            p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
21668            crate::gpu::BatchGraphOutcome::Declined
21669        );
21670        assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
21671        assert!(p.try_multi_burst(3, 0, 4).is_none());
21672        assert!(
21673            p.graph_declines().is_empty(),
21674            "no attention decline logged: {:?}",
21675            p.graph_declines()
21676        );
21677        // (No assertion on graph_prefill_preferred: with no attention
21678        // decline it follows the device — a test process that brought a
21679        // wgpu adapter up routes this resident MoE through the graph.)
21680
21681        let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
21682        assert_eq!(plain().graph_attn_decline_reason(), None);
21683        assert_eq!(plain().wgpu_graph_attn_decline(), None);
21684        assert!(
21685            plain().graph_attn_geom(0).is_none(),
21686            "uniform models keep the historical arms"
21687        );
21688        let mut q = plain();
21689        q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
21690        assert_eq!(
21691            q.graph_attn_decline_reason(),
21692            Some("learned attention sinks")
21693        );
21694        assert_eq!(
21695            q.graph_attn_geom(1).unwrap().sink,
21696            Some(&[0.25f32, -0.25][..])
21697        );
21698        let mut q = plain();
21699        q.set_attn_geometry(None, Some(2)).unwrap();
21700        assert_eq!(
21701            q.graph_attn_decline_reason(),
21702            Some("V heads narrower than Q/K heads")
21703        );
21704        assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
21705        let mut q = plain();
21706        q.sliding_layers = Some(vec![true, false]);
21707        q.swa = Some((4, usize::MAX));
21708        assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
21709        assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
21710        assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
21711
21712        // YaRN's post-rotation amplitude factor rides the per-layer graph
21713        // geometry. It is intentionally not an angular scale folded into
21714        // `invf`: the WGPU RoPE kernel receives it separately.
21715        let mut q = mimo_test_pipeline();
21716        q.rope_scale = 2.0;
21717        q.rope_scale_local = 1.25;
21718        assert_eq!(q.wgpu_graph_attn_decline(), None);
21719        assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
21720        assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
21721
21722        // A uniform YaRN model cannot yet pass the factor through the fused
21723        // RKV arm; decline safely instead of producing scale-1 output.
21724        let mut q = plain();
21725        q.rope_scale = 2.0;
21726        assert_eq!(
21727            q.wgpu_graph_attn_decline(),
21728            Some("scaled RoPE without per-layer geometry")
21729        );
21730    }
21731
21732    /// The native q1 attention path has a narrower contract than the WGPU
21733    /// per-layer geometry graph, but it must not discard the exact Flint
21734    /// combination of uniform GQA + sliding windows + learned sinks.  The
21735    /// actual Metal kernel is covered independently with a direct sink
21736    /// softmax test; this is the pure admission test, with no model run.
21737    #[cfg(target_os = "macos")]
21738    #[test]
21739    fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
21740        // rd=64 is in the native rope kernel's lane-pair contract; the
21741        // synthetic stack otherwise has the same uniform K/V geometry that
21742        // Flint needs for q1 (unlike the deliberately ragged MiMo fixture).
21743        let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
21744        p.swa = Some((16, usize::MAX));
21745        p.sliding_layers = Some(vec![true, true]);
21746        p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
21747        p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
21748
21749        assert_eq!(
21750            p.graph_attn_decline_reason(),
21751            Some("learned attention sinks")
21752        );
21753        let swa_graph = p.metal_graph_swa();
21754        assert!(swa_graph, "uniform sliding sink geometry must reach q1");
21755        let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
21756            && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
21757        assert!(
21758            native_sink_attend,
21759            "the q1 sink admission must follow the SWA door"
21760        );
21761        // The batched chunk softmax has no value-less sink column, so its
21762        // separate admission gate must keep this prompt on the host path.
21763        assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
21764    }
21765
21766    #[test]
21767    fn mimo_verify_rewind_preserves_lagging_host_caches() {
21768        let mut p = mimo_test_pipeline();
21769        for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
21770            let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
21771            for _ in 0..if li == 0 { 2 } else { 12 } {
21772                layer.append(&row, &row, &[]);
21773            }
21774        }
21775        p.mimo_verify_rewind(9).unwrap();
21776        assert_eq!(p.kv_cache.layers[0].seq_len, 2);
21777        for layer in &p.kv_cache.layers[1..] {
21778            assert_eq!(layer.seq_len, 9);
21779        }
21780    }
21781
21782    /// CMF_LAYER_DUMP: the decode walk and the batched prefill both write
21783    /// every (position, layer) hidden, the two sets agree byte for byte,
21784    /// and the last layer's file is the stack output.
21785    #[test]
21786    fn layer_dump_covers_every_position_and_layer_on_both_walks() {
21787        let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
21788        let _ = std::fs::remove_dir_all(&dir);
21789        let mut p = mimo_test_pipeline();
21790        let hs = p.hidden_size;
21791        let ids = [5u32, 9, 11, 2, 40];
21792        p.layer_dump = Some(dir.join("decode"));
21793        for (pos, &id) in ids.iter().enumerate() {
21794            let e = p.embed_single(id);
21795            let _ = p.forward_layers(&e, pos, None);
21796        }
21797        p.clear_sequence_state();
21798        p.layer_dump = Some(dir.join("prefill"));
21799        let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21800        for pos in 0..ids.len() {
21801            for li in 0..p.num_layers {
21802                let name = format!("p{pos:06}_l{li:02}.f32");
21803                let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
21804                let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
21805                assert_eq!(a.len(), hs * 4, "{name}");
21806                assert_eq!(a, b, "{name}");
21807            }
21808        }
21809        let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
21810        let vals: Vec<f32> = last
21811            .chunks(4)
21812            .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
21813            .collect();
21814        assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
21815        let _ = std::fs::remove_dir_all(&dir);
21816    }
21817
21818    #[test]
21819    fn attn_geometry_and_sinks_are_validated() {
21820        let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
21821        assert!(
21822            p.set_attn_geometry(Some(vec![2]), None).is_err(),
21823            "one entry per layer"
21824        );
21825        assert!(
21826            p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
21827            "3 does not divide 4"
21828        );
21829        assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
21830        assert!(p.set_attn_geometry(None, Some(0)).is_err());
21831        assert!(
21832            p.set_attn_geometry(None, Some(5)).is_err(),
21833            "V wider than the head"
21834        );
21835        p.set_attn_geometry(None, Some(4)).unwrap();
21836        assert_eq!(
21837            p.v_head_dim, None,
21838            "v_head_dim == head_dim is the uniform case"
21839        );
21840        p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
21841        p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
21842        assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
21843        assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
21844        assert!(
21845            p.kv_cache.layers[1].sinks.is_some(),
21846            "a reshape keeps the layer's sinks"
21847        );
21848        assert_eq!(p.layer_geom(1).0, 4);
21849        assert!(
21850            p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
21851            "one sink per Q head"
21852        );
21853        assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
21854        assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
21855    }
21856
21857    /// The O(1) Nyström state replaces a plain full-context softmax; it
21858    /// must never be armed on a sliding, sink or narrow-V layer.
21859    #[test]
21860    fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
21861        let cfg = || {
21862            Some(crate::nystrom::O1Cfg {
21863                layers: crate::nystrom::O1Layers::All,
21864                m: 4,
21865                w: 8,
21866                sink: 2,
21867                rect: crate::nystrom::O1Rect::Aggregate,
21868            })
21869        };
21870        let mut p = mimo_test_pipeline();
21871        p.set_o1(cfg());
21872        assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
21873        let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
21874        q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
21875        q.sliding_layers = Some(vec![false, false, true]);
21876        q.swa = Some((4, usize::MAX));
21877        q.set_o1(cfg());
21878        assert_eq!(q.o1_flags, vec![true, false, false]);
21879    }
21880
21881    #[test]
21882    fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
21883        const B: usize = 19;
21884        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21885        p.set_o1(Some(crate::nystrom::O1Cfg {
21886            layers: crate::nystrom::O1Layers::All,
21887            m: 4,
21888            w: 8,
21889            sink: 2,
21890            rect: crate::nystrom::O1Rect::Aggregate,
21891        }));
21892        p.o1_begin_with_prefix(Some(B));
21893        let ids: Vec<u32> = (0..B as u32).collect();
21894        let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21895
21896        assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
21897        assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
21898        let next = p.embed_single(B as u32);
21899        let _ = p.forward_layers(&next, B, None);
21900        assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
21901    }
21902
21903    #[test]
21904    fn o1_pair_transition_commits_scratch_before_epoch_publication() {
21905        const B: usize = 19;
21906        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21907        // Keep a real recurrent layer ahead of the Full O(1) layer so the
21908        // pair test observes the GDN lane-2 scratch swap at the same
21909        // boundary, rather than only exercising an artificial scratch vec.
21910        let gdn_cfg = crate::linear_core::GdnCfg {
21911            num_v_heads: 2,
21912            num_k_heads: 1,
21913            key_head_dim: 2,
21914            value_head_dim: 4,
21915            conv_kernel: 3,
21916            hidden_size: 8,
21917            rms_eps: 1e-6,
21918            output_gate_sigmoid: false,
21919        };
21920        let synth = |n: usize, salt: usize| -> Vec<f32> {
21921            (0..n)
21922                .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
21923                .collect()
21924        };
21925        let qt = |rows: usize, cols: usize, salt: usize| {
21926            crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21927        };
21928        let c_dim = gdn_cfg.conv_dim();
21929        let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
21930        p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
21931            in_proj_qkv: qt(c_dim, 8, 1),
21932            in_proj_z: qt(vd, 8, 2),
21933            in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
21934            in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
21935            conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
21936            a_log: vec![0.2, 0.5],
21937            dt_bias: synth(gdn_cfg.num_v_heads, 6),
21938            norm: vec![1.0; gdn_cfg.value_head_dim],
21939            out_proj: qt(8, vd, 7),
21940        });
21941        p.gdn_cfg = Some(gdn_cfg);
21942        p.set_o1(Some(crate::nystrom::O1Cfg {
21943            layers: crate::nystrom::O1Layers::All,
21944            m: 4,
21945            w: 8,
21946            sink: 2,
21947            rect: crate::nystrom::O1Rect::Aggregate,
21948        }));
21949        p.o1_begin_with_prefix(Some(B));
21950        for pos in 0..B - 2 {
21951            let emb = p.embed_single(pos as u32);
21952            let _ = p.forward_layers(&emb, pos, None);
21953        }
21954        let lane1_state = p.kv_cache.layers[0].linear_state.clone();
21955
21956        let e1 = p.embed_single((B - 2) as u32);
21957        let e2 = p.embed_single((B - 1) as u32);
21958        let _ = p.forward_pair(&e1, &e2, B - 2);
21959
21960        assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
21961        assert!(
21962            p.kv_cache
21963                .layers
21964                .iter()
21965                .enumerate()
21966                .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
21967        );
21968        assert!(!p.kv_cache.layers[0].linear_state.is_empty());
21969        assert_ne!(
21970            p.kv_cache.layers[0].linear_state, lane1_state,
21971            "real pair must commit GDN lane 2 before returning"
21972        );
21973        assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
21974        let next = p.embed_single(B as u32);
21975        let _ = p.forward_layers(&next, B, None);
21976        assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
21977    }
21978
21979    #[test]
21980    fn o1_error_observation_stays_terminal_until_reset() {
21981        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21982        p.set_o1(Some(crate::nystrom::O1Cfg {
21983            layers: crate::nystrom::O1Layers::All,
21984            m: 4,
21985            w: 8,
21986            sink: 2,
21987            rect: crate::nystrom::O1Rect::Aggregate,
21988        }));
21989        p.o1_begin();
21990        p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
21991
21992        assert!(p.o1_seal_checked().is_err());
21993        assert!(
21994            p.o1_seal_checked().is_err(),
21995            "retry must see the sticky error"
21996        );
21997        let k = vec![0.2f32; 4];
21998        let v = vec![0.3f32; 4];
21999        p.kv_cache.layers[0].append(&k, &v, &[]);
22000        assert_eq!(p.kv_cache.layers[0].seq_len, 0);
22001
22002        p.reset_session();
22003        p.o1_begin();
22004        p.kv_cache.layers[0].append(&k, &v, &[]);
22005        assert_eq!(p.kv_cache.layers[0].seq_len, 1);
22006    }
22007
22008    #[test]
22009    fn nll_graph_failure_is_terminal_and_request_is_reusable() {
22010        let ids = vec![1u32, 2, 3, 4, 5, 6];
22011        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22012        p.graph_logits = Some(vec![123.0]);
22013        p.graph_want_logits = true;
22014        p.graph_failed
22015            .store(true, std::sync::atomic::Ordering::Relaxed);
22016        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
22017        let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
22018        assert!(err.contains("before NLL"));
22019        assert!(p.graph_logits.is_none());
22020        assert!(!p.graph_want_logits);
22021        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22022        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
22023
22024        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22025        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
22026        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
22027        assert_eq!(actual.1, expected.1);
22028        assert!((actual.0 - expected.0).abs() < 1e-9);
22029    }
22030
22031    #[test]
22032    fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
22033        let ids = vec![1u32, 2, 3, 4, 5, 6];
22034        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22035        p.nll_test_fail_at = Some(1);
22036        let err = p
22037            .nll_ids_from(&ids, 0)
22038            .expect_err("one-shot forward failure");
22039        assert!(err.contains("forward") || err.contains("score row"));
22040        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22041        assert!(!p.graph_want_logits);
22042        assert!(p.graph_logits.is_none());
22043        assert!(p.kv_history.is_empty());
22044
22045        let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22046        let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
22047        let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
22048        assert_eq!(actual.1, expected.1);
22049        assert!((actual.0 - expected.0).abs() < 1e-9);
22050    }
22051
22052    #[test]
22053    fn nll_serial_failure_before_first_row_is_reported() {
22054        let ids = vec![1u32, 2, 3, 4];
22055        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22056        p.nll_test_force_serial = true;
22057        p.nll_test_fail_at = Some(0);
22058        let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
22059        assert!(err.contains("serial forward"));
22060        assert!(p.kv_history.is_empty());
22061        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22062        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
22063    }
22064
22065    #[test]
22066    fn ffn_probe_failure_discards_recorder_and_state() {
22067        let ids = vec![1u32, 2, 3, 4];
22068        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22069        p.nll_test_fail_at = Some(0);
22070        let err = p
22071            .probe_ffn_mass_batch(&ids)
22072            .expect_err("probe forward failure");
22073        assert!(err.contains("NLL"));
22074        assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
22075        assert!(p.kv_history.is_empty());
22076        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22077    }
22078
22079    #[test]
22080    fn nll_test_controls_are_pipeline_scoped() {
22081        let ids = vec![1u32, 2, 3, 4];
22082        let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22083        let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22084        failing.nll_test_force_serial = true;
22085        failing.nll_test_fail_at = Some(0);
22086
22087        assert!(!failing.can_prefill_batched());
22088        assert!(unaffected.can_prefill_batched());
22089        let expected = unaffected
22090            .nll_ids_from(&ids, 0)
22091            .expect("unaffected pipeline remains usable");
22092        let err = failing
22093            .nll_ids_from(&ids, 0)
22094            .expect_err("failure injection belongs to failing pipeline");
22095        assert!(err.contains("serial forward"));
22096        assert!(failing.nll_test_fail_at.is_none());
22097        assert!(unaffected.can_prefill_batched());
22098        let actual = unaffected
22099            .nll_ids_from(&ids, 0)
22100            .expect("unaffected pipeline remains reusable");
22101        assert_eq!(actual.1, expected.1);
22102        assert!((actual.0 - expected.0).abs() < 1e-9);
22103    }
22104
22105    #[test]
22106    fn forward_ids_failure_channel_is_terminal_and_reusable() {
22107        let ids = vec![1u32, 2, 3, 4, 5, 6];
22108        let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22109        p.graph_logits = Some(vec![123.0]);
22110        p.graph_want_logits = true;
22111        p.graph_failed
22112            .store(true, std::sync::atomic::Ordering::Relaxed);
22113        p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
22114
22115        let err = p
22116            .forward_ids(&ids, None)
22117            .expect_err("a failed forward must not become a valid head result");
22118        assert!(err.contains("forward_ids setup"));
22119        assert!(p.graph_logits.is_none());
22120        assert!(!p.graph_want_logits);
22121        assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22122        assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
22123        assert_eq!(p.kv_cache.seq_len(), 0);
22124
22125        let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
22126            .forward_ids(&ids, None)
22127            .expect("fresh forward_ids");
22128        let actual = p
22129            .forward_ids(&ids, None)
22130            .expect("pipeline remains reusable after a failed forward");
22131        assert_eq!(actual.len(), expected.len());
22132        assert!(
22133            actual
22134                .iter()
22135                .zip(expected)
22136                .all(|(a, b)| (a - b).abs() < 1e-9)
22137        );
22138        assert_eq!(p.kv_cache.seq_len(), ids.len());
22139    }
22140
22141    #[test]
22142    fn sigmoid_router_floor_is_explicit_per_architecture() {
22143        // Flint's reference uses +1e-20 while the generic LFM2-compatible
22144        // path uses +1e-6.  At low (but representable)
22145        // sigmoid scores, silently sharing the latter changes expert weights
22146        // by orders of magnitude and can make a routed layer look coherent
22147        // while discarding its expert contribution.
22148        let zero = || DenseFfn {
22149            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22150            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22151            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22152            act: Act::Silu,
22153            down_t: None,
22154            segs: Vec::new(),
22155        };
22156        let m = MoeFfn {
22157            router: QTensor::from_f32(vec![0.0; 4], 2, 2),
22158            experts: vec![zero(), zero()],
22159            top_k: 1,
22160            norm_topk_prob: true,
22161            router_sigmoid: true,
22162            sigmoid_denom_eps: 1e-20,
22163            expert_bias: None,
22164            routed_scaling: 2.5,
22165            route_tau: None,
22166            shared: None,
22167            stats: std::cell::RefCell::new(Vec::new()),
22168            act_sq: std::cell::RefCell::new(Vec::new()),
22169            act_rows: std::cell::RefCell::new(Vec::new()),
22170            mask: None,
22171            per_expert_scale: None,
22172            router_input_norm: false,
22173            resonance: None,
22174            grown: Vec::new(),
22175        };
22176        let logits = [-20.0f32, -20.0];
22177        let (_, p, flint_wsum) = moe_route(&logits, &m, None);
22178        let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
22179        let expected = (p[0] + 1e-20) / m.routed_scaling;
22180        assert!((flint_wsum - expected).abs() < 1e-15);
22181        assert!(legacy_wsum > flint_wsum * 100.0);
22182    }
22183
22184    #[test]
22185    fn resonance_scores_match_formula_and_stable_tie() {
22186        let r = Resonance {
22187            // Three descriptors, hidden=2, one projection row each.
22188            mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
22189            u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
22190            k: 1,
22191            bias: vec![1.5, 0.5, 0.0],
22192            shell: Vec::new(),
22193        };
22194        let x = [1.0f32, 1.0];
22195        let mut got = vec![0.0; 3];
22196        r.scores(&x, &mut got);
22197        // Expert 0 and 1 are an exact score tie; the CPU top-1 contract uses
22198        // the lower index.  The values also check d² - (U·d)², not just tie
22199        // ordering.
22200        assert!((got[0] - 0.5).abs() < 1e-6);
22201        assert!((got[1] - 0.5).abs() < 1e-6);
22202        assert!(got[2].abs() < 1e-6);
22203        let best = got
22204            .iter()
22205            .enumerate()
22206            .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
22207            .map(|(i, _)| i);
22208        assert_eq!(best, Some(0));
22209        assert!(got.iter().all(|v| v.is_finite()));
22210    }
22211
22212    /// The growth shell (spec §2): a grown expert whose reconstruction
22213    /// error lies outside its shell scores −∞, one inside keeps the exact
22214    /// resonance score, trunk rows (`+inf` shell) are bit-identical to the
22215    /// shell-less computation; the process-wide switch disables it.
22216    #[test]
22217    fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
22218        // hidden = 2, rank 1. Experts 0/1 = trunk (shell +inf); 2 and 3 =
22219        // grown, the same descriptor (μ = (0, 1), u = (1, 1)) with shells
22220        // 6.0 and 0.25. At x' = (3, 0): d = (3, −1), d² = 10, proj =
22221        // (3 − 1)² = 4, err = 6 exactly — on the boundary of expert 2's
22222        // shell (kept: the rule is strict `>`), outside expert 3's.
22223        let plain = Resonance {
22224            mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
22225            u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
22226            k: 1,
22227            bias: vec![1.5, 0.5, 0.0, 0.0],
22228            shell: Vec::new(),
22229        };
22230        let shelled = Resonance {
22231            mu: plain.mu.clone(),
22232            u: plain.u.clone(),
22233            k: 1,
22234            bias: plain.bias.clone(),
22235            shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
22236        };
22237        assert!(!plain.has_shell());
22238        assert!(shelled.has_shell());
22239        let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
22240        let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
22241        set_growth_shell(Some(true));
22242        assert!(growth_shell_enabled());
22243        // x = (1, 1): the grown experts reconstruct it exactly (err 0):
22244        // inside both shells, every row the shell-less bits.
22245        let x = [1.0f32, 1.0];
22246        plain.scores(&x, &mut a);
22247        shelled.scores(&x, &mut b);
22248        assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
22249        assert!(a[2] == 0.0 && a[3] == 0.0);
22250        // x' = (3, 0): expert 3 → −∞, expert 2 (err == shell) and the
22251        // trunk rows keep their exact bits.
22252        let xo = [3.0f32, 0.0];
22253        plain.scores(&xo, &mut a);
22254        shelled.scores(&xo, &mut b);
22255        assert_eq!(a[2], -6.0);
22256        assert_eq!(a[3], -6.0);
22257        assert_eq!(
22258            bits(&a[..3]),
22259            bits(&b[..3]),
22260            "trunk rows + the boundary row"
22261        );
22262        assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
22263        assert_eq!(shelled.effective_shell(4), shelled.shell);
22264        // The switch (`CMF_GROWTH_SHELL=off` / `growth-eval --shell off`):
22265        // all +inf, the shell-less bits everywhere.
22266        set_growth_shell(Some(false));
22267        assert!(!growth_shell_enabled());
22268        assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
22269        shelled.scores(&xo, &mut b);
22270        assert_eq!(bits(&a), bits(&b));
22271        set_growth_shell(None);
22272        // A shell vector shorter than the expert count masks nothing
22273        // beyond it (a legacy layer whose tail has no shell).
22274        let short = Resonance {
22275            shell: vec![f32::INFINITY, f32::INFINITY],
22276            ..shelled
22277        };
22278        set_growth_shell(Some(true));
22279        short.scores(&xo, &mut b);
22280        assert_eq!(bits(&a), bits(&b));
22281        set_growth_shell(None);
22282    }
22283
22284    /// `moe_route` with −∞ logits (a grown expert outside its shell):
22285    /// top-1 is the best finite expert with weight exactly 1.0 on both
22286    /// the softmax and the sigmoid path; all −∞ degrades to uniform.
22287    #[test]
22288    fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
22289        let zero = || DenseFfn {
22290            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22291            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22292            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22293            act: Act::Silu,
22294            down_t: None,
22295            segs: Vec::new(),
22296        };
22297        let moe = |sigmoid: bool| MoeFfn {
22298            router: QTensor::from_f32(vec![0.0; 8], 4, 2),
22299            experts: vec![zero(), zero(), zero(), zero()],
22300            top_k: 1,
22301            norm_topk_prob: true,
22302            router_sigmoid: sigmoid,
22303            sigmoid_denom_eps: 1e-6,
22304            expert_bias: None,
22305            routed_scaling: 1.0,
22306            route_tau: None,
22307            shared: None,
22308            stats: std::cell::RefCell::new(Vec::new()),
22309            act_sq: std::cell::RefCell::new(Vec::new()),
22310            act_rows: std::cell::RefCell::new(Vec::new()),
22311            mask: None,
22312            per_expert_scale: None,
22313            router_input_norm: false,
22314            resonance: None,
22315            grown: Vec::new(),
22316        };
22317        let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
22318        for sigmoid in [false, true] {
22319            let m = moe(sigmoid);
22320            let (idx, p, wsum) = moe_route(&logits, &m, None);
22321            assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
22322            assert_eq!(p[1], 0.0);
22323            assert_eq!(p[3], 0.0);
22324            assert!(p[2] > p[0] && p[0] > 0.0);
22325            assert!(p.iter().all(|v| v.is_finite()));
22326            let w = p[2] / wsum;
22327            if sigmoid {
22328                // The sigmoid renorm keeps its reference floor (+1e-6).
22329                assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
22330            } else {
22331                assert_eq!(
22332                    w, 1.0,
22333                    "softmax: the renormalized top-1 weight is exactly 1"
22334                );
22335            }
22336            // Masked experts stay masked even when they are the only ones
22337            // "admitted" by an allow-list that covers everything.
22338            let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
22339            assert_eq!(idx, vec![2]);
22340        }
22341        // A finite expert always beats −∞ whatever the bias / order.
22342        let m = moe(false);
22343        let (idx, _, _) = moe_route(
22344            &[
22345                f32::NEG_INFINITY,
22346                f32::NEG_INFINITY,
22347                f32::NEG_INFINITY,
22348                -9.0,
22349            ],
22350            &m,
22351            None,
22352        );
22353        assert_eq!(idx, vec![3]);
22354        // Every expert at −∞ (cannot happen on a grown file — trunk rows
22355        // have no shell): uniform, finite, lowest index.
22356        let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
22357        assert_eq!(idx, vec![0]);
22358        assert!(p.iter().all(|&v| v == 0.25));
22359        assert!(wsum.is_finite() && wsum > 0.0);
22360    }
22361
22362    /// The resonance router (top-1) selects by the raw score as the
22363    /// trainer and the graph do — not by softmax probabilities, where two
22364    /// scores closer than 2^-25 collapse to the same `exp(l − max) = 1.0`
22365    /// and the LOWER index wins a token whose score is strictly smaller.
22366    #[test]
22367    fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
22368        let zero = || DenseFfn {
22369            gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22370            up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22371            down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22372            act: Act::Silu,
22373            down_t: None,
22374            segs: Vec::new(),
22375        };
22376        let moe = |resonant: bool, norm_topk: bool| MoeFfn {
22377            router: QTensor::from_f32(vec![0.0; 6], 3, 2),
22378            experts: vec![zero(), zero(), zero()],
22379            top_k: 1,
22380            norm_topk_prob: norm_topk,
22381            router_sigmoid: false,
22382            sigmoid_denom_eps: 1e-6,
22383            expert_bias: None,
22384            routed_scaling: 1.0,
22385            route_tau: None,
22386            shared: None,
22387            stats: std::cell::RefCell::new(Vec::new()),
22388            act_sq: std::cell::RefCell::new(Vec::new()),
22389            act_rows: std::cell::RefCell::new(Vec::new()),
22390            mask: None,
22391            per_expert_scale: None,
22392            router_input_norm: false,
22393            resonance: resonant.then(|| Resonance {
22394                mu: vec![0.0; 6],
22395                u: Vec::new(),
22396                k: 0,
22397                bias: vec![0.0; 3],
22398                shell: Vec::new(),
22399            }),
22400            grown: Vec::new(),
22401        };
22402        // lo = −0.1, hi = the next f32 towards zero: hi − lo = 2^-27 <
22403        // 2^-25, so exp(lo − hi) rounds to exactly 1.0 — a softmax tie.
22404        let lo = -0.1f32;
22405        let hi = f32::from_bits(lo.to_bits() - 1);
22406        assert!(hi > lo && hi - lo < 2f32.powi(-25));
22407        assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
22408        // The gated MoE (softmax) path: the tie hands the token to index 0.
22409        let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
22410        assert_eq!(
22411            idx,
22412            vec![0],
22413            "softmax tie → lower index (the gated contract)"
22414        );
22415        // The resonance path: the strictly larger raw score wins, weight
22416        // exactly 1.0 with and without norm_topk.
22417        for norm in [true, false] {
22418            let m = moe(true, norm);
22419            let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
22420            assert_eq!(idx, vec![1], "norm_topk {norm}");
22421            assert_eq!(p, vec![0.0, 1.0, 0.0]);
22422            assert_eq!(p[1] / wsum, 1.0);
22423            // An exact tie: the first maximum (as `resonance_winner` and
22424            // `embryo_core_route_pick`).
22425            let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
22426            assert_eq!(idx, vec![0]);
22427            // `−∞` never wins; the admitted set is honoured.
22428            let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
22429            assert_eq!(idx, vec![2]);
22430            let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
22431            assert_eq!(idx, vec![0]);
22432            assert_eq!(p[0] / wsum, 1.0);
22433            // Every admitted expert at −∞: the generic path's uniform
22434            // fallback (lowest index, finite weights).
22435            let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
22436            assert_eq!(idx, vec![0]);
22437            assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
22438        }
22439    }
22440}