Skip to main content

cortiq_engine/
dsv4.rs

1//! DeepSeek-V4 blocks that no other supported architecture has.
2//!
3//! Transcribed from the reference `inference/model.py` + `inference/kernel.py`
4//! shipped with the checkpoint, not inferred from tensor names — the pieces
5//! below have enough hidden structure (a second normalization on the heads, a
6//! bias that steers selection but not weights, a mixing matrix normalized by
7//! Sinkhorn) that guessing produces a model which *almost* answers.
8//!
9//! Each function is the smallest unit the reference defines, so it can be
10//! checked on its own. The forward that stitches them together comes after
11//! the attention and compressor land.
12
13/// Hyper-connections. The hidden state of this model is not a vector: it is
14/// `hc` copies of one (`hc_mult`, 4 in the release). A block folds them to
15/// one, runs attention or the FFN, then expands back — there is no ordinary
16/// residual anywhere in the stack.
17///
18/// `mixes` is the per-token projection `F.linear(x.flatten(), hc_fn) * rsqrt`
19/// of length `(2 + hc) * hc`; it splits into three parts:
20///   * `pre[j]`  — how much of copy `j` goes into the folded vector,
21///   * `post[j]` — how much of the block's output returns to copy `j`,
22///   * `comb`    — an `hc x hc` matrix mixing the old copies into the new.
23///
24/// `comb` is made doubly stochastic by Sinkhorn: a row softmax, then
25/// alternating row/column normalization. The reference runs the column step
26/// once before the loop and `iters - 1` times inside it, which is why the
27/// loop below starts from the column-normalized matrix.
28pub fn hc_split_sinkhorn(
29    mixes: &[f32],
30    hc_scale: &[f32; 3],
31    hc_base: &[f32],
32    hc: usize,
33    iters: usize,
34    eps: f32,
35    pre: &mut [f32],
36    post: &mut [f32],
37    comb: &mut [f32],
38) {
39    debug_assert_eq!(mixes.len(), (2 + hc) * hc);
40    debug_assert_eq!(comb.len(), hc * hc);
41    for j in 0..hc {
42        pre[j] = sigmoid(mixes[j] * hc_scale[0] + hc_base[j]) + eps;
43        // The post weights carry a factor 2 in the reference — with a
44        // sigmoid alone the block's output could never exceed the residual.
45        post[j] = 2.0 * sigmoid(mixes[j + hc] * hc_scale[1] + hc_base[j + hc]);
46    }
47    for j in 0..hc {
48        for k in 0..hc {
49            let idx = j * hc + k + hc * 2;
50            comb[j * hc + k] = mixes[idx] * hc_scale[2] + hc_base[idx];
51        }
52    }
53    // row softmax + eps
54    for j in 0..hc {
55        let row = &mut comb[j * hc..(j + 1) * hc];
56        let m = row.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
57        let mut sum = 0.0;
58        for v in row.iter_mut() {
59            *v = (*v - m).exp();
60            sum += *v;
61        }
62        for v in row.iter_mut() {
63            *v = *v / sum + eps;
64        }
65    }
66    // one column normalization, then (iters - 1) row/column rounds
67    normalize_cols(comb, hc, eps);
68    for _ in 0..iters.saturating_sub(1) {
69        normalize_rows(comb, hc, eps);
70        normalize_cols(comb, hc, eps);
71    }
72}
73
74fn normalize_rows(m: &mut [f32], n: usize, eps: f32) {
75    for j in 0..n {
76        let s: f32 = m[j * n..(j + 1) * n].iter().sum::<f32>() + eps;
77        for v in m[j * n..(j + 1) * n].iter_mut() {
78            *v /= s;
79        }
80    }
81}
82
83fn normalize_cols(m: &mut [f32], n: usize, eps: f32) {
84    for k in 0..n {
85        let mut s = eps;
86        for j in 0..n {
87            s += m[j * n + k];
88        }
89        for j in 0..n {
90            m[j * n + k] /= s;
91        }
92    }
93}
94
95#[inline]
96fn sigmoid(x: f32) -> f32 {
97    1.0 / (1.0 + (-x).exp())
98}
99
100/// The projection feeding `hc_split_sinkhorn`: the `hc` copies are flattened
101/// to one `hc*dim` vector, RMS-scaled (no learned weight — the reference uses
102/// a bare rsqrt of the mean square), and projected by `hc_fn` `[mix_hc, hc*dim]`.
103pub fn hc_mixes(
104    x_flat: &[f32],
105    hc_fn: &[f32],
106    mix_hc: usize,
107    eps: f32,
108    pool: Option<&crate::pool::Pool>,
109    out: &mut [f32],
110) {
111    let n = x_flat.len();
112    debug_assert_eq!(hc_fn.len(), mix_hc * n);
113    debug_assert_eq!(out.len(), mix_hc);
114    let ms = x_flat.iter().map(|v| v * v).sum::<f32>() / n as f32;
115    let rsqrt = 1.0 / (ms + eps).sqrt();
116    // A dense f32 matvec of mix_hc rows over hc*dim — 1.6 MB read per call on
117    // the release, and TWO calls per layer, so 135 MB a token. It ran on one
118    // thread and cost more than the whole attention block.
119    match pool {
120        Some(p) if n >= 4096 => {
121            let addr = crate::pool::SendMut::new(out.as_mut_ptr());
122            p.run_rows(mix_hc, &|start, end| {
123                for i in start..end {
124                    let row = &hc_fn[i * n..(i + 1) * n];
125                    let v = row.iter().zip(x_flat).map(|(a, b)| a * b).sum::<f32>() * rsqrt;
126                    unsafe { *addr.at(i) = v };
127                }
128            });
129        }
130        _ => {
131            for (i, o) in out.iter_mut().enumerate() {
132                let row = &hc_fn[i * n..(i + 1) * n];
133                *o = row.iter().zip(x_flat).map(|(a, b)| a * b).sum::<f32>() * rsqrt;
134            }
135        }
136    }
137}
138
139/// Fold `hc` copies into one vector: `y = Σ_j pre[j] · x[j]`.
140pub fn hc_fold(x: &[f32], pre: &[f32], hc: usize, dim: usize, out: &mut [f32]) {
141    debug_assert_eq!(x.len(), hc * dim);
142    out.fill(0.0);
143    for j in 0..hc {
144        let w = pre[j];
145        let src = &x[j * dim..(j + 1) * dim];
146        for (o, v) in out.iter_mut().zip(src) {
147            *o += w * v;
148        }
149    }
150}
151
152/// Expand the block's output back into `hc` copies:
153/// `y[j] = post[j] · out + Σ_k comb[k][j] · residual[k]`.
154///
155/// Note the transpose: the reference sums over the SECOND-to-last axis of
156/// `comb.unsqueeze(-1) * residual.unsqueeze(-2)`, i.e. copy `k` of the
157/// residual contributes to new copy `j` with weight `comb[k][j]`.
158pub fn hc_expand(
159    block_out: &[f32],
160    residual: &[f32],
161    post: &[f32],
162    comb: &[f32],
163    hc: usize,
164    dim: usize,
165    out: &mut [f32],
166) {
167    debug_assert_eq!(residual.len(), hc * dim);
168    debug_assert_eq!(out.len(), hc * dim);
169    for j in 0..hc {
170        let dst = &mut out[j * dim..(j + 1) * dim];
171        let p = post[j];
172        for (d, o) in dst.iter_mut().enumerate() {
173            *o = p * block_out[d];
174        }
175        for k in 0..hc {
176            let w = comb[k * hc + j];
177            let src = &residual[k * dim..(k + 1) * dim];
178            for (o, v) in dst.iter_mut().zip(src) {
179                *o += w * v;
180            }
181        }
182    }
183}
184
185/// The head fold, run once after the last layer: same shape as `hc_fold`'s
186/// weights but WITHOUT Sinkhorn — a plain sigmoid gate per copy.
187pub fn hc_head_pre(mixes: &[f32], scale: f32, base: &[f32], hc: usize, eps: f32, pre: &mut [f32]) {
188    for j in 0..hc {
189        pre[j] = sigmoid(mixes[j] * scale + base[j]) + eps;
190    }
191}
192
193/// MoE routing. Three details decide whether this model answers or merely
194/// produces fluent text:
195///   * the score is `sqrt(softplus(x))`, not a softmax or a sigmoid;
196///   * the selection bias shifts WHICH experts win but never the weights —
197///     those come from the pre-bias scores;
198///   * the weights are renormalized over the chosen experts, then scaled.
199///
200/// `bias` is `None` on the hash layers, where `indices` come from a
201/// token-id table instead (see `hash_route`).
202/// `forced` fixes the chosen experts (the hash layers' token-id table). They
203/// have to be known here rather than swapped in afterwards: the weights are
204/// the scores gathered at whichever indices win, so substituting the indices
205/// later leaves every weight attached to a different expert.
206pub fn route(
207    scores_in: &[f32],
208    bias: Option<&[f32]>,
209    top_k: usize,
210    route_scale: f32,
211    forced: Option<&[usize]>,
212    mask: Option<&[bool]>,
213    indices: &mut Vec<usize>,
214    weights: &mut Vec<f32>,
215) {
216    let n = scores_in.len();
217    let mut scores = Vec::with_capacity(n);
218    for &s in scores_in {
219        // softplus, guarded like the reference's F.softplus (linear past 20)
220        let sp = if s > 20.0 { s } else { (1.0 + s.exp()).ln() };
221        scores.push(sp.sqrt());
222    }
223    indices.clear();
224    weights.clear();
225    match forced {
226        Some(f) => indices.extend(f.iter().copied()),
227        None => {
228            let mut shifted: Vec<f32> = match bias {
229                Some(b) => scores.iter().zip(b).map(|(s, b)| s + b).collect(),
230                None => scores.clone(),
231            };
232            if let Some(m) = mask {
233                for (i, s) in shifted.iter_mut().enumerate() {
234                    if !m.get(i).copied().unwrap_or(true) {
235                        *s = f32::NEG_INFINITY;
236                    }
237                }
238            }
239            for _ in 0..top_k.min(n) {
240                let mut best = 0usize;
241                let mut bv = f32::NEG_INFINITY;
242                for (i, &v) in shifted.iter().enumerate() {
243                    if v > bv {
244                        bv = v;
245                        best = i;
246                    }
247                }
248                if !bv.is_finite() {
249                    break;
250                }
251                indices.push(best);
252                shifted[best] = f32::NEG_INFINITY;
253            }
254        }
255    }
256    // The weight is always the PRE-bias score of the chosen expert.
257    for &i in indices.iter() {
258        weights.push(scores.get(i).copied().unwrap_or(0.0));
259    }
260    let sum: f32 = weights.iter().sum();
261    if sum > 0.0 {
262        for w in weights.iter_mut() {
263            *w = *w / sum * route_scale;
264        }
265    }
266}
267
268/// Hash layers: the experts of token `tid` are a row of the `tid2eid` table,
269/// and the router does not run at all. Their weights still come from the
270/// scored path (the reference gathers `original_scores` at those indices).
271pub fn hash_route(tid2eid: &[f32], vocab: usize, top_k: usize, tid: u32) -> Vec<usize> {
272    let row = (tid as usize).min(vocab.saturating_sub(1)) * top_k;
273    (0..top_k)
274        .map(|k| tid2eid.get(row + k).copied().unwrap_or(0.0) as usize)
275        .collect()
276}
277
278/// Rotary on the LAST `rd` dims only — the rest of the head carries no
279/// position. `inverse` runs the rotation backwards, which the reference
280/// applies to the attention OUTPUT before the o-projection (the value
281/// stream carries the same rope-tagged tail as the keys, and it has to be
282/// untagged again). Missing that step leaves a model that reads fluently
283/// and attends to the wrong offsets.
284pub fn rope_tail(v: &mut [f32], inv_freq: &[f32], pos: usize, rd: usize, inverse: bool) {
285    let n = v.len();
286    debug_assert!(
287        rd <= n && rd % 2 == 0,
288        "rope tail {rd} wider than the vector {n}"
289    );
290    // A tail wider than the vector is a configuration mistake, and `n - rd`
291    // would wrap into an index in the billions rather than say so.
292    let rd = rd.min(n) & !1;
293    let base = n - rd;
294    // ADJACENT pairs, not halves. The reference forms its complex numbers
295    // with `unflatten(-1, (-1, 2))` + `view_as_complex`, i.e. (x0,x1),
296    // (x2,x3), … — the interleaved convention. Half-split pairing agrees
297    // with it exactly at position 0, where the rotation is the identity,
298    // and disagrees everywhere else. That is why short answers came out
299    // right and everything longer drifted, repeated itself and could not
300    // count: every position past the first was rotated into the wrong
301    // basis.
302    for i in 0..rd / 2 {
303        let theta = pos as f32 * inv_freq[i];
304        let (s, c) = (theta.sin(), theta.cos());
305        let s = if inverse { -s } else { s };
306        let a = v[base + 2 * i];
307        let b = v[base + 2 * i + 1];
308        v[base + 2 * i] = a * c - b * s;
309        v[base + 2 * i + 1] = a * s + b * c;
310    }
311}
312
313/// RMS normalize in place with no learned weight — the reference applies
314/// this to each attention head AFTER `wq_b`, on top of the `q_norm` that
315/// already normalized the LoRA rank. Two normalizations, not one.
316pub fn rms_inplace(v: &mut [f32], eps: f32) {
317    let ms = v.iter().map(|x| x * x).sum::<f32>() / v.len() as f32;
318    let inv = 1.0 / (ms + eps).sqrt();
319    for x in v.iter_mut() {
320        *x *= inv;
321    }
322}
323
324/// Attention over an explicit position LIST (window ⊕ compressed), with a
325/// learned per-head sink. The sink is an extra logit with no value vector:
326/// it lets a head attend to "nothing", so its softmax denominator carries
327/// `exp(sink - max)` while contributing no output. Index `usize::MAX`
328/// marks a masked slot (the reference writes -1 into topk_idxs).
329pub fn sparse_attend(
330    q: &[f32],
331    kv: &[f32],
332    idxs: &[usize],
333    sink: f32,
334    scale: f32,
335    head_dim: usize,
336    out: &mut [f32],
337) {
338    let mut m = sink;
339    let mut scores = Vec::with_capacity(idxs.len());
340    for &p in idxs {
341        if p == usize::MAX {
342            scores.push(f32::NEG_INFINITY);
343            continue;
344        }
345        let k = &kv[p * head_dim..(p + 1) * head_dim];
346        let dot: f32 = q.iter().zip(k).map(|(a, b)| a * b).sum::<f32>() * scale;
347        m = m.max(dot);
348        scores.push(dot);
349    }
350    let mut denom = (sink - m).exp();
351    out.fill(0.0);
352    for (&p, &s) in idxs.iter().zip(&scores) {
353        if p == usize::MAX {
354            continue;
355        }
356        let w = (s - m).exp();
357        denom += w;
358        let v = &kv[p * head_dim..(p + 1) * head_dim];
359        for (o, x) in out.iter_mut().zip(v) {
360            *o += w * x;
361        }
362    }
363    if std::env::var("CMF_ATTN_DEBUG").is_ok() {
364        eprintln!(
365            "    [порт] позиций={} score={:?} sink={sink:.4} denom={denom:.4} |q|={:.3}",
366            idxs.iter().filter(|&&p| p != usize::MAX).count(),
367            scores
368                .iter()
369                .map(|x| (x * 10000.0).round() / 10000.0)
370                .collect::<Vec<_>>(),
371            q.iter().map(|x| x * x).sum::<f32>().sqrt()
372        );
373    }
374    let inv = 1.0 / denom;
375    for o in out.iter_mut() {
376        *o *= inv;
377    }
378}
379
380/// The grouped low-rank output projection: heads are split into `groups`,
381/// each group's slice is projected to `lora` by its own block of `wo_a`,
382/// and the concatenation goes through `wo_b`. `wo_a` is stored
383/// `[groups, lora, per_group]`.
384/// `wo_a_row` is `(row, x) -> dot`, reading one row of `wo_a` against the
385/// slice of `attn` its group owns; `wo_b` is the plain projection of the
386/// concatenated groups. Both arrive as closures so the caller can serve them
387/// straight from quantized tensors.
388pub fn o_project(
389    attn: &[f32],
390    wo_a_row: &(dyn Fn(usize, &[f32], &mut [f32]) -> f32 + Sync),
391    scratch_len: usize,
392    wo_b: &dyn Fn(&[f32], &mut [f32]),
393    groups: usize,
394    lora: usize,
395    pool: Option<&crate::pool::Pool>,
396    out: &mut [f32],
397) {
398    let per_group = attn.len() / groups;
399    let mut mid = vec![0.0f32; groups * lora];
400    let slice_of = |i: usize| {
401        let g = i / lora;
402        &attn[g * per_group..(g + 1) * per_group]
403    };
404    match pool {
405        // Each row of `mid` is one dot product against its group's slice —
406        // independent, so the rows split cleanly. This is the largest
407        // single-threaded cost in the decode otherwise: on the release
408        // checkpoint wo_a is 33M weights, read once per layer per token.
409        Some(p) if mid.len() >= 256 => {
410            let addr = crate::pool::SendMut::new(mid.as_mut_ptr());
411            p.run_rows(mid.len(), &|start, end| {
412                let mut sc = vec![0.0f32; scratch_len];
413                for i in start..end {
414                    let v = wo_a_row(i, slice_of(i), &mut sc);
415                    unsafe { *addr.at(i) = v };
416                }
417            });
418        }
419        _ => {
420            let mut sc = vec![0.0f32; scratch_len];
421            for (i, m) in mid.iter_mut().enumerate() {
422                *m = wo_a_row(i, slice_of(i), &mut sc);
423            }
424        }
425    }
426    wo_b(&mid, out);
427}
428
429pub fn compress_window(
430    kv: &[f32],
431    score: &[f32],
432    ape: &[f32],
433    ratio: usize,
434    width: usize,
435    out: &mut [f32],
436) {
437    debug_assert_eq!(kv.len(), ratio * width);
438    debug_assert_eq!(ape.len(), ratio * width);
439    let biased: Vec<f32> = score.iter().zip(ape).map(|(s, a)| s + a).collect();
440    pool_by_score(kv, &biased, ratio, width, out);
441}
442
443/// Softmax over the `slots` axis, per dimension, then the weighted sum —
444/// the pooling both the plain and the overlapping compressor end in.
445/// `-inf` scores are how an absent slot votes for nothing, so the
446/// max-subtraction has to survive a whole column of them.
447pub fn pool_by_score(kv: &[f32], score: &[f32], slots: usize, width: usize, out: &mut [f32]) {
448    debug_assert_eq!(kv.len(), slots * width);
449    debug_assert_eq!(score.len(), slots * width);
450    out.fill(0.0);
451    for d in 0..width {
452        let mut m = f32::NEG_INFINITY;
453        for t in 0..slots {
454            m = m.max(score[t * width + d]);
455        }
456        if !m.is_finite() {
457            continue;
458        }
459        let mut denom = 0.0;
460        for t in 0..slots {
461            denom += (score[t * width + d] - m).exp();
462        }
463        if denom <= 0.0 {
464            continue;
465        }
466        for t in 0..slots {
467            out[d] += ((score[t * width + d] - m).exp() / denom) * kv[t * width + d];
468        }
469    }
470}
471
472/// The overlapping compressor (the release uses it wherever the ratio is 4).
473///
474/// Each token contributes `2*d` values: the first half belongs to the window
475/// that started half a stride earlier, the second half to the current one.
476/// At fold time the reference pools `2*ratio` entries of width `d` — the
477/// PREVIOUS window's slots taking their first half, the current window's
478/// slots taking their second half — then the current window becomes the
479/// previous one. An absent previous window votes with `-inf`.
480#[allow(clippy::too_many_arguments)]
481pub fn compress_window_overlap(
482    prev_kv: &[f32],
483    prev_score: &[f32],
484    cur_kv: &[f32],
485    cur_score: &[f32],
486    ratio: usize,
487    d: usize,
488    out: &mut [f32],
489) {
490    let slots = 2 * ratio;
491    let mut kv = vec![0.0f32; slots * d];
492    let mut sc = vec![f32::NEG_INFINITY; slots * d];
493    let have_prev = prev_kv.len() == ratio * 2 * d;
494    for t in 0..ratio {
495        if have_prev {
496            // the previous window's slots, first half of the dimensions
497            kv[t * d..(t + 1) * d].copy_from_slice(&prev_kv[t * 2 * d..t * 2 * d + d]);
498            sc[t * d..(t + 1) * d].copy_from_slice(&prev_score[t * 2 * d..t * 2 * d + d]);
499        }
500        // the current window's slots, second half
501        let src = t * 2 * d + d;
502        let dst = (ratio + t) * d;
503        kv[dst..dst + d].copy_from_slice(&cur_kv[src..src + d]);
504        sc[dst..dst + d].copy_from_slice(&cur_score[src..src + d]);
505    }
506    pool_by_score(&kv, &sc, slots, d, out);
507}
508
509/// The sparse indexer's scoring pass. For each query it ranks the
510/// compressed positions and keeps the best `topk`.
511///
512/// Three details from the reference that a shape-only reading misses:
513///   * the query comes from the SHARED LoRA output `qr` (the output of
514///     `q_norm(wq_a(x))`, before attention's own `wq_b`), through the
515///     indexer's own `wq_b` — not from attention's queries;
516///   * scores are **relu'd** before the per-head weighting, so a head can
517///     only ever vote for a position, never against it;
518///   * the per-head weights are a projection of the hidden state scaled by
519///     `head_dim^-0.5 * n_heads^-0.5`.
520///
521/// `causal_limit` is the number of compressed positions this query may see
522/// (`(pos + 1) / ratio`); anything at or past it is masked.
523#[allow(clippy::too_many_arguments)]
524pub fn index_scores(
525    q_heads: &[f32],
526    kv: &[f32],
527    head_weights: &[f32],
528    n_heads: usize,
529    head_dim: usize,
530    n_pos: usize,
531    causal_limit: usize,
532    pool: Option<&crate::pool::Pool>,
533    out: &mut Vec<f32>,
534) {
535    out.clear();
536    out.resize(n_pos, 0.0);
537    let score_at = |t: usize| -> f32 {
538        if t >= causal_limit {
539            return f32::NEG_INFINITY;
540        }
541        let k = &kv[t * head_dim..(t + 1) * head_dim];
542        let mut acc = 0.0;
543        for h in 0..n_heads {
544            let q = &q_heads[h * head_dim..(h + 1) * head_dim];
545            let dot: f32 = q.iter().zip(k).map(|(a, b)| a * b).sum();
546            // relu BEFORE weighting: a head votes for a position or abstains
547            acc += dot.max(0.0) * head_weights[h];
548        }
549        acc
550    };
551    // Positions are independent, and their number grows with the context —
552    // this was the one loop in the attention step still walking the whole
553    // compressed axis on one thread.
554    match pool {
555        Some(p) if n_pos >= 64 => {
556            let addr = crate::pool::SendMut::new(out.as_mut_ptr());
557            p.run_rows(n_pos, &|start, end| {
558                for t in start..end {
559                    unsafe { *addr.at(t) = score_at(t) };
560                }
561            });
562        }
563        _ => {
564            for (t, o) in out.iter_mut().enumerate() {
565                *o = score_at(t);
566            }
567        }
568    }
569}
570
571/// Top-`k` positions by score, ties broken by the lower index so the choice
572/// is deterministic across backends. Masked slots (-inf) never win, and a
573/// short history simply returns fewer than `k`.
574pub fn top_k_positions(scores: &[f32], k: usize, out: &mut Vec<usize>) {
575    out.clear();
576    // When k reaches the whole list there is nothing to choose: every finite
577    // position wins, and they come out in index order anyway. The general
578    // path is k rounds of argmax — O(k·n) — and at index_topk = 512 against a
579    // compressed axis that is still shorter than that, it was doing 160k
580    // comparisons a layer to arrive at "all of them". This grows with the
581    // context, which is exactly when it hurts.
582    if k >= scores.len() {
583        out.extend(
584            scores
585                .iter()
586                .enumerate()
587                .filter(|(_, v)| v.is_finite())
588                .map(|(i, _)| i),
589        );
590        return;
591    }
592    let mut taken = vec![false; scores.len()];
593    for _ in 0..k.min(scores.len()) {
594        let mut best = usize::MAX;
595        let mut bv = f32::NEG_INFINITY;
596        for (i, &v) in scores.iter().enumerate() {
597            if !taken[i] && v > bv && v.is_finite() {
598                bv = v;
599                best = i;
600            }
601        }
602        if best == usize::MAX {
603            break;
604        }
605        taken[best] = true;
606        out.push(best);
607    }
608    out.sort_unstable();
609}
610
611/// SwiGLU expert: `w2(silu(w1(x)) * w3(x))`, with the routing weight folded
612/// in before the down projection exactly as the reference does.
613///
614/// `limit` is the reference's `swiglu_limit` (10.0 in the release), and its
615/// asymmetry is not a typo: `up` is clamped on BOTH sides, `gate` only from
616/// above — the reference leaves silu's negative tail alone. A limit of 0
617/// disables the clamp, which is also what the reference does.
618#[allow(clippy::too_many_arguments)]
619pub fn expert_swiglu(
620    x: &[f32],
621    w1: &dyn Fn(&[f32], &mut [f32]),
622    w3: &dyn Fn(&[f32], &mut [f32]),
623    w2: &dyn Fn(&[f32], &mut [f32]),
624    inter: usize,
625    weight: f32,
626    limit: f32,
627    out: &mut [f32],
628) {
629    let mut gate = vec![0.0f32; inter];
630    let mut up = vec![0.0f32; inter];
631    w1(x, &mut gate);
632    w3(x, &mut up);
633    if limit > 0.0 {
634        for u in up.iter_mut() {
635            *u = u.clamp(-limit, limit);
636        }
637        for g in gate.iter_mut() {
638            *g = g.min(limit);
639        }
640    }
641    for (g, u) in gate.iter_mut().zip(&up) {
642        let silu = *g / (1.0 + (-*g).exp());
643        *g = silu * u * weight;
644    }
645    w2(&gate, out);
646}
647
648/// Everything one layer needs that is not a plain matrix: the shapes and
649/// scalars the reference reads out of `ModelArgs`.
650#[derive(Debug, Clone, Copy)]
651pub struct Dsv4Cfg {
652    pub dim: usize,
653    pub n_heads: usize,
654    pub head_dim: usize,
655    pub rope_head_dim: usize,
656    pub q_lora_rank: usize,
657    pub o_lora_rank: usize,
658    pub o_groups: usize,
659    pub hc_mult: usize,
660    pub hc_sinkhorn_iters: usize,
661    pub hc_eps: f32,
662    pub norm_eps: f32,
663    pub n_routed_experts: usize,
664    pub top_k: usize,
665    pub moe_inter: usize,
666    pub route_scale: f32,
667    /// The reference's `swiglu_limit`; 0 disables the clamp.
668    pub swiglu_limit: f32,
669    /// Sliding-window size (`window_size`, 128 in the release).
670    pub window: usize,
671    pub index_topk: usize,
672    pub vocab: usize,
673}
674
675/// The per-block hyper-connection cycle, which is the same shape around
676/// attention and around the FFN: fold the copies, normalize, run the
677/// block, expand back. `block` sees a plain `dim`-vector and knows nothing
678/// about the copies — that separation is what keeps attention and the MoE
679/// free of hyper-connection bookkeeping.
680///
681/// `hc_fn` is `[mix_hc, hc*dim]`, `hc_base` is `[mix_hc]`, `hc_scale` is 3.
682#[allow(clippy::too_many_arguments)]
683#[allow(clippy::too_many_arguments)]
684pub fn hc_block<F: FnMut(&[f32], &mut [f32])>(
685    state: &mut [f32],
686    hc_fn: &[f32],
687    hc_scale: &[f32; 3],
688    hc_base: &[f32],
689    norm_w: &[f32],
690    cfg: &Dsv4Cfg,
691    scratch: &mut HcScratch,
692    pool: Option<&crate::pool::Pool>,
693    mut block: F,
694) {
695    let (hc, dim) = (cfg.hc_mult, cfg.dim);
696    let mix_hc = (2 + hc) * hc;
697    hc_mixes(state, hc_fn, mix_hc, cfg.norm_eps, pool, &mut scratch.mixes);
698    hc_split_sinkhorn(
699        &scratch.mixes,
700        hc_scale,
701        hc_base,
702        hc,
703        cfg.hc_sinkhorn_iters,
704        cfg.hc_eps,
705        &mut scratch.pre,
706        &mut scratch.post,
707        &mut scratch.comb,
708    );
709    hc_fold(state, &scratch.pre, hc, dim, &mut scratch.folded);
710    // RMSNorm with the layer's learned weight, on the folded vector.
711    let ms = scratch.folded.iter().map(|v| v * v).sum::<f32>() / dim as f32;
712    let inv = 1.0 / (ms + cfg.norm_eps).sqrt();
713    for (v, w) in scratch.folded.iter_mut().zip(norm_w) {
714        *v = *v * inv * w;
715    }
716    block(&scratch.folded, &mut scratch.block_out);
717    scratch.residual.copy_from_slice(state);
718    hc_expand(
719        &scratch.block_out,
720        &scratch.residual,
721        &scratch.post,
722        &scratch.comb,
723        hc,
724        dim,
725        state,
726    );
727}
728
729/// Reusable buffers for `hc_block` — one allocation per pipeline, not per
730/// layer per token.
731pub struct HcScratch {
732    pub mixes: Vec<f32>,
733    pub pre: Vec<f32>,
734    pub post: Vec<f32>,
735    pub comb: Vec<f32>,
736    pub folded: Vec<f32>,
737    pub block_out: Vec<f32>,
738    pub residual: Vec<f32>,
739}
740
741impl HcScratch {
742    pub fn new(cfg: &Dsv4Cfg) -> Self {
743        let (hc, dim) = (cfg.hc_mult, cfg.dim);
744        Self {
745            mixes: vec![0.0; (2 + hc) * hc],
746            pre: vec![0.0; hc],
747            post: vec![0.0; hc],
748            comb: vec![0.0; hc * hc],
749            folded: vec![0.0; dim],
750            block_out: vec![0.0; dim],
751            residual: vec![0.0; hc * dim],
752        }
753    }
754}
755
756/// The final fold, after the last layer: `hc` copies to one vector, with a
757/// plain sigmoid gate (no Sinkhorn), then the model's output norm.
758pub fn hc_head_fold(
759    state: &[f32],
760    hc_fn: &[f32],
761    hc_scale: f32,
762    hc_base: &[f32],
763    cfg: &Dsv4Cfg,
764    pool: Option<&crate::pool::Pool>,
765    out: &mut [f32],
766) {
767    let (hc, dim) = (cfg.hc_mult, cfg.dim);
768    let mut mixes = vec![0.0f32; hc];
769    hc_mixes(state, hc_fn, hc, cfg.norm_eps, pool, &mut mixes);
770    let mut pre = vec![0.0f32; hc];
771    hc_head_pre(&mixes, hc_scale, hc_base, hc, cfg.hc_eps, &mut pre);
772    hc_fold(state, &pre, hc, dim, out);
773}
774
775/// One layer's weights. Everything quantized rides as `QTensor` so the
776/// existing kernels (and the mmap) serve them; the small fp32 pieces —
777/// norms, the hyper-connection projections, the sink, the compressor's
778/// position bias — are plain vectors, exactly as the reference keeps them
779/// in fp32 regardless of the checkpoint's storage dtype.
780pub struct Dsv4Layer {
781    pub attn_norm: Vec<f32>,
782    pub ffn_norm: Vec<f32>,
783    // attention: the double LoRA, the compressed KV, the grouped output
784    pub wq_a: crate::qtensor::QTensor,
785    pub q_norm: Vec<f32>,
786    pub wq_b: crate::qtensor::QTensor,
787    pub wkv: crate::qtensor::QTensor,
788    pub kv_norm: Vec<f32>,
789    pub wo_a: crate::qtensor::QTensor,
790    pub wo_b: crate::qtensor::QTensor,
791    pub attn_sink: Vec<f32>,
792    /// `None` on the pure sliding-window layers (`compress_ratio == 0`).
793    pub compressor: Option<Dsv4Compressor>,
794    /// Only on the layers whose ratio is 4.
795    pub indexer: Option<Dsv4Indexer>,
796    // hyper-connections, one set for the attention half and one for the FFN
797    pub hc_attn_fn: Vec<f32>,
798    pub hc_attn_base: Vec<f32>,
799    pub hc_attn_scale: [f32; 3],
800    pub hc_ffn_fn: Vec<f32>,
801    pub hc_ffn_base: Vec<f32>,
802    pub hc_ffn_scale: [f32; 3],
803    // MoE
804    pub gate: crate::qtensor::QTensor,
805    /// noaux_tc selection bias — `None` on the hash layers.
806    pub gate_bias: Option<Vec<f32>>,
807    /// Token-id → expert table on the hash layers, `None` elsewhere.
808    pub tid2eid: Option<Vec<f32>>,
809    pub experts: Vec<Dsv4Expert>,
810    pub shared: Dsv4Expert,
811    /// Task-conditional restriction over the routed experts
812    /// (`CMF_MOE_MASK` + `CMF_MOE_MASK_COVER`): `false` experts are not
813    /// selectable and the weights renormalize over what remains. `None` on
814    /// the hash layers — their table names specific experts, so masking
815    /// there would silently reroute rather than restrict.
816    pub mask: Option<Vec<bool>>,
817}
818
819pub struct Dsv4Expert {
820    pub w1: crate::qtensor::QTensor,
821    pub w2: crate::qtensor::QTensor,
822    pub w3: crate::qtensor::QTensor,
823}
824
825pub struct Dsv4Compressor {
826    pub wkv: crate::qtensor::QTensor,
827    pub wgate: crate::qtensor::QTensor,
828    pub norm: Vec<f32>,
829    /// `[ratio, coff*head_dim]` — the in-window position bias.
830    pub ape: Vec<f32>,
831    pub ratio: usize,
832    /// Overlapping windows (the reference sets this when ratio == 4), which
833    /// doubles the projection width.
834    pub overlap: bool,
835}
836
837pub struct Dsv4Indexer {
838    pub wq_b: crate::qtensor::QTensor,
839    pub weights_proj: crate::qtensor::QTensor,
840    pub compressor: Dsv4Compressor,
841}
842
843/// Model-global pieces: the embedding, the output head and the final
844/// hyper-connection fold.
845pub struct Dsv4Globals {
846    /// RoPE frequencies for the layers that carry a KV compressor: base
847    /// `compress_rope_theta` (160 000 in the release) WITH YaRN.
848    pub inv_freq_compress: Vec<f32>,
849    /// …and for the pure sliding-window layers: base `rope_theta` (10 000)
850    /// with YaRN OFF. The reference picks per layer:
851    ///   if compress_ratio { original_seq_len, compress_rope_theta }
852    ///   else              { 0, rope_theta }   // "disable YaRN"
853    /// One shared table gets both groups wrong — the model still retrieves
854    /// facts, because attention still attends, but every position is rotated
855    /// by the wrong angle, so it repeats itself and cannot count.
856    pub inv_freq_window: Vec<f32>,
857    pub embed: crate::qtensor::QTensor,
858    pub norm: Vec<f32>,
859    pub head: crate::qtensor::QTensor,
860    pub hc_head_fn: Vec<f32>,
861    pub hc_head_base: Vec<f32>,
862    pub hc_head_scale: f32,
863}
864
865/// Per-sequence state. The compressor and the indexer each keep their own
866/// compressed cache and a partial window, so decode picks up mid-window
867/// exactly where prefill left off.
868pub struct Dsv4State {
869    /// Sliding-window KV per layer, `[window, head_dim]` ring.
870    pub window: Vec<Vec<f32>>,
871    /// Compressed KV per layer, appended once per `ratio` tokens.
872    pub compressed: Vec<Vec<f32>>,
873    /// The indexer's own compressed cache per layer.
874    pub index_kv: Vec<Vec<f32>>,
875    /// Partial window being accumulated, per layer: kv and score streams.
876    pub pending_kv: Vec<Vec<f32>>,
877    pub pending_score: Vec<Vec<f32>>,
878    /// The window before it, kept only by the overlapping compressor —
879    /// its fold reads half its dimensions from the previous stride.
880    pub prev_kv: Vec<Vec<f32>>,
881    pub prev_score: Vec<Vec<f32>>,
882    /// The indexer's compressor runs alongside the attention one and keeps
883    /// its own window — same shape, different width and different weights.
884    pub pending_ix_kv: Vec<Vec<f32>>,
885    pub pending_ix_score: Vec<Vec<f32>>,
886    pub prev_ix_kv: Vec<Vec<f32>>,
887    pub prev_ix_score: Vec<Vec<f32>>,
888    pub pos: usize,
889    /// Identifies this sequence's caches on the device. A fresh state gets a
890    /// fresh id, so a device buffer left over from the previous conversation
891    /// can never be read as if it belonged to this one.
892    pub kv_id: u64,
893    /// When the token graph owns a layer's caches, the CONTENTS live on the
894    /// card and only these counts stay here — how much of the window is
895    /// filled, and how many compressed entries each cache holds. All three
896    /// follow from the position, so keeping them costs nothing and reading
897    /// them back would cost a round trip.
898    pub dev_filled: Vec<usize>,
899    pub dev_n_comp: Vec<usize>,
900    pub dev_n_ix: Vec<usize>,
901    /// True once this sequence has run a layer on the card with the device
902    /// owning its state. The host copies above are stale from then on, so
903    /// the CPU path must not be used for that layer again.
904    pub dev_owned: bool,
905    /// The device-layer set of the FIRST chained token. If it ever differs,
906    /// some layer's caches are on the wrong side and the answer would be
907    /// quietly wrong — the loop refuses instead.
908    pub dev_set: Vec<bool>,
909    /// Which layers run their MoE on the card from a PARTIAL expert pack.
910    /// Their walk attention must stay on the host: the device attention
911    /// frame and the device MoE frame of one layer share pooled slots and
912    /// poison each other across tokens (see `attention_step`).
913    pub partial_set: Vec<bool>,
914    /// More than one layer walks past the device prefix. The stale-slot
915    /// poison needs a CHAIN of walk frames handing state through the pooled
916    /// slots; a single tail layer (the canonical shape) never chains and
917    /// its device attention is measured exact.
918    pub split_deep: bool,
919}
920
921impl Dsv4State {
922    pub fn new(layers: usize) -> Self {
923        use std::sync::atomic::{AtomicU64, Ordering};
924        static NEXT: AtomicU64 = AtomicU64::new(1);
925        Self {
926            kv_id: NEXT.fetch_add(1, Ordering::Relaxed),
927            dev_filled: vec![0; layers],
928            dev_n_comp: vec![0; layers],
929            dev_n_ix: vec![0; layers],
930            dev_owned: false,
931            dev_set: Vec::new(),
932            partial_set: Vec::new(),
933            split_deep: false,
934            window: vec![Vec::new(); layers],
935            compressed: vec![Vec::new(); layers],
936            index_kv: vec![Vec::new(); layers],
937            pending_kv: vec![Vec::new(); layers],
938            pending_score: vec![Vec::new(); layers],
939            prev_kv: vec![Vec::new(); layers],
940            prev_score: vec![Vec::new(); layers],
941            pending_ix_kv: vec![Vec::new(); layers],
942            pending_ix_score: vec![Vec::new(); layers],
943            prev_ix_kv: vec![Vec::new(); layers],
944            prev_ix_score: vec![Vec::new(); layers],
945            pos: 0,
946        }
947    }
948}
949
950/// One attention block for a single position. `hidden` is the folded,
951/// normalized vector `hc_block` hands over; the result goes back to it.
952///
953/// The order matters and is the reference's: q through the LoRA pair with
954/// a normalization at each end, kv compressed to one head's width, rope on
955/// the tails, the window and the compressed positions concatenated into
956/// one index list, sparse attention with the sink, the INVERSE rope on the
957/// output, then the grouped low-rank projection.
958#[allow(clippy::too_many_arguments)]
959/// Advance one compressor by a token and return its folded entry when the
960/// window closes. Both the attention compressor and the indexer's own run
961/// through here — the indexer's was simply never called, so its cache stayed
962/// empty and every layer that has an indexer selected ZERO compressed
963/// positions, discarding a correctly-built long-range memory.
964#[allow(clippy::too_many_arguments)]
965fn compressor_step(
966    cp: &Dsv4Compressor,
967    hidden: &[f32],
968    pos: usize,
969    rd: usize,
970    norm_eps: f32,
971    inv_freq: &[f32],
972    pool: Option<&crate::pool::Pool>,
973    pending_kv: &mut Vec<f32>,
974    pending_score: &mut Vec<f32>,
975    prev_kv: &mut Vec<f32>,
976    prev_score: &mut Vec<f32>,
977) -> Option<Vec<f32>> {
978    let width = cp.wkv.rows();
979    let ew = if cp.overlap { width / 2 } else { width };
980    let mut ckv = vec![0.0f32; width];
981    let mut cscore = vec![0.0f32; width];
982    // Same input, so one dispatch instead of two — and this runs twice a
983    // layer (the compressor and the indexer's own), 43 layers a token.
984    crate::qtensor::QTensor::matvec_many(
985        [&cp.wkv, &cp.wgate],
986        hidden,
987        [&mut ckv, &mut cscore],
988        pool,
989    );
990    if cp.overlap {
991        // The reference biases the score as the token arrives and keeps it
992        // biased across the shift, so ape is added ONCE, here.
993        let slot = pos % cp.ratio;
994        for (c, a) in cscore
995            .iter_mut()
996            .zip(&cp.ape[slot * width..(slot + 1) * width])
997        {
998            *c += a;
999        }
1000    }
1001    pending_kv.extend_from_slice(&ckv);
1002    pending_score.extend_from_slice(&cscore);
1003    if pending_kv.len() / width < cp.ratio {
1004        return None;
1005    }
1006    let mut folded = vec![0.0f32; ew];
1007    if cp.overlap {
1008        compress_window_overlap(
1009            prev_kv,
1010            prev_score,
1011            pending_kv,
1012            pending_score,
1013            cp.ratio,
1014            ew,
1015            &mut folded,
1016        );
1017        *prev_kv = std::mem::take(pending_kv);
1018        *prev_score = std::mem::take(pending_score);
1019    } else {
1020        compress_window(
1021            pending_kv,
1022            pending_score,
1023            &cp.ape,
1024            cp.ratio,
1025            width,
1026            &mut folded,
1027        );
1028    }
1029    rms_weighted(&mut folded, &cp.norm, norm_eps);
1030    // The entry carries the same rope-tagged tail as a window key, at the
1031    // position of the window's first token.
1032    rope_tail(&mut folded, inv_freq, pos + 1 - cp.ratio, rd, false);
1033    pending_kv.clear();
1034    pending_score.clear();
1035    Some(folded)
1036}
1037
1038/// `CMF_DSV4_PROFILE=1` accumulates wall time per stage and prints the split
1039/// when the process ends. Guessing which half of a layer costs what is how
1040/// one ends up optimising the cheap one: the fused attention block came out a
1041/// wash on the release checkpoint, and no amount of reasoning about MAC
1042/// counts settles whether that is because attention was already cheap or
1043/// because the device arm was slow.
1044pub(crate) mod prof {
1045    use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
1046
1047    pub static ATTN_NS: AtomicU64 = AtomicU64::new(0);
1048    pub static MOE_NS: AtomicU64 = AtomicU64::new(0);
1049    pub static CALLS: AtomicU64 = AtomicU64::new(0);
1050    /// Everything in a layer that is neither attention nor the experts: the
1051    /// hyper-connection fold and expand, the two norms, the residual.
1052    pub static HC_NS: AtomicU64 = AtomicU64::new(0);
1053    /// The head: final norm plus lm_head over 129280 rows.
1054    pub static HEAD_NS: AtomicU64 = AtomicU64::new(0);
1055    /// Host prep of the device layer: attention_step's CPU half (indexer,
1056    /// compressor, qr) — suspected owner of the unaccounted milliseconds.
1057    pub static PREP_NS: AtomicU64 = AtomicU64::new(0);
1058    /// The per-layer KV/window cache uploads before the frame.
1059    pub static CACHEW_NS: AtomicU64 = AtomicU64::new(0);
1060    /// The whole forward, so the buckets can be checked against a total
1061    /// instead of against a guess. 78 ms of measured work in a 108 ms token
1062    /// left 30 ms that no counter had ever looked at.
1063    pub static ALL_NS: AtomicU64 = AtomicU64::new(0);
1064    pub static TOKENS: AtomicU64 = AtomicU64::new(0);
1065
1066    /// One token = one visit to layer zero. Counting `moe_step` calls instead
1067    /// counts layers.
1068    pub fn note_layer(li: usize) {
1069        CALLS.fetch_add(1, Ordering::Relaxed);
1070        if li == 0 {
1071            // The first token pays for the whole expert set reaching the card
1072            // — tens of seconds of it. Left in, that one-time cost is divided
1073            // by every later call and reads as a per-call price: it is what
1074            // made "the host encodes for 4.45 ms a layer" out of an upload
1075            // that happens once. Everything measured before the SECOND token
1076            // starts is therefore thrown away, and the report describes
1077            // steady state, which is the only thing worth optimising.
1078            // `swap` and not a TOKENS comparison: resetting TOKENS to 1 made
1079            // the test true again on every later token, so the report
1080            // described one token instead of the run.
1081            if TOKENS.fetch_add(1, Ordering::Relaxed) == 1 && !ZEROED.swap(true, Ordering::Relaxed)
1082            {
1083                for a in [
1084                    &ATTN_NS, &MOE_NS, &HC_NS, &HEAD_NS, &ALL_NS, &CALLS, &PREP_NS, &CACHEW_NS,
1085                ] {
1086                    a.store(0, Ordering::Relaxed);
1087                }
1088                TOKENS.store(1, Ordering::Relaxed);
1089                #[cfg(feature = "gpu")]
1090                for a in [
1091                    &crate::gpu_wgpu::MOE_ENC_NS,
1092                    &crate::gpu_wgpu::MOE_WAIT_NS,
1093                    &crate::gpu_wgpu::MOE_BUFS_NS,
1094                    &crate::gpu_wgpu::MOE_UP_NS,
1095                    &crate::gpu_wgpu::MOE_PASS_NS,
1096                    &crate::gpu_wgpu::ATT_ENC_NS,
1097                    &crate::gpu_wgpu::ATT_WAIT_NS,
1098                    &crate::gpu_wgpu::CHAIN_ENC_NS,
1099                    &crate::gpu_wgpu::CHAIN_WAIT_NS,
1100                    &crate::gpu_wgpu::CHAIN_LAYERS,
1101                    &crate::gpu_wgpu::CHAIN_RUNS,
1102                    &crate::gpu_wgpu::SUBMITS,
1103                    &crate::gpu_wgpu::PASSES,
1104                ] {
1105                    a.store(0, Ordering::Relaxed);
1106                }
1107            }
1108        }
1109    }
1110    static REPORT: AtomicBool = AtomicBool::new(false);
1111    /// The one-time "drop the first token's numbers" latch.
1112    static ZEROED: AtomicBool = AtomicBool::new(false);
1113
1114    pub fn on() -> bool {
1115        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1116        *ON.get_or_init(|| std::env::var("CMF_DSV4_PROFILE").is_ok_and(|v| v != "0"))
1117    }
1118
1119    /// Print once, from wherever the last caller happens to be — a process
1120    /// that exits through several paths would otherwise report zero or twice.
1121    pub fn report() {
1122        if !on() || REPORT.swap(true, Ordering::Relaxed) {
1123            return;
1124        }
1125        // CALLS counts layer visits, not tokens — dividing by it and calling
1126        // the result "per token" is off by the layer count, which is 43 on
1127        // the release and reads as a plausible number either way.
1128        let calls = CALLS.load(Ordering::Relaxed).max(1);
1129        let toks = TOKENS.load(Ordering::Relaxed).max(1);
1130        let (a, m) = (
1131            ATTN_NS.load(Ordering::Relaxed) as f64 / 1e6,
1132            MOE_NS.load(Ordering::Relaxed) as f64 / 1e6,
1133        );
1134        let all = ALL_NS.load(Ordering::Relaxed) as f64 / 1e6;
1135        // HC_NS wraps the FFN half's hc_block WHOLE, and moe_step runs
1136        // inside that block — so the raw counter double-counts every MoE
1137        // millisecond as hyper-connection time. Reported as the difference:
1138        // the glue alone. (This inflation is what made moving the
1139        // hyper-connections to the card look like a 19 ms win when the glue
1140        // is ~4.)
1141        let hc = (HC_NS.load(Ordering::Relaxed) as f64 / 1e6
1142            - MOE_NS.load(Ordering::Relaxed) as f64 / 1e6)
1143            .max(0.0);
1144        let hd = HEAD_NS.load(Ordering::Relaxed) as f64 / 1e6;
1145        let prep = PREP_NS.load(Ordering::Relaxed) as f64 / 1e6;
1146        let cw = CACHEW_NS.load(Ordering::Relaxed) as f64 / 1e6;
1147        eprintln!(
1148            "[dsv4-профиль] ХОСТ-ПРЕП слоя: {:.0} мс/токен, KV-заливки: {:.0} мс/токен",
1149            prep / toks as f64,
1150            cw / toks as f64
1151        );
1152        #[cfg(feature = "gpu")]
1153        {
1154            let f = crate::gpu_wgpu::DSV4_FILLS.load(Ordering::Relaxed);
1155            let fb = crate::gpu_wgpu::DSV4_FILL_BYTES.load(Ordering::Relaxed);
1156            eprintln!(
1157                "[dsv4-профиль] ЗАЛИВКИ СЛОТОВ: {:.1} эксп/токен, {:.0} МБ/токен",
1158                f as f64 / toks as f64,
1159                fb as f64 / 1e6 / toks as f64
1160            );
1161        }
1162        eprintln!(
1163            "[dsv4-профиль] {calls} вызовов слоя за {toks} токенов | \
1164             на токен: внимание {:.0} мс, MoE {:.0} мс, гипер-связи+нормы {:.0} мс, \
1165             голова {:.0} мс | на вызов: внимание {:.2}, MoE {:.2}, связи {:.2}",
1166            a / toks as f64,
1167            m / toks as f64,
1168            hc / toks as f64,
1169            hd / toks as f64,
1170            a / calls as f64,
1171            m / calls as f64,
1172            hc / calls as f64,
1173        );
1174        eprintln!(
1175            "[dsv4-профиль] весь проход {:.0} мс на токен; вне счётчиков {:.0} мс",
1176            all / toks as f64,
1177            (all - a - m - hd) / toks as f64,
1178        );
1179        #[cfg(feature = "gpu")]
1180        {
1181            let ae = crate::gpu_wgpu::ATT_ENC_NS.load(Ordering::Relaxed) as f64 / 1e6;
1182            let aw = crate::gpu_wgpu::ATT_WAIT_NS.load(Ordering::Relaxed) as f64 / 1e6;
1183            if ae + aw > 0.0 {
1184                eprintln!(
1185                    "[dsv4-профиль] кадр внимания на вызов: кодирование {:.2} мс, \
1186                     отправка и ожидание {:.2} мс",
1187                    ae / calls as f64,
1188                    aw / calls as f64,
1189                );
1190            }
1191            // At the OUTER level on purpose: this used to sit inside the MoE
1192            // frame's own report, and the chain does not use the MoE frame —
1193            // so the one number that says where a chained token goes was
1194            // printed only when the chain was not running.
1195            let ub = crate::gpu_wgpu::UPLOAD_BYTES.load(Ordering::Relaxed);
1196            let un = crate::gpu_wgpu::UPLOAD_NS.load(Ordering::Relaxed);
1197            if ub > 0 && un > 0 {
1198                eprintln!(
1199                    "[dsv4-профиль] ЗАЛИВКА весов: {:.1} ГБ за {:.1} с ({:.0} МБ/с)",
1200                    ub as f64 / 1e9,
1201                    un as f64 / 1e9,
1202                    ub as f64 / (un as f64 / 1e9) / 1e6,
1203                );
1204            }
1205            let sub = crate::gpu_wgpu::SUBMITS.load(Ordering::Relaxed);
1206            if sub > 0 {
1207                eprintln!(
1208                    "[dsv4-профиль] ОТПРАВОК на карту: {:.1} на токен, ПРОХОДОВ {:.0} \
1209                     ({:.1} на слой)",
1210                    sub as f64 / toks as f64,
1211                    crate::gpu_wgpu::PASSES.load(Ordering::Relaxed) as f64 / toks as f64,
1212                    crate::gpu_wgpu::PASSES.load(Ordering::Relaxed) as f64 / calls as f64,
1213                );
1214            }
1215            let cl = crate::gpu_wgpu::CHAIN_LAYERS.load(Ordering::Relaxed);
1216            if cl > 0 {
1217                let toks2 = toks.max(1) as f64;
1218                eprintln!(
1219                    "[dsv4-профиль] ЦЕПОЧКА на токен: кодирование {:.2} мс, \
1220                     ожидание {:.2} мс ({} слоёв, {} отправок)",
1221                    crate::gpu_wgpu::CHAIN_ENC_NS.load(Ordering::Relaxed) as f64 / 1e6 / toks2,
1222                    crate::gpu_wgpu::CHAIN_WAIT_NS.load(Ordering::Relaxed) as f64 / 1e6 / toks2,
1223                    cl / toks.max(1),
1224                    crate::gpu_wgpu::CHAIN_RUNS.load(Ordering::Relaxed) / toks.max(1),
1225                );
1226            }
1227            let e = crate::gpu_wgpu::MOE_ENC_NS.load(Ordering::Relaxed) as f64 / 1e6;
1228            let wt = crate::gpu_wgpu::MOE_WAIT_NS.load(Ordering::Relaxed) as f64 / 1e6;
1229            if e + wt > 0.0 {
1230                let ns = |a: &std::sync::atomic::AtomicU64| {
1231                    a.load(Ordering::Relaxed) as f64 / 1e6 / calls as f64
1232                };
1233                eprintln!(
1234                    "[dsv4-профиль] кадр MoE на вызов: кодирование {:.2} мс, \
1235                     отправка и ожидание {:.2} мс",
1236                    e / calls as f64,
1237                    wt / calls as f64,
1238                );
1239                let an = crate::gpu_wgpu::ATT_GPU_N.load(Ordering::Relaxed);
1240                if an > 0 {
1241                    let g = |i: usize| {
1242                        crate::gpu_wgpu::ATT_GPU_NS[i].load(Ordering::Relaxed) as f64
1243                            / 1e6
1244                            / an as f64
1245                    };
1246                    eprintln!(
1247                        "[dsv4-профиль]   ВНИМАНИЕ НА КАРТЕ на вызов: одиночное {:.3} мс, \
1248                         оценки {:.3} мс, применение {:.3} мс",
1249                        g(0),
1250                        g(1),
1251                        g(2),
1252                    );
1253                }
1254                let gn = crate::gpu_wgpu::MOE_GPU_N.load(Ordering::Relaxed);
1255                let gns = crate::gpu_wgpu::MOE_GPU_NS[0].load(Ordering::Relaxed);
1256                if gn > 0 && gns > 0 {
1257                    eprintln!(
1258                        "[dsv4-профиль]   MoE НА КАРТЕ: {:.3} мс на вызов ({gn} замеров)",
1259                        gns as f64 / 1e6 / gn as f64,
1260                    );
1261                } else if gn > 0 {
1262                    // Zero across thousands of samples is a broken query, not
1263                    // an instant kernel, and printing it as a time is how a
1264                    // profile starts lying.
1265                    eprintln!(
1266                        "[dsv4-профиль]   MoE НА КАРТЕ: метки вернули НОЛЬ на {gn} замерах — \
1267                         запрос времени не сработал, число не использовать"
1268                    );
1269                }
1270                eprintln!(
1271                    "[dsv4-профиль]   из кодирования: буферы экспертов {:.2} мс, \
1272                     загрузки {:.2} мс, проходы {:.2} мс",
1273                    ns(&crate::gpu_wgpu::MOE_BUFS_NS),
1274                    ns(&crate::gpu_wgpu::MOE_UP_NS),
1275                    ns(&crate::gpu_wgpu::MOE_PASS_NS),
1276                );
1277            }
1278        }
1279    }
1280}
1281
1282/// Print the per-token split, if `CMF_DSV4_PROFILE` asked for one.
1283pub fn profile_report() {
1284    prof::report();
1285}
1286
1287/// `CMF_DSV4_GPU_ATTN=1` moves the attention block onto the device as one
1288/// submission. Off by default: it needs every attention weight in q4tp and a
1289/// working wgpu context, and a frame that declines mid-layer after the state
1290/// has been advanced would be worse than one that never ran.
1291fn gpu_attn_enabled() -> bool {
1292    #[cfg(feature = "gpu")]
1293    {
1294        use std::sync::OnceLock;
1295        static ON: OnceLock<bool> = OnceLock::new();
1296        *ON.get_or_init(|| {
1297            let want = std::env::var("CMF_DSV4_GPU_ATTN")
1298                .map(|v| v != "0")
1299                .unwrap_or(true);
1300            let have = want && crate::gpu::backend_available();
1301            if want && !have && std::env::var("CMF_DSV4_GPU_ATTN").is_ok() {
1302                tracing::warn!(
1303                    "CMF_DSV4_GPU_ATTN задан, но устройства нет — блок внимания                      остаётся на CPU. Проверьте CMF_GPU=wgpu и Vulkan-ICD."
1304                );
1305            }
1306            if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
1307                eprintln!("кадр dsv4: запрошен={want} доступен={have}");
1308            }
1309            have
1310        })
1311    }
1312    #[cfg(not(feature = "gpu"))]
1313    {
1314        false
1315    }
1316}
1317
1318/// The device half of `attention_step`. Returns false — having changed
1319/// nothing — whenever it cannot do the whole block, so the caller's CPU path
1320/// is still correct to run.
1321#[cfg(feature = "gpu")]
1322#[allow(clippy::too_many_arguments)]
1323fn attn_frame(
1324    l: &Dsv4Layer,
1325    cfg: &Dsv4Cfg,
1326    st: &Dsv4State,
1327    li: usize,
1328    hidden: &[f32],
1329    qn: &[f32],
1330    idxs: &[usize],
1331    inv_freq: &[f32],
1332    pos: usize,
1333    win_len: usize,
1334    scale: f32,
1335    // Present: the frame also does this layer's hyper-connection handover
1336    // and leaves the MoE half's input on the card. `out` may then be empty.
1337    hc: Option<&crate::gpu_wgpu::Dsv4HcTail>,
1338    out: &mut [f32],
1339) -> bool {
1340    let hd = cfg.head_dim;
1341    let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b)) = (
1342        l.wq_a.model_idx(),
1343        l.wq_b.model_idx(),
1344        l.wo_a.model_idx(),
1345        l.wo_b.model_idx(),
1346    ) else {
1347        return false;
1348    };
1349    let Some(model) = l.wq_b.model_arc() else {
1350        return false;
1351    };
1352    // Fixed window region, then the compressed tail — so a token writes one
1353    // window slot's worth of movement and whatever the compressor just added,
1354    // not the whole cache. `cap` has to cover the longest run this sequence
1355    // will reach; the compressed axis grows by one entry per `ratio` tokens.
1356    let n_comp = st.compressed[li].len() / hd;
1357    let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
1358    let kv_id = st.kv_id;
1359    // The window is rewritten whole. A ring would write one slot instead of
1360    // 128 — 2 KB against 256 — and was tried: it bought NOTHING (the cost is
1361    // per-dispatch driver bookkeeping, not the copy) and moved perplexity by
1362    // 6e-5 because the attended positions arrive in a different order and the
1363    // softmax accumulates differently. Not a trade worth making.
1364    if !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, 0, &st.window[li], cap) {
1365        return false;
1366    }
1367    // The compressed axis only ever grows, so write the TAIL. Rewriting it
1368    // whole was 22 MB a token at 1024 positions — the cache write, not the
1369    // arithmetic, was what the attention block had left to pay.
1370    // The compressed tail is written WHOLE every token. Writing only the new
1371    // part was tried and gave nothing measurable, and the bookkeeping it
1372    // needs — a per-layer tail count invalidated by every buffer growth — is
1373    // exactly the kind of state that drifts silently and shows up as a model
1374    // that stops early. Not worth carrying for zero.
1375    if n_comp > 0
1376        && !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, cfg.window * hd, &st.compressed[li], cap)
1377    {
1378        return false;
1379    }
1380    let idx32: Vec<u32> = idxs
1381        .iter()
1382        .map(|&p| {
1383            if p < win_len {
1384                p as u32
1385            } else {
1386                (cfg.window + (p - win_len)) as u32
1387            }
1388        })
1389        .collect();
1390    let w = crate::gpu_wgpu::Dsv4AttnW {
1391        wq_a,
1392        wq_b,
1393        wo_a,
1394        wo_b,
1395        q_norm: &l.q_norm,
1396        sink: &l.attn_sink,
1397    };
1398    let g = crate::gpu_wgpu::Dsv4AttnGeom {
1399        dim: cfg.dim,
1400        nh: cfg.n_heads,
1401        hd,
1402        rd: cfg.rope_head_dim,
1403        q_lora: cfg.q_lora_rank,
1404        o_lora: cfg.o_lora_rank,
1405        o_groups: cfg.o_groups,
1406        eps: cfg.norm_eps,
1407        scale,
1408    };
1409    // The host fold, explicitly. The frame used to read this half's input
1410    // from the pooled x2 slot — which a device MoE frame of the SAME layer
1411    // overwrites each token with the NEXT layer's input, so the second
1412    // token of any chain+partial configuration attended over garbage
1413    // (perplexity 5.3 against the 4.578 gold on every budget small enough
1414    // to split a layer). The host has the exact vector either way; one
1415    // hidden-width upload per call is what correctness costs.
1416    crate::gpu_wgpu::dsv4_attn_frame(
1417        &model,
1418        &w,
1419        g,
1420        hidden,
1421        Some(qn),
1422        kv_id,
1423        li,
1424        &idx32,
1425        inv_freq,
1426        pos,
1427        hc,
1428        out,
1429    )
1430}
1431
1432/// What the host still owes the device before a layer frame can run: the
1433/// shared LoRA vector the indexer reads, and the attended position list.
1434#[derive(Default)]
1435pub struct AttnPrep {
1436    pub qr: Vec<f32>,
1437    pub idxs: Vec<usize>,
1438    pub win_len: usize,
1439}
1440
1441#[allow(clippy::too_many_arguments)]
1442pub fn attention_step(
1443    hidden: &[f32],
1444    l: &Dsv4Layer,
1445    cfg: &Dsv4Cfg,
1446    st: &mut Dsv4State,
1447    li: usize,
1448    // Chosen by the caller from the layer's kind — see Dsv4Globals.
1449    inv_freq: &[f32],
1450    pool: Option<&crate::pool::Pool>,
1451    // When set, stop once the caches are advanced and the index list is
1452    // built, and hand those back instead of running attention: the layer
1453    // frame does the rest on the device.
1454    prep_out: Option<&mut AttnPrep>,
1455    out: &mut [f32],
1456) {
1457    let _t0 = prof::on().then(std::time::Instant::now);
1458    let _guard = scopeguard_attn(_t0);
1459    let (hd, rd) = (cfg.head_dim, cfg.rope_head_dim);
1460    let pos = st.pos;
1461    if std::env::var("CMF_FREQ_DEBUG").is_ok() && li == 0 && pos == 0 {
1462        eprintln!(
1463            "    [порт] rd={rd} частот={} inv_freq[0..4]={:?}",
1464            inv_freq.len(),
1465            &inv_freq[..4.min(inv_freq.len())]
1466        );
1467    }
1468
1469    // ── q and kv: both read the same hidden state, so they go out as ONE
1470    // dispatch. The norms after them differ, and they stay separate.
1471    // (q: wq_a → q_norm → wq_b → per-head norm → rope tail;
1472    //  kv: one head's width, shared by every query head.)
1473    let mut qr = vec![0.0f32; cfg.q_lora_rank];
1474    let mut kv = vec![0.0f32; hd];
1475    crate::qtensor::QTensor::matvec_many([&l.wq_a, &l.wkv], hidden, [&mut qr, &mut kv], pool);
1476    rms_weighted(&mut qr, &l.q_norm, cfg.norm_eps);
1477    // The queries are built further down, after the frame has had its chance
1478    // at the whole block. `qr` is needed either way: the indexer reads it.
1479    // A PARTIAL layer walks its attention on the host. Its device MoE
1480    // frame refills the pooled walk slots (x2, the hyper-connection state)
1481    // each token with the NEXT layer's values, so the same layer's device
1482    // attention frame attends over the previous token's leftovers on the
1483    // second token — measured as perplexity 5.3 against the 4.578 gold on
1484    // every budget small enough to split a layer, and exact the moment
1485    // that one layer's attention walks on the host. Layers whose MoE runs
1486    // on the HOST keep their device attention: nothing refills their
1487    // slots mid-walk, and the MAX_LI ladder measures them bit-exact.
1488    // …and it spreads: the partial layer's MoE frame cycles slots that the
1489    // FOLLOWING host-MoE layers' device attention also reads, so in any
1490    // configuration that holds a partial layer, every layer past the chain
1491    // prefix walks its attention on the host. A configuration with no
1492    // partial layer keeps device attention everywhere — the canonical
1493    // stand and the MAX_LI ladder both measure that bit-exact.
1494    let split_config = st.partial_set.iter().any(|&p| p) && st.split_deep;
1495    let past_chain =
1496        st.dev_owned && (li >= st.dev_set.len() || !st.dev_set.get(li).copied().unwrap_or(false));
1497    if std::env::var("CMF_DSV4_GATE_DBG").is_ok() {
1498        eprintln!(
1499            "[gate] li={li} pos={} split={split_config} past={past_chain} dev_owned={} set_len={} part_len={}",
1500            st.pos,
1501            st.dev_owned,
1502            st.dev_set.len(),
1503            st.partial_set.len()
1504        );
1505    }
1506    let on_gpu = gpu_attn_enabled() && !(split_config && past_chain);
1507
1508    rms_weighted(&mut kv, &l.kv_norm, cfg.norm_eps);
1509    rope_tail(&mut kv, inv_freq, pos, rd, false);
1510
1511    // ── the compressor: accumulate `ratio` tokens, then fold them into
1512    // one compressed entry. The reference fires when (pos+1) % ratio == 0,
1513    // so a partial window simply waits — which is why the state carries
1514    // the pending streams across tokens.
1515    if let Some(cp) = &l.compressor {
1516        let mut pk = std::mem::take(&mut st.pending_kv[li]);
1517        let mut ps = std::mem::take(&mut st.pending_score[li]);
1518        let mut qk = std::mem::take(&mut st.prev_kv[li]);
1519        let mut qs = std::mem::take(&mut st.prev_score[li]);
1520        let entry = compressor_step(
1521            cp,
1522            hidden,
1523            pos,
1524            rd,
1525            cfg.norm_eps,
1526            inv_freq,
1527            pool,
1528            &mut pk,
1529            &mut ps,
1530            &mut qk,
1531            &mut qs,
1532        );
1533        st.pending_kv[li] = pk;
1534        st.pending_score[li] = ps;
1535        st.prev_kv[li] = qk;
1536        st.prev_score[li] = qs;
1537        if let Some(e) = entry {
1538            st.compressed[li].extend_from_slice(&e);
1539        }
1540    }
1541    // The indexer scores against ITS OWN compressed cache, built by its own
1542    // compressor. Without this the cache is empty, `n_ix` is zero, and every
1543    // indexer layer picks no compressed positions at all — the long-range
1544    // memory is built and then never read.
1545    if let Some(ix) = &l.indexer {
1546        let mut pk = std::mem::take(&mut st.pending_ix_kv[li]);
1547        let mut ps = std::mem::take(&mut st.pending_ix_score[li]);
1548        let mut qk = std::mem::take(&mut st.prev_ix_kv[li]);
1549        let mut qs = std::mem::take(&mut st.prev_ix_score[li]);
1550        let entry = compressor_step(
1551            &ix.compressor,
1552            hidden,
1553            pos,
1554            rd,
1555            cfg.norm_eps,
1556            inv_freq,
1557            pool,
1558            &mut pk,
1559            &mut ps,
1560            &mut qk,
1561            &mut qs,
1562        );
1563        st.pending_ix_kv[li] = pk;
1564        st.pending_ix_score[li] = ps;
1565        st.prev_ix_kv[li] = qk;
1566        st.prev_ix_score[li] = qs;
1567        if let Some(e) = entry {
1568            st.index_kv[li].extend_from_slice(&e);
1569        }
1570    }
1571
1572    st.window[li].extend_from_slice(&kv);
1573    // The reference keeps the window in a ring of `window_size`; holding the
1574    // last N in order is the same set, and without this the "window" grows
1575    // for the whole generation — wrong attention AND unbounded memory.
1576    let cap = cfg.window * hd;
1577    if st.window[li].len() > cap {
1578        let drop = st.window[li].len() - cap;
1579        st.window[li].drain(..drop);
1580    }
1581    let win_len = st.window[li].len() / hd;
1582    let n_pos = win_len + st.compressed[li].len() / hd;
1583
1584    // Index list: every window position, plus whatever the indexer picked
1585    // (or, without an indexer, every compressed position).
1586    //
1587    // CMF_DSV4_NO_COMPRESSED=1 attends to the sliding window ALONE. That is
1588    // not a mode anyone should serve — it drops the model's long-range
1589    // memory — but it separates two failure modes that look identical from
1590    // the outside: output that degrades because the compressed path is
1591    // wrong, and output that degrades because the weights are too coarse.
1592    let mut idxs: Vec<usize> = (0..win_len).collect();
1593    if !st.compressed[li].is_empty() && !no_compressed() {
1594        let n_comp = st.compressed[li].len() / hd;
1595        match &l.indexer {
1596            Some(ix) => {
1597                // The indexer scores from the SHARED LoRA output through
1598                // its own wq_b — not from attention's queries — and its
1599                // per-head weights are a projection of the hidden state,
1600                // scaled by head_dim^-0.5 * n_heads^-0.5 as the reference
1601                // folds into `weights_proj`'s output.
1602                //
1603                // The reference also applies a randomized Hadamard rotation
1604                // to the queries here and to the keys in the indexer's
1605                // compressor, then simulates FP4 on both. That transform is
1606                // orthogonal (`hadamard_transform` scaled by d^-0.5) and it
1607                // hits BOTH sides of the same dot product, so it cancels:
1608                // its purpose is to condition the FP4 quantization, which we
1609                // do not do either. Omitting the pair is exact, and keeping
1610                // f32 is strictly more accurate than the reference — not an
1611                // approximation to be fixed later.
1612                let ih = ix.weights_proj.rows();
1613                let idim = ix.wq_b.rows() / ih.max(1);
1614                let mut qi = vec![0.0f32; ix.wq_b.rows()];
1615                ix.wq_b.matvec(&qr, &mut qi, pool);
1616                for h in 0..ih {
1617                    rope_tail(&mut qi[h * idim..(h + 1) * idim], inv_freq, pos, rd, false);
1618                }
1619                let mut hw = vec![0.0f32; ih];
1620                ix.weights_proj.matvec(hidden, &mut hw, pool);
1621                let sc_factor = (idim as f32).powf(-0.5) * (ih as f32).powf(-0.5);
1622                for w in hw.iter_mut() {
1623                    *w *= sc_factor;
1624                }
1625                let n_ix = st.index_kv[li].len() / idim.max(1);
1626                let mut sc = Vec::new();
1627                index_scores(
1628                    &qi,
1629                    &st.index_kv[li],
1630                    &hw,
1631                    ih,
1632                    idim,
1633                    n_ix.min(n_comp),
1634                    n_ix.min(n_comp),
1635                    pool,
1636                    &mut sc,
1637                );
1638                let mut picked = Vec::new();
1639                top_k_positions(&sc, cfg.index_topk, &mut picked);
1640                idxs.extend(picked.into_iter().map(|p| win_len + p));
1641            }
1642            None => idxs.extend((0..n_comp).map(|p| win_len + p)),
1643        }
1644    }
1645    debug_assert!(idxs.iter().all(|&p| p < n_pos));
1646    if let Some(p) = prep_out {
1647        p.qr = qr;
1648        p.idxs = idxs;
1649        p.win_len = win_len;
1650        return;
1651    }
1652
1653    // ── the whole block on the device, or nothing ──
1654    let scale = (hd as f32).powf(-0.5);
1655    #[cfg(feature = "gpu")]
1656    if on_gpu
1657        && {
1658            if std::env::var("CMF_DSV4_XCHK").is_ok() {
1659                // The frame reads this half's input from the card's x2
1660                // slot; the host walked its own. Disagreement = the
1661                // chain→walk handoff, and the number says by how much.
1662                if let Some(card) = crate::gpu_wgpu::dsv4_dbg_read_tag(45, 0, hidden.len()) {
1663                    let md = hidden
1664                        .iter()
1665                        .zip(card.iter())
1666                        .map(|(a, b)| (a - b).abs())
1667                        .fold(0.0f32, f32::max);
1668                    eprintln!("[xchk] li={li} pos={pos} x2 maxdiff={md:.3e}");
1669                }
1670            }
1671            true
1672        }
1673        && attn_frame(
1674            l, cfg, st, li, hidden, &qr, &idxs, inv_freq, pos, win_len, scale, None, out,
1675        )
1676    {
1677        return;
1678    }
1679
1680    // ── queries: wq_b, then a norm and the rope tail per head ──
1681    let mut q = vec![0.0f32; cfg.n_heads * hd];
1682    l.wq_b.matvec(&qr, &mut q, pool);
1683    for h in 0..cfg.n_heads {
1684        let head = &mut q[h * hd..(h + 1) * hd];
1685        rms_inplace(head, cfg.norm_eps);
1686        rope_tail(head, inv_freq, pos, rd, false);
1687    }
1688    let mut cache: Vec<f32> = st.window[li].clone();
1689    cache.extend_from_slice(&st.compressed[li]);
1690
1691    // ── sparse attention per head, then the inverse rope ──
1692    let mut attn = vec![0.0f32; cfg.n_heads * hd];
1693    for h in 0..cfg.n_heads {
1694        let qh = &q[h * hd..(h + 1) * hd];
1695        // Straight into this head's slice of the output: the scratch vector
1696        // that used to sit here was an allocation and a copy per head, so 64
1697        // of each per layer per token, for a value that was never read
1698        // anywhere else.
1699        let oh = &mut attn[h * hd..(h + 1) * hd];
1700        sparse_attend(qh, &cache, &idxs, l.attn_sink[h], scale, hd, oh);
1701        rope_tail(oh, inv_freq, pos, rd, true);
1702    }
1703
1704    // ── grouped low-rank output ──
1705    // Read the two blocks through the quantized readers. Materializing them
1706    // here instead costs ~270 MB of dequantization per layer per token on
1707    // the release checkpoint (wo_a and wo_b are 33M weights each), which is
1708    // the difference between decoding and not.
1709    o_project(
1710        &attn,
1711        &|r, x, sc| l.wo_a.row_dot(r, x, sc),
1712        l.wo_a.cols(),
1713        &|mid, dst| l.wo_b.matvec(mid, dst, pool),
1714        cfg.o_groups,
1715        cfg.o_lora_rank,
1716        pool,
1717        out,
1718    );
1719}
1720
1721/// RMSNorm with a learned weight, in place.
1722pub fn rms_weighted(v: &mut [f32], w: &[f32], eps: f32) {
1723    let ms = v.iter().map(|x| x * x).sum::<f32>() / v.len() as f32;
1724    let inv = 1.0 / (ms + eps).sqrt();
1725    for (x, g) in v.iter_mut().zip(w) {
1726        *x = *x * inv * g;
1727    }
1728}
1729
1730// The MoE half of a block: route, run the chosen experts plus the shared one,
1731// and sum. `token_id` is only read on the hash layers. Per-layer expert
1732// routing mass is also recorded here for task-conditional expert sets
1733// (`CMF_MOE_STATS`). An older implementation counted every top-k winner as
1734// one. That is the wrong quantity for DeepSeek-V4: a weak eighth route and
1735// the dominant route then consume the same `cover` budget, so a compact mask
1736// can retain frequent noise while dropping a rarer expert that carries much
1737// more of the block output. Accumulate the normalized route weights as
1738// fixed-point integers instead. The JSON stays the same `{layer: [u64]}`
1739// shape and old count files remain valid inputs because the mask builder only
1740// compares relative mass within a layer.
1741//
1742// Decode drives this from one thread; the pool parallelizes inside the
1743// matvecs, below this point.
1744thread_local! {
1745    static ROUTE_COUNTS: std::cell::RefCell<Vec<Vec<u64>>> =
1746        const { std::cell::RefCell::new(Vec::new()) };
1747}
1748
1749fn route_stats_on() -> bool {
1750    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1751    *ON.get_or_init(|| std::env::var("CMF_MOE_STATS").is_ok())
1752}
1753
1754fn record_route(li: usize, n_layers_hint: usize, n_experts: usize, routed: &[(usize, f32)]) {
1755    ROUTE_COUNTS.with(|c| {
1756        let mut c = c.borrow_mut();
1757        if c.len() <= li.max(n_layers_hint) {
1758            c.resize(li.max(n_layers_hint) + 1, Vec::new());
1759        }
1760        let row = &mut c[li];
1761        if row.len() < n_experts {
1762            row.resize(n_experts, 0);
1763        }
1764        for &(e, weight) in routed {
1765            if e < row.len() {
1766                // One unit keeps a finite selected route visible even if a
1767                // future quantized router rounds an extremely small weight
1768                // below the fixed-point scale.
1769                let mass = (weight.abs() as f64 * 1_000_000.0).round() as u64;
1770                row[e] = row[e].saturating_add(mass.max(1));
1771            }
1772        }
1773    });
1774}
1775
1776/// Take the recorded routing field, leaving the counters empty.
1777pub fn take_route_counts() -> Vec<Vec<u64>> {
1778    ROUTE_COUNTS.with(|c| std::mem::take(&mut *c.borrow_mut()))
1779}
1780
1781/// Charge elapsed time to a counter when it goes out of scope — the two
1782/// steps have several early returns each, and a timer that only stops on the
1783/// long path measures the short one as free.
1784struct Charge(
1785    Option<std::time::Instant>,
1786    &'static std::sync::atomic::AtomicU64,
1787);
1788impl Drop for Charge {
1789    fn drop(&mut self) {
1790        if let Some(t) = self.0 {
1791            self.1.fetch_add(
1792                t.elapsed().as_nanos() as u64,
1793                std::sync::atomic::Ordering::Relaxed,
1794            );
1795        }
1796    }
1797}
1798fn scopeguard_attn(t: Option<std::time::Instant>) -> Charge {
1799    Charge(t, &prof::ATTN_NS)
1800}
1801fn scopeguard_moe(t: Option<std::time::Instant>, li: usize) -> Charge {
1802    if t.is_some() {
1803        prof::note_layer(li);
1804    }
1805    Charge(t, &prof::MOE_NS)
1806}
1807
1808/// The whole token, one submission per layer. Returns false having changed
1809/// nothing if the device declines any layer — the caller's loop is then still
1810/// correct to run.
1811#[cfg(feature = "gpu")]
1812#[allow(clippy::too_many_arguments)]
1813fn dsv4_layer_loop(
1814    state: &mut [f32],
1815    layers: &[Dsv4Layer],
1816    g: &Dsv4Globals,
1817    cfg: &Dsv4Cfg,
1818    st: &mut Dsv4State,
1819    token_id: u32,
1820    inv_freq: &[f32],
1821    pool: Option<&crate::pool::Pool>,
1822    scratch: &mut HcScratch,
1823) -> bool {
1824    let dim = cfg.dim;
1825    let freqs_of = |l: &Dsv4Layer| -> &[f32] {
1826        let f = if l.compressor.is_some() {
1827            &g.inv_freq_compress
1828        } else {
1829            &g.inv_freq_window
1830        };
1831        if f.is_empty() { inv_freq } else { f.as_slice() }
1832    };
1833    // PRE-FLIGHT. The prep inside the loop advances the window and the
1834    // compressor caches, so a refusal halfway leaves state that the CPU
1835    // fallback would advance a SECOND time — which is not a slow answer but a
1836    // wrong one. Everything that can decline is therefore asked before the
1837    // first byte of state moves. The expert upload happens here too, which is
1838    // where it belonged anyway.
1839    // The head goes to the card BEFORE the experts ask for room. It is the
1840    // single most-used tensor in the file — every token reads all of it —
1841    // and it is a rounding error next to the expert stack: 265 MB against
1842    // ninety-odd gigabytes on the release. Uploaded in first-touch order it
1843    // arrived last, after the budget was gone, and stayed on the host for
1844    // the life of the process.
1845    {
1846        static SAID: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
1847        if !SAID.swap(true, std::sync::atomic::Ordering::Relaxed) {
1848            if let (Some(idx), Some(model)) = (g.head.model_idx(), g.head.model_arc()) {
1849                let ok = crate::gpu_wgpu::dsv4_weight_ready(&model, idx);
1850                tracing::info!("dsv4: голова на карте: {}", if ok { "да" } else { "нет" });
1851            }
1852        }
1853    }
1854    let mut on_dev = vec![false; layers.len()];
1855    let mut partial_dev = vec![false; layers.len()];
1856    let mut attn_ready = vec![false; layers.len()];
1857    // Two phases are essential for the unified pool: first pin every small
1858    // attention/compressor skeleton, then give all remaining weight budget
1859    // to the one expert arena. Allocating the arena after layer zero alone
1860    // would honestly fit it, but starve layer one's attention weights.
1861    for (li, l) in layers.iter().enumerate() {
1862        if l.wq_a.model_idx().is_none()
1863            || l.wq_b.model_idx().is_none()
1864            || l.wo_a.model_idx().is_none()
1865            || l.wo_b.model_idx().is_none()
1866        {
1867            return false;
1868        }
1869        let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
1870            return false;
1871        };
1872        // A layer whose experts do not fit is not a reason to abandon the
1873        // token: 100 GB of experts against a 98 GB card means SOME layer will
1874        // always miss. Those run on the host, with the state fetched and put
1875        // back around them — two transfers for the few that need it.
1876        // The attention weights have to be asked for too. Experts fill the
1877        // card first, and a wo_b that misses at layer 11 used to surface as a
1878        // mid-loop refusal — after the caches had advanced, which the CPU
1879        // fallback then advanced again.
1880        // …and, when the layer is to prepare itself, everything that
1881        // preparation reads: the KV projection, both compressors and the
1882        // indexer. Leaving them out is how the chain came to refuse ninety
1883        // times a token on the release — the experts had taken the card by
1884        // the time `dsv4_encode_prep` asked, and it declined silently into a
1885        // fallback that looked like "the chain simply does not help".
1886        let mut want = vec![
1887            l.wq_a.model_idx(),
1888            l.wq_b.model_idx(),
1889            l.wo_a.model_idx(),
1890            l.wo_b.model_idx(),
1891        ];
1892        if chain_enabled() {
1893            want.push(l.wkv.model_idx());
1894            if let Some(cp) = &l.compressor {
1895                want.push(cp.wkv.model_idx());
1896                want.push(cp.wgate.model_idx());
1897            }
1898            if let Some(ix) = &l.indexer {
1899                want.push(ix.wq_b.model_idx());
1900                want.push(ix.weights_proj.model_idx());
1901                want.push(ix.compressor.wkv.model_idx());
1902                want.push(ix.compressor.wgate.model_idx());
1903            }
1904        }
1905        attn_ready[li] = want
1906            .into_iter()
1907            .flatten()
1908            .all(|i| crate::gpu_wgpu::dsv4_weight_ready(&model, i));
1909    }
1910    for (li, l) in layers.iter().enumerate() {
1911        let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
1912            return false;
1913        };
1914        let gu_q2 = l
1915            .experts
1916            .first()
1917            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
1918        // Size expert storage only after EVERY layer's skeleton is resident.
1919        // The old per-layer packs could interleave these allocations; one
1920        // global allocation cannot, so the ordering is now explicit.
1921        let pk = pack_for(l, cfg, li);
1922        if let Some(pk) = pk {
1923            let dn_q2 = l
1924                .experts
1925                .first()
1926                .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
1927            let experts_ok = if pk.global.is_some() {
1928                crate::gpu_wgpu::dsv4_global_moe_ready(&model)
1929            } else {
1930                crate::gpu_wgpu::dsv4_experts_ready(
1931                    &model,
1932                    &pk.tensors,
1933                    cfg.moe_inter,
1934                    dim,
1935                    gu_q2,
1936                    dn_q2,
1937                )
1938            };
1939            on_dev[li] = attn_ready[li] && experts_ok && pk.route_complete();
1940            partial_dev[li] = attn_ready[li] && experts_ok && !pk.route_complete();
1941        }
1942    }
1943    let active_dev: Vec<bool> = on_dev
1944        .iter()
1945        .zip(&partial_dev)
1946        .map(|(&full, &partial)| full || partial)
1947        .collect();
1948    if !active_dev.iter().any(|&x| x) {
1949        return false;
1950    }
1951    // The attention gate below needs to know about partial layers BEFORE
1952    // the decode path commits the device set — a perplexity run only ever
1953    // prefills, and with this left empty every split budget scored the
1954    // model wrong (measured; see `attention_step`).
1955    if st.partial_set.len() != partial_dev.len() || st.partial_set != partial_dev {
1956        st.partial_set = partial_dev.clone();
1957        st.split_deep = active_dev
1958            .iter()
1959            .zip(&partial_dev)
1960            .filter(|(a, p)| !**a || **p)
1961            .count()
1962            > 1;
1963    }
1964
1965    // Which layers the card actually took, said once. A layer that falls to
1966    // the host costs an order of magnitude more than one that does not, and
1967    // "the GPU path is on" hid the difference between all of them and most.
1968    {
1969        static SAID: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
1970        if !SAID.swap(true, std::sync::atomic::Ordering::Relaxed) {
1971            let host: Vec<usize> = active_dev
1972                .iter()
1973                .enumerate()
1974                .filter(|&(_, d)| !*d)
1975                .map(|(i, _)| i)
1976                .collect();
1977            let partial: Vec<(usize, usize)> = partial_dev
1978                .iter()
1979                .enumerate()
1980                .filter(|&(_, d)| *d)
1981                .filter_map(|(li, _)| pack_for(&layers[li], cfg, li).map(|p| (li, p.globals.len())))
1982                .collect();
1983            if host.is_empty() && partial.is_empty() {
1984                tracing::info!("dsv4: все {} слоёв на карте", on_dev.len());
1985            } else {
1986                tracing::info!(
1987                    "dsv4: {} из {} слоёв используют карту; частичные {:?}; на хосте {:?}",
1988                    active_dev.len() - host.len(),
1989                    on_dev.len(),
1990                    partial,
1991                    host,
1992                );
1993            }
1994        }
1995    }
1996
1997    // Layer zero's opening fold has no frame before it to have prepared it.
1998    let (mut folded, post0, comb0) = hc_fold_norm(
1999        state,
2000        &layers[0].hc_attn_fn,
2001        &layers[0].hc_attn_scale,
2002        &layers[0].hc_attn_base,
2003        &layers[0].attn_norm,
2004        cfg,
2005        pool,
2006    );
2007    if !crate::gpu_wgpu::dsv4_state_write(state) || !crate::gpu_wgpu::dsv4_hc_write(&post0, &comb0)
2008    {
2009        return false;
2010    }
2011    // The device-owned set must not move once a token has run on it — but
2012    // the two directions are not the same risk. At a tight budget the set
2013    // GROWS between tokens as more weights finish uploading, and a layer that
2014    // merely joined can be left on the host: its caches are there and nothing
2015    // is inconsistent. Refusing on that was costing the whole fast path once
2016    // per token — 125 times in a 48-token run on an emulated 24 GB card, on
2017    // which the engine is slow enough already.
2018    //
2019    // A layer LEAVING the set is the dangerous direction: its caches are on
2020    // the card and the host would advance its own. That still refuses.
2021    if st.dev_owned && st.dev_set != active_dev {
2022        let left: Vec<usize> = (0..active_dev.len().min(st.dev_set.len()))
2023            .filter(|&i| st.dev_set[i] && !active_dev[i])
2024            .collect();
2025        if !left.is_empty() {
2026            tracing::warn!("слои {left:?} ушли с карты — кеши на разных сторонах");
2027            return false;
2028        }
2029        // A layer that was active remains device-owned. Its full/partial mode
2030        // is still derived from the current pack; only cache ownership is
2031        // sticky across tokens.
2032    }
2033    let chain = chain_enabled();
2034    // CMF_DSV4_LAYERS_PROBE=N — TIMING ONLY, the answer is garbage. Runs the
2035    // first N layers and leaves the rest alone. Decode time against N is a
2036    // line whose SLOPE is the per-layer cost and whose intercept is
2037    // everything that happens once a token. Unlike the skip probe it does
2038    // not change what a layer does — which on a MoE model is the difference
2039    // between a measurement and an artefact, because dropping any stage
2040    // changes the routing and the routing changes what the experts cost.
2041    let layer_cap = {
2042        static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
2043        *N.get_or_init(|| {
2044            std::env::var("CMF_DSV4_LAYERS_PROBE")
2045                .ok()
2046                .and_then(|v| v.parse::<usize>().ok())
2047                .unwrap_or(usize::MAX)
2048        })
2049    };
2050    let mut run: Vec<usize> = Vec::new();
2051    let mut sink_out = vec![0.0f32; dim];
2052    // `state` starts current on both sides. A device run makes the host copy
2053    // stale unless that same run carries it home. Tracking this explicitly
2054    // avoids a separate state fence before a host layer and, for a final host
2055    // layer, the old upload-immediately-followed-by-readback pair.
2056    let mut state_on_host = true;
2057    for (li, l) in layers.iter().enumerate() {
2058        if li >= layer_cap {
2059            break;
2060        }
2061        // The device path never ticked the profiler, so every per-token
2062        // number it printed described the two host-path tokens at the start
2063        // of a run — the ones that also pay for the upload. Ticking here is
2064        // what makes the chain's encode-and-wait split a per-token figure at
2065        // all.
2066        if prof::on() {
2067            prof::note_layer(li);
2068        }
2069        if chain && on_dev[li] {
2070            // Hash layers used to break the run in two: their forced expert
2071            // list changes per token, went through the (tag, len) upload
2072            // pool, and every layer of a submission shared one buffer. The
2073            // list has a per-layer slot now, so they chain like the rest.
2074            run.push(li);
2075            // CMF_DSV4_CHAIN_MAX=N caps a run's length. Diagnostic, not a
2076            // tuning knob: length-1 runs put ONE layer per submission, which
2077            // separates "the layer frame is wrong" from "layers in one
2078            // encoder contaminate each other" in a single ppl run.
2079            if run.len() >= chain_max() || dspark_wants(li) {
2080                let need_qn = run[0] == 0 || !on_dev[run[0] - 1];
2081                let captured = *run.last().unwrap();
2082                if !dsv4_chain_run(
2083                    layers,
2084                    &run,
2085                    cfg,
2086                    g,
2087                    st,
2088                    token_id,
2089                    &mut folded,
2090                    Some(state),
2091                    1,
2092                    &[],
2093                    need_qn,
2094                    pool,
2095                ) {
2096                    return false;
2097                }
2098                state_on_host = true;
2099                verify_fp("walk", st.pos, captured, state);
2100                dspark_note(captured, state, cfg);
2101                run.clear();
2102            }
2103            continue;
2104        }
2105        if chain && !run.is_empty() {
2106            // The very next layer is on the host, so bring its state back in
2107            // the chain's existing readback. Reading it in a second submit
2108            // below cost one fence per token on the release's 42+1 split.
2109            if !dsv4_chain_run(
2110                layers,
2111                &run,
2112                cfg,
2113                g,
2114                st,
2115                token_id,
2116                &mut folded,
2117                Some(state),
2118                1,
2119                &[],
2120                run[0] == 0 || !on_dev[run[0] - 1],
2121                pool,
2122            ) {
2123                return false;
2124            }
2125            state_on_host = true;
2126            let last = *run.last().unwrap();
2127            verify_fp("walk", st.pos, last, state);
2128            dspark_note(last, state, cfg);
2129        }
2130        run.clear();
2131        if partial_dev[li] && chain1_on() {
2132            if let Some(home) = dsv4_chain1_layer(
2133                state,
2134                &mut folded,
2135                layers,
2136                l,
2137                cfg,
2138                st,
2139                token_id,
2140                li,
2141                freqs_of(l),
2142                pool,
2143                state_on_host,
2144            ) {
2145                // chain1 advances the canonical host mirrors and rewrites
2146                // the device window/compressed cache from them, but it does
2147                // not go through dsv4_chain_run (the usual owner of these
2148                // arithmetic counters).  A speculative token-axis pass that
2149                // takes over on the next token must start from the same
2150                // extents, otherwise its very first partial layer attends to
2151                // an empty/stale prefix.
2152                st.dev_filled[li] = (st.window[li].len() / cfg.head_dim).min(cfg.window);
2153                st.dev_n_comp[li] = st.compressed[li].len() / cfg.head_dim;
2154                st.dev_n_ix[li] = l.indexer.as_ref().map_or(0, |ix| {
2155                    let ih = ix.weights_proj.rows();
2156                    let idim = ix.wq_b.rows() / ih.max(1);
2157                    st.index_kv[li].len() / idim.max(1)
2158                });
2159                state_on_host = home;
2160                if verify_fp_on(st.pos) {
2161                    if !state_on_host && !crate::gpu_wgpu::dsv4_state_read(state) {
2162                        return false;
2163                    }
2164                    state_on_host = true;
2165                    verify_fp("walk", st.pos, li, state);
2166                }
2167                // The draft captures THIS layer's state — which lives on
2168                // the card when no cold came home. Noting the stale host
2169                // array fed the draft garbage: 1265 drafted, 0 accepted.
2170                if dspark_wants(li) {
2171                    if !state_on_host && crate::gpu_wgpu::dsv4_state_read(state) {
2172                        state_on_host = true;
2173                    }
2174                    if state_on_host {
2175                        dspark_note(li, state, cfg);
2176                    }
2177                }
2178                continue;
2179            }
2180        }
2181        if partial_dev[li] && partial_walk_on() {
2182            // Attention and the resident expert subset stay on the card. The
2183            // router still sees every expert and returns only the winners
2184            // that did not fit; those are completed on the CPU and their
2185            // exact linear contribution is added back to device state.
2186            let Some(home) = dsv4_partial_layer(
2187                state,
2188                &mut folded,
2189                layers,
2190                l,
2191                cfg,
2192                st,
2193                token_id,
2194                li,
2195                freqs_of(l),
2196                pool,
2197                state_on_host,
2198            ) else {
2199                return false;
2200            };
2201            state_on_host = home;
2202            if home {
2203                dspark_note(li, state, cfg);
2204            }
2205            continue;
2206        }
2207        if !on_dev[li] {
2208            if !state_on_host && !crate::gpu_wgpu::dsv4_state_read(state) {
2209                return false;
2210            }
2211            state_on_host = true;
2212            let freqs = freqs_of(l);
2213            hc_block(
2214                state,
2215                &l.hc_attn_fn,
2216                &l.hc_attn_scale,
2217                &l.hc_attn_base,
2218                &l.attn_norm,
2219                cfg,
2220                scratch,
2221                pool,
2222                |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
2223            );
2224            hc_block(
2225                state,
2226                &l.hc_ffn_fn,
2227                &l.hc_ffn_scale,
2228                &l.hc_ffn_base,
2229                &l.ffn_norm,
2230                cfg,
2231                scratch,
2232                pool,
2233                // The layer the card had no room for. Its experts are
2234                // reached one matvec at a time and the probe sends each to
2235                // the device — right per op, and a fence per op: this one
2236                // layer is why a token that submits ONCE for 42 layers
2237                // submits 13 times. CMF_DSV4_HOST_CPU_MOE=1 keeps them on
2238                // the host instead, trading arithmetic for round trips.
2239                |f, o| {
2240                    if host_cpu_moe() {
2241                        crate::gpu::cpu_scope(|| moe_step(f, l, cfg, token_id, li, pool, o))
2242                    } else {
2243                        moe_step(f, l, cfg, token_id, li, pool, o)
2244                    }
2245                },
2246            );
2247            // Only a following DEVICE layer needs the fold/hc slots and an
2248            // uploaded state. Consecutive host layers consume `state`
2249            // directly, and a final host layer is already exactly where the
2250            // head needs it — uploading then reading it back was pure sync.
2251            if layers.get(li + 1).is_some() && on_dev.get(li + 1).copied().unwrap_or(false) {
2252                let n = &layers[li + 1];
2253                let (f, p2, c2) = hc_fold_norm(
2254                    state,
2255                    &n.hc_attn_fn,
2256                    &n.hc_attn_scale,
2257                    &n.hc_attn_base,
2258                    &n.attn_norm,
2259                    cfg,
2260                    pool,
2261                );
2262                folded = f;
2263                if !crate::gpu_wgpu::dsv4_hc_write(&p2, &c2) {
2264                    return false;
2265                }
2266                if !crate::gpu_wgpu::dsv4_state_write(state) {
2267                    return false;
2268                }
2269            }
2270            dspark_note(li, state, cfg);
2271            continue;
2272        }
2273        let mut prep = AttnPrep::default();
2274        let _tp = prof::on().then(std::time::Instant::now);
2275        attention_step(
2276            &folded,
2277            l,
2278            cfg,
2279            st,
2280            li,
2281            freqs_of(l),
2282            pool,
2283            Some(&mut prep),
2284            &mut sink_out,
2285        );
2286        if let Some(t) = _tp {
2287            prof::PREP_NS.fetch_add(
2288                t.elapsed().as_nanos() as u64,
2289                std::sync::atomic::Ordering::Relaxed,
2290            );
2291        }
2292        // The caches the frame will read.
2293        let hd = cfg.head_dim;
2294        let n_comp = st.compressed[li].len() / hd;
2295        let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
2296        let kv_id = st.kv_id;
2297        let _tc = prof::on().then(std::time::Instant::now);
2298        if !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, 0, &st.window[li], cap)
2299            || (n_comp > 0
2300                && !crate::gpu_wgpu::dsv4_cache_write(
2301                    kv_id,
2302                    li,
2303                    cfg.window * hd,
2304                    &st.compressed[li],
2305                    cap,
2306                ))
2307        {
2308            return false;
2309        }
2310        if let Some(t) = _tc {
2311            prof::CACHEW_NS.fetch_add(
2312                t.elapsed().as_nanos() as u64,
2313                std::sync::atomic::Ordering::Relaxed,
2314            );
2315        }
2316        let idx32: Vec<u32> = prep
2317            .idxs
2318            .iter()
2319            .map(|&p| {
2320                if p < prep.win_len {
2321                    p as u32
2322                } else {
2323                    (cfg.window + (p - prep.win_len)) as u32
2324                }
2325            })
2326            .collect();
2327        let Some(pk) = pack_for(l, cfg, li) else {
2328            return false;
2329        };
2330        let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b)) = (
2331            l.wq_a.model_idx(),
2332            l.wq_b.model_idx(),
2333            l.wo_a.model_idx(),
2334            l.wo_b.model_idx(),
2335        ) else {
2336            return false;
2337        };
2338        let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
2339            return false;
2340        };
2341        let forced: Option<Vec<usize>> = l.tid2eid.as_ref().and_then(|tbl| {
2342            let v: Vec<usize> = if pk.needs_remap() {
2343                hash_route(tbl, cfg.vocab, cfg.top_k, token_id)
2344            } else {
2345                hash_route(tbl, cfg.vocab, cfg.top_k, token_id)
2346                    .into_iter()
2347                    .map(|gi| pk.to_slot[gi])
2348                    .collect()
2349            };
2350            if v.contains(&usize::MAX) {
2351                None
2352            } else {
2353                Some(v)
2354            }
2355        });
2356        if l.tid2eid.is_some() && forced.is_none() {
2357            return false;
2358        }
2359        let nxt = layers.get(li + 1);
2360        let w = crate::gpu_wgpu::Dsv4LayerW {
2361            attn: crate::gpu_wgpu::Dsv4AttnW {
2362                wq_a,
2363                wq_b,
2364                wo_a,
2365                wo_b,
2366                q_norm: &l.q_norm,
2367                sink: &l.attn_sink,
2368            },
2369            moe: crate::gpu_wgpu::Dsv4MoeW {
2370                router: &[],
2371                experts: &pk.tensors,
2372                logits: &[],
2373                // The PACK's bias, whose address outlives the process: the
2374                // frame's const cache is keyed on it, and a per-layer Vec
2375                // here handed every layer the first layer's — the exact
2376                // transient-Vec trap the const_buf war story describes,
2377                // reintroduced by this session and caught because the OFF
2378                // baseline moved.
2379                bias: pk.bias.as_deref(),
2380                mask: pk.mask.as_deref(),
2381                forced: forced.as_deref(),
2382                remap: pk.needs_remap().then_some(pk.remap.as_slice()),
2383                global: None,
2384                has_shared: true,
2385                shared_weight: 1.0,
2386                preweighted: false,
2387                qwen_softmax: false,
2388            },
2389            hc_ffn_fn: &l.hc_ffn_fn,
2390            hc_ffn_scale: &l.hc_ffn_scale,
2391            hc_ffn_base: &l.hc_ffn_base,
2392            hc_next_fn: nxt.map(|n| n.hc_attn_fn.as_slice()),
2393            hc_next_scale: nxt.map_or(&l.hc_attn_scale, |n| &n.hc_attn_scale),
2394            hc_next_base: nxt.map_or(&l.hc_attn_base, |n| n.hc_attn_base.as_slice()),
2395            ffn_norm: &l.ffn_norm,
2396            next_norm: nxt.map_or(&l.attn_norm, |n| n.attn_norm.as_slice()),
2397            next_q_norm: nxt.map_or(&l.q_norm, |n| n.q_norm.as_slice()),
2398            next_wq_a: nxt.and_then(|n| n.wq_a.model_idx()),
2399            router: &pk.router,
2400        };
2401        let geom = crate::gpu_wgpu::Dsv4LayerGeom {
2402            attn: crate::gpu_wgpu::Dsv4AttnGeom {
2403                dim,
2404                nh: cfg.n_heads,
2405                hd,
2406                rd: cfg.rope_head_dim,
2407                q_lora: cfg.q_lora_rank,
2408                o_lora: cfg.o_lora_rank,
2409                o_groups: cfg.o_groups,
2410                eps: cfg.norm_eps,
2411                scale: (hd as f32).powf(-0.5),
2412            },
2413            moe: crate::gpu_wgpu::Dsv4MoeGeom {
2414                hidden: dim,
2415                inter: cfg.moe_inter,
2416                top_k: cfg.top_k,
2417                route_scale: cfg.route_scale,
2418                swiglu_limit: cfg.swiglu_limit,
2419                gu_q2: l.experts.first().is_some_and(|e| {
2420                    e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
2421                }),
2422            },
2423            hc: cfg.hc_mult,
2424            hc_eps: cfg.hc_eps,
2425            sinkhorn_iters: cfg.hc_sinkhorn_iters,
2426        };
2427        let mut next = vec![0.0f32; dim];
2428        if !crate::gpu_wgpu::dsv4_layer_frame(
2429            &model,
2430            &w,
2431            geom,
2432            kv_id,
2433            li,
2434            Some(&prep.qr),
2435            &idx32,
2436            freqs_of(l),
2437            st.pos,
2438            &mut next,
2439            None,
2440            None,
2441            &mut Vec::new(),
2442        ) {
2443            return false;
2444        }
2445        state_on_host = false;
2446        folded = next;
2447        dspark_note(li, state, cfg);
2448    }
2449    let mut state_home = false;
2450    if chain {
2451        if !run.is_empty() {
2452            // The token's LAST run brings the state back with it. Only the
2453            // last: an earlier run's state is one the layers after it still
2454            // change.
2455            let need_qn = run[0] == 0 || !on_dev[run[0] - 1];
2456            let last_on_dev = *on_dev.last().unwrap_or(&false);
2457            let carry = last_on_dev && run.last() == Some(&(layers.len() - 1));
2458            let ok = if carry {
2459                let r = dsv4_chain_run(
2460                    layers,
2461                    &run,
2462                    cfg,
2463                    g,
2464                    st,
2465                    token_id,
2466                    &mut folded,
2467                    Some(state),
2468                    1,
2469                    &[],
2470                    need_qn,
2471                    pool,
2472                );
2473                state_home = r;
2474                state_on_host = r;
2475                if r {
2476                    dspark_note(*run.last().unwrap(), state, cfg);
2477                }
2478                r
2479            } else {
2480                let r = dsv4_chain_run(
2481                    layers,
2482                    &run,
2483                    cfg,
2484                    g,
2485                    st,
2486                    token_id,
2487                    &mut folded,
2488                    None,
2489                    1,
2490                    &[],
2491                    need_qn,
2492                    pool,
2493                );
2494                if r {
2495                    state_on_host = false;
2496                }
2497                r
2498            };
2499            if !ok {
2500                return false;
2501            }
2502        }
2503        if st.dev_set.is_empty() {
2504            st.dev_set = active_dev.clone();
2505            st.partial_set = partial_dev.clone();
2506            // The set is committed, so the card must keep it. Eviction by
2507            // score is right while the set is still being chosen and wrong
2508            // afterwards: an evicted layer drops off the card while its
2509            // caches stay there, and the loop then refuses the whole fast
2510            // path rather than read state from two sides.
2511            let mut idxs = Vec::new();
2512            for (li, l) in layers.iter().enumerate() {
2513                if !active_dev.get(li).copied().unwrap_or(false) {
2514                    continue;
2515                }
2516                for t in [&l.wq_a, &l.wq_b, &l.wkv, &l.wo_a, &l.wo_b, &l.gate] {
2517                    idxs.extend(t.model_idx());
2518                }
2519                if let Some(pk) = pack_for(l, cfg, li) {
2520                    for &(a, b, c) in &pk.tensors {
2521                        idxs.extend([a, b, c]);
2522                    }
2523                }
2524            }
2525            // Why a HOST layer stayed on the host, said in numbers. Its MoE
2526            // can still run on the card with a partial pack — `moe_frame` has
2527            // the remap and hands cold picks back — so the interesting figure
2528            // is how many experts it got. Zero means the upload order never
2529            // reached it; a few hundred means the readiness gate refused. The
2530            // two have different fixes and reading the code cannot tell them
2531            // apart.
2532            for (li, l) in layers.iter().enumerate() {
2533                if active_dev.get(li).copied().unwrap_or(false) {
2534                    continue;
2535                }
2536                let packed = pack_for(l, cfg, li).map_or(0, |p| p.globals.len());
2537                tracing::info!(
2538                    "слой {li} на хосте: упаковано {packed} экспертов из {}",
2539                    cfg.n_routed_experts
2540                );
2541            }
2542            let pinned = layers
2543                .iter()
2544                .find_map(|l| l.experts.first().and_then(|e| e.w1.model_arc()))
2545                .map_or(0, |m| crate::gpu_wgpu::pin_weights(&m, &idxs));
2546            tracing::info!(
2547                "закреплено на карте: {pinned} тензоров {} слоёв",
2548                on_dev.iter().filter(|&&x| x).count()
2549            );
2550        }
2551    }
2552    if state_home || state_on_host {
2553        return true;
2554    }
2555    crate::gpu_wgpu::dsv4_state_read(state)
2556}
2557
2558/// Run a layer whose attention skeleton fits but only a subset of its MoE
2559/// experts does. This path is selected from the live VRAM budget, never from
2560/// a layer number. It is exact: routing spans all experts and cold winners
2561/// are folded back into the hyper-connection state before the next layer.
2562#[cfg(feature = "gpu")]
2563#[allow(clippy::too_many_arguments)]
2564fn dsv4_partial_layer(
2565    state: &mut [f32],
2566    folded: &mut Vec<f32>,
2567    layers: &[Dsv4Layer],
2568    l: &Dsv4Layer,
2569    cfg: &Dsv4Cfg,
2570    st: &mut Dsv4State,
2571    token_id: u32,
2572    li: usize,
2573    freqs: &[f32],
2574    pool: Option<&crate::pool::Pool>,
2575    state_on_host: bool,
2576) -> Option<bool> {
2577    let dim = cfg.dim;
2578    // The self-poisoning this walk was parked for: its frames read the
2579    // pooled post/comb/state slots, and whatever layer ran a frame LAST —
2580    // on this token or the previous one — left its own there. The walk
2581    // now seeds its OWN slots from the state it holds at entry, and is
2582    // immune to the neighbours. The state is home whenever the previous
2583    // layer exited through this walk or the host branch; a device exit
2584    // (full-layer frame) leaves it on the card, where the slots are
2585    // already this token's — nothing to reseed then.
2586    if state_on_host {
2587        let (f, post, comb) = hc_fold_norm(
2588            state,
2589            &l.hc_attn_fn,
2590            &l.hc_attn_scale,
2591            &l.hc_attn_base,
2592            &l.attn_norm,
2593            cfg,
2594            pool,
2595        );
2596        *folded = f;
2597        if !crate::gpu_wgpu::dsv4_hc_write(&post, &comb)
2598            || !crate::gpu_wgpu::dsv4_state_write(state)
2599        {
2600            return None;
2601        }
2602    }
2603    let mut prep = AttnPrep::default();
2604    let mut sink = vec![0.0f32; dim];
2605    attention_step(
2606        folded,
2607        l,
2608        cfg,
2609        st,
2610        li,
2611        freqs,
2612        pool,
2613        Some(&mut prep),
2614        &mut sink,
2615    );
2616    let hd = cfg.head_dim;
2617    let n_comp = st.compressed[li].len() / hd;
2618    let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
2619    if !crate::gpu_wgpu::dsv4_cache_write(st.kv_id, li, 0, &st.window[li], cap)
2620        || (n_comp > 0
2621            && !crate::gpu_wgpu::dsv4_cache_write(
2622                st.kv_id,
2623                li,
2624                cfg.window * hd,
2625                &st.compressed[li],
2626                cap,
2627            ))
2628    {
2629        return None;
2630    }
2631    let a_tail = crate::gpu_wgpu::Dsv4HcTail {
2632        fn_: &l.hc_ffn_fn,
2633        scale: &l.hc_ffn_scale,
2634        base: &l.hc_ffn_base,
2635        norm: &l.ffn_norm,
2636        hc: cfg.hc_mult,
2637        sinkhorn_iters: cfg.hc_sinkhorn_iters,
2638        hc_eps: cfg.hc_eps,
2639        eps: cfg.norm_eps,
2640    };
2641    let scale = (cfg.head_dim as f32).powf(-0.5);
2642    // Optional FreeToken-style split point. Reading the normed FFN input
2643    // here adds one fence between attention and MoE, but it also lets the
2644    // host predict and execute cold experts while the resident experts are
2645    // running on the GPU. Keep the old no-readback path as the default: on
2646    // a warm/full pack the extra fence has nothing to hide and only hurts.
2647    let mut ffn_input = if cpu_overlap_on() {
2648        vec![0.0f32; dim]
2649    } else {
2650        Vec::new()
2651    };
2652    if !attn_frame(
2653        l,
2654        cfg,
2655        st,
2656        li,
2657        folded,
2658        &prep.qr,
2659        &prep.idxs,
2660        freqs,
2661        st.pos,
2662        prep.win_len,
2663        scale,
2664        Some(&a_tail),
2665        &mut ffn_input,
2666    ) {
2667        return None;
2668    }
2669    let nxt = layers.get(li + 1);
2670    let forced = l
2671        .tid2eid
2672        .as_ref()
2673        .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
2674    let mut next = vec![0.0f32; dim];
2675    let (cold_sum, cold_count) = moe_frame(
2676        &ffn_input,
2677        l,
2678        cfg,
2679        li,
2680        &[],
2681        forced.as_deref(),
2682        pool,
2683        Some(&a_tail),
2684        // Do not pre-fold the next layer yet. That fold reuses the canonical
2685        // `post` slot; a cold correction still needs THIS layer's post. Once
2686        // the corrected state is home, the exact next fold is cheap on the
2687        // host and seeds either another partial frame or the next full run.
2688        None,
2689        &mut next,
2690    )?;
2691    // The resident contribution has already been expanded on the device. If
2692    // there were cold winners, add `post[j] * cold_sum` and retrieve the
2693    // corrected state in that submission; otherwise a plain readback is
2694    // enough. This state handoff is what makes partial layers composable at
2695    // arbitrary positions, not just at the tail of one checkpoint.
2696    let state_ok = if cold_count == 0 {
2697        crate::gpu_wgpu::dsv4_state_read(state)
2698    } else {
2699        crate::gpu_wgpu::dsv4_state_add_cold(&cold_sum, cfg.hc_mult, state)
2700    };
2701    if !state_ok {
2702        return None;
2703    }
2704    if let Some(n) = nxt {
2705        let (f, post, comb) = hc_fold_norm(
2706            state,
2707            &n.hc_attn_fn,
2708            &n.hc_attn_scale,
2709            &n.hc_attn_base,
2710            &n.attn_norm,
2711            cfg,
2712            pool,
2713        );
2714        *folded = f;
2715        if !crate::gpu_wgpu::dsv4_hc_write(&post, &comb)
2716            || !crate::gpu_wgpu::dsv4_state_write(state)
2717        {
2718            return None;
2719        }
2720    }
2721    // NB: the CALLER notes this layer for the draft's ring — a note here
2722    // as well double-counts the capture and fails `dspark_take`'s
2723    // completeness check (seen 4 of 3, measured), which reads exactly like
2724    // the starvation it was meant to fix.
2725    Some(true)
2726}
2727
2728/// `CMF_DSV4_CHAIN1=1`: a partial layer runs as ONE submission — attention,
2729/// folds and the subset MoE in a single frame, the state staying on the
2730/// card when every winner was resident (the common case once the slots
2731/// warm). Cold winners pay the walk's exact correction from the preserved
2732/// post. Enabled by default after parity and long-run measurements on RTX
2733/// 5090, RTX PRO 6000 and A40; `CMF_DSV4_CHAIN1=0` keeps the old bisect.
2734#[cfg(feature = "gpu")]
2735fn chain1_on() -> bool {
2736    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2737    *ON.get_or_init(|| {
2738        std::env::var("CMF_DSV4_CHAIN1")
2739            .map(|v| v != "0")
2740            .unwrap_or(true)
2741    })
2742}
2743
2744/// `CMF_DSV4_CPU_OVERLAP=1`: on the two-frame partial walk, read the exact
2745/// normalized MoE input after attention and use it to overlap cold CPU
2746/// experts with the resident GPU frame. This is deliberately independent
2747/// from `CMF_DSV4_PARTIAL_WALK`: it is a measured alternative to chain-of-one,
2748/// not a new default for full packs.
2749#[cfg(feature = "gpu")]
2750fn cpu_overlap_on() -> bool {
2751    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2752    *ON.get_or_init(|| std::env::var("CMF_DSV4_CPU_OVERLAP").is_ok_and(|v| v != "0"))
2753}
2754
2755#[cfg(feature = "gpu")]
2756#[allow(clippy::too_many_arguments)]
2757fn dsv4_chain1_layer(
2758    state: &mut [f32],
2759    folded: &mut Vec<f32>,
2760    layers: &[Dsv4Layer],
2761    l: &Dsv4Layer,
2762    cfg: &Dsv4Cfg,
2763    st: &mut Dsv4State,
2764    token_id: u32,
2765    li: usize,
2766    freqs: &[f32],
2767    pool: Option<&crate::pool::Pool>,
2768    state_on_host: bool,
2769) -> Option<bool> {
2770    let dim = cfg.dim;
2771    let pk = pack_for(l, cfg, li)?;
2772    if pk.route_complete() {
2773        return None;
2774    }
2775    let model = l.experts.first().and_then(|e| e.w1.model_arc())?;
2776    // The same entry self-seed the repaired walk uses: the frame reads the
2777    // pooled post/comb/state slots, and this layer's own are the only ones
2778    // it may trust.
2779    if state_on_host {
2780        let (f, post, comb) = hc_fold_norm(
2781            state,
2782            &l.hc_attn_fn,
2783            &l.hc_attn_scale,
2784            &l.hc_attn_base,
2785            &l.attn_norm,
2786            cfg,
2787            pool,
2788        );
2789        *folded = f;
2790        if !crate::gpu_wgpu::dsv4_hc_write(&post, &comb)
2791            || !crate::gpu_wgpu::dsv4_state_write(state)
2792        {
2793            return None;
2794        }
2795    }
2796    let mut prep = AttnPrep::default();
2797    let mut sink = vec![0.0f32; dim];
2798    attention_step(
2799        folded,
2800        l,
2801        cfg,
2802        st,
2803        li,
2804        freqs,
2805        pool,
2806        Some(&mut prep),
2807        &mut sink,
2808    );
2809    let hd = cfg.head_dim;
2810    let n_comp = st.compressed[li].len() / hd;
2811    let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
2812    let kv_id = st.kv_id;
2813    if !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, 0, &st.window[li], cap)
2814        || (n_comp > 0
2815            && !crate::gpu_wgpu::dsv4_cache_write(
2816                kv_id,
2817                li,
2818                cfg.window * hd,
2819                &st.compressed[li],
2820                cap,
2821            ))
2822    {
2823        return None;
2824    }
2825    let idx32: Vec<u32> = prep
2826        .idxs
2827        .iter()
2828        .map(|&p| {
2829            if p < prep.win_len {
2830                p as u32
2831            } else {
2832                (cfg.window + (p - prep.win_len)) as u32
2833            }
2834        })
2835        .collect();
2836    let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b)) = (
2837        l.wq_a.model_idx(),
2838        l.wq_b.model_idx(),
2839        l.wo_a.model_idx(),
2840        l.wo_b.model_idx(),
2841    ) else {
2842        return None;
2843    };
2844    // Under the subset contract the forced list stays GLOBAL: the remap
2845    // either finds each hash winner a slot or returns it cold.
2846    let forced: Option<Vec<usize>> = l
2847        .tid2eid
2848        .as_ref()
2849        .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
2850    let global_remap = pk
2851        .global
2852        .as_ref()
2853        .map(|gl| gl.pool.remap(gl.layer, cfg.n_routed_experts));
2854    let dynv = pk.dynslots.lock().unwrap();
2855    let live_remap = global_remap.as_deref().unwrap_or(dynv.remap.as_slice());
2856    let nxt = layers.get(li + 1);
2857    let w = crate::gpu_wgpu::Dsv4LayerW {
2858        attn: crate::gpu_wgpu::Dsv4AttnW {
2859            wq_a,
2860            wq_b,
2861            wo_a,
2862            wo_b,
2863            q_norm: &l.q_norm,
2864            sink: &l.attn_sink,
2865        },
2866        moe: crate::gpu_wgpu::Dsv4MoeW {
2867            router: &[],
2868            experts: &pk.tensors,
2869            logits: &[],
2870            // GLOBAL bias under the subset contract — the ranking spans
2871            // every expert, so a packed-order bias would misalign it.
2872            bias: pk.bias.as_deref(),
2873            mask: pk.mask.as_deref(),
2874            forced: forced.as_deref(),
2875            remap: Some(live_remap),
2876            global: pk.global.as_ref().map(|gl| crate::gpu_wgpu::Dsv4GlobalMoe {
2877                pool_uid: gl.pool.uid,
2878                shared_slot: gl.shared_slot,
2879                segment_slots: gl.pool.segment_slots as u32,
2880            }),
2881            has_shared: true,
2882            shared_weight: 1.0,
2883            preweighted: false,
2884            qwen_softmax: false,
2885        },
2886        hc_ffn_fn: &l.hc_ffn_fn,
2887        hc_ffn_scale: &l.hc_ffn_scale,
2888        hc_ffn_base: &l.hc_ffn_base,
2889        hc_next_fn: nxt.map(|n| n.hc_attn_fn.as_slice()),
2890        hc_next_scale: nxt.map_or(&l.hc_attn_scale, |n| &n.hc_attn_scale),
2891        hc_next_base: nxt.map_or(&l.hc_attn_base, |n| n.hc_attn_base.as_slice()),
2892        ffn_norm: &l.ffn_norm,
2893        next_norm: nxt.map_or(&l.attn_norm, |n| n.attn_norm.as_slice()),
2894        next_q_norm: nxt.map_or(&l.q_norm, |n| n.q_norm.as_slice()),
2895        next_wq_a: nxt.and_then(|n| n.wq_a.model_idx()),
2896        router: &pk.router,
2897    };
2898    let geom = crate::gpu_wgpu::Dsv4LayerGeom {
2899        attn: crate::gpu_wgpu::Dsv4AttnGeom {
2900            dim,
2901            nh: cfg.n_heads,
2902            hd,
2903            rd: cfg.rope_head_dim,
2904            q_lora: cfg.q_lora_rank,
2905            o_lora: cfg.o_lora_rank,
2906            o_groups: cfg.o_groups,
2907            eps: cfg.norm_eps,
2908            scale: (hd as f32).powf(-0.5),
2909        },
2910        moe: crate::gpu_wgpu::Dsv4MoeGeom {
2911            hidden: dim,
2912            inter: cfg.moe_inter,
2913            top_k: cfg.top_k,
2914            route_scale: cfg.route_scale,
2915            swiglu_limit: cfg.swiglu_limit,
2916            gu_q2: l
2917                .experts
2918                .first()
2919                .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
2920        },
2921        hc: cfg.hc_mult,
2922        hc_eps: cfg.hc_eps,
2923        sinkhorn_iters: cfg.hc_sinkhorn_iters,
2924    };
2925    let mut next = vec![0.0f32; dim];
2926    let mut cold: Vec<(usize, f32)> = Vec::new();
2927    let mut routed: Vec<(usize, f32)> = Vec::new();
2928    let mut cold_x: Vec<f32> = Vec::new();
2929    let need_routes = route_stats_on() || pk.global.is_some();
2930    if !crate::gpu_wgpu::dsv4_layer_frame(
2931        &model,
2932        &w,
2933        geom,
2934        kv_id,
2935        li,
2936        Some(&prep.qr),
2937        &idx32,
2938        freqs,
2939        st.pos,
2940        &mut next,
2941        Some(&mut cold),
2942        need_routes.then_some(&mut routed),
2943        &mut cold_x,
2944    ) {
2945        return None;
2946    }
2947    if route_stats_on() {
2948        record_route(li, layers.len(), cfg.n_routed_experts, &routed);
2949    }
2950    drop(dynv);
2951    // The cold/readback slot already carries the device's real winners.
2952    // Feed all of them back to the allocator: this both installs misses and
2953    // refreshes hit ages, without a host-side router prediction or another
2954    // fence. A prediction disagreement therefore remains impossible here.
2955    if let Some(gl) = pk.global.as_ref() {
2956        let picks: Vec<usize> = routed.iter().map(|&(gi, _)| gi).collect();
2957        gl.pool
2958            .ensure_picks(&model, gl.layer, &picks, &l.experts, cfg.top_k, 1);
2959    }
2960    if cold.is_empty() {
2961        // Every winner was resident: the state stays on the card and the
2962        // frame's own next-fold is exact. This is the single-submission
2963        // path the whole function exists for.
2964        *folded = next;
2965        return Some(false);
2966    }
2967    // Cold winners: complete on the frame's own normed input, correct the
2968    // device state from the preserved post, and bring it home.
2969    if cold_x.len() < dim {
2970        return None;
2971    }
2972    let mut cold_sum = vec![0.0f32; dim];
2973    {
2974        let results: Vec<std::sync::Mutex<Vec<f32>>> = cold
2975            .iter()
2976            .map(|_| std::sync::Mutex::new(Vec::new()))
2977            .collect();
2978        let (cold_ref, results_ref, x_ref) = (&cold, &results, &cold_x[..dim]);
2979        std::thread::scope(|sc| {
2980            for i in 0..cold_ref.len() {
2981                let (gi, wt) = cold_ref[i];
2982                let Some(exp) = l.experts.get(gi) else {
2983                    continue;
2984                };
2985                let r = &results_ref[i];
2986                sc.spawn(move || {
2987                    let mut a = vec![0.0f32; cfg.dim];
2988                    crate::gpu::cpu_scope(|| run_expert(x_ref, exp, cfg, wt, None, &mut a));
2989                    *r.lock().unwrap() = a;
2990                });
2991            }
2992        });
2993        for r in &results {
2994            let a = r.lock().unwrap();
2995            for (o, v) in cold_sum.iter_mut().zip(a.iter()) {
2996                *o += v;
2997            }
2998        }
2999    }
3000    if !crate::gpu_wgpu::dsv4_state_add_cold_preserved(&cold_sum, cfg.hc_mult, state, kv_id, li) {
3001        return None;
3002    }
3003    // Reactive refill: the winners the slots did not hold are the likeliest
3004    // winners of the NEXT token — pull them in now, LRU-evicting.
3005    if pk.global.is_none() {
3006        let mut dynv = pk.dynslots.lock().unwrap();
3007        dynv.clock += 1;
3008        let clock = dynv.clock;
3009        for &(gi, _) in &cold {
3010            if gi >= dynv.remap.len() || dynv.remap[gi] != u32::MAX {
3011                continue;
3012            }
3013            let victim = (0..dynv.owner.len())
3014                .filter(|&sl| dynv.last[sl] != clock)
3015                .min_by_key(|&sl| dynv.last[sl]);
3016            let Some(victim) = victim else { break };
3017            let Some(exp) = l.experts.get(gi) else {
3018                continue;
3019            };
3020            let t3 = (|| {
3021                Some((
3022                    exp.w1.model_idx()?,
3023                    exp.w3.model_idx()?,
3024                    exp.w2.model_idx()?,
3025                ))
3026            })();
3027            let Some(t3) = t3 else { continue };
3028            let gu_q2 = exp.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP);
3029            let pack_first = pk.tensors.first().map(|t| t.0).unwrap_or(usize::MAX);
3030            if !crate::gpu_wgpu::dsv4_slot_fill(
3031                &model,
3032                pack_first,
3033                victim,
3034                gi,
3035                t3,
3036                cfg.moe_inter,
3037                cfg.dim,
3038                gu_q2,
3039            ) {
3040                break;
3041            }
3042            let old = dynv.owner[victim] as usize;
3043            if old < dynv.remap.len() {
3044                dynv.remap[old] = u32::MAX;
3045            }
3046            dynv.remap[gi] = victim as u32;
3047            dynv.owner[victim] = gi as u32;
3048            dynv.last[victim] = clock;
3049            dynv.mutated = true;
3050        }
3051    }
3052    if let Some(n) = nxt {
3053        let (f, post, comb) = hc_fold_norm(
3054            state,
3055            &n.hc_attn_fn,
3056            &n.hc_attn_scale,
3057            &n.hc_attn_base,
3058            &n.attn_norm,
3059            cfg,
3060            pool,
3061        );
3062        *folded = f;
3063        if !crate::gpu_wgpu::dsv4_hc_write(&post, &comb) {
3064            return None;
3065        }
3066    }
3067    Some(true)
3068}
3069
3070#[cfg(feature = "gpu")]
3071fn chain_max() -> usize {
3072    static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
3073    *N.get_or_init(|| {
3074        std::env::var("CMF_DSV4_CHAIN_MAX")
3075            .ok()
3076            .and_then(|v| v.parse().ok())
3077            .unwrap_or(usize::MAX)
3078    })
3079}
3080
3081/// `CMF_DSV4_CHAIN=1`: put a run of consecutive device-capable layers in ONE
3082/// submission. Off by default until it has been measured on a real card.
3083#[cfg(feature = "gpu")]
3084/// `CMF_DSV4_HOST_CPU_MOE=1`: a layer that fell off the card runs its MoE on
3085/// the host WITHOUT the per-op device route — one fence a token instead of
3086/// one a matvec. Whether that wins is a measurement.
3087/// `CMF_DSV4_PARTIAL_WALK=1`: the fused device walk of a partial layer.
3088/// OFF until its self-poisoning is repaired: its attention frame reads the
3089/// pooled slots its own MoE frame rewrote on the previous token, so every
3090/// token after the first attends over leftovers — the drafts it captures
3091/// от такого состояния never match the verify (acceptance 0, measured).
3092/// The host branch walks these layers correctly; the pack stays resident
3093/// for the verify tail.
3094fn partial_walk_on() -> bool {
3095    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3096    *ON.get_or_init(|| std::env::var("CMF_DSV4_PARTIAL_WALK").is_ok_and(|v| v != "0"))
3097}
3098
3099fn host_cpu_moe() -> bool {
3100    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3101    *ON.get_or_init(|| std::env::var("CMF_DSV4_HOST_CPU_MOE").is_ok_and(|v| v != "0"))
3102}
3103
3104fn chain_enabled() -> bool {
3105    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3106    *ON.get_or_init(|| {
3107        std::env::var("CMF_DSV4_CHAIN")
3108            .map(|v| v != "0")
3109            .unwrap_or(true)
3110    })
3111}
3112
3113/// Encode a maximal run of consecutive device-capable layers and submit it
3114/// ONCE. Every layer in the run builds its own attention inputs on the card,
3115/// so nothing comes back between them — that is the whole saving.
3116///
3117/// The run's state belongs to the device from here on: `st.window`,
3118/// `st.compressed` and the compressor streams for these layers are stale on
3119/// the host afterwards, and only the counts in `st.dev_*` are kept. A layer
3120/// that has ever been in a run must therefore never be handed to the CPU
3121/// path again, which `dev_owned` records.
3122#[cfg(feature = "gpu")]
3123#[allow(clippy::too_many_arguments)]
3124fn dsv4_chain_run(
3125    layers: &[Dsv4Layer],
3126    run: &[usize],
3127    cfg: &Dsv4Cfg,
3128    g: &Dsv4Globals,
3129    st: &mut Dsv4State,
3130    token_id: u32,
3131    // In AND out: the run reads the fold it starts from and MUST leave the
3132    // fold it produced, because whatever follows — a host layer, or the next
3133    // run after a cap — seeds from this. Passing it read-only left every
3134    // later segment starting from a stale fold: exact with one unbroken run,
3135    // release-scale garbage the moment anything splits the chain.
3136    folded: &mut Vec<f32>,
3137    // When present, the hyper-connection state rides home in the run's own
3138    // submission instead of costing a second fence afterwards. Only the
3139    // token's LAST run passes it — an earlier one would read a state the
3140    // layers after it still change.
3141    state_out: Option<&mut [f32]>,
3142    // How many consecutive tokens this run carries. One is decode; more is a
3143    // prompt chunk or a speculative verify, which are the same shape of work.
3144    batch: usize,
3145    // Their ids, needed only when `batch > 1`: a hash layer forces its expert
3146    // list from the token's id, so the batch needs one list per token and the
3147    // single `token_id` above cannot supply them.
3148    batch_ids: &[u32],
3149    // Whether the device's qn buffer is stale: true at layer zero and after
3150    // a host layer. When the previous layer was chained, its frame's tail
3151    // already left THIS layer's LoRA vector on the card, and recomputing it
3152    // here was a full wq_a matvec on the CPU per run — at CHAIN_MAX=1 that
3153    // is one per LAYER, which is how a 43-fence path measured slower than
3154    // an 86-fence one.
3155    need_qn: bool,
3156    pool: Option<&crate::pool::Pool>,
3157) -> bool {
3158    if run.is_empty() {
3159        return true;
3160    }
3161    let (dim, hd) = (cfg.dim, cfg.head_dim);
3162    let first = run[0];
3163    let Some(model) = layers[first].experts.first().and_then(|e| e.w1.model_arc()) else {
3164        return false;
3165    };
3166    // Batch callers seed every token's fold and qn in its own slot. Seeding
3167    // the legacy shared slot here is not merely redundant: `folded` carries
3168    // only the eventual LAST output and is empty before the batch runs.
3169    if batch <= 1 && need_qn {
3170        let mut qn0 = vec![0.0f32; cfg.q_lora_rank];
3171        layers[first].wq_a.matvec(folded, &mut qn0, pool);
3172        rms_weighted(&mut qn0, &layers[first].q_norm, cfg.norm_eps);
3173        if !crate::gpu_wgpu::dsv4_chain_seed(folded, &qn0) {
3174            return false;
3175        }
3176    } else if batch <= 1 && !crate::gpu_wgpu::dsv4_chain_seed_fold(folded) {
3177        return false;
3178    }
3179
3180    // Held apart from the borrowing structs below, which point into them.
3181    let mut packs = Vec::with_capacity(run.len());
3182    let mut forceds: Vec<Option<Vec<usize>>> = Vec::with_capacity(run.len());
3183    for &li in run {
3184        let Some(pk) = pack_for(&layers[li], cfg, li) else {
3185            return false;
3186        };
3187        // A chain cannot complete a cold pick between dependent layers, so
3188        // every expert OPEN in the route must be resident and the pack must
3189        // not have mutated. A masked-complete or hot-reordered pack carries
3190        // its immutable global-to-slot remap into the frame.
3191        // The verify batch reached here with cap-limited partial packs
3192        // and accepted 0 of 625 drafts — wrong experts, plausible sums.
3193        if !pk.route_complete() || pk.is_mutated() {
3194            return false;
3195        }
3196        let forced: Option<Vec<usize>> = layers[li].tid2eid.as_ref().and_then(|tbl| {
3197            let v: Vec<usize> = if pk.needs_remap() {
3198                hash_route(tbl, cfg.vocab, cfg.top_k, token_id)
3199            } else {
3200                hash_route(tbl, cfg.vocab, cfg.top_k, token_id)
3201                    .into_iter()
3202                    .map(|gi| pk.to_slot[gi])
3203                    .collect()
3204            };
3205            if v.contains(&usize::MAX) {
3206                None
3207            } else {
3208                Some(v)
3209            }
3210        });
3211        if layers[li].tid2eid.is_some() && forced.is_none() {
3212            return false;
3213        }
3214        forceds.push(forced);
3215        packs.push(pk);
3216    }
3217
3218    let mut items = Vec::with_capacity(run.len());
3219    let mut freqs = Vec::with_capacity(run.len());
3220    for (i, &li) in run.iter().enumerate() {
3221        let l = &layers[li];
3222        let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b), Some(wkv)) = (
3223            l.wq_a.model_idx(),
3224            l.wq_b.model_idx(),
3225            l.wo_a.model_idx(),
3226            l.wo_b.model_idx(),
3227            l.wkv.model_idx(),
3228        ) else {
3229            return false;
3230        };
3231        let comp = match &l.compressor {
3232            None => None,
3233            Some(cp) => {
3234                let (Some(a), Some(b)) = (cp.wkv.model_idx(), cp.wgate.model_idx()) else {
3235                    return false;
3236                };
3237                Some((
3238                    crate::gpu_wgpu::Dsv4CompW {
3239                        wkv: a,
3240                        wgate: b,
3241                        norm: &cp.norm,
3242                        ape: &cp.ape,
3243                    },
3244                    crate::gpu_wgpu::Dsv4CompGeom {
3245                        width: cp.wkv.rows(),
3246                        hidden: dim,
3247                        ratio: cp.ratio,
3248                        overlap: cp.overlap,
3249                        rope_dim: cfg.rope_head_dim,
3250                        eps: cfg.norm_eps,
3251                    },
3252                ))
3253            }
3254        };
3255        let ix = match &l.indexer {
3256            None => None,
3257            Some(ixr) => {
3258                let cp = &ixr.compressor;
3259                let (Some(a), Some(b), Some(qb), Some(wp)) = (
3260                    cp.wkv.model_idx(),
3261                    cp.wgate.model_idx(),
3262                    ixr.wq_b.model_idx(),
3263                    ixr.weights_proj.model_idx(),
3264                ) else {
3265                    return false;
3266                };
3267                let ih = ixr.weights_proj.rows();
3268                Some((
3269                    crate::gpu_wgpu::Dsv4CompW {
3270                        wkv: a,
3271                        wgate: b,
3272                        norm: &cp.norm,
3273                        ape: &cp.ape,
3274                    },
3275                    crate::gpu_wgpu::Dsv4CompGeom {
3276                        width: cp.wkv.rows(),
3277                        hidden: dim,
3278                        ratio: cp.ratio,
3279                        overlap: cp.overlap,
3280                        rope_dim: cfg.rope_head_dim,
3281                        eps: cfg.norm_eps,
3282                    },
3283                    crate::gpu_wgpu::Dsv4IxW {
3284                        wq_b: qb,
3285                        weights_proj: wp,
3286                    },
3287                    crate::gpu_wgpu::Dsv4IxGeom {
3288                        ih,
3289                        idim: ixr.wq_b.rows() / ih.max(1),
3290                        q_lora: cfg.q_lora_rank,
3291                        hidden: dim,
3292                        rope_dim: cfg.rope_head_dim,
3293                        eps: cfg.norm_eps,
3294                        top_k: cfg.index_topk,
3295                        window: cfg.window,
3296                    },
3297                ))
3298            }
3299        };
3300        // The cache has to be big enough BEFORE the frame appends into it:
3301        // a chained layer never calls dsv4_cache_write, which is what used
3302        // to create and grow it.
3303        let ew_c0 = l.compressor.as_ref().map_or(0, |cp| {
3304            if cp.overlap {
3305                cp.wkv.rows() / 2
3306            } else {
3307                cp.wkv.rows()
3308            }
3309        });
3310        let comp_extra = l
3311            .compressor
3312            .as_ref()
3313            .map_or(0, |cp| batch.max(1).div_ceil(cp.ratio.max(1)));
3314        let need = cfg.window * hd
3315            + (st.dev_n_comp[li] + comp_extra + 1) * ew_c0.max(1)
3316            + (batch.max(1) + 1) * hd;
3317        if !crate::gpu_wgpu::dsv4_cache_ensure(st.kv_id, li, need.next_power_of_two()) {
3318            return false;
3319        }
3320        let ew_c = comp.as_ref().map_or(
3321            0,
3322            |(_, cg)| {
3323                if cg.overlap { cg.width / 2 } else { cg.width }
3324            },
3325        );
3326        let ew_i = ix.as_ref().map_or(
3327            0,
3328            |(_, cg, _, _)| {
3329                if cg.overlap { cg.width / 2 } else { cg.width }
3330            },
3331        );
3332        let prep = crate::gpu_wgpu::Dsv4Prep {
3333            wkv,
3334            kv_norm: &l.kv_norm,
3335            comp,
3336            ix,
3337            filled: st.dev_filled[li],
3338            window: cfg.window,
3339            n_comp: st.dev_n_comp[li],
3340            n_ix: st.dev_n_ix[li],
3341            comp_dst_off: cfg.window * hd + st.dev_n_comp[li] * ew_c,
3342            ix_dst_off: st.dev_n_ix[li] * ew_i,
3343            idx_cap: cfg.window
3344                + if l.indexer.is_some() {
3345                    cfg.index_topk
3346                } else {
3347                    st.dev_n_comp[li] + comp_extra + 1
3348                },
3349        };
3350        let nxt = layers.get(li + 1);
3351        let w = crate::gpu_wgpu::Dsv4LayerW {
3352            attn: crate::gpu_wgpu::Dsv4AttnW {
3353                wq_a,
3354                wq_b,
3355                wo_a,
3356                wo_b,
3357                q_norm: &l.q_norm,
3358                sink: &l.attn_sink,
3359            },
3360            moe: crate::gpu_wgpu::Dsv4MoeW {
3361                router: &packs[i].router,
3362                experts: &packs[i].tensors,
3363                logits: &[],
3364                // The PACK's slice, not a per-run Vec: the address stability
3365                // is the whole point (see Pack::bias).
3366                bias: packs[i].bias.as_deref(),
3367                mask: packs[i].mask.as_deref(),
3368                forced: forceds[i].as_deref(),
3369                remap: packs[i].needs_remap().then_some(packs[i].remap.as_slice()),
3370                global: None,
3371                has_shared: true,
3372                shared_weight: 1.0,
3373                preweighted: false,
3374                qwen_softmax: false,
3375            },
3376            hc_ffn_fn: &l.hc_ffn_fn,
3377            hc_ffn_scale: &l.hc_ffn_scale,
3378            hc_ffn_base: &l.hc_ffn_base,
3379            hc_next_fn: nxt.map(|n| n.hc_attn_fn.as_slice()),
3380            hc_next_scale: nxt.map_or(&l.hc_attn_scale, |n| &n.hc_attn_scale),
3381            hc_next_base: nxt.map_or(&l.hc_attn_base, |n| n.hc_attn_base.as_slice()),
3382            ffn_norm: &l.ffn_norm,
3383            next_norm: nxt.map_or(&l.attn_norm, |n| n.attn_norm.as_slice()),
3384            next_q_norm: nxt.map_or(&l.q_norm, |n| n.q_norm.as_slice()),
3385            next_wq_a: nxt.and_then(|n| n.wq_a.model_idx()),
3386            router: &packs[i].router,
3387        };
3388        let geom = crate::gpu_wgpu::Dsv4LayerGeom {
3389            attn: crate::gpu_wgpu::Dsv4AttnGeom {
3390                dim,
3391                nh: cfg.n_heads,
3392                hd,
3393                rd: cfg.rope_head_dim,
3394                q_lora: cfg.q_lora_rank,
3395                o_lora: cfg.o_lora_rank,
3396                o_groups: cfg.o_groups,
3397                eps: cfg.norm_eps,
3398                scale: (hd as f32).powf(-0.5),
3399            },
3400            moe: crate::gpu_wgpu::Dsv4MoeGeom {
3401                hidden: dim,
3402                inter: cfg.moe_inter,
3403                top_k: cfg.top_k,
3404                route_scale: cfg.route_scale,
3405                swiglu_limit: cfg.swiglu_limit,
3406                gu_q2: l.experts.first().is_some_and(|e| {
3407                    e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
3408                }),
3409            },
3410            hc: cfg.hc_mult,
3411            hc_eps: cfg.hc_eps,
3412            sinkhorn_iters: cfg.hc_sinkhorn_iters,
3413        };
3414        freqs.push(if l.compressor.is_some() {
3415            g.inv_freq_compress.as_slice()
3416        } else {
3417            g.inv_freq_window.as_slice()
3418        });
3419        items.push((w, geom, prep));
3420    }
3421
3422    let mut out = vec![0.0f32; dim * batch.max(1)];
3423    if batch > 1 {
3424        // A batch keeps its own state per token. When a host tail follows,
3425        // all of those states ride home beside the folds in the same fence.
3426        // One forced row per token: same layers, the hash rows re-derived
3427        // from each token's own id.
3428        let mut forced_pt: Vec<Vec<Option<Vec<usize>>>> = Vec::with_capacity(batch);
3429        for t in 0..batch {
3430            let id = batch_ids.get(t).copied().unwrap_or(token_id);
3431            let mut row = Vec::with_capacity(run.len());
3432            for (i, &li) in run.iter().enumerate() {
3433                row.push(layers[li].tid2eid.as_ref().and_then(|tbl| {
3434                    let v: Vec<usize> = if packs[i].needs_remap() {
3435                        hash_route(tbl, cfg.vocab, cfg.top_k, id)
3436                    } else {
3437                        hash_route(tbl, cfg.vocab, cfg.top_k, id)
3438                            .into_iter()
3439                            .map(|gi| packs[i].to_slot[gi])
3440                            .collect()
3441                    };
3442                    if v.contains(&usize::MAX) {
3443                        None
3444                    } else {
3445                        Some(v)
3446                    }
3447                }));
3448                if layers[li].tid2eid.is_some() && row[i].is_none() {
3449                    return false;
3450                }
3451            }
3452            forced_pt.push(row);
3453        }
3454        if !crate::gpu_wgpu::dsv4_chain_batch(
3455            &model,
3456            &items,
3457            st.kv_id,
3458            first,
3459            &freqs,
3460            st.pos,
3461            batch,
3462            Some(&forced_pt),
3463            &mut out,
3464            state_out,
3465        ) {
3466            return false;
3467        }
3468        // The caller wants the LAST token's fold: it is the one whose logits
3469        // continue the sequence.
3470        *folded = out[(batch - 1) * dim..batch * dim].to_vec();
3471    } else {
3472        if !crate::gpu_wgpu::dsv4_layer_chain(
3473            &model, &items, st.kv_id, first, &freqs, st.pos, &mut out, state_out,
3474        ) {
3475            return false;
3476        }
3477        *folded = out;
3478    }
3479    // The device advanced these; the host keeps only the arithmetic. A batch
3480    // advanced them once per token, in order, so the host replays the same
3481    // rule that many times rather than inventing a closed form for it.
3482    for (i, &li) in run.iter().enumerate() {
3483        for t in 0..batch.max(1) {
3484            let pos = st.pos + t;
3485            st.dev_filled[li] = (st.dev_filled[li] + 1).min(cfg.window);
3486            if let Some((_, cg, ..)) = items[i].2.ix.as_ref() {
3487                if (pos + 1) % cg.ratio == 0 {
3488                    st.dev_n_ix[li] += 1;
3489                }
3490            }
3491            if let Some((_, cg)) = items[i].2.comp.as_ref() {
3492                if (pos + 1) % cg.ratio == 0 {
3493                    st.dev_n_comp[li] += 1;
3494                }
3495            }
3496        }
3497    }
3498    st.dev_owned = true;
3499    true
3500}
3501
3502/// `CMF_DSV4_HC_DEV=0` puts the hyper-connections back on the host.
3503#[cfg(feature = "gpu")]
3504fn hc_on_device() -> bool {
3505    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3506    *ON.get_or_init(|| {
3507        // OPT-IN. On the release checkpoint this path reads 3.234 against
3508        // the CPU's 3.282 — divergent — and the speed is unchanged, so there
3509        // is no trade to weigh: it must not be the default until it is
3510        // exact. The toy's near-agreement (129.787 vs 129.792) hid a real
3511        // fault the release exposes.
3512        std::env::var("CMF_DSV4_HC_DEV").is_ok_and(|v| v != "0") && crate::gpu::backend_available()
3513    })
3514}
3515
3516/// The two-frame path with the hyper-connections on the card.
3517///
3518/// The host still prepares each layer's attention inputs — the compressor,
3519/// the indexer and the window, which are exact there — but it no longer
3520/// folds, Sinkhorns or norms, and it no longer carries the MoE half's input
3521/// between the halves: the attention frame leaves it on the device and the
3522/// MoE frame reads it from there. One readback a layer instead of two, and
3523/// 19 ms of host arithmetic a token gone.
3524#[cfg(feature = "gpu")]
3525#[allow(clippy::too_many_arguments)]
3526fn dsv4_two_frame_loop(
3527    state: &mut [f32],
3528    layers: &[Dsv4Layer],
3529    g: &Dsv4Globals,
3530    cfg: &Dsv4Cfg,
3531    st: &mut Dsv4State,
3532    token_id: u32,
3533    inv_freq: &[f32],
3534    pool: Option<&crate::pool::Pool>,
3535    scratch: &mut HcScratch,
3536) -> bool {
3537    let dim = cfg.dim;
3538    let freqs_of = |l: &Dsv4Layer| -> &[f32] {
3539        let f = if l.compressor.is_some() {
3540            &g.inv_freq_compress
3541        } else {
3542            &g.inv_freq_window
3543        };
3544        if f.is_empty() { inv_freq } else { f.as_slice() }
3545    };
3546    // Layer zero's fold has no frame before it, exactly as in the layer path.
3547    let (mut folded, post0, comb0) = hc_fold_norm(
3548        state,
3549        &layers[0].hc_attn_fn,
3550        &layers[0].hc_attn_scale,
3551        &layers[0].hc_attn_base,
3552        &layers[0].attn_norm,
3553        cfg,
3554        pool,
3555    );
3556    if !crate::gpu_wgpu::dsv4_state_write(state) || !crate::gpu_wgpu::dsv4_hc_write(&post0, &comb0)
3557    {
3558        return false;
3559    }
3560    // PRE-FLIGHT, before the first byte of state moves: a mid-loop refusal
3561    // would hand the token back to the ordinary loop AFTER these caches
3562    // advanced, and the second advance is not a slow answer but a wrong one.
3563    // The same discipline the layer loop states in the same words.
3564    let mut on_dev = vec![false; layers.len()];
3565    for (li, l) in layers.iter().enumerate() {
3566        let Some(pk) = pack_for(l, cfg, li) else {
3567            return false;
3568        };
3569        let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
3570            return false;
3571        };
3572        let gu_q2 = l
3573            .experts
3574            .first()
3575            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
3576        let attn_ok = [
3577            l.wq_a.model_idx(),
3578            l.wq_b.model_idx(),
3579            l.wo_a.model_idx(),
3580            l.wo_b.model_idx(),
3581        ]
3582        .into_iter()
3583        .flatten()
3584        .all(|i| crate::gpu_wgpu::dsv4_weight_ready(&model, i));
3585        on_dev[li] = attn_ok
3586            && pk.route_complete()
3587            && crate::gpu_wgpu::dsv4_experts_ready(
3588                &model,
3589                &pk.tensors,
3590                cfg.moe_inter,
3591                dim,
3592                gu_q2,
3593                l.experts.first().is_some_and(|e| {
3594                    e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
3595                }),
3596            );
3597    }
3598    if !on_dev.iter().any(|&x| x) {
3599        return false;
3600    }
3601    let mut sink = vec![0.0f32; dim];
3602    for (li, l) in layers.iter().enumerate() {
3603        // A layer the card cannot hold runs on the host WHOLE, with the
3604        // state fetched and put back around it — the mixed ownership the
3605        // layer loop already proved out.
3606        if !on_dev[li] {
3607            if !crate::gpu_wgpu::dsv4_state_read(state) {
3608                return false;
3609            }
3610            let freqs = freqs_of(l);
3611            hc_block(
3612                state,
3613                &l.hc_attn_fn,
3614                &l.hc_attn_scale,
3615                &l.hc_attn_base,
3616                &l.attn_norm,
3617                cfg,
3618                scratch,
3619                pool,
3620                |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
3621            );
3622            hc_block(
3623                state,
3624                &l.hc_ffn_fn,
3625                &l.hc_ffn_scale,
3626                &l.hc_ffn_base,
3627                &l.ffn_norm,
3628                cfg,
3629                scratch,
3630                pool,
3631                |f, o| moe_step(f, l, cfg, token_id, li, pool, o),
3632            );
3633            let nref = layers.get(li + 1).unwrap_or(l);
3634            let (f, p2, c2) = hc_fold_norm(
3635                state,
3636                &nref.hc_attn_fn,
3637                &nref.hc_attn_scale,
3638                &nref.hc_attn_base,
3639                &nref.attn_norm,
3640                cfg,
3641                pool,
3642            );
3643            folded = f;
3644            if !crate::gpu_wgpu::dsv4_hc_write(&p2, &c2)
3645                || !crate::gpu_wgpu::dsv4_state_write(state)
3646            {
3647                return false;
3648            }
3649            continue;
3650        }
3651        // The host's half: the caches and the attended list, untouched.
3652        let mut prep = AttnPrep::default();
3653        attention_step(
3654            &folded,
3655            l,
3656            cfg,
3657            st,
3658            li,
3659            freqs_of(l),
3660            pool,
3661            Some(&mut prep),
3662            &mut sink,
3663        );
3664        let hd = cfg.head_dim;
3665        let n_comp = st.compressed[li].len() / hd;
3666        let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
3667        if !crate::gpu_wgpu::dsv4_cache_write(st.kv_id, li, 0, &st.window[li], cap)
3668            || (n_comp > 0
3669                && !crate::gpu_wgpu::dsv4_cache_write(
3670                    st.kv_id,
3671                    li,
3672                    cfg.window * hd,
3673                    &st.compressed[li],
3674                    cap,
3675                ))
3676        {
3677            return false;
3678        }
3679        let _idx32: Vec<u32> = prep
3680            .idxs
3681            .iter()
3682            .map(|&p| {
3683                if p < prep.win_len {
3684                    p as u32
3685                } else {
3686                    (cfg.window + (p - prep.win_len)) as u32
3687                }
3688            })
3689            .collect();
3690        let nxt = layers.get(li + 1);
3691        let a_tail = crate::gpu_wgpu::Dsv4HcTail {
3692            fn_: &l.hc_ffn_fn,
3693            scale: &l.hc_ffn_scale,
3694            base: &l.hc_ffn_base,
3695            norm: &l.ffn_norm,
3696            hc: cfg.hc_mult,
3697            sinkhorn_iters: cfg.hc_sinkhorn_iters,
3698            hc_eps: cfg.hc_eps,
3699            eps: cfg.norm_eps,
3700        };
3701        let scale = (cfg.head_dim as f32).powf(-0.5);
3702        if !attn_frame(
3703            l,
3704            cfg,
3705            st,
3706            li,
3707            &folded,
3708            &prep.qr,
3709            &prep.idxs,
3710            freqs_of(l),
3711            st.pos,
3712            prep.win_len,
3713            scale,
3714            Some(&a_tail),
3715            &mut [],
3716        ) {
3717            return false;
3718        }
3719        let m_tail = nxt.map(|n| crate::gpu_wgpu::Dsv4HcTail {
3720            fn_: &n.hc_attn_fn,
3721            scale: &n.hc_attn_scale,
3722            base: &n.hc_attn_base,
3723            norm: &n.attn_norm,
3724            hc: cfg.hc_mult,
3725            sinkhorn_iters: cfg.hc_sinkhorn_iters,
3726            hc_eps: cfg.hc_eps,
3727            eps: cfg.norm_eps,
3728        });
3729        let mut next = vec![0.0f32; dim];
3730        let pair = m_tail
3731            .as_ref()
3732            .zip(nxt)
3733            .map(|(t, n)| (t, n.attn_norm.as_slice()));
3734        let forced = l
3735            .tid2eid
3736            .as_ref()
3737            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
3738        if moe_frame(
3739            &[],
3740            l,
3741            cfg,
3742            li,
3743            &[],
3744            forced.as_deref(),
3745            pool,
3746            Some(&a_tail),
3747            pair,
3748            &mut next,
3749        )
3750        .is_none()
3751        {
3752            return false;
3753        }
3754        folded = next;
3755    }
3756    let _ = scratch;
3757    crate::gpu_wgpu::dsv4_state_read(state)
3758}
3759
3760/// The host half of one hyper-connection block: mixes, Sinkhorn, fold, norm.
3761/// The device does this for every layer but the first, whose state it has not
3762/// seen yet.
3763#[cfg(feature = "gpu")]
3764#[allow(clippy::too_many_arguments)]
3765fn hc_fold_norm(
3766    state: &[f32],
3767    hc_fn: &[f32],
3768    hc_scale: &[f32; 3],
3769    hc_base: &[f32],
3770    norm_w: &[f32],
3771    cfg: &Dsv4Cfg,
3772    pool: Option<&crate::pool::Pool>,
3773) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
3774    let (hc, dim) = (cfg.hc_mult, cfg.dim);
3775    let mix_hc = (2 + hc) * hc;
3776    let mut mixes = vec![0.0f32; mix_hc];
3777    hc_mixes(state, hc_fn, mix_hc, cfg.norm_eps, pool, &mut mixes);
3778    let mut pre = vec![0.0f32; hc];
3779    let mut post = vec![0.0f32; hc];
3780    let mut comb = vec![0.0f32; hc * hc];
3781    hc_split_sinkhorn(
3782        &mixes,
3783        hc_scale,
3784        hc_base,
3785        hc,
3786        cfg.hc_sinkhorn_iters,
3787        cfg.hc_eps,
3788        &mut pre,
3789        &mut post,
3790        &mut comb,
3791    );
3792    let mut folded = vec![0.0f32; dim];
3793    hc_fold(state, &pre, hc, dim, &mut folded);
3794    rms_weighted(&mut folded, norm_w, cfg.norm_eps);
3795    // post and comb travel with the fold: the frame's opening expand needs
3796    // exactly those, and they are not recoverable from the state alone.
3797    (folded, post, comb)
3798}
3799
3800/// `CMF_DSV4_GPU_LAYER=1`: one submission per layer instead of two, with the
3801/// hyper-connection glue and the router on the device.
3802///
3803/// CORRECT — perplexity 5.211 against the CPU's 5.211 on the release, 128.576
3804/// against 128.576 on the toy — and SLOWER on this hardware: 6.0 tok/s where
3805/// the two-frame path gets 9.3. The reason is not the frame, it is the
3806/// all-or-nothing granularity underneath it. A layer whose experts miss VRAM
3807/// runs entirely on the host, attention included (6.5 ms a call against 0.9),
3808/// and with 100 GB of experts against a 98 GB card a fifth of the layers
3809/// miss. The two-frame path only loses the MoE half of those layers.
3810///
3811/// So the barrier it saves is real and the fallback it forces costs more. The
3812/// fix is the granularity: pack the experts that FIT, route over all of them
3813/// anyway, and run the few cold picks of a token on the host — per EXPERT,
3814/// not per layer. Then no layer ever leaves the device and this frame wins by
3815/// the 15 ms a token it was built to save.
3816#[cfg(feature = "gpu")]
3817fn gpu_layer_enabled() -> bool {
3818    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3819    *ON.get_or_init(|| {
3820        std::env::var("CMF_DSV4_GPU_LAYER")
3821            .map(|v| v != "0")
3822            .unwrap_or(true)
3823            && crate::gpu::backend_available()
3824    })
3825}
3826
3827/// The packed expert set of one layer: which globals made it in, and their
3828/// directory indices in packing order with the shared expert last. Built once
3829/// — the mask does not change during a run — and keyed by layer.
3830#[cfg(feature = "gpu")]
3831struct PackDyn {
3832    remap: Vec<u32>,
3833    owner: Vec<u32>,
3834    last: Vec<u64>,
3835    clock: u64,
3836    /// Per-expert recent-use tally (halved every 64 tokens): the q* rule.
3837    /// A slot upload only pays for itself when the expert is REUSED —
3838    /// FreeToken's split — so a fetch needs `seen >= CMF_DSV4_FETCH_MIN_SEEN`
3839    /// prior recent picks; a first-timer stays a cold pick and the CPU
3840    /// reads it at the shelf. The automatic default comes from a small
3841    /// one-time host→device bandwidth probe; an explicit env still wins.
3842    seen: Vec<u16>,
3843    /// Set on the first slot refill. The chain and the batch verify hand
3844    /// the device `remap: None` and trust the banks to still hold the
3845    /// BUILD-TIME packing — a mutated pack must never be claimed by them.
3846    mutated: bool,
3847}
3848
3849/// One logical cache line in the model-wide expert pool. `layer` is the
3850/// first expert tensor's directory index rather than the ordinal: trunk and
3851/// MTP stages both use small ordinal numbers, while this identity is unique
3852/// for the lifetime of the mapped model.
3853#[cfg(feature = "gpu")]
3854#[derive(Clone, Copy)]
3855struct GlobalOwner {
3856    layer: usize,
3857    expert: usize,
3858    pinned: bool,
3859}
3860
3861#[cfg(feature = "gpu")]
3862struct GlobalPoolDyn {
3863    slot_for: std::collections::HashMap<(usize, usize), u32>,
3864    owner: Vec<Option<GlobalOwner>>,
3865    last: Vec<u64>,
3866    seen: std::collections::HashMap<(usize, usize), u16>,
3867    occupancy: std::collections::HashMap<usize, usize>,
3868    clock: u64,
3869}
3870
3871/// FreeToken's unified `(layer, expert) -> slot` table, with one deliberate
3872/// addition learned from this engine's routing traces: each trunk layer gets
3873/// a small protected floor. A plain global LRU under a deterministic
3874/// layer-by-layer sweep measured 4.7% hits, while equal per-layer LRUs hit
3875/// about 70%. The floor prevents cyclic scan eviction; every slot above it is
3876/// still borrowed and evicted globally, so skewed layers use otherwise idle
3877/// capacity.
3878#[cfg(feature = "gpu")]
3879struct GlobalPool {
3880    uid: u64,
3881    capacity: usize,
3882    segment_slots: usize,
3883    floor: usize,
3884    state: std::sync::Mutex<GlobalPoolDyn>,
3885}
3886
3887#[cfg(feature = "gpu")]
3888impl GlobalPool {
3889    fn remap(&self, layer: usize, n: usize) -> Vec<u32> {
3890        let st = self.state.lock().unwrap();
3891        debug_assert_eq!(self.capacity, st.owner.len());
3892        (0..n)
3893            .map(|e| st.slot_for.get(&(layer, e)).copied().unwrap_or(u32::MAX))
3894            .collect()
3895    }
3896
3897    fn seed_layer(
3898        &self,
3899        model: &std::sync::Arc<cortiq_core::CmfModel>,
3900        layer: usize,
3901        shared: (usize, usize, usize),
3902        _routed: &[(usize, (usize, usize, usize))],
3903    ) -> Option<u32> {
3904        let mut st = self.state.lock().unwrap();
3905        let shared_key = (layer, usize::MAX);
3906        let shared_slot = if let Some(&slot) = st.slot_for.get(&shared_key) {
3907            slot
3908        } else {
3909            let slot = st.owner.iter().position(Option::is_none)?;
3910            if !crate::gpu_wgpu::dsv4_global_slot_fill(model, slot, shared) {
3911                return None;
3912            }
3913            st.owner[slot] = Some(GlobalOwner {
3914                layer,
3915                expert: usize::MAX,
3916                pinned: true,
3917            });
3918            st.slot_for.insert(shared_key, slot as u32);
3919            *st.occupancy.entry(layer).or_insert(0) += 1;
3920            slot as u32
3921        };
3922        // Do not fill the remaining 40+ GiB by expert id. Route locality is
3923        // checkpoint- and prompt-dependent; the old eager seed spent a
3924        // minute uploading rows that the first token immediately replaced.
3925        // Empty slots are populated by the real top-k before its dispatch.
3926        Some(shared_slot)
3927    }
3928
3929    fn ensure_picks(
3930        &self,
3931        model: &std::sync::Arc<cortiq_core::CmfModel>,
3932        layer: usize,
3933        picks: &[usize],
3934        experts: &[Dsv4Expert],
3935        quota: usize,
3936        min_seen: u16,
3937    ) -> Vec<u32> {
3938        let mut st = self.state.lock().unwrap();
3939        st.clock = st.clock.saturating_add(1);
3940        let clock = st.clock;
3941        if clock % 64 == 0 {
3942            for v in st.seen.values_mut() {
3943                *v >>= 1;
3944            }
3945        }
3946        for &expert in picks {
3947            let key = (layer, expert);
3948            let seen = st.seen.entry(key).or_insert(0);
3949            *seen = seen.saturating_add(1);
3950            if let Some(&slot) = st.slot_for.get(&key) {
3951                st.last[slot as usize] = clock;
3952            }
3953        }
3954        let mut fetched = 0usize;
3955        for &expert in picks {
3956            if fetched >= quota {
3957                break;
3958            }
3959            let key = (layer, expert);
3960            if st.slot_for.contains_key(&key) || st.seen.get(&key).copied().unwrap_or(0) < min_seen
3961            {
3962                continue;
3963            }
3964            let Some(exp) = experts.get(expert) else {
3965                continue;
3966            };
3967            let Some(tensors) = (|| {
3968                Some((
3969                    exp.w1.model_idx()?,
3970                    exp.w3.model_idx()?,
3971                    exp.w2.model_idx()?,
3972                ))
3973            })() else {
3974                continue;
3975            };
3976            let empty = st.owner.iter().position(Option::is_none);
3977            // First evict from a layer that currently borrows above its
3978            // floor. Do not evict any expert required by this very dispatch.
3979            let over_floor = |o: GlobalOwner, occ: &std::collections::HashMap<usize, usize>| {
3980                occ.get(&o.layer).copied().unwrap_or(0) > self.floor
3981            };
3982            let eligible =
3983                |o: GlobalOwner| !o.pinned && !(o.layer == layer && picks.contains(&o.expert));
3984            let victim = empty
3985                .or_else(|| {
3986                    st.owner
3987                        .iter()
3988                        .enumerate()
3989                        .filter_map(|(slot, &o)| {
3990                            o.filter(|&x| eligible(x) && over_floor(x, &st.occupancy))
3991                                .map(|_| slot)
3992                        })
3993                        .min_by_key(|&slot| st.last[slot])
3994                })
3995                .or_else(|| {
3996                    // If every layer sits exactly at its floor, replace within
3997                    // the requesting layer. This is per-layer LRU behaviour and
3998                    // cannot trigger the cyclic global scan collapse.
3999                    st.owner
4000                        .iter()
4001                        .enumerate()
4002                        .filter_map(|(slot, &o)| {
4003                            o.filter(|&x| eligible(x) && x.layer == layer).map(|_| slot)
4004                        })
4005                        .min_by_key(|&slot| st.last[slot])
4006                })
4007                .or_else(|| {
4008                    // A new/MTP layer has no protected share yet. Let it borrow
4009                    // the globally oldest unpinned line; trunk floors are a
4010                    // locality guarantee, not a permanent admission ban.
4011                    st.owner
4012                        .iter()
4013                        .enumerate()
4014                        .filter_map(|(slot, &o)| o.filter(|&x| eligible(x)).map(|_| slot))
4015                        .min_by_key(|&slot| st.last[slot])
4016                });
4017            let Some(victim) = victim else { break };
4018            if !crate::gpu_wgpu::dsv4_global_slot_fill(model, victim, tensors) {
4019                break;
4020            }
4021            if let Some(old) = st.owner[victim] {
4022                st.slot_for.remove(&(old.layer, old.expert));
4023                if let Some(n) = st.occupancy.get_mut(&old.layer) {
4024                    *n = n.saturating_sub(1);
4025                }
4026            }
4027            st.owner[victim] = Some(GlobalOwner {
4028                layer,
4029                expert,
4030                pinned: false,
4031            });
4032            st.slot_for.insert(key, victim as u32);
4033            *st.occupancy.entry(layer).or_insert(0) += 1;
4034            st.last[victim] = clock;
4035            fetched += 1;
4036        }
4037        drop(st);
4038        self.remap(layer, experts.len())
4039    }
4040
4041    /// Reserve a small immutable slice of the unified arena for the draft.
4042    /// The draft graph cannot pause between its dependent stages to repair a
4043    /// slot that the exact trunk evicted, so its bounded resident subset is
4044    /// pinned.  These are still the SAME physical banks: no second expert
4045    /// allocation and no duplicate upload cache are created.
4046    fn pin_picks(
4047        &self,
4048        model: &std::sync::Arc<cortiq_core::CmfModel>,
4049        layer: usize,
4050        picks: &[usize],
4051        experts: &[Dsv4Expert],
4052    ) -> Vec<u32> {
4053        let remap = self.ensure_picks(model, layer, picks, experts, picks.len(), 1);
4054        let mut st = self.state.lock().unwrap();
4055        for &expert in picks {
4056            if let Some(&slot) = st.slot_for.get(&(layer, expert)) {
4057                if let Some(owner) = st.owner[slot as usize].as_mut() {
4058                    owner.pinned = true;
4059                }
4060            }
4061        }
4062        remap
4063    }
4064}
4065
4066#[cfg(feature = "gpu")]
4067#[derive(Clone)]
4068struct GlobalPack {
4069    pool: std::sync::Arc<GlobalPool>,
4070    layer: usize,
4071    shared_slot: u32,
4072}
4073
4074#[cfg(feature = "gpu")]
4075impl Pack {
4076    /// True once any slot was refilled away from the build-time packing.
4077    fn is_mutated(&self) -> bool {
4078        self.global.is_some() || self.dynslots.lock().unwrap().mutated
4079    }
4080
4081    /// Complete means complete for the route the model will actually take.
4082    /// A task mask can close most of the 256 rows; packing every OPEN row is
4083    /// then a full device layer, not a partial layer with 200 imaginary cold
4084    /// experts. Hash layers deliberately carry no mask because their forced
4085    /// rows remain the exact checkpoint contract.
4086    fn route_complete(&self) -> bool {
4087        if self.global.is_some() {
4088            return false;
4089        }
4090        let need = self
4091            .mask
4092            .as_deref()
4093            .map_or(self.remap.len(), |m| m.iter().filter(|&&x| x != 0).count());
4094        self.globals.len() >= need
4095    }
4096
4097    /// A global-to-slot table is needed for a masked set and for a complete
4098    /// pack whose hot-first order is not identity. Without it a full but
4099    /// reordered pack silently runs the right router index on the wrong bank.
4100    fn needs_remap(&self) -> bool {
4101        self.global.is_some()
4102            || self.mask.is_some()
4103            || self
4104                .remap
4105                .iter()
4106                .enumerate()
4107                .any(|(i, &slot)| slot != i as u32)
4108    }
4109}
4110
4111/// The packed expert set of one layer: which globals made it in, and their
4112/// directory indices in packing order with the shared expert last. Keyed by
4113/// layer; the STATIC fields are built once, the dynamic slot state evolves.
4114#[cfg(feature = "gpu")]
4115struct Pack {
4116    /// The router as dense f32, expanded once. It is 4 MB a layer against a
4117    /// 112 GB model, it lives as long as the process — so the address-keyed
4118    /// device cache is sound for it, unlike anything built per call.
4119    router: Vec<f32>,
4120    /// global expert id -> packed slot, `usize::MAX` for the ones left out.
4121    to_slot: Vec<usize>,
4122    /// The same, as the u32 table the router reads.
4123    remap: Vec<u32>,
4124    /// packed order, globals only (shared is not in here).
4125    globals: Vec<usize>,
4126    tensors: Vec<(usize, usize, usize)>,
4127    /// Global 0/1 route mask consumed by the GPU router. Stable storage is
4128    /// part of the pack because the device constant cache keys by address.
4129    /// None on exact/hash routing.
4130    mask: Option<Vec<u32>>,
4131    /// FreeToken-style dynamic slots: the packed subset FOLLOWS the router
4132    /// instead of staying whatever load-time frequency guessed. `remap` here
4133    /// is the LIVE table (the immutable `remap` above is the initial state
4134    /// and stays only as the build artifact); `owner[slot]` is the global
4135    /// expert id occupying the slot; `last[slot]`/`clock` drive LRU. The
4136    /// device bank buffers accept `write_buffer` at slot offsets, and the
4137    /// frame re-uploads the remap every call — so a refill is two queue
4138    /// writes and no cache invalidation anywhere.
4139    dynslots: std::sync::Mutex<PackDyn>,
4140    /// The noaux_tc bias in GLOBAL order, kept here because it is the same
4141    /// every token and the pack lives as long as the process. Global order is
4142    /// required by masked/remapped routing; its stable address lets many
4143    /// layers share one submission. A bias uploaded through
4144    /// the per-call pool is written by every layer of a run BEFORE the run's
4145    /// single submit — queue writes do not interleave with passes — so every
4146    /// layer routed with the LAST layer's bias. On the release every scored
4147    /// layer carries one, which is the 50.280.
4148    bias: Option<Vec<f32>>,
4149    /// Present only on the descriptor-indexed Q4TP path. `remap` above is
4150    /// merely the build-time snapshot there; every frame takes a fresh map
4151    /// from this common allocator after its refills/evictions.
4152    global: Option<GlobalPack>,
4153}
4154
4155#[cfg(feature = "gpu")]
4156/// Candidate order for a budget-limited pack: hottest expert first, by the
4157/// measured tally `CMF_DSV4_PACK_FREQ` points at (`layer<TAB>expert<TAB>count`
4158/// lines). None when the variable is unset, the file is unreadable, or the
4159/// tally has nothing for this layer — the caller keeps id order then. Ties
4160/// and untallied experts follow in id order, so the choice is deterministic.
4161fn pack_freq_order(li: usize, n: usize) -> Option<Vec<usize>> {
4162    use std::collections::HashMap;
4163    use std::sync::OnceLock;
4164    static FREQ: OnceLock<Option<HashMap<(usize, usize), u64>>> = OnceLock::new();
4165    let map = FREQ
4166        .get_or_init(|| {
4167            let path = std::env::var("CMF_DSV4_PACK_FREQ").ok()?;
4168            let text = match std::fs::read_to_string(&path) {
4169                Ok(t) => t,
4170                Err(e) => {
4171                    eprintln!("CMF_DSV4_PACK_FREQ={path} не читается ({e}) — порядок по id");
4172                    return None;
4173                }
4174            };
4175            let mut m = HashMap::new();
4176            for line in text.lines() {
4177                let mut it = line.split('\t');
4178                if let (Some(l), Some(e), Some(c)) = (it.next(), it.next(), it.next()) {
4179                    if let (Ok(l), Ok(e), Ok(c)) =
4180                        (l.trim().parse(), e.trim().parse(), c.trim().parse::<u64>())
4181                    {
4182                        *m.entry((l, e)).or_insert(0) += c;
4183                    }
4184                }
4185            }
4186            Some(m)
4187        })
4188        .as_ref()?;
4189    if !(0..n).any(|e| map.contains_key(&(li, e))) {
4190        return None;
4191    }
4192    let mut idx: Vec<usize> = (0..n).collect();
4193    idx.sort_by_key(|&e| {
4194        (
4195            std::cmp::Reverse(map.get(&(li, e)).copied().unwrap_or(0)),
4196            e,
4197        )
4198    });
4199    Some(idx)
4200}
4201
4202#[cfg(feature = "gpu")]
4203fn global_pool_for(
4204    model: &std::sync::Arc<cortiq_core::CmfModel>,
4205    cfg: &Dsv4Cfg,
4206    gu_q2: bool,
4207    dn_q2: bool,
4208) -> Option<std::sync::Arc<GlobalPool>> {
4209    use std::collections::HashMap;
4210    use std::sync::{Arc, Mutex, OnceLock};
4211    if dn_q2
4212        || !crate::gpu_wgpu::dsv4_global_moe_supported()
4213        || std::env::var("CMF_MOE_MASK").is_ok()
4214    {
4215        return None;
4216    }
4217    static POOLS: OnceLock<Mutex<HashMap<u64, Arc<GlobalPool>>>> = OnceLock::new();
4218    let pools = POOLS.get_or_init(|| Mutex::new(HashMap::new()));
4219    if let Some(p) = pools.lock().unwrap().get(&model.uid()).cloned() {
4220        return Some(p);
4221    }
4222    let requested = crate::gpu_wgpu::dsv4_experts_fit(cfg.moe_inter, cfg.dim, gu_q2, false);
4223    let (capacity, segment_slots) =
4224        crate::gpu_wgpu::dsv4_global_moe_create(model, requested, cfg.moe_inter, cfg.dim, gu_q2)?;
4225    let layers = model.header.arch.num_layers.max(1);
4226    let p = Arc::new(GlobalPool {
4227        uid: model.uid(),
4228        capacity,
4229        segment_slots,
4230        // At least shared + one routed line remain protected per layer when
4231        // geometry permits it. Larger cards naturally raise the floor.
4232        floor: (capacity / layers).max(2),
4233        state: Mutex::new(GlobalPoolDyn {
4234            slot_for: HashMap::new(),
4235            owner: vec![None; capacity],
4236            last: vec![0; capacity],
4237            seen: HashMap::new(),
4238            occupancy: HashMap::new(),
4239            clock: 0,
4240        }),
4241    });
4242    pools.lock().unwrap().insert(model.uid(), p.clone());
4243    Some(p)
4244}
4245
4246#[cfg(feature = "gpu")]
4247fn pack_for(l: &Dsv4Layer, cfg: &Dsv4Cfg, li: usize) -> Option<std::sync::Arc<Pack>> {
4248    use std::collections::HashMap;
4249    use std::sync::{Arc, Mutex, OnceLock};
4250    static CACHE: OnceLock<Mutex<HashMap<(u64, usize, usize), Option<Arc<Pack>>>>> =
4251        OnceLock::new();
4252    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
4253    // Keyed by the layer's IDENTITY, not its ordinal. The draft's three
4254    // stages are layers too and they number 0, 1, 2 — under an ordinal key
4255    // they would be handed the trunk's first three packs: another layer's
4256    // router, another layer's tensor indices, another layer's bias. The gate
4257    // tensor is what actually distinguishes them.
4258    let model_uid = l
4259        .experts
4260        .first()
4261        .and_then(|e| e.w1.model_arc())
4262        .map_or(0, |m| m.uid());
4263    // Dense f32 routers need not have a directory handle, so the gate index
4264    // alone can be `None` for every layer. Pair the ordinal with the first
4265    // expert's mapped identity; model UID keeps long-lived multi-model
4266    // servers separate, while the expert index distinguishes trunk and MTP
4267    // layers that reuse ordinal 0/1/2.
4268    let first_expert = l
4269        .experts
4270        .first()
4271        .and_then(|e| e.w1.model_idx())
4272        .unwrap_or(usize::MAX);
4273    let key = (model_uid, li, first_expert);
4274    if let Some(v) = cache.lock().unwrap().get(&key) {
4275        return v.clone();
4276    }
4277    // `CMF_DSV4_PACK_MAX_LI=N` — do not pack layers above N at all. A layer
4278    // with no pack stays wholly host-owned, which is what both the batched
4279    // prefill and a speculative verify need of the tail: a device-owned
4280    // partial layer can join neither the batch (incomplete pack) nor the
4281    // causal host tail (its caches live on the card). This also carves the
4282    // VRAM the tail would have taken for the draft's own pack.
4283    if let Ok(v) = std::env::var("CMF_DSV4_PACK_MAX_LI") {
4284        if v.parse::<usize>().is_ok_and(|max| li > max) {
4285            cache.lock().unwrap().insert(key, None);
4286            return None;
4287        }
4288    }
4289    let build = || -> Option<Arc<Pack>> {
4290        let mut to_slot = vec![usize::MAX; cfg.n_routed_experts];
4291        let mut globals = Vec::new();
4292        let mut tensors = Vec::new();
4293        let idx3 = |e: &Dsv4Expert| -> Option<(usize, usize, usize)> {
4294            Some((e.w1.model_idx()?, e.w3.model_idx()?, e.w2.model_idx()?))
4295        };
4296        let route_mask: Option<Vec<u32>> = if l.tid2eid.is_none() {
4297            l.mask
4298                .as_deref()
4299                .map(|m| m.iter().map(|&open| u32::from(open)).collect())
4300        } else {
4301            None
4302        };
4303        // How many experts the card still has room for, minus one for the
4304        // shared expert, which always rides. Everything past that stays on the
4305        // host and is reached through the remap — the router still ranges over
4306        // all of them, so this costs speed and not a single bit of quality.
4307        let gu_q2 = l
4308            .experts
4309            .first()
4310            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
4311        let dn_q2 = l
4312            .experts
4313            .first()
4314            .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
4315        // Exact default on Q4TP: one physical/logical cache for every
4316        // `(layer, expert)` pair. Explicit mask experiments keep the old
4317        // local path so this branch never combines two independent changes
4318        // to model semantics.
4319        if route_mask.is_none() {
4320            if let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) {
4321                if let Some(gp) = global_pool_for(&model, cfg, gu_q2, dn_q2) {
4322                    let layer_key = first_expert;
4323                    let order = pack_freq_order(li, l.experts.len())
4324                        .unwrap_or_else(|| (0..l.experts.len()).collect());
4325                    let routed: Vec<_> = order
4326                        .into_iter()
4327                        .filter_map(|gi| Some((gi, idx3(&l.experts[gi])?)))
4328                        .collect();
4329                    let shared = idx3(&l.shared)?;
4330                    let shared_slot = gp.seed_layer(&model, layer_key, shared, &routed)?;
4331                    let remap = gp.remap(layer_key, cfg.n_routed_experts);
4332                    let to_slot: Vec<usize> = remap
4333                        .iter()
4334                        .map(|&s| {
4335                            if s == u32::MAX {
4336                                usize::MAX
4337                            } else {
4338                                s as usize
4339                            }
4340                        })
4341                        .collect();
4342                    let globals: Vec<usize> = remap
4343                        .iter()
4344                        .enumerate()
4345                        .filter_map(|(e, &s)| (s != u32::MAX).then_some(e))
4346                        .collect();
4347                    let (rows, cols) = (l.gate.rows(), l.gate.cols());
4348                    let mut router = vec![0.0f32; rows * cols];
4349                    for r in 0..rows {
4350                        l.gate.row_f32(r, &mut router[r * cols..(r + 1) * cols]);
4351                    }
4352                    return Some(Arc::new(Pack {
4353                        bias: l.gate_bias.clone(),
4354                        mask: None,
4355                        router,
4356                        to_slot,
4357                        remap: remap.clone(),
4358                        globals,
4359                        // Physical tensors live in the global GPU bank map,
4360                        // not in a second layer-local concatenation.
4361                        tensors: Vec::new(),
4362                        dynslots: std::sync::Mutex::new(PackDyn {
4363                            remap,
4364                            owner: Vec::new(),
4365                            last: Vec::new(),
4366                            clock: 0,
4367                            mutated: true,
4368                            seen: vec![0; cfg.n_routed_experts],
4369                        }),
4370                        global: Some(GlobalPack {
4371                            pool: gp,
4372                            layer: layer_key,
4373                            shared_slot,
4374                        }),
4375                    }));
4376                }
4377            }
4378        }
4379        // Pack what fits and leave the rest to the host. The router still
4380        // ranges over every expert; a missing winner is returned as a cold
4381        // pick and completed on the CPU. This is deliberately budget-driven,
4382        // not layer-driven: the same model scales from a small card (more
4383        // partial/host layers) to a large one (all experts resident) without
4384        // a checkpoint-specific cutoff.
4385        // `CMF_DSV4_PACK_MAX=N` caps the packing directly, so a toy can
4386        // reproduce the subset path without needing a card that runs out.
4387        if let Some(n) = std::env::var("CMF_DSV4_PACK_MAX")
4388            .ok()
4389            .and_then(|v| v.parse::<usize>().ok())
4390        {
4391            let mut to_slot = vec![usize::MAX; cfg.n_routed_experts];
4392            let mut globals = Vec::new();
4393            let mut tensors = Vec::new();
4394            for (gi, e) in l.experts.iter().enumerate() {
4395                if route_mask
4396                    .as_deref()
4397                    .is_some_and(|m| m.get(gi).copied().unwrap_or(1) == 0)
4398                {
4399                    continue;
4400                }
4401                if globals.len() >= n {
4402                    break;
4403                }
4404                to_slot[gi] = globals.len();
4405                globals.push(gi);
4406                tensors.push(idx3(e)?);
4407            }
4408            tensors.push(idx3(&l.shared)?);
4409            let (rows, cols) = (l.gate.rows(), l.gate.cols());
4410            let mut router = vec![0.0f32; rows * cols];
4411            for r in 0..rows {
4412                l.gate.row_f32(r, &mut router[r * cols..(r + 1) * cols]);
4413            }
4414            let remap: Vec<u32> = to_slot
4415                .iter()
4416                .map(|&sl| {
4417                    if sl == usize::MAX {
4418                        u32::MAX
4419                    } else {
4420                        sl as u32
4421                    }
4422                })
4423                .collect();
4424            return Some(Arc::new(Pack {
4425                bias: l.gate_bias.clone(),
4426                mask: route_mask.clone(),
4427                router,
4428                to_slot,
4429                dynslots: std::sync::Mutex::new(PackDyn {
4430                    remap: remap.clone(),
4431                    owner: globals.iter().map(|&g| g as u32).collect(),
4432                    last: vec![0; globals.len()],
4433                    clock: 0,
4434                    mutated: false,
4435                    seen: vec![0; cfg.n_routed_experts],
4436                }),
4437                remap,
4438                globals,
4439                tensors,
4440                global: None,
4441            }));
4442        }
4443        let dn_q2_fit = l
4444            .experts
4445            .first()
4446            .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
4447        let room = crate::gpu_wgpu::dsv4_experts_fit(cfg.moe_inter, cfg.dim, gu_q2, dn_q2_fit)
4448            .saturating_sub(1);
4449        // A greedy pack starves the tail: the first layers take the whole
4450        // expert budget and the tail falls off the device chain. Divide the
4451        // room still available by the layers still to pack. This depends on
4452        // format and VRAM geometry, not a card name: a small card gives every
4453        // layer a useful partial pack; when the whole model fits the quotient
4454        // naturally reaches all experts. Hash-routed head layers stay whole
4455        // whenever possible because their checkpoint table names exact rows.
4456        //
4457        // CMF_DSV4_PACK_LAYER_CAP=N remains an override; 0 restores the old
4458        // greedy packing for a performance bisect.
4459        let remaining_layers = l
4460            .experts
4461            .first()
4462            .and_then(|e| e.w1.model_arc())
4463            .map(|m| m.header.arch.num_layers.saturating_sub(li).max(1))
4464            .unwrap_or(1);
4465        let auto_cap = if l.mask.is_some() {
4466            // The mask is already the layer-specific cap. Its total mass was
4467            // counted by dspark_reserve_note before packing, so an additional
4468            // equal-per-layer cap only turns naturally uneven masked layers
4469            // partial and makes batched verification reject every draft.
4470            room
4471        } else if l.tid2eid.is_some() && room >= cfg.n_routed_experts {
4472            cfg.n_routed_experts
4473        } else {
4474            room.div_ceil(remaining_layers).max(1)
4475        };
4476        let room = match std::env::var("CMF_DSV4_PACK_LAYER_CAP")
4477            .ok()
4478            .and_then(|v| v.parse::<usize>().ok())
4479        {
4480            Some(0) => room,
4481            Some(cap) => room.min(cap),
4482            None => room.min(auto_cap),
4483        };
4484        // When the budget packs a SUBSET, which subset matters: a partial
4485        // layer completes its cold picks from the host, so every resident
4486        // expert that the routing actually reaches is host work saved.
4487        // `CMF_DSV4_PACK_FREQ` names a measured tally
4488        // (`CMF_DSV4_TRUNK_PICK_DUMP` wrote it) and reorders the candidates
4489        // hottest-first; layers absent from the tally keep id order. The
4490        // router still ranges over every expert either way — residency
4491        // choice changes speed, never a bit of the answer.
4492        let order =
4493            pack_freq_order(li, l.experts.len()).unwrap_or_else(|| (0..l.experts.len()).collect());
4494        for gi in order {
4495            let e = &l.experts[gi];
4496            if l.mask
4497                .as_deref()
4498                .is_some_and(|m| !m.get(gi).copied().unwrap_or(true))
4499            {
4500                continue;
4501            }
4502            if globals.len() >= room {
4503                break;
4504            }
4505            to_slot[gi] = globals.len();
4506            globals.push(gi);
4507            match idx3(e) {
4508                Some(t) => tensors.push(t),
4509                None => {
4510                    if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
4511                        eprintln!("слой {li}: эксперт {gi} без индексов в каталоге");
4512                    }
4513                    return None;
4514                }
4515            }
4516        }
4517        if globals.is_empty() {
4518            // Two very different causes, and blaming the mask for the other
4519            // one sent a reader looking for a mask that was never set: an
4520            // actual empty mask, or a VRAM budget with no room left for even
4521            // one expert (`room` is 0, which is what a nearly-full card does
4522            // to the last layers).
4523            if room == 0 {
4524                static SAID_ZERO: std::sync::atomic::AtomicBool =
4525                    std::sync::atomic::AtomicBool::new(false);
4526                if !SAID_ZERO.swap(true, std::sync::atomic::Ordering::Relaxed) {
4527                    tracing::warn!(
4528                        "начиная со слоя {li}, в бюджете VRAM не осталось места даже под одного \
4529                         эксперта — остальные веса остаются mmap-backed и читаются по требованию"
4530                    );
4531                }
4532            } else {
4533                tracing::warn!("слой {li}: маска не оставила ни одного эксперта");
4534            }
4535            return None;
4536        }
4537        tensors.push(idx3(&l.shared)?); // shared rides last, as the kernels expect
4538        let (rows, cols) = (l.gate.rows(), l.gate.cols());
4539        let mut router = vec![0.0f32; rows * cols];
4540        for r in 0..rows {
4541            l.gate.row_f32(r, &mut router[r * cols..(r + 1) * cols]);
4542        }
4543        let remap: Vec<u32> = to_slot
4544            .iter()
4545            .map(|&sl| {
4546                if sl == usize::MAX {
4547                    u32::MAX
4548                } else {
4549                    sl as u32
4550                }
4551            })
4552            .collect();
4553        Some(Arc::new(Pack {
4554            bias: l.gate_bias.clone(),
4555            mask: route_mask,
4556            router,
4557            to_slot,
4558            dynslots: std::sync::Mutex::new(PackDyn {
4559                remap: remap.clone(),
4560                owner: globals.iter().map(|&g| g as u32).collect(),
4561                last: vec![0; globals.len()],
4562                clock: 0,
4563                mutated: false,
4564                seen: vec![0; cfg.n_routed_experts],
4565            }),
4566            remap,
4567            globals,
4568            tensors,
4569            global: None,
4570        }))
4571    };
4572    let v = build();
4573    cache.lock().unwrap().insert(key, v.clone());
4574    v
4575}
4576
4577/// The whole MoE block in one submission, experts resident (default on;
4578/// `CMF_DSV4_GPU_MOE2=0` restores the host path). Returns false having
4579/// changed nothing if it cannot — a missing pack, a refused budget — so the
4580/// caller's CPU path stays correct to run. The early divergence this frame
4581/// once carried (0.44 relative, perplexity 5.162 vs 5.211) was the partial
4582/// -capture and hidden-seed defects, fixed since: perplexity gold 4.578 is
4583/// bit-exact against the CPU on every budget from 64 to 96.5 GB.
4584#[cfg(feature = "gpu")]
4585fn moe_frame(
4586    hidden: &[f32],
4587    l: &Dsv4Layer,
4588    cfg: &Dsv4Cfg,
4589    li: usize,
4590    logits: &[f32],
4591    forced: Option<&[usize]>,
4592    pool: Option<&crate::pool::Pool>,
4593    // The state handover: expand always when the device owns the state,
4594    // fold only when there is a next layer.
4595    hc_cur: Option<&crate::gpu_wgpu::Dsv4HcTail>,
4596    hc_next: Option<(&crate::gpu_wgpu::Dsv4HcTail, &[f32])>,
4597    out: &mut [f32],
4598) -> Option<(Vec<f32>, usize)> {
4599    macro_rules! no {
4600        ($($t:tt)*) => {{
4601            if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
4602                eprintln!("кадр MoE отклонён: {}", format_args!($($t)*));
4603            }
4604            return None;
4605        }};
4606    }
4607    let Some(pk) = pack_for(l, cfg, li) else {
4608        no!("слой {li}: упаковка экспертов не построена");
4609    };
4610    // The router is a small f32 tensor and is usually NOT mapped; the handle
4611    // has to come from something that is.
4612    let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
4613        no!("слой {li}: эксперты не отображены из файла");
4614    };
4615    let subset = !pk.route_complete();
4616    let needs_remap = pk.needs_remap();
4617    // Dynamic slots (the FreeToken move): predict this token's winners on
4618    // the host and pull the missing ones into LRU slots BEFORE the frame
4619    // runs — up to CMF_DSV4_FETCH_MAX experts a layer a token. The device
4620    // still routes for real, so a wrong prediction costs one unused fill
4621    // and never a wrong number: an unmapped winner comes back as a cold
4622    // pick and the CPU completes it, exactly as before.
4623    fn fetch_quota() -> usize {
4624        static Q: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
4625        *Q.get_or_init(|| {
4626            std::env::var("CMF_DSV4_FETCH_MAX")
4627                .ok()
4628                .and_then(|v| v.parse().ok())
4629                .unwrap_or_else(|| crate::gpu_wgpu::dsv4_fetch_defaults().0)
4630        })
4631    }
4632    fn fetch_min_seen() -> u16 {
4633        static M: std::sync::OnceLock<u16> = std::sync::OnceLock::new();
4634        *M.get_or_init(|| {
4635            std::env::var("CMF_DSV4_FETCH_MIN_SEEN")
4636                .ok()
4637                .and_then(|v| v.parse().ok())
4638                .unwrap_or_else(|| crate::gpu_wgpu::dsv4_fetch_defaults().1)
4639        })
4640    }
4641    let mut dynv = pk.dynslots.lock().unwrap();
4642    // The winners, from the same logits the device will rank — used by the
4643    // slot refill below AND by the FreeToken-style overlap: the picks that
4644    // will NOT be resident are computed on the CPU while the device frame
4645    // runs, instead of serially after its wait.
4646    let mut pidx = Vec::new();
4647    let mut pwt = Vec::new();
4648    // A chained caller may leave the FFN input only on the device. The
4649    // overlap path intentionally reads it back; when it does, score a HOST
4650    // prediction without changing the device's routing source. The GPU still
4651    // recomputes and ranks its own logits when `logits` is empty, so a rounding
4652    // disagreement can waste an early CPU result but cannot change a token.
4653    let mut predicted_logits = Vec::new();
4654    let prediction_logits: &[f32] = if subset && logits.is_empty() && !hidden.is_empty() {
4655        predicted_logits.resize(cfg.n_routed_experts, 0.0);
4656        l.gate.matvec(hidden, &mut predicted_logits, pool);
4657        &predicted_logits
4658    } else {
4659        logits
4660    };
4661    if subset && !prediction_logits.is_empty() {
4662        route(
4663            prediction_logits,
4664            l.gate_bias.as_deref(),
4665            cfg.top_k,
4666            cfg.route_scale,
4667            forced,
4668            l.mask.as_deref(),
4669            &mut pidx,
4670            &mut pwt,
4671        );
4672    }
4673    let global_remap = pk.global.as_ref().map(|gl| {
4674        gl.pool.ensure_picks(
4675            &model, gl.layer, &pidx, &l.experts,
4676            // A global demand cache has empty lines during warmup and each
4677            // CPU cold completion costs far more than one expert upload.
4678            // Materialise every predicted winner immediately, FreeToken
4679            // style; device routing still returns any prediction drift as an
4680            // exact cold pick.
4681            cfg.top_k, 1,
4682        )
4683    });
4684    if subset && fetch_quota() > 0 && !pidx.is_empty() && !dynv.owner.is_empty() {
4685        dynv.clock += 1;
4686        let clock = dynv.clock;
4687        if clock % 64 == 0 {
4688            for v in dynv.seen.iter_mut() {
4689                *v >>= 1;
4690            }
4691        }
4692        for &pick in &pidx {
4693            dynv.seen[pick] = dynv.seen[pick].saturating_add(1);
4694            let sl = dynv.remap[pick];
4695            if sl != u32::MAX {
4696                dynv.last[sl as usize] = clock;
4697            }
4698        }
4699        let mut fetched = 0usize;
4700        for &pick in &pidx {
4701            if fetched >= fetch_quota() {
4702                break;
4703            }
4704            if dynv.remap[pick] != u32::MAX {
4705                continue;
4706            }
4707            if dynv.seen[pick] < fetch_min_seen() {
4708                continue; // one-shot so far: the CPU reads it at the shelf
4709            }
4710            // Victim: the LRU slot among those this token does not need.
4711            let victim = (0..dynv.owner.len())
4712                .filter(|&sl| dynv.last[sl] != clock)
4713                .min_by_key(|&sl| dynv.last[sl]);
4714            let Some(victim) = victim else { break };
4715            let Some(exp) = l.experts.get(pick) else {
4716                continue;
4717            };
4718            let t3 = (|| {
4719                Some((
4720                    exp.w1.model_idx()?,
4721                    exp.w3.model_idx()?,
4722                    exp.w2.model_idx()?,
4723                ))
4724            })();
4725            let Some(t3) = t3 else { continue };
4726            let gu_q2 = exp.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP);
4727            let pack_first = pk.tensors.first().map(|t| t.0).unwrap_or(usize::MAX);
4728            if !crate::gpu_wgpu::dsv4_slot_fill(
4729                &model,
4730                pack_first,
4731                victim,
4732                pick,
4733                t3,
4734                cfg.moe_inter,
4735                cfg.dim,
4736                gu_q2,
4737            ) {
4738                break;
4739            }
4740            let old = dynv.owner[victim] as usize;
4741            if old < dynv.remap.len() {
4742                dynv.remap[old] = u32::MAX;
4743            }
4744            dynv.remap[pick] = victim as u32;
4745            dynv.owner[victim] = pick as u32;
4746            dynv.last[victim] = clock;
4747            dynv.mutated = true;
4748            fetched += 1;
4749        }
4750    }
4751    // With a complete pack the forced row is translated to packed numbering.
4752    // With a subset it stays global: the router's remap either finds its slot
4753    // or returns the forced expert as a cold pick, exactly like a scored one.
4754    let fpack: Option<Vec<usize>> = match forced {
4755        Some(f) if needs_remap => Some(f.to_vec()),
4756        Some(f) => {
4757            let v: Vec<usize> = f.iter().map(|&g| pk.to_slot[g]).collect();
4758            if v.contains(&usize::MAX) {
4759                no!("слой {li}: хеш-слой называет эксперта вне упаковки");
4760            }
4761            Some(v)
4762        }
4763        None => None,
4764    };
4765    // Routing ranges over EVERY expert; the remap turns a winner into a slot
4766    // or marks it cold. Nothing is masked, so nothing is lost.
4767    // Empty logits are the device-scored case: the frame computes them from
4768    // pk.router, whose rows are already in global order, so there is nothing
4769    // to reorder — and indexing an empty slice is how this line greeted the
4770    // first engaged run.
4771    let lg: Vec<f32> = if logits.is_empty() || needs_remap {
4772        logits.to_vec()
4773    } else {
4774        pk.globals.iter().map(|&g| logits[g]).collect()
4775    };
4776    let live_remap = global_remap.as_deref().unwrap_or(dynv.remap.as_slice());
4777    let w = crate::gpu_wgpu::Dsv4MoeW {
4778        router: &pk.router,
4779        experts: &pk.tensors,
4780        logits: &lg,
4781        bias: pk.bias.as_deref(),
4782        mask: pk.mask.as_deref(),
4783        forced: fpack.as_deref(),
4784        remap: needs_remap.then_some(live_remap),
4785        global: pk.global.as_ref().map(|gl| crate::gpu_wgpu::Dsv4GlobalMoe {
4786            pool_uid: gl.pool.uid,
4787            shared_slot: gl.shared_slot,
4788            segment_slots: gl.pool.segment_slots as u32,
4789        }),
4790        has_shared: true,
4791        shared_weight: 1.0,
4792        preweighted: false,
4793        qwen_softmax: false,
4794    };
4795    let g = crate::gpu_wgpu::Dsv4MoeGeom {
4796        hidden: cfg.dim,
4797        inter: cfg.moe_inter,
4798        top_k: cfg.top_k,
4799        route_scale: cfg.route_scale,
4800        swiglu_limit: cfg.swiglu_limit,
4801        gu_q2: l
4802            .experts
4803            .first()
4804            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
4805    };
4806    let mut cold = Vec::new();
4807    let mut cold_x = Vec::new();
4808    // The FreeToken overlap: the predicted winners that will NOT be
4809    // resident are computed on the CPU WHILE the device frame runs,
4810    // instead of serially after its wait. Unweighted (weight 1) — the
4811    // device's own cold weights scale the result at the merge, so a
4812    // routing drift between the host's ranking and the card's costs one
4813    // wasted thread, never a wrong number. Only the per-layer path has
4814    // the input on the host (`hidden` non-empty); the chain keeps its
4815    // own economy.
4816    let overlap: Vec<usize> = if !hidden.is_empty() {
4817        pidx.iter()
4818            .copied()
4819            .filter(|&pick| live_remap.get(pick).copied().unwrap_or(u32::MAX) == u32::MAX)
4820            .collect()
4821    } else {
4822        Vec::new()
4823    };
4824    let mut early: std::collections::HashMap<usize, Vec<f32>> = std::collections::HashMap::new();
4825    let frame_ok = std::thread::scope(|sc| {
4826        let handles: Vec<_> = overlap
4827            .iter()
4828            .filter_map(|&gi| l.experts.get(gi).map(|exp| (gi, exp)))
4829            .map(|(gi, exp)| {
4830                sc.spawn(move || {
4831                    let mut a = vec![0.0f32; cfg.dim];
4832                    crate::gpu::cpu_scope(|| run_expert(hidden, exp, cfg, 1.0, None, &mut a));
4833                    (gi, a)
4834                })
4835            })
4836            .collect();
4837        let ok = crate::gpu_wgpu::dsv4_moe_frame(
4838            &model,
4839            &w,
4840            g,
4841            hidden,
4842            &mut cold,
4843            &mut cold_x,
4844            hc_cur,
4845            hc_next,
4846            out,
4847        );
4848        for h in handles {
4849            if let Ok((gi, a)) = h.join() {
4850                early.insert(gi, a);
4851            }
4852        }
4853        ok
4854    });
4855    if !frame_ok {
4856        return None;
4857    }
4858    // The picks the card had no room for, finished here and added in. Their
4859    // weights already carry the top-k normalisation the device applied.
4860    if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
4861        let csum: f32 = cold.iter().map(|c| c.1).sum();
4862        eprintln!(
4863            "[холодные] слой {li}: вернулось {} из {} | сумма холодных {csum:.4} | \
4864             route_scale {:.4} | {:?}",
4865            cold.len(),
4866            cfg.top_k,
4867            cfg.route_scale,
4868            &cold[..cold.len().min(3)]
4869        );
4870    }
4871    let mut acc = vec![0.0f32; cfg.dim];
4872    let mut cold_sum = vec![0.0f32; cfg.dim];
4873    let cold_input = if hidden.is_empty() {
4874        cold_x.as_slice()
4875    } else {
4876        hidden
4877    };
4878    // Cold means out-of-core by contract. The tensors remain mmap-backed:
4879    // missing pages are faulted from the CMF file and the OS may evict
4880    // them again under RAM pressure. Do not let the generic matvec probe
4881    // turn this into an unbounded second GPU cache behind the packer's
4882    // back.
4883    //
4884    // The unit of parallelism is the EXPERT, not the row: a 2048-row
4885    // matvec split across 380 workers is five rows per worker — all
4886    // dispatch, no arithmetic. One worker per cold expert, whole matvecs
4887    // inside (inner pool None), was the difference between ~7 ms and ~1 ms
4888    // per cold expert on the 384-core stand. cpu_scope is thread-local, so
4889    // it sits INSIDE the worker closure.
4890    if !early.is_empty() {
4891        // The overlap already computed (most of) the cold picks; scale by
4892        // the DEVICE's weight and add in cold order — the same order the
4893        // serial path used, so parity holds. A cold pick the prediction
4894        // missed (ranking drift) is computed inline, cpu_scope'd.
4895        for &(gi, wt) in &cold {
4896            if let Some(a) = early.get(&gi) {
4897                for ((o, sum), v) in out.iter_mut().zip(&mut cold_sum).zip(a.iter()) {
4898                    *o += v * wt;
4899                    *sum += v * wt;
4900                }
4901                continue;
4902            }
4903            let Some(exp) = l.experts.get(gi) else {
4904                continue;
4905            };
4906            crate::gpu::cpu_scope(|| run_expert(cold_input, exp, cfg, wt, pool, &mut acc));
4907            for ((o, sum), a) in out.iter_mut().zip(&mut cold_sum).zip(&acc) {
4908                *o += a;
4909                *sum += a;
4910            }
4911        }
4912        return Some((cold_sum, cold.len()));
4913    }
4914    match pool {
4915        Some(p) if cold.len() > 1 => {
4916            let results: Vec<std::sync::Mutex<Vec<f32>>> = cold
4917                .iter()
4918                .map(|_| std::sync::Mutex::new(Vec::new()))
4919                .collect();
4920            let (cold_ref, results_ref) = (&cold, &results);
4921            p.run_rows(cold.len(), &move |cs, ce| {
4922                for i in cs..ce {
4923                    let (gi, wt) = cold_ref[i];
4924                    let Some(exp) = l.experts.get(gi) else {
4925                        continue;
4926                    };
4927                    let mut a = vec![0.0f32; cfg.dim];
4928                    crate::gpu::cpu_scope(|| run_expert(cold_input, exp, cfg, wt, None, &mut a));
4929                    *results_ref[i].lock().unwrap() = a;
4930                }
4931            });
4932            // Serial reduce in cold order — the accumulation order the
4933            // scalar path had, so parity holds bit for bit.
4934            for r in &results {
4935                let a = r.lock().unwrap();
4936                if a.is_empty() {
4937                    continue;
4938                }
4939                for ((o, sum), v) in out.iter_mut().zip(&mut cold_sum).zip(a.iter()) {
4940                    *o += v;
4941                    *sum += v;
4942                }
4943            }
4944        }
4945        _ => {
4946            for &(gi, wt) in &cold {
4947                let Some(exp) = l.experts.get(gi) else {
4948                    continue;
4949                };
4950                crate::gpu::cpu_scope(|| run_expert(cold_input, exp, cfg, wt, pool, &mut acc));
4951                for ((o, sum), a) in out.iter_mut().zip(&mut cold_sum).zip(&acc) {
4952                    *o += a;
4953                    *sum += a;
4954                }
4955            }
4956        }
4957    }
4958    Some((cold_sum, cold.len()))
4959}
4960
4961/// How much of each layer's compressed cache already sits on the card. ONE
4962/// map: a reader and a writer with a `static` each are two maps, and the
4963/// reader would never see a thing the writer put down.
4964/// The reallocation counter as of the last successful tail write. Any change
4965/// means some buffer was rebuilt and every tail count is stale.
4966#[cfg(feature = "gpu")]
4967fn last_grew(now: u64) -> u64 {
4968    use std::sync::atomic::{AtomicU64, Ordering};
4969    static SEEN: AtomicU64 = AtomicU64::new(0);
4970    let was = SEEN.load(Ordering::Relaxed);
4971    if was != now {
4972        SEEN.store(now, Ordering::Relaxed);
4973        compressed_map().lock().unwrap().clear();
4974        return u64::MAX; // force a full write this round
4975    }
4976    now
4977}
4978
4979#[cfg(feature = "gpu")]
4980fn compressed_map() -> &'static std::sync::Mutex<std::collections::HashMap<(u64, usize), usize>> {
4981    use std::collections::HashMap;
4982    use std::sync::{Mutex, OnceLock};
4983    static W: OnceLock<Mutex<HashMap<(u64, usize), usize>>> = OnceLock::new();
4984    W.get_or_init(|| Mutex::new(HashMap::new()))
4985}
4986
4987#[cfg(feature = "gpu")]
4988fn compressed_written(kv_id: u64, li: usize) -> usize {
4989    compressed_map()
4990        .lock()
4991        .unwrap()
4992        .get(&(kv_id, li))
4993        .copied()
4994        .unwrap_or(0)
4995}
4996
4997/// Reset to zero whenever a write fails or the buffer grows — a grown buffer
4998/// keeps none of its contents.
4999#[cfg(feature = "gpu")]
5000fn note_compressed(kv_id: u64, li: usize, n: usize) {
5001    compressed_map().lock().unwrap().insert((kv_id, li), n);
5002}
5003
5004#[cfg(feature = "gpu")]
5005fn gpu_moe2_enabled() -> bool {
5006    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5007    *ON.get_or_init(|| {
5008        std::env::var("CMF_DSV4_GPU_MOE2")
5009            .map(|v| v != "0")
5010            .unwrap_or(true)
5011            && crate::gpu::backend_available()
5012    })
5013}
5014
5015pub fn moe_step(
5016    hidden: &[f32],
5017    l: &Dsv4Layer,
5018    cfg: &Dsv4Cfg,
5019    token_id: u32,
5020    // Layer index — only used to bucket routing statistics.
5021    li: usize,
5022    pool: Option<&crate::pool::Pool>,
5023    out: &mut [f32],
5024) {
5025    let _t0 = prof::on().then(std::time::Instant::now);
5026    let _guard = scopeguard_moe(_t0, li);
5027    let mut logits = vec![0.0f32; cfg.n_routed_experts];
5028    l.gate.matvec(hidden, &mut logits, pool);
5029    let (mut idx, mut w) = (Vec::new(), Vec::new());
5030    route(
5031        &logits,
5032        l.gate_bias.as_deref(),
5033        cfg.top_k,
5034        cfg.route_scale,
5035        l.tid2eid
5036            .as_ref()
5037            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id))
5038            .as_deref(),
5039        l.mask.as_deref(),
5040        &mut idx,
5041        &mut w,
5042    );
5043    if route_stats_on() {
5044        let routed: Vec<(usize, f32)> = idx.iter().copied().zip(w.iter().copied()).collect();
5045        record_route(li, 0, cfg.n_routed_experts, &routed);
5046    }
5047    // Same trace the generic MoE path writes (`CMF_MOE_TRACE`): one
5048    // `layer:e1,e2,…` line per routed token. The first arena run on this
5049    // architecture measured a 4.5% hit rate — random level for the arena's
5050    // size — and only a per-token trace can say whether that is the
5051    // router's true entropy or the cache structure destroying locality.
5052    crate::pipeline::moe_trace_at(li as i32, &idx);
5053    // The whole block on the device, in one submission, or nothing. Routing
5054    // happens there too — the logits above are what it starts from, so the
5055    // CPU's own choice is discarded rather than second-guessed.
5056    #[cfg(feature = "gpu")]
5057    if gpu_moe2_enabled() && crate::gpu::enabled_here() {
5058        let forced = l
5059            .tid2eid
5060            .as_ref()
5061            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
5062        if moe_frame(
5063            hidden,
5064            l,
5065            cfg,
5066            li,
5067            &logits,
5068            forced.as_deref(),
5069            pool,
5070            None,
5071            None,
5072            out,
5073        )
5074        .is_some()
5075        {
5076            // CMF_DSV4_MOE_CHECK=1 recomputes the same block on the CPU and
5077            // reports where they part. A wrong MoE does not fail — it answers
5078            // differently — and the toy agreed bit for bit while the release
5079            // did not, so the difference lives in something the toy has no
5080            // instance of. Only a per-layer number will say which.
5081            if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
5082                let mut want = vec![0.0f32; out.len()];
5083                let mut acc = vec![0.0f32; cfg.dim];
5084                // This is a CPU oracle. Without cpu_scope the generic
5085                // quantized matvec probe uploaded a second copy of every
5086                // selected expert, so the diagnostic itself exhausted VRAM
5087                // after otherwise-correct global-pool layers.
5088                crate::gpu::cpu_scope(|| {
5089                    for (e, &ei) in idx.iter().enumerate() {
5090                        let Some(exp) = l.experts.get(ei) else {
5091                            continue;
5092                        };
5093                        run_expert(
5094                            hidden,
5095                            exp,
5096                            cfg,
5097                            w.get(e).copied().unwrap_or(0.0),
5098                            pool,
5099                            &mut acc,
5100                        );
5101                        for (o, a) in want.iter_mut().zip(&acc) {
5102                            *o += a;
5103                        }
5104                    }
5105                    run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
5106                    for (o, a) in want.iter_mut().zip(&acc) {
5107                        *o += a;
5108                    }
5109                });
5110                let num: f32 = want
5111                    .iter()
5112                    .zip(out.iter())
5113                    .map(|(a, b)| (a - b) * (a - b))
5114                    .sum();
5115                let den: f32 = want.iter().map(|a| a * a).sum::<f32>().max(1e-20);
5116                let rel = (num / den).sqrt();
5117                if rel > 1e-3 {
5118                    let packed = pack_for(l, cfg, li).map_or(0, |p| p.globals.len());
5119                    eprintln!(
5120                        "[кадр MoE] слой {li}: расхождение {rel:.3e} | выбрано {} | \
5121                         упаковано {packed} из {} | хеш={} | смещение={}",
5122                        idx.len(),
5123                        cfg.n_routed_experts,
5124                        l.tid2eid.is_some(),
5125                        l.gate_bias.is_some()
5126                    );
5127                }
5128            }
5129            return;
5130        }
5131    }
5132    // Cheap tally for the batching question: how many DISTINCT experts a
5133    // group of tokens reaches. If five tokens want thirty different experts,
5134    // a batched MoE reads thirty weights and amortises nothing — which is
5135    // the difference between a speculative verify that pays for itself and
5136    // one that does not. Disarmed it costs one thread-local read.
5137    PICK_TALLY.with(|t| {
5138        if let Some(v) = t.borrow_mut().as_mut() {
5139            v.push((li, idx.to_vec()));
5140        }
5141    });
5142    if dump_path().is_some() {
5143        PICKED.with(|p| {
5144            let mut p = p.borrow_mut();
5145            if p.len() <= li {
5146                p.resize(li + 1, Vec::new());
5147            }
5148            p[li] = idx.clone();
5149        });
5150    }
5151    // One submission for the whole block — the chosen experts plus the
5152    // shared one. Per-expert dispatches are what made MoE slow elsewhere,
5153    // and the device keeps the weights across tokens, so the cost is the
5154    // arithmetic rather than the traffic. A refusal (missing kernel, mixed
5155    // layouts, weights that do not fit the budget) falls to the CPU whole,
5156    // never half.
5157    // CORRECT but SLOWER, so off by default. Parity holds on real weights
5158    // (perplexity 6.808 → 6.839 at 64 tokens, 5.102 → 5.146 at 200), and an
5159    // honest alternating A/B says 1.0 tok/s against the CPU's 2.2. The first
5160    // measurement claimed the opposite — 0.7 → 2.0 — because the CPU arm ran
5161    // first and paged in 158 GB for the GPU arm to inherit.
5162    //
5163    // The cost is not arithmetic, it is round trips: this submits and reads
5164    // back once per layer, forty-three times a token, and a discrete card
5165    // charges milliseconds for each. Fixing it means one submission per
5166    // token — the whole-token graph — not a faster kernel.
5167    //
5168    // `CMF_DSV4_GPU_MOE=1` opts in.
5169    fn gpu_moe_on() -> bool {
5170        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5171        *ON.get_or_init(|| std::env::var("CMF_DSV4_GPU_MOE").is_ok_and(|v| v != "0"))
5172    }
5173    if gpu_moe_on() && crate::gpu::enabled_here() {
5174        let mut jobs = Vec::with_capacity(idx.len() + 1);
5175        let mut model_ref = None;
5176        let mut ok = true;
5177        for (e, &ei) in idx.iter().enumerate() {
5178            let Some(exp) = l.experts.get(ei) else {
5179                continue;
5180            };
5181            ok &= crate::pipeline::moe_push_job_parts(
5182                &exp.w1,
5183                &exp.w3,
5184                &exp.w2,
5185                hidden,
5186                w.get(e).copied().unwrap_or(0.0),
5187                cfg.swiglu_limit,
5188                &mut jobs,
5189                &mut model_ref,
5190            )
5191            .is_some();
5192        }
5193        ok &= crate::pipeline::moe_push_job_parts(
5194            &l.shared.w1,
5195            &l.shared.w3,
5196            &l.shared.w2,
5197            hidden,
5198            1.0,
5199            cfg.swiglu_limit,
5200            &mut jobs,
5201            &mut model_ref,
5202        )
5203        .is_some();
5204        if ok {
5205            if let Some(m) = model_ref.as_ref() {
5206                if crate::gpu::moe_block(m, &jobs, out) {
5207                    // CMF_DSV4_GPU_CHECK=1 recomputes the same block on the
5208                    // CPU and reports the divergence. A GPU MoE that is wrong
5209                    // does not fail — it answers differently — so the only way
5210                    // to know is to ask both.
5211                    if std::env::var("CMF_DSV4_GPU_CHECK").is_ok() {
5212                        let mut want = vec![0.0f32; out.len()];
5213                        let mut acc = vec![0.0f32; cfg.dim];
5214                        for (e, &ei) in idx.iter().enumerate() {
5215                            let Some(exp) = l.experts.get(ei) else {
5216                                continue;
5217                            };
5218                            run_expert(
5219                                hidden,
5220                                exp,
5221                                cfg,
5222                                w.get(e).copied().unwrap_or(0.0),
5223                                pool,
5224                                &mut acc,
5225                            );
5226                            for (o, a) in want.iter_mut().zip(&acc) {
5227                                *o += a;
5228                            }
5229                        }
5230                        run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
5231                        for (o, a) in want.iter_mut().zip(&acc) {
5232                            *o += a;
5233                        }
5234                        let num: f32 = want
5235                            .iter()
5236                            .zip(out.iter())
5237                            .map(|(a, b)| (a - b) * (a - b))
5238                            .sum();
5239                        let den: f32 = want.iter().map(|a| a * a).sum::<f32>().max(1e-20);
5240                        eprintln!(
5241                            "[dsv4-gpu] слой {li}: расхождение {:.3e} | |CPU|={:.5} |GPU|={:.5} | экспертов {}",
5242                            (num / den).sqrt(),
5243                            den.sqrt(),
5244                            out.iter().map(|x| x * x).sum::<f32>().sqrt(),
5245                            jobs.len()
5246                        );
5247                    }
5248                    return;
5249                }
5250            }
5251        }
5252    }
5253    out.fill(0.0);
5254    // Layers the packer had no room for land here whole. Same shape as the
5255    // frame's cold completion: one worker per expert (the shared one rides
5256    // as an extra job), whole matvecs inside, cpu_scope INSIDE the worker —
5257    // on the main thread it would gate nothing, and the generic matvec
5258    // would upload every expert to the card tensor by tensor, which is
5259    // exactly the per-token PCIe churn this path exists to avoid.
5260    let jobs: Vec<(Option<usize>, f32)> = idx
5261        .iter()
5262        .enumerate()
5263        .map(|(e, &ei)| (Some(ei), w.get(e).copied().unwrap_or(0.0)))
5264        .chain(std::iter::once((None, 1.0)))
5265        .collect();
5266    match pool {
5267        Some(p) if jobs.len() > 1 => {
5268            let results: Vec<std::sync::Mutex<Vec<f32>>> = jobs
5269                .iter()
5270                .map(|_| std::sync::Mutex::new(Vec::new()))
5271                .collect();
5272            let (jobs_ref, results_ref) = (&jobs, &results);
5273            p.run_rows(jobs.len(), &move |cs, ce| {
5274                for i in cs..ce {
5275                    let (ei, wt) = jobs_ref[i];
5276                    let exp = match ei {
5277                        Some(ei) => match l.experts.get(ei) {
5278                            Some(x) => x,
5279                            None => continue,
5280                        },
5281                        None => &l.shared,
5282                    };
5283                    let mut a = vec![0.0f32; cfg.dim];
5284                    crate::gpu::cpu_scope(|| run_expert(hidden, exp, cfg, wt, None, &mut a));
5285                    *results_ref[i].lock().unwrap() = a;
5286                }
5287            });
5288            for r in &results {
5289                let a = r.lock().unwrap();
5290                for (o, v) in out.iter_mut().zip(a.iter()) {
5291                    *o += v;
5292                }
5293            }
5294        }
5295        _ => {
5296            let mut acc = vec![0.0f32; cfg.dim];
5297            for &(ei, wt) in &jobs {
5298                let exp = match ei {
5299                    Some(ei) => match l.experts.get(ei) {
5300                        Some(x) => x,
5301                        None => continue,
5302                    },
5303                    None => &l.shared,
5304                };
5305                crate::gpu::cpu_scope(|| run_expert(hidden, exp, cfg, wt, pool, &mut acc));
5306                for (o, a) in out.iter_mut().zip(&acc) {
5307                    *o += a;
5308                }
5309            }
5310        }
5311    }
5312}
5313
5314/// The routed and shared experts both come through here, so the clamp and
5315/// the weight folding have exactly one implementation — `expert_swiglu`.
5316fn run_expert(
5317    x: &[f32],
5318    e: &Dsv4Expert,
5319    cfg: &Dsv4Cfg,
5320    weight: f32,
5321    pool: Option<&crate::pool::Pool>,
5322    out: &mut [f32],
5323) {
5324    expert_swiglu(
5325        x,
5326        &|src, dst| e.w1.matvec(src, dst, pool),
5327        &|src, dst| e.w3.matvec(src, dst, pool),
5328        &|src, dst| e.w2.matvec(src, dst, pool),
5329        cfg.moe_inter,
5330        weight,
5331        cfg.swiglu_limit,
5332        out,
5333    );
5334}
5335
5336/// The same expert computation for several inputs, streaming each selected
5337/// weight once. Used by DSpark's trained five-position block: running five
5338/// ordinary `moe_step`s rereads the shared expert five times and every
5339/// coincident routed expert once per position.
5340fn moe_step_block(
5341    xs: &[f32],
5342    b: usize,
5343    l: &Dsv4Layer,
5344    cfg: &Dsv4Cfg,
5345    token_ids: &[u32],
5346    tally_layer: usize,
5347    pool: Option<&crate::pool::Pool>,
5348    out: &mut [f32],
5349) {
5350    let (dim, inter) = (cfg.dim, cfg.moe_inter);
5351    debug_assert_eq!(xs.len(), b * dim);
5352    debug_assert_eq!(out.len(), b * dim);
5353    out.fill(0.0);
5354
5355    let mut logits = vec![0.0f32; b * cfg.n_routed_experts];
5356    l.gate.matmat(xs, b, &mut logits, pool);
5357    let mut picks: Vec<Vec<usize>> = Vec::with_capacity(b);
5358    let mut weights: Vec<Vec<f32>> = Vec::with_capacity(b);
5359    for bi in 0..b {
5360        let mut idx = Vec::new();
5361        let mut wt = Vec::new();
5362        let forced = l.tid2eid.as_ref().map(|tbl| {
5363            hash_route(
5364                tbl,
5365                cfg.vocab,
5366                cfg.top_k,
5367                token_ids.get(bi).copied().unwrap_or(0),
5368            )
5369        });
5370        route(
5371            &logits[bi * cfg.n_routed_experts..(bi + 1) * cfg.n_routed_experts],
5372            l.gate_bias.as_deref(),
5373            cfg.top_k,
5374            cfg.route_scale,
5375            forced.as_deref(),
5376            l.mask.as_deref(),
5377            &mut idx,
5378            &mut wt,
5379        );
5380        PICK_TALLY.with(|t| {
5381            if let Some(v) = t.borrow_mut().as_mut() {
5382                v.push((tally_layer, idx.clone()));
5383            }
5384        });
5385        picks.push(idx);
5386        weights.push(wt);
5387    }
5388
5389    // Preserve the scalar path's accumulation order by keeping every routed
5390    // slot separate; grouping below changes only when a weight is read.
5391    let mut routed = vec![0.0f32; b * cfg.top_k * dim];
5392    // Group the token slots by expert first — the list is also the unit of
5393    // parallelism below.
5394    let mut active: Vec<(usize, Vec<(usize, usize, f32)>)> = Vec::new();
5395    for ei in 0..l.experts.len() {
5396        let mut jobs = Vec::new();
5397        for bi in 0..b {
5398            for (slot, &picked) in picks[bi].iter().enumerate() {
5399                if picked == ei {
5400                    jobs.push((bi, slot, weights[bi][slot]));
5401                }
5402            }
5403        }
5404        if !jobs.is_empty() {
5405            active.push((ei, jobs));
5406        }
5407    }
5408    // One expert's forward, single-threaded, returning the scaled down
5409    // projections in job order.
5410    let expert_fwd =
5411        |ei: usize, jobs: &[(usize, usize, f32)], inner: Option<&crate::pool::Pool>| -> Vec<f32> {
5412            let e = &l.experts[ei];
5413            let n = jobs.len();
5414            let mut xj = vec![0.0f32; n * dim];
5415            for (j, &(bi, _, _)) in jobs.iter().enumerate() {
5416                xj[j * dim..(j + 1) * dim].copy_from_slice(&xs[bi * dim..(bi + 1) * dim]);
5417            }
5418            let mut gate = vec![0.0f32; n * inter];
5419            let mut up = vec![0.0f32; n * inter];
5420            e.w1.matmat(&xj, n, &mut gate, inner);
5421            e.w3.matmat(&xj, n, &mut up, inner);
5422            for (j, &(_, _, wt)) in jobs.iter().enumerate() {
5423                let (gj, uj) = (
5424                    &mut gate[j * inter..(j + 1) * inter],
5425                    &mut up[j * inter..(j + 1) * inter],
5426                );
5427                if cfg.swiglu_limit > 0.0 {
5428                    for u in uj.iter_mut() {
5429                        *u = u.clamp(-cfg.swiglu_limit, cfg.swiglu_limit);
5430                    }
5431                    for g in gj.iter_mut() {
5432                        *g = g.min(cfg.swiglu_limit);
5433                    }
5434                }
5435                for (g, &u) in gj.iter_mut().zip(uj.iter()) {
5436                    *g = (*g / (1.0 + (-*g).exp())) * u * wt;
5437                }
5438            }
5439            let mut down = vec![0.0f32; n * dim];
5440            e.w2.matmat(&gate, n, &mut down, inner);
5441            down
5442        };
5443    // ~370 non-resident experts a token used to run this loop ONE AFTER
5444    // ANOTHER: a 2048-row matvec cannot occupy a big pool, and the loop
5445    // serialised the only real parallelism there is — across experts.
5446    // Measured on a 384-core host with DeepSeek-V4-Flash: ~3.3 s/token
5447    // flat across every fetch-side improvement, because the wall was
5448    // here. Parallel across experts, each single-threaded and pinned to
5449    // the CPU on ITS OWN worker (cpu_scope is thread-local, so it must
5450    // be entered inside the closure, not around the pool call — the
5451    // documented trap).
5452    match pool {
5453        Some(p) if active.len() > 1 => {
5454            let results: Vec<std::sync::Mutex<Vec<f32>>> = active
5455                .iter()
5456                .map(|_| std::sync::Mutex::new(Vec::new()))
5457                .collect();
5458            let active_ref = &active;
5459            let results_ref = &results;
5460            let fwd = &expert_fwd;
5461            p.run_rows(active_ref.len(), &move |s, e| {
5462                for i in s..e {
5463                    let (ei, jobs) = &active_ref[i];
5464                    let d = crate::gpu::cpu_scope(|| fwd(*ei, jobs, None));
5465                    *results_ref[i].lock().unwrap() = d;
5466                }
5467            });
5468            for (i, (_, jobs)) in active.iter().enumerate() {
5469                let down = results[i].lock().unwrap();
5470                for (j, &(bi, slot, _)) in jobs.iter().enumerate() {
5471                    routed[(bi * cfg.top_k + slot) * dim..(bi * cfg.top_k + slot + 1) * dim]
5472                        .copy_from_slice(&down[j * dim..(j + 1) * dim]);
5473                }
5474            }
5475        }
5476        _ => {
5477            for (ei, jobs) in &active {
5478                let down = expert_fwd(*ei, jobs, pool);
5479                for (j, &(bi, slot, _)) in jobs.iter().enumerate() {
5480                    routed[(bi * cfg.top_k + slot) * dim..(bi * cfg.top_k + slot + 1) * dim]
5481                        .copy_from_slice(&down[j * dim..(j + 1) * dim]);
5482                }
5483            }
5484        }
5485    }
5486
5487    // Shared expert: all positions always use it, so this is the highest
5488    // certainty weight-sharing win in the block.
5489    let mut sg = vec![0.0f32; b * inter];
5490    let mut su = vec![0.0f32; b * inter];
5491    l.shared.w1.matmat(xs, b, &mut sg, pool);
5492    l.shared.w3.matmat(xs, b, &mut su, pool);
5493    for bi in 0..b {
5494        let (gj, uj) = (
5495            &mut sg[bi * inter..(bi + 1) * inter],
5496            &mut su[bi * inter..(bi + 1) * inter],
5497        );
5498        if cfg.swiglu_limit > 0.0 {
5499            for u in uj.iter_mut() {
5500                *u = u.clamp(-cfg.swiglu_limit, cfg.swiglu_limit);
5501            }
5502            for g in gj.iter_mut() {
5503                *g = g.min(cfg.swiglu_limit);
5504            }
5505        }
5506        for (g, &u) in gj.iter_mut().zip(uj.iter()) {
5507            *g = (*g / (1.0 + (-*g).exp())) * u;
5508        }
5509    }
5510    let mut shared = vec![0.0f32; b * dim];
5511    l.shared.w2.matmat(&sg, b, &mut shared, pool);
5512
5513    for bi in 0..b {
5514        let dst = &mut out[bi * dim..(bi + 1) * dim];
5515        for slot in 0..picks[bi].len() {
5516            let src = &routed[(bi * cfg.top_k + slot) * dim..(bi * cfg.top_k + slot + 1) * dim];
5517            for (o, &v) in dst.iter_mut().zip(src) {
5518                *o += v;
5519            }
5520        }
5521        for (o, &v) in dst.iter_mut().zip(&shared[bi * dim..(bi + 1) * dim]) {
5522            *o += v;
5523        }
5524    }
5525}
5526
5527/// Complete only the routed experts absent from a partial GPU pack.  Jobs
5528/// are grouped by expert so coincident speculative positions stream that
5529/// expert's weights once; per-token slot accumulation remains in route order,
5530/// matching the ordinary exact cold-correction path.
5531fn cold_step_block(
5532    xs: &[f32],
5533    b: usize,
5534    l: &Dsv4Layer,
5535    cfg: &Dsv4Cfg,
5536    cold: &[Vec<(usize, f32)>],
5537    pool: Option<&crate::pool::Pool>,
5538    out: &mut [f32],
5539) {
5540    let (dim, inter) = (cfg.dim, cfg.moe_inter);
5541    debug_assert_eq!(xs.len(), b * dim);
5542    debug_assert_eq!(cold.len(), b);
5543    debug_assert_eq!(out.len(), b * dim);
5544    out.fill(0.0);
5545    let slots = cold.iter().map(Vec::len).max().unwrap_or(0);
5546    if slots == 0 {
5547        return;
5548    }
5549    // The ordinary partial walk evaluates every cold winner with matvec.
5550    // The grouped arm streams a coincident expert once for the whole verify
5551    // block; release-scale fingerprints and row-zero logits were identical,
5552    // and it moved the fixed A40 bench 1.9 → 2.3 tok/s.  Keep the scalar arm
5553    // as a parity escape hatch for a new quant layout/adapter.
5554    let grouped = std::env::var("CMF_DSV4_COLD_MATMAT")
5555        .map(|v| v != "0")
5556        .unwrap_or(true);
5557    if !grouped {
5558        let jobs: Vec<(usize, usize, usize, f32)> = cold
5559            .iter()
5560            .enumerate()
5561            .flat_map(|(bi, row)| {
5562                row.iter()
5563                    .enumerate()
5564                    .map(move |(slot, &(ei, wt))| (bi, slot, ei, wt))
5565            })
5566            .collect();
5567        let results: Vec<std::sync::Mutex<Vec<f32>>> = jobs
5568            .iter()
5569            .map(|_| std::sync::Mutex::new(Vec::new()))
5570            .collect();
5571        match pool {
5572            Some(p) if jobs.len() > 1 => {
5573                let (jobs_ref, results_ref) = (&jobs, &results);
5574                p.run_rows(jobs.len(), &move |s, e| {
5575                    for i in s..e {
5576                        let (_, _, ei, wt) = jobs_ref[i];
5577                        let Some(exp) = l.experts.get(ei) else {
5578                            continue;
5579                        };
5580                        let bi = jobs_ref[i].0;
5581                        let mut a = vec![0.0f32; dim];
5582                        crate::gpu::cpu_scope(|| {
5583                            run_expert(&xs[bi * dim..(bi + 1) * dim], exp, cfg, wt, None, &mut a)
5584                        });
5585                        *results_ref[i].lock().unwrap() = a;
5586                    }
5587                });
5588            }
5589            _ => {
5590                for (i, &(bi, _, ei, wt)) in jobs.iter().enumerate() {
5591                    let Some(exp) = l.experts.get(ei) else {
5592                        continue;
5593                    };
5594                    let mut a = vec![0.0f32; dim];
5595                    crate::gpu::cpu_scope(|| {
5596                        run_expert(&xs[bi * dim..(bi + 1) * dim], exp, cfg, wt, pool, &mut a)
5597                    });
5598                    *results[i].lock().unwrap() = a;
5599                }
5600            }
5601        }
5602        // Reduce in each token's routing order, exactly like
5603        // dsv4_chain1_layer. The jobs vector was built in that order.
5604        for (i, &(bi, _, _, _)) in jobs.iter().enumerate() {
5605            let a = results[i].lock().unwrap();
5606            for (o, &v) in out[bi * dim..(bi + 1) * dim].iter_mut().zip(a.iter()) {
5607                *o += v;
5608            }
5609        }
5610        return;
5611    }
5612    let mut active: Vec<(usize, Vec<(usize, usize, f32)>)> = Vec::new();
5613    for ei in 0..l.experts.len() {
5614        let mut jobs = Vec::new();
5615        for bi in 0..b {
5616            for (slot, &(picked, wt)) in cold[bi].iter().enumerate() {
5617                if picked == ei {
5618                    jobs.push((bi, slot, wt));
5619                }
5620            }
5621        }
5622        if !jobs.is_empty() {
5623            active.push((ei, jobs));
5624        }
5625    }
5626    let expert_fwd =
5627        |ei: usize, jobs: &[(usize, usize, f32)], inner: Option<&crate::pool::Pool>| -> Vec<f32> {
5628            let e = &l.experts[ei];
5629            let n = jobs.len();
5630            let mut xj = vec![0.0f32; n * dim];
5631            for (j, &(bi, _, _)) in jobs.iter().enumerate() {
5632                xj[j * dim..(j + 1) * dim].copy_from_slice(&xs[bi * dim..(bi + 1) * dim]);
5633            }
5634            let mut gate = vec![0.0f32; n * inter];
5635            let mut up = vec![0.0f32; n * inter];
5636            e.w1.matmat(&xj, n, &mut gate, inner);
5637            e.w3.matmat(&xj, n, &mut up, inner);
5638            for (j, &(_, _, wt)) in jobs.iter().enumerate() {
5639                let (gj, uj) = (
5640                    &mut gate[j * inter..(j + 1) * inter],
5641                    &mut up[j * inter..(j + 1) * inter],
5642                );
5643                if cfg.swiglu_limit > 0.0 {
5644                    for u in uj.iter_mut() {
5645                        *u = u.clamp(-cfg.swiglu_limit, cfg.swiglu_limit);
5646                    }
5647                    for g in gj.iter_mut() {
5648                        *g = g.min(cfg.swiglu_limit);
5649                    }
5650                }
5651                for (g, &u) in gj.iter_mut().zip(uj.iter()) {
5652                    *g = (*g / (1.0 + (-*g).exp())) * u * wt;
5653                }
5654            }
5655            let mut down = vec![0.0f32; n * dim];
5656            e.w2.matmat(&gate, n, &mut down, inner);
5657            down
5658        };
5659    let mut routed = vec![0.0f32; b * slots * dim];
5660    match pool {
5661        Some(p) if active.len() > 1 => {
5662            let results: Vec<std::sync::Mutex<Vec<f32>>> = active
5663                .iter()
5664                .map(|_| std::sync::Mutex::new(Vec::new()))
5665                .collect();
5666            let active_ref = &active;
5667            let results_ref = &results;
5668            let fwd = &expert_fwd;
5669            p.run_rows(active.len(), &move |s, e| {
5670                for i in s..e {
5671                    let (ei, jobs) = &active_ref[i];
5672                    let d = crate::gpu::cpu_scope(|| fwd(*ei, jobs, None));
5673                    *results_ref[i].lock().unwrap() = d;
5674                }
5675            });
5676            for (i, (_, jobs)) in active.iter().enumerate() {
5677                let down = results[i].lock().unwrap();
5678                for (j, &(bi, slot, _)) in jobs.iter().enumerate() {
5679                    routed[(bi * slots + slot) * dim..(bi * slots + slot + 1) * dim]
5680                        .copy_from_slice(&down[j * dim..(j + 1) * dim]);
5681                }
5682            }
5683        }
5684        _ => {
5685            for (ei, jobs) in &active {
5686                let down = expert_fwd(*ei, jobs, pool);
5687                for (j, &(bi, slot, _)) in jobs.iter().enumerate() {
5688                    routed[(bi * slots + slot) * dim..(bi * slots + slot + 1) * dim]
5689                        .copy_from_slice(&down[j * dim..(j + 1) * dim]);
5690                }
5691            }
5692        }
5693    }
5694    for bi in 0..b {
5695        let dst = &mut out[bi * dim..(bi + 1) * dim];
5696        for slot in 0..cold[bi].len() {
5697            let src = &routed[(bi * slots + slot) * dim..(bi * slots + slot + 1) * dim];
5698            for (o, &v) in dst.iter_mut().zip(src) {
5699                *o += v;
5700            }
5701        }
5702    }
5703}
5704
5705/// Feed the exact route of the speculative batch's guaranteed-accepted first
5706/// token back into the same FreeToken-style live slots ordinary decode uses.
5707/// Rejected draft suffixes must not train or pollute the LRU, so the caller
5708/// deliberately passes only row zero.
5709#[cfg(feature = "gpu")]
5710fn refill_route_slots(l: &Dsv4Layer, cfg: &Dsv4Cfg, pk: &Pack, picks: &[usize]) {
5711    if picks.is_empty() || pk.route_complete() {
5712        return;
5713    }
5714    let quota = {
5715        static Q: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
5716        *Q.get_or_init(|| {
5717            std::env::var("CMF_DSV4_FETCH_MAX")
5718                .ok()
5719                .and_then(|v| v.parse().ok())
5720                .unwrap_or_else(|| crate::gpu_wgpu::dsv4_fetch_defaults().0)
5721        })
5722    };
5723    if quota == 0 {
5724        return;
5725    }
5726    let min_seen = {
5727        static M: std::sync::OnceLock<u16> = std::sync::OnceLock::new();
5728        *M.get_or_init(|| {
5729            std::env::var("CMF_DSV4_FETCH_MIN_SEEN")
5730                .ok()
5731                .and_then(|v| v.parse().ok())
5732                .unwrap_or_else(|| crate::gpu_wgpu::dsv4_fetch_defaults().1)
5733        })
5734    };
5735    let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
5736        return;
5737    };
5738    let mut dynv = pk.dynslots.lock().unwrap();
5739    if dynv.owner.is_empty() {
5740        return;
5741    }
5742    dynv.clock += 1;
5743    let clock = dynv.clock;
5744    if clock % 64 == 0 {
5745        for seen in &mut dynv.seen {
5746            *seen >>= 1;
5747        }
5748    }
5749    for &pick in picks {
5750        if pick >= dynv.seen.len() {
5751            continue;
5752        }
5753        dynv.seen[pick] = dynv.seen[pick].saturating_add(1);
5754        let slot = dynv.remap[pick];
5755        if slot != u32::MAX {
5756            dynv.last[slot as usize] = clock;
5757        }
5758    }
5759    let pack_first = pk.tensors.first().map(|t| t.0).unwrap_or(usize::MAX);
5760    let mut fetched = 0usize;
5761    for &pick in picks {
5762        if fetched >= quota || pick >= dynv.remap.len() {
5763            break;
5764        }
5765        if dynv.remap[pick] != u32::MAX || dynv.seen[pick] < min_seen {
5766            continue;
5767        }
5768        let victim = (0..dynv.owner.len())
5769            .filter(|&slot| dynv.last[slot] != clock)
5770            .min_by_key(|&slot| dynv.last[slot]);
5771        let Some(victim) = victim else { break };
5772        let Some(exp) = l.experts.get(pick) else {
5773            continue;
5774        };
5775        let tensors = (|| {
5776            Some((
5777                exp.w1.model_idx()?,
5778                exp.w3.model_idx()?,
5779                exp.w2.model_idx()?,
5780            ))
5781        })();
5782        let Some(tensors) = tensors else { continue };
5783        let gu_q2 = exp.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP);
5784        if !crate::gpu_wgpu::dsv4_slot_fill(
5785            &model,
5786            pack_first,
5787            victim,
5788            pick,
5789            tensors,
5790            cfg.moe_inter,
5791            cfg.dim,
5792            gu_q2,
5793        ) {
5794            break;
5795        }
5796        let old = dynv.owner[victim] as usize;
5797        if old < dynv.remap.len() {
5798            dynv.remap[old] = u32::MAX;
5799        }
5800        dynv.remap[pick] = victim as u32;
5801        dynv.owner[victim] = pick as u32;
5802        dynv.last[victim] = clock;
5803        dynv.mutated = true;
5804        fetched += 1;
5805    }
5806}
5807
5808/// Grouped output projection for a block. `wo_a` cannot use a plain matmat
5809/// because each group sees a different attention slice; reading a quantized
5810/// row once and applying it to every block position gives the same dot order
5811/// without rereading/dequantizing that row B times.
5812fn o_project_block(
5813    attn: &[f32],
5814    b: usize,
5815    wo_a: &crate::qtensor::QTensor,
5816    wo_b: &crate::qtensor::QTensor,
5817    groups: usize,
5818    lora: usize,
5819    pool: Option<&crate::pool::Pool>,
5820    out: &mut [f32],
5821) {
5822    let attn_len = attn.len() / b;
5823    let per_group = attn_len / groups;
5824    let rows = groups * lora;
5825    let mut mid = vec![0.0f32; b * rows];
5826    let mid_addr = crate::pool::SendMut::new(mid.as_mut_ptr());
5827    let run = |start: usize, end: usize| {
5828        let mut wr = vec![0.0f32; wo_a.cols()];
5829        for r in start..end {
5830            wo_a.row_f32(r, &mut wr);
5831            let group = r / lora;
5832            for bi in 0..b {
5833                let x = &attn
5834                    [bi * attn_len + group * per_group..bi * attn_len + (group + 1) * per_group];
5835                let v = wr.iter().zip(x).map(|(w, x)| w * x).sum();
5836                unsafe { *mid_addr.at(bi * rows + r) = v };
5837            }
5838        }
5839    };
5840    match pool {
5841        Some(p) if rows >= 256 => p.run_rows(rows, &run),
5842        _ => run(0, rows),
5843    }
5844    wo_b.matmat(&mid, b, out, pool);
5845}
5846
5847/// `CMF_DSV4_TRACE=1` prints the hidden state's RMS after each half-block and
5848/// the logits' shape at the end. A 300B model that decodes nonsense gives no
5849/// other handle: this says whether the state grew, collapsed or went
5850/// non-finite, and at which layer — before anyone reaches for a debugger on a
5851/// hundred-gigabyte file.
5852fn no_compressed() -> bool {
5853    static OFF: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5854    *OFF.get_or_init(|| std::env::var("CMF_DSV4_NO_COMPRESSED").is_ok_and(|v| v != "0"))
5855}
5856
5857fn trace_on() -> bool {
5858    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5859    *ON.get_or_init(|| std::env::var("CMF_DSV4_TRACE").is_ok_and(|v| v != "0"))
5860}
5861
5862fn rms_of(v: &[f32]) -> f32 {
5863    if v.is_empty() {
5864        return 0.0;
5865    }
5866    (v.iter().map(|x| x * x).sum::<f32>() / v.len() as f32).sqrt()
5867}
5868
5869#[cfg(feature = "gpu")]
5870fn verify_fp_on(pos: usize) -> bool {
5871    static POS: std::sync::OnceLock<Option<usize>> = std::sync::OnceLock::new();
5872    let want = *POS.get_or_init(|| {
5873        std::env::var("CMF_DSV4_FP_POS")
5874            .ok()
5875            .and_then(|v| v.parse().ok())
5876    });
5877    want == Some(pos)
5878}
5879
5880#[cfg(feature = "gpu")]
5881fn verify_fp(tag: &str, pos: usize, li: usize, state: &[f32]) {
5882    if !verify_fp_on(pos) {
5883        return;
5884    }
5885    let mut fp = 0xcbf29ce484222325u64;
5886    for &x in state {
5887        fp ^= x.to_bits() as u64;
5888        fp = fp.wrapping_mul(0x100000001b3);
5889    }
5890    eprintln!(
5891        "[dsv4-fp] {tag} pos={pos} li={li} fp={fp:016x} rms={:.7} head={:?}",
5892        rms_of(state),
5893        &state[..4.min(state.len())]
5894    );
5895}
5896
5897/// `CMF_DSV4_DUMP=<path>` appends one JSON line per token: the embedding, the
5898/// hyper-connection state after every layer, the folded-and-normed head input
5899/// and the logits. It exists to be diffed against the reference forward on
5900/// the same weights — the numerical parity this port has never had, which at
5901/// toy scale is a few thousand floats and entirely tractable.
5902thread_local! {
5903    /// The attention body's input and output per layer, interleaved.
5904    static BODY: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
5905    /// Experts chosen per layer for the token being decoded — the dump needs
5906    /// them, because two implementations that pick DIFFERENT experts diverge
5907    /// hugely for a reason that is not a bug in either.
5908    static PICKED: std::cell::RefCell<Vec<Vec<usize>>> =
5909        const { std::cell::RefCell::new(Vec::new()) };
5910    /// (layer, chosen experts) in call order, when armed.
5911    static PICK_TALLY: std::cell::RefCell<Option<Vec<(usize, Vec<usize>)>>> =
5912        const { std::cell::RefCell::new(None) };
5913}
5914
5915/// Start recording expert picks. Idempotent; the previous tally is dropped.
5916pub fn pick_tally_arm() {
5917    PICK_TALLY.with(|t| *t.borrow_mut() = Some(Vec::new()));
5918}
5919
5920/// Take what was recorded and stop recording.
5921pub fn pick_tally_take() -> Vec<(usize, Vec<usize>)> {
5922    PICK_TALLY.with(|t| t.borrow_mut().take().unwrap_or_default())
5923}
5924
5925/// How many distinct experts a set of per-token pick lists reaches, and how
5926/// many picks it makes. The ratio is what a batched MoE can hope to save.
5927pub fn tally_unique(picks: &[(usize, Vec<usize>)]) -> (usize, usize) {
5928    // Keyed by (layer, expert). Expert 17 of layer 3 and expert 17 of layer 4
5929    // are different weights, and counting them as one understated the traffic
5930    // a batch has to read — badly for the draft, whose three stages each have
5931    // their own 256.
5932    let mut seen = std::collections::HashSet::new();
5933    let mut total = 0;
5934    for (li, v) in picks {
5935        total += v.len();
5936        for &e in v {
5937            seen.insert((*li, e));
5938        }
5939    }
5940    (seen.len(), total)
5941}
5942
5943fn dump_path() -> Option<&'static str> {
5944    static P: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
5945    P.get_or_init(|| std::env::var("CMF_DSV4_DUMP").ok())
5946        .as_deref()
5947}
5948
5949fn dump_line(json: &str) {
5950    if let Some(p) = dump_path() {
5951        use std::io::Write as _;
5952        if let Ok(mut f) = std::fs::OpenOptions::new()
5953            .create(true)
5954            .append(true)
5955            .open(p)
5956        {
5957            let _ = writeln!(f, "{json}");
5958        }
5959    }
5960}
5961
5962fn vec_json(v: &[f32]) -> String {
5963    let mut s = String::with_capacity(v.len() * 9);
5964    s.push('[');
5965    for (i, x) in v.iter().enumerate() {
5966        if i > 0 {
5967            s.push(',');
5968        }
5969        s.push_str(&format!("{x:.6e}"));
5970    }
5971    s.push(']');
5972    s
5973}
5974
5975/// One token through the whole stack.
5976///
5977/// The hidden state is `hc_mult` copies of a `dim`-vector from the very
5978/// first line to the very last: the embedding is replicated, every layer
5979/// folds/expands around its two halves, and only `hc_head_fold` collapses
5980/// it before the output norm and the head. There is no point in this
5981/// function where an ordinary residual would fit.
5982#[allow(clippy::too_many_arguments)]
5983/// A chunk of prompt tokens. Stage one of the batched prefill (see
5984/// docs/DSV4_PREFILL.md): the walk itself, with the head skipped for every
5985/// token but the last.
5986///
5987/// Prefill costs `len × per-token` today, and on a 2500-token prompt that is
5988/// a minute and a half before the first word. The stages that follow batch
5989/// the weight reads — which is where the nine-fold gap to the bandwidth
5990/// floor lives — but this one is the scaffolding they hang on, and it
5991/// already stops computing 129 280 logits for tokens nobody asks about.
5992#[allow(clippy::too_many_arguments)]
5993/// `CMF_DSV4_BATCH=N` — how many prompt tokens go through the card in one
5994/// submission. 1 keeps the walk. The chunk still bounds it: a batch never
5995/// spans two chunks, so cancellation stays as responsive as it was.
5996fn batch_prefill() -> usize {
5997    static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
5998    *N.get_or_init(|| {
5999        std::env::var("CMF_DSV4_BATCH")
6000            .ok()
6001            .and_then(|v| v.parse::<usize>().ok())
6002            .filter(|&n| (1..=32).contains(&n))
6003            // A partial expert pack used to make the device-chain batch
6004            // decline, so the conservative default was one.  The exact host
6005            // batch below now handles that geometry: routing still spans all
6006            // experts and every cold winner is computed from the mmap-backed
6007            // checkpoint.  Eight amortises expert reads without making a long
6008            // prompt's cancellation granularity coarse.
6009            .unwrap_or(8)
6010    })
6011}
6012
6013/// The prompt as batches instead of a walk, when every layer will take one.
6014///
6015/// Refuses before touching any state, never half way: the caller's fallback
6016/// is the per-token walk, and a batch that advanced the caches and then gave
6017/// up would have them advanced twice. So everything that can decline is asked
6018/// first, and after the first dispatch the only outcomes are success and a
6019/// hard failure.
6020///
6021/// Hash layers are the one shape it cannot take: their expert list is forced
6022/// by the TOKEN's id and the layer description carries one list, not one per
6023/// token. The release has three of them (0, 1, 2); a file without them
6024/// batches the whole stack.
6025#[allow(clippy::too_many_arguments)]
6026fn forward_chunk_batched(
6027    g: &Dsv4Globals,
6028    layers: &[Dsv4Layer],
6029    cfg: &Dsv4Cfg,
6030    st: &mut Dsv4State,
6031    ids: &[u32],
6032    pos0: usize,
6033    inv_freq: &[f32],
6034    pool: Option<&crate::pool::Pool>,
6035    logits: &mut Vec<f32>,
6036    want_logits: bool,
6037) -> bool {
6038    #[cfg(not(feature = "gpu"))]
6039    {
6040        let _ = (
6041            g,
6042            layers,
6043            cfg,
6044            st,
6045            ids,
6046            pos0,
6047            inv_freq,
6048            pool,
6049            logits,
6050            want_logits,
6051        );
6052        false
6053    }
6054    #[cfg(feature = "gpu")]
6055    {
6056        let b = ids.len();
6057        // Complete packs form the fused device prefix.  Partial packs belong
6058        // to the exact causal tail: that tail routes over all experts and
6059        // completes cold winners, so gpu_end == 0 is a useful (and common on
6060        // smaller cards) batch rather than a reason to walk token by token.
6061        let gpu_end = st
6062            .dev_set
6063            .iter()
6064            .enumerate()
6065            .position(|(li, &on)| {
6066                !on || pack_for(&layers[li], cfg, li).is_none_or(|p| !p.route_complete())
6067            })
6068            .unwrap_or(st.dev_set.len());
6069        let why = if b < 2 {
6070            "токенов меньше двух"
6071        } else if !chain_enabled() {
6072            "цепочка выключена"
6073        } else if !st.dev_owned {
6074            "карта ещё не владеет состоянием"
6075        } else if st.dev_set.len() != layers.len() {
6076            "набор слоёв ещё не зафиксирован"
6077        } else if st.dev_set[gpu_end.min(st.dev_set.len())..]
6078            .iter()
6079            .enumerate()
6080            .any(|(i, &on)| on && !st.partial_set.get(gpu_end + i).copied().unwrap_or(false))
6081        {
6082            "слои на карте не образуют префикс"
6083        } else {
6084            ""
6085        };
6086        if !why.is_empty() {
6087            // This is an expected preflight refusal: the first prompt chunk
6088            // establishes device ownership and a one-token tail is too small
6089            // to batch.  The caller immediately takes the exact walk, so a
6090            // warning here looked like a generation failure when nothing had
6091            // failed. Keep the reason available only to the diagnostic gate.
6092            tracing::debug!("dsv4: пакет preflight — {why}");
6093            if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
6094                eprintln!("dsv4: пакет preflight — {why}");
6095            }
6096            return false;
6097        }
6098        let (hc, dim) = (cfg.hc_mult, cfg.dim);
6099        let mut emb = vec![0.0f32; dim];
6100        let mut states = vec![0.0f32; b * hc * dim];
6101        for (t, &id) in ids.iter().enumerate() {
6102            let mut state = vec![0.0f32; hc * dim];
6103            g.embed.row_f32(id as usize, &mut emb);
6104            for j in 0..hc {
6105                state[j * dim..(j + 1) * dim].copy_from_slice(&emb);
6106            }
6107            let (folded, post0, comb0) = hc_fold_norm(
6108                &state,
6109                &layers[0].hc_attn_fn,
6110                &layers[0].hc_attn_scale,
6111                &layers[0].hc_attn_base,
6112                &layers[0].attn_norm,
6113                cfg,
6114                pool,
6115            );
6116            let mut qn0 = vec![0.0f32; layers[0].wq_a.rows()];
6117            layers[0].wq_a.matvec(&folded, &mut qn0, pool);
6118            rms_weighted(&mut qn0, &layers[0].q_norm, cfg.norm_eps);
6119            if !crate::gpu_wgpu::dsv4_state_write_t(&state, t)
6120                || !crate::gpu_wgpu::dsv4_hc_write_t(&post0, &comb0, t)
6121                || !crate::gpu_wgpu::dsv4_chain_seed_t(&folded, &qn0, t)
6122                || !crate::gpu_wgpu::dsv4_chain_seed_bt(t, b, &state, &post0, &comb0, &folded, &qn0)
6123            {
6124                return false;
6125            }
6126            states[t * hc * dim..(t + 1) * hc * dim].copy_from_slice(&state);
6127        }
6128        let run: Vec<usize> = (0..gpu_end).collect();
6129        let mut folded = Vec::new();
6130        st.pos = pos0;
6131        if gpu_end > 0 {
6132            if !dsv4_chain_run(
6133                layers,
6134                &run,
6135                cfg,
6136                g,
6137                st,
6138                *ids.last().unwrap(),
6139                &mut folded,
6140                Some(&mut states),
6141                b,
6142                ids,
6143                true,
6144                pool,
6145            ) {
6146                return false;
6147            }
6148        }
6149        // Finish the trailing host layers in causal token order. Their KV
6150        // caches are host-owned, while the device prefix advanced its own
6151        // caches inside the one submission above. On the release this loop
6152        // is exactly layer 42; keeping it general makes smaller VRAM budgets
6153        // correct as long as the resident layers remain one prefix.
6154        let mut scratch = HcScratch::new(cfg);
6155        host_tail_walk_batch(
6156            g,
6157            layers,
6158            cfg,
6159            st,
6160            gpu_end,
6161            &mut states,
6162            ids,
6163            pos0,
6164            b,
6165            inv_freq,
6166            &mut scratch,
6167            pool,
6168            None,
6169        );
6170        st.pos = pos0 + b;
6171        // Said once. A gate that compares a batched prompt against a walked
6172        // one proves nothing if the batch quietly declined — the numbers match
6173        // because the same code produced both. This line is what tells the
6174        // two apart.
6175        {
6176            static SAID: std::sync::Once = std::sync::Once::new();
6177            SAID.call_once(|| tracing::warn!("dsv4: префилл пакетами по {b}"));
6178        }
6179        // Only the last token's logits are read; the rest of the chunk exists
6180        // to fill the caches. The head consumes the hyper-connection state,
6181        // not the chain's intermediate fold — skipping this final learned
6182        // fold used to make a full-device batch fast and wrong.
6183        if want_logits {
6184            let last = &states[(b - 1) * hc * dim..b * hc * dim];
6185            let mut h = vec![0.0f32; dim];
6186            hc_head_fold(
6187                last,
6188                &g.hc_head_fn,
6189                g.hc_head_scale,
6190                &g.hc_head_base,
6191                cfg,
6192                pool,
6193                &mut h,
6194            );
6195            rms_weighted(&mut h, &g.norm, cfg.norm_eps);
6196            logits.resize(cfg.vocab, 0.0);
6197            g.head.matvec(&h, logits, pool);
6198        } else {
6199            logits.clear();
6200        }
6201        true
6202    }
6203}
6204
6205/// Everything a speculative verify must be able to put back.
6206///
6207/// Device caches roll back by restore-then-replay: the shadow puts the
6208/// window rings and compressor streams where they were BEFORE the pass, and
6209/// the replay re-appends the accepted tokens' state from the hidden inputs
6210/// the pass retained. Append-only regions roll back by count. Host-owned
6211/// tail layers roll back by clone-and-rewalk.
6212#[cfg(feature = "gpu")]
6213pub struct Dsv4SpecTxn {
6214    pos0: usize,
6215    batch: usize,
6216    pub(crate) gpu_end: usize,
6217    dev_filled: Vec<usize>,
6218    dev_n_comp: Vec<usize>,
6219    dev_n_ix: Vec<usize>,
6220    host: Vec<(usize, HostLayerSnap)>,
6221    /// Per host layer, per verified token: the layer's state right after
6222    /// that token's attention — what a rollback restores INSTEAD of
6223    /// re-walking the tail it already walked (the values are identical;
6224    /// only the side effects were ever needed).
6225    host_steps: Vec<(usize, Vec<HostLayerSnap>)>,
6226    /// Every token's hyper-connection state as it left the device prefix,
6227    /// BEFORE the host tail walked (and mutated) anything: the rewalk's
6228    /// input, and the head's.
6229    pub states: Vec<f32>,
6230    shadow: Option<crate::gpu_wgpu::Dsv4SpecShadow>,
6231}
6232
6233#[cfg(feature = "gpu")]
6234struct HostLayerSnap {
6235    window: Vec<f32>,
6236    compressed: Vec<f32>,
6237    index_kv: Vec<f32>,
6238    pending_kv: Vec<f32>,
6239    pending_score: Vec<f32>,
6240    prev_kv: Vec<f32>,
6241    prev_score: Vec<f32>,
6242    pending_ix_kv: Vec<f32>,
6243    pending_ix_score: Vec<f32>,
6244    prev_ix_kv: Vec<f32>,
6245    prev_ix_score: Vec<f32>,
6246}
6247
6248#[cfg(feature = "gpu")]
6249fn host_snap(st: &Dsv4State, li: usize) -> HostLayerSnap {
6250    HostLayerSnap {
6251        window: st.window[li].clone(),
6252        compressed: st.compressed[li].clone(),
6253        index_kv: st.index_kv[li].clone(),
6254        pending_kv: st.pending_kv[li].clone(),
6255        pending_score: st.pending_score[li].clone(),
6256        prev_kv: st.prev_kv[li].clone(),
6257        prev_score: st.prev_score[li].clone(),
6258        pending_ix_kv: st.pending_ix_kv[li].clone(),
6259        pending_ix_score: st.pending_ix_score[li].clone(),
6260        prev_ix_kv: st.prev_ix_kv[li].clone(),
6261        prev_ix_score: st.prev_ix_score[li].clone(),
6262    }
6263}
6264
6265#[cfg(feature = "gpu")]
6266fn host_restore(st: &mut Dsv4State, li: usize, s: &HostLayerSnap) {
6267    st.window[li] = s.window.clone();
6268    st.compressed[li] = s.compressed.clone();
6269    st.index_kv[li] = s.index_kv.clone();
6270    st.pending_kv[li] = s.pending_kv.clone();
6271    st.pending_score[li] = s.pending_score.clone();
6272    st.prev_kv[li] = s.prev_kv.clone();
6273    st.prev_score[li] = s.prev_score.clone();
6274    st.pending_ix_kv[li] = s.pending_ix_kv.clone();
6275    st.pending_ix_score[li] = s.pending_ix_score.clone();
6276    st.prev_ix_kv[li] = s.prev_ix_kv.clone();
6277    st.prev_ix_score[li] = s.prev_ix_score.clone();
6278}
6279
6280/// One host-tail walk of token `t`'s state through layers `gpu_end..`,
6281/// mutating `state` in place and the layers' host caches. Exactly the loop
6282/// the batch runs, factored so the verify can re-run it for accepted tokens.
6283#[cfg(feature = "gpu")]
6284#[allow(clippy::too_many_arguments)]
6285fn host_tail_walk(
6286    g: &Dsv4Globals,
6287    layers: &[Dsv4Layer],
6288    cfg: &Dsv4Cfg,
6289    st: &mut Dsv4State,
6290    gpu_end: usize,
6291    state: &mut [f32],
6292    token_id: u32,
6293    pos: usize,
6294    inv_freq: &[f32],
6295    scratch: &mut HcScratch,
6296    pool: Option<&crate::pool::Pool>,
6297) {
6298    st.pos = pos;
6299    for (li, l) in layers.iter().enumerate().skip(gpu_end) {
6300        let freqs = if l.compressor.is_some() {
6301            &g.inv_freq_compress
6302        } else {
6303            &g.inv_freq_window
6304        };
6305        let freqs = if freqs.is_empty() {
6306            inv_freq
6307        } else {
6308            freqs.as_slice()
6309        };
6310        hc_block(
6311            state,
6312            &l.hc_attn_fn,
6313            &l.hc_attn_scale,
6314            &l.hc_attn_base,
6315            &l.attn_norm,
6316            cfg,
6317            scratch,
6318            pool,
6319            |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
6320        );
6321        hc_block(
6322            state,
6323            &l.hc_ffn_fn,
6324            &l.hc_ffn_scale,
6325            &l.hc_ffn_base,
6326            &l.ffn_norm,
6327            cfg,
6328            scratch,
6329            pool,
6330            |f, o| {
6331                if host_cpu_moe() {
6332                    crate::gpu::cpu_scope(|| moe_step(f, l, cfg, token_id, li, pool, o))
6333                } else {
6334                    moe_step(f, l, cfg, token_id, li, pool, o)
6335                }
6336            },
6337        );
6338        dspark_note(li, state, cfg);
6339    }
6340}
6341
6342/// The host tail for a whole batch: attention stays causal per token (its
6343/// window mutates), the MoE half runs through the block-grouped path — the
6344/// same accumulation order as the position walk, which the block tests pin
6345/// bit for bit. This is the verify's tail; the single-token paths keep
6346/// `hc_block`.
6347#[cfg(feature = "gpu")]
6348#[allow(clippy::too_many_arguments)]
6349fn host_tail_walk_batch(
6350    g: &Dsv4Globals,
6351    layers: &[Dsv4Layer],
6352    cfg: &Dsv4Cfg,
6353    st: &mut Dsv4State,
6354    gpu_end: usize,
6355    states: &mut [f32],
6356    ids: &[u32],
6357    pos0: usize,
6358    b: usize,
6359    inv_freq: &[f32],
6360    scratch: &mut HcScratch,
6361    pool: Option<&crate::pool::Pool>,
6362    mut steps: Option<&mut Vec<(usize, Vec<HostLayerSnap>)>>,
6363) {
6364    let (hc, dim) = (cfg.hc_mult, cfg.dim);
6365    let mix_hc = (2 + hc) * hc;
6366    let mut folds = vec![0.0f32; b * dim];
6367    let mut mo = vec![0.0f32; b * dim];
6368    let mut posts = vec![0.0f32; b * hc];
6369    let mut combs = vec![0.0f32; b * hc * hc];
6370    let mut resid = vec![0.0f32; b * hc * dim];
6371    let spec_time = {
6372        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6373        *ON.get_or_init(|| std::env::var("CMF_DSV4_SPEC_TIME").is_ok_and(|v| v != "0"))
6374    };
6375    for (li, l) in layers.iter().enumerate().skip(gpu_end) {
6376        let t_attn = std::time::Instant::now();
6377        let freqs = if l.compressor.is_some() {
6378            &g.inv_freq_compress
6379        } else {
6380            &g.inv_freq_window
6381        };
6382        let freqs = if freqs.is_empty() {
6383            inv_freq
6384        } else {
6385            freqs.as_slice()
6386        };
6387        for t in 0..b {
6388            st.pos = pos0 + t;
6389            let state = &mut states[t * hc * dim..(t + 1) * hc * dim];
6390            hc_block(
6391                state,
6392                &l.hc_attn_fn,
6393                &l.hc_attn_scale,
6394                &l.hc_attn_base,
6395                &l.attn_norm,
6396                cfg,
6397                scratch,
6398                pool,
6399                |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
6400            );
6401            if let Some(steps) = steps.as_mut() {
6402                match steps.iter_mut().find(|(l, _)| *l == li) {
6403                    Some((_, v)) => v.push(host_snap(st, li)),
6404                    None => steps.push((li, vec![host_snap(st, li)])),
6405                }
6406            }
6407        }
6408        let t_glue = std::time::Instant::now();
6409        for t in 0..b {
6410            let state = &states[t * hc * dim..(t + 1) * hc * dim];
6411            hc_mixes(
6412                state,
6413                &l.hc_ffn_fn,
6414                mix_hc,
6415                cfg.norm_eps,
6416                pool,
6417                &mut scratch.mixes,
6418            );
6419            hc_split_sinkhorn(
6420                &scratch.mixes,
6421                &l.hc_ffn_scale,
6422                &l.hc_ffn_base,
6423                hc,
6424                cfg.hc_sinkhorn_iters,
6425                cfg.hc_eps,
6426                &mut scratch.pre,
6427                &mut posts[t * hc..(t + 1) * hc],
6428                &mut combs[t * hc * hc..(t + 1) * hc * hc],
6429            );
6430            let fold = &mut folds[t * dim..(t + 1) * dim];
6431            hc_fold(state, &scratch.pre, hc, dim, fold);
6432            let ms = fold.iter().map(|v| v * v).sum::<f32>() / dim as f32;
6433            let inv = 1.0 / (ms + cfg.norm_eps).sqrt();
6434            for (v, w) in fold.iter_mut().zip(&l.ffn_norm) {
6435                *v = *v * inv * w;
6436            }
6437            resid[t * hc * dim..(t + 1) * hc * dim]
6438                .copy_from_slice(&states[t * hc * dim..(t + 1) * hc * dim]);
6439        }
6440        let t_moe = std::time::Instant::now();
6441        // A tail layer with a device expert pack (partial or full) runs its
6442        // hot winners on the card per token and completes the cold ones on
6443        // the host — the same exact split the partial walk uses. Default on
6444        // (measured: the tail fell 27.4 → 18.2 ms of the verify round);
6445        // `CMF_DSV4_TAIL_PACK=0` restores the batched host block.
6446        let tail_pack = {
6447            static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6448            *ON.get_or_init(|| {
6449                std::env::var("CMF_DSV4_TAIL_PACK")
6450                    .map(|v| v != "0")
6451                    .unwrap_or(true)
6452            })
6453        };
6454        let mut packed_done = false;
6455        if tail_pack && pack_for(l, cfg, li).is_some() {
6456            packed_done = true;
6457            for t in 0..b {
6458                let f = &folds[t * dim..(t + 1) * dim];
6459                let forced = l.tid2eid.as_ref().map(|tbl| {
6460                    hash_route(tbl, cfg.vocab, cfg.top_k, ids.get(t).copied().unwrap_or(0))
6461                });
6462                let o = &mut mo[t * dim..(t + 1) * dim];
6463                match moe_frame(f, l, cfg, li, &[], forced.as_deref(), pool, None, None, o) {
6464                    Some((cold_sum, n)) => {
6465                        if n > 0 {
6466                            for (od, cd) in o.iter_mut().zip(cold_sum.iter()) {
6467                                *od += cd;
6468                            }
6469                        }
6470                    }
6471                    None => {
6472                        packed_done = false;
6473                        break;
6474                    }
6475                }
6476            }
6477        }
6478        if !packed_done {
6479            if host_cpu_moe() {
6480                crate::gpu::cpu_scope(|| moe_step_block(&folds, b, l, cfg, ids, li, pool, &mut mo));
6481            } else {
6482                moe_step_block(&folds, b, l, cfg, ids, li, pool, &mut mo);
6483            }
6484        }
6485        let t_exp = std::time::Instant::now();
6486        for t in 0..b {
6487            let state = &mut states[t * hc * dim..(t + 1) * hc * dim];
6488            hc_expand(
6489                &mo[t * dim..(t + 1) * dim],
6490                &resid[t * hc * dim..(t + 1) * hc * dim],
6491                &posts[t * hc..(t + 1) * hc],
6492                &combs[t * hc * hc..(t + 1) * hc * hc],
6493                hc,
6494                dim,
6495                state,
6496            );
6497            dspark_note(li, state, cfg);
6498        }
6499        if spec_time {
6500            eprintln!(
6501                "хвост слоя {li}: attn {:.1} мс, клей {:.1}, moe {:.1}, expand {:.1}",
6502                (t_glue - t_attn).as_secs_f64() * 1e3,
6503                (t_moe - t_glue).as_secs_f64() * 1e3,
6504                (t_exp - t_moe).as_secs_f64() * 1e3,
6505                t_exp.elapsed().as_secs_f64() * 1e3,
6506            );
6507        }
6508    }
6509}
6510
6511/// One exact token-axis layer over a partial expert pack.  The device runs
6512/// attention, routing over all experts, the resident MoE rows and the
6513/// hyper-connection join once for the whole batch.  Cold winners are grouped
6514/// by expert on the host, corrected into the returned state, and only then is
6515/// the next dependent layer allowed to start.
6516#[cfg(feature = "gpu")]
6517#[allow(clippy::too_many_arguments)]
6518fn partial_layer_batch(
6519    g: &Dsv4Globals,
6520    layers: &[Dsv4Layer],
6521    cfg: &Dsv4Cfg,
6522    st: &mut Dsv4State,
6523    li: usize,
6524    states: &mut [f32],
6525    ids: &[u32],
6526    pos0: usize,
6527    b: usize,
6528    pool: Option<&crate::pool::Pool>,
6529) -> bool {
6530    let l = &layers[li];
6531    let (hc, dim, hd) = (cfg.hc_mult, cfg.dim, cfg.head_dim);
6532    if states.len() < b * hc * dim || ids.len() < b {
6533        return false;
6534    }
6535    let Some(pk) = pack_for(l, cfg, li) else {
6536        return false;
6537    };
6538    if pk.route_complete() {
6539        return false;
6540    }
6541    let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
6542        return false;
6543    };
6544    let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b), Some(wkv)) = (
6545        l.wq_a.model_idx(),
6546        l.wq_b.model_idx(),
6547        l.wo_a.model_idx(),
6548        l.wo_b.model_idx(),
6549        l.wkv.model_idx(),
6550    ) else {
6551        return false;
6552    };
6553    let comp = match &l.compressor {
6554        None => None,
6555        Some(cp) => {
6556            let (Some(a), Some(bx)) = (cp.wkv.model_idx(), cp.wgate.model_idx()) else {
6557                return false;
6558            };
6559            Some((
6560                crate::gpu_wgpu::Dsv4CompW {
6561                    wkv: a,
6562                    wgate: bx,
6563                    norm: &cp.norm,
6564                    ape: &cp.ape,
6565                },
6566                crate::gpu_wgpu::Dsv4CompGeom {
6567                    width: cp.wkv.rows(),
6568                    hidden: dim,
6569                    ratio: cp.ratio,
6570                    overlap: cp.overlap,
6571                    rope_dim: cfg.rope_head_dim,
6572                    eps: cfg.norm_eps,
6573                },
6574            ))
6575        }
6576    };
6577    let ix = match &l.indexer {
6578        None => None,
6579        Some(ixr) => {
6580            let cp = &ixr.compressor;
6581            let (Some(a), Some(bx), Some(qb), Some(wp)) = (
6582                cp.wkv.model_idx(),
6583                cp.wgate.model_idx(),
6584                ixr.wq_b.model_idx(),
6585                ixr.weights_proj.model_idx(),
6586            ) else {
6587                return false;
6588            };
6589            let ih = ixr.weights_proj.rows();
6590            Some((
6591                crate::gpu_wgpu::Dsv4CompW {
6592                    wkv: a,
6593                    wgate: bx,
6594                    norm: &cp.norm,
6595                    ape: &cp.ape,
6596                },
6597                crate::gpu_wgpu::Dsv4CompGeom {
6598                    width: cp.wkv.rows(),
6599                    hidden: dim,
6600                    ratio: cp.ratio,
6601                    overlap: cp.overlap,
6602                    rope_dim: cfg.rope_head_dim,
6603                    eps: cfg.norm_eps,
6604                },
6605                crate::gpu_wgpu::Dsv4IxW {
6606                    wq_b: qb,
6607                    weights_proj: wp,
6608                },
6609                crate::gpu_wgpu::Dsv4IxGeom {
6610                    ih,
6611                    idim: ixr.wq_b.rows() / ih.max(1),
6612                    q_lora: cfg.q_lora_rank,
6613                    hidden: dim,
6614                    rope_dim: cfg.rope_head_dim,
6615                    eps: cfg.norm_eps,
6616                    top_k: cfg.index_topk,
6617                    window: cfg.window,
6618                },
6619            ))
6620        }
6621    };
6622    let ew_c = comp.as_ref().map_or(
6623        0,
6624        |(_, cg)| {
6625            if cg.overlap { cg.width / 2 } else { cg.width }
6626        },
6627    );
6628    let ew_i = ix.as_ref().map_or(
6629        0,
6630        |(_, cg, _, _)| {
6631            if cg.overlap { cg.width / 2 } else { cg.width }
6632        },
6633    );
6634    let comp_extra = comp
6635        .as_ref()
6636        .map_or(0, |(_, cg)| b.div_ceil(cg.ratio.max(1)));
6637    let need = cfg.window * hd + (st.dev_n_comp[li] + comp_extra + 1) * ew_c.max(1) + (b + 1) * hd;
6638    if !crate::gpu_wgpu::dsv4_cache_ensure(st.kv_id, li, need.next_power_of_two()) {
6639        return false;
6640    }
6641    let base = crate::gpu_wgpu::Dsv4Prep {
6642        wkv,
6643        kv_norm: &l.kv_norm,
6644        comp,
6645        ix,
6646        filled: st.dev_filled[li],
6647        window: cfg.window,
6648        n_comp: st.dev_n_comp[li],
6649        n_ix: st.dev_n_ix[li],
6650        comp_dst_off: cfg.window * hd + st.dev_n_comp[li] * ew_c,
6651        ix_dst_off: st.dev_n_ix[li] * ew_i,
6652        idx_cap: cfg.window
6653            + if l.indexer.is_some() {
6654                cfg.index_topk
6655            } else {
6656                st.dev_n_comp[li] + comp_extra + 1
6657            },
6658    };
6659    let mut preps = Vec::with_capacity(b);
6660    for t in 0..b {
6661        let mut p = base.clone();
6662        p.filled = (base.filled + t).min(base.window);
6663        let advanced = |ratio: usize| -> usize {
6664            if ratio == 0 {
6665                0
6666            } else {
6667                (0..t).filter(|k| (pos0 + k + 1) % ratio == 0).count()
6668            }
6669        };
6670        if let Some((_, cg)) = base.comp.as_ref() {
6671            p.n_comp = base.n_comp + advanced(cg.ratio);
6672            p.comp_dst_off = base.comp_dst_off + (p.n_comp - base.n_comp) * ew_c;
6673        }
6674        if let Some((_, cg, _, _)) = base.ix.as_ref() {
6675            p.n_ix = base.n_ix + advanced(cg.ratio);
6676            p.ix_dst_off = base.ix_dst_off + (p.n_ix - base.n_ix) * ew_i;
6677        }
6678        preps.push(p);
6679    }
6680
6681    // Every row enters with an exact host state because the previous partial
6682    // layer was corrected before returning.  Seed the token-axis slots and
6683    // the q-LoRA vector the batched attention consumes.
6684    for t in 0..b {
6685        let state = &states[t * hc * dim..(t + 1) * hc * dim];
6686        let (fold, post, comb) = hc_fold_norm(
6687            state,
6688            &l.hc_attn_fn,
6689            &l.hc_attn_scale,
6690            &l.hc_attn_base,
6691            &l.attn_norm,
6692            cfg,
6693            pool,
6694        );
6695        let mut qn = vec![0.0f32; cfg.q_lora_rank];
6696        l.wq_a.matvec(&fold, &mut qn, pool);
6697        rms_weighted(&mut qn, &l.q_norm, cfg.norm_eps);
6698        if !crate::gpu_wgpu::dsv4_chain_seed_bt(t, b, state, &post, &comb, &fold, &qn) {
6699            return false;
6700        }
6701    }
6702    let forced_rows: Vec<Option<Vec<usize>>> = ids
6703        .iter()
6704        .take(b)
6705        .map(|&id| {
6706            l.tid2eid
6707                .as_ref()
6708                .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, id))
6709        })
6710        .collect();
6711    let dynv = pk.dynslots.lock().unwrap();
6712    let w = crate::gpu_wgpu::Dsv4LayerW {
6713        attn: crate::gpu_wgpu::Dsv4AttnW {
6714            wq_a,
6715            wq_b,
6716            wo_a,
6717            wo_b,
6718            q_norm: &l.q_norm,
6719            sink: &l.attn_sink,
6720        },
6721        moe: crate::gpu_wgpu::Dsv4MoeW {
6722            router: &[],
6723            experts: &pk.tensors,
6724            logits: &[],
6725            bias: pk.bias.as_deref(),
6726            mask: pk.mask.as_deref(),
6727            forced: None,
6728            remap: Some(&dynv.remap),
6729            global: None,
6730            has_shared: true,
6731            shared_weight: 1.0,
6732            preweighted: false,
6733            qwen_softmax: false,
6734        },
6735        hc_ffn_fn: &l.hc_ffn_fn,
6736        hc_ffn_scale: &l.hc_ffn_scale,
6737        hc_ffn_base: &l.hc_ffn_base,
6738        // Stop after this layer's state.  The next-layer fold must see the
6739        // cold-corrected state, not the resident-only state on the card.
6740        hc_next_fn: None,
6741        hc_next_scale: &l.hc_attn_scale,
6742        hc_next_base: &l.hc_attn_base,
6743        ffn_norm: &l.ffn_norm,
6744        next_norm: &l.attn_norm,
6745        next_q_norm: &l.q_norm,
6746        next_wq_a: None,
6747        router: &pk.router,
6748    };
6749    let geom = crate::gpu_wgpu::Dsv4LayerGeom {
6750        attn: crate::gpu_wgpu::Dsv4AttnGeom {
6751            dim,
6752            nh: cfg.n_heads,
6753            hd,
6754            rd: cfg.rope_head_dim,
6755            q_lora: cfg.q_lora_rank,
6756            o_lora: cfg.o_lora_rank,
6757            o_groups: cfg.o_groups,
6758            eps: cfg.norm_eps,
6759            scale: (hd as f32).powf(-0.5),
6760        },
6761        moe: crate::gpu_wgpu::Dsv4MoeGeom {
6762            hidden: dim,
6763            inter: cfg.moe_inter,
6764            top_k: cfg.top_k,
6765            route_scale: cfg.route_scale,
6766            swiglu_limit: cfg.swiglu_limit,
6767            gu_q2: l
6768                .experts
6769                .first()
6770                .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
6771        },
6772        hc,
6773        hc_eps: cfg.hc_eps,
6774        sinkhorn_iters: cfg.hc_sinkhorn_iters,
6775    };
6776    let freqs = if l.compressor.is_some() {
6777        g.inv_freq_compress.as_slice()
6778    } else {
6779        g.inv_freq_window.as_slice()
6780    };
6781    let Some(mut got) = crate::gpu_wgpu::dsv4_layer_batch_partial(
6782        &model,
6783        &w,
6784        geom,
6785        st.kv_id,
6786        li,
6787        b,
6788        &preps,
6789        Some(&forced_rows),
6790        freqs,
6791        pos0,
6792    ) else {
6793        return false;
6794    };
6795    drop(dynv);
6796    let mut cold_sum = vec![0.0f32; b * dim];
6797    cold_step_block(&got.cold_x, b, l, cfg, &got.cold, pool, &mut cold_sum);
6798    for t in 0..b {
6799        let state = &mut got.states[t * hc * dim..(t + 1) * hc * dim];
6800        let post = &got.posts[t * hc..(t + 1) * hc];
6801        let cold = &cold_sum[t * dim..(t + 1) * dim];
6802        for j in 0..hc {
6803            for d in 0..dim {
6804                state[j * dim + d] += post[j] * cold[d];
6805            }
6806        }
6807    }
6808    states[..b * hc * dim].copy_from_slice(&got.states);
6809    if !crate::gpu_wgpu::dsv4_spec_cap_write_host(li, b, hc * dim, &got.states) {
6810        return false;
6811    }
6812    if let Some(first_route) = got.routed.first() {
6813        refill_route_slots(l, cfg, &pk, first_route);
6814    }
6815    for t in 0..b {
6816        let pos = pos0 + t;
6817        st.dev_filled[li] = (st.dev_filled[li] + 1).min(cfg.window);
6818        if let Some((_, cg, ..)) = base.ix.as_ref() {
6819            if (pos + 1) % cg.ratio == 0 {
6820                st.dev_n_ix[li] += 1;
6821            }
6822        }
6823        if let Some((_, cg)) = base.comp.as_ref() {
6824            if (pos + 1) % cg.ratio == 0 {
6825                st.dev_n_comp[li] += 1;
6826                note_compressed(st.kv_id, li, st.dev_n_comp[li]);
6827            }
6828        }
6829    }
6830    true
6831}
6832
6833/// A speculative verify pass: run `ids` (the committed next token followed
6834/// by draft proposals) at positions `pos0..pos0+B` through the trunk in one
6835/// batched submission, WITHOUT giving up the ability to roll back, and
6836/// return every position's greedy answer. The caller decides the accepted
6837/// prefix and calls [`dsv4_spec_finish`], which either keeps everything
6838/// (`accepted == B`) or restores-and-replays to the accepted length.
6839///
6840/// `logits_out` takes B rows of vocab logits, `argmax_out` their argmaxes.
6841#[cfg(feature = "gpu")]
6842#[allow(clippy::too_many_arguments)]
6843pub fn dsv4_verify_chunk(
6844    g: &Dsv4Globals,
6845    layers: &[Dsv4Layer],
6846    cfg: &Dsv4Cfg,
6847    st: &mut Dsv4State,
6848    ids: &[u32],
6849    pos0: usize,
6850    inv_freq: &[f32],
6851    pool: Option<&crate::pool::Pool>,
6852    cap_targets: &[usize],
6853    argmax_out: &mut Vec<u32>,
6854    logits_out: &mut Vec<f32>,
6855    walked_out: &mut Vec<f32>,
6856) -> Option<Dsv4SpecTxn> {
6857    let b = ids.len();
6858    // The complete prefix still runs as one fused chain.  Immediately after
6859    // it, contiguous PARTIAL packs can now stay on the device too: each one
6860    // is corrected with its cold experts before the next layer is seeded.
6861    let complete_end = st
6862        .dev_set
6863        .iter()
6864        .enumerate()
6865        .position(|(li, &on)| {
6866            !on || pack_for(&layers[li], cfg, li).is_none_or(|p| !p.route_complete())
6867        })
6868        .unwrap_or(st.dev_set.len());
6869    fn partial_batch_on() -> bool {
6870        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6871        *ON.get_or_init(|| {
6872            std::env::var("CMF_DSV4_PARTIAL_BATCH")
6873                .map(|v| v != "0")
6874                .unwrap_or(true)
6875        })
6876    }
6877    let device_end = if partial_batch_on() {
6878        (complete_end..layers.len())
6879            .take_while(|&li| {
6880                st.dev_set.get(li).copied().unwrap_or(false)
6881                    && st.partial_set.get(li).copied().unwrap_or(false)
6882                    && pack_for(&layers[li], cfg, li).is_some_and(|p| !p.route_complete())
6883            })
6884            .last()
6885            .map_or(complete_end, |li| li + 1)
6886    } else {
6887        complete_end
6888    };
6889    // A PARTIAL layer after a host gap is allowed to walk in the host tail.
6890    // A FULL device layer there would violate the contiguous-prefix contract.
6891    let full_beyond = st.dev_set[device_end.min(st.dev_set.len())..]
6892        .iter()
6893        .enumerate()
6894        .any(|(i, &on)| on && !st.partial_set.get(device_end + i).copied().unwrap_or(false));
6895    // `CMF_DSV4_HOST_VERIFY=1` lets the verify run with NO device prefix:
6896    // every layer walks in the host tail, batched — which is where a
6897    // many-core host amortises the weight read and the unpack across the
6898    // draft (the whole point of a batched verify). Off, a partial layer 0
6899    // (dynamic-slot packs) silently priced the entire speculation at zero:
6900    // 625 drafted, 0 verified, all cost and no candidate.
6901    fn host_verify_on() -> bool {
6902        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6903        *ON.get_or_init(|| {
6904            std::env::var("CMF_DSV4_HOST_VERIFY")
6905                .map(|v| v != "0")
6906                // On a small card every layer can have a useful partial pack
6907                // while no layer has a complete one.  The exact batched tail
6908                // is specifically built for that shape; silently pricing
6909                // speculation at zero here defeated the automatic fast path.
6910                .unwrap_or(true)
6911        })
6912    }
6913    if b < 2
6914        || !chain_enabled()
6915        || !st.dev_owned
6916        || st.dev_set.len() != layers.len()
6917        || (device_end == 0 && !host_verify_on())
6918        || full_beyond
6919    {
6920        return None;
6921    }
6922    let (hc, dim, hd) = (cfg.hc_mult, cfg.dim, cfg.head_dim);
6923    // ── the transaction ──
6924    let metas: Vec<(usize, usize, usize, usize)> = (0..device_end)
6925        .map(|li| (li, hd, cfg.window, st.dev_filled[li]))
6926        .collect();
6927    let shadow = crate::gpu_wgpu::dsv4_spec_shadow(st.kv_id, &metas, b)?;
6928    let mut txn = Dsv4SpecTxn {
6929        pos0,
6930        batch: b,
6931        gpu_end: device_end,
6932        dev_filled: st.dev_filled.clone(),
6933        dev_n_comp: st.dev_n_comp.clone(),
6934        dev_n_ix: st.dev_n_ix.clone(),
6935        host: (device_end..layers.len())
6936            .map(|li| (li, host_snap(st, li)))
6937            .collect(),
6938        states: Vec::new(),
6939        host_steps: Vec::new(),
6940        shadow: Some(shadow),
6941    };
6942    // The capture targets that live on the device: photograph their states.
6943    let dev_caps: Vec<usize> = cap_targets
6944        .iter()
6945        .copied()
6946        .filter(|&t| t < device_end)
6947        .collect();
6948    crate::gpu_wgpu::dsv4_spec_retain_arm(device_end, &dev_caps);
6949
6950    // ── seed and run the batch (the prefill batch's own shape) ──
6951    let mut emb = vec![0.0f32; dim];
6952    let mut states = vec![0.0f32; b * hc * dim];
6953    for (t, &id) in ids.iter().enumerate() {
6954        let mut state = vec![0.0f32; hc * dim];
6955        g.embed.row_f32(id as usize, &mut emb);
6956        for j in 0..hc {
6957            state[j * dim..(j + 1) * dim].copy_from_slice(&emb);
6958        }
6959        states[t * hc * dim..(t + 1) * hc * dim].copy_from_slice(&state);
6960        let (folded, post0, comb0) = hc_fold_norm(
6961            &state,
6962            &layers[0].hc_attn_fn,
6963            &layers[0].hc_attn_scale,
6964            &layers[0].hc_attn_base,
6965            &layers[0].attn_norm,
6966            cfg,
6967            pool,
6968        );
6969        let mut qn0 = vec![0.0f32; layers[0].wq_a.rows()];
6970        layers[0].wq_a.matvec(&folded, &mut qn0, pool);
6971        rms_weighted(&mut qn0, &layers[0].q_norm, cfg.norm_eps);
6972        if !crate::gpu_wgpu::dsv4_state_write_t(&state, t)
6973            || !crate::gpu_wgpu::dsv4_hc_write_t(&post0, &comb0, t)
6974            || !crate::gpu_wgpu::dsv4_chain_seed_t(&folded, &qn0, t)
6975            || !crate::gpu_wgpu::dsv4_chain_seed_bt(t, b, &state, &post0, &comb0, &folded, &qn0)
6976        {
6977            crate::gpu_wgpu::dsv4_spec_retain_arm(0, &[]);
6978            return None;
6979        }
6980    }
6981    let spec_time = {
6982        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6983        *ON.get_or_init(|| std::env::var("CMF_DSV4_SPEC_TIME").is_ok_and(|v| v != "0"))
6984    };
6985    let t0 = std::time::Instant::now();
6986    let mut folded = Vec::new();
6987    st.pos = pos0;
6988    // Diagnostic split: the fused prefix normally has one fence.  Splitting
6989    // only while fingerprinting tells us which complete layer first departs
6990    // from scalar decode; it must never become a user-facing tuning flag.
6991    let fp_split = verify_fp_on(pos0) && std::env::var("CMF_DSV4_FP_SPLIT").is_ok_and(|v| v != "0");
6992    let mut ok = true;
6993    if fp_split {
6994        for li in 0..complete_end {
6995            let one = [li];
6996            ok = dsv4_chain_run(
6997                layers,
6998                &one,
6999                cfg,
7000                g,
7001                st,
7002                *ids.last().unwrap(),
7003                &mut folded,
7004                Some(&mut states),
7005                b,
7006                ids,
7007                li == 0,
7008                pool,
7009            );
7010            if !ok {
7011                break;
7012            }
7013            verify_fp("verify", pos0, li, &states[..hc * dim]);
7014        }
7015    } else if complete_end > 0 {
7016        let run: Vec<usize> = (0..complete_end).collect();
7017        ok = dsv4_chain_run(
7018            layers,
7019            &run,
7020            cfg,
7021            g,
7022            st,
7023            *ids.last().unwrap(),
7024            &mut folded,
7025            Some(&mut states),
7026            b,
7027            ids,
7028            true,
7029            pool,
7030        );
7031        if ok {
7032            verify_fp("verify", pos0, complete_end - 1, &states[..hc * dim]);
7033        }
7034    }
7035    if ok {
7036        for li in complete_end..device_end {
7037            if !partial_layer_batch(g, layers, cfg, st, li, &mut states, ids, pos0, b, pool) {
7038                ok = false;
7039                break;
7040            }
7041            verify_fp("verify", pos0, li, &states[..hc * dim]);
7042        }
7043    }
7044    crate::gpu_wgpu::dsv4_spec_retain_arm(0, &[]);
7045    if !ok {
7046        // Nothing committed on the host; the device may hold half-appended
7047        // state, so put the snapshot back before declining.
7048        if let Some(sh) = txn.shadow.take() {
7049            let _ = crate::gpu_wgpu::dsv4_spec_restore(&sh);
7050        }
7051        st.dev_filled = txn.dev_filled;
7052        st.dev_n_comp = txn.dev_n_comp;
7053        st.dev_n_ix = txn.dev_n_ix;
7054        st.pos = pos0;
7055        return None;
7056    }
7057    txn.states = states.clone();
7058    let t_chain = t0.elapsed();
7059    if std::env::var("CMF_DSV4_FOLD_DBG").is_ok() {
7060        // Any indexer fold this window landed: read the entry back and
7061        // print a fingerprint, so the fused and per-token folds can be
7062        // held against each other on the release shapes.
7063        for li in 0..device_end {
7064            let Some(ixr) = &layers[li].indexer else {
7065                continue;
7066            };
7067            let ratio = ixr.compressor.ratio;
7068            for t in 0..b {
7069                if (pos0 + t + 1) % ratio == 0 {
7070                    let ew = {
7071                        let w = ixr.compressor.wkv.rows();
7072                        if ixr.compressor.overlap { w / 2 } else { w }
7073                    };
7074                    let idx_new = txn.dev_n_ix[li]
7075                        + (0..=t).filter(|k| (pos0 + k + 1) % ratio == 0).count()
7076                        - 1;
7077                    if let Some(v) =
7078                        crate::gpu_wgpu::dsv4_dbg_read_ix(st.kv_id, li, idx_new * ew, ew.min(8))
7079                    {
7080                        let sum: f32 = v.iter().sum();
7081                        eprintln!(
7082                            "[fold] li={li} pos={} entry={idx_new} head={:?} sum={sum:.6}",
7083                            pos0 + t,
7084                            &v[..4.min(v.len())]
7085                        );
7086                    }
7087                }
7088            }
7089        }
7090    }
7091
7092    // ── host tail + every position's head ──
7093    let mut scratch = HcScratch::new(cfg);
7094    argmax_out.clear();
7095    logits_out.clear();
7096    logits_out.resize(b * cfg.vocab, 0.0);
7097    let mut head_in = vec![0.0f32; b * dim];
7098    let mut host_steps: Vec<(usize, Vec<HostLayerSnap>)> = Vec::new();
7099    host_tail_walk_batch(
7100        g,
7101        layers,
7102        cfg,
7103        st,
7104        device_end,
7105        &mut states,
7106        ids,
7107        pos0,
7108        b,
7109        inv_freq,
7110        &mut scratch,
7111        pool,
7112        Some(&mut host_steps),
7113    );
7114    txn.host_steps = host_steps;
7115    for t in 0..b {
7116        let state = &states[t * hc * dim..(t + 1) * hc * dim];
7117        let h = &mut head_in[t * dim..(t + 1) * dim];
7118        hc_head_fold(
7119            state,
7120            &g.hc_head_fn,
7121            g.hc_head_scale,
7122            &g.hc_head_base,
7123            cfg,
7124            pool,
7125            h,
7126        );
7127        rms_weighted(h, &g.norm, cfg.norm_eps);
7128    }
7129    // The experimental B-wide head uses a different reduction kernel from
7130    // ordinary decode.  On the release q4tp it changed row-zero argmax under
7131    // a force-reject transaction, so it is diagnostic-only until parity is
7132    // proven; speculative execution must inherit the canonical head exactly.
7133    let batch_head = std::env::var("CMF_DSV4_SPEC_BATCH_HEAD").is_ok_and(|v| v != "0");
7134    let head_gpu = batch_head
7135        && g.head.model_idx().is_some_and(|hi| {
7136            let model = layers[0].experts.first().and_then(|e| e.w1.model_arc());
7137            model.is_some_and(|m| {
7138                crate::gpu_wgpu::q4tp_matvec_batch_for_test(
7139                    &m, hi, &head_in, b, cfg.vocab, dim, logits_out,
7140                )
7141            })
7142        });
7143    for t in 0..b {
7144        if !head_gpu {
7145            let h = &head_in[t * dim..(t + 1) * dim];
7146            g.head
7147                .matvec(h, &mut logits_out[t * cfg.vocab..(t + 1) * cfg.vocab], pool);
7148        }
7149        let row = &logits_out[t * cfg.vocab..(t + 1) * cfg.vocab];
7150        let mut best = 0usize;
7151        for v in 1..cfg.vocab {
7152            if row[v] > row[best] {
7153                best = v;
7154            }
7155        }
7156        argmax_out.push(best as u32);
7157    }
7158    walked_out.clear();
7159    walked_out.extend_from_slice(&states);
7160    st.pos = pos0 + b;
7161    if spec_time {
7162        eprintln!(
7163            "verify: тень+сид+цепочка {:.1} мс, хвост+голова {:.1} мс",
7164            t_chain.as_secs_f64() * 1e3,
7165            (t0.elapsed() - t_chain).as_secs_f64() * 1e3,
7166        );
7167    }
7168    Some(txn)
7169}
7170
7171/// Keep the accepted prefix of a verify pass and put everything else back.
7172///
7173/// `accepted` counts the FED tokens whose state stays (at least 1 — the
7174/// first fed token was already committed by the caller). With
7175/// `accepted == batch` this is free; otherwise the device restores its
7176/// snapshot and replays the accepted tokens' state appends, and the host
7177/// tail re-walks them.
7178#[cfg(feature = "gpu")]
7179pub fn dsv4_spec_finish(
7180    g: &Dsv4Globals,
7181    layers: &[Dsv4Layer],
7182    cfg: &Dsv4Cfg,
7183    st: &mut Dsv4State,
7184    mut txn: Dsv4SpecTxn,
7185    accepted: usize,
7186    ids: &[u32],
7187    inv_freq: &[f32],
7188    pool: Option<&crate::pool::Pool>,
7189) -> bool {
7190    macro_rules! sfail {
7191        ($($t:tt)*) => {{
7192            if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
7193                eprintln!("spec_finish: {}", format_args!($($t)*));
7194            }
7195            return false;
7196        }};
7197    }
7198    let b = txn.batch;
7199    let k = accepted.min(b);
7200    let (hc, dim, hd) = (cfg.hc_mult, cfg.dim, cfg.head_dim);
7201    // The staged batch never slid the windows; land the accepted prefix now,
7202    // whatever k is.
7203    let win_metas: Vec<(usize, usize, usize, usize)> = (0..txn.gpu_end)
7204        .map(|li| (li, txn.dev_filled[li], cfg.window, hd))
7205        .collect();
7206    if !crate::gpu_wgpu::dsv4_spec_commit_windows(st.kv_id, &win_metas, b, k) {
7207        sfail!("коммит окон");
7208    }
7209    if k == b {
7210        // Every stream mutation was the walk's own kernels in walk order —
7211        // nothing to put back.
7212        return true;
7213    }
7214    // ── device: restore to the snapshot, then replay the accepted tokens ──
7215    let Some(sh) = txn.shadow.take() else {
7216        sfail!("нет тени")
7217    };
7218    if !crate::gpu_wgpu::dsv4_spec_restore(&sh) {
7219        sfail!("restore");
7220    }
7221    let Some(model) = layers[0].experts.first().and_then(|e| e.w1.model_arc()) else {
7222        sfail!("нет модели");
7223    };
7224    let mut plan: Vec<(usize, crate::gpu_wgpu::Dsv4Prep)> = Vec::new();
7225    let mut freqs_own: Vec<&[f32]> = Vec::new();
7226    for li in 0..txn.gpu_end {
7227        let l = &layers[li];
7228        let Some(wkv) = l.wkv.model_idx() else {
7229            sfail!("wkv слоя {li}")
7230        };
7231        let comp = match &l.compressor {
7232            None => None,
7233            Some(cp) => {
7234                let (Some(a), Some(bx)) = (cp.wkv.model_idx(), cp.wgate.model_idx()) else {
7235                    sfail!("компрессор слоя {li}");
7236                };
7237                Some((
7238                    crate::gpu_wgpu::Dsv4CompW {
7239                        wkv: a,
7240                        wgate: bx,
7241                        norm: &cp.norm,
7242                        ape: &cp.ape,
7243                    },
7244                    crate::gpu_wgpu::Dsv4CompGeom {
7245                        width: cp.wkv.rows(),
7246                        hidden: dim,
7247                        ratio: cp.ratio,
7248                        overlap: cp.overlap,
7249                        rope_dim: cfg.rope_head_dim,
7250                        eps: cfg.norm_eps,
7251                    },
7252                ))
7253            }
7254        };
7255        let ix = match &l.indexer {
7256            None => None,
7257            Some(ixr) => {
7258                let cp = &ixr.compressor;
7259                let (Some(a), Some(bx), Some(qb), Some(wp)) = (
7260                    cp.wkv.model_idx(),
7261                    cp.wgate.model_idx(),
7262                    ixr.wq_b.model_idx(),
7263                    ixr.weights_proj.model_idx(),
7264                ) else {
7265                    sfail!("индексер слоя {li}");
7266                };
7267                let ih = ixr.weights_proj.rows();
7268                Some((
7269                    crate::gpu_wgpu::Dsv4CompW {
7270                        wkv: a,
7271                        wgate: bx,
7272                        norm: &cp.norm,
7273                        ape: &cp.ape,
7274                    },
7275                    crate::gpu_wgpu::Dsv4CompGeom {
7276                        width: cp.wkv.rows(),
7277                        hidden: dim,
7278                        ratio: cp.ratio,
7279                        overlap: cp.overlap,
7280                        rope_dim: cfg.rope_head_dim,
7281                        eps: cfg.norm_eps,
7282                    },
7283                    crate::gpu_wgpu::Dsv4IxW {
7284                        wq_b: qb,
7285                        weights_proj: wp,
7286                    },
7287                    crate::gpu_wgpu::Dsv4IxGeom {
7288                        ih,
7289                        idim: ixr.wq_b.rows() / ih.max(1),
7290                        q_lora: cfg.q_lora_rank,
7291                        hidden: dim,
7292                        rope_dim: cfg.rope_head_dim,
7293                        eps: cfg.norm_eps,
7294                        top_k: cfg.index_topk,
7295                        window: cfg.window,
7296                    },
7297                ))
7298            }
7299        };
7300        let ew_c = comp.as_ref().map_or(
7301            0,
7302            |(_, cg)| {
7303                if cg.overlap { cg.width / 2 } else { cg.width }
7304            },
7305        );
7306        let ew_i = ix.as_ref().map_or(
7307            0,
7308            |(_, cg, _, _)| {
7309                if cg.overlap { cg.width / 2 } else { cg.width }
7310            },
7311        );
7312        let prep = crate::gpu_wgpu::Dsv4Prep {
7313            wkv,
7314            kv_norm: &l.kv_norm,
7315            comp,
7316            ix,
7317            filled: txn.dev_filled[li],
7318            window: cfg.window,
7319            n_comp: txn.dev_n_comp[li],
7320            n_ix: txn.dev_n_ix[li],
7321            comp_dst_off: cfg.window * hd + txn.dev_n_comp[li] * ew_c,
7322            ix_dst_off: txn.dev_n_ix[li] * ew_i,
7323            idx_cap: cfg.window
7324                + if l.indexer.is_some() {
7325                    cfg.index_topk
7326                } else {
7327                    0
7328                },
7329        };
7330        let fr = if l.compressor.is_some() {
7331            g.inv_freq_compress.as_slice()
7332        } else {
7333            g.inv_freq_window.as_slice()
7334        };
7335        freqs_own.push(if fr.is_empty() { inv_freq } else { fr });
7336        plan.push((li, prep));
7337    }
7338    if !crate::gpu_wgpu::dsv4_spec_replay(
7339        &model,
7340        &plan,
7341        st.kv_id,
7342        txn.pos0,
7343        b,
7344        k,
7345        &freqs_own,
7346        hd,
7347        dim,
7348        cfg.rope_head_dim,
7349        cfg.norm_eps,
7350        true,
7351    ) {
7352        sfail!("replay k={k}");
7353    }
7354    // ── host counts: the snapshot advanced by k tokens ──
7355    let advanced = |ratio: usize| -> usize {
7356        if ratio == 0 {
7357            return 0;
7358        }
7359        (0..k).filter(|t| (txn.pos0 + t + 1) % ratio == 0).count()
7360    };
7361    for li in 0..txn.gpu_end {
7362        let l = &layers[li];
7363        st.dev_filled[li] = (txn.dev_filled[li] + k).min(cfg.window);
7364        let ac = l.compressor.as_ref().map_or(0, |cp| advanced(cp.ratio));
7365        let ai = l
7366            .indexer
7367            .as_ref()
7368            .map_or(0, |ix| advanced(ix.compressor.ratio));
7369        st.dev_n_comp[li] = txn.dev_n_comp[li] + ac;
7370        st.dev_n_ix[li] = txn.dev_n_ix[li] + ai;
7371        note_compressed(st.kv_id, li, st.dev_n_comp[li]);
7372    }
7373    // ── host tail: the verify pass already walked these tokens; restore
7374    //    the per-token snapshot it took instead of walking them again. ──
7375    if k >= 1 && txn.host_steps.iter().all(|(_, v)| v.len() >= k) && !txn.host_steps.is_empty() {
7376        for (li, v) in &txn.host_steps {
7377            host_restore(st, *li, &v[k - 1]);
7378        }
7379    } else {
7380        for (li, snap) in &txn.host {
7381            host_restore(st, *li, snap);
7382        }
7383        let mut scratch = HcScratch::new(cfg);
7384        let mut states = txn.states.clone();
7385        host_tail_walk_batch(
7386            g,
7387            layers,
7388            cfg,
7389            st,
7390            txn.gpu_end,
7391            &mut states[..k * hc * dim],
7392            ids,
7393            txn.pos0,
7394            k,
7395            inv_freq,
7396            &mut scratch,
7397            pool,
7398            None,
7399        );
7400    }
7401    st.pos = txn.pos0 + k;
7402    true
7403}
7404
7405pub fn forward_chunk(
7406    g: &Dsv4Globals,
7407    layers: &[Dsv4Layer],
7408    cfg: &Dsv4Cfg,
7409    st: &mut Dsv4State,
7410    ids: &[u32],
7411    pos0: usize,
7412    inv_freq: &[f32],
7413    pool: Option<&crate::pool::Pool>,
7414    logits: &mut Vec<f32>,
7415    want_logits: bool,
7416) {
7417    let bs = batch_prefill();
7418    if bs > 1 {
7419        // The first token walks, always. The batch will only run where every
7420        // layer has already proved it takes the card, and that proof is a
7421        // completed single-token run — with the whole prompt arriving as one
7422        // chunk there is otherwise no first run to give it, and the batch
7423        // declines for the entire prompt while a gate comparing it against
7424        // the walk reports agreement it never tested.
7425        let mut i = 0;
7426        if !st.dev_owned && !ids.is_empty() {
7427            st.pos = pos0;
7428            forward_token_inner(
7429                g,
7430                layers,
7431                cfg,
7432                st,
7433                ids[0],
7434                inv_freq,
7435                pool,
7436                logits,
7437                ids.len() == 1,
7438            );
7439            i = 1;
7440        }
7441        while i < ids.len() {
7442            let end = (i + bs).min(ids.len());
7443            st.pos = pos0 + i;
7444            if !forward_chunk_batched(
7445                g,
7446                layers,
7447                cfg,
7448                st,
7449                &ids[i..end],
7450                pos0 + i,
7451                inv_freq,
7452                pool,
7453                logits,
7454                want_logits && end == ids.len(),
7455            ) {
7456                break;
7457            }
7458            i = end;
7459        }
7460        if i == ids.len() {
7461            return;
7462        }
7463        // Refused before touching anything; the walk starts where it left off.
7464        for (k, &id) in ids.iter().enumerate().skip(i) {
7465            st.pos = pos0 + k;
7466            let last = want_logits && k + 1 == ids.len();
7467            forward_token_inner(g, layers, cfg, st, id, inv_freq, pool, logits, last);
7468        }
7469        return;
7470    }
7471    for (i, &id) in ids.iter().enumerate() {
7472        st.pos = pos0 + i;
7473        let last = want_logits && i + 1 == ids.len();
7474        forward_token_inner(g, layers, cfg, st, id, inv_freq, pool, logits, last);
7475    }
7476}
7477
7478pub fn forward_token(
7479    g: &Dsv4Globals,
7480    layers: &[Dsv4Layer],
7481    cfg: &Dsv4Cfg,
7482    st: &mut Dsv4State,
7483    token_id: u32,
7484    inv_freq: &[f32],
7485    pool: Option<&crate::pool::Pool>,
7486    logits: &mut Vec<f32>,
7487) {
7488    forward_token_inner(g, layers, cfg, st, token_id, inv_freq, pool, logits, true);
7489}
7490
7491#[allow(clippy::too_many_arguments)]
7492fn forward_token_inner(
7493    g: &Dsv4Globals,
7494    layers: &[Dsv4Layer],
7495    cfg: &Dsv4Cfg,
7496    st: &mut Dsv4State,
7497    token_id: u32,
7498    inv_freq: &[f32],
7499    pool: Option<&crate::pool::Pool>,
7500    logits: &mut Vec<f32>,
7501    // Prompt tokens other than the last one have their logits thrown away.
7502    want_logits: bool,
7503) {
7504    let _t_all = prof::on().then(std::time::Instant::now);
7505    let _all_guard = Charge(_t_all, &prof::ALL_NS);
7506    let (hc, dim) = (cfg.hc_mult, cfg.dim);
7507
7508    // Embedding, replicated into the copies.
7509    let mut emb = vec![0.0f32; dim];
7510    g.embed.row_f32(token_id as usize, &mut emb);
7511    let mut state = vec![0.0f32; hc * dim];
7512    for j in 0..hc {
7513        state[j * dim..(j + 1) * dim].copy_from_slice(&emb);
7514    }
7515
7516    let mut scratch = HcScratch::new(cfg);
7517    let mut dump: Vec<String> = Vec::new();
7518    if dump_path().is_some() {
7519        dump.push(format!("\"embed\":{}", vec_json(&emb)));
7520        PICKED.with(|p| p.borrow_mut().clear());
7521        BODY.with(|b| b.borrow_mut().clear());
7522        dump.push(",\"layers\":[".into());
7523    }
7524    if trace_on() {
7525        eprintln!(
7526            "[dsv4] tok={token_id} pos={} embed rms={:.5}",
7527            st.pos,
7528            rms_of(&emb)
7529        );
7530    }
7531    // ── one submission per layer, when the device will take it ──
7532    #[cfg(feature = "gpu")]
7533    let layer_frames = gpu_layer_enabled()
7534        && dsv4_layer_loop(
7535            &mut state,
7536            layers,
7537            g,
7538            cfg,
7539            st,
7540            token_id,
7541            inv_freq,
7542            pool,
7543            &mut scratch,
7544        );
7545    #[cfg(not(feature = "gpu"))]
7546    let layer_frames = false;
7547
7548    // ── the fast two-frame path: hyper-connections on the card ──
7549    // Measured on the release, the fold, the Sinkhorn and the norms cost 19
7550    // ms of a 57 ms token on the host and hundredths of one on the device.
7551    // With both frames doing their own, the host carries nothing between a
7552    // layer's halves and the MoE half's input never leaves the card — one
7553    // readback a layer instead of two.
7554    #[cfg(feature = "gpu")]
7555    let hc_dev = hc_on_device()
7556        && !layer_frames
7557        && gpu_attn_enabled()
7558        && gpu_moe2_enabled()
7559        && dump_path().is_none();
7560    #[cfg(not(feature = "gpu"))]
7561    let hc_dev = false;
7562    // The device loop's verdict as a VALUE, not as a cfg-gated `if`. It used
7563    // to be the latter, with the CPU loop in the `else` arm — so a build
7564    // without the gpu feature compiled no layer loop at all and every token
7565    // passed through untouched. The window test said so ("sliding window
7566    // never filled") and only in the CPU-only build, which is the one
7567    // configuration the gate was not running.
7568    #[cfg(feature = "gpu")]
7569    let two_frame_done = hc_dev
7570        && dsv4_two_frame_loop(
7571            &mut state,
7572            layers,
7573            g,
7574            cfg,
7575            st,
7576            token_id,
7577            inv_freq,
7578            pool,
7579            &mut scratch,
7580        );
7581    #[cfg(not(feature = "gpu"))]
7582    let two_frame_done = false;
7583    if !two_frame_done {
7584        for (li, l) in layers.iter().enumerate() {
7585            if layer_frames {
7586                break;
7587            }
7588            // attention half
7589            hc_block(
7590                &mut state,
7591                &l.hc_attn_fn,
7592                &l.hc_attn_scale,
7593                &l.hc_attn_base,
7594                &l.attn_norm,
7595                cfg,
7596                &mut scratch,
7597                pool,
7598                |folded, out| {
7599                    if dump_path().is_some() {
7600                        // The body's own input and output, so the reference can be
7601                        // fed the port's input: then only the body can differ.
7602                        BODY.with(|b| b.borrow_mut().push(vec_json(folded)));
7603                    }
7604                    // The layer's kind decides its frequencies, not the model's.
7605                    let freqs = if l.compressor.is_some() {
7606                        &g.inv_freq_compress
7607                    } else {
7608                        &g.inv_freq_window
7609                    };
7610                    let freqs = if freqs.is_empty() {
7611                        inv_freq
7612                    } else {
7613                        freqs.as_slice()
7614                    };
7615                    attention_step(folded, l, cfg, st, li, freqs, pool, None, out);
7616                    if dump_path().is_some() {
7617                        BODY.with(|b| b.borrow_mut().push(vec_json(out)));
7618                    }
7619                },
7620            );
7621            if dump_path().is_some() {
7622                // After the attention half only — this is what separates an
7623                // attention discrepancy from an expert one.
7624                dump.push(format!(
7625                    "{}{}",
7626                    if li == 0 { "" } else { "," },
7627                    vec_json(&state)
7628                ));
7629            }
7630            // FFN half
7631            let _t_hc2 = prof::on().then(std::time::Instant::now);
7632            hc_block(
7633                &mut state,
7634                &l.hc_ffn_fn,
7635                &l.hc_ffn_scale,
7636                &l.hc_ffn_base,
7637                &l.ffn_norm,
7638                cfg,
7639                &mut scratch,
7640                pool,
7641                |folded, out| moe_step(folded, l, cfg, token_id, li, pool, out),
7642            );
7643            if let Some(t) = _t_hc2 {
7644                // The block's own time minus the expert step inside it — what the
7645                // fold, the norm and the expand cost on their own.
7646                prof::HC_NS.fetch_add(
7647                    t.elapsed().as_nanos() as u64,
7648                    std::sync::atomic::Ordering::Relaxed,
7649                );
7650            }
7651            if dump_path().is_some() {
7652                dump.push(format!(",{}", vec_json(&state)));
7653            }
7654            if trace_on() && (st.pos % 64 == 0 || st.pos == 199) {
7655                eprintln!(
7656                    "[dsv4]  кеши слоя {li}: окно={} сжатых={} индекс={} (ratio={:?})",
7657                    st.window[li].len() / cfg.head_dim.max(1),
7658                    st.compressed[li].len() / cfg.head_dim.max(1),
7659                    st.index_kv[li].len().max(1) / 128,
7660                    l.compressor.as_ref().map(|c| (c.ratio, c.overlap)),
7661                );
7662            }
7663            if trace_on() {
7664                let bad = state.iter().filter(|v| !v.is_finite()).count();
7665                eprintln!(
7666                    "[dsv4]  layer {li:>2}: rms={:.5}{}",
7667                    rms_of(&state),
7668                    if bad > 0 {
7669                        format!("  NON-FINITE x{bad}")
7670                    } else {
7671                        String::new()
7672                    }
7673                );
7674            }
7675            dspark_note(li, &state, cfg);
7676        }
7677    }
7678    st.pos += 1;
7679
7680    // Collapse the copies, normalize, project to the vocabulary.
7681    let mut h = vec![0.0f32; dim];
7682    hc_head_fold(
7683        &state,
7684        &g.hc_head_fn,
7685        g.hc_head_scale,
7686        &g.hc_head_base,
7687        cfg,
7688        pool,
7689        &mut h,
7690    );
7691    if !want_logits {
7692        logits.clear();
7693        return;
7694    }
7695    let _t_head = prof::on().then(std::time::Instant::now);
7696    rms_weighted(&mut h, &g.norm, cfg.norm_eps);
7697    logits.clear();
7698    logits.resize(g.head.rows(), 0.0);
7699    g.head.matvec(&h, logits, pool);
7700    if let Some(t) = _t_head {
7701        prof::HEAD_NS.fetch_add(
7702            t.elapsed().as_nanos() as u64,
7703            std::sync::atomic::Ordering::Relaxed,
7704        );
7705    }
7706    if dump_path().is_some() {
7707        dump.push("]".into());
7708        let picked = PICKED.with(|p| {
7709            p.borrow()
7710                .iter()
7711                .map(|v| {
7712                    format!(
7713                        "[{}]",
7714                        v.iter()
7715                            .map(|e| e.to_string())
7716                            .collect::<Vec<_>>()
7717                            .join(",")
7718                    )
7719                })
7720                .collect::<Vec<_>>()
7721                .join(",")
7722        });
7723        dump.push(format!(",\"experts\":[{picked}]"));
7724        let body = BODY.with(|b| b.borrow().join(","));
7725        dump.push(format!(",\"attn_io\":[{body}]"));
7726        dump_line(&format!(
7727            "{{\"tok\":{token_id},\"pos\":{},{},\"head\":{},\"logits\":{}}}",
7728            st.pos - 1,
7729            dump.join(""),
7730            vec_json(&h),
7731            vec_json(logits)
7732        ));
7733    }
7734    if trace_on() {
7735        let (mut top, mut best) = (0usize, f32::NEG_INFINITY);
7736        for (i, &v) in logits.iter().enumerate() {
7737            if v > best {
7738                best = v;
7739                top = i;
7740            }
7741        }
7742        let lo = logits.iter().cloned().fold(f32::MAX, f32::min);
7743        eprintln!(
7744            "[dsv4]  head: rms={:.5} logits[{}..{:.3}] argmax={top}",
7745            rms_of(&h),
7746            format_args!("{lo:.3}"),
7747            best
7748        );
7749    }
7750}
7751
7752/// Build the runtime weights from a converted `.cmf`.
7753///
7754/// Names are the converter's output (see `canon_name`'s deepseek_v4 arm):
7755/// attention keeps DeepSeek's own spelling under `self_attn.`, the MoE is
7756/// rewritten into the layout every other MoE here uses, and the hyper-
7757/// connection tensors ride under the layer prefix.
7758pub fn load(
7759    model: &std::sync::Arc<cortiq_core::CmfModel>,
7760    cfg: &Dsv4Cfg,
7761    n_layers: usize,
7762) -> Result<(Dsv4Globals, Vec<Dsv4Layer>), String> {
7763    let q = |name: &str| -> Result<crate::qtensor::QTensor, String> {
7764        crate::qtensor::QTensor::from_model(model, name)
7765    };
7766    // The small pieces — norms, the sink, ape, the hyper-connection
7767    // projections — are read as plain f32. They are not all 2-D (a norm is a
7768    // vector), so this cannot go through QTensor, which requires a matrix.
7769    let f = |name: &str| -> Result<Vec<f32>, String> {
7770        crate::loader::load_f32(model, name, &crate::loader::Overlay::None)
7771    };
7772
7773    // Two frequency tables, chosen per layer by whether it compresses. The
7774    // release's compress_rope_theta (160 000) is not in config.json — it
7775    // lives in inference/config.json — so it is pinned here with the other
7776    // constants the header cannot carry.
7777    let rope_of = |base: f32, yarn: bool| -> Vec<f32> {
7778        if yarn {
7779            crate::attention::yarn_inv_freq(cfg.rope_head_dim, base, 16.0, 65536, 32.0, 1.0)
7780        } else {
7781            crate::attention::rope_inv_freq(cfg.rope_head_dim, base)
7782        }
7783    };
7784    let globals = Dsv4Globals {
7785        inv_freq_compress: rope_of(160_000.0, true),
7786        inv_freq_window: rope_of(10_000.0, false),
7787        embed: q("model.embed_tokens.weight")?,
7788        norm: f("model.norm.weight")?,
7789        head: q("lm_head.weight")?,
7790        hc_head_fn: f("model.hc_head_fn")?,
7791        hc_head_base: f("model.hc_head_base")?,
7792        hc_head_scale: *f("model.hc_head_scale")?
7793            .first()
7794            .ok_or("dsv4: empty hc_head_scale")?,
7795    };
7796
7797    let mut layers = Vec::with_capacity(n_layers);
7798    for li in 0..n_layers {
7799        layers.push(load_layer(
7800            model,
7801            cfg,
7802            &format!("model.layers.{li}"),
7803            Scheme::Main,
7804        )?);
7805    }
7806    // The projection this loader exists to serve: with a RAM tier configured,
7807    // pin the MASKED expert set with one sequential sweep of the file at
7808    // streaming rate, before decode discovers it miss by miss in random
7809    // order. Experts outside a layer's mask are skipped; a layer without a
7810    // mask keeps all of its experts (the budget caps the sweep).
7811    #[cfg(feature = "gpu")]
7812    if crate::gpu_wgpu::host_banks_on() {
7813        // Host banks: one background sweep, layer by layer, oldest first.
7814        let sets: Vec<(usize, Vec<(usize, usize, usize)>, bool)> = layers
7815            .iter()
7816            .filter_map(|l| {
7817                let idx3 = |e: &Dsv4Expert| {
7818                    Some((e.w1.model_idx()?, e.w3.model_idx()?, e.w2.model_idx()?))
7819                };
7820                let mut v: Vec<_> = l.experts.iter().filter_map(idx3).collect();
7821                let first = v.first()?.0;
7822                v.push(idx3(&l.shared)?);
7823                let gu_q2 =
7824                    l.experts.first()?.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP);
7825                Some((first, v, gu_q2))
7826            })
7827            .collect();
7828        let m2 = model.clone();
7829        let (inter, dim) = (cfg.moe_inter, cfg.dim);
7830        std::thread::spawn(move || {
7831            for (first, v, gu_q2) in sets {
7832                crate::gpu_wgpu::dsv4_host_bank_build(&m2, first, &v, inter, dim, gu_q2);
7833            }
7834            tracing::info!("host banks: built");
7835        });
7836    }
7837    #[cfg(feature = "gpu")]
7838    {
7839        let masks: Vec<Option<Vec<bool>>> = layers.iter().map(|l| l.mask.clone()).collect();
7840        crate::gpu_wgpu::prefetch_tier(model, &|name: &str| {
7841            let Some(i) = name.find(".experts.") else {
7842                return false;
7843            };
7844            let rest = &name[i + 9..];
7845            let e: usize = match rest[..rest.find('.').unwrap_or(rest.len())].parse() {
7846                Ok(v) => v,
7847                Err(_) => return false,
7848            };
7849            let li: usize = {
7850                let Some(j) = name.find("layers.") else {
7851                    return false;
7852                };
7853                let r = &name[j + 7..];
7854                match r[..r.find('.').unwrap_or(r.len())].parse() {
7855                    Ok(v) => v,
7856                    Err(_) => return false,
7857                }
7858            };
7859            match masks.get(li).and_then(|m| m.as_ref()) {
7860                Some(m) => m.get(e).copied().unwrap_or(false),
7861                None => true,
7862            }
7863        });
7864    }
7865    Ok((globals, layers))
7866}
7867
7868/// Where a layer's tensors live in the file.
7869///
7870/// The MTP modules are the same layer as any other — attention, a
7871/// hyper-connection pair, a gated MoE over 256 experts — but the converter
7872/// wrote them under DeepSeek's internal names rather than the HF ones it used
7873/// for the trunk. Two schemes, one loader: a second copy would drift.
7874#[derive(Clone, Copy, PartialEq, Eq, Debug)]
7875pub enum Scheme {
7876    Main,
7877    Mtp,
7878}
7879
7880impl Scheme {
7881    fn attn(self) -> &'static str {
7882        match self {
7883            Scheme::Main => "self_attn",
7884            Scheme::Mtp => "attn",
7885        }
7886    }
7887    fn attn_norm(self) -> &'static str {
7888        match self {
7889            Scheme::Main => "input_layernorm.weight",
7890            Scheme::Mtp => "attn_norm.weight",
7891        }
7892    }
7893    fn ffn_norm(self) -> &'static str {
7894        match self {
7895            Scheme::Main => "post_attention_layernorm.weight",
7896            Scheme::Mtp => "ffn_norm.weight",
7897        }
7898    }
7899    fn mlp(self) -> &'static str {
7900        match self {
7901            Scheme::Main => "mlp",
7902            Scheme::Mtp => "ffn",
7903        }
7904    }
7905    /// The router's per-expert bias. Absent on the trunk's hash layers, which
7906    /// is how they are recognised; always present on an MTP module.
7907    fn gate_bias(self) -> &'static str {
7908        match self {
7909            Scheme::Main => "expert_bias",
7910            Scheme::Mtp => "gate.bias",
7911        }
7912    }
7913    fn shared(self) -> &'static str {
7914        match self {
7915            Scheme::Main => "shared_expert",
7916            Scheme::Mtp => "shared_experts",
7917        }
7918    }
7919    /// gate, down, up — in that order, which is w1/w2/w3 upstream.
7920    fn w(self, i: u8) -> &'static str {
7921        match (self, i) {
7922            (Scheme::Main, 1) => "gate_proj.weight",
7923            (Scheme::Main, 2) => "down_proj.weight",
7924            (Scheme::Main, _) => "up_proj.weight",
7925            (Scheme::Mtp, 1) => "w1.weight",
7926            (Scheme::Mtp, 2) => "w2.weight",
7927            (Scheme::Mtp, _) => "w3.weight",
7928        }
7929    }
7930}
7931
7932/// One layer, wherever it lives in the file.
7933pub fn load_layer(
7934    model: &std::sync::Arc<cortiq_core::CmfModel>,
7935    cfg: &Dsv4Cfg,
7936    p: &str,
7937    s: Scheme,
7938) -> Result<Dsv4Layer, String> {
7939    let q = |name: &str| -> Result<crate::qtensor::QTensor, String> {
7940        crate::qtensor::QTensor::from_model(model, name)
7941    };
7942    let f = |name: &str| -> Result<Vec<f32>, String> {
7943        crate::loader::load_f32(model, name, &crate::loader::Overlay::None)
7944    };
7945    let opt_f = |name: &str| -> Option<Vec<f32>> { f(name).ok() };
7946    let at = s.attn();
7947    let ml = s.mlp();
7948    {
7949        let scale3 = |name: &str| -> Result<[f32; 3], String> {
7950            let v = f(name)?;
7951            if v.len() < 3 {
7952                return Err(format!("{name}: expected 3 scales, got {}", v.len()));
7953            }
7954            Ok([v[0], v[1], v[2]])
7955        };
7956        // The compressor exists on every layer whose ratio is non-zero;
7957        // its presence in the file is the only signal we need.
7958        let compressor = match q(&format!("{p}.{at}.compressor.wkv.weight")) {
7959            Ok(wkv) => {
7960                let ape = f(&format!("{p}.{at}.compressor.ape"))?;
7961                // ape is [ratio, coff*head_dim]; coff is 2 when the windows
7962                // overlap, which the release does at ratio 4.
7963                let width = wkv.rows();
7964                let ratio = (ape.len() / width.max(1)).max(1);
7965                Some(Dsv4Compressor {
7966                    wkv,
7967                    wgate: q(&format!("{p}.{at}.compressor.wgate.weight"))?,
7968                    norm: f(&format!("{p}.{at}.compressor.norm.weight"))?,
7969                    ape,
7970                    ratio,
7971                    overlap: ratio == 4,
7972                })
7973            }
7974            Err(_) => None,
7975        };
7976        let indexer = match q(&format!("{p}.{at}.indexer.wq_b.weight")) {
7977            Ok(wq_b) => {
7978                let ape = f(&format!("{p}.{at}.indexer.compressor.ape"))?;
7979                let cwkv = q(&format!("{p}.{at}.indexer.compressor.wkv.weight"))?;
7980                let width = cwkv.rows();
7981                let ratio = (ape.len() / width.max(1)).max(1);
7982                Some(Dsv4Indexer {
7983                    wq_b,
7984                    weights_proj: q(&format!("{p}.{at}.indexer.weights_proj.weight"))?,
7985                    compressor: Dsv4Compressor {
7986                        wkv: cwkv,
7987                        wgate: q(&format!("{p}.{at}.indexer.compressor.wgate.weight"))?,
7988                        norm: f(&format!("{p}.{at}.indexer.compressor.norm.weight"))?,
7989                        ape,
7990                        ratio,
7991                        overlap: ratio == 4,
7992                    },
7993                })
7994            }
7995            Err(_) => None,
7996        };
7997
7998        let mut experts = Vec::with_capacity(cfg.n_routed_experts);
7999        for e in 0..cfg.n_routed_experts {
8000            let ep = format!("{p}.{ml}.experts.{e}");
8001            experts.push(Dsv4Expert {
8002                w1: q(&format!("{ep}.{w}", w = s.w(1)))?,
8003                w2: q(&format!("{ep}.{w}", w = s.w(2)))?,
8004                w3: q(&format!("{ep}.{w}", w = s.w(3)))?,
8005            });
8006        }
8007
8008        Ok(Dsv4Layer {
8009            attn_norm: f(&format!("{p}.{an}", an = s.attn_norm()))?,
8010            ffn_norm: f(&format!("{p}.{fnm}", fnm = s.ffn_norm()))?,
8011            wq_a: q(&format!("{p}.{at}.wq_a.weight"))?,
8012            q_norm: f(&format!("{p}.{at}.q_norm.weight"))?,
8013            wq_b: q(&format!("{p}.{at}.wq_b.weight"))?,
8014            wkv: q(&format!("{p}.{at}.wkv.weight"))?,
8015            kv_norm: f(&format!("{p}.{at}.kv_norm.weight"))?,
8016            wo_a: q(&format!("{p}.{at}.wo_a.weight"))?,
8017            wo_b: q(&format!("{p}.{at}.wo_b.weight"))?,
8018            attn_sink: f(&format!("{p}.{at}.attn_sink"))?,
8019            compressor,
8020            indexer,
8021            hc_attn_fn: f(&format!("{p}.hc_attn_fn"))?,
8022            hc_attn_base: f(&format!("{p}.hc_attn_base"))?,
8023            hc_attn_scale: scale3(&format!("{p}.hc_attn_scale"))?,
8024            hc_ffn_fn: f(&format!("{p}.hc_ffn_fn"))?,
8025            hc_ffn_base: f(&format!("{p}.hc_ffn_base"))?,
8026            hc_ffn_scale: scale3(&format!("{p}.hc_ffn_scale"))?,
8027            gate: q(&format!("{p}.{ml}.gate.weight"))?,
8028            // The bias is absent exactly on the hash layers, and the table
8029            // is present exactly there — the file itself says which is which.
8030            gate_bias: opt_f(&format!("{p}.{ml}.{b}", b = s.gate_bias())),
8031            tid2eid: opt_f(&format!("{p}.{ml}.tid2eid")),
8032            experts,
8033            mask: if model.tensor(&format!("{p}.{ml}.tid2eid")).is_some() {
8034                None
8035            } else {
8036                crate::loader::moe_task_mask(model, &format!("{p}."), cfg.n_routed_experts)
8037            },
8038            shared: Dsv4Expert {
8039                w1: q(&format!("{p}.{ml}.{sh}.{w}", sh = s.shared(), w = s.w(1)))?,
8040                w2: q(&format!("{p}.{ml}.{sh}.{w}", sh = s.shared(), w = s.w(2)))?,
8041                w3: q(&format!("{p}.{ml}.{sh}.{w}", sh = s.shared(), w = s.w(3)))?,
8042            },
8043        })
8044    }
8045}
8046
8047/// One module of the speculation stack.
8048///
8049/// The release carries three, so the draft is three deep, and the last one
8050/// also holds a confidence head — the model scores its own proposals rather
8051/// than leaving acceptance to a threshold we would have to invent. Each
8052/// module is a full layer with its own 256 experts; what makes it an MTP
8053/// module rather than a 44th layer is `main_proj`, which folds the previous
8054/// hidden state into the next embedding before the layer runs.
8055pub struct Dsv4Mtp {
8056    pub layer: Dsv4Layer,
8057    /// Stage 0 only: the projection that turns the trunk's captured hidden
8058    /// states into the block's input. Later stages take the block from the
8059    /// stage before them, so they carry none.
8060    pub main_proj: Option<crate::qtensor::QTensor>,
8061    pub main_norm: Option<Vec<f32>>,
8062    /// Last module only: what turns a draft hidden state into logits.
8063    pub norm: Option<Vec<f32>>,
8064    pub hc_head_fn: Option<Vec<f32>>,
8065    pub hc_head_base: Option<Vec<f32>>,
8066    pub hc_head_scale: Option<f32>,
8067    pub confidence: Option<crate::qtensor::QTensor>,
8068    /// Last stage only: a rank-256 bigram table that biases the draft's
8069    /// logits, and whose embedding also feeds the confidence head. Cheap
8070    /// enough that the draft samples through it position by position while
8071    /// the network itself runs the whole block at once.
8072    pub markov_w1: Option<crate::qtensor::QTensor>,
8073    pub markov_w2: Option<crate::qtensor::QTensor>,
8074}
8075
8076/// Load as much of the speculation stack as the file carries, up to
8077/// `max_depth`. Missing is not an error: a checkpoint without MTP simply
8078/// yields an empty stack, and the caller falls back to plain decoding.
8079pub fn load_mtp(
8080    model: &std::sync::Arc<cortiq_core::CmfModel>,
8081    cfg: &Dsv4Cfg,
8082    max_depth: usize,
8083) -> Vec<Dsv4Mtp> {
8084    let f = |name: &str| -> Option<Vec<f32>> {
8085        crate::loader::load_f32(model, name, &crate::loader::Overlay::None).ok()
8086    };
8087    let mut out = Vec::new();
8088    for d in 0..max_depth {
8089        let p = format!("model.mtp.{d}");
8090        // A stage is recognised by its attention, not by `main_proj`: only
8091        // stage 0 has that, and only the last has the head. Keying on either
8092        // end found one module of three.
8093        if model.tensor(&format!("{p}.attn.wq_a.weight")).is_none() {
8094            break;
8095        }
8096        let layer = match load_layer(model, cfg, &p, Scheme::Mtp) {
8097            Ok(l) => l,
8098            Err(e) => {
8099                eprintln!("MTP {d}: пропущен, {e}");
8100                break;
8101            }
8102        };
8103        out.push(Dsv4Mtp {
8104            layer,
8105            main_proj: crate::qtensor::QTensor::from_model(model, &format!("{p}.main_proj.weight"))
8106                .ok(),
8107            main_norm: f(&format!("{p}.main_norm.weight")),
8108            norm: f(&format!("{p}.norm.weight")),
8109            hc_head_fn: f(&format!("{p}.hc_head_fn")),
8110            hc_head_base: f(&format!("{p}.hc_head_base")),
8111            hc_head_scale: f(&format!("{p}.hc_head_scale")).and_then(|v| v.first().copied()),
8112            confidence: crate::qtensor::QTensor::from_model(
8113                model,
8114                &format!("{p}.confidence_head.proj.weight"),
8115            )
8116            .ok(),
8117            markov_w1: crate::qtensor::QTensor::from_model(
8118                model,
8119                &format!("{p}.markov_head.markov_w1.weight"),
8120            )
8121            .ok(),
8122            markov_w2: crate::qtensor::QTensor::from_model(
8123                model,
8124                &format!("{p}.markov_head.markov_w2.weight"),
8125            )
8126            .ok(),
8127        });
8128    }
8129    dspark_apply_mask(&mut out);
8130    if !out.is_empty() {
8131        let mp = out
8132            .iter()
8133            .find_map(|m| m.main_proj.as_ref())
8134            .map(|t| format!("[{}, {}]", t.rows(), t.cols()))
8135            .unwrap_or_else(|| "нет".into());
8136        eprintln!(
8137            "MTP: {} стади(я/и/й), main_proj {mp}, экспертов {}, \
8138             голова уверенности {}, марков {}",
8139            out.len(),
8140            out[0].layer.experts.len(),
8141            if out.iter().any(|m| m.confidence.is_some()) {
8142                "есть"
8143            } else {
8144                "нет"
8145            },
8146            if out.iter().any(|m| m.markov_w1.is_some()) {
8147                "есть"
8148            } else {
8149                "нет"
8150            },
8151        );
8152    }
8153    out
8154}
8155
8156#[cfg(test)]
8157mod tests {
8158    use super::*;
8159
8160    // A whole model, small enough to reason about: 2 layers, 4 heads, 8
8161    // experts. Weights are deterministic and tiny, which is the point —
8162    // this test is about shapes, indexing and cache bookkeeping, the things
8163    // that a 138 GB file would surface only after an hour of loading.
8164    fn toy() -> (Dsv4Globals, Vec<Dsv4Layer>, Dsv4Cfg) {
8165        use crate::qtensor::QTensor;
8166        let cfg = Dsv4Cfg {
8167            dim: 32,
8168            n_heads: 4,
8169            head_dim: 8,
8170            rope_head_dim: 4,
8171            q_lora_rank: 16,
8172            o_lora_rank: 16,
8173            o_groups: 2,
8174            hc_mult: 4,
8175            hc_sinkhorn_iters: 20,
8176            hc_eps: 1e-6,
8177            norm_eps: 1e-6,
8178            n_routed_experts: 8,
8179            top_k: 2,
8180            moe_inter: 16,
8181            route_scale: 1.0,
8182            swiglu_limit: 10.0,
8183            window: 6,
8184            index_topk: 8,
8185            vocab: 24,
8186        };
8187        // Deterministic pseudo-random in a narrow band: big enough to move
8188        // the state, small enough that nothing saturates.
8189        let w = |n: usize, seed: usize| -> Vec<f32> {
8190            (0..n)
8191                .map(|i| (((i * 7 + seed * 13) % 101) as f32 / 101.0 - 0.5) * 0.3)
8192                .collect()
8193        };
8194        let t = |rows: usize, cols: usize, seed: usize| {
8195            QTensor::from_f32(w(rows * cols, seed), rows, cols)
8196        };
8197        let ones = |n: usize| vec![1.0f32; n];
8198
8199        let (dim, hc) = (cfg.dim, cfg.hc_mult);
8200        // q is n_heads*head_dim wide and kv is one head wide; rope rides the
8201        // tail of each rather than widening anything.
8202        let q_width = cfg.n_heads * cfg.head_dim;
8203        let kv_width = cfg.head_dim;
8204        let o_per_group = q_width / cfg.o_groups;
8205        let mut layers = Vec::new();
8206        for li in 0..2 {
8207            let experts: Vec<Dsv4Expert> = (0..cfg.n_routed_experts)
8208                .map(|e| Dsv4Expert {
8209                    w1: t(cfg.moe_inter, dim, 40 + e + li * 8),
8210                    w2: t(dim, cfg.moe_inter, 60 + e + li * 8),
8211                    w3: t(cfg.moe_inter, dim, 80 + e + li * 8),
8212                })
8213                .collect();
8214            // Layer 0 is a hash layer (table-routed); layer 1 routes normally
8215            // and carries the compressor — both paths get exercised.
8216            layers.push(Dsv4Layer {
8217                attn_norm: ones(dim),
8218                ffn_norm: ones(dim),
8219                wq_a: t(cfg.q_lora_rank, dim, 1 + li),
8220                q_norm: ones(cfg.q_lora_rank),
8221                wq_b: t(q_width, cfg.q_lora_rank, 3 + li),
8222                wkv: t(kv_width, dim, 5 + li),
8223                kv_norm: ones(kv_width),
8224                wo_a: t(cfg.o_groups * cfg.o_lora_rank, o_per_group, 7 + li),
8225                wo_b: t(dim, cfg.o_groups * cfg.o_lora_rank, 9 + li),
8226                attn_sink: vec![0.1; cfg.n_heads],
8227                // Layer 1 carries the OVERLAPPING compressor, as the release
8228                // does at ratio 4: the projection is twice the entry width.
8229                compressor: if li == 1 {
8230                    Some(Dsv4Compressor {
8231                        wkv: t(2 * kv_width, dim, 11),
8232                        wgate: t(2 * kv_width, dim, 13),
8233                        norm: ones(kv_width),
8234                        ape: vec![0.01; 4 * 2 * kv_width],
8235                        ratio: 4,
8236                        overlap: true,
8237                    })
8238                } else {
8239                    None
8240                },
8241                indexer: if li == 1 {
8242                    Some(Dsv4Indexer {
8243                        wq_b: t(2 * 16, cfg.q_lora_rank, 41),
8244                        weights_proj: t(2, dim, 43),
8245                        compressor: Dsv4Compressor {
8246                            wkv: t(2 * 16, dim, 45),
8247                            wgate: t(2 * 16, dim, 47),
8248                            norm: ones(16),
8249                            ape: vec![0.01; 4 * 2 * 16],
8250                            ratio: 4,
8251                            overlap: true,
8252                        },
8253                    })
8254                } else {
8255                    None
8256                },
8257                hc_attn_fn: w((2 + hc) * hc * hc * dim, 15 + li),
8258                hc_attn_base: w((2 + hc) * hc, 17 + li),
8259                hc_attn_scale: [1.0, 1.0, 1.0],
8260                hc_ffn_fn: w((2 + hc) * hc * hc * dim, 19 + li),
8261                hc_ffn_base: w((2 + hc) * hc, 21 + li),
8262                hc_ffn_scale: [1.0, 1.0, 1.0],
8263                gate: t(cfg.n_routed_experts, dim, 23 + li),
8264                gate_bias: if li == 1 {
8265                    Some(vec![0.0; cfg.n_routed_experts])
8266                } else {
8267                    None
8268                },
8269                tid2eid: if li == 0 {
8270                    Some(
8271                        (0..cfg.vocab * cfg.top_k)
8272                            .map(|i| (i % cfg.n_routed_experts) as f32)
8273                            .collect(),
8274                    )
8275                } else {
8276                    None
8277                },
8278                experts,
8279                mask: None,
8280                shared: Dsv4Expert {
8281                    w1: t(cfg.moe_inter, dim, 25 + li),
8282                    w2: t(dim, cfg.moe_inter, 27 + li),
8283                    w3: t(cfg.moe_inter, dim, 29 + li),
8284                },
8285            });
8286        }
8287        let inv = |base: f32| -> Vec<f32> {
8288            (0..cfg.rope_head_dim / 2)
8289                .map(|i| 1.0 / base.powf(2.0 * i as f32 / cfg.rope_head_dim as f32))
8290                .collect()
8291        };
8292        let g = Dsv4Globals {
8293            inv_freq_compress: inv(160000.0),
8294            inv_freq_window: inv(10000.0),
8295            embed: t(cfg.vocab, dim, 31),
8296            norm: ones(dim),
8297            head: t(cfg.vocab, dim, 33),
8298            hc_head_fn: w(hc * hc * dim, 35),
8299            hc_head_base: w(hc, 37),
8300            hc_head_scale: 1.0,
8301        };
8302        (g, layers, cfg)
8303    }
8304
8305    /// The whole stack, decoding a sequence. Every block is on the path:
8306    /// hyper-connections, the double-LoRA attention with its sink, the KV
8307    /// compressor firing on its ratio boundary, hash routing on one layer
8308    /// and score routing on the other.
8309    #[test]
8310    fn forward_token_decodes_a_sequence_without_falling_over() {
8311        let (g, layers, cfg) = toy();
8312        let mut st = Dsv4State::new(layers.len());
8313        let inv_freq: Vec<f32> = (0..cfg.rope_head_dim / 2)
8314            .map(|i| 1.0 / 10000f32.powf(2.0 * i as f32 / cfg.rope_head_dim as f32))
8315            .collect();
8316        let mut logits = Vec::new();
8317
8318        // Ten tokens: more than twice the compressor's ratio, so the
8319        // compressed cache is written on a boundary and read afterwards.
8320        let mut first: Option<Vec<f32>> = None;
8321        for (step, tok) in [3u32, 7, 1, 9, 4, 2, 8, 5, 6, 0].into_iter().enumerate() {
8322            forward_token(
8323                &g,
8324                &layers,
8325                &cfg,
8326                &mut st,
8327                tok,
8328                &inv_freq,
8329                None,
8330                &mut logits,
8331            );
8332            assert_eq!(logits.len(), cfg.vocab, "step {step}: logit count");
8333            assert!(
8334                logits.iter().all(|v| v.is_finite()),
8335                "step {step}: non-finite logit — {logits:?}"
8336            );
8337            // A model that has collapsed returns the same distribution
8338            // regardless of input; that is the failure this catches.
8339            let spread = logits.iter().cloned().fold(f32::MIN, f32::max)
8340                - logits.iter().cloned().fold(f32::MAX, f32::min);
8341            assert!(spread > 1e-6, "step {step}: logits are flat ({spread})");
8342            if step == 0 {
8343                first = Some(logits.clone());
8344            }
8345            assert_eq!(st.pos, step + 1, "position bookkeeping");
8346        }
8347
8348        // The cache has to have grown, and the compressor layer must have
8349        // emitted compressed entries (10 tokens / ratio 4 = 2 windows).
8350        assert!(!st.window[0].is_empty(), "sliding window never filled");
8351        // Ten tokens through a window of six: it must have slid, not grown.
8352        for (li, w) in st.window.iter().enumerate() {
8353            assert!(
8354                w.len() / cfg.head_dim <= cfg.window,
8355                "layer {li}: window holds {} positions, cap is {}",
8356                w.len() / cfg.head_dim,
8357                cfg.window
8358            );
8359        }
8360        assert!(
8361            !st.compressed[1].is_empty(),
8362            "compressor layer produced no compressed KV in 10 tokens"
8363        );
8364        // Ten tokens at ratio 4 fold twice, and the entries must be one head
8365        // wide — the overlapping projection is 2x that, so a width mistake
8366        // shows up here rather than as quiet nonsense.
8367        assert_eq!(
8368            st.compressed[1].len() / cfg.head_dim,
8369            2,
8370            "expected two folds in ten tokens at ratio 4"
8371        );
8372        assert!(
8373            !st.prev_kv[1].is_empty(),
8374            "the overlapping compressor never kept a previous window"
8375        );
8376        // Every layer that HAS an indexer must have filled the indexer's own
8377        // cache: it is what decides which compressed positions attention
8378        // reads, and an empty one silently discards the whole long-range
8379        // memory rather than failing.
8380        for (li, l) in layers.iter().enumerate() {
8381            if l.indexer.is_some() {
8382                assert!(
8383                    !st.index_kv[li].is_empty(),
8384                    "layer {li} has an indexer but its cache stayed empty"
8385                );
8386            }
8387        }
8388
8389        // Context must matter: the same token at position 0 of a fresh state
8390        // and at the end of a filled one cannot give identical logits.
8391        let mut fresh = Dsv4State::new(layers.len());
8392        let mut relogits = Vec::new();
8393        forward_token(
8394            &g,
8395            &layers,
8396            &cfg,
8397            &mut fresh,
8398            3,
8399            &inv_freq,
8400            None,
8401            &mut relogits,
8402        );
8403        assert_eq!(
8404            relogits,
8405            first.unwrap(),
8406            "the same token from a fresh state must reproduce exactly"
8407        );
8408    }
8409
8410    /// The reference clamps `up` on both sides but `gate` only from above.
8411    /// Getting that symmetric would quietly change every expert's output on
8412    /// the tokens that saturate, which is the hardest kind of bug to see.
8413    #[test]
8414    fn swiglu_limit_clamps_up_both_ways_and_gate_only_from_above() {
8415        let inter = 4;
8416        // gate = [-50, 50, 1, -1], up = [50, -50, 1, -1]
8417        let gate_src = [-50.0f32, 50.0, 1.0, -1.0];
8418        let up_src = [50.0f32, -50.0, 1.0, -1.0];
8419        let limit = 10.0f32;
8420        let mut got = vec![0.0f32; inter];
8421        expert_swiglu(
8422            &[0.0],
8423            &|_, d| d.copy_from_slice(&gate_src),
8424            &|_, d| d.copy_from_slice(&up_src),
8425            &|src, d| d.copy_from_slice(src),
8426            inter,
8427            1.0,
8428            limit,
8429            &mut got,
8430        );
8431        let silu = |g: f32| g / (1.0 + (-g).exp());
8432        // gate: only the +50 is cut, the -50 rides through silu untouched.
8433        let want = [
8434            silu(-50.0) * limit,
8435            silu(limit) * -limit,
8436            silu(1.0) * 1.0,
8437            -silu(-1.0),
8438        ];
8439        for (i, w) in want.iter().enumerate() {
8440            assert!(
8441                (got[i] - w).abs() < 1e-5,
8442                "lane {i}: got {} want {w}",
8443                got[i]
8444            );
8445        }
8446        // And with the clamp off nothing is touched.
8447        let mut raw = vec![0.0f32; inter];
8448        expert_swiglu(
8449            &[0.0],
8450            &|_, d| d.copy_from_slice(&gate_src),
8451            &|_, d| d.copy_from_slice(&up_src),
8452            &|src, d| d.copy_from_slice(src),
8453            inter,
8454            1.0,
8455            0.0,
8456            &mut raw,
8457        );
8458        assert!(
8459            (raw[1] - silu(50.0) * -50.0).abs() < 1e-3,
8460            "limit 0 must not clamp"
8461        );
8462    }
8463
8464    /// The grouped projection writes its intermediate from several threads
8465    /// at once. Disjoint indices are the whole argument for that being safe,
8466    /// so the pooled result has to equal the serial one exactly — a race
8467    /// here would show up as occasional wrong tokens, not as a crash.
8468    #[test]
8469    fn grouped_projection_is_identical_with_and_without_a_pool() {
8470        let (groups, lora, per_group, dim) = (4usize, 128usize, 64usize, 32usize);
8471        let attn: Vec<f32> = (0..groups * per_group)
8472            .map(|i| ((i * 13) as f32 * 0.021).sin())
8473            .collect();
8474        let wo_a: Vec<f32> = (0..groups * lora * per_group)
8475            .map(|i| ((i * 7) as f32 * 0.011).cos())
8476            .collect();
8477        let wo_b: Vec<f32> = (0..dim * groups * lora)
8478            .map(|i| ((i * 5) as f32 * 0.009).sin())
8479            .collect();
8480        let row = |r: usize, x: &[f32], _sc: &mut [f32]| -> f32 {
8481            wo_a[r * per_group..(r + 1) * per_group]
8482                .iter()
8483                .zip(x)
8484                .map(|(a, b)| a * b)
8485                .sum()
8486        };
8487        let project = |mid: &[f32], dst: &mut [f32]| {
8488            for (d, o) in dst.iter_mut().enumerate() {
8489                *o = wo_b[d * mid.len()..(d + 1) * mid.len()]
8490                    .iter()
8491                    .zip(mid)
8492                    .map(|(a, b)| a * b)
8493                    .sum();
8494            }
8495        };
8496
8497        let mut serial = vec![0.0f32; dim];
8498        o_project(
8499            &attn,
8500            &row,
8501            per_group,
8502            &project,
8503            groups,
8504            lora,
8505            None,
8506            &mut serial,
8507        );
8508
8509        let pool = crate::pool::Pool::new(4);
8510        let mut pooled = vec![0.0f32; dim];
8511        o_project(
8512            &attn,
8513            &row,
8514            per_group,
8515            &project,
8516            groups,
8517            lora,
8518            Some(&pool),
8519            &mut pooled,
8520        );
8521        assert_eq!(serial, pooled, "the pooled projection diverged");
8522        assert!(
8523            serial.iter().any(|v| v.abs() > 1e-6),
8524            "test data is degenerate"
8525        );
8526    }
8527
8528    #[test]
8529    fn block_grouped_projection_matches_position_walk() {
8530        let (_g, layers, cfg) = toy();
8531        let l = &layers[1];
8532        let b = 5;
8533        let attn_len = cfg.n_heads * cfg.head_dim;
8534        let attn: Vec<f32> = (0..b * attn_len)
8535            .map(|i| ((i * 17) as f32 * 0.013).sin())
8536            .collect();
8537        let mut walked = vec![0.0f32; b * cfg.dim];
8538        for bi in 0..b {
8539            o_project(
8540                &attn[bi * attn_len..(bi + 1) * attn_len],
8541                &|r, x, sc| l.wo_a.row_dot(r, x, sc),
8542                l.wo_a.cols(),
8543                &|mid, dst| l.wo_b.matvec(mid, dst, None),
8544                cfg.o_groups,
8545                cfg.o_lora_rank,
8546                None,
8547                &mut walked[bi * cfg.dim..(bi + 1) * cfg.dim],
8548            );
8549        }
8550        let mut batched = vec![0.0f32; b * cfg.dim];
8551        o_project_block(
8552            &attn,
8553            b,
8554            &l.wo_a,
8555            &l.wo_b,
8556            cfg.o_groups,
8557            cfg.o_lora_rank,
8558            None,
8559            &mut batched,
8560        );
8561        assert_eq!(batched, walked);
8562    }
8563
8564    #[test]
8565    fn block_moe_matches_position_walk_in_route_order() {
8566        let (_g, layers, cfg) = toy();
8567        // The scored layer exercises repeated and distinct experts without
8568        // tying the result to a token-id table.
8569        let l = &layers[1];
8570        let b = 5;
8571        let xs: Vec<f32> = (0..b * cfg.dim)
8572            .map(|i| ((i * 11) as f32 * 0.019).cos())
8573            .collect();
8574        let ids = [1u32, 2, 3, 4, 5];
8575        let mut walked = vec![0.0f32; b * cfg.dim];
8576        for bi in 0..b {
8577            moe_step(
8578                &xs[bi * cfg.dim..(bi + 1) * cfg.dim],
8579                l,
8580                &cfg,
8581                ids[bi],
8582                1,
8583                None,
8584                &mut walked[bi * cfg.dim..(bi + 1) * cfg.dim],
8585            );
8586        }
8587        let mut batched = vec![0.0f32; b * cfg.dim];
8588        moe_step_block(&xs, b, l, &cfg, &ids, 1, None, &mut batched);
8589        assert_eq!(batched, walked);
8590    }
8591
8592    /// The overlapping compressor folds 2*ratio slots, not ratio: the
8593    /// previous window contributes its first half of dimensions and the
8594    /// current one its second half. Treating it as a plain compressor makes
8595    /// the entry twice as wide as the cache expects, which lands the whole
8596    /// thing in the wrong store rather than raising anything.
8597    #[test]
8598    fn overlapping_compressor_folds_both_windows() {
8599        let (ratio, d) = (2usize, 3usize);
8600        // Current window: two tokens, 2*d wide each. Second half is what the
8601        // current window contributes.
8602        let cur_kv: Vec<f32> = vec![
8603            1.0, 1.0, 1.0, /*|*/ 10.0, 20.0, 30.0, // token 0
8604            2.0, 2.0, 2.0, /*|*/ 40.0, 50.0, 60.0, // token 1
8605        ];
8606        // Make the current window's second-half scores dominate everywhere.
8607        let cur_sc: Vec<f32> = vec![
8608            0.0, 0.0, 0.0, /*|*/ 0.0, 0.0, 100.0, //
8609            0.0, 0.0, 0.0, /*|*/ 100.0, 100.0, 0.0,
8610        ];
8611        // Previous window: its FIRST half is what it contributes.
8612        let prev_kv: Vec<f32> = vec![
8613            7.0, 8.0, 9.0, /*|*/ 0.0, 0.0, 0.0, //
8614            5.0, 6.0, 7.0, /*|*/ 0.0, 0.0, 0.0,
8615        ];
8616        let prev_sc = vec![0.0f32; ratio * 2 * d];
8617
8618        let mut out = vec![0.0f32; d];
8619        compress_window_overlap(&prev_kv, &prev_sc, &cur_kv, &cur_sc, ratio, d, &mut out);
8620        // dim 0 and 1: token 1's second half wins (score 100)
8621        assert!((out[0] - 40.0).abs() < 1e-3, "dim0 = {}", out[0]);
8622        assert!((out[1] - 50.0).abs() < 1e-3, "dim1 = {}", out[1]);
8623        // dim 2: token 0's second half wins
8624        assert!((out[2] - 30.0).abs() < 1e-3, "dim2 = {}", out[2]);
8625
8626        // With no previous window the fold still works and uses only the
8627        // current one — this is the very first window of a generation.
8628        let mut first = vec![0.0f32; d];
8629        compress_window_overlap(&[], &[], &cur_kv, &cur_sc, ratio, d, &mut first);
8630        assert!(
8631            first.iter().all(|v| v.is_finite()),
8632            "first window: {first:?}"
8633        );
8634        assert!((first[0] - 40.0).abs() < 1e-3, "first dim0 = {}", first[0]);
8635
8636        // And a previous window with real scores does pull the result.
8637        let mut both = vec![0.0f32; d];
8638        let strong_prev = vec![100.0f32; ratio * 2 * d];
8639        compress_window_overlap(
8640            &prev_kv,
8641            &strong_prev,
8642            &cur_kv,
8643            &cur_sc,
8644            ratio,
8645            d,
8646            &mut both,
8647        );
8648        assert!(
8649            (both[0] - 40.0).abs() > 1.0,
8650            "a scored previous window must move the fold, got {}",
8651            both[0]
8652        );
8653    }
8654
8655    /// Numerical parity with the reference. The vectors below come from
8656    /// running `kernel.py::hc_split_sinkhorn`'s own formula on a fixed
8657    /// input; matching them pins the exponent order, the eps placement and
8658    /// the off-by-one in the iteration count all at once — a property test
8659    /// alone would pass with any of those wrong.
8660    #[test]
8661    fn sinkhorn_matches_the_reference_numbers() {
8662        let hc = 4;
8663        let mixes: Vec<f32> = (0..24).map(|i| (i as f32 * 0.37).sin() * 3.0).collect();
8664        let base: Vec<f32> = (0..24).map(|i| (i as f32 * 0.11).cos()).collect();
8665        let (mut pre, mut post, mut comb) = (vec![0.0; hc], vec![0.0; hc], vec![0.0; hc * hc]);
8666        hc_split_sinkhorn(
8667            &mixes,
8668            &[1.0, 1.0, 1.0],
8669            &base,
8670            hc,
8671            20,
8672            1e-6,
8673            &mut pre,
8674            &mut post,
8675            &mut comb,
8676        );
8677        let want_pre = [0.7310596, 0.8888268, 0.9525191, 0.97424865];
8678        let want_post = [1.9600224, 1.9534285, 1.9201256, 1.8160983];
8679        let want_comb = [
8680            0.5996052,
8681            0.282_535_9,
8682            0.09218107,
8683            0.025676856,
8684            0.17564717,
8685            0.22228767,
8686            0.271_745_4,
8687            0.330_318_8,
8688            0.029528176,
8689            0.12206022,
8690            0.32619134,
8691            0.5222193,
8692            0.19521846,
8693            0.37311527,
8694            0.30988118,
8695            0.12178412,
8696        ];
8697        for (i, w) in want_pre.iter().enumerate() {
8698            assert!((pre[i] - w).abs() < 1e-5, "pre[{i}]: {} vs {w}", pre[i]);
8699        }
8700        for (i, w) in want_post.iter().enumerate() {
8701            assert!((post[i] - w).abs() < 1e-5, "post[{i}]: {} vs {w}", post[i]);
8702        }
8703        for (i, w) in want_comb.iter().enumerate() {
8704            assert!((comb[i] - w).abs() < 1e-4, "comb[{i}]: {} vs {w}", comb[i]);
8705        }
8706    }
8707
8708    /// Sinkhorn's whole point is a doubly stochastic matrix: every row and
8709    /// every column sums to one. If the alternating normalization is wrong
8710    /// (or the loop count is off by one) the sums drift, and the residual
8711    /// mixing quietly gains or loses mass on every layer.
8712    #[test]
8713    fn sinkhorn_leaves_the_mixing_matrix_doubly_stochastic() {
8714        let hc = 4;
8715        let mix_hc = (2 + hc) * hc;
8716        // a deliberately lopsided projection
8717        let mixes: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.37).sin() * 3.0).collect();
8718        let base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.11).cos()).collect();
8719        let (mut pre, mut post, mut comb) = (vec![0.0; hc], vec![0.0; hc], vec![0.0; hc * hc]);
8720        hc_split_sinkhorn(
8721            &mixes,
8722            &[1.0, 1.0, 1.0],
8723            &base,
8724            hc,
8725            20,
8726            1e-6,
8727            &mut pre,
8728            &mut post,
8729            &mut comb,
8730        );
8731        for j in 0..hc {
8732            let r: f32 = comb[j * hc..(j + 1) * hc].iter().sum();
8733            assert!((r - 1.0).abs() < 2e-3, "row {j} sums to {r}");
8734            let c: f32 = (0..hc).map(|k| comb[k * hc + j]).sum();
8735            assert!((c - 1.0).abs() < 2e-3, "col {j} sums to {c}");
8736        }
8737        // pre is a gate in (eps, 1+eps); post carries the factor 2
8738        assert!(pre.iter().all(|&v| v > 0.0 && v < 1.001));
8739        assert!(post.iter().all(|&v| (0.0..=2.0).contains(&v)));
8740    }
8741
8742    /// Folding four copies and expanding them back must preserve a constant
8743    /// state exactly when the block contributes nothing: with post = 0 the
8744    /// expansion is a doubly stochastic mix of identical copies, i.e. itself.
8745    #[test]
8746    fn expand_of_identical_copies_is_a_fixed_point() {
8747        let (hc, dim) = (4usize, 3usize);
8748        let residual: Vec<f32> = std::iter::repeat_n([1.5f32, -2.0, 0.25], hc)
8749            .flatten()
8750            .collect();
8751        let comb = {
8752            // exactly doubly stochastic: uniform
8753            vec![0.25f32; hc * hc]
8754        };
8755        let post = vec![0.0f32; hc];
8756        let mut out = vec![0.0f32; hc * dim];
8757        hc_expand(&[0.0; 3], &residual, &post, &comb, hc, dim, &mut out);
8758        for (o, r) in out.iter().zip(&residual) {
8759            assert!((o - r).abs() < 1e-6, "{o} vs {r}");
8760        }
8761    }
8762
8763    /// The bias must move the SELECTION without touching the weights: with a
8764    /// large bias on a low-scoring expert it gets picked, but its weight is
8765    /// still its own (small) score, renormalized.
8766    #[test]
8767    fn selection_bias_steers_the_choice_but_not_the_weights() {
8768        let scores = [3.0f32, 0.1, 2.0, 0.05];
8769        let bias = [0.0f32, 10.0, 0.0, 0.0];
8770        let (mut idx, mut w) = (Vec::new(), Vec::new());
8771        route(&scores, Some(&bias), 2, 1.5, None, None, &mut idx, &mut w);
8772        assert_eq!(idx[0], 1, "the biased expert must win selection");
8773        assert_eq!(idx[1], 0);
8774        // weights come from sqrt(softplus(score)) BEFORE the bias, so the
8775        // biased expert's share must be the smaller of the two
8776        assert!(w[0] < w[1], "biased expert kept its own (small) weight");
8777        let sum: f32 = w.iter().sum();
8778        assert!((sum - 1.5).abs() < 1e-5, "weights renormalize then scale");
8779    }
8780
8781    /// The sink is an extra logit with no value: it must lower every
8782    /// weight without adding output. With a huge sink the head should
8783    /// attend to almost nothing.
8784    #[test]
8785    fn attention_sink_drains_weight_without_contributing_output() {
8786        let hd = 2;
8787        let q = [1.0f32, 0.0];
8788        let kv = [1.0f32, 0.0, 0.0, 1.0];
8789        let mut out = vec![0.0f32; hd];
8790        sparse_attend(&q, &kv, &[0, 1], f32::NEG_INFINITY, 1.0, hd, &mut out);
8791        let plain = out.clone();
8792        assert!(plain[0] > plain[1], "the aligned key must dominate");
8793        sparse_attend(&q, &kv, &[0, 1], 20.0, 1.0, hd, &mut out);
8794        assert!(
8795            out[0] < plain[0] * 0.01 && out[1] < plain[1] * 0.01,
8796            "a large sink must drain nearly all the mass: {out:?}"
8797        );
8798    }
8799
8800    /// A masked slot must be ignored entirely — not folded in as a zero
8801    /// key, which would still add exp(0) to the denominator.
8802    #[test]
8803    fn masked_positions_leave_the_denominator_alone() {
8804        let hd = 2;
8805        let q = [1.0f32, 0.0];
8806        let kv = [1.0f32, 0.0, 0.0, 1.0];
8807        let (mut a, mut b) = (vec![0.0f32; hd], vec![0.0f32; hd]);
8808        sparse_attend(&q, &kv, &[0], f32::NEG_INFINITY, 1.0, hd, &mut a);
8809        sparse_attend(
8810            &q,
8811            &kv,
8812            &[0, usize::MAX],
8813            f32::NEG_INFINITY,
8814            1.0,
8815            hd,
8816            &mut b,
8817        );
8818        for (x, y) in a.iter().zip(&b) {
8819            assert!((x - y).abs() < 1e-6, "{x} vs {y}");
8820        }
8821    }
8822
8823    /// Forward then inverse rotation is the identity — the property the
8824    /// output path depends on.
8825    #[test]
8826    fn rope_tail_inverts_itself() {
8827        let inv_freq = [1.0f32, 0.5];
8828        let orig = [9.0f32, 8.0, 1.0, 2.0, 3.0, 4.0];
8829        let mut v = orig;
8830        rope_tail(&mut v, &inv_freq, 7, 4, false);
8831        assert!(v[..2] == orig[..2], "the non-rope head must not move");
8832        assert!(v[2..] != orig[2..], "the tail must actually rotate");
8833        rope_tail(&mut v, &inv_freq, 7, 4, true);
8834        for (a, b) in v.iter().zip(&orig) {
8835            assert!((a - b).abs() < 1e-5, "{a} vs {b}");
8836        }
8837    }
8838
8839    /// The window pooling is a softmax per DIMENSION over the ratio, with
8840    /// the position bias inside the exponent.
8841    #[test]
8842    fn compressor_pools_the_window_per_dimension() {
8843        let (ratio, width) = (2usize, 2usize);
8844        let kv = [1.0f32, 10.0, 3.0, 20.0];
8845        // dim 0: equal scores → mean; dim 1: second token wins by a mile
8846        let score = [0.0f32, 0.0, 0.0, 50.0];
8847        let ape = vec![0.0f32; ratio * width];
8848        let mut out = vec![0.0f32; width];
8849        compress_window(&kv, &score, &ape, ratio, width, &mut out);
8850        assert!(
8851            (out[0] - 2.0).abs() < 1e-5,
8852            "equal scores average: {}",
8853            out[0]
8854        );
8855        assert!(
8856            (out[1] - 20.0).abs() < 1e-3,
8857            "a dominant score wins: {}",
8858            out[1]
8859        );
8860    }
8861
8862    /// A negative dot product must not drag a position down: the relu
8863    /// means heads abstain rather than veto.
8864    #[test]
8865    fn index_scores_relu_before_weighting() {
8866        let (nh, hd) = (2usize, 2usize);
8867        // head 0 aligns with position 0, head 1 anti-aligns with it
8868        let q = [1.0f32, 0.0, -1.0, 0.0];
8869        let kv = [1.0f32, 0.0, 0.0, 1.0];
8870        let w = [1.0f32, 1.0];
8871        let mut sc = Vec::new();
8872        index_scores(&q, &kv, &w, nh, hd, 2, 2, None, &mut sc);
8873        // without the relu the anti-aligned head would cancel head 0 to zero
8874        assert!(sc[0] > 0.9, "abstention, not veto: {:?}", sc);
8875    }
8876
8877    #[test]
8878    fn index_scores_mask_the_future() {
8879        let (nh, hd) = (1usize, 2usize);
8880        let q = [1.0f32, 0.0];
8881        let kv = [1.0f32, 0.0, 1.0, 0.0, 1.0, 0.0];
8882        let w = [1.0f32];
8883        let mut sc = Vec::new();
8884        index_scores(&q, &kv, &w, nh, hd, 3, 2, None, &mut sc);
8885        assert!(sc[0].is_finite() && sc[1].is_finite());
8886        assert!(sc[2] == f32::NEG_INFINITY, "position 2 is in the future");
8887        let mut idx = Vec::new();
8888        top_k_positions(&sc, 3, &mut idx);
8889        assert_eq!(idx, vec![0, 1], "a masked slot never wins a slot");
8890    }
8891
8892    #[test]
8893    fn top_k_is_deterministic_on_ties() {
8894        let sc = [1.0f32, 1.0, 1.0, 0.0];
8895        let mut idx = Vec::new();
8896        top_k_positions(&sc, 2, &mut idx);
8897        assert_eq!(idx, vec![0, 1], "ties resolve to the lower index");
8898    }
8899
8900    /// The block cycle must leave the state's SHAPE intact (hc copies in,
8901    /// hc copies out) and must actually route the block's output back in:
8902    /// a block that writes a constant has to move every copy.
8903    #[test]
8904    fn hc_block_preserves_the_copy_structure_and_applies_the_block() {
8905        let cfg = Dsv4Cfg {
8906            dim: 4,
8907            n_heads: 1,
8908            head_dim: 4,
8909            rope_head_dim: 2,
8910            q_lora_rank: 4,
8911            o_lora_rank: 2,
8912            o_groups: 1,
8913            hc_mult: 4,
8914            hc_sinkhorn_iters: 20,
8915            hc_eps: 1e-6,
8916            norm_eps: 1e-6,
8917            n_routed_experts: 2,
8918            top_k: 1,
8919            moe_inter: 4,
8920            route_scale: 1.0,
8921            swiglu_limit: 10.0,
8922            window: 128,
8923            index_topk: 4,
8924            vocab: 8,
8925        };
8926        let (hc, dim) = (cfg.hc_mult, cfg.dim);
8927        let mix_hc = (2 + hc) * hc;
8928        let hc_fn: Vec<f32> = (0..mix_hc * hc * dim)
8929            .map(|i| ((i % 13) as f32 - 6.0) * 0.05)
8930            .collect();
8931        let hc_base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.2).sin()).collect();
8932        let norm_w = vec![1.0f32; dim];
8933        let mut state: Vec<f32> = (0..hc * dim).map(|i| (i as f32 * 0.3).cos()).collect();
8934        let before = state.clone();
8935        let mut scratch = HcScratch::new(&cfg);
8936        hc_block(
8937            &mut state,
8938            &hc_fn,
8939            &[1.0, 1.0, 1.0],
8940            &hc_base,
8941            &norm_w,
8942            &cfg,
8943            &mut scratch,
8944            None,
8945            |_folded, out: &mut [f32]| out.iter_mut().for_each(|o| *o = 1.0),
8946        );
8947        assert_eq!(state.len(), before.len(), "copy structure must survive");
8948        assert!(state.iter().all(|v| v.is_finite()), "{state:?}");
8949        assert!(
8950            state.iter().zip(&before).any(|(a, b)| (a - b).abs() > 1e-4),
8951            "the block's output has to reach the state"
8952        );
8953    }
8954
8955    #[test]
8956    fn hash_route_reads_the_table_row() {
8957        // vocab 3, top_k 2
8958        let table = [7.0f32, 9.0, 1.0, 2.0, 5.0, 6.0];
8959        assert_eq!(hash_route(&table, 3, 2, 0), vec![7, 9]);
8960        assert_eq!(hash_route(&table, 3, 2, 2), vec![5, 6]);
8961        // out-of-range ids clamp instead of panicking
8962        assert_eq!(hash_route(&table, 3, 2, 99), vec![5, 6]);
8963    }
8964
8965    /// A task mask restricts SELECTION and nothing else: the weights still
8966    /// come from the pre-bias scores and still renormalize, now over what
8967    /// survives. Masking must never reroute — an expert the mask forbids has
8968    /// to be absent, not replaced by a neighbour with the wrong weight.
8969    #[test]
8970    fn a_task_mask_restricts_selection_and_renormalizes() {
8971        // Expert 3 scores highest, then 1, then 2, then 0.
8972        let scores = [0.1f32, 4.0, 1.0, 9.0];
8973        let (mut idx, mut w) = (Vec::new(), Vec::new());
8974        route(&scores, None, 2, 1.0, None, None, &mut idx, &mut w);
8975        assert_eq!(idx, vec![3, 1], "unmasked: the two best win");
8976        let sum: f32 = w.iter().sum();
8977        assert!((sum - 1.0).abs() < 1e-5, "weights must sum to route_scale");
8978
8979        // Forbid the winner: the next two take its place and the weights
8980        // renormalize over them.
8981        let mask = [true, false, true, true];
8982        let (mut i2, mut w2) = (Vec::new(), Vec::new());
8983        route(&scores, None, 2, 1.0, None, Some(&mask), &mut i2, &mut w2);
8984        assert_eq!(i2, vec![3, 2], "masked expert must not be selected");
8985        let sum2: f32 = w2.iter().sum();
8986        assert!((sum2 - 1.0).abs() < 1e-5, "masked weights must renormalize");
8987
8988        // A mask leaving fewer than top_k experts yields fewer, not garbage.
8989        let tight = [false, false, false, true];
8990        let (mut i3, mut w3) = (Vec::new(), Vec::new());
8991        route(&scores, None, 2, 1.0, None, Some(&tight), &mut i3, &mut w3);
8992        assert_eq!(i3, vec![3]);
8993        assert_eq!(w3.len(), 1);
8994    }
8995
8996    /// On a hash layer the reference gathers the scores AT THE TABLE's
8997    /// experts. Choosing top-k first and swapping the indices afterwards
8998    /// leaves every weight attached to a different expert than the one it
8999    /// scales — silently, since both lists are the right length.
9000    #[test]
9001    fn hash_layers_weight_the_experts_the_table_names() {
9002        // Expert 3 scores highest, expert 0 lowest; the table names 0 and 1.
9003        let scores = [0.1f32, 0.4, 0.2, 5.0];
9004        let table = vec![0.0f32, 1.0];
9005        let idx_forced = hash_route(&table, 1, 2, 0);
9006        assert_eq!(idx_forced, vec![0, 1]);
9007
9008        let (mut idx, mut w) = (Vec::new(), Vec::new());
9009        route(
9010            &scores,
9011            None,
9012            2,
9013            1.0,
9014            Some(&idx_forced),
9015            None,
9016            &mut idx,
9017            &mut w,
9018        );
9019        assert_eq!(idx, vec![0, 1], "the table must decide the experts");
9020
9021        // The weights must be the table experts' own scores, normalized.
9022        let sp = |x: f32| (1.0 + x.exp()).ln().sqrt();
9023        let (s0, s1) = (sp(scores[0]), sp(scores[1]));
9024        let tot = s0 + s1;
9025        assert!(
9026            (w[0] - s0 / tot).abs() < 1e-6,
9027            "w[0]={} want {}",
9028            w[0],
9029            s0 / tot
9030        );
9031        assert!(
9032            (w[1] - s1 / tot).abs() < 1e-6,
9033            "w[1]={} want {}",
9034            w[1],
9035            s1 / tot
9036        );
9037
9038        // And the top-k path is untouched: expert 3 still wins there.
9039        let (mut idx2, mut w2) = (Vec::new(), Vec::new());
9040        route(&scores, None, 2, 1.0, None, None, &mut idx2, &mut w2);
9041        assert_eq!(idx2[0], 3, "without a table the highest score still wins");
9042    }
9043}
9044
9045// ══ DSpark: the block-parallel draft ══════════════════════════════════
9046//
9047// Not a classic MTP chain. One pass through the three stages produces the
9048// WHOLE block of `block_size` positions at once: position 0 carries the token
9049// the trunk just emitted, the rest carry a noise token, and every position
9050// attends to every other one — which is why the block cannot be measured a
9051// position at a time and pretend to be faithful. Depth comes from the block,
9052// not from the stage count.
9053//
9054// The stages' KV cache is built from the trunk's hidden state, not from the
9055// draft's own tokens: one entry per real position, `kv_norm(wkv(main_x))`,
9056// in a ring of `window`. The block's own keys and values are appended for
9057// the duration of the block and then discarded.
9058
9059/// The noise token the block's unknown positions carry
9060/// (`dspark_noise_token_id`).
9061pub const DSPARK_NOISE_TOKEN: u32 = 128799;
9062/// `dspark_block_size` — the width of the draft block, and NOT a tuning knob.
9063///
9064/// All five positions attend to each other and the model was trained with
9065/// exactly four noise slots behind the real token, so a narrower block is a
9066/// different draft model, not a cheaper one. What the survival curve argues
9067/// for is verifying fewer of the five — see `dspark_verify_k` — which costs
9068/// less without changing what the draft computes.
9069pub fn dspark_block() -> usize {
9070    5
9071}
9072
9073/// How many of the block's proposals the trunk actually checks.
9074///
9075/// Survival is [0.67, 0.50, 0.29, 0.08, 0.04]: positions four and five are
9076/// paid for on every verify and delivered on a twelfth of them. Three yields
9077/// 2.46 tokens a cycle against five's 2.58, for three fifths of the verify.
9078/// `CMF_DSPARK_VERIFY_K=N` sets it.
9079#[cfg(feature = "gpu")]
9080fn dspark_native_on() -> bool {
9081    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9082    *ON.get_or_init(|| {
9083        std::env::var("CMF_DSPARK_NATIVE")
9084            .map(|v| v != "0")
9085            // The q4 checkpoint's native draft accepts far more proposals
9086            // than its upload-time q4→q2 recode. A q2 file stays q2 below.
9087            .unwrap_or(true)
9088    })
9089}
9090
9091pub fn dspark_verify_k() -> usize {
9092    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9093    *K.get_or_init(|| {
9094        std::env::var("CMF_DSPARK_VERIFY_K")
9095            .ok()
9096            .and_then(|v| v.parse::<usize>().ok())
9097            .filter(|&n| (1..=DSPARK_BLOCK_MAX).contains(&n))
9098            .unwrap_or(DSPARK_BLOCK_MAX)
9099    })
9100}
9101
9102/// The trained block width.
9103pub const DSPARK_BLOCK_MAX: usize = 5;
9104
9105/// Per-sequence state of the draft: one KV ring per stage, and the trunk
9106/// hidden states the block's input is projected from.
9107pub struct DsparkState {
9108    /// `[stage][window * kv_width]`, written at `pos % window`.
9109    pub win: Vec<Vec<f32>>,
9110    /// How many real positions each ring holds, capped at `window`.
9111    pub filled: Vec<usize>,
9112    /// The trunk's captured hidden, `dim * n_targets`, refreshed every token.
9113    pub main_hidden: Vec<f32>,
9114    /// True once `main_hidden` holds this position's capture.
9115    pub have_hidden: bool,
9116}
9117
9118impl DsparkState {
9119    pub fn new(stages: usize, cfg: &Dsv4Cfg, targets: usize) -> Self {
9120        Self {
9121            win: vec![Vec::new(); stages],
9122            filled: vec![0; stages],
9123            main_hidden: vec![0.0; cfg.dim * targets],
9124            have_hidden: false,
9125        }
9126    }
9127}
9128
9129/// Which trunk layers the draft reads. Upstream names them explicitly
9130/// (`dspark_target_layer_ids`); the file says the same thing less directly —
9131/// `main_proj` has one `dim`-wide input block per captured layer — and the
9132/// release captures the last three. Deriving it from the weight keeps the
9133/// two from disagreeing.
9134pub fn dspark_targets(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg, n_layers: usize) -> Vec<usize> {
9135    let Some(mp) = mtp.iter().find_map(|m| m.main_proj.as_ref()) else {
9136        return Vec::new();
9137    };
9138    let n = (mp.cols() / cfg.dim.max(1)).clamp(1, n_layers);
9139    (n_layers - n..n_layers).collect()
9140}
9141
9142thread_local! {
9143    /// The armed capture: which layers to take, and the buffer they fill.
9144    /// A thread-local rather than a parameter because the capture has to
9145    /// reach into the middle of a layer loop that eight call sites share,
9146    /// and threading an optional buffer through all of them to serve one
9147    /// diagnostic is a worse trade than this.
9148    static DSPARK_CAP: std::cell::RefCell<(Vec<usize>, Vec<f32>, usize)> =
9149        const { std::cell::RefCell::new((Vec::new(), Vec::new(), 0)) };
9150}
9151
9152/// Arm the capture for the layers `targets`, in order.
9153pub fn dspark_arm(targets: &[usize], dim: usize) {
9154    DSPARK_CAP.with(|c| {
9155        let mut c = c.borrow_mut();
9156        c.0 = targets.to_vec();
9157        c.1 = vec![0.0; dim * targets.len()];
9158        c.2 = 0;
9159    });
9160}
9161
9162/// Whether the armed MTP capture needs the state immediately after `li`.
9163/// The normal decode path keeps a full run in one submission; DSpark is the
9164/// only caller that needs an intermediate state to cross the device boundary.
9165fn dspark_wants(li: usize) -> bool {
9166    DSPARK_CAP.with(|c| c.borrow().0.contains(&li))
9167}
9168
9169/// Called after every host layer. Free when nothing is armed.
9170pub fn dspark_note(li: usize, state: &[f32], cfg: &Dsv4Cfg) {
9171    DSPARK_CAP.with(|c| {
9172        let mut c = c.borrow_mut();
9173        if c.0.is_empty() {
9174            return;
9175        }
9176        if let Some(slot) = c.0.iter().position(|&t| t == li) {
9177            let (_, buf, seen) = &mut *c;
9178            dspark_capture(state, cfg, slot, buf);
9179            // Counted, not "was the last one" — under the device chain only
9180            // the layers left on the host call this, and taking the last
9181            // target as the signal would hand the draft a buffer whose other
9182            // slots still hold the previous token, or nothing at all.
9183            *seen = if slot == 0 { 1 } else { *seen + 1 };
9184            if std::env::var("CMF_DSPARK_CAP_DBG").is_ok() {
9185                eprintln!("[cap] note li={li} slot={slot} seen={}", *seen);
9186            }
9187        }
9188    });
9189}
9190
9191/// Read one slot of the armed capture buffer as-is, complete or not. The
9192/// speculative verify fills the DEVICE targets from its own photographs and
9193/// only needs the host layers' slots from here — `dspark_take`'s
9194/// completeness contract would never be met on that path.
9195pub fn dspark_peek_slot(slot: usize, dim: usize, out: &mut [f32]) -> bool {
9196    DSPARK_CAP.with(|c| {
9197        let c = c.borrow();
9198        let lo = slot * dim;
9199        if c.1.len() < lo + dim {
9200            return false;
9201        }
9202        out[..dim].copy_from_slice(&c.1[lo..lo + dim]);
9203        true
9204    })
9205}
9206
9207/// Move the capture out, if this token produced a complete one.
9208pub fn dspark_take(out: &mut Vec<f32>) -> bool {
9209    DSPARK_CAP.with(|c| {
9210        let mut c = c.borrow_mut();
9211        if c.0.is_empty() || c.2 != c.0.len() {
9212            if std::env::var("CMF_DSPARK_CAP_DBG").is_ok() {
9213                eprintln!("[cap] take FAIL armed={:?} seen={}", c.0, c.2);
9214            }
9215            return false;
9216        }
9217        out.clear();
9218        out.extend_from_slice(&c.1);
9219        c.2 = 0;
9220        true
9221    })
9222}
9223
9224/// The trunk's contribution: the mean over the hyper-connection copies,
9225/// appended in target order. Costs one pass over `hc * dim` per captured
9226/// layer and nothing else.
9227pub fn dspark_capture(state: &[f32], cfg: &Dsv4Cfg, slot: usize, out: &mut [f32]) {
9228    let (hc, dim) = (cfg.hc_mult, cfg.dim);
9229    let dst = &mut out[slot * dim..(slot + 1) * dim];
9230    let inv = 1.0 / hc as f32;
9231    for d in 0..dim {
9232        let mut s = 0.0;
9233        for j in 0..hc {
9234            s += state[j * dim + d];
9235        }
9236        dst[d] = s * inv;
9237    }
9238}
9239
9240/// `CMF_DSPARK_PICK_DUMP=path` — accumulate the draft's expert picks per
9241/// stage and periodically rewrite `path` with `stage<TAB>expert<TAB>count`
9242/// lines. Rewritten every 32 blocks rather than at exit, so a run that is
9243/// killed still leaves the tallies on disk.
9244pub fn dspark_freq_note(picks: &[(usize, Vec<usize>)]) {
9245    static FREQ: std::sync::Mutex<Option<(std::collections::HashMap<(usize, usize), u64>, u64)>> =
9246        std::sync::Mutex::new(None);
9247    let Ok(path) = std::env::var("CMF_DSPARK_PICK_DUMP") else {
9248        return;
9249    };
9250    let mut g = FREQ.lock().unwrap();
9251    let (map, blocks) = g.get_or_insert_with(|| (std::collections::HashMap::new(), 0));
9252    for (stage, idx) in picks {
9253        for &e in idx {
9254            *map.entry((*stage, e)).or_insert(0) += 1;
9255        }
9256    }
9257    *blocks += 1;
9258    if *blocks % 32 == 0 {
9259        let mut lines: Vec<_> = map.iter().collect();
9260        lines.sort();
9261        let body: String = lines
9262            .iter()
9263            .map(|((s, e), n)| format!("{s}\t{e}\t{n}\n"))
9264            .collect();
9265        let _ = std::fs::write(&path, body);
9266    }
9267}
9268
9269/// `CMF_DSV4_TRUNK_PICK_DUMP=path` — the same tally for the TRUNK's layers:
9270/// `layer<TAB>expert<TAB>count`, rewritten every 32 tokens. The pick lists
9271/// come from the probe's own tally window, so only layers that route on the
9272/// host are counted — which is exactly the population a partial pack serves.
9273pub fn trunk_freq_note(picks: &[(usize, Vec<usize>)]) {
9274    static FREQ: std::sync::Mutex<Option<(std::collections::HashMap<(usize, usize), u64>, u64)>> =
9275        std::sync::Mutex::new(None);
9276    let Ok(path) = std::env::var("CMF_DSV4_TRUNK_PICK_DUMP") else {
9277        return;
9278    };
9279    let mut g = FREQ.lock().unwrap();
9280    let (map, blocks) = g.get_or_insert_with(|| (std::collections::HashMap::new(), 0));
9281    for (li, idx) in picks {
9282        for &e in idx {
9283            *map.entry((*li, e)).or_insert(0) += 1;
9284        }
9285    }
9286    *blocks += 1;
9287    if *blocks % 32 == 0 {
9288        let mut lines: Vec<_> = map.iter().collect();
9289        lines.sort();
9290        let body: String = lines
9291            .iter()
9292            .map(|((l, e), n)| format!("{l}\t{e}\t{n}\n"))
9293            .collect();
9294        let _ = std::fs::write(&path, body);
9295    }
9296}
9297
9298/// `CMF_DSPARK_MASK=path` — restrict the draft's routed experts to an
9299/// explicit per-stage keep-set: line `d` of the file lists the expert ids
9300/// stage `d` may route to, comma-separated. Weights renormalize over what
9301/// remains (the `Dsv4Layer::mask` contract). The draft only proposes — the
9302/// trunk still verifies every token — so a thinner draft costs acceptance,
9303/// never correctness. This is the offline dial for sizing a resident
9304/// device pack before one exists.
9305fn dspark_apply_mask(out: &mut [Dsv4Mtp]) {
9306    let Ok(path) = std::env::var("CMF_DSPARK_MASK") else {
9307        return;
9308    };
9309    let Ok(text) = std::fs::read_to_string(&path) else {
9310        eprintln!("DSpark: CMF_DSPARK_MASK={path} не читается — маска не применена");
9311        return;
9312    };
9313    for (d, line) in text.lines().enumerate() {
9314        let Some(m) = out.get_mut(d) else { break };
9315        let n = m.layer.experts.len();
9316        let mut mask = vec![false; n];
9317        let mut kept = 0usize;
9318        for tok in line.split(',') {
9319            if let Ok(e) = tok.trim().parse::<usize>() {
9320                if e < n && !mask[e] {
9321                    mask[e] = true;
9322                    kept += 1;
9323                }
9324            }
9325        }
9326        if kept == 0 {
9327            continue;
9328        }
9329        eprintln!("DSpark: стадия {d} ограничена {kept}/{n} экспертами");
9330        m.layer.mask = Some(mask);
9331    }
9332}
9333
9334/// The draft's device residency: which experts of each stage live on the
9335/// card, and how the device router reaches them.
9336///
9337/// The draft only proposes — the trunk verifies every token — so the pack
9338/// is free to keep a SUBSET of each stage's experts and mask the routing to
9339/// it: acceptance pays, correctness never does. The subset is chosen by
9340/// measured routing frequency (`CMF_DSPARK_PACK` names the tally file that
9341/// `CMF_DSPARK_PICK_DUMP` wrote; `CMF_DSPARK_RESIDENT` caps experts per
9342/// stage, default 48).
9343#[cfg(feature = "gpu")]
9344pub struct DsparkPack {
9345    pub stages: Vec<DsparkStagePack>,
9346    /// Gate/up requantized to q2tp at upload (the binary registered an
9347    /// encoder); the graph then dispatches the q2tp kernels.
9348    pub gu_q2: bool,
9349    /// The down planes too (native in the file, never requantized at
9350    /// upload); the graph dispatches the 2-bit down kernel.
9351    pub dn_q2: bool,
9352    /// Dequantized router and bias per stage, f32 — address-stable for the
9353    /// life of the pack, which is what the device's const cache needs.
9354    pub routers: Vec<Vec<f32>>,
9355    pub biases: Vec<Option<Vec<f32>>>,
9356}
9357
9358#[cfg(feature = "gpu")]
9359pub struct DsparkStagePack {
9360    /// Selectable experts (true = resident).
9361    pub mask: Vec<bool>,
9362    /// Global expert id → pack slot; usize::MAX where cold.
9363    pub to_slot: Vec<usize>,
9364    /// The same two as the device consumes them — u32, address-stable for
9365    /// the pack's lifetime (the const cache keys on the pointer).
9366    pub mask_u32: Vec<u32>,
9367    pub map_u32: Vec<u32>,
9368    /// (gate, up, down) directory indices, pack order, shared LAST.
9369    pub tensors: Vec<(usize, usize, usize)>,
9370    pub n_resident: usize,
9371    /// A protected subset inside the trunk's model-wide physical banks.
9372    /// None keeps the legacy independent local pack for adapters without
9373    /// descriptor-indexed storage arrays and for q2tp.
9374    global: Option<GlobalPack>,
9375}
9376
9377/// The q2tp encoder, registered by the binary that has one (the CLI's
9378/// converter owns the rung-search implementation and the engine must not
9379/// depend on the CLI). When present, the draft's gate/up experts are
9380/// requantized q4tp → q2tp AT UPLOAD — half the VRAM and the same kernels
9381/// the trunk's q2tp experts already use. Draft-only fidelity: acceptance
9382/// pays, correctness never does.
9383pub static DSPARK_Q2TP_ENCODE: std::sync::OnceLock<fn(&[f32], usize, usize) -> Vec<u8>> =
9384    std::sync::OnceLock::new();
9385
9386/// `CMF_DSPARK_GPU=1` — the probe (and later the speculative loop) drafts
9387/// on the card instead of the CPU/disk tier.
9388#[cfg(feature = "gpu")]
9389pub fn dspark_gpu_on() -> bool {
9390    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9391    *ON.get_or_init(|| {
9392        std::env::var("CMF_DSPARK_GPU")
9393            .map(|v| v != "0")
9394            .unwrap_or(true)
9395    })
9396}
9397
9398/// The pack, built once per process (the stand runs one model).
9399#[cfg(feature = "gpu")]
9400pub fn dspark_pack_get(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg) -> Option<&'static DsparkPack> {
9401    static P: std::sync::OnceLock<Option<Box<DsparkPack>>> = std::sync::OnceLock::new();
9402    P.get_or_init(|| dspark_pack_build(mtp, cfg).map(Box::new))
9403        .as_deref()
9404}
9405
9406/// Build and upload the draft's pack. Returns `None` when the stack is
9407/// absent, the budget refuses, or a stage's weights are not where the
9408/// device path needs them — the caller falls back to the CPU draft.
9409/// Reserve the VRAM the speculative draft's device pack will take, so the
9410/// trunk's greedy packing leaves it room. Called at load, before any trunk
9411/// pack is built; a no-op when there is no MTP stack or speculation is off.
9412/// The estimate uses the draft's native dtypes — an upload-time re-encode
9413/// only shrinks it, which errs on the safe side of the physical ceiling.
9414///
9415/// A budget that cannot pack the trunk to the draft's capture layers gets NO
9416/// reservation.  Host-batch verify is exact there, but the measured A40 q4tp
9417/// result is 0.63 tok/s versus the faster ordinary exact walk: reserving the
9418/// draft shrinks every trunk layer and makes speculation a net loss.  The
9419/// threshold is geometric (nine tenths of the trunk's own expert bytes plus
9420/// the draft), never a card name.
9421#[cfg(feature = "gpu")]
9422pub fn dspark_reserve_note(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg, layers: &[Dsv4Layer]) {
9423    if mtp.is_empty() || std::env::var("CMF_DSV4_SPEC").is_ok_and(|v| v == "0") || !dspark_gpu_on()
9424    {
9425        return;
9426    }
9427    // `=1` is the diagnostic force path used to measure a configuration the
9428    // zero-knob geometric gate would reject.  Production auto-selection keeps
9429    // the gate below; forcing must reserve before the trunk is packed or the
9430    // late draft upload simply OOMs.
9431    let forced = std::env::var("CMF_DSV4_SPEC").is_ok_and(|v| v == "1");
9432    let dt = |q2: bool| {
9433        if q2 {
9434            cortiq_core::TensorDtype::Q2TiledP
9435        } else {
9436            cortiq_core::TensorDtype::Q4TiledP
9437        }
9438    };
9439    let gu_q2 = mtp[0]
9440        .layer
9441        .experts
9442        .first()
9443        .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
9444    let dn_q2 = mtp[0]
9445        .layer
9446        .experts
9447        .first()
9448        .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
9449    // A native-q4 draft can share the descriptor-indexed trunk arena and
9450    // therefore needs no second expert reservation.  Do not arm automatic
9451    // speculation here yet: a frequency pack says which experts are common,
9452    // not that the draft's proposals pay for a five-token exact verify.  The
9453    // explicit diagnostic gate can exercise the shared path without OOM;
9454    // zero-knob production remains the measured faster exact walk until an
9455    // acceptance profile has passed its own quality/speed gate.
9456    if !gu_q2
9457        && !dn_q2
9458        && std::env::var("CMF_MOE_MASK").is_err()
9459        && crate::gpu_wgpu::dsv4_global_moe_supported()
9460    {
9461        crate::gpu_wgpu::DRAFT_PACK_RESERVE.store(0, std::sync::atomic::Ordering::Relaxed);
9462        crate::gpu_wgpu::DRAFT_RESERVE.store(0, std::sync::atomic::Ordering::Relaxed);
9463        return;
9464    }
9465    let gu = cortiq_core::quant::expected_nbytes(dt(gu_q2), &[cfg.moe_inter, cfg.dim]).unwrap_or(0);
9466    let dn = cortiq_core::quant::expected_nbytes(dt(dn_q2), &[cfg.dim, cfg.moe_inter]).unwrap_or(0);
9467    let per = (2 * gu + dn) as u64;
9468    let n_res: usize = std::env::var("CMF_DSPARK_RESIDENT")
9469        .ok()
9470        .and_then(|v| v.parse().ok())
9471        // The default matches the measured acceptance plateau's low edge:
9472        // residency below it costs acceptance, above it only costs VRAM.
9473        .unwrap_or(40);
9474    // Routed residents per stage, plus each stage's shared expert.
9475    let bytes = per * (n_res * mtp.len() + mtp.len() + 1) as u64;
9476    // The trunk's own expert bytes, from the route it will actually serve.
9477    // A task-specialist mask changes the physical working set: counting all
9478    // 256 rows here made a compact, fully resident masked trunk look like the
9479    // 158 GB general model, so the zero-knob gate silently disabled the draft
9480    // that is responsible for the second half of its speedup.  Hash layers
9481    // deliberately have no mask and still count every checkpoint-named row.
9482    // Keep the production gate used by the zero-knob path: if nearly all of
9483    // the effective trunk plus the draft cannot fit, spend the whole budget
9484    // on trunk slots.
9485    let trunk: u64 = layers
9486        .iter()
9487        .map(|l| {
9488            let Some(e) = l.experts.first() else {
9489                return 0;
9490            };
9491            let gu = cortiq_core::quant::expected_nbytes(
9492                dt(e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
9493                &[cfg.moe_inter, cfg.dim],
9494            )
9495            .unwrap_or(0);
9496            let dn = cortiq_core::quant::expected_nbytes(
9497                dt(e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
9498                &[cfg.dim, cfg.moe_inter],
9499            )
9500            .unwrap_or(0);
9501            let routed = l
9502                .mask
9503                .as_deref()
9504                .map_or(l.experts.len(), |m| m.iter().filter(|&&open| open).count());
9505            ((2 * gu + dn) * (routed + 1)) as u64
9506        })
9507        .sum();
9508    if let Some(budget) = crate::gpu_wgpu::dsv4_vram_budget() {
9509        if !forced && budget < trunk / 10 * 9 + bytes {
9510            return;
9511        }
9512    }
9513    // The pack is not the draft's whole physical footprint.  Its three
9514    // attention skeletons, block-axis activations, captures and retained
9515    // verify states are ordinary wgpu allocations and therefore do not
9516    // appear in the resident-weight ledger.  Keeping only `bytes` here made
9517    // q4tp fit on paper and then panic the A40 driver while building DSpark.
9518    // A geometry-scaled workspace (bounded to 512..1024 MiB) is separate from
9519    // the expert reservation: dsv4_draft_fit must hand back only PACK bytes,
9520    // never turn scratch headroom into more resident experts.
9521    let mib = 1024 * 1024u64;
9522    let workspace = match crate::gpu_wgpu::dsv4_vram_budget() {
9523        // Smaller discrete heaps have less slack between the reported weight
9524        // ceiling and the driver's physical allocation ceiling.  One GiB is
9525        // still only ~2% of an A40 and is cheaper than an OOM/restart.
9526        Some(b) if b <= 64 * 1024 * mib => 1024 * mib,
9527        _ => {
9528            ((cfg.dim * cfg.hc_mult * DSPARK_BLOCK_MAX * 4096) as u64).clamp(512 * mib, 1024 * mib)
9529        }
9530    };
9531    crate::gpu_wgpu::DRAFT_PACK_RESERVE.store(bytes, std::sync::atomic::Ordering::Relaxed);
9532    crate::gpu_wgpu::DRAFT_RESERVE.store(
9533        bytes.saturating_add(workspace),
9534        std::sync::atomic::Ordering::Relaxed,
9535    );
9536}
9537
9538#[cfg(not(feature = "gpu"))]
9539pub fn dspark_reserve_note(_mtp: &[Dsv4Mtp], _cfg: &Dsv4Cfg, _layers: &[Dsv4Layer]) {}
9540
9541#[cfg(feature = "gpu")]
9542pub fn dspark_pack_build(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg) -> Option<DsparkPack> {
9543    if mtp.is_empty() {
9544        return None;
9545    }
9546    let model = mtp[0]
9547        .layer
9548        .experts
9549        .first()
9550        .and_then(|e| e.w1.model_arc())?;
9551    let native_q2 = mtp[0]
9552        .layer
9553        .experts
9554        .first()
9555        .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
9556    let dn_native = mtp[0]
9557        .layer
9558        .experts
9559        .first()
9560        .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
9561    let global_share = !native_q2
9562        && !dn_native
9563        && std::env::var("CMF_MOE_MASK").is_err()
9564        && crate::gpu_wgpu::dsv4_global_moe_supported()
9565        && crate::gpu_wgpu::dsv4_global_moe_ready(&model);
9566    let n_res: usize = std::env::var("CMF_DSPARK_RESIDENT")
9567        .ok()
9568        .and_then(|v| v.parse().ok())
9569        .unwrap_or_else(|| {
9570            if global_share {
9571                // The measured acceptance plateau starts here. These
9572                // rows occupy existing arena slots rather than asking
9573                // the weight allocator for another multi-GiB pack.
9574                return 40;
9575            }
9576            // No knob: take what the card actually has left, whatever the
9577            // card is. The stages split the fit evenly after their shared
9578            // experts. Forty is the measured acceptance plateau: larger
9579            // packs still make the router and upload more rows without a
9580            // useful increase in accepted tokens (64 was slower on A40).
9581            let gu_q2 = native_q2 || (!dspark_native_on() && DSPARK_Q2TP_ENCODE.get().is_some());
9582            let room = crate::gpu_wgpu::dsv4_draft_fit(cfg.moe_inter, cfg.dim, gu_q2, dn_native);
9583            (room.saturating_sub(mtp.len() + 1) / mtp.len().max(1)).clamp(8, 40)
9584        });
9585    // Frequency tallies: lines of `stage<TAB>expert<TAB>count`. Named by
9586    // `CMF_DSPARK_PACK`, or found as `<model>.dspark.tsv` beside the model
9587    // file — ship the tally next to the checkpoint and no knob is needed.
9588    let mut freq: Vec<Vec<(u64, usize)>> = vec![Vec::new(); mtp.len()];
9589    let pack_path = std::env::var("CMF_DSPARK_PACK").ok().or_else(|| {
9590        let m = mtp[0].layer.experts.first()?.w1.model_arc()?;
9591        let mut s = m.path.as_os_str().to_os_string();
9592        s.push(".dspark.tsv");
9593        let p = std::path::PathBuf::from(s);
9594        p.exists().then(|| p.to_string_lossy().into_owned())
9595    });
9596    if let Some(path) = pack_path {
9597        if let Ok(text) = std::fs::read_to_string(&path) {
9598            for line in text.lines() {
9599                let mut it = line.split_whitespace();
9600                if let (Some(s), Some(e), Some(n)) = (it.next(), it.next(), it.next()) {
9601                    if let (Ok(s), Ok(e), Ok(n)) =
9602                        (s.parse::<usize>(), e.parse::<usize>(), n.parse::<u64>())
9603                    {
9604                        if s < freq.len() {
9605                            freq[s].push((n, e));
9606                        }
9607                    }
9608                }
9609            }
9610        }
9611    }
9612    let mut stages = Vec::with_capacity(mtp.len());
9613    let mut routers = Vec::with_capacity(mtp.len());
9614    let mut biases = Vec::with_capacity(mtp.len());
9615    for (si, m) in mtp.iter().enumerate() {
9616        let l = &m.layer;
9617        let n = l.experts.len();
9618        // Frequency order, then the untallied ids — a cold start still
9619        // packs SOMETHING deterministic.
9620        let mut order: Vec<usize> = {
9621            let mut f = freq[si].clone();
9622            f.sort_by(|a, b| b.0.cmp(&a.0));
9623            let mut seen = vec![false; n];
9624            let mut o: Vec<usize> = f
9625                .into_iter()
9626                .map(|(_, e)| e)
9627                .filter(|&e| {
9628                    if e < n && !seen[e] {
9629                        seen[e] = true;
9630                        true
9631                    } else {
9632                        false
9633                    }
9634                })
9635                .collect();
9636            o.extend((0..n).filter(|&e| !seen[e]));
9637            o
9638        };
9639        order.truncate(n_res.min(n));
9640        let (global, mask, to_slot, tensors) = if global_share {
9641            let pk = pack_for(l, cfg, model.header.arch.num_layers + si)?;
9642            let gl = pk.global.clone()?;
9643            let remap = gl.pool.pin_picks(&model, gl.layer, &order, &l.experts);
9644            let mut mask = vec![false; n];
9645            let mut to_slot = vec![usize::MAX; n];
9646            for e in 0..n {
9647                if remap.get(e).copied().unwrap_or(u32::MAX) != u32::MAX {
9648                    mask[e] = true;
9649                    to_slot[e] = remap[e] as usize;
9650                }
9651            }
9652            (Some(gl), mask, to_slot, Vec::new())
9653        } else {
9654            let mut mask = vec![false; n];
9655            let mut to_slot = vec![usize::MAX; n];
9656            let mut tensors = Vec::with_capacity(order.len() + 1);
9657            for (slot, &e) in order.iter().enumerate() {
9658                let ex = &l.experts[e];
9659                let (Some(w1), Some(w3), Some(w2)) =
9660                    (ex.w1.model_idx(), ex.w3.model_idx(), ex.w2.model_idx())
9661                else {
9662                    return None;
9663                };
9664                mask[e] = true;
9665                to_slot[e] = slot;
9666                tensors.push((w1, w3, w2));
9667            }
9668            let (Some(s1), Some(s3), Some(s2)) = (
9669                l.shared.w1.model_idx(),
9670                l.shared.w3.model_idx(),
9671                l.shared.w2.model_idx(),
9672            ) else {
9673                return None;
9674            };
9675            tensors.push((s1, s3, s2));
9676            (None, mask, to_slot, tensors)
9677        };
9678        // The router and bias, dequantized once.
9679        let mut router = vec![0.0f32; n * cfg.dim];
9680        for (r, row) in (0..n).zip(router.chunks_mut(cfg.dim)) {
9681            l.gate.row_f32(r, row);
9682        }
9683        routers.push(router);
9684        biases.push(l.gate_bias.clone());
9685        let mask_u32: Vec<u32> = mask.iter().map(|&m| m as u32).collect();
9686        let map_u32: Vec<u32> = to_slot
9687            .iter()
9688            .map(|&x| if x == usize::MAX { u32::MAX } else { x as u32 })
9689            .collect();
9690        stages.push(DsparkStagePack {
9691            mask,
9692            to_slot,
9693            mask_u32,
9694            map_u32,
9695            tensors,
9696            n_resident: order.len(),
9697            global,
9698        });
9699    }
9700    // ── upload: the small skeleton FIRST, the expert stacks after — the
9701    //    documented admission order (experts fill the card and the skeleton
9702    //    then misses). ──
9703    let mut skeleton = Vec::new();
9704    for m in mtp {
9705        let l = &m.layer;
9706        for t in [&l.wq_a, &l.wq_b, &l.wkv, &l.wo_a, &l.wo_b] {
9707            skeleton.push(t.model_idx()?);
9708        }
9709    }
9710    if let Some(mp) = mtp[0].main_proj.as_ref() {
9711        skeleton.push(mp.model_idx()?);
9712    }
9713    for &idx in &skeleton {
9714        if !crate::gpu_wgpu::dsv4_weight_ready(&model, idx) {
9715            eprintln!("DSpark: скелет драфта не влез в VRAM — GPU-черновик выключен");
9716            return None;
9717        }
9718    }
9719    // The dtype in the FILE decides: a properly converted CMF stores the
9720    // draft's gate/up as q2tp and uploads through the same path as the
9721    // trunk's 2-bit experts. The at-upload requant is only the fallback for
9722    // files published before the converter's q2tp profile covered the MTP
9723    // stack (and only when the binary registered an encoder).
9724    let gu_q2 = native_q2
9725        || (!crate::dsv4::dspark_native_on() && crate::dsv4::DSPARK_Q2TP_ENCODE.get().is_some());
9726    for (si, sp) in stages.iter().enumerate() {
9727        let ok = if sp.global.is_some() {
9728            true
9729        } else if native_q2 {
9730            crate::gpu_wgpu::dsv4_experts_ready(
9731                &model,
9732                &sp.tensors,
9733                cfg.moe_inter,
9734                cfg.dim,
9735                true,
9736                dn_native,
9737            )
9738        } else if gu_q2 {
9739            crate::gpu_wgpu::moe_expert_bufs_requant_gu(&model, &sp.tensors, cfg.moe_inter, cfg.dim)
9740                .is_some()
9741        } else {
9742            crate::gpu_wgpu::dsv4_experts_ready(
9743                &model,
9744                &sp.tensors,
9745                cfg.moe_inter,
9746                cfg.dim,
9747                false,
9748                false,
9749            )
9750        };
9751        if !ok {
9752            eprintln!(
9753                "DSpark: эксперты стадии {si} ({} + shared) не влезли в VRAM — GPU-черновик выключен",
9754                sp.n_resident
9755            );
9756            return None;
9757        }
9758    }
9759    let _ = crate::gpu_wgpu::pin_weights(&model, &skeleton);
9760    eprintln!(
9761        "DSpark: {} — {} стадии по {} экспертов + shared",
9762        if global_share {
9763            "защищённая доля единого пула"
9764        } else {
9765            "отдельный пак драфта на карте"
9766        },
9767        stages.len(),
9768        stages
9769            .iter()
9770            .map(|s| s.n_resident.to_string())
9771            .collect::<Vec<_>>()
9772            .join("/")
9773    );
9774    Some(DsparkPack {
9775        stages,
9776        gu_q2: if global_share { false } else { gu_q2 },
9777        dn_q2: dn_native,
9778        routers,
9779        biases,
9780    })
9781}
9782
9783/// Append one real position's entry to every stage's KV ring, from the
9784/// trunk captures in `ds.main_hidden`. The draft does this for the position
9785/// it drafts at; a speculative decode also owes an entry for every accepted
9786/// position it never drafted from — a hole in the ring silently starves
9787/// later blocks of context, which reads as "acceptance decayed" and not as
9788/// a bug.
9789pub fn dspark_ring_append(
9790    g: &Dsv4Globals,
9791    mtp: &[Dsv4Mtp],
9792    cfg: &Dsv4Cfg,
9793    ds: &mut DsparkState,
9794    pos: usize,
9795    pool: Option<&crate::pool::Pool>,
9796) {
9797    let (dim, hd, rd) = (cfg.dim, cfg.head_dim, cfg.rope_head_dim);
9798    let inv_freq = &g.inv_freq_window;
9799    let Some(stage0) = mtp.first() else { return };
9800    let (Some(mp), Some(mn)) = (stage0.main_proj.as_ref(), stage0.main_norm.as_ref()) else {
9801        return;
9802    };
9803    let mut main_x = vec![0.0f32; dim];
9804    mp.matvec(&ds.main_hidden, &mut main_x, pool);
9805    rms_weighted(&mut main_x, mn, cfg.norm_eps);
9806    for (si, m) in mtp.iter().enumerate() {
9807        let kvw = m.layer.wkv.rows();
9808        if ds.win[si].len() < cfg.window * kvw {
9809            ds.win[si].resize(cfg.window * kvw, 0.0);
9810        }
9811        let mut kv = vec![0.0f32; kvw];
9812        m.layer.wkv.matvec(&main_x, &mut kv, pool);
9813        rms_weighted(&mut kv, &m.layer.kv_norm, cfg.norm_eps);
9814        rope_tail(&mut kv[kvw - hd..], inv_freq, pos, rd, false);
9815        let slot = pos % cfg.window;
9816        ds.win[si][slot * kvw..(slot + 1) * kvw].copy_from_slice(&kv);
9817        ds.filled[si] = (pos + 1).min(cfg.window);
9818    }
9819}
9820
9821/// The draft block on the card: one submission for all three stages and
9822/// five positions, states home in one fence, the head on the host. The
9823/// markov bias is skipped (its per-position chain through the previous
9824/// PROPOSAL is the one part a single graph cannot batch) — compare against
9825/// the CPU draft under `CMF_DSPARK_NO_MARKOV=1`.
9826#[cfg(feature = "gpu")]
9827#[allow(clippy::too_many_arguments)]
9828pub fn dspark_draft_gpu(
9829    g: &Dsv4Globals,
9830    mtp: &[Dsv4Mtp],
9831    cfg: &Dsv4Cfg,
9832    ds: &mut DsparkState,
9833    pack: &DsparkPack,
9834    kv_id: u64,
9835    last_token: u32,
9836    pos: usize,
9837    pool: Option<&crate::pool::Pool>,
9838    out_conf: &mut Vec<f32>,
9839) -> Vec<u32> {
9840    let (hc, dim) = (cfg.hc_mult, cfg.dim);
9841    let block = dspark_block();
9842    let Some(model) = mtp[0].layer.experts.first().and_then(|e| e.w1.model_arc()) else {
9843        return Vec::new();
9844    };
9845    let (Some(mp), Some(mn)) = (mtp[0].main_proj.as_ref(), mtp[0].main_norm.as_ref()) else {
9846        return Vec::new();
9847    };
9848    let Some(mp_idx) = mp.model_idx() else {
9849        return Vec::new();
9850    };
9851    let mut stages = Vec::with_capacity(mtp.len());
9852    for (si, m) in mtp.iter().enumerate() {
9853        let l = &m.layer;
9854        let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b), Some(wkv)) = (
9855            l.wq_a.model_idx(),
9856            l.wq_b.model_idx(),
9857            l.wo_a.model_idx(),
9858            l.wo_b.model_idx(),
9859            l.wkv.model_idx(),
9860        ) else {
9861            return Vec::new();
9862        };
9863        let sp = &pack.stages[si];
9864        stages.push(crate::gpu_wgpu::DsparkStageW {
9865            wq_a,
9866            wq_b,
9867            wo_a,
9868            wo_b,
9869            wkv,
9870            q_norm: &l.q_norm,
9871            kv_norm: &l.kv_norm,
9872            attn_norm: &l.attn_norm,
9873            ffn_norm: &l.ffn_norm,
9874            sink: &l.attn_sink,
9875            hc_attn_fn: &l.hc_attn_fn,
9876            hc_attn_scale: &l.hc_attn_scale,
9877            hc_attn_base: &l.hc_attn_base,
9878            hc_ffn_fn: &l.hc_ffn_fn,
9879            hc_ffn_scale: &l.hc_ffn_scale,
9880            hc_ffn_base: &l.hc_ffn_base,
9881            router: &pack.routers[si],
9882            bias: pack.biases[si].as_deref(),
9883            experts: &sp.tensors,
9884            mask_u32: &sp.mask_u32,
9885            map_u32: &sp.map_u32,
9886            global: sp.global.as_ref().map(|gl| crate::gpu_wgpu::Dsv4GlobalMoe {
9887                pool_uid: gl.pool.uid,
9888                shared_slot: gl.shared_slot,
9889                segment_slots: gl.pool.segment_slots as u32,
9890            }),
9891        });
9892    }
9893    let geom = crate::gpu_wgpu::DsparkGeom {
9894        dim,
9895        hc,
9896        nh: cfg.n_heads,
9897        hd: cfg.head_dim,
9898        rd: cfg.rope_head_dim,
9899        q_lora: cfg.q_lora_rank,
9900        o_lora: cfg.o_lora_rank,
9901        o_groups: cfg.o_groups,
9902        inter: cfg.moe_inter,
9903        n_experts: cfg.n_routed_experts,
9904        top_k: cfg.top_k,
9905        window: cfg.window,
9906        eps: cfg.norm_eps,
9907        hc_eps: cfg.hc_eps,
9908        sinkhorn_iters: cfg.hc_sinkhorn_iters,
9909        route_scale: cfg.route_scale,
9910        swiglu_limit: cfg.swiglu_limit,
9911        scale: (cfg.head_dim as f32).powf(-0.5),
9912        gu_q2: pack.gu_q2,
9913        dn_q2: pack.dn_q2,
9914    };
9915    // ── seed states: the real token, then noise, replicated over copies ──
9916    let ids: Vec<u32> = (0..block)
9917        .map(|i| {
9918            if i == 0 {
9919                last_token
9920            } else {
9921                DSPARK_NOISE_TOKEN
9922            }
9923        })
9924        .collect();
9925    let mut states0 = vec![0.0f32; block * hc * dim];
9926    let mut emb = vec![0.0f32; dim];
9927    for (i, &id) in ids.iter().enumerate() {
9928        g.embed.row_f32(id as usize, &mut emb);
9929        for j in 0..hc {
9930            states0[(i * hc + j) * dim..(i * hc + j + 1) * dim].copy_from_slice(&emb);
9931        }
9932    }
9933    let dspark_time = {
9934        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9935        *ON.get_or_init(|| std::env::var("CMF_DSPARK_TIME").is_ok_and(|v| v != "0"))
9936    };
9937    let t0 = std::time::Instant::now();
9938    let filled = (pos + 1).min(cfg.window);
9939    let mut states = vec![0.0f32; block * hc * dim];
9940    if !crate::gpu_wgpu::dspark_graph(
9941        &model,
9942        &stages,
9943        geom,
9944        kv_id,
9945        mp_idx,
9946        mn,
9947        &ds.main_hidden,
9948        &states0,
9949        pos,
9950        filled,
9951        &g.inv_freq_window,
9952        block,
9953        &mut states,
9954    ) {
9955        return Vec::new();
9956    }
9957    for si in 0..mtp.len() {
9958        ds.filled[si] = filled;
9959    }
9960    let t_graph = t0.elapsed();
9961
9962    // ── head, on the host: fold, norm, one B-wide matmat, argmax ──
9963    let last = &mtp[mtp.len() - 1];
9964    let (Some(hfn), Some(hbase), Some(hscale), Some(hnorm)) = (
9965        last.hc_head_fn.as_ref(),
9966        last.hc_head_base.as_ref(),
9967        last.hc_head_scale,
9968        last.norm.as_ref(),
9969    ) else {
9970        return Vec::new();
9971    };
9972    let mut head_in = vec![0.0f32; block * dim];
9973    let mut pre_norms = vec![vec![0.0f32; dim]; block];
9974    for i in 0..block {
9975        hc_head_fold(
9976            &states[i * hc * dim..(i + 1) * hc * dim],
9977            hfn,
9978            hscale,
9979            hbase,
9980            cfg,
9981            pool,
9982            &mut head_in[i * dim..(i + 1) * dim],
9983        );
9984        pre_norms[i].copy_from_slice(&head_in[i * dim..(i + 1) * dim]);
9985        rms_weighted(&mut head_in[i * dim..(i + 1) * dim], hnorm, cfg.norm_eps);
9986    }
9987    let t_fold = t0.elapsed();
9988    let mut logits = vec![0.0f32; block * cfg.vocab];
9989    // The B-axis q4tp kernel, one submission: `matmat` at B=5 falls to the
9990    // CPU tile path and measured 46 ms of a 60 ms draft.
9991    let head_gpu = g.head.model_idx().is_some_and(|hi| {
9992        crate::gpu_wgpu::q4tp_matvec_batch_for_test(
9993            &model,
9994            hi,
9995            &head_in,
9996            block,
9997            cfg.vocab,
9998            dim,
9999            &mut logits,
10000        )
10001    });
10002    if !head_gpu {
10003        g.head.matmat(&head_in, block, &mut logits, pool);
10004    }
10005    let t_head = t0.elapsed();
10006    // The markov bigram is not optional: without it acceptance fell 1.02 →
10007    // 0.42 on natural text. Its chain runs through the previous PROPOSAL,
10008    // so it stays position-by-position; the w2 matvec is big enough that
10009    // the QTensor route puts it on the card by itself.
10010    let mut proposals = Vec::with_capacity(block);
10011    out_conf.clear();
10012    let mut prev = last_token;
10013    let mut mk_embed = vec![0.0f32; last.markov_w1.as_ref().map_or(0, |t| t.cols())];
10014    let mut bias = vec![0.0f32; cfg.vocab];
10015    for i in 0..block {
10016        let row = &mut logits[i * cfg.vocab..(i + 1) * cfg.vocab];
10017        if let (Some(w1), Some(w2)) = (last.markov_w1.as_ref(), last.markov_w2.as_ref()) {
10018            w1.row_f32(prev as usize, &mut mk_embed);
10019            w2.matvec(&mk_embed, &mut bias, pool);
10020            for (a, b) in row.iter_mut().zip(&bias) {
10021                *a += *b;
10022            }
10023        }
10024        let mut best = 0usize;
10025        for v in 1..row.len() {
10026            if row[v] > row[best] {
10027                best = v;
10028            }
10029        }
10030        if let Some(cf) = last.confidence.as_ref() {
10031            let mut cat = pre_norms[i].clone();
10032            cat.extend_from_slice(&mk_embed);
10033            let mut sc = [0.0f32; 1];
10034            if cat.len() == cf.cols() {
10035                cf.matvec(&cat, &mut sc, pool);
10036            }
10037            out_conf.push(sc[0]);
10038        }
10039        proposals.push(best as u32);
10040        prev = best as u32;
10041    }
10042    if dspark_time {
10043        eprintln!(
10044            "DSpark GPU: граф {:.1} мс, фолды {:.1}, голова {:.1}, марков+argmax {:.1}",
10045            t_graph.as_secs_f64() * 1e3,
10046            (t_fold - t_graph).as_secs_f64() * 1e3,
10047            (t_head - t_fold).as_secs_f64() * 1e3,
10048            (t0.elapsed() - t_head).as_secs_f64() * 1e3,
10049        );
10050    }
10051    proposals
10052}
10053
10054/// One draft: `DSPARK_BLOCK` proposed tokens and a confidence per position.
10055///
10056/// `pos` is the position of `last_token` — the block predicts `pos+1 ..
10057/// pos+BLOCK`. Returns the proposals in order; `out_conf` takes the
10058/// confidence head's score where the last stage carries one.
10059#[allow(clippy::too_many_arguments)]
10060pub fn dspark_draft(
10061    g: &Dsv4Globals,
10062    mtp: &[Dsv4Mtp],
10063    cfg: &Dsv4Cfg,
10064    ds: &mut DsparkState,
10065    last_token: u32,
10066    pos: usize,
10067    pool: Option<&crate::pool::Pool>,
10068    out_conf: &mut Vec<f32>,
10069) -> Vec<u32> {
10070    let (hc, dim, hd, rd) = (cfg.hc_mult, cfg.dim, cfg.head_dim, cfg.rope_head_dim);
10071    let block = dspark_block();
10072    let inv_freq = &g.inv_freq_window;
10073
10074    // ── the block's input: main_norm(main_proj(captured hiddens)) ──
10075    let Some(stage0) = mtp.first() else {
10076        return Vec::new();
10077    };
10078    let (Some(_mp), Some(_mn)) = (stage0.main_proj.as_ref(), stage0.main_norm.as_ref()) else {
10079        return Vec::new();
10080    };
10081    dspark_ring_append(g, mtp, cfg, ds, pos, pool);
10082
10083    // ── the block: the real token, then noise ──
10084    let ids: Vec<u32> = (0..block)
10085        .map(|i| {
10086            if i == 0 {
10087                last_token
10088            } else {
10089                DSPARK_NOISE_TOKEN
10090            }
10091        })
10092        .collect();
10093    let mut states = vec![vec![0.0f32; hc * dim]; block];
10094    let mut emb = vec![0.0f32; dim];
10095    for (i, &id) in ids.iter().enumerate() {
10096        g.embed.row_f32(id as usize, &mut emb);
10097        for j in 0..hc {
10098            states[i][j * dim..(j + 1) * dim].copy_from_slice(&emb);
10099        }
10100    }
10101
10102    let mut scratch = HcScratch::new(cfg);
10103    for (si, m) in mtp.iter().enumerate() {
10104        let l = &m.layer;
10105        let kvw = l.wkv.rows();
10106        // ── attention half: fold every position first, because each one's
10107        //    keys are visible to all the others. ──
10108        let mut post = vec![vec![0.0f32; hc]; block];
10109        let mut comb = vec![vec![0.0f32; hc * hc]; block];
10110        let mut resid = vec![vec![0.0f32; hc * dim]; block];
10111        let mut folded = vec![vec![0.0f32; dim]; block];
10112        let mix_hc = (2 + hc) * hc;
10113        for i in 0..block {
10114            hc_mixes(
10115                &states[i],
10116                &l.hc_attn_fn,
10117                mix_hc,
10118                cfg.norm_eps,
10119                pool,
10120                &mut scratch.mixes,
10121            );
10122            hc_split_sinkhorn(
10123                &scratch.mixes,
10124                &l.hc_attn_scale,
10125                &l.hc_attn_base,
10126                hc,
10127                cfg.hc_sinkhorn_iters,
10128                cfg.hc_eps,
10129                &mut scratch.pre,
10130                &mut post[i],
10131                &mut comb[i],
10132            );
10133            hc_fold(&states[i], &scratch.pre, hc, dim, &mut folded[i]);
10134            rms_weighted(&mut folded[i], &l.attn_norm, cfg.norm_eps);
10135            resid[i].copy_from_slice(&states[i]);
10136        }
10137        // Keys and values of the block itself — kept for this block only.
10138        let folded_all: Vec<f32> = folded.iter().flatten().copied().collect();
10139        let mut blk_kv = vec![0.0f32; block * kvw];
10140        l.wkv.matmat(&folded_all, block, &mut blk_kv, pool);
10141        for i in 0..block {
10142            let dst = &mut blk_kv[i * kvw..(i + 1) * kvw];
10143            rms_weighted(dst, &l.kv_norm, cfg.norm_eps);
10144            rope_tail(&mut dst[kvw - hd..], inv_freq, pos + 1 + i, rd, false);
10145        }
10146        // The attended set: every cached real position, then the whole block.
10147        let win_len = ds.filled[si];
10148        let mut cache = Vec::with_capacity((win_len + block) * hd);
10149        for p in 0..win_len {
10150            let e = &ds.win[si][p * kvw..(p + 1) * kvw];
10151            cache.extend_from_slice(&e[kvw - hd..]);
10152        }
10153        for i in 0..block {
10154            let e = &blk_kv[i * kvw..(i + 1) * kvw];
10155            cache.extend_from_slice(&e[kvw - hd..]);
10156        }
10157        let idxs: Vec<usize> = (0..win_len + block).collect();
10158        let scale = (hd as f32).powf(-0.5);
10159        let qrank = l.wq_a.rows();
10160        let qdim = cfg.n_heads * hd;
10161        let mut qr = vec![0.0f32; block * qrank];
10162        l.wq_a.matmat(&folded_all, block, &mut qr, pool);
10163        for i in 0..block {
10164            rms_weighted(&mut qr[i * qrank..(i + 1) * qrank], &l.q_norm, cfg.norm_eps);
10165        }
10166        let mut q = vec![0.0f32; block * qdim];
10167        l.wq_b.matmat(&qr, block, &mut q, pool);
10168        let mut attn = vec![0.0f32; block * qdim];
10169        for i in 0..block {
10170            let qi = &mut q[i * qdim..(i + 1) * qdim];
10171            let ai = &mut attn[i * qdim..(i + 1) * qdim];
10172            let qpos = pos + 1 + i;
10173            for h in 0..cfg.n_heads {
10174                let head = &mut qi[h * hd..(h + 1) * hd];
10175                rms_inplace(head, cfg.norm_eps);
10176                rope_tail(head, inv_freq, qpos, rd, false);
10177            }
10178            for h in 0..cfg.n_heads {
10179                let qh = &qi[h * hd..(h + 1) * hd];
10180                let oh = &mut ai[h * hd..(h + 1) * hd];
10181                sparse_attend(qh, &cache, &idxs, l.attn_sink[h], scale, hd, oh);
10182                rope_tail(oh, inv_freq, qpos, rd, true);
10183            }
10184        }
10185        let mut blk_out = vec![0.0f32; block * dim];
10186        o_project_block(
10187            &attn,
10188            block,
10189            &l.wo_a,
10190            &l.wo_b,
10191            cfg.o_groups,
10192            cfg.o_lora_rank,
10193            pool,
10194            &mut blk_out,
10195        );
10196        for i in 0..block {
10197            let mut next = vec![0.0f32; hc * dim];
10198            hc_expand(
10199                &blk_out[i * dim..(i + 1) * dim],
10200                &resid[i],
10201                &post[i],
10202                &comb[i],
10203                hc,
10204                dim,
10205                &mut next,
10206            );
10207            states[i] = next;
10208        }
10209        // ── MoE: fold every position, group equal experts, then expand in
10210        //    the original per-position route order. ──
10211        let mut ffn_fold = vec![0.0f32; block * dim];
10212        let mut ffn_post = vec![vec![0.0f32; hc]; block];
10213        let mut ffn_comb = vec![vec![0.0f32; hc * hc]; block];
10214        let mut ffn_resid = vec![vec![0.0f32; hc * dim]; block];
10215        for i in 0..block {
10216            hc_mixes(
10217                &states[i],
10218                &l.hc_ffn_fn,
10219                mix_hc,
10220                cfg.norm_eps,
10221                pool,
10222                &mut scratch.mixes,
10223            );
10224            hc_split_sinkhorn(
10225                &scratch.mixes,
10226                &l.hc_ffn_scale,
10227                &l.hc_ffn_base,
10228                hc,
10229                cfg.hc_sinkhorn_iters,
10230                cfg.hc_eps,
10231                &mut scratch.pre,
10232                &mut ffn_post[i],
10233                &mut ffn_comb[i],
10234            );
10235            hc_fold(
10236                &states[i],
10237                &scratch.pre,
10238                hc,
10239                dim,
10240                &mut ffn_fold[i * dim..(i + 1) * dim],
10241            );
10242            rms_weighted(
10243                &mut ffn_fold[i * dim..(i + 1) * dim],
10244                &l.ffn_norm,
10245                cfg.norm_eps,
10246            );
10247            ffn_resid[i].copy_from_slice(&states[i]);
10248        }
10249        let mut moe_out = vec![0.0f32; block * dim];
10250        moe_step_block(&ffn_fold, block, l, cfg, &ids, si, pool, &mut moe_out);
10251        for i in 0..block {
10252            let mut next = vec![0.0f32; hc * dim];
10253            hc_expand(
10254                &moe_out[i * dim..(i + 1) * dim],
10255                &ffn_resid[i],
10256                &ffn_post[i],
10257                &ffn_comb[i],
10258                hc,
10259                dim,
10260                &mut next,
10261            );
10262            states[i] = next;
10263        }
10264    }
10265
10266    // ── head: the last stage's fold, the trunk's own head ──
10267    let last = &mtp[mtp.len() - 1];
10268    let (Some(hfn), Some(hbase), Some(hscale), Some(hnorm)) = (
10269        last.hc_head_fn.as_ref(),
10270        last.hc_head_base.as_ref(),
10271        last.hc_head_scale,
10272        last.norm.as_ref(),
10273    ) else {
10274        return Vec::new();
10275    };
10276    let mut proposals = Vec::with_capacity(block);
10277    out_conf.clear();
10278    let mut prev = last_token;
10279    let mut head_in = vec![0.0f32; block * dim];
10280    let mut pre_norms = vec![vec![0.0f32; dim]; block];
10281    for i in 0..block {
10282        hc_head_fold(
10283            &states[i],
10284            hfn,
10285            hscale,
10286            hbase,
10287            cfg,
10288            pool,
10289            &mut head_in[i * dim..(i + 1) * dim],
10290        );
10291        pre_norms[i].copy_from_slice(&head_in[i * dim..(i + 1) * dim]);
10292        rms_weighted(&mut head_in[i * dim..(i + 1) * dim], hnorm, cfg.norm_eps);
10293    }
10294    let mut logits = vec![0.0f32; block * cfg.vocab];
10295    g.head.matmat(&head_in, block, &mut logits, pool);
10296    let mut mk_embed = vec![0.0f32; last.markov_w1.as_ref().map_or(0, |t| t.cols())];
10297    for i in 0..block {
10298        let logits_i = &mut logits[i * cfg.vocab..(i + 1) * cfg.vocab];
10299        // The markov head biases the logits from the PREVIOUS token — a
10300        // rank-256 bigram the draft samples through position by position,
10301        // while the network itself ran the whole block at once.
10302        // `CMF_DSPARK_NO_MARKOV=1` drops it: the bias is sequential through
10303        // the block (each position needs the previous PROPOSAL), which is
10304        // the one part of the draft a single device graph cannot batch — so
10305        // its acceptance value has to be known before it earns that
10306        // complexity.
10307        let no_markov = {
10308            static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10309            *ON.get_or_init(|| std::env::var("CMF_DSPARK_NO_MARKOV").is_ok_and(|v| v != "0"))
10310        };
10311        if no_markov {
10312            // Still feed the confidence head's embedding slot below.
10313            if let Some(w1) = last.markov_w1.as_ref() {
10314                w1.row_f32(prev as usize, &mut mk_embed);
10315            }
10316        } else if let (Some(w1), Some(w2)) = (last.markov_w1.as_ref(), last.markov_w2.as_ref()) {
10317            w1.row_f32(prev as usize, &mut mk_embed);
10318            let mut bias = vec![0.0f32; cfg.vocab];
10319            w2.matvec(&mk_embed, &mut bias, pool);
10320            for (a, b) in logits_i.iter_mut().zip(&bias) {
10321                *a += *b;
10322            }
10323        }
10324        let mut best = 0usize;
10325        for v in 1..logits_i.len() {
10326            if logits_i[v] > logits_i[best] {
10327                best = v;
10328            }
10329        }
10330        if let Some(cf) = last.confidence.as_ref() {
10331            let mut cat = pre_norms[i].clone();
10332            cat.extend_from_slice(&mk_embed);
10333            let mut s = [0.0f32; 1];
10334            if cat.len() == cf.cols() {
10335                cf.matvec(&cat, &mut s, pool);
10336            }
10337            out_conf.push(s[0]);
10338        }
10339        proposals.push(best as u32);
10340        prev = best as u32;
10341    }
10342    proposals
10343}