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