Skip to main content

cortiq_engine/
qwen4_exp.rs

1//! Exact text-only Qwen3.8-Flash-Next (`qwen4_exp`) stack.
2//!
3//! The implementation deliberately composes `QTensor` projections instead
4//! of owning backend-specific buffers.  Consequently q4tp weights use the
5//! same CPU/Vulkan/DX12/Metal kernels, resident arena and expert LRU as every
6//! other CMF model; weights that do not fit VRAM remain mmap-backed in RAM.
7
8use crate::linear_core::{GdnCfg, GdnWeights, gdn_forward};
9use crate::loader::{Overlay, build_ffn_at, load_f32, load_matrix};
10use crate::pipeline::{FfnKind, MoeFfn, moe_ffn};
11use crate::pool::Pool;
12use crate::qtensor::QTensor;
13use cortiq_core::{CmfError, CmfModel, LayerType, ModelArch, Qwen4ExpConfig, TensorDtype};
14use std::cmp::Ordering;
15use std::sync::Arc;
16
17const PRIME_1: u64 = 10_007;
18const SPLITMIX_GAMMA: u64 = 0x9E37_79B9_7F4A_7C15;
19const SPLITMIX_M1: u64 = 0xBF58_476D_1CE4_E5B9;
20const SPLITMIX_M2: u64 = 0x94D0_49BB_1331_11EB;
21
22pub struct GatedResidual {
23    norm: Vec<f32>,
24    down: QTensor,
25    up: QTensor,
26    inject: Option<QTensor>,
27    /// Directory indices of the same three matrices: the device frame binds
28    /// the file bytes directly (f16/f32/q8_2f), the host path keeps the
29    /// dequantized `QTensor` views above.
30    down_idx: Option<usize>,
31    up_idx: Option<usize>,
32    inject_idx: Option<usize>,
33}
34
35/// Directory indices of every skeleton matrix the device frame binds for
36/// one layer. `None` when a tensor is missing from the file: the device
37/// path then stays off and the host path runs as before.
38struct LayerIdx {
39    /// qkv, z, a, b, out
40    gdn: Option<[usize; 5]>,
41    /// q, k, v, o, index_qk
42    qsa: Option<[usize; 5]>,
43    /// key_proj, value_proj
44    ple: Option<(usize, usize)>,
45    router: usize,
46    shared_gate: Option<usize>,
47}
48
49pub struct QsaWeights {
50    q_proj: QTensor,
51    k_proj: QTensor,
52    v_proj: QTensor,
53    o_proj: QTensor,
54    q_norm: Vec<f32>,
55    k_norm: Vec<f32>,
56    index_qk: QTensor,
57    index_q_norm: Vec<f32>,
58    index_k_norm: Vec<f32>,
59}
60
61pub struct PleWeights {
62    shards: Vec<QTensor>,
63    rows_per_shard: usize,
64    row_dim: usize,
65    key_proj: QTensor,
66    value_proj: QTensor,
67    norm_key: Vec<f32>,
68    norm_query: Vec<f32>,
69    norm_conv: Vec<f32>,
70    conv: Vec<f32>,
71    multipliers: Vec<i64>,
72    vocab_sizes: Vec<i64>,
73    offsets: Vec<i64>,
74}
75
76pub enum Mixer {
77    Gdn(GdnWeights),
78    Qsa(QsaWeights),
79}
80
81pub struct Layer {
82    attn_hc: GatedResidual,
83    mlp_hc: GatedResidual,
84    mixer: Mixer,
85    moe: MoeFfn,
86    ple: Option<PleWeights>,
87    /// Directory triples for the 512 routed experts.  The dynamic GPU cache
88    /// binds by directory index; keeping the table beside the layer avoids
89    /// rebuilding 24,576 triples on every token.
90    expert_ids: Vec<(usize, usize, usize)>,
91    /// Qwen carries one gated shared expert per layer. It has the same
92    /// geometry as a routed expert but owns a pinned cache line, exactly as
93    /// the established dynamic DSV4 pool does for its shared branch.
94    shared_ids: Option<(usize, usize, usize)>,
95    idx: Option<LayerIdx>,
96}
97
98pub struct Globals {
99    embed: QTensor,
100    lm_head: QTensor,
101    head_hc: GatedResidual,
102    lm_head_idx: Option<usize>,
103    /// The embedding table, re-read on the card by the MTP draft chain.
104    embed_idx: Option<usize>,
105    /// Every skeleton matrix the device frame reads, for the one-time
106    /// upload-and-pin before the expert arena takes the rest of the card.
107    skeleton_idxs: Vec<usize>,
108}
109
110#[derive(Clone)]
111pub struct Cfg {
112    hidden: usize,
113    hc: usize,
114    eps: f64,
115    n_heads: usize,
116    n_kv_heads: usize,
117    head_dim: usize,
118    rotary_dim: usize,
119    index_heads: usize,
120    index_kv_heads: usize,
121    index_dim: usize,
122    index_budget: usize,
123    compress_ratio: usize,
124    gdn: GdnCfg,
125    ngram_size: usize,
126    heads_per_ngram: usize,
127    ple_kernel: usize,
128    ple_dilation: usize,
129    eos: u32,
130}
131
132#[derive(Default)]
133struct QsaState {
134    raw_keys: Vec<f32>,
135    keys: Vec<f32>,
136    values: Vec<f32>,
137}
138
139#[derive(Default)]
140struct LayerState {
141    gdn: Vec<f32>,
142    qsa: QsaState,
143    /// Chronological normalized PLE values, at most (kernel-1)*dilation rows.
144    ple_history: Vec<f32>,
145    ple_history_rows: usize,
146}
147
148pub struct State {
149    hyper: Vec<f32>,
150    layers: Vec<LayerState>,
151    token_history: Vec<u32>,
152    gpu_pool: Option<QwenGpuPool>,
153    pub pos: usize,
154    /// Device-resident token path (`gpu_wgpu::qwen4`): the hyper state,
155    /// GDN/QSA/PLE caches and the frame scratch live on the card.
156    #[cfg(feature = "gpu")]
157    dev: Option<crate::gpu_wgpu::qwen4::Dev>,
158    /// Routed winners per layer from the previous token: the arena admits
159    /// experts one token late, because the route is decided on the card
160    /// and only read back with the cold list.
161    picks_prev: Vec<Vec<usize>>,
162    /// The device path refused once (setup or mid-token); stay on the host.
163    device_off: bool,
164    /// Positions the host-side caches (`layers`) hold. The device path
165    /// advances `pos` and `token_history` but keeps its caches on the card,
166    /// so after it turns off mid-sequence this lags `pos` and the host path
167    /// replays the history first.
168    host_pos: usize,
169    #[cfg(feature = "gpu")]
170    profile: Option<ExpertProfile>,
171    /// The next device forward is a verify window: snapshot the recurrent
172    /// state per token and return every token's logits in `window_logits`.
173    verify_window: bool,
174    window_logits: Vec<Vec<f32>>,
175    #[cfg(feature = "gpu")]
176    mtp: Option<MtpHead>,
177    mtp_tried: bool,
178}
179
180/// Summed wall time and count of expert uploads (every thread), for the
181/// per-token profile line: against the admission wall time it says whether
182/// the uploads overlap.
183pub(crate) static FILL_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
184pub(crate) static FILL_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
185/// Wall time of the parallel fill sections (against FILL_NS: their overlap).
186pub(crate) static FILLS_WALL_NS: std::sync::atomic::AtomicU64 =
187    std::sync::atomic::AtomicU64::new(0);
188
189/// An expert's (gate, up, down) bytes straight from the model's mapping.
190#[cfg(feature = "gpu")]
191fn expert_slices(model: &CmfModel, t: (usize, usize, usize)) -> Option<[&[u8]; 3]> {
192    let bytes = model.primary_bytes();
193    let part = |i: usize| -> Option<&[u8]> {
194        let e = model.tensors.get(i)?;
195        let abs = model.entry_abs_offset(e)?;
196        bytes.get(abs..abs + e.nbytes as usize)
197    };
198    Some([part(t.0)?, part(t.1)?, part(t.2)?])
199}
200
201/// `CMF_QWEN_FILL_TRACE=1`: one line per admission (caller, layer, expert, slot).
202fn fill_trace() -> bool {
203    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
204    *S.get_or_init(|| std::env::var("CMF_QWEN_FILL_TRACE").as_deref() == Ok("1"))
205}
206
207pub(crate) struct QwenGpuPool {
208    /// Layers whose remap is encoded in a frame that has not run yet: no
209    /// admission may evict their experts until that frame is done.
210    pub(crate) hold_layers: [Option<usize>; 2],
211    /// The last `staging` slots never enter the LRU: a frame's cold winners
212    /// are uploaded there (token slot × rank) and read by the next frame's
213    /// cold pass, so no admission or eviction can touch them in between.
214    staging_base: usize,
215    staging: usize,
216    /// The pinned staging ring admissions go through (flushed before every
217    /// frame submit); None uploads directly.
218    #[cfg(feature = "gpu")]
219    stager: Option<crate::gpu_wgpu::qwen4::Stager>,
220    /// The host-heap tier admissions copy from by DMA (`HostTier`); None
221    /// uploads through `write_buffer` (or the staging ring).
222    #[cfg(feature = "gpu")]
223    pub(crate) tier: Option<Arc<crate::gpu_wgpu::qwen4::HostTier>>,
224    /// Persistent workers for the admission copies (`CMF_QWEN_ADMIT_THREADS`,
225    /// default 16; 0 spawns scoped threads per batch as before).
226    admit_pool: Option<Arc<Pool>>,
227    /// The host tiers behind this arena (RAM tier, then the file), set by
228    /// the device path; None reads experts straight from the memory map.
229    pub(crate) store: Option<Arc<crate::expert_store::ExpertStore>>,
230    pub(crate) segment_slots: usize,
231    floor: usize,
232    n_experts: usize,
233    fetch_quota: usize,
234    fetch_min_seen: u16,
235    fetch_max_env: &'static str,
236    fetch_min_env: &'static str,
237    owner: Vec<Option<(usize, usize)>>,
238    /// Qwen has one fixed expert count on every layer.  A dense
239    /// `[layer][expert]` map avoids hundreds of hash lookups per layer and
240    /// makes clearing/rebuilding the cache allocation-free.
241    slot_for: Vec<u32>,
242    shared_slot: Vec<u32>,
243    seen: Vec<u16>,
244    seen_epoch: Vec<u32>,
245    /// Reverse-filled so `pop()` preserves the old 0,1,2... allocation
246    /// order while avoiding an O(capacity) `position(None)` scan per fill.
247    free: Vec<usize>,
248    occupancy: Vec<usize>,
249    last: Vec<u64>,
250    clock: u64,
251}
252
253/// An admission whose serial bookkeeping is complete but whose reserved
254/// expert fills have not run yet.
255///
256/// This is deliberately a narrow, consuming lease over [`QwenGpuPool`].  A
257/// caller may use [`Self::cold_ids`] only to overlap discardable host work
258/// with the fills.  It must call [`Self::finish`] before using any slot: a
259/// failed upload invalidates the whole admission and returns `None`.  Dropping
260/// an unfinished lease removes its unfilled reservations, so no stale logical
261/// slot can escape an abandoned overlap attempt.
262#[cfg(feature = "gpu")]
263#[must_use = "a pending admission must be finished or deliberately dropped"]
264pub(crate) struct QwenGpuPendingEnsure<'pool, 'input> {
265    pool: Option<&'pool mut QwenGpuPool>,
266    model: &'input Arc<CmfModel>,
267    picks: &'input [usize],
268    triples: &'input [(usize, usize, usize)],
269    layer: usize,
270    base: usize,
271    shared_slot: u32,
272    plan: Vec<(usize, usize)>,
273}
274
275#[cfg(feature = "gpu")]
276impl QwenGpuPendingEnsure<'_, '_> {
277    /// Routed picks which are definitely absent after reservation.  The
278    /// order is the caller's route order, including any duplicate input, so
279    /// it can be compared directly to the completed remap.
280    pub(crate) fn cold_ids(&self) -> Option<Vec<usize>> {
281        let pool = self.pool.as_deref()?;
282        cold_picks_from_slots(&pool.slot_for, self.base, pool.n_experts, self.picks)
283    }
284
285    /// Complete the reserved fills and return the same remap contract as
286    /// [`QwenGpuPool::ensure`].  This consumes the lease, ending its mutable
287    /// borrow of the pool before the caller submits uploads or encodes a
288    /// frame.
289    pub(crate) fn finish(self) -> Option<(Vec<u32>, u32)> {
290        self.finish_inner(false)
291            .map(|(remap, shared_slot, _)| (remap, shared_slot))
292    }
293
294    /// Complete the fills and submit any staged uploads before returning the
295    /// remap. This is for a caller whose next operation is a separate DSV4
296    /// frame rather than the generic Qwen frame that normally flushes itself.
297    /// The upload queue is flushed even when an admitted fill failed, matching
298    /// MiniCPM tail's existing failed-admission cleanup contract.
299    pub(crate) fn finish_and_flush(self) -> Option<(Vec<u32>, u32, usize)> {
300        self.finish_inner(true)
301    }
302
303    fn finish_inner(mut self, flush_uploads: bool) -> Option<(Vec<u32>, u32, usize)> {
304        // Taking the pool makes `Drop` a no-op. From this point this method
305        // owns the same cleanup responsibility as the old monolithic
306        // `ensure` implementation.
307        let pool = self.pool.take()?;
308        let model = self.model;
309        let triples = self.triples;
310        let layer = self.layer;
311        let base = self.base;
312        let shared_slot = self.shared_slot;
313        let plan = std::mem::take(&mut self.plan);
314        // phase 2: the uploads, in parallel (they were one after another:
315        // at a 12 GB budget that was the larger half of every token)
316        let ok = pool.run_fills(model, layer, &plan, triples, false);
317        let mut failed = false;
318        for (&(slot, e), ok) in plan.iter().zip(&ok) {
319            if !ok {
320                pool.owner[slot] = None;
321                pool.slot_for[base + e] = u32::MAX;
322                pool.occupancy[layer] = pool.occupancy[layer].saturating_sub(1);
323                pool.free.push(slot);
324                failed = true;
325            }
326        }
327        let result = if failed || triples.len() != pool.n_experts {
328            None
329        } else {
330            let remap = pool.slot_for[base..base + pool.n_experts].to_vec();
331            Some((remap, shared_slot, pool.segment_slots))
332        };
333        if flush_uploads {
334            pool.flush_uploads();
335        }
336        result
337    }
338}
339
340#[cfg(feature = "gpu")]
341impl Drop for QwenGpuPendingEnsure<'_, '_> {
342    fn drop(&mut self) {
343        let Some(pool) = self.pool.as_deref_mut() else {
344            return;
345        };
346        // A lease can be discarded after its caller has started speculative
347        // CPU work but before any GPU bytes were written. Clear only the
348        // reservations still owned by this exact plan; an old evicted expert
349        // remains absent rather than being incorrectly remapped to bytes it
350        // no longer owns.
351        for &(slot, expert) in &self.plan {
352            let Some(key) = self.base.checked_add(expert) else {
353                continue;
354            };
355            if pool.owner.get(slot).copied().flatten() == Some((self.layer, expert))
356                && pool.slot_for.get(key) == Some(&(slot as u32))
357            {
358                pool.owner[slot] = None;
359                pool.slot_for[key] = u32::MAX;
360                if let Some(occupancy) = pool.occupancy.get_mut(self.layer) {
361                    *occupancy = occupancy.saturating_sub(1);
362                }
363                pool.free.push(slot);
364            }
365        }
366    }
367}
368
369/// Select misses from a post-reservation logical slot map in the exact route
370/// order.  Kept pure so an overlap caller can validate its optimistic host
371/// work without opening a GPU device.
372#[cfg(feature = "gpu")]
373fn cold_picks_from_slots(
374    slot_for: &[u32],
375    base: usize,
376    n_experts: usize,
377    picks: &[usize],
378) -> Option<Vec<usize>> {
379    let slots = slot_for.get(base..base.checked_add(n_experts)?)?;
380    let mut cold = Vec::with_capacity(picks.len());
381    for &expert in picks {
382        let slot = *slots.get(expert)?;
383        if slot == u32::MAX {
384            cold.push(expert);
385        }
386    }
387    Some(cold)
388}
389
390/// Parse a bounded pool percentage without allowing a malformed operator
391/// knob to turn into an unbounded allocation.  Kept pure so the Qwen and GLM
392/// policies can be regression-tested without initializing a GPU adapter.
393fn bounded_pool_pct(raw: Option<&str>, default: usize, min: usize, max: usize) -> usize {
394    raw.and_then(|v| v.parse::<usize>().ok())
395        .unwrap_or(default)
396        .clamp(min, max)
397}
398
399fn bounded_pool_slots(raw: Option<&str>, safe: usize, cap_override: bool) -> usize {
400    let floor = if cap_override { safe.min(8) } else { 8 };
401    raw.and_then(|v| v.parse::<usize>().ok())
402        .map(|v| if cap_override { v.min(safe) } else { v })
403        .unwrap_or(safe)
404        .max(floor)
405}
406
407/// Divide a resident arena only over layers that can actually reach it.
408///
409/// `floor` counts every owner, including a layer's pinned shared expert.  Do
410/// not round this up: when a deliberately small operator-provided arena has
411/// fewer slots than active layers, a larger floor would promise more protected
412/// slots than exist and turn the fallback into an accidental global policy.
413/// The caller's normal own-layer/global fallbacks still make progress at zero.
414fn active_layer_floor(capacity: usize, active_layers: usize) -> usize {
415    capacity / active_layers.max(1)
416}
417
418impl State {
419    pub fn new(n_layers: usize) -> Self {
420        Self {
421            hyper: Vec::new(),
422            layers: (0..n_layers).map(|_| LayerState::default()).collect(),
423            token_history: Vec::new(),
424            gpu_pool: None,
425            pos: 0,
426            #[cfg(feature = "gpu")]
427            dev: None,
428            picks_prev: vec![Vec::new(); n_layers],
429            device_off: false,
430            host_pos: 0,
431            #[cfg(feature = "gpu")]
432            profile: None,
433            verify_window: false,
434            window_logits: Vec::new(),
435            #[cfg(feature = "gpu")]
436            mtp: None,
437            mtp_tried: false,
438        }
439    }
440
441    fn reset(&mut self) {
442        self.hyper.clear();
443        self.token_history.clear();
444        self.pos = 0;
445        self.host_pos = 0;
446        for st in &mut self.layers {
447            *st = LayerState::default();
448        }
449    }
450}
451
452#[cfg(feature = "gpu")]
453impl QwenGpuPool {
454    pub(crate) fn create(
455        model: &Arc<CmfModel>,
456        inter: usize,
457        hidden: usize,
458        n_layers: usize,
459        n_experts: usize,
460        gu_q2: bool,
461    ) -> Option<Self> {
462        Self::create_with_policy(
463            model,
464            inter,
465            hidden,
466            n_layers,
467            n_experts,
468            gu_q2,
469            "CMF_QWEN_POOL_PCT",
470            75,
471            25,
472            85,
473            "CMF_QWEN_EXPERT_SLOTS",
474            "CMF_QWEN_FETCH_MAX",
475            "CMF_QWEN_FETCH_MIN_SEEN",
476            None,
477            0,
478        )
479    }
480
481    /// The device token path sizes the arena itself: the skeleton is already
482    /// resident and pinned, so the slot count is whatever the budget still
483    /// holds after it and a cache reserve, not a percentage of the card.
484    pub(crate) fn create_explicit(
485        model: &Arc<CmfModel>,
486        inter: usize,
487        hidden: usize,
488        n_layers: usize,
489        n_experts: usize,
490        gu_q2: bool,
491        slots: usize,
492        staging: usize,
493    ) -> Option<Self> {
494        Self::create_with_policy(
495            model,
496            inter,
497            hidden,
498            n_layers,
499            n_experts,
500            gu_q2,
501            "CMF_QWEN_POOL_PCT",
502            75,
503            25,
504            85,
505            "CMF_QWEN_EXPERT_SLOTS",
506            "CMF_QWEN_FETCH_MAX",
507            "CMF_QWEN_FETCH_MIN_SEEN",
508            Some(slots),
509            staging,
510        )
511        .map(|mut pool| {
512            // Admissions go straight through the queue (`write_buffer`) by
513            // default; CMF_QWEN_STAGE_MB=<n> puts them through a pinned
514            // staging ring of n MB per buffer. Measured on an RTX 3090 with
515            // the file in the page cache, the ring lost at every budget: the
516            // card waited ~25 ms longer per 8-token prompt frame (prompt
517            // ingest 48-49 -> 56-60 tok/s without it), and decode of a
518            // 200-token story went 32.0/32.3 -> 33.6/34.0 tok/s at the full
519            // card and 17.1/17.7 -> 19.5/19.4 at 12 GB (131 cold experts a
520            // token). The ring was introduced on hosts whose page cache could
521            // not hold the file; there it may still win.
522            let stage_mb = std::env::var("CMF_QWEN_STAGE_MB")
523                .ok()
524                .and_then(|v| v.parse::<u64>().ok())
525                .unwrap_or(0);
526            pool.stager = crate::gpu_wgpu::qwen4::Stager::new(stage_mb);
527            // Spawning a thread per expert and batch (mmap + munmap of its
528            // stack) contends with the copies' own page faults on the
529            // address-space lock: a pool of parked workers instead.
530            let admit_threads = std::env::var("CMF_QWEN_ADMIT_THREADS")
531                .ok()
532                .and_then(|v| v.parse::<usize>().ok())
533                .unwrap_or(16);
534            pool.admit_pool = (admit_threads > 0).then(|| Arc::new(Pool::new(admit_threads)));
535            // A cold expert costs the host hundreds of microseconds; the
536            // device path admits on the first miss and fetches at least a
537            // handful per layer and token. Both stay operator-tunable.
538            pool.fetch_min_seen = std::env::var("CMF_QWEN_FETCH_MIN_SEEN")
539                .ok()
540                .and_then(|v| v.parse().ok())
541                .unwrap_or(1);
542            pool.fetch_quota = std::env::var("CMF_QWEN_FETCH_MAX")
543                .ok()
544                .and_then(|v| v.parse().ok())
545                .unwrap_or(pool.fetch_quota.max(16));
546            pool
547        })
548    }
549
550    /// Apply MiniCPM's narrow CPU-tail policy after generic construction.
551    /// Generic Qwen/device callers deliberately retain the constructor's
552    /// all-layer policy; MiniCPM knows that only its graph-produced tail can
553    /// touch this bank.
554    ///
555    /// The floor is policy-only: it never changes an admitted slot, router
556    /// score, route, or remap.  It merely changes which already-resident
557    /// routed expert is considered first for a future replacement.  The
558    /// two-worker admission default is likewise placement-only.  An explicit
559    /// `CMF_QWEN_ADMIT_THREADS` always wins, including an explicit `0`.
560    pub(crate) fn configure_minicpm_tail(&mut self, active_layers: usize) {
561        let layers = active_layers.clamp(1, self.occupancy.len().max(1));
562        self.floor = active_layer_floor(self.staging_base, layers);
563        if std::env::var_os("CMF_QWEN_ADMIT_THREADS").is_none() {
564            // MiniCPM's top-8 tail has only a handful of 1.7-MB admissions
565            // per layer.  The generic 16-worker Qwen default causes page
566            // fault/copy contention here; two parked workers leave the CPU
567            // cold-expert correction and the graph driver room to run.
568            self.admit_pool = Some(Arc::new(Pool::new(2)));
569        }
570    }
571
572    /// The MTP head's own bank: exactly `slots` slots (whole segments), no
573    /// carve-out — it is sized before the main arena, which leaves the
574    /// reserve.
575    pub(crate) fn create_exact(
576        model: &Arc<CmfModel>,
577        inter: usize,
578        hidden: usize,
579        n_experts: usize,
580        gu_q2: bool,
581        slots: usize,
582    ) -> Option<Self> {
583        Self::create_with_policy(
584            model,
585            inter,
586            hidden,
587            1,
588            n_experts,
589            gu_q2,
590            "CMF_QWEN_MTP_BANK",
591            75,
592            25,
593            85,
594            "CMF_QWEN_MTP_BANK_SLOTS",
595            "CMF_QWEN_FETCH_MAX",
596            "CMF_QWEN_FETCH_MIN_SEEN",
597            Some(slots),
598            0,
599        )
600    }
601
602    /// Bytes one routed expert occupies in the arena (gate + up + down).
603    pub(crate) fn per_expert_bytes(inter: usize, hidden: usize, gu_q2: bool) -> Option<usize> {
604        let gu = cortiq_core::quant::expected_nbytes(
605            if gu_q2 {
606                TensorDtype::Q2TiledP
607            } else {
608                TensorDtype::Q4TiledP
609            },
610            &[inter, hidden],
611        )?;
612        let dn = cortiq_core::quant::expected_nbytes(TensorDtype::Q4TiledP, &[hidden, inter])?;
613        2usize.checked_mul(gu)?.checked_add(dn)
614    }
615
616    /// A free slot, else the least recently used slot of a layer above its
617    /// working-set floor, else one of this layer's own, never a shared slot
618    /// or one of `protect` (this layer's current winners).
619    fn victim(&mut self, layer: usize, protect: &[usize]) -> Option<usize> {
620        if let Some(s) = self.free.pop() {
621            return Some(s);
622        }
623        let hold = self.hold_layers;
624        let eligible = |owner: (usize, usize)| {
625            owner.1 != usize::MAX
626                && (owner.0 != layer || !protect.contains(&owner.1))
627                // a frame already encoded against this layer's remap is in
628                // flight: its slots must stay what the remap says
629                && !hold.contains(&Some(owner.0))
630        };
631        // Preserve a per-layer working set. A plain global LRU collapses
632        // under the deterministic 0..47 layer sweep: late layers evict early
633        // ones immediately before their next visit (the same failure
634        // measured in DSV4).
635        self.owner
636            .iter()
637            .enumerate()
638            .filter_map(|(slot, &o)| {
639                o.filter(|&x| {
640                    eligible(x) && self.occupancy.get(x.0).copied().unwrap_or(0) > self.floor
641                })
642                .map(|_| slot)
643            })
644            .min_by_key(|&slot| self.last[slot])
645            .or_else(|| {
646                self.owner
647                    .iter()
648                    .enumerate()
649                    .filter_map(|(slot, &o)| {
650                        o.filter(|&x| eligible(x) && x.0 == layer).map(|_| slot)
651                    })
652                    .min_by_key(|&slot| self.last[slot])
653            })
654            .or_else(|| {
655                self.owner
656                    .iter()
657                    .enumerate()
658                    .filter_map(|(slot, &o)| o.filter(|&x| eligible(x)).map(|_| slot))
659                    .min_by_key(|&slot| self.last[slot])
660            })
661    }
662
663    /// Admit one expert right now (a routed winner the arena did not hold),
664    /// regardless of the admission hysteresis. Returns its slot.
665    pub(crate) fn admit_now(
666        &mut self,
667        model: &Arc<CmfModel>,
668        layer: usize,
669        expert: usize,
670        triples: &[(usize, usize, usize)],
671        protect: &[usize],
672    ) -> Option<u32> {
673        if expert >= self.n_experts || triples.len() != self.n_experts {
674            return None;
675        }
676        let key = layer * self.n_experts + expert;
677        if self.slot_for[key] != u32::MAX {
678            return Some(self.slot_for[key]);
679        }
680        let victim = self.victim(layer, protect)?;
681        if !self.fill_slot(model, victim, layer, expert, triples[expert]) {
682            if self.owner[victim].is_none() {
683                self.free.push(victim);
684            }
685            return None;
686        }
687        if let Some(old) = self.owner[victim] {
688            self.dropped(old);
689            if old.1 == usize::MAX {
690                self.shared_slot[old.0] = u32::MAX;
691            } else {
692                self.slot_for[old.0 * self.n_experts + old.1] = u32::MAX;
693                self.occupancy[old.0] = self.occupancy[old.0].saturating_sub(1);
694            }
695        }
696        self.clock = self.clock.saturating_add(1);
697        self.owner[victim] = Some((layer, expert));
698        self.slot_for[key] = victim as u32;
699        self.occupancy[layer] += 1;
700        self.seen[key] = self.seen[key].max(1);
701        self.last[victim] = self.clock;
702        Some(victim as u32)
703    }
704
705    /// Upload one expert into `slot`: through the staging ring when there
706    /// is one with room, else straight through the queue.
707    #[cfg(feature = "gpu")]
708    fn fill_slot(
709        &self,
710        model: &Arc<CmfModel>,
711        slot: usize,
712        layer: usize,
713        expert: usize,
714        triple: (usize, usize, usize),
715    ) -> bool {
716        let t0 = std::time::Instant::now();
717        let key = layer * self.n_experts + expert;
718        // the host-heap tier: a DMA copy when it holds the expert; else the
719        // expert goes into the tier first (one memcpy into host RAM) when it
720        // has a slot, and through the queue otherwise
721        let tier = self.tier.as_ref();
722        if tier.is_some_and(|t| t.hit(key, slot)) {
723            if slot < self.staging_base
724                && let Some(store) = self.store.as_ref()
725            {
726                store.note_vram(layer, expert, true);
727            }
728            return true;
729        }
730        let up = |parts: [&[u8]; 3]| -> bool {
731            if tier.is_some_and(|t| t.fill(key, Some(slot), parts)) {
732                return true;
733            }
734            crate::gpu_wgpu::qwen4::upload_expert_parts(self.stager.as_ref(), model, slot, parts)
735        };
736        let stored = self
737            .store
738            .as_ref()
739            .and_then(|store| store.with_expert(layer, expert, true, up));
740        let ok = match stored {
741            Some(ok) => ok,
742            None => {
743                if tier.is_some()
744                    && let Some(parts) = expert_slices(model, triple)
745                {
746                    up(parts)
747                } else if let Some(st) = self.stager.as_ref()
748                    && crate::gpu_wgpu::qwen4::stage_expert(st, model, slot, triple)
749                {
750                    true
751                } else {
752                    crate::gpu_wgpu::dsv4_global_slot_fill(model, slot, triple)
753                }
754            }
755        };
756        // an arena slot (not a frame's staging slot): the RAM tier's copy
757        // of this expert is now redundant
758        if ok && slot < self.staging_base
759            && let Some(store) = self.store.as_ref()
760        {
761            store.note_vram(layer, expert, true);
762        }
763        FILL_NS.fetch_add(
764            t0.elapsed().as_nanos() as u64,
765            std::sync::atomic::Ordering::Relaxed,
766        );
767        FILL_N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
768        ok
769    }
770
771    /// `fill_slot` for every `(slot, expert)` job of `layer`, in parallel:
772    /// on the admission pool when there is one, else one scoped thread per
773    /// job (`chunked`: at most 32 threads). One result per job.
774    #[cfg(feature = "gpu")]
775    fn run_fills(
776        &self,
777        model: &Arc<CmfModel>,
778        layer: usize,
779        jobs: &[(usize, usize)],
780        triples: &[(usize, usize, usize)],
781        chunked: bool,
782    ) -> Vec<bool> {
783        let t0 = std::time::Instant::now();
784        let me = self;
785        // `current_device` is thread-local.  Unlike the persistent `Pool`
786        // path below (which propagates its descriptor's device), the scoped
787        // admission fallback creates fresh threads.  Preserve the caller's
788        // adapter there too: a fill on adapter 0 followed by a frame on
789        // adapter N would otherwise make a valid logical slot name stale
790        // physical bytes.
791        let device = crate::gpu::current_device();
792        let out: Vec<bool> = if jobs.len() <= 1 {
793            jobs.iter()
794                .map(|&(slot, e)| me.fill_slot(model, slot, layer, e, triples[e]))
795                .collect()
796        } else if let Some(pool) = self.admit_pool.as_ref() {
797            let done: Vec<std::sync::atomic::AtomicBool> = jobs
798                .iter()
799                .map(|_| std::sync::atomic::AtomicBool::new(false))
800                .collect();
801            let next = std::sync::atomic::AtomicUsize::new(0);
802            pool.run(&|_, _| {
803                loop {
804                    let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
805                    let Some(&(slot, e)) = jobs.get(i) else { break };
806                    let ok = me.fill_slot(model, slot, layer, e, triples[e]);
807                    done[i].store(ok, std::sync::atomic::Ordering::Relaxed);
808                }
809            });
810            done.iter()
811                .map(|d| d.load(std::sync::atomic::Ordering::Relaxed))
812                .collect()
813        } else if chunked {
814            const ADMIT_THREADS: usize = 32;
815            let nth = jobs.len().min(ADMIT_THREADS);
816            let per = jobs.len().div_ceil(nth);
817            std::thread::scope(|scope| {
818                let handles: Vec<_> = jobs
819                    .chunks(per)
820                    .map(|part| {
821                        let model = model.clone();
822                        let h = scope.spawn(move || {
823                            crate::gpu::with_device(device, || {
824                                part.iter()
825                                    .map(|&(slot, e)| {
826                                        me.fill_slot(&model, slot, layer, e, triples[e])
827                                    })
828                                    .collect::<Vec<bool>>()
829                            })
830                        });
831                        (part.len(), h)
832                    })
833                    .collect();
834                // a panicked thread fails its whole part, keeping the
835                // results aligned with `jobs`
836                handles
837                    .into_iter()
838                    .flat_map(|(n, h)| h.join().unwrap_or_else(|_| vec![false; n]))
839                    .collect()
840            })
841        } else {
842            std::thread::scope(|scope| {
843                let hs: Vec<_> = jobs
844                    .iter()
845                    .map(|&(slot, e)| {
846                        let model = model.clone();
847                        let triple = triples[e];
848                        scope.spawn(move || {
849                            crate::gpu::with_device(device, || {
850                                me.fill_slot(&model, slot, layer, e, triple)
851                            })
852                        })
853                    })
854                    .collect();
855                hs.into_iter().map(|h| h.join().unwrap_or(false)).collect()
856            })
857        };
858        FILLS_WALL_NS.fetch_add(
859            t0.elapsed().as_nanos() as u64,
860            std::sync::atomic::Ordering::Relaxed,
861        );
862        out
863    }
864
865    /// An arena slot gave up `old`: the RAM tier wants it back.
866    fn dropped(&self, old: (usize, usize)) {
867        if old.1 != usize::MAX
868            && let Some(store) = self.store.as_ref()
869        {
870            store.note_vram(old.0, old.1, false);
871        }
872    }
873
874    /// Submit what the staging ring and the host tier hold ahead of the
875    /// next frame.
876    #[cfg(feature = "gpu")]
877    pub(crate) fn flush_uploads(&mut self) {
878        if let Some(st) = self.stager.as_mut() {
879            st.flush();
880        }
881        if let Some(cb) = self.tier.as_ref().and_then(|t| t.take()) {
882            crate::gpu_wgpu::qwen4::submit_cb(cb);
883        }
884    }
885
886    /// The staged uploads as a command buffer to submit ahead of a frame
887    /// in the same queue submission; `rearm_uploads` follows the submit.
888    /// (With both a staging ring and a host tier, the ring's copies go out
889    /// on their own submission first: they target other slots.)
890    pub(crate) fn take_uploads(&mut self) -> Option<wgpu::CommandBuffer> {
891        let staged = self.stager.as_mut().and_then(|st| st.take());
892        let tiered = self.tier.as_ref().and_then(|t| t.take());
893        match (staged, tiered) {
894            (Some(a), Some(b)) => {
895                crate::gpu_wgpu::qwen4::submit_cb(a);
896                Some(b)
897            }
898            (a, b) => a.or(b),
899        }
900    }
901
902    /// A submission that followed every copy `take_uploads` handed out has
903    /// completed: the host tier may reuse the slots they read.
904    pub(crate) fn uploads_done(&self) {
905        if let Some(t) = self.tier.as_ref() {
906            t.done();
907        }
908    }
909
910    pub(crate) fn rearm_uploads(&mut self) {
911        if let Some(st) = self.stager.as_mut() {
912            st.rearm();
913        }
914    }
915
916    /// Upload a frame's cold winners into the staging slots of token slot
917    /// `tok` (rank `j` each), outside the LRU. Returns the slot per expert,
918    /// `None` where the upload failed or no staging exists.
919    pub(crate) fn stage_cold(
920        &self,
921        model: &Arc<CmfModel>,
922        layer: usize,
923        tok: usize,
924        experts: &[usize],
925        triples: &[(usize, usize, usize)],
926        top_k: usize,
927    ) -> Vec<Option<u32>> {
928        if self.staging == 0 || (tok + 1) * top_k > self.staging || triples.len() != self.n_experts
929        {
930            return vec![None; experts.len()];
931        }
932        let base = self.staging_base + tok * top_k;
933        if fill_trace() {
934            eprintln!("fill staging tok={tok} experts={experts:?}");
935        }
936        let jobs: Vec<(usize, usize)> = experts
937            .iter()
938            .enumerate()
939            .filter(|&(_, &e)| e < self.n_experts)
940            .map(|(j, &e)| (base + j, e))
941            .collect();
942        let ok = self.run_fills(model, layer, &jobs, triples, false);
943        let mut out = vec![None; experts.len()];
944        let mut ji = 0;
945        for (j, &e) in experts.iter().enumerate() {
946            if e < self.n_experts {
947                if ok[ji] {
948                    out[j] = Some(jobs[ji].0 as u32);
949                }
950                ji += 1;
951            }
952        }
953        out
954    }
955
956    /// `admit_now` for a layer's whole cold list: slots are reserved in
957    /// order, the uploads (a 1.7 MB memcpy each) run on parallel threads.
958    /// Returns one slot per expert, `None` where the arena could not take it.
959    pub(crate) fn admit_many(
960        &mut self,
961        model: &Arc<CmfModel>,
962        layer: usize,
963        experts: &[usize],
964        triples: &[(usize, usize, usize)],
965        protect: &[usize],
966    ) -> Vec<Option<u32>> {
967        if triples.len() != self.n_experts {
968            return vec![None; experts.len()];
969        }
970        let plan = self.reserve_many(layer, experts, protect);
971        let uploads: Vec<(usize, usize)> = plan
972            .iter()
973            .filter_map(|(_, p)| p.filter(|&(_, e)| e != usize::MAX))
974            .collect();
975        let failed = self.upload_reserved(model, layer, &uploads, triples, false);
976        Self::plan_slots(plan, &failed)
977    }
978
979    /// Phase 1 of an admission: a slot per expert, reserved in order (the
980    /// bookkeeping is serial). `(expert, Some((slot, expert)))` for a new
981    /// reservation that still needs its upload, `(expert, Some((slot,
982    /// usize::MAX)))` for one already resident, `(expert, None)` where the
983    /// arena could not take it.
984    fn reserve_many(
985        &mut self,
986        layer: usize,
987        experts: &[usize],
988        protect: &[usize],
989    ) -> Vec<(usize, Option<(usize, usize)>)> {
990        let mut plan: Vec<(usize, Option<(usize, usize)>)> = Vec::with_capacity(experts.len());
991        for &expert in experts {
992            if expert >= self.n_experts {
993                plan.push((expert, None));
994                continue;
995            }
996            let key = layer * self.n_experts + expert;
997            if self.slot_for[key] != u32::MAX {
998                plan.push((expert, Some((self.slot_for[key] as usize, usize::MAX))));
999                continue;
1000            }
1001            let Some(victim) = self.victim(layer, protect) else {
1002                plan.push((expert, None));
1003                continue;
1004            };
1005            // take the slot now so a later expert of this list cannot pick it
1006            let old = self.owner[victim].take();
1007            if let Some(old) = old {
1008                self.dropped(old);
1009                if old.1 == usize::MAX {
1010                    self.shared_slot[old.0] = u32::MAX;
1011                } else {
1012                    self.slot_for[old.0 * self.n_experts + old.1] = u32::MAX;
1013                    self.occupancy[old.0] = self.occupancy[old.0].saturating_sub(1);
1014                }
1015            }
1016            self.owner[victim] = Some((layer, expert));
1017            self.slot_for[key] = victim as u32;
1018            self.occupancy[layer] += 1;
1019            self.seen[key] = self.seen[key].max(1);
1020            self.clock = self.clock.saturating_add(1);
1021            self.last[victim] = self.clock;
1022            if fill_trace() {
1023                eprintln!("fill cold layer={layer} expert={expert} slot={victim}");
1024            }
1025            plan.push((expert, Some((victim, expert))));
1026        }
1027        plan
1028    }
1029
1030    /// Phase 2: the reserved uploads (a 1.7 MB memcpy each) on parallel
1031    /// threads; a failed one rolls its reservation back. `pooled` spreads
1032    /// them over at most `ADMIT_THREADS` threads instead of one thread per
1033    /// expert. Returns the experts that failed.
1034    fn upload_reserved(
1035        &mut self,
1036        model: &Arc<CmfModel>,
1037        layer: usize,
1038        uploads: &[(usize, usize)],
1039        triples: &[(usize, usize, usize)],
1040        pooled: bool,
1041    ) -> Vec<usize> {
1042        if let Some(store) = self.store.as_ref() {
1043            let es: Vec<usize> = uploads.iter().map(|&(_, e)| e).collect();
1044            store.prefetch(layer, &es);
1045        }
1046        // host-tier hits are a queued copy each: no thread for those
1047        let mut hit = vec![false; uploads.len()];
1048        if let Some(t) = self.tier.as_ref() {
1049            for (i, &(slot, e)) in uploads.iter().enumerate() {
1050                if t.hit(layer * self.n_experts + e, slot) {
1051                    hit[i] = true;
1052                    if slot < self.staging_base
1053                        && let Some(store) = self.store.as_ref()
1054                    {
1055                        store.note_vram(layer, e, true);
1056                    }
1057                }
1058            }
1059        }
1060        let all = uploads;
1061        let rest: Vec<(usize, usize)> = all
1062            .iter()
1063            .zip(&hit)
1064            .filter(|(_, h)| !**h)
1065            .map(|(&u, _)| u)
1066            .collect();
1067        let results = self.run_fills(model, layer, &rest, triples, pooled);
1068        let mut rest_ok = results.into_iter();
1069        let results: Vec<bool> = hit
1070            .iter()
1071            .map(|&h| h || rest_ok.next().unwrap_or(false))
1072            .collect();
1073        let uploads = all;
1074        let mut failed: Vec<usize> = Vec::new();
1075        for (i, &(slot, e)) in uploads.iter().enumerate() {
1076            if !results.get(i).copied().unwrap_or(false) {
1077                // roll the reservation back: the slot holds nothing usable now
1078                self.owner[slot] = None;
1079                self.slot_for[layer * self.n_experts + e] = u32::MAX;
1080                self.occupancy[layer] = self.occupancy[layer].saturating_sub(1);
1081                self.free.push(slot);
1082                failed.push(e);
1083            }
1084        }
1085        failed
1086    }
1087
1088    fn plan_slots(plan: Vec<(usize, Option<(usize, usize)>)>, failed: &[usize]) -> Vec<Option<u32>> {
1089        plan.into_iter()
1090            .map(|(expert, p)| match p {
1091                Some((slot, _)) if !failed.contains(&expert) => Some(slot as u32),
1092                _ => None,
1093            })
1094            .collect()
1095    }
1096
1097    /// `admit_many` for every token of a frame at once: the slots are
1098    /// reserved token by token in order, exactly as consecutive
1099    /// `admit_many` calls reserve them (same victims, same slots), and all
1100    /// the frame's uploads then run in one parallel batch instead of one
1101    /// small batch per token. `reqs`: each token's cold experts and its
1102    /// protected picks. (`CMF_QWEN_ADMIT_BATCH=0`: per token.)
1103    pub(crate) fn admit_frame(
1104        &mut self,
1105        model: &Arc<CmfModel>,
1106        layer: usize,
1107        reqs: &[(Vec<usize>, Vec<usize>)],
1108        triples: &[(usize, usize, usize)],
1109    ) -> Vec<Vec<Option<u32>>> {
1110        if triples.len() != self.n_experts {
1111            return reqs.iter().map(|(e, _)| vec![None; e.len()]).collect();
1112        }
1113        let plans: Vec<_> = reqs
1114            .iter()
1115            .map(|(experts, protect)| self.reserve_many(layer, experts, protect))
1116            .collect();
1117        // A slot reserved twice in the frame (a later token evicted an
1118        // earlier token's fresh admission) ends up holding the later
1119        // expert, as with per-token admission: upload only that one.
1120        let mut uploads: Vec<(usize, usize)> = Vec::new();
1121        for plan in &plans {
1122            for &(_, p) in plan {
1123                if let Some((slot, e)) = p.filter(|&(_, e)| e != usize::MAX) {
1124                    uploads.retain(|&(s, _)| s != slot);
1125                    uploads.push((slot, e));
1126                }
1127            }
1128        }
1129        let failed = self.upload_reserved(model, layer, &uploads, triples, true);
1130        plans
1131            .into_iter()
1132            .map(|plan| Self::plan_slots(plan, &failed))
1133            .collect()
1134    }
1135
1136    /// Fill free slots with the given `(layer, expert)` pairs in order, the
1137    /// hottest first, until the arena is full. Returns how many went in.
1138    pub(crate) fn prefill(
1139        &mut self,
1140        model: &Arc<CmfModel>,
1141        ranked: &[(usize, usize)],
1142        triples: &[Vec<(usize, usize, usize)>],
1143    ) -> usize {
1144        if let Some(store) = self.store.clone() {
1145            return self.prefill_parallel(model, ranked, triples, &store);
1146        }
1147        let mut n = 0;
1148        for &(layer, expert) in ranked {
1149            if self.free.is_empty() {
1150                break;
1151            }
1152            if layer >= triples.len() || expert >= self.n_experts {
1153                continue;
1154            }
1155            let key = layer * self.n_experts + expert;
1156            if self.slot_for[key] != u32::MAX {
1157                continue;
1158            }
1159            let Some(&triple) = triples[layer].get(expert) else {
1160                continue;
1161            };
1162            let Some(slot) = self.free.pop() else { break };
1163            if !crate::gpu_wgpu::dsv4_global_slot_fill(model, slot, triple) {
1164                self.free.push(slot);
1165                break;
1166            }
1167            self.owner[slot] = Some((layer, expert));
1168            self.slot_for[key] = slot as u32;
1169            self.occupancy[layer] += 1;
1170            self.seen[key] = self.seen[key].max(1);
1171            self.clock = self.clock.saturating_add(1);
1172            self.last[slot] = self.clock;
1173            n += 1;
1174        }
1175        n
1176    }
1177
1178    /// `prefill` through the host tiers: the slots are planned serially, the
1179    /// reads and uploads run on many threads (a fast drive needs a deep
1180    /// queue), and the queue is flushed every few hundred experts so wgpu's
1181    /// write staging never holds the whole arena at once.
1182    fn prefill_parallel(
1183        &mut self,
1184        model: &Arc<CmfModel>,
1185        ranked: &[(usize, usize)],
1186        triples: &[Vec<(usize, usize, usize)>],
1187        store: &Arc<crate::expert_store::ExpertStore>,
1188    ) -> usize {
1189        let mut plan: Vec<(usize, usize, usize)> = Vec::new();
1190        for &(layer, expert) in ranked {
1191            if layer >= triples.len() || expert >= self.n_experts {
1192                continue;
1193            }
1194            let key = layer * self.n_experts + expert;
1195            if self.slot_for[key] != u32::MAX {
1196                continue;
1197            }
1198            let Some(slot) = self.free.pop() else { break };
1199            plan.push((slot, layer, expert));
1200        }
1201        let threads = std::thread::available_parallelism()
1202            .map_or(8, |n| n.get())
1203            .clamp(4, 32);
1204        let next = std::sync::atomic::AtomicUsize::new(0);
1205        let done: Vec<std::sync::atomic::AtomicBool> = (0..plan.len())
1206            .map(|_| std::sync::atomic::AtomicBool::new(false))
1207            .collect();
1208        std::thread::scope(|scope| {
1209            for _ in 0..threads {
1210                scope.spawn(|| {
1211                    loop {
1212                        let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1213                        let Some(&(slot, layer, expert)) = plan.get(i) else {
1214                            break;
1215                        };
1216                        // into VRAM only: a RAM copy of an arena expert is
1217                        // redundant (the background loader takes the rest)
1218                        let ok = store
1219                            .with_expert(layer, expert, false, |parts| {
1220                                crate::gpu_wgpu::qwen4::upload_expert_parts(
1221                                    None, model, slot, parts,
1222                                )
1223                            })
1224                            .unwrap_or(false);
1225                        done[i].store(ok, std::sync::atomic::Ordering::Relaxed);
1226                        if i % 384 == 383 {
1227                            crate::gpu_wgpu::qwen4::flush_writes();
1228                        }
1229                    }
1230                });
1231            }
1232        });
1233        crate::gpu_wgpu::qwen4::flush_writes();
1234        let mut n = 0;
1235        for (i, &(slot, layer, expert)) in plan.iter().enumerate() {
1236            if !done[i].load(std::sync::atomic::Ordering::Relaxed) {
1237                self.free.push(slot);
1238                continue;
1239            }
1240            let key = layer * self.n_experts + expert;
1241            self.owner[slot] = Some((layer, expert));
1242            self.slot_for[key] = slot as u32;
1243            self.occupancy[layer] += 1;
1244            self.seen[key] = self.seen[key].max(1);
1245            self.clock = self.clock.saturating_add(1);
1246            self.last[slot] = self.clock;
1247            store.note_vram(layer, expert, true);
1248            n += 1;
1249        }
1250        n
1251    }
1252
1253    /// The live `(layer, expert) → slot` row and the layer's pinned shared
1254    /// slot, without admitting anything.
1255    pub(crate) fn remap_snapshot(&self, layer: usize) -> (Vec<u32>, u32) {
1256        let base = layer * self.n_experts;
1257        (
1258            self.slot_for[base..base + self.n_experts].to_vec(),
1259            self.shared_slot.get(layer).copied().unwrap_or(u32::MAX),
1260        )
1261    }
1262
1263    /// DeepSeek V4.1 uses the same segmented, model-wide bank as Qwen's
1264    /// dynamic MoE path, but its routed experts are Q4TP in the production
1265    /// profile.  Keep the policy knob architecture-specific while sharing
1266    /// the allocator and LRU implementation.
1267    pub(crate) fn create_for_dsv41(
1268        model: &Arc<CmfModel>,
1269        inter: usize,
1270        hidden: usize,
1271        n_layers: usize,
1272        n_experts: usize,
1273        gu_q2: bool,
1274    ) -> Option<Self> {
1275        Self::create_with_policy(
1276            model,
1277            inter,
1278            hidden,
1279            n_layers,
1280            n_experts,
1281            gu_q2,
1282            "CMF_DSV41_POOL_PCT",
1283            75,
1284            25,
1285            85,
1286            "CMF_DSV41_EXPERT_SLOTS",
1287            "CMF_DSV41_FETCH_MAX",
1288            "CMF_DSV41_FETCH_MIN_SEEN",
1289            None,
1290            0,
1291        )
1292    }
1293
1294    /// GLM-5.3-Flash's Q2 expert arena shares the card with a much larger
1295    /// static attention/control footprint and with transient cold-expert
1296    /// staging allocations.  Keep that model-specific policy here while
1297    /// reusing the same LRU/cache machinery as Qwen.  The lower default is
1298    /// intentionally applied before the common allocator's workspace carve
1299    /// out; it treats the configured budget as a physical envelope rather
1300    /// than as a promise that the whole budget may become resident weights.
1301    pub(crate) fn create_for_glm(
1302        model: &Arc<CmfModel>,
1303        inter: usize,
1304        hidden: usize,
1305        n_layers: usize,
1306        n_experts: usize,
1307        gu_q2: bool,
1308    ) -> Option<Self> {
1309        Self::create_with_policy(
1310            model,
1311            inter,
1312            hidden,
1313            n_layers,
1314            n_experts,
1315            gu_q2,
1316            "CMF_GLM_POOL_PCT",
1317            40,
1318            20,
1319            40,
1320            "CMF_GLM_EXPERT_SLOTS",
1321            "CMF_GLM_FETCH_MAX",
1322            "CMF_GLM_FETCH_MIN_SEEN",
1323            None,
1324            0,
1325        )
1326    }
1327
1328    fn create_with_policy(
1329        model: &Arc<CmfModel>,
1330        inter: usize,
1331        hidden: usize,
1332        n_layers: usize,
1333        n_experts: usize,
1334        gu_q2: bool,
1335        pct_env: &str,
1336        default_pct: usize,
1337        min_pct: usize,
1338        max_pct: usize,
1339        slots_env: &str,
1340        fetch_max_env: &'static str,
1341        fetch_min_env: &'static str,
1342        explicit_slots: Option<usize>,
1343        staging: usize,
1344    ) -> Option<Self> {
1345        if !crate::gpu_wgpu::dsv4_global_moe_supported() {
1346            return None;
1347        }
1348        let gu = cortiq_core::quant::expected_nbytes(
1349            if gu_q2 {
1350                TensorDtype::Q2TiledP
1351            } else {
1352                TensorDtype::Q4TiledP
1353            },
1354            &[inter, hidden],
1355        )?;
1356        let dn = cortiq_core::quant::expected_nbytes(
1357            cortiq_core::TensorDtype::Q4TiledP,
1358            &[hidden, inter],
1359        )?;
1360        let per = 2usize.checked_mul(gu)?.checked_add(dn)?;
1361        let budget = crate::gpu_wgpu::dsv4_vram_budget()? as usize;
1362        // The Q8_2f attention/GDN skeleton, f32 HyperConnection projections,
1363        // KV/state and the full-vocabulary head live next to this arena. The
1364        // common allocator subtracts another 2-4 GiB workspace below this
1365        // request. Qwen's 75% profile preserves locality. GLM has a separate
1366        // physical-card envelope: the measured RTX-3090 budget can use the
1367        // full 100% request (the allocator subtracts workspace below), while
1368        // the 16-GB compatibility profile stays at the proven 40% cap. The
1369        // global allocator still subtracts its workspace reserve and refuses
1370        // an unsafe allocation.
1371        let max_pct = if pct_env == "CMF_GLM_POOL_PCT" && budget >= 20_000_000_000 {
1372            // A 24-GiB card has room for the measured static trunk plus a
1373            // larger expert arena.  The allocator below still subtracts its
1374            // workspace reserve and rounds the result, while the 16-GiB
1375            // compatibility budget remains capped at the conservative 40%.
1376            100
1377        } else {
1378            max_pct
1379        };
1380        let default_pct = if pct_env == "CMF_GLM_POOL_PCT" && budget >= 20_000_000_000 {
1381            100
1382        } else {
1383            default_pct
1384        };
1385        let pool_pct = bounded_pool_pct(
1386            std::env::var(pct_env).ok().as_deref(),
1387            default_pct,
1388            min_pct,
1389            max_pct,
1390        );
1391        let safe_requested = budget.saturating_mul(pool_pct) / 100 / per.max(1);
1392        // GLM's explicit slot knob is still subject to the same
1393        // physical-envelope cap as its percentage policy.  This prevents
1394        // `CMF_GPU_VRAM_MB=16000` plus an oversized slot override from
1395        // recreating pod-7's physical OOM.  Qwen retains its established
1396        // operator-controlled slot override semantics.
1397        let bounded_override = pct_env == "CMF_GLM_POOL_PCT" || pct_env == "CMF_DSV41_POOL_PCT";
1398        let requested = match explicit_slots {
1399            Some(n) => n.max(1),
1400            None => bounded_pool_slots(
1401                std::env::var(slots_env).ok().as_deref(),
1402                safe_requested,
1403                bounded_override,
1404            ),
1405        };
1406        let (capacity, segment_slots) = if pct_env == "CMF_DSV41_POOL_PCT" {
1407            crate::gpu_wgpu::dsv4_global_moe_create_for_dsv41(
1408                model, requested, inter, hidden, gu_q2,
1409            )?
1410        } else if explicit_slots.is_some() && pct_env == "CMF_QWEN_POOL_PCT" {
1411            // the device path: the caller already left its reserve and the
1412            // workspace (see the arena sizing in `forward_tokens_device`)
1413            crate::gpu_wgpu::dsv4_global_moe_create_s8_exact(
1414                model, requested, inter, hidden, gu_q2,
1415            )?
1416        } else if pct_env == "CMF_QWEN_MTP_BANK" {
1417            // the draft head's bank: exactly its experts, no workspace
1418            // carve-out (the main arena already left the card's reserve)
1419            crate::gpu_wgpu::dsv4_global_moe_create_slots(model, requested, inter, hidden, gu_q2)?
1420        } else {
1421            // Generic Qwen/GLM/DSV4 callers keep the established S8 bank.
1422            crate::gpu_wgpu::dsv4_global_moe_create(model, requested, inter, hidden, gu_q2)?
1423        };
1424        let (auto_quota, auto_min_seen) = crate::gpu_wgpu::dsv4_fetch_defaults();
1425        let fetch_quota = std::env::var(fetch_max_env)
1426            .ok()
1427            .and_then(|v| v.parse::<usize>().ok())
1428            .unwrap_or(auto_quota);
1429        // GLM routes through a 45-layer sweep with a larger cold-expert
1430        // penalty. Q2's mixed profile waits for three observations so
1431        // one-shot routes do not trigger a synchronous upload; Q4 keeps the
1432        // first-recurrence policy because its larger rows make CPU misses
1433        // more expensive. Qwen retains its established hysteresis.
1434        let glm_policy = pct_env == "CMF_GLM_POOL_PCT";
1435        let fetch_min_seen = std::env::var(fetch_min_env)
1436            .ok()
1437            .and_then(|v| v.parse::<u16>().ok())
1438            // Repair-8's first-recurrence policy is the measured GLM
1439            // baseline. Repair-9's Q2-specific three-observation gate
1440            // regressed throughput and is intentionally removed here;
1441            // persistent-device scheduling must not be substituted by an
1442            // admission heuristic.
1443            .unwrap_or(if glm_policy || pct_env == "CMF_DSV41_POOL_PCT" {
1444                1
1445            } else {
1446                auto_min_seen.max(2)
1447            });
1448        if std::env::var_os("CMF_QWEN_PROF").is_some()
1449            || std::env::var_os("CMF_GLM_PROF").is_some()
1450            || std::env::var_os("CMF_DSV41_PROF").is_some()
1451        {
1452            eprintln!(
1453                "dynamic-pool capacity={capacity} segment_slots={segment_slots} requested={requested} pct={pool_pct} env={pct_env} fetch_quota={fetch_quota} min_seen={fetch_min_seen} gu={}",
1454                if gu_q2 { "q2tp" } else { "q4tp" }
1455            );
1456        }
1457        let staging = staging.min(capacity / 2);
1458        let staging_base = capacity - staging;
1459        Some(Self {
1460            hold_layers: [None, None],
1461            staging_base,
1462            staging,
1463            #[cfg(feature = "gpu")]
1464            stager: None,
1465            #[cfg(feature = "gpu")]
1466            tier: None,
1467            admit_pool: None,
1468            store: None,
1469            segment_slots,
1470            floor: (staging_base / n_layers.max(1)).max(2),
1471            n_experts,
1472            fetch_quota,
1473            fetch_min_seen,
1474            fetch_max_env,
1475            fetch_min_env,
1476            owner: vec![None; capacity],
1477            slot_for: vec![u32::MAX; n_layers.checked_mul(n_experts)?],
1478            shared_slot: vec![u32::MAX; n_layers],
1479            seen: vec![0; n_layers.checked_mul(n_experts)?],
1480            seen_epoch: vec![0; n_layers.checked_mul(n_experts)?],
1481            free: (0..staging_base).rev().collect(),
1482            occupancy: vec![0; n_layers.max(1)],
1483            last: vec![0; capacity],
1484            clock: 0,
1485        })
1486    }
1487
1488    /// Reserve slots for an admission without starting its potentially slow
1489    /// fills.  Normal callers should use [`Self::ensure`]; this split form is
1490    /// only for an exact caller that can overlap independent, discardable CPU
1491    /// work with the fills and validates the completed remap before use.
1492    pub(crate) fn begin_ensure<'pool, 'input>(
1493        &'pool mut self,
1494        model: &'input Arc<CmfModel>,
1495        layer: usize,
1496        picks: &'input [usize],
1497        triples: &'input [(usize, usize, usize)],
1498        shared: Option<(usize, usize, usize)>,
1499    ) -> Option<QwenGpuPendingEnsure<'pool, 'input>> {
1500        self.clock = self.clock.saturating_add(1);
1501        let now = self.clock;
1502        let shared_slot = if let Some(triple) = shared {
1503            let slot = *self.shared_slot.get(layer)?;
1504            if slot != u32::MAX {
1505                self.last[slot as usize] = now;
1506                slot
1507            } else {
1508                let slot = self.free.pop().or_else(|| {
1509                    self.owner
1510                        .iter()
1511                        .enumerate()
1512                        // A shared expert is pinned for the model lifetime.
1513                        .filter(|(_, o)| o.is_some_and(|(_, e)| e != usize::MAX))
1514                        .min_by_key(|(slot, _)| self.last[*slot])
1515                        .map(|(slot, _)| slot)
1516                })?;
1517                if !crate::gpu_wgpu::dsv4_global_slot_fill(model, slot, triple) {
1518                    if self.owner[slot].is_none() {
1519                        self.free.push(slot);
1520                    }
1521                    return None;
1522                }
1523                if let Some(old) = self.owner[slot] {
1524                    if old.1 == usize::MAX {
1525                        self.shared_slot[old.0] = u32::MAX;
1526                    } else {
1527                        self.slot_for[old.0 * self.n_experts + old.1] = u32::MAX;
1528                        self.occupancy[old.0] = self.occupancy[old.0].saturating_sub(1);
1529                    }
1530                }
1531                self.owner[slot] = Some((layer, usize::MAX));
1532                self.shared_slot[layer] = slot as u32;
1533                self.occupancy[layer] += 1;
1534                self.last[slot] = now;
1535                slot as u32
1536            }
1537        } else {
1538            0
1539        };
1540        // The HashMap implementation decayed every observed key by scanning
1541        // the whole table each 64 layer calls. With a dense 48×512 table that
1542        // scan is unnecessary: apply the same shifts lazily when a key is
1543        // next touched. Admission only examines current picks, so behaviour is
1544        // identical while idle experts cost zero work.
1545        let epoch = (now / 64).min(u32::MAX as u64) as u32;
1546        let base = layer.checked_mul(self.n_experts)?;
1547        for &expert in picks {
1548            if expert >= self.n_experts {
1549                return None;
1550            }
1551            let key = base + expert;
1552            let delta = epoch.saturating_sub(self.seen_epoch[key]);
1553            self.seen[key] = if delta >= u16::BITS {
1554                0
1555            } else {
1556                self.seen[key] >> delta
1557            };
1558            self.seen_epoch[key] = epoch;
1559            self.seen[key] = self.seen[key].saturating_add(1);
1560            let slot = self.slot_for[key];
1561            if slot != u32::MAX {
1562                self.last[slot as usize] = now;
1563            }
1564        }
1565        let quota = std::env::var(self.fetch_max_env)
1566            .ok()
1567            .and_then(|v| v.parse::<usize>().ok())
1568            .unwrap_or(self.fetch_quota);
1569        let min_seen = std::env::var(self.fetch_min_env)
1570            .ok()
1571            .and_then(|v| v.parse::<u16>().ok())
1572            .unwrap_or(self.fetch_min_seen);
1573        // phase 1: reserve a slot per prefetch (the bookkeeping is serial)
1574        let mut plan: Vec<(usize, usize)> = Vec::new();
1575        for &expert in picks {
1576            let key = base + expert;
1577            if self.slot_for[key] != u32::MAX || plan.len() >= quota || self.seen[key] < min_seen {
1578                continue;
1579            }
1580            if triples.get(expert).is_none() {
1581                return None;
1582            }
1583            let victim = self.victim(layer, picks)?;
1584            if let Some(old) = self.owner[victim] {
1585                self.dropped(old);
1586                if old.1 == usize::MAX {
1587                    self.shared_slot[old.0] = u32::MAX;
1588                } else {
1589                    self.slot_for[old.0 * self.n_experts + old.1] = u32::MAX;
1590                    self.occupancy[old.0] = self.occupancy[old.0].saturating_sub(1);
1591                }
1592            }
1593            self.owner[victim] = Some((layer, expert));
1594            self.slot_for[key] = victim as u32;
1595            self.occupancy[layer] += 1;
1596            self.last[victim] = now;
1597            if fill_trace() {
1598                eprintln!(
1599                    "fill prefetch layer={layer} expert={expert} slot={victim} seen={}",
1600                    self.seen[key]
1601                );
1602            }
1603            plan.push((victim, expert));
1604        }
1605        Some(QwenGpuPendingEnsure {
1606            pool: Some(self),
1607            model,
1608            picks,
1609            triples,
1610            layer,
1611            base,
1612            shared_slot,
1613            plan,
1614        })
1615    }
1616
1617    pub(crate) fn ensure(
1618        &mut self,
1619        model: &Arc<CmfModel>,
1620        layer: usize,
1621        picks: &[usize],
1622        triples: &[(usize, usize, usize)],
1623        shared: Option<(usize, usize, usize)>,
1624    ) -> Option<(Vec<u32>, u32)> {
1625        self.begin_ensure(model, layer, picks, triples, shared)?.finish()
1626    }
1627}
1628
1629fn err(s: impl Into<String>) -> CmfError {
1630    CmfError::Parse(format!("qwen4_exp: {}", s.into()))
1631}
1632
1633fn f(model: &CmfModel, name: &str) -> Result<Vec<f32>, CmfError> {
1634    load_f32(model, name, &Overlay::None).map_err(err)
1635}
1636
1637fn t(model: &Arc<CmfModel>, name: &str) -> Result<QTensor, CmfError> {
1638    load_matrix(model, name, false, &Overlay::None)
1639}
1640
1641fn load_hc(model: &Arc<CmfModel>, prefix: &str, inject: bool) -> Result<GatedResidual, CmfError> {
1642    Ok(GatedResidual {
1643        norm: f(model, &format!("{prefix}hc_norm.weight"))?,
1644        down: t(model, &format!("{prefix}input_mix_weight_down.weight"))?,
1645        up: t(model, &format!("{prefix}input_mix_weight_up.weight"))?,
1646        inject: inject
1647            .then(|| t(model, &format!("{prefix}block_inject_weight.weight")))
1648            .transpose()?,
1649        down_idx: model.tensor_index(&format!("{prefix}input_mix_weight_down.weight")),
1650        up_idx: model.tensor_index(&format!("{prefix}input_mix_weight_up.weight")),
1651        inject_idx: inject
1652            .then(|| model.tensor_index(&format!("{prefix}block_inject_weight.weight")))
1653            .flatten(),
1654    })
1655}
1656
1657impl GatedResidual {
1658    fn idxs(&self) -> Vec<usize> {
1659        let mut v = Vec::new();
1660        v.extend(self.down_idx);
1661        v.extend(self.up_idx);
1662        v.extend(self.inject_idx);
1663        v
1664    }
1665}
1666
1667/// Resolve the device frame's matrices for one layer by name.
1668fn layer_idx(model: &CmfModel, p: &str, is_gdn: bool, has_ple: bool) -> Option<LayerIdx> {
1669    let ix = |n: String| model.tensor_index(&n);
1670    let gdn = if is_gdn {
1671        let q = format!("{p}linear_attn.");
1672        Some([
1673            ix(format!("{q}in_proj_qkv.weight"))?,
1674            ix(format!("{q}in_proj_z.weight"))?,
1675            ix(format!("{q}in_proj_a.weight"))?,
1676            ix(format!("{q}in_proj_b.weight"))?,
1677            ix(format!("{q}out_proj.weight"))?,
1678        ])
1679    } else {
1680        None
1681    };
1682    let qsa = if !is_gdn {
1683        let q = format!("{p}self_attn.");
1684        Some([
1685            ix(format!("{q}q_proj.weight"))?,
1686            ix(format!("{q}k_proj.weight"))?,
1687            ix(format!("{q}v_proj.weight"))?,
1688            ix(format!("{q}o_proj.weight"))?,
1689            ix(format!("{q}indexer.index_qk_proj.weight"))?,
1690        ])
1691    } else {
1692        None
1693    };
1694    let ple = if has_ple {
1695        Some((
1696            ix(format!("{p}ple.key_proj.weight"))?,
1697            ix(format!("{p}ple.value_proj.weight"))?,
1698        ))
1699    } else {
1700        None
1701    };
1702    Some(LayerIdx {
1703        gdn,
1704        qsa,
1705        ple,
1706        router: ix(format!("{p}mlp.gate.weight"))?,
1707        shared_gate: ix(format!("{p}mlp.shared_expert_gate.weight")),
1708    })
1709}
1710
1711fn load_gdn(model: &Arc<CmfModel>, prefix: &str) -> Result<GdnWeights, CmfError> {
1712    Ok(GdnWeights {
1713        in_proj_qkv: t(model, &format!("{prefix}in_proj_qkv.weight"))?,
1714        in_proj_z: t(model, &format!("{prefix}in_proj_z.weight"))?,
1715        in_proj_a: t(model, &format!("{prefix}in_proj_a.weight"))?,
1716        in_proj_b: t(model, &format!("{prefix}in_proj_b.weight"))?,
1717        conv1d: f(model, &format!("{prefix}conv1d.weight"))?,
1718        a_log: f(model, &format!("{prefix}A_log"))?,
1719        dt_bias: f(model, &format!("{prefix}dt_bias"))?,
1720        norm: f(model, &format!("{prefix}norm.weight"))?,
1721        out_proj: t(model, &format!("{prefix}out_proj.weight"))?,
1722    })
1723}
1724
1725fn load_qsa(model: &Arc<CmfModel>, prefix: &str) -> Result<QsaWeights, CmfError> {
1726    Ok(QsaWeights {
1727        q_proj: t(model, &format!("{prefix}q_proj.weight"))?,
1728        k_proj: t(model, &format!("{prefix}k_proj.weight"))?,
1729        v_proj: t(model, &format!("{prefix}v_proj.weight"))?,
1730        o_proj: t(model, &format!("{prefix}o_proj.weight"))?,
1731        q_norm: f(model, &format!("{prefix}q_norm.weight"))?,
1732        k_norm: f(model, &format!("{prefix}k_norm.weight"))?,
1733        index_qk: t(model, &format!("{prefix}indexer.index_qk_proj.weight"))?,
1734        index_q_norm: f(model, &format!("{prefix}indexer.q_norm.weight"))
1735            .or_else(|_| f(model, &format!("{prefix}indexer.q_layernorm.weight")))?,
1736        index_k_norm: f(model, &format!("{prefix}indexer.k_norm.weight"))
1737            .or_else(|_| f(model, &format!("{prefix}indexer.k_layernorm.weight")))?,
1738    })
1739}
1740
1741fn splitmix64(mut value: u64) -> u64 {
1742    value = value.wrapping_add(SPLITMIX_GAMMA);
1743    value = (value ^ (value >> 30)).wrapping_mul(SPLITMIX_M1);
1744    value = (value ^ (value >> 27)).wrapping_mul(SPLITMIX_M2);
1745    value ^ (value >> 31)
1746}
1747
1748fn is_prime(value: usize) -> bool {
1749    if value < 2 {
1750        return false;
1751    }
1752    if value.is_multiple_of(2) {
1753        return value == 2;
1754    }
1755    let mut d = 3usize;
1756    while d <= value / d {
1757        if value.is_multiple_of(d) {
1758            return false;
1759        }
1760        d += 2;
1761    }
1762    true
1763}
1764
1765fn nth_prime_after(start: usize, count: usize) -> usize {
1766    let mut p = start;
1767    for _ in 0..count {
1768        p += 1;
1769        while !is_prime(p) {
1770            p += 1;
1771        }
1772    }
1773    p
1774}
1775
1776fn ple_tables(
1777    qc: &Qwen4ExpConfig,
1778    vocab: usize,
1779    ple_index: usize,
1780) -> (Vec<i64>, Vec<i64>, Vec<i64>) {
1781    let max_mul = i64::MAX as u64 / vocab.max(1) as u64;
1782    let half_bound = (max_mul / 2).max(1);
1783    let base = qc.seed.wrapping_add(PRIME_1.wrapping_mul(ple_index as u64));
1784    let multipliers = (0..qc.ngram_size)
1785        .map(|i| {
1786            let v = base.wrapping_add(SPLITMIX_GAMMA.wrapping_mul((i + 1) as u64));
1787            (2 * (splitmix64(v) % half_bound) + 1) as i64
1788        })
1789        .collect();
1790    let heads = (qc.ngram_size - 1) * qc.heads_per_ngram;
1791    let mut sizes = Vec::with_capacity(heads);
1792    let mut offsets = Vec::with_capacity(heads);
1793    let mut off = 0i64;
1794    for head in 0..heads {
1795        let global = ple_index * heads + head;
1796        let sz = nth_prime_after(qc.ngram_vocab_size_base - 1, global + 1) as i64;
1797        sizes.push(sz);
1798        offsets.push(off);
1799        off += sz;
1800    }
1801    (multipliers, sizes, offsets)
1802}
1803
1804fn load_ple(
1805    model: &Arc<CmfModel>,
1806    prefix: &str,
1807    qc: &Qwen4ExpConfig,
1808    vocab: usize,
1809    ple_index: usize,
1810) -> Result<PleWeights, CmfError> {
1811    let mut shards = Vec::with_capacity(qc.split_ngram_parts);
1812    for si in 0..qc.split_ngram_parts {
1813        shards.push(t(
1814            model,
1815            &format!("{prefix}ple_embedding.ngram_embedding.shard_{si}.weight"),
1816        )?);
1817    }
1818    let first = shards
1819        .first()
1820        .ok_or_else(|| err("PLE has no embedding shards"))?;
1821    let (rows_per_shard, row_dim) = (first.rows(), first.cols());
1822    if shards
1823        .iter()
1824        .any(|s| s.rows() != rows_per_shard || s.cols() != row_dim)
1825    {
1826        return Err(err("PLE embedding shard shapes disagree"));
1827    }
1828    let (multipliers, vocab_sizes, offsets) = ple_tables(qc, vocab, ple_index);
1829    Ok(PleWeights {
1830        shards,
1831        rows_per_shard,
1832        row_dim,
1833        key_proj: t(model, &format!("{prefix}key_proj.weight"))?,
1834        value_proj: t(model, &format!("{prefix}value_proj.weight"))?,
1835        norm_key: f(model, &format!("{prefix}norm_key.weight"))?,
1836        norm_query: f(model, &format!("{prefix}norm_query.weight"))?,
1837        norm_conv: f(model, &format!("{prefix}norm_conv.weight"))?,
1838        conv: f(model, &format!("{prefix}conv1d.weight"))?,
1839        multipliers,
1840        vocab_sizes,
1841        offsets,
1842    })
1843}
1844
1845/// Load the dedicated stack. All large matrices remain mmap-backed QTensor
1846/// views, so loading does not allocate a second copy of the model.
1847pub fn load(
1848    model: &Arc<CmfModel>,
1849    arch: &ModelArch,
1850) -> Result<(Globals, Vec<Layer>, Cfg, State), CmfError> {
1851    let qc = arch
1852        .qwen4_exp
1853        .as_ref()
1854        .ok_or_else(|| err("missing qwen4_exp descriptor"))?;
1855    if qc.hc_count == 0 || qc.indexer_compress_ratio == 0 || qc.ngram_size < 2 {
1856        return Err(err("invalid zero geometry"));
1857    }
1858    let gdn = GdnCfg {
1859        num_v_heads: arch.linear_num_value_heads.unwrap_or(48),
1860        num_k_heads: arch.linear_num_key_heads.unwrap_or(16),
1861        key_head_dim: arch.linear_key_head_dim.unwrap_or(128),
1862        value_head_dim: arch.linear_value_head_dim.unwrap_or(128),
1863        conv_kernel: arch.linear_conv_kernel_dim.unwrap_or(4),
1864        hidden_size: arch.hidden_size,
1865        rms_eps: arch.rms_norm_eps,
1866        output_gate_sigmoid: true,
1867    };
1868    let eos = model
1869        .header
1870        .tokenizer_config
1871        .as_ref()
1872        // The generation bundle lists both <|im_end|> and <|endoftext|>.
1873        // PLE segmentation uses text_config.eos_token_id, which for this
1874        // release is the pad/BOS id (248044), not the first generation stop.
1875        .and_then(|tc| {
1876            tc.pad_token_id
1877                .or(tc.bos_token_id)
1878                .or_else(|| tc.eos_token_ids.last().copied())
1879        })
1880        .unwrap_or(248_044);
1881    let cfg = Cfg {
1882        hidden: arch.hidden_size,
1883        hc: qc.hc_count,
1884        eps: arch.rms_norm_eps,
1885        n_heads: arch.num_attention_heads,
1886        n_kv_heads: arch.num_kv_heads,
1887        head_dim: arch.head_dim,
1888        rotary_dim: ((arch.head_dim as f32 * arch.partial_rotary_factor) as usize).max(2),
1889        index_heads: qc.indexer_n_heads,
1890        index_kv_heads: qc.indexer_kv_heads,
1891        index_dim: qc.indexer_head_dim,
1892        index_budget: qc.indexer_budget,
1893        compress_ratio: qc.indexer_compress_ratio,
1894        gdn,
1895        ngram_size: qc.ngram_size,
1896        heads_per_ngram: qc.heads_per_ngram,
1897        ple_kernel: qc.ple_conv_kernel_size,
1898        ple_dilation: qc.ngram_size,
1899        eos,
1900    };
1901    if cfg.n_heads % cfg.n_kv_heads != 0 || cfg.index_kv_heads != 1 {
1902        return Err(err("unsupported QSA head grouping"));
1903    }
1904
1905    let head_hc = load_hc(model, "model.hyper_connection_mixer.", false)?;
1906    let lm_head_idx = model.tensor_index("lm_head.weight");
1907    let embed_idx = model.tensor_index("model.embed_tokens.weight");
1908    let mut skeleton_idxs: Vec<usize> = head_hc.idxs();
1909    skeleton_idxs.extend(lm_head_idx);
1910    // with an MTP sidecar the draft chain re-embeds on the card: the table
1911    // joins the skeleton so the expert arena is sized around it
1912    if std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
1913        && std::env::var("CMF_QWEN_MTP_DEVICE_DRAFT").as_deref() == Ok("1")
1914    {
1915        let side = cortiq_core::mtp_sidecar_path(&model.path);
1916        if side != model.path && side.exists() {
1917            skeleton_idxs.extend(embed_idx);
1918        }
1919    }
1920    let globals_partial = (
1921        t(model, "model.embed_tokens.weight")?,
1922        t(model, "lm_head.weight")?,
1923        head_hc,
1924    );
1925    let mut layers = Vec::with_capacity(arch.num_layers);
1926    let mut ple_index = 0usize;
1927    for li in 0..arch.num_layers {
1928        let p = format!("model.layers.{li}.");
1929        let mixer = match arch.layer_types.get(li) {
1930            Some(LayerType::LinearAttention) => {
1931                Mixer::Gdn(load_gdn(model, &format!("{p}linear_attn."))?)
1932            }
1933            _ => Mixer::Qsa(load_qsa(model, &format!("{p}self_attn."))?),
1934        };
1935        let ple = if qc.ple_layer_ids.contains(&(li + 1)) {
1936            let w = load_ple(model, &format!("{p}ple."), qc, arch.vocab_size, ple_index)?;
1937            ple_index += 1;
1938            Some(w)
1939        } else {
1940            None
1941        };
1942        let moe = match build_ffn_at(model, arch, &p, false, &Overlay::None)? {
1943            FfnKind::Moe(m) => m,
1944            _ => return Err(err(format!("layer {li} is not MoE"))),
1945        };
1946        let expert_ids: Vec<_> = moe
1947            .experts
1948            .iter()
1949            .map(|e| {
1950                Some((
1951                    e.gate_proj.model_idx()?,
1952                    e.up_proj.model_idx()?,
1953                    e.down_proj.model_idx()?,
1954                ))
1955            })
1956            .collect::<Option<_>>()
1957            .ok_or_else(|| err(format!("layer {li} experts are not mmap-backed")))?;
1958        let shared_ids = moe
1959            .shared
1960            .as_ref()
1961            .and_then(|(e, _)| {
1962                Some((
1963                    e.gate_proj.model_idx()?,
1964                    e.up_proj.model_idx()?,
1965                    e.down_proj.model_idx()?,
1966                ))
1967            })
1968            .ok_or_else(|| err(format!("layer {li} shared expert is not mmap-backed")))?;
1969        let attn_hc = load_hc(model, &format!("{p}attn_hyper_connection."), true)?;
1970        let mlp_hc = load_hc(model, &format!("{p}mlp_hyper_connection."), true)?;
1971        let idx = layer_idx(model, &p, matches!(mixer, Mixer::Gdn(_)), ple.is_some());
1972        skeleton_idxs.extend(attn_hc.idxs());
1973        skeleton_idxs.extend(mlp_hc.idxs());
1974        if let Some(ix) = &idx {
1975            skeleton_idxs.extend(ix.gdn.iter().flatten().copied());
1976            skeleton_idxs.extend(ix.qsa.iter().flatten().copied());
1977            if let Some((k, v)) = ix.ple {
1978                skeleton_idxs.extend([k, v]);
1979            }
1980            skeleton_idxs.push(ix.router);
1981            skeleton_idxs.extend(ix.shared_gate);
1982        }
1983        layers.push(Layer {
1984            attn_hc,
1985            mlp_hc,
1986            mixer,
1987            moe,
1988            ple,
1989            expert_ids,
1990            shared_ids: Some(shared_ids),
1991            idx,
1992        });
1993    }
1994    let globals = Globals {
1995        embed: globals_partial.0,
1996        lm_head: globals_partial.1,
1997        head_hc: globals_partial.2,
1998        lm_head_idx,
1999        embed_idx,
2000        skeleton_idxs,
2001    };
2002    let state = State::new(layers.len());
2003    Ok((globals, layers, cfg, state))
2004}
2005
2006#[inline]
2007fn sigmoid(x: f32) -> f32 {
2008    1.0 / (1.0 + (-x).exp())
2009}
2010
2011#[inline]
2012fn silu(x: f32) -> f32 {
2013    x * sigmoid(x)
2014}
2015
2016fn group_rms_zero_into(x: &[f32], weight: &[f32], group: usize, eps: f64, out: &mut [f32]) {
2017    debug_assert_eq!(x.len(), weight.len());
2018    debug_assert_eq!(x.len(), out.len());
2019    debug_assert!(group > 0 && x.len().is_multiple_of(group));
2020    for (gi, chunk) in x.chunks(group).enumerate() {
2021        let ss = chunk.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>();
2022        let inv = (ss / group as f64 + eps).sqrt().recip() as f32;
2023        let off = gi * group;
2024        for j in 0..group {
2025            out[off + j] = chunk[j] * inv * (1.0 + weight[off + j]);
2026        }
2027    }
2028}
2029
2030fn group_rms_zero(x: &[f32], weight: &[f32], group: usize, eps: f64) -> Vec<f32> {
2031    let mut out = vec![0.0f32; x.len()];
2032    group_rms_zero_into(x, weight, group, eps, &mut out);
2033    out
2034}
2035
2036fn hc_mix(
2037    w: &GatedResidual,
2038    hyper: &[f32],
2039    cfg: &Cfg,
2040    pool: Option<&Pool>,
2041) -> (Vec<f32>, Option<Vec<f32>>) {
2042    let mut normed = crate::attention::take_buf(hyper.len());
2043    group_rms_zero_into(hyper, &w.norm, cfg.hidden, cfg.eps, &mut normed);
2044    let mut low = crate::attention::take_buf(w.down.rows());
2045    // `down` and the four-row injection gate read the same normalized
2046    // 4-stream state. Run their rows under one pool publication: on the
2047    // Qwen stack this removes 96 barriers per token while preserving each
2048    // row's exact dot-product order.
2049    let mut inject = w.inject.as_ref().map(|iw| vec![0.0f32; iw.rows()]);
2050    match (&w.inject, inject.as_mut()) {
2051        (Some(iw), Some(inj)) => {
2052            QTensor::matvec_many([&w.down, iw], &normed, [&mut low, inj], pool)
2053        }
2054        _ => w.down.matvec(&normed, &mut low, pool),
2055    }
2056    for v in &mut low {
2057        *v = silu(*v / cfg.hc as f32);
2058    }
2059    let mut mix = crate::attention::take_buf(hyper.len());
2060    w.up.matvec(&low, &mut mix, pool);
2061    let mut folded = vec![0.0f32; cfg.hidden];
2062    for stream in 0..cfg.hc {
2063        let off = stream * cfg.hidden;
2064        for d in 0..cfg.hidden {
2065            folded[d] += sigmoid(mix[off + d]) * normed[off + d] / cfg.hc as f32;
2066        }
2067    }
2068    let inject = inject.map(|mut v| {
2069        for x in &mut v {
2070            *x = 2.0 * sigmoid(*x / cfg.hc as f32);
2071        }
2072        v
2073    });
2074    crate::attention::recycle_buf(&mut mix);
2075    crate::attention::recycle_buf(&mut low);
2076    crate::attention::recycle_buf(&mut normed);
2077    (folded, inject)
2078}
2079
2080fn inject(hyper: &mut [f32], block: &[f32], weights: &[f32], cfg: &Cfg) {
2081    for stream in 0..cfg.hc {
2082        let off = stream * cfg.hidden;
2083        for d in 0..cfg.hidden {
2084            hyper[off + d] += weights[stream] * block[d];
2085        }
2086    }
2087}
2088
2089fn trace_stats(label: &str, li: usize, position: usize, values: &[f32]) {
2090    if std::env::var_os("CMF_QWEN_TRACE").is_none() {
2091        return;
2092    }
2093    let mut sumsq = 0.0f64;
2094    let mut max = 0.0f32;
2095    let mut finite = 0usize;
2096    for &v in values {
2097        if v.is_finite() {
2098            sumsq += (v as f64) * (v as f64);
2099            max = max.max(v.abs());
2100            finite += 1;
2101        }
2102    }
2103    let rms = if finite == 0 {
2104        f64::NAN
2105    } else {
2106        (sumsq / finite as f64).sqrt()
2107    };
2108    eprintln!(
2109        "qwen4_exp pos={position} layer={li:02} {label}: rms={rms:.6} max={max:.6} finite={finite}/{}",
2110        values.len()
2111    );
2112}
2113
2114fn dump_values(label: &str, li: usize, position: usize, values: &[f32]) {
2115    let Some(root) = std::env::var_os("CMF_QWEN_DUMP") else {
2116        return;
2117    };
2118    if let Ok(wanted) = std::env::var("CMF_QWEN_DUMP_LAYER")
2119        && wanted.parse::<usize>().ok() != Some(li)
2120    {
2121        return;
2122    }
2123    let root = std::path::PathBuf::from(root);
2124    if std::fs::create_dir_all(&root).is_err() {
2125        return;
2126    }
2127    let bytes = unsafe {
2128        std::slice::from_raw_parts(values.as_ptr().cast::<u8>(), std::mem::size_of_val(values))
2129    };
2130    let _ = std::fs::write(
2131        root.join(format!("p{position:06}_l{li:02}_{label}.f32")),
2132        bytes,
2133    );
2134}
2135
2136/// `CMF_QWEN_DEVICE_TAP=<layer>`: the host path's intermediates of that
2137/// layer, kept for the device path to compare against (check mode).
2138static TAP: std::sync::Mutex<Vec<(String, usize, usize, Vec<f32>)>> =
2139    std::sync::Mutex::new(Vec::new());
2140
2141fn tap_layer() -> Option<usize> {
2142    std::env::var("CMF_QWEN_DEVICE_TAP").ok()?.parse().ok()
2143}
2144
2145fn observe(label: &str, li: usize, position: usize, values: &[f32]) {
2146    if tap_layer() == Some(li) && position < 3 {
2147        TAP.lock()
2148            .unwrap()
2149            .push((label.to_string(), li, position, values.to_vec()));
2150    }
2151    trace_stats(label, li, position, values);
2152    dump_values(label, li, position, values);
2153}
2154
2155fn rms_zero_head(v: &mut [f32], weight: &[f32], head_dim: usize, eps: f64) {
2156    for head in v.chunks_mut(head_dim) {
2157        let ss = head.iter().map(|&x| (x as f64) * (x as f64)).sum::<f64>();
2158        let inv = (ss / head_dim as f64 + eps).sqrt().recip() as f32;
2159        for (x, &w) in head.iter_mut().zip(weight) {
2160            *x *= inv * (1.0 + w);
2161        }
2162    }
2163}
2164
2165fn rope(v: &mut [f32], pos: usize, inv_freq: &[f32], rotary_dim: usize) {
2166    let rd = rotary_dim.min(v.len()).min(inv_freq.len() * 2);
2167    let half = rd / 2;
2168    for i in 0..half {
2169        let a = (pos as f32 * inv_freq[i]).cos();
2170        let b = (pos as f32 * inv_freq[i]).sin();
2171        let x1 = v[i];
2172        let x2 = v[i + half];
2173        v[i] = x1 * a - x2 * b;
2174        v[i + half] = x2 * a + x1 * b;
2175    }
2176}
2177
2178fn selected_tokens(
2179    q: &[f32],
2180    raw_keys: &[f32],
2181    npos: usize,
2182    w: &QsaWeights,
2183    cfg: &Cfg,
2184    inv_freq: &[f32],
2185) -> Vec<usize> {
2186    let cr = cfg.compress_ratio;
2187    let complete = npos / cr;
2188    let mut scores = Vec::with_capacity(complete);
2189    for block in 0..complete {
2190        let mut k = vec![0.0f32; cfg.index_dim];
2191        for ti in block * cr..(block + 1) * cr {
2192            let src = &raw_keys[ti * cfg.index_dim..(ti + 1) * cfg.index_dim];
2193            for (d, &x) in src.iter().enumerate() {
2194                k[d] += x / cr as f32;
2195            }
2196        }
2197        rms_zero_head(&mut k, &w.index_k_norm, cfg.index_dim, cfg.eps);
2198        rope(
2199            &mut k,
2200            block * cr,
2201            inv_freq,
2202            cfg.rotary_dim.min(cfg.index_dim),
2203        );
2204        let mut s = 0.0f32;
2205        for h in 0..cfg.index_heads {
2206            let qh = &q[h * cfg.index_dim..(h + 1) * cfg.index_dim];
2207            let dot = qh.iter().zip(&k).map(|(&a, &b)| a * b).sum::<f32>();
2208            s += dot.max(0.0);
2209        }
2210        scores.push((block, s / (cfg.index_dim as f32).sqrt()));
2211    }
2212    let keep = (cfg.index_budget / cr).min(complete);
2213    if complete > keep {
2214        scores.select_nth_unstable_by(keep, |a, b| {
2215            b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal)
2216        });
2217        scores.truncate(keep);
2218    }
2219    let mut out = Vec::with_capacity(keep * cr + cr.saturating_sub(1));
2220    for (block, _) in scores {
2221        out.extend(block * cr..(block + 1) * cr);
2222    }
2223    out.extend(complete * cr..npos);
2224    out
2225}
2226
2227fn qsa_forward(
2228    x: &[f32],
2229    w: &QsaWeights,
2230    cfg: &Cfg,
2231    st: &mut QsaState,
2232    position: usize,
2233    inv_freq: &[f32],
2234    pool: Option<&Pool>,
2235) -> Vec<f32> {
2236    let mut iqk =
2237        crate::attention::take_buf((cfg.index_heads + cfg.index_kv_heads) * cfg.index_dim);
2238    let mut qg = crate::attention::take_buf(cfg.n_heads * cfg.head_dim * 2);
2239    let mut k = crate::attention::take_buf(cfg.n_kv_heads * cfg.head_dim);
2240    let mut v = crate::attention::take_buf(cfg.n_kv_heads * cfg.head_dim);
2241    // Indexer QK and attention Q/K/V all read the same folded state. Their
2242    // q8-family rows are independent, so publish one virtual row range to
2243    // the pool instead of four back-to-back barriers.
2244    QTensor::matvec_many(
2245        [&w.index_qk, &w.q_proj, &w.k_proj, &w.v_proj],
2246        x,
2247        [&mut iqk, &mut qg, &mut k, &mut v],
2248        pool,
2249    );
2250    let qlen = cfg.index_heads * cfg.index_dim;
2251    let mut iq = crate::attention::take_buf(qlen);
2252    iq.copy_from_slice(&iqk[..qlen]);
2253    rms_zero_head(&mut iq, &w.index_q_norm, cfg.index_dim, cfg.eps);
2254    for h in 0..cfg.index_heads {
2255        rope(
2256            &mut iq[h * cfg.index_dim..(h + 1) * cfg.index_dim],
2257            position,
2258            inv_freq,
2259            cfg.rotary_dim.min(cfg.index_dim),
2260        );
2261    }
2262    st.raw_keys.extend_from_slice(&iqk[qlen..]);
2263
2264    let mut q = crate::attention::take_buf(cfg.n_heads * cfg.head_dim);
2265    let mut gate = crate::attention::take_buf(q.len());
2266    for h in 0..cfg.n_heads {
2267        let src = h * cfg.head_dim * 2;
2268        let dst = h * cfg.head_dim;
2269        q[dst..dst + cfg.head_dim].copy_from_slice(&qg[src..src + cfg.head_dim]);
2270        gate[dst..dst + cfg.head_dim]
2271            .copy_from_slice(&qg[src + cfg.head_dim..src + cfg.head_dim * 2]);
2272    }
2273    rms_zero_head(&mut q, &w.q_norm, cfg.head_dim, cfg.eps);
2274    rms_zero_head(&mut k, &w.k_norm, cfg.head_dim, cfg.eps);
2275    for h in 0..cfg.n_heads {
2276        rope(
2277            &mut q[h * cfg.head_dim..(h + 1) * cfg.head_dim],
2278            position,
2279            inv_freq,
2280            cfg.rotary_dim,
2281        );
2282    }
2283    for h in 0..cfg.n_kv_heads {
2284        rope(
2285            &mut k[h * cfg.head_dim..(h + 1) * cfg.head_dim],
2286            position,
2287            inv_freq,
2288            cfg.rotary_dim,
2289        );
2290    }
2291    st.keys.extend_from_slice(&k);
2292    st.values.extend_from_slice(&v);
2293    let npos = position + 1;
2294    let selected = selected_tokens(&iq, &st.raw_keys, npos, w, cfg, inv_freq);
2295    let groups = cfg.n_heads / cfg.n_kv_heads;
2296    let scale = (cfg.head_dim as f32).sqrt().recip();
2297    let mut merged = crate::attention::take_buf(cfg.n_heads * cfg.head_dim);
2298    for qh in 0..cfg.n_heads {
2299        let kvh = qh / groups;
2300        let qs = &q[qh * cfg.head_dim..(qh + 1) * cfg.head_dim];
2301        let mut scores = Vec::with_capacity(selected.len());
2302        let mut max = f32::NEG_INFINITY;
2303        for &ti in &selected {
2304            let ko = (ti * cfg.n_kv_heads + kvh) * cfg.head_dim;
2305            let s = qs
2306                .iter()
2307                .zip(&st.keys[ko..ko + cfg.head_dim])
2308                .map(|(&a, &b)| a * b)
2309                .sum::<f32>()
2310                * scale;
2311            max = max.max(s);
2312            scores.push(s);
2313        }
2314        let z = scores.iter().map(|&s| (s - max).exp()).sum::<f32>();
2315        let out = &mut merged[qh * cfg.head_dim..(qh + 1) * cfg.head_dim];
2316        for (&ti, &score) in selected.iter().zip(&scores) {
2317            let p = (score - max).exp() / z.max(f32::MIN_POSITIVE);
2318            let vo = (ti * cfg.n_kv_heads + kvh) * cfg.head_dim;
2319            for d in 0..cfg.head_dim {
2320                out[d] += p * st.values[vo + d];
2321            }
2322        }
2323        let go = qh * cfg.head_dim;
2324        for d in 0..cfg.head_dim {
2325            out[d] *= sigmoid(gate[go + d]);
2326        }
2327    }
2328    let mut out = vec![0.0f32; cfg.hidden];
2329    w.o_proj.matvec(&merged, &mut out, pool);
2330    crate::attention::recycle_buf(&mut merged);
2331    crate::attention::recycle_buf(&mut gate);
2332    crate::attention::recycle_buf(&mut q);
2333    crate::attention::recycle_buf(&mut iq);
2334    crate::attention::recycle_buf(&mut v);
2335    crate::attention::recycle_buf(&mut k);
2336    crate::attention::recycle_buf(&mut qg);
2337    crate::attention::recycle_buf(&mut iqk);
2338    out
2339}
2340
2341fn shifted_token(history: &[u32], current: u32, shift: usize, eos: u32) -> u32 {
2342    if shift == 0 {
2343        return current;
2344    }
2345    if history.len() < shift {
2346        return eos;
2347    }
2348    let source = history.len() - shift;
2349    if history[source + 1..].contains(&eos) || history.last() == Some(&eos) {
2350        eos
2351    } else {
2352        history[source]
2353    }
2354}
2355
2356fn ple_embedding(w: &PleWeights, cfg: &Cfg, history: &[u32], token: u32) -> Vec<f32> {
2357    let ids = ple_row_ids(w, cfg, history, token);
2358    let mut out = vec![0.0f32; ids.len() * w.row_dim];
2359    for (hi, &id) in ids.iter().enumerate() {
2360        ple_row(w, id, &mut out[hi * w.row_dim..(hi + 1) * w.row_dim]);
2361    }
2362    out
2363}
2364
2365/// One n-gram table row, dequantized into `dst` (`row_dim` long).
2366fn ple_row(w: &PleWeights, id: i64, dst: &mut [f32]) {
2367    let global = id as usize;
2368    let shard = global / w.rows_per_shard;
2369    let local = global % w.rows_per_shard;
2370    debug_assert!(shard < w.shards.len());
2371    w.shards[shard].row_f32(local, dst);
2372}
2373
2374/// The table rows a token's n-gram heads read, in head order.
2375fn ple_row_ids(w: &PleWeights, cfg: &Cfg, history: &[u32], token: u32) -> Vec<i64> {
2376    let shifted: Vec<i64> = (0..cfg.ngram_size)
2377        .map(|s| shifted_token(history, token, s, cfg.eos) as i64)
2378        .collect();
2379    let mut ids = Vec::with_capacity((cfg.ngram_size - 1) * cfg.heads_per_ngram);
2380    for ngram in 2..=cfg.ngram_size {
2381        let mut mixed = shifted[0].wrapping_mul(w.multipliers[0]);
2382        for p in 1..ngram {
2383            mixed ^= shifted[p].wrapping_mul(w.multipliers[p]);
2384        }
2385        let h0 = (ngram - 2) * cfg.heads_per_ngram;
2386        for hi in h0..h0 + cfg.heads_per_ngram {
2387            ids.push(mixed.rem_euclid(w.vocab_sizes[hi]) + w.offsets[hi]);
2388        }
2389    }
2390    ids
2391}
2392
2393/// The n-gram rows of every PLE layer for every token of a frame, each
2394/// against its own history: `[layer][token]` → heads × `row_dim`. The rows
2395/// are scattered over a ~25 GB table, so on a host whose page cache does
2396/// not hold the file every row is a disk read; read one after another they
2397/// cost tens of milliseconds per token. They are independent, so the pool
2398/// reads them all at once.
2399fn ple_frame_rows(
2400    layers: &[Layer],
2401    cfg: &Cfg,
2402    history: &[u32],
2403    ids: &[u32],
2404    pool: Option<&Pool>,
2405) -> Vec<Vec<Vec<f32>>> {
2406    let mut out: Vec<Vec<Vec<f32>>> = Vec::with_capacity(layers.len());
2407    // (layer, token, head, row id), and the destination of each row
2408    let mut jobs: Vec<(usize, usize, usize, i64)> = Vec::new();
2409    let mut hist: Vec<u32> = history.to_vec();
2410    for (li, l) in layers.iter().enumerate() {
2411        let Some(pw) = l.ple.as_ref() else {
2412            out.push(Vec::new());
2413            continue;
2414        };
2415        hist.truncate(history.len());
2416        let mut rows = Vec::with_capacity(ids.len());
2417        for (t, &id) in ids.iter().enumerate() {
2418            let rid = ple_row_ids(pw, cfg, &hist, id);
2419            rows.push(vec![0.0f32; rid.len() * pw.row_dim]);
2420            jobs.extend(rid.into_iter().enumerate().map(|(h, r)| (li, t, h, r)));
2421            hist.push(id);
2422        }
2423        out.push(rows);
2424    }
2425    // the tables alone: a layer also holds host-side caches that are not Sync
2426    let tables: Vec<Option<&PleWeights>> = layers.iter().map(|l| l.ple.as_ref()).collect();
2427    let dst: Vec<crate::pool::SendMut> = jobs
2428        .iter()
2429        .map(|&(li, t, h, _)| {
2430            let dim = tables[li].map_or(0, |w| w.row_dim);
2431            crate::pool::SendMut::new(out[li][t][h * dim..].as_mut_ptr())
2432        })
2433        .collect();
2434    let read = |s: usize, e: usize| {
2435        for j in s..e {
2436            let (li, _, _, id) = jobs[j];
2437            let Some(w) = tables[li] else {
2438                continue;
2439            };
2440            // SAFETY: every job owns a distinct `row_dim` slice of `out`,
2441            // which outlives the dispatch.
2442            let row = unsafe { std::slice::from_raw_parts_mut(dst[j].at(0), w.row_dim) };
2443            ple_row(w, id, row);
2444        }
2445    };
2446    match pool {
2447        Some(p) if jobs.len() > 1 => p.run_rows(jobs.len(), &read),
2448        _ => read(0, jobs.len()),
2449    }
2450    out
2451}
2452
2453fn ple_forward(
2454    hyper: &[f32],
2455    token: u32,
2456    history: &[u32],
2457    w: &PleWeights,
2458    cfg: &Cfg,
2459    st: &mut LayerState,
2460    pool: Option<&Pool>,
2461) -> Vec<f32> {
2462    let emb = ple_embedding(w, cfg, history, token);
2463    let mut key = vec![0.0f32; cfg.hc * cfg.hidden];
2464    let mut value = vec![0.0f32; cfg.hidden];
2465    w.key_proj.matvec(&emb, &mut key, pool);
2466    w.value_proj.matvec(&emb, &mut value, pool);
2467    let key = group_rms_zero(&key, &w.norm_key, cfg.hidden, cfg.eps);
2468    let query = group_rms_zero(hyper, &w.norm_query, cfg.hidden, cfg.eps);
2469    let mut gated = vec![0.0f32; cfg.hc * cfg.hidden];
2470    for stream in 0..cfg.hc {
2471        let off = stream * cfg.hidden;
2472        let dot = key[off..off + cfg.hidden]
2473            .iter()
2474            .zip(&query[off..off + cfg.hidden])
2475            .map(|(&a, &b)| a * b)
2476            .sum::<f32>()
2477            / (cfg.hidden as f32).sqrt();
2478        let signed_root = dot.signum() * dot.abs().max(1e-6).sqrt();
2479        let g = sigmoid(signed_root);
2480        for d in 0..cfg.hidden {
2481            gated[off + d] = g * value[d];
2482        }
2483    }
2484    let normed = group_rms_zero(&gated, &w.norm_conv, cfg.hidden, cfg.eps);
2485    let hist_cap = (cfg.ple_kernel - 1) * cfg.ple_dilation;
2486    let width = gated.len();
2487    let mut conv = vec![0.0f32; width];
2488    for channel in 0..width {
2489        let mut sum = w.conv[channel * cfg.ple_kernel + cfg.ple_kernel - 1] * normed[channel];
2490        for tap in 0..cfg.ple_kernel - 1 {
2491            let lag = (cfg.ple_kernel - 1 - tap) * cfg.ple_dilation;
2492            if lag <= st.ple_history_rows {
2493                let row = st.ple_history_rows - lag;
2494                sum +=
2495                    w.conv[channel * cfg.ple_kernel + tap] * st.ple_history[row * width + channel];
2496            }
2497        }
2498        conv[channel] = silu(sum);
2499    }
2500    if hist_cap > 0 {
2501        if st.ple_history_rows < hist_cap {
2502            st.ple_history.extend_from_slice(&normed);
2503            st.ple_history_rows += 1;
2504        } else {
2505            st.ple_history.copy_within(width.., 0);
2506            let off = (hist_cap - 1) * width;
2507            st.ple_history[off..off + width].copy_from_slice(&normed);
2508        }
2509    }
2510    for (o, &g) in conv.iter_mut().zip(&gated) {
2511        *o += g;
2512    }
2513    conv
2514}
2515
2516#[cfg(feature = "gpu")]
2517fn dynamic_moe_gpu(
2518    layer: &Layer,
2519    li: usize,
2520    x: &[f32],
2521    state: &mut State,
2522    pool: Option<&Pool>,
2523) -> Option<(Vec<f32>, Vec<usize>)> {
2524    let m = &layer.moe;
2525    if !crate::gpu::enabled_here()
2526        || m.router_sigmoid
2527        || !m.norm_topk_prob
2528        || (m.routed_scaling - 1.0).abs() > 1e-9
2529        || m.per_expert_scale.is_some()
2530        || m.route_tau.is_some()
2531        || m.mask.is_some()
2532    {
2533        return None;
2534    }
2535    let model = m.experts.first()?.gate_proj.model_arc()?;
2536    let gu_q2 = m
2537        .experts
2538        .first()
2539        .is_some_and(|e| e.gate_proj.model_dtype() == Some(TensorDtype::Q2TiledP));
2540    let dynamic_mode = std::env::var("CMF_QWEN_DYNAMIC_MOE").ok();
2541    let dynamic_enabled = match dynamic_mode.as_deref() {
2542        Some("1") => true,
2543        Some("0") => false,
2544        // Q2 gate/up experts are small enough for a useful model-wide cache
2545        // on a 16 GB class card. Q4 moves twice as much cold data and measured
2546        // substantially slower than its CPU/GPU auto plan, so it remains
2547        // opt-in. Unsupported backends and smaller cards fall through to the
2548        // exact CPU path; this also makes the same artifact safe on Metal.
2549        None | Some("auto") => {
2550            gu_q2
2551                && crate::gpu_wgpu::dsv4_global_moe_supported()
2552                && crate::gpu_wgpu::dsv4_vram_budget().is_some_and(|b| b >= 14_000_000_000)
2553        }
2554        Some(_) => false,
2555    };
2556    if !dynamic_enabled {
2557        return None;
2558    }
2559    if state.gpu_pool.is_none() {
2560        state.gpu_pool = QwenGpuPool::create(
2561            &model,
2562            m.experts.first()?.gate_proj.rows(),
2563            x.len(),
2564            state.layers.len(),
2565            m.experts.len(),
2566            gu_q2,
2567        );
2568    }
2569    let mut logits = vec![0.0f32; m.experts.len()];
2570    m.router.matvec(x, &mut logits, pool);
2571    let (picks, probabilities, wsum) = crate::pipeline::moe_route(&logits, m, None);
2572    let (remap, shared_slot) =
2573        state
2574            .gpu_pool
2575            .as_mut()?
2576            .ensure(&model, li, &picks, &layer.expert_ids, layer.shared_ids)?;
2577    let shared_weight = m.shared.as_ref().map_or(1.0, |(_, gate)| {
2578        gate.as_ref().map_or(1.0, |gate| {
2579            let mut y = [0.0f32; 1];
2580            gate.matvec(x, &mut y, pool);
2581            sigmoid(y[0])
2582        })
2583    });
2584    let has_shared = m.shared.is_some();
2585    let mut mix_weights = vec![0.0f32; m.experts.len()];
2586    for &expert in &picks {
2587        mix_weights[expert] =
2588            probabilities[expert] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[expert]);
2589    }
2590    let cold_ids: Vec<_> = picks
2591        .iter()
2592        .copied()
2593        .filter(|&expert| remap[expert] == u32::MAX)
2594        .collect();
2595    let cold_jobs: Vec<_> = cold_ids
2596        .iter()
2597        .copied()
2598        .map(|expert| (&m.experts[expert], mix_weights[expert]))
2599        .collect();
2600    let gp = state.gpu_pool.as_ref()?;
2601    let weights = crate::gpu_wgpu::Dsv4MoeW {
2602        router: &[],
2603        experts: &layer.expert_ids,
2604        logits: &logits,
2605        // With forced ids this is a weight table, not selection bias. The
2606        // preweighted flag keeps the shader from exponentiating/reducing it.
2607        bias: Some(&mix_weights),
2608        mask: None,
2609        forced: Some(&picks),
2610        remap: Some(&remap),
2611        global: Some(crate::gpu_wgpu::Dsv4GlobalMoe {
2612            pool_uid: model.uid(),
2613            shared_slot,
2614            segment_slots: gp.segment_slots as u32,
2615        }),
2616        has_shared,
2617        shared_weight,
2618        preweighted: true,
2619        qwen_softmax: true,
2620    };
2621    let geom = crate::gpu_wgpu::Dsv4MoeGeom {
2622        hidden: x.len(),
2623        inter: m.experts.first()?.gate_proj.rows(),
2624        top_k: m.top_k,
2625        route_scale: 1.0,
2626        swiglu_limit: 0.0,
2627        gu_q2,
2628        bf16: false,
2629    };
2630    let mut out = vec![0.0f32; x.len()];
2631    let mut cold = Vec::new();
2632    let mut cold_x = Vec::new();
2633    let (frame_ok, mut cold_cpu) = std::thread::scope(|scope| {
2634        let cpu = (!cold_jobs.is_empty())
2635            .then(|| scope.spawn(|| crate::pipeline::moe_cold_experts_cpu(&cold_jobs, x, pool)));
2636        let ok = crate::gpu_wgpu::dsv4_moe_frame(
2637            &model,
2638            &weights,
2639            geom,
2640            x,
2641            &mut cold,
2642            &mut cold_x,
2643            None,
2644            None,
2645            &mut out,
2646        );
2647        let early = cpu
2648            .map(|job| job.join().ok())
2649            .flatten()
2650            .unwrap_or_else(|| vec![0.0; x.len()]);
2651        (ok, early)
2652    });
2653    if !frame_ok
2654        || cold.len() != cold_jobs.len()
2655        || cold.iter().map(|&(e, _)| e).ne(cold_ids.iter().copied())
2656    {
2657        return None;
2658    }
2659    // The GPU returned the cold ids as a contract check; the CPU work used
2660    // the exact host route and ran concurrently with the resident kernels.
2661    for (o, c) in out.iter_mut().zip(&cold_cpu) {
2662        *o += c;
2663    }
2664    crate::attention::recycle_buf(&mut cold_cpu);
2665    Some((out, picks))
2666}
2667
2668/// Decode one token and return full-vocabulary logits. The state is entirely
2669/// host-owned; GPU use is opportunistic per projection and therefore safe to
2670/// change between requests or even between layers.
2671pub fn forward_token(
2672    globals: &Globals,
2673    layers: &[Layer],
2674    cfg: &Cfg,
2675    state: &mut State,
2676    token_id: u32,
2677    position: usize,
2678    inv_freq: &[f32],
2679    pool: Option<&Pool>,
2680    logits: &mut Vec<f32>,
2681    want_logits: bool,
2682) {
2683    #[cfg(feature = "gpu")]
2684    if forward_token_device(
2685        globals,
2686        layers,
2687        cfg,
2688        state,
2689        token_id,
2690        position,
2691        inv_freq,
2692        pool,
2693        logits,
2694        want_logits,
2695    ) {
2696        return;
2697    }
2698    let prof = std::env::var_os("CMF_QWEN_PROF").is_some();
2699    #[cfg(target_arch = "x86_64")]
2700    if position == 0
2701        && std::env::var_os("CMF_POOL_SPIN").is_none()
2702        && let Some(workers) = pool
2703    {
2704        // Qwen4Exp publishes hundreds of short HC/router/q8 jobs per token.
2705        // On the 20-worker 4090 pod, letting workers park between them costs
2706        // 3.5 tok/s; 200k bounded spins sustains 7.8-7.9. Scale with the real
2707        // pool and cap it. ARM keeps the established 4k default (200k was a
2708        // measured regression on Apple silicon); an explicit env always wins.
2709        workers.set_spin_budget((workers.n_workers() * 10_000).clamp(30_000, 200_000));
2710    }
2711    #[cfg(feature = "gpu")]
2712    let gpu_moe_before = if prof {
2713        use std::sync::atomic::Ordering;
2714        Some((
2715            crate::gpu_wgpu::MOE_ENC_NS.load(Ordering::Relaxed),
2716            crate::gpu_wgpu::MOE_WAIT_NS.load(Ordering::Relaxed),
2717            crate::gpu_wgpu::MOE_GPU_NS[0].load(Ordering::Relaxed),
2718            crate::gpu_wgpu::MOE_GPU_N.load(Ordering::Relaxed),
2719            crate::gpu_wgpu::DSV4_FILLS.load(Ordering::Relaxed),
2720            crate::gpu_wgpu::DSV4_FILL_BYTES.load(Ordering::Relaxed),
2721        ))
2722    } else {
2723        None
2724    };
2725    let token_t0 = std::time::Instant::now();
2726    let mut ple_dt = std::time::Duration::ZERO;
2727    let mut attn_hc_dt = std::time::Duration::ZERO;
2728    let mut mixer_dt = std::time::Duration::ZERO;
2729    let mut mlp_hc_dt = std::time::Duration::ZERO;
2730    let mut moe_dt = std::time::Duration::ZERO;
2731    if position == 0 || state.pos != position {
2732        state.reset();
2733    }
2734    if state.host_pos != position && state.token_history.len() == position {
2735        // The device path ran positions host_pos..position and then turned
2736        // off (a refused frame): the host's K/V, GDN and PLE caches never
2737        // saw them, and attention would index keys that were never stored.
2738        // Rebuild them from the token history (slow, but the text stays
2739        // that of the device path).
2740        tracing::warn!(
2741            "qwen4: host caches rebuilt over {position} tokens after the device path turned off"
2742        );
2743        if std::env::var_os("CMF_QWEN_PROF").is_some() {
2744            eprintln!("qwen4: replaying {position} tokens on the host path");
2745        }
2746        let history = std::mem::take(&mut state.token_history);
2747        state.reset();
2748        for (p, &id) in history.iter().enumerate() {
2749            let mut lg = Vec::new();
2750            forward_token(
2751                globals, layers, cfg, state, id, p, inv_freq, pool, &mut lg, false,
2752            );
2753        }
2754    }
2755    // Every token starts from its own embedding. Only recurrent/KV/PLE
2756    // caches cross token boundaries; carrying the prior token's final hyper
2757    // state here would turn the Transformer residual into an accidental RNN.
2758    let mut emb = vec![0.0f32; cfg.hidden];
2759    if (token_id as usize) < globals.embed.rows() {
2760        globals.embed.row_f32(token_id as usize, &mut emb);
2761    }
2762    observe("embedding", 0, position, &emb);
2763    state.hyper.clear();
2764    state.hyper.reserve(cfg.hc * cfg.hidden);
2765    for _ in 0..cfg.hc {
2766        state.hyper.extend_from_slice(&emb);
2767    }
2768    for (li, layer) in layers.iter().enumerate() {
2769        // Feed the logical layer into the shared residency manager. This lets
2770        // Vulkan/DX12/Metal keep the hottest projections in VRAM while older
2771        // layers fall back to the mmap-backed CPU representation.
2772        crate::gpu::set_layer(li as i64);
2773        let st = &mut state.layers[li];
2774        if let Some(ple) = &layer.ple
2775            && std::env::var_os("CMF_QWEN_NO_PLE").is_none()
2776        {
2777            let t0 = std::time::Instant::now();
2778            let side = ple_forward(
2779                &state.hyper,
2780                token_id,
2781                &state.token_history,
2782                ple,
2783                cfg,
2784                st,
2785                pool,
2786            );
2787            for (h, s) in state.hyper.iter_mut().zip(side) {
2788                *h += s;
2789            }
2790            ple_dt += t0.elapsed();
2791            observe("post_ple", li, position, &state.hyper);
2792        }
2793        let t0 = std::time::Instant::now();
2794        let (mixed, inject_w) = hc_mix(&layer.attn_hc, &state.hyper, cfg, pool);
2795        attn_hc_dt += t0.elapsed();
2796        observe("attn_in", li, position, &mixed);
2797        let t0 = std::time::Instant::now();
2798        let block = match &layer.mixer {
2799            Mixer::Gdn(w) => gdn_forward(&mixed, w, &cfg.gdn, &mut st.gdn, pool),
2800            Mixer::Qsa(w) => qsa_forward(&mixed, w, cfg, &mut st.qsa, position, inv_freq, pool),
2801        };
2802        mixer_dt += t0.elapsed();
2803        observe("attn_out", li, position, &block);
2804        inject(
2805            &mut state.hyper,
2806            &block,
2807            inject_w.as_deref().expect("layer HC has injection"),
2808            cfg,
2809        );
2810        observe("post_attn", li, position, &state.hyper);
2811        let t0 = std::time::Instant::now();
2812        let (mixed, inject_w) = hc_mix(&layer.mlp_hc, &state.hyper, cfg, pool);
2813        mlp_hc_dt += t0.elapsed();
2814        observe("moe_in", li, position, &mixed);
2815        let t0 = std::time::Instant::now();
2816        #[cfg(feature = "gpu")]
2817        let gpu_moe = dynamic_moe_gpu(layer, li, &mixed, state, pool);
2818        #[cfg(not(feature = "gpu"))]
2819        let gpu_moe: Option<(Vec<f32>, Vec<usize>)> = None;
2820        let block = match gpu_moe {
2821            Some((block, _)) => block,
2822            None => {
2823                // `moe_ffn` already routed once. The old implementation ran
2824                // the 512×hidden router a SECOND time merely to predict a
2825                // future cache fill, but the dynamic path computes the exact
2826                // current route before dispatch and never consumed that
2827                // prediction. Removing it saves 48 matrix passes per token.
2828                moe_ffn(&layer.moe, &mixed, pool, None)
2829            }
2830        };
2831        moe_dt += t0.elapsed();
2832        observe("moe_out", li, position, &block);
2833        inject(
2834            &mut state.hyper,
2835            &block,
2836            inject_w.as_deref().expect("layer HC has injection"),
2837            cfg,
2838        );
2839        observe("post_moe", li, position, &state.hyper);
2840    }
2841    crate::gpu::set_layer(-1);
2842    let head_t0 = std::time::Instant::now();
2843    if want_logits {
2844        let (hidden, _) = hc_mix(&globals.head_hc, &state.hyper, cfg, pool);
2845        observe("head_in", layers.len(), position, &hidden);
2846        logits.resize(globals.lm_head.rows(), 0.0);
2847        globals.lm_head.matvec(&hidden, logits, pool);
2848        observe("logits", layers.len(), position, logits);
2849    } else {
2850        logits.clear();
2851    }
2852    let head_dt = head_t0.elapsed();
2853    state.token_history.push(token_id);
2854    state.pos = position + 1;
2855    state.host_pos = position + 1;
2856    if prof {
2857        eprintln!(
2858            "qwen-prof pos={position} total={:.3}s ple={:.3}s attn_hc={:.3}s mixer={:.3}s mlp_hc={:.3}s moe={:.3}s head={:.3}s",
2859            token_t0.elapsed().as_secs_f64(),
2860            ple_dt.as_secs_f64(),
2861            attn_hc_dt.as_secs_f64(),
2862            mixer_dt.as_secs_f64(),
2863            mlp_hc_dt.as_secs_f64(),
2864            moe_dt.as_secs_f64(),
2865            head_dt.as_secs_f64(),
2866        );
2867        #[cfg(feature = "gpu")]
2868        if let Some((enc0, wait0, card0, calls0, fills0, fill_bytes0)) = gpu_moe_before {
2869            use std::sync::atomic::Ordering;
2870            let enc = crate::gpu_wgpu::MOE_ENC_NS.load(Ordering::Relaxed) - enc0;
2871            let wait = crate::gpu_wgpu::MOE_WAIT_NS.load(Ordering::Relaxed) - wait0;
2872            let card = crate::gpu_wgpu::MOE_GPU_NS[0].load(Ordering::Relaxed) - card0;
2873            let calls = crate::gpu_wgpu::MOE_GPU_N.load(Ordering::Relaxed) - calls0;
2874            let fills = crate::gpu_wgpu::DSV4_FILLS.load(Ordering::Relaxed) - fills0;
2875            let fill_bytes = crate::gpu_wgpu::DSV4_FILL_BYTES.load(Ordering::Relaxed) - fill_bytes0;
2876            eprintln!(
2877                "qwen-moe-gpu pos={position} calls={calls} fills={fills} fill={:.1}MB encode={:.1}ms wait={:.1}ms card={:.1}ms",
2878                fill_bytes as f64 / 1e6,
2879                enc as f64 / 1e6,
2880                wait as f64 / 1e6,
2881                card as f64 / 1e6,
2882            );
2883        }
2884    }
2885}
2886
2887#[cfg(feature = "gpu")]
2888fn device_geom(cfg: &Cfg, moe: &MoeFfn, gu_q2: bool) -> crate::gpu_wgpu::qwen4::Geom {
2889    use crate::gpu_wgpu::qwen4::{GdnGeom, Geom};
2890    Geom {
2891        hidden: cfg.hidden,
2892        hc: cfg.hc,
2893        eps: cfg.eps as f32,
2894        n_heads: cfg.n_heads,
2895        n_kv_heads: cfg.n_kv_heads,
2896        head_dim: cfg.head_dim,
2897        rotary_dim: cfg.rotary_dim,
2898        index_heads: cfg.index_heads,
2899        index_dim: cfg.index_dim,
2900        index_budget: cfg.index_budget,
2901        compress_ratio: cfg.compress_ratio,
2902        gdn: GdnGeom {
2903            nv: cfg.gdn.num_v_heads,
2904            nk: cfg.gdn.num_k_heads,
2905            dk: cfg.gdn.key_head_dim,
2906            dv: cfg.gdn.value_head_dim,
2907            kk: cfg.gdn.conv_kernel,
2908        },
2909        ple_kernel: cfg.ple_kernel,
2910        ple_dilation: cfg.ple_dilation,
2911        top_k: moe.top_k,
2912        n_experts: moe.experts.len(),
2913        inter: moe.experts.first().map_or(0, |e| e.gate_proj.rows()),
2914        gu_q2,
2915    }
2916}
2917
2918#[cfg(feature = "gpu")]
2919fn device_layer_w<'a>(
2920    layer: &'a Layer,
2921    ix: &LayerIdx,
2922) -> Option<crate::gpu_wgpu::qwen4::LayerW<'a>> {
2923    use crate::gpu_wgpu::qwen4::{HcW, LayerW, MixerW, PleW};
2924    let hc = |g: &'a GatedResidual| -> Option<HcW<'a>> {
2925        Some(HcW {
2926            norm: &g.norm,
2927            down: g.down_idx?,
2928            up: g.up_idx?,
2929            inject: g.inject_idx,
2930        })
2931    };
2932    let mixer = match &layer.mixer {
2933        Mixer::Gdn(w) => {
2934            let [qkv, z, a, b, out] = ix.gdn?;
2935            MixerW::Gdn {
2936                qkv,
2937                z,
2938                a,
2939                b,
2940                out,
2941                conv1d: &w.conv1d,
2942                a_log: &w.a_log,
2943                dt_bias: &w.dt_bias,
2944                norm: &w.norm,
2945            }
2946        }
2947        Mixer::Qsa(w) => {
2948            let [q, k, v, o, index_qk] = ix.qsa?;
2949            MixerW::Qsa {
2950                q,
2951                k,
2952                v,
2953                o,
2954                index_qk,
2955                q_norm: &w.q_norm,
2956                k_norm: &w.k_norm,
2957                iq_norm: &w.index_q_norm,
2958                ik_norm: &w.index_k_norm,
2959            }
2960        }
2961    };
2962    let ple = match (&layer.ple, ix.ple) {
2963        (Some(w), Some((key_proj, value_proj))) => Some(PleW {
2964            key_proj,
2965            value_proj,
2966            norm_key: &w.norm_key,
2967            norm_query: &w.norm_query,
2968            norm_conv: &w.norm_conv,
2969            conv: &w.conv,
2970        }),
2971        (None, None) => None,
2972        _ => return None,
2973    };
2974    Some(LayerW {
2975        attn_hc: hc(&layer.attn_hc)?,
2976        mlp_hc: hc(&layer.mlp_hc)?,
2977        mixer,
2978        ple,
2979        router: ix.router,
2980        shared_gate: ix.shared_gate,
2981    })
2982}
2983
2984#[cfg(feature = "gpu")]
2985#[allow(clippy::type_complexity)]
2986static DEV_TAP: std::sync::Mutex<Vec<(usize, Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>)>> =
2987    std::sync::Mutex::new(Vec::new());
2988
2989#[cfg(feature = "gpu")]
2990fn tap_compare(label: &str, host: &[f32], dev: &[f32]) {
2991    let n = host.len().min(dev.len());
2992    let mut max_abs = 0.0f32;
2993    let mut at = 0usize;
2994    let (mut dot, mut na, mut nb) = (0.0f64, 0.0f64, 0.0f64);
2995    for i in 0..n {
2996        let d = (host[i] - dev[i]).abs();
2997        if d > max_abs {
2998            max_abs = d;
2999            at = i;
3000        }
3001        dot += host[i] as f64 * dev[i] as f64;
3002        na += host[i] as f64 * host[i] as f64;
3003        nb += dev[i] as f64 * dev[i] as f64;
3004    }
3005    eprintln!(
3006        "qwen4-tap {label:>10}: cos={:.6} max_abs={max_abs:.4} at {at} (host {:.4} dev {:.4}) |host|={:.3} |dev|={:.3} n={n}",
3007        dot / (na.sqrt() * nb.sqrt()).max(1e-30),
3008        host.get(at).copied().unwrap_or(0.0),
3009        dev.get(at).copied().unwrap_or(0.0),
3010        na.sqrt(),
3011        nb.sqrt()
3012    );
3013}
3014
3015/// Routing hits per `(layer, expert)` observed by the device path, and the
3016/// sidecar that carries them between runs (`CMF_QWEN_PROFILE`): a warm arena
3017/// from the first token instead of a few hundred tokens of LRU learning.
3018#[cfg(feature = "gpu")]
3019struct ExpertProfile {
3020    n_layers: usize,
3021    n_experts: usize,
3022    counts: Vec<u32>,
3023    tokens: u64,
3024}
3025
3026#[cfg(feature = "gpu")]
3027impl ExpertProfile {
3028    const MAGIC: &'static [u8; 8] = b"CMFQ4PF\0";
3029
3030    fn new(n_layers: usize, n_experts: usize) -> Self {
3031        Self {
3032            n_layers,
3033            n_experts,
3034            counts: vec![0; n_layers * n_experts],
3035            tokens: 0,
3036        }
3037    }
3038
3039    fn load(path: &str, n_layers: usize, n_experts: usize) -> Option<Self> {
3040        let bytes = std::fs::read(path).ok()?;
3041        if bytes.len() < 24 || &bytes[..8] != Self::MAGIC {
3042            return None;
3043        }
3044        let rd = |o: usize| u64::from_le_bytes(bytes[o..o + 8].try_into().unwrap());
3045        let (nl, ne) = (rd(8) as usize, rd(16) as usize);
3046        if nl != n_layers || ne != n_experts || bytes.len() < 32 + nl * ne * 4 {
3047            return None;
3048        }
3049        let tokens = rd(24);
3050        let counts = bytes[32..32 + nl * ne * 4]
3051            .chunks_exact(4)
3052            .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
3053            .collect();
3054        Some(Self {
3055            n_layers,
3056            n_experts,
3057            counts,
3058            tokens,
3059        })
3060    }
3061
3062    fn save(&self, path: &str) {
3063        let mut out = Vec::with_capacity(32 + self.counts.len() * 4);
3064        out.extend_from_slice(Self::MAGIC);
3065        out.extend_from_slice(&(self.n_layers as u64).to_le_bytes());
3066        out.extend_from_slice(&(self.n_experts as u64).to_le_bytes());
3067        out.extend_from_slice(&self.tokens.to_le_bytes());
3068        for &c in &self.counts {
3069            out.extend_from_slice(&c.to_le_bytes());
3070        }
3071        if let Err(e) = std::fs::write(path, out) {
3072            tracing::warn!("expert profile not saved to {path}: {e}");
3073        }
3074    }
3075
3076    fn note(&mut self, layer: usize, picks: &[usize]) {
3077        for &e in picks {
3078            if let Some(c) = self.counts.get_mut(layer * self.n_experts + e) {
3079                *c = c.saturating_add(1);
3080            }
3081        }
3082    }
3083
3084    /// `(layer, expert)` pairs, hottest first, zero counts left out.
3085    fn ranked(&self) -> Vec<(usize, usize)> {
3086        let mut v: Vec<(u32, usize, usize)> = self
3087            .counts
3088            .iter()
3089            .enumerate()
3090            .filter(|&(_, &c)| c > 0)
3091            .map(|(i, &c)| (c, i / self.n_experts, i % self.n_experts))
3092            .collect();
3093        v.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
3094        v.into_iter().map(|(_, l, e)| (l, e)).collect()
3095    }
3096}
3097
3098/// `CMF_QWEN_ADMIT_BATCH=0`: a multi-token frame admits its cold winners
3099/// token by token (the slots are the same either way; the batch only
3100/// runs the uploads of the whole frame in parallel).
3101#[cfg(feature = "gpu")]
3102fn admit_batch_env() -> bool {
3103    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3104    *ON.get_or_init(|| std::env::var("CMF_QWEN_ADMIT_BATCH").as_deref() != Ok("0"))
3105}
3106
3107/// Token-level profile of the device path (`CMF_QWEN_PROF`).
3108#[cfg(feature = "gpu")]
3109#[derive(Default)]
3110struct DevProf {
3111    encode: std::time::Duration,
3112    wait: std::time::Duration,
3113    cold_cpu: std::time::Duration,
3114    admit: std::time::Duration,
3115    head: std::time::Duration,
3116    cold_experts: usize,
3117    cold_dev: usize,
3118    layers_with_cold: usize,
3119    chains: usize,
3120    aborted: usize,
3121    /// finishing encoders into command buffers (encode-ahead included)
3122    finish: std::time::Duration,
3123    /// spin on the frame's fence only
3124    spin: std::time::Duration,
3125    /// from the fence to the next submit: cold parse, admissions, finalize
3126    post: std::time::Duration,
3127    /// gathering the frame's PLE n-gram rows from the mapped table
3128    ple: std::time::Duration,
3129    /// GPU stage times from the timestamp probe (`CMF_QWEN_TS=1`), ns,
3130    /// indexed by `TS_NAMES`
3131    gpu: [f64; 17],
3132    gpu_last_end: Option<f64>,
3133}
3134
3135#[cfg(feature = "gpu")]
3136const TS_NAMES: [&str; 17] = [
3137    "pending", "ple", "attn_hc", "gdn_proj", "gdn_rec", "gdn_out", "qsa_proj", "qsa_attn",
3138    "qsa_out", "moe_hc", "router", "route", "gu", "dn", "tail", "head", "idle",
3139];
3140
3141#[cfg(feature = "gpu")]
3142impl DevProf {
3143    /// Fold one frame's marks (`TS_MARKS` of them) into the stage sums.
3144    fn gpu_note(&mut self, fi: usize, n: usize, m: &[f64], gdn: Option<bool>) {
3145        if m.len() < 13 {
3146            return;
3147        }
3148        let d = |a: usize, b: usize| (m[b] - m[a]).max(0.0);
3149        if let Some(prev) = self.gpu_last_end {
3150            self.gpu[16] += (m[0] - prev).max(0.0);
3151        }
3152        self.gpu_last_end = Some(m[8]);
3153        self.gpu[0] += d(0, 1);
3154        if fi >= n {
3155            self.gpu[15] += d(7, 8);
3156            return;
3157        }
3158        self.gpu[1] += d(1, 2);
3159        self.gpu[2] += d(2, 3);
3160        let o = if gdn == Some(true) { 3 } else { 6 };
3161        self.gpu[o] += d(3, 9);
3162        self.gpu[o + 1] += d(9, 10);
3163        self.gpu[o + 2] += d(10, 4);
3164        self.gpu[9] += d(4, 5);
3165        self.gpu[10] += d(5, 11);
3166        self.gpu[11] += d(11, 6);
3167        self.gpu[12] += d(6, 12);
3168        self.gpu[13] += d(12, 7);
3169        self.gpu[14] += d(7, 8);
3170    }
3171}
3172
3173/// The whole token on the card. Returns `false` when the device path is
3174/// not available (the caller then runs the host path); a refusal after
3175/// the first token of a sequence is reported once and turns the path off.
3176/// Tokens one device frame carries; the prompt is fed in chunks of this many.
3177#[cfg(feature = "gpu")]
3178pub fn prefill_chunk() -> usize {
3179    std::env::var("CMF_QWEN_PREFILL_CHUNK")
3180        .ok()
3181        .and_then(|v| v.parse::<usize>().ok())
3182        .unwrap_or(crate::gpu_wgpu::qwen4::TMAX)
3183        .clamp(1, crate::gpu_wgpu::qwen4::TMAX)
3184}
3185
3186#[cfg(not(feature = "gpu"))]
3187pub fn prefill_chunk() -> usize {
3188    1
3189}
3190
3191/// Several consecutive tokens (`ids` at `pos0..`), logits of the last one
3192/// when `want_logits`. The device path runs them in one frame per layer;
3193/// otherwise each token takes the host path.
3194#[allow(clippy::too_many_arguments)]
3195pub fn forward_tokens(
3196    globals: &Globals,
3197    layers: &[Layer],
3198    cfg: &Cfg,
3199    state: &mut State,
3200    ids: &[u32],
3201    pos0: usize,
3202    inv_freq: &[f32],
3203    pool: Option<&Pool>,
3204    logits: &mut Vec<f32>,
3205    want_logits: bool,
3206) {
3207    #[cfg(feature = "gpu")]
3208    if !ids.is_empty()
3209        && ids.len() <= crate::gpu_wgpu::qwen4::TMAX
3210        && forward_tokens_device(
3211            globals,
3212            layers,
3213            cfg,
3214            state,
3215            ids,
3216            pos0,
3217            inv_freq,
3218            pool,
3219            logits,
3220            want_logits,
3221        )
3222    {
3223        return;
3224    }
3225    for (i, &id) in ids.iter().enumerate() {
3226        let last = i + 1 == ids.len();
3227        let mut lg = Vec::new();
3228        forward_token(
3229            globals,
3230            layers,
3231            cfg,
3232            state,
3233            id,
3234            pos0 + i,
3235            inv_freq,
3236            pool,
3237            &mut lg,
3238            want_logits && last,
3239        );
3240        if last {
3241            *logits = lg;
3242        }
3243    }
3244}
3245
3246/// GPTQ calibration capture: `ids` from position 0 through the device
3247/// frames, with the MoE input rows and router logits of layers `lo..hi`
3248/// copied out after routing. Returns per layer `[ids.len()][hidden + n_experts]`.
3249#[cfg(feature = "gpu")]
3250#[allow(clippy::too_many_arguments)]
3251pub fn capture_window(
3252    globals: &Globals,
3253    layers: &[Layer],
3254    cfg: &Cfg,
3255    state: &mut State,
3256    ids: &[u32],
3257    lo: usize,
3258    hi: usize,
3259    inv_freq: &[f32],
3260    pool: Option<&Pool>,
3261) -> Result<Vec<Vec<f32>>, String> {
3262    use crate::gpu_wgpu::qwen4 as q4;
3263    let first = layers.first().ok_or("no layers")?;
3264    let hidden = first.moe.experts.first().ok_or("no experts")?.gate_proj.cols();
3265    let ne = first.moe.experts.len();
3266    let hi = hi.min(layers.len());
3267    if lo >= hi || !q4::capture_begin(lo, hi, hidden, ne) {
3268        return Err("capture unavailable".into());
3269    }
3270    let mut out = vec![Vec::with_capacity(ids.len() * (hidden + ne)); hi - lo];
3271    let chunk = prefill_chunk();
3272    let mut pos = 0;
3273    let res = (|| {
3274        while pos < ids.len() {
3275            let end = (pos + chunk).min(ids.len());
3276            let mut lg = Vec::new();
3277            forward_tokens(globals, layers, cfg, state, &ids[pos..end], pos, inv_freq, pool, &mut lg, false);
3278            let (rows, written) = q4::capture_read(end - pos).ok_or("capture readback")?;
3279            if written < hi - lo {
3280                return Err(format!("the device path ran {written} of {} captured layer frames (host fallback?)", hi - lo));
3281            }
3282            for (k, r) in rows.into_iter().enumerate() {
3283                out[k].extend(r);
3284            }
3285            pos = end;
3286        }
3287        Ok(())
3288    })();
3289    q4::capture_end();
3290    res.map(|_| out)
3291}
3292
3293/// Directory triples (gate, up, down tensor indices) of layer `li`'s routed
3294/// experts, and of its shared expert when it has one.
3295pub fn expert_dirs(layers: &[Layer], li: usize) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
3296    let l = layers.get(li)?;
3297    Some((l.expert_ids.clone(), l.shared_ids))
3298}
3299
3300#[cfg(feature = "gpu")]
3301#[allow(clippy::too_many_arguments)]
3302fn forward_token_device(
3303    globals: &Globals,
3304    layers: &[Layer],
3305    cfg: &Cfg,
3306    state: &mut State,
3307    token_id: u32,
3308    position: usize,
3309    inv_freq: &[f32],
3310    pool: Option<&Pool>,
3311    logits: &mut Vec<f32>,
3312    want_logits: bool,
3313) -> bool {
3314    forward_tokens_device(
3315        globals,
3316        layers,
3317        cfg,
3318        state,
3319        &[token_id],
3320        position,
3321        inv_freq,
3322        pool,
3323        logits,
3324        want_logits,
3325    )
3326}
3327
3328#[cfg(feature = "gpu")]
3329#[allow(clippy::too_many_arguments)]
3330fn forward_tokens_device(
3331    globals: &Globals,
3332    layers: &[Layer],
3333    cfg: &Cfg,
3334    state: &mut State,
3335    ids: &[u32],
3336    pos0: usize,
3337    inv_freq: &[f32],
3338    pool: Option<&Pool>,
3339    logits: &mut Vec<f32>,
3340    want_logits: bool,
3341) -> bool {
3342    use crate::gpu_wgpu::qwen4 as q4;
3343    let ntok = ids.len();
3344    if ntok == 0 || ntok > q4::TMAX {
3345        return false;
3346    }
3347    let position = pos0;
3348    let token_id = ids[0];
3349    if state.device_off
3350        || std::env::var("CMF_QWEN_DEVICE").as_deref() == Ok("0")
3351        || !crate::gpu::enabled_here()
3352    {
3353        return false;
3354    }
3355    fn off(state: &mut State, why: &str) -> bool {
3356        if !state.device_off {
3357            tracing::warn!("qwen4 device path off: {why}");
3358            if std::env::var_os("CMF_QWEN_PROF").is_some() {
3359                eprintln!("qwen4-device: off ({why})");
3360            }
3361        }
3362        state.device_off = true;
3363        false
3364    }
3365    if !q4::available() {
3366        return off(state, "kernels or the global expert arena unavailable");
3367    }
3368    let Some(first) = layers.first() else {
3369        return off(state, "no layers");
3370    };
3371    let Some(e0) = first.moe.experts.first() else {
3372        return off(state, "no experts");
3373    };
3374    let Some(model) = e0.gate_proj.model_arc() else {
3375        return off(state, "experts are not mmap-backed");
3376    };
3377    let gu_q2 = e0.gate_proj.model_dtype() == Some(TensorDtype::Q2TiledP);
3378    if !gu_q2 && e0.gate_proj.model_dtype() != Some(TensorDtype::Q4TiledP) {
3379        return off(state, "expert gate/up are neither q2tp nor q4tp");
3380    }
3381    if first.moe.router_sigmoid
3382        || !first.moe.norm_topk_prob
3383        || first.moe.route_tau.is_some()
3384        || first.moe.mask.is_some()
3385        || (first.moe.routed_scaling - 1.0).abs() > 1e-9
3386    {
3387        return off(state, "non-Qwen routing settings");
3388    }
3389    let g = device_geom(cfg, &first.moe, gu_q2);
3390    let prof = std::env::var_os("CMF_QWEN_PROF").is_some();
3391    let check = std::env::var("CMF_QWEN_DEVICE_CHECK").as_deref() == Ok("1");
3392    let t_token = std::time::Instant::now();
3393
3394    // ── first use: device state, resident skeleton, the expert arena ──
3395    if state.dev.is_none() {
3396        if layers.iter().any(|l| l.idx.is_none()) || globals.lm_head_idx.is_none() {
3397            return off(state, "skeleton tensors missing from the directory");
3398        }
3399        let kinds: Vec<(bool, bool)> = layers
3400            .iter()
3401            .map(|l| (matches!(l.mixer, Mixer::Gdn(_)), l.ple.is_some()))
3402            .collect();
3403        let Some(dev) = q4::Dev::new(model.uid(), &g, &kinds) else {
3404            return off(state, "device state allocation refused");
3405        };
3406        let t0 = std::time::Instant::now();
3407        let Some(bytes) = q4::prewarm(&model, &globals.skeleton_idxs) else {
3408            return off(state, "skeleton does not fit the VRAM budget");
3409        };
3410        tracing::info!(
3411            "qwen4 device: {} skeleton tensors resident ({} MB) in {:.2}s",
3412            globals.skeleton_idxs.len(),
3413            bytes >> 20,
3414            t0.elapsed().as_secs_f64()
3415        );
3416        if prof {
3417            eprintln!(
3418                "qwen4-device: skeleton {} MB resident in {:.2}s",
3419                bytes >> 20,
3420                t0.elapsed().as_secs_f64()
3421            );
3422        }
3423        state.dev = Some(dev);
3424        state.picks_prev = vec![Vec::new(); layers.len()];
3425    }
3426    if state.gpu_pool.is_none() {
3427        // The draft head's skeleton and its 512-expert bank come first, so
3428        // the main arena is sized from what is left.
3429        if state.mtp.is_none() && !state.mtp_tried {
3430            state.mtp_tried = true;
3431            state.mtp = MtpHead::load(&model, model.arch());
3432            if let Some(head) = state.mtp.as_mut()
3433                && let Err(why) = head.setup(&g, gu_q2)
3434            {
3435                tracing::warn!("qwen4 MTP off: {why}");
3436                state.mtp = None;
3437            }
3438        }
3439        let Some(budget) = q4::vram_budget() else {
3440            return off(state, "no VRAM budget");
3441        };
3442        let resident = q4::resident_bytes();
3443        let Some(per) = QwenGpuPool::per_expert_bytes(g.inter, g.hidden, gu_q2) else {
3444            return off(state, "expert geometry");
3445        };
3446        let reserve_mb = std::env::var("CMF_QWEN_KV_RESERVE_MB")
3447            .ok()
3448            .and_then(|v| v.parse::<u64>().ok())
3449            // 768 MiB beside the allocator's own driver reserve covers the
3450            // frame scratch and f32 K/V to ~16k context; raise it for longer
3451            // sequences (`CMF_QWEN_KV_RESERVE_MB`), lower it for more experts.
3452            .unwrap_or(768);
3453        // The generic bank allocator would carve another (budget/10)
3454        // clamped to 2-4 GiB below the request, a workspace for the DSV4/GLM
3455        // paths that this path never uses: its KV and frames live in the
3456        // reserve above. Below a 24 GiB budget the carve-out stays (slot
3457        // counts as before); on a 32 GB card it would idle ~2.8 GiB, about
3458        // 1,700 expert slots. `CMF_QWEN_WORKSPACE_MB` overrides.
3459        let workspace = std::env::var("CMF_QWEN_WORKSPACE_MB")
3460            .ok()
3461            .and_then(|v| v.parse::<u64>().ok())
3462            .map(|m| m << 20)
3463            .unwrap_or(if budget >= 24 << 30 {
3464                0
3465            } else {
3466                (budget / 10).clamp(2 << 30, 4 << 30)
3467            });
3468        // The verify window's recurrent-state snapshots (`ensure_snaps`,
3469        // one row per window position on every GDN and PLE layer) are
3470        // allocated later, out of the reserve. The 768 MiB default held the
3471        // four rows of the default k = 3 on the test cards; a longer window
3472        // (k up to 7, eight rows) reserves its extra rows on top.
3473        let window = state.mtp.as_ref().map_or(0, |h| h.k + 1);
3474        let snap_extra = {
3475            let kinds: Vec<(bool, bool)> = layers
3476                .iter()
3477                .map(|l| (matches!(l.mixer, Mixer::Gdn(_)), l.ple.is_some()))
3478                .collect();
3479            q4::Dev::snap_bytes(&g, &kinds, window.saturating_sub(MTP_MEASURED_WINDOW))
3480        };
3481        let free = budget
3482            .saturating_sub(resident)
3483            .saturating_sub(reserve_mb << 20)
3484            .saturating_sub(workspace)
3485            .saturating_sub(snap_extra);
3486        let slots = (free / per as u64) as usize;
3487        let n_layers = layers.len();
3488        // staging for the cold passes: one slot per token slot and rank
3489        let staging = q4::TMAX * g.top_k;
3490        let Some(arena) = QwenGpuPool::create_explicit(
3491            &model,
3492            g.inter,
3493            g.hidden,
3494            n_layers,
3495            g.n_experts,
3496            gu_q2,
3497            slots,
3498            staging,
3499        ) else {
3500            return off(state, "expert arena allocation refused");
3501        };
3502        if prof {
3503            eprintln!(
3504                "qwen4-device: budget {} MB, skeleton resident {} MB, reserve {} MB, workspace {} MB, window snapshots beyond {MTP_MEASURED_WINDOW} rows {} MB, arena request {} slots ({} MB), got {} slots",
3505                budget >> 20,
3506                resident >> 20,
3507                reserve_mb,
3508                workspace >> 20,
3509                snap_extra >> 20,
3510                slots,
3511                (slots as u64 * per as u64) >> 20,
3512                arena.owner.len()
3513            );
3514        }
3515        let (cap_slots, cap_staging) = (arena.staging_base, arena.staging);
3516        state.gpu_pool = Some(arena);
3517        let pinned = {
3518            let arena = state.gpu_pool.as_mut().unwrap();
3519            layers.iter().enumerate().all(|(li, l)| {
3520                arena
3521                    .ensure(&model, li, &[], &l.expert_ids, l.shared_ids)
3522                    .is_some_and(|(_, s)| s != u32::MAX)
3523            })
3524        };
3525        if !pinned {
3526            return off(state, "shared expert could not be pinned");
3527        }
3528        let n_experts = g.n_experts;
3529        let triples: Vec<Vec<(usize, usize, usize)>> =
3530            layers.iter().map(|l| l.expert_ids.clone()).collect();
3531        // The host tiers behind the arena: a RAM tier sized from free memory
3532        // and direct reads from the file (see `expert_store`).
3533        let store = (std::env::var("CMF_QWEN_STORE").as_deref() != Ok("0"))
3534            .then(|| {
3535                crate::expert_store::ExpertStore::new(&model, &triples, n_experts, cap_slots)
3536            })
3537            .flatten();
3538        // n-gram rows are random reads: no read-around on a fault
3539        let advised =
3540            crate::expert_store::advise_random(&model, |n| n.contains("ngram_embedding.shard_"));
3541        if let Some(store) = store.as_ref() {
3542            state.gpu_pool.as_mut().unwrap().store = Some(store.clone());
3543            if prof {
3544                eprintln!(
3545                    "qwen4-device: expert store io {:?}, RAM tier {} experts, n-gram table {} MiB advised random",
3546                    store.io_mode(),
3547                    store.tier_capacity(),
3548                    advised >> 20
3549                );
3550            }
3551        }
3552        let mut profile = std::env::var("CMF_QWEN_PROFILE")
3553            .ok()
3554            .and_then(|path| ExpertProfile::load(&path, layers.len(), n_experts));
3555        let mut warm_order: Vec<(usize, usize)> = Vec::new();
3556        if let Some(pr) = profile.as_ref() {
3557            let ranked = pr.ranked();
3558            warm_order = ranked.clone();
3559            let t0 = std::time::Instant::now();
3560            let n = state
3561                .gpu_pool
3562                .as_mut()
3563                .map_or(0, |a| a.prefill(&model, &ranked, &triples));
3564            tracing::info!(
3565                "qwen4 device: arena prefilled with {n} of {} profiled experts ({} tokens of history) in {:.2}s",
3566                ranked.len(),
3567                pr.tokens,
3568                t0.elapsed().as_secs_f64()
3569            );
3570            if prof {
3571                eprintln!(
3572                    "qwen4-device: arena prefilled with {n} of {} profiled experts in {:.2}s (capacity {} slots, {} staging)",
3573                    ranked.len(),
3574                    t0.elapsed().as_secs_f64(),
3575                    cap_slots,
3576                    cap_staging
3577                );
3578            }
3579        }
3580        // The host-heap tier behind the arena (Linux): admissions become DMA
3581        // copies (`HostTier`). Off unless asked: CMF_QWEN_HTIER_MB=auto sizes
3582        // it for every expert the arena cannot hold, =<MiB> to that size.
3583        // Measured on the RTX 3090 (no resizable BAR, §8.7 of the device
3584        // doc) against the admission pool alone it adds 2-5 % of steady
3585        // decode and ingest, while pinning 28-37 GiB holds up the frames
3586        // beside it (first token of a one-shot run +7-11 s) and squeezes
3587        // the page cache: worth it only for a long-lived process. Whatever
3588        // is asked, it never pins more than half of the memory this process
3589        // can still take (cgroup and MemAvailable) nor comes within 16 GiB
3590        // of it.
3591        {
3592            let n_keys = layers.len() * n_experts;
3593            let want_slots = n_keys.saturating_sub(cap_slots) + cap_slots / 8;
3594            let stride = (per as u64).div_ceil(256) * 256 + 1024;
3595            let want = want_slots as u64 * stride;
3596            let asked = match std::env::var("CMF_QWEN_HTIER_MB").ok().as_deref() {
3597                None | Some("") => 0,
3598                Some("auto") => want,
3599                Some(v) => v.parse::<u64>().map_or(0, |mb| mb << 20),
3600            };
3601            let cap_bytes = if asked == 0 {
3602                0
3603            } else {
3604                crate::expert_store::host_available_bytes().map_or(0, |avail| {
3605                    asked
3606                        .min(avail / 2)
3607                        .min(avail.saturating_sub(16 << 30))
3608                })
3609            };
3610            if cap_bytes > 0 {
3611                let t0 = std::time::Instant::now();
3612                let tier = q4::HostTier::new(&model, n_keys, cap_bytes);
3613                if prof {
3614                    match tier.as_ref() {
3615                        Some(t) => eprintln!(
3616                            "qwen4-device: host tier {} slots ({} MiB, first GiB in {:.0} ms, the rest in the background after the first frame)",
3617                            t.capacity(),
3618                            (t.capacity() as u64 * t.stride()) >> 20,
3619                            t0.elapsed().as_secs_f64() * 1e3
3620                        ),
3621                        None => eprintln!("qwen4-device: host tier unavailable"),
3622                    }
3623                }
3624                // CMF_QWEN_HTIER_FILL=<threads> (default 8, 0 = lazy only):
3625                // copy every expert the arena does not hold into the tier
3626                // in the background, the profiled ones first, so prompt
3627                // ingest admits by DMA alone.
3628                let fill_threads = std::env::var("CMF_QWEN_HTIER_FILL")
3629                    .ok()
3630                    .and_then(|v| v.parse::<usize>().ok())
3631                    .unwrap_or(8);
3632                if let Some(t) = tier.as_ref()
3633                    && fill_threads > 0
3634                {
3635                    let arena = state.gpu_pool.as_ref().unwrap();
3636                    let mut order: Vec<usize> = Vec::with_capacity(n_keys);
3637                    let mut seen = vec![false; n_keys];
3638                    let ranked = profile.as_ref().map(|p| p.ranked()).unwrap_or_default();
3639                    for &(l, e) in &ranked {
3640                        if l < layers.len() && e < n_experts {
3641                            let k = l * n_experts + e;
3642                            if !seen[k] && arena.slot_for[k] == u32::MAX {
3643                                seen[k] = true;
3644                                order.push(k);
3645                            }
3646                        }
3647                    }
3648                    for e in 0..n_experts {
3649                        for l in 0..layers.len() {
3650                            let k = l * n_experts + e;
3651                            if !seen[k] && arena.slot_for[k] == u32::MAX {
3652                                seen[k] = true;
3653                                order.push(k);
3654                            }
3655                        }
3656                    }
3657                    let flat: Vec<(usize, usize, usize)> =
3658                        triples.iter().flat_map(|r| r.iter().copied()).collect();
3659                    if flat.len() == n_keys {
3660                        t.start_fill(model.clone(), Arc::new(flat), order, fill_threads);
3661                    }
3662                }
3663                state.gpu_pool.as_mut().unwrap().tier = tier;
3664            }
3665        }
3666        if std::env::var_os("CMF_QWEN_PROFILE_SAVE").is_some() && profile.is_none() {
3667            profile = Some(ExpertProfile::new(layers.len(), n_experts));
3668        }
3669        state.profile = profile;
3670        // Warm the RAM tier in the background: the profiled experts the
3671        // arena could not take, then every other expert (layer by layer
3672        // interleaved) while the tier has room for what VRAM lacks.
3673        if let Some(store) = store.as_ref() {
3674            let mut seen = vec![false; layers.len() * n_experts];
3675            for &(l, e) in &warm_order {
3676                if l < layers.len() && e < n_experts {
3677                    seen[l * n_experts + e] = true;
3678                }
3679            }
3680            for e in 0..n_experts {
3681                for l in 0..layers.len() {
3682                    if !seen[l * n_experts + e] {
3683                        warm_order.push((l, e));
3684                    }
3685                }
3686            }
3687            store.start_background(warm_order);
3688        }
3689    }
3690    if position == 0 || state.pos != position {
3691        state.reset();
3692        let ok = state.dev.as_mut().is_some_and(|d| d.reset());
3693        if !ok {
3694            return off(state, "device reset failed");
3695        }
3696    }
3697    let dev = state.dev.as_mut().unwrap();
3698    let arena = state.gpu_pool.as_mut().unwrap();
3699
3700    // ── the tokens ──
3701    let mut pf = DevProf::default();
3702    let cold_on_device = std::env::var("CMF_QWEN_COLD_DEVICE").as_deref() != Ok("0");
3703    // Several tokens per frame rule the gated chain out (one frame per layer).
3704    let chain_len = if ntok > 1 {
3705        1
3706    } else {
3707        std::env::var("CMF_QWEN_CHAIN")
3708            .ok()
3709            .and_then(|v| v.parse::<usize>().ok())
3710            .unwrap_or(1)
3711            .max(1)
3712    };
3713    let n = layers.len();
3714    let top_k = g.top_k;
3715    // a layer frame's readback: the cold lists of every token, then the
3716    // MoE inputs of every token
3717    let cs = q4::cold_stride(&g);
3718    let hidden_bytes = cfg.hidden * 4;
3719    let cold_part = (ntok * cs).div_ceil(16) * 16;
3720    let frame_bytes = cold_part + (ntok * hidden_bytes).div_ceil(16) * 16;
3721    let snapshot = state.verify_window;
3722    if snapshot {
3723        dev.ensure_snaps(&g, ntok);
3724    }
3725    let mut cold_vec: Vec<Option<Vec<f32>>> = vec![None; ntok];
3726    let mut cold_slots: Vec<Vec<(u32, f32)>> = vec![Vec::with_capacity(top_k); ntok];
3727    let mut failed: Option<&'static str> = None;
3728    let head_hc = device_hc_w(&globals.head_hc);
3729    let lm_head_idx = globals.lm_head_idx.unwrap_or(usize::MAX);
3730    // The readback stage of a chain holds its layer frames and, on the
3731    // head, the logits of the last token, or of every position of a verify
3732    // window. A 248k vocabulary is ~0.95 MiB a row: the initial 4 MiB stage
3733    // holds a window of four (MTP k = 3); five rows (k = 4) overran it.
3734    let logit_rows = match (want_logits, snapshot) {
3735        (false, _) => 0,
3736        (true, false) => 1,
3737        (true, true) => ntok,
3738    };
3739    let stage_need = q4::chain_stage_bytes(
3740        chain_len.min(n),
3741        frame_bytes,
3742        logit_rows,
3743        q4::head_stride(&model, lm_head_idx).unwrap_or(0),
3744    );
3745    if !dev.ensure_stage(stage_need) {
3746        failed = Some("readback stage allocation");
3747    }
3748    for (t, &id) in ids.iter().enumerate() {
3749        let mut emb = vec![0.0f32; cfg.hidden];
3750        if (id as usize) < globals.embed.rows() {
3751            globals.embed.row_f32(id as usize, &mut emb);
3752        }
3753        dev.seed(t, &emb, cfg.hc);
3754    }
3755    // the PLE rows of every token of the chunk, against its own history
3756    let tp = std::time::Instant::now();
3757    let ple_rows = ple_frame_rows(layers, cfg, &state.token_history, ids, pool);
3758    pf.ple += tp.elapsed();
3759    // Frames 0..n are the layers, frame n the head. A chain of frames goes
3760    // out in one submission; its layers are gated on the card so that a
3761    // cold expert in one of them leaves every later frame unexecuted, and
3762    // the chain resumes after the miss once the arena admitted it.
3763    let frames_total = if want_logits { n + 1 } else { n };
3764    dev.gated = chain_len > 1;
3765    let mut fi = 0usize;
3766    let mut vocab_out = 0usize;
3767    // Single-frame chains are encoded one frame ahead, while the card runs
3768    // the current one: with one frame per chain the next chain always
3769    // starts at the next frame, miss or not.
3770    // It is finished into a command buffer right there: wgpu records the
3771    // real commands at finish, and doing it after the fence would put that
3772    // replay between one frame's fence and the next frame's submit.
3773    let mut prepared: Option<(wgpu::CommandBuffer, u64, Option<usize>, usize)> = None;
3774    let dump_hyper = std::env::var("CMF_QWEN_DUMP_HYPER").ok();
3775    let mut t_fence: Option<std::time::Instant> = None;
3776    // the row stride of the head's logits in the stage (bytes)
3777    let mut lstride_out = 0usize;
3778    while fi < frames_total && failed.is_none() {
3779        let hi = (fi + chain_len).min(frames_total);
3780        let t0 = std::time::Instant::now();
3781        let encode_chain = |lo: usize,
3782                            hi: usize,
3783                            dev: &mut q4::Dev,
3784                            arena: &mut QwenGpuPool,
3785                            picks_prev: &[Vec<usize>],
3786                            pf: &mut DevProf,
3787                            vocab_out: &mut usize|
3788         -> Result<
3789            (wgpu::CommandEncoder, u64, Option<usize>, usize),
3790            &'static str,
3791        > {
3792            let mut enc = q4::new_encoder("qwen4-chain").ok_or("encoder")?;
3793            let merge = q4::merge_guard(&enc);
3794            q4::ts_mark(&mut enc, 0);
3795            // chain head: the previous frame's MoE output with its cold winners
3796            if !q4::encode_pending(&mut enc, dev, &g, ntok) {
3797                return Err("pending inject");
3798            }
3799            q4::pending_done(dev);
3800            q4::ts_mark(&mut enc, 1);
3801            let mut stage_off = 0u64;
3802            let mut logits_off = None;
3803            let mut lstride = 0usize;
3804            for f in lo..hi {
3805                let inject_prev = f > lo;
3806                if f == n {
3807                    for i in 2..8 {
3808                        q4::ts_mark(&mut enc, i);
3809                    }
3810                    // the last position's final hyper state, for the MTP's next cell
3811                    dev.keep_r(&mut enc, ntok - 1);
3812                    let hw = head_hc.as_ref().ok_or("head hyper-connection indices")?;
3813                    // logits of the chunk's last token; of every token in a
3814                    // verify window
3815                    let first = if snapshot { 0 } else { ntok - 1 };
3816                    let (lb, vocab, ls) = q4::encode_head(
3817                        &mut enc,
3818                        dev,
3819                        &model,
3820                        &g,
3821                        hw,
3822                        lm_head_idx,
3823                        inject_prev,
3824                        ntok,
3825                    )
3826                    .ok_or("head frame declined")?;
3827                    let bytes = ((ntok - first) * ls) as u64;
3828                    if !q4::copy_to_stage(&mut enc, dev, &lb, (first * ls) as u64, stage_off, bytes)
3829                    {
3830                        return Err("logits overrun the readback stage");
3831                    }
3832                    logits_off = Some(stage_off as usize);
3833                    *vocab_out = vocab;
3834                    lstride = ls;
3835                    stage_off += bytes.div_ceil(16) * 16;
3836                    continue;
3837                }
3838                let layer = &layers[f];
3839                let ix = layer.idx.as_ref().ok_or("layer index")?;
3840                let w = device_layer_w(layer, ix).ok_or("layer weights")?;
3841                let ta = std::time::Instant::now();
3842                let (remap, shared_slot) = match arena.ensure(
3843                    &model,
3844                    f,
3845                    &picks_prev[f],
3846                    &layer.expert_ids,
3847                    layer.shared_ids,
3848                ) {
3849                    Some(r) => r,
3850                    None => arena.remap_snapshot(f),
3851                };
3852                pf.admit += ta.elapsed();
3853                if shared_slot == u32::MAX {
3854                    return Err("shared expert slot");
3855                }
3856                let out = q4::encode_layer(
3857                    &mut enc,
3858                    dev,
3859                    &model,
3860                    &g,
3861                    &w,
3862                    f,
3863                    pos0,
3864                    ntok,
3865                    inv_freq,
3866                    &remap,
3867                    shared_slot,
3868                    &ple_rows[f],
3869                    inject_prev,
3870                    snapshot,
3871                )
3872                .ok_or("layer frame declined")?;
3873                if !q4::copy_to_stage(&mut enc, dev, &out.cold, 0, stage_off, (ntok * cs) as u64)
3874                    || !q4::copy_to_stage(
3875                        &mut enc,
3876                        dev,
3877                        &out.x2,
3878                        0,
3879                        stage_off + cold_part as u64,
3880                        (ntok * hidden_bytes) as u64,
3881                    )
3882                {
3883                    return Err("layer frame overruns the readback stage");
3884                }
3885                stage_off += frame_bytes as u64;
3886            }
3887            q4::ts_mark(&mut enc, 8);
3888            q4::ts_resolve(&mut enc);
3889            dev.arm_chain(lo, hi);
3890            drop(merge);
3891            Ok((enc, stage_off, logits_off, lstride))
3892        };
3893        let (cb, stage_off, logits_off, ls) = match prepared.take() {
3894            Some(p) => p,
3895            None => match encode_chain(
3896                fi,
3897                hi,
3898                dev,
3899                arena,
3900                &state.picks_prev,
3901                &mut pf,
3902                &mut vocab_out,
3903            ) {
3904                Ok((e, a, b, c)) => {
3905                    let tf = std::time::Instant::now();
3906                    let cb = q4::finish_frame(e);
3907                    pf.finish += tf.elapsed();
3908                    (cb, a, b, c)
3909                }
3910                Err(why) => {
3911                    failed = Some(why);
3912                    break;
3913                }
3914            },
3915        };
3916        if ls != 0 {
3917            lstride_out = ls;
3918        }
3919        pf.encode += t0.elapsed();
3920        // the chain head's pending inject gets the previous frame's cold data
3921        if !q4::finalize_pending(dev, &g, ntok, &cold_vec, &cold_slots) {
3922            failed = Some("finalize");
3923            break;
3924        }
3925        let t0 = std::time::Instant::now();
3926        // staged admissions land before the frame that reads their slots:
3927        // their copies go first in the frame's own queue submission
3928        let uploads = arena.take_uploads();
3929        let Some(pend) = q4::submit_frame(dev, uploads, cb, stage_off) else {
3930            failed = Some("submit");
3931            break;
3932        };
3933        arena.rearm_uploads();
3934        if let Some(tf) = t_fence.take() {
3935            pf.post += tf.elapsed();
3936        }
3937        // one frame per chain: the next frame is encoded while this one runs
3938        arena.hold_layers = [None, None];
3939        if chain_len == 1 && hi < frames_total {
3940            let te = std::time::Instant::now();
3941            match encode_chain(
3942                hi,
3943                hi + 1,
3944                dev,
3945                arena,
3946                &state.picks_prev,
3947                &mut pf,
3948                &mut vocab_out,
3949            ) {
3950                Ok((e, a, b, c)) => {
3951                    let tf = std::time::Instant::now();
3952                    prepared = Some((q4::finish_frame(e), a, b, c));
3953                    pf.finish += tf.elapsed();
3954                }
3955                Err(why) => {
3956                    failed = Some(why);
3957                    break;
3958                }
3959            }
3960            pf.encode += te.elapsed();
3961            // its remap is now fixed: the admissions below must not evict
3962            // that layer's experts, nor this layer's own winners of the frame
3963            arena.hold_layers = [(hi < n).then_some(hi), (fi < n).then_some(fi)];
3964        }
3965        let ts = std::time::Instant::now();
3966        let Some(bytes) = pend.wait() else {
3967            failed = Some("readback");
3968            break;
3969        };
3970        arena.uploads_done();
3971        pf.spin += ts.elapsed();
3972        pf.wait += t0.elapsed();
3973        if let Some(m) = q4::ts_read() {
3974            pf.gpu_note(fi, n, &m, layers.get(fi).map(|l| matches!(l.mixer, Mixer::Gdn(_))));
3975        }
3976        t_fence = Some(std::time::Instant::now());
3977        pf.chains += 1;
3978        // the first frame of the chain that routed to a cold expert
3979        let mut miss_at: Option<usize> = None;
3980        let mut off = 0usize;
3981        'frames: for f in fi..hi {
3982            if f == n {
3983                break;
3984            }
3985            let layer = &layers[f];
3986            dev.commit(f);
3987            if let Some(dir) = dump_hyper.as_deref()
3988                && let Some(rows) = q4::read_hyper(dev, ntok, cfg.hc * cfg.hidden)
3989            {
3990                let _ = std::fs::create_dir_all(&dir);
3991                for (t, row) in rows.iter().enumerate() {
3992                    let path = std::path::Path::new(dir)
3993                        .join(format!("pos{:05}_layer{f:02}.f32", pos0 + t));
3994                    let bytes: Vec<u8> = row.iter().flat_map(|v| v.to_le_bytes()).collect();
3995                    let _ = std::fs::write(path, bytes);
3996                }
3997            }
3998            let mut any_cold_in_frame = false;
3999            let frame_off = off;
4000            off += frame_bytes;
4001            // Several tokens: reserve every token's cold winners in token
4002            // order (the slots per-token admission would pick) and upload
4003            // them in one parallel batch.
4004            let mut pre_slots: Vec<Option<Vec<Option<u32>>>> = vec![None; ntok];
4005            if cold_on_device && ntok > 1 && admit_batch_env() {
4006                let ta = std::time::Instant::now();
4007                let mut toks = Vec::new();
4008                let mut reqs = Vec::new();
4009                for t in 0..ntok {
4010                    let w_off = frame_off + t * cs;
4011                    let word = |j: usize| {
4012                        let b = &bytes[w_off + 4 * j..w_off + 4 * j + 4];
4013                        u32::from_le_bytes([b[0], b[1], b[2], b[3]])
4014                    };
4015                    let eids: Vec<usize> = (0..top_k)
4016                        .map(|j| word(2 * j))
4017                        .filter(|&e| e != u32::MAX && (e as usize) < layer.moe.experts.len())
4018                        .map(|e| e as usize)
4019                        .collect();
4020                    if eids.is_empty() {
4021                        continue;
4022                    }
4023                    let picks: Vec<usize> = (0..top_k)
4024                        .map(|j| word(2 * top_k + 2 * j))
4025                        .filter(|&e| e != u32::MAX && (e as usize) < g.n_experts)
4026                        .map(|e| e as usize)
4027                        .collect();
4028                    toks.push(t);
4029                    reqs.push((eids, picks));
4030                }
4031                if !reqs.is_empty() {
4032                    let got = arena.admit_frame(&model, f, &reqs, &layer.expert_ids);
4033                    for (t, sl) in toks.into_iter().zip(got) {
4034                        pre_slots[t] = Some(sl);
4035                    }
4036                }
4037                pf.admit += ta.elapsed();
4038            }
4039            for t in 0..ntok {
4040                let w_off = frame_off + t * cs;
4041                let words: Vec<u32> = bytes[w_off..w_off + 4 * top_k * 4]
4042                    .chunks_exact(4)
4043                    .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4044                    .collect();
4045                let x_off = frame_off + cold_part + t * hidden_bytes;
4046                let picks: Vec<usize> = (0..top_k)
4047                    .map(|j| words[2 * top_k + 2 * j])
4048                    .filter(|&e| e != u32::MAX && (e as usize) < g.n_experts)
4049                    .map(|e| e as usize)
4050                    .collect();
4051                if let Some(pr) = state.profile.as_mut() {
4052                    pr.note(f, &picks);
4053                }
4054                if check && ntok == 1 && tap_layer() == Some(f) && position < 3 && f + 1 == hi {
4055                    // the frame's scratch is still the layer's own: read it
4056                    let bufs = q4::tap_bufs(dev, &g);
4057                    let h = cfg.hidden;
4058                    let parts: Vec<(&wgpu::Buffer, u64)> = vec![
4059                        (&bufs[0], (h * 4) as u64),
4060                        (&bufs[1], (h * 4) as u64),
4061                        (&bufs[2], (h * 4) as u64),
4062                        (&bufs[3], (h * 4) as u64),
4063                        (&bufs[4], (cfg.hc * h * 4) as u64),
4064                    ];
4065                    if let Some(enc) = q4::new_encoder("qwen4-tap")
4066                        && let Some(tb) = q4::submit_readback(enc, &parts)
4067                    {
4068                        let fl = |o: usize, cnt: usize| -> Vec<f32> {
4069                            tb[o..o + cnt * 4]
4070                                .chunks_exact(4)
4071                                .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4072                                .collect()
4073                        };
4074                        let al = |cnt: usize| ((cnt * 4) as u64).div_ceil(16) as usize * 16;
4075                        let mut o = 0;
4076                        let x = fl(o, h);
4077                        o += al(h);
4078                        let blk = fl(o, h);
4079                        o += al(h);
4080                        let x2v = fl(o, h);
4081                        o += al(h);
4082                        let mo = fl(o, h);
4083                        o += al(h);
4084                        let hyper = fl(o, cfg.hc * h);
4085                        DEV_TAP
4086                            .lock()
4087                            .unwrap()
4088                            .push((position, x, blk, x2v, mo, hyper));
4089                    }
4090                }
4091                let any_cold = (0..top_k).any(|j| words[2 * j] != u32::MAX);
4092                if t + 1 == ntok {
4093                    state.picks_prev[f] = picks.clone();
4094                }
4095                cold_slots[t].clear();
4096                cold_vec[t] = None;
4097                if !any_cold {
4098                    continue;
4099                }
4100                any_cold_in_frame = true;
4101                // Cold winners: admitted to the arena right away and computed
4102                // by the card at the head of the next chain; only what the
4103                // arena cannot take is completed on the host.
4104                let ta = std::time::Instant::now();
4105                let x2: Vec<f32> = bytes[x_off..x_off + cfg.hidden * 4]
4106                    .chunks_exact(4)
4107                    .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4108                    .collect();
4109                let mut jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = Vec::new();
4110                let cold_list: Vec<(usize, f32)> = (0..top_k)
4111                    .filter_map(|j| {
4112                        let e = words[2 * j];
4113                        (e != u32::MAX && (e as usize) < layer.moe.experts.len())
4114                            .then(|| (e as usize, f32::from_bits(words[2 * j + 1])))
4115                    })
4116                    .collect();
4117                let eids: Vec<usize> = cold_list.iter().map(|&(e, _)| e).collect();
4118                // into the arena when it has room (the next token then finds
4119                // them warm), the rest into this token slot's staging slots
4120                let slots = if cold_on_device {
4121                    let mut s = match pre_slots[t].take() {
4122                        Some(s) if s.len() == eids.len() => s,
4123                        _ => arena.admit_many(&model, f, &eids, &layer.expert_ids, &picks),
4124                    };
4125                    if s.iter().any(Option::is_none) {
4126                        let rest: Vec<usize> = eids
4127                            .iter()
4128                            .zip(&s)
4129                            .filter(|(_, x)| x.is_none())
4130                            .map(|(&e, _)| e)
4131                            .collect();
4132                        let mut staged = arena
4133                            .stage_cold(&model, f, t, &rest, &layer.expert_ids, top_k)
4134                            .into_iter();
4135                        for x in s.iter_mut().filter(|x| x.is_none()) {
4136                            *x = staged.next().flatten();
4137                        }
4138                    }
4139                    s
4140                } else {
4141                    vec![None; eids.len()]
4142                };
4143                for (&(e, wgt), slot) in cold_list.iter().zip(slots) {
4144                    match slot {
4145                        Some(sl) => cold_slots[t].push((sl, wgt)),
4146                        None => jobs.push((&layer.moe.experts[e], wgt)),
4147                    }
4148                }
4149                pf.admit += ta.elapsed();
4150                pf.cold_experts += cold_slots[t].len() + jobs.len();
4151                pf.cold_dev += cold_slots[t].len();
4152                if !jobs.is_empty() {
4153                    let tc = std::time::Instant::now();
4154                    cold_vec[t] = Some(crate::pipeline::moe_cold_experts_cpu(&jobs, &x2, pool));
4155                    pf.cold_cpu += tc.elapsed();
4156                }
4157                if check
4158                    && ntok == 1
4159                    && tap_layer() == Some(f)
4160                    && position < 3
4161                    && let Some(cv) = cold_vec[t].as_ref()
4162                    && let Some(last) = DEV_TAP.lock().unwrap().last_mut()
4163                {
4164                    for (m, c) in last.4.iter_mut().zip(cv) {
4165                        *m += c;
4166                    }
4167                }
4168            }
4169            if any_cold_in_frame {
4170                pf.layers_with_cold += 1;
4171                miss_at = Some(f);
4172                break 'frames;
4173            }
4174        }
4175        match miss_at {
4176            Some(j) => {
4177                // frames after the miss did not run: the chain resumes there
4178                dev.discard(j + 1, hi);
4179                pf.aborted += hi - (j + 1);
4180                fi = j + 1;
4181            }
4182            None => {
4183                if let Some(lo) = logits_off {
4184                    let stride = lstride_out;
4185                    let read = |o: usize| -> Vec<f32> {
4186                        bytes[o..o + vocab_out * 4]
4187                            .chunks_exact(4)
4188                            .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4189                            .collect()
4190                    };
4191                    if snapshot {
4192                        state.window_logits = (0..ntok).map(|t| read(lo + t * stride)).collect();
4193                        *logits = state.window_logits.last().cloned().unwrap_or_default();
4194                    } else {
4195                        *logits = read(lo);
4196                    }
4197                }
4198                fi = hi;
4199            }
4200        }
4201    }
4202    arena.flush_uploads();
4203    if let Some(why) = failed {
4204        return off(state, why);
4205    }
4206    // A token boundary never carries an injection: the next token reseeds
4207    // the hyper state from its embedding. (A chain that ends with the head
4208    // leaves the flag set by its layers; without this the next token's
4209    // first chain would inject this token's last MoE output.)
4210    dev.clear_pending();
4211    if !want_logits {
4212        logits.clear();
4213    }
4214    if !check {
4215        state.token_history.extend_from_slice(ids);
4216        state.pos = pos0 + ntok;
4217    }
4218    if let Some(d) = state.dev.as_mut() {
4219        d.pos = pos0 + ntok;
4220    }
4221    if let (Some(pr), Ok(path)) = (
4222        state.profile.as_mut(),
4223        std::env::var("CMF_QWEN_PROFILE_SAVE"),
4224    ) {
4225        pr.tokens += ntok as u64;
4226        if pr.tokens % 32 < ntok as u64 {
4227            pr.save(&path);
4228        }
4229    }
4230    if prof {
4231        let store_line = arena
4232            .store
4233            .as_ref()
4234            .map(|s| s.report())
4235            .unwrap_or_default();
4236        let fill_ns = FILL_NS.swap(0, std::sync::atomic::Ordering::Relaxed);
4237        let fill_n = FILL_N.swap(0, std::sync::atomic::Ordering::Relaxed);
4238        let fills_wall = FILLS_WALL_NS.swap(0, std::sync::atomic::Ordering::Relaxed) as f64 / 1e6;
4239        let store_line = match arena.tier.as_ref() {
4240            Some(t) => {
4241                use std::sync::atomic::Ordering::Relaxed;
4242                format!(
4243                    "{store_line} htier hits={} fills={} fill_sum={:.1}ms nofit={} online={} bg_fills={}",
4244                    t.hits.swap(0, Relaxed),
4245                    t.fills.swap(0, Relaxed),
4246                    t.fill_ns.swap(0, Relaxed) as f64 / 1e6,
4247                    t.misses.swap(0, Relaxed),
4248                    t.online(),
4249                    t.bg_fills.load(Relaxed)
4250                )
4251            }
4252            None => store_line,
4253        };
4254        eprintln!(
4255            "qwen4-device pos={position} ntok={ntok} total={:.1}ms encode={:.1}ms finish={:.1}ms wait={:.1}ms spin={:.1}ms post={:.1}ms ple={:.1}ms cold_cpu={:.1}ms admit={:.1}ms chains={} aborted_frames={} cold={} experts ({} on device) in {} layers fills={fill_n} fill_sum={:.1}ms fill_wall={fills_wall:.1}ms {store_line}",
4256            t_token.elapsed().as_secs_f64() * 1e3,
4257            pf.encode.as_secs_f64() * 1e3,
4258            pf.finish.as_secs_f64() * 1e3,
4259            pf.wait.as_secs_f64() * 1e3,
4260            pf.spin.as_secs_f64() * 1e3,
4261            pf.post.as_secs_f64() * 1e3,
4262            pf.ple.as_secs_f64() * 1e3,
4263            pf.cold_cpu.as_secs_f64() * 1e3,
4264            pf.admit.as_secs_f64() * 1e3,
4265            pf.chains,
4266            pf.aborted,
4267            pf.cold_experts,
4268            pf.cold_dev,
4269            pf.layers_with_cold,
4270            fill_ns as f64 / 1e6,
4271        );
4272    }
4273    if prof && pf.gpu_last_end.is_some() {
4274        let mut line = format!("qwen4-ts pos={position} ntok={ntok}");
4275        for (name, v) in TS_NAMES.iter().zip(pf.gpu) {
4276            line.push_str(&format!(" {name}={:.2}", v / 1e6));
4277        }
4278        line.push_str(&format!(" busy={:.2} ms", pf.gpu[..16].iter().sum::<f64>() / 1e6));
4279        eprintln!("{line}");
4280    }
4281    if check && ntok == 1 {
4282        // The host path keeps its own state; run it on every token (its
4283        // caches must see the whole sequence) and compare the logits where
4284        // both produced them.
4285        let dev_logits = logits.clone();
4286        let mut host = Vec::new();
4287        state.device_off = true;
4288        forward_token(
4289            globals,
4290            layers,
4291            cfg,
4292            state,
4293            token_id,
4294            position,
4295            inv_freq,
4296            pool,
4297            &mut host,
4298            want_logits,
4299        );
4300        state.device_off = false;
4301        if let Some(tl) = tap_layer() {
4302            let dev_taps = std::mem::take(&mut *DEV_TAP.lock().unwrap());
4303            let host_taps = std::mem::take(&mut *TAP.lock().unwrap());
4304            for (pos, x, blk, x2, mo, hyper) in &dev_taps {
4305                let find = |lab: &str| {
4306                    host_taps
4307                        .iter()
4308                        .find(|(l, li, p, _)| l == lab && *li == tl && p == pos)
4309                        .map(|(_, _, _, v)| v.as_slice())
4310                };
4311                eprintln!("qwen4-tap layer {tl} pos {pos}:");
4312                if let Some(h) = find("attn_in") {
4313                    tap_compare("attn_in", h, x);
4314                }
4315                if let Some(h) = find("attn_out") {
4316                    tap_compare("attn_out", h, blk);
4317                }
4318                if let Some(h) = find("post_attn") {
4319                    tap_compare("post_attn", h, hyper);
4320                }
4321                if let Some(h) = find("moe_in") {
4322                    tap_compare("moe_in", h, x2);
4323                }
4324                if let Some(h) = find("moe_out") {
4325                    tap_compare("moe_res", h, mo);
4326                }
4327            }
4328        }
4329        if !want_logits {
4330            return true;
4331        }
4332        let n = host.len().min(dev_logits.len());
4333        let mut max_abs = 0.0f32;
4334        let (mut dot, mut na, mut nb) = (0.0f64, 0.0f64, 0.0f64);
4335        for i in 0..n {
4336            max_abs = max_abs.max((host[i] - dev_logits[i]).abs());
4337            dot += host[i] as f64 * dev_logits[i] as f64;
4338            na += host[i] as f64 * host[i] as f64;
4339            nb += dev_logits[i] as f64 * dev_logits[i] as f64;
4340        }
4341        let argmax = |v: &[f32]| {
4342            v.iter()
4343                .enumerate()
4344                .fold((0usize, f32::NEG_INFINITY), |a, (i, &x)| {
4345                    if x > a.1 { (i, x) } else { a }
4346                })
4347                .0
4348        };
4349        eprintln!(
4350            "qwen4-check pos={position} cos={:.6} max_abs={max_abs:.4} argmax host={} dev={}",
4351            dot / (na.sqrt() * nb.sqrt()).max(1e-30),
4352            argmax(&host),
4353            argmax(&dev_logits)
4354        );
4355        *logits = dev_logits;
4356    }
4357    true
4358}
4359
4360#[cfg(feature = "gpu")]
4361fn device_hc_w(g: &GatedResidual) -> Option<crate::gpu_wgpu::qwen4::HcW<'_>> {
4362    Some(crate::gpu_wgpu::qwen4::HcW {
4363        norm: &g.norm,
4364        down: g.down_idx?,
4365        up: g.up_idx?,
4366        inject: g.inject_idx,
4367    })
4368}
4369
4370/// The verify window (k + 1 positions) the 768 MiB default of
4371/// `CMF_QWEN_KV_RESERVE_MB` was measured with: MTP k = 3.
4372#[cfg(feature = "gpu")]
4373const MTP_MEASURED_WINDOW: usize = 4;
4374
4375/// Drafts per MTP round from `CMF_QWEN_MTP_K` (default 3), and whether the
4376/// value had to be clamped. The verify window, the last accepted token plus
4377/// k drafts, is one device frame, so k + 1 <= TMAX: k in 1..=7.
4378#[cfg(feature = "gpu")]
4379fn mtp_k(raw: Option<&str>) -> (usize, bool) {
4380    let max = crate::gpu_wgpu::qwen4::TMAX - 1;
4381    match raw.map(|v| v.trim().parse::<usize>()) {
4382        None => (3, false),
4383        Some(Ok(v)) => (v.clamp(1, max), !(1..=max).contains(&v)),
4384        Some(Err(_)) => (3, true),
4385    }
4386}
4387
4388/// One speculative round's outcome, in the shape the generation loop
4389/// consumes: the drafts the model confirmed, the logits the loop samples
4390/// its own next token from, how many were drafted.
4391#[cfg(feature = "gpu")]
4392pub struct SpecRound {
4393    pub accepted: Vec<u32>,
4394    pub logits: Vec<f32>,
4395    pub drafted: usize,
4396}
4397
4398/// The Qwen3.8-Flash-Next multi-token-prediction head from the sidecar
4399/// `<stem>.mtp.cmf`: one hyper-connected hybrid layer (QSA attention, its
4400/// own 512 routed experts and shared expert) between a fused input and a
4401/// final mixer, sharing the main model's embedding and lm_head.
4402///
4403///   e = fc_embedding(rms(embed(tok))·(1+w_e))
4404///   h = fc_hidden(rms_{hc·hidden}(R)·(1+w_h)) per stream
4405///   R' = h + e → attention HC → QSA → MLP HC → MoE → inject → mixer → head
4406///
4407/// Cell i pairs the main model's final hyper state at position i with the
4408/// token at i+1 (rope position i) and predicts the token at i+2; its own
4409/// state feeds the next draft of a chain. Drafts only propose: the verify
4410/// window decides every emitted token.
4411#[cfg(feature = "gpu")]
4412pub struct MtpHead {
4413    side: Arc<CmfModel>,
4414    layer: Layer,
4415    enorm: Vec<f32>,
4416    hnorm: Vec<f32>,
4417    fc_e: usize,
4418    fc_h: usize,
4419    mixer: GatedResidual,
4420    skeleton: Vec<usize>,
4421    dev: Option<crate::gpu_wgpu::qwen4::Dev>,
4422    arena: Option<QwenGpuPool>,
4423    pub k: usize,
4424    pub drafted: u64,
4425    pub accepted: u64,
4426    pub rounds: u64,
4427}
4428
4429#[cfg(feature = "gpu")]
4430impl MtpHead {
4431    /// Open the sidecar beside the main file, when there is one that fits.
4432    pub fn load(main: &Arc<CmfModel>, arch: &ModelArch) -> Option<Self> {
4433        if std::env::var("CMF_QWEN_MTP").as_deref() == Ok("0") {
4434            return None;
4435        }
4436        let path = cortiq_core::mtp_sidecar_path(&main.path);
4437        if path == main.path || !path.exists() {
4438            return None;
4439        }
4440        let side = match CmfModel::open(&path) {
4441            Ok(m) => Arc::new(m),
4442            Err(e) => {
4443                tracing::warn!("qwen4 MTP sidecar {}: {e}", path.display());
4444                return None;
4445            }
4446        };
4447        let sa = side.arch();
4448        if sa.arch_name != arch.arch_name
4449            || sa.hidden_size != arch.hidden_size
4450            || sa.vocab_size != arch.vocab_size
4451        {
4452            tracing::warn!(
4453                "qwen4 MTP sidecar {}: geometry differs from the backbone",
4454                path.display()
4455            );
4456            return None;
4457        }
4458        let p = "model.mtp.layers.0.";
4459        let build =
4460            || -> Result<(Layer, GatedResidual, Vec<f32>, Vec<f32>, usize, usize), CmfError> {
4461                let mixer = Mixer::Qsa(load_qsa(&side, &format!("{p}self_attn."))?);
4462                let moe = match build_ffn_at(&side, sa, p, false, &Overlay::None)? {
4463                    FfnKind::Moe(m) => m,
4464                    _ => return Err(err("MTP layer is not MoE")),
4465                };
4466                let expert_ids: Vec<_> = moe
4467                    .experts
4468                    .iter()
4469                    .map(|e| {
4470                        Some((
4471                            e.gate_proj.model_idx()?,
4472                            e.up_proj.model_idx()?,
4473                            e.down_proj.model_idx()?,
4474                        ))
4475                    })
4476                    .collect::<Option<_>>()
4477                    .ok_or_else(|| err("MTP experts are not mmap-backed"))?;
4478                let shared_ids = moe
4479                    .shared
4480                    .as_ref()
4481                    .and_then(|(e, _)| {
4482                        Some((
4483                            e.gate_proj.model_idx()?,
4484                            e.up_proj.model_idx()?,
4485                            e.down_proj.model_idx()?,
4486                        ))
4487                    })
4488                    .ok_or_else(|| err("MTP shared expert is not mmap-backed"))?;
4489                let attn_hc = load_hc(&side, &format!("{p}attn_hyper_connection."), true)?;
4490                let mlp_hc = load_hc(&side, &format!("{p}mlp_hyper_connection."), true)?;
4491                let idx = layer_idx(&side, p, false, false);
4492                let layer = Layer {
4493                    attn_hc,
4494                    mlp_hc,
4495                    mixer,
4496                    moe,
4497                    ple: None,
4498                    expert_ids,
4499                    shared_ids: Some(shared_ids),
4500                    idx,
4501                };
4502                let mixer_hc = load_hc(&side, "model.mtp.hyper_connection_mixer.", false)?;
4503                let enorm = f(&side, "model.mtp.enorm.weight")?;
4504                let hnorm = f(&side, "model.mtp.hnorm.weight")?;
4505                let fc_e = side
4506                    .tensor_index("model.mtp.fc_embedding.weight")
4507                    .ok_or_else(|| err("fc_embedding"))?;
4508                let fc_h = side
4509                    .tensor_index("model.mtp.fc_hidden.weight")
4510                    .ok_or_else(|| err("fc_hidden"))?;
4511                Ok((layer, mixer_hc, enorm, hnorm, fc_e, fc_h))
4512            };
4513        let (layer, mixer, enorm, hnorm, fc_e, fc_h) = match build() {
4514            Ok(v) => v,
4515            Err(e) => {
4516                tracing::warn!("qwen4 MTP sidecar {}: {e}", path.display());
4517                return None;
4518            }
4519        };
4520        let ix = layer.idx.as_ref()?;
4521        let mut skeleton: Vec<usize> = Vec::new();
4522        skeleton.extend(layer.attn_hc.idxs());
4523        skeleton.extend(layer.mlp_hc.idxs());
4524        skeleton.extend(mixer.idxs());
4525        skeleton.extend(ix.qsa.iter().flatten().copied());
4526        skeleton.push(ix.router);
4527        skeleton.extend(ix.shared_gate);
4528        skeleton.extend([fc_e, fc_h]);
4529        let raw = std::env::var("CMF_QWEN_MTP_K").ok();
4530        let (k, clamped) = mtp_k(raw.as_deref());
4531        if clamped {
4532            tracing::warn!(
4533                "CMF_QWEN_MTP_K={} is outside 1..={}: the verify window (k + 1 positions) is one device frame of at most {} tokens; using k = {k}",
4534                raw.as_deref().unwrap_or(""),
4535                crate::gpu_wgpu::qwen4::TMAX - 1,
4536                crate::gpu_wgpu::qwen4::TMAX
4537            );
4538        }
4539        tracing::info!("qwen4 MTP: draft head from {} (k = {k})", path.display());
4540        Some(Self {
4541            side,
4542            layer,
4543            enorm,
4544            hnorm,
4545            fc_e,
4546            fc_h,
4547            mixer,
4548            skeleton,
4549            dev: None,
4550            arena: None,
4551            k,
4552            drafted: 0,
4553            accepted: 0,
4554            rounds: 0,
4555        })
4556    }
4557
4558    /// Resident skeleton, all 512 experts in their own arena bank.
4559    fn setup(&mut self, g: &crate::gpu_wgpu::qwen4::Geom, gu_q2: bool) -> Result<(), &'static str> {
4560        use crate::gpu_wgpu::qwen4 as q4;
4561        if self.dev.is_some() && self.arena.is_some() {
4562            return Ok(());
4563        }
4564        let dev = q4::Dev::new(self.side.uid(), g, &[(false, false)]).ok_or("MTP device state")?;
4565        q4::prewarm(&self.side, &self.skeleton).ok_or("MTP skeleton does not fit")?;
4566        let n_exp = self.layer.moe.experts.len();
4567        // whole segments of 8: the shared expert needs one slot beyond the 512
4568        let mut arena =
4569            QwenGpuPool::create_exact(&self.side, g.inter, g.hidden, n_exp, gu_q2, n_exp + 8)
4570                .ok_or("MTP expert bank")?;
4571        arena
4572            .ensure(
4573                &self.side,
4574                0,
4575                &[],
4576                &self.layer.expert_ids,
4577                self.layer.shared_ids,
4578            )
4579            .filter(|(_, s)| *s != u32::MAX)
4580            .ok_or("MTP shared expert")?;
4581        let all: Vec<(usize, usize)> = (0..n_exp).map(|e| (0, e)).collect();
4582        let got = arena.prefill(
4583            &self.side,
4584            &all,
4585            std::slice::from_ref(&self.layer.expert_ids),
4586        );
4587        if got < n_exp {
4588            return Err("MTP experts do not all fit their bank");
4589        }
4590        self.dev = Some(dev);
4591        self.arena = Some(arena);
4592        Ok(())
4593    }
4594}
4595
4596/// One speculative round: `k` greedy drafts from the MTP head, verified in
4597/// one batched forward of the main model (`all_ids[next_pos]` plus the
4598/// drafts at `next_pos..`). Returns `None` when the device path or the
4599/// head is unavailable; the caller then decodes plainly.
4600#[cfg(feature = "gpu")]
4601#[allow(clippy::too_many_arguments)]
4602pub fn spec_round(
4603    globals: &Globals,
4604    layers: &[Layer],
4605    cfg: &Cfg,
4606    state: &mut State,
4607    next_pos: usize,
4608    all_ids: &[u32],
4609    inv_freq: &[f32],
4610    pool: Option<&Pool>,
4611) -> Option<SpecRound> {
4612    use crate::gpu_wgpu::qwen4 as q4;
4613    if state.device_off || state.dev.is_none() || next_pos == 0 || all_ids.len() != next_pos + 1 {
4614        return None;
4615    }
4616    if state.mtp.is_none() && !state.mtp_tried {
4617        state.mtp_tried = true;
4618        let model = layers.first()?.moe.experts.first()?.gate_proj.model_arc()?;
4619        state.mtp = MtpHead::load(&model, model.arch());
4620    }
4621    let first = layers.first()?;
4622    let e0 = first.moe.experts.first()?;
4623    let gu_q2 = e0.gate_proj.model_dtype() == Some(TensorDtype::Q2TiledP);
4624    let g = device_geom(cfg, &first.moe, gu_q2);
4625    let prof = std::env::var_os("CMF_QWEN_PROF").is_some();
4626    let t_round = std::time::Instant::now();
4627    // ── drafts ──
4628    let (k, drafts) = {
4629        let head = state.mtp.as_mut()?;
4630        if let Err(why) = head.setup(&g, gu_q2) {
4631            tracing::warn!("qwen4 MTP off: {why}");
4632            state.mtp = None;
4633            return None;
4634        }
4635        let k = head.k;
4636        let mdev = head.dev.as_mut()?;
4637        let arena = head.arena.as_mut()?;
4638        let (remap, shared_slot) = arena.remap_snapshot(0);
4639        let ix = head.layer.idx.as_ref()?;
4640        let w = device_layer_w(&head.layer, ix)?;
4641        let mixer_w = device_hc_w(&head.mixer)?;
4642        let main_dev = state.dev.as_ref()?;
4643        let main_model = e0.gate_proj.model_arc()?;
4644        let lm_head = globals.lm_head_idx?;
4645        // a host round trip reads one logits row per cell back
4646        if !mdev.ensure_stage(q4::chain_stage_bytes(
4647            0,
4648            0,
4649            1,
4650            q4::head_stride(&main_model, lm_head)?,
4651        )) {
4652            return None;
4653        }
4654        let mut drafts: Vec<u32> = Vec::with_capacity(k);
4655        let tok_in = all_ids[next_pos];
4656        // ── the whole chain in one submit: each cell's argmax is re-embedded
4657        // on the card for the next; the host reads k ids, not k×vocab logits ──
4658        // opt-in: it pins the 0.6 GB embedding table, which costs the arena
4659        // more than the host round trips cost the round
4660        let device_chain = std::env::var("CMF_QWEN_MTP_DEVICE_DRAFT").as_deref() == Ok("1");
4661        let mut chained = false;
4662        if device_chain
4663            && let Some(embed_idx) = globals.embed_idx
4664            && let Some(ids_buf) = q4::draft_ids(tok_in)
4665        {
4666            let mut run = || -> Option<Vec<u32>> {
4667                let mut enc = q4::new_encoder("qwen4-mtp-draft")?;
4668                let merge = q4::merge_guard(&enc);
4669                for j in 0..k {
4670                    let cell = next_pos - 1 + j;
4671                    // the cells share one submit: each gets its own frame salt
4672                    // so its position uniforms and bind groups are its own
4673                    let _salt = q4::frame_salt(j);
4674                    let r = if j == 0 {
4675                        q4::whole(&main_dev.r_last)
4676                    } else {
4677                        mdev.hyper(0)
4678                    };
4679                    q4::encode_draft_gather(
4680                        &mut enc,
4681                        mdev,
4682                        &main_model,
4683                        &g,
4684                        embed_idx,
4685                        &ids_buf,
4686                        j,
4687                    )?;
4688                    if !q4::encode_mtp_input(
4689                        &mut enc,
4690                        mdev,
4691                        &head.side,
4692                        &g,
4693                        None,
4694                        &head.enorm,
4695                        &head.hnorm,
4696                        head.fc_e,
4697                        head.fc_h,
4698                        r,
4699                    ) {
4700                        return None;
4701                    }
4702                    q4::encode_layer(
4703                        &mut enc,
4704                        mdev,
4705                        &head.side,
4706                        &g,
4707                        &w,
4708                        0,
4709                        cell,
4710                        1,
4711                        inv_freq,
4712                        &remap,
4713                        shared_slot,
4714                        &[],
4715                        false,
4716                        false,
4717                    )?;
4718                    if !q4::encode_pending(&mut enc, mdev, &g, 1)
4719                        || !q4::finalize_pending(mdev, &g, 1, &[None], &[Vec::new()])
4720                    {
4721                        return None;
4722                    }
4723                    q4::pending_done(mdev);
4724                    let (lb, vocab, _) = q4::encode_head_with(
4725                        &mut enc,
4726                        mdev,
4727                        &head.side,
4728                        &main_model,
4729                        &g,
4730                        &mixer_w,
4731                        lm_head,
4732                        false,
4733                        1,
4734                    )?;
4735                    q4::encode_argmax(&mut enc, mdev, &lb, vocab, &ids_buf, j + 1)?;
4736                }
4737                if !q4::copy_to_stage(&mut enc, mdev, &ids_buf, 4, 0, (k * 4) as u64) {
4738                    return None;
4739                }
4740                drop(merge);
4741                let bytes = q4::submit_chain(mdev, enc, (k * 4) as u64)?.wait()?;
4742                Some(
4743                    bytes[..k * 4]
4744                        .chunks_exact(4)
4745                        .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4746                        .collect(),
4747                )
4748            };
4749            if let Some(d) = run() {
4750                drafts = d;
4751                chained = true;
4752            }
4753        }
4754        if !chained {
4755            // host round trip per cell (the embedding table is not on the card)
4756            let mut tok_in = tok_in;
4757            for j in 0..k {
4758                let cell = next_pos - 1 + j;
4759                let mut emb = vec![0.0f32; cfg.hidden];
4760                if (tok_in as usize) < globals.embed.rows() {
4761                    globals.embed.row_f32(tok_in as usize, &mut emb);
4762                }
4763                let r = if j == 0 {
4764                    q4::whole(&main_dev.r_last)
4765                } else {
4766                    mdev.hyper(0)
4767                };
4768                let mut enc = q4::new_encoder("qwen4-mtp-draft")?;
4769                let merge = q4::merge_guard(&enc);
4770                if !q4::encode_mtp_input(
4771                    &mut enc,
4772                    mdev,
4773                    &head.side,
4774                    &g,
4775                    Some(&emb),
4776                    &head.enorm,
4777                    &head.hnorm,
4778                    head.fc_e,
4779                    head.fc_h,
4780                    r,
4781                ) {
4782                    return None;
4783                }
4784                q4::encode_layer(
4785                    &mut enc,
4786                    mdev,
4787                    &head.side,
4788                    &g,
4789                    &w,
4790                    0,
4791                    cell,
4792                    1,
4793                    inv_freq,
4794                    &remap,
4795                    shared_slot,
4796                    &[],
4797                    false,
4798                    false,
4799                )?;
4800                if !q4::encode_pending(&mut enc, mdev, &g, 1)
4801                    || !q4::finalize_pending(mdev, &g, 1, &[None], &[Vec::new()])
4802                {
4803                    return None;
4804                }
4805                q4::pending_done(mdev);
4806                let (lb, vocab, _) = q4::encode_head_with(
4807                    &mut enc,
4808                    mdev,
4809                    &head.side,
4810                    &main_model,
4811                    &g,
4812                    &mixer_w,
4813                    lm_head,
4814                    false,
4815                    1,
4816                )?;
4817                if !q4::copy_to_stage(&mut enc, mdev, &lb, 0, 0, (vocab * 4) as u64) {
4818                    return None;
4819                }
4820                drop(merge);
4821                let bytes = q4::submit_chain(mdev, enc, (vocab * 4) as u64)?.wait()?;
4822                let mut best = (0usize, f32::NEG_INFINITY);
4823                for (i, ch) in bytes[..vocab * 4].chunks_exact(4).enumerate() {
4824                    let v = f32::from_le_bytes([ch[0], ch[1], ch[2], ch[3]]);
4825                    if v > best.1 {
4826                        best = (i, v);
4827                    }
4828                }
4829                drafts.push(best.0 as u32);
4830                tok_in = best.0 as u32;
4831            }
4832        }
4833        (k, drafts)
4834    };
4835    let t_draft = t_round.elapsed();
4836    // ── verify: the window through the main model, every token's logits ──
4837    let mut ids = Vec::with_capacity(k + 1);
4838    ids.push(all_ids[next_pos]);
4839    ids.extend_from_slice(&drafts);
4840    state.verify_window = true;
4841    let mut last_logits = Vec::new();
4842    let ok = forward_tokens_device(
4843        globals,
4844        layers,
4845        cfg,
4846        state,
4847        &ids,
4848        next_pos,
4849        inv_freq,
4850        pool,
4851        &mut last_logits,
4852        true,
4853    );
4854    state.verify_window = false;
4855    if !ok || state.window_logits.len() != k + 1 {
4856        return None;
4857    }
4858    let argmax = |v: &[f32]| {
4859        v.iter()
4860            .enumerate()
4861            .fold((0usize, f32::NEG_INFINITY), |a, (i, &x)| {
4862                if x > a.1 { (i, x) } else { a }
4863            })
4864            .0
4865    };
4866    let mut a = 0usize;
4867    while a < k && argmax(&state.window_logits[a]) == drafts[a] as usize {
4868        a += 1;
4869    }
4870    let logits = std::mem::take(&mut state.window_logits).swap_remove(a);
4871    // ── roll back what the rejected drafts changed ──
4872    if a < k {
4873        let dev = state.dev.as_mut()?;
4874        if !dev.restore(&g, a + 1) {
4875            return None;
4876        }
4877        let keep = state.token_history.len() - (k - a);
4878        state.token_history.truncate(keep);
4879        state.pos = next_pos + a + 1;
4880        dev.pos = state.pos;
4881        // the last accepted position's R for the next round's first cell
4882        if let Some(mut enc) = q4::new_encoder("qwen4-keep-r") {
4883            dev.keep_r(&mut enc, a);
4884            q4::submit_only(enc);
4885        }
4886    }
4887    if let Some(head) = state.mtp.as_mut() {
4888        head.rounds += 1;
4889        head.drafted += k as u64;
4890        head.accepted += a as u64;
4891        if prof {
4892            eprintln!(
4893                "qwen4-mtp round={} drafted={k} accepted={a} draft={:.1}ms total={:.1}ms (acceptance {:.1}%) ids={:?}",
4894                head.rounds,
4895                t_draft.as_secs_f64() * 1e3,
4896                t_round.elapsed().as_secs_f64() * 1e3,
4897                head.accepted as f64 * 100.0 / head.drafted.max(1) as f64,
4898                drafts
4899            );
4900        }
4901    }
4902    Some(SpecRound {
4903        accepted: drafts[..a].to_vec(),
4904        logits,
4905        drafted: k,
4906    })
4907}
4908
4909#[cfg(test)]
4910mod tests {
4911    use super::*;
4912
4913    #[cfg(feature = "gpu")]
4914    #[test]
4915    fn pending_admission_cold_picks_keep_route_order() {
4916        let slots = [3, u32::MAX, 7, u32::MAX, 11, u32::MAX, 13, u32::MAX];
4917        assert_eq!(
4918            cold_picks_from_slots(&slots, 4, 4, &[3, 1, 0, 1]),
4919            Some(vec![3, 1, 1])
4920        );
4921        assert_eq!(cold_picks_from_slots(&slots, 4, 4, &[4]), None);
4922        assert_eq!(cold_picks_from_slots(&slots, 5, 4, &[0]), None);
4923    }
4924
4925    #[cfg(feature = "gpu")]
4926    #[test]
4927    fn mtp_k_parses_and_clamps_to_the_frame() {
4928        assert_eq!(mtp_k(None), (3, false));
4929        for k in 1..=7 {
4930            assert_eq!(mtp_k(Some(&k.to_string())), (k, false));
4931        }
4932        assert_eq!(mtp_k(Some("0")), (1, true));
4933        assert_eq!(mtp_k(Some("8")), (7, true));
4934        assert_eq!(mtp_k(Some("four")), (3, true));
4935        // every accepted k leaves a window that fits one frame
4936        assert!(mtp_k(Some("99")).0 < crate::gpu_wgpu::qwen4::TMAX);
4937    }
4938
4939    #[test]
4940    fn dynamic_pool_percent_is_bounded_per_policy() {
4941        // GLM's 40% default leaves a physical-VRAM reserve for static
4942        // attention/control tensors and transient cold-expert staging; the
4943        // operator may tune it, but never outside the policy envelope.
4944        assert_eq!(bounded_pool_pct(None, 40, 20, 60), 40);
4945        assert_eq!(bounded_pool_pct(Some("55"), 40, 20, 40), 40);
4946        assert_eq!(bounded_pool_pct(Some("999"), 40, 20, 40), 40);
4947        assert_eq!(bounded_pool_pct(Some("0"), 40, 20, 40), 20);
4948        assert_eq!(bounded_pool_pct(Some("bad"), 40, 20, 40), 40);
4949        // Qwen retains its established wider range independently.
4950        assert_eq!(bounded_pool_pct(Some("90"), 75, 25, 85), 85);
4951        assert_eq!(bounded_pool_slots(Some("9999"), 752, true), 752);
4952        assert_eq!(bounded_pool_slots(Some("9999"), 752, false), 9999);
4953    }
4954
4955    #[test]
4956    fn active_tail_floor_uses_only_reachable_layers() {
4957        // The MiniCPM 35B graph leaves 13 of 40 MoE layers in its CPU tail.
4958        // A 512-slot arena therefore protects 39 owners per active layer,
4959        // not the generic 12-owner all-layer floor.
4960        assert_eq!(active_layer_floor(512, 40), 12);
4961        assert_eq!(active_layer_floor(512, 13), 39);
4962        assert!(active_layer_floor(512, 13) * 13 <= 512);
4963        // Never over-promise protected residency on an intentionally tiny
4964        // operator override.
4965        assert_eq!(active_layer_floor(16, 13), 1);
4966        assert_eq!(active_layer_floor(12, 13), 0);
4967    }
4968
4969    #[test]
4970    fn deterministic_hash_tables_match_contract() {
4971        let q = Qwen4ExpConfig {
4972            hc_count: 4,
4973            hc_lowrank: 320,
4974            indexer_n_heads: 4,
4975            indexer_kv_heads: 1,
4976            indexer_head_dim: 128,
4977            indexer_budget: 2048,
4978            indexer_compress_ratio: 4,
4979            ple_layer_ids: vec![2],
4980            ple_embed_dim: 2560,
4981            ple_conv_kernel_size: 4,
4982            ngram_size: 3,
4983            heads_per_ngram: 8,
4984            ngram_vocab_size_base: 20_000_000,
4985            make_ngram_vocab_size_divisible_by: 128,
4986            split_ngram_parts: 128,
4987            seed: 1234,
4988        };
4989        let (m, sizes, offsets) = ple_tables(&q, 248_320, 0);
4990        assert_eq!(
4991            m,
4992            [23_703_573_157_769, 20_109_073_645_365, 8_052_911_324_071]
4993        );
4994        assert!(m.iter().all(|x| x & 1 == 1));
4995        assert_eq!(sizes.len(), 16);
4996        assert!(sizes.iter().all(|&x| is_prime(x as usize)));
4997        assert_eq!(offsets[0], 0);
4998        assert_eq!(offsets[1], sizes[0]);
4999    }
5000
5001    #[test]
5002    fn eos_breaks_ngram_context_only_after_it() {
5003        let eos = 99;
5004        assert_eq!(shifted_token(&[1, 2], 3, 1, eos), 2);
5005        assert_eq!(shifted_token(&[1, eos], 3, 1, eos), eos);
5006        assert_eq!(shifted_token(&[1, 2], eos, 1, eos), 2);
5007        assert_eq!(shifted_token(&[], 3, 2, eos), eos);
5008    }
5009}