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 [&ATTN_NS, &MOE_NS, &HC_NS, &HEAD_NS, &ALL_NS, &CALLS, &PREP_NS, &CACHEW_NS] {
1084                    a.store(0, Ordering::Relaxed);
1085                }
1086                TOKENS.store(1, Ordering::Relaxed);
1087                #[cfg(feature = "gpu")]
1088                for a in [
1089                    &crate::gpu_wgpu::MOE_ENC_NS,
1090                    &crate::gpu_wgpu::MOE_WAIT_NS,
1091                    &crate::gpu_wgpu::MOE_BUFS_NS,
1092                    &crate::gpu_wgpu::MOE_UP_NS,
1093                    &crate::gpu_wgpu::MOE_PASS_NS,
1094                    &crate::gpu_wgpu::ATT_ENC_NS,
1095                    &crate::gpu_wgpu::ATT_WAIT_NS,
1096                    &crate::gpu_wgpu::CHAIN_ENC_NS,
1097                    &crate::gpu_wgpu::CHAIN_WAIT_NS,
1098                    &crate::gpu_wgpu::CHAIN_LAYERS,
1099                    &crate::gpu_wgpu::CHAIN_RUNS,
1100                    &crate::gpu_wgpu::SUBMITS,
1101                    &crate::gpu_wgpu::PASSES,
1102                ] {
1103                    a.store(0, Ordering::Relaxed);
1104                }
1105            }
1106        }
1107    }
1108    static REPORT: AtomicBool = AtomicBool::new(false);
1109    /// The one-time "drop the first token's numbers" latch.
1110    static ZEROED: AtomicBool = AtomicBool::new(false);
1111
1112    pub fn on() -> bool {
1113        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1114        *ON.get_or_init(|| std::env::var("CMF_DSV4_PROFILE").is_ok_and(|v| v != "0"))
1115    }
1116
1117    /// Print once, from wherever the last caller happens to be — a process
1118    /// that exits through several paths would otherwise report zero or twice.
1119    pub fn report() {
1120        if !on() || REPORT.swap(true, Ordering::Relaxed) {
1121            return;
1122        }
1123        // CALLS counts layer visits, not tokens — dividing by it and calling
1124        // the result "per token" is off by the layer count, which is 43 on
1125        // the release and reads as a plausible number either way.
1126        let calls = CALLS.load(Ordering::Relaxed).max(1);
1127        let toks = TOKENS.load(Ordering::Relaxed).max(1);
1128        let (a, m) = (
1129            ATTN_NS.load(Ordering::Relaxed) as f64 / 1e6,
1130            MOE_NS.load(Ordering::Relaxed) as f64 / 1e6,
1131        );
1132        let all = ALL_NS.load(Ordering::Relaxed) as f64 / 1e6;
1133        // HC_NS wraps the FFN half's hc_block WHOLE, and moe_step runs
1134        // inside that block — so the raw counter double-counts every MoE
1135        // millisecond as hyper-connection time. Reported as the difference:
1136        // the glue alone. (This inflation is what made moving the
1137        // hyper-connections to the card look like a 19 ms win when the glue
1138        // is ~4.)
1139        let hc = (HC_NS.load(Ordering::Relaxed) as f64 / 1e6
1140            - MOE_NS.load(Ordering::Relaxed) as f64 / 1e6)
1141            .max(0.0);
1142        let hd = HEAD_NS.load(Ordering::Relaxed) as f64 / 1e6;
1143        let prep = PREP_NS.load(Ordering::Relaxed) as f64 / 1e6;
1144        let cw = CACHEW_NS.load(Ordering::Relaxed) as f64 / 1e6;
1145        eprintln!(
1146            "[dsv4-профиль] ХОСТ-ПРЕП слоя: {:.0} мс/токен, KV-заливки: {:.0} мс/токен",
1147            prep / toks as f64,
1148            cw / toks as f64
1149        );
1150        #[cfg(feature = "gpu")]
1151        {
1152            let f = crate::gpu_wgpu::DSV4_FILLS.load(Ordering::Relaxed);
1153            let fb = crate::gpu_wgpu::DSV4_FILL_BYTES.load(Ordering::Relaxed);
1154            eprintln!(
1155                "[dsv4-профиль] ЗАЛИВКИ СЛОТОВ: {:.1} эксп/токен, {:.0} МБ/токен",
1156                f as f64 / toks as f64,
1157                fb as f64 / 1e6 / toks as f64
1158            );
1159        }
1160        eprintln!(
1161            "[dsv4-профиль] {calls} вызовов слоя за {toks} токенов | \
1162             на токен: внимание {:.0} мс, MoE {:.0} мс, гипер-связи+нормы {:.0} мс, \
1163             голова {:.0} мс | на вызов: внимание {:.2}, MoE {:.2}, связи {:.2}",
1164            a / toks as f64,
1165            m / toks as f64,
1166            hc / toks as f64,
1167            hd / toks as f64,
1168            a / calls as f64,
1169            m / calls as f64,
1170            hc / calls as f64,
1171        );
1172        eprintln!(
1173            "[dsv4-профиль] весь проход {:.0} мс на токен; вне счётчиков {:.0} мс",
1174            all / toks as f64,
1175            (all - a - m - hd) / toks as f64,
1176        );
1177        #[cfg(feature = "gpu")]
1178        {
1179            let ae = crate::gpu_wgpu::ATT_ENC_NS.load(Ordering::Relaxed) as f64 / 1e6;
1180            let aw = crate::gpu_wgpu::ATT_WAIT_NS.load(Ordering::Relaxed) as f64 / 1e6;
1181            if ae + aw > 0.0 {
1182                eprintln!(
1183                    "[dsv4-профиль] кадр внимания на вызов: кодирование {:.2} мс, \
1184                     отправка и ожидание {:.2} мс",
1185                    ae / calls as f64,
1186                    aw / calls as f64,
1187                );
1188            }
1189            // At the OUTER level on purpose: this used to sit inside the MoE
1190            // frame's own report, and the chain does not use the MoE frame —
1191            // so the one number that says where a chained token goes was
1192            // printed only when the chain was not running.
1193            let ub = crate::gpu_wgpu::UPLOAD_BYTES.load(Ordering::Relaxed);
1194            let un = crate::gpu_wgpu::UPLOAD_NS.load(Ordering::Relaxed);
1195            if ub > 0 && un > 0 {
1196                eprintln!(
1197                    "[dsv4-профиль] ЗАЛИВКА весов: {:.1} ГБ за {:.1} с ({:.0} МБ/с)",
1198                    ub as f64 / 1e9,
1199                    un as f64 / 1e9,
1200                    ub as f64 / (un as f64 / 1e9) / 1e6,
1201                );
1202            }
1203            let sub = crate::gpu_wgpu::SUBMITS.load(Ordering::Relaxed);
1204            if sub > 0 {
1205                eprintln!(
1206                    "[dsv4-профиль] ОТПРАВОК на карту: {:.1} на токен, ПРОХОДОВ {:.0} \
1207                     ({:.1} на слой)",
1208                    sub as f64 / toks as f64,
1209                    crate::gpu_wgpu::PASSES.load(Ordering::Relaxed) as f64 / toks as f64,
1210                    crate::gpu_wgpu::PASSES.load(Ordering::Relaxed) as f64 / calls as f64,
1211                );
1212            }
1213            let cl = crate::gpu_wgpu::CHAIN_LAYERS.load(Ordering::Relaxed);
1214            if cl > 0 {
1215                let toks2 = toks.max(1) as f64;
1216                eprintln!(
1217                    "[dsv4-профиль] ЦЕПОЧКА на токен: кодирование {:.2} мс, \
1218                     ожидание {:.2} мс ({} слоёв, {} отправок)",
1219                    crate::gpu_wgpu::CHAIN_ENC_NS.load(Ordering::Relaxed) as f64 / 1e6 / toks2,
1220                    crate::gpu_wgpu::CHAIN_WAIT_NS.load(Ordering::Relaxed) as f64 / 1e6 / toks2,
1221                    cl / toks.max(1),
1222                    crate::gpu_wgpu::CHAIN_RUNS.load(Ordering::Relaxed) / toks.max(1),
1223                );
1224            }
1225            let e = crate::gpu_wgpu::MOE_ENC_NS.load(Ordering::Relaxed) as f64 / 1e6;
1226            let wt = crate::gpu_wgpu::MOE_WAIT_NS.load(Ordering::Relaxed) as f64 / 1e6;
1227            if e + wt > 0.0 {
1228                let ns = |a: &std::sync::atomic::AtomicU64| {
1229                    a.load(Ordering::Relaxed) as f64 / 1e6 / calls as f64
1230                };
1231                eprintln!(
1232                    "[dsv4-профиль] кадр MoE на вызов: кодирование {:.2} мс, \
1233                     отправка и ожидание {:.2} мс",
1234                    e / calls as f64,
1235                    wt / calls as f64,
1236                );
1237                let an = crate::gpu_wgpu::ATT_GPU_N.load(Ordering::Relaxed);
1238                if an > 0 {
1239                    let g = |i: usize| {
1240                        crate::gpu_wgpu::ATT_GPU_NS[i].load(Ordering::Relaxed) as f64
1241                            / 1e6
1242                            / an as f64
1243                    };
1244                    eprintln!(
1245                        "[dsv4-профиль]   ВНИМАНИЕ НА КАРТЕ на вызов: одиночное {:.3} мс, \
1246                         оценки {:.3} мс, применение {:.3} мс",
1247                        g(0),
1248                        g(1),
1249                        g(2),
1250                    );
1251                }
1252                let gn = crate::gpu_wgpu::MOE_GPU_N.load(Ordering::Relaxed);
1253                let gns = crate::gpu_wgpu::MOE_GPU_NS[0].load(Ordering::Relaxed);
1254                if gn > 0 && gns > 0 {
1255                    eprintln!(
1256                        "[dsv4-профиль]   MoE НА КАРТЕ: {:.3} мс на вызов ({gn} замеров)",
1257                        gns as f64 / 1e6 / gn as f64,
1258                    );
1259                } else if gn > 0 {
1260                    // Zero across thousands of samples is a broken query, not
1261                    // an instant kernel, and printing it as a time is how a
1262                    // profile starts lying.
1263                    eprintln!(
1264                        "[dsv4-профиль]   MoE НА КАРТЕ: метки вернули НОЛЬ на {gn} замерах — \
1265                         запрос времени не сработал, число не использовать"
1266                    );
1267                }
1268                eprintln!(
1269                    "[dsv4-профиль]   из кодирования: буферы экспертов {:.2} мс, \
1270                     загрузки {:.2} мс, проходы {:.2} мс",
1271                    ns(&crate::gpu_wgpu::MOE_BUFS_NS),
1272                    ns(&crate::gpu_wgpu::MOE_UP_NS),
1273                    ns(&crate::gpu_wgpu::MOE_PASS_NS),
1274                );
1275            }
1276        }
1277    }
1278}
1279
1280/// Print the per-token split, if `CMF_DSV4_PROFILE` asked for one.
1281pub fn profile_report() {
1282    prof::report();
1283}
1284
1285/// `CMF_DSV4_GPU_ATTN=1` moves the attention block onto the device as one
1286/// submission. Off by default: it needs every attention weight in q4tp and a
1287/// working wgpu context, and a frame that declines mid-layer after the state
1288/// has been advanced would be worse than one that never ran.
1289fn gpu_attn_enabled() -> bool {
1290    #[cfg(feature = "gpu")]
1291    {
1292        use std::sync::OnceLock;
1293        static ON: OnceLock<bool> = OnceLock::new();
1294        *ON.get_or_init(|| {
1295            let want = std::env::var("CMF_DSV4_GPU_ATTN")
1296                .map(|v| v != "0")
1297                .unwrap_or(true);
1298            let have = want && crate::gpu::backend_available();
1299            if want && !have && std::env::var("CMF_DSV4_GPU_ATTN").is_ok() {
1300                tracing::warn!(
1301                    "CMF_DSV4_GPU_ATTN задан, но устройства нет — блок внимания                      остаётся на CPU. Проверьте CMF_GPU=wgpu и Vulkan-ICD."
1302                );
1303            }
1304            if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
1305                eprintln!("кадр dsv4: запрошен={want} доступен={have}");
1306            }
1307            have
1308        })
1309    }
1310    #[cfg(not(feature = "gpu"))]
1311    {
1312        false
1313    }
1314}
1315
1316/// The device half of `attention_step`. Returns false — having changed
1317/// nothing — whenever it cannot do the whole block, so the caller's CPU path
1318/// is still correct to run.
1319#[cfg(feature = "gpu")]
1320#[allow(clippy::too_many_arguments)]
1321fn attn_frame(
1322    l: &Dsv4Layer,
1323    cfg: &Dsv4Cfg,
1324    st: &Dsv4State,
1325    li: usize,
1326    hidden: &[f32],
1327    qn: &[f32],
1328    idxs: &[usize],
1329    inv_freq: &[f32],
1330    pos: usize,
1331    win_len: usize,
1332    scale: f32,
1333    // Present: the frame also does this layer's hyper-connection handover
1334    // and leaves the MoE half's input on the card. `out` may then be empty.
1335    hc: Option<&crate::gpu_wgpu::Dsv4HcTail>,
1336    out: &mut [f32],
1337) -> bool {
1338    let hd = cfg.head_dim;
1339    let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b)) = (
1340        l.wq_a.model_idx(),
1341        l.wq_b.model_idx(),
1342        l.wo_a.model_idx(),
1343        l.wo_b.model_idx(),
1344    ) else {
1345        return false;
1346    };
1347    let Some(model) = l.wq_b.model_arc() else {
1348        return false;
1349    };
1350    // Fixed window region, then the compressed tail — so a token writes one
1351    // window slot's worth of movement and whatever the compressor just added,
1352    // not the whole cache. `cap` has to cover the longest run this sequence
1353    // will reach; the compressed axis grows by one entry per `ratio` tokens.
1354    let n_comp = st.compressed[li].len() / hd;
1355    let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
1356    let kv_id = st.kv_id;
1357    // The window is rewritten whole. A ring would write one slot instead of
1358    // 128 — 2 KB against 256 — and was tried: it bought NOTHING (the cost is
1359    // per-dispatch driver bookkeeping, not the copy) and moved perplexity by
1360    // 6e-5 because the attended positions arrive in a different order and the
1361    // softmax accumulates differently. Not a trade worth making.
1362    if !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, 0, &st.window[li], cap) {
1363        return false;
1364    }
1365    // The compressed axis only ever grows, so write the TAIL. Rewriting it
1366    // whole was 22 MB a token at 1024 positions — the cache write, not the
1367    // arithmetic, was what the attention block had left to pay.
1368    // The compressed tail is written WHOLE every token. Writing only the new
1369    // part was tried and gave nothing measurable, and the bookkeeping it
1370    // needs — a per-layer tail count invalidated by every buffer growth — is
1371    // exactly the kind of state that drifts silently and shows up as a model
1372    // that stops early. Not worth carrying for zero.
1373    if n_comp > 0
1374        && !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, cfg.window * hd, &st.compressed[li], cap)
1375    {
1376        return false;
1377    }
1378    let idx32: Vec<u32> = idxs
1379        .iter()
1380        .map(|&p| {
1381            if p < win_len {
1382                p as u32
1383            } else {
1384                (cfg.window + (p - win_len)) as u32
1385            }
1386        })
1387        .collect();
1388    let w = crate::gpu_wgpu::Dsv4AttnW {
1389        wq_a,
1390        wq_b,
1391        wo_a,
1392        wo_b,
1393        q_norm: &l.q_norm,
1394        sink: &l.attn_sink,
1395    };
1396    let g = crate::gpu_wgpu::Dsv4AttnGeom {
1397        dim: cfg.dim,
1398        nh: cfg.n_heads,
1399        hd,
1400        rd: cfg.rope_head_dim,
1401        q_lora: cfg.q_lora_rank,
1402        o_lora: cfg.o_lora_rank,
1403        o_groups: cfg.o_groups,
1404        eps: cfg.norm_eps,
1405        scale,
1406    };
1407    // The host fold, explicitly. The frame used to read this half's input
1408    // from the pooled x2 slot — which a device MoE frame of the SAME layer
1409    // overwrites each token with the NEXT layer's input, so the second
1410    // token of any chain+partial configuration attended over garbage
1411    // (perplexity 5.3 against the 4.578 gold on every budget small enough
1412    // to split a layer). The host has the exact vector either way; one
1413    // hidden-width upload per call is what correctness costs.
1414    crate::gpu_wgpu::dsv4_attn_frame(
1415        &model,
1416        &w,
1417        g,
1418        hidden,
1419        Some(qn),
1420        kv_id,
1421        li,
1422        &idx32,
1423        inv_freq,
1424        pos,
1425        hc,
1426        out,
1427    )
1428}
1429
1430/// What the host still owes the device before a layer frame can run: the
1431/// shared LoRA vector the indexer reads, and the attended position list.
1432#[derive(Default)]
1433pub struct AttnPrep {
1434    pub qr: Vec<f32>,
1435    pub idxs: Vec<usize>,
1436    pub win_len: usize,
1437}
1438
1439#[allow(clippy::too_many_arguments)]
1440pub fn attention_step(
1441    hidden: &[f32],
1442    l: &Dsv4Layer,
1443    cfg: &Dsv4Cfg,
1444    st: &mut Dsv4State,
1445    li: usize,
1446    // Chosen by the caller from the layer's kind — see Dsv4Globals.
1447    inv_freq: &[f32],
1448    pool: Option<&crate::pool::Pool>,
1449    // When set, stop once the caches are advanced and the index list is
1450    // built, and hand those back instead of running attention: the layer
1451    // frame does the rest on the device.
1452    prep_out: Option<&mut AttnPrep>,
1453    out: &mut [f32],
1454) {
1455    let _t0 = prof::on().then(std::time::Instant::now);
1456    let _guard = scopeguard_attn(_t0);
1457    let (hd, rd) = (cfg.head_dim, cfg.rope_head_dim);
1458    let pos = st.pos;
1459    if std::env::var("CMF_FREQ_DEBUG").is_ok() && li == 0 && pos == 0 {
1460        eprintln!(
1461            "    [порт] rd={rd} частот={} inv_freq[0..4]={:?}",
1462            inv_freq.len(),
1463            &inv_freq[..4.min(inv_freq.len())]
1464        );
1465    }
1466
1467    // ── q and kv: both read the same hidden state, so they go out as ONE
1468    // dispatch. The norms after them differ, and they stay separate.
1469    // (q: wq_a → q_norm → wq_b → per-head norm → rope tail;
1470    //  kv: one head's width, shared by every query head.)
1471    let mut qr = vec![0.0f32; cfg.q_lora_rank];
1472    let mut kv = vec![0.0f32; hd];
1473    crate::qtensor::QTensor::matvec_many([&l.wq_a, &l.wkv], hidden, [&mut qr, &mut kv], pool);
1474    rms_weighted(&mut qr, &l.q_norm, cfg.norm_eps);
1475    // The queries are built further down, after the frame has had its chance
1476    // at the whole block. `qr` is needed either way: the indexer reads it.
1477    // A PARTIAL layer walks its attention on the host. Its device MoE
1478    // frame refills the pooled walk slots (x2, the hyper-connection state)
1479    // each token with the NEXT layer's values, so the same layer's device
1480    // attention frame attends over the previous token's leftovers on the
1481    // second token — measured as perplexity 5.3 against the 4.578 gold on
1482    // every budget small enough to split a layer, and exact the moment
1483    // that one layer's attention walks on the host. Layers whose MoE runs
1484    // on the HOST keep their device attention: nothing refills their
1485    // slots mid-walk, and the MAX_LI ladder measures them bit-exact.
1486    // …and it spreads: the partial layer's MoE frame cycles slots that the
1487    // FOLLOWING host-MoE layers' device attention also reads, so in any
1488    // configuration that holds a partial layer, every layer past the chain
1489    // prefix walks its attention on the host. A configuration with no
1490    // partial layer keeps device attention everywhere — the canonical
1491    // stand and the MAX_LI ladder both measure that bit-exact.
1492    let split_config = st.partial_set.iter().any(|&p| p) && st.split_deep;
1493    let past_chain =
1494        st.dev_owned && (li >= st.dev_set.len() || !st.dev_set.get(li).copied().unwrap_or(false));
1495    if std::env::var("CMF_DSV4_GATE_DBG").is_ok() {
1496        eprintln!(
1497            "[gate] li={li} pos={} split={split_config} past={past_chain} dev_owned={} set_len={} part_len={}",
1498            st.pos,
1499            st.dev_owned,
1500            st.dev_set.len(),
1501            st.partial_set.len()
1502        );
1503    }
1504    let on_gpu = gpu_attn_enabled() && !(split_config && past_chain);
1505
1506    rms_weighted(&mut kv, &l.kv_norm, cfg.norm_eps);
1507    rope_tail(&mut kv, inv_freq, pos, rd, false);
1508
1509    // ── the compressor: accumulate `ratio` tokens, then fold them into
1510    // one compressed entry. The reference fires when (pos+1) % ratio == 0,
1511    // so a partial window simply waits — which is why the state carries
1512    // the pending streams across tokens.
1513    if let Some(cp) = &l.compressor {
1514        let mut pk = std::mem::take(&mut st.pending_kv[li]);
1515        let mut ps = std::mem::take(&mut st.pending_score[li]);
1516        let mut qk = std::mem::take(&mut st.prev_kv[li]);
1517        let mut qs = std::mem::take(&mut st.prev_score[li]);
1518        let entry = compressor_step(
1519            cp,
1520            hidden,
1521            pos,
1522            rd,
1523            cfg.norm_eps,
1524            inv_freq,
1525            pool,
1526            &mut pk,
1527            &mut ps,
1528            &mut qk,
1529            &mut qs,
1530        );
1531        st.pending_kv[li] = pk;
1532        st.pending_score[li] = ps;
1533        st.prev_kv[li] = qk;
1534        st.prev_score[li] = qs;
1535        if let Some(e) = entry {
1536            st.compressed[li].extend_from_slice(&e);
1537        }
1538    }
1539    // The indexer scores against ITS OWN compressed cache, built by its own
1540    // compressor. Without this the cache is empty, `n_ix` is zero, and every
1541    // indexer layer picks no compressed positions at all — the long-range
1542    // memory is built and then never read.
1543    if let Some(ix) = &l.indexer {
1544        let mut pk = std::mem::take(&mut st.pending_ix_kv[li]);
1545        let mut ps = std::mem::take(&mut st.pending_ix_score[li]);
1546        let mut qk = std::mem::take(&mut st.prev_ix_kv[li]);
1547        let mut qs = std::mem::take(&mut st.prev_ix_score[li]);
1548        let entry = compressor_step(
1549            &ix.compressor,
1550            hidden,
1551            pos,
1552            rd,
1553            cfg.norm_eps,
1554            inv_freq,
1555            pool,
1556            &mut pk,
1557            &mut ps,
1558            &mut qk,
1559            &mut qs,
1560        );
1561        st.pending_ix_kv[li] = pk;
1562        st.pending_ix_score[li] = ps;
1563        st.prev_ix_kv[li] = qk;
1564        st.prev_ix_score[li] = qs;
1565        if let Some(e) = entry {
1566            st.index_kv[li].extend_from_slice(&e);
1567        }
1568    }
1569
1570    st.window[li].extend_from_slice(&kv);
1571    // The reference keeps the window in a ring of `window_size`; holding the
1572    // last N in order is the same set, and without this the "window" grows
1573    // for the whole generation — wrong attention AND unbounded memory.
1574    let cap = cfg.window * hd;
1575    if st.window[li].len() > cap {
1576        let drop = st.window[li].len() - cap;
1577        st.window[li].drain(..drop);
1578    }
1579    let win_len = st.window[li].len() / hd;
1580    let n_pos = win_len + st.compressed[li].len() / hd;
1581
1582    // Index list: every window position, plus whatever the indexer picked
1583    // (or, without an indexer, every compressed position).
1584    //
1585    // CMF_DSV4_NO_COMPRESSED=1 attends to the sliding window ALONE. That is
1586    // not a mode anyone should serve — it drops the model's long-range
1587    // memory — but it separates two failure modes that look identical from
1588    // the outside: output that degrades because the compressed path is
1589    // wrong, and output that degrades because the weights are too coarse.
1590    let mut idxs: Vec<usize> = (0..win_len).collect();
1591    if !st.compressed[li].is_empty() && !no_compressed() {
1592        let n_comp = st.compressed[li].len() / hd;
1593        match &l.indexer {
1594            Some(ix) => {
1595                // The indexer scores from the SHARED LoRA output through
1596                // its own wq_b — not from attention's queries — and its
1597                // per-head weights are a projection of the hidden state,
1598                // scaled by head_dim^-0.5 * n_heads^-0.5 as the reference
1599                // folds into `weights_proj`'s output.
1600                //
1601                // The reference also applies a randomized Hadamard rotation
1602                // to the queries here and to the keys in the indexer's
1603                // compressor, then simulates FP4 on both. That transform is
1604                // orthogonal (`hadamard_transform` scaled by d^-0.5) and it
1605                // hits BOTH sides of the same dot product, so it cancels:
1606                // its purpose is to condition the FP4 quantization, which we
1607                // do not do either. Omitting the pair is exact, and keeping
1608                // f32 is strictly more accurate than the reference — not an
1609                // approximation to be fixed later.
1610                let ih = ix.weights_proj.rows();
1611                let idim = ix.wq_b.rows() / ih.max(1);
1612                let mut qi = vec![0.0f32; ix.wq_b.rows()];
1613                ix.wq_b.matvec(&qr, &mut qi, pool);
1614                for h in 0..ih {
1615                    rope_tail(&mut qi[h * idim..(h + 1) * idim], inv_freq, pos, rd, false);
1616                }
1617                let mut hw = vec![0.0f32; ih];
1618                ix.weights_proj.matvec(hidden, &mut hw, pool);
1619                let sc_factor = (idim as f32).powf(-0.5) * (ih as f32).powf(-0.5);
1620                for w in hw.iter_mut() {
1621                    *w *= sc_factor;
1622                }
1623                let n_ix = st.index_kv[li].len() / idim.max(1);
1624                let mut sc = Vec::new();
1625                index_scores(
1626                    &qi,
1627                    &st.index_kv[li],
1628                    &hw,
1629                    ih,
1630                    idim,
1631                    n_ix.min(n_comp),
1632                    n_ix.min(n_comp),
1633                    pool,
1634                    &mut sc,
1635                );
1636                let mut picked = Vec::new();
1637                top_k_positions(&sc, cfg.index_topk, &mut picked);
1638                idxs.extend(picked.into_iter().map(|p| win_len + p));
1639            }
1640            None => idxs.extend((0..n_comp).map(|p| win_len + p)),
1641        }
1642    }
1643    debug_assert!(idxs.iter().all(|&p| p < n_pos));
1644    if let Some(p) = prep_out {
1645        p.qr = qr;
1646        p.idxs = idxs;
1647        p.win_len = win_len;
1648        return;
1649    }
1650
1651    // ── the whole block on the device, or nothing ──
1652    let scale = (hd as f32).powf(-0.5);
1653    #[cfg(feature = "gpu")]
1654    if on_gpu
1655        && {
1656            if std::env::var("CMF_DSV4_XCHK").is_ok() {
1657                // The frame reads this half's input from the card's x2
1658                // slot; the host walked its own. Disagreement = the
1659                // chain→walk handoff, and the number says by how much.
1660                if let Some(card) = crate::gpu_wgpu::dsv4_dbg_read_tag(45, 0, hidden.len()) {
1661                    let md = hidden
1662                        .iter()
1663                        .zip(card.iter())
1664                        .map(|(a, b)| (a - b).abs())
1665                        .fold(0.0f32, f32::max);
1666                    eprintln!("[xchk] li={li} pos={pos} x2 maxdiff={md:.3e}");
1667                }
1668            }
1669            true
1670        }
1671        && attn_frame(
1672            l, cfg, st, li, hidden, &qr, &idxs, inv_freq, pos, win_len, scale, None, out,
1673        )
1674    {
1675        return;
1676    }
1677
1678    // ── queries: wq_b, then a norm and the rope tail per head ──
1679    let mut q = vec![0.0f32; cfg.n_heads * hd];
1680    l.wq_b.matvec(&qr, &mut q, pool);
1681    for h in 0..cfg.n_heads {
1682        let head = &mut q[h * hd..(h + 1) * hd];
1683        rms_inplace(head, cfg.norm_eps);
1684        rope_tail(head, inv_freq, pos, rd, false);
1685    }
1686    let mut cache: Vec<f32> = st.window[li].clone();
1687    cache.extend_from_slice(&st.compressed[li]);
1688
1689    // ── sparse attention per head, then the inverse rope ──
1690    let mut attn = vec![0.0f32; cfg.n_heads * hd];
1691    for h in 0..cfg.n_heads {
1692        let qh = &q[h * hd..(h + 1) * hd];
1693        // Straight into this head's slice of the output: the scratch vector
1694        // that used to sit here was an allocation and a copy per head, so 64
1695        // of each per layer per token, for a value that was never read
1696        // anywhere else.
1697        let oh = &mut attn[h * hd..(h + 1) * hd];
1698        sparse_attend(qh, &cache, &idxs, l.attn_sink[h], scale, hd, oh);
1699        rope_tail(oh, inv_freq, pos, rd, true);
1700    }
1701
1702    // ── grouped low-rank output ──
1703    // Read the two blocks through the quantized readers. Materializing them
1704    // here instead costs ~270 MB of dequantization per layer per token on
1705    // the release checkpoint (wo_a and wo_b are 33M weights each), which is
1706    // the difference between decoding and not.
1707    o_project(
1708        &attn,
1709        &|r, x, sc| l.wo_a.row_dot(r, x, sc),
1710        l.wo_a.cols(),
1711        &|mid, dst| l.wo_b.matvec(mid, dst, pool),
1712        cfg.o_groups,
1713        cfg.o_lora_rank,
1714        pool,
1715        out,
1716    );
1717}
1718
1719/// RMSNorm with a learned weight, in place.
1720pub fn rms_weighted(v: &mut [f32], w: &[f32], eps: f32) {
1721    let ms = v.iter().map(|x| x * x).sum::<f32>() / v.len() as f32;
1722    let inv = 1.0 / (ms + eps).sqrt();
1723    for (x, g) in v.iter_mut().zip(w) {
1724        *x = *x * inv * g;
1725    }
1726}
1727
1728// The MoE half of a block: route, run the chosen experts plus the shared one,
1729// and sum. `token_id` is only read on the hash layers. Per-layer expert
1730// routing mass is also recorded here for task-conditional expert sets
1731// (`CMF_MOE_STATS`). An older implementation counted every top-k winner as
1732// one. That is the wrong quantity for DeepSeek-V4: a weak eighth route and
1733// the dominant route then consume the same `cover` budget, so a compact mask
1734// can retain frequent noise while dropping a rarer expert that carries much
1735// more of the block output. Accumulate the normalized route weights as
1736// fixed-point integers instead. The JSON stays the same `{layer: [u64]}`
1737// shape and old count files remain valid inputs because the mask builder only
1738// compares relative mass within a layer.
1739//
1740// Decode drives this from one thread; the pool parallelizes inside the
1741// matvecs, below this point.
1742thread_local! {
1743    static ROUTE_COUNTS: std::cell::RefCell<Vec<Vec<u64>>> =
1744        const { std::cell::RefCell::new(Vec::new()) };
1745}
1746
1747fn route_stats_on() -> bool {
1748    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1749    *ON.get_or_init(|| std::env::var("CMF_MOE_STATS").is_ok())
1750}
1751
1752fn record_route(
1753    li: usize,
1754    n_layers_hint: usize,
1755    n_experts: usize,
1756    routed: &[(usize, f32)],
1757) {
1758    ROUTE_COUNTS.with(|c| {
1759        let mut c = c.borrow_mut();
1760        if c.len() <= li.max(n_layers_hint) {
1761            c.resize(li.max(n_layers_hint) + 1, Vec::new());
1762        }
1763        let row = &mut c[li];
1764        if row.len() < n_experts {
1765            row.resize(n_experts, 0);
1766        }
1767        for &(e, weight) in routed {
1768            if e < row.len() {
1769                // One unit keeps a finite selected route visible even if a
1770                // future quantized router rounds an extremely small weight
1771                // below the fixed-point scale.
1772                let mass = (weight.abs() as f64 * 1_000_000.0).round() as u64;
1773                row[e] = row[e].saturating_add(mass.max(1));
1774            }
1775        }
1776    });
1777}
1778
1779/// Take the recorded routing field, leaving the counters empty.
1780pub fn take_route_counts() -> Vec<Vec<u64>> {
1781    ROUTE_COUNTS.with(|c| std::mem::take(&mut *c.borrow_mut()))
1782}
1783
1784/// Charge elapsed time to a counter when it goes out of scope — the two
1785/// steps have several early returns each, and a timer that only stops on the
1786/// long path measures the short one as free.
1787struct Charge(
1788    Option<std::time::Instant>,
1789    &'static std::sync::atomic::AtomicU64,
1790);
1791impl Drop for Charge {
1792    fn drop(&mut self) {
1793        if let Some(t) = self.0 {
1794            self.1.fetch_add(
1795                t.elapsed().as_nanos() as u64,
1796                std::sync::atomic::Ordering::Relaxed,
1797            );
1798        }
1799    }
1800}
1801fn scopeguard_attn(t: Option<std::time::Instant>) -> Charge {
1802    Charge(t, &prof::ATTN_NS)
1803}
1804fn scopeguard_moe(t: Option<std::time::Instant>, li: usize) -> Charge {
1805    if t.is_some() {
1806        prof::note_layer(li);
1807    }
1808    Charge(t, &prof::MOE_NS)
1809}
1810
1811/// The whole token, one submission per layer. Returns false having changed
1812/// nothing if the device declines any layer — the caller's loop is then still
1813/// correct to run.
1814#[cfg(feature = "gpu")]
1815#[allow(clippy::too_many_arguments)]
1816fn dsv4_layer_loop(
1817    state: &mut [f32],
1818    layers: &[Dsv4Layer],
1819    g: &Dsv4Globals,
1820    cfg: &Dsv4Cfg,
1821    st: &mut Dsv4State,
1822    token_id: u32,
1823    inv_freq: &[f32],
1824    pool: Option<&crate::pool::Pool>,
1825    scratch: &mut HcScratch,
1826) -> bool {
1827    let dim = cfg.dim;
1828    let freqs_of = |l: &Dsv4Layer| -> &[f32] {
1829        let f = if l.compressor.is_some() {
1830            &g.inv_freq_compress
1831        } else {
1832            &g.inv_freq_window
1833        };
1834        if f.is_empty() { inv_freq } else { f.as_slice() }
1835    };
1836    // PRE-FLIGHT. The prep inside the loop advances the window and the
1837    // compressor caches, so a refusal halfway leaves state that the CPU
1838    // fallback would advance a SECOND time — which is not a slow answer but a
1839    // wrong one. Everything that can decline is therefore asked before the
1840    // first byte of state moves. The expert upload happens here too, which is
1841    // where it belonged anyway.
1842    // The head goes to the card BEFORE the experts ask for room. It is the
1843    // single most-used tensor in the file — every token reads all of it —
1844    // and it is a rounding error next to the expert stack: 265 MB against
1845    // ninety-odd gigabytes on the release. Uploaded in first-touch order it
1846    // arrived last, after the budget was gone, and stayed on the host for
1847    // the life of the process.
1848    {
1849        static SAID: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
1850        if !SAID.swap(true, std::sync::atomic::Ordering::Relaxed) {
1851            if let (Some(idx), Some(model)) = (g.head.model_idx(), g.head.model_arc()) {
1852                let ok = crate::gpu_wgpu::dsv4_weight_ready(&model, idx);
1853                tracing::info!("dsv4: голова на карте: {}", if ok { "да" } else { "нет" });
1854            }
1855        }
1856    }
1857    let mut on_dev = vec![false; layers.len()];
1858    let mut partial_dev = vec![false; layers.len()];
1859    for (li, l) in layers.iter().enumerate() {
1860        if l.wq_a.model_idx().is_none()
1861            || l.wq_b.model_idx().is_none()
1862            || l.wo_a.model_idx().is_none()
1863            || l.wo_b.model_idx().is_none()
1864        {
1865            return false;
1866        }
1867        let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
1868            return false;
1869        };
1870        let gu_q2 = l
1871            .experts
1872            .first()
1873            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
1874        // A layer whose experts do not fit is not a reason to abandon the
1875        // token: 100 GB of experts against a 98 GB card means SOME layer will
1876        // always miss. Those run on the host, with the state fetched and put
1877        // back around them — two transfers for the few that need it.
1878        // The attention weights have to be asked for too. Experts fill the
1879        // card first, and a wo_b that misses at layer 11 used to surface as a
1880        // mid-loop refusal — after the caches had advanced, which the CPU
1881        // fallback then advanced again.
1882        // …and, when the layer is to prepare itself, everything that
1883        // preparation reads: the KV projection, both compressors and the
1884        // indexer. Leaving them out is how the chain came to refuse ninety
1885        // times a token on the release — the experts had taken the card by
1886        // the time `dsv4_encode_prep` asked, and it declined silently into a
1887        // fallback that looked like "the chain simply does not help".
1888        let mut want = vec![
1889            l.wq_a.model_idx(),
1890            l.wq_b.model_idx(),
1891            l.wo_a.model_idx(),
1892            l.wo_b.model_idx(),
1893        ];
1894        if chain_enabled() {
1895            want.push(l.wkv.model_idx());
1896            if let Some(cp) = &l.compressor {
1897                want.push(cp.wkv.model_idx());
1898                want.push(cp.wgate.model_idx());
1899            }
1900            if let Some(ix) = &l.indexer {
1901                want.push(ix.wq_b.model_idx());
1902                want.push(ix.weights_proj.model_idx());
1903                want.push(ix.compressor.wkv.model_idx());
1904                want.push(ix.compressor.wgate.model_idx());
1905            }
1906        }
1907        let attn_ok = want
1908            .into_iter()
1909            .flatten()
1910            .all(|i| crate::gpu_wgpu::dsv4_weight_ready(&model, i));
1911        // Size the expert pack only AFTER this layer's attention skeleton is
1912        // resident. Otherwise the pack consumes the apparent free budget,
1913        // the much smaller skeleton arrives next, and the supposedly fitting
1914        // pack misses by exactly those bytes.
1915        let pk = pack_for(l, cfg, li);
1916        if let Some(pk) = pk {
1917            let dn_q2 = l
1918                .experts
1919                .first()
1920                .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
1921            let experts_ok = crate::gpu_wgpu::dsv4_experts_ready(
1922                &model,
1923                &pk.tensors,
1924                cfg.moe_inter,
1925                dim,
1926                gu_q2,
1927                dn_q2,
1928            );
1929            on_dev[li] = attn_ok && experts_ok && pk.route_complete();
1930            partial_dev[li] = attn_ok && experts_ok && !pk.route_complete();
1931        }
1932    }
1933    let active_dev: Vec<bool> = on_dev
1934        .iter()
1935        .zip(&partial_dev)
1936        .map(|(&full, &partial)| full || partial)
1937        .collect();
1938    if !active_dev.iter().any(|&x| x) {
1939        return false;
1940    }
1941    // The attention gate below needs to know about partial layers BEFORE
1942    // the decode path commits the device set — a perplexity run only ever
1943    // prefills, and with this left empty every split budget scored the
1944    // model wrong (measured; see `attention_step`).
1945    if st.partial_set.len() != partial_dev.len() || st.partial_set != partial_dev {
1946        st.partial_set = partial_dev.clone();
1947        st.split_deep = active_dev
1948            .iter()
1949            .zip(&partial_dev)
1950            .filter(|(a, p)| !**a || **p)
1951            .count()
1952            > 1;
1953    }
1954
1955    // Which layers the card actually took, said once. A layer that falls to
1956    // the host costs an order of magnitude more than one that does not, and
1957    // "the GPU path is on" hid the difference between all of them and most.
1958    {
1959        static SAID: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
1960        if !SAID.swap(true, std::sync::atomic::Ordering::Relaxed) {
1961            let host: Vec<usize> = active_dev
1962                .iter()
1963                .enumerate()
1964                .filter(|&(_, d)| !*d)
1965                .map(|(i, _)| i)
1966                .collect();
1967            let partial: Vec<(usize, usize)> = partial_dev
1968                .iter()
1969                .enumerate()
1970                .filter(|&(_, d)| *d)
1971                .filter_map(|(li, _)| pack_for(&layers[li], cfg, li).map(|p| (li, p.globals.len())))
1972                .collect();
1973            if host.is_empty() && partial.is_empty() {
1974                tracing::info!("dsv4: все {} слоёв на карте", on_dev.len());
1975            } else {
1976                tracing::info!(
1977                    "dsv4: {} из {} слоёв используют карту; частичные {:?}; на хосте {:?}",
1978                    active_dev.len() - host.len(),
1979                    on_dev.len(),
1980                    partial,
1981                    host,
1982                );
1983            }
1984        }
1985    }
1986
1987    // Layer zero's opening fold has no frame before it to have prepared it.
1988    let (mut folded, post0, comb0) = hc_fold_norm(
1989        state,
1990        &layers[0].hc_attn_fn,
1991        &layers[0].hc_attn_scale,
1992        &layers[0].hc_attn_base,
1993        &layers[0].attn_norm,
1994        cfg,
1995        pool,
1996    );
1997    if !crate::gpu_wgpu::dsv4_state_write(state) || !crate::gpu_wgpu::dsv4_hc_write(&post0, &comb0)
1998    {
1999        return false;
2000    }
2001    // The device-owned set must not move once a token has run on it — but
2002    // the two directions are not the same risk. At a tight budget the set
2003    // GROWS between tokens as more weights finish uploading, and a layer that
2004    // merely joined can be left on the host: its caches are there and nothing
2005    // is inconsistent. Refusing on that was costing the whole fast path once
2006    // per token — 125 times in a 48-token run on an emulated 24 GB card, on
2007    // which the engine is slow enough already.
2008    //
2009    // A layer LEAVING the set is the dangerous direction: its caches are on
2010    // the card and the host would advance its own. That still refuses.
2011    if st.dev_owned && st.dev_set != active_dev {
2012        let left: Vec<usize> = (0..active_dev.len().min(st.dev_set.len()))
2013            .filter(|&i| st.dev_set[i] && !active_dev[i])
2014            .collect();
2015        if !left.is_empty() {
2016            tracing::warn!("слои {left:?} ушли с карты — кеши на разных сторонах");
2017            return false;
2018        }
2019        // A layer that was active remains device-owned. Its full/partial mode
2020        // is still derived from the current pack; only cache ownership is
2021        // sticky across tokens.
2022    }
2023    let chain = chain_enabled();
2024    // CMF_DSV4_LAYERS_PROBE=N — TIMING ONLY, the answer is garbage. Runs the
2025    // first N layers and leaves the rest alone. Decode time against N is a
2026    // line whose SLOPE is the per-layer cost and whose intercept is
2027    // everything that happens once a token. Unlike the skip probe it does
2028    // not change what a layer does — which on a MoE model is the difference
2029    // between a measurement and an artefact, because dropping any stage
2030    // changes the routing and the routing changes what the experts cost.
2031    let layer_cap = {
2032        static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
2033        *N.get_or_init(|| {
2034            std::env::var("CMF_DSV4_LAYERS_PROBE")
2035                .ok()
2036                .and_then(|v| v.parse::<usize>().ok())
2037                .unwrap_or(usize::MAX)
2038        })
2039    };
2040    let mut run: Vec<usize> = Vec::new();
2041    let mut sink_out = vec![0.0f32; dim];
2042    // `state` starts current on both sides. A device run makes the host copy
2043    // stale unless that same run carries it home. Tracking this explicitly
2044    // avoids a separate state fence before a host layer and, for a final host
2045    // layer, the old upload-immediately-followed-by-readback pair.
2046    let mut state_on_host = true;
2047    for (li, l) in layers.iter().enumerate() {
2048        if li >= layer_cap {
2049            break;
2050        }
2051        // The device path never ticked the profiler, so every per-token
2052        // number it printed described the two host-path tokens at the start
2053        // of a run — the ones that also pay for the upload. Ticking here is
2054        // what makes the chain's encode-and-wait split a per-token figure at
2055        // all.
2056        if prof::on() {
2057            prof::note_layer(li);
2058        }
2059        if chain && on_dev[li] {
2060            // Hash layers used to break the run in two: their forced expert
2061            // list changes per token, went through the (tag, len) upload
2062            // pool, and every layer of a submission shared one buffer. The
2063            // list has a per-layer slot now, so they chain like the rest.
2064            run.push(li);
2065            // CMF_DSV4_CHAIN_MAX=N caps a run's length. Diagnostic, not a
2066            // tuning knob: length-1 runs put ONE layer per submission, which
2067            // separates "the layer frame is wrong" from "layers in one
2068            // encoder contaminate each other" in a single ppl run.
2069            if run.len() >= chain_max() || dspark_wants(li) {
2070                let need_qn = run[0] == 0 || !on_dev[run[0] - 1];
2071                let captured = *run.last().unwrap();
2072                if !dsv4_chain_run(
2073                    layers,
2074                    &run,
2075                    cfg,
2076                    g,
2077                    st,
2078                    token_id,
2079                    &mut folded,
2080                    Some(state),
2081                    1,
2082                    &[],
2083                    need_qn,
2084                    pool,
2085                ) {
2086                    return false;
2087                }
2088                state_on_host = true;
2089                verify_fp("walk", st.pos, captured, state);
2090                dspark_note(captured, state, cfg);
2091                run.clear();
2092            }
2093            continue;
2094        }
2095        if chain && !run.is_empty() {
2096            // The very next layer is on the host, so bring its state back in
2097            // the chain's existing readback. Reading it in a second submit
2098            // below cost one fence per token on the release's 42+1 split.
2099            if !dsv4_chain_run(
2100                layers,
2101                &run,
2102                cfg,
2103                g,
2104                st,
2105                token_id,
2106                &mut folded,
2107                Some(state),
2108                1,
2109                &[],
2110                run[0] == 0 || !on_dev[run[0] - 1],
2111                pool,
2112            ) {
2113                return false;
2114            }
2115            state_on_host = true;
2116            let last = *run.last().unwrap();
2117            verify_fp("walk", st.pos, last, state);
2118            dspark_note(last, state, cfg);
2119        }
2120        run.clear();
2121        if partial_dev[li] && chain1_on() {
2122            if let Some(home) = dsv4_chain1_layer(
2123                state,
2124                &mut folded,
2125                layers,
2126                l,
2127                cfg,
2128                st,
2129                token_id,
2130                li,
2131                freqs_of(l),
2132                pool,
2133                state_on_host,
2134            ) {
2135                // chain1 advances the canonical host mirrors and rewrites
2136                // the device window/compressed cache from them, but it does
2137                // not go through dsv4_chain_run (the usual owner of these
2138                // arithmetic counters).  A speculative token-axis pass that
2139                // takes over on the next token must start from the same
2140                // extents, otherwise its very first partial layer attends to
2141                // an empty/stale prefix.
2142                st.dev_filled[li] = (st.window[li].len() / cfg.head_dim).min(cfg.window);
2143                st.dev_n_comp[li] = st.compressed[li].len() / cfg.head_dim;
2144                st.dev_n_ix[li] = l.indexer.as_ref().map_or(0, |ix| {
2145                    let ih = ix.weights_proj.rows();
2146                    let idim = ix.wq_b.rows() / ih.max(1);
2147                    st.index_kv[li].len() / idim.max(1)
2148                });
2149                state_on_host = home;
2150                if verify_fp_on(st.pos) {
2151                    if !state_on_host && !crate::gpu_wgpu::dsv4_state_read(state) {
2152                        return false;
2153                    }
2154                    state_on_host = true;
2155                    verify_fp("walk", st.pos, li, state);
2156                }
2157                // The draft captures THIS layer's state — which lives on
2158                // the card when no cold came home. Noting the stale host
2159                // array fed the draft garbage: 1265 drafted, 0 accepted.
2160                if dspark_wants(li) {
2161                    if !state_on_host && crate::gpu_wgpu::dsv4_state_read(state) {
2162                        state_on_host = true;
2163                    }
2164                    if state_on_host {
2165                        dspark_note(li, state, cfg);
2166                    }
2167                }
2168                continue;
2169            }
2170        }
2171        if partial_dev[li] && partial_walk_on() {
2172            // Attention and the resident expert subset stay on the card. The
2173            // router still sees every expert and returns only the winners
2174            // that did not fit; those are completed on the CPU and their
2175            // exact linear contribution is added back to device state.
2176            let Some(home) = dsv4_partial_layer(
2177                state,
2178                &mut folded,
2179                layers,
2180                l,
2181                cfg,
2182                st,
2183                token_id,
2184                li,
2185                freqs_of(l),
2186                pool,
2187                state_on_host,
2188            ) else {
2189                return false;
2190            };
2191            state_on_host = home;
2192            if home {
2193                dspark_note(li, state, cfg);
2194            }
2195            continue;
2196        }
2197        if !on_dev[li] {
2198            if !state_on_host && !crate::gpu_wgpu::dsv4_state_read(state) {
2199                return false;
2200            }
2201            state_on_host = true;
2202            let freqs = freqs_of(l);
2203            hc_block(
2204                state,
2205                &l.hc_attn_fn,
2206                &l.hc_attn_scale,
2207                &l.hc_attn_base,
2208                &l.attn_norm,
2209                cfg,
2210                scratch,
2211                pool,
2212                |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
2213            );
2214            hc_block(
2215                state,
2216                &l.hc_ffn_fn,
2217                &l.hc_ffn_scale,
2218                &l.hc_ffn_base,
2219                &l.ffn_norm,
2220                cfg,
2221                scratch,
2222                pool,
2223                // The layer the card had no room for. Its experts are
2224                // reached one matvec at a time and the probe sends each to
2225                // the device — right per op, and a fence per op: this one
2226                // layer is why a token that submits ONCE for 42 layers
2227                // submits 13 times. CMF_DSV4_HOST_CPU_MOE=1 keeps them on
2228                // the host instead, trading arithmetic for round trips.
2229                |f, o| {
2230                    if host_cpu_moe() {
2231                        crate::gpu::cpu_scope(|| moe_step(f, l, cfg, token_id, li, pool, o))
2232                    } else {
2233                        moe_step(f, l, cfg, token_id, li, pool, o)
2234                    }
2235                },
2236            );
2237            // Only a following DEVICE layer needs the fold/hc slots and an
2238            // uploaded state. Consecutive host layers consume `state`
2239            // directly, and a final host layer is already exactly where the
2240            // head needs it — uploading then reading it back was pure sync.
2241            if layers.get(li + 1).is_some() && on_dev.get(li + 1).copied().unwrap_or(false) {
2242                let n = &layers[li + 1];
2243                let (f, p2, c2) = hc_fold_norm(
2244                    state,
2245                    &n.hc_attn_fn,
2246                    &n.hc_attn_scale,
2247                    &n.hc_attn_base,
2248                    &n.attn_norm,
2249                    cfg,
2250                    pool,
2251                );
2252                folded = f;
2253                if !crate::gpu_wgpu::dsv4_hc_write(&p2, &c2) {
2254                    return false;
2255                }
2256                if !crate::gpu_wgpu::dsv4_state_write(state) {
2257                    return false;
2258                }
2259            }
2260            dspark_note(li, state, cfg);
2261            continue;
2262        }
2263        let mut prep = AttnPrep::default();
2264        let _tp = prof::on().then(std::time::Instant::now);
2265        attention_step(
2266            &folded,
2267            l,
2268            cfg,
2269            st,
2270            li,
2271            freqs_of(l),
2272            pool,
2273            Some(&mut prep),
2274            &mut sink_out,
2275        );
2276        if let Some(t) = _tp {
2277            prof::PREP_NS.fetch_add(
2278                t.elapsed().as_nanos() as u64,
2279                std::sync::atomic::Ordering::Relaxed,
2280            );
2281        }
2282        // The caches the frame will read.
2283        let hd = cfg.head_dim;
2284        let n_comp = st.compressed[li].len() / hd;
2285        let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
2286        let kv_id = st.kv_id;
2287        let _tc = prof::on().then(std::time::Instant::now);
2288        if !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, 0, &st.window[li], cap)
2289            || (n_comp > 0
2290                && !crate::gpu_wgpu::dsv4_cache_write(
2291                    kv_id,
2292                    li,
2293                    cfg.window * hd,
2294                    &st.compressed[li],
2295                    cap,
2296                ))
2297        {
2298            return false;
2299        }
2300        if let Some(t) = _tc {
2301            prof::CACHEW_NS.fetch_add(
2302                t.elapsed().as_nanos() as u64,
2303                std::sync::atomic::Ordering::Relaxed,
2304            );
2305        }
2306        let idx32: Vec<u32> = prep
2307            .idxs
2308            .iter()
2309            .map(|&p| {
2310                if p < prep.win_len {
2311                    p as u32
2312                } else {
2313                    (cfg.window + (p - prep.win_len)) as u32
2314                }
2315            })
2316            .collect();
2317        let Some(pk) = pack_for(l, cfg, li) else {
2318            return false;
2319        };
2320        let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b)) = (
2321            l.wq_a.model_idx(),
2322            l.wq_b.model_idx(),
2323            l.wo_a.model_idx(),
2324            l.wo_b.model_idx(),
2325        ) else {
2326            return false;
2327        };
2328        let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
2329            return false;
2330        };
2331        let forced: Option<Vec<usize>> = l.tid2eid.as_ref().and_then(|tbl| {
2332            let v: Vec<usize> = if pk.needs_remap() {
2333                hash_route(tbl, cfg.vocab, cfg.top_k, token_id)
2334            } else {
2335                hash_route(tbl, cfg.vocab, cfg.top_k, token_id)
2336                    .into_iter()
2337                    .map(|gi| pk.to_slot[gi])
2338                    .collect()
2339            };
2340            if v.contains(&usize::MAX) {
2341                None
2342            } else {
2343                Some(v)
2344            }
2345        });
2346        if l.tid2eid.is_some() && forced.is_none() {
2347            return false;
2348        }
2349        let nxt = layers.get(li + 1);
2350        let w = crate::gpu_wgpu::Dsv4LayerW {
2351            attn: crate::gpu_wgpu::Dsv4AttnW {
2352                wq_a,
2353                wq_b,
2354                wo_a,
2355                wo_b,
2356                q_norm: &l.q_norm,
2357                sink: &l.attn_sink,
2358            },
2359            moe: crate::gpu_wgpu::Dsv4MoeW {
2360                router: &[],
2361                experts: &pk.tensors,
2362                logits: &[],
2363                // The PACK's bias, whose address outlives the process: the
2364                // frame's const cache is keyed on it, and a per-layer Vec
2365                // here handed every layer the first layer's — the exact
2366                // transient-Vec trap the const_buf war story describes,
2367                // reintroduced by this session and caught because the OFF
2368                // baseline moved.
2369                bias: pk.bias.as_deref(),
2370                mask: pk.mask.as_deref(),
2371                forced: forced.as_deref(),
2372                remap: pk.needs_remap().then_some(pk.remap.as_slice()),
2373            },
2374            hc_ffn_fn: &l.hc_ffn_fn,
2375            hc_ffn_scale: &l.hc_ffn_scale,
2376            hc_ffn_base: &l.hc_ffn_base,
2377            hc_next_fn: nxt.map(|n| n.hc_attn_fn.as_slice()),
2378            hc_next_scale: nxt.map_or(&l.hc_attn_scale, |n| &n.hc_attn_scale),
2379            hc_next_base: nxt.map_or(&l.hc_attn_base, |n| n.hc_attn_base.as_slice()),
2380            ffn_norm: &l.ffn_norm,
2381            next_norm: nxt.map_or(&l.attn_norm, |n| n.attn_norm.as_slice()),
2382            next_q_norm: nxt.map_or(&l.q_norm, |n| n.q_norm.as_slice()),
2383            next_wq_a: nxt.and_then(|n| n.wq_a.model_idx()),
2384            router: &pk.router,
2385        };
2386        let geom = crate::gpu_wgpu::Dsv4LayerGeom {
2387            attn: crate::gpu_wgpu::Dsv4AttnGeom {
2388                dim,
2389                nh: cfg.n_heads,
2390                hd,
2391                rd: cfg.rope_head_dim,
2392                q_lora: cfg.q_lora_rank,
2393                o_lora: cfg.o_lora_rank,
2394                o_groups: cfg.o_groups,
2395                eps: cfg.norm_eps,
2396                scale: (hd as f32).powf(-0.5),
2397            },
2398            moe: crate::gpu_wgpu::Dsv4MoeGeom {
2399                hidden: dim,
2400                inter: cfg.moe_inter,
2401                top_k: cfg.top_k,
2402                route_scale: cfg.route_scale,
2403                swiglu_limit: cfg.swiglu_limit,
2404                gu_q2: l.experts.first().is_some_and(|e| {
2405                    e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
2406                }),
2407            },
2408            hc: cfg.hc_mult,
2409            hc_eps: cfg.hc_eps,
2410            sinkhorn_iters: cfg.hc_sinkhorn_iters,
2411        };
2412        let mut next = vec![0.0f32; dim];
2413        if !crate::gpu_wgpu::dsv4_layer_frame(
2414            &model,
2415            &w,
2416            geom,
2417            kv_id,
2418            li,
2419            Some(&prep.qr),
2420            &idx32,
2421            freqs_of(l),
2422            st.pos,
2423            &mut next,
2424            None,
2425            None,
2426            &mut Vec::new(),
2427        ) {
2428            return false;
2429        }
2430        state_on_host = false;
2431        folded = next;
2432        dspark_note(li, state, cfg);
2433    }
2434    let mut state_home = false;
2435    if chain {
2436        if !run.is_empty() {
2437            // The token's LAST run brings the state back with it. Only the
2438            // last: an earlier run's state is one the layers after it still
2439            // change.
2440            let need_qn = run[0] == 0 || !on_dev[run[0] - 1];
2441            let last_on_dev = *on_dev.last().unwrap_or(&false);
2442            let carry = last_on_dev && run.last() == Some(&(layers.len() - 1));
2443            let ok = if carry {
2444                let r = dsv4_chain_run(
2445                    layers,
2446                    &run,
2447                    cfg,
2448                    g,
2449                    st,
2450                    token_id,
2451                    &mut folded,
2452                    Some(state),
2453                    1,
2454                    &[],
2455                    need_qn,
2456                    pool,
2457                );
2458                state_home = r;
2459                state_on_host = r;
2460                if r {
2461                    dspark_note(*run.last().unwrap(), state, cfg);
2462                }
2463                r
2464            } else {
2465                let r = dsv4_chain_run(
2466                    layers,
2467                    &run,
2468                    cfg,
2469                    g,
2470                    st,
2471                    token_id,
2472                    &mut folded,
2473                    None,
2474                    1,
2475                    &[],
2476                    need_qn,
2477                    pool,
2478                );
2479                if r {
2480                    state_on_host = false;
2481                }
2482                r
2483            };
2484            if !ok {
2485                return false;
2486            }
2487        }
2488        if st.dev_set.is_empty() {
2489            st.dev_set = active_dev.clone();
2490            st.partial_set = partial_dev.clone();
2491            // The set is committed, so the card must keep it. Eviction by
2492            // score is right while the set is still being chosen and wrong
2493            // afterwards: an evicted layer drops off the card while its
2494            // caches stay there, and the loop then refuses the whole fast
2495            // path rather than read state from two sides.
2496            let mut idxs = Vec::new();
2497            for (li, l) in layers.iter().enumerate() {
2498                if !active_dev.get(li).copied().unwrap_or(false) {
2499                    continue;
2500                }
2501                for t in [&l.wq_a, &l.wq_b, &l.wkv, &l.wo_a, &l.wo_b, &l.gate] {
2502                    idxs.extend(t.model_idx());
2503                }
2504                if let Some(pk) = pack_for(l, cfg, li) {
2505                    for &(a, b, c) in &pk.tensors {
2506                        idxs.extend([a, b, c]);
2507                    }
2508                }
2509            }
2510            // Why a HOST layer stayed on the host, said in numbers. Its MoE
2511            // can still run on the card with a partial pack — `moe_frame` has
2512            // the remap and hands cold picks back — so the interesting figure
2513            // is how many experts it got. Zero means the upload order never
2514            // reached it; a few hundred means the readiness gate refused. The
2515            // two have different fixes and reading the code cannot tell them
2516            // apart.
2517            for (li, l) in layers.iter().enumerate() {
2518                if active_dev.get(li).copied().unwrap_or(false) {
2519                    continue;
2520                }
2521                let packed = pack_for(l, cfg, li).map_or(0, |p| p.globals.len());
2522                tracing::info!(
2523                    "слой {li} на хосте: упаковано {packed} экспертов из {}",
2524                    cfg.n_routed_experts
2525                );
2526            }
2527            let pinned = layers
2528                .iter()
2529                .find_map(|l| l.experts.first().and_then(|e| e.w1.model_arc()))
2530                .map_or(0, |m| crate::gpu_wgpu::pin_weights(&m, &idxs));
2531            tracing::info!(
2532                "закреплено на карте: {pinned} тензоров {} слоёв",
2533                on_dev.iter().filter(|&&x| x).count()
2534            );
2535        }
2536    }
2537    if state_home || state_on_host {
2538        return true;
2539    }
2540    crate::gpu_wgpu::dsv4_state_read(state)
2541}
2542
2543/// Run a layer whose attention skeleton fits but only a subset of its MoE
2544/// experts does. This path is selected from the live VRAM budget, never from
2545/// a layer number. It is exact: routing spans all experts and cold winners
2546/// are folded back into the hyper-connection state before the next layer.
2547#[cfg(feature = "gpu")]
2548#[allow(clippy::too_many_arguments)]
2549fn dsv4_partial_layer(
2550    state: &mut [f32],
2551    folded: &mut Vec<f32>,
2552    layers: &[Dsv4Layer],
2553    l: &Dsv4Layer,
2554    cfg: &Dsv4Cfg,
2555    st: &mut Dsv4State,
2556    token_id: u32,
2557    li: usize,
2558    freqs: &[f32],
2559    pool: Option<&crate::pool::Pool>,
2560    state_on_host: bool,
2561) -> Option<bool> {
2562    let dim = cfg.dim;
2563    // The self-poisoning this walk was parked for: its frames read the
2564    // pooled post/comb/state slots, and whatever layer ran a frame LAST —
2565    // on this token or the previous one — left its own there. The walk
2566    // now seeds its OWN slots from the state it holds at entry, and is
2567    // immune to the neighbours. The state is home whenever the previous
2568    // layer exited through this walk or the host branch; a device exit
2569    // (full-layer frame) leaves it on the card, where the slots are
2570    // already this token's — nothing to reseed then.
2571    if state_on_host {
2572        let (f, post, comb) = hc_fold_norm(
2573            state,
2574            &l.hc_attn_fn,
2575            &l.hc_attn_scale,
2576            &l.hc_attn_base,
2577            &l.attn_norm,
2578            cfg,
2579            pool,
2580        );
2581        *folded = f;
2582        if !crate::gpu_wgpu::dsv4_hc_write(&post, &comb)
2583            || !crate::gpu_wgpu::dsv4_state_write(state)
2584        {
2585            return None;
2586        }
2587    }
2588    let mut prep = AttnPrep::default();
2589    let mut sink = vec![0.0f32; dim];
2590    attention_step(
2591        folded,
2592        l,
2593        cfg,
2594        st,
2595        li,
2596        freqs,
2597        pool,
2598        Some(&mut prep),
2599        &mut sink,
2600    );
2601    let hd = cfg.head_dim;
2602    let n_comp = st.compressed[li].len() / hd;
2603    let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
2604    if !crate::gpu_wgpu::dsv4_cache_write(st.kv_id, li, 0, &st.window[li], cap)
2605        || (n_comp > 0
2606            && !crate::gpu_wgpu::dsv4_cache_write(
2607                st.kv_id,
2608                li,
2609                cfg.window * hd,
2610                &st.compressed[li],
2611                cap,
2612            ))
2613    {
2614        return None;
2615    }
2616    let a_tail = crate::gpu_wgpu::Dsv4HcTail {
2617        fn_: &l.hc_ffn_fn,
2618        scale: &l.hc_ffn_scale,
2619        base: &l.hc_ffn_base,
2620        norm: &l.ffn_norm,
2621        hc: cfg.hc_mult,
2622        sinkhorn_iters: cfg.hc_sinkhorn_iters,
2623        hc_eps: cfg.hc_eps,
2624        eps: cfg.norm_eps,
2625    };
2626    let scale = (cfg.head_dim as f32).powf(-0.5);
2627    // Optional FreeToken-style split point. Reading the normed FFN input
2628    // here adds one fence between attention and MoE, but it also lets the
2629    // host predict and execute cold experts while the resident experts are
2630    // running on the GPU. Keep the old no-readback path as the default: on
2631    // a warm/full pack the extra fence has nothing to hide and only hurts.
2632    let mut ffn_input = if cpu_overlap_on() {
2633        vec![0.0f32; dim]
2634    } else {
2635        Vec::new()
2636    };
2637    if !attn_frame(
2638        l,
2639        cfg,
2640        st,
2641        li,
2642        folded,
2643        &prep.qr,
2644        &prep.idxs,
2645        freqs,
2646        st.pos,
2647        prep.win_len,
2648        scale,
2649        Some(&a_tail),
2650        &mut ffn_input,
2651    ) {
2652        return None;
2653    }
2654    let nxt = layers.get(li + 1);
2655    let forced = l
2656        .tid2eid
2657        .as_ref()
2658        .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
2659    let mut next = vec![0.0f32; dim];
2660    let (cold_sum, cold_count) = moe_frame(
2661        &ffn_input,
2662        l,
2663        cfg,
2664        li,
2665        &[],
2666        forced.as_deref(),
2667        pool,
2668        Some(&a_tail),
2669        // Do not pre-fold the next layer yet. That fold reuses the canonical
2670        // `post` slot; a cold correction still needs THIS layer's post. Once
2671        // the corrected state is home, the exact next fold is cheap on the
2672        // host and seeds either another partial frame or the next full run.
2673        None,
2674        &mut next,
2675    )?;
2676    // The resident contribution has already been expanded on the device. If
2677    // there were cold winners, add `post[j] * cold_sum` and retrieve the
2678    // corrected state in that submission; otherwise a plain readback is
2679    // enough. This state handoff is what makes partial layers composable at
2680    // arbitrary positions, not just at the tail of one checkpoint.
2681    let state_ok = if cold_count == 0 {
2682        crate::gpu_wgpu::dsv4_state_read(state)
2683    } else {
2684        crate::gpu_wgpu::dsv4_state_add_cold(&cold_sum, cfg.hc_mult, state)
2685    };
2686    if !state_ok {
2687        return None;
2688    }
2689    if let Some(n) = nxt {
2690        let (f, post, comb) = hc_fold_norm(
2691            state,
2692            &n.hc_attn_fn,
2693            &n.hc_attn_scale,
2694            &n.hc_attn_base,
2695            &n.attn_norm,
2696            cfg,
2697            pool,
2698        );
2699        *folded = f;
2700        if !crate::gpu_wgpu::dsv4_hc_write(&post, &comb)
2701            || !crate::gpu_wgpu::dsv4_state_write(state)
2702        {
2703            return None;
2704        }
2705    }
2706    // NB: the CALLER notes this layer for the draft's ring — a note here
2707    // as well double-counts the capture and fails `dspark_take`'s
2708    // completeness check (seen 4 of 3, measured), which reads exactly like
2709    // the starvation it was meant to fix.
2710    Some(true)
2711}
2712
2713/// `CMF_DSV4_CHAIN1=1`: a partial layer runs as ONE submission — attention,
2714/// folds and the subset MoE in a single frame, the state staying on the
2715/// card when every winner was resident (the common case once the slots
2716/// warm). Cold winners pay the walk's exact correction from the preserved
2717/// post. Enabled by default after parity and long-run measurements on RTX
2718/// 5090, RTX PRO 6000 and A40; `CMF_DSV4_CHAIN1=0` keeps the old bisect.
2719#[cfg(feature = "gpu")]
2720fn chain1_on() -> bool {
2721    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2722    *ON.get_or_init(|| {
2723        std::env::var("CMF_DSV4_CHAIN1")
2724            .map(|v| v != "0")
2725            .unwrap_or(true)
2726    })
2727}
2728
2729/// `CMF_DSV4_CPU_OVERLAP=1`: on the two-frame partial walk, read the exact
2730/// normalized MoE input after attention and use it to overlap cold CPU
2731/// experts with the resident GPU frame. This is deliberately independent
2732/// from `CMF_DSV4_PARTIAL_WALK`: it is a measured alternative to chain-of-one,
2733/// not a new default for full packs.
2734#[cfg(feature = "gpu")]
2735fn cpu_overlap_on() -> bool {
2736    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2737    *ON.get_or_init(|| std::env::var("CMF_DSV4_CPU_OVERLAP").is_ok_and(|v| v != "0"))
2738}
2739
2740#[cfg(feature = "gpu")]
2741#[allow(clippy::too_many_arguments)]
2742fn dsv4_chain1_layer(
2743    state: &mut [f32],
2744    folded: &mut Vec<f32>,
2745    layers: &[Dsv4Layer],
2746    l: &Dsv4Layer,
2747    cfg: &Dsv4Cfg,
2748    st: &mut Dsv4State,
2749    token_id: u32,
2750    li: usize,
2751    freqs: &[f32],
2752    pool: Option<&crate::pool::Pool>,
2753    state_on_host: bool,
2754) -> Option<bool> {
2755    let dim = cfg.dim;
2756    let pk = pack_for(l, cfg, li)?;
2757    if pk.route_complete() {
2758        return None;
2759    }
2760    let model = l.experts.first().and_then(|e| e.w1.model_arc())?;
2761    // The same entry self-seed the repaired walk uses: the frame reads the
2762    // pooled post/comb/state slots, and this layer's own are the only ones
2763    // it may trust.
2764    if state_on_host {
2765        let (f, post, comb) = hc_fold_norm(
2766            state,
2767            &l.hc_attn_fn,
2768            &l.hc_attn_scale,
2769            &l.hc_attn_base,
2770            &l.attn_norm,
2771            cfg,
2772            pool,
2773        );
2774        *folded = f;
2775        if !crate::gpu_wgpu::dsv4_hc_write(&post, &comb)
2776            || !crate::gpu_wgpu::dsv4_state_write(state)
2777        {
2778            return None;
2779        }
2780    }
2781    let mut prep = AttnPrep::default();
2782    let mut sink = vec![0.0f32; dim];
2783    attention_step(
2784        folded,
2785        l,
2786        cfg,
2787        st,
2788        li,
2789        freqs,
2790        pool,
2791        Some(&mut prep),
2792        &mut sink,
2793    );
2794    let hd = cfg.head_dim;
2795    let n_comp = st.compressed[li].len() / hd;
2796    let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
2797    let kv_id = st.kv_id;
2798    if !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, 0, &st.window[li], cap)
2799        || (n_comp > 0
2800            && !crate::gpu_wgpu::dsv4_cache_write(
2801                kv_id,
2802                li,
2803                cfg.window * hd,
2804                &st.compressed[li],
2805                cap,
2806            ))
2807    {
2808        return None;
2809    }
2810    let idx32: Vec<u32> = prep
2811        .idxs
2812        .iter()
2813        .map(|&p| {
2814            if p < prep.win_len {
2815                p as u32
2816            } else {
2817                (cfg.window + (p - prep.win_len)) as u32
2818            }
2819        })
2820        .collect();
2821    let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b)) = (
2822        l.wq_a.model_idx(),
2823        l.wq_b.model_idx(),
2824        l.wo_a.model_idx(),
2825        l.wo_b.model_idx(),
2826    ) else {
2827        return None;
2828    };
2829    // Under the subset contract the forced list stays GLOBAL: the remap
2830    // either finds each hash winner a slot or returns it cold.
2831    let forced: Option<Vec<usize>> = l
2832        .tid2eid
2833        .as_ref()
2834        .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
2835    let dynv = pk.dynslots.lock().unwrap();
2836    let nxt = layers.get(li + 1);
2837    let w = crate::gpu_wgpu::Dsv4LayerW {
2838        attn: crate::gpu_wgpu::Dsv4AttnW {
2839            wq_a,
2840            wq_b,
2841            wo_a,
2842            wo_b,
2843            q_norm: &l.q_norm,
2844            sink: &l.attn_sink,
2845        },
2846        moe: crate::gpu_wgpu::Dsv4MoeW {
2847            router: &[],
2848            experts: &pk.tensors,
2849            logits: &[],
2850            // GLOBAL bias under the subset contract — the ranking spans
2851            // every expert, so a packed-order bias would misalign it.
2852            bias: pk.bias.as_deref(),
2853            mask: pk.mask.as_deref(),
2854            forced: forced.as_deref(),
2855            remap: Some(&dynv.remap),
2856        },
2857        hc_ffn_fn: &l.hc_ffn_fn,
2858        hc_ffn_scale: &l.hc_ffn_scale,
2859        hc_ffn_base: &l.hc_ffn_base,
2860        hc_next_fn: nxt.map(|n| n.hc_attn_fn.as_slice()),
2861        hc_next_scale: nxt.map_or(&l.hc_attn_scale, |n| &n.hc_attn_scale),
2862        hc_next_base: nxt.map_or(&l.hc_attn_base, |n| n.hc_attn_base.as_slice()),
2863        ffn_norm: &l.ffn_norm,
2864        next_norm: nxt.map_or(&l.attn_norm, |n| n.attn_norm.as_slice()),
2865        next_q_norm: nxt.map_or(&l.q_norm, |n| n.q_norm.as_slice()),
2866        next_wq_a: nxt.and_then(|n| n.wq_a.model_idx()),
2867        router: &pk.router,
2868    };
2869    let geom = crate::gpu_wgpu::Dsv4LayerGeom {
2870        attn: crate::gpu_wgpu::Dsv4AttnGeom {
2871            dim,
2872            nh: cfg.n_heads,
2873            hd,
2874            rd: cfg.rope_head_dim,
2875            q_lora: cfg.q_lora_rank,
2876            o_lora: cfg.o_lora_rank,
2877            o_groups: cfg.o_groups,
2878            eps: cfg.norm_eps,
2879            scale: (hd as f32).powf(-0.5),
2880        },
2881        moe: crate::gpu_wgpu::Dsv4MoeGeom {
2882            hidden: dim,
2883            inter: cfg.moe_inter,
2884            top_k: cfg.top_k,
2885            route_scale: cfg.route_scale,
2886            swiglu_limit: cfg.swiglu_limit,
2887            gu_q2: l
2888                .experts
2889                .first()
2890                .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
2891        },
2892        hc: cfg.hc_mult,
2893        hc_eps: cfg.hc_eps,
2894        sinkhorn_iters: cfg.hc_sinkhorn_iters,
2895    };
2896    let mut next = vec![0.0f32; dim];
2897    let mut cold: Vec<(usize, f32)> = Vec::new();
2898    let mut routed: Vec<(usize, f32)> = Vec::new();
2899    let mut cold_x: Vec<f32> = Vec::new();
2900    if !crate::gpu_wgpu::dsv4_layer_frame(
2901        &model,
2902        &w,
2903        geom,
2904        kv_id,
2905        li,
2906        Some(&prep.qr),
2907        &idx32,
2908        freqs,
2909        st.pos,
2910        &mut next,
2911        Some(&mut cold),
2912        route_stats_on().then_some(&mut routed),
2913        &mut cold_x,
2914    ) {
2915        return None;
2916    }
2917    if route_stats_on() {
2918        record_route(li, layers.len(), cfg.n_routed_experts, &routed);
2919    }
2920    drop(dynv);
2921    if cold.is_empty() {
2922        // Every winner was resident: the state stays on the card and the
2923        // frame's own next-fold is exact. This is the single-submission
2924        // path the whole function exists for.
2925        *folded = next;
2926        return Some(false);
2927    }
2928    // Cold winners: complete on the frame's own normed input, correct the
2929    // device state from the preserved post, and bring it home.
2930    if cold_x.len() < dim {
2931        return None;
2932    }
2933    let mut cold_sum = vec![0.0f32; dim];
2934    {
2935        let results: Vec<std::sync::Mutex<Vec<f32>>> =
2936            cold.iter().map(|_| std::sync::Mutex::new(Vec::new())).collect();
2937        let (cold_ref, results_ref, x_ref) = (&cold, &results, &cold_x[..dim]);
2938        std::thread::scope(|sc| {
2939            for i in 0..cold_ref.len() {
2940                let (gi, wt) = cold_ref[i];
2941                let Some(exp) = l.experts.get(gi) else { continue };
2942                let r = &results_ref[i];
2943                sc.spawn(move || {
2944                    let mut a = vec![0.0f32; cfg.dim];
2945                    crate::gpu::cpu_scope(|| run_expert(x_ref, exp, cfg, wt, None, &mut a));
2946                    *r.lock().unwrap() = a;
2947                });
2948            }
2949        });
2950        for r in &results {
2951            let a = r.lock().unwrap();
2952            for (o, v) in cold_sum.iter_mut().zip(a.iter()) {
2953                *o += v;
2954            }
2955        }
2956    }
2957    if !crate::gpu_wgpu::dsv4_state_add_cold_preserved(&cold_sum, cfg.hc_mult, state, kv_id, li) {
2958        return None;
2959    }
2960    // Reactive refill: the winners the slots did not hold are the likeliest
2961    // winners of the NEXT token — pull them in now, LRU-evicting.
2962    {
2963        let mut dynv = pk.dynslots.lock().unwrap();
2964        dynv.clock += 1;
2965        let clock = dynv.clock;
2966        for &(gi, _) in &cold {
2967            if gi >= dynv.remap.len() || dynv.remap[gi] != u32::MAX {
2968                continue;
2969            }
2970            let victim = (0..dynv.owner.len())
2971                .filter(|&sl| dynv.last[sl] != clock)
2972                .min_by_key(|&sl| dynv.last[sl]);
2973            let Some(victim) = victim else { break };
2974            let Some(exp) = l.experts.get(gi) else { continue };
2975            let t3 = (|| Some((exp.w1.model_idx()?, exp.w3.model_idx()?, exp.w2.model_idx()?)))();
2976            let Some(t3) = t3 else { continue };
2977            let gu_q2 =
2978                exp.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP);
2979            let pack_first = pk.tensors.first().map(|t| t.0).unwrap_or(usize::MAX);
2980            if !crate::gpu_wgpu::dsv4_slot_fill(
2981                &model, pack_first, victim, gi, t3, cfg.moe_inter, cfg.dim, gu_q2,
2982            ) {
2983                break;
2984            }
2985            let old = dynv.owner[victim] as usize;
2986            if old < dynv.remap.len() {
2987                dynv.remap[old] = u32::MAX;
2988            }
2989            dynv.remap[gi] = victim as u32;
2990            dynv.owner[victim] = gi as u32;
2991            dynv.last[victim] = clock;
2992            dynv.mutated = true;
2993        }
2994    }
2995    if let Some(n) = nxt {
2996        let (f, post, comb) = hc_fold_norm(
2997            state,
2998            &n.hc_attn_fn,
2999            &n.hc_attn_scale,
3000            &n.hc_attn_base,
3001            &n.attn_norm,
3002            cfg,
3003            pool,
3004        );
3005        *folded = f;
3006        if !crate::gpu_wgpu::dsv4_hc_write(&post, &comb) {
3007            return None;
3008        }
3009    }
3010    Some(true)
3011}
3012
3013#[cfg(feature = "gpu")]
3014fn chain_max() -> usize {
3015    static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
3016    *N.get_or_init(|| {
3017        std::env::var("CMF_DSV4_CHAIN_MAX")
3018            .ok()
3019            .and_then(|v| v.parse().ok())
3020            .unwrap_or(usize::MAX)
3021    })
3022}
3023
3024/// `CMF_DSV4_CHAIN=1`: put a run of consecutive device-capable layers in ONE
3025/// submission. Off by default until it has been measured on a real card.
3026#[cfg(feature = "gpu")]
3027/// `CMF_DSV4_HOST_CPU_MOE=1`: a layer that fell off the card runs its MoE on
3028/// the host WITHOUT the per-op device route — one fence a token instead of
3029/// one a matvec. Whether that wins is a measurement.
3030/// `CMF_DSV4_PARTIAL_WALK=1`: the fused device walk of a partial layer.
3031/// OFF until its self-poisoning is repaired: its attention frame reads the
3032/// pooled slots its own MoE frame rewrote on the previous token, so every
3033/// token after the first attends over leftovers — the drafts it captures
3034/// от такого состояния never match the verify (acceptance 0, measured).
3035/// The host branch walks these layers correctly; the pack stays resident
3036/// for the verify tail.
3037fn partial_walk_on() -> bool {
3038    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3039    *ON.get_or_init(|| std::env::var("CMF_DSV4_PARTIAL_WALK").is_ok_and(|v| v != "0"))
3040}
3041
3042fn host_cpu_moe() -> bool {
3043    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3044    *ON.get_or_init(|| std::env::var("CMF_DSV4_HOST_CPU_MOE").is_ok_and(|v| v != "0"))
3045}
3046
3047fn chain_enabled() -> bool {
3048    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3049    *ON.get_or_init(|| {
3050        std::env::var("CMF_DSV4_CHAIN")
3051            .map(|v| v != "0")
3052            .unwrap_or(true)
3053    })
3054}
3055
3056/// Encode a maximal run of consecutive device-capable layers and submit it
3057/// ONCE. Every layer in the run builds its own attention inputs on the card,
3058/// so nothing comes back between them — that is the whole saving.
3059///
3060/// The run's state belongs to the device from here on: `st.window`,
3061/// `st.compressed` and the compressor streams for these layers are stale on
3062/// the host afterwards, and only the counts in `st.dev_*` are kept. A layer
3063/// that has ever been in a run must therefore never be handed to the CPU
3064/// path again, which `dev_owned` records.
3065#[cfg(feature = "gpu")]
3066#[allow(clippy::too_many_arguments)]
3067fn dsv4_chain_run(
3068    layers: &[Dsv4Layer],
3069    run: &[usize],
3070    cfg: &Dsv4Cfg,
3071    g: &Dsv4Globals,
3072    st: &mut Dsv4State,
3073    token_id: u32,
3074    // In AND out: the run reads the fold it starts from and MUST leave the
3075    // fold it produced, because whatever follows — a host layer, or the next
3076    // run after a cap — seeds from this. Passing it read-only left every
3077    // later segment starting from a stale fold: exact with one unbroken run,
3078    // release-scale garbage the moment anything splits the chain.
3079    folded: &mut Vec<f32>,
3080    // When present, the hyper-connection state rides home in the run's own
3081    // submission instead of costing a second fence afterwards. Only the
3082    // token's LAST run passes it — an earlier one would read a state the
3083    // layers after it still change.
3084    state_out: Option<&mut [f32]>,
3085    // How many consecutive tokens this run carries. One is decode; more is a
3086    // prompt chunk or a speculative verify, which are the same shape of work.
3087    batch: usize,
3088    // Their ids, needed only when `batch > 1`: a hash layer forces its expert
3089    // list from the token's id, so the batch needs one list per token and the
3090    // single `token_id` above cannot supply them.
3091    batch_ids: &[u32],
3092    // Whether the device's qn buffer is stale: true at layer zero and after
3093    // a host layer. When the previous layer was chained, its frame's tail
3094    // already left THIS layer's LoRA vector on the card, and recomputing it
3095    // here was a full wq_a matvec on the CPU per run — at CHAIN_MAX=1 that
3096    // is one per LAYER, which is how a 43-fence path measured slower than
3097    // an 86-fence one.
3098    need_qn: bool,
3099    pool: Option<&crate::pool::Pool>,
3100) -> bool {
3101    if run.is_empty() {
3102        return true;
3103    }
3104    let (dim, hd) = (cfg.dim, cfg.head_dim);
3105    let first = run[0];
3106    let Some(model) = layers[first].experts.first().and_then(|e| e.w1.model_arc()) else {
3107        return false;
3108    };
3109    // Batch callers seed every token's fold and qn in its own slot. Seeding
3110    // the legacy shared slot here is not merely redundant: `folded` carries
3111    // only the eventual LAST output and is empty before the batch runs.
3112    if batch <= 1 && need_qn {
3113        let mut qn0 = vec![0.0f32; cfg.q_lora_rank];
3114        layers[first].wq_a.matvec(folded, &mut qn0, pool);
3115        rms_weighted(&mut qn0, &layers[first].q_norm, cfg.norm_eps);
3116        if !crate::gpu_wgpu::dsv4_chain_seed(folded, &qn0) {
3117            return false;
3118        }
3119    } else if batch <= 1 && !crate::gpu_wgpu::dsv4_chain_seed_fold(folded) {
3120        return false;
3121    }
3122
3123    // Held apart from the borrowing structs below, which point into them.
3124    let mut packs = Vec::with_capacity(run.len());
3125    let mut forceds: Vec<Option<Vec<usize>>> = Vec::with_capacity(run.len());
3126    for &li in run {
3127        let Some(pk) = pack_for(&layers[li], cfg, li) else {
3128            return false;
3129        };
3130        // A chain cannot complete a cold pick between dependent layers, so
3131        // every expert OPEN in the route must be resident and the pack must
3132        // not have mutated. A masked-complete or hot-reordered pack carries
3133        // its immutable global-to-slot remap into the frame.
3134        // The verify batch reached here with cap-limited partial packs
3135        // and accepted 0 of 625 drafts — wrong experts, plausible sums.
3136        if !pk.route_complete() || pk.is_mutated() {
3137            return false;
3138        }
3139        let forced: Option<Vec<usize>> = layers[li].tid2eid.as_ref().and_then(|tbl| {
3140            let v: Vec<usize> = if pk.needs_remap() {
3141                hash_route(tbl, cfg.vocab, cfg.top_k, token_id)
3142            } else {
3143                hash_route(tbl, cfg.vocab, cfg.top_k, token_id)
3144                    .into_iter()
3145                    .map(|gi| pk.to_slot[gi])
3146                    .collect()
3147            };
3148            if v.contains(&usize::MAX) {
3149                None
3150            } else {
3151                Some(v)
3152            }
3153        });
3154        if layers[li].tid2eid.is_some() && forced.is_none() {
3155            return false;
3156        }
3157        forceds.push(forced);
3158        packs.push(pk);
3159    }
3160
3161    let mut items = Vec::with_capacity(run.len());
3162    let mut freqs = Vec::with_capacity(run.len());
3163    for (i, &li) in run.iter().enumerate() {
3164        let l = &layers[li];
3165        let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b), Some(wkv)) = (
3166            l.wq_a.model_idx(),
3167            l.wq_b.model_idx(),
3168            l.wo_a.model_idx(),
3169            l.wo_b.model_idx(),
3170            l.wkv.model_idx(),
3171        ) else {
3172            return false;
3173        };
3174        let comp = match &l.compressor {
3175            None => None,
3176            Some(cp) => {
3177                let (Some(a), Some(b)) = (cp.wkv.model_idx(), cp.wgate.model_idx()) else {
3178                    return false;
3179                };
3180                Some((
3181                    crate::gpu_wgpu::Dsv4CompW {
3182                        wkv: a,
3183                        wgate: b,
3184                        norm: &cp.norm,
3185                        ape: &cp.ape,
3186                    },
3187                    crate::gpu_wgpu::Dsv4CompGeom {
3188                        width: cp.wkv.rows(),
3189                        hidden: dim,
3190                        ratio: cp.ratio,
3191                        overlap: cp.overlap,
3192                        rope_dim: cfg.rope_head_dim,
3193                        eps: cfg.norm_eps,
3194                    },
3195                ))
3196            }
3197        };
3198        let ix = match &l.indexer {
3199            None => None,
3200            Some(ixr) => {
3201                let cp = &ixr.compressor;
3202                let (Some(a), Some(b), Some(qb), Some(wp)) = (
3203                    cp.wkv.model_idx(),
3204                    cp.wgate.model_idx(),
3205                    ixr.wq_b.model_idx(),
3206                    ixr.weights_proj.model_idx(),
3207                ) else {
3208                    return false;
3209                };
3210                let ih = ixr.weights_proj.rows();
3211                Some((
3212                    crate::gpu_wgpu::Dsv4CompW {
3213                        wkv: a,
3214                        wgate: b,
3215                        norm: &cp.norm,
3216                        ape: &cp.ape,
3217                    },
3218                    crate::gpu_wgpu::Dsv4CompGeom {
3219                        width: cp.wkv.rows(),
3220                        hidden: dim,
3221                        ratio: cp.ratio,
3222                        overlap: cp.overlap,
3223                        rope_dim: cfg.rope_head_dim,
3224                        eps: cfg.norm_eps,
3225                    },
3226                    crate::gpu_wgpu::Dsv4IxW {
3227                        wq_b: qb,
3228                        weights_proj: wp,
3229                    },
3230                    crate::gpu_wgpu::Dsv4IxGeom {
3231                        ih,
3232                        idim: ixr.wq_b.rows() / ih.max(1),
3233                        q_lora: cfg.q_lora_rank,
3234                        hidden: dim,
3235                        rope_dim: cfg.rope_head_dim,
3236                        eps: cfg.norm_eps,
3237                        top_k: cfg.index_topk,
3238                        window: cfg.window,
3239                    },
3240                ))
3241            }
3242        };
3243        // The cache has to be big enough BEFORE the frame appends into it:
3244        // a chained layer never calls dsv4_cache_write, which is what used
3245        // to create and grow it.
3246        let ew_c0 = l.compressor.as_ref().map_or(0, |cp| {
3247            if cp.overlap {
3248                cp.wkv.rows() / 2
3249            } else {
3250                cp.wkv.rows()
3251            }
3252        });
3253        let comp_extra = l
3254            .compressor
3255            .as_ref()
3256            .map_or(0, |cp| batch.max(1).div_ceil(cp.ratio.max(1)));
3257        let need = cfg.window * hd
3258            + (st.dev_n_comp[li] + comp_extra + 1) * ew_c0.max(1)
3259            + (batch.max(1) + 1) * hd;
3260        if !crate::gpu_wgpu::dsv4_cache_ensure(st.kv_id, li, need.next_power_of_two()) {
3261            return false;
3262        }
3263        let ew_c = comp.as_ref().map_or(
3264            0,
3265            |(_, cg)| {
3266                if cg.overlap { cg.width / 2 } else { cg.width }
3267            },
3268        );
3269        let ew_i = ix.as_ref().map_or(
3270            0,
3271            |(_, cg, _, _)| {
3272                if cg.overlap { cg.width / 2 } else { cg.width }
3273            },
3274        );
3275        let prep = crate::gpu_wgpu::Dsv4Prep {
3276            wkv,
3277            kv_norm: &l.kv_norm,
3278            comp,
3279            ix,
3280            filled: st.dev_filled[li],
3281            window: cfg.window,
3282            n_comp: st.dev_n_comp[li],
3283            n_ix: st.dev_n_ix[li],
3284            comp_dst_off: cfg.window * hd + st.dev_n_comp[li] * ew_c,
3285            ix_dst_off: st.dev_n_ix[li] * ew_i,
3286            idx_cap: cfg.window
3287                + if l.indexer.is_some() {
3288                    cfg.index_topk
3289                } else {
3290                    st.dev_n_comp[li] + comp_extra + 1
3291                },
3292        };
3293        let nxt = layers.get(li + 1);
3294        let w = crate::gpu_wgpu::Dsv4LayerW {
3295            attn: crate::gpu_wgpu::Dsv4AttnW {
3296                wq_a,
3297                wq_b,
3298                wo_a,
3299                wo_b,
3300                q_norm: &l.q_norm,
3301                sink: &l.attn_sink,
3302            },
3303            moe: crate::gpu_wgpu::Dsv4MoeW {
3304                router: &packs[i].router,
3305                experts: &packs[i].tensors,
3306                logits: &[],
3307                // The PACK's slice, not a per-run Vec: the address stability
3308                // is the whole point (see Pack::bias).
3309                bias: packs[i].bias.as_deref(),
3310                mask: packs[i].mask.as_deref(),
3311                forced: forceds[i].as_deref(),
3312                remap: packs[i].needs_remap().then_some(packs[i].remap.as_slice()),
3313            },
3314            hc_ffn_fn: &l.hc_ffn_fn,
3315            hc_ffn_scale: &l.hc_ffn_scale,
3316            hc_ffn_base: &l.hc_ffn_base,
3317            hc_next_fn: nxt.map(|n| n.hc_attn_fn.as_slice()),
3318            hc_next_scale: nxt.map_or(&l.hc_attn_scale, |n| &n.hc_attn_scale),
3319            hc_next_base: nxt.map_or(&l.hc_attn_base, |n| n.hc_attn_base.as_slice()),
3320            ffn_norm: &l.ffn_norm,
3321            next_norm: nxt.map_or(&l.attn_norm, |n| n.attn_norm.as_slice()),
3322            next_q_norm: nxt.map_or(&l.q_norm, |n| n.q_norm.as_slice()),
3323            next_wq_a: nxt.and_then(|n| n.wq_a.model_idx()),
3324            router: &packs[i].router,
3325        };
3326        let geom = crate::gpu_wgpu::Dsv4LayerGeom {
3327            attn: crate::gpu_wgpu::Dsv4AttnGeom {
3328                dim,
3329                nh: cfg.n_heads,
3330                hd,
3331                rd: cfg.rope_head_dim,
3332                q_lora: cfg.q_lora_rank,
3333                o_lora: cfg.o_lora_rank,
3334                o_groups: cfg.o_groups,
3335                eps: cfg.norm_eps,
3336                scale: (hd as f32).powf(-0.5),
3337            },
3338            moe: crate::gpu_wgpu::Dsv4MoeGeom {
3339                hidden: dim,
3340                inter: cfg.moe_inter,
3341                top_k: cfg.top_k,
3342                route_scale: cfg.route_scale,
3343                swiglu_limit: cfg.swiglu_limit,
3344                gu_q2: l.experts.first().is_some_and(|e| {
3345                    e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
3346                }),
3347            },
3348            hc: cfg.hc_mult,
3349            hc_eps: cfg.hc_eps,
3350            sinkhorn_iters: cfg.hc_sinkhorn_iters,
3351        };
3352        freqs.push(if l.compressor.is_some() {
3353            g.inv_freq_compress.as_slice()
3354        } else {
3355            g.inv_freq_window.as_slice()
3356        });
3357        items.push((w, geom, prep));
3358    }
3359
3360    let mut out = vec![0.0f32; dim * batch.max(1)];
3361    if batch > 1 {
3362        // A batch keeps its own state per token. When a host tail follows,
3363        // all of those states ride home beside the folds in the same fence.
3364        // One forced row per token: same layers, the hash rows re-derived
3365        // from each token's own id.
3366        let mut forced_pt: Vec<Vec<Option<Vec<usize>>>> = Vec::with_capacity(batch);
3367        for t in 0..batch {
3368            let id = batch_ids.get(t).copied().unwrap_or(token_id);
3369            let mut row = Vec::with_capacity(run.len());
3370            for (i, &li) in run.iter().enumerate() {
3371                row.push(layers[li].tid2eid.as_ref().and_then(|tbl| {
3372                    let v: Vec<usize> = if packs[i].needs_remap() {
3373                        hash_route(tbl, cfg.vocab, cfg.top_k, id)
3374                    } else {
3375                        hash_route(tbl, cfg.vocab, cfg.top_k, id)
3376                            .into_iter()
3377                            .map(|gi| packs[i].to_slot[gi])
3378                            .collect()
3379                    };
3380                    if v.contains(&usize::MAX) {
3381                        None
3382                    } else {
3383                        Some(v)
3384                    }
3385                }));
3386                if layers[li].tid2eid.is_some() && row[i].is_none() {
3387                    return false;
3388                }
3389            }
3390            forced_pt.push(row);
3391        }
3392        if !crate::gpu_wgpu::dsv4_chain_batch(
3393            &model,
3394            &items,
3395            st.kv_id,
3396            first,
3397            &freqs,
3398            st.pos,
3399            batch,
3400            Some(&forced_pt),
3401            &mut out,
3402            state_out,
3403        ) {
3404            return false;
3405        }
3406        // The caller wants the LAST token's fold: it is the one whose logits
3407        // continue the sequence.
3408        *folded = out[(batch - 1) * dim..batch * dim].to_vec();
3409    } else {
3410        if !crate::gpu_wgpu::dsv4_layer_chain(
3411            &model, &items, st.kv_id, first, &freqs, st.pos, &mut out, state_out,
3412        ) {
3413            return false;
3414        }
3415        *folded = out;
3416    }
3417    // The device advanced these; the host keeps only the arithmetic. A batch
3418    // advanced them once per token, in order, so the host replays the same
3419    // rule that many times rather than inventing a closed form for it.
3420    for (i, &li) in run.iter().enumerate() {
3421        for t in 0..batch.max(1) {
3422            let pos = st.pos + t;
3423            st.dev_filled[li] = (st.dev_filled[li] + 1).min(cfg.window);
3424            if let Some((_, cg, ..)) = items[i].2.ix.as_ref() {
3425                if (pos + 1) % cg.ratio == 0 {
3426                    st.dev_n_ix[li] += 1;
3427                }
3428            }
3429            if let Some((_, cg)) = items[i].2.comp.as_ref() {
3430                if (pos + 1) % cg.ratio == 0 {
3431                    st.dev_n_comp[li] += 1;
3432                }
3433            }
3434        }
3435    }
3436    st.dev_owned = true;
3437    true
3438}
3439
3440/// `CMF_DSV4_HC_DEV=0` puts the hyper-connections back on the host.
3441#[cfg(feature = "gpu")]
3442fn hc_on_device() -> bool {
3443    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3444    *ON.get_or_init(|| {
3445        // OPT-IN. On the release checkpoint this path reads 3.234 against
3446        // the CPU's 3.282 — divergent — and the speed is unchanged, so there
3447        // is no trade to weigh: it must not be the default until it is
3448        // exact. The toy's near-agreement (129.787 vs 129.792) hid a real
3449        // fault the release exposes.
3450        std::env::var("CMF_DSV4_HC_DEV").is_ok_and(|v| v != "0") && crate::gpu::backend_available()
3451    })
3452}
3453
3454/// The two-frame path with the hyper-connections on the card.
3455///
3456/// The host still prepares each layer's attention inputs — the compressor,
3457/// the indexer and the window, which are exact there — but it no longer
3458/// folds, Sinkhorns or norms, and it no longer carries the MoE half's input
3459/// between the halves: the attention frame leaves it on the device and the
3460/// MoE frame reads it from there. One readback a layer instead of two, and
3461/// 19 ms of host arithmetic a token gone.
3462#[cfg(feature = "gpu")]
3463#[allow(clippy::too_many_arguments)]
3464fn dsv4_two_frame_loop(
3465    state: &mut [f32],
3466    layers: &[Dsv4Layer],
3467    g: &Dsv4Globals,
3468    cfg: &Dsv4Cfg,
3469    st: &mut Dsv4State,
3470    token_id: u32,
3471    inv_freq: &[f32],
3472    pool: Option<&crate::pool::Pool>,
3473    scratch: &mut HcScratch,
3474) -> bool {
3475    let dim = cfg.dim;
3476    let freqs_of = |l: &Dsv4Layer| -> &[f32] {
3477        let f = if l.compressor.is_some() {
3478            &g.inv_freq_compress
3479        } else {
3480            &g.inv_freq_window
3481        };
3482        if f.is_empty() { inv_freq } else { f.as_slice() }
3483    };
3484    // Layer zero's fold has no frame before it, exactly as in the layer path.
3485    let (mut folded, post0, comb0) = hc_fold_norm(
3486        state,
3487        &layers[0].hc_attn_fn,
3488        &layers[0].hc_attn_scale,
3489        &layers[0].hc_attn_base,
3490        &layers[0].attn_norm,
3491        cfg,
3492        pool,
3493    );
3494    if !crate::gpu_wgpu::dsv4_state_write(state) || !crate::gpu_wgpu::dsv4_hc_write(&post0, &comb0)
3495    {
3496        return false;
3497    }
3498    // PRE-FLIGHT, before the first byte of state moves: a mid-loop refusal
3499    // would hand the token back to the ordinary loop AFTER these caches
3500    // advanced, and the second advance is not a slow answer but a wrong one.
3501    // The same discipline the layer loop states in the same words.
3502    let mut on_dev = vec![false; layers.len()];
3503    for (li, l) in layers.iter().enumerate() {
3504        let Some(pk) = pack_for(l, cfg, li) else {
3505            return false;
3506        };
3507        let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
3508            return false;
3509        };
3510        let gu_q2 = l
3511            .experts
3512            .first()
3513            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
3514        let attn_ok = [
3515            l.wq_a.model_idx(),
3516            l.wq_b.model_idx(),
3517            l.wo_a.model_idx(),
3518            l.wo_b.model_idx(),
3519        ]
3520        .into_iter()
3521        .flatten()
3522        .all(|i| crate::gpu_wgpu::dsv4_weight_ready(&model, i));
3523        on_dev[li] = attn_ok
3524            && pk.route_complete()
3525            && crate::gpu_wgpu::dsv4_experts_ready(
3526                &model,
3527                &pk.tensors,
3528                cfg.moe_inter,
3529                dim,
3530                gu_q2,
3531                l.experts.first().is_some_and(|e| {
3532                    e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
3533                }),
3534            );
3535    }
3536    if !on_dev.iter().any(|&x| x) {
3537        return false;
3538    }
3539    let mut sink = vec![0.0f32; dim];
3540    for (li, l) in layers.iter().enumerate() {
3541        // A layer the card cannot hold runs on the host WHOLE, with the
3542        // state fetched and put back around it — the mixed ownership the
3543        // layer loop already proved out.
3544        if !on_dev[li] {
3545            if !crate::gpu_wgpu::dsv4_state_read(state) {
3546                return false;
3547            }
3548            let freqs = freqs_of(l);
3549            hc_block(
3550                state,
3551                &l.hc_attn_fn,
3552                &l.hc_attn_scale,
3553                &l.hc_attn_base,
3554                &l.attn_norm,
3555                cfg,
3556                scratch,
3557                pool,
3558                |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
3559            );
3560            hc_block(
3561                state,
3562                &l.hc_ffn_fn,
3563                &l.hc_ffn_scale,
3564                &l.hc_ffn_base,
3565                &l.ffn_norm,
3566                cfg,
3567                scratch,
3568                pool,
3569                |f, o| moe_step(f, l, cfg, token_id, li, pool, o),
3570            );
3571            let nref = layers.get(li + 1).unwrap_or(l);
3572            let (f, p2, c2) = hc_fold_norm(
3573                state,
3574                &nref.hc_attn_fn,
3575                &nref.hc_attn_scale,
3576                &nref.hc_attn_base,
3577                &nref.attn_norm,
3578                cfg,
3579                pool,
3580            );
3581            folded = f;
3582            if !crate::gpu_wgpu::dsv4_hc_write(&p2, &c2)
3583                || !crate::gpu_wgpu::dsv4_state_write(state)
3584            {
3585                return false;
3586            }
3587            continue;
3588        }
3589        // The host's half: the caches and the attended list, untouched.
3590        let mut prep = AttnPrep::default();
3591        attention_step(
3592            &folded,
3593            l,
3594            cfg,
3595            st,
3596            li,
3597            freqs_of(l),
3598            pool,
3599            Some(&mut prep),
3600            &mut sink,
3601        );
3602        let hd = cfg.head_dim;
3603        let n_comp = st.compressed[li].len() / hd;
3604        let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
3605        if !crate::gpu_wgpu::dsv4_cache_write(st.kv_id, li, 0, &st.window[li], cap)
3606            || (n_comp > 0
3607                && !crate::gpu_wgpu::dsv4_cache_write(
3608                    st.kv_id,
3609                    li,
3610                    cfg.window * hd,
3611                    &st.compressed[li],
3612                    cap,
3613                ))
3614        {
3615            return false;
3616        }
3617        let _idx32: Vec<u32> = prep
3618            .idxs
3619            .iter()
3620            .map(|&p| {
3621                if p < prep.win_len {
3622                    p as u32
3623                } else {
3624                    (cfg.window + (p - prep.win_len)) as u32
3625                }
3626            })
3627            .collect();
3628        let nxt = layers.get(li + 1);
3629        let a_tail = crate::gpu_wgpu::Dsv4HcTail {
3630            fn_: &l.hc_ffn_fn,
3631            scale: &l.hc_ffn_scale,
3632            base: &l.hc_ffn_base,
3633            norm: &l.ffn_norm,
3634            hc: cfg.hc_mult,
3635            sinkhorn_iters: cfg.hc_sinkhorn_iters,
3636            hc_eps: cfg.hc_eps,
3637            eps: cfg.norm_eps,
3638        };
3639        let scale = (cfg.head_dim as f32).powf(-0.5);
3640        if !attn_frame(
3641            l,
3642            cfg,
3643            st,
3644            li,
3645            &folded,
3646            &prep.qr,
3647            &prep.idxs,
3648            freqs_of(l),
3649            st.pos,
3650            prep.win_len,
3651            scale,
3652            Some(&a_tail),
3653            &mut [],
3654        ) {
3655            return false;
3656        }
3657        let m_tail = nxt.map(|n| crate::gpu_wgpu::Dsv4HcTail {
3658            fn_: &n.hc_attn_fn,
3659            scale: &n.hc_attn_scale,
3660            base: &n.hc_attn_base,
3661            norm: &n.attn_norm,
3662            hc: cfg.hc_mult,
3663            sinkhorn_iters: cfg.hc_sinkhorn_iters,
3664            hc_eps: cfg.hc_eps,
3665            eps: cfg.norm_eps,
3666        });
3667        let mut next = vec![0.0f32; dim];
3668        let pair = m_tail
3669            .as_ref()
3670            .zip(nxt)
3671            .map(|(t, n)| (t, n.attn_norm.as_slice()));
3672        let forced = l
3673            .tid2eid
3674            .as_ref()
3675            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
3676        if moe_frame(
3677            &[],
3678            l,
3679            cfg,
3680            li,
3681            &[],
3682            forced.as_deref(),
3683            pool,
3684            Some(&a_tail),
3685            pair,
3686            &mut next,
3687        )
3688        .is_none()
3689        {
3690            return false;
3691        }
3692        folded = next;
3693    }
3694    let _ = scratch;
3695    crate::gpu_wgpu::dsv4_state_read(state)
3696}
3697
3698/// The host half of one hyper-connection block: mixes, Sinkhorn, fold, norm.
3699/// The device does this for every layer but the first, whose state it has not
3700/// seen yet.
3701#[cfg(feature = "gpu")]
3702#[allow(clippy::too_many_arguments)]
3703fn hc_fold_norm(
3704    state: &[f32],
3705    hc_fn: &[f32],
3706    hc_scale: &[f32; 3],
3707    hc_base: &[f32],
3708    norm_w: &[f32],
3709    cfg: &Dsv4Cfg,
3710    pool: Option<&crate::pool::Pool>,
3711) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
3712    let (hc, dim) = (cfg.hc_mult, cfg.dim);
3713    let mix_hc = (2 + hc) * hc;
3714    let mut mixes = vec![0.0f32; mix_hc];
3715    hc_mixes(state, hc_fn, mix_hc, cfg.norm_eps, pool, &mut mixes);
3716    let mut pre = vec![0.0f32; hc];
3717    let mut post = vec![0.0f32; hc];
3718    let mut comb = vec![0.0f32; hc * hc];
3719    hc_split_sinkhorn(
3720        &mixes,
3721        hc_scale,
3722        hc_base,
3723        hc,
3724        cfg.hc_sinkhorn_iters,
3725        cfg.hc_eps,
3726        &mut pre,
3727        &mut post,
3728        &mut comb,
3729    );
3730    let mut folded = vec![0.0f32; dim];
3731    hc_fold(state, &pre, hc, dim, &mut folded);
3732    rms_weighted(&mut folded, norm_w, cfg.norm_eps);
3733    // post and comb travel with the fold: the frame's opening expand needs
3734    // exactly those, and they are not recoverable from the state alone.
3735    (folded, post, comb)
3736}
3737
3738/// `CMF_DSV4_GPU_LAYER=1`: one submission per layer instead of two, with the
3739/// hyper-connection glue and the router on the device.
3740///
3741/// CORRECT — perplexity 5.211 against the CPU's 5.211 on the release, 128.576
3742/// against 128.576 on the toy — and SLOWER on this hardware: 6.0 tok/s where
3743/// the two-frame path gets 9.3. The reason is not the frame, it is the
3744/// all-or-nothing granularity underneath it. A layer whose experts miss VRAM
3745/// runs entirely on the host, attention included (6.5 ms a call against 0.9),
3746/// and with 100 GB of experts against a 98 GB card a fifth of the layers
3747/// miss. The two-frame path only loses the MoE half of those layers.
3748///
3749/// So the barrier it saves is real and the fallback it forces costs more. The
3750/// fix is the granularity: pack the experts that FIT, route over all of them
3751/// anyway, and run the few cold picks of a token on the host — per EXPERT,
3752/// not per layer. Then no layer ever leaves the device and this frame wins by
3753/// the 15 ms a token it was built to save.
3754#[cfg(feature = "gpu")]
3755fn gpu_layer_enabled() -> bool {
3756    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3757    *ON.get_or_init(|| {
3758        std::env::var("CMF_DSV4_GPU_LAYER")
3759            .map(|v| v != "0")
3760            .unwrap_or(true)
3761            && crate::gpu::backend_available()
3762    })
3763}
3764
3765/// The packed expert set of one layer: which globals made it in, and their
3766/// directory indices in packing order with the shared expert last. Built once
3767/// — the mask does not change during a run — and keyed by layer.
3768#[cfg(feature = "gpu")]
3769struct PackDyn {
3770    remap: Vec<u32>,
3771    owner: Vec<u32>,
3772    last: Vec<u64>,
3773    clock: u64,
3774    /// Per-expert recent-use tally (halved every 64 tokens): the q* rule.
3775    /// A slot upload only pays for itself when the expert is REUSED —
3776    /// FreeToken's split — so a fetch needs `seen >= CMF_DSV4_FETCH_MIN_SEEN`
3777    /// prior recent picks; a first-timer stays a cold pick and the CPU
3778    /// reads it at the shelf. The automatic default comes from a small
3779    /// one-time host→device bandwidth probe; an explicit env still wins.
3780    seen: Vec<u16>,
3781    /// Set on the first slot refill. The chain and the batch verify hand
3782    /// the device `remap: None` and trust the banks to still hold the
3783    /// BUILD-TIME packing — a mutated pack must never be claimed by them.
3784    mutated: bool,
3785}
3786
3787#[cfg(feature = "gpu")]
3788impl Pack {
3789    /// True once any slot was refilled away from the build-time packing.
3790    fn is_mutated(&self) -> bool {
3791        self.dynslots.lock().unwrap().mutated
3792    }
3793
3794    /// Complete means complete for the route the model will actually take.
3795    /// A task mask can close most of the 256 rows; packing every OPEN row is
3796    /// then a full device layer, not a partial layer with 200 imaginary cold
3797    /// experts. Hash layers deliberately carry no mask because their forced
3798    /// rows remain the exact checkpoint contract.
3799    fn route_complete(&self) -> bool {
3800        let need = self
3801            .mask
3802            .as_deref()
3803            .map_or(self.remap.len(), |m| m.iter().filter(|&&x| x != 0).count());
3804        self.globals.len() >= need
3805    }
3806
3807    /// A global-to-slot table is needed for a masked set and for a complete
3808    /// pack whose hot-first order is not identity. Without it a full but
3809    /// reordered pack silently runs the right router index on the wrong bank.
3810    fn needs_remap(&self) -> bool {
3811        self.mask.is_some()
3812            || self
3813                .remap
3814                .iter()
3815                .enumerate()
3816                .any(|(i, &slot)| slot != i as u32)
3817    }
3818}
3819
3820/// The packed expert set of one layer: which globals made it in, and their
3821/// directory indices in packing order with the shared expert last. Keyed by
3822/// layer; the STATIC fields are built once, the dynamic slot state evolves.
3823#[cfg(feature = "gpu")]
3824struct Pack {
3825    /// The router as dense f32, expanded once. It is 4 MB a layer against a
3826    /// 112 GB model, it lives as long as the process — so the address-keyed
3827    /// device cache is sound for it, unlike anything built per call.
3828    router: Vec<f32>,
3829    /// global expert id -> packed slot, `usize::MAX` for the ones left out.
3830    to_slot: Vec<usize>,
3831    /// The same, as the u32 table the router reads.
3832    remap: Vec<u32>,
3833    /// packed order, globals only (shared is not in here).
3834    globals: Vec<usize>,
3835    tensors: Vec<(usize, usize, usize)>,
3836    /// Global 0/1 route mask consumed by the GPU router. Stable storage is
3837    /// part of the pack because the device constant cache keys by address.
3838    /// None on exact/hash routing.
3839    mask: Option<Vec<u32>>,
3840    /// FreeToken-style dynamic slots: the packed subset FOLLOWS the router
3841    /// instead of staying whatever load-time frequency guessed. `remap` here
3842    /// is the LIVE table (the immutable `remap` above is the initial state
3843    /// and stays only as the build artifact); `owner[slot]` is the global
3844    /// expert id occupying the slot; `last[slot]`/`clock` drive LRU. The
3845    /// device bank buffers accept `write_buffer` at slot offsets, and the
3846    /// frame re-uploads the remap every call — so a refill is two queue
3847    /// writes and no cache invalidation anywhere.
3848    dynslots: std::sync::Mutex<PackDyn>,
3849    /// The noaux_tc bias in GLOBAL order, kept here because it is the same
3850    /// every token and the pack lives as long as the process. Global order is
3851    /// required by masked/remapped routing; its stable address lets many
3852    /// layers share one submission. A bias uploaded through
3853    /// the per-call pool is written by every layer of a run BEFORE the run's
3854    /// single submit — queue writes do not interleave with passes — so every
3855    /// layer routed with the LAST layer's bias. On the release every scored
3856    /// layer carries one, which is the 50.280.
3857    bias: Option<Vec<f32>>,
3858}
3859
3860#[cfg(feature = "gpu")]
3861/// Candidate order for a budget-limited pack: hottest expert first, by the
3862/// measured tally `CMF_DSV4_PACK_FREQ` points at (`layer<TAB>expert<TAB>count`
3863/// lines). None when the variable is unset, the file is unreadable, or the
3864/// tally has nothing for this layer — the caller keeps id order then. Ties
3865/// and untallied experts follow in id order, so the choice is deterministic.
3866fn pack_freq_order(li: usize, n: usize) -> Option<Vec<usize>> {
3867    use std::collections::HashMap;
3868    use std::sync::OnceLock;
3869    static FREQ: OnceLock<Option<HashMap<(usize, usize), u64>>> = OnceLock::new();
3870    let map = FREQ
3871        .get_or_init(|| {
3872            let path = std::env::var("CMF_DSV4_PACK_FREQ").ok()?;
3873            let text = match std::fs::read_to_string(&path) {
3874                Ok(t) => t,
3875                Err(e) => {
3876                    eprintln!("CMF_DSV4_PACK_FREQ={path} не читается ({e}) — порядок по id");
3877                    return None;
3878                }
3879            };
3880            let mut m = HashMap::new();
3881            for line in text.lines() {
3882                let mut it = line.split('\t');
3883                if let (Some(l), Some(e), Some(c)) = (it.next(), it.next(), it.next()) {
3884                    if let (Ok(l), Ok(e), Ok(c)) =
3885                        (l.trim().parse(), e.trim().parse(), c.trim().parse::<u64>())
3886                    {
3887                        *m.entry((l, e)).or_insert(0) += c;
3888                    }
3889                }
3890            }
3891            Some(m)
3892        })
3893        .as_ref()?;
3894    if !(0..n).any(|e| map.contains_key(&(li, e))) {
3895        return None;
3896    }
3897    let mut idx: Vec<usize> = (0..n).collect();
3898    idx.sort_by_key(|&e| {
3899        (
3900            std::cmp::Reverse(map.get(&(li, e)).copied().unwrap_or(0)),
3901            e,
3902        )
3903    });
3904    Some(idx)
3905}
3906
3907#[cfg(feature = "gpu")]
3908fn pack_for(l: &Dsv4Layer, cfg: &Dsv4Cfg, li: usize) -> Option<std::sync::Arc<Pack>> {
3909    use std::collections::HashMap;
3910    use std::sync::{Arc, Mutex, OnceLock};
3911    static CACHE: OnceLock<Mutex<HashMap<(u64, usize, usize), Option<Arc<Pack>>>>> =
3912        OnceLock::new();
3913    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
3914    // Keyed by the layer's IDENTITY, not its ordinal. The draft's three
3915    // stages are layers too and they number 0, 1, 2 — under an ordinal key
3916    // they would be handed the trunk's first three packs: another layer's
3917    // router, another layer's tensor indices, another layer's bias. The gate
3918    // tensor is what actually distinguishes them.
3919    let model_uid = l
3920        .experts
3921        .first()
3922        .and_then(|e| e.w1.model_arc())
3923        .map_or(0, |m| m.uid());
3924    // Dense f32 routers need not have a directory handle, so the gate index
3925    // alone can be `None` for every layer. Pair the ordinal with the first
3926    // expert's mapped identity; model UID keeps long-lived multi-model
3927    // servers separate, while the expert index distinguishes trunk and MTP
3928    // layers that reuse ordinal 0/1/2.
3929    let first_expert = l
3930        .experts
3931        .first()
3932        .and_then(|e| e.w1.model_idx())
3933        .unwrap_or(usize::MAX);
3934    let key = (model_uid, li, first_expert);
3935    if let Some(v) = cache.lock().unwrap().get(&key) {
3936        return v.clone();
3937    }
3938    // `CMF_DSV4_PACK_MAX_LI=N` — do not pack layers above N at all. A layer
3939    // with no pack stays wholly host-owned, which is what both the batched
3940    // prefill and a speculative verify need of the tail: a device-owned
3941    // partial layer can join neither the batch (incomplete pack) nor the
3942    // causal host tail (its caches live on the card). This also carves the
3943    // VRAM the tail would have taken for the draft's own pack.
3944    if let Ok(v) = std::env::var("CMF_DSV4_PACK_MAX_LI") {
3945        if v.parse::<usize>().is_ok_and(|max| li > max) {
3946            cache.lock().unwrap().insert(key, None);
3947            return None;
3948        }
3949    }
3950    let build = || -> Option<Arc<Pack>> {
3951        let mut to_slot = vec![usize::MAX; cfg.n_routed_experts];
3952        let mut globals = Vec::new();
3953        let mut tensors = Vec::new();
3954        let idx3 = |e: &Dsv4Expert| -> Option<(usize, usize, usize)> {
3955            Some((e.w1.model_idx()?, e.w3.model_idx()?, e.w2.model_idx()?))
3956        };
3957        let route_mask: Option<Vec<u32>> = if l.tid2eid.is_none() {
3958            l.mask
3959                .as_deref()
3960                .map(|m| m.iter().map(|&open| u32::from(open)).collect())
3961        } else {
3962            None
3963        };
3964        // How many experts the card still has room for, minus one for the
3965        // shared expert, which always rides. Everything past that stays on the
3966        // host and is reached through the remap — the router still ranges over
3967        // all of them, so this costs speed and not a single bit of quality.
3968        let gu_q2 = l
3969            .experts
3970            .first()
3971            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
3972        // Pack what fits and leave the rest to the host. The router still
3973        // ranges over every expert; a missing winner is returned as a cold
3974        // pick and completed on the CPU. This is deliberately budget-driven,
3975        // not layer-driven: the same model scales from a small card (more
3976        // partial/host layers) to a large one (all experts resident) without
3977        // a checkpoint-specific cutoff.
3978        // `CMF_DSV4_PACK_MAX=N` caps the packing directly, so a toy can
3979        // reproduce the subset path without needing a card that runs out.
3980        if let Some(n) = std::env::var("CMF_DSV4_PACK_MAX")
3981            .ok()
3982            .and_then(|v| v.parse::<usize>().ok())
3983        {
3984            let mut to_slot = vec![usize::MAX; cfg.n_routed_experts];
3985            let mut globals = Vec::new();
3986            let mut tensors = Vec::new();
3987            for (gi, e) in l.experts.iter().enumerate() {
3988                if route_mask
3989                    .as_deref()
3990                    .is_some_and(|m| m.get(gi).copied().unwrap_or(1) == 0)
3991                {
3992                    continue;
3993                }
3994                if globals.len() >= n {
3995                    break;
3996                }
3997                to_slot[gi] = globals.len();
3998                globals.push(gi);
3999                tensors.push(idx3(e)?);
4000            }
4001            tensors.push(idx3(&l.shared)?);
4002            let (rows, cols) = (l.gate.rows(), l.gate.cols());
4003            let mut router = vec![0.0f32; rows * cols];
4004            for r in 0..rows {
4005                l.gate.row_f32(r, &mut router[r * cols..(r + 1) * cols]);
4006            }
4007            let remap: Vec<u32> = to_slot
4008                .iter()
4009                .map(|&sl| {
4010                    if sl == usize::MAX {
4011                        u32::MAX
4012                    } else {
4013                        sl as u32
4014                    }
4015                })
4016                .collect();
4017            return Some(Arc::new(Pack {
4018                bias: l.gate_bias.clone(),
4019                mask: route_mask.clone(),
4020                router,
4021                to_slot,
4022                dynslots: std::sync::Mutex::new(PackDyn {
4023                    remap: remap.clone(),
4024                    owner: globals.iter().map(|&g| g as u32).collect(),
4025                    last: vec![0; globals.len()],
4026                    clock: 0,
4027                    mutated: false,
4028                    seen: vec![0; cfg.n_routed_experts],
4029                }),
4030                remap,
4031                globals,
4032                tensors,
4033            }));
4034        }
4035        let dn_q2_fit = l
4036            .experts
4037            .first()
4038            .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
4039        let room = crate::gpu_wgpu::dsv4_experts_fit(cfg.moe_inter, cfg.dim, gu_q2, dn_q2_fit)
4040            .saturating_sub(1);
4041        // A greedy pack starves the tail: the first layers take the whole
4042        // expert budget and the tail falls off the device chain. Divide the
4043        // room still available by the layers still to pack. This depends on
4044        // format and VRAM geometry, not a card name: a small card gives every
4045        // layer a useful partial pack; when the whole model fits the quotient
4046        // naturally reaches all experts. Hash-routed head layers stay whole
4047        // whenever possible because their checkpoint table names exact rows.
4048        //
4049        // CMF_DSV4_PACK_LAYER_CAP=N remains an override; 0 restores the old
4050        // greedy packing for a performance bisect.
4051        let remaining_layers = l
4052            .experts
4053            .first()
4054            .and_then(|e| e.w1.model_arc())
4055            .map(|m| m.header.arch.num_layers.saturating_sub(li).max(1))
4056            .unwrap_or(1);
4057        let auto_cap = if l.mask.is_some() {
4058            // The mask is already the layer-specific cap. Its total mass was
4059            // counted by dspark_reserve_note before packing, so an additional
4060            // equal-per-layer cap only turns naturally uneven masked layers
4061            // partial and makes batched verification reject every draft.
4062            room
4063        } else if l.tid2eid.is_some() && room >= cfg.n_routed_experts {
4064            cfg.n_routed_experts
4065        } else {
4066            room.div_ceil(remaining_layers).max(1)
4067        };
4068        let room = match std::env::var("CMF_DSV4_PACK_LAYER_CAP")
4069            .ok()
4070            .and_then(|v| v.parse::<usize>().ok())
4071        {
4072            Some(0) => room,
4073            Some(cap) => room.min(cap),
4074            None => room.min(auto_cap),
4075        };
4076        // When the budget packs a SUBSET, which subset matters: a partial
4077        // layer completes its cold picks from the host, so every resident
4078        // expert that the routing actually reaches is host work saved.
4079        // `CMF_DSV4_PACK_FREQ` names a measured tally
4080        // (`CMF_DSV4_TRUNK_PICK_DUMP` wrote it) and reorders the candidates
4081        // hottest-first; layers absent from the tally keep id order. The
4082        // router still ranges over every expert either way — residency
4083        // choice changes speed, never a bit of the answer.
4084        let order =
4085            pack_freq_order(li, l.experts.len()).unwrap_or_else(|| (0..l.experts.len()).collect());
4086        for gi in order {
4087            let e = &l.experts[gi];
4088            if l.mask
4089                .as_deref()
4090                .is_some_and(|m| !m.get(gi).copied().unwrap_or(true))
4091            {
4092                continue;
4093            }
4094            if globals.len() >= room {
4095                break;
4096            }
4097            to_slot[gi] = globals.len();
4098            globals.push(gi);
4099            match idx3(e) {
4100                Some(t) => tensors.push(t),
4101                None => {
4102                    if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
4103                        eprintln!("слой {li}: эксперт {gi} без индексов в каталоге");
4104                    }
4105                    return None;
4106                }
4107            }
4108        }
4109        if globals.is_empty() {
4110            // Two very different causes, and blaming the mask for the other
4111            // one sent a reader looking for a mask that was never set: an
4112            // actual empty mask, or a VRAM budget with no room left for even
4113            // one expert (`room` is 0, which is what a nearly-full card does
4114            // to the last layers).
4115            if room == 0 {
4116                static SAID_ZERO: std::sync::atomic::AtomicBool =
4117                    std::sync::atomic::AtomicBool::new(false);
4118                if !SAID_ZERO.swap(true, std::sync::atomic::Ordering::Relaxed) {
4119                    tracing::warn!(
4120                        "начиная со слоя {li}, в бюджете VRAM не осталось места даже под одного \
4121                         эксперта — остальные веса остаются mmap-backed и читаются по требованию"
4122                    );
4123                }
4124            } else {
4125                tracing::warn!("слой {li}: маска не оставила ни одного эксперта");
4126            }
4127            return None;
4128        }
4129        tensors.push(idx3(&l.shared)?); // shared rides last, as the kernels expect
4130        let (rows, cols) = (l.gate.rows(), l.gate.cols());
4131        let mut router = vec![0.0f32; rows * cols];
4132        for r in 0..rows {
4133            l.gate.row_f32(r, &mut router[r * cols..(r + 1) * cols]);
4134        }
4135        let remap: Vec<u32> = to_slot
4136            .iter()
4137            .map(|&sl| {
4138                if sl == usize::MAX {
4139                    u32::MAX
4140                } else {
4141                    sl as u32
4142                }
4143            })
4144            .collect();
4145        Some(Arc::new(Pack {
4146            bias: l.gate_bias.clone(),
4147            mask: route_mask,
4148            router,
4149            to_slot,
4150            dynslots: std::sync::Mutex::new(PackDyn {
4151                remap: remap.clone(),
4152                owner: globals.iter().map(|&g| g as u32).collect(),
4153                last: vec![0; globals.len()],
4154                clock: 0,
4155                mutated: false,
4156                seen: vec![0; cfg.n_routed_experts],
4157            }),
4158            remap,
4159            globals,
4160            tensors,
4161        }))
4162    };
4163    let v = build();
4164    cache.lock().unwrap().insert(key, v.clone());
4165    v
4166}
4167
4168/// The whole MoE block in one submission, experts resident (default on;
4169/// `CMF_DSV4_GPU_MOE2=0` restores the host path). Returns false having
4170/// changed nothing if it cannot — a missing pack, a refused budget — so the
4171/// caller's CPU path stays correct to run. The early divergence this frame
4172/// once carried (0.44 relative, perplexity 5.162 vs 5.211) was the partial
4173/// -capture and hidden-seed defects, fixed since: perplexity gold 4.578 is
4174/// bit-exact against the CPU on every budget from 64 to 96.5 GB.
4175#[cfg(feature = "gpu")]
4176fn moe_frame(
4177    hidden: &[f32],
4178    l: &Dsv4Layer,
4179    cfg: &Dsv4Cfg,
4180    li: usize,
4181    logits: &[f32],
4182    forced: Option<&[usize]>,
4183    pool: Option<&crate::pool::Pool>,
4184    // The state handover: expand always when the device owns the state,
4185    // fold only when there is a next layer.
4186    hc_cur: Option<&crate::gpu_wgpu::Dsv4HcTail>,
4187    hc_next: Option<(&crate::gpu_wgpu::Dsv4HcTail, &[f32])>,
4188    out: &mut [f32],
4189) -> Option<(Vec<f32>, usize)> {
4190    macro_rules! no {
4191        ($($t:tt)*) => {{
4192            if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
4193                eprintln!("кадр MoE отклонён: {}", format_args!($($t)*));
4194            }
4195            return None;
4196        }};
4197    }
4198    let Some(pk) = pack_for(l, cfg, li) else {
4199        no!("слой {li}: упаковка экспертов не построена");
4200    };
4201    // The router is a small f32 tensor and is usually NOT mapped; the handle
4202    // has to come from something that is.
4203    let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
4204        no!("слой {li}: эксперты не отображены из файла");
4205    };
4206    let subset = !pk.route_complete();
4207    let needs_remap = pk.needs_remap();
4208    // Dynamic slots (the FreeToken move): predict this token's winners on
4209    // the host and pull the missing ones into LRU slots BEFORE the frame
4210    // runs — up to CMF_DSV4_FETCH_MAX experts a layer a token. The device
4211    // still routes for real, so a wrong prediction costs one unused fill
4212    // and never a wrong number: an unmapped winner comes back as a cold
4213    // pick and the CPU completes it, exactly as before.
4214    fn fetch_quota() -> usize {
4215        static Q: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
4216        *Q.get_or_init(|| {
4217            std::env::var("CMF_DSV4_FETCH_MAX")
4218                .ok()
4219                .and_then(|v| v.parse().ok())
4220                .unwrap_or_else(|| crate::gpu_wgpu::dsv4_fetch_defaults().0)
4221        })
4222    }
4223    let mut dynv = pk.dynslots.lock().unwrap();
4224    // The winners, from the same logits the device will rank — used by the
4225    // slot refill below AND by the FreeToken-style overlap: the picks that
4226    // will NOT be resident are computed on the CPU while the device frame
4227    // runs, instead of serially after its wait.
4228    let mut pidx = Vec::new();
4229    let mut pwt = Vec::new();
4230    // A chained caller may leave the FFN input only on the device. The
4231    // overlap path intentionally reads it back; when it does, score a HOST
4232    // prediction without changing the device's routing source. The GPU still
4233    // recomputes and ranks its own logits when `logits` is empty, so a rounding
4234    // disagreement can waste an early CPU result but cannot change a token.
4235    let mut predicted_logits = Vec::new();
4236    let prediction_logits: &[f32] = if subset && logits.is_empty() && !hidden.is_empty() {
4237        predicted_logits.resize(cfg.n_routed_experts, 0.0);
4238        l.gate.matvec(hidden, &mut predicted_logits, pool);
4239        &predicted_logits
4240    } else {
4241        logits
4242    };
4243    if subset && !prediction_logits.is_empty() {
4244        route(
4245            prediction_logits,
4246            l.gate_bias.as_deref(),
4247            cfg.top_k,
4248            cfg.route_scale,
4249            forced,
4250            l.mask.as_deref(),
4251            &mut pidx,
4252            &mut pwt,
4253        );
4254    }
4255    if subset && fetch_quota() > 0 && !pidx.is_empty() && !dynv.owner.is_empty() {
4256        dynv.clock += 1;
4257        let clock = dynv.clock;
4258        if clock % 64 == 0 {
4259            for v in dynv.seen.iter_mut() {
4260                *v >>= 1;
4261            }
4262        }
4263        for &pick in &pidx {
4264            dynv.seen[pick] = dynv.seen[pick].saturating_add(1);
4265            let sl = dynv.remap[pick];
4266            if sl != u32::MAX {
4267                dynv.last[sl as usize] = clock;
4268            }
4269        }
4270        fn fetch_min_seen() -> u16 {
4271            static M: std::sync::OnceLock<u16> = std::sync::OnceLock::new();
4272            *M.get_or_init(|| {
4273                std::env::var("CMF_DSV4_FETCH_MIN_SEEN")
4274                    .ok()
4275                    .and_then(|v| v.parse().ok())
4276                    .unwrap_or_else(|| crate::gpu_wgpu::dsv4_fetch_defaults().1)
4277            })
4278        }
4279        let mut fetched = 0usize;
4280        for &pick in &pidx {
4281            if fetched >= fetch_quota() {
4282                break;
4283            }
4284            if dynv.remap[pick] != u32::MAX {
4285                continue;
4286            }
4287            if dynv.seen[pick] < fetch_min_seen() {
4288                continue; // one-shot so far: the CPU reads it at the shelf
4289            }
4290            // Victim: the LRU slot among those this token does not need.
4291            let victim = (0..dynv.owner.len())
4292                .filter(|&sl| dynv.last[sl] != clock)
4293                .min_by_key(|&sl| dynv.last[sl]);
4294            let Some(victim) = victim else { break };
4295            let Some(exp) = l.experts.get(pick) else { continue };
4296            let t3 = (|| {
4297                Some((
4298                    exp.w1.model_idx()?,
4299                    exp.w3.model_idx()?,
4300                    exp.w2.model_idx()?,
4301                ))
4302            })();
4303            let Some(t3) = t3 else { continue };
4304            let gu_q2 =
4305                exp.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP);
4306            let pack_first = pk.tensors.first().map(|t| t.0).unwrap_or(usize::MAX);
4307            if !crate::gpu_wgpu::dsv4_slot_fill(
4308                &model,
4309                pack_first,
4310                victim,
4311                pick,
4312                t3,
4313                cfg.moe_inter,
4314                cfg.dim,
4315                gu_q2,
4316            ) {
4317                break;
4318            }
4319            let old = dynv.owner[victim] as usize;
4320            if old < dynv.remap.len() {
4321                dynv.remap[old] = u32::MAX;
4322            }
4323            dynv.remap[pick] = victim as u32;
4324            dynv.owner[victim] = pick as u32;
4325            dynv.last[victim] = clock;
4326            dynv.mutated = true;
4327            fetched += 1;
4328        }
4329    }
4330    // With a complete pack the forced row is translated to packed numbering.
4331    // With a subset it stays global: the router's remap either finds its slot
4332    // or returns the forced expert as a cold pick, exactly like a scored one.
4333    let fpack: Option<Vec<usize>> = match forced {
4334        Some(f) if needs_remap => Some(f.to_vec()),
4335        Some(f) => {
4336            let v: Vec<usize> = f.iter().map(|&g| pk.to_slot[g]).collect();
4337            if v.contains(&usize::MAX) {
4338                no!("слой {li}: хеш-слой называет эксперта вне упаковки");
4339            }
4340            Some(v)
4341        }
4342        None => None,
4343    };
4344    // Routing ranges over EVERY expert; the remap turns a winner into a slot
4345    // or marks it cold. Nothing is masked, so nothing is lost.
4346    // Empty logits are the device-scored case: the frame computes them from
4347    // pk.router, whose rows are already in global order, so there is nothing
4348    // to reorder — and indexing an empty slice is how this line greeted the
4349    // first engaged run.
4350    let lg: Vec<f32> = if logits.is_empty() || needs_remap {
4351        logits.to_vec()
4352    } else {
4353        pk.globals.iter().map(|&g| logits[g]).collect()
4354    };
4355    let w = crate::gpu_wgpu::Dsv4MoeW {
4356        router: &pk.router,
4357        experts: &pk.tensors,
4358        logits: &lg,
4359        bias: pk.bias.as_deref(),
4360        mask: pk.mask.as_deref(),
4361        forced: fpack.as_deref(),
4362        remap: needs_remap.then_some(dynv.remap.as_slice()),
4363    };
4364    let g = crate::gpu_wgpu::Dsv4MoeGeom {
4365        hidden: cfg.dim,
4366        inter: cfg.moe_inter,
4367        top_k: cfg.top_k,
4368        route_scale: cfg.route_scale,
4369        swiglu_limit: cfg.swiglu_limit,
4370        gu_q2: l
4371            .experts
4372            .first()
4373            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
4374    };
4375    let mut cold = Vec::new();
4376    let mut cold_x = Vec::new();
4377    // The FreeToken overlap: the predicted winners that will NOT be
4378    // resident are computed on the CPU WHILE the device frame runs,
4379    // instead of serially after its wait. Unweighted (weight 1) — the
4380    // device's own cold weights scale the result at the merge, so a
4381    // routing drift between the host's ranking and the card's costs one
4382    // wasted thread, never a wrong number. Only the per-layer path has
4383    // the input on the host (`hidden` non-empty); the chain keeps its
4384    // own economy.
4385    let overlap: Vec<usize> = if !hidden.is_empty() {
4386        pidx.iter()
4387            .copied()
4388            .filter(|&pick| dynv.remap.get(pick).copied().unwrap_or(u32::MAX) == u32::MAX)
4389            .collect()
4390    } else {
4391        Vec::new()
4392    };
4393    let mut early: std::collections::HashMap<usize, Vec<f32>> = std::collections::HashMap::new();
4394    let frame_ok = std::thread::scope(|sc| {
4395        let handles: Vec<_> = overlap
4396            .iter()
4397            .filter_map(|&gi| l.experts.get(gi).map(|exp| (gi, exp)))
4398            .map(|(gi, exp)| {
4399                sc.spawn(move || {
4400                    let mut a = vec![0.0f32; cfg.dim];
4401                    crate::gpu::cpu_scope(|| run_expert(hidden, exp, cfg, 1.0, None, &mut a));
4402                    (gi, a)
4403                })
4404            })
4405            .collect();
4406        let ok = crate::gpu_wgpu::dsv4_moe_frame(
4407            &model,
4408            &w,
4409            g,
4410            hidden,
4411            &mut cold,
4412            &mut cold_x,
4413            hc_cur,
4414            hc_next,
4415            out,
4416        );
4417        for h in handles {
4418            if let Ok((gi, a)) = h.join() {
4419                early.insert(gi, a);
4420            }
4421        }
4422        ok
4423    });
4424    if !frame_ok {
4425        return None;
4426    }
4427    // The picks the card had no room for, finished here and added in. Their
4428    // weights already carry the top-k normalisation the device applied.
4429    if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
4430        let csum: f32 = cold.iter().map(|c| c.1).sum();
4431        eprintln!(
4432            "[холодные] слой {li}: вернулось {} из {} | сумма холодных {csum:.4} | \
4433             route_scale {:.4} | {:?}",
4434            cold.len(),
4435            cfg.top_k,
4436            cfg.route_scale,
4437            &cold[..cold.len().min(3)]
4438        );
4439    }
4440    let mut acc = vec![0.0f32; cfg.dim];
4441    let mut cold_sum = vec![0.0f32; cfg.dim];
4442    let cold_input = if hidden.is_empty() {
4443        cold_x.as_slice()
4444    } else {
4445        hidden
4446    };
4447    // Cold means out-of-core by contract. The tensors remain mmap-backed:
4448    // missing pages are faulted from the CMF file and the OS may evict
4449    // them again under RAM pressure. Do not let the generic matvec probe
4450    // turn this into an unbounded second GPU cache behind the packer's
4451    // back.
4452    //
4453    // The unit of parallelism is the EXPERT, not the row: a 2048-row
4454    // matvec split across 380 workers is five rows per worker — all
4455    // dispatch, no arithmetic. One worker per cold expert, whole matvecs
4456    // inside (inner pool None), was the difference between ~7 ms and ~1 ms
4457    // per cold expert on the 384-core stand. cpu_scope is thread-local, so
4458    // it sits INSIDE the worker closure.
4459    if !early.is_empty() {
4460        // The overlap already computed (most of) the cold picks; scale by
4461        // the DEVICE's weight and add in cold order — the same order the
4462        // serial path used, so parity holds. A cold pick the prediction
4463        // missed (ranking drift) is computed inline, cpu_scope'd.
4464        for &(gi, wt) in &cold {
4465            if let Some(a) = early.get(&gi) {
4466                for ((o, sum), v) in out.iter_mut().zip(&mut cold_sum).zip(a.iter()) {
4467                    *o += v * wt;
4468                    *sum += v * wt;
4469                }
4470                continue;
4471            }
4472            let Some(exp) = l.experts.get(gi) else { continue };
4473            crate::gpu::cpu_scope(|| run_expert(cold_input, exp, cfg, wt, pool, &mut acc));
4474            for ((o, sum), a) in out.iter_mut().zip(&mut cold_sum).zip(&acc) {
4475                *o += a;
4476                *sum += a;
4477            }
4478        }
4479        return Some((cold_sum, cold.len()));
4480    }
4481    match pool {
4482        Some(p) if cold.len() > 1 => {
4483            let results: Vec<std::sync::Mutex<Vec<f32>>> =
4484                cold.iter().map(|_| std::sync::Mutex::new(Vec::new())).collect();
4485            let (cold_ref, results_ref) = (&cold, &results);
4486            p.run_rows(cold.len(), &move |cs, ce| {
4487                for i in cs..ce {
4488                    let (gi, wt) = cold_ref[i];
4489                    let Some(exp) = l.experts.get(gi) else { continue };
4490                    let mut a = vec![0.0f32; cfg.dim];
4491                    crate::gpu::cpu_scope(|| run_expert(cold_input, exp, cfg, wt, None, &mut a));
4492                    *results_ref[i].lock().unwrap() = a;
4493                }
4494            });
4495            // Serial reduce in cold order — the accumulation order the
4496            // scalar path had, so parity holds bit for bit.
4497            for r in &results {
4498                let a = r.lock().unwrap();
4499                if a.is_empty() {
4500                    continue;
4501                }
4502                for ((o, sum), v) in out.iter_mut().zip(&mut cold_sum).zip(a.iter()) {
4503                    *o += v;
4504                    *sum += v;
4505                }
4506            }
4507        }
4508        _ => {
4509            for &(gi, wt) in &cold {
4510                let Some(exp) = l.experts.get(gi) else {
4511                    continue;
4512                };
4513                crate::gpu::cpu_scope(|| run_expert(cold_input, exp, cfg, wt, pool, &mut acc));
4514                for ((o, sum), a) in out.iter_mut().zip(&mut cold_sum).zip(&acc) {
4515                    *o += a;
4516                    *sum += a;
4517                }
4518            }
4519        }
4520    }
4521    Some((cold_sum, cold.len()))
4522}
4523
4524/// How much of each layer's compressed cache already sits on the card. ONE
4525/// map: a reader and a writer with a `static` each are two maps, and the
4526/// reader would never see a thing the writer put down.
4527/// The reallocation counter as of the last successful tail write. Any change
4528/// means some buffer was rebuilt and every tail count is stale.
4529#[cfg(feature = "gpu")]
4530fn last_grew(now: u64) -> u64 {
4531    use std::sync::atomic::{AtomicU64, Ordering};
4532    static SEEN: AtomicU64 = AtomicU64::new(0);
4533    let was = SEEN.load(Ordering::Relaxed);
4534    if was != now {
4535        SEEN.store(now, Ordering::Relaxed);
4536        compressed_map().lock().unwrap().clear();
4537        return u64::MAX; // force a full write this round
4538    }
4539    now
4540}
4541
4542#[cfg(feature = "gpu")]
4543fn compressed_map() -> &'static std::sync::Mutex<std::collections::HashMap<(u64, usize), usize>> {
4544    use std::collections::HashMap;
4545    use std::sync::{Mutex, OnceLock};
4546    static W: OnceLock<Mutex<HashMap<(u64, usize), usize>>> = OnceLock::new();
4547    W.get_or_init(|| Mutex::new(HashMap::new()))
4548}
4549
4550#[cfg(feature = "gpu")]
4551fn compressed_written(kv_id: u64, li: usize) -> usize {
4552    compressed_map()
4553        .lock()
4554        .unwrap()
4555        .get(&(kv_id, li))
4556        .copied()
4557        .unwrap_or(0)
4558}
4559
4560/// Reset to zero whenever a write fails or the buffer grows — a grown buffer
4561/// keeps none of its contents.
4562#[cfg(feature = "gpu")]
4563fn note_compressed(kv_id: u64, li: usize, n: usize) {
4564    compressed_map().lock().unwrap().insert((kv_id, li), n);
4565}
4566
4567#[cfg(feature = "gpu")]
4568fn gpu_moe2_enabled() -> bool {
4569    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4570    *ON.get_or_init(|| {
4571        std::env::var("CMF_DSV4_GPU_MOE2")
4572            .map(|v| v != "0")
4573            .unwrap_or(true)
4574            && crate::gpu::backend_available()
4575    })
4576}
4577
4578pub fn moe_step(
4579    hidden: &[f32],
4580    l: &Dsv4Layer,
4581    cfg: &Dsv4Cfg,
4582    token_id: u32,
4583    // Layer index — only used to bucket routing statistics.
4584    li: usize,
4585    pool: Option<&crate::pool::Pool>,
4586    out: &mut [f32],
4587) {
4588    let _t0 = prof::on().then(std::time::Instant::now);
4589    let _guard = scopeguard_moe(_t0, li);
4590    let mut logits = vec![0.0f32; cfg.n_routed_experts];
4591    l.gate.matvec(hidden, &mut logits, pool);
4592    let (mut idx, mut w) = (Vec::new(), Vec::new());
4593    route(
4594        &logits,
4595        l.gate_bias.as_deref(),
4596        cfg.top_k,
4597        cfg.route_scale,
4598        l.tid2eid
4599            .as_ref()
4600            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id))
4601            .as_deref(),
4602        l.mask.as_deref(),
4603        &mut idx,
4604        &mut w,
4605    );
4606    if route_stats_on() {
4607        let routed: Vec<(usize, f32)> = idx.iter().copied().zip(w.iter().copied()).collect();
4608        record_route(li, 0, cfg.n_routed_experts, &routed);
4609    }
4610    // Same trace the generic MoE path writes (`CMF_MOE_TRACE`): one
4611    // `layer:e1,e2,…` line per routed token. The first arena run on this
4612    // architecture measured a 4.5% hit rate — random level for the arena's
4613    // size — and only a per-token trace can say whether that is the
4614    // router's true entropy or the cache structure destroying locality.
4615    crate::pipeline::moe_trace_at(li as i32, &idx);
4616    // The whole block on the device, in one submission, or nothing. Routing
4617    // happens there too — the logits above are what it starts from, so the
4618    // CPU's own choice is discarded rather than second-guessed.
4619    #[cfg(feature = "gpu")]
4620    if gpu_moe2_enabled() && crate::gpu::enabled_here() {
4621        let forced = l
4622            .tid2eid
4623            .as_ref()
4624            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
4625        if moe_frame(
4626            hidden,
4627            l,
4628            cfg,
4629            li,
4630            &logits,
4631            forced.as_deref(),
4632            pool,
4633            None,
4634            None,
4635            out,
4636        )
4637        .is_some()
4638        {
4639            // CMF_DSV4_MOE_CHECK=1 recomputes the same block on the CPU and
4640            // reports where they part. A wrong MoE does not fail — it answers
4641            // differently — and the toy agreed bit for bit while the release
4642            // did not, so the difference lives in something the toy has no
4643            // instance of. Only a per-layer number will say which.
4644            if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
4645                let mut want = vec![0.0f32; out.len()];
4646                let mut acc = vec![0.0f32; cfg.dim];
4647                for (e, &ei) in idx.iter().enumerate() {
4648                    let Some(exp) = l.experts.get(ei) else {
4649                        continue;
4650                    };
4651                    run_expert(
4652                        hidden,
4653                        exp,
4654                        cfg,
4655                        w.get(e).copied().unwrap_or(0.0),
4656                        pool,
4657                        &mut acc,
4658                    );
4659                    for (o, a) in want.iter_mut().zip(&acc) {
4660                        *o += a;
4661                    }
4662                }
4663                run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
4664                for (o, a) in want.iter_mut().zip(&acc) {
4665                    *o += a;
4666                }
4667                let num: f32 = want
4668                    .iter()
4669                    .zip(out.iter())
4670                    .map(|(a, b)| (a - b) * (a - b))
4671                    .sum();
4672                let den: f32 = want.iter().map(|a| a * a).sum::<f32>().max(1e-20);
4673                let rel = (num / den).sqrt();
4674                if rel > 1e-3 {
4675                    let packed = pack_for(l, cfg, li).map_or(0, |p| p.globals.len());
4676                    eprintln!(
4677                        "[кадр MoE] слой {li}: расхождение {rel:.3e} | выбрано {} | \
4678                         упаковано {packed} из {} | хеш={} | смещение={}",
4679                        idx.len(),
4680                        cfg.n_routed_experts,
4681                        l.tid2eid.is_some(),
4682                        l.gate_bias.is_some()
4683                    );
4684                }
4685            }
4686            return;
4687        }
4688    }
4689    // Cheap tally for the batching question: how many DISTINCT experts a
4690    // group of tokens reaches. If five tokens want thirty different experts,
4691    // a batched MoE reads thirty weights and amortises nothing — which is
4692    // the difference between a speculative verify that pays for itself and
4693    // one that does not. Disarmed it costs one thread-local read.
4694    PICK_TALLY.with(|t| {
4695        if let Some(v) = t.borrow_mut().as_mut() {
4696            v.push((li, idx.to_vec()));
4697        }
4698    });
4699    if dump_path().is_some() {
4700        PICKED.with(|p| {
4701            let mut p = p.borrow_mut();
4702            if p.len() <= li {
4703                p.resize(li + 1, Vec::new());
4704            }
4705            p[li] = idx.clone();
4706        });
4707    }
4708    // One submission for the whole block — the chosen experts plus the
4709    // shared one. Per-expert dispatches are what made MoE slow elsewhere,
4710    // and the device keeps the weights across tokens, so the cost is the
4711    // arithmetic rather than the traffic. A refusal (missing kernel, mixed
4712    // layouts, weights that do not fit the budget) falls to the CPU whole,
4713    // never half.
4714    // CORRECT but SLOWER, so off by default. Parity holds on real weights
4715    // (perplexity 6.808 → 6.839 at 64 tokens, 5.102 → 5.146 at 200), and an
4716    // honest alternating A/B says 1.0 tok/s against the CPU's 2.2. The first
4717    // measurement claimed the opposite — 0.7 → 2.0 — because the CPU arm ran
4718    // first and paged in 158 GB for the GPU arm to inherit.
4719    //
4720    // The cost is not arithmetic, it is round trips: this submits and reads
4721    // back once per layer, forty-three times a token, and a discrete card
4722    // charges milliseconds for each. Fixing it means one submission per
4723    // token — the whole-token graph — not a faster kernel.
4724    //
4725    // `CMF_DSV4_GPU_MOE=1` opts in.
4726    fn gpu_moe_on() -> bool {
4727        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4728        *ON.get_or_init(|| std::env::var("CMF_DSV4_GPU_MOE").is_ok_and(|v| v != "0"))
4729    }
4730    if gpu_moe_on() && crate::gpu::enabled_here() {
4731        let mut jobs = Vec::with_capacity(idx.len() + 1);
4732        let mut model_ref = None;
4733        let mut ok = true;
4734        for (e, &ei) in idx.iter().enumerate() {
4735            let Some(exp) = l.experts.get(ei) else {
4736                continue;
4737            };
4738            ok &= crate::pipeline::moe_push_job_parts(
4739                &exp.w1,
4740                &exp.w3,
4741                &exp.w2,
4742                hidden,
4743                w.get(e).copied().unwrap_or(0.0),
4744                cfg.swiglu_limit,
4745                &mut jobs,
4746                &mut model_ref,
4747            )
4748            .is_some();
4749        }
4750        ok &= crate::pipeline::moe_push_job_parts(
4751            &l.shared.w1,
4752            &l.shared.w3,
4753            &l.shared.w2,
4754            hidden,
4755            1.0,
4756            cfg.swiglu_limit,
4757            &mut jobs,
4758            &mut model_ref,
4759        )
4760        .is_some();
4761        if ok {
4762            if let Some(m) = model_ref.as_ref() {
4763                if crate::gpu::moe_block(m, &jobs, out) {
4764                    // CMF_DSV4_GPU_CHECK=1 recomputes the same block on the
4765                    // CPU and reports the divergence. A GPU MoE that is wrong
4766                    // does not fail — it answers differently — so the only way
4767                    // to know is to ask both.
4768                    if std::env::var("CMF_DSV4_GPU_CHECK").is_ok() {
4769                        let mut want = vec![0.0f32; out.len()];
4770                        let mut acc = vec![0.0f32; cfg.dim];
4771                        for (e, &ei) in idx.iter().enumerate() {
4772                            let Some(exp) = l.experts.get(ei) else {
4773                                continue;
4774                            };
4775                            run_expert(
4776                                hidden,
4777                                exp,
4778                                cfg,
4779                                w.get(e).copied().unwrap_or(0.0),
4780                                pool,
4781                                &mut acc,
4782                            );
4783                            for (o, a) in want.iter_mut().zip(&acc) {
4784                                *o += a;
4785                            }
4786                        }
4787                        run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
4788                        for (o, a) in want.iter_mut().zip(&acc) {
4789                            *o += a;
4790                        }
4791                        let num: f32 = want
4792                            .iter()
4793                            .zip(out.iter())
4794                            .map(|(a, b)| (a - b) * (a - b))
4795                            .sum();
4796                        let den: f32 = want.iter().map(|a| a * a).sum::<f32>().max(1e-20);
4797                        eprintln!(
4798                            "[dsv4-gpu] слой {li}: расхождение {:.3e} | |CPU|={:.5} |GPU|={:.5} | экспертов {}",
4799                            (num / den).sqrt(),
4800                            den.sqrt(),
4801                            out.iter().map(|x| x * x).sum::<f32>().sqrt(),
4802                            jobs.len()
4803                        );
4804                    }
4805                    return;
4806                }
4807            }
4808        }
4809    }
4810    out.fill(0.0);
4811    // Layers the packer had no room for land here whole. Same shape as the
4812    // frame's cold completion: one worker per expert (the shared one rides
4813    // as an extra job), whole matvecs inside, cpu_scope INSIDE the worker —
4814    // on the main thread it would gate nothing, and the generic matvec
4815    // would upload every expert to the card tensor by tensor, which is
4816    // exactly the per-token PCIe churn this path exists to avoid.
4817    let jobs: Vec<(Option<usize>, f32)> = idx
4818        .iter()
4819        .enumerate()
4820        .map(|(e, &ei)| (Some(ei), w.get(e).copied().unwrap_or(0.0)))
4821        .chain(std::iter::once((None, 1.0)))
4822        .collect();
4823    match pool {
4824        Some(p) if jobs.len() > 1 => {
4825            let results: Vec<std::sync::Mutex<Vec<f32>>> =
4826                jobs.iter().map(|_| std::sync::Mutex::new(Vec::new())).collect();
4827            let (jobs_ref, results_ref) = (&jobs, &results);
4828            p.run_rows(jobs.len(), &move |cs, ce| {
4829                for i in cs..ce {
4830                    let (ei, wt) = jobs_ref[i];
4831                    let exp = match ei {
4832                        Some(ei) => match l.experts.get(ei) {
4833                            Some(x) => x,
4834                            None => continue,
4835                        },
4836                        None => &l.shared,
4837                    };
4838                    let mut a = vec![0.0f32; cfg.dim];
4839                    crate::gpu::cpu_scope(|| run_expert(hidden, exp, cfg, wt, None, &mut a));
4840                    *results_ref[i].lock().unwrap() = a;
4841                }
4842            });
4843            for r in &results {
4844                let a = r.lock().unwrap();
4845                for (o, v) in out.iter_mut().zip(a.iter()) {
4846                    *o += v;
4847                }
4848            }
4849        }
4850        _ => {
4851            let mut acc = vec![0.0f32; cfg.dim];
4852            for &(ei, wt) in &jobs {
4853                let exp = match ei {
4854                    Some(ei) => match l.experts.get(ei) {
4855                        Some(x) => x,
4856                        None => continue,
4857                    },
4858                    None => &l.shared,
4859                };
4860                crate::gpu::cpu_scope(|| run_expert(hidden, exp, cfg, wt, pool, &mut acc));
4861                for (o, a) in out.iter_mut().zip(&acc) {
4862                    *o += a;
4863                }
4864            }
4865        }
4866    }
4867}
4868
4869/// The routed and shared experts both come through here, so the clamp and
4870/// the weight folding have exactly one implementation — `expert_swiglu`.
4871fn run_expert(
4872    x: &[f32],
4873    e: &Dsv4Expert,
4874    cfg: &Dsv4Cfg,
4875    weight: f32,
4876    pool: Option<&crate::pool::Pool>,
4877    out: &mut [f32],
4878) {
4879    expert_swiglu(
4880        x,
4881        &|src, dst| e.w1.matvec(src, dst, pool),
4882        &|src, dst| e.w3.matvec(src, dst, pool),
4883        &|src, dst| e.w2.matvec(src, dst, pool),
4884        cfg.moe_inter,
4885        weight,
4886        cfg.swiglu_limit,
4887        out,
4888    );
4889}
4890
4891/// The same expert computation for several inputs, streaming each selected
4892/// weight once. Used by DSpark's trained five-position block: running five
4893/// ordinary `moe_step`s rereads the shared expert five times and every
4894/// coincident routed expert once per position.
4895fn moe_step_block(
4896    xs: &[f32],
4897    b: usize,
4898    l: &Dsv4Layer,
4899    cfg: &Dsv4Cfg,
4900    token_ids: &[u32],
4901    tally_layer: usize,
4902    pool: Option<&crate::pool::Pool>,
4903    out: &mut [f32],
4904) {
4905    let (dim, inter) = (cfg.dim, cfg.moe_inter);
4906    debug_assert_eq!(xs.len(), b * dim);
4907    debug_assert_eq!(out.len(), b * dim);
4908    out.fill(0.0);
4909
4910    let mut logits = vec![0.0f32; b * cfg.n_routed_experts];
4911    l.gate.matmat(xs, b, &mut logits, pool);
4912    let mut picks: Vec<Vec<usize>> = Vec::with_capacity(b);
4913    let mut weights: Vec<Vec<f32>> = Vec::with_capacity(b);
4914    for bi in 0..b {
4915        let mut idx = Vec::new();
4916        let mut wt = Vec::new();
4917        let forced = l.tid2eid.as_ref().map(|tbl| {
4918            hash_route(
4919                tbl,
4920                cfg.vocab,
4921                cfg.top_k,
4922                token_ids.get(bi).copied().unwrap_or(0),
4923            )
4924        });
4925        route(
4926            &logits[bi * cfg.n_routed_experts..(bi + 1) * cfg.n_routed_experts],
4927            l.gate_bias.as_deref(),
4928            cfg.top_k,
4929            cfg.route_scale,
4930            forced.as_deref(),
4931            l.mask.as_deref(),
4932            &mut idx,
4933            &mut wt,
4934        );
4935        PICK_TALLY.with(|t| {
4936            if let Some(v) = t.borrow_mut().as_mut() {
4937                v.push((tally_layer, idx.clone()));
4938            }
4939        });
4940        picks.push(idx);
4941        weights.push(wt);
4942    }
4943
4944    // Preserve the scalar path's accumulation order by keeping every routed
4945    // slot separate; grouping below changes only when a weight is read.
4946    let mut routed = vec![0.0f32; b * cfg.top_k * dim];
4947    // Group the token slots by expert first — the list is also the unit of
4948    // parallelism below.
4949    let mut active: Vec<(usize, Vec<(usize, usize, f32)>)> = Vec::new();
4950    for ei in 0..l.experts.len() {
4951        let mut jobs = Vec::new();
4952        for bi in 0..b {
4953            for (slot, &picked) in picks[bi].iter().enumerate() {
4954                if picked == ei {
4955                    jobs.push((bi, slot, weights[bi][slot]));
4956                }
4957            }
4958        }
4959        if !jobs.is_empty() {
4960            active.push((ei, jobs));
4961        }
4962    }
4963    // One expert's forward, single-threaded, returning the scaled down
4964    // projections in job order.
4965    let expert_fwd = |ei: usize, jobs: &[(usize, usize, f32)], inner: Option<&crate::pool::Pool>| -> Vec<f32> {
4966        let e = &l.experts[ei];
4967        let n = jobs.len();
4968        let mut xj = vec![0.0f32; n * dim];
4969        for (j, &(bi, _, _)) in jobs.iter().enumerate() {
4970            xj[j * dim..(j + 1) * dim].copy_from_slice(&xs[bi * dim..(bi + 1) * dim]);
4971        }
4972        let mut gate = vec![0.0f32; n * inter];
4973        let mut up = vec![0.0f32; n * inter];
4974        e.w1.matmat(&xj, n, &mut gate, inner);
4975        e.w3.matmat(&xj, n, &mut up, inner);
4976        for (j, &(_, _, wt)) in jobs.iter().enumerate() {
4977            let (gj, uj) = (
4978                &mut gate[j * inter..(j + 1) * inter],
4979                &mut up[j * inter..(j + 1) * inter],
4980            );
4981            if cfg.swiglu_limit > 0.0 {
4982                for u in uj.iter_mut() {
4983                    *u = u.clamp(-cfg.swiglu_limit, cfg.swiglu_limit);
4984                }
4985                for g in gj.iter_mut() {
4986                    *g = g.min(cfg.swiglu_limit);
4987                }
4988            }
4989            for (g, &u) in gj.iter_mut().zip(uj.iter()) {
4990                *g = (*g / (1.0 + (-*g).exp())) * u * wt;
4991            }
4992        }
4993        let mut down = vec![0.0f32; n * dim];
4994        e.w2.matmat(&gate, n, &mut down, inner);
4995        down
4996    };
4997    // ~370 non-resident experts a token used to run this loop ONE AFTER
4998    // ANOTHER: a 2048-row matvec cannot occupy a big pool, and the loop
4999    // serialised the only real parallelism there is — across experts.
5000    // Measured on a 384-core host with DeepSeek-V4-Flash: ~3.3 s/token
5001    // flat across every fetch-side improvement, because the wall was
5002    // here. Parallel across experts, each single-threaded and pinned to
5003    // the CPU on ITS OWN worker (cpu_scope is thread-local, so it must
5004    // be entered inside the closure, not around the pool call — the
5005    // documented trap).
5006    match pool {
5007        Some(p) if active.len() > 1 => {
5008            let results: Vec<std::sync::Mutex<Vec<f32>>> =
5009                active.iter().map(|_| std::sync::Mutex::new(Vec::new())).collect();
5010            let active_ref = &active;
5011            let results_ref = &results;
5012            let fwd = &expert_fwd;
5013            p.run_rows(active_ref.len(), &move |s, e| {
5014                for i in s..e {
5015                    let (ei, jobs) = &active_ref[i];
5016                    let d = crate::gpu::cpu_scope(|| fwd(*ei, jobs, None));
5017                    *results_ref[i].lock().unwrap() = d;
5018                }
5019            });
5020            for (i, (_, jobs)) in active.iter().enumerate() {
5021                let down = results[i].lock().unwrap();
5022                for (j, &(bi, slot, _)) in jobs.iter().enumerate() {
5023                    routed[(bi * cfg.top_k + slot) * dim..(bi * cfg.top_k + slot + 1) * dim]
5024                        .copy_from_slice(&down[j * dim..(j + 1) * dim]);
5025                }
5026            }
5027        }
5028        _ => {
5029            for (ei, jobs) in &active {
5030                let down = expert_fwd(*ei, jobs, pool);
5031                for (j, &(bi, slot, _)) in jobs.iter().enumerate() {
5032                    routed[(bi * cfg.top_k + slot) * dim..(bi * cfg.top_k + slot + 1) * dim]
5033                        .copy_from_slice(&down[j * dim..(j + 1) * dim]);
5034                }
5035            }
5036        }
5037    }
5038
5039    // Shared expert: all positions always use it, so this is the highest
5040    // certainty weight-sharing win in the block.
5041    let mut sg = vec![0.0f32; b * inter];
5042    let mut su = vec![0.0f32; b * inter];
5043    l.shared.w1.matmat(xs, b, &mut sg, pool);
5044    l.shared.w3.matmat(xs, b, &mut su, pool);
5045    for bi in 0..b {
5046        let (gj, uj) = (
5047            &mut sg[bi * inter..(bi + 1) * inter],
5048            &mut su[bi * inter..(bi + 1) * inter],
5049        );
5050        if cfg.swiglu_limit > 0.0 {
5051            for u in uj.iter_mut() {
5052                *u = u.clamp(-cfg.swiglu_limit, cfg.swiglu_limit);
5053            }
5054            for g in gj.iter_mut() {
5055                *g = g.min(cfg.swiglu_limit);
5056            }
5057        }
5058        for (g, &u) in gj.iter_mut().zip(uj.iter()) {
5059            *g = (*g / (1.0 + (-*g).exp())) * u;
5060        }
5061    }
5062    let mut shared = vec![0.0f32; b * dim];
5063    l.shared.w2.matmat(&sg, b, &mut shared, pool);
5064
5065    for bi in 0..b {
5066        let dst = &mut out[bi * dim..(bi + 1) * dim];
5067        for slot in 0..picks[bi].len() {
5068            let src = &routed[(bi * cfg.top_k + slot) * dim..(bi * cfg.top_k + slot + 1) * dim];
5069            for (o, &v) in dst.iter_mut().zip(src) {
5070                *o += v;
5071            }
5072        }
5073        for (o, &v) in dst.iter_mut().zip(&shared[bi * dim..(bi + 1) * dim]) {
5074            *o += v;
5075        }
5076    }
5077}
5078
5079/// Complete only the routed experts absent from a partial GPU pack.  Jobs
5080/// are grouped by expert so coincident speculative positions stream that
5081/// expert's weights once; per-token slot accumulation remains in route order,
5082/// matching the ordinary exact cold-correction path.
5083fn cold_step_block(
5084    xs: &[f32],
5085    b: usize,
5086    l: &Dsv4Layer,
5087    cfg: &Dsv4Cfg,
5088    cold: &[Vec<(usize, f32)>],
5089    pool: Option<&crate::pool::Pool>,
5090    out: &mut [f32],
5091) {
5092    let (dim, inter) = (cfg.dim, cfg.moe_inter);
5093    debug_assert_eq!(xs.len(), b * dim);
5094    debug_assert_eq!(cold.len(), b);
5095    debug_assert_eq!(out.len(), b * dim);
5096    out.fill(0.0);
5097    let slots = cold.iter().map(Vec::len).max().unwrap_or(0);
5098    if slots == 0 {
5099        return;
5100    }
5101    // The ordinary partial walk evaluates every cold winner with matvec.
5102    // The grouped arm streams a coincident expert once for the whole verify
5103    // block; release-scale fingerprints and row-zero logits were identical,
5104    // and it moved the fixed A40 bench 1.9 → 2.3 tok/s.  Keep the scalar arm
5105    // as a parity escape hatch for a new quant layout/adapter.
5106    let grouped = std::env::var("CMF_DSV4_COLD_MATMAT")
5107        .map(|v| v != "0")
5108        .unwrap_or(true);
5109    if !grouped {
5110        let jobs: Vec<(usize, usize, usize, f32)> = cold
5111            .iter()
5112            .enumerate()
5113            .flat_map(|(bi, row)| {
5114                row.iter()
5115                    .enumerate()
5116                    .map(move |(slot, &(ei, wt))| (bi, slot, ei, wt))
5117            })
5118            .collect();
5119        let results: Vec<std::sync::Mutex<Vec<f32>>> =
5120            jobs.iter().map(|_| std::sync::Mutex::new(Vec::new())).collect();
5121        match pool {
5122            Some(p) if jobs.len() > 1 => {
5123                let (jobs_ref, results_ref) = (&jobs, &results);
5124                p.run_rows(jobs.len(), &move |s, e| {
5125                    for i in s..e {
5126                        let (_, _, ei, wt) = jobs_ref[i];
5127                        let Some(exp) = l.experts.get(ei) else { continue };
5128                        let bi = jobs_ref[i].0;
5129                        let mut a = vec![0.0f32; dim];
5130                        crate::gpu::cpu_scope(|| {
5131                            run_expert(
5132                                &xs[bi * dim..(bi + 1) * dim],
5133                                exp,
5134                                cfg,
5135                                wt,
5136                                None,
5137                                &mut a,
5138                            )
5139                        });
5140                        *results_ref[i].lock().unwrap() = a;
5141                    }
5142                });
5143            }
5144            _ => {
5145                for (i, &(bi, _, ei, wt)) in jobs.iter().enumerate() {
5146                    let Some(exp) = l.experts.get(ei) else { continue };
5147                    let mut a = vec![0.0f32; dim];
5148                    crate::gpu::cpu_scope(|| {
5149                        run_expert(
5150                            &xs[bi * dim..(bi + 1) * dim],
5151                            exp,
5152                            cfg,
5153                            wt,
5154                            pool,
5155                            &mut a,
5156                        )
5157                    });
5158                    *results[i].lock().unwrap() = a;
5159                }
5160            }
5161        }
5162        // Reduce in each token's routing order, exactly like
5163        // dsv4_chain1_layer. The jobs vector was built in that order.
5164        for (i, &(bi, _, _, _)) in jobs.iter().enumerate() {
5165            let a = results[i].lock().unwrap();
5166            for (o, &v) in out[bi * dim..(bi + 1) * dim].iter_mut().zip(a.iter()) {
5167                *o += v;
5168            }
5169        }
5170        return;
5171    }
5172    let mut active: Vec<(usize, Vec<(usize, usize, f32)>)> = Vec::new();
5173    for ei in 0..l.experts.len() {
5174        let mut jobs = Vec::new();
5175        for bi in 0..b {
5176            for (slot, &(picked, wt)) in cold[bi].iter().enumerate() {
5177                if picked == ei {
5178                    jobs.push((bi, slot, wt));
5179                }
5180            }
5181        }
5182        if !jobs.is_empty() {
5183            active.push((ei, jobs));
5184        }
5185    }
5186    let expert_fwd = |ei: usize,
5187                      jobs: &[(usize, usize, f32)],
5188                      inner: Option<&crate::pool::Pool>|
5189     -> Vec<f32> {
5190        let e = &l.experts[ei];
5191        let n = jobs.len();
5192        let mut xj = vec![0.0f32; n * dim];
5193        for (j, &(bi, _, _)) in jobs.iter().enumerate() {
5194            xj[j * dim..(j + 1) * dim].copy_from_slice(&xs[bi * dim..(bi + 1) * dim]);
5195        }
5196        let mut gate = vec![0.0f32; n * inter];
5197        let mut up = vec![0.0f32; n * inter];
5198        e.w1.matmat(&xj, n, &mut gate, inner);
5199        e.w3.matmat(&xj, n, &mut up, inner);
5200        for (j, &(_, _, wt)) in jobs.iter().enumerate() {
5201            let (gj, uj) = (
5202                &mut gate[j * inter..(j + 1) * inter],
5203                &mut up[j * inter..(j + 1) * inter],
5204            );
5205            if cfg.swiglu_limit > 0.0 {
5206                for u in uj.iter_mut() {
5207                    *u = u.clamp(-cfg.swiglu_limit, cfg.swiglu_limit);
5208                }
5209                for g in gj.iter_mut() {
5210                    *g = g.min(cfg.swiglu_limit);
5211                }
5212            }
5213            for (g, &u) in gj.iter_mut().zip(uj.iter()) {
5214                *g = (*g / (1.0 + (-*g).exp())) * u * wt;
5215            }
5216        }
5217        let mut down = vec![0.0f32; n * dim];
5218        e.w2.matmat(&gate, n, &mut down, inner);
5219        down
5220    };
5221    let mut routed = vec![0.0f32; b * slots * dim];
5222    match pool {
5223        Some(p) if active.len() > 1 => {
5224            let results: Vec<std::sync::Mutex<Vec<f32>>> =
5225                active.iter().map(|_| std::sync::Mutex::new(Vec::new())).collect();
5226            let active_ref = &active;
5227            let results_ref = &results;
5228            let fwd = &expert_fwd;
5229            p.run_rows(active.len(), &move |s, e| {
5230                for i in s..e {
5231                    let (ei, jobs) = &active_ref[i];
5232                    let d = crate::gpu::cpu_scope(|| fwd(*ei, jobs, None));
5233                    *results_ref[i].lock().unwrap() = d;
5234                }
5235            });
5236            for (i, (_, jobs)) in active.iter().enumerate() {
5237                let down = results[i].lock().unwrap();
5238                for (j, &(bi, slot, _)) in jobs.iter().enumerate() {
5239                    routed[(bi * slots + slot) * dim..(bi * slots + slot + 1) * dim]
5240                        .copy_from_slice(&down[j * dim..(j + 1) * dim]);
5241                }
5242            }
5243        }
5244        _ => {
5245            for (ei, jobs) in &active {
5246                let down = expert_fwd(*ei, jobs, pool);
5247                for (j, &(bi, slot, _)) in jobs.iter().enumerate() {
5248                    routed[(bi * slots + slot) * dim..(bi * slots + slot + 1) * dim]
5249                        .copy_from_slice(&down[j * dim..(j + 1) * dim]);
5250                }
5251            }
5252        }
5253    }
5254    for bi in 0..b {
5255        let dst = &mut out[bi * dim..(bi + 1) * dim];
5256        for slot in 0..cold[bi].len() {
5257            let src = &routed[(bi * slots + slot) * dim..(bi * slots + slot + 1) * dim];
5258            for (o, &v) in dst.iter_mut().zip(src) {
5259                *o += v;
5260            }
5261        }
5262    }
5263}
5264
5265/// Feed the exact route of the speculative batch's guaranteed-accepted first
5266/// token back into the same FreeToken-style live slots ordinary decode uses.
5267/// Rejected draft suffixes must not train or pollute the LRU, so the caller
5268/// deliberately passes only row zero.
5269#[cfg(feature = "gpu")]
5270fn refill_route_slots(l: &Dsv4Layer, cfg: &Dsv4Cfg, pk: &Pack, picks: &[usize]) {
5271    if picks.is_empty() || pk.route_complete() {
5272        return;
5273    }
5274    let quota = {
5275        static Q: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
5276        *Q.get_or_init(|| {
5277            std::env::var("CMF_DSV4_FETCH_MAX")
5278                .ok()
5279                .and_then(|v| v.parse().ok())
5280                .unwrap_or_else(|| crate::gpu_wgpu::dsv4_fetch_defaults().0)
5281        })
5282    };
5283    if quota == 0 {
5284        return;
5285    }
5286    let min_seen = {
5287        static M: std::sync::OnceLock<u16> = std::sync::OnceLock::new();
5288        *M.get_or_init(|| {
5289            std::env::var("CMF_DSV4_FETCH_MIN_SEEN")
5290                .ok()
5291                .and_then(|v| v.parse().ok())
5292                .unwrap_or_else(|| crate::gpu_wgpu::dsv4_fetch_defaults().1)
5293        })
5294    };
5295    let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
5296        return;
5297    };
5298    let mut dynv = pk.dynslots.lock().unwrap();
5299    if dynv.owner.is_empty() {
5300        return;
5301    }
5302    dynv.clock += 1;
5303    let clock = dynv.clock;
5304    if clock % 64 == 0 {
5305        for seen in &mut dynv.seen {
5306            *seen >>= 1;
5307        }
5308    }
5309    for &pick in picks {
5310        if pick >= dynv.seen.len() {
5311            continue;
5312        }
5313        dynv.seen[pick] = dynv.seen[pick].saturating_add(1);
5314        let slot = dynv.remap[pick];
5315        if slot != u32::MAX {
5316            dynv.last[slot as usize] = clock;
5317        }
5318    }
5319    let pack_first = pk.tensors.first().map(|t| t.0).unwrap_or(usize::MAX);
5320    let mut fetched = 0usize;
5321    for &pick in picks {
5322        if fetched >= quota || pick >= dynv.remap.len() {
5323            break;
5324        }
5325        if dynv.remap[pick] != u32::MAX || dynv.seen[pick] < min_seen {
5326            continue;
5327        }
5328        let victim = (0..dynv.owner.len())
5329            .filter(|&slot| dynv.last[slot] != clock)
5330            .min_by_key(|&slot| dynv.last[slot]);
5331        let Some(victim) = victim else { break };
5332        let Some(exp) = l.experts.get(pick) else {
5333            continue;
5334        };
5335        let tensors = (|| {
5336            Some((
5337                exp.w1.model_idx()?,
5338                exp.w3.model_idx()?,
5339                exp.w2.model_idx()?,
5340            ))
5341        })();
5342        let Some(tensors) = tensors else { continue };
5343        let gu_q2 = exp.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP);
5344        if !crate::gpu_wgpu::dsv4_slot_fill(
5345            &model,
5346            pack_first,
5347            victim,
5348            pick,
5349            tensors,
5350            cfg.moe_inter,
5351            cfg.dim,
5352            gu_q2,
5353        ) {
5354            break;
5355        }
5356        let old = dynv.owner[victim] as usize;
5357        if old < dynv.remap.len() {
5358            dynv.remap[old] = u32::MAX;
5359        }
5360        dynv.remap[pick] = victim as u32;
5361        dynv.owner[victim] = pick as u32;
5362        dynv.last[victim] = clock;
5363        dynv.mutated = true;
5364        fetched += 1;
5365    }
5366}
5367
5368/// Grouped output projection for a block. `wo_a` cannot use a plain matmat
5369/// because each group sees a different attention slice; reading a quantized
5370/// row once and applying it to every block position gives the same dot order
5371/// without rereading/dequantizing that row B times.
5372fn o_project_block(
5373    attn: &[f32],
5374    b: usize,
5375    wo_a: &crate::qtensor::QTensor,
5376    wo_b: &crate::qtensor::QTensor,
5377    groups: usize,
5378    lora: usize,
5379    pool: Option<&crate::pool::Pool>,
5380    out: &mut [f32],
5381) {
5382    let attn_len = attn.len() / b;
5383    let per_group = attn_len / groups;
5384    let rows = groups * lora;
5385    let mut mid = vec![0.0f32; b * rows];
5386    let mid_addr = crate::pool::SendMut::new(mid.as_mut_ptr());
5387    let run = |start: usize, end: usize| {
5388        let mut wr = vec![0.0f32; wo_a.cols()];
5389        for r in start..end {
5390            wo_a.row_f32(r, &mut wr);
5391            let group = r / lora;
5392            for bi in 0..b {
5393                let x = &attn
5394                    [bi * attn_len + group * per_group..bi * attn_len + (group + 1) * per_group];
5395                let v = wr.iter().zip(x).map(|(w, x)| w * x).sum();
5396                unsafe { *mid_addr.at(bi * rows + r) = v };
5397            }
5398        }
5399    };
5400    match pool {
5401        Some(p) if rows >= 256 => p.run_rows(rows, &run),
5402        _ => run(0, rows),
5403    }
5404    wo_b.matmat(&mid, b, out, pool);
5405}
5406
5407/// `CMF_DSV4_TRACE=1` prints the hidden state's RMS after each half-block and
5408/// the logits' shape at the end. A 300B model that decodes nonsense gives no
5409/// other handle: this says whether the state grew, collapsed or went
5410/// non-finite, and at which layer — before anyone reaches for a debugger on a
5411/// hundred-gigabyte file.
5412fn no_compressed() -> bool {
5413    static OFF: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5414    *OFF.get_or_init(|| std::env::var("CMF_DSV4_NO_COMPRESSED").is_ok_and(|v| v != "0"))
5415}
5416
5417fn trace_on() -> bool {
5418    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5419    *ON.get_or_init(|| std::env::var("CMF_DSV4_TRACE").is_ok_and(|v| v != "0"))
5420}
5421
5422fn rms_of(v: &[f32]) -> f32 {
5423    if v.is_empty() {
5424        return 0.0;
5425    }
5426    (v.iter().map(|x| x * x).sum::<f32>() / v.len() as f32).sqrt()
5427}
5428
5429#[cfg(feature = "gpu")]
5430fn verify_fp_on(pos: usize) -> bool {
5431    static POS: std::sync::OnceLock<Option<usize>> = std::sync::OnceLock::new();
5432    let want = *POS.get_or_init(|| {
5433        std::env::var("CMF_DSV4_FP_POS")
5434            .ok()
5435            .and_then(|v| v.parse().ok())
5436    });
5437    want == Some(pos)
5438}
5439
5440#[cfg(feature = "gpu")]
5441fn verify_fp(tag: &str, pos: usize, li: usize, state: &[f32]) {
5442    if !verify_fp_on(pos) {
5443        return;
5444    }
5445    let mut fp = 0xcbf29ce484222325u64;
5446    for &x in state {
5447        fp ^= x.to_bits() as u64;
5448        fp = fp.wrapping_mul(0x100000001b3);
5449    }
5450    eprintln!(
5451        "[dsv4-fp] {tag} pos={pos} li={li} fp={fp:016x} rms={:.7} head={:?}",
5452        rms_of(state),
5453        &state[..4.min(state.len())]
5454    );
5455}
5456
5457/// `CMF_DSV4_DUMP=<path>` appends one JSON line per token: the embedding, the
5458/// hyper-connection state after every layer, the folded-and-normed head input
5459/// and the logits. It exists to be diffed against the reference forward on
5460/// the same weights — the numerical parity this port has never had, which at
5461/// toy scale is a few thousand floats and entirely tractable.
5462thread_local! {
5463    /// The attention body's input and output per layer, interleaved.
5464    static BODY: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
5465    /// Experts chosen per layer for the token being decoded — the dump needs
5466    /// them, because two implementations that pick DIFFERENT experts diverge
5467    /// hugely for a reason that is not a bug in either.
5468    static PICKED: std::cell::RefCell<Vec<Vec<usize>>> =
5469        const { std::cell::RefCell::new(Vec::new()) };
5470    /// (layer, chosen experts) in call order, when armed.
5471    static PICK_TALLY: std::cell::RefCell<Option<Vec<(usize, Vec<usize>)>>> =
5472        const { std::cell::RefCell::new(None) };
5473}
5474
5475/// Start recording expert picks. Idempotent; the previous tally is dropped.
5476pub fn pick_tally_arm() {
5477    PICK_TALLY.with(|t| *t.borrow_mut() = Some(Vec::new()));
5478}
5479
5480/// Take what was recorded and stop recording.
5481pub fn pick_tally_take() -> Vec<(usize, Vec<usize>)> {
5482    PICK_TALLY.with(|t| t.borrow_mut().take().unwrap_or_default())
5483}
5484
5485/// How many distinct experts a set of per-token pick lists reaches, and how
5486/// many picks it makes. The ratio is what a batched MoE can hope to save.
5487pub fn tally_unique(picks: &[(usize, Vec<usize>)]) -> (usize, usize) {
5488    // Keyed by (layer, expert). Expert 17 of layer 3 and expert 17 of layer 4
5489    // are different weights, and counting them as one understated the traffic
5490    // a batch has to read — badly for the draft, whose three stages each have
5491    // their own 256.
5492    let mut seen = std::collections::HashSet::new();
5493    let mut total = 0;
5494    for (li, v) in picks {
5495        total += v.len();
5496        for &e in v {
5497            seen.insert((*li, e));
5498        }
5499    }
5500    (seen.len(), total)
5501}
5502
5503fn dump_path() -> Option<&'static str> {
5504    static P: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
5505    P.get_or_init(|| std::env::var("CMF_DSV4_DUMP").ok())
5506        .as_deref()
5507}
5508
5509fn dump_line(json: &str) {
5510    if let Some(p) = dump_path() {
5511        use std::io::Write as _;
5512        if let Ok(mut f) = std::fs::OpenOptions::new()
5513            .create(true)
5514            .append(true)
5515            .open(p)
5516        {
5517            let _ = writeln!(f, "{json}");
5518        }
5519    }
5520}
5521
5522fn vec_json(v: &[f32]) -> String {
5523    let mut s = String::with_capacity(v.len() * 9);
5524    s.push('[');
5525    for (i, x) in v.iter().enumerate() {
5526        if i > 0 {
5527            s.push(',');
5528        }
5529        s.push_str(&format!("{x:.6e}"));
5530    }
5531    s.push(']');
5532    s
5533}
5534
5535/// One token through the whole stack.
5536///
5537/// The hidden state is `hc_mult` copies of a `dim`-vector from the very
5538/// first line to the very last: the embedding is replicated, every layer
5539/// folds/expands around its two halves, and only `hc_head_fold` collapses
5540/// it before the output norm and the head. There is no point in this
5541/// function where an ordinary residual would fit.
5542#[allow(clippy::too_many_arguments)]
5543/// A chunk of prompt tokens. Stage one of the batched prefill (see
5544/// docs/DSV4_PREFILL.md): the walk itself, with the head skipped for every
5545/// token but the last.
5546///
5547/// Prefill costs `len × per-token` today, and on a 2500-token prompt that is
5548/// a minute and a half before the first word. The stages that follow batch
5549/// the weight reads — which is where the nine-fold gap to the bandwidth
5550/// floor lives — but this one is the scaffolding they hang on, and it
5551/// already stops computing 129 280 logits for tokens nobody asks about.
5552#[allow(clippy::too_many_arguments)]
5553/// `CMF_DSV4_BATCH=N` — how many prompt tokens go through the card in one
5554/// submission. 1 keeps the walk. The chunk still bounds it: a batch never
5555/// spans two chunks, so cancellation stays as responsive as it was.
5556fn batch_prefill() -> usize {
5557    static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
5558    *N.get_or_init(|| {
5559        std::env::var("CMF_DSV4_BATCH")
5560            .ok()
5561            .and_then(|v| v.parse::<usize>().ok())
5562            .filter(|&n| (1..=32).contains(&n))
5563            // A partial expert pack used to make the device-chain batch
5564            // decline, so the conservative default was one.  The exact host
5565            // batch below now handles that geometry: routing still spans all
5566            // experts and every cold winner is computed from the mmap-backed
5567            // checkpoint.  Eight amortises expert reads without making a long
5568            // prompt's cancellation granularity coarse.
5569            .unwrap_or(8)
5570    })
5571}
5572
5573/// The prompt as batches instead of a walk, when every layer will take one.
5574///
5575/// Refuses before touching any state, never half way: the caller's fallback
5576/// is the per-token walk, and a batch that advanced the caches and then gave
5577/// up would have them advanced twice. So everything that can decline is asked
5578/// first, and after the first dispatch the only outcomes are success and a
5579/// hard failure.
5580///
5581/// Hash layers are the one shape it cannot take: their expert list is forced
5582/// by the TOKEN's id and the layer description carries one list, not one per
5583/// token. The release has three of them (0, 1, 2); a file without them
5584/// batches the whole stack.
5585#[allow(clippy::too_many_arguments)]
5586fn forward_chunk_batched(
5587    g: &Dsv4Globals,
5588    layers: &[Dsv4Layer],
5589    cfg: &Dsv4Cfg,
5590    st: &mut Dsv4State,
5591    ids: &[u32],
5592    pos0: usize,
5593    inv_freq: &[f32],
5594    pool: Option<&crate::pool::Pool>,
5595    logits: &mut Vec<f32>,
5596    want_logits: bool,
5597) -> bool {
5598    #[cfg(not(feature = "gpu"))]
5599    {
5600        let _ = (
5601            g,
5602            layers,
5603            cfg,
5604            st,
5605            ids,
5606            pos0,
5607            inv_freq,
5608            pool,
5609            logits,
5610            want_logits,
5611        );
5612        false
5613    }
5614    #[cfg(feature = "gpu")]
5615    {
5616        let b = ids.len();
5617        // Complete packs form the fused device prefix.  Partial packs belong
5618        // to the exact causal tail: that tail routes over all experts and
5619        // completes cold winners, so gpu_end == 0 is a useful (and common on
5620        // smaller cards) batch rather than a reason to walk token by token.
5621        let gpu_end = st
5622            .dev_set
5623            .iter()
5624            .enumerate()
5625            .position(|(li, &on)| {
5626                !on || pack_for(&layers[li], cfg, li)
5627                    .is_none_or(|p| !p.route_complete())
5628            })
5629            .unwrap_or(st.dev_set.len());
5630        let why = if b < 2 {
5631            "токенов меньше двух"
5632        } else if !chain_enabled() {
5633            "цепочка выключена"
5634        } else if !st.dev_owned {
5635            "карта ещё не владеет состоянием"
5636        } else if st.dev_set.len() != layers.len() {
5637            "набор слоёв ещё не зафиксирован"
5638        } else if st.dev_set[gpu_end.min(st.dev_set.len())..]
5639                .iter()
5640                .enumerate()
5641                .any(|(i, &on)| on && !st.partial_set.get(gpu_end + i).copied().unwrap_or(false))
5642        {
5643            "слои на карте не образуют префикс"
5644        } else {
5645            ""
5646        };
5647        if !why.is_empty() {
5648            static SAID: std::sync::Once = std::sync::Once::new();
5649            SAID.call_once(|| tracing::warn!("dsv4: пакет отказал — {why}"));
5650            return false;
5651        }
5652        let (hc, dim) = (cfg.hc_mult, cfg.dim);
5653        let mut emb = vec![0.0f32; dim];
5654        let mut states = vec![0.0f32; b * hc * dim];
5655        for (t, &id) in ids.iter().enumerate() {
5656            let mut state = vec![0.0f32; hc * dim];
5657            g.embed.row_f32(id as usize, &mut emb);
5658            for j in 0..hc {
5659                state[j * dim..(j + 1) * dim].copy_from_slice(&emb);
5660            }
5661            let (folded, post0, comb0) = hc_fold_norm(
5662                &state,
5663                &layers[0].hc_attn_fn,
5664                &layers[0].hc_attn_scale,
5665                &layers[0].hc_attn_base,
5666                &layers[0].attn_norm,
5667                cfg,
5668                pool,
5669            );
5670            let mut qn0 = vec![0.0f32; layers[0].wq_a.rows()];
5671            layers[0].wq_a.matvec(&folded, &mut qn0, pool);
5672            rms_weighted(&mut qn0, &layers[0].q_norm, cfg.norm_eps);
5673            if !crate::gpu_wgpu::dsv4_state_write_t(&state, t)
5674                || !crate::gpu_wgpu::dsv4_hc_write_t(&post0, &comb0, t)
5675                || !crate::gpu_wgpu::dsv4_chain_seed_t(&folded, &qn0, t)
5676                || !crate::gpu_wgpu::dsv4_chain_seed_bt(t, b, &state, &post0, &comb0, &folded, &qn0)
5677            {
5678                return false;
5679            }
5680            states[t * hc * dim..(t + 1) * hc * dim].copy_from_slice(&state);
5681        }
5682        let run: Vec<usize> = (0..gpu_end).collect();
5683        let mut folded = Vec::new();
5684        st.pos = pos0;
5685        if gpu_end > 0 {
5686            if !dsv4_chain_run(
5687                layers,
5688                &run,
5689                cfg,
5690                g,
5691                st,
5692                *ids.last().unwrap(),
5693                &mut folded,
5694                Some(&mut states),
5695                b,
5696                ids,
5697                true,
5698                pool,
5699            ) {
5700                return false;
5701            }
5702        }
5703        // Finish the trailing host layers in causal token order. Their KV
5704        // caches are host-owned, while the device prefix advanced its own
5705        // caches inside the one submission above. On the release this loop
5706        // is exactly layer 42; keeping it general makes smaller VRAM budgets
5707        // correct as long as the resident layers remain one prefix.
5708        let mut scratch = HcScratch::new(cfg);
5709        host_tail_walk_batch(
5710            g,
5711            layers,
5712            cfg,
5713            st,
5714            gpu_end,
5715            &mut states,
5716            ids,
5717            pos0,
5718            b,
5719            inv_freq,
5720            &mut scratch,
5721            pool,
5722            None,
5723        );
5724        st.pos = pos0 + b;
5725        // Said once. A gate that compares a batched prompt against a walked
5726        // one proves nothing if the batch quietly declined — the numbers match
5727        // because the same code produced both. This line is what tells the
5728        // two apart.
5729        {
5730            static SAID: std::sync::Once = std::sync::Once::new();
5731            SAID.call_once(|| tracing::warn!("dsv4: префилл пакетами по {b}"));
5732        }
5733        // Only the last token's logits are read; the rest of the chunk exists
5734        // to fill the caches. The head consumes the hyper-connection state,
5735        // not the chain's intermediate fold — skipping this final learned
5736        // fold used to make a full-device batch fast and wrong.
5737        if want_logits {
5738            let last = &states[(b - 1) * hc * dim..b * hc * dim];
5739            let mut h = vec![0.0f32; dim];
5740            hc_head_fold(
5741                last,
5742                &g.hc_head_fn,
5743                g.hc_head_scale,
5744                &g.hc_head_base,
5745                cfg,
5746                pool,
5747                &mut h,
5748            );
5749            rms_weighted(&mut h, &g.norm, cfg.norm_eps);
5750            logits.resize(cfg.vocab, 0.0);
5751            g.head.matvec(&h, logits, pool);
5752        } else {
5753            logits.clear();
5754        }
5755        true
5756    }
5757}
5758
5759/// Everything a speculative verify must be able to put back.
5760///
5761/// Device caches roll back by restore-then-replay: the shadow puts the
5762/// window rings and compressor streams where they were BEFORE the pass, and
5763/// the replay re-appends the accepted tokens' state from the hidden inputs
5764/// the pass retained. Append-only regions roll back by count. Host-owned
5765/// tail layers roll back by clone-and-rewalk.
5766#[cfg(feature = "gpu")]
5767pub struct Dsv4SpecTxn {
5768    pos0: usize,
5769    batch: usize,
5770    pub(crate) gpu_end: usize,
5771    dev_filled: Vec<usize>,
5772    dev_n_comp: Vec<usize>,
5773    dev_n_ix: Vec<usize>,
5774    host: Vec<(usize, HostLayerSnap)>,
5775    /// Per host layer, per verified token: the layer's state right after
5776    /// that token's attention — what a rollback restores INSTEAD of
5777    /// re-walking the tail it already walked (the values are identical;
5778    /// only the side effects were ever needed).
5779    host_steps: Vec<(usize, Vec<HostLayerSnap>)>,
5780    /// Every token's hyper-connection state as it left the device prefix,
5781    /// BEFORE the host tail walked (and mutated) anything: the rewalk's
5782    /// input, and the head's.
5783    pub states: Vec<f32>,
5784    shadow: Option<crate::gpu_wgpu::Dsv4SpecShadow>,
5785}
5786
5787#[cfg(feature = "gpu")]
5788struct HostLayerSnap {
5789    window: Vec<f32>,
5790    compressed: Vec<f32>,
5791    index_kv: Vec<f32>,
5792    pending_kv: Vec<f32>,
5793    pending_score: Vec<f32>,
5794    prev_kv: Vec<f32>,
5795    prev_score: Vec<f32>,
5796    pending_ix_kv: Vec<f32>,
5797    pending_ix_score: Vec<f32>,
5798    prev_ix_kv: Vec<f32>,
5799    prev_ix_score: Vec<f32>,
5800}
5801
5802#[cfg(feature = "gpu")]
5803fn host_snap(st: &Dsv4State, li: usize) -> HostLayerSnap {
5804    HostLayerSnap {
5805        window: st.window[li].clone(),
5806        compressed: st.compressed[li].clone(),
5807        index_kv: st.index_kv[li].clone(),
5808        pending_kv: st.pending_kv[li].clone(),
5809        pending_score: st.pending_score[li].clone(),
5810        prev_kv: st.prev_kv[li].clone(),
5811        prev_score: st.prev_score[li].clone(),
5812        pending_ix_kv: st.pending_ix_kv[li].clone(),
5813        pending_ix_score: st.pending_ix_score[li].clone(),
5814        prev_ix_kv: st.prev_ix_kv[li].clone(),
5815        prev_ix_score: st.prev_ix_score[li].clone(),
5816    }
5817}
5818
5819#[cfg(feature = "gpu")]
5820fn host_restore(st: &mut Dsv4State, li: usize, s: &HostLayerSnap) {
5821    st.window[li] = s.window.clone();
5822    st.compressed[li] = s.compressed.clone();
5823    st.index_kv[li] = s.index_kv.clone();
5824    st.pending_kv[li] = s.pending_kv.clone();
5825    st.pending_score[li] = s.pending_score.clone();
5826    st.prev_kv[li] = s.prev_kv.clone();
5827    st.prev_score[li] = s.prev_score.clone();
5828    st.pending_ix_kv[li] = s.pending_ix_kv.clone();
5829    st.pending_ix_score[li] = s.pending_ix_score.clone();
5830    st.prev_ix_kv[li] = s.prev_ix_kv.clone();
5831    st.prev_ix_score[li] = s.prev_ix_score.clone();
5832}
5833
5834/// One host-tail walk of token `t`'s state through layers `gpu_end..`,
5835/// mutating `state` in place and the layers' host caches. Exactly the loop
5836/// the batch runs, factored so the verify can re-run it for accepted tokens.
5837#[cfg(feature = "gpu")]
5838#[allow(clippy::too_many_arguments)]
5839fn host_tail_walk(
5840    g: &Dsv4Globals,
5841    layers: &[Dsv4Layer],
5842    cfg: &Dsv4Cfg,
5843    st: &mut Dsv4State,
5844    gpu_end: usize,
5845    state: &mut [f32],
5846    token_id: u32,
5847    pos: usize,
5848    inv_freq: &[f32],
5849    scratch: &mut HcScratch,
5850    pool: Option<&crate::pool::Pool>,
5851) {
5852    st.pos = pos;
5853    for (li, l) in layers.iter().enumerate().skip(gpu_end) {
5854        let freqs = if l.compressor.is_some() {
5855            &g.inv_freq_compress
5856        } else {
5857            &g.inv_freq_window
5858        };
5859        let freqs = if freqs.is_empty() {
5860            inv_freq
5861        } else {
5862            freqs.as_slice()
5863        };
5864        hc_block(
5865            state,
5866            &l.hc_attn_fn,
5867            &l.hc_attn_scale,
5868            &l.hc_attn_base,
5869            &l.attn_norm,
5870            cfg,
5871            scratch,
5872            pool,
5873            |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
5874        );
5875        hc_block(
5876            state,
5877            &l.hc_ffn_fn,
5878            &l.hc_ffn_scale,
5879            &l.hc_ffn_base,
5880            &l.ffn_norm,
5881            cfg,
5882            scratch,
5883            pool,
5884            |f, o| {
5885                if host_cpu_moe() {
5886                    crate::gpu::cpu_scope(|| moe_step(f, l, cfg, token_id, li, pool, o))
5887                } else {
5888                    moe_step(f, l, cfg, token_id, li, pool, o)
5889                }
5890            },
5891        );
5892        dspark_note(li, state, cfg);
5893    }
5894}
5895
5896/// The host tail for a whole batch: attention stays causal per token (its
5897/// window mutates), the MoE half runs through the block-grouped path — the
5898/// same accumulation order as the position walk, which the block tests pin
5899/// bit for bit. This is the verify's tail; the single-token paths keep
5900/// `hc_block`.
5901#[cfg(feature = "gpu")]
5902#[allow(clippy::too_many_arguments)]
5903fn host_tail_walk_batch(
5904    g: &Dsv4Globals,
5905    layers: &[Dsv4Layer],
5906    cfg: &Dsv4Cfg,
5907    st: &mut Dsv4State,
5908    gpu_end: usize,
5909    states: &mut [f32],
5910    ids: &[u32],
5911    pos0: usize,
5912    b: usize,
5913    inv_freq: &[f32],
5914    scratch: &mut HcScratch,
5915    pool: Option<&crate::pool::Pool>,
5916    mut steps: Option<&mut Vec<(usize, Vec<HostLayerSnap>)>>,
5917) {
5918    let (hc, dim) = (cfg.hc_mult, cfg.dim);
5919    let mix_hc = (2 + hc) * hc;
5920    let mut folds = vec![0.0f32; b * dim];
5921    let mut mo = vec![0.0f32; b * dim];
5922    let mut posts = vec![0.0f32; b * hc];
5923    let mut combs = vec![0.0f32; b * hc * hc];
5924    let mut resid = vec![0.0f32; b * hc * dim];
5925    let spec_time = {
5926        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5927        *ON.get_or_init(|| std::env::var("CMF_DSV4_SPEC_TIME").is_ok_and(|v| v != "0"))
5928    };
5929    for (li, l) in layers.iter().enumerate().skip(gpu_end) {
5930        let t_attn = std::time::Instant::now();
5931        let freqs = if l.compressor.is_some() {
5932            &g.inv_freq_compress
5933        } else {
5934            &g.inv_freq_window
5935        };
5936        let freqs = if freqs.is_empty() {
5937            inv_freq
5938        } else {
5939            freqs.as_slice()
5940        };
5941        for t in 0..b {
5942            st.pos = pos0 + t;
5943            let state = &mut states[t * hc * dim..(t + 1) * hc * dim];
5944            hc_block(
5945                state,
5946                &l.hc_attn_fn,
5947                &l.hc_attn_scale,
5948                &l.hc_attn_base,
5949                &l.attn_norm,
5950                cfg,
5951                scratch,
5952                pool,
5953                |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
5954            );
5955            if let Some(steps) = steps.as_mut() {
5956                match steps.iter_mut().find(|(l, _)| *l == li) {
5957                    Some((_, v)) => v.push(host_snap(st, li)),
5958                    None => steps.push((li, vec![host_snap(st, li)])),
5959                }
5960            }
5961        }
5962        let t_glue = std::time::Instant::now();
5963        for t in 0..b {
5964            let state = &states[t * hc * dim..(t + 1) * hc * dim];
5965            hc_mixes(
5966                state,
5967                &l.hc_ffn_fn,
5968                mix_hc,
5969                cfg.norm_eps,
5970                pool,
5971                &mut scratch.mixes,
5972            );
5973            hc_split_sinkhorn(
5974                &scratch.mixes,
5975                &l.hc_ffn_scale,
5976                &l.hc_ffn_base,
5977                hc,
5978                cfg.hc_sinkhorn_iters,
5979                cfg.hc_eps,
5980                &mut scratch.pre,
5981                &mut posts[t * hc..(t + 1) * hc],
5982                &mut combs[t * hc * hc..(t + 1) * hc * hc],
5983            );
5984            let fold = &mut folds[t * dim..(t + 1) * dim];
5985            hc_fold(state, &scratch.pre, hc, dim, fold);
5986            let ms = fold.iter().map(|v| v * v).sum::<f32>() / dim as f32;
5987            let inv = 1.0 / (ms + cfg.norm_eps).sqrt();
5988            for (v, w) in fold.iter_mut().zip(&l.ffn_norm) {
5989                *v = *v * inv * w;
5990            }
5991            resid[t * hc * dim..(t + 1) * hc * dim]
5992                .copy_from_slice(&states[t * hc * dim..(t + 1) * hc * dim]);
5993        }
5994        let t_moe = std::time::Instant::now();
5995        // A tail layer with a device expert pack (partial or full) runs its
5996        // hot winners on the card per token and completes the cold ones on
5997        // the host — the same exact split the partial walk uses. Default on
5998        // (measured: the tail fell 27.4 → 18.2 ms of the verify round);
5999        // `CMF_DSV4_TAIL_PACK=0` restores the batched host block.
6000        let tail_pack = {
6001            static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6002            *ON.get_or_init(|| {
6003                std::env::var("CMF_DSV4_TAIL_PACK")
6004                    .map(|v| v != "0")
6005                    .unwrap_or(true)
6006            })
6007        };
6008        let mut packed_done = false;
6009        if tail_pack && pack_for(l, cfg, li).is_some() {
6010            packed_done = true;
6011            for t in 0..b {
6012                let f = &folds[t * dim..(t + 1) * dim];
6013                let forced = l.tid2eid.as_ref().map(|tbl| {
6014                    hash_route(tbl, cfg.vocab, cfg.top_k, ids.get(t).copied().unwrap_or(0))
6015                });
6016                let o = &mut mo[t * dim..(t + 1) * dim];
6017                match moe_frame(f, l, cfg, li, &[], forced.as_deref(), pool, None, None, o) {
6018                    Some((cold_sum, n)) => {
6019                        if n > 0 {
6020                            for (od, cd) in o.iter_mut().zip(cold_sum.iter()) {
6021                                *od += cd;
6022                            }
6023                        }
6024                    }
6025                    None => {
6026                        packed_done = false;
6027                        break;
6028                    }
6029                }
6030            }
6031        }
6032        if !packed_done {
6033            if host_cpu_moe() {
6034                crate::gpu::cpu_scope(|| moe_step_block(&folds, b, l, cfg, ids, li, pool, &mut mo));
6035            } else {
6036                moe_step_block(&folds, b, l, cfg, ids, li, pool, &mut mo);
6037            }
6038        }
6039        let t_exp = std::time::Instant::now();
6040        for t in 0..b {
6041            let state = &mut states[t * hc * dim..(t + 1) * hc * dim];
6042            hc_expand(
6043                &mo[t * dim..(t + 1) * dim],
6044                &resid[t * hc * dim..(t + 1) * hc * dim],
6045                &posts[t * hc..(t + 1) * hc],
6046                &combs[t * hc * hc..(t + 1) * hc * hc],
6047                hc,
6048                dim,
6049                state,
6050            );
6051            dspark_note(li, state, cfg);
6052        }
6053        if spec_time {
6054            eprintln!(
6055                "хвост слоя {li}: attn {:.1} мс, клей {:.1}, moe {:.1}, expand {:.1}",
6056                (t_glue - t_attn).as_secs_f64() * 1e3,
6057                (t_moe - t_glue).as_secs_f64() * 1e3,
6058                (t_exp - t_moe).as_secs_f64() * 1e3,
6059                t_exp.elapsed().as_secs_f64() * 1e3,
6060            );
6061        }
6062    }
6063}
6064
6065/// One exact token-axis layer over a partial expert pack.  The device runs
6066/// attention, routing over all experts, the resident MoE rows and the
6067/// hyper-connection join once for the whole batch.  Cold winners are grouped
6068/// by expert on the host, corrected into the returned state, and only then is
6069/// the next dependent layer allowed to start.
6070#[cfg(feature = "gpu")]
6071#[allow(clippy::too_many_arguments)]
6072fn partial_layer_batch(
6073    g: &Dsv4Globals,
6074    layers: &[Dsv4Layer],
6075    cfg: &Dsv4Cfg,
6076    st: &mut Dsv4State,
6077    li: usize,
6078    states: &mut [f32],
6079    ids: &[u32],
6080    pos0: usize,
6081    b: usize,
6082    pool: Option<&crate::pool::Pool>,
6083) -> bool {
6084    let l = &layers[li];
6085    let (hc, dim, hd) = (cfg.hc_mult, cfg.dim, cfg.head_dim);
6086    if states.len() < b * hc * dim || ids.len() < b {
6087        return false;
6088    }
6089    let Some(pk) = pack_for(l, cfg, li) else {
6090        return false;
6091    };
6092    if pk.route_complete() {
6093        return false;
6094    }
6095    let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
6096        return false;
6097    };
6098    let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b), Some(wkv)) = (
6099        l.wq_a.model_idx(),
6100        l.wq_b.model_idx(),
6101        l.wo_a.model_idx(),
6102        l.wo_b.model_idx(),
6103        l.wkv.model_idx(),
6104    ) else {
6105        return false;
6106    };
6107    let comp = match &l.compressor {
6108        None => None,
6109        Some(cp) => {
6110            let (Some(a), Some(bx)) = (cp.wkv.model_idx(), cp.wgate.model_idx()) else {
6111                return false;
6112            };
6113            Some((
6114                crate::gpu_wgpu::Dsv4CompW {
6115                    wkv: a,
6116                    wgate: bx,
6117                    norm: &cp.norm,
6118                    ape: &cp.ape,
6119                },
6120                crate::gpu_wgpu::Dsv4CompGeom {
6121                    width: cp.wkv.rows(),
6122                    hidden: dim,
6123                    ratio: cp.ratio,
6124                    overlap: cp.overlap,
6125                    rope_dim: cfg.rope_head_dim,
6126                    eps: cfg.norm_eps,
6127                },
6128            ))
6129        }
6130    };
6131    let ix = match &l.indexer {
6132        None => None,
6133        Some(ixr) => {
6134            let cp = &ixr.compressor;
6135            let (Some(a), Some(bx), Some(qb), Some(wp)) = (
6136                cp.wkv.model_idx(),
6137                cp.wgate.model_idx(),
6138                ixr.wq_b.model_idx(),
6139                ixr.weights_proj.model_idx(),
6140            ) else {
6141                return false;
6142            };
6143            let ih = ixr.weights_proj.rows();
6144            Some((
6145                crate::gpu_wgpu::Dsv4CompW {
6146                    wkv: a,
6147                    wgate: bx,
6148                    norm: &cp.norm,
6149                    ape: &cp.ape,
6150                },
6151                crate::gpu_wgpu::Dsv4CompGeom {
6152                    width: cp.wkv.rows(),
6153                    hidden: dim,
6154                    ratio: cp.ratio,
6155                    overlap: cp.overlap,
6156                    rope_dim: cfg.rope_head_dim,
6157                    eps: cfg.norm_eps,
6158                },
6159                crate::gpu_wgpu::Dsv4IxW {
6160                    wq_b: qb,
6161                    weights_proj: wp,
6162                },
6163                crate::gpu_wgpu::Dsv4IxGeom {
6164                    ih,
6165                    idim: ixr.wq_b.rows() / ih.max(1),
6166                    q_lora: cfg.q_lora_rank,
6167                    hidden: dim,
6168                    rope_dim: cfg.rope_head_dim,
6169                    eps: cfg.norm_eps,
6170                    top_k: cfg.index_topk,
6171                    window: cfg.window,
6172                },
6173            ))
6174        }
6175    };
6176    let ew_c = comp.as_ref().map_or(0, |(_, cg)| {
6177        if cg.overlap { cg.width / 2 } else { cg.width }
6178    });
6179    let ew_i = ix.as_ref().map_or(0, |(_, cg, _, _)| {
6180        if cg.overlap { cg.width / 2 } else { cg.width }
6181    });
6182    let comp_extra = comp
6183        .as_ref()
6184        .map_or(0, |(_, cg)| b.div_ceil(cg.ratio.max(1)));
6185    let need = cfg.window * hd
6186        + (st.dev_n_comp[li] + comp_extra + 1) * ew_c.max(1)
6187        + (b + 1) * hd;
6188    if !crate::gpu_wgpu::dsv4_cache_ensure(st.kv_id, li, need.next_power_of_two()) {
6189        return false;
6190    }
6191    let base = crate::gpu_wgpu::Dsv4Prep {
6192        wkv,
6193        kv_norm: &l.kv_norm,
6194        comp,
6195        ix,
6196        filled: st.dev_filled[li],
6197        window: cfg.window,
6198        n_comp: st.dev_n_comp[li],
6199        n_ix: st.dev_n_ix[li],
6200        comp_dst_off: cfg.window * hd + st.dev_n_comp[li] * ew_c,
6201        ix_dst_off: st.dev_n_ix[li] * ew_i,
6202        idx_cap: cfg.window
6203            + if l.indexer.is_some() {
6204                cfg.index_topk
6205            } else {
6206                st.dev_n_comp[li] + comp_extra + 1
6207            },
6208    };
6209    let mut preps = Vec::with_capacity(b);
6210    for t in 0..b {
6211        let mut p = base.clone();
6212        p.filled = (base.filled + t).min(base.window);
6213        let advanced = |ratio: usize| -> usize {
6214            if ratio == 0 {
6215                0
6216            } else {
6217                (0..t).filter(|k| (pos0 + k + 1) % ratio == 0).count()
6218            }
6219        };
6220        if let Some((_, cg)) = base.comp.as_ref() {
6221            p.n_comp = base.n_comp + advanced(cg.ratio);
6222            p.comp_dst_off = base.comp_dst_off + (p.n_comp - base.n_comp) * ew_c;
6223        }
6224        if let Some((_, cg, _, _)) = base.ix.as_ref() {
6225            p.n_ix = base.n_ix + advanced(cg.ratio);
6226            p.ix_dst_off = base.ix_dst_off + (p.n_ix - base.n_ix) * ew_i;
6227        }
6228        preps.push(p);
6229    }
6230
6231    // Every row enters with an exact host state because the previous partial
6232    // layer was corrected before returning.  Seed the token-axis slots and
6233    // the q-LoRA vector the batched attention consumes.
6234    for t in 0..b {
6235        let state = &states[t * hc * dim..(t + 1) * hc * dim];
6236        let (fold, post, comb) = hc_fold_norm(
6237            state,
6238            &l.hc_attn_fn,
6239            &l.hc_attn_scale,
6240            &l.hc_attn_base,
6241            &l.attn_norm,
6242            cfg,
6243            pool,
6244        );
6245        let mut qn = vec![0.0f32; cfg.q_lora_rank];
6246        l.wq_a.matvec(&fold, &mut qn, pool);
6247        rms_weighted(&mut qn, &l.q_norm, cfg.norm_eps);
6248        if !crate::gpu_wgpu::dsv4_chain_seed_bt(t, b, state, &post, &comb, &fold, &qn) {
6249            return false;
6250        }
6251    }
6252    let forced_rows: Vec<Option<Vec<usize>>> = ids
6253        .iter()
6254        .take(b)
6255        .map(|&id| {
6256            l.tid2eid
6257                .as_ref()
6258                .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, id))
6259        })
6260        .collect();
6261    let dynv = pk.dynslots.lock().unwrap();
6262    let w = crate::gpu_wgpu::Dsv4LayerW {
6263        attn: crate::gpu_wgpu::Dsv4AttnW {
6264            wq_a,
6265            wq_b,
6266            wo_a,
6267            wo_b,
6268            q_norm: &l.q_norm,
6269            sink: &l.attn_sink,
6270        },
6271        moe: crate::gpu_wgpu::Dsv4MoeW {
6272            router: &[],
6273            experts: &pk.tensors,
6274            logits: &[],
6275            bias: pk.bias.as_deref(),
6276            mask: pk.mask.as_deref(),
6277            forced: None,
6278            remap: Some(&dynv.remap),
6279        },
6280        hc_ffn_fn: &l.hc_ffn_fn,
6281        hc_ffn_scale: &l.hc_ffn_scale,
6282        hc_ffn_base: &l.hc_ffn_base,
6283        // Stop after this layer's state.  The next-layer fold must see the
6284        // cold-corrected state, not the resident-only state on the card.
6285        hc_next_fn: None,
6286        hc_next_scale: &l.hc_attn_scale,
6287        hc_next_base: &l.hc_attn_base,
6288        ffn_norm: &l.ffn_norm,
6289        next_norm: &l.attn_norm,
6290        next_q_norm: &l.q_norm,
6291        next_wq_a: None,
6292        router: &pk.router,
6293    };
6294    let geom = crate::gpu_wgpu::Dsv4LayerGeom {
6295        attn: crate::gpu_wgpu::Dsv4AttnGeom {
6296            dim,
6297            nh: cfg.n_heads,
6298            hd,
6299            rd: cfg.rope_head_dim,
6300            q_lora: cfg.q_lora_rank,
6301            o_lora: cfg.o_lora_rank,
6302            o_groups: cfg.o_groups,
6303            eps: cfg.norm_eps,
6304            scale: (hd as f32).powf(-0.5),
6305        },
6306        moe: crate::gpu_wgpu::Dsv4MoeGeom {
6307            hidden: dim,
6308            inter: cfg.moe_inter,
6309            top_k: cfg.top_k,
6310            route_scale: cfg.route_scale,
6311            swiglu_limit: cfg.swiglu_limit,
6312            gu_q2: l
6313                .experts
6314                .first()
6315                .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
6316        },
6317        hc,
6318        hc_eps: cfg.hc_eps,
6319        sinkhorn_iters: cfg.hc_sinkhorn_iters,
6320    };
6321    let freqs = if l.compressor.is_some() {
6322        g.inv_freq_compress.as_slice()
6323    } else {
6324        g.inv_freq_window.as_slice()
6325    };
6326    let Some(mut got) = crate::gpu_wgpu::dsv4_layer_batch_partial(
6327        &model,
6328        &w,
6329        geom,
6330        st.kv_id,
6331        li,
6332        b,
6333        &preps,
6334        Some(&forced_rows),
6335        freqs,
6336        pos0,
6337    ) else {
6338        return false;
6339    };
6340    drop(dynv);
6341    let mut cold_sum = vec![0.0f32; b * dim];
6342    cold_step_block(&got.cold_x, b, l, cfg, &got.cold, pool, &mut cold_sum);
6343    for t in 0..b {
6344        let state = &mut got.states[t * hc * dim..(t + 1) * hc * dim];
6345        let post = &got.posts[t * hc..(t + 1) * hc];
6346        let cold = &cold_sum[t * dim..(t + 1) * dim];
6347        for j in 0..hc {
6348            for d in 0..dim {
6349                state[j * dim + d] += post[j] * cold[d];
6350            }
6351        }
6352    }
6353    states[..b * hc * dim].copy_from_slice(&got.states);
6354    if !crate::gpu_wgpu::dsv4_spec_cap_write_host(li, b, hc * dim, &got.states) {
6355        return false;
6356    }
6357    if let Some(first_route) = got.routed.first() {
6358        refill_route_slots(l, cfg, &pk, first_route);
6359    }
6360    for t in 0..b {
6361        let pos = pos0 + t;
6362        st.dev_filled[li] = (st.dev_filled[li] + 1).min(cfg.window);
6363        if let Some((_, cg, ..)) = base.ix.as_ref() {
6364            if (pos + 1) % cg.ratio == 0 {
6365                st.dev_n_ix[li] += 1;
6366            }
6367        }
6368        if let Some((_, cg)) = base.comp.as_ref() {
6369            if (pos + 1) % cg.ratio == 0 {
6370                st.dev_n_comp[li] += 1;
6371                note_compressed(st.kv_id, li, st.dev_n_comp[li]);
6372            }
6373        }
6374    }
6375    true
6376}
6377
6378/// A speculative verify pass: run `ids` (the committed next token followed
6379/// by draft proposals) at positions `pos0..pos0+B` through the trunk in one
6380/// batched submission, WITHOUT giving up the ability to roll back, and
6381/// return every position's greedy answer. The caller decides the accepted
6382/// prefix and calls [`dsv4_spec_finish`], which either keeps everything
6383/// (`accepted == B`) or restores-and-replays to the accepted length.
6384///
6385/// `logits_out` takes B rows of vocab logits, `argmax_out` their argmaxes.
6386#[cfg(feature = "gpu")]
6387#[allow(clippy::too_many_arguments)]
6388pub fn dsv4_verify_chunk(
6389    g: &Dsv4Globals,
6390    layers: &[Dsv4Layer],
6391    cfg: &Dsv4Cfg,
6392    st: &mut Dsv4State,
6393    ids: &[u32],
6394    pos0: usize,
6395    inv_freq: &[f32],
6396    pool: Option<&crate::pool::Pool>,
6397    cap_targets: &[usize],
6398    argmax_out: &mut Vec<u32>,
6399    logits_out: &mut Vec<f32>,
6400    walked_out: &mut Vec<f32>,
6401) -> Option<Dsv4SpecTxn> {
6402    let b = ids.len();
6403    // The complete prefix still runs as one fused chain.  Immediately after
6404    // it, contiguous PARTIAL packs can now stay on the device too: each one
6405    // is corrected with its cold experts before the next layer is seeded.
6406    let complete_end = st
6407        .dev_set
6408        .iter()
6409        .enumerate()
6410        .position(|(li, &on)| {
6411            !on || pack_for(&layers[li], cfg, li)
6412                .is_none_or(|p| !p.route_complete())
6413        })
6414        .unwrap_or(st.dev_set.len());
6415    fn partial_batch_on() -> bool {
6416        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6417        *ON.get_or_init(|| {
6418            std::env::var("CMF_DSV4_PARTIAL_BATCH")
6419                .map(|v| v != "0")
6420                .unwrap_or(true)
6421        })
6422    }
6423    let device_end = if partial_batch_on() {
6424        (complete_end..layers.len())
6425            .take_while(|&li| {
6426                st.dev_set.get(li).copied().unwrap_or(false)
6427                    && st.partial_set.get(li).copied().unwrap_or(false)
6428                    && pack_for(&layers[li], cfg, li).is_some_and(|p| !p.route_complete())
6429            })
6430            .last()
6431            .map_or(complete_end, |li| li + 1)
6432    } else {
6433        complete_end
6434    };
6435    // A PARTIAL layer after a host gap is allowed to walk in the host tail.
6436    // A FULL device layer there would violate the contiguous-prefix contract.
6437    let full_beyond = st.dev_set[device_end.min(st.dev_set.len())..]
6438        .iter()
6439        .enumerate()
6440        .any(|(i, &on)| on && !st.partial_set.get(device_end + i).copied().unwrap_or(false));
6441    // `CMF_DSV4_HOST_VERIFY=1` lets the verify run with NO device prefix:
6442    // every layer walks in the host tail, batched — which is where a
6443    // many-core host amortises the weight read and the unpack across the
6444    // draft (the whole point of a batched verify). Off, a partial layer 0
6445    // (dynamic-slot packs) silently priced the entire speculation at zero:
6446    // 625 drafted, 0 verified, all cost and no candidate.
6447    fn host_verify_on() -> bool {
6448        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6449        *ON.get_or_init(|| {
6450            std::env::var("CMF_DSV4_HOST_VERIFY")
6451                .map(|v| v != "0")
6452                // On a small card every layer can have a useful partial pack
6453                // while no layer has a complete one.  The exact batched tail
6454                // is specifically built for that shape; silently pricing
6455                // speculation at zero here defeated the automatic fast path.
6456                .unwrap_or(true)
6457        })
6458    }
6459    if b < 2
6460        || !chain_enabled()
6461        || !st.dev_owned
6462        || st.dev_set.len() != layers.len()
6463        || (device_end == 0 && !host_verify_on())
6464        || full_beyond
6465    {
6466        return None;
6467    }
6468    let (hc, dim, hd) = (cfg.hc_mult, cfg.dim, cfg.head_dim);
6469    // ── the transaction ──
6470    let metas: Vec<(usize, usize, usize, usize)> = (0..device_end)
6471        .map(|li| (li, hd, cfg.window, st.dev_filled[li]))
6472        .collect();
6473    let shadow = crate::gpu_wgpu::dsv4_spec_shadow(st.kv_id, &metas, b)?;
6474    let mut txn = Dsv4SpecTxn {
6475        pos0,
6476        batch: b,
6477        gpu_end: device_end,
6478        dev_filled: st.dev_filled.clone(),
6479        dev_n_comp: st.dev_n_comp.clone(),
6480        dev_n_ix: st.dev_n_ix.clone(),
6481        host: (device_end..layers.len())
6482            .map(|li| (li, host_snap(st, li)))
6483            .collect(),
6484        states: Vec::new(),
6485        host_steps: Vec::new(),
6486        shadow: Some(shadow),
6487    };
6488    // The capture targets that live on the device: photograph their states.
6489    let dev_caps: Vec<usize> = cap_targets
6490        .iter()
6491        .copied()
6492        .filter(|&t| t < device_end)
6493        .collect();
6494    crate::gpu_wgpu::dsv4_spec_retain_arm(device_end, &dev_caps);
6495
6496    // ── seed and run the batch (the prefill batch's own shape) ──
6497    let mut emb = vec![0.0f32; dim];
6498    let mut states = vec![0.0f32; b * hc * dim];
6499    for (t, &id) in ids.iter().enumerate() {
6500        let mut state = vec![0.0f32; hc * dim];
6501        g.embed.row_f32(id as usize, &mut emb);
6502        for j in 0..hc {
6503            state[j * dim..(j + 1) * dim].copy_from_slice(&emb);
6504        }
6505        states[t * hc * dim..(t + 1) * hc * dim].copy_from_slice(&state);
6506        let (folded, post0, comb0) = hc_fold_norm(
6507            &state,
6508            &layers[0].hc_attn_fn,
6509            &layers[0].hc_attn_scale,
6510            &layers[0].hc_attn_base,
6511            &layers[0].attn_norm,
6512            cfg,
6513            pool,
6514        );
6515        let mut qn0 = vec![0.0f32; layers[0].wq_a.rows()];
6516        layers[0].wq_a.matvec(&folded, &mut qn0, pool);
6517        rms_weighted(&mut qn0, &layers[0].q_norm, cfg.norm_eps);
6518        if !crate::gpu_wgpu::dsv4_state_write_t(&state, t)
6519            || !crate::gpu_wgpu::dsv4_hc_write_t(&post0, &comb0, t)
6520            || !crate::gpu_wgpu::dsv4_chain_seed_t(&folded, &qn0, t)
6521            || !crate::gpu_wgpu::dsv4_chain_seed_bt(t, b, &state, &post0, &comb0, &folded, &qn0)
6522        {
6523            crate::gpu_wgpu::dsv4_spec_retain_arm(0, &[]);
6524            return None;
6525        }
6526    }
6527    let spec_time = {
6528        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6529        *ON.get_or_init(|| std::env::var("CMF_DSV4_SPEC_TIME").is_ok_and(|v| v != "0"))
6530    };
6531    let t0 = std::time::Instant::now();
6532    let mut folded = Vec::new();
6533    st.pos = pos0;
6534    // Diagnostic split: the fused prefix normally has one fence.  Splitting
6535    // only while fingerprinting tells us which complete layer first departs
6536    // from scalar decode; it must never become a user-facing tuning flag.
6537    let fp_split = verify_fp_on(pos0)
6538        && std::env::var("CMF_DSV4_FP_SPLIT").is_ok_and(|v| v != "0");
6539    let mut ok = true;
6540    if fp_split {
6541        for li in 0..complete_end {
6542            let one = [li];
6543            ok = dsv4_chain_run(
6544                layers,
6545                &one,
6546                cfg,
6547                g,
6548                st,
6549                *ids.last().unwrap(),
6550                &mut folded,
6551                Some(&mut states),
6552                b,
6553                ids,
6554                li == 0,
6555                pool,
6556            );
6557            if !ok {
6558                break;
6559            }
6560            verify_fp("verify", pos0, li, &states[..hc * dim]);
6561        }
6562    } else if complete_end > 0 {
6563        let run: Vec<usize> = (0..complete_end).collect();
6564        ok = dsv4_chain_run(
6565            layers,
6566            &run,
6567            cfg,
6568            g,
6569            st,
6570            *ids.last().unwrap(),
6571            &mut folded,
6572            Some(&mut states),
6573            b,
6574            ids,
6575            true,
6576            pool,
6577        );
6578        if ok {
6579            verify_fp("verify", pos0, complete_end - 1, &states[..hc * dim]);
6580        }
6581    }
6582    if ok {
6583        for li in complete_end..device_end {
6584            if !partial_layer_batch(g, layers, cfg, st, li, &mut states, ids, pos0, b, pool) {
6585                ok = false;
6586                break;
6587            }
6588            verify_fp("verify", pos0, li, &states[..hc * dim]);
6589        }
6590    }
6591    crate::gpu_wgpu::dsv4_spec_retain_arm(0, &[]);
6592    if !ok {
6593        // Nothing committed on the host; the device may hold half-appended
6594        // state, so put the snapshot back before declining.
6595        if let Some(sh) = txn.shadow.take() {
6596            let _ = crate::gpu_wgpu::dsv4_spec_restore(&sh);
6597        }
6598        st.dev_filled = txn.dev_filled;
6599        st.dev_n_comp = txn.dev_n_comp;
6600        st.dev_n_ix = txn.dev_n_ix;
6601        st.pos = pos0;
6602        return None;
6603    }
6604    txn.states = states.clone();
6605    let t_chain = t0.elapsed();
6606    if std::env::var("CMF_DSV4_FOLD_DBG").is_ok() {
6607        // Any indexer fold this window landed: read the entry back and
6608        // print a fingerprint, so the fused and per-token folds can be
6609        // held against each other on the release shapes.
6610        for li in 0..device_end {
6611            let Some(ixr) = &layers[li].indexer else {
6612                continue;
6613            };
6614            let ratio = ixr.compressor.ratio;
6615            for t in 0..b {
6616                if (pos0 + t + 1) % ratio == 0 {
6617                    let ew = {
6618                        let w = ixr.compressor.wkv.rows();
6619                        if ixr.compressor.overlap { w / 2 } else { w }
6620                    };
6621                    let idx_new = txn.dev_n_ix[li]
6622                        + (0..=t).filter(|k| (pos0 + k + 1) % ratio == 0).count()
6623                        - 1;
6624                    if let Some(v) =
6625                        crate::gpu_wgpu::dsv4_dbg_read_ix(st.kv_id, li, idx_new * ew, ew.min(8))
6626                    {
6627                        let sum: f32 = v.iter().sum();
6628                        eprintln!(
6629                            "[fold] li={li} pos={} entry={idx_new} head={:?} sum={sum:.6}",
6630                            pos0 + t,
6631                            &v[..4.min(v.len())]
6632                        );
6633                    }
6634                }
6635            }
6636        }
6637    }
6638
6639    // ── host tail + every position's head ──
6640    let mut scratch = HcScratch::new(cfg);
6641    argmax_out.clear();
6642    logits_out.clear();
6643    logits_out.resize(b * cfg.vocab, 0.0);
6644    let mut head_in = vec![0.0f32; b * dim];
6645    let mut host_steps: Vec<(usize, Vec<HostLayerSnap>)> = Vec::new();
6646    host_tail_walk_batch(
6647        g,
6648        layers,
6649        cfg,
6650        st,
6651        device_end,
6652        &mut states,
6653        ids,
6654        pos0,
6655        b,
6656        inv_freq,
6657        &mut scratch,
6658        pool,
6659        Some(&mut host_steps),
6660    );
6661    txn.host_steps = host_steps;
6662    for t in 0..b {
6663        let state = &states[t * hc * dim..(t + 1) * hc * dim];
6664        let h = &mut head_in[t * dim..(t + 1) * dim];
6665        hc_head_fold(
6666            state,
6667            &g.hc_head_fn,
6668            g.hc_head_scale,
6669            &g.hc_head_base,
6670            cfg,
6671            pool,
6672            h,
6673        );
6674        rms_weighted(h, &g.norm, cfg.norm_eps);
6675    }
6676    // The experimental B-wide head uses a different reduction kernel from
6677    // ordinary decode.  On the release q4tp it changed row-zero argmax under
6678    // a force-reject transaction, so it is diagnostic-only until parity is
6679    // proven; speculative execution must inherit the canonical head exactly.
6680    let batch_head = std::env::var("CMF_DSV4_SPEC_BATCH_HEAD").is_ok_and(|v| v != "0");
6681    let head_gpu = batch_head && g.head.model_idx().is_some_and(|hi| {
6682        let model = layers[0].experts.first().and_then(|e| e.w1.model_arc());
6683        model.is_some_and(|m| {
6684            crate::gpu_wgpu::q4tp_matvec_batch_for_test(
6685                &m, hi, &head_in, b, cfg.vocab, dim, logits_out,
6686            )
6687        })
6688    });
6689    for t in 0..b {
6690        if !head_gpu {
6691            let h = &head_in[t * dim..(t + 1) * dim];
6692            g.head
6693                .matvec(h, &mut logits_out[t * cfg.vocab..(t + 1) * cfg.vocab], pool);
6694        }
6695        let row = &logits_out[t * cfg.vocab..(t + 1) * cfg.vocab];
6696        let mut best = 0usize;
6697        for v in 1..cfg.vocab {
6698            if row[v] > row[best] {
6699                best = v;
6700            }
6701        }
6702        argmax_out.push(best as u32);
6703    }
6704    walked_out.clear();
6705    walked_out.extend_from_slice(&states);
6706    st.pos = pos0 + b;
6707    if spec_time {
6708        eprintln!(
6709            "verify: тень+сид+цепочка {:.1} мс, хвост+голова {:.1} мс",
6710            t_chain.as_secs_f64() * 1e3,
6711            (t0.elapsed() - t_chain).as_secs_f64() * 1e3,
6712        );
6713    }
6714    Some(txn)
6715}
6716
6717/// Keep the accepted prefix of a verify pass and put everything else back.
6718///
6719/// `accepted` counts the FED tokens whose state stays (at least 1 — the
6720/// first fed token was already committed by the caller). With
6721/// `accepted == batch` this is free; otherwise the device restores its
6722/// snapshot and replays the accepted tokens' state appends, and the host
6723/// tail re-walks them.
6724#[cfg(feature = "gpu")]
6725pub fn dsv4_spec_finish(
6726    g: &Dsv4Globals,
6727    layers: &[Dsv4Layer],
6728    cfg: &Dsv4Cfg,
6729    st: &mut Dsv4State,
6730    mut txn: Dsv4SpecTxn,
6731    accepted: usize,
6732    ids: &[u32],
6733    inv_freq: &[f32],
6734    pool: Option<&crate::pool::Pool>,
6735) -> bool {
6736    macro_rules! sfail {
6737        ($($t:tt)*) => {{
6738            if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
6739                eprintln!("spec_finish: {}", format_args!($($t)*));
6740            }
6741            return false;
6742        }};
6743    }
6744    let b = txn.batch;
6745    let k = accepted.min(b);
6746    let (hc, dim, hd) = (cfg.hc_mult, cfg.dim, cfg.head_dim);
6747    // The staged batch never slid the windows; land the accepted prefix now,
6748    // whatever k is.
6749    let win_metas: Vec<(usize, usize, usize, usize)> = (0..txn.gpu_end)
6750        .map(|li| (li, txn.dev_filled[li], cfg.window, hd))
6751        .collect();
6752    if !crate::gpu_wgpu::dsv4_spec_commit_windows(st.kv_id, &win_metas, b, k) {
6753        sfail!("коммит окон");
6754    }
6755    if k == b {
6756        // Every stream mutation was the walk's own kernels in walk order —
6757        // nothing to put back.
6758        return true;
6759    }
6760    // ── device: restore to the snapshot, then replay the accepted tokens ──
6761    let Some(sh) = txn.shadow.take() else {
6762        sfail!("нет тени")
6763    };
6764    if !crate::gpu_wgpu::dsv4_spec_restore(&sh) {
6765        sfail!("restore");
6766    }
6767    let Some(model) = layers[0].experts.first().and_then(|e| e.w1.model_arc()) else {
6768        sfail!("нет модели");
6769    };
6770    let mut plan: Vec<(usize, crate::gpu_wgpu::Dsv4Prep)> = Vec::new();
6771    let mut freqs_own: Vec<&[f32]> = Vec::new();
6772    for li in 0..txn.gpu_end {
6773        let l = &layers[li];
6774        let Some(wkv) = l.wkv.model_idx() else {
6775            sfail!("wkv слоя {li}")
6776        };
6777        let comp = match &l.compressor {
6778            None => None,
6779            Some(cp) => {
6780                let (Some(a), Some(bx)) = (cp.wkv.model_idx(), cp.wgate.model_idx()) else {
6781                    sfail!("компрессор слоя {li}");
6782                };
6783                Some((
6784                    crate::gpu_wgpu::Dsv4CompW {
6785                        wkv: a,
6786                        wgate: bx,
6787                        norm: &cp.norm,
6788                        ape: &cp.ape,
6789                    },
6790                    crate::gpu_wgpu::Dsv4CompGeom {
6791                        width: cp.wkv.rows(),
6792                        hidden: dim,
6793                        ratio: cp.ratio,
6794                        overlap: cp.overlap,
6795                        rope_dim: cfg.rope_head_dim,
6796                        eps: cfg.norm_eps,
6797                    },
6798                ))
6799            }
6800        };
6801        let ix = match &l.indexer {
6802            None => None,
6803            Some(ixr) => {
6804                let cp = &ixr.compressor;
6805                let (Some(a), Some(bx), Some(qb), Some(wp)) = (
6806                    cp.wkv.model_idx(),
6807                    cp.wgate.model_idx(),
6808                    ixr.wq_b.model_idx(),
6809                    ixr.weights_proj.model_idx(),
6810                ) else {
6811                    sfail!("индексер слоя {li}");
6812                };
6813                let ih = ixr.weights_proj.rows();
6814                Some((
6815                    crate::gpu_wgpu::Dsv4CompW {
6816                        wkv: a,
6817                        wgate: bx,
6818                        norm: &cp.norm,
6819                        ape: &cp.ape,
6820                    },
6821                    crate::gpu_wgpu::Dsv4CompGeom {
6822                        width: cp.wkv.rows(),
6823                        hidden: dim,
6824                        ratio: cp.ratio,
6825                        overlap: cp.overlap,
6826                        rope_dim: cfg.rope_head_dim,
6827                        eps: cfg.norm_eps,
6828                    },
6829                    crate::gpu_wgpu::Dsv4IxW {
6830                        wq_b: qb,
6831                        weights_proj: wp,
6832                    },
6833                    crate::gpu_wgpu::Dsv4IxGeom {
6834                        ih,
6835                        idim: ixr.wq_b.rows() / ih.max(1),
6836                        q_lora: cfg.q_lora_rank,
6837                        hidden: dim,
6838                        rope_dim: cfg.rope_head_dim,
6839                        eps: cfg.norm_eps,
6840                        top_k: cfg.index_topk,
6841                        window: cfg.window,
6842                    },
6843                ))
6844            }
6845        };
6846        let ew_c = comp.as_ref().map_or(
6847            0,
6848            |(_, cg)| {
6849                if cg.overlap { cg.width / 2 } else { cg.width }
6850            },
6851        );
6852        let ew_i = ix.as_ref().map_or(
6853            0,
6854            |(_, cg, _, _)| {
6855                if cg.overlap { cg.width / 2 } else { cg.width }
6856            },
6857        );
6858        let prep = crate::gpu_wgpu::Dsv4Prep {
6859            wkv,
6860            kv_norm: &l.kv_norm,
6861            comp,
6862            ix,
6863            filled: txn.dev_filled[li],
6864            window: cfg.window,
6865            n_comp: txn.dev_n_comp[li],
6866            n_ix: txn.dev_n_ix[li],
6867            comp_dst_off: cfg.window * hd + txn.dev_n_comp[li] * ew_c,
6868            ix_dst_off: txn.dev_n_ix[li] * ew_i,
6869            idx_cap: cfg.window
6870                + if l.indexer.is_some() {
6871                    cfg.index_topk
6872                } else {
6873                    0
6874                },
6875        };
6876        let fr = if l.compressor.is_some() {
6877            g.inv_freq_compress.as_slice()
6878        } else {
6879            g.inv_freq_window.as_slice()
6880        };
6881        freqs_own.push(if fr.is_empty() { inv_freq } else { fr });
6882        plan.push((li, prep));
6883    }
6884    if !crate::gpu_wgpu::dsv4_spec_replay(
6885        &model,
6886        &plan,
6887        st.kv_id,
6888        txn.pos0,
6889        b,
6890        k,
6891        &freqs_own,
6892        hd,
6893        dim,
6894        cfg.rope_head_dim,
6895        cfg.norm_eps,
6896        true,
6897    ) {
6898        sfail!("replay k={k}");
6899    }
6900    // ── host counts: the snapshot advanced by k tokens ──
6901    let advanced = |ratio: usize| -> usize {
6902        if ratio == 0 {
6903            return 0;
6904        }
6905        (0..k).filter(|t| (txn.pos0 + t + 1) % ratio == 0).count()
6906    };
6907    for li in 0..txn.gpu_end {
6908        let l = &layers[li];
6909        st.dev_filled[li] = (txn.dev_filled[li] + k).min(cfg.window);
6910        let ac = l.compressor.as_ref().map_or(0, |cp| advanced(cp.ratio));
6911        let ai = l
6912            .indexer
6913            .as_ref()
6914            .map_or(0, |ix| advanced(ix.compressor.ratio));
6915        st.dev_n_comp[li] = txn.dev_n_comp[li] + ac;
6916        st.dev_n_ix[li] = txn.dev_n_ix[li] + ai;
6917        note_compressed(st.kv_id, li, st.dev_n_comp[li]);
6918    }
6919    // ── host tail: the verify pass already walked these tokens; restore
6920    //    the per-token snapshot it took instead of walking them again. ──
6921    if k >= 1 && txn.host_steps.iter().all(|(_, v)| v.len() >= k) && !txn.host_steps.is_empty() {
6922        for (li, v) in &txn.host_steps {
6923            host_restore(st, *li, &v[k - 1]);
6924        }
6925    } else {
6926        for (li, snap) in &txn.host {
6927            host_restore(st, *li, snap);
6928        }
6929        let mut scratch = HcScratch::new(cfg);
6930        let mut states = txn.states.clone();
6931        host_tail_walk_batch(
6932            g,
6933            layers,
6934            cfg,
6935            st,
6936            txn.gpu_end,
6937            &mut states[..k * hc * dim],
6938            ids,
6939            txn.pos0,
6940            k,
6941            inv_freq,
6942            &mut scratch,
6943            pool,
6944            None,
6945        );
6946    }
6947    st.pos = txn.pos0 + k;
6948    true
6949}
6950
6951pub fn forward_chunk(
6952    g: &Dsv4Globals,
6953    layers: &[Dsv4Layer],
6954    cfg: &Dsv4Cfg,
6955    st: &mut Dsv4State,
6956    ids: &[u32],
6957    pos0: usize,
6958    inv_freq: &[f32],
6959    pool: Option<&crate::pool::Pool>,
6960    logits: &mut Vec<f32>,
6961    want_logits: bool,
6962) {
6963    let bs = batch_prefill();
6964    if bs > 1 {
6965        // The first token walks, always. The batch will only run where every
6966        // layer has already proved it takes the card, and that proof is a
6967        // completed single-token run — with the whole prompt arriving as one
6968        // chunk there is otherwise no first run to give it, and the batch
6969        // declines for the entire prompt while a gate comparing it against
6970        // the walk reports agreement it never tested.
6971        let mut i = 0;
6972        if !st.dev_owned && !ids.is_empty() {
6973            st.pos = pos0;
6974            forward_token_inner(
6975                g,
6976                layers,
6977                cfg,
6978                st,
6979                ids[0],
6980                inv_freq,
6981                pool,
6982                logits,
6983                ids.len() == 1,
6984            );
6985            i = 1;
6986        }
6987        while i < ids.len() {
6988            let end = (i + bs).min(ids.len());
6989            st.pos = pos0 + i;
6990            if !forward_chunk_batched(
6991                g,
6992                layers,
6993                cfg,
6994                st,
6995                &ids[i..end],
6996                pos0 + i,
6997                inv_freq,
6998                pool,
6999                logits,
7000                want_logits && end == ids.len(),
7001            ) {
7002                break;
7003            }
7004            i = end;
7005        }
7006        if i == ids.len() {
7007            return;
7008        }
7009        // Refused before touching anything; the walk starts where it left off.
7010        for (k, &id) in ids.iter().enumerate().skip(i) {
7011            st.pos = pos0 + k;
7012            let last = want_logits && k + 1 == ids.len();
7013            forward_token_inner(g, layers, cfg, st, id, inv_freq, pool, logits, last);
7014        }
7015        return;
7016    }
7017    for (i, &id) in ids.iter().enumerate() {
7018        st.pos = pos0 + i;
7019        let last = want_logits && i + 1 == ids.len();
7020        forward_token_inner(g, layers, cfg, st, id, inv_freq, pool, logits, last);
7021    }
7022}
7023
7024pub fn forward_token(
7025    g: &Dsv4Globals,
7026    layers: &[Dsv4Layer],
7027    cfg: &Dsv4Cfg,
7028    st: &mut Dsv4State,
7029    token_id: u32,
7030    inv_freq: &[f32],
7031    pool: Option<&crate::pool::Pool>,
7032    logits: &mut Vec<f32>,
7033) {
7034    forward_token_inner(g, layers, cfg, st, token_id, inv_freq, pool, logits, true);
7035}
7036
7037#[allow(clippy::too_many_arguments)]
7038fn forward_token_inner(
7039    g: &Dsv4Globals,
7040    layers: &[Dsv4Layer],
7041    cfg: &Dsv4Cfg,
7042    st: &mut Dsv4State,
7043    token_id: u32,
7044    inv_freq: &[f32],
7045    pool: Option<&crate::pool::Pool>,
7046    logits: &mut Vec<f32>,
7047    // Prompt tokens other than the last one have their logits thrown away.
7048    want_logits: bool,
7049) {
7050    let _t_all = prof::on().then(std::time::Instant::now);
7051    let _all_guard = Charge(_t_all, &prof::ALL_NS);
7052    let (hc, dim) = (cfg.hc_mult, cfg.dim);
7053
7054    // Embedding, replicated into the copies.
7055    let mut emb = vec![0.0f32; dim];
7056    g.embed.row_f32(token_id as usize, &mut emb);
7057    let mut state = vec![0.0f32; hc * dim];
7058    for j in 0..hc {
7059        state[j * dim..(j + 1) * dim].copy_from_slice(&emb);
7060    }
7061
7062    let mut scratch = HcScratch::new(cfg);
7063    let mut dump: Vec<String> = Vec::new();
7064    if dump_path().is_some() {
7065        dump.push(format!("\"embed\":{}", vec_json(&emb)));
7066        PICKED.with(|p| p.borrow_mut().clear());
7067        BODY.with(|b| b.borrow_mut().clear());
7068        dump.push(",\"layers\":[".into());
7069    }
7070    if trace_on() {
7071        eprintln!(
7072            "[dsv4] tok={token_id} pos={} embed rms={:.5}",
7073            st.pos,
7074            rms_of(&emb)
7075        );
7076    }
7077    // ── one submission per layer, when the device will take it ──
7078    #[cfg(feature = "gpu")]
7079    let layer_frames = gpu_layer_enabled()
7080        && dsv4_layer_loop(
7081            &mut state,
7082            layers,
7083            g,
7084            cfg,
7085            st,
7086            token_id,
7087            inv_freq,
7088            pool,
7089            &mut scratch,
7090        );
7091    #[cfg(not(feature = "gpu"))]
7092    let layer_frames = false;
7093
7094    // ── the fast two-frame path: hyper-connections on the card ──
7095    // Measured on the release, the fold, the Sinkhorn and the norms cost 19
7096    // ms of a 57 ms token on the host and hundredths of one on the device.
7097    // With both frames doing their own, the host carries nothing between a
7098    // layer's halves and the MoE half's input never leaves the card — one
7099    // readback a layer instead of two.
7100    #[cfg(feature = "gpu")]
7101    let hc_dev = hc_on_device()
7102        && !layer_frames
7103        && gpu_attn_enabled()
7104        && gpu_moe2_enabled()
7105        && dump_path().is_none();
7106    #[cfg(not(feature = "gpu"))]
7107    let hc_dev = false;
7108    // The device loop's verdict as a VALUE, not as a cfg-gated `if`. It used
7109    // to be the latter, with the CPU loop in the `else` arm — so a build
7110    // without the gpu feature compiled no layer loop at all and every token
7111    // passed through untouched. The window test said so ("sliding window
7112    // never filled") and only in the CPU-only build, which is the one
7113    // configuration the gate was not running.
7114    #[cfg(feature = "gpu")]
7115    let two_frame_done = hc_dev
7116        && dsv4_two_frame_loop(
7117            &mut state,
7118            layers,
7119            g,
7120            cfg,
7121            st,
7122            token_id,
7123            inv_freq,
7124            pool,
7125            &mut scratch,
7126        );
7127    #[cfg(not(feature = "gpu"))]
7128    let two_frame_done = false;
7129    if !two_frame_done {
7130        for (li, l) in layers.iter().enumerate() {
7131            if layer_frames {
7132                break;
7133            }
7134            // attention half
7135            hc_block(
7136                &mut state,
7137                &l.hc_attn_fn,
7138                &l.hc_attn_scale,
7139                &l.hc_attn_base,
7140                &l.attn_norm,
7141                cfg,
7142                &mut scratch,
7143                pool,
7144                |folded, out| {
7145                    if dump_path().is_some() {
7146                        // The body's own input and output, so the reference can be
7147                        // fed the port's input: then only the body can differ.
7148                        BODY.with(|b| b.borrow_mut().push(vec_json(folded)));
7149                    }
7150                    // The layer's kind decides its frequencies, not the model's.
7151                    let freqs = if l.compressor.is_some() {
7152                        &g.inv_freq_compress
7153                    } else {
7154                        &g.inv_freq_window
7155                    };
7156                    let freqs = if freqs.is_empty() {
7157                        inv_freq
7158                    } else {
7159                        freqs.as_slice()
7160                    };
7161                    attention_step(folded, l, cfg, st, li, freqs, pool, None, out);
7162                    if dump_path().is_some() {
7163                        BODY.with(|b| b.borrow_mut().push(vec_json(out)));
7164                    }
7165                },
7166            );
7167            if dump_path().is_some() {
7168                // After the attention half only — this is what separates an
7169                // attention discrepancy from an expert one.
7170                dump.push(format!(
7171                    "{}{}",
7172                    if li == 0 { "" } else { "," },
7173                    vec_json(&state)
7174                ));
7175            }
7176            // FFN half
7177            let _t_hc2 = prof::on().then(std::time::Instant::now);
7178            hc_block(
7179                &mut state,
7180                &l.hc_ffn_fn,
7181                &l.hc_ffn_scale,
7182                &l.hc_ffn_base,
7183                &l.ffn_norm,
7184                cfg,
7185                &mut scratch,
7186                pool,
7187                |folded, out| moe_step(folded, l, cfg, token_id, li, pool, out),
7188            );
7189            if let Some(t) = _t_hc2 {
7190                // The block's own time minus the expert step inside it — what the
7191                // fold, the norm and the expand cost on their own.
7192                prof::HC_NS.fetch_add(
7193                    t.elapsed().as_nanos() as u64,
7194                    std::sync::atomic::Ordering::Relaxed,
7195                );
7196            }
7197            if dump_path().is_some() {
7198                dump.push(format!(",{}", vec_json(&state)));
7199            }
7200            if trace_on() && (st.pos % 64 == 0 || st.pos == 199) {
7201                eprintln!(
7202                    "[dsv4]  кеши слоя {li}: окно={} сжатых={} индекс={} (ratio={:?})",
7203                    st.window[li].len() / cfg.head_dim.max(1),
7204                    st.compressed[li].len() / cfg.head_dim.max(1),
7205                    st.index_kv[li].len().max(1) / 128,
7206                    l.compressor.as_ref().map(|c| (c.ratio, c.overlap)),
7207                );
7208            }
7209            if trace_on() {
7210                let bad = state.iter().filter(|v| !v.is_finite()).count();
7211                eprintln!(
7212                    "[dsv4]  layer {li:>2}: rms={:.5}{}",
7213                    rms_of(&state),
7214                    if bad > 0 {
7215                        format!("  NON-FINITE x{bad}")
7216                    } else {
7217                        String::new()
7218                    }
7219                );
7220            }
7221            dspark_note(li, &state, cfg);
7222        }
7223    }
7224    st.pos += 1;
7225
7226    // Collapse the copies, normalize, project to the vocabulary.
7227    let mut h = vec![0.0f32; dim];
7228    hc_head_fold(
7229        &state,
7230        &g.hc_head_fn,
7231        g.hc_head_scale,
7232        &g.hc_head_base,
7233        cfg,
7234        pool,
7235        &mut h,
7236    );
7237    if !want_logits {
7238        logits.clear();
7239        return;
7240    }
7241    let _t_head = prof::on().then(std::time::Instant::now);
7242    rms_weighted(&mut h, &g.norm, cfg.norm_eps);
7243    logits.clear();
7244    logits.resize(g.head.rows(), 0.0);
7245    g.head.matvec(&h, logits, pool);
7246    if let Some(t) = _t_head {
7247        prof::HEAD_NS.fetch_add(
7248            t.elapsed().as_nanos() as u64,
7249            std::sync::atomic::Ordering::Relaxed,
7250        );
7251    }
7252    if dump_path().is_some() {
7253        dump.push("]".into());
7254        let picked = PICKED.with(|p| {
7255            p.borrow()
7256                .iter()
7257                .map(|v| {
7258                    format!(
7259                        "[{}]",
7260                        v.iter()
7261                            .map(|e| e.to_string())
7262                            .collect::<Vec<_>>()
7263                            .join(",")
7264                    )
7265                })
7266                .collect::<Vec<_>>()
7267                .join(",")
7268        });
7269        dump.push(format!(",\"experts\":[{picked}]"));
7270        let body = BODY.with(|b| b.borrow().join(","));
7271        dump.push(format!(",\"attn_io\":[{body}]"));
7272        dump_line(&format!(
7273            "{{\"tok\":{token_id},\"pos\":{},{},\"head\":{},\"logits\":{}}}",
7274            st.pos - 1,
7275            dump.join(""),
7276            vec_json(&h),
7277            vec_json(logits)
7278        ));
7279    }
7280    if trace_on() {
7281        let (mut top, mut best) = (0usize, f32::NEG_INFINITY);
7282        for (i, &v) in logits.iter().enumerate() {
7283            if v > best {
7284                best = v;
7285                top = i;
7286            }
7287        }
7288        let lo = logits.iter().cloned().fold(f32::MAX, f32::min);
7289        eprintln!(
7290            "[dsv4]  head: rms={:.5} logits[{}..{:.3}] argmax={top}",
7291            rms_of(&h),
7292            format_args!("{lo:.3}"),
7293            best
7294        );
7295    }
7296}
7297
7298/// Build the runtime weights from a converted `.cmf`.
7299///
7300/// Names are the converter's output (see `canon_name`'s deepseek_v4 arm):
7301/// attention keeps DeepSeek's own spelling under `self_attn.`, the MoE is
7302/// rewritten into the layout every other MoE here uses, and the hyper-
7303/// connection tensors ride under the layer prefix.
7304pub fn load(
7305    model: &std::sync::Arc<cortiq_core::CmfModel>,
7306    cfg: &Dsv4Cfg,
7307    n_layers: usize,
7308) -> Result<(Dsv4Globals, Vec<Dsv4Layer>), String> {
7309    let q = |name: &str| -> Result<crate::qtensor::QTensor, String> {
7310        crate::qtensor::QTensor::from_model(model, name)
7311    };
7312    // The small pieces — norms, the sink, ape, the hyper-connection
7313    // projections — are read as plain f32. They are not all 2-D (a norm is a
7314    // vector), so this cannot go through QTensor, which requires a matrix.
7315    let f = |name: &str| -> Result<Vec<f32>, String> {
7316        crate::loader::load_f32(model, name, &crate::loader::Overlay::None)
7317    };
7318
7319    // Two frequency tables, chosen per layer by whether it compresses. The
7320    // release's compress_rope_theta (160 000) is not in config.json — it
7321    // lives in inference/config.json — so it is pinned here with the other
7322    // constants the header cannot carry.
7323    let rope_of = |base: f32, yarn: bool| -> Vec<f32> {
7324        if yarn {
7325            crate::attention::yarn_inv_freq(cfg.rope_head_dim, base, 16.0, 65536, 32.0, 1.0)
7326        } else {
7327            crate::attention::rope_inv_freq(cfg.rope_head_dim, base)
7328        }
7329    };
7330    let globals = Dsv4Globals {
7331        inv_freq_compress: rope_of(160_000.0, true),
7332        inv_freq_window: rope_of(10_000.0, false),
7333        embed: q("model.embed_tokens.weight")?,
7334        norm: f("model.norm.weight")?,
7335        head: q("lm_head.weight")?,
7336        hc_head_fn: f("model.hc_head_fn")?,
7337        hc_head_base: f("model.hc_head_base")?,
7338        hc_head_scale: *f("model.hc_head_scale")?
7339            .first()
7340            .ok_or("dsv4: empty hc_head_scale")?,
7341    };
7342
7343    let mut layers = Vec::with_capacity(n_layers);
7344    for li in 0..n_layers {
7345        layers.push(load_layer(
7346            model,
7347            cfg,
7348            &format!("model.layers.{li}"),
7349            Scheme::Main,
7350        )?);
7351    }
7352    // The projection this loader exists to serve: with a RAM tier configured,
7353    // pin the MASKED expert set with one sequential sweep of the file at
7354    // streaming rate, before decode discovers it miss by miss in random
7355    // order. Experts outside a layer's mask are skipped; a layer without a
7356    // mask keeps all of its experts (the budget caps the sweep).
7357    #[cfg(feature = "gpu")]
7358    if crate::gpu_wgpu::host_banks_on() {
7359        // Host banks: one background sweep, layer by layer, oldest first.
7360        let sets: Vec<(usize, Vec<(usize, usize, usize)>, bool)> = layers
7361            .iter()
7362            .filter_map(|l| {
7363                let idx3 = |e: &Dsv4Expert| {
7364                    Some((e.w1.model_idx()?, e.w3.model_idx()?, e.w2.model_idx()?))
7365                };
7366                let mut v: Vec<_> = l.experts.iter().filter_map(idx3).collect();
7367                let first = v.first()?.0;
7368                v.push(idx3(&l.shared)?);
7369                let gu_q2 = l.experts.first()?.w1.model_dtype()
7370                    == Some(cortiq_core::TensorDtype::Q2TiledP);
7371                Some((first, v, gu_q2))
7372            })
7373            .collect();
7374        let m2 = model.clone();
7375        let (inter, dim) = (cfg.moe_inter, cfg.dim);
7376        std::thread::spawn(move || {
7377            for (first, v, gu_q2) in sets {
7378                crate::gpu_wgpu::dsv4_host_bank_build(&m2, first, &v, inter, dim, gu_q2);
7379            }
7380            tracing::info!("host banks: built");
7381        });
7382    }
7383    #[cfg(feature = "gpu")]
7384    {
7385        let masks: Vec<Option<Vec<bool>>> = layers.iter().map(|l| l.mask.clone()).collect();
7386        crate::gpu_wgpu::prefetch_tier(model, &|name: &str| {
7387            let Some(i) = name.find(".experts.") else {
7388                return false;
7389            };
7390            let rest = &name[i + 9..];
7391            let e: usize = match rest[..rest.find('.').unwrap_or(rest.len())].parse() {
7392                Ok(v) => v,
7393                Err(_) => return false,
7394            };
7395            let li: usize = {
7396                let Some(j) = name.find("layers.") else { return false };
7397                let r = &name[j + 7..];
7398                match r[..r.find('.').unwrap_or(r.len())].parse() {
7399                    Ok(v) => v,
7400                    Err(_) => return false,
7401                }
7402            };
7403            match masks.get(li).and_then(|m| m.as_ref()) {
7404                Some(m) => m.get(e).copied().unwrap_or(false),
7405                None => true,
7406            }
7407        });
7408    }
7409    Ok((globals, layers))
7410}
7411
7412/// Where a layer's tensors live in the file.
7413///
7414/// The MTP modules are the same layer as any other — attention, a
7415/// hyper-connection pair, a gated MoE over 256 experts — but the converter
7416/// wrote them under DeepSeek's internal names rather than the HF ones it used
7417/// for the trunk. Two schemes, one loader: a second copy would drift.
7418#[derive(Clone, Copy, PartialEq, Eq, Debug)]
7419pub enum Scheme {
7420    Main,
7421    Mtp,
7422}
7423
7424impl Scheme {
7425    fn attn(self) -> &'static str {
7426        match self {
7427            Scheme::Main => "self_attn",
7428            Scheme::Mtp => "attn",
7429        }
7430    }
7431    fn attn_norm(self) -> &'static str {
7432        match self {
7433            Scheme::Main => "input_layernorm.weight",
7434            Scheme::Mtp => "attn_norm.weight",
7435        }
7436    }
7437    fn ffn_norm(self) -> &'static str {
7438        match self {
7439            Scheme::Main => "post_attention_layernorm.weight",
7440            Scheme::Mtp => "ffn_norm.weight",
7441        }
7442    }
7443    fn mlp(self) -> &'static str {
7444        match self {
7445            Scheme::Main => "mlp",
7446            Scheme::Mtp => "ffn",
7447        }
7448    }
7449    /// The router's per-expert bias. Absent on the trunk's hash layers, which
7450    /// is how they are recognised; always present on an MTP module.
7451    fn gate_bias(self) -> &'static str {
7452        match self {
7453            Scheme::Main => "expert_bias",
7454            Scheme::Mtp => "gate.bias",
7455        }
7456    }
7457    fn shared(self) -> &'static str {
7458        match self {
7459            Scheme::Main => "shared_expert",
7460            Scheme::Mtp => "shared_experts",
7461        }
7462    }
7463    /// gate, down, up — in that order, which is w1/w2/w3 upstream.
7464    fn w(self, i: u8) -> &'static str {
7465        match (self, i) {
7466            (Scheme::Main, 1) => "gate_proj.weight",
7467            (Scheme::Main, 2) => "down_proj.weight",
7468            (Scheme::Main, _) => "up_proj.weight",
7469            (Scheme::Mtp, 1) => "w1.weight",
7470            (Scheme::Mtp, 2) => "w2.weight",
7471            (Scheme::Mtp, _) => "w3.weight",
7472        }
7473    }
7474}
7475
7476/// One layer, wherever it lives in the file.
7477pub fn load_layer(
7478    model: &std::sync::Arc<cortiq_core::CmfModel>,
7479    cfg: &Dsv4Cfg,
7480    p: &str,
7481    s: Scheme,
7482) -> Result<Dsv4Layer, String> {
7483    let q = |name: &str| -> Result<crate::qtensor::QTensor, String> {
7484        crate::qtensor::QTensor::from_model(model, name)
7485    };
7486    let f = |name: &str| -> Result<Vec<f32>, String> {
7487        crate::loader::load_f32(model, name, &crate::loader::Overlay::None)
7488    };
7489    let opt_f = |name: &str| -> Option<Vec<f32>> { f(name).ok() };
7490    let at = s.attn();
7491    let ml = s.mlp();
7492    {
7493        let scale3 = |name: &str| -> Result<[f32; 3], String> {
7494            let v = f(name)?;
7495            if v.len() < 3 {
7496                return Err(format!("{name}: expected 3 scales, got {}", v.len()));
7497            }
7498            Ok([v[0], v[1], v[2]])
7499        };
7500        // The compressor exists on every layer whose ratio is non-zero;
7501        // its presence in the file is the only signal we need.
7502        let compressor = match q(&format!("{p}.{at}.compressor.wkv.weight")) {
7503            Ok(wkv) => {
7504                let ape = f(&format!("{p}.{at}.compressor.ape"))?;
7505                // ape is [ratio, coff*head_dim]; coff is 2 when the windows
7506                // overlap, which the release does at ratio 4.
7507                let width = wkv.rows();
7508                let ratio = (ape.len() / width.max(1)).max(1);
7509                Some(Dsv4Compressor {
7510                    wkv,
7511                    wgate: q(&format!("{p}.{at}.compressor.wgate.weight"))?,
7512                    norm: f(&format!("{p}.{at}.compressor.norm.weight"))?,
7513                    ape,
7514                    ratio,
7515                    overlap: ratio == 4,
7516                })
7517            }
7518            Err(_) => None,
7519        };
7520        let indexer = match q(&format!("{p}.{at}.indexer.wq_b.weight")) {
7521            Ok(wq_b) => {
7522                let ape = f(&format!("{p}.{at}.indexer.compressor.ape"))?;
7523                let cwkv = q(&format!("{p}.{at}.indexer.compressor.wkv.weight"))?;
7524                let width = cwkv.rows();
7525                let ratio = (ape.len() / width.max(1)).max(1);
7526                Some(Dsv4Indexer {
7527                    wq_b,
7528                    weights_proj: q(&format!("{p}.{at}.indexer.weights_proj.weight"))?,
7529                    compressor: Dsv4Compressor {
7530                        wkv: cwkv,
7531                        wgate: q(&format!("{p}.{at}.indexer.compressor.wgate.weight"))?,
7532                        norm: f(&format!("{p}.{at}.indexer.compressor.norm.weight"))?,
7533                        ape,
7534                        ratio,
7535                        overlap: ratio == 4,
7536                    },
7537                })
7538            }
7539            Err(_) => None,
7540        };
7541
7542        let mut experts = Vec::with_capacity(cfg.n_routed_experts);
7543        for e in 0..cfg.n_routed_experts {
7544            let ep = format!("{p}.{ml}.experts.{e}");
7545            experts.push(Dsv4Expert {
7546                w1: q(&format!("{ep}.{w}", w = s.w(1)))?,
7547                w2: q(&format!("{ep}.{w}", w = s.w(2)))?,
7548                w3: q(&format!("{ep}.{w}", w = s.w(3)))?,
7549            });
7550        }
7551
7552        Ok(Dsv4Layer {
7553            attn_norm: f(&format!("{p}.{an}", an = s.attn_norm()))?,
7554            ffn_norm: f(&format!("{p}.{fnm}", fnm = s.ffn_norm()))?,
7555            wq_a: q(&format!("{p}.{at}.wq_a.weight"))?,
7556            q_norm: f(&format!("{p}.{at}.q_norm.weight"))?,
7557            wq_b: q(&format!("{p}.{at}.wq_b.weight"))?,
7558            wkv: q(&format!("{p}.{at}.wkv.weight"))?,
7559            kv_norm: f(&format!("{p}.{at}.kv_norm.weight"))?,
7560            wo_a: q(&format!("{p}.{at}.wo_a.weight"))?,
7561            wo_b: q(&format!("{p}.{at}.wo_b.weight"))?,
7562            attn_sink: f(&format!("{p}.{at}.attn_sink"))?,
7563            compressor,
7564            indexer,
7565            hc_attn_fn: f(&format!("{p}.hc_attn_fn"))?,
7566            hc_attn_base: f(&format!("{p}.hc_attn_base"))?,
7567            hc_attn_scale: scale3(&format!("{p}.hc_attn_scale"))?,
7568            hc_ffn_fn: f(&format!("{p}.hc_ffn_fn"))?,
7569            hc_ffn_base: f(&format!("{p}.hc_ffn_base"))?,
7570            hc_ffn_scale: scale3(&format!("{p}.hc_ffn_scale"))?,
7571            gate: q(&format!("{p}.{ml}.gate.weight"))?,
7572            // The bias is absent exactly on the hash layers, and the table
7573            // is present exactly there — the file itself says which is which.
7574            gate_bias: opt_f(&format!("{p}.{ml}.{b}", b = s.gate_bias())),
7575            tid2eid: opt_f(&format!("{p}.{ml}.tid2eid")),
7576            experts,
7577            mask: if model.tensor(&format!("{p}.{ml}.tid2eid")).is_some() {
7578                None
7579            } else {
7580                crate::loader::moe_task_mask(model, &format!("{p}."), cfg.n_routed_experts)
7581            },
7582            shared: Dsv4Expert {
7583                w1: q(&format!("{p}.{ml}.{sh}.{w}", sh = s.shared(), w = s.w(1)))?,
7584                w2: q(&format!("{p}.{ml}.{sh}.{w}", sh = s.shared(), w = s.w(2)))?,
7585                w3: q(&format!("{p}.{ml}.{sh}.{w}", sh = s.shared(), w = s.w(3)))?,
7586            },
7587        })
7588    }
7589}
7590
7591/// One module of the speculation stack.
7592///
7593/// The release carries three, so the draft is three deep, and the last one
7594/// also holds a confidence head — the model scores its own proposals rather
7595/// than leaving acceptance to a threshold we would have to invent. Each
7596/// module is a full layer with its own 256 experts; what makes it an MTP
7597/// module rather than a 44th layer is `main_proj`, which folds the previous
7598/// hidden state into the next embedding before the layer runs.
7599pub struct Dsv4Mtp {
7600    pub layer: Dsv4Layer,
7601    /// Stage 0 only: the projection that turns the trunk's captured hidden
7602    /// states into the block's input. Later stages take the block from the
7603    /// stage before them, so they carry none.
7604    pub main_proj: Option<crate::qtensor::QTensor>,
7605    pub main_norm: Option<Vec<f32>>,
7606    /// Last module only: what turns a draft hidden state into logits.
7607    pub norm: Option<Vec<f32>>,
7608    pub hc_head_fn: Option<Vec<f32>>,
7609    pub hc_head_base: Option<Vec<f32>>,
7610    pub hc_head_scale: Option<f32>,
7611    pub confidence: Option<crate::qtensor::QTensor>,
7612    /// Last stage only: a rank-256 bigram table that biases the draft's
7613    /// logits, and whose embedding also feeds the confidence head. Cheap
7614    /// enough that the draft samples through it position by position while
7615    /// the network itself runs the whole block at once.
7616    pub markov_w1: Option<crate::qtensor::QTensor>,
7617    pub markov_w2: Option<crate::qtensor::QTensor>,
7618}
7619
7620/// Load as much of the speculation stack as the file carries, up to
7621/// `max_depth`. Missing is not an error: a checkpoint without MTP simply
7622/// yields an empty stack, and the caller falls back to plain decoding.
7623pub fn load_mtp(
7624    model: &std::sync::Arc<cortiq_core::CmfModel>,
7625    cfg: &Dsv4Cfg,
7626    max_depth: usize,
7627) -> Vec<Dsv4Mtp> {
7628    let f = |name: &str| -> Option<Vec<f32>> {
7629        crate::loader::load_f32(model, name, &crate::loader::Overlay::None).ok()
7630    };
7631    let mut out = Vec::new();
7632    for d in 0..max_depth {
7633        let p = format!("model.mtp.{d}");
7634        // A stage is recognised by its attention, not by `main_proj`: only
7635        // stage 0 has that, and only the last has the head. Keying on either
7636        // end found one module of three.
7637        if model.tensor(&format!("{p}.attn.wq_a.weight")).is_none() {
7638            break;
7639        }
7640        let layer = match load_layer(model, cfg, &p, Scheme::Mtp) {
7641            Ok(l) => l,
7642            Err(e) => {
7643                eprintln!("MTP {d}: пропущен, {e}");
7644                break;
7645            }
7646        };
7647        out.push(Dsv4Mtp {
7648            layer,
7649            main_proj: crate::qtensor::QTensor::from_model(model, &format!("{p}.main_proj.weight"))
7650                .ok(),
7651            main_norm: f(&format!("{p}.main_norm.weight")),
7652            norm: f(&format!("{p}.norm.weight")),
7653            hc_head_fn: f(&format!("{p}.hc_head_fn")),
7654            hc_head_base: f(&format!("{p}.hc_head_base")),
7655            hc_head_scale: f(&format!("{p}.hc_head_scale")).and_then(|v| v.first().copied()),
7656            confidence: crate::qtensor::QTensor::from_model(
7657                model,
7658                &format!("{p}.confidence_head.proj.weight"),
7659            )
7660            .ok(),
7661            markov_w1: crate::qtensor::QTensor::from_model(
7662                model,
7663                &format!("{p}.markov_head.markov_w1.weight"),
7664            )
7665            .ok(),
7666            markov_w2: crate::qtensor::QTensor::from_model(
7667                model,
7668                &format!("{p}.markov_head.markov_w2.weight"),
7669            )
7670            .ok(),
7671        });
7672    }
7673    dspark_apply_mask(&mut out);
7674    if !out.is_empty() {
7675        let mp = out
7676            .iter()
7677            .find_map(|m| m.main_proj.as_ref())
7678            .map(|t| format!("[{}, {}]", t.rows(), t.cols()))
7679            .unwrap_or_else(|| "нет".into());
7680        eprintln!(
7681            "MTP: {} стади(я/и/й), main_proj {mp}, экспертов {}, \
7682             голова уверенности {}, марков {}",
7683            out.len(),
7684            out[0].layer.experts.len(),
7685            if out.iter().any(|m| m.confidence.is_some()) {
7686                "есть"
7687            } else {
7688                "нет"
7689            },
7690            if out.iter().any(|m| m.markov_w1.is_some()) {
7691                "есть"
7692            } else {
7693                "нет"
7694            },
7695        );
7696    }
7697    out
7698}
7699
7700#[cfg(test)]
7701mod tests {
7702    use super::*;
7703
7704    // A whole model, small enough to reason about: 2 layers, 4 heads, 8
7705    // experts. Weights are deterministic and tiny, which is the point —
7706    // this test is about shapes, indexing and cache bookkeeping, the things
7707    // that a 138 GB file would surface only after an hour of loading.
7708    fn toy() -> (Dsv4Globals, Vec<Dsv4Layer>, Dsv4Cfg) {
7709        use crate::qtensor::QTensor;
7710        let cfg = Dsv4Cfg {
7711            dim: 32,
7712            n_heads: 4,
7713            head_dim: 8,
7714            rope_head_dim: 4,
7715            q_lora_rank: 16,
7716            o_lora_rank: 16,
7717            o_groups: 2,
7718            hc_mult: 4,
7719            hc_sinkhorn_iters: 20,
7720            hc_eps: 1e-6,
7721            norm_eps: 1e-6,
7722            n_routed_experts: 8,
7723            top_k: 2,
7724            moe_inter: 16,
7725            route_scale: 1.0,
7726            swiglu_limit: 10.0,
7727            window: 6,
7728            index_topk: 8,
7729            vocab: 24,
7730        };
7731        // Deterministic pseudo-random in a narrow band: big enough to move
7732        // the state, small enough that nothing saturates.
7733        let w = |n: usize, seed: usize| -> Vec<f32> {
7734            (0..n)
7735                .map(|i| (((i * 7 + seed * 13) % 101) as f32 / 101.0 - 0.5) * 0.3)
7736                .collect()
7737        };
7738        let t = |rows: usize, cols: usize, seed: usize| {
7739            QTensor::from_f32(w(rows * cols, seed), rows, cols)
7740        };
7741        let ones = |n: usize| vec![1.0f32; n];
7742
7743        let (dim, hc) = (cfg.dim, cfg.hc_mult);
7744        // q is n_heads*head_dim wide and kv is one head wide; rope rides the
7745        // tail of each rather than widening anything.
7746        let q_width = cfg.n_heads * cfg.head_dim;
7747        let kv_width = cfg.head_dim;
7748        let o_per_group = q_width / cfg.o_groups;
7749        let mut layers = Vec::new();
7750        for li in 0..2 {
7751            let experts: Vec<Dsv4Expert> = (0..cfg.n_routed_experts)
7752                .map(|e| Dsv4Expert {
7753                    w1: t(cfg.moe_inter, dim, 40 + e + li * 8),
7754                    w2: t(dim, cfg.moe_inter, 60 + e + li * 8),
7755                    w3: t(cfg.moe_inter, dim, 80 + e + li * 8),
7756                })
7757                .collect();
7758            // Layer 0 is a hash layer (table-routed); layer 1 routes normally
7759            // and carries the compressor — both paths get exercised.
7760            layers.push(Dsv4Layer {
7761                attn_norm: ones(dim),
7762                ffn_norm: ones(dim),
7763                wq_a: t(cfg.q_lora_rank, dim, 1 + li),
7764                q_norm: ones(cfg.q_lora_rank),
7765                wq_b: t(q_width, cfg.q_lora_rank, 3 + li),
7766                wkv: t(kv_width, dim, 5 + li),
7767                kv_norm: ones(kv_width),
7768                wo_a: t(cfg.o_groups * cfg.o_lora_rank, o_per_group, 7 + li),
7769                wo_b: t(dim, cfg.o_groups * cfg.o_lora_rank, 9 + li),
7770                attn_sink: vec![0.1; cfg.n_heads],
7771                // Layer 1 carries the OVERLAPPING compressor, as the release
7772                // does at ratio 4: the projection is twice the entry width.
7773                compressor: if li == 1 {
7774                    Some(Dsv4Compressor {
7775                        wkv: t(2 * kv_width, dim, 11),
7776                        wgate: t(2 * kv_width, dim, 13),
7777                        norm: ones(kv_width),
7778                        ape: vec![0.01; 4 * 2 * kv_width],
7779                        ratio: 4,
7780                        overlap: true,
7781                    })
7782                } else {
7783                    None
7784                },
7785                indexer: if li == 1 {
7786                    Some(Dsv4Indexer {
7787                        wq_b: t(2 * 16, cfg.q_lora_rank, 41),
7788                        weights_proj: t(2, dim, 43),
7789                        compressor: Dsv4Compressor {
7790                            wkv: t(2 * 16, dim, 45),
7791                            wgate: t(2 * 16, dim, 47),
7792                            norm: ones(16),
7793                            ape: vec![0.01; 4 * 2 * 16],
7794                            ratio: 4,
7795                            overlap: true,
7796                        },
7797                    })
7798                } else {
7799                    None
7800                },
7801                hc_attn_fn: w((2 + hc) * hc * hc * dim, 15 + li),
7802                hc_attn_base: w((2 + hc) * hc, 17 + li),
7803                hc_attn_scale: [1.0, 1.0, 1.0],
7804                hc_ffn_fn: w((2 + hc) * hc * hc * dim, 19 + li),
7805                hc_ffn_base: w((2 + hc) * hc, 21 + li),
7806                hc_ffn_scale: [1.0, 1.0, 1.0],
7807                gate: t(cfg.n_routed_experts, dim, 23 + li),
7808                gate_bias: if li == 1 {
7809                    Some(vec![0.0; cfg.n_routed_experts])
7810                } else {
7811                    None
7812                },
7813                tid2eid: if li == 0 {
7814                    Some(
7815                        (0..cfg.vocab * cfg.top_k)
7816                            .map(|i| (i % cfg.n_routed_experts) as f32)
7817                            .collect(),
7818                    )
7819                } else {
7820                    None
7821                },
7822                experts,
7823                mask: None,
7824                shared: Dsv4Expert {
7825                    w1: t(cfg.moe_inter, dim, 25 + li),
7826                    w2: t(dim, cfg.moe_inter, 27 + li),
7827                    w3: t(cfg.moe_inter, dim, 29 + li),
7828                },
7829            });
7830        }
7831        let inv = |base: f32| -> Vec<f32> {
7832            (0..cfg.rope_head_dim / 2)
7833                .map(|i| 1.0 / base.powf(2.0 * i as f32 / cfg.rope_head_dim as f32))
7834                .collect()
7835        };
7836        let g = Dsv4Globals {
7837            inv_freq_compress: inv(160000.0),
7838            inv_freq_window: inv(10000.0),
7839            embed: t(cfg.vocab, dim, 31),
7840            norm: ones(dim),
7841            head: t(cfg.vocab, dim, 33),
7842            hc_head_fn: w(hc * hc * dim, 35),
7843            hc_head_base: w(hc, 37),
7844            hc_head_scale: 1.0,
7845        };
7846        (g, layers, cfg)
7847    }
7848
7849    /// The whole stack, decoding a sequence. Every block is on the path:
7850    /// hyper-connections, the double-LoRA attention with its sink, the KV
7851    /// compressor firing on its ratio boundary, hash routing on one layer
7852    /// and score routing on the other.
7853    #[test]
7854    fn forward_token_decodes_a_sequence_without_falling_over() {
7855        let (g, layers, cfg) = toy();
7856        let mut st = Dsv4State::new(layers.len());
7857        let inv_freq: Vec<f32> = (0..cfg.rope_head_dim / 2)
7858            .map(|i| 1.0 / 10000f32.powf(2.0 * i as f32 / cfg.rope_head_dim as f32))
7859            .collect();
7860        let mut logits = Vec::new();
7861
7862        // Ten tokens: more than twice the compressor's ratio, so the
7863        // compressed cache is written on a boundary and read afterwards.
7864        let mut first: Option<Vec<f32>> = None;
7865        for (step, tok) in [3u32, 7, 1, 9, 4, 2, 8, 5, 6, 0].into_iter().enumerate() {
7866            forward_token(
7867                &g,
7868                &layers,
7869                &cfg,
7870                &mut st,
7871                tok,
7872                &inv_freq,
7873                None,
7874                &mut logits,
7875            );
7876            assert_eq!(logits.len(), cfg.vocab, "step {step}: logit count");
7877            assert!(
7878                logits.iter().all(|v| v.is_finite()),
7879                "step {step}: non-finite logit — {logits:?}"
7880            );
7881            // A model that has collapsed returns the same distribution
7882            // regardless of input; that is the failure this catches.
7883            let spread = logits.iter().cloned().fold(f32::MIN, f32::max)
7884                - logits.iter().cloned().fold(f32::MAX, f32::min);
7885            assert!(spread > 1e-6, "step {step}: logits are flat ({spread})");
7886            if step == 0 {
7887                first = Some(logits.clone());
7888            }
7889            assert_eq!(st.pos, step + 1, "position bookkeeping");
7890        }
7891
7892        // The cache has to have grown, and the compressor layer must have
7893        // emitted compressed entries (10 tokens / ratio 4 = 2 windows).
7894        assert!(!st.window[0].is_empty(), "sliding window never filled");
7895        // Ten tokens through a window of six: it must have slid, not grown.
7896        for (li, w) in st.window.iter().enumerate() {
7897            assert!(
7898                w.len() / cfg.head_dim <= cfg.window,
7899                "layer {li}: window holds {} positions, cap is {}",
7900                w.len() / cfg.head_dim,
7901                cfg.window
7902            );
7903        }
7904        assert!(
7905            !st.compressed[1].is_empty(),
7906            "compressor layer produced no compressed KV in 10 tokens"
7907        );
7908        // Ten tokens at ratio 4 fold twice, and the entries must be one head
7909        // wide — the overlapping projection is 2x that, so a width mistake
7910        // shows up here rather than as quiet nonsense.
7911        assert_eq!(
7912            st.compressed[1].len() / cfg.head_dim,
7913            2,
7914            "expected two folds in ten tokens at ratio 4"
7915        );
7916        assert!(
7917            !st.prev_kv[1].is_empty(),
7918            "the overlapping compressor never kept a previous window"
7919        );
7920        // Every layer that HAS an indexer must have filled the indexer's own
7921        // cache: it is what decides which compressed positions attention
7922        // reads, and an empty one silently discards the whole long-range
7923        // memory rather than failing.
7924        for (li, l) in layers.iter().enumerate() {
7925            if l.indexer.is_some() {
7926                assert!(
7927                    !st.index_kv[li].is_empty(),
7928                    "layer {li} has an indexer but its cache stayed empty"
7929                );
7930            }
7931        }
7932
7933        // Context must matter: the same token at position 0 of a fresh state
7934        // and at the end of a filled one cannot give identical logits.
7935        let mut fresh = Dsv4State::new(layers.len());
7936        let mut relogits = Vec::new();
7937        forward_token(
7938            &g,
7939            &layers,
7940            &cfg,
7941            &mut fresh,
7942            3,
7943            &inv_freq,
7944            None,
7945            &mut relogits,
7946        );
7947        assert_eq!(
7948            relogits,
7949            first.unwrap(),
7950            "the same token from a fresh state must reproduce exactly"
7951        );
7952    }
7953
7954    /// The reference clamps `up` on both sides but `gate` only from above.
7955    /// Getting that symmetric would quietly change every expert's output on
7956    /// the tokens that saturate, which is the hardest kind of bug to see.
7957    #[test]
7958    fn swiglu_limit_clamps_up_both_ways_and_gate_only_from_above() {
7959        let inter = 4;
7960        // gate = [-50, 50, 1, -1], up = [50, -50, 1, -1]
7961        let gate_src = [-50.0f32, 50.0, 1.0, -1.0];
7962        let up_src = [50.0f32, -50.0, 1.0, -1.0];
7963        let limit = 10.0f32;
7964        let mut got = vec![0.0f32; inter];
7965        expert_swiglu(
7966            &[0.0],
7967            &|_, d| d.copy_from_slice(&gate_src),
7968            &|_, d| d.copy_from_slice(&up_src),
7969            &|src, d| d.copy_from_slice(src),
7970            inter,
7971            1.0,
7972            limit,
7973            &mut got,
7974        );
7975        let silu = |g: f32| g / (1.0 + (-g).exp());
7976        // gate: only the +50 is cut, the -50 rides through silu untouched.
7977        let want = [
7978            silu(-50.0) * limit,
7979            silu(limit) * -limit,
7980            silu(1.0) * 1.0,
7981            -silu(-1.0),
7982        ];
7983        for (i, w) in want.iter().enumerate() {
7984            assert!(
7985                (got[i] - w).abs() < 1e-5,
7986                "lane {i}: got {} want {w}",
7987                got[i]
7988            );
7989        }
7990        // And with the clamp off nothing is touched.
7991        let mut raw = vec![0.0f32; inter];
7992        expert_swiglu(
7993            &[0.0],
7994            &|_, d| d.copy_from_slice(&gate_src),
7995            &|_, d| d.copy_from_slice(&up_src),
7996            &|src, d| d.copy_from_slice(src),
7997            inter,
7998            1.0,
7999            0.0,
8000            &mut raw,
8001        );
8002        assert!(
8003            (raw[1] - silu(50.0) * -50.0).abs() < 1e-3,
8004            "limit 0 must not clamp"
8005        );
8006    }
8007
8008    /// The grouped projection writes its intermediate from several threads
8009    /// at once. Disjoint indices are the whole argument for that being safe,
8010    /// so the pooled result has to equal the serial one exactly — a race
8011    /// here would show up as occasional wrong tokens, not as a crash.
8012    #[test]
8013    fn grouped_projection_is_identical_with_and_without_a_pool() {
8014        let (groups, lora, per_group, dim) = (4usize, 128usize, 64usize, 32usize);
8015        let attn: Vec<f32> = (0..groups * per_group)
8016            .map(|i| ((i * 13) as f32 * 0.021).sin())
8017            .collect();
8018        let wo_a: Vec<f32> = (0..groups * lora * per_group)
8019            .map(|i| ((i * 7) as f32 * 0.011).cos())
8020            .collect();
8021        let wo_b: Vec<f32> = (0..dim * groups * lora)
8022            .map(|i| ((i * 5) as f32 * 0.009).sin())
8023            .collect();
8024        let row = |r: usize, x: &[f32], _sc: &mut [f32]| -> f32 {
8025            wo_a[r * per_group..(r + 1) * per_group]
8026                .iter()
8027                .zip(x)
8028                .map(|(a, b)| a * b)
8029                .sum()
8030        };
8031        let project = |mid: &[f32], dst: &mut [f32]| {
8032            for (d, o) in dst.iter_mut().enumerate() {
8033                *o = wo_b[d * mid.len()..(d + 1) * mid.len()]
8034                    .iter()
8035                    .zip(mid)
8036                    .map(|(a, b)| a * b)
8037                    .sum();
8038            }
8039        };
8040
8041        let mut serial = vec![0.0f32; dim];
8042        o_project(
8043            &attn,
8044            &row,
8045            per_group,
8046            &project,
8047            groups,
8048            lora,
8049            None,
8050            &mut serial,
8051        );
8052
8053        let pool = crate::pool::Pool::new(4);
8054        let mut pooled = vec![0.0f32; dim];
8055        o_project(
8056            &attn,
8057            &row,
8058            per_group,
8059            &project,
8060            groups,
8061            lora,
8062            Some(&pool),
8063            &mut pooled,
8064        );
8065        assert_eq!(serial, pooled, "the pooled projection diverged");
8066        assert!(
8067            serial.iter().any(|v| v.abs() > 1e-6),
8068            "test data is degenerate"
8069        );
8070    }
8071
8072    #[test]
8073    fn block_grouped_projection_matches_position_walk() {
8074        let (_g, layers, cfg) = toy();
8075        let l = &layers[1];
8076        let b = 5;
8077        let attn_len = cfg.n_heads * cfg.head_dim;
8078        let attn: Vec<f32> = (0..b * attn_len)
8079            .map(|i| ((i * 17) as f32 * 0.013).sin())
8080            .collect();
8081        let mut walked = vec![0.0f32; b * cfg.dim];
8082        for bi in 0..b {
8083            o_project(
8084                &attn[bi * attn_len..(bi + 1) * attn_len],
8085                &|r, x, sc| l.wo_a.row_dot(r, x, sc),
8086                l.wo_a.cols(),
8087                &|mid, dst| l.wo_b.matvec(mid, dst, None),
8088                cfg.o_groups,
8089                cfg.o_lora_rank,
8090                None,
8091                &mut walked[bi * cfg.dim..(bi + 1) * cfg.dim],
8092            );
8093        }
8094        let mut batched = vec![0.0f32; b * cfg.dim];
8095        o_project_block(
8096            &attn,
8097            b,
8098            &l.wo_a,
8099            &l.wo_b,
8100            cfg.o_groups,
8101            cfg.o_lora_rank,
8102            None,
8103            &mut batched,
8104        );
8105        assert_eq!(batched, walked);
8106    }
8107
8108    #[test]
8109    fn block_moe_matches_position_walk_in_route_order() {
8110        let (_g, layers, cfg) = toy();
8111        // The scored layer exercises repeated and distinct experts without
8112        // tying the result to a token-id table.
8113        let l = &layers[1];
8114        let b = 5;
8115        let xs: Vec<f32> = (0..b * cfg.dim)
8116            .map(|i| ((i * 11) as f32 * 0.019).cos())
8117            .collect();
8118        let ids = [1u32, 2, 3, 4, 5];
8119        let mut walked = vec![0.0f32; b * cfg.dim];
8120        for bi in 0..b {
8121            moe_step(
8122                &xs[bi * cfg.dim..(bi + 1) * cfg.dim],
8123                l,
8124                &cfg,
8125                ids[bi],
8126                1,
8127                None,
8128                &mut walked[bi * cfg.dim..(bi + 1) * cfg.dim],
8129            );
8130        }
8131        let mut batched = vec![0.0f32; b * cfg.dim];
8132        moe_step_block(&xs, b, l, &cfg, &ids, 1, None, &mut batched);
8133        assert_eq!(batched, walked);
8134    }
8135
8136    /// The overlapping compressor folds 2*ratio slots, not ratio: the
8137    /// previous window contributes its first half of dimensions and the
8138    /// current one its second half. Treating it as a plain compressor makes
8139    /// the entry twice as wide as the cache expects, which lands the whole
8140    /// thing in the wrong store rather than raising anything.
8141    #[test]
8142    fn overlapping_compressor_folds_both_windows() {
8143        let (ratio, d) = (2usize, 3usize);
8144        // Current window: two tokens, 2*d wide each. Second half is what the
8145        // current window contributes.
8146        let cur_kv: Vec<f32> = vec![
8147            1.0, 1.0, 1.0, /*|*/ 10.0, 20.0, 30.0, // token 0
8148            2.0, 2.0, 2.0, /*|*/ 40.0, 50.0, 60.0, // token 1
8149        ];
8150        // Make the current window's second-half scores dominate everywhere.
8151        let cur_sc: Vec<f32> = vec![
8152            0.0, 0.0, 0.0, /*|*/ 0.0, 0.0, 100.0, //
8153            0.0, 0.0, 0.0, /*|*/ 100.0, 100.0, 0.0,
8154        ];
8155        // Previous window: its FIRST half is what it contributes.
8156        let prev_kv: Vec<f32> = vec![
8157            7.0, 8.0, 9.0, /*|*/ 0.0, 0.0, 0.0, //
8158            5.0, 6.0, 7.0, /*|*/ 0.0, 0.0, 0.0,
8159        ];
8160        let prev_sc = vec![0.0f32; ratio * 2 * d];
8161
8162        let mut out = vec![0.0f32; d];
8163        compress_window_overlap(&prev_kv, &prev_sc, &cur_kv, &cur_sc, ratio, d, &mut out);
8164        // dim 0 and 1: token 1's second half wins (score 100)
8165        assert!((out[0] - 40.0).abs() < 1e-3, "dim0 = {}", out[0]);
8166        assert!((out[1] - 50.0).abs() < 1e-3, "dim1 = {}", out[1]);
8167        // dim 2: token 0's second half wins
8168        assert!((out[2] - 30.0).abs() < 1e-3, "dim2 = {}", out[2]);
8169
8170        // With no previous window the fold still works and uses only the
8171        // current one — this is the very first window of a generation.
8172        let mut first = vec![0.0f32; d];
8173        compress_window_overlap(&[], &[], &cur_kv, &cur_sc, ratio, d, &mut first);
8174        assert!(
8175            first.iter().all(|v| v.is_finite()),
8176            "first window: {first:?}"
8177        );
8178        assert!((first[0] - 40.0).abs() < 1e-3, "first dim0 = {}", first[0]);
8179
8180        // And a previous window with real scores does pull the result.
8181        let mut both = vec![0.0f32; d];
8182        let strong_prev = vec![100.0f32; ratio * 2 * d];
8183        compress_window_overlap(
8184            &prev_kv,
8185            &strong_prev,
8186            &cur_kv,
8187            &cur_sc,
8188            ratio,
8189            d,
8190            &mut both,
8191        );
8192        assert!(
8193            (both[0] - 40.0).abs() > 1.0,
8194            "a scored previous window must move the fold, got {}",
8195            both[0]
8196        );
8197    }
8198
8199    /// Numerical parity with the reference. The vectors below come from
8200    /// running `kernel.py::hc_split_sinkhorn`'s own formula on a fixed
8201    /// input; matching them pins the exponent order, the eps placement and
8202    /// the off-by-one in the iteration count all at once — a property test
8203    /// alone would pass with any of those wrong.
8204    #[test]
8205    fn sinkhorn_matches_the_reference_numbers() {
8206        let hc = 4;
8207        let mixes: Vec<f32> = (0..24).map(|i| (i as f32 * 0.37).sin() * 3.0).collect();
8208        let base: Vec<f32> = (0..24).map(|i| (i as f32 * 0.11).cos()).collect();
8209        let (mut pre, mut post, mut comb) = (vec![0.0; hc], vec![0.0; hc], vec![0.0; hc * hc]);
8210        hc_split_sinkhorn(
8211            &mixes,
8212            &[1.0, 1.0, 1.0],
8213            &base,
8214            hc,
8215            20,
8216            1e-6,
8217            &mut pre,
8218            &mut post,
8219            &mut comb,
8220        );
8221        let want_pre = [0.7310596, 0.8888268, 0.9525191, 0.97424865];
8222        let want_post = [1.9600224, 1.9534285, 1.9201256, 1.8160983];
8223        let want_comb = [
8224            0.5996052,
8225            0.282_535_9,
8226            0.09218107,
8227            0.025676856,
8228            0.17564717,
8229            0.22228767,
8230            0.271_745_4,
8231            0.330_318_8,
8232            0.029528176,
8233            0.12206022,
8234            0.32619134,
8235            0.5222193,
8236            0.19521846,
8237            0.37311527,
8238            0.30988118,
8239            0.12178412,
8240        ];
8241        for (i, w) in want_pre.iter().enumerate() {
8242            assert!((pre[i] - w).abs() < 1e-5, "pre[{i}]: {} vs {w}", pre[i]);
8243        }
8244        for (i, w) in want_post.iter().enumerate() {
8245            assert!((post[i] - w).abs() < 1e-5, "post[{i}]: {} vs {w}", post[i]);
8246        }
8247        for (i, w) in want_comb.iter().enumerate() {
8248            assert!((comb[i] - w).abs() < 1e-4, "comb[{i}]: {} vs {w}", comb[i]);
8249        }
8250    }
8251
8252    /// Sinkhorn's whole point is a doubly stochastic matrix: every row and
8253    /// every column sums to one. If the alternating normalization is wrong
8254    /// (or the loop count is off by one) the sums drift, and the residual
8255    /// mixing quietly gains or loses mass on every layer.
8256    #[test]
8257    fn sinkhorn_leaves_the_mixing_matrix_doubly_stochastic() {
8258        let hc = 4;
8259        let mix_hc = (2 + hc) * hc;
8260        // a deliberately lopsided projection
8261        let mixes: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.37).sin() * 3.0).collect();
8262        let base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.11).cos()).collect();
8263        let (mut pre, mut post, mut comb) = (vec![0.0; hc], vec![0.0; hc], vec![0.0; hc * hc]);
8264        hc_split_sinkhorn(
8265            &mixes,
8266            &[1.0, 1.0, 1.0],
8267            &base,
8268            hc,
8269            20,
8270            1e-6,
8271            &mut pre,
8272            &mut post,
8273            &mut comb,
8274        );
8275        for j in 0..hc {
8276            let r: f32 = comb[j * hc..(j + 1) * hc].iter().sum();
8277            assert!((r - 1.0).abs() < 2e-3, "row {j} sums to {r}");
8278            let c: f32 = (0..hc).map(|k| comb[k * hc + j]).sum();
8279            assert!((c - 1.0).abs() < 2e-3, "col {j} sums to {c}");
8280        }
8281        // pre is a gate in (eps, 1+eps); post carries the factor 2
8282        assert!(pre.iter().all(|&v| v > 0.0 && v < 1.001));
8283        assert!(post.iter().all(|&v| (0.0..=2.0).contains(&v)));
8284    }
8285
8286    /// Folding four copies and expanding them back must preserve a constant
8287    /// state exactly when the block contributes nothing: with post = 0 the
8288    /// expansion is a doubly stochastic mix of identical copies, i.e. itself.
8289    #[test]
8290    fn expand_of_identical_copies_is_a_fixed_point() {
8291        let (hc, dim) = (4usize, 3usize);
8292        let residual: Vec<f32> = std::iter::repeat_n([1.5f32, -2.0, 0.25], hc)
8293            .flatten()
8294            .collect();
8295        let comb = {
8296            // exactly doubly stochastic: uniform
8297            vec![0.25f32; hc * hc]
8298        };
8299        let post = vec![0.0f32; hc];
8300        let mut out = vec![0.0f32; hc * dim];
8301        hc_expand(&[0.0; 3], &residual, &post, &comb, hc, dim, &mut out);
8302        for (o, r) in out.iter().zip(&residual) {
8303            assert!((o - r).abs() < 1e-6, "{o} vs {r}");
8304        }
8305    }
8306
8307    /// The bias must move the SELECTION without touching the weights: with a
8308    /// large bias on a low-scoring expert it gets picked, but its weight is
8309    /// still its own (small) score, renormalized.
8310    #[test]
8311    fn selection_bias_steers_the_choice_but_not_the_weights() {
8312        let scores = [3.0f32, 0.1, 2.0, 0.05];
8313        let bias = [0.0f32, 10.0, 0.0, 0.0];
8314        let (mut idx, mut w) = (Vec::new(), Vec::new());
8315        route(&scores, Some(&bias), 2, 1.5, None, None, &mut idx, &mut w);
8316        assert_eq!(idx[0], 1, "the biased expert must win selection");
8317        assert_eq!(idx[1], 0);
8318        // weights come from sqrt(softplus(score)) BEFORE the bias, so the
8319        // biased expert's share must be the smaller of the two
8320        assert!(w[0] < w[1], "biased expert kept its own (small) weight");
8321        let sum: f32 = w.iter().sum();
8322        assert!((sum - 1.5).abs() < 1e-5, "weights renormalize then scale");
8323    }
8324
8325    /// The sink is an extra logit with no value: it must lower every
8326    /// weight without adding output. With a huge sink the head should
8327    /// attend to almost nothing.
8328    #[test]
8329    fn attention_sink_drains_weight_without_contributing_output() {
8330        let hd = 2;
8331        let q = [1.0f32, 0.0];
8332        let kv = [1.0f32, 0.0, 0.0, 1.0];
8333        let mut out = vec![0.0f32; hd];
8334        sparse_attend(&q, &kv, &[0, 1], f32::NEG_INFINITY, 1.0, hd, &mut out);
8335        let plain = out.clone();
8336        assert!(plain[0] > plain[1], "the aligned key must dominate");
8337        sparse_attend(&q, &kv, &[0, 1], 20.0, 1.0, hd, &mut out);
8338        assert!(
8339            out[0] < plain[0] * 0.01 && out[1] < plain[1] * 0.01,
8340            "a large sink must drain nearly all the mass: {out:?}"
8341        );
8342    }
8343
8344    /// A masked slot must be ignored entirely — not folded in as a zero
8345    /// key, which would still add exp(0) to the denominator.
8346    #[test]
8347    fn masked_positions_leave_the_denominator_alone() {
8348        let hd = 2;
8349        let q = [1.0f32, 0.0];
8350        let kv = [1.0f32, 0.0, 0.0, 1.0];
8351        let (mut a, mut b) = (vec![0.0f32; hd], vec![0.0f32; hd]);
8352        sparse_attend(&q, &kv, &[0], f32::NEG_INFINITY, 1.0, hd, &mut a);
8353        sparse_attend(
8354            &q,
8355            &kv,
8356            &[0, usize::MAX],
8357            f32::NEG_INFINITY,
8358            1.0,
8359            hd,
8360            &mut b,
8361        );
8362        for (x, y) in a.iter().zip(&b) {
8363            assert!((x - y).abs() < 1e-6, "{x} vs {y}");
8364        }
8365    }
8366
8367    /// Forward then inverse rotation is the identity — the property the
8368    /// output path depends on.
8369    #[test]
8370    fn rope_tail_inverts_itself() {
8371        let inv_freq = [1.0f32, 0.5];
8372        let orig = [9.0f32, 8.0, 1.0, 2.0, 3.0, 4.0];
8373        let mut v = orig;
8374        rope_tail(&mut v, &inv_freq, 7, 4, false);
8375        assert!(v[..2] == orig[..2], "the non-rope head must not move");
8376        assert!(v[2..] != orig[2..], "the tail must actually rotate");
8377        rope_tail(&mut v, &inv_freq, 7, 4, true);
8378        for (a, b) in v.iter().zip(&orig) {
8379            assert!((a - b).abs() < 1e-5, "{a} vs {b}");
8380        }
8381    }
8382
8383    /// The window pooling is a softmax per DIMENSION over the ratio, with
8384    /// the position bias inside the exponent.
8385    #[test]
8386    fn compressor_pools_the_window_per_dimension() {
8387        let (ratio, width) = (2usize, 2usize);
8388        let kv = [1.0f32, 10.0, 3.0, 20.0];
8389        // dim 0: equal scores → mean; dim 1: second token wins by a mile
8390        let score = [0.0f32, 0.0, 0.0, 50.0];
8391        let ape = vec![0.0f32; ratio * width];
8392        let mut out = vec![0.0f32; width];
8393        compress_window(&kv, &score, &ape, ratio, width, &mut out);
8394        assert!(
8395            (out[0] - 2.0).abs() < 1e-5,
8396            "equal scores average: {}",
8397            out[0]
8398        );
8399        assert!(
8400            (out[1] - 20.0).abs() < 1e-3,
8401            "a dominant score wins: {}",
8402            out[1]
8403        );
8404    }
8405
8406    /// A negative dot product must not drag a position down: the relu
8407    /// means heads abstain rather than veto.
8408    #[test]
8409    fn index_scores_relu_before_weighting() {
8410        let (nh, hd) = (2usize, 2usize);
8411        // head 0 aligns with position 0, head 1 anti-aligns with it
8412        let q = [1.0f32, 0.0, -1.0, 0.0];
8413        let kv = [1.0f32, 0.0, 0.0, 1.0];
8414        let w = [1.0f32, 1.0];
8415        let mut sc = Vec::new();
8416        index_scores(&q, &kv, &w, nh, hd, 2, 2, None, &mut sc);
8417        // without the relu the anti-aligned head would cancel head 0 to zero
8418        assert!(sc[0] > 0.9, "abstention, not veto: {:?}", sc);
8419    }
8420
8421    #[test]
8422    fn index_scores_mask_the_future() {
8423        let (nh, hd) = (1usize, 2usize);
8424        let q = [1.0f32, 0.0];
8425        let kv = [1.0f32, 0.0, 1.0, 0.0, 1.0, 0.0];
8426        let w = [1.0f32];
8427        let mut sc = Vec::new();
8428        index_scores(&q, &kv, &w, nh, hd, 3, 2, None, &mut sc);
8429        assert!(sc[0].is_finite() && sc[1].is_finite());
8430        assert!(sc[2] == f32::NEG_INFINITY, "position 2 is in the future");
8431        let mut idx = Vec::new();
8432        top_k_positions(&sc, 3, &mut idx);
8433        assert_eq!(idx, vec![0, 1], "a masked slot never wins a slot");
8434    }
8435
8436    #[test]
8437    fn top_k_is_deterministic_on_ties() {
8438        let sc = [1.0f32, 1.0, 1.0, 0.0];
8439        let mut idx = Vec::new();
8440        top_k_positions(&sc, 2, &mut idx);
8441        assert_eq!(idx, vec![0, 1], "ties resolve to the lower index");
8442    }
8443
8444    /// The block cycle must leave the state's SHAPE intact (hc copies in,
8445    /// hc copies out) and must actually route the block's output back in:
8446    /// a block that writes a constant has to move every copy.
8447    #[test]
8448    fn hc_block_preserves_the_copy_structure_and_applies_the_block() {
8449        let cfg = Dsv4Cfg {
8450            dim: 4,
8451            n_heads: 1,
8452            head_dim: 4,
8453            rope_head_dim: 2,
8454            q_lora_rank: 4,
8455            o_lora_rank: 2,
8456            o_groups: 1,
8457            hc_mult: 4,
8458            hc_sinkhorn_iters: 20,
8459            hc_eps: 1e-6,
8460            norm_eps: 1e-6,
8461            n_routed_experts: 2,
8462            top_k: 1,
8463            moe_inter: 4,
8464            route_scale: 1.0,
8465            swiglu_limit: 10.0,
8466            window: 128,
8467            index_topk: 4,
8468            vocab: 8,
8469        };
8470        let (hc, dim) = (cfg.hc_mult, cfg.dim);
8471        let mix_hc = (2 + hc) * hc;
8472        let hc_fn: Vec<f32> = (0..mix_hc * hc * dim)
8473            .map(|i| ((i % 13) as f32 - 6.0) * 0.05)
8474            .collect();
8475        let hc_base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.2).sin()).collect();
8476        let norm_w = vec![1.0f32; dim];
8477        let mut state: Vec<f32> = (0..hc * dim).map(|i| (i as f32 * 0.3).cos()).collect();
8478        let before = state.clone();
8479        let mut scratch = HcScratch::new(&cfg);
8480        hc_block(
8481            &mut state,
8482            &hc_fn,
8483            &[1.0, 1.0, 1.0],
8484            &hc_base,
8485            &norm_w,
8486            &cfg,
8487            &mut scratch,
8488            None,
8489            |_folded, out: &mut [f32]| out.iter_mut().for_each(|o| *o = 1.0),
8490        );
8491        assert_eq!(state.len(), before.len(), "copy structure must survive");
8492        assert!(state.iter().all(|v| v.is_finite()), "{state:?}");
8493        assert!(
8494            state.iter().zip(&before).any(|(a, b)| (a - b).abs() > 1e-4),
8495            "the block's output has to reach the state"
8496        );
8497    }
8498
8499    #[test]
8500    fn hash_route_reads_the_table_row() {
8501        // vocab 3, top_k 2
8502        let table = [7.0f32, 9.0, 1.0, 2.0, 5.0, 6.0];
8503        assert_eq!(hash_route(&table, 3, 2, 0), vec![7, 9]);
8504        assert_eq!(hash_route(&table, 3, 2, 2), vec![5, 6]);
8505        // out-of-range ids clamp instead of panicking
8506        assert_eq!(hash_route(&table, 3, 2, 99), vec![5, 6]);
8507    }
8508
8509    /// A task mask restricts SELECTION and nothing else: the weights still
8510    /// come from the pre-bias scores and still renormalize, now over what
8511    /// survives. Masking must never reroute — an expert the mask forbids has
8512    /// to be absent, not replaced by a neighbour with the wrong weight.
8513    #[test]
8514    fn a_task_mask_restricts_selection_and_renormalizes() {
8515        // Expert 3 scores highest, then 1, then 2, then 0.
8516        let scores = [0.1f32, 4.0, 1.0, 9.0];
8517        let (mut idx, mut w) = (Vec::new(), Vec::new());
8518        route(&scores, None, 2, 1.0, None, None, &mut idx, &mut w);
8519        assert_eq!(idx, vec![3, 1], "unmasked: the two best win");
8520        let sum: f32 = w.iter().sum();
8521        assert!((sum - 1.0).abs() < 1e-5, "weights must sum to route_scale");
8522
8523        // Forbid the winner: the next two take its place and the weights
8524        // renormalize over them.
8525        let mask = [true, false, true, true];
8526        let (mut i2, mut w2) = (Vec::new(), Vec::new());
8527        route(&scores, None, 2, 1.0, None, Some(&mask), &mut i2, &mut w2);
8528        assert_eq!(i2, vec![3, 2], "masked expert must not be selected");
8529        let sum2: f32 = w2.iter().sum();
8530        assert!((sum2 - 1.0).abs() < 1e-5, "masked weights must renormalize");
8531
8532        // A mask leaving fewer than top_k experts yields fewer, not garbage.
8533        let tight = [false, false, false, true];
8534        let (mut i3, mut w3) = (Vec::new(), Vec::new());
8535        route(&scores, None, 2, 1.0, None, Some(&tight), &mut i3, &mut w3);
8536        assert_eq!(i3, vec![3]);
8537        assert_eq!(w3.len(), 1);
8538    }
8539
8540    /// On a hash layer the reference gathers the scores AT THE TABLE's
8541    /// experts. Choosing top-k first and swapping the indices afterwards
8542    /// leaves every weight attached to a different expert than the one it
8543    /// scales — silently, since both lists are the right length.
8544    #[test]
8545    fn hash_layers_weight_the_experts_the_table_names() {
8546        // Expert 3 scores highest, expert 0 lowest; the table names 0 and 1.
8547        let scores = [0.1f32, 0.4, 0.2, 5.0];
8548        let table = vec![0.0f32, 1.0];
8549        let idx_forced = hash_route(&table, 1, 2, 0);
8550        assert_eq!(idx_forced, vec![0, 1]);
8551
8552        let (mut idx, mut w) = (Vec::new(), Vec::new());
8553        route(
8554            &scores,
8555            None,
8556            2,
8557            1.0,
8558            Some(&idx_forced),
8559            None,
8560            &mut idx,
8561            &mut w,
8562        );
8563        assert_eq!(idx, vec![0, 1], "the table must decide the experts");
8564
8565        // The weights must be the table experts' own scores, normalized.
8566        let sp = |x: f32| (1.0 + x.exp()).ln().sqrt();
8567        let (s0, s1) = (sp(scores[0]), sp(scores[1]));
8568        let tot = s0 + s1;
8569        assert!(
8570            (w[0] - s0 / tot).abs() < 1e-6,
8571            "w[0]={} want {}",
8572            w[0],
8573            s0 / tot
8574        );
8575        assert!(
8576            (w[1] - s1 / tot).abs() < 1e-6,
8577            "w[1]={} want {}",
8578            w[1],
8579            s1 / tot
8580        );
8581
8582        // And the top-k path is untouched: expert 3 still wins there.
8583        let (mut idx2, mut w2) = (Vec::new(), Vec::new());
8584        route(&scores, None, 2, 1.0, None, None, &mut idx2, &mut w2);
8585        assert_eq!(idx2[0], 3, "without a table the highest score still wins");
8586    }
8587}
8588
8589// ══ DSpark: the block-parallel draft ══════════════════════════════════
8590//
8591// Not a classic MTP chain. One pass through the three stages produces the
8592// WHOLE block of `block_size` positions at once: position 0 carries the token
8593// the trunk just emitted, the rest carry a noise token, and every position
8594// attends to every other one — which is why the block cannot be measured a
8595// position at a time and pretend to be faithful. Depth comes from the block,
8596// not from the stage count.
8597//
8598// The stages' KV cache is built from the trunk's hidden state, not from the
8599// draft's own tokens: one entry per real position, `kv_norm(wkv(main_x))`,
8600// in a ring of `window`. The block's own keys and values are appended for
8601// the duration of the block and then discarded.
8602
8603/// The noise token the block's unknown positions carry
8604/// (`dspark_noise_token_id`).
8605pub const DSPARK_NOISE_TOKEN: u32 = 128799;
8606/// `dspark_block_size` — the width of the draft block, and NOT a tuning knob.
8607///
8608/// All five positions attend to each other and the model was trained with
8609/// exactly four noise slots behind the real token, so a narrower block is a
8610/// different draft model, not a cheaper one. What the survival curve argues
8611/// for is verifying fewer of the five — see `dspark_verify_k` — which costs
8612/// less without changing what the draft computes.
8613pub fn dspark_block() -> usize {
8614    5
8615}
8616
8617/// How many of the block's proposals the trunk actually checks.
8618///
8619/// Survival is [0.67, 0.50, 0.29, 0.08, 0.04]: positions four and five are
8620/// paid for on every verify and delivered on a twelfth of them. Three yields
8621/// 2.46 tokens a cycle against five's 2.58, for three fifths of the verify.
8622/// `CMF_DSPARK_VERIFY_K=N` sets it.
8623#[cfg(feature = "gpu")]
8624fn dspark_native_on() -> bool {
8625    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8626    *ON.get_or_init(|| {
8627        std::env::var("CMF_DSPARK_NATIVE")
8628            .map(|v| v != "0")
8629            // The q4 checkpoint's native draft accepts far more proposals
8630            // than its upload-time q4→q2 recode. A q2 file stays q2 below.
8631            .unwrap_or(true)
8632    })
8633}
8634
8635pub fn dspark_verify_k() -> usize {
8636    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
8637    *K.get_or_init(|| {
8638        std::env::var("CMF_DSPARK_VERIFY_K")
8639            .ok()
8640            .and_then(|v| v.parse::<usize>().ok())
8641            .filter(|&n| (1..=DSPARK_BLOCK_MAX).contains(&n))
8642            .unwrap_or(DSPARK_BLOCK_MAX)
8643    })
8644}
8645
8646/// The trained block width.
8647pub const DSPARK_BLOCK_MAX: usize = 5;
8648
8649/// Per-sequence state of the draft: one KV ring per stage, and the trunk
8650/// hidden states the block's input is projected from.
8651pub struct DsparkState {
8652    /// `[stage][window * kv_width]`, written at `pos % window`.
8653    pub win: Vec<Vec<f32>>,
8654    /// How many real positions each ring holds, capped at `window`.
8655    pub filled: Vec<usize>,
8656    /// The trunk's captured hidden, `dim * n_targets`, refreshed every token.
8657    pub main_hidden: Vec<f32>,
8658    /// True once `main_hidden` holds this position's capture.
8659    pub have_hidden: bool,
8660}
8661
8662impl DsparkState {
8663    pub fn new(stages: usize, cfg: &Dsv4Cfg, targets: usize) -> Self {
8664        Self {
8665            win: vec![Vec::new(); stages],
8666            filled: vec![0; stages],
8667            main_hidden: vec![0.0; cfg.dim * targets],
8668            have_hidden: false,
8669        }
8670    }
8671}
8672
8673/// Which trunk layers the draft reads. Upstream names them explicitly
8674/// (`dspark_target_layer_ids`); the file says the same thing less directly —
8675/// `main_proj` has one `dim`-wide input block per captured layer — and the
8676/// release captures the last three. Deriving it from the weight keeps the
8677/// two from disagreeing.
8678pub fn dspark_targets(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg, n_layers: usize) -> Vec<usize> {
8679    let Some(mp) = mtp.iter().find_map(|m| m.main_proj.as_ref()) else {
8680        return Vec::new();
8681    };
8682    let n = (mp.cols() / cfg.dim.max(1)).clamp(1, n_layers);
8683    (n_layers - n..n_layers).collect()
8684}
8685
8686thread_local! {
8687    /// The armed capture: which layers to take, and the buffer they fill.
8688    /// A thread-local rather than a parameter because the capture has to
8689    /// reach into the middle of a layer loop that eight call sites share,
8690    /// and threading an optional buffer through all of them to serve one
8691    /// diagnostic is a worse trade than this.
8692    static DSPARK_CAP: std::cell::RefCell<(Vec<usize>, Vec<f32>, usize)> =
8693        const { std::cell::RefCell::new((Vec::new(), Vec::new(), 0)) };
8694}
8695
8696/// Arm the capture for the layers `targets`, in order.
8697pub fn dspark_arm(targets: &[usize], dim: usize) {
8698    DSPARK_CAP.with(|c| {
8699        let mut c = c.borrow_mut();
8700        c.0 = targets.to_vec();
8701        c.1 = vec![0.0; dim * targets.len()];
8702        c.2 = 0;
8703    });
8704}
8705
8706/// Whether the armed MTP capture needs the state immediately after `li`.
8707/// The normal decode path keeps a full run in one submission; DSpark is the
8708/// only caller that needs an intermediate state to cross the device boundary.
8709fn dspark_wants(li: usize) -> bool {
8710    DSPARK_CAP.with(|c| c.borrow().0.contains(&li))
8711}
8712
8713/// Called after every host layer. Free when nothing is armed.
8714pub fn dspark_note(li: usize, state: &[f32], cfg: &Dsv4Cfg) {
8715    DSPARK_CAP.with(|c| {
8716        let mut c = c.borrow_mut();
8717        if c.0.is_empty() {
8718            return;
8719        }
8720        if let Some(slot) = c.0.iter().position(|&t| t == li) {
8721            let (_, buf, seen) = &mut *c;
8722            dspark_capture(state, cfg, slot, buf);
8723            // Counted, not "was the last one" — under the device chain only
8724            // the layers left on the host call this, and taking the last
8725            // target as the signal would hand the draft a buffer whose other
8726            // slots still hold the previous token, or nothing at all.
8727            *seen = if slot == 0 { 1 } else { *seen + 1 };
8728            if std::env::var("CMF_DSPARK_CAP_DBG").is_ok() {
8729                eprintln!("[cap] note li={li} slot={slot} seen={}", *seen);
8730            }
8731        }
8732    });
8733}
8734
8735/// Read one slot of the armed capture buffer as-is, complete or not. The
8736/// speculative verify fills the DEVICE targets from its own photographs and
8737/// only needs the host layers' slots from here — `dspark_take`'s
8738/// completeness contract would never be met on that path.
8739pub fn dspark_peek_slot(slot: usize, dim: usize, out: &mut [f32]) -> bool {
8740    DSPARK_CAP.with(|c| {
8741        let c = c.borrow();
8742        let lo = slot * dim;
8743        if c.1.len() < lo + dim {
8744            return false;
8745        }
8746        out[..dim].copy_from_slice(&c.1[lo..lo + dim]);
8747        true
8748    })
8749}
8750
8751/// Move the capture out, if this token produced a complete one.
8752pub fn dspark_take(out: &mut Vec<f32>) -> bool {
8753    DSPARK_CAP.with(|c| {
8754        let mut c = c.borrow_mut();
8755        if c.0.is_empty() || c.2 != c.0.len() {
8756            if std::env::var("CMF_DSPARK_CAP_DBG").is_ok() {
8757                eprintln!("[cap] take FAIL armed={:?} seen={}", c.0, c.2);
8758            }
8759            return false;
8760        }
8761        out.clear();
8762        out.extend_from_slice(&c.1);
8763        c.2 = 0;
8764        true
8765    })
8766}
8767
8768/// The trunk's contribution: the mean over the hyper-connection copies,
8769/// appended in target order. Costs one pass over `hc * dim` per captured
8770/// layer and nothing else.
8771pub fn dspark_capture(state: &[f32], cfg: &Dsv4Cfg, slot: usize, out: &mut [f32]) {
8772    let (hc, dim) = (cfg.hc_mult, cfg.dim);
8773    let dst = &mut out[slot * dim..(slot + 1) * dim];
8774    let inv = 1.0 / hc as f32;
8775    for d in 0..dim {
8776        let mut s = 0.0;
8777        for j in 0..hc {
8778            s += state[j * dim + d];
8779        }
8780        dst[d] = s * inv;
8781    }
8782}
8783
8784/// `CMF_DSPARK_PICK_DUMP=path` — accumulate the draft's expert picks per
8785/// stage and periodically rewrite `path` with `stage<TAB>expert<TAB>count`
8786/// lines. Rewritten every 32 blocks rather than at exit, so a run that is
8787/// killed still leaves the tallies on disk.
8788pub fn dspark_freq_note(picks: &[(usize, Vec<usize>)]) {
8789    static FREQ: std::sync::Mutex<Option<(std::collections::HashMap<(usize, usize), u64>, u64)>> =
8790        std::sync::Mutex::new(None);
8791    let Ok(path) = std::env::var("CMF_DSPARK_PICK_DUMP") else {
8792        return;
8793    };
8794    let mut g = FREQ.lock().unwrap();
8795    let (map, blocks) = g.get_or_insert_with(|| (std::collections::HashMap::new(), 0));
8796    for (stage, idx) in picks {
8797        for &e in idx {
8798            *map.entry((*stage, e)).or_insert(0) += 1;
8799        }
8800    }
8801    *blocks += 1;
8802    if *blocks % 32 == 0 {
8803        let mut lines: Vec<_> = map.iter().collect();
8804        lines.sort();
8805        let body: String = lines
8806            .iter()
8807            .map(|((s, e), n)| format!("{s}\t{e}\t{n}\n"))
8808            .collect();
8809        let _ = std::fs::write(&path, body);
8810    }
8811}
8812
8813/// `CMF_DSV4_TRUNK_PICK_DUMP=path` — the same tally for the TRUNK's layers:
8814/// `layer<TAB>expert<TAB>count`, rewritten every 32 tokens. The pick lists
8815/// come from the probe's own tally window, so only layers that route on the
8816/// host are counted — which is exactly the population a partial pack serves.
8817pub fn trunk_freq_note(picks: &[(usize, Vec<usize>)]) {
8818    static FREQ: std::sync::Mutex<Option<(std::collections::HashMap<(usize, usize), u64>, u64)>> =
8819        std::sync::Mutex::new(None);
8820    let Ok(path) = std::env::var("CMF_DSV4_TRUNK_PICK_DUMP") else {
8821        return;
8822    };
8823    let mut g = FREQ.lock().unwrap();
8824    let (map, blocks) = g.get_or_insert_with(|| (std::collections::HashMap::new(), 0));
8825    for (li, idx) in picks {
8826        for &e in idx {
8827            *map.entry((*li, e)).or_insert(0) += 1;
8828        }
8829    }
8830    *blocks += 1;
8831    if *blocks % 32 == 0 {
8832        let mut lines: Vec<_> = map.iter().collect();
8833        lines.sort();
8834        let body: String = lines
8835            .iter()
8836            .map(|((l, e), n)| format!("{l}\t{e}\t{n}\n"))
8837            .collect();
8838        let _ = std::fs::write(&path, body);
8839    }
8840}
8841
8842/// `CMF_DSPARK_MASK=path` — restrict the draft's routed experts to an
8843/// explicit per-stage keep-set: line `d` of the file lists the expert ids
8844/// stage `d` may route to, comma-separated. Weights renormalize over what
8845/// remains (the `Dsv4Layer::mask` contract). The draft only proposes — the
8846/// trunk still verifies every token — so a thinner draft costs acceptance,
8847/// never correctness. This is the offline dial for sizing a resident
8848/// device pack before one exists.
8849fn dspark_apply_mask(out: &mut [Dsv4Mtp]) {
8850    let Ok(path) = std::env::var("CMF_DSPARK_MASK") else {
8851        return;
8852    };
8853    let Ok(text) = std::fs::read_to_string(&path) else {
8854        eprintln!("DSpark: CMF_DSPARK_MASK={path} не читается — маска не применена");
8855        return;
8856    };
8857    for (d, line) in text.lines().enumerate() {
8858        let Some(m) = out.get_mut(d) else { break };
8859        let n = m.layer.experts.len();
8860        let mut mask = vec![false; n];
8861        let mut kept = 0usize;
8862        for tok in line.split(',') {
8863            if let Ok(e) = tok.trim().parse::<usize>() {
8864                if e < n && !mask[e] {
8865                    mask[e] = true;
8866                    kept += 1;
8867                }
8868            }
8869        }
8870        if kept == 0 {
8871            continue;
8872        }
8873        eprintln!("DSpark: стадия {d} ограничена {kept}/{n} экспертами");
8874        m.layer.mask = Some(mask);
8875    }
8876}
8877
8878/// The draft's device residency: which experts of each stage live on the
8879/// card, and how the device router reaches them.
8880///
8881/// The draft only proposes — the trunk verifies every token — so the pack
8882/// is free to keep a SUBSET of each stage's experts and mask the routing to
8883/// it: acceptance pays, correctness never does. The subset is chosen by
8884/// measured routing frequency (`CMF_DSPARK_PACK` names the tally file that
8885/// `CMF_DSPARK_PICK_DUMP` wrote; `CMF_DSPARK_RESIDENT` caps experts per
8886/// stage, default 48).
8887#[cfg(feature = "gpu")]
8888pub struct DsparkPack {
8889    pub stages: Vec<DsparkStagePack>,
8890    /// Gate/up requantized to q2tp at upload (the binary registered an
8891    /// encoder); the graph then dispatches the q2tp kernels.
8892    pub gu_q2: bool,
8893    /// The down planes too (native in the file, never requantized at
8894    /// upload); the graph dispatches the 2-bit down kernel.
8895    pub dn_q2: bool,
8896    /// Dequantized router and bias per stage, f32 — address-stable for the
8897    /// life of the pack, which is what the device's const cache needs.
8898    pub routers: Vec<Vec<f32>>,
8899    pub biases: Vec<Option<Vec<f32>>>,
8900}
8901
8902#[cfg(feature = "gpu")]
8903pub struct DsparkStagePack {
8904    /// Selectable experts (true = resident).
8905    pub mask: Vec<bool>,
8906    /// Global expert id → pack slot; usize::MAX where cold.
8907    pub to_slot: Vec<usize>,
8908    /// The same two as the device consumes them — u32, address-stable for
8909    /// the pack's lifetime (the const cache keys on the pointer).
8910    pub mask_u32: Vec<u32>,
8911    pub map_u32: Vec<u32>,
8912    /// (gate, up, down) directory indices, pack order, shared LAST.
8913    pub tensors: Vec<(usize, usize, usize)>,
8914    pub n_resident: usize,
8915}
8916
8917/// The q2tp encoder, registered by the binary that has one (the CLI's
8918/// converter owns the rung-search implementation and the engine must not
8919/// depend on the CLI). When present, the draft's gate/up experts are
8920/// requantized q4tp → q2tp AT UPLOAD — half the VRAM and the same kernels
8921/// the trunk's q2tp experts already use. Draft-only fidelity: acceptance
8922/// pays, correctness never does.
8923pub static DSPARK_Q2TP_ENCODE: std::sync::OnceLock<fn(&[f32], usize, usize) -> Vec<u8>> =
8924    std::sync::OnceLock::new();
8925
8926/// `CMF_DSPARK_GPU=1` — the probe (and later the speculative loop) drafts
8927/// on the card instead of the CPU/disk tier.
8928#[cfg(feature = "gpu")]
8929pub fn dspark_gpu_on() -> bool {
8930    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8931    *ON.get_or_init(|| {
8932        std::env::var("CMF_DSPARK_GPU")
8933            .map(|v| v != "0")
8934            .unwrap_or(true)
8935    })
8936}
8937
8938/// The pack, built once per process (the stand runs one model).
8939#[cfg(feature = "gpu")]
8940pub fn dspark_pack_get(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg) -> Option<&'static DsparkPack> {
8941    static P: std::sync::OnceLock<Option<Box<DsparkPack>>> = std::sync::OnceLock::new();
8942    P.get_or_init(|| dspark_pack_build(mtp, cfg).map(Box::new))
8943        .as_deref()
8944}
8945
8946/// Build and upload the draft's pack. Returns `None` when the stack is
8947/// absent, the budget refuses, or a stage's weights are not where the
8948/// device path needs them — the caller falls back to the CPU draft.
8949/// Reserve the VRAM the speculative draft's device pack will take, so the
8950/// trunk's greedy packing leaves it room. Called at load, before any trunk
8951/// pack is built; a no-op when there is no MTP stack or speculation is off.
8952/// The estimate uses the draft's native dtypes — an upload-time re-encode
8953/// only shrinks it, which errs on the safe side of the physical ceiling.
8954///
8955/// A budget that cannot pack the trunk to the draft's capture layers gets NO
8956/// reservation.  Host-batch verify is exact there, but the measured A40 q4tp
8957/// result is 0.63 tok/s versus the faster ordinary exact walk: reserving the
8958/// draft shrinks every trunk layer and makes speculation a net loss.  The
8959/// threshold is geometric (nine tenths of the trunk's own expert bytes plus
8960/// the draft), never a card name.
8961#[cfg(feature = "gpu")]
8962pub fn dspark_reserve_note(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg, layers: &[Dsv4Layer]) {
8963    if mtp.is_empty() || std::env::var("CMF_DSV4_SPEC").is_ok_and(|v| v == "0") || !dspark_gpu_on()
8964    {
8965        return;
8966    }
8967    // `=1` is the diagnostic force path used to measure a configuration the
8968    // zero-knob geometric gate would reject.  Production auto-selection keeps
8969    // the gate below; forcing must reserve before the trunk is packed or the
8970    // late draft upload simply OOMs.
8971    let forced = std::env::var("CMF_DSV4_SPEC").is_ok_and(|v| v == "1");
8972    let dt = |q2: bool| {
8973        if q2 {
8974            cortiq_core::TensorDtype::Q2TiledP
8975        } else {
8976            cortiq_core::TensorDtype::Q4TiledP
8977        }
8978    };
8979    let gu_q2 = mtp[0]
8980        .layer
8981        .experts
8982        .first()
8983        .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
8984    let dn_q2 = mtp[0]
8985        .layer
8986        .experts
8987        .first()
8988        .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
8989    let gu = cortiq_core::quant::expected_nbytes(dt(gu_q2), &[cfg.moe_inter, cfg.dim]).unwrap_or(0);
8990    let dn = cortiq_core::quant::expected_nbytes(dt(dn_q2), &[cfg.dim, cfg.moe_inter]).unwrap_or(0);
8991    let per = (2 * gu + dn) as u64;
8992    let n_res: usize = std::env::var("CMF_DSPARK_RESIDENT")
8993        .ok()
8994        .and_then(|v| v.parse().ok())
8995        // The default matches the measured acceptance plateau's low edge:
8996        // residency below it costs acceptance, above it only costs VRAM.
8997        .unwrap_or(40);
8998    // Routed residents per stage, plus each stage's shared expert.
8999    let bytes = per * (n_res * mtp.len() + mtp.len() + 1) as u64;
9000    // The trunk's own expert bytes, from the route it will actually serve.
9001    // A task-specialist mask changes the physical working set: counting all
9002    // 256 rows here made a compact, fully resident masked trunk look like the
9003    // 158 GB general model, so the zero-knob gate silently disabled the draft
9004    // that is responsible for the second half of its speedup.  Hash layers
9005    // deliberately have no mask and still count every checkpoint-named row.
9006    // Keep the production gate used by the zero-knob path: if nearly all of
9007    // the effective trunk plus the draft cannot fit, spend the whole budget
9008    // on trunk slots.
9009    let trunk: u64 = layers
9010        .iter()
9011        .map(|l| {
9012            let Some(e) = l.experts.first() else {
9013                return 0;
9014            };
9015            let gu = cortiq_core::quant::expected_nbytes(
9016                dt(e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
9017                &[cfg.moe_inter, cfg.dim],
9018            )
9019            .unwrap_or(0);
9020            let dn = cortiq_core::quant::expected_nbytes(
9021                dt(e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
9022                &[cfg.dim, cfg.moe_inter],
9023            )
9024            .unwrap_or(0);
9025            let routed = l
9026                .mask
9027                .as_deref()
9028                .map_or(l.experts.len(), |m| m.iter().filter(|&&open| open).count());
9029            ((2 * gu + dn) * (routed + 1)) as u64
9030        })
9031        .sum();
9032    if let Some(budget) = crate::gpu_wgpu::dsv4_vram_budget() {
9033        if !forced && budget < trunk / 10 * 9 + bytes {
9034            return;
9035        }
9036    }
9037    // The pack is not the draft's whole physical footprint.  Its three
9038    // attention skeletons, block-axis activations, captures and retained
9039    // verify states are ordinary wgpu allocations and therefore do not
9040    // appear in the resident-weight ledger.  Keeping only `bytes` here made
9041    // q4tp fit on paper and then panic the A40 driver while building DSpark.
9042    // A geometry-scaled workspace (bounded to 512..1024 MiB) is separate from
9043    // the expert reservation: dsv4_draft_fit must hand back only PACK bytes,
9044    // never turn scratch headroom into more resident experts.
9045    let mib = 1024 * 1024u64;
9046    let workspace = match crate::gpu_wgpu::dsv4_vram_budget() {
9047        // Smaller discrete heaps have less slack between the reported weight
9048        // ceiling and the driver's physical allocation ceiling.  One GiB is
9049        // still only ~2% of an A40 and is cheaper than an OOM/restart.
9050        Some(b) if b <= 64 * 1024 * mib => 1024 * mib,
9051        _ => ((cfg.dim * cfg.hc_mult * DSPARK_BLOCK_MAX * 4096) as u64)
9052            .clamp(512 * mib, 1024 * mib),
9053    };
9054    crate::gpu_wgpu::DRAFT_PACK_RESERVE.store(bytes, std::sync::atomic::Ordering::Relaxed);
9055    crate::gpu_wgpu::DRAFT_RESERVE
9056        .store(bytes.saturating_add(workspace), std::sync::atomic::Ordering::Relaxed);
9057}
9058
9059#[cfg(not(feature = "gpu"))]
9060pub fn dspark_reserve_note(_mtp: &[Dsv4Mtp], _cfg: &Dsv4Cfg, _layers: &[Dsv4Layer]) {}
9061
9062#[cfg(feature = "gpu")]
9063pub fn dspark_pack_build(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg) -> Option<DsparkPack> {
9064    if mtp.is_empty() {
9065        return None;
9066    }
9067    let n_res: usize =
9068        std::env::var("CMF_DSPARK_RESIDENT")
9069            .ok()
9070            .and_then(|v| v.parse().ok())
9071            .unwrap_or_else(|| {
9072                // No knob: take what the card actually has left, whatever the
9073                // card is. The stages split the fit evenly after their shared
9074                // experts. Forty is the measured acceptance plateau: larger
9075                // packs still make the router and upload more rows without a
9076                // useful increase in accepted tokens (64 was slower on A40).
9077                let native_q2 = mtp[0].layer.experts.first().is_some_and(|e| {
9078                    e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
9079                });
9080                let gu_q2 =
9081                    native_q2 || (!dspark_native_on() && DSPARK_Q2TP_ENCODE.get().is_some());
9082                let dn_q2 = mtp[0].layer.experts.first().is_some_and(|e| {
9083                    e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
9084                });
9085                let room = crate::gpu_wgpu::dsv4_draft_fit(cfg.moe_inter, cfg.dim, gu_q2, dn_q2);
9086                (room.saturating_sub(mtp.len() + 1) / mtp.len().max(1)).clamp(8, 40)
9087            });
9088    // Frequency tallies: lines of `stage<TAB>expert<TAB>count`. Named by
9089    // `CMF_DSPARK_PACK`, or found as `<model>.dspark.tsv` beside the model
9090    // file — ship the tally next to the checkpoint and no knob is needed.
9091    let mut freq: Vec<Vec<(u64, usize)>> = vec![Vec::new(); mtp.len()];
9092    let pack_path = std::env::var("CMF_DSPARK_PACK").ok().or_else(|| {
9093        let m = mtp[0].layer.experts.first()?.w1.model_arc()?;
9094        let mut s = m.path.as_os_str().to_os_string();
9095        s.push(".dspark.tsv");
9096        let p = std::path::PathBuf::from(s);
9097        p.exists().then(|| p.to_string_lossy().into_owned())
9098    });
9099    if let Some(path) = pack_path {
9100        if let Ok(text) = std::fs::read_to_string(&path) {
9101            for line in text.lines() {
9102                let mut it = line.split_whitespace();
9103                if let (Some(s), Some(e), Some(n)) = (it.next(), it.next(), it.next()) {
9104                    if let (Ok(s), Ok(e), Ok(n)) =
9105                        (s.parse::<usize>(), e.parse::<usize>(), n.parse::<u64>())
9106                    {
9107                        if s < freq.len() {
9108                            freq[s].push((n, e));
9109                        }
9110                    }
9111                }
9112            }
9113        }
9114    }
9115    let mut stages = Vec::with_capacity(mtp.len());
9116    let mut routers = Vec::with_capacity(mtp.len());
9117    let mut biases = Vec::with_capacity(mtp.len());
9118    for (si, m) in mtp.iter().enumerate() {
9119        let l = &m.layer;
9120        let n = l.experts.len();
9121        // Frequency order, then the untallied ids — a cold start still
9122        // packs SOMETHING deterministic.
9123        let mut order: Vec<usize> = {
9124            let mut f = freq[si].clone();
9125            f.sort_by(|a, b| b.0.cmp(&a.0));
9126            let mut seen = vec![false; n];
9127            let mut o: Vec<usize> = f
9128                .into_iter()
9129                .map(|(_, e)| e)
9130                .filter(|&e| {
9131                    if e < n && !seen[e] {
9132                        seen[e] = true;
9133                        true
9134                    } else {
9135                        false
9136                    }
9137                })
9138                .collect();
9139            o.extend((0..n).filter(|&e| !seen[e]));
9140            o
9141        };
9142        order.truncate(n_res.min(n));
9143        let mut mask = vec![false; n];
9144        let mut to_slot = vec![usize::MAX; n];
9145        let mut tensors = Vec::with_capacity(order.len() + 1);
9146        for (slot, &e) in order.iter().enumerate() {
9147            let ex = &l.experts[e];
9148            let (Some(w1), Some(w3), Some(w2)) =
9149                (ex.w1.model_idx(), ex.w3.model_idx(), ex.w2.model_idx())
9150            else {
9151                return None;
9152            };
9153            mask[e] = true;
9154            to_slot[e] = slot;
9155            tensors.push((w1, w3, w2));
9156        }
9157        let (Some(s1), Some(s3), Some(s2)) = (
9158            l.shared.w1.model_idx(),
9159            l.shared.w3.model_idx(),
9160            l.shared.w2.model_idx(),
9161        ) else {
9162            return None;
9163        };
9164        tensors.push((s1, s3, s2));
9165        // The router and bias, dequantized once.
9166        let mut router = vec![0.0f32; n * cfg.dim];
9167        for (r, row) in (0..n).zip(router.chunks_mut(cfg.dim)) {
9168            l.gate.row_f32(r, row);
9169        }
9170        routers.push(router);
9171        biases.push(l.gate_bias.clone());
9172        let mask_u32: Vec<u32> = mask.iter().map(|&m| m as u32).collect();
9173        let map_u32: Vec<u32> = to_slot
9174            .iter()
9175            .map(|&x| if x == usize::MAX { u32::MAX } else { x as u32 })
9176            .collect();
9177        stages.push(DsparkStagePack {
9178            mask,
9179            to_slot,
9180            mask_u32,
9181            map_u32,
9182            tensors,
9183            n_resident: order.len(),
9184        });
9185    }
9186    // ── upload: the small skeleton FIRST, the expert stacks after — the
9187    //    documented admission order (experts fill the card and the skeleton
9188    //    then misses). ──
9189    let model = mtp[0]
9190        .layer
9191        .experts
9192        .first()
9193        .and_then(|e| e.w1.model_arc())?;
9194    let mut skeleton = Vec::new();
9195    for m in mtp {
9196        let l = &m.layer;
9197        for t in [&l.wq_a, &l.wq_b, &l.wkv, &l.wo_a, &l.wo_b] {
9198            skeleton.push(t.model_idx()?);
9199        }
9200    }
9201    if let Some(mp) = mtp[0].main_proj.as_ref() {
9202        skeleton.push(mp.model_idx()?);
9203    }
9204    for &idx in &skeleton {
9205        if !crate::gpu_wgpu::dsv4_weight_ready(&model, idx) {
9206            eprintln!("DSpark: скелет драфта не влез в VRAM — GPU-черновик выключен");
9207            return None;
9208        }
9209    }
9210    // The dtype in the FILE decides: a properly converted CMF stores the
9211    // draft's gate/up as q2tp and uploads through the same path as the
9212    // trunk's 2-bit experts. The at-upload requant is only the fallback for
9213    // files published before the converter's q2tp profile covered the MTP
9214    // stack (and only when the binary registered an encoder).
9215    let native_q2 = mtp[0]
9216        .layer
9217        .experts
9218        .first()
9219        .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
9220    let gu_q2 = native_q2
9221        || (!crate::dsv4::dspark_native_on()
9222            && crate::dsv4::DSPARK_Q2TP_ENCODE.get().is_some());
9223    let dn_native = mtp[0]
9224        .layer
9225        .experts
9226        .first()
9227        .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
9228    for (si, sp) in stages.iter().enumerate() {
9229        let ok = if native_q2 {
9230            crate::gpu_wgpu::dsv4_experts_ready(
9231                &model,
9232                &sp.tensors,
9233                cfg.moe_inter,
9234                cfg.dim,
9235                true,
9236                dn_native,
9237            )
9238        } else if gu_q2 {
9239            crate::gpu_wgpu::moe_expert_bufs_requant_gu(&model, &sp.tensors, cfg.moe_inter, cfg.dim)
9240                .is_some()
9241        } else {
9242            crate::gpu_wgpu::dsv4_experts_ready(
9243                &model,
9244                &sp.tensors,
9245                cfg.moe_inter,
9246                cfg.dim,
9247                false,
9248                false,
9249            )
9250        };
9251        if !ok {
9252            eprintln!(
9253                "DSpark: эксперты стадии {si} ({} + shared) не влезли в VRAM — GPU-черновик выключен",
9254                sp.n_resident
9255            );
9256            return None;
9257        }
9258    }
9259    let _ = crate::gpu_wgpu::pin_weights(&model, &skeleton);
9260    eprintln!(
9261        "DSpark: пак драфта на карте — {} стадии по {} экспертов + shared",
9262        stages.len(),
9263        stages
9264            .iter()
9265            .map(|s| s.n_resident.to_string())
9266            .collect::<Vec<_>>()
9267            .join("/")
9268    );
9269    Some(DsparkPack {
9270        stages,
9271        gu_q2,
9272        dn_q2: dn_native,
9273        routers,
9274        biases,
9275    })
9276}
9277
9278/// Append one real position's entry to every stage's KV ring, from the
9279/// trunk captures in `ds.main_hidden`. The draft does this for the position
9280/// it drafts at; a speculative decode also owes an entry for every accepted
9281/// position it never drafted from — a hole in the ring silently starves
9282/// later blocks of context, which reads as "acceptance decayed" and not as
9283/// a bug.
9284pub fn dspark_ring_append(
9285    g: &Dsv4Globals,
9286    mtp: &[Dsv4Mtp],
9287    cfg: &Dsv4Cfg,
9288    ds: &mut DsparkState,
9289    pos: usize,
9290    pool: Option<&crate::pool::Pool>,
9291) {
9292    let (dim, hd, rd) = (cfg.dim, cfg.head_dim, cfg.rope_head_dim);
9293    let inv_freq = &g.inv_freq_window;
9294    let Some(stage0) = mtp.first() else { return };
9295    let (Some(mp), Some(mn)) = (stage0.main_proj.as_ref(), stage0.main_norm.as_ref()) else {
9296        return;
9297    };
9298    let mut main_x = vec![0.0f32; dim];
9299    mp.matvec(&ds.main_hidden, &mut main_x, pool);
9300    rms_weighted(&mut main_x, mn, cfg.norm_eps);
9301    for (si, m) in mtp.iter().enumerate() {
9302        let kvw = m.layer.wkv.rows();
9303        if ds.win[si].len() < cfg.window * kvw {
9304            ds.win[si].resize(cfg.window * kvw, 0.0);
9305        }
9306        let mut kv = vec![0.0f32; kvw];
9307        m.layer.wkv.matvec(&main_x, &mut kv, pool);
9308        rms_weighted(&mut kv, &m.layer.kv_norm, cfg.norm_eps);
9309        rope_tail(&mut kv[kvw - hd..], inv_freq, pos, rd, false);
9310        let slot = pos % cfg.window;
9311        ds.win[si][slot * kvw..(slot + 1) * kvw].copy_from_slice(&kv);
9312        ds.filled[si] = (pos + 1).min(cfg.window);
9313    }
9314}
9315
9316/// The draft block on the card: one submission for all three stages and
9317/// five positions, states home in one fence, the head on the host. The
9318/// markov bias is skipped (its per-position chain through the previous
9319/// PROPOSAL is the one part a single graph cannot batch) — compare against
9320/// the CPU draft under `CMF_DSPARK_NO_MARKOV=1`.
9321#[cfg(feature = "gpu")]
9322#[allow(clippy::too_many_arguments)]
9323pub fn dspark_draft_gpu(
9324    g: &Dsv4Globals,
9325    mtp: &[Dsv4Mtp],
9326    cfg: &Dsv4Cfg,
9327    ds: &mut DsparkState,
9328    pack: &DsparkPack,
9329    kv_id: u64,
9330    last_token: u32,
9331    pos: usize,
9332    pool: Option<&crate::pool::Pool>,
9333    out_conf: &mut Vec<f32>,
9334) -> Vec<u32> {
9335    let (hc, dim) = (cfg.hc_mult, cfg.dim);
9336    let block = dspark_block();
9337    let Some(model) = mtp[0].layer.experts.first().and_then(|e| e.w1.model_arc()) else {
9338        return Vec::new();
9339    };
9340    let (Some(mp), Some(mn)) = (mtp[0].main_proj.as_ref(), mtp[0].main_norm.as_ref()) else {
9341        return Vec::new();
9342    };
9343    let Some(mp_idx) = mp.model_idx() else {
9344        return Vec::new();
9345    };
9346    let mut stages = Vec::with_capacity(mtp.len());
9347    for (si, m) in mtp.iter().enumerate() {
9348        let l = &m.layer;
9349        let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b), Some(wkv)) = (
9350            l.wq_a.model_idx(),
9351            l.wq_b.model_idx(),
9352            l.wo_a.model_idx(),
9353            l.wo_b.model_idx(),
9354            l.wkv.model_idx(),
9355        ) else {
9356            return Vec::new();
9357        };
9358        let sp = &pack.stages[si];
9359        stages.push(crate::gpu_wgpu::DsparkStageW {
9360            wq_a,
9361            wq_b,
9362            wo_a,
9363            wo_b,
9364            wkv,
9365            q_norm: &l.q_norm,
9366            kv_norm: &l.kv_norm,
9367            attn_norm: &l.attn_norm,
9368            ffn_norm: &l.ffn_norm,
9369            sink: &l.attn_sink,
9370            hc_attn_fn: &l.hc_attn_fn,
9371            hc_attn_scale: &l.hc_attn_scale,
9372            hc_attn_base: &l.hc_attn_base,
9373            hc_ffn_fn: &l.hc_ffn_fn,
9374            hc_ffn_scale: &l.hc_ffn_scale,
9375            hc_ffn_base: &l.hc_ffn_base,
9376            router: &pack.routers[si],
9377            bias: pack.biases[si].as_deref(),
9378            experts: &sp.tensors,
9379            mask_u32: &sp.mask_u32,
9380            map_u32: &sp.map_u32,
9381        });
9382    }
9383    let geom = crate::gpu_wgpu::DsparkGeom {
9384        dim,
9385        hc,
9386        nh: cfg.n_heads,
9387        hd: cfg.head_dim,
9388        rd: cfg.rope_head_dim,
9389        q_lora: cfg.q_lora_rank,
9390        o_lora: cfg.o_lora_rank,
9391        o_groups: cfg.o_groups,
9392        inter: cfg.moe_inter,
9393        n_experts: cfg.n_routed_experts,
9394        top_k: cfg.top_k,
9395        window: cfg.window,
9396        eps: cfg.norm_eps,
9397        hc_eps: cfg.hc_eps,
9398        sinkhorn_iters: cfg.hc_sinkhorn_iters,
9399        route_scale: cfg.route_scale,
9400        swiglu_limit: cfg.swiglu_limit,
9401        scale: (cfg.head_dim as f32).powf(-0.5),
9402        gu_q2: pack.gu_q2,
9403        dn_q2: pack.dn_q2,
9404    };
9405    // ── seed states: the real token, then noise, replicated over copies ──
9406    let ids: Vec<u32> = (0..block)
9407        .map(|i| {
9408            if i == 0 {
9409                last_token
9410            } else {
9411                DSPARK_NOISE_TOKEN
9412            }
9413        })
9414        .collect();
9415    let mut states0 = vec![0.0f32; block * hc * dim];
9416    let mut emb = vec![0.0f32; dim];
9417    for (i, &id) in ids.iter().enumerate() {
9418        g.embed.row_f32(id as usize, &mut emb);
9419        for j in 0..hc {
9420            states0[(i * hc + j) * dim..(i * hc + j + 1) * dim].copy_from_slice(&emb);
9421        }
9422    }
9423    let dspark_time = {
9424        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9425        *ON.get_or_init(|| std::env::var("CMF_DSPARK_TIME").is_ok_and(|v| v != "0"))
9426    };
9427    let t0 = std::time::Instant::now();
9428    let filled = (pos + 1).min(cfg.window);
9429    let mut states = vec![0.0f32; block * hc * dim];
9430    if !crate::gpu_wgpu::dspark_graph(
9431        &model,
9432        &stages,
9433        geom,
9434        kv_id,
9435        mp_idx,
9436        mn,
9437        &ds.main_hidden,
9438        &states0,
9439        pos,
9440        filled,
9441        &g.inv_freq_window,
9442        block,
9443        &mut states,
9444    ) {
9445        return Vec::new();
9446    }
9447    for si in 0..mtp.len() {
9448        ds.filled[si] = filled;
9449    }
9450    let t_graph = t0.elapsed();
9451
9452    // ── head, on the host: fold, norm, one B-wide matmat, argmax ──
9453    let last = &mtp[mtp.len() - 1];
9454    let (Some(hfn), Some(hbase), Some(hscale), Some(hnorm)) = (
9455        last.hc_head_fn.as_ref(),
9456        last.hc_head_base.as_ref(),
9457        last.hc_head_scale,
9458        last.norm.as_ref(),
9459    ) else {
9460        return Vec::new();
9461    };
9462    let mut head_in = vec![0.0f32; block * dim];
9463    let mut pre_norms = vec![vec![0.0f32; dim]; block];
9464    for i in 0..block {
9465        hc_head_fold(
9466            &states[i * hc * dim..(i + 1) * hc * dim],
9467            hfn,
9468            hscale,
9469            hbase,
9470            cfg,
9471            pool,
9472            &mut head_in[i * dim..(i + 1) * dim],
9473        );
9474        pre_norms[i].copy_from_slice(&head_in[i * dim..(i + 1) * dim]);
9475        rms_weighted(&mut head_in[i * dim..(i + 1) * dim], hnorm, cfg.norm_eps);
9476    }
9477    let t_fold = t0.elapsed();
9478    let mut logits = vec![0.0f32; block * cfg.vocab];
9479    // The B-axis q4tp kernel, one submission: `matmat` at B=5 falls to the
9480    // CPU tile path and measured 46 ms of a 60 ms draft.
9481    let head_gpu = g.head.model_idx().is_some_and(|hi| {
9482        crate::gpu_wgpu::q4tp_matvec_batch_for_test(
9483            &model,
9484            hi,
9485            &head_in,
9486            block,
9487            cfg.vocab,
9488            dim,
9489            &mut logits,
9490        )
9491    });
9492    if !head_gpu {
9493        g.head.matmat(&head_in, block, &mut logits, pool);
9494    }
9495    let t_head = t0.elapsed();
9496    // The markov bigram is not optional: without it acceptance fell 1.02 →
9497    // 0.42 on natural text. Its chain runs through the previous PROPOSAL,
9498    // so it stays position-by-position; the w2 matvec is big enough that
9499    // the QTensor route puts it on the card by itself.
9500    let mut proposals = Vec::with_capacity(block);
9501    out_conf.clear();
9502    let mut prev = last_token;
9503    let mut mk_embed = vec![0.0f32; last.markov_w1.as_ref().map_or(0, |t| t.cols())];
9504    let mut bias = vec![0.0f32; cfg.vocab];
9505    for i in 0..block {
9506        let row = &mut logits[i * cfg.vocab..(i + 1) * cfg.vocab];
9507        if let (Some(w1), Some(w2)) = (last.markov_w1.as_ref(), last.markov_w2.as_ref()) {
9508            w1.row_f32(prev as usize, &mut mk_embed);
9509            w2.matvec(&mk_embed, &mut bias, pool);
9510            for (a, b) in row.iter_mut().zip(&bias) {
9511                *a += *b;
9512            }
9513        }
9514        let mut best = 0usize;
9515        for v in 1..row.len() {
9516            if row[v] > row[best] {
9517                best = v;
9518            }
9519        }
9520        if let Some(cf) = last.confidence.as_ref() {
9521            let mut cat = pre_norms[i].clone();
9522            cat.extend_from_slice(&mk_embed);
9523            let mut sc = [0.0f32; 1];
9524            if cat.len() == cf.cols() {
9525                cf.matvec(&cat, &mut sc, pool);
9526            }
9527            out_conf.push(sc[0]);
9528        }
9529        proposals.push(best as u32);
9530        prev = best as u32;
9531    }
9532    if dspark_time {
9533        eprintln!(
9534            "DSpark GPU: граф {:.1} мс, фолды {:.1}, голова {:.1}, марков+argmax {:.1}",
9535            t_graph.as_secs_f64() * 1e3,
9536            (t_fold - t_graph).as_secs_f64() * 1e3,
9537            (t_head - t_fold).as_secs_f64() * 1e3,
9538            (t0.elapsed() - t_head).as_secs_f64() * 1e3,
9539        );
9540    }
9541    proposals
9542}
9543
9544/// One draft: `DSPARK_BLOCK` proposed tokens and a confidence per position.
9545///
9546/// `pos` is the position of `last_token` — the block predicts `pos+1 ..
9547/// pos+BLOCK`. Returns the proposals in order; `out_conf` takes the
9548/// confidence head's score where the last stage carries one.
9549#[allow(clippy::too_many_arguments)]
9550pub fn dspark_draft(
9551    g: &Dsv4Globals,
9552    mtp: &[Dsv4Mtp],
9553    cfg: &Dsv4Cfg,
9554    ds: &mut DsparkState,
9555    last_token: u32,
9556    pos: usize,
9557    pool: Option<&crate::pool::Pool>,
9558    out_conf: &mut Vec<f32>,
9559) -> Vec<u32> {
9560    let (hc, dim, hd, rd) = (cfg.hc_mult, cfg.dim, cfg.head_dim, cfg.rope_head_dim);
9561    let block = dspark_block();
9562    let inv_freq = &g.inv_freq_window;
9563
9564    // ── the block's input: main_norm(main_proj(captured hiddens)) ──
9565    let Some(stage0) = mtp.first() else {
9566        return Vec::new();
9567    };
9568    let (Some(_mp), Some(_mn)) = (stage0.main_proj.as_ref(), stage0.main_norm.as_ref()) else {
9569        return Vec::new();
9570    };
9571    dspark_ring_append(g, mtp, cfg, ds, pos, pool);
9572
9573    // ── the block: the real token, then noise ──
9574    let ids: Vec<u32> = (0..block)
9575        .map(|i| {
9576            if i == 0 {
9577                last_token
9578            } else {
9579                DSPARK_NOISE_TOKEN
9580            }
9581        })
9582        .collect();
9583    let mut states = vec![vec![0.0f32; hc * dim]; block];
9584    let mut emb = vec![0.0f32; dim];
9585    for (i, &id) in ids.iter().enumerate() {
9586        g.embed.row_f32(id as usize, &mut emb);
9587        for j in 0..hc {
9588            states[i][j * dim..(j + 1) * dim].copy_from_slice(&emb);
9589        }
9590    }
9591
9592    let mut scratch = HcScratch::new(cfg);
9593    for (si, m) in mtp.iter().enumerate() {
9594        let l = &m.layer;
9595        let kvw = l.wkv.rows();
9596        // ── attention half: fold every position first, because each one's
9597        //    keys are visible to all the others. ──
9598        let mut post = vec![vec![0.0f32; hc]; block];
9599        let mut comb = vec![vec![0.0f32; hc * hc]; block];
9600        let mut resid = vec![vec![0.0f32; hc * dim]; block];
9601        let mut folded = vec![vec![0.0f32; dim]; block];
9602        let mix_hc = (2 + hc) * hc;
9603        for i in 0..block {
9604            hc_mixes(
9605                &states[i],
9606                &l.hc_attn_fn,
9607                mix_hc,
9608                cfg.norm_eps,
9609                pool,
9610                &mut scratch.mixes,
9611            );
9612            hc_split_sinkhorn(
9613                &scratch.mixes,
9614                &l.hc_attn_scale,
9615                &l.hc_attn_base,
9616                hc,
9617                cfg.hc_sinkhorn_iters,
9618                cfg.hc_eps,
9619                &mut scratch.pre,
9620                &mut post[i],
9621                &mut comb[i],
9622            );
9623            hc_fold(&states[i], &scratch.pre, hc, dim, &mut folded[i]);
9624            rms_weighted(&mut folded[i], &l.attn_norm, cfg.norm_eps);
9625            resid[i].copy_from_slice(&states[i]);
9626        }
9627        // Keys and values of the block itself — kept for this block only.
9628        let folded_all: Vec<f32> = folded.iter().flatten().copied().collect();
9629        let mut blk_kv = vec![0.0f32; block * kvw];
9630        l.wkv.matmat(&folded_all, block, &mut blk_kv, pool);
9631        for i in 0..block {
9632            let dst = &mut blk_kv[i * kvw..(i + 1) * kvw];
9633            rms_weighted(dst, &l.kv_norm, cfg.norm_eps);
9634            rope_tail(&mut dst[kvw - hd..], inv_freq, pos + 1 + i, rd, false);
9635        }
9636        // The attended set: every cached real position, then the whole block.
9637        let win_len = ds.filled[si];
9638        let mut cache = Vec::with_capacity((win_len + block) * hd);
9639        for p in 0..win_len {
9640            let e = &ds.win[si][p * kvw..(p + 1) * kvw];
9641            cache.extend_from_slice(&e[kvw - hd..]);
9642        }
9643        for i in 0..block {
9644            let e = &blk_kv[i * kvw..(i + 1) * kvw];
9645            cache.extend_from_slice(&e[kvw - hd..]);
9646        }
9647        let idxs: Vec<usize> = (0..win_len + block).collect();
9648        let scale = (hd as f32).powf(-0.5);
9649        let qrank = l.wq_a.rows();
9650        let qdim = cfg.n_heads * hd;
9651        let mut qr = vec![0.0f32; block * qrank];
9652        l.wq_a.matmat(&folded_all, block, &mut qr, pool);
9653        for i in 0..block {
9654            rms_weighted(&mut qr[i * qrank..(i + 1) * qrank], &l.q_norm, cfg.norm_eps);
9655        }
9656        let mut q = vec![0.0f32; block * qdim];
9657        l.wq_b.matmat(&qr, block, &mut q, pool);
9658        let mut attn = vec![0.0f32; block * qdim];
9659        for i in 0..block {
9660            let qi = &mut q[i * qdim..(i + 1) * qdim];
9661            let ai = &mut attn[i * qdim..(i + 1) * qdim];
9662            let qpos = pos + 1 + i;
9663            for h in 0..cfg.n_heads {
9664                let head = &mut qi[h * hd..(h + 1) * hd];
9665                rms_inplace(head, cfg.norm_eps);
9666                rope_tail(head, inv_freq, qpos, rd, false);
9667            }
9668            for h in 0..cfg.n_heads {
9669                let qh = &qi[h * hd..(h + 1) * hd];
9670                let oh = &mut ai[h * hd..(h + 1) * hd];
9671                sparse_attend(qh, &cache, &idxs, l.attn_sink[h], scale, hd, oh);
9672                rope_tail(oh, inv_freq, qpos, rd, true);
9673            }
9674        }
9675        let mut blk_out = vec![0.0f32; block * dim];
9676        o_project_block(
9677            &attn,
9678            block,
9679            &l.wo_a,
9680            &l.wo_b,
9681            cfg.o_groups,
9682            cfg.o_lora_rank,
9683            pool,
9684            &mut blk_out,
9685        );
9686        for i in 0..block {
9687            let mut next = vec![0.0f32; hc * dim];
9688            hc_expand(
9689                &blk_out[i * dim..(i + 1) * dim],
9690                &resid[i],
9691                &post[i],
9692                &comb[i],
9693                hc,
9694                dim,
9695                &mut next,
9696            );
9697            states[i] = next;
9698        }
9699        // ── MoE: fold every position, group equal experts, then expand in
9700        //    the original per-position route order. ──
9701        let mut ffn_fold = vec![0.0f32; block * dim];
9702        let mut ffn_post = vec![vec![0.0f32; hc]; block];
9703        let mut ffn_comb = vec![vec![0.0f32; hc * hc]; block];
9704        let mut ffn_resid = vec![vec![0.0f32; hc * dim]; block];
9705        for i in 0..block {
9706            hc_mixes(
9707                &states[i],
9708                &l.hc_ffn_fn,
9709                mix_hc,
9710                cfg.norm_eps,
9711                pool,
9712                &mut scratch.mixes,
9713            );
9714            hc_split_sinkhorn(
9715                &scratch.mixes,
9716                &l.hc_ffn_scale,
9717                &l.hc_ffn_base,
9718                hc,
9719                cfg.hc_sinkhorn_iters,
9720                cfg.hc_eps,
9721                &mut scratch.pre,
9722                &mut ffn_post[i],
9723                &mut ffn_comb[i],
9724            );
9725            hc_fold(
9726                &states[i],
9727                &scratch.pre,
9728                hc,
9729                dim,
9730                &mut ffn_fold[i * dim..(i + 1) * dim],
9731            );
9732            rms_weighted(
9733                &mut ffn_fold[i * dim..(i + 1) * dim],
9734                &l.ffn_norm,
9735                cfg.norm_eps,
9736            );
9737            ffn_resid[i].copy_from_slice(&states[i]);
9738        }
9739        let mut moe_out = vec![0.0f32; block * dim];
9740        moe_step_block(&ffn_fold, block, l, cfg, &ids, si, pool, &mut moe_out);
9741        for i in 0..block {
9742            let mut next = vec![0.0f32; hc * dim];
9743            hc_expand(
9744                &moe_out[i * dim..(i + 1) * dim],
9745                &ffn_resid[i],
9746                &ffn_post[i],
9747                &ffn_comb[i],
9748                hc,
9749                dim,
9750                &mut next,
9751            );
9752            states[i] = next;
9753        }
9754    }
9755
9756    // ── head: the last stage's fold, the trunk's own head ──
9757    let last = &mtp[mtp.len() - 1];
9758    let (Some(hfn), Some(hbase), Some(hscale), Some(hnorm)) = (
9759        last.hc_head_fn.as_ref(),
9760        last.hc_head_base.as_ref(),
9761        last.hc_head_scale,
9762        last.norm.as_ref(),
9763    ) else {
9764        return Vec::new();
9765    };
9766    let mut proposals = Vec::with_capacity(block);
9767    out_conf.clear();
9768    let mut prev = last_token;
9769    let mut head_in = vec![0.0f32; block * dim];
9770    let mut pre_norms = vec![vec![0.0f32; dim]; block];
9771    for i in 0..block {
9772        hc_head_fold(
9773            &states[i],
9774            hfn,
9775            hscale,
9776            hbase,
9777            cfg,
9778            pool,
9779            &mut head_in[i * dim..(i + 1) * dim],
9780        );
9781        pre_norms[i].copy_from_slice(&head_in[i * dim..(i + 1) * dim]);
9782        rms_weighted(&mut head_in[i * dim..(i + 1) * dim], hnorm, cfg.norm_eps);
9783    }
9784    let mut logits = vec![0.0f32; block * cfg.vocab];
9785    g.head.matmat(&head_in, block, &mut logits, pool);
9786    let mut mk_embed = vec![0.0f32; last.markov_w1.as_ref().map_or(0, |t| t.cols())];
9787    for i in 0..block {
9788        let logits_i = &mut logits[i * cfg.vocab..(i + 1) * cfg.vocab];
9789        // The markov head biases the logits from the PREVIOUS token — a
9790        // rank-256 bigram the draft samples through position by position,
9791        // while the network itself ran the whole block at once.
9792        // `CMF_DSPARK_NO_MARKOV=1` drops it: the bias is sequential through
9793        // the block (each position needs the previous PROPOSAL), which is
9794        // the one part of the draft a single device graph cannot batch — so
9795        // its acceptance value has to be known before it earns that
9796        // complexity.
9797        let no_markov = {
9798            static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9799            *ON.get_or_init(|| std::env::var("CMF_DSPARK_NO_MARKOV").is_ok_and(|v| v != "0"))
9800        };
9801        if no_markov {
9802            // Still feed the confidence head's embedding slot below.
9803            if let Some(w1) = last.markov_w1.as_ref() {
9804                w1.row_f32(prev as usize, &mut mk_embed);
9805            }
9806        } else if let (Some(w1), Some(w2)) = (last.markov_w1.as_ref(), last.markov_w2.as_ref()) {
9807            w1.row_f32(prev as usize, &mut mk_embed);
9808            let mut bias = vec![0.0f32; cfg.vocab];
9809            w2.matvec(&mk_embed, &mut bias, pool);
9810            for (a, b) in logits_i.iter_mut().zip(&bias) {
9811                *a += *b;
9812            }
9813        }
9814        let mut best = 0usize;
9815        for v in 1..logits_i.len() {
9816            if logits_i[v] > logits_i[best] {
9817                best = v;
9818            }
9819        }
9820        if let Some(cf) = last.confidence.as_ref() {
9821            let mut cat = pre_norms[i].clone();
9822            cat.extend_from_slice(&mk_embed);
9823            let mut s = [0.0f32; 1];
9824            if cat.len() == cf.cols() {
9825                cf.matvec(&cat, &mut s, pool);
9826            }
9827            out_conf.push(s[0]);
9828        }
9829        proposals.push(best as u32);
9830        prev = best as u32;
9831    }
9832    proposals
9833}