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    /// The whole forward, so the buckets can be checked against a total
1056    /// instead of against a guess. 78 ms of measured work in a 108 ms token
1057    /// left 30 ms that no counter had ever looked at.
1058    pub static ALL_NS: AtomicU64 = AtomicU64::new(0);
1059    pub static TOKENS: AtomicU64 = AtomicU64::new(0);
1060
1061    /// One token = one visit to layer zero. Counting `moe_step` calls instead
1062    /// counts layers.
1063    pub fn note_layer(li: usize) {
1064        CALLS.fetch_add(1, Ordering::Relaxed);
1065        if li == 0 {
1066            // The first token pays for the whole expert set reaching the card
1067            // — tens of seconds of it. Left in, that one-time cost is divided
1068            // by every later call and reads as a per-call price: it is what
1069            // made "the host encodes for 4.45 ms a layer" out of an upload
1070            // that happens once. Everything measured before the SECOND token
1071            // starts is therefore thrown away, and the report describes
1072            // steady state, which is the only thing worth optimising.
1073            // `swap` and not a TOKENS comparison: resetting TOKENS to 1 made
1074            // the test true again on every later token, so the report
1075            // described one token instead of the run.
1076            if TOKENS.fetch_add(1, Ordering::Relaxed) == 1 && !ZEROED.swap(true, Ordering::Relaxed)
1077            {
1078                for a in [&ATTN_NS, &MOE_NS, &HC_NS, &HEAD_NS, &ALL_NS, &CALLS] {
1079                    a.store(0, Ordering::Relaxed);
1080                }
1081                TOKENS.store(1, Ordering::Relaxed);
1082                #[cfg(feature = "gpu")]
1083                for a in [
1084                    &crate::gpu_wgpu::MOE_ENC_NS,
1085                    &crate::gpu_wgpu::MOE_WAIT_NS,
1086                    &crate::gpu_wgpu::MOE_BUFS_NS,
1087                    &crate::gpu_wgpu::MOE_UP_NS,
1088                    &crate::gpu_wgpu::MOE_PASS_NS,
1089                    &crate::gpu_wgpu::ATT_ENC_NS,
1090                    &crate::gpu_wgpu::ATT_WAIT_NS,
1091                    &crate::gpu_wgpu::CHAIN_ENC_NS,
1092                    &crate::gpu_wgpu::CHAIN_WAIT_NS,
1093                    &crate::gpu_wgpu::CHAIN_LAYERS,
1094                    &crate::gpu_wgpu::CHAIN_RUNS,
1095                    &crate::gpu_wgpu::SUBMITS,
1096                    &crate::gpu_wgpu::PASSES,
1097                ] {
1098                    a.store(0, Ordering::Relaxed);
1099                }
1100            }
1101        }
1102    }
1103    static REPORT: AtomicBool = AtomicBool::new(false);
1104    /// The one-time "drop the first token's numbers" latch.
1105    static ZEROED: AtomicBool = AtomicBool::new(false);
1106
1107    pub fn on() -> bool {
1108        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1109        *ON.get_or_init(|| std::env::var("CMF_DSV4_PROFILE").is_ok_and(|v| v != "0"))
1110    }
1111
1112    /// Print once, from wherever the last caller happens to be — a process
1113    /// that exits through several paths would otherwise report zero or twice.
1114    pub fn report() {
1115        if !on() || REPORT.swap(true, Ordering::Relaxed) {
1116            return;
1117        }
1118        // CALLS counts layer visits, not tokens — dividing by it and calling
1119        // the result "per token" is off by the layer count, which is 43 on
1120        // the release and reads as a plausible number either way.
1121        let calls = CALLS.load(Ordering::Relaxed).max(1);
1122        let toks = TOKENS.load(Ordering::Relaxed).max(1);
1123        let (a, m) = (
1124            ATTN_NS.load(Ordering::Relaxed) as f64 / 1e6,
1125            MOE_NS.load(Ordering::Relaxed) as f64 / 1e6,
1126        );
1127        let all = ALL_NS.load(Ordering::Relaxed) as f64 / 1e6;
1128        // HC_NS wraps the FFN half's hc_block WHOLE, and moe_step runs
1129        // inside that block — so the raw counter double-counts every MoE
1130        // millisecond as hyper-connection time. Reported as the difference:
1131        // the glue alone. (This inflation is what made moving the
1132        // hyper-connections to the card look like a 19 ms win when the glue
1133        // is ~4.)
1134        let hc = (HC_NS.load(Ordering::Relaxed) as f64 / 1e6
1135            - MOE_NS.load(Ordering::Relaxed) as f64 / 1e6)
1136            .max(0.0);
1137        let hd = HEAD_NS.load(Ordering::Relaxed) as f64 / 1e6;
1138        eprintln!(
1139            "[dsv4-профиль] {calls} вызовов слоя за {toks} токенов | \
1140             на токен: внимание {:.0} мс, MoE {:.0} мс, гипер-связи+нормы {:.0} мс, \
1141             голова {:.0} мс | на вызов: внимание {:.2}, MoE {:.2}, связи {:.2}",
1142            a / toks as f64,
1143            m / toks as f64,
1144            hc / toks as f64,
1145            hd / toks as f64,
1146            a / calls as f64,
1147            m / calls as f64,
1148            hc / calls as f64,
1149        );
1150        eprintln!(
1151            "[dsv4-профиль] весь проход {:.0} мс на токен; вне счётчиков {:.0} мс",
1152            all / toks as f64,
1153            (all - a - m - hd) / toks as f64,
1154        );
1155        #[cfg(feature = "gpu")]
1156        {
1157            let ae = crate::gpu_wgpu::ATT_ENC_NS.load(Ordering::Relaxed) as f64 / 1e6;
1158            let aw = crate::gpu_wgpu::ATT_WAIT_NS.load(Ordering::Relaxed) as f64 / 1e6;
1159            if ae + aw > 0.0 {
1160                eprintln!(
1161                    "[dsv4-профиль] кадр внимания на вызов: кодирование {:.2} мс, \
1162                     отправка и ожидание {:.2} мс",
1163                    ae / calls as f64,
1164                    aw / calls as f64,
1165                );
1166            }
1167            // At the OUTER level on purpose: this used to sit inside the MoE
1168            // frame's own report, and the chain does not use the MoE frame —
1169            // so the one number that says where a chained token goes was
1170            // printed only when the chain was not running.
1171            let ub = crate::gpu_wgpu::UPLOAD_BYTES.load(Ordering::Relaxed);
1172            let un = crate::gpu_wgpu::UPLOAD_NS.load(Ordering::Relaxed);
1173            if ub > 0 && un > 0 {
1174                eprintln!(
1175                    "[dsv4-профиль] ЗАЛИВКА весов: {:.1} ГБ за {:.1} с ({:.0} МБ/с)",
1176                    ub as f64 / 1e9,
1177                    un as f64 / 1e9,
1178                    ub as f64 / (un as f64 / 1e9) / 1e6,
1179                );
1180            }
1181            let sub = crate::gpu_wgpu::SUBMITS.load(Ordering::Relaxed);
1182            if sub > 0 {
1183                eprintln!(
1184                    "[dsv4-профиль] ОТПРАВОК на карту: {:.1} на токен, ПРОХОДОВ {:.0} \
1185                     ({:.1} на слой)",
1186                    sub as f64 / toks as f64,
1187                    crate::gpu_wgpu::PASSES.load(Ordering::Relaxed) as f64 / toks as f64,
1188                    crate::gpu_wgpu::PASSES.load(Ordering::Relaxed) as f64 / calls as f64,
1189                );
1190            }
1191            let cl = crate::gpu_wgpu::CHAIN_LAYERS.load(Ordering::Relaxed);
1192            if cl > 0 {
1193                let toks2 = toks.max(1) as f64;
1194                eprintln!(
1195                    "[dsv4-профиль] ЦЕПОЧКА на токен: кодирование {:.2} мс, \
1196                     ожидание {:.2} мс ({} слоёв, {} отправок)",
1197                    crate::gpu_wgpu::CHAIN_ENC_NS.load(Ordering::Relaxed) as f64 / 1e6 / toks2,
1198                    crate::gpu_wgpu::CHAIN_WAIT_NS.load(Ordering::Relaxed) as f64 / 1e6 / toks2,
1199                    cl / toks.max(1),
1200                    crate::gpu_wgpu::CHAIN_RUNS.load(Ordering::Relaxed) / toks.max(1),
1201                );
1202            }
1203            let e = crate::gpu_wgpu::MOE_ENC_NS.load(Ordering::Relaxed) as f64 / 1e6;
1204            let wt = crate::gpu_wgpu::MOE_WAIT_NS.load(Ordering::Relaxed) as f64 / 1e6;
1205            if e + wt > 0.0 {
1206                let ns = |a: &std::sync::atomic::AtomicU64| {
1207                    a.load(Ordering::Relaxed) as f64 / 1e6 / calls as f64
1208                };
1209                eprintln!(
1210                    "[dsv4-профиль] кадр MoE на вызов: кодирование {:.2} мс, \
1211                     отправка и ожидание {:.2} мс",
1212                    e / calls as f64,
1213                    wt / calls as f64,
1214                );
1215                let an = crate::gpu_wgpu::ATT_GPU_N.load(Ordering::Relaxed);
1216                if an > 0 {
1217                    let g = |i: usize| {
1218                        crate::gpu_wgpu::ATT_GPU_NS[i].load(Ordering::Relaxed) as f64
1219                            / 1e6 / an as f64
1220                    };
1221                    eprintln!(
1222                        "[dsv4-профиль]   ВНИМАНИЕ НА КАРТЕ на вызов: одиночное {:.3} мс, \
1223                         оценки {:.3} мс, применение {:.3} мс",
1224                        g(0), g(1), g(2),
1225                    );
1226                }
1227                let gn = crate::gpu_wgpu::MOE_GPU_N.load(Ordering::Relaxed);
1228                let gns = crate::gpu_wgpu::MOE_GPU_NS[0].load(Ordering::Relaxed);
1229                if gn > 0 && gns > 0 {
1230                    eprintln!(
1231                        "[dsv4-профиль]   MoE НА КАРТЕ: {:.3} мс на вызов ({gn} замеров)",
1232                        gns as f64 / 1e6 / gn as f64,
1233                    );
1234                } else if gn > 0 {
1235                    // Zero across thousands of samples is a broken query, not
1236                    // an instant kernel, and printing it as a time is how a
1237                    // profile starts lying.
1238                    eprintln!(
1239                        "[dsv4-профиль]   MoE НА КАРТЕ: метки вернули НОЛЬ на {gn} замерах — \
1240                         запрос времени не сработал, число не использовать"
1241                    );
1242                }
1243                eprintln!(
1244                    "[dsv4-профиль]   из кодирования: буферы экспертов {:.2} мс, \
1245                     загрузки {:.2} мс, проходы {:.2} мс",
1246                    ns(&crate::gpu_wgpu::MOE_BUFS_NS),
1247                    ns(&crate::gpu_wgpu::MOE_UP_NS),
1248                    ns(&crate::gpu_wgpu::MOE_PASS_NS),
1249                );
1250            }
1251        }
1252    }
1253}
1254
1255/// Print the per-token split, if `CMF_DSV4_PROFILE` asked for one.
1256pub fn profile_report() {
1257    prof::report();
1258}
1259
1260/// `CMF_DSV4_GPU_ATTN=1` moves the attention block onto the device as one
1261/// submission. Off by default: it needs every attention weight in q4tp and a
1262/// working wgpu context, and a frame that declines mid-layer after the state
1263/// has been advanced would be worse than one that never ran.
1264fn gpu_attn_enabled() -> bool {
1265    #[cfg(feature = "gpu")]
1266    {
1267        use std::sync::OnceLock;
1268        static ON: OnceLock<bool> = OnceLock::new();
1269        *ON.get_or_init(|| {
1270            let want = std::env::var("CMF_DSV4_GPU_ATTN")
1271                .map(|v| v != "0")
1272                .unwrap_or(true);
1273            let have = want && crate::gpu::backend_available();
1274            if want && !have && std::env::var("CMF_DSV4_GPU_ATTN").is_ok() {
1275                tracing::warn!(
1276                    "CMF_DSV4_GPU_ATTN задан, но устройства нет — блок внимания                      остаётся на CPU. Проверьте CMF_GPU=wgpu и Vulkan-ICD."
1277                );
1278            }
1279            if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
1280                eprintln!("кадр dsv4: запрошен={want} доступен={have}");
1281            }
1282            have
1283        })
1284    }
1285    #[cfg(not(feature = "gpu"))]
1286    {
1287        false
1288    }
1289}
1290
1291/// The device half of `attention_step`. Returns false — having changed
1292/// nothing — whenever it cannot do the whole block, so the caller's CPU path
1293/// is still correct to run.
1294#[cfg(feature = "gpu")]
1295#[allow(clippy::too_many_arguments)]
1296fn attn_frame(
1297    l: &Dsv4Layer,
1298    cfg: &Dsv4Cfg,
1299    st: &Dsv4State,
1300    li: usize,
1301    hidden: &[f32],
1302    qn: &[f32],
1303    idxs: &[usize],
1304    inv_freq: &[f32],
1305    pos: usize,
1306    win_len: usize,
1307    scale: f32,
1308    // Present: the frame also does this layer's hyper-connection handover
1309    // and leaves the MoE half's input on the card. `out` may then be empty.
1310    hc: Option<&crate::gpu_wgpu::Dsv4HcTail>,
1311    out: &mut [f32],
1312) -> bool {
1313    let hd = cfg.head_dim;
1314    let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b)) = (
1315        l.wq_a.model_idx(),
1316        l.wq_b.model_idx(),
1317        l.wo_a.model_idx(),
1318        l.wo_b.model_idx(),
1319    ) else {
1320        return false;
1321    };
1322    let Some(model) = l.wq_b.model_arc() else {
1323        return false;
1324    };
1325    // Fixed window region, then the compressed tail — so a token writes one
1326    // window slot's worth of movement and whatever the compressor just added,
1327    // not the whole cache. `cap` has to cover the longest run this sequence
1328    // will reach; the compressed axis grows by one entry per `ratio` tokens.
1329    let n_comp = st.compressed[li].len() / hd;
1330    let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
1331    let kv_id = st.kv_id;
1332    // The window is rewritten whole. A ring would write one slot instead of
1333    // 128 — 2 KB against 256 — and was tried: it bought NOTHING (the cost is
1334    // per-dispatch driver bookkeeping, not the copy) and moved perplexity by
1335    // 6e-5 because the attended positions arrive in a different order and the
1336    // softmax accumulates differently. Not a trade worth making.
1337    if !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, 0, &st.window[li], cap) {
1338        return false;
1339    }
1340    // The compressed axis only ever grows, so write the TAIL. Rewriting it
1341    // whole was 22 MB a token at 1024 positions — the cache write, not the
1342    // arithmetic, was what the attention block had left to pay.
1343    // The compressed tail is written WHOLE every token. Writing only the new
1344    // part was tried and gave nothing measurable, and the bookkeeping it
1345    // needs — a per-layer tail count invalidated by every buffer growth — is
1346    // exactly the kind of state that drifts silently and shows up as a model
1347    // that stops early. Not worth carrying for zero.
1348    if n_comp > 0
1349        && !crate::gpu_wgpu::dsv4_cache_write(
1350            kv_id,
1351            li,
1352            cfg.window * hd,
1353            &st.compressed[li],
1354            cap,
1355        )
1356    {
1357        return false;
1358    }
1359    let idx32: Vec<u32> = idxs
1360        .iter()
1361        .map(|&p| {
1362            if p < win_len {
1363                p as u32
1364            } else {
1365                (cfg.window + (p - win_len)) as u32
1366            }
1367        })
1368        .collect();
1369    let w = crate::gpu_wgpu::Dsv4AttnW {
1370        wq_a,
1371        wq_b,
1372        wo_a,
1373        wo_b,
1374        q_norm: &l.q_norm,
1375        sink: &l.attn_sink,
1376    };
1377    let g = crate::gpu_wgpu::Dsv4AttnGeom {
1378        dim: cfg.dim,
1379        nh: cfg.n_heads,
1380        hd,
1381        rd: cfg.rope_head_dim,
1382        q_lora: cfg.q_lora_rank,
1383        o_lora: cfg.o_lora_rank,
1384        o_groups: cfg.o_groups,
1385        eps: cfg.norm_eps,
1386        scale,
1387    };
1388    // The host fold, explicitly. The frame used to read this half's input
1389    // from the pooled x2 slot — which a device MoE frame of the SAME layer
1390    // overwrites each token with the NEXT layer's input, so the second
1391    // token of any chain+partial configuration attended over garbage
1392    // (perplexity 5.3 against the 4.578 gold on every budget small enough
1393    // to split a layer). The host has the exact vector either way; one
1394    // hidden-width upload per call is what correctness costs.
1395    crate::gpu_wgpu::dsv4_attn_frame(
1396        &model, &w, g, hidden, Some(qn), kv_id, li, &idx32, inv_freq, pos, hc, out,
1397    )
1398}
1399
1400/// What the host still owes the device before a layer frame can run: the
1401/// shared LoRA vector the indexer reads, and the attended position list.
1402#[derive(Default)]
1403pub struct AttnPrep {
1404    pub qr: Vec<f32>,
1405    pub idxs: Vec<usize>,
1406    pub win_len: usize,
1407}
1408
1409#[allow(clippy::too_many_arguments)]
1410pub fn attention_step(
1411    hidden: &[f32],
1412    l: &Dsv4Layer,
1413    cfg: &Dsv4Cfg,
1414    st: &mut Dsv4State,
1415    li: usize,
1416    // Chosen by the caller from the layer's kind — see Dsv4Globals.
1417    inv_freq: &[f32],
1418    pool: Option<&crate::pool::Pool>,
1419    // When set, stop once the caches are advanced and the index list is
1420    // built, and hand those back instead of running attention: the layer
1421    // frame does the rest on the device.
1422    prep_out: Option<&mut AttnPrep>,
1423    out: &mut [f32],
1424) {
1425    let _t0 = prof::on().then(std::time::Instant::now);
1426    let _guard = scopeguard_attn(_t0);
1427    let (hd, rd) = (cfg.head_dim, cfg.rope_head_dim);
1428    let pos = st.pos;
1429    if std::env::var("CMF_FREQ_DEBUG").is_ok() && li == 0 && pos == 0 {
1430        eprintln!(
1431            "    [порт] rd={rd} частот={} inv_freq[0..4]={:?}",
1432            inv_freq.len(),
1433            &inv_freq[..4.min(inv_freq.len())]
1434        );
1435    }
1436
1437    // ── q and kv: both read the same hidden state, so they go out as ONE
1438    // dispatch. The norms after them differ, and they stay separate.
1439    // (q: wq_a → q_norm → wq_b → per-head norm → rope tail;
1440    //  kv: one head's width, shared by every query head.)
1441    let mut qr = vec![0.0f32; cfg.q_lora_rank];
1442    let mut kv = vec![0.0f32; hd];
1443    crate::qtensor::QTensor::matvec_many([&l.wq_a, &l.wkv], hidden, [&mut qr, &mut kv], pool);
1444    rms_weighted(&mut qr, &l.q_norm, cfg.norm_eps);
1445    // The queries are built further down, after the frame has had its chance
1446    // at the whole block. `qr` is needed either way: the indexer reads it.
1447    // A PARTIAL layer walks its attention on the host. Its device MoE
1448    // frame refills the pooled walk slots (x2, the hyper-connection state)
1449    // each token with the NEXT layer's values, so the same layer's device
1450    // attention frame attends over the previous token's leftovers on the
1451    // second token — measured as perplexity 5.3 against the 4.578 gold on
1452    // every budget small enough to split a layer, and exact the moment
1453    // that one layer's attention walks on the host. Layers whose MoE runs
1454    // on the HOST keep their device attention: nothing refills their
1455    // slots mid-walk, and the MAX_LI ladder measures them bit-exact.
1456    // …and it spreads: the partial layer's MoE frame cycles slots that the
1457    // FOLLOWING host-MoE layers' device attention also reads, so in any
1458    // configuration that holds a partial layer, every layer past the chain
1459    // prefix walks its attention on the host. A configuration with no
1460    // partial layer keeps device attention everywhere — the canonical
1461    // stand and the MAX_LI ladder both measure that bit-exact.
1462    let split_config = st.partial_set.iter().any(|&p| p) && st.split_deep;
1463    let past_chain = st.dev_owned
1464        && (li >= st.dev_set.len() || !st.dev_set.get(li).copied().unwrap_or(false));
1465    if std::env::var("CMF_DSV4_GATE_DBG").is_ok() {
1466        eprintln!(
1467            "[gate] li={li} pos={} split={split_config} past={past_chain} dev_owned={} set_len={} part_len={}",
1468            st.pos, st.dev_owned, st.dev_set.len(), st.partial_set.len()
1469        );
1470    }
1471    let on_gpu = gpu_attn_enabled() && !(split_config && past_chain);
1472
1473    rms_weighted(&mut kv, &l.kv_norm, cfg.norm_eps);
1474    rope_tail(&mut kv, inv_freq, pos, rd, false);
1475
1476    // ── the compressor: accumulate `ratio` tokens, then fold them into
1477    // one compressed entry. The reference fires when (pos+1) % ratio == 0,
1478    // so a partial window simply waits — which is why the state carries
1479    // the pending streams across tokens.
1480    if let Some(cp) = &l.compressor {
1481        let mut pk = std::mem::take(&mut st.pending_kv[li]);
1482        let mut ps = std::mem::take(&mut st.pending_score[li]);
1483        let mut qk = std::mem::take(&mut st.prev_kv[li]);
1484        let mut qs = std::mem::take(&mut st.prev_score[li]);
1485        let entry = compressor_step(
1486            cp,
1487            hidden,
1488            pos,
1489            rd,
1490            cfg.norm_eps,
1491            inv_freq,
1492            pool,
1493            &mut pk,
1494            &mut ps,
1495            &mut qk,
1496            &mut qs,
1497        );
1498        st.pending_kv[li] = pk;
1499        st.pending_score[li] = ps;
1500        st.prev_kv[li] = qk;
1501        st.prev_score[li] = qs;
1502        if let Some(e) = entry {
1503            st.compressed[li].extend_from_slice(&e);
1504        }
1505    }
1506    // The indexer scores against ITS OWN compressed cache, built by its own
1507    // compressor. Without this the cache is empty, `n_ix` is zero, and every
1508    // indexer layer picks no compressed positions at all — the long-range
1509    // memory is built and then never read.
1510    if let Some(ix) = &l.indexer {
1511        let mut pk = std::mem::take(&mut st.pending_ix_kv[li]);
1512        let mut ps = std::mem::take(&mut st.pending_ix_score[li]);
1513        let mut qk = std::mem::take(&mut st.prev_ix_kv[li]);
1514        let mut qs = std::mem::take(&mut st.prev_ix_score[li]);
1515        let entry = compressor_step(
1516            &ix.compressor,
1517            hidden,
1518            pos,
1519            rd,
1520            cfg.norm_eps,
1521            inv_freq,
1522            pool,
1523            &mut pk,
1524            &mut ps,
1525            &mut qk,
1526            &mut qs,
1527        );
1528        st.pending_ix_kv[li] = pk;
1529        st.pending_ix_score[li] = ps;
1530        st.prev_ix_kv[li] = qk;
1531        st.prev_ix_score[li] = qs;
1532        if let Some(e) = entry {
1533            st.index_kv[li].extend_from_slice(&e);
1534        }
1535    }
1536
1537    st.window[li].extend_from_slice(&kv);
1538    // The reference keeps the window in a ring of `window_size`; holding the
1539    // last N in order is the same set, and without this the "window" grows
1540    // for the whole generation — wrong attention AND unbounded memory.
1541    let cap = cfg.window * hd;
1542    if st.window[li].len() > cap {
1543        let drop = st.window[li].len() - cap;
1544        st.window[li].drain(..drop);
1545    }
1546    let win_len = st.window[li].len() / hd;
1547    let n_pos = win_len + st.compressed[li].len() / hd;
1548
1549    // Index list: every window position, plus whatever the indexer picked
1550    // (or, without an indexer, every compressed position).
1551    //
1552    // CMF_DSV4_NO_COMPRESSED=1 attends to the sliding window ALONE. That is
1553    // not a mode anyone should serve — it drops the model's long-range
1554    // memory — but it separates two failure modes that look identical from
1555    // the outside: output that degrades because the compressed path is
1556    // wrong, and output that degrades because the weights are too coarse.
1557    let mut idxs: Vec<usize> = (0..win_len).collect();
1558    if !st.compressed[li].is_empty() && !no_compressed() {
1559        let n_comp = st.compressed[li].len() / hd;
1560        match &l.indexer {
1561            Some(ix) => {
1562                // The indexer scores from the SHARED LoRA output through
1563                // its own wq_b — not from attention's queries — and its
1564                // per-head weights are a projection of the hidden state,
1565                // scaled by head_dim^-0.5 * n_heads^-0.5 as the reference
1566                // folds into `weights_proj`'s output.
1567                //
1568                // The reference also applies a randomized Hadamard rotation
1569                // to the queries here and to the keys in the indexer's
1570                // compressor, then simulates FP4 on both. That transform is
1571                // orthogonal (`hadamard_transform` scaled by d^-0.5) and it
1572                // hits BOTH sides of the same dot product, so it cancels:
1573                // its purpose is to condition the FP4 quantization, which we
1574                // do not do either. Omitting the pair is exact, and keeping
1575                // f32 is strictly more accurate than the reference — not an
1576                // approximation to be fixed later.
1577                let ih = ix.weights_proj.rows();
1578                let idim = ix.wq_b.rows() / ih.max(1);
1579                let mut qi = vec![0.0f32; ix.wq_b.rows()];
1580                ix.wq_b.matvec(&qr, &mut qi, pool);
1581                for h in 0..ih {
1582                    rope_tail(&mut qi[h * idim..(h + 1) * idim], inv_freq, pos, rd, false);
1583                }
1584                let mut hw = vec![0.0f32; ih];
1585                ix.weights_proj.matvec(hidden, &mut hw, pool);
1586                let sc_factor = (idim as f32).powf(-0.5) * (ih as f32).powf(-0.5);
1587                for w in hw.iter_mut() {
1588                    *w *= sc_factor;
1589                }
1590                let n_ix = st.index_kv[li].len() / idim.max(1);
1591                let mut sc = Vec::new();
1592                index_scores(
1593                    &qi,
1594                    &st.index_kv[li],
1595                    &hw,
1596                    ih,
1597                    idim,
1598                    n_ix.min(n_comp),
1599                    n_ix.min(n_comp),
1600                    pool,
1601                    &mut sc,
1602                );
1603                let mut picked = Vec::new();
1604                top_k_positions(&sc, cfg.index_topk, &mut picked);
1605                idxs.extend(picked.into_iter().map(|p| win_len + p));
1606            }
1607            None => idxs.extend((0..n_comp).map(|p| win_len + p)),
1608        }
1609    }
1610    debug_assert!(idxs.iter().all(|&p| p < n_pos));
1611    if let Some(p) = prep_out {
1612        p.qr = qr;
1613        p.idxs = idxs;
1614        p.win_len = win_len;
1615        return;
1616    }
1617
1618    // ── the whole block on the device, or nothing ──
1619    let scale = (hd as f32).powf(-0.5);
1620    #[cfg(feature = "gpu")]
1621    if on_gpu
1622        && {
1623            if std::env::var("CMF_DSV4_XCHK").is_ok() {
1624                // The frame reads this half's input from the card's x2
1625                // slot; the host walked its own. Disagreement = the
1626                // chain→walk handoff, and the number says by how much.
1627                if let Some(card) = crate::gpu_wgpu::dsv4_dbg_read_tag(45, 0, hidden.len()) {
1628                    let md = hidden
1629                        .iter()
1630                        .zip(card.iter())
1631                        .map(|(a, b)| (a - b).abs())
1632                        .fold(0.0f32, f32::max);
1633                    eprintln!("[xchk] li={li} pos={pos} x2 maxdiff={md:.3e}");
1634                }
1635            }
1636            true
1637        }
1638        && attn_frame(
1639            l, cfg, st, li, hidden, &qr, &idxs, inv_freq, pos, win_len, scale, None, out,
1640        )
1641    {
1642        return;
1643    }
1644
1645    // ── queries: wq_b, then a norm and the rope tail per head ──
1646    let mut q = vec![0.0f32; cfg.n_heads * hd];
1647    l.wq_b.matvec(&qr, &mut q, pool);
1648    for h in 0..cfg.n_heads {
1649        let head = &mut q[h * hd..(h + 1) * hd];
1650        rms_inplace(head, cfg.norm_eps);
1651        rope_tail(head, inv_freq, pos, rd, false);
1652    }
1653    let mut cache: Vec<f32> = st.window[li].clone();
1654    cache.extend_from_slice(&st.compressed[li]);
1655
1656    // ── sparse attention per head, then the inverse rope ──
1657    let mut attn = vec![0.0f32; cfg.n_heads * hd];
1658    for h in 0..cfg.n_heads {
1659        let qh = &q[h * hd..(h + 1) * hd];
1660        // Straight into this head's slice of the output: the scratch vector
1661        // that used to sit here was an allocation and a copy per head, so 64
1662        // of each per layer per token, for a value that was never read
1663        // anywhere else.
1664        let oh = &mut attn[h * hd..(h + 1) * hd];
1665        sparse_attend(qh, &cache, &idxs, l.attn_sink[h], scale, hd, oh);
1666        rope_tail(oh, inv_freq, pos, rd, true);
1667    }
1668
1669    // ── grouped low-rank output ──
1670    // Read the two blocks through the quantized readers. Materializing them
1671    // here instead costs ~270 MB of dequantization per layer per token on
1672    // the release checkpoint (wo_a and wo_b are 33M weights each), which is
1673    // the difference between decoding and not.
1674    o_project(
1675        &attn,
1676        &|r, x, sc| l.wo_a.row_dot(r, x, sc),
1677        l.wo_a.cols(),
1678        &|mid, dst| l.wo_b.matvec(mid, dst, pool),
1679        cfg.o_groups,
1680        cfg.o_lora_rank,
1681        pool,
1682        out,
1683    );
1684}
1685
1686/// RMSNorm with a learned weight, in place.
1687pub fn rms_weighted(v: &mut [f32], w: &[f32], eps: f32) {
1688    let ms = v.iter().map(|x| x * x).sum::<f32>() / v.len() as f32;
1689    let inv = 1.0 / (ms + eps).sqrt();
1690    for (x, g) in v.iter_mut().zip(w) {
1691        *x = *x * inv * g;
1692    }
1693}
1694
1695/// The MoE half of a block: route, run the chosen experts plus the shared
1696/// one, and sum. `token_id` is only read on the hash layers.
1697/// Per-layer expert-selection counts, the routing field a task-conditional
1698/// expert set is derived from (`CMF_MOE_STATS`). The generic MoE path keeps
1699/// these on its `MoeFfn`; this architecture has its own experts and never
1700/// touches that struct, so without this the field cannot be recorded for
1701/// DeepSeek-V4 at all — and its hash layers already make defrag useless, so
1702/// the only interesting question is what the OTHER forty layers do.
1703///
1704/// Decode drives this from one thread; the pool parallelizes inside the
1705/// matvecs, below this point.
1706thread_local! {
1707    static ROUTE_COUNTS: std::cell::RefCell<Vec<Vec<u64>>> =
1708        const { std::cell::RefCell::new(Vec::new()) };
1709}
1710
1711fn route_stats_on() -> bool {
1712    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1713    *ON.get_or_init(|| std::env::var("CMF_MOE_STATS").is_ok())
1714}
1715
1716fn record_route(li: usize, n_layers_hint: usize, n_experts: usize, idx: &[usize]) {
1717    ROUTE_COUNTS.with(|c| {
1718        let mut c = c.borrow_mut();
1719        if c.len() <= li.max(n_layers_hint) {
1720            c.resize(li.max(n_layers_hint) + 1, Vec::new());
1721        }
1722        let row = &mut c[li];
1723        if row.len() < n_experts {
1724            row.resize(n_experts, 0);
1725        }
1726        for &e in idx {
1727            if e < row.len() {
1728                row[e] += 1;
1729            }
1730        }
1731    });
1732}
1733
1734/// Take the recorded routing field, leaving the counters empty.
1735pub fn take_route_counts() -> Vec<Vec<u64>> {
1736    ROUTE_COUNTS.with(|c| std::mem::take(&mut *c.borrow_mut()))
1737}
1738
1739/// Charge elapsed time to a counter when it goes out of scope — the two
1740/// steps have several early returns each, and a timer that only stops on the
1741/// long path measures the short one as free.
1742struct Charge(Option<std::time::Instant>, &'static std::sync::atomic::AtomicU64);
1743impl Drop for Charge {
1744    fn drop(&mut self) {
1745        if let Some(t) = self.0 {
1746            self.1.fetch_add(
1747                t.elapsed().as_nanos() as u64,
1748                std::sync::atomic::Ordering::Relaxed,
1749            );
1750        }
1751    }
1752}
1753fn scopeguard_attn(t: Option<std::time::Instant>) -> Charge {
1754    Charge(t, &prof::ATTN_NS)
1755}
1756fn scopeguard_moe(t: Option<std::time::Instant>, li: usize) -> Charge {
1757    if t.is_some() {
1758        prof::note_layer(li);
1759    }
1760    Charge(t, &prof::MOE_NS)
1761}
1762
1763/// The whole token, one submission per layer. Returns false having changed
1764/// nothing if the device declines any layer — the caller's loop is then still
1765/// correct to run.
1766#[cfg(feature = "gpu")]
1767#[allow(clippy::too_many_arguments)]
1768fn dsv4_layer_loop(
1769    state: &mut [f32],
1770    layers: &[Dsv4Layer],
1771    g: &Dsv4Globals,
1772    cfg: &Dsv4Cfg,
1773    st: &mut Dsv4State,
1774    token_id: u32,
1775    inv_freq: &[f32],
1776    pool: Option<&crate::pool::Pool>,
1777    scratch: &mut HcScratch,
1778) -> bool {
1779    let dim = cfg.dim;
1780    let freqs_of = |l: &Dsv4Layer| -> &[f32] {
1781        let f = if l.compressor.is_some() {
1782            &g.inv_freq_compress
1783        } else {
1784            &g.inv_freq_window
1785        };
1786        if f.is_empty() { inv_freq } else { f.as_slice() }
1787    };
1788    // PRE-FLIGHT. The prep inside the loop advances the window and the
1789    // compressor caches, so a refusal halfway leaves state that the CPU
1790    // fallback would advance a SECOND time — which is not a slow answer but a
1791    // wrong one. Everything that can decline is therefore asked before the
1792    // first byte of state moves. The expert upload happens here too, which is
1793    // where it belonged anyway.
1794    // The head goes to the card BEFORE the experts ask for room. It is the
1795    // single most-used tensor in the file — every token reads all of it —
1796    // and it is a rounding error next to the expert stack: 265 MB against
1797    // ninety-odd gigabytes on the release. Uploaded in first-touch order it
1798    // arrived last, after the budget was gone, and stayed on the host for
1799    // the life of the process.
1800    {
1801        static SAID: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
1802        if !SAID.swap(true, std::sync::atomic::Ordering::Relaxed) {
1803            if let (Some(idx), Some(model)) = (g.head.model_idx(), g.head.model_arc()) {
1804                let ok = crate::gpu_wgpu::dsv4_weight_ready(&model, idx);
1805                tracing::info!("dsv4: голова на карте: {}", if ok { "да" } else { "нет" });
1806            }
1807        }
1808    }
1809    let mut on_dev = vec![false; layers.len()];
1810    let mut partial_dev = vec![false; layers.len()];
1811    for (li, l) in layers.iter().enumerate() {
1812        if l.wq_a.model_idx().is_none()
1813            || l.wq_b.model_idx().is_none()
1814            || l.wo_a.model_idx().is_none()
1815            || l.wo_b.model_idx().is_none()
1816        {
1817            return false;
1818        }
1819        let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
1820            return false;
1821        };
1822        let gu_q2 = l
1823            .experts
1824            .first()
1825            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
1826        // A layer whose experts do not fit is not a reason to abandon the
1827        // token: 100 GB of experts against a 98 GB card means SOME layer will
1828        // always miss. Those run on the host, with the state fetched and put
1829        // back around them — two transfers for the few that need it.
1830        // The attention weights have to be asked for too. Experts fill the
1831        // card first, and a wo_b that misses at layer 11 used to surface as a
1832        // mid-loop refusal — after the caches had advanced, which the CPU
1833        // fallback then advanced again.
1834        // …and, when the layer is to prepare itself, everything that
1835        // preparation reads: the KV projection, both compressors and the
1836        // indexer. Leaving them out is how the chain came to refuse ninety
1837        // times a token on the release — the experts had taken the card by
1838        // the time `dsv4_encode_prep` asked, and it declined silently into a
1839        // fallback that looked like "the chain simply does not help".
1840        let mut want = vec![
1841            l.wq_a.model_idx(),
1842            l.wq_b.model_idx(),
1843            l.wo_a.model_idx(),
1844            l.wo_b.model_idx(),
1845        ];
1846        if chain_enabled() {
1847            want.push(l.wkv.model_idx());
1848            if let Some(cp) = &l.compressor {
1849                want.push(cp.wkv.model_idx());
1850                want.push(cp.wgate.model_idx());
1851            }
1852            if let Some(ix) = &l.indexer {
1853                want.push(ix.wq_b.model_idx());
1854                want.push(ix.weights_proj.model_idx());
1855                want.push(ix.compressor.wkv.model_idx());
1856                want.push(ix.compressor.wgate.model_idx());
1857            }
1858        }
1859        let attn_ok = want
1860            .into_iter()
1861            .flatten()
1862            .all(|i| crate::gpu_wgpu::dsv4_weight_ready(&model, i));
1863        // Size the expert pack only AFTER this layer's attention skeleton is
1864        // resident. Otherwise the pack consumes the apparent free budget,
1865        // the much smaller skeleton arrives next, and the supposedly fitting
1866        // pack misses by exactly those bytes.
1867        let pk = pack_for(l, cfg, li);
1868        if let Some(pk) = pk {
1869            let dn_q2 = l
1870                .experts
1871                .first()
1872                .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
1873            let experts_ok = crate::gpu_wgpu::dsv4_experts_ready(
1874                &model,
1875                &pk.tensors,
1876                cfg.moe_inter,
1877                dim,
1878                gu_q2,
1879                dn_q2,
1880            );
1881            on_dev[li] = attn_ok && experts_ok && pk.globals.len() == cfg.n_routed_experts;
1882            partial_dev[li] = attn_ok && experts_ok && pk.globals.len() < cfg.n_routed_experts;
1883        }
1884    }
1885    let active_dev: Vec<bool> = on_dev
1886        .iter()
1887        .zip(&partial_dev)
1888        .map(|(&full, &partial)| full || partial)
1889        .collect();
1890    if !active_dev.iter().any(|&x| x) {
1891        return false;
1892    }
1893    // The attention gate below needs to know about partial layers BEFORE
1894    // the decode path commits the device set — a perplexity run only ever
1895    // prefills, and with this left empty every split budget scored the
1896    // model wrong (measured; see `attention_step`).
1897    if st.partial_set.len() != partial_dev.len() || st.partial_set != partial_dev {
1898        st.partial_set = partial_dev.clone();
1899        st.split_deep = active_dev
1900            .iter()
1901            .zip(&partial_dev)
1902            .filter(|(a, p)| !**a || **p)
1903            .count()
1904            > 1;
1905    }
1906
1907    // Which layers the card actually took, said once. A layer that falls to
1908    // the host costs an order of magnitude more than one that does not, and
1909    // "the GPU path is on" hid the difference between all of them and most.
1910    {
1911        static SAID: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
1912        if !SAID.swap(true, std::sync::atomic::Ordering::Relaxed) {
1913            let host: Vec<usize> = active_dev
1914                .iter()
1915                .enumerate()
1916                .filter(|&(_, d)| !*d)
1917                .map(|(i, _)| i)
1918                .collect();
1919            let partial: Vec<(usize, usize)> = partial_dev
1920                .iter()
1921                .enumerate()
1922                .filter(|&(_, d)| *d)
1923                .filter_map(|(li, _)| pack_for(&layers[li], cfg, li).map(|p| (li, p.globals.len())))
1924                .collect();
1925            if host.is_empty() && partial.is_empty() {
1926                tracing::info!("dsv4: все {} слоёв на карте", on_dev.len());
1927            } else {
1928                tracing::info!(
1929                    "dsv4: {} из {} слоёв используют карту; частичные {:?}; на хосте {:?}",
1930                    active_dev.len() - host.len(),
1931                    on_dev.len(),
1932                    partial,
1933                    host,
1934                );
1935            }
1936        }
1937    }
1938
1939    // Layer zero's opening fold has no frame before it to have prepared it.
1940    let (mut folded, post0, comb0) = hc_fold_norm(
1941        state,
1942        &layers[0].hc_attn_fn,
1943        &layers[0].hc_attn_scale,
1944        &layers[0].hc_attn_base,
1945        &layers[0].attn_norm,
1946        cfg,
1947        pool,
1948    );
1949    if !crate::gpu_wgpu::dsv4_state_write(state)
1950        || !crate::gpu_wgpu::dsv4_hc_write(&post0, &comb0)
1951    {
1952        return false;
1953    }
1954    // The device-owned set must not move once a token has run on it — but
1955    // the two directions are not the same risk. At a tight budget the set
1956    // GROWS between tokens as more weights finish uploading, and a layer that
1957    // merely joined can be left on the host: its caches are there and nothing
1958    // is inconsistent. Refusing on that was costing the whole fast path once
1959    // per token — 125 times in a 48-token run on an emulated 24 GB card, on
1960    // which the engine is slow enough already.
1961    //
1962    // A layer LEAVING the set is the dangerous direction: its caches are on
1963    // the card and the host would advance its own. That still refuses.
1964    if st.dev_owned && st.dev_set != active_dev {
1965        let left: Vec<usize> = (0..active_dev.len().min(st.dev_set.len()))
1966            .filter(|&i| st.dev_set[i] && !active_dev[i])
1967            .collect();
1968        if !left.is_empty() {
1969            tracing::warn!("слои {left:?} ушли с карты — кеши на разных сторонах");
1970            return false;
1971        }
1972        // A layer that was active remains device-owned. Its full/partial mode
1973        // is still derived from the current pack; only cache ownership is
1974        // sticky across tokens.
1975    }
1976    let chain = chain_enabled();
1977    // CMF_DSV4_LAYERS_PROBE=N — TIMING ONLY, the answer is garbage. Runs the
1978    // first N layers and leaves the rest alone. Decode time against N is a
1979    // line whose SLOPE is the per-layer cost and whose intercept is
1980    // everything that happens once a token. Unlike the skip probe it does
1981    // not change what a layer does — which on a MoE model is the difference
1982    // between a measurement and an artefact, because dropping any stage
1983    // changes the routing and the routing changes what the experts cost.
1984    let layer_cap = {
1985        static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
1986        *N.get_or_init(|| {
1987            std::env::var("CMF_DSV4_LAYERS_PROBE")
1988                .ok()
1989                .and_then(|v| v.parse::<usize>().ok())
1990                .unwrap_or(usize::MAX)
1991        })
1992    };
1993    let mut run: Vec<usize> = Vec::new();
1994    let mut sink_out = vec![0.0f32; dim];
1995    // `state` starts current on both sides. A device run makes the host copy
1996    // stale unless that same run carries it home. Tracking this explicitly
1997    // avoids a separate state fence before a host layer and, for a final host
1998    // layer, the old upload-immediately-followed-by-readback pair.
1999    let mut state_on_host = true;
2000    for (li, l) in layers.iter().enumerate() {
2001        if li >= layer_cap {
2002            break;
2003        }
2004        // The device path never ticked the profiler, so every per-token
2005        // number it printed described the two host-path tokens at the start
2006        // of a run — the ones that also pay for the upload. Ticking here is
2007        // what makes the chain's encode-and-wait split a per-token figure at
2008        // all.
2009        if prof::on() {
2010            prof::note_layer(li);
2011        }
2012        if chain && on_dev[li] {
2013            // Hash layers used to break the run in two: their forced expert
2014            // list changes per token, went through the (tag, len) upload
2015            // pool, and every layer of a submission shared one buffer. The
2016            // list has a per-layer slot now, so they chain like the rest.
2017            run.push(li);
2018            // CMF_DSV4_CHAIN_MAX=N caps a run's length. Diagnostic, not a
2019            // tuning knob: length-1 runs put ONE layer per submission, which
2020            // separates "the layer frame is wrong" from "layers in one
2021            // encoder contaminate each other" in a single ppl run.
2022            if run.len() >= chain_max() || dspark_wants(li) {
2023                let need_qn = run[0] == 0 || !on_dev[run[0] - 1];
2024                let captured = *run.last().unwrap();
2025                if !dsv4_chain_run(
2026                    layers, &run, cfg, g, st, token_id, &mut folded,
2027                    Some(state), 1, &[], need_qn, pool,
2028                ) {
2029                    return false;
2030                }
2031                state_on_host = true;
2032                dspark_note(captured, state, cfg);
2033                run.clear();
2034            }
2035            continue;
2036        }
2037        if chain && !run.is_empty() {
2038            // The very next layer is on the host, so bring its state back in
2039            // the chain's existing readback. Reading it in a second submit
2040            // below cost one fence per token on the release's 42+1 split.
2041            if !dsv4_chain_run(
2042                layers,
2043                &run,
2044                cfg,
2045                g,
2046                st,
2047                token_id,
2048                &mut folded,
2049                Some(state),
2050                1,
2051                &[],
2052                run[0] == 0 || !on_dev[run[0] - 1],
2053                pool,
2054            ) {
2055                return false;
2056            }
2057            state_on_host = true;
2058            dspark_note(*run.last().unwrap(), state, cfg);
2059        }
2060        run.clear();
2061        if partial_dev[li] && partial_walk_on() {
2062            // Attention and the resident expert subset stay on the card. The
2063            // router still sees every expert and returns only the winners
2064            // that did not fit; those are completed on the CPU and their
2065            // exact linear contribution is added back to device state.
2066            let Some(home) = dsv4_partial_layer(
2067                state,
2068                &mut folded,
2069                layers,
2070                l,
2071                cfg,
2072                st,
2073                token_id,
2074                li,
2075                freqs_of(l),
2076                pool,
2077            ) else {
2078                return false;
2079            };
2080            state_on_host = home;
2081            if home {
2082                dspark_note(li, state, cfg);
2083            }
2084            continue;
2085        }
2086        if !on_dev[li] {
2087            if !state_on_host && !crate::gpu_wgpu::dsv4_state_read(state) {
2088                return false;
2089            }
2090            state_on_host = true;
2091            let freqs = freqs_of(l);
2092            hc_block(
2093                state,
2094                &l.hc_attn_fn,
2095                &l.hc_attn_scale,
2096                &l.hc_attn_base,
2097                &l.attn_norm,
2098                cfg,
2099                scratch,
2100                pool,
2101                |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
2102            );
2103            hc_block(
2104                state,
2105                &l.hc_ffn_fn,
2106                &l.hc_ffn_scale,
2107                &l.hc_ffn_base,
2108                &l.ffn_norm,
2109                cfg,
2110                scratch,
2111                pool,
2112                // The layer the card had no room for. Its experts are
2113                // reached one matvec at a time and the probe sends each to
2114                // the device — right per op, and a fence per op: this one
2115                // layer is why a token that submits ONCE for 42 layers
2116                // submits 13 times. CMF_DSV4_HOST_CPU_MOE=1 keeps them on
2117                // the host instead, trading arithmetic for round trips.
2118                |f, o| {
2119                    if host_cpu_moe() {
2120                        crate::gpu::cpu_scope(|| moe_step(f, l, cfg, token_id, li, pool, o))
2121                    } else {
2122                        moe_step(f, l, cfg, token_id, li, pool, o)
2123                    }
2124                },
2125            );
2126            // Only a following DEVICE layer needs the fold/hc slots and an
2127            // uploaded state. Consecutive host layers consume `state`
2128            // directly, and a final host layer is already exactly where the
2129            // head needs it — uploading then reading it back was pure sync.
2130            if layers.get(li + 1).is_some() && on_dev.get(li + 1).copied().unwrap_or(false) {
2131                let n = &layers[li + 1];
2132                let (f, p2, c2) = hc_fold_norm(
2133                    state,
2134                    &n.hc_attn_fn,
2135                    &n.hc_attn_scale,
2136                    &n.hc_attn_base,
2137                    &n.attn_norm,
2138                    cfg,
2139                    pool,
2140                );
2141                folded = f;
2142                if !crate::gpu_wgpu::dsv4_hc_write(&p2, &c2) {
2143                    return false;
2144                }
2145                if !crate::gpu_wgpu::dsv4_state_write(state) {
2146                    return false;
2147                }
2148            }
2149            dspark_note(li, state, cfg);
2150            continue;
2151        }
2152        let mut prep = AttnPrep::default();
2153        attention_step(
2154            &folded,
2155            l,
2156            cfg,
2157            st,
2158            li,
2159            freqs_of(l),
2160            pool,
2161            Some(&mut prep),
2162            &mut sink_out,
2163        );
2164        // The caches the frame will read.
2165        let hd = cfg.head_dim;
2166        let n_comp = st.compressed[li].len() / hd;
2167        let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
2168        let kv_id = st.kv_id;
2169        if !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, 0, &st.window[li], cap)
2170            || (n_comp > 0
2171                && !crate::gpu_wgpu::dsv4_cache_write(
2172                    kv_id,
2173                    li,
2174                    cfg.window * hd,
2175                    &st.compressed[li],
2176                    cap,
2177                ))
2178        {
2179            return false;
2180        }
2181        let idx32: Vec<u32> = prep
2182            .idxs
2183            .iter()
2184            .map(|&p| {
2185                if p < prep.win_len {
2186                    p as u32
2187                } else {
2188                    (cfg.window + (p - prep.win_len)) as u32
2189                }
2190            })
2191            .collect();
2192        let Some(pk) = pack_for(l, cfg, li) else {
2193            return false;
2194        };
2195        let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b)) = (
2196            l.wq_a.model_idx(),
2197            l.wq_b.model_idx(),
2198            l.wo_a.model_idx(),
2199            l.wo_b.model_idx(),
2200        ) else {
2201            return false;
2202        };
2203        let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
2204            return false;
2205        };
2206        let forced: Option<Vec<usize>> = l.tid2eid.as_ref().and_then(|tbl| {
2207            let v: Vec<usize> = hash_route(tbl, cfg.vocab, cfg.top_k, token_id)
2208                .into_iter()
2209                .map(|gi| pk.to_slot[gi])
2210                .collect();
2211            if v.iter().any(|&x| x == usize::MAX) {
2212                None
2213            } else {
2214                Some(v)
2215            }
2216        });
2217        if l.tid2eid.is_some() && forced.is_none() {
2218            return false;
2219        }
2220        let nxt = layers.get(li + 1);
2221        let w = crate::gpu_wgpu::Dsv4LayerW {
2222            attn: crate::gpu_wgpu::Dsv4AttnW {
2223                wq_a,
2224                wq_b,
2225                wo_a,
2226                wo_b,
2227                q_norm: &l.q_norm,
2228                sink: &l.attn_sink,
2229            },
2230            moe: crate::gpu_wgpu::Dsv4MoeW {
2231                router: &[],
2232                experts: &pk.tensors,
2233                logits: &[],
2234                // The PACK's bias, whose address outlives the process: the
2235                // frame's const cache is keyed on it, and a per-layer Vec
2236                // here handed every layer the first layer's — the exact
2237                // transient-Vec trap the const_buf war story describes,
2238                // reintroduced by this session and caught because the OFF
2239                // baseline moved.
2240                bias: pk.bias.as_deref(),
2241                forced: forced.as_deref(),
2242                remap: None,
2243            },
2244            hc_ffn_fn: &l.hc_ffn_fn,
2245            hc_ffn_scale: &l.hc_ffn_scale,
2246            hc_ffn_base: &l.hc_ffn_base,
2247            hc_next_fn: nxt.map(|n| n.hc_attn_fn.as_slice()),
2248            hc_next_scale: nxt.map_or(&l.hc_attn_scale, |n| &n.hc_attn_scale),
2249            hc_next_base: nxt.map_or(&l.hc_attn_base, |n| n.hc_attn_base.as_slice()),
2250            ffn_norm: &l.ffn_norm,
2251            next_norm: nxt.map_or(&l.attn_norm, |n| n.attn_norm.as_slice()),
2252            next_q_norm: nxt.map_or(&l.q_norm, |n| n.q_norm.as_slice()),
2253            next_wq_a: nxt.and_then(|n| n.wq_a.model_idx()),
2254            router: &pk.router,
2255        };
2256        let geom = crate::gpu_wgpu::Dsv4LayerGeom {
2257            attn: crate::gpu_wgpu::Dsv4AttnGeom {
2258                dim,
2259                nh: cfg.n_heads,
2260                hd,
2261                rd: cfg.rope_head_dim,
2262                q_lora: cfg.q_lora_rank,
2263                o_lora: cfg.o_lora_rank,
2264                o_groups: cfg.o_groups,
2265                eps: cfg.norm_eps,
2266                scale: (hd as f32).powf(-0.5),
2267            },
2268            moe: crate::gpu_wgpu::Dsv4MoeGeom {
2269                hidden: dim,
2270                inter: cfg.moe_inter,
2271                top_k: cfg.top_k,
2272                route_scale: cfg.route_scale,
2273                swiglu_limit: cfg.swiglu_limit,
2274                gu_q2: l.experts.first().is_some_and(|e| {
2275                    e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
2276                }),
2277            },
2278            hc: cfg.hc_mult,
2279            hc_eps: cfg.hc_eps,
2280            sinkhorn_iters: cfg.hc_sinkhorn_iters,
2281        };
2282        let mut next = vec![0.0f32; dim];
2283        if !crate::gpu_wgpu::dsv4_layer_frame(
2284            &model,
2285            &w,
2286            geom,
2287            kv_id,
2288            li,
2289            Some(&prep.qr),
2290            &idx32,
2291            freqs_of(l),
2292            st.pos,
2293            &mut next,
2294        ) {
2295            return false;
2296        }
2297        state_on_host = false;
2298        folded = next;
2299        dspark_note(li, state, cfg);
2300    }
2301    let mut state_home = false;
2302    if chain {
2303        if !run.is_empty() {
2304            // The token's LAST run brings the state back with it. Only the
2305            // last: an earlier run's state is one the layers after it still
2306            // change.
2307            let need_qn = run[0] == 0 || !on_dev[run[0] - 1];
2308            let last_on_dev = *on_dev.last().unwrap_or(&false);
2309            let carry = last_on_dev && run.last() == Some(&(layers.len() - 1));
2310            let ok = if carry {
2311                let r = dsv4_chain_run(
2312                    layers, &run, cfg, g, st, token_id, &mut folded,
2313                    Some(state), 1, &[], need_qn, pool,
2314                );
2315                state_home = r;
2316                state_on_host = r;
2317                if r {
2318                    dspark_note(*run.last().unwrap(), state, cfg);
2319                }
2320                r
2321            } else {
2322                let r = dsv4_chain_run(
2323                    layers, &run, cfg, g, st, token_id, &mut folded, None, 1, &[], need_qn, pool,
2324                );
2325                if r {
2326                    state_on_host = false;
2327                }
2328                r
2329            };
2330            if !ok {
2331                return false;
2332            }
2333        }
2334        if st.dev_set.is_empty() {
2335            st.dev_set = active_dev.clone();
2336            st.partial_set = partial_dev.clone();
2337            // The set is committed, so the card must keep it. Eviction by
2338            // score is right while the set is still being chosen and wrong
2339            // afterwards: an evicted layer drops off the card while its
2340            // caches stay there, and the loop then refuses the whole fast
2341            // path rather than read state from two sides.
2342            let mut idxs = Vec::new();
2343            for (li, l) in layers.iter().enumerate() {
2344                if !active_dev.get(li).copied().unwrap_or(false) {
2345                    continue;
2346                }
2347                for t in [&l.wq_a, &l.wq_b, &l.wkv, &l.wo_a, &l.wo_b, &l.gate] {
2348                    idxs.extend(t.model_idx());
2349                }
2350                if let Some(pk) = pack_for(l, cfg, li) {
2351                    for &(a, b, c) in &pk.tensors {
2352                        idxs.extend([a, b, c]);
2353                    }
2354                }
2355            }
2356            // Why a HOST layer stayed on the host, said in numbers. Its MoE
2357            // can still run on the card with a partial pack — `moe_frame` has
2358            // the remap and hands cold picks back — so the interesting figure
2359            // is how many experts it got. Zero means the upload order never
2360            // reached it; a few hundred means the readiness gate refused. The
2361            // two have different fixes and reading the code cannot tell them
2362            // apart.
2363            for (li, l) in layers.iter().enumerate() {
2364                if active_dev.get(li).copied().unwrap_or(false) {
2365                    continue;
2366                }
2367                let packed = pack_for(l, cfg, li).map_or(0, |p| p.globals.len());
2368                tracing::info!(
2369                    "слой {li} на хосте: упаковано {packed} экспертов из {}",
2370                    cfg.n_routed_experts
2371                );
2372            }
2373            let pinned = layers
2374                .iter()
2375                .find_map(|l| l.experts.first().and_then(|e| e.w1.model_arc()))
2376                .map_or(0, |m| crate::gpu_wgpu::pin_weights(&m, &idxs));
2377            tracing::info!(
2378                "закреплено на карте: {pinned} тензоров {} слоёв",
2379                on_dev.iter().filter(|&&x| x).count()
2380            );
2381        }
2382    }
2383    if state_home || state_on_host {
2384        return true;
2385    }
2386    crate::gpu_wgpu::dsv4_state_read(state)
2387}
2388
2389/// Run a layer whose attention skeleton fits but only a subset of its MoE
2390/// experts does. This path is selected from the live VRAM budget, never from
2391/// a layer number. It is exact: routing spans all experts and cold winners
2392/// are folded back into the hyper-connection state before the next layer.
2393#[cfg(feature = "gpu")]
2394#[allow(clippy::too_many_arguments)]
2395fn dsv4_partial_layer(
2396    state: &mut [f32],
2397    folded: &mut Vec<f32>,
2398    layers: &[Dsv4Layer],
2399    l: &Dsv4Layer,
2400    cfg: &Dsv4Cfg,
2401    st: &mut Dsv4State,
2402    token_id: u32,
2403    li: usize,
2404    freqs: &[f32],
2405    pool: Option<&crate::pool::Pool>,
2406) -> Option<bool> {
2407    let dim = cfg.dim;
2408    let mut prep = AttnPrep::default();
2409    let mut sink = vec![0.0f32; dim];
2410    attention_step(
2411        folded,
2412        l,
2413        cfg,
2414        st,
2415        li,
2416        freqs,
2417        pool,
2418        Some(&mut prep),
2419        &mut sink,
2420    );
2421    let hd = cfg.head_dim;
2422    let n_comp = st.compressed[li].len() / hd;
2423    let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
2424    if !crate::gpu_wgpu::dsv4_cache_write(st.kv_id, li, 0, &st.window[li], cap)
2425        || (n_comp > 0
2426            && !crate::gpu_wgpu::dsv4_cache_write(
2427                st.kv_id,
2428                li,
2429                cfg.window * hd,
2430                &st.compressed[li],
2431                cap,
2432            ))
2433    {
2434        return None;
2435    }
2436    let a_tail = crate::gpu_wgpu::Dsv4HcTail {
2437        fn_: &l.hc_ffn_fn,
2438        scale: &l.hc_ffn_scale,
2439        base: &l.hc_ffn_base,
2440        norm: &l.ffn_norm,
2441        hc: cfg.hc_mult,
2442        sinkhorn_iters: cfg.hc_sinkhorn_iters,
2443        hc_eps: cfg.hc_eps,
2444        eps: cfg.norm_eps,
2445    };
2446    let scale = (cfg.head_dim as f32).powf(-0.5);
2447    if !attn_frame(
2448        l,
2449        cfg,
2450        st,
2451        li,
2452        folded,
2453        &prep.qr,
2454        &prep.idxs,
2455        freqs,
2456        st.pos,
2457        prep.win_len,
2458        scale,
2459        Some(&a_tail),
2460        &mut [],
2461    ) {
2462        return None;
2463    }
2464    let nxt = layers.get(li + 1);
2465    let forced = l
2466        .tid2eid
2467        .as_ref()
2468        .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
2469    let mut next = vec![0.0f32; dim];
2470    let (cold_sum, cold_count) = moe_frame(
2471        &[],
2472        l,
2473        cfg,
2474        li,
2475        &[],
2476        forced.as_deref(),
2477        pool,
2478        Some(&a_tail),
2479        // Do not pre-fold the next layer yet. That fold reuses the canonical
2480        // `post` slot; a cold correction still needs THIS layer's post. Once
2481        // the corrected state is home, the exact next fold is cheap on the
2482        // host and seeds either another partial frame or the next full run.
2483        None,
2484        &mut next,
2485    )?;
2486    // The resident contribution has already been expanded on the device. If
2487    // there were cold winners, add `post[j] * cold_sum` and retrieve the
2488    // corrected state in that submission; otherwise a plain readback is
2489    // enough. This state handoff is what makes partial layers composable at
2490    // arbitrary positions, not just at the tail of one checkpoint.
2491    let state_ok = if cold_count == 0 {
2492        crate::gpu_wgpu::dsv4_state_read(state)
2493    } else {
2494        crate::gpu_wgpu::dsv4_state_add_cold(&cold_sum, cfg.hc_mult, state)
2495    };
2496    if !state_ok {
2497        return None;
2498    }
2499    if let Some(n) = nxt {
2500        let (f, post, comb) = hc_fold_norm(
2501            state,
2502            &n.hc_attn_fn,
2503            &n.hc_attn_scale,
2504            &n.hc_attn_base,
2505            &n.attn_norm,
2506            cfg,
2507            pool,
2508        );
2509        *folded = f;
2510        if !crate::gpu_wgpu::dsv4_hc_write(&post, &comb)
2511            || !crate::gpu_wgpu::dsv4_state_write(state)
2512        {
2513            return None;
2514        }
2515    }
2516    // NB: the CALLER notes this layer for the draft's ring — a note here
2517    // as well double-counts the capture and fails `dspark_take`'s
2518    // completeness check (seen 4 of 3, measured), which reads exactly like
2519    // the starvation it was meant to fix.
2520    Some(true)
2521}
2522
2523#[cfg(feature = "gpu")]
2524fn chain_max() -> usize {
2525    static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
2526    *N.get_or_init(|| {
2527        std::env::var("CMF_DSV4_CHAIN_MAX")
2528            .ok()
2529            .and_then(|v| v.parse().ok())
2530            .unwrap_or(usize::MAX)
2531    })
2532}
2533
2534/// `CMF_DSV4_CHAIN=1`: put a run of consecutive device-capable layers in ONE
2535/// submission. Off by default until it has been measured on a real card.
2536#[cfg(feature = "gpu")]
2537/// `CMF_DSV4_HOST_CPU_MOE=1`: a layer that fell off the card runs its MoE on
2538/// the host WITHOUT the per-op device route — one fence a token instead of
2539/// one a matvec. Whether that wins is a measurement.
2540/// `CMF_DSV4_PARTIAL_WALK=1`: the fused device walk of a partial layer.
2541/// OFF until its self-poisoning is repaired: its attention frame reads the
2542/// pooled slots its own MoE frame rewrote on the previous token, so every
2543/// token after the first attends over leftovers — the drafts it captures
2544/// от такого состояния never match the verify (acceptance 0, measured).
2545/// The host branch walks these layers correctly; the pack stays resident
2546/// for the verify tail.
2547fn partial_walk_on() -> bool {
2548    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2549    *ON.get_or_init(|| std::env::var("CMF_DSV4_PARTIAL_WALK").is_ok_and(|v| v != "0"))
2550}
2551
2552fn host_cpu_moe() -> bool {
2553    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2554    *ON.get_or_init(|| std::env::var("CMF_DSV4_HOST_CPU_MOE").is_ok_and(|v| v != "0"))
2555}
2556
2557fn chain_enabled() -> bool {
2558    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2559    *ON.get_or_init(|| std::env::var("CMF_DSV4_CHAIN").map(|v| v != "0").unwrap_or(true))
2560}
2561
2562/// Encode a maximal run of consecutive device-capable layers and submit it
2563/// ONCE. Every layer in the run builds its own attention inputs on the card,
2564/// so nothing comes back between them — that is the whole saving.
2565///
2566/// The run's state belongs to the device from here on: `st.window`,
2567/// `st.compressed` and the compressor streams for these layers are stale on
2568/// the host afterwards, and only the counts in `st.dev_*` are kept. A layer
2569/// that has ever been in a run must therefore never be handed to the CPU
2570/// path again, which `dev_owned` records.
2571#[cfg(feature = "gpu")]
2572#[allow(clippy::too_many_arguments)]
2573fn dsv4_chain_run(
2574    layers: &[Dsv4Layer],
2575    run: &[usize],
2576    cfg: &Dsv4Cfg,
2577    g: &Dsv4Globals,
2578    st: &mut Dsv4State,
2579    token_id: u32,
2580    // In AND out: the run reads the fold it starts from and MUST leave the
2581    // fold it produced, because whatever follows — a host layer, or the next
2582    // run after a cap — seeds from this. Passing it read-only left every
2583    // later segment starting from a stale fold: exact with one unbroken run,
2584    // release-scale garbage the moment anything splits the chain.
2585    folded: &mut Vec<f32>,
2586    // When present, the hyper-connection state rides home in the run's own
2587    // submission instead of costing a second fence afterwards. Only the
2588    // token's LAST run passes it — an earlier one would read a state the
2589    // layers after it still change.
2590    state_out: Option<&mut [f32]>,
2591    // How many consecutive tokens this run carries. One is decode; more is a
2592    // prompt chunk or a speculative verify, which are the same shape of work.
2593    batch: usize,
2594    // Their ids, needed only when `batch > 1`: a hash layer forces its expert
2595    // list from the token's id, so the batch needs one list per token and the
2596    // single `token_id` above cannot supply them.
2597    batch_ids: &[u32],
2598    // Whether the device's qn buffer is stale: true at layer zero and after
2599    // a host layer. When the previous layer was chained, its frame's tail
2600    // already left THIS layer's LoRA vector on the card, and recomputing it
2601    // here was a full wq_a matvec on the CPU per run — at CHAIN_MAX=1 that
2602    // is one per LAYER, which is how a 43-fence path measured slower than
2603    // an 86-fence one.
2604    need_qn: bool,
2605    pool: Option<&crate::pool::Pool>,
2606) -> bool {
2607    if run.is_empty() {
2608        return true;
2609    }
2610    let (dim, hd) = (cfg.dim, cfg.head_dim);
2611    let first = run[0];
2612    let Some(model) = layers[first].experts.first().and_then(|e| e.w1.model_arc()) else {
2613        return false;
2614    };
2615    // Batch callers seed every token's fold and qn in its own slot. Seeding
2616    // the legacy shared slot here is not merely redundant: `folded` carries
2617    // only the eventual LAST output and is empty before the batch runs.
2618    if batch <= 1 && need_qn {
2619        let mut qn0 = vec![0.0f32; cfg.q_lora_rank];
2620        layers[first].wq_a.matvec(folded, &mut qn0, pool);
2621        rms_weighted(&mut qn0, &layers[first].q_norm, cfg.norm_eps);
2622        if !crate::gpu_wgpu::dsv4_chain_seed(folded, &qn0) {
2623            return false;
2624        }
2625    } else if batch <= 1 && !crate::gpu_wgpu::dsv4_chain_seed_fold(folded) {
2626        return false;
2627    }
2628
2629    // Held apart from the borrowing structs below, which point into them.
2630    let mut packs = Vec::with_capacity(run.len());
2631    let mut forceds: Vec<Option<Vec<usize>>> = Vec::with_capacity(run.len());
2632    for &li in run {
2633        let Some(pk) = pack_for(&layers[li], cfg, li) else {
2634            return false;
2635        };
2636        let forced: Option<Vec<usize>> = layers[li].tid2eid.as_ref().and_then(|tbl| {
2637            let v: Vec<usize> = hash_route(tbl, cfg.vocab, cfg.top_k, token_id)
2638                .into_iter()
2639                .map(|gi| pk.to_slot[gi])
2640                .collect();
2641            if v.iter().any(|&x| x == usize::MAX) { None } else { Some(v) }
2642        });
2643        if layers[li].tid2eid.is_some() && forced.is_none() {
2644            return false;
2645        }
2646        forceds.push(forced);
2647        packs.push(pk);
2648    }
2649
2650    let mut items = Vec::with_capacity(run.len());
2651    let mut freqs = Vec::with_capacity(run.len());
2652    for (i, &li) in run.iter().enumerate() {
2653        let l = &layers[li];
2654        let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b), Some(wkv)) = (
2655            l.wq_a.model_idx(),
2656            l.wq_b.model_idx(),
2657            l.wo_a.model_idx(),
2658            l.wo_b.model_idx(),
2659            l.wkv.model_idx(),
2660        ) else {
2661            return false;
2662        };
2663        let comp = match &l.compressor {
2664            None => None,
2665            Some(cp) => {
2666                let (Some(a), Some(b)) = (cp.wkv.model_idx(), cp.wgate.model_idx()) else {
2667                    return false;
2668                };
2669                Some((
2670                    crate::gpu_wgpu::Dsv4CompW { wkv: a, wgate: b, norm: &cp.norm, ape: &cp.ape },
2671                    crate::gpu_wgpu::Dsv4CompGeom {
2672                        width: cp.wkv.rows(),
2673                        hidden: dim,
2674                        ratio: cp.ratio,
2675                        overlap: cp.overlap,
2676                        rope_dim: cfg.rope_head_dim,
2677                        eps: cfg.norm_eps,
2678                    },
2679                ))
2680            }
2681        };
2682        let ix = match &l.indexer {
2683            None => None,
2684            Some(ixr) => {
2685                let cp = &ixr.compressor;
2686                let (Some(a), Some(b), Some(qb), Some(wp)) = (
2687                    cp.wkv.model_idx(),
2688                    cp.wgate.model_idx(),
2689                    ixr.wq_b.model_idx(),
2690                    ixr.weights_proj.model_idx(),
2691                ) else {
2692                    return false;
2693                };
2694                let ih = ixr.weights_proj.rows();
2695                Some((
2696                    crate::gpu_wgpu::Dsv4CompW { wkv: a, wgate: b, norm: &cp.norm, ape: &cp.ape },
2697                    crate::gpu_wgpu::Dsv4CompGeom {
2698                        width: cp.wkv.rows(),
2699                        hidden: dim,
2700                        ratio: cp.ratio,
2701                        overlap: cp.overlap,
2702                        rope_dim: cfg.rope_head_dim,
2703                        eps: cfg.norm_eps,
2704                    },
2705                    crate::gpu_wgpu::Dsv4IxW { wq_b: qb, weights_proj: wp },
2706                    crate::gpu_wgpu::Dsv4IxGeom {
2707                        ih,
2708                        idim: ixr.wq_b.rows() / ih.max(1),
2709                        q_lora: cfg.q_lora_rank,
2710                        hidden: dim,
2711                        rope_dim: cfg.rope_head_dim,
2712                        eps: cfg.norm_eps,
2713                        top_k: cfg.index_topk,
2714                        window: cfg.window,
2715                    },
2716                ))
2717            }
2718        };
2719        // The cache has to be big enough BEFORE the frame appends into it:
2720        // a chained layer never calls dsv4_cache_write, which is what used
2721        // to create and grow it.
2722        let ew_c0 = l.compressor.as_ref().map_or(0, |cp| {
2723            if cp.overlap { cp.wkv.rows() / 2 } else { cp.wkv.rows() }
2724        });
2725        let comp_extra = l.compressor.as_ref().map_or(0, |cp| {
2726            batch.max(1).div_ceil(cp.ratio.max(1))
2727        });
2728        let need = cfg.window * hd
2729            + (st.dev_n_comp[li] + comp_extra + 1) * ew_c0.max(1)
2730            + (batch.max(1) + 1) * hd;
2731        if !crate::gpu_wgpu::dsv4_cache_ensure(st.kv_id, li, need.next_power_of_two()) {
2732            return false;
2733        }
2734        let ew_c = comp.as_ref().map_or(0, |(_, cg)| {
2735            if cg.overlap { cg.width / 2 } else { cg.width }
2736        });
2737        let ew_i = ix.as_ref().map_or(0, |(_, cg, _, _)| {
2738            if cg.overlap { cg.width / 2 } else { cg.width }
2739        });
2740        let prep = crate::gpu_wgpu::Dsv4Prep {
2741            wkv,
2742            kv_norm: &l.kv_norm,
2743            comp,
2744            ix,
2745            filled: st.dev_filled[li],
2746            window: cfg.window,
2747            n_comp: st.dev_n_comp[li],
2748            n_ix: st.dev_n_ix[li],
2749            comp_dst_off: cfg.window * hd + st.dev_n_comp[li] * ew_c,
2750            ix_dst_off: st.dev_n_ix[li] * ew_i,
2751            idx_cap: cfg.window
2752                + if l.indexer.is_some() {
2753                    cfg.index_topk
2754                } else {
2755                    st.dev_n_comp[li] + comp_extra + 1
2756                },
2757        };
2758        let nxt = layers.get(li + 1);
2759        let w = crate::gpu_wgpu::Dsv4LayerW {
2760            attn: crate::gpu_wgpu::Dsv4AttnW {
2761                wq_a, wq_b, wo_a, wo_b,
2762                q_norm: &l.q_norm,
2763                sink: &l.attn_sink,
2764            },
2765            moe: crate::gpu_wgpu::Dsv4MoeW {
2766                router: &packs[i].router,
2767                experts: &packs[i].tensors,
2768                logits: &[],
2769                // The PACK's slice, not a per-run Vec: the address stability
2770                // is the whole point (see Pack::bias).
2771                bias: packs[i].bias.as_deref(),
2772                forced: forceds[i].as_deref(),
2773                remap: None,
2774            },
2775            hc_ffn_fn: &l.hc_ffn_fn,
2776            hc_ffn_scale: &l.hc_ffn_scale,
2777            hc_ffn_base: &l.hc_ffn_base,
2778            hc_next_fn: nxt.map(|n| n.hc_attn_fn.as_slice()),
2779            hc_next_scale: nxt.map_or(&l.hc_attn_scale, |n| &n.hc_attn_scale),
2780            hc_next_base: nxt.map_or(&l.hc_attn_base, |n| n.hc_attn_base.as_slice()),
2781            ffn_norm: &l.ffn_norm,
2782            next_norm: nxt.map_or(&l.attn_norm, |n| n.attn_norm.as_slice()),
2783            next_q_norm: nxt.map_or(&l.q_norm, |n| n.q_norm.as_slice()),
2784            next_wq_a: nxt.and_then(|n| n.wq_a.model_idx()),
2785            router: &packs[i].router,
2786        };
2787        let geom = crate::gpu_wgpu::Dsv4LayerGeom {
2788            attn: crate::gpu_wgpu::Dsv4AttnGeom {
2789                dim,
2790                nh: cfg.n_heads,
2791                hd,
2792                rd: cfg.rope_head_dim,
2793                q_lora: cfg.q_lora_rank,
2794                o_lora: cfg.o_lora_rank,
2795                o_groups: cfg.o_groups,
2796                eps: cfg.norm_eps,
2797                scale: (hd as f32).powf(-0.5),
2798            },
2799            moe: crate::gpu_wgpu::Dsv4MoeGeom {
2800                hidden: dim,
2801                inter: cfg.moe_inter,
2802                top_k: cfg.top_k,
2803                route_scale: cfg.route_scale,
2804                swiglu_limit: cfg.swiglu_limit,
2805                gu_q2: l.experts.first().is_some_and(|e| {
2806                    e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
2807                }),
2808            },
2809            hc: cfg.hc_mult,
2810            hc_eps: cfg.hc_eps,
2811            sinkhorn_iters: cfg.hc_sinkhorn_iters,
2812        };
2813        freqs.push(if l.compressor.is_some() {
2814            g.inv_freq_compress.as_slice()
2815        } else {
2816            g.inv_freq_window.as_slice()
2817        });
2818        items.push((w, geom, prep));
2819    }
2820
2821    let mut out = vec![0.0f32; dim * batch.max(1)];
2822    if batch > 1 {
2823        // A batch keeps its own state per token. When a host tail follows,
2824        // all of those states ride home beside the folds in the same fence.
2825        // One forced row per token: same layers, the hash rows re-derived
2826        // from each token's own id.
2827        let mut forced_pt: Vec<Vec<Option<Vec<usize>>>> = Vec::with_capacity(batch);
2828        for t in 0..batch {
2829            let id = batch_ids.get(t).copied().unwrap_or(token_id);
2830            let mut row = Vec::with_capacity(run.len());
2831            for (i, &li) in run.iter().enumerate() {
2832                row.push(layers[li].tid2eid.as_ref().and_then(|tbl| {
2833                    let v: Vec<usize> = hash_route(tbl, cfg.vocab, cfg.top_k, id)
2834                        .into_iter()
2835                        .map(|gi| packs[i].to_slot[gi])
2836                        .collect();
2837                    if v.iter().any(|&x| x == usize::MAX) { None } else { Some(v) }
2838                }));
2839                if layers[li].tid2eid.is_some() && row[i].is_none() {
2840                    return false;
2841                }
2842            }
2843            forced_pt.push(row);
2844        }
2845        if !crate::gpu_wgpu::dsv4_chain_batch(
2846            &model, &items, st.kv_id, first, &freqs, st.pos, batch,
2847            Some(&forced_pt), &mut out, state_out,
2848        ) {
2849            return false;
2850        }
2851        // The caller wants the LAST token's fold: it is the one whose logits
2852        // continue the sequence.
2853        *folded = out[(batch - 1) * dim..batch * dim].to_vec();
2854    } else {
2855        if !crate::gpu_wgpu::dsv4_layer_chain(
2856            &model, &items, st.kv_id, first, &freqs, st.pos, &mut out, state_out,
2857        ) {
2858            return false;
2859        }
2860        *folded = out;
2861    }
2862    // The device advanced these; the host keeps only the arithmetic. A batch
2863    // advanced them once per token, in order, so the host replays the same
2864    // rule that many times rather than inventing a closed form for it.
2865    for (i, &li) in run.iter().enumerate() {
2866      for t in 0..batch.max(1) {
2867        let pos = st.pos + t;
2868        st.dev_filled[li] = (st.dev_filled[li] + 1).min(cfg.window);
2869        if let Some((_, cg, ..)) = items[i].2.ix.as_ref() {
2870            if (pos + 1) % cg.ratio == 0 {
2871                st.dev_n_ix[li] += 1;
2872            }
2873        }
2874        if let Some((_, cg)) = items[i].2.comp.as_ref() {
2875            if (pos + 1) % cg.ratio == 0 {
2876                st.dev_n_comp[li] += 1;
2877            }
2878        }
2879      }
2880    }
2881    st.dev_owned = true;
2882    true
2883}
2884
2885/// `CMF_DSV4_HC_DEV=0` puts the hyper-connections back on the host.
2886#[cfg(feature = "gpu")]
2887fn hc_on_device() -> bool {
2888    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2889    *ON.get_or_init(|| {
2890        // OPT-IN. On the release checkpoint this path reads 3.234 against
2891        // the CPU's 3.282 — divergent — and the speed is unchanged, so there
2892        // is no trade to weigh: it must not be the default until it is
2893        // exact. The toy's near-agreement (129.787 vs 129.792) hid a real
2894        // fault the release exposes.
2895        std::env::var("CMF_DSV4_HC_DEV").is_ok_and(|v| v != "0")
2896            && crate::gpu::backend_available()
2897    })
2898}
2899
2900/// The two-frame path with the hyper-connections on the card.
2901///
2902/// The host still prepares each layer's attention inputs — the compressor,
2903/// the indexer and the window, which are exact there — but it no longer
2904/// folds, Sinkhorns or norms, and it no longer carries the MoE half's input
2905/// between the halves: the attention frame leaves it on the device and the
2906/// MoE frame reads it from there. One readback a layer instead of two, and
2907/// 19 ms of host arithmetic a token gone.
2908#[cfg(feature = "gpu")]
2909#[allow(clippy::too_many_arguments)]
2910fn dsv4_two_frame_loop(
2911    state: &mut [f32],
2912    layers: &[Dsv4Layer],
2913    g: &Dsv4Globals,
2914    cfg: &Dsv4Cfg,
2915    st: &mut Dsv4State,
2916    token_id: u32,
2917    inv_freq: &[f32],
2918    pool: Option<&crate::pool::Pool>,
2919    scratch: &mut HcScratch,
2920) -> bool {
2921    let dim = cfg.dim;
2922    let freqs_of = |l: &Dsv4Layer| -> &[f32] {
2923        let f = if l.compressor.is_some() {
2924            &g.inv_freq_compress
2925        } else {
2926            &g.inv_freq_window
2927        };
2928        if f.is_empty() { inv_freq } else { f.as_slice() }
2929    };
2930    // Layer zero's fold has no frame before it, exactly as in the layer path.
2931    let (mut folded, post0, comb0) = hc_fold_norm(
2932        state,
2933        &layers[0].hc_attn_fn,
2934        &layers[0].hc_attn_scale,
2935        &layers[0].hc_attn_base,
2936        &layers[0].attn_norm,
2937        cfg,
2938        pool,
2939    );
2940    if !crate::gpu_wgpu::dsv4_state_write(state)
2941        || !crate::gpu_wgpu::dsv4_hc_write(&post0, &comb0)
2942    {
2943        return false;
2944    }
2945    // PRE-FLIGHT, before the first byte of state moves: a mid-loop refusal
2946    // would hand the token back to the ordinary loop AFTER these caches
2947    // advanced, and the second advance is not a slow answer but a wrong one.
2948    // The same discipline the layer loop states in the same words.
2949    let mut on_dev = vec![false; layers.len()];
2950    for (li, l) in layers.iter().enumerate() {
2951        let Some(pk) = pack_for(l, cfg, li) else {
2952            return false;
2953        };
2954        let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
2955            return false;
2956        };
2957        let gu_q2 = l
2958            .experts
2959            .first()
2960            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
2961        let attn_ok = [
2962            l.wq_a.model_idx(),
2963            l.wq_b.model_idx(),
2964            l.wo_a.model_idx(),
2965            l.wo_b.model_idx(),
2966        ]
2967        .into_iter()
2968        .flatten()
2969        .all(|i| crate::gpu_wgpu::dsv4_weight_ready(&model, i));
2970        on_dev[li] = attn_ok
2971            && pk.globals.len() == cfg.n_routed_experts
2972            && crate::gpu_wgpu::dsv4_experts_ready(
2973                &model,
2974                &pk.tensors,
2975                cfg.moe_inter,
2976                dim,
2977                gu_q2,
2978                l.experts.first().is_some_and(|e| {
2979                    e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
2980                }),
2981            );
2982    }
2983    if !on_dev.iter().any(|&x| x) {
2984        return false;
2985    }
2986    let mut sink = vec![0.0f32; dim];
2987    for (li, l) in layers.iter().enumerate() {
2988        // A layer the card cannot hold runs on the host WHOLE, with the
2989        // state fetched and put back around it — the mixed ownership the
2990        // layer loop already proved out.
2991        if !on_dev[li] {
2992            if !crate::gpu_wgpu::dsv4_state_read(state) {
2993                return false;
2994            }
2995            let freqs = freqs_of(l);
2996            hc_block(
2997                state,
2998                &l.hc_attn_fn,
2999                &l.hc_attn_scale,
3000                &l.hc_attn_base,
3001                &l.attn_norm,
3002                cfg,
3003                scratch,
3004                pool,
3005                |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
3006            );
3007            hc_block(
3008                state,
3009                &l.hc_ffn_fn,
3010                &l.hc_ffn_scale,
3011                &l.hc_ffn_base,
3012                &l.ffn_norm,
3013                cfg,
3014                scratch,
3015                pool,
3016                |f, o| moe_step(f, l, cfg, token_id, li, pool, o),
3017            );
3018            let nref = layers.get(li + 1).unwrap_or(l);
3019            let (f, p2, c2) = hc_fold_norm(
3020                state,
3021                &nref.hc_attn_fn,
3022                &nref.hc_attn_scale,
3023                &nref.hc_attn_base,
3024                &nref.attn_norm,
3025                cfg,
3026                pool,
3027            );
3028            folded = f;
3029            if !crate::gpu_wgpu::dsv4_hc_write(&p2, &c2)
3030                || !crate::gpu_wgpu::dsv4_state_write(state)
3031            {
3032                return false;
3033            }
3034            continue;
3035        }
3036        // The host's half: the caches and the attended list, untouched.
3037        let mut prep = AttnPrep::default();
3038        attention_step(
3039            &folded, l, cfg, st, li, freqs_of(l), pool, Some(&mut prep), &mut sink,
3040        );
3041        let hd = cfg.head_dim;
3042        let n_comp = st.compressed[li].len() / hd;
3043        let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
3044        if !crate::gpu_wgpu::dsv4_cache_write(st.kv_id, li, 0, &st.window[li], cap)
3045            || (n_comp > 0
3046                && !crate::gpu_wgpu::dsv4_cache_write(
3047                    st.kv_id, li, cfg.window * hd, &st.compressed[li], cap,
3048                ))
3049        {
3050            return false;
3051        }
3052        let idx32: Vec<u32> = prep
3053            .idxs
3054            .iter()
3055            .map(|&p| {
3056                if p < prep.win_len {
3057                    p as u32
3058                } else {
3059                    (cfg.window + (p - prep.win_len)) as u32
3060                }
3061            })
3062            .collect();
3063        let nxt = layers.get(li + 1);
3064        let a_tail = crate::gpu_wgpu::Dsv4HcTail {
3065            fn_: &l.hc_ffn_fn,
3066            scale: &l.hc_ffn_scale,
3067            base: &l.hc_ffn_base,
3068            norm: &l.ffn_norm,
3069            hc: cfg.hc_mult,
3070            sinkhorn_iters: cfg.hc_sinkhorn_iters,
3071            hc_eps: cfg.hc_eps,
3072            eps: cfg.norm_eps,
3073        };
3074        let scale = (cfg.head_dim as f32).powf(-0.5);
3075        if !attn_frame(
3076            l, cfg, st, li, &folded, &prep.qr, &prep.idxs, freqs_of(l), st.pos,
3077            prep.win_len, scale, Some(&a_tail), &mut [],
3078        ) {
3079            return false;
3080        }
3081        let m_tail = nxt.map(|n| crate::gpu_wgpu::Dsv4HcTail {
3082            fn_: &n.hc_attn_fn,
3083            scale: &n.hc_attn_scale,
3084            base: &n.hc_attn_base,
3085            norm: &n.attn_norm,
3086            hc: cfg.hc_mult,
3087            sinkhorn_iters: cfg.hc_sinkhorn_iters,
3088            hc_eps: cfg.hc_eps,
3089            eps: cfg.norm_eps,
3090        });
3091        let mut next = vec![0.0f32; dim];
3092        let pair = m_tail
3093            .as_ref()
3094            .zip(nxt)
3095            .map(|(t, n)| (t, n.attn_norm.as_slice()));
3096        let forced = l
3097            .tid2eid
3098            .as_ref()
3099            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
3100        if moe_frame(&[], l, cfg, li, &[], forced.as_deref(), pool, Some(&a_tail), pair, &mut next)
3101            .is_none()
3102        {
3103            return false;
3104        }
3105        folded = next;
3106    }
3107    let _ = scratch;
3108    crate::gpu_wgpu::dsv4_state_read(state)
3109}
3110
3111/// The host half of one hyper-connection block: mixes, Sinkhorn, fold, norm.
3112/// The device does this for every layer but the first, whose state it has not
3113/// seen yet.
3114#[cfg(feature = "gpu")]
3115#[allow(clippy::too_many_arguments)]
3116fn hc_fold_norm(
3117    state: &[f32],
3118    hc_fn: &[f32],
3119    hc_scale: &[f32; 3],
3120    hc_base: &[f32],
3121    norm_w: &[f32],
3122    cfg: &Dsv4Cfg,
3123    pool: Option<&crate::pool::Pool>,
3124) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
3125    let (hc, dim) = (cfg.hc_mult, cfg.dim);
3126    let mix_hc = (2 + hc) * hc;
3127    let mut mixes = vec![0.0f32; mix_hc];
3128    hc_mixes(state, hc_fn, mix_hc, cfg.norm_eps, pool, &mut mixes);
3129    let mut pre = vec![0.0f32; hc];
3130    let mut post = vec![0.0f32; hc];
3131    let mut comb = vec![0.0f32; hc * hc];
3132    hc_split_sinkhorn(
3133        &mixes,
3134        hc_scale,
3135        hc_base,
3136        hc,
3137        cfg.hc_sinkhorn_iters,
3138        cfg.hc_eps,
3139        &mut pre,
3140        &mut post,
3141        &mut comb,
3142    );
3143    let mut folded = vec![0.0f32; dim];
3144    hc_fold(state, &pre, hc, dim, &mut folded);
3145    rms_weighted(&mut folded, norm_w, cfg.norm_eps);
3146    // post and comb travel with the fold: the frame's opening expand needs
3147    // exactly those, and they are not recoverable from the state alone.
3148    (folded, post, comb)
3149}
3150
3151/// `CMF_DSV4_GPU_LAYER=1`: one submission per layer instead of two, with the
3152/// hyper-connection glue and the router on the device.
3153///
3154/// CORRECT — perplexity 5.211 against the CPU's 5.211 on the release, 128.576
3155/// against 128.576 on the toy — and SLOWER on this hardware: 6.0 tok/s where
3156/// the two-frame path gets 9.3. The reason is not the frame, it is the
3157/// all-or-nothing granularity underneath it. A layer whose experts miss VRAM
3158/// runs entirely on the host, attention included (6.5 ms a call against 0.9),
3159/// and with 100 GB of experts against a 98 GB card a fifth of the layers
3160/// miss. The two-frame path only loses the MoE half of those layers.
3161///
3162/// So the barrier it saves is real and the fallback it forces costs more. The
3163/// fix is the granularity: pack the experts that FIT, route over all of them
3164/// anyway, and run the few cold picks of a token on the host — per EXPERT,
3165/// not per layer. Then no layer ever leaves the device and this frame wins by
3166/// the 15 ms a token it was built to save.
3167#[cfg(feature = "gpu")]
3168fn gpu_layer_enabled() -> bool {
3169    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3170    *ON.get_or_init(|| {
3171        std::env::var("CMF_DSV4_GPU_LAYER").map(|v| v != "0").unwrap_or(true)
3172            && crate::gpu::backend_available()
3173    })
3174}
3175
3176/// The packed expert set of one layer: which globals made it in, and their
3177/// directory indices in packing order with the shared expert last. Built once
3178/// — the mask does not change during a run — and keyed by layer.
3179#[cfg(feature = "gpu")]
3180struct Pack {
3181    /// The router as dense f32, expanded once. It is 4 MB a layer against a
3182    /// 112 GB model, it lives as long as the process — so the address-keyed
3183    /// device cache is sound for it, unlike anything built per call.
3184    router: Vec<f32>,
3185    /// global expert id -> packed slot, `usize::MAX` for the ones left out.
3186    to_slot: Vec<usize>,
3187    /// The same, as the u32 table the router reads.
3188    remap: Vec<u32>,
3189    /// packed order, globals only (shared is not in here).
3190    globals: Vec<usize>,
3191    tensors: Vec<(usize, usize, usize)>,
3192    /// The noaux_tc bias in PACKED order, kept here because it is the same
3193    /// every token and the pack lives as long as the process: a stable
3194    /// address means a stable device buffer, and a stable device buffer is
3195    /// what lets many layers share one submission. A bias uploaded through
3196    /// the per-call pool is written by every layer of a run BEFORE the run's
3197    /// single submit — queue writes do not interleave with passes — so every
3198    /// layer routed with the LAST layer's bias. On the release every scored
3199    /// layer carries one, which is the 50.280.
3200    bias: Option<Vec<f32>>,
3201}
3202
3203#[cfg(feature = "gpu")]
3204/// Candidate order for a budget-limited pack: hottest expert first, by the
3205/// measured tally `CMF_DSV4_PACK_FREQ` points at (`layer<TAB>expert<TAB>count`
3206/// lines). None when the variable is unset, the file is unreadable, or the
3207/// tally has nothing for this layer — the caller keeps id order then. Ties
3208/// and untallied experts follow in id order, so the choice is deterministic.
3209fn pack_freq_order(li: usize, n: usize) -> Option<Vec<usize>> {
3210    use std::collections::HashMap;
3211    use std::sync::OnceLock;
3212    static FREQ: OnceLock<Option<HashMap<(usize, usize), u64>>> = OnceLock::new();
3213    let map = FREQ
3214        .get_or_init(|| {
3215            let path = std::env::var("CMF_DSV4_PACK_FREQ").ok()?;
3216            let text = match std::fs::read_to_string(&path) {
3217                Ok(t) => t,
3218                Err(e) => {
3219                    eprintln!("CMF_DSV4_PACK_FREQ={path} не читается ({e}) — порядок по id");
3220                    return None;
3221                }
3222            };
3223            let mut m = HashMap::new();
3224            for line in text.lines() {
3225                let mut it = line.split('\t');
3226                if let (Some(l), Some(e), Some(c)) = (it.next(), it.next(), it.next()) {
3227                    if let (Ok(l), Ok(e), Ok(c)) =
3228                        (l.trim().parse(), e.trim().parse(), c.trim().parse::<u64>())
3229                    {
3230                        *m.entry((l, e)).or_insert(0) += c;
3231                    }
3232                }
3233            }
3234            Some(m)
3235        })
3236        .as_ref()?;
3237    if !(0..n).any(|e| map.contains_key(&(li, e))) {
3238        return None;
3239    }
3240    let mut idx: Vec<usize> = (0..n).collect();
3241    idx.sort_by_key(|&e| (std::cmp::Reverse(map.get(&(li, e)).copied().unwrap_or(0)), e));
3242    Some(idx)
3243}
3244
3245#[cfg(feature = "gpu")]
3246fn pack_for(l: &Dsv4Layer, cfg: &Dsv4Cfg, li: usize) -> Option<std::sync::Arc<Pack>> {
3247    use std::collections::HashMap;
3248    use std::sync::{Arc, Mutex, OnceLock};
3249    static CACHE: OnceLock<Mutex<HashMap<(u64, usize, usize), Option<Arc<Pack>>>>> =
3250        OnceLock::new();
3251    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
3252    // Keyed by the layer's IDENTITY, not its ordinal. The draft's three
3253    // stages are layers too and they number 0, 1, 2 — under an ordinal key
3254    // they would be handed the trunk's first three packs: another layer's
3255    // router, another layer's tensor indices, another layer's bias. The gate
3256    // tensor is what actually distinguishes them.
3257    let model_uid = l
3258        .experts
3259        .first()
3260        .and_then(|e| e.w1.model_arc())
3261        .map_or(0, |m| m.uid());
3262    // Dense f32 routers need not have a directory handle, so the gate index
3263    // alone can be `None` for every layer. Pair the ordinal with the first
3264    // expert's mapped identity; model UID keeps long-lived multi-model
3265    // servers separate, while the expert index distinguishes trunk and MTP
3266    // layers that reuse ordinal 0/1/2.
3267    let first_expert = l
3268        .experts
3269        .first()
3270        .and_then(|e| e.w1.model_idx())
3271        .unwrap_or(usize::MAX);
3272    let key = (model_uid, li, first_expert);
3273    if let Some(v) = cache.lock().unwrap().get(&key) {
3274        return v.clone();
3275    }
3276    // `CMF_DSV4_PACK_MAX_LI=N` — do not pack layers above N at all. A layer
3277    // with no pack stays wholly host-owned, which is what both the batched
3278    // prefill and a speculative verify need of the tail: a device-owned
3279    // partial layer can join neither the batch (incomplete pack) nor the
3280    // causal host tail (its caches live on the card). This also carves the
3281    // VRAM the tail would have taken for the draft's own pack.
3282    if let Ok(v) = std::env::var("CMF_DSV4_PACK_MAX_LI") {
3283        if v.parse::<usize>().is_ok_and(|max| li > max) {
3284            cache.lock().unwrap().insert(key, None);
3285            return None;
3286        }
3287    }
3288    let build = || -> Option<Arc<Pack>> {
3289        let mut to_slot = vec![usize::MAX; cfg.n_routed_experts];
3290        let mut globals = Vec::new();
3291        let mut tensors = Vec::new();
3292        let idx3 = |e: &Dsv4Expert| -> Option<(usize, usize, usize)> {
3293            Some((e.w1.model_idx()?, e.w3.model_idx()?, e.w2.model_idx()?))
3294        };
3295        // How many experts the card still has room for, minus one for the
3296        // shared expert, which always rides. Everything past that stays on the
3297        // host and is reached through the remap — the router still ranges over
3298        // all of them, so this costs speed and not a single bit of quality.
3299        let gu_q2 = l
3300            .experts
3301            .first()
3302            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
3303        // Pack what fits and leave the rest to the host. The router still
3304        // ranges over every expert; a missing winner is returned as a cold
3305        // pick and completed on the CPU. This is deliberately budget-driven,
3306        // not layer-driven: the same model scales from a small card (more
3307        // partial/host layers) to a large one (all experts resident) without
3308        // a checkpoint-specific cutoff.
3309        // `CMF_DSV4_PACK_MAX=N` caps the packing directly, so a toy can
3310        // reproduce the subset path without needing a card that runs out.
3311        if let Some(n) = std::env::var("CMF_DSV4_PACK_MAX")
3312            .ok()
3313            .and_then(|v| v.parse::<usize>().ok())
3314        {
3315            let mut to_slot = vec![usize::MAX; cfg.n_routed_experts];
3316            let mut globals = Vec::new();
3317            let mut tensors = Vec::new();
3318            for (gi, e) in l.experts.iter().enumerate().take(n) {
3319                to_slot[gi] = globals.len();
3320                globals.push(gi);
3321                tensors.push(idx3(e)?);
3322            }
3323            tensors.push(idx3(&l.shared)?);
3324            let (rows, cols) = (l.gate.rows(), l.gate.cols());
3325            let mut router = vec![0.0f32; rows * cols];
3326            for r in 0..rows {
3327                l.gate.row_f32(r, &mut router[r * cols..(r + 1) * cols]);
3328            }
3329            let remap: Vec<u32> = to_slot
3330                .iter()
3331                .map(|&sl| if sl == usize::MAX { u32::MAX } else { sl as u32 })
3332                .collect();
3333            return Some(Arc::new(Pack {
3334                bias: l.gate_bias.as_deref().map(|b| {
3335                    globals.iter().map(|&g| b[g]).collect()
3336                }),
3337                router,
3338                to_slot,
3339                remap,
3340                globals,
3341                tensors,
3342            }));
3343        }
3344        let dn_q2_fit = l
3345            .experts
3346            .first()
3347            .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
3348        let room = crate::gpu_wgpu::dsv4_experts_fit(cfg.moe_inter, cfg.dim, gu_q2, dn_q2_fit)
3349            .saturating_sub(1);
3350        // When the budget packs a SUBSET, which subset matters: a partial
3351        // layer completes its cold picks from the host, so every resident
3352        // expert that the routing actually reaches is host work saved.
3353        // `CMF_DSV4_PACK_FREQ` names a measured tally
3354        // (`CMF_DSV4_TRUNK_PICK_DUMP` wrote it) and reorders the candidates
3355        // hottest-first; layers absent from the tally keep id order. The
3356        // router still ranges over every expert either way — residency
3357        // choice changes speed, never a bit of the answer.
3358        let order = pack_freq_order(li, l.experts.len())
3359            .unwrap_or_else(|| (0..l.experts.len()).collect());
3360        for gi in order {
3361            let e = &l.experts[gi];
3362            if l.mask.as_deref().is_some_and(|m| !m.get(gi).copied().unwrap_or(true)) {
3363                continue;
3364            }
3365            if globals.len() >= room {
3366                break;
3367            }
3368            to_slot[gi] = globals.len();
3369            globals.push(gi);
3370            match idx3(e) {
3371                Some(t) => tensors.push(t),
3372                None => {
3373                    if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
3374                        eprintln!("слой {li}: эксперт {gi} без индексов в каталоге");
3375                    }
3376                    return None;
3377                }
3378            }
3379        }
3380        if globals.is_empty() {
3381            // Two very different causes, and blaming the mask for the other
3382            // one sent a reader looking for a mask that was never set: an
3383            // actual empty mask, or a VRAM budget with no room left for even
3384            // one expert (`room` is 0, which is what a nearly-full card does
3385            // to the last layers).
3386            if room == 0 {
3387                static SAID_ZERO: std::sync::atomic::AtomicBool =
3388                    std::sync::atomic::AtomicBool::new(false);
3389                if !SAID_ZERO.swap(true, std::sync::atomic::Ordering::Relaxed) {
3390                    tracing::warn!(
3391                        "начиная со слоя {li}, в бюджете VRAM не осталось места даже под одного \
3392                         эксперта — остальные веса остаются mmap-backed и читаются по требованию"
3393                    );
3394                }
3395            } else {
3396                tracing::warn!("слой {li}: маска не оставила ни одного эксперта");
3397            }
3398            return None;
3399        }
3400        tensors.push(idx3(&l.shared)?); // shared rides last, as the kernels expect
3401        let (rows, cols) = (l.gate.rows(), l.gate.cols());
3402        let mut router = vec![0.0f32; rows * cols];
3403        for r in 0..rows {
3404            l.gate.row_f32(r, &mut router[r * cols..(r + 1) * cols]);
3405        }
3406        let remap: Vec<u32> = to_slot
3407            .iter()
3408            .map(|&sl| if sl == usize::MAX { u32::MAX } else { sl as u32 })
3409            .collect();
3410        Some(Arc::new(Pack {
3411            bias: l.gate_bias.as_deref().map(|b| {
3412                globals.iter().map(|&g| b[g]).collect()
3413            }),
3414            router,
3415            to_slot,
3416            remap,
3417            globals,
3418            tensors,
3419        }))
3420    };
3421    let v = build();
3422    cache.lock().unwrap().insert(key, v.clone());
3423    v
3424}
3425
3426/// The whole MoE block in one submission, experts resident (default on;
3427/// `CMF_DSV4_GPU_MOE2=0` restores the host path). Returns false having
3428/// changed nothing if it cannot — a missing pack, a refused budget — so the
3429/// caller's CPU path stays correct to run. The early divergence this frame
3430/// once carried (0.44 relative, perplexity 5.162 vs 5.211) was the partial
3431/// -capture and hidden-seed defects, fixed since: perplexity gold 4.578 is
3432/// bit-exact against the CPU on every budget from 64 to 96.5 GB.
3433#[cfg(feature = "gpu")]
3434fn moe_frame(
3435    hidden: &[f32],
3436    l: &Dsv4Layer,
3437    cfg: &Dsv4Cfg,
3438    li: usize,
3439    logits: &[f32],
3440    forced: Option<&[usize]>,
3441    pool: Option<&crate::pool::Pool>,
3442    // The state handover: expand always when the device owns the state,
3443    // fold only when there is a next layer.
3444    hc_cur: Option<&crate::gpu_wgpu::Dsv4HcTail>,
3445    hc_next: Option<(&crate::gpu_wgpu::Dsv4HcTail, &[f32])>,
3446    out: &mut [f32],
3447) -> Option<(Vec<f32>, usize)> {
3448    macro_rules! no {
3449        ($($t:tt)*) => {{
3450            if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
3451                eprintln!("кадр MoE отклонён: {}", format_args!($($t)*));
3452            }
3453            return None;
3454        }};
3455    }
3456    let Some(pk) = pack_for(l, cfg, li) else {
3457        no!("слой {li}: упаковка экспертов не построена");
3458    };
3459    // The router is a small f32 tensor and is usually NOT mapped; the handle
3460    // has to come from something that is.
3461    let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
3462        no!("слой {li}: эксперты не отображены из файла");
3463    };
3464    let subset = pk.globals.len() < cfg.n_routed_experts;
3465    // With a complete pack the forced row is translated to packed numbering.
3466    // With a subset it stays global: the router's remap either finds its slot
3467    // or returns the forced expert as a cold pick, exactly like a scored one.
3468    let fpack: Option<Vec<usize>> = match forced {
3469        Some(f) if subset => Some(f.to_vec()),
3470        Some(f) => {
3471            let v: Vec<usize> = f.iter().map(|&g| pk.to_slot[g]).collect();
3472            if v.iter().any(|&s| s == usize::MAX) {
3473                no!("слой {li}: хеш-слой называет эксперта вне упаковки");
3474            }
3475            Some(v)
3476        }
3477        None => None,
3478    };
3479    // Routing ranges over EVERY expert; the remap turns a winner into a slot
3480    // or marks it cold. Nothing is masked, so nothing is lost.
3481    // Empty logits are the device-scored case: the frame computes them from
3482    // pk.router, whose rows are already in global order, so there is nothing
3483    // to reorder — and indexing an empty slice is how this line greeted the
3484    // first engaged run.
3485    let lg: Vec<f32> = if logits.is_empty() || subset {
3486        logits.to_vec()
3487    } else {
3488        pk.globals.iter().map(|&g| logits[g]).collect()
3489    };
3490    let bias: Option<Vec<f32>> = l.gate_bias.as_deref().map(|b| {
3491        if subset {
3492            b.to_vec()
3493        } else {
3494            pk.globals.iter().map(|&g| b[g]).collect()
3495        }
3496    });
3497    let w = crate::gpu_wgpu::Dsv4MoeW {
3498        router: &pk.router,
3499        experts: &pk.tensors,
3500        logits: &lg,
3501        bias: bias.as_deref(),
3502        forced: fpack.as_deref(),
3503        remap: if subset { Some(&pk.remap) } else { None },
3504    };
3505    let g = crate::gpu_wgpu::Dsv4MoeGeom {
3506        hidden: cfg.dim,
3507        inter: cfg.moe_inter,
3508        top_k: cfg.top_k,
3509        route_scale: cfg.route_scale,
3510        swiglu_limit: cfg.swiglu_limit,
3511        gu_q2: l.experts.first().is_some_and(|e| {
3512            e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
3513        }),
3514    };
3515    let mut cold = Vec::new();
3516    let mut cold_x = Vec::new();
3517    if !crate::gpu_wgpu::dsv4_moe_frame(
3518        &model,
3519        &w,
3520        g,
3521        hidden,
3522        &mut cold,
3523        &mut cold_x,
3524        hc_cur,
3525        hc_next,
3526        out,
3527    ) {
3528        return None;
3529    }
3530    // The picks the card had no room for, finished here and added in. Their
3531    // weights already carry the top-k normalisation the device applied.
3532    if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
3533        let csum: f32 = cold.iter().map(|c| c.1).sum();
3534        eprintln!(
3535            "[холодные] слой {li}: вернулось {} из {} | сумма холодных {csum:.4} | \
3536             route_scale {:.4} | {:?}",
3537            cold.len(),
3538            cfg.top_k,
3539            cfg.route_scale,
3540            &cold[..cold.len().min(3)]
3541        );
3542    }
3543    let mut acc = vec![0.0f32; cfg.dim];
3544    let mut cold_sum = vec![0.0f32; cfg.dim];
3545    let cold_input = if hidden.is_empty() { cold_x.as_slice() } else { hidden };
3546    for &(gi, wt) in &cold {
3547        let Some(exp) = l.experts.get(gi) else { continue };
3548        // Cold means out-of-core by contract. The tensors remain mmap-backed:
3549        // missing pages are faulted from the CMF file and the OS may evict
3550        // them again under RAM pressure. Do not let the generic matvec probe
3551        // turn this into an unbounded second GPU cache behind the packer's
3552        // back.
3553        crate::gpu::cpu_scope(|| run_expert(cold_input, exp, cfg, wt, pool, &mut acc));
3554        for ((o, sum), a) in out.iter_mut().zip(&mut cold_sum).zip(&acc) {
3555            *o += a;
3556            *sum += a;
3557        }
3558    }
3559    Some((cold_sum, cold.len()))
3560}
3561
3562/// How much of each layer's compressed cache already sits on the card. ONE
3563/// map: a reader and a writer with a `static` each are two maps, and the
3564/// reader would never see a thing the writer put down.
3565/// The reallocation counter as of the last successful tail write. Any change
3566/// means some buffer was rebuilt and every tail count is stale.
3567#[cfg(feature = "gpu")]
3568fn last_grew(now: u64) -> u64 {
3569    use std::sync::atomic::{AtomicU64, Ordering};
3570    static SEEN: AtomicU64 = AtomicU64::new(0);
3571    let was = SEEN.load(Ordering::Relaxed);
3572    if was != now {
3573        SEEN.store(now, Ordering::Relaxed);
3574        compressed_map().lock().unwrap().clear();
3575        return u64::MAX; // force a full write this round
3576    }
3577    now
3578}
3579
3580#[cfg(feature = "gpu")]
3581fn compressed_map() -> &'static std::sync::Mutex<std::collections::HashMap<(u64, usize), usize>> {
3582    use std::collections::HashMap;
3583    use std::sync::{Mutex, OnceLock};
3584    static W: OnceLock<Mutex<HashMap<(u64, usize), usize>>> = OnceLock::new();
3585    W.get_or_init(|| Mutex::new(HashMap::new()))
3586}
3587
3588#[cfg(feature = "gpu")]
3589fn compressed_written(kv_id: u64, li: usize) -> usize {
3590    compressed_map()
3591        .lock()
3592        .unwrap()
3593        .get(&(kv_id, li))
3594        .copied()
3595        .unwrap_or(0)
3596}
3597
3598/// Reset to zero whenever a write fails or the buffer grows — a grown buffer
3599/// keeps none of its contents.
3600#[cfg(feature = "gpu")]
3601fn note_compressed(kv_id: u64, li: usize, n: usize) {
3602    compressed_map().lock().unwrap().insert((kv_id, li), n);
3603}
3604
3605#[cfg(feature = "gpu")]
3606fn gpu_moe2_enabled() -> bool {
3607    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3608    *ON.get_or_init(|| {
3609        std::env::var("CMF_DSV4_GPU_MOE2").map(|v| v != "0").unwrap_or(true)
3610            && crate::gpu::backend_available()
3611    })
3612}
3613
3614pub fn moe_step(
3615    hidden: &[f32],
3616    l: &Dsv4Layer,
3617    cfg: &Dsv4Cfg,
3618    token_id: u32,
3619    // Layer index — only used to bucket routing statistics.
3620    li: usize,
3621    pool: Option<&crate::pool::Pool>,
3622    out: &mut [f32],
3623) {
3624    let _t0 = prof::on().then(std::time::Instant::now);
3625    let _guard = scopeguard_moe(_t0, li);
3626    let mut logits = vec![0.0f32; cfg.n_routed_experts];
3627    l.gate.matvec(hidden, &mut logits, pool);
3628    let (mut idx, mut w) = (Vec::new(), Vec::new());
3629    route(
3630        &logits,
3631        l.gate_bias.as_deref(),
3632        cfg.top_k,
3633        cfg.route_scale,
3634        l.tid2eid
3635            .as_ref()
3636            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id))
3637            .as_deref(),
3638        l.mask.as_deref(),
3639        &mut idx,
3640        &mut w,
3641    );
3642    if route_stats_on() {
3643        record_route(li, 0, cfg.n_routed_experts, &idx);
3644    }
3645    // The whole block on the device, in one submission, or nothing. Routing
3646    // happens there too — the logits above are what it starts from, so the
3647    // CPU's own choice is discarded rather than second-guessed.
3648    #[cfg(feature = "gpu")]
3649    if gpu_moe2_enabled() && crate::gpu::enabled_here() {
3650        let forced = l
3651            .tid2eid
3652            .as_ref()
3653            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
3654        if moe_frame(hidden, l, cfg, li, &logits, forced.as_deref(), pool, None, None, out)
3655            .is_some()
3656        {
3657            // CMF_DSV4_MOE_CHECK=1 recomputes the same block on the CPU and
3658            // reports where they part. A wrong MoE does not fail — it answers
3659            // differently — and the toy agreed bit for bit while the release
3660            // did not, so the difference lives in something the toy has no
3661            // instance of. Only a per-layer number will say which.
3662            if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
3663                let mut want = vec![0.0f32; out.len()];
3664                let mut acc = vec![0.0f32; cfg.dim];
3665                for (e, &ei) in idx.iter().enumerate() {
3666                    let Some(exp) = l.experts.get(ei) else { continue };
3667                    run_expert(hidden, exp, cfg, w.get(e).copied().unwrap_or(0.0), pool, &mut acc);
3668                    for (o, a) in want.iter_mut().zip(&acc) {
3669                        *o += a;
3670                    }
3671                }
3672                run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
3673                for (o, a) in want.iter_mut().zip(&acc) {
3674                    *o += a;
3675                }
3676                let num: f32 = want.iter().zip(out.iter()).map(|(a, b)| (a - b) * (a - b)).sum();
3677                let den: f32 = want.iter().map(|a| a * a).sum::<f32>().max(1e-20);
3678                let rel = (num / den).sqrt();
3679                if rel > 1e-3 {
3680                    let packed = pack_for(l, cfg, li).map_or(0, |p| p.globals.len());
3681                    eprintln!(
3682                        "[кадр MoE] слой {li}: расхождение {rel:.3e} | выбрано {} | \
3683                         упаковано {packed} из {} | хеш={} | смещение={}",
3684                        idx.len(),
3685                        cfg.n_routed_experts,
3686                        l.tid2eid.is_some(),
3687                        l.gate_bias.is_some()
3688                    );
3689                }
3690            }
3691            return;
3692        }
3693    }
3694    // Cheap tally for the batching question: how many DISTINCT experts a
3695    // group of tokens reaches. If five tokens want thirty different experts,
3696    // a batched MoE reads thirty weights and amortises nothing — which is
3697    // the difference between a speculative verify that pays for itself and
3698    // one that does not. Disarmed it costs one thread-local read.
3699    PICK_TALLY.with(|t| {
3700        if let Some(v) = t.borrow_mut().as_mut() {
3701            v.push((li, idx.to_vec()));
3702        }
3703    });
3704    if dump_path().is_some() {
3705        PICKED.with(|p| {
3706            let mut p = p.borrow_mut();
3707            if p.len() <= li {
3708                p.resize(li + 1, Vec::new());
3709            }
3710            p[li] = idx.clone();
3711        });
3712    }
3713    // One submission for the whole block — the chosen experts plus the
3714    // shared one. Per-expert dispatches are what made MoE slow elsewhere,
3715    // and the device keeps the weights across tokens, so the cost is the
3716    // arithmetic rather than the traffic. A refusal (missing kernel, mixed
3717    // layouts, weights that do not fit the budget) falls to the CPU whole,
3718    // never half.
3719    // CORRECT but SLOWER, so off by default. Parity holds on real weights
3720    // (perplexity 6.808 → 6.839 at 64 tokens, 5.102 → 5.146 at 200), and an
3721    // honest alternating A/B says 1.0 tok/s against the CPU's 2.2. The first
3722    // measurement claimed the opposite — 0.7 → 2.0 — because the CPU arm ran
3723    // first and paged in 158 GB for the GPU arm to inherit.
3724    //
3725    // The cost is not arithmetic, it is round trips: this submits and reads
3726    // back once per layer, forty-three times a token, and a discrete card
3727    // charges milliseconds for each. Fixing it means one submission per
3728    // token — the whole-token graph — not a faster kernel.
3729    //
3730    // `CMF_DSV4_GPU_MOE=1` opts in.
3731    fn gpu_moe_on() -> bool {
3732        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3733        *ON.get_or_init(|| std::env::var("CMF_DSV4_GPU_MOE").is_ok_and(|v| v != "0"))
3734    }
3735    if gpu_moe_on() && crate::gpu::enabled_here() {
3736        let mut jobs = Vec::with_capacity(idx.len() + 1);
3737        let mut model_ref = None;
3738        let mut ok = true;
3739        for (e, &ei) in idx.iter().enumerate() {
3740            let Some(exp) = l.experts.get(ei) else { continue };
3741            ok &= crate::pipeline::moe_push_job_parts(
3742                &exp.w1,
3743                &exp.w3,
3744                &exp.w2,
3745                hidden,
3746                w.get(e).copied().unwrap_or(0.0),
3747                cfg.swiglu_limit,
3748                &mut jobs,
3749                &mut model_ref,
3750            )
3751            .is_some();
3752        }
3753        ok &= crate::pipeline::moe_push_job_parts(
3754            &l.shared.w1,
3755            &l.shared.w3,
3756            &l.shared.w2,
3757            hidden,
3758            1.0,
3759            cfg.swiglu_limit,
3760            &mut jobs,
3761            &mut model_ref,
3762        )
3763        .is_some();
3764        if ok {
3765            if let Some(m) = model_ref.as_ref() {
3766                if crate::gpu::moe_block(m, &jobs, out) {
3767                    // CMF_DSV4_GPU_CHECK=1 recomputes the same block on the
3768                    // CPU and reports the divergence. A GPU MoE that is wrong
3769                    // does not fail — it answers differently — so the only way
3770                    // to know is to ask both.
3771                    if std::env::var("CMF_DSV4_GPU_CHECK").is_ok() {
3772                        let mut want = vec![0.0f32; out.len()];
3773                        let mut acc = vec![0.0f32; cfg.dim];
3774                        for (e, &ei) in idx.iter().enumerate() {
3775                            let Some(exp) = l.experts.get(ei) else { continue };
3776                            run_expert(
3777                                hidden, exp, cfg,
3778                                w.get(e).copied().unwrap_or(0.0), pool, &mut acc,
3779                            );
3780                            for (o, a) in want.iter_mut().zip(&acc) {
3781                                *o += a;
3782                            }
3783                        }
3784                        run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
3785                        for (o, a) in want.iter_mut().zip(&acc) {
3786                            *o += a;
3787                        }
3788                        let num: f32 = want
3789                            .iter()
3790                            .zip(out.iter())
3791                            .map(|(a, b)| (a - b) * (a - b))
3792                            .sum();
3793                        let den: f32 = want.iter().map(|a| a * a).sum::<f32>().max(1e-20);
3794                        eprintln!(
3795                            "[dsv4-gpu] слой {li}: расхождение {:.3e} | |CPU|={:.5} |GPU|={:.5} | экспертов {}",
3796                            (num / den).sqrt(),
3797                            den.sqrt(),
3798                            out.iter().map(|x| x * x).sum::<f32>().sqrt(),
3799                            jobs.len()
3800                        );
3801                    }
3802                    return;
3803                }
3804            }
3805        }
3806    }
3807    out.fill(0.0);
3808    let mut acc = vec![0.0f32; cfg.dim];
3809    for (e, &ei) in idx.iter().enumerate() {
3810        let Some(exp) = l.experts.get(ei) else {
3811            continue;
3812        };
3813        run_expert(
3814            hidden,
3815            exp,
3816            cfg,
3817            w.get(e).copied().unwrap_or(0.0),
3818            pool,
3819            &mut acc,
3820        );
3821        for (o, a) in out.iter_mut().zip(&acc) {
3822            *o += a;
3823        }
3824    }
3825    // The shared expert always runs, at weight 1.
3826    run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
3827    for (o, a) in out.iter_mut().zip(&acc) {
3828        *o += a;
3829    }
3830}
3831
3832/// The routed and shared experts both come through here, so the clamp and
3833/// the weight folding have exactly one implementation — `expert_swiglu`.
3834fn run_expert(
3835    x: &[f32],
3836    e: &Dsv4Expert,
3837    cfg: &Dsv4Cfg,
3838    weight: f32,
3839    pool: Option<&crate::pool::Pool>,
3840    out: &mut [f32],
3841) {
3842    expert_swiglu(
3843        x,
3844        &|src, dst| e.w1.matvec(src, dst, pool),
3845        &|src, dst| e.w3.matvec(src, dst, pool),
3846        &|src, dst| e.w2.matvec(src, dst, pool),
3847        cfg.moe_inter,
3848        weight,
3849        cfg.swiglu_limit,
3850        out,
3851    );
3852}
3853
3854/// The same expert computation for several inputs, streaming each selected
3855/// weight once. Used by DSpark's trained five-position block: running five
3856/// ordinary `moe_step`s rereads the shared expert five times and every
3857/// coincident routed expert once per position.
3858fn moe_step_block(
3859    xs: &[f32],
3860    b: usize,
3861    l: &Dsv4Layer,
3862    cfg: &Dsv4Cfg,
3863    token_ids: &[u32],
3864    tally_layer: usize,
3865    pool: Option<&crate::pool::Pool>,
3866    out: &mut [f32],
3867) {
3868    let (dim, inter) = (cfg.dim, cfg.moe_inter);
3869    debug_assert_eq!(xs.len(), b * dim);
3870    debug_assert_eq!(out.len(), b * dim);
3871    out.fill(0.0);
3872
3873    let mut logits = vec![0.0f32; b * cfg.n_routed_experts];
3874    l.gate.matmat(xs, b, &mut logits, pool);
3875    let mut picks: Vec<Vec<usize>> = Vec::with_capacity(b);
3876    let mut weights: Vec<Vec<f32>> = Vec::with_capacity(b);
3877    for bi in 0..b {
3878        let mut idx = Vec::new();
3879        let mut wt = Vec::new();
3880        let forced = l.tid2eid.as_ref().map(|tbl| {
3881            hash_route(
3882                tbl,
3883                cfg.vocab,
3884                cfg.top_k,
3885                token_ids.get(bi).copied().unwrap_or(0),
3886            )
3887        });
3888        route(
3889            &logits[bi * cfg.n_routed_experts..(bi + 1) * cfg.n_routed_experts],
3890            l.gate_bias.as_deref(),
3891            cfg.top_k,
3892            cfg.route_scale,
3893            forced.as_deref(),
3894            l.mask.as_deref(),
3895            &mut idx,
3896            &mut wt,
3897        );
3898        PICK_TALLY.with(|t| {
3899            if let Some(v) = t.borrow_mut().as_mut() {
3900                v.push((tally_layer, idx.clone()));
3901            }
3902        });
3903        picks.push(idx);
3904        weights.push(wt);
3905    }
3906
3907    // Preserve the scalar path's accumulation order by keeping every routed
3908    // slot separate; grouping below changes only when a weight is read.
3909    let mut routed = vec![0.0f32; b * cfg.top_k * dim];
3910    for ei in 0..l.experts.len() {
3911        let mut jobs = Vec::new();
3912        for bi in 0..b {
3913            for (slot, &picked) in picks[bi].iter().enumerate() {
3914                if picked == ei {
3915                    jobs.push((bi, slot, weights[bi][slot]));
3916                }
3917            }
3918        }
3919        if jobs.is_empty() {
3920            continue;
3921        }
3922        let e = &l.experts[ei];
3923        let n = jobs.len();
3924        let mut xj = vec![0.0f32; n * dim];
3925        for (j, &(bi, _, _)) in jobs.iter().enumerate() {
3926            xj[j * dim..(j + 1) * dim]
3927                .copy_from_slice(&xs[bi * dim..(bi + 1) * dim]);
3928        }
3929        let mut gate = vec![0.0f32; n * inter];
3930        let mut up = vec![0.0f32; n * inter];
3931        e.w1.matmat(&xj, n, &mut gate, pool);
3932        e.w3.matmat(&xj, n, &mut up, pool);
3933        for (j, &(_, _, wt)) in jobs.iter().enumerate() {
3934            let (gj, uj) = (
3935                &mut gate[j * inter..(j + 1) * inter],
3936                &mut up[j * inter..(j + 1) * inter],
3937            );
3938            if cfg.swiglu_limit > 0.0 {
3939                for u in uj.iter_mut() {
3940                    *u = u.clamp(-cfg.swiglu_limit, cfg.swiglu_limit);
3941                }
3942                for g in gj.iter_mut() {
3943                    *g = g.min(cfg.swiglu_limit);
3944                }
3945            }
3946            for (g, &u) in gj.iter_mut().zip(uj.iter()) {
3947                *g = (*g / (1.0 + (-*g).exp())) * u * wt;
3948            }
3949        }
3950        let mut down = vec![0.0f32; n * dim];
3951        e.w2.matmat(&gate, n, &mut down, pool);
3952        for (j, &(bi, slot, _)) in jobs.iter().enumerate() {
3953            routed[(bi * cfg.top_k + slot) * dim..(bi * cfg.top_k + slot + 1) * dim]
3954                .copy_from_slice(&down[j * dim..(j + 1) * dim]);
3955        }
3956    }
3957
3958    // Shared expert: all positions always use it, so this is the highest
3959    // certainty weight-sharing win in the block.
3960    let mut sg = vec![0.0f32; b * inter];
3961    let mut su = vec![0.0f32; b * inter];
3962    l.shared.w1.matmat(xs, b, &mut sg, pool);
3963    l.shared.w3.matmat(xs, b, &mut su, pool);
3964    for bi in 0..b {
3965        let (gj, uj) = (
3966            &mut sg[bi * inter..(bi + 1) * inter],
3967            &mut su[bi * inter..(bi + 1) * inter],
3968        );
3969        if cfg.swiglu_limit > 0.0 {
3970            for u in uj.iter_mut() {
3971                *u = u.clamp(-cfg.swiglu_limit, cfg.swiglu_limit);
3972            }
3973            for g in gj.iter_mut() {
3974                *g = g.min(cfg.swiglu_limit);
3975            }
3976        }
3977        for (g, &u) in gj.iter_mut().zip(uj.iter()) {
3978            *g = (*g / (1.0 + (-*g).exp())) * u;
3979        }
3980    }
3981    let mut shared = vec![0.0f32; b * dim];
3982    l.shared.w2.matmat(&sg, b, &mut shared, pool);
3983
3984    for bi in 0..b {
3985        let dst = &mut out[bi * dim..(bi + 1) * dim];
3986        for slot in 0..picks[bi].len() {
3987            let src = &routed[(bi * cfg.top_k + slot) * dim
3988                ..(bi * cfg.top_k + slot + 1) * dim];
3989            for (o, &v) in dst.iter_mut().zip(src) {
3990                *o += v;
3991            }
3992        }
3993        for (o, &v) in dst.iter_mut().zip(&shared[bi * dim..(bi + 1) * dim]) {
3994            *o += v;
3995        }
3996    }
3997}
3998
3999/// Grouped output projection for a block. `wo_a` cannot use a plain matmat
4000/// because each group sees a different attention slice; reading a quantized
4001/// row once and applying it to every block position gives the same dot order
4002/// without rereading/dequantizing that row B times.
4003fn o_project_block(
4004    attn: &[f32],
4005    b: usize,
4006    wo_a: &crate::qtensor::QTensor,
4007    wo_b: &crate::qtensor::QTensor,
4008    groups: usize,
4009    lora: usize,
4010    pool: Option<&crate::pool::Pool>,
4011    out: &mut [f32],
4012) {
4013    let attn_len = attn.len() / b;
4014    let per_group = attn_len / groups;
4015    let rows = groups * lora;
4016    let mut mid = vec![0.0f32; b * rows];
4017    let mid_addr = crate::pool::SendMut::new(mid.as_mut_ptr());
4018    let run = |start: usize, end: usize| {
4019        let mut wr = vec![0.0f32; wo_a.cols()];
4020        for r in start..end {
4021            wo_a.row_f32(r, &mut wr);
4022            let group = r / lora;
4023            for bi in 0..b {
4024                let x = &attn[bi * attn_len + group * per_group
4025                    ..bi * attn_len + (group + 1) * per_group];
4026                let v = wr.iter().zip(x).map(|(w, x)| w * x).sum();
4027                unsafe { *mid_addr.at(bi * rows + r) = v };
4028            }
4029        }
4030    };
4031    match pool {
4032        Some(p) if rows >= 256 => p.run_rows(rows, &run),
4033        _ => run(0, rows),
4034    }
4035    wo_b.matmat(&mid, b, out, pool);
4036}
4037
4038/// `CMF_DSV4_TRACE=1` prints the hidden state's RMS after each half-block and
4039/// the logits' shape at the end. A 300B model that decodes nonsense gives no
4040/// other handle: this says whether the state grew, collapsed or went
4041/// non-finite, and at which layer — before anyone reaches for a debugger on a
4042/// hundred-gigabyte file.
4043fn no_compressed() -> bool {
4044    static OFF: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4045    *OFF.get_or_init(|| std::env::var("CMF_DSV4_NO_COMPRESSED").is_ok_and(|v| v != "0"))
4046}
4047
4048fn trace_on() -> bool {
4049    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4050    *ON.get_or_init(|| std::env::var("CMF_DSV4_TRACE").is_ok_and(|v| v != "0"))
4051}
4052
4053fn rms_of(v: &[f32]) -> f32 {
4054    if v.is_empty() {
4055        return 0.0;
4056    }
4057    (v.iter().map(|x| x * x).sum::<f32>() / v.len() as f32).sqrt()
4058}
4059
4060/// `CMF_DSV4_DUMP=<path>` appends one JSON line per token: the embedding, the
4061/// hyper-connection state after every layer, the folded-and-normed head input
4062/// and the logits. It exists to be diffed against the reference forward on
4063/// the same weights — the numerical parity this port has never had, which at
4064/// toy scale is a few thousand floats and entirely tractable.
4065thread_local! {
4066    /// The attention body's input and output per layer, interleaved.
4067    static BODY: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
4068    /// Experts chosen per layer for the token being decoded — the dump needs
4069    /// them, because two implementations that pick DIFFERENT experts diverge
4070    /// hugely for a reason that is not a bug in either.
4071    static PICKED: std::cell::RefCell<Vec<Vec<usize>>> =
4072        const { std::cell::RefCell::new(Vec::new()) };
4073    /// (layer, chosen experts) in call order, when armed.
4074    static PICK_TALLY: std::cell::RefCell<Option<Vec<(usize, Vec<usize>)>>> =
4075        const { std::cell::RefCell::new(None) };
4076}
4077
4078/// Start recording expert picks. Idempotent; the previous tally is dropped.
4079pub fn pick_tally_arm() {
4080    PICK_TALLY.with(|t| *t.borrow_mut() = Some(Vec::new()));
4081}
4082
4083/// Take what was recorded and stop recording.
4084pub fn pick_tally_take() -> Vec<(usize, Vec<usize>)> {
4085    PICK_TALLY.with(|t| t.borrow_mut().take().unwrap_or_default())
4086}
4087
4088/// How many distinct experts a set of per-token pick lists reaches, and how
4089/// many picks it makes. The ratio is what a batched MoE can hope to save.
4090pub fn tally_unique(picks: &[(usize, Vec<usize>)]) -> (usize, usize) {
4091    // Keyed by (layer, expert). Expert 17 of layer 3 and expert 17 of layer 4
4092    // are different weights, and counting them as one understated the traffic
4093    // a batch has to read — badly for the draft, whose three stages each have
4094    // their own 256.
4095    let mut seen = std::collections::HashSet::new();
4096    let mut total = 0;
4097    for (li, v) in picks {
4098        total += v.len();
4099        for &e in v {
4100            seen.insert((*li, e));
4101        }
4102    }
4103    (seen.len(), total)
4104}
4105
4106fn dump_path() -> Option<&'static str> {
4107    static P: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
4108    P.get_or_init(|| std::env::var("CMF_DSV4_DUMP").ok())
4109        .as_deref()
4110}
4111
4112fn dump_line(json: &str) {
4113    if let Some(p) = dump_path() {
4114        use std::io::Write as _;
4115        if let Ok(mut f) = std::fs::OpenOptions::new()
4116            .create(true)
4117            .append(true)
4118            .open(p)
4119        {
4120            let _ = writeln!(f, "{json}");
4121        }
4122    }
4123}
4124
4125fn vec_json(v: &[f32]) -> String {
4126    let mut s = String::with_capacity(v.len() * 9);
4127    s.push('[');
4128    for (i, x) in v.iter().enumerate() {
4129        if i > 0 {
4130            s.push(',');
4131        }
4132        s.push_str(&format!("{x:.6e}"));
4133    }
4134    s.push(']');
4135    s
4136}
4137
4138/// One token through the whole stack.
4139///
4140/// The hidden state is `hc_mult` copies of a `dim`-vector from the very
4141/// first line to the very last: the embedding is replicated, every layer
4142/// folds/expands around its two halves, and only `hc_head_fold` collapses
4143/// it before the output norm and the head. There is no point in this
4144/// function where an ordinary residual would fit.
4145#[allow(clippy::too_many_arguments)]
4146/// A chunk of prompt tokens. Stage one of the batched prefill (see
4147/// docs/DSV4_PREFILL.md): the walk itself, with the head skipped for every
4148/// token but the last.
4149///
4150/// Prefill costs `len × per-token` today, and on a 2500-token prompt that is
4151/// a minute and a half before the first word. The stages that follow batch
4152/// the weight reads — which is where the nine-fold gap to the bandwidth
4153/// floor lives — but this one is the scaffolding they hang on, and it
4154/// already stops computing 129 280 logits for tokens nobody asks about.
4155#[allow(clippy::too_many_arguments)]
4156/// `CMF_DSV4_BATCH=N` — how many prompt tokens go through the card in one
4157/// submission. 1 keeps the walk. The chunk still bounds it: a batch never
4158/// spans two chunks, so cancellation stays as responsive as it was.
4159fn batch_prefill() -> usize {
4160    static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
4161    *N.get_or_init(|| {
4162        std::env::var("CMF_DSV4_BATCH")
4163            .ok()
4164            .and_then(|v| v.parse::<usize>().ok())
4165            .filter(|&n| (1..=32).contains(&n))
4166            .unwrap_or(1)
4167    })
4168}
4169
4170/// The prompt as batches instead of a walk, when every layer will take one.
4171///
4172/// Refuses before touching any state, never half way: the caller's fallback
4173/// is the per-token walk, and a batch that advanced the caches and then gave
4174/// up would have them advanced twice. So everything that can decline is asked
4175/// first, and after the first dispatch the only outcomes are success and a
4176/// hard failure.
4177///
4178/// Hash layers are the one shape it cannot take: their expert list is forced
4179/// by the TOKEN's id and the layer description carries one list, not one per
4180/// token. The release has three of them (0, 1, 2); a file without them
4181/// batches the whole stack.
4182#[allow(clippy::too_many_arguments)]
4183fn forward_chunk_batched(
4184    g: &Dsv4Globals,
4185    layers: &[Dsv4Layer],
4186    cfg: &Dsv4Cfg,
4187    st: &mut Dsv4State,
4188    ids: &[u32],
4189    pos0: usize,
4190    inv_freq: &[f32],
4191    pool: Option<&crate::pool::Pool>,
4192    logits: &mut Vec<f32>,
4193    want_logits: bool,
4194) -> bool {
4195    #[cfg(not(feature = "gpu"))]
4196    {
4197        let _ = (
4198            g, layers, cfg, st, ids, pos0, inv_freq, pool, logits, want_logits,
4199        );
4200        false
4201    }
4202    #[cfg(feature = "gpu")]
4203    {
4204        let b = ids.len();
4205        // The batch encoder currently requires a complete expert pack. A
4206        // partial layer is still device-owned for decode, but becomes part of
4207        // the causal host tail here instead of being silently treated as a
4208        // full chain layer.
4209        let gpu_end = st
4210            .dev_set
4211            .iter()
4212            .enumerate()
4213            .position(|(li, &on)| {
4214                !on
4215                    || pack_for(&layers[li], cfg, li)
4216                        .is_none_or(|p| p.globals.len() < cfg.n_routed_experts)
4217            })
4218            .unwrap_or(st.dev_set.len());
4219        let why = if b < 2 {
4220            "токенов меньше двух"
4221        } else if !chain_enabled() {
4222            "цепочка выключена"
4223        } else if !st.dev_owned {
4224            "карта ещё не владеет состоянием"
4225        } else if st.dev_set.len() != layers.len() {
4226            "набор слоёв ещё не зафиксирован"
4227        } else if gpu_end == 0
4228            || st.dev_set[gpu_end.min(st.dev_set.len())..]
4229                .iter()
4230                .enumerate()
4231                .any(|(i, &on)| {
4232                    on && !st.partial_set.get(gpu_end + i).copied().unwrap_or(false)
4233                })
4234        {
4235            "слои на карте не образуют префикс"
4236        } else {
4237            ""
4238        };
4239        if !why.is_empty() {
4240            static SAID: std::sync::Once = std::sync::Once::new();
4241            SAID.call_once(|| tracing::warn!("dsv4: пакет отказал — {why}"));
4242            return false;
4243        }
4244        let (hc, dim) = (cfg.hc_mult, cfg.dim);
4245        let mut emb = vec![0.0f32; dim];
4246        for (t, &id) in ids.iter().enumerate() {
4247            let mut state = vec![0.0f32; hc * dim];
4248            g.embed.row_f32(id as usize, &mut emb);
4249            for j in 0..hc {
4250                state[j * dim..(j + 1) * dim].copy_from_slice(&emb);
4251            }
4252            let (folded, post0, comb0) = hc_fold_norm(
4253                &state,
4254                &layers[0].hc_attn_fn,
4255                &layers[0].hc_attn_scale,
4256                &layers[0].hc_attn_base,
4257                &layers[0].attn_norm,
4258                cfg,
4259                pool,
4260            );
4261            let mut qn0 = vec![0.0f32; layers[0].wq_a.rows()];
4262            layers[0].wq_a.matvec(&folded, &mut qn0, pool);
4263            rms_weighted(&mut qn0, &layers[0].q_norm, cfg.norm_eps);
4264            if !crate::gpu_wgpu::dsv4_state_write_t(&state, t)
4265                || !crate::gpu_wgpu::dsv4_hc_write_t(&post0, &comb0, t)
4266                || !crate::gpu_wgpu::dsv4_chain_seed_t(&folded, &qn0, t)
4267                || !crate::gpu_wgpu::dsv4_chain_seed_bt(t, b, &state, &post0, &comb0, &folded, &qn0)
4268            {
4269                return false;
4270            }
4271        }
4272        let run: Vec<usize> = (0..gpu_end).collect();
4273        let mut folded = Vec::new();
4274        let mut states = vec![0.0f32; b * hc * dim];
4275        st.pos = pos0;
4276        if !dsv4_chain_run(
4277            layers,
4278            &run,
4279            cfg,
4280            g,
4281            st,
4282            *ids.last().unwrap(),
4283            &mut folded,
4284            Some(&mut states),
4285            b,
4286            ids,
4287            true,
4288            pool,
4289        ) {
4290            return false;
4291        }
4292        // Finish the trailing host layers in causal token order. Their KV
4293        // caches are host-owned, while the device prefix advanced its own
4294        // caches inside the one submission above. On the release this loop
4295        // is exactly layer 42; keeping it general makes smaller VRAM budgets
4296        // correct as long as the resident layers remain one prefix.
4297        let mut scratch = HcScratch::new(cfg);
4298        for t in 0..b {
4299            st.pos = pos0 + t;
4300            let state = &mut states[t * hc * dim..(t + 1) * hc * dim];
4301            for (li, l) in layers.iter().enumerate().skip(gpu_end) {
4302                let freqs = if l.compressor.is_some() {
4303                    &g.inv_freq_compress
4304                } else {
4305                    &g.inv_freq_window
4306                };
4307                let freqs = if freqs.is_empty() {
4308                    inv_freq
4309                } else {
4310                    freqs.as_slice()
4311                };
4312                hc_block(
4313                    state,
4314                    &l.hc_attn_fn,
4315                    &l.hc_attn_scale,
4316                    &l.hc_attn_base,
4317                    &l.attn_norm,
4318                    cfg,
4319                    &mut scratch,
4320                    pool,
4321                    |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
4322                );
4323                hc_block(
4324                    state,
4325                    &l.hc_ffn_fn,
4326                    &l.hc_ffn_scale,
4327                    &l.hc_ffn_base,
4328                    &l.ffn_norm,
4329                    cfg,
4330                    &mut scratch,
4331                    pool,
4332                    |f, o| {
4333                        if host_cpu_moe() {
4334                            crate::gpu::cpu_scope(|| moe_step(f, l, cfg, ids[t], li, pool, o))
4335                        } else {
4336                            moe_step(f, l, cfg, ids[t], li, pool, o)
4337                        }
4338                    },
4339                );
4340                dspark_note(li, state, cfg);
4341            }
4342        }
4343        st.pos = pos0 + b;
4344        // Said once. A gate that compares a batched prompt against a walked
4345        // one proves nothing if the batch quietly declined — the numbers match
4346        // because the same code produced both. This line is what tells the
4347        // two apart.
4348        {
4349            static SAID: std::sync::Once = std::sync::Once::new();
4350            SAID.call_once(|| tracing::warn!("dsv4: префилл пакетами по {b}"));
4351        }
4352        // Only the last token's logits are read; the rest of the chunk exists
4353        // to fill the caches. The head consumes the hyper-connection state,
4354        // not the chain's intermediate fold — skipping this final learned
4355        // fold used to make a full-device batch fast and wrong.
4356        if want_logits {
4357            let last = &states[(b - 1) * hc * dim..b * hc * dim];
4358            let mut h = vec![0.0f32; dim];
4359            hc_head_fold(
4360                last,
4361                &g.hc_head_fn,
4362                g.hc_head_scale,
4363                &g.hc_head_base,
4364                cfg,
4365                pool,
4366                &mut h,
4367            );
4368            rms_weighted(&mut h, &g.norm, cfg.norm_eps);
4369            logits.resize(cfg.vocab, 0.0);
4370            g.head.matvec(&h, logits, pool);
4371        } else {
4372            logits.clear();
4373        }
4374        true
4375    }
4376}
4377
4378/// Everything a speculative verify must be able to put back.
4379///
4380/// Device caches roll back by restore-then-replay: the shadow puts the
4381/// window rings and compressor streams where they were BEFORE the pass, and
4382/// the replay re-appends the accepted tokens' state from the hidden inputs
4383/// the pass retained. Append-only regions roll back by count. Host-owned
4384/// tail layers roll back by clone-and-rewalk.
4385#[cfg(feature = "gpu")]
4386pub struct Dsv4SpecTxn {
4387    pos0: usize,
4388    batch: usize,
4389    gpu_end: usize,
4390    dev_filled: Vec<usize>,
4391    dev_n_comp: Vec<usize>,
4392    dev_n_ix: Vec<usize>,
4393    host: Vec<(usize, HostLayerSnap)>,
4394    /// Per host layer, per verified token: the layer's state right after
4395    /// that token's attention — what a rollback restores INSTEAD of
4396    /// re-walking the tail it already walked (the values are identical;
4397    /// only the side effects were ever needed).
4398    host_steps: Vec<(usize, Vec<HostLayerSnap>)>,
4399    /// Every token's hyper-connection state as it left the device prefix,
4400    /// BEFORE the host tail walked (and mutated) anything: the rewalk's
4401    /// input, and the head's.
4402    pub states: Vec<f32>,
4403    shadow: Option<crate::gpu_wgpu::Dsv4SpecShadow>,
4404}
4405
4406#[cfg(feature = "gpu")]
4407struct HostLayerSnap {
4408    window: Vec<f32>,
4409    compressed: Vec<f32>,
4410    index_kv: Vec<f32>,
4411    pending_kv: Vec<f32>,
4412    pending_score: Vec<f32>,
4413    prev_kv: Vec<f32>,
4414    prev_score: Vec<f32>,
4415    pending_ix_kv: Vec<f32>,
4416    pending_ix_score: Vec<f32>,
4417    prev_ix_kv: Vec<f32>,
4418    prev_ix_score: Vec<f32>,
4419}
4420
4421#[cfg(feature = "gpu")]
4422fn host_snap(st: &Dsv4State, li: usize) -> HostLayerSnap {
4423    HostLayerSnap {
4424        window: st.window[li].clone(),
4425        compressed: st.compressed[li].clone(),
4426        index_kv: st.index_kv[li].clone(),
4427        pending_kv: st.pending_kv[li].clone(),
4428        pending_score: st.pending_score[li].clone(),
4429        prev_kv: st.prev_kv[li].clone(),
4430        prev_score: st.prev_score[li].clone(),
4431        pending_ix_kv: st.pending_ix_kv[li].clone(),
4432        pending_ix_score: st.pending_ix_score[li].clone(),
4433        prev_ix_kv: st.prev_ix_kv[li].clone(),
4434        prev_ix_score: st.prev_ix_score[li].clone(),
4435    }
4436}
4437
4438#[cfg(feature = "gpu")]
4439fn host_restore(st: &mut Dsv4State, li: usize, s: &HostLayerSnap) {
4440    st.window[li] = s.window.clone();
4441    st.compressed[li] = s.compressed.clone();
4442    st.index_kv[li] = s.index_kv.clone();
4443    st.pending_kv[li] = s.pending_kv.clone();
4444    st.pending_score[li] = s.pending_score.clone();
4445    st.prev_kv[li] = s.prev_kv.clone();
4446    st.prev_score[li] = s.prev_score.clone();
4447    st.pending_ix_kv[li] = s.pending_ix_kv.clone();
4448    st.pending_ix_score[li] = s.pending_ix_score.clone();
4449    st.prev_ix_kv[li] = s.prev_ix_kv.clone();
4450    st.prev_ix_score[li] = s.prev_ix_score.clone();
4451}
4452
4453/// One host-tail walk of token `t`'s state through layers `gpu_end..`,
4454/// mutating `state` in place and the layers' host caches. Exactly the loop
4455/// the batch runs, factored so the verify can re-run it for accepted tokens.
4456#[cfg(feature = "gpu")]
4457#[allow(clippy::too_many_arguments)]
4458fn host_tail_walk(
4459    g: &Dsv4Globals,
4460    layers: &[Dsv4Layer],
4461    cfg: &Dsv4Cfg,
4462    st: &mut Dsv4State,
4463    gpu_end: usize,
4464    state: &mut [f32],
4465    token_id: u32,
4466    pos: usize,
4467    inv_freq: &[f32],
4468    scratch: &mut HcScratch,
4469    pool: Option<&crate::pool::Pool>,
4470) {
4471    st.pos = pos;
4472    for (li, l) in layers.iter().enumerate().skip(gpu_end) {
4473        let freqs = if l.compressor.is_some() {
4474            &g.inv_freq_compress
4475        } else {
4476            &g.inv_freq_window
4477        };
4478        let freqs = if freqs.is_empty() { inv_freq } else { freqs.as_slice() };
4479        hc_block(
4480            state,
4481            &l.hc_attn_fn,
4482            &l.hc_attn_scale,
4483            &l.hc_attn_base,
4484            &l.attn_norm,
4485            cfg,
4486            scratch,
4487            pool,
4488            |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
4489        );
4490        hc_block(
4491            state,
4492            &l.hc_ffn_fn,
4493            &l.hc_ffn_scale,
4494            &l.hc_ffn_base,
4495            &l.ffn_norm,
4496            cfg,
4497            scratch,
4498            pool,
4499            |f, o| {
4500                if host_cpu_moe() {
4501                    crate::gpu::cpu_scope(|| moe_step(f, l, cfg, token_id, li, pool, o))
4502                } else {
4503                    moe_step(f, l, cfg, token_id, li, pool, o)
4504                }
4505            },
4506        );
4507        dspark_note(li, state, cfg);
4508    }
4509}
4510
4511/// The host tail for a whole batch: attention stays causal per token (its
4512/// window mutates), the MoE half runs through the block-grouped path — the
4513/// same accumulation order as the position walk, which the block tests pin
4514/// bit for bit. This is the verify's tail; the single-token paths keep
4515/// `hc_block`.
4516#[cfg(feature = "gpu")]
4517#[allow(clippy::too_many_arguments)]
4518fn host_tail_walk_batch(
4519    g: &Dsv4Globals,
4520    layers: &[Dsv4Layer],
4521    cfg: &Dsv4Cfg,
4522    st: &mut Dsv4State,
4523    gpu_end: usize,
4524    states: &mut [f32],
4525    ids: &[u32],
4526    pos0: usize,
4527    b: usize,
4528    inv_freq: &[f32],
4529    scratch: &mut HcScratch,
4530    pool: Option<&crate::pool::Pool>,
4531    mut steps: Option<&mut Vec<(usize, Vec<HostLayerSnap>)>>,
4532) {
4533    let (hc, dim) = (cfg.hc_mult, cfg.dim);
4534    let mix_hc = (2 + hc) * hc;
4535    let mut folds = vec![0.0f32; b * dim];
4536    let mut mo = vec![0.0f32; b * dim];
4537    let mut posts = vec![0.0f32; b * hc];
4538    let mut combs = vec![0.0f32; b * hc * hc];
4539    let mut resid = vec![0.0f32; b * hc * dim];
4540    let spec_time = {
4541        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4542        *ON.get_or_init(|| std::env::var("CMF_DSV4_SPEC_TIME").is_ok_and(|v| v != "0"))
4543    };
4544    for (li, l) in layers.iter().enumerate().skip(gpu_end) {
4545        let t_attn = std::time::Instant::now();
4546        let freqs = if l.compressor.is_some() {
4547            &g.inv_freq_compress
4548        } else {
4549            &g.inv_freq_window
4550        };
4551        let freqs = if freqs.is_empty() { inv_freq } else { freqs.as_slice() };
4552        for t in 0..b {
4553            st.pos = pos0 + t;
4554            let state = &mut states[t * hc * dim..(t + 1) * hc * dim];
4555            hc_block(
4556                state,
4557                &l.hc_attn_fn,
4558                &l.hc_attn_scale,
4559                &l.hc_attn_base,
4560                &l.attn_norm,
4561                cfg,
4562                scratch,
4563                pool,
4564                |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
4565            );
4566            if let Some(steps) = steps.as_mut() {
4567                match steps.iter_mut().find(|(l, _)| *l == li) {
4568                    Some((_, v)) => v.push(host_snap(st, li)),
4569                    None => steps.push((li, vec![host_snap(st, li)])),
4570                }
4571            }
4572        }
4573        let t_glue = std::time::Instant::now();
4574        for t in 0..b {
4575            let state = &states[t * hc * dim..(t + 1) * hc * dim];
4576            hc_mixes(state, &l.hc_ffn_fn, mix_hc, cfg.norm_eps, pool, &mut scratch.mixes);
4577            hc_split_sinkhorn(
4578                &scratch.mixes,
4579                &l.hc_ffn_scale,
4580                &l.hc_ffn_base,
4581                hc,
4582                cfg.hc_sinkhorn_iters,
4583                cfg.hc_eps,
4584                &mut scratch.pre,
4585                &mut posts[t * hc..(t + 1) * hc],
4586                &mut combs[t * hc * hc..(t + 1) * hc * hc],
4587            );
4588            let fold = &mut folds[t * dim..(t + 1) * dim];
4589            hc_fold(state, &scratch.pre, hc, dim, fold);
4590            let ms = fold.iter().map(|v| v * v).sum::<f32>() / dim as f32;
4591            let inv = 1.0 / (ms + cfg.norm_eps).sqrt();
4592            for (v, w) in fold.iter_mut().zip(&l.ffn_norm) {
4593                *v = *v * inv * w;
4594            }
4595            resid[t * hc * dim..(t + 1) * hc * dim]
4596                .copy_from_slice(&states[t * hc * dim..(t + 1) * hc * dim]);
4597        }
4598        let t_moe = std::time::Instant::now();
4599        // A tail layer with a device expert pack (partial or full) runs its
4600        // hot winners on the card per token and completes the cold ones on
4601        // the host — the same exact split the partial walk uses. Default on
4602        // (measured: the tail fell 27.4 → 18.2 ms of the verify round);
4603        // `CMF_DSV4_TAIL_PACK=0` restores the batched host block.
4604        let tail_pack = {
4605            static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4606            *ON.get_or_init(|| {
4607                std::env::var("CMF_DSV4_TAIL_PACK").map(|v| v != "0").unwrap_or(true)
4608            })
4609        };
4610        let mut packed_done = false;
4611        if tail_pack && pack_for(l, cfg, li).is_some() {
4612            packed_done = true;
4613            for t in 0..b {
4614                let f = &folds[t * dim..(t + 1) * dim];
4615                let forced = l.tid2eid.as_ref().map(|tbl| {
4616                    hash_route(tbl, cfg.vocab, cfg.top_k, ids.get(t).copied().unwrap_or(0))
4617                });
4618                let o = &mut mo[t * dim..(t + 1) * dim];
4619                match moe_frame(f, l, cfg, li, &[], forced.as_deref(), pool, None, None, o) {
4620                    Some((cold_sum, n)) => {
4621                        if n > 0 {
4622                            for (od, cd) in o.iter_mut().zip(cold_sum.iter()) {
4623                                *od += cd;
4624                            }
4625                        }
4626                    }
4627                    None => {
4628                        packed_done = false;
4629                        break;
4630                    }
4631                }
4632            }
4633        }
4634        if !packed_done {
4635            if host_cpu_moe() {
4636                crate::gpu::cpu_scope(|| {
4637                    moe_step_block(&folds, b, l, cfg, ids, li, pool, &mut mo)
4638                });
4639            } else {
4640                moe_step_block(&folds, b, l, cfg, ids, li, pool, &mut mo);
4641            }
4642        }
4643        let t_exp = std::time::Instant::now();
4644        for t in 0..b {
4645            let state = &mut states[t * hc * dim..(t + 1) * hc * dim];
4646            hc_expand(
4647                &mo[t * dim..(t + 1) * dim],
4648                &resid[t * hc * dim..(t + 1) * hc * dim],
4649                &posts[t * hc..(t + 1) * hc],
4650                &combs[t * hc * hc..(t + 1) * hc * hc],
4651                hc,
4652                dim,
4653                state,
4654            );
4655            dspark_note(li, state, cfg);
4656        }
4657        if spec_time {
4658            eprintln!(
4659                "хвост слоя {li}: attn {:.1} мс, клей {:.1}, moe {:.1}, expand {:.1}",
4660                (t_glue - t_attn).as_secs_f64() * 1e3,
4661                (t_moe - t_glue).as_secs_f64() * 1e3,
4662                (t_exp - t_moe).as_secs_f64() * 1e3,
4663                t_exp.elapsed().as_secs_f64() * 1e3,
4664            );
4665        }
4666    }
4667}
4668
4669/// A speculative verify pass: run `ids` (the committed next token followed
4670/// by draft proposals) at positions `pos0..pos0+B` through the trunk in one
4671/// batched submission, WITHOUT giving up the ability to roll back, and
4672/// return every position's greedy answer. The caller decides the accepted
4673/// prefix and calls [`dsv4_spec_finish`], which either keeps everything
4674/// (`accepted == B`) or restores-and-replays to the accepted length.
4675///
4676/// `logits_out` takes B rows of vocab logits, `argmax_out` their argmaxes.
4677#[cfg(feature = "gpu")]
4678#[allow(clippy::too_many_arguments)]
4679pub fn dsv4_verify_chunk(
4680    g: &Dsv4Globals,
4681    layers: &[Dsv4Layer],
4682    cfg: &Dsv4Cfg,
4683    st: &mut Dsv4State,
4684    ids: &[u32],
4685    pos0: usize,
4686    inv_freq: &[f32],
4687    pool: Option<&crate::pool::Pool>,
4688    cap_targets: &[usize],
4689    argmax_out: &mut Vec<u32>,
4690    logits_out: &mut Vec<f32>,
4691    walked_out: &mut Vec<f32>,
4692) -> Option<Dsv4SpecTxn> {
4693    let b = ids.len();
4694    let gpu_end = st
4695        .dev_set
4696        .iter()
4697        .enumerate()
4698        .position(|(li, &on)| {
4699            !on || pack_for(&layers[li], cfg, li)
4700                .is_none_or(|p| p.globals.len() < cfg.n_routed_experts)
4701        })
4702        .unwrap_or(st.dev_set.len());
4703    // A PARTIAL layer past the prefix is fine: it walks in the host tail
4704    // like any host layer. Only a FULL device layer out there means the
4705    // prefix assumption is really broken.
4706    let full_beyond = st.dev_set[gpu_end.min(st.dev_set.len())..]
4707        .iter()
4708        .enumerate()
4709        .any(|(i, &on)| {
4710            on && !st.partial_set.get(gpu_end + i).copied().unwrap_or(false)
4711        });
4712    if b < 2
4713        || !chain_enabled()
4714        || !st.dev_owned
4715        || st.dev_set.len() != layers.len()
4716        || gpu_end == 0
4717        || full_beyond
4718    {
4719        return None;
4720    }
4721    let (hc, dim, hd) = (cfg.hc_mult, cfg.dim, cfg.head_dim);
4722    // ── the transaction ──
4723    let metas: Vec<(usize, usize, usize, usize)> = (0..gpu_end)
4724        .map(|li| (li, hd, cfg.window, st.dev_filled[li]))
4725        .collect();
4726    let shadow = crate::gpu_wgpu::dsv4_spec_shadow(st.kv_id, &metas, b)?;
4727    let mut txn = Dsv4SpecTxn {
4728        pos0,
4729        batch: b,
4730        gpu_end,
4731        dev_filled: st.dev_filled.clone(),
4732        dev_n_comp: st.dev_n_comp.clone(),
4733        dev_n_ix: st.dev_n_ix.clone(),
4734        host: (gpu_end..layers.len())
4735            .map(|li| (li, host_snap(st, li)))
4736            .collect(),
4737        states: Vec::new(),
4738        host_steps: Vec::new(),
4739        shadow: Some(shadow),
4740    };
4741    // The capture targets that live on the device: photograph their states.
4742    let dev_caps: Vec<usize> = cap_targets.iter().copied().filter(|&t| t < gpu_end).collect();
4743    crate::gpu_wgpu::dsv4_spec_retain_arm(gpu_end, &dev_caps);
4744
4745    // ── seed and run the batch (the prefill batch's own shape) ──
4746    let mut emb = vec![0.0f32; dim];
4747    for (t, &id) in ids.iter().enumerate() {
4748        let mut state = vec![0.0f32; hc * dim];
4749        g.embed.row_f32(id as usize, &mut emb);
4750        for j in 0..hc {
4751            state[j * dim..(j + 1) * dim].copy_from_slice(&emb);
4752        }
4753        let (folded, post0, comb0) = hc_fold_norm(
4754            &state,
4755            &layers[0].hc_attn_fn,
4756            &layers[0].hc_attn_scale,
4757            &layers[0].hc_attn_base,
4758            &layers[0].attn_norm,
4759            cfg,
4760            pool,
4761        );
4762        let mut qn0 = vec![0.0f32; layers[0].wq_a.rows()];
4763        layers[0].wq_a.matvec(&folded, &mut qn0, pool);
4764        rms_weighted(&mut qn0, &layers[0].q_norm, cfg.norm_eps);
4765        if !crate::gpu_wgpu::dsv4_state_write_t(&state, t)
4766            || !crate::gpu_wgpu::dsv4_hc_write_t(&post0, &comb0, t)
4767            || !crate::gpu_wgpu::dsv4_chain_seed_t(&folded, &qn0, t)
4768            || !crate::gpu_wgpu::dsv4_chain_seed_bt(t, b, &state, &post0, &comb0, &folded, &qn0)
4769        {
4770            crate::gpu_wgpu::dsv4_spec_retain_arm(0, &[]);
4771            return None;
4772        }
4773    }
4774    let spec_time = {
4775        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4776        *ON.get_or_init(|| std::env::var("CMF_DSV4_SPEC_TIME").is_ok_and(|v| v != "0"))
4777    };
4778    let t0 = std::time::Instant::now();
4779    let run: Vec<usize> = (0..gpu_end).collect();
4780    let mut folded = Vec::new();
4781    let mut states = vec![0.0f32; b * hc * dim];
4782    st.pos = pos0;
4783    let ok = dsv4_chain_run(
4784        layers,
4785        &run,
4786        cfg,
4787        g,
4788        st,
4789        *ids.last().unwrap(),
4790        &mut folded,
4791        Some(&mut states),
4792        b,
4793        ids,
4794        true,
4795        pool,
4796    );
4797    crate::gpu_wgpu::dsv4_spec_retain_arm(0, &[]);
4798    if !ok {
4799        // Nothing committed on the host; the device may hold half-appended
4800        // state, so put the snapshot back before declining.
4801        if let Some(sh) = txn.shadow.take() {
4802            let _ = crate::gpu_wgpu::dsv4_spec_restore(&sh);
4803        }
4804        st.dev_filled = txn.dev_filled;
4805        st.dev_n_comp = txn.dev_n_comp;
4806        st.dev_n_ix = txn.dev_n_ix;
4807        st.pos = pos0;
4808        return None;
4809    }
4810    txn.states = states.clone();
4811    let t_chain = t0.elapsed();
4812    if std::env::var("CMF_DSV4_FOLD_DBG").is_ok() {
4813        // Any indexer fold this window landed: read the entry back and
4814        // print a fingerprint, so the fused and per-token folds can be
4815        // held against each other on the release shapes.
4816        for li in 0..gpu_end {
4817            let Some(ixr) = &layers[li].indexer else { continue };
4818            let ratio = ixr.compressor.ratio;
4819            for t in 0..b {
4820                if (pos0 + t + 1) % ratio == 0 {
4821                    let ew = {
4822                        let w = ixr.compressor.wkv.rows();
4823                        if ixr.compressor.overlap { w / 2 } else { w }
4824                    };
4825                    let idx_new = txn.dev_n_ix[li] + (0..=t)
4826                        .filter(|k| (pos0 + k + 1) % ratio == 0)
4827                        .count()
4828                        - 1;
4829                    if let Some(v) =
4830                        crate::gpu_wgpu::dsv4_dbg_read_ix(st.kv_id, li, idx_new * ew, ew.min(8))
4831                    {
4832                        let sum: f32 = v.iter().sum();
4833                        eprintln!(
4834                            "[fold] li={li} pos={} entry={idx_new} head={:?} sum={sum:.6}",
4835                            pos0 + t,
4836                            &v[..4.min(v.len())]
4837                        );
4838                    }
4839                }
4840            }
4841        }
4842    }
4843
4844    // ── host tail + every position's head ──
4845    let mut scratch = HcScratch::new(cfg);
4846    argmax_out.clear();
4847    logits_out.clear();
4848    logits_out.resize(b * cfg.vocab, 0.0);
4849    let mut head_in = vec![0.0f32; b * dim];
4850    let mut host_steps: Vec<(usize, Vec<HostLayerSnap>)> = Vec::new();
4851    host_tail_walk_batch(
4852        g, layers, cfg, st, gpu_end, &mut states, ids, pos0, b, inv_freq, &mut scratch, pool,
4853        Some(&mut host_steps),
4854    );
4855    txn.host_steps = host_steps;
4856    for t in 0..b {
4857        let state = &states[t * hc * dim..(t + 1) * hc * dim];
4858        let h = &mut head_in[t * dim..(t + 1) * dim];
4859        hc_head_fold(state, &g.hc_head_fn, g.hc_head_scale, &g.hc_head_base, cfg, pool, h);
4860        rms_weighted(h, &g.norm, cfg.norm_eps);
4861    }
4862    // One B-wide head submission instead of B fenced matvecs.
4863    let head_gpu = g.head.model_idx().is_some_and(|hi| {
4864        let model = layers[0].experts.first().and_then(|e| e.w1.model_arc());
4865        model.is_some_and(|m| {
4866            crate::gpu_wgpu::q4tp_matvec_batch_for_test(
4867                &m, hi, &head_in, b, cfg.vocab, dim, logits_out,
4868            )
4869        })
4870    });
4871    for t in 0..b {
4872        if !head_gpu {
4873            let h = &head_in[t * dim..(t + 1) * dim];
4874            g.head.matvec(h, &mut logits_out[t * cfg.vocab..(t + 1) * cfg.vocab], pool);
4875        }
4876        let row = &logits_out[t * cfg.vocab..(t + 1) * cfg.vocab];
4877        let mut best = 0usize;
4878        for v in 1..cfg.vocab {
4879            if row[v] > row[best] {
4880                best = v;
4881            }
4882        }
4883        argmax_out.push(best as u32);
4884    }
4885    walked_out.clear();
4886    walked_out.extend_from_slice(&states);
4887    st.pos = pos0 + b;
4888    if spec_time {
4889        eprintln!(
4890            "verify: тень+сид+цепочка {:.1} мс, хвост+голова {:.1} мс",
4891            t_chain.as_secs_f64() * 1e3,
4892            (t0.elapsed() - t_chain).as_secs_f64() * 1e3,
4893        );
4894    }
4895    Some(txn)
4896}
4897
4898/// Keep the accepted prefix of a verify pass and put everything else back.
4899///
4900/// `accepted` counts the FED tokens whose state stays (at least 1 — the
4901/// first fed token was already committed by the caller). With
4902/// `accepted == batch` this is free; otherwise the device restores its
4903/// snapshot and replays the accepted tokens' state appends, and the host
4904/// tail re-walks them.
4905#[cfg(feature = "gpu")]
4906pub fn dsv4_spec_finish(
4907    g: &Dsv4Globals,
4908    layers: &[Dsv4Layer],
4909    cfg: &Dsv4Cfg,
4910    st: &mut Dsv4State,
4911    mut txn: Dsv4SpecTxn,
4912    accepted: usize,
4913    ids: &[u32],
4914    inv_freq: &[f32],
4915    pool: Option<&crate::pool::Pool>,
4916) -> bool {
4917    macro_rules! sfail {
4918        ($($t:tt)*) => {{
4919            if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
4920                eprintln!("spec_finish: {}", format_args!($($t)*));
4921            }
4922            return false;
4923        }};
4924    }
4925    let b = txn.batch;
4926    let k = accepted.min(b);
4927    let (hc, dim, hd) = (cfg.hc_mult, cfg.dim, cfg.head_dim);
4928    // The staged batch never slid the windows; land the accepted prefix now,
4929    // whatever k is.
4930    let win_metas: Vec<(usize, usize, usize, usize)> = (0..txn.gpu_end)
4931        .map(|li| (li, txn.dev_filled[li], cfg.window, hd))
4932        .collect();
4933    if !crate::gpu_wgpu::dsv4_spec_commit_windows(st.kv_id, &win_metas, b, k) {
4934        sfail!("коммит окон");
4935    }
4936    if k == b {
4937        // Every stream mutation was the walk's own kernels in walk order —
4938        // nothing to put back.
4939        return true;
4940    }
4941    // ── device: restore to the snapshot, then replay the accepted tokens ──
4942    let Some(sh) = txn.shadow.take() else { sfail!("нет тени") };
4943    if !crate::gpu_wgpu::dsv4_spec_restore(&sh) {
4944        sfail!("restore");
4945    }
4946    let Some(model) = layers[0].experts.first().and_then(|e| e.w1.model_arc()) else {
4947        sfail!("нет модели");
4948    };
4949    let mut plan: Vec<(usize, crate::gpu_wgpu::Dsv4Prep)> = Vec::new();
4950    let mut freqs_own: Vec<&[f32]> = Vec::new();
4951    for li in 0..txn.gpu_end {
4952        let l = &layers[li];
4953        let Some(wkv) = l.wkv.model_idx() else { sfail!("wkv слоя {li}") };
4954        let comp = match &l.compressor {
4955            None => None,
4956            Some(cp) => {
4957                let (Some(a), Some(bx)) = (cp.wkv.model_idx(), cp.wgate.model_idx()) else {
4958                    sfail!("компрессор слоя {li}");
4959                };
4960                Some((
4961                    crate::gpu_wgpu::Dsv4CompW { wkv: a, wgate: bx, norm: &cp.norm, ape: &cp.ape },
4962                    crate::gpu_wgpu::Dsv4CompGeom {
4963                        width: cp.wkv.rows(),
4964                        hidden: dim,
4965                        ratio: cp.ratio,
4966                        overlap: cp.overlap,
4967                        rope_dim: cfg.rope_head_dim,
4968                        eps: cfg.norm_eps,
4969                    },
4970                ))
4971            }
4972        };
4973        let ix = match &l.indexer {
4974            None => None,
4975            Some(ixr) => {
4976                let cp = &ixr.compressor;
4977                let (Some(a), Some(bx), Some(qb), Some(wp)) = (
4978                    cp.wkv.model_idx(),
4979                    cp.wgate.model_idx(),
4980                    ixr.wq_b.model_idx(),
4981                    ixr.weights_proj.model_idx(),
4982                ) else {
4983                    sfail!("индексер слоя {li}");
4984                };
4985                let ih = ixr.weights_proj.rows();
4986                Some((
4987                    crate::gpu_wgpu::Dsv4CompW { wkv: a, wgate: bx, norm: &cp.norm, ape: &cp.ape },
4988                    crate::gpu_wgpu::Dsv4CompGeom {
4989                        width: cp.wkv.rows(),
4990                        hidden: dim,
4991                        ratio: cp.ratio,
4992                        overlap: cp.overlap,
4993                        rope_dim: cfg.rope_head_dim,
4994                        eps: cfg.norm_eps,
4995                    },
4996                    crate::gpu_wgpu::Dsv4IxW { wq_b: qb, weights_proj: wp },
4997                    crate::gpu_wgpu::Dsv4IxGeom {
4998                        ih,
4999                        idim: ixr.wq_b.rows() / ih.max(1),
5000                        q_lora: cfg.q_lora_rank,
5001                        hidden: dim,
5002                        rope_dim: cfg.rope_head_dim,
5003                        eps: cfg.norm_eps,
5004                        top_k: cfg.index_topk,
5005                        window: cfg.window,
5006                    },
5007                ))
5008            }
5009        };
5010        let ew_c = comp.as_ref().map_or(0, |(_, cg)| {
5011            if cg.overlap { cg.width / 2 } else { cg.width }
5012        });
5013        let ew_i = ix.as_ref().map_or(0, |(_, cg, _, _)| {
5014            if cg.overlap { cg.width / 2 } else { cg.width }
5015        });
5016        let prep = crate::gpu_wgpu::Dsv4Prep {
5017            wkv,
5018            kv_norm: &l.kv_norm,
5019            comp,
5020            ix,
5021            filled: txn.dev_filled[li],
5022            window: cfg.window,
5023            n_comp: txn.dev_n_comp[li],
5024            n_ix: txn.dev_n_ix[li],
5025            comp_dst_off: cfg.window * hd + txn.dev_n_comp[li] * ew_c,
5026            ix_dst_off: txn.dev_n_ix[li] * ew_i,
5027            idx_cap: cfg.window + if l.indexer.is_some() { cfg.index_topk } else { 0 },
5028        };
5029        let fr = if l.compressor.is_some() {
5030            g.inv_freq_compress.as_slice()
5031        } else {
5032            g.inv_freq_window.as_slice()
5033        };
5034        freqs_own.push(if fr.is_empty() { inv_freq } else { fr });
5035        plan.push((li, prep));
5036    }
5037    if !crate::gpu_wgpu::dsv4_spec_replay(
5038        &model,
5039        &plan,
5040        st.kv_id,
5041        txn.pos0,
5042        b,
5043        k,
5044        &freqs_own,
5045        hd,
5046        dim,
5047        cfg.rope_head_dim,
5048        cfg.norm_eps,
5049        true,
5050    ) {
5051        sfail!("replay k={k}");
5052    }
5053    // ── host counts: the snapshot advanced by k tokens ──
5054    let advanced = |ratio: usize| -> usize {
5055        if ratio == 0 {
5056            return 0;
5057        }
5058        (0..k).filter(|t| (txn.pos0 + t + 1) % ratio == 0).count()
5059    };
5060    for li in 0..txn.gpu_end {
5061        let l = &layers[li];
5062        st.dev_filled[li] = (txn.dev_filled[li] + k).min(cfg.window);
5063        let ac = l.compressor.as_ref().map_or(0, |cp| advanced(cp.ratio));
5064        let ai = l.indexer.as_ref().map_or(0, |ix| advanced(ix.compressor.ratio));
5065        st.dev_n_comp[li] = txn.dev_n_comp[li] + ac;
5066        st.dev_n_ix[li] = txn.dev_n_ix[li] + ai;
5067        note_compressed(st.kv_id, li, st.dev_n_comp[li]);
5068    }
5069    // ── host tail: the verify pass already walked these tokens; restore
5070    //    the per-token snapshot it took instead of walking them again. ──
5071    if k >= 1 && txn.host_steps.iter().all(|(_, v)| v.len() >= k) && !txn.host_steps.is_empty()
5072    {
5073        for (li, v) in &txn.host_steps {
5074            host_restore(st, *li, &v[k - 1]);
5075        }
5076    } else {
5077        for (li, snap) in &txn.host {
5078            host_restore(st, *li, snap);
5079        }
5080        let mut scratch = HcScratch::new(cfg);
5081        let mut states = txn.states.clone();
5082        host_tail_walk_batch(
5083            g, layers, cfg, st, txn.gpu_end, &mut states[..k * hc * dim], ids, txn.pos0, k,
5084            inv_freq, &mut scratch, pool, None,
5085        );
5086    }
5087    st.pos = txn.pos0 + k;
5088    true
5089}
5090
5091pub fn forward_chunk(
5092    g: &Dsv4Globals,
5093    layers: &[Dsv4Layer],
5094    cfg: &Dsv4Cfg,
5095    st: &mut Dsv4State,
5096    ids: &[u32],
5097    pos0: usize,
5098    inv_freq: &[f32],
5099    pool: Option<&crate::pool::Pool>,
5100    logits: &mut Vec<f32>,
5101    want_logits: bool,
5102) {
5103    let bs = batch_prefill();
5104    if bs > 1 {
5105        // The first token walks, always. The batch will only run where every
5106        // layer has already proved it takes the card, and that proof is a
5107        // completed single-token run — with the whole prompt arriving as one
5108        // chunk there is otherwise no first run to give it, and the batch
5109        // declines for the entire prompt while a gate comparing it against
5110        // the walk reports agreement it never tested.
5111        let mut i = 0;
5112        if !st.dev_owned && !ids.is_empty() {
5113            st.pos = pos0;
5114            forward_token_inner(g, layers, cfg, st, ids[0], inv_freq, pool, logits, ids.len() == 1);
5115            i = 1;
5116        }
5117        while i < ids.len() {
5118            let end = (i + bs).min(ids.len());
5119            st.pos = pos0 + i;
5120            if !forward_chunk_batched(
5121                g,
5122                layers,
5123                cfg,
5124                st,
5125                &ids[i..end],
5126                pos0 + i,
5127                inv_freq,
5128                pool,
5129                logits,
5130                want_logits && end == ids.len(),
5131            ) {
5132                break;
5133            }
5134            i = end;
5135        }
5136        if i == ids.len() {
5137            return;
5138        }
5139        // Refused before touching anything; the walk starts where it left off.
5140        for (k, &id) in ids.iter().enumerate().skip(i) {
5141            st.pos = pos0 + k;
5142            let last = want_logits && k + 1 == ids.len();
5143            forward_token_inner(g, layers, cfg, st, id, inv_freq, pool, logits, last);
5144        }
5145        return;
5146    }
5147    for (i, &id) in ids.iter().enumerate() {
5148        st.pos = pos0 + i;
5149        let last = want_logits && i + 1 == ids.len();
5150        forward_token_inner(g, layers, cfg, st, id, inv_freq, pool, logits, last);
5151    }
5152}
5153
5154pub fn forward_token(
5155    g: &Dsv4Globals,
5156    layers: &[Dsv4Layer],
5157    cfg: &Dsv4Cfg,
5158    st: &mut Dsv4State,
5159    token_id: u32,
5160    inv_freq: &[f32],
5161    pool: Option<&crate::pool::Pool>,
5162    logits: &mut Vec<f32>,
5163) {
5164    forward_token_inner(g, layers, cfg, st, token_id, inv_freq, pool, logits, true);
5165}
5166
5167#[allow(clippy::too_many_arguments)]
5168fn forward_token_inner(
5169    g: &Dsv4Globals,
5170    layers: &[Dsv4Layer],
5171    cfg: &Dsv4Cfg,
5172    st: &mut Dsv4State,
5173    token_id: u32,
5174    inv_freq: &[f32],
5175    pool: Option<&crate::pool::Pool>,
5176    logits: &mut Vec<f32>,
5177    // Prompt tokens other than the last one have their logits thrown away.
5178    want_logits: bool,
5179) {
5180    let _t_all = prof::on().then(std::time::Instant::now);
5181    let _all_guard = Charge(_t_all, &prof::ALL_NS);
5182    let (hc, dim) = (cfg.hc_mult, cfg.dim);
5183
5184    // Embedding, replicated into the copies.
5185    let mut emb = vec![0.0f32; dim];
5186    g.embed.row_f32(token_id as usize, &mut emb);
5187    let mut state = vec![0.0f32; hc * dim];
5188    for j in 0..hc {
5189        state[j * dim..(j + 1) * dim].copy_from_slice(&emb);
5190    }
5191
5192    let mut scratch = HcScratch::new(cfg);
5193    let mut dump: Vec<String> = Vec::new();
5194    if dump_path().is_some() {
5195        dump.push(format!("\"embed\":{}", vec_json(&emb)));
5196        PICKED.with(|p| p.borrow_mut().clear());
5197        BODY.with(|b| b.borrow_mut().clear());
5198        dump.push(",\"layers\":[".into());
5199    }
5200    if trace_on() {
5201        eprintln!(
5202            "[dsv4] tok={token_id} pos={} embed rms={:.5}",
5203            st.pos,
5204            rms_of(&emb)
5205        );
5206    }
5207    // ── one submission per layer, when the device will take it ──
5208    #[cfg(feature = "gpu")]
5209    let layer_frames = gpu_layer_enabled()
5210        && dsv4_layer_loop(
5211            &mut state, layers, g, cfg, st, token_id, inv_freq, pool, &mut scratch,
5212        );
5213    #[cfg(not(feature = "gpu"))]
5214    let layer_frames = false;
5215
5216    // ── the fast two-frame path: hyper-connections on the card ──
5217    // Measured on the release, the fold, the Sinkhorn and the norms cost 19
5218    // ms of a 57 ms token on the host and hundredths of one on the device.
5219    // With both frames doing their own, the host carries nothing between a
5220    // layer's halves and the MoE half's input never leaves the card — one
5221    // readback a layer instead of two.
5222    #[cfg(feature = "gpu")]
5223    let hc_dev = hc_on_device()
5224        && !layer_frames
5225        && gpu_attn_enabled()
5226        && gpu_moe2_enabled()
5227        && dump_path().is_none();
5228    #[cfg(not(feature = "gpu"))]
5229    let hc_dev = false;
5230    // The device loop's verdict as a VALUE, not as a cfg-gated `if`. It used
5231    // to be the latter, with the CPU loop in the `else` arm — so a build
5232    // without the gpu feature compiled no layer loop at all and every token
5233    // passed through untouched. The window test said so ("sliding window
5234    // never filled") and only in the CPU-only build, which is the one
5235    // configuration the gate was not running.
5236    #[cfg(feature = "gpu")]
5237    let two_frame_done = hc_dev
5238        && dsv4_two_frame_loop(
5239            &mut state, layers, g, cfg, st, token_id, inv_freq, pool, &mut scratch,
5240        );
5241    #[cfg(not(feature = "gpu"))]
5242    let two_frame_done = false;
5243    if !two_frame_done {
5244
5245    for (li, l) in layers.iter().enumerate() {
5246        if layer_frames {
5247            break;
5248        }
5249        // attention half
5250        hc_block(
5251            &mut state,
5252            &l.hc_attn_fn,
5253            &l.hc_attn_scale,
5254            &l.hc_attn_base,
5255            &l.attn_norm,
5256            cfg,
5257            &mut scratch,
5258            pool,
5259            |folded, out| {
5260                if dump_path().is_some() {
5261                    // The body's own input and output, so the reference can be
5262                    // fed the port's input: then only the body can differ.
5263                    BODY.with(|b| b.borrow_mut().push(vec_json(folded)));
5264                }
5265                // The layer's kind decides its frequencies, not the model's.
5266                let freqs = if l.compressor.is_some() {
5267                    &g.inv_freq_compress
5268                } else {
5269                    &g.inv_freq_window
5270                };
5271                let freqs = if freqs.is_empty() {
5272                    inv_freq
5273                } else {
5274                    freqs.as_slice()
5275                };
5276                attention_step(folded, l, cfg, st, li, freqs, pool, None, out);
5277                if dump_path().is_some() {
5278                    BODY.with(|b| b.borrow_mut().push(vec_json(out)));
5279                }
5280            },
5281        );
5282        if dump_path().is_some() {
5283            // After the attention half only — this is what separates an
5284            // attention discrepancy from an expert one.
5285            dump.push(format!(
5286                "{}{}",
5287                if li == 0 { "" } else { "," },
5288                vec_json(&state)
5289            ));
5290        }
5291        // FFN half
5292        let _t_hc2 = prof::on().then(std::time::Instant::now);
5293        hc_block(
5294            &mut state,
5295            &l.hc_ffn_fn,
5296            &l.hc_ffn_scale,
5297            &l.hc_ffn_base,
5298            &l.ffn_norm,
5299            cfg,
5300            &mut scratch,
5301            pool,
5302            |folded, out| moe_step(folded, l, cfg, token_id, li, pool, out),
5303        );
5304        if let Some(t) = _t_hc2 {
5305            // The block's own time minus the expert step inside it — what the
5306            // fold, the norm and the expand cost on their own.
5307            prof::HC_NS.fetch_add(
5308                t.elapsed().as_nanos() as u64,
5309                std::sync::atomic::Ordering::Relaxed,
5310            );
5311        }
5312        if dump_path().is_some() {
5313            dump.push(format!(",{}", vec_json(&state)));
5314        }
5315        if trace_on() && (st.pos % 64 == 0 || st.pos == 199) {
5316            eprintln!(
5317                "[dsv4]  кеши слоя {li}: окно={} сжатых={} индекс={} (ratio={:?})",
5318                st.window[li].len() / cfg.head_dim.max(1),
5319                st.compressed[li].len() / cfg.head_dim.max(1),
5320                st.index_kv[li].len().max(1) / 128,
5321                l.compressor.as_ref().map(|c| (c.ratio, c.overlap)),
5322            );
5323        }
5324        if trace_on() {
5325            let bad = state.iter().filter(|v| !v.is_finite()).count();
5326            eprintln!(
5327                "[dsv4]  layer {li:>2}: rms={:.5}{}",
5328                rms_of(&state),
5329                if bad > 0 {
5330                    format!("  NON-FINITE x{bad}")
5331                } else {
5332                    String::new()
5333                }
5334            );
5335        }
5336        dspark_note(li, &state, cfg);
5337    }
5338    }
5339    st.pos += 1;
5340
5341    // Collapse the copies, normalize, project to the vocabulary.
5342    let mut h = vec![0.0f32; dim];
5343    hc_head_fold(
5344        &state,
5345        &g.hc_head_fn,
5346        g.hc_head_scale,
5347        &g.hc_head_base,
5348        cfg,
5349        pool,
5350        &mut h,
5351    );
5352    if !want_logits {
5353        logits.clear();
5354        return;
5355    }
5356    let _t_head = prof::on().then(std::time::Instant::now);
5357    rms_weighted(&mut h, &g.norm, cfg.norm_eps);
5358    logits.clear();
5359    logits.resize(g.head.rows(), 0.0);
5360    g.head.matvec(&h, logits, pool);
5361    if let Some(t) = _t_head {
5362        prof::HEAD_NS.fetch_add(
5363            t.elapsed().as_nanos() as u64,
5364            std::sync::atomic::Ordering::Relaxed,
5365        );
5366    }
5367    if dump_path().is_some() {
5368        dump.push("]".into());
5369        let picked = PICKED.with(|p| {
5370            p.borrow()
5371                .iter()
5372                .map(|v| {
5373                    format!(
5374                        "[{}]",
5375                        v.iter()
5376                            .map(|e| e.to_string())
5377                            .collect::<Vec<_>>()
5378                            .join(",")
5379                    )
5380                })
5381                .collect::<Vec<_>>()
5382                .join(",")
5383        });
5384        dump.push(format!(",\"experts\":[{picked}]"));
5385        let body = BODY.with(|b| b.borrow().join(","));
5386        dump.push(format!(",\"attn_io\":[{body}]"));
5387        dump_line(&format!(
5388            "{{\"tok\":{token_id},\"pos\":{},{},\"head\":{},\"logits\":{}}}",
5389            st.pos - 1,
5390            dump.join(""),
5391            vec_json(&h),
5392            vec_json(logits)
5393        ));
5394    }
5395    if trace_on() {
5396        let (mut top, mut best) = (0usize, f32::NEG_INFINITY);
5397        for (i, &v) in logits.iter().enumerate() {
5398            if v > best {
5399                best = v;
5400                top = i;
5401            }
5402        }
5403        let lo = logits.iter().cloned().fold(f32::MAX, f32::min);
5404        eprintln!(
5405            "[dsv4]  head: rms={:.5} logits[{}..{:.3}] argmax={top}",
5406            rms_of(&h),
5407            format_args!("{lo:.3}"),
5408            best
5409        );
5410    }
5411}
5412
5413/// Build the runtime weights from a converted `.cmf`.
5414///
5415/// Names are the converter's output (see `canon_name`'s deepseek_v4 arm):
5416/// attention keeps DeepSeek's own spelling under `self_attn.`, the MoE is
5417/// rewritten into the layout every other MoE here uses, and the hyper-
5418/// connection tensors ride under the layer prefix.
5419pub fn load(
5420    model: &std::sync::Arc<cortiq_core::CmfModel>,
5421    cfg: &Dsv4Cfg,
5422    n_layers: usize,
5423) -> Result<(Dsv4Globals, Vec<Dsv4Layer>), String> {
5424    let q = |name: &str| -> Result<crate::qtensor::QTensor, String> {
5425        crate::qtensor::QTensor::from_model(model, name)
5426    };
5427    // The small pieces — norms, the sink, ape, the hyper-connection
5428    // projections — are read as plain f32. They are not all 2-D (a norm is a
5429    // vector), so this cannot go through QTensor, which requires a matrix.
5430    let f = |name: &str| -> Result<Vec<f32>, String> {
5431        crate::loader::load_f32(model, name, &crate::loader::Overlay::None)
5432    };
5433
5434    // Two frequency tables, chosen per layer by whether it compresses. The
5435    // release's compress_rope_theta (160 000) is not in config.json — it
5436    // lives in inference/config.json — so it is pinned here with the other
5437    // constants the header cannot carry.
5438    let rope_of = |base: f32, yarn: bool| -> Vec<f32> {
5439        if yarn {
5440            crate::attention::yarn_inv_freq(cfg.rope_head_dim, base, 16.0, 65536, 32.0, 1.0)
5441        } else {
5442            crate::attention::rope_inv_freq(cfg.rope_head_dim, base)
5443        }
5444    };
5445    let globals = Dsv4Globals {
5446        inv_freq_compress: rope_of(160_000.0, true),
5447        inv_freq_window: rope_of(10_000.0, false),
5448        embed: q("model.embed_tokens.weight")?,
5449        norm: f("model.norm.weight")?,
5450        head: q("lm_head.weight")?,
5451        hc_head_fn: f("model.hc_head_fn")?,
5452        hc_head_base: f("model.hc_head_base")?,
5453        hc_head_scale: *f("model.hc_head_scale")?
5454            .first()
5455            .ok_or("dsv4: empty hc_head_scale")?,
5456    };
5457
5458    let mut layers = Vec::with_capacity(n_layers);
5459    for li in 0..n_layers {
5460        layers.push(load_layer(
5461            model,
5462            cfg,
5463            &format!("model.layers.{li}"),
5464            Scheme::Main,
5465        )?);
5466    }
5467    Ok((globals, layers))
5468}
5469
5470/// Where a layer's tensors live in the file.
5471///
5472/// The MTP modules are the same layer as any other — attention, a
5473/// hyper-connection pair, a gated MoE over 256 experts — but the converter
5474/// wrote them under DeepSeek's internal names rather than the HF ones it used
5475/// for the trunk. Two schemes, one loader: a second copy would drift.
5476#[derive(Clone, Copy, PartialEq, Eq, Debug)]
5477pub enum Scheme {
5478    Main,
5479    Mtp,
5480}
5481
5482impl Scheme {
5483    fn attn(self) -> &'static str {
5484        match self {
5485            Scheme::Main => "self_attn",
5486            Scheme::Mtp => "attn",
5487        }
5488    }
5489    fn attn_norm(self) -> &'static str {
5490        match self {
5491            Scheme::Main => "input_layernorm.weight",
5492            Scheme::Mtp => "attn_norm.weight",
5493        }
5494    }
5495    fn ffn_norm(self) -> &'static str {
5496        match self {
5497            Scheme::Main => "post_attention_layernorm.weight",
5498            Scheme::Mtp => "ffn_norm.weight",
5499        }
5500    }
5501    fn mlp(self) -> &'static str {
5502        match self {
5503            Scheme::Main => "mlp",
5504            Scheme::Mtp => "ffn",
5505        }
5506    }
5507    /// The router's per-expert bias. Absent on the trunk's hash layers, which
5508    /// is how they are recognised; always present on an MTP module.
5509    fn gate_bias(self) -> &'static str {
5510        match self {
5511            Scheme::Main => "expert_bias",
5512            Scheme::Mtp => "gate.bias",
5513        }
5514    }
5515    fn shared(self) -> &'static str {
5516        match self {
5517            Scheme::Main => "shared_expert",
5518            Scheme::Mtp => "shared_experts",
5519        }
5520    }
5521    /// gate, down, up — in that order, which is w1/w2/w3 upstream.
5522    fn w(self, i: u8) -> &'static str {
5523        match (self, i) {
5524            (Scheme::Main, 1) => "gate_proj.weight",
5525            (Scheme::Main, 2) => "down_proj.weight",
5526            (Scheme::Main, _) => "up_proj.weight",
5527            (Scheme::Mtp, 1) => "w1.weight",
5528            (Scheme::Mtp, 2) => "w2.weight",
5529            (Scheme::Mtp, _) => "w3.weight",
5530        }
5531    }
5532}
5533
5534/// One layer, wherever it lives in the file.
5535pub fn load_layer(
5536    model: &std::sync::Arc<cortiq_core::CmfModel>,
5537    cfg: &Dsv4Cfg,
5538    p: &str,
5539    s: Scheme,
5540) -> Result<Dsv4Layer, String> {
5541    let q = |name: &str| -> Result<crate::qtensor::QTensor, String> {
5542        crate::qtensor::QTensor::from_model(model, name)
5543    };
5544    let f = |name: &str| -> Result<Vec<f32>, String> {
5545        crate::loader::load_f32(model, name, &crate::loader::Overlay::None)
5546    };
5547    let opt_f = |name: &str| -> Option<Vec<f32>> { f(name).ok() };
5548    let at = s.attn();
5549    let ml = s.mlp();
5550    {
5551        let scale3 = |name: &str| -> Result<[f32; 3], String> {
5552            let v = f(name)?;
5553            if v.len() < 3 {
5554                return Err(format!("{name}: expected 3 scales, got {}", v.len()));
5555            }
5556            Ok([v[0], v[1], v[2]])
5557        };
5558        // The compressor exists on every layer whose ratio is non-zero;
5559        // its presence in the file is the only signal we need.
5560        let compressor = match q(&format!("{p}.{at}.compressor.wkv.weight")) {
5561            Ok(wkv) => {
5562                let ape = f(&format!("{p}.{at}.compressor.ape"))?;
5563                // ape is [ratio, coff*head_dim]; coff is 2 when the windows
5564                // overlap, which the release does at ratio 4.
5565                let width = wkv.rows();
5566                let ratio = (ape.len() / width.max(1)).max(1);
5567                Some(Dsv4Compressor {
5568                    wkv,
5569                    wgate: q(&format!("{p}.{at}.compressor.wgate.weight"))?,
5570                    norm: f(&format!("{p}.{at}.compressor.norm.weight"))?,
5571                    ape,
5572                    ratio,
5573                    overlap: ratio == 4,
5574                })
5575            }
5576            Err(_) => None,
5577        };
5578        let indexer = match q(&format!("{p}.{at}.indexer.wq_b.weight")) {
5579            Ok(wq_b) => {
5580                let ape = f(&format!("{p}.{at}.indexer.compressor.ape"))?;
5581                let cwkv = q(&format!("{p}.{at}.indexer.compressor.wkv.weight"))?;
5582                let width = cwkv.rows();
5583                let ratio = (ape.len() / width.max(1)).max(1);
5584                Some(Dsv4Indexer {
5585                    wq_b,
5586                    weights_proj: q(&format!("{p}.{at}.indexer.weights_proj.weight"))?,
5587                    compressor: Dsv4Compressor {
5588                        wkv: cwkv,
5589                        wgate: q(&format!("{p}.{at}.indexer.compressor.wgate.weight"))?,
5590                        norm: f(&format!("{p}.{at}.indexer.compressor.norm.weight"))?,
5591                        ape,
5592                        ratio,
5593                        overlap: ratio == 4,
5594                    },
5595                })
5596            }
5597            Err(_) => None,
5598        };
5599
5600        let mut experts = Vec::with_capacity(cfg.n_routed_experts);
5601        for e in 0..cfg.n_routed_experts {
5602            let ep = format!("{p}.{ml}.experts.{e}");
5603            experts.push(Dsv4Expert {
5604                w1: q(&format!("{ep}.{w}", w = s.w(1)))?,
5605                w2: q(&format!("{ep}.{w}", w = s.w(2)))?,
5606                w3: q(&format!("{ep}.{w}", w = s.w(3)))?,
5607            });
5608        }
5609
5610        Ok(Dsv4Layer {
5611            attn_norm: f(&format!("{p}.{an}", an = s.attn_norm()))?,
5612            ffn_norm: f(&format!("{p}.{fnm}", fnm = s.ffn_norm()))?,
5613            wq_a: q(&format!("{p}.{at}.wq_a.weight"))?,
5614            q_norm: f(&format!("{p}.{at}.q_norm.weight"))?,
5615            wq_b: q(&format!("{p}.{at}.wq_b.weight"))?,
5616            wkv: q(&format!("{p}.{at}.wkv.weight"))?,
5617            kv_norm: f(&format!("{p}.{at}.kv_norm.weight"))?,
5618            wo_a: q(&format!("{p}.{at}.wo_a.weight"))?,
5619            wo_b: q(&format!("{p}.{at}.wo_b.weight"))?,
5620            attn_sink: f(&format!("{p}.{at}.attn_sink"))?,
5621            compressor,
5622            indexer,
5623            hc_attn_fn: f(&format!("{p}.hc_attn_fn"))?,
5624            hc_attn_base: f(&format!("{p}.hc_attn_base"))?,
5625            hc_attn_scale: scale3(&format!("{p}.hc_attn_scale"))?,
5626            hc_ffn_fn: f(&format!("{p}.hc_ffn_fn"))?,
5627            hc_ffn_base: f(&format!("{p}.hc_ffn_base"))?,
5628            hc_ffn_scale: scale3(&format!("{p}.hc_ffn_scale"))?,
5629            gate: q(&format!("{p}.{ml}.gate.weight"))?,
5630            // The bias is absent exactly on the hash layers, and the table
5631            // is present exactly there — the file itself says which is which.
5632            gate_bias: opt_f(&format!("{p}.{ml}.{b}", b = s.gate_bias())),
5633            tid2eid: opt_f(&format!("{p}.{ml}.tid2eid")),
5634            experts,
5635            mask: if model.tensor(&format!("{p}.{ml}.tid2eid")).is_some() {
5636                None
5637            } else {
5638                crate::loader::moe_task_mask(&format!("{p}."), cfg.n_routed_experts)
5639            },
5640            shared: Dsv4Expert {
5641                w1: q(&format!("{p}.{ml}.{sh}.{w}", sh = s.shared(), w = s.w(1)))?,
5642                w2: q(&format!("{p}.{ml}.{sh}.{w}", sh = s.shared(), w = s.w(2)))?,
5643                w3: q(&format!("{p}.{ml}.{sh}.{w}", sh = s.shared(), w = s.w(3)))?,
5644            },
5645        })
5646    }
5647}
5648
5649/// One module of the speculation stack.
5650///
5651/// The release carries three, so the draft is three deep, and the last one
5652/// also holds a confidence head — the model scores its own proposals rather
5653/// than leaving acceptance to a threshold we would have to invent. Each
5654/// module is a full layer with its own 256 experts; what makes it an MTP
5655/// module rather than a 44th layer is `main_proj`, which folds the previous
5656/// hidden state into the next embedding before the layer runs.
5657pub struct Dsv4Mtp {
5658    pub layer: Dsv4Layer,
5659    /// Stage 0 only: the projection that turns the trunk's captured hidden
5660    /// states into the block's input. Later stages take the block from the
5661    /// stage before them, so they carry none.
5662    pub main_proj: Option<crate::qtensor::QTensor>,
5663    pub main_norm: Option<Vec<f32>>,
5664    /// Last module only: what turns a draft hidden state into logits.
5665    pub norm: Option<Vec<f32>>,
5666    pub hc_head_fn: Option<Vec<f32>>,
5667    pub hc_head_base: Option<Vec<f32>>,
5668    pub hc_head_scale: Option<f32>,
5669    pub confidence: Option<crate::qtensor::QTensor>,
5670    /// Last stage only: a rank-256 bigram table that biases the draft's
5671    /// logits, and whose embedding also feeds the confidence head. Cheap
5672    /// enough that the draft samples through it position by position while
5673    /// the network itself runs the whole block at once.
5674    pub markov_w1: Option<crate::qtensor::QTensor>,
5675    pub markov_w2: Option<crate::qtensor::QTensor>,
5676}
5677
5678/// Load as much of the speculation stack as the file carries, up to
5679/// `max_depth`. Missing is not an error: a checkpoint without MTP simply
5680/// yields an empty stack, and the caller falls back to plain decoding.
5681pub fn load_mtp(
5682    model: &std::sync::Arc<cortiq_core::CmfModel>,
5683    cfg: &Dsv4Cfg,
5684    max_depth: usize,
5685) -> Vec<Dsv4Mtp> {
5686    let f = |name: &str| -> Option<Vec<f32>> {
5687        crate::loader::load_f32(model, name, &crate::loader::Overlay::None).ok()
5688    };
5689    let mut out = Vec::new();
5690    for d in 0..max_depth {
5691        let p = format!("model.mtp.{d}");
5692        // A stage is recognised by its attention, not by `main_proj`: only
5693        // stage 0 has that, and only the last has the head. Keying on either
5694        // end found one module of three.
5695        if model.tensor(&format!("{p}.attn.wq_a.weight")).is_none() {
5696            break;
5697        }
5698        let layer = match load_layer(model, cfg, &p, Scheme::Mtp) {
5699            Ok(l) => l,
5700            Err(e) => {
5701                eprintln!("MTP {d}: пропущен, {e}");
5702                break;
5703            }
5704        };
5705        out.push(Dsv4Mtp {
5706            layer,
5707            main_proj: crate::qtensor::QTensor::from_model(model, &format!("{p}.main_proj.weight"))
5708                .ok(),
5709            main_norm: f(&format!("{p}.main_norm.weight")),
5710            norm: f(&format!("{p}.norm.weight")),
5711            hc_head_fn: f(&format!("{p}.hc_head_fn")),
5712            hc_head_base: f(&format!("{p}.hc_head_base")),
5713            hc_head_scale: f(&format!("{p}.hc_head_scale")).and_then(|v| v.first().copied()),
5714            confidence: crate::qtensor::QTensor::from_model(
5715                model,
5716                &format!("{p}.confidence_head.proj.weight"),
5717            )
5718            .ok(),
5719            markov_w1: crate::qtensor::QTensor::from_model(
5720                model,
5721                &format!("{p}.markov_head.markov_w1.weight"),
5722            )
5723            .ok(),
5724            markov_w2: crate::qtensor::QTensor::from_model(
5725                model,
5726                &format!("{p}.markov_head.markov_w2.weight"),
5727            )
5728            .ok(),
5729        });
5730    }
5731    dspark_apply_mask(&mut out);
5732    if !out.is_empty() {
5733        let mp = out
5734            .iter()
5735            .find_map(|m| m.main_proj.as_ref())
5736            .map(|t| format!("[{}, {}]", t.rows(), t.cols()))
5737            .unwrap_or_else(|| "нет".into());
5738        eprintln!(
5739            "MTP: {} стади(я/и/й), main_proj {mp}, экспертов {}, \
5740             голова уверенности {}, марков {}",
5741            out.len(),
5742            out[0].layer.experts.len(),
5743            if out.iter().any(|m| m.confidence.is_some()) { "есть" } else { "нет" },
5744            if out.iter().any(|m| m.markov_w1.is_some()) { "есть" } else { "нет" },
5745        );
5746    }
5747    out
5748}
5749
5750#[cfg(test)]
5751mod tests {
5752    use super::*;
5753
5754    // A whole model, small enough to reason about: 2 layers, 4 heads, 8
5755    // experts. Weights are deterministic and tiny, which is the point —
5756    // this test is about shapes, indexing and cache bookkeeping, the things
5757    // that a 138 GB file would surface only after an hour of loading.
5758    fn toy() -> (Dsv4Globals, Vec<Dsv4Layer>, Dsv4Cfg) {
5759        use crate::qtensor::QTensor;
5760        let cfg = Dsv4Cfg {
5761            dim: 32,
5762            n_heads: 4,
5763            head_dim: 8,
5764            rope_head_dim: 4,
5765            q_lora_rank: 16,
5766            o_lora_rank: 16,
5767            o_groups: 2,
5768            hc_mult: 4,
5769            hc_sinkhorn_iters: 20,
5770            hc_eps: 1e-6,
5771            norm_eps: 1e-6,
5772            n_routed_experts: 8,
5773            top_k: 2,
5774            moe_inter: 16,
5775            route_scale: 1.0,
5776            swiglu_limit: 10.0,
5777            window: 6,
5778            index_topk: 8,
5779            vocab: 24,
5780        };
5781        // Deterministic pseudo-random in a narrow band: big enough to move
5782        // the state, small enough that nothing saturates.
5783        let w = |n: usize, seed: usize| -> Vec<f32> {
5784            (0..n)
5785                .map(|i| (((i * 7 + seed * 13) % 101) as f32 / 101.0 - 0.5) * 0.3)
5786                .collect()
5787        };
5788        let t = |rows: usize, cols: usize, seed: usize| {
5789            QTensor::from_f32(w(rows * cols, seed), rows, cols)
5790        };
5791        let ones = |n: usize| vec![1.0f32; n];
5792
5793        let (dim, hc) = (cfg.dim, cfg.hc_mult);
5794        // q is n_heads*head_dim wide and kv is one head wide; rope rides the
5795        // tail of each rather than widening anything.
5796        let q_width = cfg.n_heads * cfg.head_dim;
5797        let kv_width = cfg.head_dim;
5798        let o_per_group = q_width / cfg.o_groups;
5799        let mut layers = Vec::new();
5800        for li in 0..2 {
5801            let experts: Vec<Dsv4Expert> = (0..cfg.n_routed_experts)
5802                .map(|e| Dsv4Expert {
5803                    w1: t(cfg.moe_inter, dim, 40 + e + li * 8),
5804                    w2: t(dim, cfg.moe_inter, 60 + e + li * 8),
5805                    w3: t(cfg.moe_inter, dim, 80 + e + li * 8),
5806                })
5807                .collect();
5808            // Layer 0 is a hash layer (table-routed); layer 1 routes normally
5809            // and carries the compressor — both paths get exercised.
5810            layers.push(Dsv4Layer {
5811                attn_norm: ones(dim),
5812                ffn_norm: ones(dim),
5813                wq_a: t(cfg.q_lora_rank, dim, 1 + li),
5814                q_norm: ones(cfg.q_lora_rank),
5815                wq_b: t(q_width, cfg.q_lora_rank, 3 + li),
5816                wkv: t(kv_width, dim, 5 + li),
5817                kv_norm: ones(kv_width),
5818                wo_a: t(cfg.o_groups * cfg.o_lora_rank, o_per_group, 7 + li),
5819                wo_b: t(dim, cfg.o_groups * cfg.o_lora_rank, 9 + li),
5820                attn_sink: vec![0.1; cfg.n_heads],
5821                // Layer 1 carries the OVERLAPPING compressor, as the release
5822                // does at ratio 4: the projection is twice the entry width.
5823                compressor: if li == 1 {
5824                    Some(Dsv4Compressor {
5825                        wkv: t(2 * kv_width, dim, 11),
5826                        wgate: t(2 * kv_width, dim, 13),
5827                        norm: ones(kv_width),
5828                        ape: vec![0.01; 4 * 2 * kv_width],
5829                        ratio: 4,
5830                        overlap: true,
5831                    })
5832                } else {
5833                    None
5834                },
5835                indexer: if li == 1 {
5836                    Some(Dsv4Indexer {
5837                        wq_b: t(2 * 16, cfg.q_lora_rank, 41),
5838                        weights_proj: t(2, dim, 43),
5839                        compressor: Dsv4Compressor {
5840                            wkv: t(2 * 16, dim, 45),
5841                            wgate: t(2 * 16, dim, 47),
5842                            norm: ones(16),
5843                            ape: vec![0.01; 4 * 2 * 16],
5844                            ratio: 4,
5845                            overlap: true,
5846                        },
5847                    })
5848                } else {
5849                    None
5850                },
5851                hc_attn_fn: w((2 + hc) * hc * hc * dim, 15 + li),
5852                hc_attn_base: w((2 + hc) * hc, 17 + li),
5853                hc_attn_scale: [1.0, 1.0, 1.0],
5854                hc_ffn_fn: w((2 + hc) * hc * hc * dim, 19 + li),
5855                hc_ffn_base: w((2 + hc) * hc, 21 + li),
5856                hc_ffn_scale: [1.0, 1.0, 1.0],
5857                gate: t(cfg.n_routed_experts, dim, 23 + li),
5858                gate_bias: if li == 1 {
5859                    Some(vec![0.0; cfg.n_routed_experts])
5860                } else {
5861                    None
5862                },
5863                tid2eid: if li == 0 {
5864                    Some(
5865                        (0..cfg.vocab * cfg.top_k)
5866                            .map(|i| (i % cfg.n_routed_experts) as f32)
5867                            .collect(),
5868                    )
5869                } else {
5870                    None
5871                },
5872                experts,
5873                mask: None,
5874                shared: Dsv4Expert {
5875                    w1: t(cfg.moe_inter, dim, 25 + li),
5876                    w2: t(dim, cfg.moe_inter, 27 + li),
5877                    w3: t(cfg.moe_inter, dim, 29 + li),
5878                },
5879            });
5880        }
5881        let inv = |base: f32| -> Vec<f32> {
5882            (0..cfg.rope_head_dim / 2)
5883                .map(|i| 1.0 / base.powf(2.0 * i as f32 / cfg.rope_head_dim as f32))
5884                .collect()
5885        };
5886        let g = Dsv4Globals {
5887            inv_freq_compress: inv(160000.0),
5888            inv_freq_window: inv(10000.0),
5889            embed: t(cfg.vocab, dim, 31),
5890            norm: ones(dim),
5891            head: t(cfg.vocab, dim, 33),
5892            hc_head_fn: w(hc * hc * dim, 35),
5893            hc_head_base: w(hc, 37),
5894            hc_head_scale: 1.0,
5895        };
5896        (g, layers, cfg)
5897    }
5898
5899    /// The whole stack, decoding a sequence. Every block is on the path:
5900    /// hyper-connections, the double-LoRA attention with its sink, the KV
5901    /// compressor firing on its ratio boundary, hash routing on one layer
5902    /// and score routing on the other.
5903    #[test]
5904    fn forward_token_decodes_a_sequence_without_falling_over() {
5905        let (g, layers, cfg) = toy();
5906        let mut st = Dsv4State::new(layers.len());
5907        let inv_freq: Vec<f32> = (0..cfg.rope_head_dim / 2)
5908            .map(|i| 1.0 / 10000f32.powf(2.0 * i as f32 / cfg.rope_head_dim as f32))
5909            .collect();
5910        let mut logits = Vec::new();
5911
5912        // Ten tokens: more than twice the compressor's ratio, so the
5913        // compressed cache is written on a boundary and read afterwards.
5914        let mut first: Option<Vec<f32>> = None;
5915        for (step, tok) in [3u32, 7, 1, 9, 4, 2, 8, 5, 6, 0].into_iter().enumerate() {
5916            forward_token(
5917                &g,
5918                &layers,
5919                &cfg,
5920                &mut st,
5921                tok,
5922                &inv_freq,
5923                None,
5924                &mut logits,
5925            );
5926            assert_eq!(logits.len(), cfg.vocab, "step {step}: logit count");
5927            assert!(
5928                logits.iter().all(|v| v.is_finite()),
5929                "step {step}: non-finite logit — {logits:?}"
5930            );
5931            // A model that has collapsed returns the same distribution
5932            // regardless of input; that is the failure this catches.
5933            let spread = logits.iter().cloned().fold(f32::MIN, f32::max)
5934                - logits.iter().cloned().fold(f32::MAX, f32::min);
5935            assert!(spread > 1e-6, "step {step}: logits are flat ({spread})");
5936            if step == 0 {
5937                first = Some(logits.clone());
5938            }
5939            assert_eq!(st.pos, step + 1, "position bookkeeping");
5940        }
5941
5942        // The cache has to have grown, and the compressor layer must have
5943        // emitted compressed entries (10 tokens / ratio 4 = 2 windows).
5944        assert!(!st.window[0].is_empty(), "sliding window never filled");
5945        // Ten tokens through a window of six: it must have slid, not grown.
5946        for (li, w) in st.window.iter().enumerate() {
5947            assert!(
5948                w.len() / cfg.head_dim <= cfg.window,
5949                "layer {li}: window holds {} positions, cap is {}",
5950                w.len() / cfg.head_dim,
5951                cfg.window
5952            );
5953        }
5954        assert!(
5955            !st.compressed[1].is_empty(),
5956            "compressor layer produced no compressed KV in 10 tokens"
5957        );
5958        // Ten tokens at ratio 4 fold twice, and the entries must be one head
5959        // wide — the overlapping projection is 2x that, so a width mistake
5960        // shows up here rather than as quiet nonsense.
5961        assert_eq!(
5962            st.compressed[1].len() / cfg.head_dim,
5963            2,
5964            "expected two folds in ten tokens at ratio 4"
5965        );
5966        assert!(
5967            !st.prev_kv[1].is_empty(),
5968            "the overlapping compressor never kept a previous window"
5969        );
5970        // Every layer that HAS an indexer must have filled the indexer's own
5971        // cache: it is what decides which compressed positions attention
5972        // reads, and an empty one silently discards the whole long-range
5973        // memory rather than failing.
5974        for (li, l) in layers.iter().enumerate() {
5975            if l.indexer.is_some() {
5976                assert!(
5977                    !st.index_kv[li].is_empty(),
5978                    "layer {li} has an indexer but its cache stayed empty"
5979                );
5980            }
5981        }
5982
5983        // Context must matter: the same token at position 0 of a fresh state
5984        // and at the end of a filled one cannot give identical logits.
5985        let mut fresh = Dsv4State::new(layers.len());
5986        let mut relogits = Vec::new();
5987        forward_token(
5988            &g,
5989            &layers,
5990            &cfg,
5991            &mut fresh,
5992            3,
5993            &inv_freq,
5994            None,
5995            &mut relogits,
5996        );
5997        assert_eq!(
5998            relogits,
5999            first.unwrap(),
6000            "the same token from a fresh state must reproduce exactly"
6001        );
6002    }
6003
6004    /// The reference clamps `up` on both sides but `gate` only from above.
6005    /// Getting that symmetric would quietly change every expert's output on
6006    /// the tokens that saturate, which is the hardest kind of bug to see.
6007    #[test]
6008    fn swiglu_limit_clamps_up_both_ways_and_gate_only_from_above() {
6009        let inter = 4;
6010        // gate = [-50, 50, 1, -1], up = [50, -50, 1, -1]
6011        let gate_src = [-50.0f32, 50.0, 1.0, -1.0];
6012        let up_src = [50.0f32, -50.0, 1.0, -1.0];
6013        let limit = 10.0f32;
6014        let mut got = vec![0.0f32; inter];
6015        expert_swiglu(
6016            &[0.0],
6017            &|_, d| d.copy_from_slice(&gate_src),
6018            &|_, d| d.copy_from_slice(&up_src),
6019            &|src, d| d.copy_from_slice(src),
6020            inter,
6021            1.0,
6022            limit,
6023            &mut got,
6024        );
6025        let silu = |g: f32| g / (1.0 + (-g).exp());
6026        // gate: only the +50 is cut, the -50 rides through silu untouched.
6027        let want = [
6028            silu(-50.0) * limit,
6029            silu(limit) * -limit,
6030            silu(1.0) * 1.0,
6031            silu(-1.0) * -1.0,
6032        ];
6033        for (i, w) in want.iter().enumerate() {
6034            assert!(
6035                (got[i] - w).abs() < 1e-5,
6036                "lane {i}: got {} want {w}",
6037                got[i]
6038            );
6039        }
6040        // And with the clamp off nothing is touched.
6041        let mut raw = vec![0.0f32; inter];
6042        expert_swiglu(
6043            &[0.0],
6044            &|_, d| d.copy_from_slice(&gate_src),
6045            &|_, d| d.copy_from_slice(&up_src),
6046            &|src, d| d.copy_from_slice(src),
6047            inter,
6048            1.0,
6049            0.0,
6050            &mut raw,
6051        );
6052        assert!(
6053            (raw[1] - silu(50.0) * -50.0).abs() < 1e-3,
6054            "limit 0 must not clamp"
6055        );
6056    }
6057
6058    /// The grouped projection writes its intermediate from several threads
6059    /// at once. Disjoint indices are the whole argument for that being safe,
6060    /// so the pooled result has to equal the serial one exactly — a race
6061    /// here would show up as occasional wrong tokens, not as a crash.
6062    #[test]
6063    fn grouped_projection_is_identical_with_and_without_a_pool() {
6064        let (groups, lora, per_group, dim) = (4usize, 128usize, 64usize, 32usize);
6065        let attn: Vec<f32> = (0..groups * per_group)
6066            .map(|i| ((i * 13) as f32 * 0.021).sin())
6067            .collect();
6068        let wo_a: Vec<f32> = (0..groups * lora * per_group)
6069            .map(|i| ((i * 7) as f32 * 0.011).cos())
6070            .collect();
6071        let wo_b: Vec<f32> = (0..dim * groups * lora)
6072            .map(|i| ((i * 5) as f32 * 0.009).sin())
6073            .collect();
6074        let row = |r: usize, x: &[f32], _sc: &mut [f32]| -> f32 {
6075            wo_a[r * per_group..(r + 1) * per_group]
6076                .iter()
6077                .zip(x)
6078                .map(|(a, b)| a * b)
6079                .sum()
6080        };
6081        let project = |mid: &[f32], dst: &mut [f32]| {
6082            for (d, o) in dst.iter_mut().enumerate() {
6083                *o = wo_b[d * mid.len()..(d + 1) * mid.len()]
6084                    .iter()
6085                    .zip(mid)
6086                    .map(|(a, b)| a * b)
6087                    .sum();
6088            }
6089        };
6090
6091        let mut serial = vec![0.0f32; dim];
6092        o_project(
6093            &attn,
6094            &row,
6095            per_group,
6096            &project,
6097            groups,
6098            lora,
6099            None,
6100            &mut serial,
6101        );
6102
6103        let pool = crate::pool::Pool::new(4);
6104        let mut pooled = vec![0.0f32; dim];
6105        o_project(
6106            &attn,
6107            &row,
6108            per_group,
6109            &project,
6110            groups,
6111            lora,
6112            Some(&pool),
6113            &mut pooled,
6114        );
6115        assert_eq!(serial, pooled, "the pooled projection diverged");
6116        assert!(
6117            serial.iter().any(|v| v.abs() > 1e-6),
6118            "test data is degenerate"
6119        );
6120    }
6121
6122    #[test]
6123    fn block_grouped_projection_matches_position_walk() {
6124        let (_g, layers, cfg) = toy();
6125        let l = &layers[1];
6126        let b = 5;
6127        let attn_len = cfg.n_heads * cfg.head_dim;
6128        let attn: Vec<f32> = (0..b * attn_len)
6129            .map(|i| ((i * 17) as f32 * 0.013).sin())
6130            .collect();
6131        let mut walked = vec![0.0f32; b * cfg.dim];
6132        for bi in 0..b {
6133            o_project(
6134                &attn[bi * attn_len..(bi + 1) * attn_len],
6135                &|r, x, sc| l.wo_a.row_dot(r, x, sc),
6136                l.wo_a.cols(),
6137                &|mid, dst| l.wo_b.matvec(mid, dst, None),
6138                cfg.o_groups,
6139                cfg.o_lora_rank,
6140                None,
6141                &mut walked[bi * cfg.dim..(bi + 1) * cfg.dim],
6142            );
6143        }
6144        let mut batched = vec![0.0f32; b * cfg.dim];
6145        o_project_block(
6146            &attn,
6147            b,
6148            &l.wo_a,
6149            &l.wo_b,
6150            cfg.o_groups,
6151            cfg.o_lora_rank,
6152            None,
6153            &mut batched,
6154        );
6155        assert_eq!(batched, walked);
6156    }
6157
6158    #[test]
6159    fn block_moe_matches_position_walk_in_route_order() {
6160        let (_g, layers, cfg) = toy();
6161        // The scored layer exercises repeated and distinct experts without
6162        // tying the result to a token-id table.
6163        let l = &layers[1];
6164        let b = 5;
6165        let xs: Vec<f32> = (0..b * cfg.dim)
6166            .map(|i| ((i * 11) as f32 * 0.019).cos())
6167            .collect();
6168        let ids = [1u32, 2, 3, 4, 5];
6169        let mut walked = vec![0.0f32; b * cfg.dim];
6170        for bi in 0..b {
6171            moe_step(
6172                &xs[bi * cfg.dim..(bi + 1) * cfg.dim],
6173                l,
6174                &cfg,
6175                ids[bi],
6176                1,
6177                None,
6178                &mut walked[bi * cfg.dim..(bi + 1) * cfg.dim],
6179            );
6180        }
6181        let mut batched = vec![0.0f32; b * cfg.dim];
6182        moe_step_block(&xs, b, l, &cfg, &ids, 1, None, &mut batched);
6183        assert_eq!(batched, walked);
6184    }
6185
6186    /// The overlapping compressor folds 2*ratio slots, not ratio: the
6187    /// previous window contributes its first half of dimensions and the
6188    /// current one its second half. Treating it as a plain compressor makes
6189    /// the entry twice as wide as the cache expects, which lands the whole
6190    /// thing in the wrong store rather than raising anything.
6191    #[test]
6192    fn overlapping_compressor_folds_both_windows() {
6193        let (ratio, d) = (2usize, 3usize);
6194        // Current window: two tokens, 2*d wide each. Second half is what the
6195        // current window contributes.
6196        let cur_kv: Vec<f32> = vec![
6197            1.0, 1.0, 1.0, /*|*/ 10.0, 20.0, 30.0, // token 0
6198            2.0, 2.0, 2.0, /*|*/ 40.0, 50.0, 60.0, // token 1
6199        ];
6200        // Make the current window's second-half scores dominate everywhere.
6201        let cur_sc: Vec<f32> = vec![
6202            0.0, 0.0, 0.0, /*|*/ 0.0, 0.0, 100.0, //
6203            0.0, 0.0, 0.0, /*|*/ 100.0, 100.0, 0.0,
6204        ];
6205        // Previous window: its FIRST half is what it contributes.
6206        let prev_kv: Vec<f32> = vec![
6207            7.0, 8.0, 9.0, /*|*/ 0.0, 0.0, 0.0, //
6208            5.0, 6.0, 7.0, /*|*/ 0.0, 0.0, 0.0,
6209        ];
6210        let prev_sc = vec![0.0f32; ratio * 2 * d];
6211
6212        let mut out = vec![0.0f32; d];
6213        compress_window_overlap(&prev_kv, &prev_sc, &cur_kv, &cur_sc, ratio, d, &mut out);
6214        // dim 0 and 1: token 1's second half wins (score 100)
6215        assert!((out[0] - 40.0).abs() < 1e-3, "dim0 = {}", out[0]);
6216        assert!((out[1] - 50.0).abs() < 1e-3, "dim1 = {}", out[1]);
6217        // dim 2: token 0's second half wins
6218        assert!((out[2] - 30.0).abs() < 1e-3, "dim2 = {}", out[2]);
6219
6220        // With no previous window the fold still works and uses only the
6221        // current one — this is the very first window of a generation.
6222        let mut first = vec![0.0f32; d];
6223        compress_window_overlap(&[], &[], &cur_kv, &cur_sc, ratio, d, &mut first);
6224        assert!(
6225            first.iter().all(|v| v.is_finite()),
6226            "first window: {first:?}"
6227        );
6228        assert!((first[0] - 40.0).abs() < 1e-3, "first dim0 = {}", first[0]);
6229
6230        // And a previous window with real scores does pull the result.
6231        let mut both = vec![0.0f32; d];
6232        let strong_prev = vec![100.0f32; ratio * 2 * d];
6233        compress_window_overlap(
6234            &prev_kv,
6235            &strong_prev,
6236            &cur_kv,
6237            &cur_sc,
6238            ratio,
6239            d,
6240            &mut both,
6241        );
6242        assert!(
6243            (both[0] - 40.0).abs() > 1.0,
6244            "a scored previous window must move the fold, got {}",
6245            both[0]
6246        );
6247    }
6248
6249    /// Numerical parity with the reference. The vectors below come from
6250    /// running `kernel.py::hc_split_sinkhorn`'s own formula on a fixed
6251    /// input; matching them pins the exponent order, the eps placement and
6252    /// the off-by-one in the iteration count all at once — a property test
6253    /// alone would pass with any of those wrong.
6254    #[test]
6255    fn sinkhorn_matches_the_reference_numbers() {
6256        let hc = 4;
6257        let mixes: Vec<f32> = (0..24).map(|i| (i as f32 * 0.37).sin() * 3.0).collect();
6258        let base: Vec<f32> = (0..24).map(|i| (i as f32 * 0.11).cos()).collect();
6259        let (mut pre, mut post, mut comb) = (vec![0.0; hc], vec![0.0; hc], vec![0.0; hc * hc]);
6260        hc_split_sinkhorn(
6261            &mixes,
6262            &[1.0, 1.0, 1.0],
6263            &base,
6264            hc,
6265            20,
6266            1e-6,
6267            &mut pre,
6268            &mut post,
6269            &mut comb,
6270        );
6271        let want_pre = [0.7310596, 0.8888268, 0.9525191, 0.97424865];
6272        let want_post = [1.9600224, 1.9534285, 1.9201256, 1.8160983];
6273        let want_comb = [
6274            0.5996052,
6275            0.28253591,
6276            0.09218107,
6277            0.025676856,
6278            0.17564717,
6279            0.22228767,
6280            0.27174541,
6281            0.33031881,
6282            0.029528176,
6283            0.12206022,
6284            0.32619134,
6285            0.5222193,
6286            0.19521846,
6287            0.37311527,
6288            0.30988118,
6289            0.12178412,
6290        ];
6291        for (i, w) in want_pre.iter().enumerate() {
6292            assert!((pre[i] - w).abs() < 1e-5, "pre[{i}]: {} vs {w}", pre[i]);
6293        }
6294        for (i, w) in want_post.iter().enumerate() {
6295            assert!((post[i] - w).abs() < 1e-5, "post[{i}]: {} vs {w}", post[i]);
6296        }
6297        for (i, w) in want_comb.iter().enumerate() {
6298            assert!((comb[i] - w).abs() < 1e-4, "comb[{i}]: {} vs {w}", comb[i]);
6299        }
6300    }
6301
6302    /// Sinkhorn's whole point is a doubly stochastic matrix: every row and
6303    /// every column sums to one. If the alternating normalization is wrong
6304    /// (or the loop count is off by one) the sums drift, and the residual
6305    /// mixing quietly gains or loses mass on every layer.
6306    #[test]
6307    fn sinkhorn_leaves_the_mixing_matrix_doubly_stochastic() {
6308        let hc = 4;
6309        let mix_hc = (2 + hc) * hc;
6310        // a deliberately lopsided projection
6311        let mixes: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.37).sin() * 3.0).collect();
6312        let base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.11).cos()).collect();
6313        let (mut pre, mut post, mut comb) = (vec![0.0; hc], vec![0.0; hc], vec![0.0; hc * hc]);
6314        hc_split_sinkhorn(
6315            &mixes,
6316            &[1.0, 1.0, 1.0],
6317            &base,
6318            hc,
6319            20,
6320            1e-6,
6321            &mut pre,
6322            &mut post,
6323            &mut comb,
6324        );
6325        for j in 0..hc {
6326            let r: f32 = comb[j * hc..(j + 1) * hc].iter().sum();
6327            assert!((r - 1.0).abs() < 2e-3, "row {j} sums to {r}");
6328            let c: f32 = (0..hc).map(|k| comb[k * hc + j]).sum();
6329            assert!((c - 1.0).abs() < 2e-3, "col {j} sums to {c}");
6330        }
6331        // pre is a gate in (eps, 1+eps); post carries the factor 2
6332        assert!(pre.iter().all(|&v| v > 0.0 && v < 1.001));
6333        assert!(post.iter().all(|&v| v >= 0.0 && v <= 2.0));
6334    }
6335
6336    /// Folding four copies and expanding them back must preserve a constant
6337    /// state exactly when the block contributes nothing: with post = 0 the
6338    /// expansion is a doubly stochastic mix of identical copies, i.e. itself.
6339    #[test]
6340    fn expand_of_identical_copies_is_a_fixed_point() {
6341        let (hc, dim) = (4usize, 3usize);
6342        let residual: Vec<f32> = std::iter::repeat([1.5f32, -2.0, 0.25])
6343            .take(hc)
6344            .flatten()
6345            .collect();
6346        let comb = {
6347            // exactly doubly stochastic: uniform
6348            vec![0.25f32; hc * hc]
6349        };
6350        let post = vec![0.0f32; hc];
6351        let mut out = vec![0.0f32; hc * dim];
6352        hc_expand(&[0.0; 3], &residual, &post, &comb, hc, dim, &mut out);
6353        for (o, r) in out.iter().zip(&residual) {
6354            assert!((o - r).abs() < 1e-6, "{o} vs {r}");
6355        }
6356    }
6357
6358    /// The bias must move the SELECTION without touching the weights: with a
6359    /// large bias on a low-scoring expert it gets picked, but its weight is
6360    /// still its own (small) score, renormalized.
6361    #[test]
6362    fn selection_bias_steers_the_choice_but_not_the_weights() {
6363        let scores = [3.0f32, 0.1, 2.0, 0.05];
6364        let bias = [0.0f32, 10.0, 0.0, 0.0];
6365        let (mut idx, mut w) = (Vec::new(), Vec::new());
6366        route(&scores, Some(&bias), 2, 1.5, None, None, &mut idx, &mut w);
6367        assert_eq!(idx[0], 1, "the biased expert must win selection");
6368        assert_eq!(idx[1], 0);
6369        // weights come from sqrt(softplus(score)) BEFORE the bias, so the
6370        // biased expert's share must be the smaller of the two
6371        assert!(w[0] < w[1], "biased expert kept its own (small) weight");
6372        let sum: f32 = w.iter().sum();
6373        assert!((sum - 1.5).abs() < 1e-5, "weights renormalize then scale");
6374    }
6375
6376    /// The sink is an extra logit with no value: it must lower every
6377    /// weight without adding output. With a huge sink the head should
6378    /// attend to almost nothing.
6379    #[test]
6380    fn attention_sink_drains_weight_without_contributing_output() {
6381        let hd = 2;
6382        let q = [1.0f32, 0.0];
6383        let kv = [1.0f32, 0.0, 0.0, 1.0];
6384        let mut out = vec![0.0f32; hd];
6385        sparse_attend(&q, &kv, &[0, 1], f32::NEG_INFINITY, 1.0, hd, &mut out);
6386        let plain = out.clone();
6387        assert!(plain[0] > plain[1], "the aligned key must dominate");
6388        sparse_attend(&q, &kv, &[0, 1], 20.0, 1.0, hd, &mut out);
6389        assert!(
6390            out[0] < plain[0] * 0.01 && out[1] < plain[1] * 0.01,
6391            "a large sink must drain nearly all the mass: {out:?}"
6392        );
6393    }
6394
6395    /// A masked slot must be ignored entirely — not folded in as a zero
6396    /// key, which would still add exp(0) to the denominator.
6397    #[test]
6398    fn masked_positions_leave_the_denominator_alone() {
6399        let hd = 2;
6400        let q = [1.0f32, 0.0];
6401        let kv = [1.0f32, 0.0, 0.0, 1.0];
6402        let (mut a, mut b) = (vec![0.0f32; hd], vec![0.0f32; hd]);
6403        sparse_attend(&q, &kv, &[0], f32::NEG_INFINITY, 1.0, hd, &mut a);
6404        sparse_attend(
6405            &q,
6406            &kv,
6407            &[0, usize::MAX],
6408            f32::NEG_INFINITY,
6409            1.0,
6410            hd,
6411            &mut b,
6412        );
6413        for (x, y) in a.iter().zip(&b) {
6414            assert!((x - y).abs() < 1e-6, "{x} vs {y}");
6415        }
6416    }
6417
6418    /// Forward then inverse rotation is the identity — the property the
6419    /// output path depends on.
6420    #[test]
6421    fn rope_tail_inverts_itself() {
6422        let inv_freq = [1.0f32, 0.5];
6423        let orig = [9.0f32, 8.0, 1.0, 2.0, 3.0, 4.0];
6424        let mut v = orig;
6425        rope_tail(&mut v, &inv_freq, 7, 4, false);
6426        assert!(v[..2] == orig[..2], "the non-rope head must not move");
6427        assert!(v[2..] != orig[2..], "the tail must actually rotate");
6428        rope_tail(&mut v, &inv_freq, 7, 4, true);
6429        for (a, b) in v.iter().zip(&orig) {
6430            assert!((a - b).abs() < 1e-5, "{a} vs {b}");
6431        }
6432    }
6433
6434    /// The window pooling is a softmax per DIMENSION over the ratio, with
6435    /// the position bias inside the exponent.
6436    #[test]
6437    fn compressor_pools_the_window_per_dimension() {
6438        let (ratio, width) = (2usize, 2usize);
6439        let kv = [1.0f32, 10.0, 3.0, 20.0];
6440        // dim 0: equal scores → mean; dim 1: second token wins by a mile
6441        let score = [0.0f32, 0.0, 0.0, 50.0];
6442        let ape = vec![0.0f32; ratio * width];
6443        let mut out = vec![0.0f32; width];
6444        compress_window(&kv, &score, &ape, ratio, width, &mut out);
6445        assert!(
6446            (out[0] - 2.0).abs() < 1e-5,
6447            "equal scores average: {}",
6448            out[0]
6449        );
6450        assert!(
6451            (out[1] - 20.0).abs() < 1e-3,
6452            "a dominant score wins: {}",
6453            out[1]
6454        );
6455    }
6456
6457    /// A negative dot product must not drag a position down: the relu
6458    /// means heads abstain rather than veto.
6459    #[test]
6460    fn index_scores_relu_before_weighting() {
6461        let (nh, hd) = (2usize, 2usize);
6462        // head 0 aligns with position 0, head 1 anti-aligns with it
6463        let q = [1.0f32, 0.0, -1.0, 0.0];
6464        let kv = [1.0f32, 0.0, 0.0, 1.0];
6465        let w = [1.0f32, 1.0];
6466        let mut sc = Vec::new();
6467        index_scores(&q, &kv, &w, nh, hd, 2, 2, None, &mut sc);
6468        // without the relu the anti-aligned head would cancel head 0 to zero
6469        assert!(sc[0] > 0.9, "abstention, not veto: {:?}", sc);
6470    }
6471
6472    #[test]
6473    fn index_scores_mask_the_future() {
6474        let (nh, hd) = (1usize, 2usize);
6475        let q = [1.0f32, 0.0];
6476        let kv = [1.0f32, 0.0, 1.0, 0.0, 1.0, 0.0];
6477        let w = [1.0f32];
6478        let mut sc = Vec::new();
6479        index_scores(&q, &kv, &w, nh, hd, 3, 2, None, &mut sc);
6480        assert!(sc[0].is_finite() && sc[1].is_finite());
6481        assert!(sc[2] == f32::NEG_INFINITY, "position 2 is in the future");
6482        let mut idx = Vec::new();
6483        top_k_positions(&sc, 3, &mut idx);
6484        assert_eq!(idx, vec![0, 1], "a masked slot never wins a slot");
6485    }
6486
6487    #[test]
6488    fn top_k_is_deterministic_on_ties() {
6489        let sc = [1.0f32, 1.0, 1.0, 0.0];
6490        let mut idx = Vec::new();
6491        top_k_positions(&sc, 2, &mut idx);
6492        assert_eq!(idx, vec![0, 1], "ties resolve to the lower index");
6493    }
6494
6495    /// The block cycle must leave the state's SHAPE intact (hc copies in,
6496    /// hc copies out) and must actually route the block's output back in:
6497    /// a block that writes a constant has to move every copy.
6498    #[test]
6499    fn hc_block_preserves_the_copy_structure_and_applies_the_block() {
6500        let cfg = Dsv4Cfg {
6501            dim: 4,
6502            n_heads: 1,
6503            head_dim: 4,
6504            rope_head_dim: 2,
6505            q_lora_rank: 4,
6506            o_lora_rank: 2,
6507            o_groups: 1,
6508            hc_mult: 4,
6509            hc_sinkhorn_iters: 20,
6510            hc_eps: 1e-6,
6511            norm_eps: 1e-6,
6512            n_routed_experts: 2,
6513            top_k: 1,
6514            moe_inter: 4,
6515            route_scale: 1.0,
6516            swiglu_limit: 10.0,
6517            window: 128,
6518            index_topk: 4,
6519            vocab: 8,
6520        };
6521        let (hc, dim) = (cfg.hc_mult, cfg.dim);
6522        let mix_hc = (2 + hc) * hc;
6523        let hc_fn: Vec<f32> = (0..mix_hc * hc * dim)
6524            .map(|i| ((i % 13) as f32 - 6.0) * 0.05)
6525            .collect();
6526        let hc_base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.2).sin()).collect();
6527        let norm_w = vec![1.0f32; dim];
6528        let mut state: Vec<f32> = (0..hc * dim).map(|i| (i as f32 * 0.3).cos()).collect();
6529        let before = state.clone();
6530        let mut scratch = HcScratch::new(&cfg);
6531        hc_block(
6532            &mut state,
6533            &hc_fn,
6534            &[1.0, 1.0, 1.0],
6535            &hc_base,
6536            &norm_w,
6537            &cfg,
6538            &mut scratch,
6539            None,
6540            |_folded, out: &mut [f32]| out.iter_mut().for_each(|o| *o = 1.0),
6541        );
6542        assert_eq!(state.len(), before.len(), "copy structure must survive");
6543        assert!(state.iter().all(|v| v.is_finite()), "{state:?}");
6544        assert!(
6545            state.iter().zip(&before).any(|(a, b)| (a - b).abs() > 1e-4),
6546            "the block's output has to reach the state"
6547        );
6548    }
6549
6550    #[test]
6551    fn hash_route_reads_the_table_row() {
6552        // vocab 3, top_k 2
6553        let table = [7.0f32, 9.0, 1.0, 2.0, 5.0, 6.0];
6554        assert_eq!(hash_route(&table, 3, 2, 0), vec![7, 9]);
6555        assert_eq!(hash_route(&table, 3, 2, 2), vec![5, 6]);
6556        // out-of-range ids clamp instead of panicking
6557        assert_eq!(hash_route(&table, 3, 2, 99), vec![5, 6]);
6558    }
6559
6560    /// A task mask restricts SELECTION and nothing else: the weights still
6561    /// come from the pre-bias scores and still renormalize, now over what
6562    /// survives. Masking must never reroute — an expert the mask forbids has
6563    /// to be absent, not replaced by a neighbour with the wrong weight.
6564    #[test]
6565    fn a_task_mask_restricts_selection_and_renormalizes() {
6566        // Expert 3 scores highest, then 1, then 2, then 0.
6567        let scores = [0.1f32, 4.0, 1.0, 9.0];
6568        let (mut idx, mut w) = (Vec::new(), Vec::new());
6569        route(&scores, None, 2, 1.0, None, None, &mut idx, &mut w);
6570        assert_eq!(idx, vec![3, 1], "unmasked: the two best win");
6571        let sum: f32 = w.iter().sum();
6572        assert!((sum - 1.0).abs() < 1e-5, "weights must sum to route_scale");
6573
6574        // Forbid the winner: the next two take its place and the weights
6575        // renormalize over them.
6576        let mask = [true, false, true, true];
6577        let (mut i2, mut w2) = (Vec::new(), Vec::new());
6578        route(&scores, None, 2, 1.0, None, Some(&mask), &mut i2, &mut w2);
6579        assert_eq!(i2, vec![3, 2], "masked expert must not be selected");
6580        let sum2: f32 = w2.iter().sum();
6581        assert!((sum2 - 1.0).abs() < 1e-5, "masked weights must renormalize");
6582
6583        // A mask leaving fewer than top_k experts yields fewer, not garbage.
6584        let tight = [false, false, false, true];
6585        let (mut i3, mut w3) = (Vec::new(), Vec::new());
6586        route(&scores, None, 2, 1.0, None, Some(&tight), &mut i3, &mut w3);
6587        assert_eq!(i3, vec![3]);
6588        assert_eq!(w3.len(), 1);
6589    }
6590
6591    /// On a hash layer the reference gathers the scores AT THE TABLE's
6592    /// experts. Choosing top-k first and swapping the indices afterwards
6593    /// leaves every weight attached to a different expert than the one it
6594    /// scales — silently, since both lists are the right length.
6595    #[test]
6596    fn hash_layers_weight_the_experts_the_table_names() {
6597        // Expert 3 scores highest, expert 0 lowest; the table names 0 and 1.
6598        let scores = [0.1f32, 0.4, 0.2, 5.0];
6599        let table = vec![0.0f32, 1.0];
6600        let idx_forced = hash_route(&table, 1, 2, 0);
6601        assert_eq!(idx_forced, vec![0, 1]);
6602
6603        let (mut idx, mut w) = (Vec::new(), Vec::new());
6604        route(
6605            &scores,
6606            None,
6607            2,
6608            1.0,
6609            Some(&idx_forced),
6610            None,
6611            &mut idx,
6612            &mut w,
6613        );
6614        assert_eq!(idx, vec![0, 1], "the table must decide the experts");
6615
6616        // The weights must be the table experts' own scores, normalized.
6617        let sp = |x: f32| (1.0 + x.exp()).ln().sqrt();
6618        let (s0, s1) = (sp(scores[0]), sp(scores[1]));
6619        let tot = s0 + s1;
6620        assert!(
6621            (w[0] - s0 / tot).abs() < 1e-6,
6622            "w[0]={} want {}",
6623            w[0],
6624            s0 / tot
6625        );
6626        assert!(
6627            (w[1] - s1 / tot).abs() < 1e-6,
6628            "w[1]={} want {}",
6629            w[1],
6630            s1 / tot
6631        );
6632
6633        // And the top-k path is untouched: expert 3 still wins there.
6634        let (mut idx2, mut w2) = (Vec::new(), Vec::new());
6635        route(&scores, None, 2, 1.0, None, None, &mut idx2, &mut w2);
6636        assert_eq!(idx2[0], 3, "without a table the highest score still wins");
6637    }
6638}
6639
6640
6641// ══ DSpark: the block-parallel draft ══════════════════════════════════
6642//
6643// Not a classic MTP chain. One pass through the three stages produces the
6644// WHOLE block of `block_size` positions at once: position 0 carries the token
6645// the trunk just emitted, the rest carry a noise token, and every position
6646// attends to every other one — which is why the block cannot be measured a
6647// position at a time and pretend to be faithful. Depth comes from the block,
6648// not from the stage count.
6649//
6650// The stages' KV cache is built from the trunk's hidden state, not from the
6651// draft's own tokens: one entry per real position, `kv_norm(wkv(main_x))`,
6652// in a ring of `window`. The block's own keys and values are appended for
6653// the duration of the block and then discarded.
6654
6655/// The noise token the block's unknown positions carry
6656/// (`dspark_noise_token_id`).
6657pub const DSPARK_NOISE_TOKEN: u32 = 128799;
6658/// `dspark_block_size` — the width of the draft block, and NOT a tuning knob.
6659///
6660/// All five positions attend to each other and the model was trained with
6661/// exactly four noise slots behind the real token, so a narrower block is a
6662/// different draft model, not a cheaper one. What the survival curve argues
6663/// for is verifying fewer of the five — see `dspark_verify_k` — which costs
6664/// less without changing what the draft computes.
6665pub fn dspark_block() -> usize {
6666    5
6667}
6668
6669/// How many of the block's proposals the trunk actually checks.
6670///
6671/// Survival is [0.67, 0.50, 0.29, 0.08, 0.04]: positions four and five are
6672/// paid for on every verify and delivered on a twelfth of them. Three yields
6673/// 2.46 tokens a cycle against five's 2.58, for three fifths of the verify.
6674/// `CMF_DSPARK_VERIFY_K=N` sets it.
6675pub fn dspark_verify_k() -> usize {
6676    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
6677    *K.get_or_init(|| {
6678        std::env::var("CMF_DSPARK_VERIFY_K")
6679            .ok()
6680            .and_then(|v| v.parse::<usize>().ok())
6681            .filter(|&n| (1..=DSPARK_BLOCK_MAX).contains(&n))
6682            .unwrap_or(DSPARK_BLOCK_MAX)
6683    })
6684}
6685
6686/// The trained block width.
6687pub const DSPARK_BLOCK_MAX: usize = 5;
6688
6689/// Per-sequence state of the draft: one KV ring per stage, and the trunk
6690/// hidden states the block's input is projected from.
6691pub struct DsparkState {
6692    /// `[stage][window * kv_width]`, written at `pos % window`.
6693    pub win: Vec<Vec<f32>>,
6694    /// How many real positions each ring holds, capped at `window`.
6695    pub filled: Vec<usize>,
6696    /// The trunk's captured hidden, `dim * n_targets`, refreshed every token.
6697    pub main_hidden: Vec<f32>,
6698    /// True once `main_hidden` holds this position's capture.
6699    pub have_hidden: bool,
6700}
6701
6702impl DsparkState {
6703    pub fn new(stages: usize, cfg: &Dsv4Cfg, targets: usize) -> Self {
6704        Self {
6705            win: vec![Vec::new(); stages],
6706            filled: vec![0; stages],
6707            main_hidden: vec![0.0; cfg.dim * targets],
6708            have_hidden: false,
6709        }
6710    }
6711}
6712
6713/// Which trunk layers the draft reads. Upstream names them explicitly
6714/// (`dspark_target_layer_ids`); the file says the same thing less directly —
6715/// `main_proj` has one `dim`-wide input block per captured layer — and the
6716/// release captures the last three. Deriving it from the weight keeps the
6717/// two from disagreeing.
6718pub fn dspark_targets(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg, n_layers: usize) -> Vec<usize> {
6719    let Some(mp) = mtp.iter().find_map(|m| m.main_proj.as_ref()) else {
6720        return Vec::new();
6721    };
6722    let n = (mp.cols() / cfg.dim.max(1)).clamp(1, n_layers);
6723    (n_layers - n..n_layers).collect()
6724}
6725
6726thread_local! {
6727    /// The armed capture: which layers to take, and the buffer they fill.
6728    /// A thread-local rather than a parameter because the capture has to
6729    /// reach into the middle of a layer loop that eight call sites share,
6730    /// and threading an optional buffer through all of them to serve one
6731    /// diagnostic is a worse trade than this.
6732    static DSPARK_CAP: std::cell::RefCell<(Vec<usize>, Vec<f32>, usize)> =
6733        const { std::cell::RefCell::new((Vec::new(), Vec::new(), 0)) };
6734}
6735
6736/// Arm the capture for the layers `targets`, in order.
6737pub fn dspark_arm(targets: &[usize], dim: usize) {
6738    DSPARK_CAP.with(|c| {
6739        let mut c = c.borrow_mut();
6740        c.0 = targets.to_vec();
6741        c.1 = vec![0.0; dim * targets.len()];
6742        c.2 = 0;
6743    });
6744}
6745
6746/// Whether the armed MTP capture needs the state immediately after `li`.
6747/// The normal decode path keeps a full run in one submission; DSpark is the
6748/// only caller that needs an intermediate state to cross the device boundary.
6749fn dspark_wants(li: usize) -> bool {
6750    DSPARK_CAP.with(|c| c.borrow().0.contains(&li))
6751}
6752
6753/// Called after every host layer. Free when nothing is armed.
6754pub fn dspark_note(li: usize, state: &[f32], cfg: &Dsv4Cfg) {
6755    DSPARK_CAP.with(|c| {
6756        let mut c = c.borrow_mut();
6757        if c.0.is_empty() {
6758            return;
6759        }
6760        if let Some(slot) = c.0.iter().position(|&t| t == li) {
6761            let (_, buf, seen) = &mut *c;
6762            dspark_capture(state, cfg, slot, buf);
6763            // Counted, not "was the last one" — under the device chain only
6764            // the layers left on the host call this, and taking the last
6765            // target as the signal would hand the draft a buffer whose other
6766            // slots still hold the previous token, or nothing at all.
6767            *seen = if slot == 0 { 1 } else { *seen + 1 };
6768            if std::env::var("CMF_DSPARK_CAP_DBG").is_ok() {
6769                eprintln!("[cap] note li={li} slot={slot} seen={}", *seen);
6770            }
6771        }
6772    });
6773}
6774
6775/// Read one slot of the armed capture buffer as-is, complete or not. The
6776/// speculative verify fills the DEVICE targets from its own photographs and
6777/// only needs the host layers' slots from here — `dspark_take`'s
6778/// completeness contract would never be met on that path.
6779pub fn dspark_peek_slot(slot: usize, dim: usize, out: &mut [f32]) -> bool {
6780    DSPARK_CAP.with(|c| {
6781        let c = c.borrow();
6782        let lo = slot * dim;
6783        if c.1.len() < lo + dim {
6784            return false;
6785        }
6786        out[..dim].copy_from_slice(&c.1[lo..lo + dim]);
6787        true
6788    })
6789}
6790
6791/// Move the capture out, if this token produced a complete one.
6792pub fn dspark_take(out: &mut Vec<f32>) -> bool {
6793    DSPARK_CAP.with(|c| {
6794        let mut c = c.borrow_mut();
6795        if c.0.is_empty() || c.2 != c.0.len() {
6796            if std::env::var("CMF_DSPARK_CAP_DBG").is_ok() {
6797                eprintln!("[cap] take FAIL armed={:?} seen={}", c.0, c.2);
6798            }
6799            return false;
6800        }
6801        out.clear();
6802        out.extend_from_slice(&c.1);
6803        c.2 = 0;
6804        true
6805    })
6806}
6807
6808/// The trunk's contribution: the mean over the hyper-connection copies,
6809/// appended in target order. Costs one pass over `hc * dim` per captured
6810/// layer and nothing else.
6811pub fn dspark_capture(state: &[f32], cfg: &Dsv4Cfg, slot: usize, out: &mut [f32]) {
6812    let (hc, dim) = (cfg.hc_mult, cfg.dim);
6813    let dst = &mut out[slot * dim..(slot + 1) * dim];
6814    let inv = 1.0 / hc as f32;
6815    for d in 0..dim {
6816        let mut s = 0.0;
6817        for j in 0..hc {
6818            s += state[j * dim + d];
6819        }
6820        dst[d] = s * inv;
6821    }
6822}
6823
6824/// `CMF_DSPARK_PICK_DUMP=path` — accumulate the draft's expert picks per
6825/// stage and periodically rewrite `path` with `stage<TAB>expert<TAB>count`
6826/// lines. Rewritten every 32 blocks rather than at exit, so a run that is
6827/// killed still leaves the tallies on disk.
6828pub fn dspark_freq_note(picks: &[(usize, Vec<usize>)]) {
6829    static FREQ: std::sync::Mutex<Option<(std::collections::HashMap<(usize, usize), u64>, u64)>> =
6830        std::sync::Mutex::new(None);
6831    let Ok(path) = std::env::var("CMF_DSPARK_PICK_DUMP") else {
6832        return;
6833    };
6834    let mut g = FREQ.lock().unwrap();
6835    let (map, blocks) = g.get_or_insert_with(|| (std::collections::HashMap::new(), 0));
6836    for (stage, idx) in picks {
6837        for &e in idx {
6838            *map.entry((*stage, e)).or_insert(0) += 1;
6839        }
6840    }
6841    *blocks += 1;
6842    if *blocks % 32 == 0 {
6843        let mut lines: Vec<_> = map.iter().collect();
6844        lines.sort();
6845        let body: String = lines
6846            .iter()
6847            .map(|((s, e), n)| format!("{s}\t{e}\t{n}\n"))
6848            .collect();
6849        let _ = std::fs::write(&path, body);
6850    }
6851}
6852
6853/// `CMF_DSV4_TRUNK_PICK_DUMP=path` — the same tally for the TRUNK's layers:
6854/// `layer<TAB>expert<TAB>count`, rewritten every 32 tokens. The pick lists
6855/// come from the probe's own tally window, so only layers that route on the
6856/// host are counted — which is exactly the population a partial pack serves.
6857pub fn trunk_freq_note(picks: &[(usize, Vec<usize>)]) {
6858    static FREQ: std::sync::Mutex<Option<(std::collections::HashMap<(usize, usize), u64>, u64)>> =
6859        std::sync::Mutex::new(None);
6860    let Ok(path) = std::env::var("CMF_DSV4_TRUNK_PICK_DUMP") else {
6861        return;
6862    };
6863    let mut g = FREQ.lock().unwrap();
6864    let (map, blocks) = g.get_or_insert_with(|| (std::collections::HashMap::new(), 0));
6865    for (li, idx) in picks {
6866        for &e in idx {
6867            *map.entry((*li, e)).or_insert(0) += 1;
6868        }
6869    }
6870    *blocks += 1;
6871    if *blocks % 32 == 0 {
6872        let mut lines: Vec<_> = map.iter().collect();
6873        lines.sort();
6874        let body: String = lines
6875            .iter()
6876            .map(|((l, e), n)| format!("{l}\t{e}\t{n}\n"))
6877            .collect();
6878        let _ = std::fs::write(&path, body);
6879    }
6880}
6881
6882/// `CMF_DSPARK_MASK=path` — restrict the draft's routed experts to an
6883/// explicit per-stage keep-set: line `d` of the file lists the expert ids
6884/// stage `d` may route to, comma-separated. Weights renormalize over what
6885/// remains (the `Dsv4Layer::mask` contract). The draft only proposes — the
6886/// trunk still verifies every token — so a thinner draft costs acceptance,
6887/// never correctness. This is the offline dial for sizing a resident
6888/// device pack before one exists.
6889fn dspark_apply_mask(out: &mut [Dsv4Mtp]) {
6890    let Ok(path) = std::env::var("CMF_DSPARK_MASK") else {
6891        return;
6892    };
6893    let Ok(text) = std::fs::read_to_string(&path) else {
6894        eprintln!("DSpark: CMF_DSPARK_MASK={path} не читается — маска не применена");
6895        return;
6896    };
6897    for (d, line) in text.lines().enumerate() {
6898        let Some(m) = out.get_mut(d) else { break };
6899        let n = m.layer.experts.len();
6900        let mut mask = vec![false; n];
6901        let mut kept = 0usize;
6902        for tok in line.split(',') {
6903            if let Ok(e) = tok.trim().parse::<usize>() {
6904                if e < n && !mask[e] {
6905                    mask[e] = true;
6906                    kept += 1;
6907                }
6908            }
6909        }
6910        if kept == 0 {
6911            continue;
6912        }
6913        eprintln!("DSpark: стадия {d} ограничена {kept}/{n} экспертами");
6914        m.layer.mask = Some(mask);
6915    }
6916}
6917
6918/// The draft's device residency: which experts of each stage live on the
6919/// card, and how the device router reaches them.
6920///
6921/// The draft only proposes — the trunk verifies every token — so the pack
6922/// is free to keep a SUBSET of each stage's experts and mask the routing to
6923/// it: acceptance pays, correctness never does. The subset is chosen by
6924/// measured routing frequency (`CMF_DSPARK_PACK` names the tally file that
6925/// `CMF_DSPARK_PICK_DUMP` wrote; `CMF_DSPARK_RESIDENT` caps experts per
6926/// stage, default 48).
6927#[cfg(feature = "gpu")]
6928pub struct DsparkPack {
6929    pub stages: Vec<DsparkStagePack>,
6930    /// Gate/up requantized to q2tp at upload (the binary registered an
6931    /// encoder); the graph then dispatches the q2tp kernels.
6932    pub gu_q2: bool,
6933    /// The down planes too (native in the file, never requantized at
6934    /// upload); the graph dispatches the 2-bit down kernel.
6935    pub dn_q2: bool,
6936    /// Dequantized router and bias per stage, f32 — address-stable for the
6937    /// life of the pack, which is what the device's const cache needs.
6938    pub routers: Vec<Vec<f32>>,
6939    pub biases: Vec<Option<Vec<f32>>>,
6940}
6941
6942#[cfg(feature = "gpu")]
6943pub struct DsparkStagePack {
6944    /// Selectable experts (true = resident).
6945    pub mask: Vec<bool>,
6946    /// Global expert id → pack slot; usize::MAX where cold.
6947    pub to_slot: Vec<usize>,
6948    /// The same two as the device consumes them — u32, address-stable for
6949    /// the pack's lifetime (the const cache keys on the pointer).
6950    pub mask_u32: Vec<u32>,
6951    pub map_u32: Vec<u32>,
6952    /// (gate, up, down) directory indices, pack order, shared LAST.
6953    pub tensors: Vec<(usize, usize, usize)>,
6954    pub n_resident: usize,
6955}
6956
6957/// The q2tp encoder, registered by the binary that has one (the CLI's
6958/// converter owns the rung-search implementation and the engine must not
6959/// depend on the CLI). When present, the draft's gate/up experts are
6960/// requantized q4tp → q2tp AT UPLOAD — half the VRAM and the same kernels
6961/// the trunk's q2tp experts already use. Draft-only fidelity: acceptance
6962/// pays, correctness never does.
6963pub static DSPARK_Q2TP_ENCODE: std::sync::OnceLock<fn(&[f32], usize, usize) -> Vec<u8>> =
6964    std::sync::OnceLock::new();
6965
6966/// `CMF_DSPARK_GPU=1` — the probe (and later the speculative loop) drafts
6967/// on the card instead of the CPU/disk tier.
6968#[cfg(feature = "gpu")]
6969pub fn dspark_gpu_on() -> bool {
6970    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6971    *ON.get_or_init(|| std::env::var("CMF_DSPARK_GPU").map(|v| v != "0").unwrap_or(true))
6972}
6973
6974/// The pack, built once per process (the stand runs one model).
6975#[cfg(feature = "gpu")]
6976pub fn dspark_pack_get(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg) -> Option<&'static DsparkPack> {
6977    static P: std::sync::OnceLock<Option<Box<DsparkPack>>> = std::sync::OnceLock::new();
6978    P.get_or_init(|| dspark_pack_build(mtp, cfg).map(Box::new)).as_deref()
6979}
6980
6981/// Build and upload the draft's pack. Returns `None` when the stack is
6982/// absent, the budget refuses, or a stage's weights are not where the
6983/// device path needs them — the caller falls back to the CPU draft.
6984/// Reserve the VRAM the speculative draft's device pack will take, so the
6985/// trunk's greedy packing leaves it room. Called at load, before any trunk
6986/// pack is built; a no-op when there is no MTP stack or speculation is off.
6987/// The estimate uses the draft's native dtypes — an upload-time re-encode
6988/// only shrinks it, which errs on the safe side of the physical ceiling.
6989///
6990/// A budget that cannot pack the trunk to the draft's capture layers (the
6991/// last three) gets NO reservation: speculation will decline there anyway,
6992/// and the carve-out would only shrink the walk's packs — measured 13% of
6993/// decode on a 64 GB budget. The threshold is geometric (nine tenths of
6994/// the trunk's own expert bytes plus the draft), never a card name.
6995#[cfg(feature = "gpu")]
6996pub fn dspark_reserve_note(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg, layers: &[Dsv4Layer]) {
6997    if mtp.is_empty()
6998        || std::env::var("CMF_DSV4_SPEC").is_ok_and(|v| v == "0")
6999        || !dspark_gpu_on()
7000    {
7001        return;
7002    }
7003    let dt = |q2: bool| {
7004        if q2 {
7005            cortiq_core::TensorDtype::Q2TiledP
7006        } else {
7007            cortiq_core::TensorDtype::Q4TiledP
7008        }
7009    };
7010    let gu_q2 = mtp[0].layer.experts.first().is_some_and(|e| {
7011        e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
7012    });
7013    let dn_q2 = mtp[0].layer.experts.first().is_some_and(|e| {
7014        e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
7015    });
7016    let gu = cortiq_core::quant::expected_nbytes(dt(gu_q2), &[cfg.moe_inter, cfg.dim])
7017        .unwrap_or(0);
7018    let dn = cortiq_core::quant::expected_nbytes(dt(dn_q2), &[cfg.dim, cfg.moe_inter])
7019        .unwrap_or(0);
7020    let per = (2 * gu + dn) as u64;
7021    let n_res: usize = std::env::var("CMF_DSPARK_RESIDENT")
7022        .ok()
7023        .and_then(|v| v.parse().ok())
7024        // The default matches the measured acceptance plateau's low edge:
7025        // residency below it costs acceptance, above it only costs VRAM.
7026        .unwrap_or(40);
7027    // Routed residents per stage, plus each stage's shared expert.
7028    let bytes = per * (n_res * mtp.len() + mtp.len() + 1) as u64;
7029    // The trunk's own expert bytes, from the layers as they are.
7030    let trunk: u64 = layers
7031        .iter()
7032        .map(|l| {
7033            let Some(e) = l.experts.first() else { return 0 };
7034            let gu = cortiq_core::quant::expected_nbytes(
7035                dt(e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
7036                &[cfg.moe_inter, cfg.dim],
7037            )
7038            .unwrap_or(0);
7039            let dn = cortiq_core::quant::expected_nbytes(
7040                dt(e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
7041                &[cfg.dim, cfg.moe_inter],
7042            )
7043            .unwrap_or(0);
7044            ((2 * gu + dn) * (l.experts.len() + 1)) as u64
7045        })
7046        .sum();
7047    if let Some(budget) = crate::gpu_wgpu::dsv4_vram_budget() {
7048        if budget < trunk / 10 * 9 + bytes {
7049            return;
7050        }
7051    }
7052    crate::gpu_wgpu::DRAFT_RESERVE.store(bytes, std::sync::atomic::Ordering::Relaxed);
7053}
7054
7055#[cfg(not(feature = "gpu"))]
7056pub fn dspark_reserve_note(_mtp: &[Dsv4Mtp], _cfg: &Dsv4Cfg, _layers: &[Dsv4Layer]) {}
7057
7058#[cfg(feature = "gpu")]
7059pub fn dspark_pack_build(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg) -> Option<DsparkPack> {
7060    if mtp.is_empty() {
7061        return None;
7062    }
7063    let n_res: usize = std::env::var("CMF_DSPARK_RESIDENT")
7064        .ok()
7065        .and_then(|v| v.parse().ok())
7066        .unwrap_or_else(|| {
7067            // No knob: take what the card actually has left, whatever the
7068            // card is. The stages split the fit evenly after their shared
7069            // experts; the clamp keeps the band where drafting is known to
7070            // be worth the VRAM at the low end and past diminishing
7071            // returns at the high one.
7072            let native_q2 = mtp[0].layer.experts.first().is_some_and(|e| {
7073                e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
7074            });
7075            let gu_q2 = native_q2 || DSPARK_Q2TP_ENCODE.get().is_some();
7076            let dn_q2 = mtp[0].layer.experts.first().is_some_and(|e| {
7077                e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
7078            });
7079            let room = crate::gpu_wgpu::dsv4_draft_fit(cfg.moe_inter, cfg.dim, gu_q2, dn_q2);
7080            (room.saturating_sub(mtp.len() + 1) / mtp.len().max(1)).clamp(8, 64)
7081        });
7082    // Frequency tallies: lines of `stage<TAB>expert<TAB>count`. Named by
7083    // `CMF_DSPARK_PACK`, or found as `<model>.dspark.tsv` beside the model
7084    // file — ship the tally next to the checkpoint and no knob is needed.
7085    let mut freq: Vec<Vec<(u64, usize)>> = vec![Vec::new(); mtp.len()];
7086    let pack_path = std::env::var("CMF_DSPARK_PACK").ok().or_else(|| {
7087        let m = mtp[0].layer.experts.first()?.w1.model_arc()?;
7088        let mut s = m.path.as_os_str().to_os_string();
7089        s.push(".dspark.tsv");
7090        let p = std::path::PathBuf::from(s);
7091        p.exists().then(|| p.to_string_lossy().into_owned())
7092    });
7093    if let Some(path) = pack_path {
7094        if let Ok(text) = std::fs::read_to_string(&path) {
7095            for line in text.lines() {
7096                let mut it = line.split_whitespace();
7097                if let (Some(s), Some(e), Some(n)) = (it.next(), it.next(), it.next()) {
7098                    if let (Ok(s), Ok(e), Ok(n)) =
7099                        (s.parse::<usize>(), e.parse::<usize>(), n.parse::<u64>())
7100                    {
7101                        if s < freq.len() {
7102                            freq[s].push((n, e));
7103                        }
7104                    }
7105                }
7106            }
7107        }
7108    }
7109    let mut stages = Vec::with_capacity(mtp.len());
7110    let mut routers = Vec::with_capacity(mtp.len());
7111    let mut biases = Vec::with_capacity(mtp.len());
7112    for (si, m) in mtp.iter().enumerate() {
7113        let l = &m.layer;
7114        let n = l.experts.len();
7115        // Frequency order, then the untallied ids — a cold start still
7116        // packs SOMETHING deterministic.
7117        let mut order: Vec<usize> = {
7118            let mut f = freq[si].clone();
7119            f.sort_by(|a, b| b.0.cmp(&a.0));
7120            let mut seen = vec![false; n];
7121            let mut o: Vec<usize> = f
7122                .into_iter()
7123                .map(|(_, e)| e)
7124                .filter(|&e| {
7125                    if e < n && !seen[e] {
7126                        seen[e] = true;
7127                        true
7128                    } else {
7129                        false
7130                    }
7131                })
7132                .collect();
7133            o.extend((0..n).filter(|&e| !seen[e]));
7134            o
7135        };
7136        order.truncate(n_res.min(n));
7137        let mut mask = vec![false; n];
7138        let mut to_slot = vec![usize::MAX; n];
7139        let mut tensors = Vec::with_capacity(order.len() + 1);
7140        for (slot, &e) in order.iter().enumerate() {
7141            let ex = &l.experts[e];
7142            let (Some(w1), Some(w3), Some(w2)) =
7143                (ex.w1.model_idx(), ex.w3.model_idx(), ex.w2.model_idx())
7144            else {
7145                return None;
7146            };
7147            mask[e] = true;
7148            to_slot[e] = slot;
7149            tensors.push((w1, w3, w2));
7150        }
7151        let (Some(s1), Some(s3), Some(s2)) = (
7152            l.shared.w1.model_idx(),
7153            l.shared.w3.model_idx(),
7154            l.shared.w2.model_idx(),
7155        ) else {
7156            return None;
7157        };
7158        tensors.push((s1, s3, s2));
7159        // The router and bias, dequantized once.
7160        let mut router = vec![0.0f32; n * cfg.dim];
7161        for (r, row) in (0..n).zip(router.chunks_mut(cfg.dim)) {
7162            l.gate.row_f32(r, row);
7163        }
7164        routers.push(router);
7165        biases.push(l.gate_bias.clone());
7166        let mask_u32: Vec<u32> = mask.iter().map(|&m| m as u32).collect();
7167        let map_u32: Vec<u32> = to_slot
7168            .iter()
7169            .map(|&x| if x == usize::MAX { u32::MAX } else { x as u32 })
7170            .collect();
7171        stages.push(DsparkStagePack {
7172            mask,
7173            to_slot,
7174            mask_u32,
7175            map_u32,
7176            tensors,
7177            n_resident: order.len(),
7178        });
7179    }
7180    // ── upload: the small skeleton FIRST, the expert stacks after — the
7181    //    documented admission order (experts fill the card and the skeleton
7182    //    then misses). ──
7183    let model = mtp[0].layer.experts.first().and_then(|e| e.w1.model_arc())?;
7184    let mut skeleton = Vec::new();
7185    for m in mtp {
7186        let l = &m.layer;
7187        for t in [&l.wq_a, &l.wq_b, &l.wkv, &l.wo_a, &l.wo_b] {
7188            skeleton.push(t.model_idx()?);
7189        }
7190    }
7191    if let Some(mp) = mtp[0].main_proj.as_ref() {
7192        skeleton.push(mp.model_idx()?);
7193    }
7194    for &idx in &skeleton {
7195        if !crate::gpu_wgpu::dsv4_weight_ready(&model, idx) {
7196            eprintln!("DSpark: скелет драфта не влез в VRAM — GPU-черновик выключен");
7197            return None;
7198        }
7199    }
7200    // The dtype in the FILE decides: a properly converted CMF stores the
7201    // draft's gate/up as q2tp and uploads through the same path as the
7202    // trunk's 2-bit experts. The at-upload requant is only the fallback for
7203    // files published before the converter's q2tp profile covered the MTP
7204    // stack (and only when the binary registered an encoder).
7205    let native_q2 = mtp[0].layer.experts.first().is_some_and(|e| {
7206        e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
7207    });
7208    let gu_q2 = native_q2 || crate::dsv4::DSPARK_Q2TP_ENCODE.get().is_some();
7209    let dn_native = mtp[0].layer.experts.first().is_some_and(|e| {
7210        e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
7211    });
7212    for (si, sp) in stages.iter().enumerate() {
7213        let ok = if native_q2 {
7214            crate::gpu_wgpu::dsv4_experts_ready(
7215                &model, &sp.tensors, cfg.moe_inter, cfg.dim, true, dn_native,
7216            )
7217        } else if gu_q2 {
7218            crate::gpu_wgpu::moe_expert_bufs_requant_gu(
7219                &model,
7220                &sp.tensors,
7221                cfg.moe_inter,
7222                cfg.dim,
7223            )
7224            .is_some()
7225        } else {
7226            crate::gpu_wgpu::dsv4_experts_ready(
7227                &model, &sp.tensors, cfg.moe_inter, cfg.dim, false, false,
7228            )
7229        };
7230        if !ok {
7231            eprintln!(
7232                "DSpark: эксперты стадии {si} ({} + shared) не влезли в VRAM — GPU-черновик выключен",
7233                sp.n_resident
7234            );
7235            return None;
7236        }
7237    }
7238    let _ = crate::gpu_wgpu::pin_weights(&model, &skeleton);
7239    eprintln!(
7240        "DSpark: пак драфта на карте — {} стадии по {} экспертов + shared",
7241        stages.len(),
7242        stages.iter().map(|s| s.n_resident.to_string()).collect::<Vec<_>>().join("/")
7243    );
7244    Some(DsparkPack {
7245        stages,
7246        gu_q2,
7247        dn_q2: dn_native,
7248        routers,
7249        biases,
7250    })
7251}
7252
7253/// Append one real position's entry to every stage's KV ring, from the
7254/// trunk captures in `ds.main_hidden`. The draft does this for the position
7255/// it drafts at; a speculative decode also owes an entry for every accepted
7256/// position it never drafted from — a hole in the ring silently starves
7257/// later blocks of context, which reads as "acceptance decayed" and not as
7258/// a bug.
7259pub fn dspark_ring_append(
7260    g: &Dsv4Globals,
7261    mtp: &[Dsv4Mtp],
7262    cfg: &Dsv4Cfg,
7263    ds: &mut DsparkState,
7264    pos: usize,
7265    pool: Option<&crate::pool::Pool>,
7266) {
7267    let (dim, hd, rd) = (cfg.dim, cfg.head_dim, cfg.rope_head_dim);
7268    let inv_freq = &g.inv_freq_window;
7269    let Some(stage0) = mtp.first() else { return };
7270    let (Some(mp), Some(mn)) = (stage0.main_proj.as_ref(), stage0.main_norm.as_ref()) else {
7271        return;
7272    };
7273    let mut main_x = vec![0.0f32; dim];
7274    mp.matvec(&ds.main_hidden, &mut main_x, pool);
7275    rms_weighted(&mut main_x, mn, cfg.norm_eps);
7276    for (si, m) in mtp.iter().enumerate() {
7277        let kvw = m.layer.wkv.rows();
7278        if ds.win[si].len() < cfg.window * kvw {
7279            ds.win[si].resize(cfg.window * kvw, 0.0);
7280        }
7281        let mut kv = vec![0.0f32; kvw];
7282        m.layer.wkv.matvec(&main_x, &mut kv, pool);
7283        rms_weighted(&mut kv, &m.layer.kv_norm, cfg.norm_eps);
7284        rope_tail(&mut kv[kvw - hd..], inv_freq, pos, rd, false);
7285        let slot = pos % cfg.window;
7286        ds.win[si][slot * kvw..(slot + 1) * kvw].copy_from_slice(&kv);
7287        ds.filled[si] = (pos + 1).min(cfg.window);
7288    }
7289}
7290
7291/// The draft block on the card: one submission for all three stages and
7292/// five positions, states home in one fence, the head on the host. The
7293/// markov bias is skipped (its per-position chain through the previous
7294/// PROPOSAL is the one part a single graph cannot batch) — compare against
7295/// the CPU draft under `CMF_DSPARK_NO_MARKOV=1`.
7296#[cfg(feature = "gpu")]
7297#[allow(clippy::too_many_arguments)]
7298pub fn dspark_draft_gpu(
7299    g: &Dsv4Globals,
7300    mtp: &[Dsv4Mtp],
7301    cfg: &Dsv4Cfg,
7302    ds: &mut DsparkState,
7303    pack: &DsparkPack,
7304    kv_id: u64,
7305    last_token: u32,
7306    pos: usize,
7307    pool: Option<&crate::pool::Pool>,
7308    out_conf: &mut Vec<f32>,
7309) -> Vec<u32> {
7310    let (hc, dim) = (cfg.hc_mult, cfg.dim);
7311    let block = dspark_block();
7312    let Some(model) = mtp[0].layer.experts.first().and_then(|e| e.w1.model_arc()) else {
7313        return Vec::new();
7314    };
7315    let (Some(mp), Some(mn)) = (mtp[0].main_proj.as_ref(), mtp[0].main_norm.as_ref()) else {
7316        return Vec::new();
7317    };
7318    let Some(mp_idx) = mp.model_idx() else {
7319        return Vec::new();
7320    };
7321    let mut stages = Vec::with_capacity(mtp.len());
7322    for (si, m) in mtp.iter().enumerate() {
7323        let l = &m.layer;
7324        let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b), Some(wkv)) = (
7325            l.wq_a.model_idx(),
7326            l.wq_b.model_idx(),
7327            l.wo_a.model_idx(),
7328            l.wo_b.model_idx(),
7329            l.wkv.model_idx(),
7330        ) else {
7331            return Vec::new();
7332        };
7333        let sp = &pack.stages[si];
7334        stages.push(crate::gpu_wgpu::DsparkStageW {
7335            wq_a,
7336            wq_b,
7337            wo_a,
7338            wo_b,
7339            wkv,
7340            q_norm: &l.q_norm,
7341            kv_norm: &l.kv_norm,
7342            attn_norm: &l.attn_norm,
7343            ffn_norm: &l.ffn_norm,
7344            sink: &l.attn_sink,
7345            hc_attn_fn: &l.hc_attn_fn,
7346            hc_attn_scale: &l.hc_attn_scale,
7347            hc_attn_base: &l.hc_attn_base,
7348            hc_ffn_fn: &l.hc_ffn_fn,
7349            hc_ffn_scale: &l.hc_ffn_scale,
7350            hc_ffn_base: &l.hc_ffn_base,
7351            router: &pack.routers[si],
7352            bias: pack.biases[si].as_deref(),
7353            experts: &sp.tensors,
7354            mask_u32: &sp.mask_u32,
7355            map_u32: &sp.map_u32,
7356        });
7357    }
7358    let geom = crate::gpu_wgpu::DsparkGeom {
7359        dim,
7360        hc,
7361        nh: cfg.n_heads,
7362        hd: cfg.head_dim,
7363        rd: cfg.rope_head_dim,
7364        q_lora: cfg.q_lora_rank,
7365        o_lora: cfg.o_lora_rank,
7366        o_groups: cfg.o_groups,
7367        inter: cfg.moe_inter,
7368        n_experts: cfg.n_routed_experts,
7369        top_k: cfg.top_k,
7370        window: cfg.window,
7371        eps: cfg.norm_eps,
7372        hc_eps: cfg.hc_eps,
7373        sinkhorn_iters: cfg.hc_sinkhorn_iters,
7374        route_scale: cfg.route_scale,
7375        swiglu_limit: cfg.swiglu_limit,
7376        scale: (cfg.head_dim as f32).powf(-0.5),
7377        gu_q2: pack.gu_q2,
7378        dn_q2: pack.dn_q2,
7379    };
7380    // ── seed states: the real token, then noise, replicated over copies ──
7381    let ids: Vec<u32> = (0..block)
7382        .map(|i| if i == 0 { last_token } else { DSPARK_NOISE_TOKEN })
7383        .collect();
7384    let mut states0 = vec![0.0f32; block * hc * dim];
7385    let mut emb = vec![0.0f32; dim];
7386    for (i, &id) in ids.iter().enumerate() {
7387        g.embed.row_f32(id as usize, &mut emb);
7388        for j in 0..hc {
7389            states0[(i * hc + j) * dim..(i * hc + j + 1) * dim].copy_from_slice(&emb);
7390        }
7391    }
7392    let dspark_time = {
7393        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7394        *ON.get_or_init(|| std::env::var("CMF_DSPARK_TIME").is_ok_and(|v| v != "0"))
7395    };
7396    let t0 = std::time::Instant::now();
7397    let filled = (pos + 1).min(cfg.window);
7398    let mut states = vec![0.0f32; block * hc * dim];
7399    if !crate::gpu_wgpu::dspark_graph(
7400        &model,
7401        &stages,
7402        geom,
7403        kv_id,
7404        mp_idx,
7405        mn,
7406        &ds.main_hidden,
7407        &states0,
7408        pos,
7409        filled,
7410        &g.inv_freq_window,
7411        block,
7412        &mut states,
7413    ) {
7414        return Vec::new();
7415    }
7416    for si in 0..mtp.len() {
7417        ds.filled[si] = filled;
7418    }
7419    let t_graph = t0.elapsed();
7420
7421    // ── head, on the host: fold, norm, one B-wide matmat, argmax ──
7422    let last = &mtp[mtp.len() - 1];
7423    let (Some(hfn), Some(hbase), Some(hscale), Some(hnorm)) = (
7424        last.hc_head_fn.as_ref(),
7425        last.hc_head_base.as_ref(),
7426        last.hc_head_scale,
7427        last.norm.as_ref(),
7428    ) else {
7429        return Vec::new();
7430    };
7431    let mut head_in = vec![0.0f32; block * dim];
7432    let mut pre_norms = vec![vec![0.0f32; dim]; block];
7433    for i in 0..block {
7434        hc_head_fold(
7435            &states[i * hc * dim..(i + 1) * hc * dim],
7436            hfn,
7437            hscale,
7438            hbase,
7439            cfg,
7440            pool,
7441            &mut head_in[i * dim..(i + 1) * dim],
7442        );
7443        pre_norms[i].copy_from_slice(&head_in[i * dim..(i + 1) * dim]);
7444        rms_weighted(&mut head_in[i * dim..(i + 1) * dim], hnorm, cfg.norm_eps);
7445    }
7446    let t_fold = t0.elapsed();
7447    let mut logits = vec![0.0f32; block * cfg.vocab];
7448    // The B-axis q4tp kernel, one submission: `matmat` at B=5 falls to the
7449    // CPU tile path and measured 46 ms of a 60 ms draft.
7450    let head_gpu = g.head.model_idx().is_some_and(|hi| {
7451        crate::gpu_wgpu::q4tp_matvec_batch_for_test(
7452            &model,
7453            hi,
7454            &head_in,
7455            block,
7456            cfg.vocab,
7457            dim,
7458            &mut logits,
7459        )
7460    });
7461    if !head_gpu {
7462        g.head.matmat(&head_in, block, &mut logits, pool);
7463    }
7464    let t_head = t0.elapsed();
7465    // The markov bigram is not optional: without it acceptance fell 1.02 →
7466    // 0.42 on natural text. Its chain runs through the previous PROPOSAL,
7467    // so it stays position-by-position; the w2 matvec is big enough that
7468    // the QTensor route puts it on the card by itself.
7469    let mut proposals = Vec::with_capacity(block);
7470    out_conf.clear();
7471    let mut prev = last_token;
7472    let mut mk_embed = vec![0.0f32; last.markov_w1.as_ref().map_or(0, |t| t.cols())];
7473    let mut bias = vec![0.0f32; cfg.vocab];
7474    for i in 0..block {
7475        let row = &mut logits[i * cfg.vocab..(i + 1) * cfg.vocab];
7476        if let (Some(w1), Some(w2)) = (last.markov_w1.as_ref(), last.markov_w2.as_ref()) {
7477            w1.row_f32(prev as usize, &mut mk_embed);
7478            w2.matvec(&mk_embed, &mut bias, pool);
7479            for (a, b) in row.iter_mut().zip(&bias) {
7480                *a += *b;
7481            }
7482        }
7483        let mut best = 0usize;
7484        for v in 1..row.len() {
7485            if row[v] > row[best] {
7486                best = v;
7487            }
7488        }
7489        if let Some(cf) = last.confidence.as_ref() {
7490            let mut cat = pre_norms[i].clone();
7491            cat.extend_from_slice(&mk_embed);
7492            let mut sc = [0.0f32; 1];
7493            if cat.len() == cf.cols() {
7494                cf.matvec(&cat, &mut sc, pool);
7495            }
7496            out_conf.push(sc[0]);
7497        }
7498        proposals.push(best as u32);
7499        prev = best as u32;
7500    }
7501    if dspark_time {
7502        eprintln!(
7503            "DSpark GPU: граф {:.1} мс, фолды {:.1}, голова {:.1}, марков+argmax {:.1}",
7504            t_graph.as_secs_f64() * 1e3,
7505            (t_fold - t_graph).as_secs_f64() * 1e3,
7506            (t_head - t_fold).as_secs_f64() * 1e3,
7507            (t0.elapsed() - t_head).as_secs_f64() * 1e3,
7508        );
7509    }
7510    proposals
7511}
7512
7513/// One draft: `DSPARK_BLOCK` proposed tokens and a confidence per position.
7514///
7515/// `pos` is the position of `last_token` — the block predicts `pos+1 ..
7516/// pos+BLOCK`. Returns the proposals in order; `out_conf` takes the
7517/// confidence head's score where the last stage carries one.
7518#[allow(clippy::too_many_arguments)]
7519pub fn dspark_draft(
7520    g: &Dsv4Globals,
7521    mtp: &[Dsv4Mtp],
7522    cfg: &Dsv4Cfg,
7523    ds: &mut DsparkState,
7524    last_token: u32,
7525    pos: usize,
7526    pool: Option<&crate::pool::Pool>,
7527    out_conf: &mut Vec<f32>,
7528) -> Vec<u32> {
7529    let (hc, dim, hd, rd) = (cfg.hc_mult, cfg.dim, cfg.head_dim, cfg.rope_head_dim);
7530    let block = dspark_block();
7531    let inv_freq = &g.inv_freq_window;
7532
7533    // ── the block's input: main_norm(main_proj(captured hiddens)) ──
7534    let Some(stage0) = mtp.first() else {
7535        return Vec::new();
7536    };
7537    let (Some(mp), Some(mn)) = (stage0.main_proj.as_ref(), stage0.main_norm.as_ref()) else {
7538        return Vec::new();
7539    };
7540    dspark_ring_append(g, mtp, cfg, ds, pos, pool);
7541
7542    // ── the block: the real token, then noise ──
7543    let ids: Vec<u32> = (0..block)
7544        .map(|i| if i == 0 { last_token } else { DSPARK_NOISE_TOKEN })
7545        .collect();
7546    let mut states = vec![vec![0.0f32; hc * dim]; block];
7547    let mut emb = vec![0.0f32; dim];
7548    for (i, &id) in ids.iter().enumerate() {
7549        g.embed.row_f32(id as usize, &mut emb);
7550        for j in 0..hc {
7551            states[i][j * dim..(j + 1) * dim].copy_from_slice(&emb);
7552        }
7553    }
7554
7555    let mut scratch = HcScratch::new(cfg);
7556    for (si, m) in mtp.iter().enumerate() {
7557        let l = &m.layer;
7558        let kvw = l.wkv.rows();
7559        // ── attention half: fold every position first, because each one's
7560        //    keys are visible to all the others. ──
7561        let mut post = vec![vec![0.0f32; hc]; block];
7562        let mut comb = vec![vec![0.0f32; hc * hc]; block];
7563        let mut resid = vec![vec![0.0f32; hc * dim]; block];
7564        let mut folded = vec![vec![0.0f32; dim]; block];
7565        let mix_hc = (2 + hc) * hc;
7566        for i in 0..block {
7567            hc_mixes(
7568                &states[i],
7569                &l.hc_attn_fn,
7570                mix_hc,
7571                cfg.norm_eps,
7572                pool,
7573                &mut scratch.mixes,
7574            );
7575            hc_split_sinkhorn(
7576                &scratch.mixes,
7577                &l.hc_attn_scale,
7578                &l.hc_attn_base,
7579                hc,
7580                cfg.hc_sinkhorn_iters,
7581                cfg.hc_eps,
7582                &mut scratch.pre,
7583                &mut post[i],
7584                &mut comb[i],
7585            );
7586            hc_fold(&states[i], &scratch.pre, hc, dim, &mut folded[i]);
7587            rms_weighted(&mut folded[i], &l.attn_norm, cfg.norm_eps);
7588            resid[i].copy_from_slice(&states[i]);
7589        }
7590        // Keys and values of the block itself — kept for this block only.
7591        let folded_all: Vec<f32> = folded.iter().flatten().copied().collect();
7592        let mut blk_kv = vec![0.0f32; block * kvw];
7593        l.wkv.matmat(&folded_all, block, &mut blk_kv, pool);
7594        for i in 0..block {
7595            let dst = &mut blk_kv[i * kvw..(i + 1) * kvw];
7596            rms_weighted(dst, &l.kv_norm, cfg.norm_eps);
7597            rope_tail(&mut dst[kvw - hd..], inv_freq, pos + 1 + i, rd, false);
7598        }
7599        // The attended set: every cached real position, then the whole block.
7600        let win_len = ds.filled[si];
7601        let mut cache = Vec::with_capacity((win_len + block) * hd);
7602        for p in 0..win_len {
7603            let e = &ds.win[si][p * kvw..(p + 1) * kvw];
7604            cache.extend_from_slice(&e[kvw - hd..]);
7605        }
7606        for i in 0..block {
7607            let e = &blk_kv[i * kvw..(i + 1) * kvw];
7608            cache.extend_from_slice(&e[kvw - hd..]);
7609        }
7610        let idxs: Vec<usize> = (0..win_len + block).collect();
7611        let scale = (hd as f32).powf(-0.5);
7612        let qrank = l.wq_a.rows();
7613        let qdim = cfg.n_heads * hd;
7614        let mut qr = vec![0.0f32; block * qrank];
7615        l.wq_a.matmat(&folded_all, block, &mut qr, pool);
7616        for i in 0..block {
7617            rms_weighted(
7618                &mut qr[i * qrank..(i + 1) * qrank],
7619                &l.q_norm,
7620                cfg.norm_eps,
7621            );
7622        }
7623        let mut q = vec![0.0f32; block * qdim];
7624        l.wq_b.matmat(&qr, block, &mut q, pool);
7625        let mut attn = vec![0.0f32; block * qdim];
7626        for i in 0..block {
7627            let qi = &mut q[i * qdim..(i + 1) * qdim];
7628            let ai = &mut attn[i * qdim..(i + 1) * qdim];
7629            let qpos = pos + 1 + i;
7630            for h in 0..cfg.n_heads {
7631                let head = &mut qi[h * hd..(h + 1) * hd];
7632                rms_inplace(head, cfg.norm_eps);
7633                rope_tail(head, inv_freq, qpos, rd, false);
7634            }
7635            for h in 0..cfg.n_heads {
7636                let qh = &qi[h * hd..(h + 1) * hd];
7637                let oh = &mut ai[h * hd..(h + 1) * hd];
7638                sparse_attend(qh, &cache, &idxs, l.attn_sink[h], scale, hd, oh);
7639                rope_tail(oh, inv_freq, qpos, rd, true);
7640            }
7641        }
7642        let mut blk_out = vec![0.0f32; block * dim];
7643        o_project_block(
7644            &attn, block, &l.wo_a, &l.wo_b, cfg.o_groups, cfg.o_lora_rank, pool,
7645            &mut blk_out,
7646        );
7647        for i in 0..block {
7648            let mut next = vec![0.0f32; hc * dim];
7649            hc_expand(
7650                &blk_out[i * dim..(i + 1) * dim],
7651                &resid[i], &post[i], &comb[i], hc, dim, &mut next,
7652            );
7653            states[i] = next;
7654        }
7655        // ── MoE: fold every position, group equal experts, then expand in
7656        //    the original per-position route order. ──
7657        let mut ffn_fold = vec![0.0f32; block * dim];
7658        let mut ffn_post = vec![vec![0.0f32; hc]; block];
7659        let mut ffn_comb = vec![vec![0.0f32; hc * hc]; block];
7660        let mut ffn_resid = vec![vec![0.0f32; hc * dim]; block];
7661        for i in 0..block {
7662            hc_mixes(
7663                &states[i],
7664                &l.hc_ffn_fn,
7665                mix_hc,
7666                cfg.norm_eps,
7667                pool,
7668                &mut scratch.mixes,
7669            );
7670            hc_split_sinkhorn(
7671                &scratch.mixes,
7672                &l.hc_ffn_scale,
7673                &l.hc_ffn_base,
7674                hc,
7675                cfg.hc_sinkhorn_iters,
7676                cfg.hc_eps,
7677                &mut scratch.pre,
7678                &mut ffn_post[i],
7679                &mut ffn_comb[i],
7680            );
7681            hc_fold(
7682                &states[i], &scratch.pre, hc, dim,
7683                &mut ffn_fold[i * dim..(i + 1) * dim],
7684            );
7685            rms_weighted(
7686                &mut ffn_fold[i * dim..(i + 1) * dim],
7687                &l.ffn_norm,
7688                cfg.norm_eps,
7689            );
7690            ffn_resid[i].copy_from_slice(&states[i]);
7691        }
7692        let mut moe_out = vec![0.0f32; block * dim];
7693        moe_step_block(
7694            &ffn_fold, block, l, cfg, &ids, si, pool, &mut moe_out,
7695        );
7696        for i in 0..block {
7697            let mut next = vec![0.0f32; hc * dim];
7698            hc_expand(
7699                &moe_out[i * dim..(i + 1) * dim],
7700                &ffn_resid[i], &ffn_post[i], &ffn_comb[i], hc, dim, &mut next,
7701            );
7702            states[i] = next;
7703        }
7704    }
7705
7706    // ── head: the last stage's fold, the trunk's own head ──
7707    let last = &mtp[mtp.len() - 1];
7708    let (Some(hfn), Some(hbase), Some(hscale), Some(hnorm)) = (
7709        last.hc_head_fn.as_ref(),
7710        last.hc_head_base.as_ref(),
7711        last.hc_head_scale,
7712        last.norm.as_ref(),
7713    ) else {
7714        return Vec::new();
7715    };
7716    let mut proposals = Vec::with_capacity(block);
7717    out_conf.clear();
7718    let mut prev = last_token;
7719    let mut head_in = vec![0.0f32; block * dim];
7720    let mut pre_norms = vec![vec![0.0f32; dim]; block];
7721    for i in 0..block {
7722        hc_head_fold(
7723            &states[i], hfn, hscale, hbase, cfg, pool,
7724            &mut head_in[i * dim..(i + 1) * dim],
7725        );
7726        pre_norms[i].copy_from_slice(&head_in[i * dim..(i + 1) * dim]);
7727        rms_weighted(
7728            &mut head_in[i * dim..(i + 1) * dim], hnorm, cfg.norm_eps,
7729        );
7730    }
7731    let mut logits = vec![0.0f32; block * cfg.vocab];
7732    g.head.matmat(&head_in, block, &mut logits, pool);
7733    let mut mk_embed = vec![0.0f32; last.markov_w1.as_ref().map_or(0, |t| t.cols())];
7734    for i in 0..block {
7735        let logits_i = &mut logits[i * cfg.vocab..(i + 1) * cfg.vocab];
7736        // The markov head biases the logits from the PREVIOUS token — a
7737        // rank-256 bigram the draft samples through position by position,
7738        // while the network itself ran the whole block at once.
7739        // `CMF_DSPARK_NO_MARKOV=1` drops it: the bias is sequential through
7740        // the block (each position needs the previous PROPOSAL), which is
7741        // the one part of the draft a single device graph cannot batch — so
7742        // its acceptance value has to be known before it earns that
7743        // complexity.
7744        let no_markov = {
7745            static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7746            *ON.get_or_init(|| std::env::var("CMF_DSPARK_NO_MARKOV").is_ok_and(|v| v != "0"))
7747        };
7748        if no_markov {
7749            // Still feed the confidence head's embedding slot below.
7750            if let Some(w1) = last.markov_w1.as_ref() {
7751                w1.row_f32(prev as usize, &mut mk_embed);
7752            }
7753        } else if let (Some(w1), Some(w2)) = (last.markov_w1.as_ref(), last.markov_w2.as_ref()) {
7754            w1.row_f32(prev as usize, &mut mk_embed);
7755            let mut bias = vec![0.0f32; cfg.vocab];
7756            w2.matvec(&mk_embed, &mut bias, pool);
7757            for (a, b) in logits_i.iter_mut().zip(&bias) {
7758                *a += *b;
7759            }
7760        }
7761        let mut best = 0usize;
7762        for v in 1..logits_i.len() {
7763            if logits_i[v] > logits_i[best] {
7764                best = v;
7765            }
7766        }
7767        if let Some(cf) = last.confidence.as_ref() {
7768            let mut cat = pre_norms[i].clone();
7769            cat.extend_from_slice(&mk_embed);
7770            let mut s = [0.0f32; 1];
7771            if cat.len() == cf.cols() {
7772                cf.matvec(&cat, &mut s, pool);
7773            }
7774            out_conf.push(s[0]);
7775        }
7776        proposals.push(best as u32);
7777        prev = best as u32;
7778    }
7779    proposals
7780}