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.iter().any(|&x| x == 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.iter().any(|&x| x == 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.iter().any(|&x| x == 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.iter().any(|&s| s == 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) * -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.28253591,
6580            0.09218107,
6581            0.025676856,
6582            0.17564717,
6583            0.22228767,
6584            0.27174541,
6585            0.33031881,
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| v >= 0.0 && v <= 2.0));
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([1.5f32, -2.0, 0.25])
6647            .take(hc)
6648            .flatten()
6649            .collect();
6650        let comb = {
6651            // exactly doubly stochastic: uniform
6652            vec![0.25f32; hc * hc]
6653        };
6654        let post = vec![0.0f32; hc];
6655        let mut out = vec![0.0f32; hc * dim];
6656        hc_expand(&[0.0; 3], &residual, &post, &comb, hc, dim, &mut out);
6657        for (o, r) in out.iter().zip(&residual) {
6658            assert!((o - r).abs() < 1e-6, "{o} vs {r}");
6659        }
6660    }
6661
6662    /// The bias must move the SELECTION without touching the weights: with a
6663    /// large bias on a low-scoring expert it gets picked, but its weight is
6664    /// still its own (small) score, renormalized.
6665    #[test]
6666    fn selection_bias_steers_the_choice_but_not_the_weights() {
6667        let scores = [3.0f32, 0.1, 2.0, 0.05];
6668        let bias = [0.0f32, 10.0, 0.0, 0.0];
6669        let (mut idx, mut w) = (Vec::new(), Vec::new());
6670        route(&scores, Some(&bias), 2, 1.5, None, None, &mut idx, &mut w);
6671        assert_eq!(idx[0], 1, "the biased expert must win selection");
6672        assert_eq!(idx[1], 0);
6673        // weights come from sqrt(softplus(score)) BEFORE the bias, so the
6674        // biased expert's share must be the smaller of the two
6675        assert!(w[0] < w[1], "biased expert kept its own (small) weight");
6676        let sum: f32 = w.iter().sum();
6677        assert!((sum - 1.5).abs() < 1e-5, "weights renormalize then scale");
6678    }
6679
6680    /// The sink is an extra logit with no value: it must lower every
6681    /// weight without adding output. With a huge sink the head should
6682    /// attend to almost nothing.
6683    #[test]
6684    fn attention_sink_drains_weight_without_contributing_output() {
6685        let hd = 2;
6686        let q = [1.0f32, 0.0];
6687        let kv = [1.0f32, 0.0, 0.0, 1.0];
6688        let mut out = vec![0.0f32; hd];
6689        sparse_attend(&q, &kv, &[0, 1], f32::NEG_INFINITY, 1.0, hd, &mut out);
6690        let plain = out.clone();
6691        assert!(plain[0] > plain[1], "the aligned key must dominate");
6692        sparse_attend(&q, &kv, &[0, 1], 20.0, 1.0, hd, &mut out);
6693        assert!(
6694            out[0] < plain[0] * 0.01 && out[1] < plain[1] * 0.01,
6695            "a large sink must drain nearly all the mass: {out:?}"
6696        );
6697    }
6698
6699    /// A masked slot must be ignored entirely — not folded in as a zero
6700    /// key, which would still add exp(0) to the denominator.
6701    #[test]
6702    fn masked_positions_leave_the_denominator_alone() {
6703        let hd = 2;
6704        let q = [1.0f32, 0.0];
6705        let kv = [1.0f32, 0.0, 0.0, 1.0];
6706        let (mut a, mut b) = (vec![0.0f32; hd], vec![0.0f32; hd]);
6707        sparse_attend(&q, &kv, &[0], f32::NEG_INFINITY, 1.0, hd, &mut a);
6708        sparse_attend(
6709            &q,
6710            &kv,
6711            &[0, usize::MAX],
6712            f32::NEG_INFINITY,
6713            1.0,
6714            hd,
6715            &mut b,
6716        );
6717        for (x, y) in a.iter().zip(&b) {
6718            assert!((x - y).abs() < 1e-6, "{x} vs {y}");
6719        }
6720    }
6721
6722    /// Forward then inverse rotation is the identity — the property the
6723    /// output path depends on.
6724    #[test]
6725    fn rope_tail_inverts_itself() {
6726        let inv_freq = [1.0f32, 0.5];
6727        let orig = [9.0f32, 8.0, 1.0, 2.0, 3.0, 4.0];
6728        let mut v = orig;
6729        rope_tail(&mut v, &inv_freq, 7, 4, false);
6730        assert!(v[..2] == orig[..2], "the non-rope head must not move");
6731        assert!(v[2..] != orig[2..], "the tail must actually rotate");
6732        rope_tail(&mut v, &inv_freq, 7, 4, true);
6733        for (a, b) in v.iter().zip(&orig) {
6734            assert!((a - b).abs() < 1e-5, "{a} vs {b}");
6735        }
6736    }
6737
6738    /// The window pooling is a softmax per DIMENSION over the ratio, with
6739    /// the position bias inside the exponent.
6740    #[test]
6741    fn compressor_pools_the_window_per_dimension() {
6742        let (ratio, width) = (2usize, 2usize);
6743        let kv = [1.0f32, 10.0, 3.0, 20.0];
6744        // dim 0: equal scores → mean; dim 1: second token wins by a mile
6745        let score = [0.0f32, 0.0, 0.0, 50.0];
6746        let ape = vec![0.0f32; ratio * width];
6747        let mut out = vec![0.0f32; width];
6748        compress_window(&kv, &score, &ape, ratio, width, &mut out);
6749        assert!(
6750            (out[0] - 2.0).abs() < 1e-5,
6751            "equal scores average: {}",
6752            out[0]
6753        );
6754        assert!(
6755            (out[1] - 20.0).abs() < 1e-3,
6756            "a dominant score wins: {}",
6757            out[1]
6758        );
6759    }
6760
6761    /// A negative dot product must not drag a position down: the relu
6762    /// means heads abstain rather than veto.
6763    #[test]
6764    fn index_scores_relu_before_weighting() {
6765        let (nh, hd) = (2usize, 2usize);
6766        // head 0 aligns with position 0, head 1 anti-aligns with it
6767        let q = [1.0f32, 0.0, -1.0, 0.0];
6768        let kv = [1.0f32, 0.0, 0.0, 1.0];
6769        let w = [1.0f32, 1.0];
6770        let mut sc = Vec::new();
6771        index_scores(&q, &kv, &w, nh, hd, 2, 2, None, &mut sc);
6772        // without the relu the anti-aligned head would cancel head 0 to zero
6773        assert!(sc[0] > 0.9, "abstention, not veto: {:?}", sc);
6774    }
6775
6776    #[test]
6777    fn index_scores_mask_the_future() {
6778        let (nh, hd) = (1usize, 2usize);
6779        let q = [1.0f32, 0.0];
6780        let kv = [1.0f32, 0.0, 1.0, 0.0, 1.0, 0.0];
6781        let w = [1.0f32];
6782        let mut sc = Vec::new();
6783        index_scores(&q, &kv, &w, nh, hd, 3, 2, None, &mut sc);
6784        assert!(sc[0].is_finite() && sc[1].is_finite());
6785        assert!(sc[2] == f32::NEG_INFINITY, "position 2 is in the future");
6786        let mut idx = Vec::new();
6787        top_k_positions(&sc, 3, &mut idx);
6788        assert_eq!(idx, vec![0, 1], "a masked slot never wins a slot");
6789    }
6790
6791    #[test]
6792    fn top_k_is_deterministic_on_ties() {
6793        let sc = [1.0f32, 1.0, 1.0, 0.0];
6794        let mut idx = Vec::new();
6795        top_k_positions(&sc, 2, &mut idx);
6796        assert_eq!(idx, vec![0, 1], "ties resolve to the lower index");
6797    }
6798
6799    /// The block cycle must leave the state's SHAPE intact (hc copies in,
6800    /// hc copies out) and must actually route the block's output back in:
6801    /// a block that writes a constant has to move every copy.
6802    #[test]
6803    fn hc_block_preserves_the_copy_structure_and_applies_the_block() {
6804        let cfg = Dsv4Cfg {
6805            dim: 4,
6806            n_heads: 1,
6807            head_dim: 4,
6808            rope_head_dim: 2,
6809            q_lora_rank: 4,
6810            o_lora_rank: 2,
6811            o_groups: 1,
6812            hc_mult: 4,
6813            hc_sinkhorn_iters: 20,
6814            hc_eps: 1e-6,
6815            norm_eps: 1e-6,
6816            n_routed_experts: 2,
6817            top_k: 1,
6818            moe_inter: 4,
6819            route_scale: 1.0,
6820            swiglu_limit: 10.0,
6821            window: 128,
6822            index_topk: 4,
6823            vocab: 8,
6824        };
6825        let (hc, dim) = (cfg.hc_mult, cfg.dim);
6826        let mix_hc = (2 + hc) * hc;
6827        let hc_fn: Vec<f32> = (0..mix_hc * hc * dim)
6828            .map(|i| ((i % 13) as f32 - 6.0) * 0.05)
6829            .collect();
6830        let hc_base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.2).sin()).collect();
6831        let norm_w = vec![1.0f32; dim];
6832        let mut state: Vec<f32> = (0..hc * dim).map(|i| (i as f32 * 0.3).cos()).collect();
6833        let before = state.clone();
6834        let mut scratch = HcScratch::new(&cfg);
6835        hc_block(
6836            &mut state,
6837            &hc_fn,
6838            &[1.0, 1.0, 1.0],
6839            &hc_base,
6840            &norm_w,
6841            &cfg,
6842            &mut scratch,
6843            None,
6844            |_folded, out: &mut [f32]| out.iter_mut().for_each(|o| *o = 1.0),
6845        );
6846        assert_eq!(state.len(), before.len(), "copy structure must survive");
6847        assert!(state.iter().all(|v| v.is_finite()), "{state:?}");
6848        assert!(
6849            state.iter().zip(&before).any(|(a, b)| (a - b).abs() > 1e-4),
6850            "the block's output has to reach the state"
6851        );
6852    }
6853
6854    #[test]
6855    fn hash_route_reads_the_table_row() {
6856        // vocab 3, top_k 2
6857        let table = [7.0f32, 9.0, 1.0, 2.0, 5.0, 6.0];
6858        assert_eq!(hash_route(&table, 3, 2, 0), vec![7, 9]);
6859        assert_eq!(hash_route(&table, 3, 2, 2), vec![5, 6]);
6860        // out-of-range ids clamp instead of panicking
6861        assert_eq!(hash_route(&table, 3, 2, 99), vec![5, 6]);
6862    }
6863
6864    /// A task mask restricts SELECTION and nothing else: the weights still
6865    /// come from the pre-bias scores and still renormalize, now over what
6866    /// survives. Masking must never reroute — an expert the mask forbids has
6867    /// to be absent, not replaced by a neighbour with the wrong weight.
6868    #[test]
6869    fn a_task_mask_restricts_selection_and_renormalizes() {
6870        // Expert 3 scores highest, then 1, then 2, then 0.
6871        let scores = [0.1f32, 4.0, 1.0, 9.0];
6872        let (mut idx, mut w) = (Vec::new(), Vec::new());
6873        route(&scores, None, 2, 1.0, None, None, &mut idx, &mut w);
6874        assert_eq!(idx, vec![3, 1], "unmasked: the two best win");
6875        let sum: f32 = w.iter().sum();
6876        assert!((sum - 1.0).abs() < 1e-5, "weights must sum to route_scale");
6877
6878        // Forbid the winner: the next two take its place and the weights
6879        // renormalize over them.
6880        let mask = [true, false, true, true];
6881        let (mut i2, mut w2) = (Vec::new(), Vec::new());
6882        route(&scores, None, 2, 1.0, None, Some(&mask), &mut i2, &mut w2);
6883        assert_eq!(i2, vec![3, 2], "masked expert must not be selected");
6884        let sum2: f32 = w2.iter().sum();
6885        assert!((sum2 - 1.0).abs() < 1e-5, "masked weights must renormalize");
6886
6887        // A mask leaving fewer than top_k experts yields fewer, not garbage.
6888        let tight = [false, false, false, true];
6889        let (mut i3, mut w3) = (Vec::new(), Vec::new());
6890        route(&scores, None, 2, 1.0, None, Some(&tight), &mut i3, &mut w3);
6891        assert_eq!(i3, vec![3]);
6892        assert_eq!(w3.len(), 1);
6893    }
6894
6895    /// On a hash layer the reference gathers the scores AT THE TABLE's
6896    /// experts. Choosing top-k first and swapping the indices afterwards
6897    /// leaves every weight attached to a different expert than the one it
6898    /// scales — silently, since both lists are the right length.
6899    #[test]
6900    fn hash_layers_weight_the_experts_the_table_names() {
6901        // Expert 3 scores highest, expert 0 lowest; the table names 0 and 1.
6902        let scores = [0.1f32, 0.4, 0.2, 5.0];
6903        let table = vec![0.0f32, 1.0];
6904        let idx_forced = hash_route(&table, 1, 2, 0);
6905        assert_eq!(idx_forced, vec![0, 1]);
6906
6907        let (mut idx, mut w) = (Vec::new(), Vec::new());
6908        route(
6909            &scores,
6910            None,
6911            2,
6912            1.0,
6913            Some(&idx_forced),
6914            None,
6915            &mut idx,
6916            &mut w,
6917        );
6918        assert_eq!(idx, vec![0, 1], "the table must decide the experts");
6919
6920        // The weights must be the table experts' own scores, normalized.
6921        let sp = |x: f32| (1.0 + x.exp()).ln().sqrt();
6922        let (s0, s1) = (sp(scores[0]), sp(scores[1]));
6923        let tot = s0 + s1;
6924        assert!(
6925            (w[0] - s0 / tot).abs() < 1e-6,
6926            "w[0]={} want {}",
6927            w[0],
6928            s0 / tot
6929        );
6930        assert!(
6931            (w[1] - s1 / tot).abs() < 1e-6,
6932            "w[1]={} want {}",
6933            w[1],
6934            s1 / tot
6935        );
6936
6937        // And the top-k path is untouched: expert 3 still wins there.
6938        let (mut idx2, mut w2) = (Vec::new(), Vec::new());
6939        route(&scores, None, 2, 1.0, None, None, &mut idx2, &mut w2);
6940        assert_eq!(idx2[0], 3, "without a table the highest score still wins");
6941    }
6942}
6943
6944// ══ DSpark: the block-parallel draft ══════════════════════════════════
6945//
6946// Not a classic MTP chain. One pass through the three stages produces the
6947// WHOLE block of `block_size` positions at once: position 0 carries the token
6948// the trunk just emitted, the rest carry a noise token, and every position
6949// attends to every other one — which is why the block cannot be measured a
6950// position at a time and pretend to be faithful. Depth comes from the block,
6951// not from the stage count.
6952//
6953// The stages' KV cache is built from the trunk's hidden state, not from the
6954// draft's own tokens: one entry per real position, `kv_norm(wkv(main_x))`,
6955// in a ring of `window`. The block's own keys and values are appended for
6956// the duration of the block and then discarded.
6957
6958/// The noise token the block's unknown positions carry
6959/// (`dspark_noise_token_id`).
6960pub const DSPARK_NOISE_TOKEN: u32 = 128799;
6961/// `dspark_block_size` — the width of the draft block, and NOT a tuning knob.
6962///
6963/// All five positions attend to each other and the model was trained with
6964/// exactly four noise slots behind the real token, so a narrower block is a
6965/// different draft model, not a cheaper one. What the survival curve argues
6966/// for is verifying fewer of the five — see `dspark_verify_k` — which costs
6967/// less without changing what the draft computes.
6968pub fn dspark_block() -> usize {
6969    5
6970}
6971
6972/// How many of the block's proposals the trunk actually checks.
6973///
6974/// Survival is [0.67, 0.50, 0.29, 0.08, 0.04]: positions four and five are
6975/// paid for on every verify and delivered on a twelfth of them. Three yields
6976/// 2.46 tokens a cycle against five's 2.58, for three fifths of the verify.
6977/// `CMF_DSPARK_VERIFY_K=N` sets it.
6978pub fn dspark_verify_k() -> usize {
6979    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
6980    *K.get_or_init(|| {
6981        std::env::var("CMF_DSPARK_VERIFY_K")
6982            .ok()
6983            .and_then(|v| v.parse::<usize>().ok())
6984            .filter(|&n| (1..=DSPARK_BLOCK_MAX).contains(&n))
6985            .unwrap_or(DSPARK_BLOCK_MAX)
6986    })
6987}
6988
6989/// The trained block width.
6990pub const DSPARK_BLOCK_MAX: usize = 5;
6991
6992/// Per-sequence state of the draft: one KV ring per stage, and the trunk
6993/// hidden states the block's input is projected from.
6994pub struct DsparkState {
6995    /// `[stage][window * kv_width]`, written at `pos % window`.
6996    pub win: Vec<Vec<f32>>,
6997    /// How many real positions each ring holds, capped at `window`.
6998    pub filled: Vec<usize>,
6999    /// The trunk's captured hidden, `dim * n_targets`, refreshed every token.
7000    pub main_hidden: Vec<f32>,
7001    /// True once `main_hidden` holds this position's capture.
7002    pub have_hidden: bool,
7003}
7004
7005impl DsparkState {
7006    pub fn new(stages: usize, cfg: &Dsv4Cfg, targets: usize) -> Self {
7007        Self {
7008            win: vec![Vec::new(); stages],
7009            filled: vec![0; stages],
7010            main_hidden: vec![0.0; cfg.dim * targets],
7011            have_hidden: false,
7012        }
7013    }
7014}
7015
7016/// Which trunk layers the draft reads. Upstream names them explicitly
7017/// (`dspark_target_layer_ids`); the file says the same thing less directly —
7018/// `main_proj` has one `dim`-wide input block per captured layer — and the
7019/// release captures the last three. Deriving it from the weight keeps the
7020/// two from disagreeing.
7021pub fn dspark_targets(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg, n_layers: usize) -> Vec<usize> {
7022    let Some(mp) = mtp.iter().find_map(|m| m.main_proj.as_ref()) else {
7023        return Vec::new();
7024    };
7025    let n = (mp.cols() / cfg.dim.max(1)).clamp(1, n_layers);
7026    (n_layers - n..n_layers).collect()
7027}
7028
7029thread_local! {
7030    /// The armed capture: which layers to take, and the buffer they fill.
7031    /// A thread-local rather than a parameter because the capture has to
7032    /// reach into the middle of a layer loop that eight call sites share,
7033    /// and threading an optional buffer through all of them to serve one
7034    /// diagnostic is a worse trade than this.
7035    static DSPARK_CAP: std::cell::RefCell<(Vec<usize>, Vec<f32>, usize)> =
7036        const { std::cell::RefCell::new((Vec::new(), Vec::new(), 0)) };
7037}
7038
7039/// Arm the capture for the layers `targets`, in order.
7040pub fn dspark_arm(targets: &[usize], dim: usize) {
7041    DSPARK_CAP.with(|c| {
7042        let mut c = c.borrow_mut();
7043        c.0 = targets.to_vec();
7044        c.1 = vec![0.0; dim * targets.len()];
7045        c.2 = 0;
7046    });
7047}
7048
7049/// Whether the armed MTP capture needs the state immediately after `li`.
7050/// The normal decode path keeps a full run in one submission; DSpark is the
7051/// only caller that needs an intermediate state to cross the device boundary.
7052fn dspark_wants(li: usize) -> bool {
7053    DSPARK_CAP.with(|c| c.borrow().0.contains(&li))
7054}
7055
7056/// Called after every host layer. Free when nothing is armed.
7057pub fn dspark_note(li: usize, state: &[f32], cfg: &Dsv4Cfg) {
7058    DSPARK_CAP.with(|c| {
7059        let mut c = c.borrow_mut();
7060        if c.0.is_empty() {
7061            return;
7062        }
7063        if let Some(slot) = c.0.iter().position(|&t| t == li) {
7064            let (_, buf, seen) = &mut *c;
7065            dspark_capture(state, cfg, slot, buf);
7066            // Counted, not "was the last one" — under the device chain only
7067            // the layers left on the host call this, and taking the last
7068            // target as the signal would hand the draft a buffer whose other
7069            // slots still hold the previous token, or nothing at all.
7070            *seen = if slot == 0 { 1 } else { *seen + 1 };
7071            if std::env::var("CMF_DSPARK_CAP_DBG").is_ok() {
7072                eprintln!("[cap] note li={li} slot={slot} seen={}", *seen);
7073            }
7074        }
7075    });
7076}
7077
7078/// Read one slot of the armed capture buffer as-is, complete or not. The
7079/// speculative verify fills the DEVICE targets from its own photographs and
7080/// only needs the host layers' slots from here — `dspark_take`'s
7081/// completeness contract would never be met on that path.
7082pub fn dspark_peek_slot(slot: usize, dim: usize, out: &mut [f32]) -> bool {
7083    DSPARK_CAP.with(|c| {
7084        let c = c.borrow();
7085        let lo = slot * dim;
7086        if c.1.len() < lo + dim {
7087            return false;
7088        }
7089        out[..dim].copy_from_slice(&c.1[lo..lo + dim]);
7090        true
7091    })
7092}
7093
7094/// Move the capture out, if this token produced a complete one.
7095pub fn dspark_take(out: &mut Vec<f32>) -> bool {
7096    DSPARK_CAP.with(|c| {
7097        let mut c = c.borrow_mut();
7098        if c.0.is_empty() || c.2 != c.0.len() {
7099            if std::env::var("CMF_DSPARK_CAP_DBG").is_ok() {
7100                eprintln!("[cap] take FAIL armed={:?} seen={}", c.0, c.2);
7101            }
7102            return false;
7103        }
7104        out.clear();
7105        out.extend_from_slice(&c.1);
7106        c.2 = 0;
7107        true
7108    })
7109}
7110
7111/// The trunk's contribution: the mean over the hyper-connection copies,
7112/// appended in target order. Costs one pass over `hc * dim` per captured
7113/// layer and nothing else.
7114pub fn dspark_capture(state: &[f32], cfg: &Dsv4Cfg, slot: usize, out: &mut [f32]) {
7115    let (hc, dim) = (cfg.hc_mult, cfg.dim);
7116    let dst = &mut out[slot * dim..(slot + 1) * dim];
7117    let inv = 1.0 / hc as f32;
7118    for d in 0..dim {
7119        let mut s = 0.0;
7120        for j in 0..hc {
7121            s += state[j * dim + d];
7122        }
7123        dst[d] = s * inv;
7124    }
7125}
7126
7127/// `CMF_DSPARK_PICK_DUMP=path` — accumulate the draft's expert picks per
7128/// stage and periodically rewrite `path` with `stage<TAB>expert<TAB>count`
7129/// lines. Rewritten every 32 blocks rather than at exit, so a run that is
7130/// killed still leaves the tallies on disk.
7131pub fn dspark_freq_note(picks: &[(usize, Vec<usize>)]) {
7132    static FREQ: std::sync::Mutex<Option<(std::collections::HashMap<(usize, usize), u64>, u64)>> =
7133        std::sync::Mutex::new(None);
7134    let Ok(path) = std::env::var("CMF_DSPARK_PICK_DUMP") else {
7135        return;
7136    };
7137    let mut g = FREQ.lock().unwrap();
7138    let (map, blocks) = g.get_or_insert_with(|| (std::collections::HashMap::new(), 0));
7139    for (stage, idx) in picks {
7140        for &e in idx {
7141            *map.entry((*stage, e)).or_insert(0) += 1;
7142        }
7143    }
7144    *blocks += 1;
7145    if *blocks % 32 == 0 {
7146        let mut lines: Vec<_> = map.iter().collect();
7147        lines.sort();
7148        let body: String = lines
7149            .iter()
7150            .map(|((s, e), n)| format!("{s}\t{e}\t{n}\n"))
7151            .collect();
7152        let _ = std::fs::write(&path, body);
7153    }
7154}
7155
7156/// `CMF_DSV4_TRUNK_PICK_DUMP=path` — the same tally for the TRUNK's layers:
7157/// `layer<TAB>expert<TAB>count`, rewritten every 32 tokens. The pick lists
7158/// come from the probe's own tally window, so only layers that route on the
7159/// host are counted — which is exactly the population a partial pack serves.
7160pub fn trunk_freq_note(picks: &[(usize, Vec<usize>)]) {
7161    static FREQ: std::sync::Mutex<Option<(std::collections::HashMap<(usize, usize), u64>, u64)>> =
7162        std::sync::Mutex::new(None);
7163    let Ok(path) = std::env::var("CMF_DSV4_TRUNK_PICK_DUMP") else {
7164        return;
7165    };
7166    let mut g = FREQ.lock().unwrap();
7167    let (map, blocks) = g.get_or_insert_with(|| (std::collections::HashMap::new(), 0));
7168    for (li, idx) in picks {
7169        for &e in idx {
7170            *map.entry((*li, e)).or_insert(0) += 1;
7171        }
7172    }
7173    *blocks += 1;
7174    if *blocks % 32 == 0 {
7175        let mut lines: Vec<_> = map.iter().collect();
7176        lines.sort();
7177        let body: String = lines
7178            .iter()
7179            .map(|((l, e), n)| format!("{l}\t{e}\t{n}\n"))
7180            .collect();
7181        let _ = std::fs::write(&path, body);
7182    }
7183}
7184
7185/// `CMF_DSPARK_MASK=path` — restrict the draft's routed experts to an
7186/// explicit per-stage keep-set: line `d` of the file lists the expert ids
7187/// stage `d` may route to, comma-separated. Weights renormalize over what
7188/// remains (the `Dsv4Layer::mask` contract). The draft only proposes — the
7189/// trunk still verifies every token — so a thinner draft costs acceptance,
7190/// never correctness. This is the offline dial for sizing a resident
7191/// device pack before one exists.
7192fn dspark_apply_mask(out: &mut [Dsv4Mtp]) {
7193    let Ok(path) = std::env::var("CMF_DSPARK_MASK") else {
7194        return;
7195    };
7196    let Ok(text) = std::fs::read_to_string(&path) else {
7197        eprintln!("DSpark: CMF_DSPARK_MASK={path} не читается — маска не применена");
7198        return;
7199    };
7200    for (d, line) in text.lines().enumerate() {
7201        let Some(m) = out.get_mut(d) else { break };
7202        let n = m.layer.experts.len();
7203        let mut mask = vec![false; n];
7204        let mut kept = 0usize;
7205        for tok in line.split(',') {
7206            if let Ok(e) = tok.trim().parse::<usize>() {
7207                if e < n && !mask[e] {
7208                    mask[e] = true;
7209                    kept += 1;
7210                }
7211            }
7212        }
7213        if kept == 0 {
7214            continue;
7215        }
7216        eprintln!("DSpark: стадия {d} ограничена {kept}/{n} экспертами");
7217        m.layer.mask = Some(mask);
7218    }
7219}
7220
7221/// The draft's device residency: which experts of each stage live on the
7222/// card, and how the device router reaches them.
7223///
7224/// The draft only proposes — the trunk verifies every token — so the pack
7225/// is free to keep a SUBSET of each stage's experts and mask the routing to
7226/// it: acceptance pays, correctness never does. The subset is chosen by
7227/// measured routing frequency (`CMF_DSPARK_PACK` names the tally file that
7228/// `CMF_DSPARK_PICK_DUMP` wrote; `CMF_DSPARK_RESIDENT` caps experts per
7229/// stage, default 48).
7230#[cfg(feature = "gpu")]
7231pub struct DsparkPack {
7232    pub stages: Vec<DsparkStagePack>,
7233    /// Gate/up requantized to q2tp at upload (the binary registered an
7234    /// encoder); the graph then dispatches the q2tp kernels.
7235    pub gu_q2: bool,
7236    /// The down planes too (native in the file, never requantized at
7237    /// upload); the graph dispatches the 2-bit down kernel.
7238    pub dn_q2: bool,
7239    /// Dequantized router and bias per stage, f32 — address-stable for the
7240    /// life of the pack, which is what the device's const cache needs.
7241    pub routers: Vec<Vec<f32>>,
7242    pub biases: Vec<Option<Vec<f32>>>,
7243}
7244
7245#[cfg(feature = "gpu")]
7246pub struct DsparkStagePack {
7247    /// Selectable experts (true = resident).
7248    pub mask: Vec<bool>,
7249    /// Global expert id → pack slot; usize::MAX where cold.
7250    pub to_slot: Vec<usize>,
7251    /// The same two as the device consumes them — u32, address-stable for
7252    /// the pack's lifetime (the const cache keys on the pointer).
7253    pub mask_u32: Vec<u32>,
7254    pub map_u32: Vec<u32>,
7255    /// (gate, up, down) directory indices, pack order, shared LAST.
7256    pub tensors: Vec<(usize, usize, usize)>,
7257    pub n_resident: usize,
7258}
7259
7260/// The q2tp encoder, registered by the binary that has one (the CLI's
7261/// converter owns the rung-search implementation and the engine must not
7262/// depend on the CLI). When present, the draft's gate/up experts are
7263/// requantized q4tp → q2tp AT UPLOAD — half the VRAM and the same kernels
7264/// the trunk's q2tp experts already use. Draft-only fidelity: acceptance
7265/// pays, correctness never does.
7266pub static DSPARK_Q2TP_ENCODE: std::sync::OnceLock<fn(&[f32], usize, usize) -> Vec<u8>> =
7267    std::sync::OnceLock::new();
7268
7269/// `CMF_DSPARK_GPU=1` — the probe (and later the speculative loop) drafts
7270/// on the card instead of the CPU/disk tier.
7271#[cfg(feature = "gpu")]
7272pub fn dspark_gpu_on() -> bool {
7273    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7274    *ON.get_or_init(|| {
7275        std::env::var("CMF_DSPARK_GPU")
7276            .map(|v| v != "0")
7277            .unwrap_or(true)
7278    })
7279}
7280
7281/// The pack, built once per process (the stand runs one model).
7282#[cfg(feature = "gpu")]
7283pub fn dspark_pack_get(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg) -> Option<&'static DsparkPack> {
7284    static P: std::sync::OnceLock<Option<Box<DsparkPack>>> = std::sync::OnceLock::new();
7285    P.get_or_init(|| dspark_pack_build(mtp, cfg).map(Box::new))
7286        .as_deref()
7287}
7288
7289/// Build and upload the draft's pack. Returns `None` when the stack is
7290/// absent, the budget refuses, or a stage's weights are not where the
7291/// device path needs them — the caller falls back to the CPU draft.
7292/// Reserve the VRAM the speculative draft's device pack will take, so the
7293/// trunk's greedy packing leaves it room. Called at load, before any trunk
7294/// pack is built; a no-op when there is no MTP stack or speculation is off.
7295/// The estimate uses the draft's native dtypes — an upload-time re-encode
7296/// only shrinks it, which errs on the safe side of the physical ceiling.
7297///
7298/// A budget that cannot pack the trunk to the draft's capture layers (the
7299/// last three) gets NO reservation: speculation will decline there anyway,
7300/// and the carve-out would only shrink the walk's packs — measured 13% of
7301/// decode on a 64 GB budget. The threshold is geometric (nine tenths of
7302/// the trunk's own expert bytes plus the draft), never a card name.
7303#[cfg(feature = "gpu")]
7304pub fn dspark_reserve_note(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg, layers: &[Dsv4Layer]) {
7305    if mtp.is_empty() || std::env::var("CMF_DSV4_SPEC").is_ok_and(|v| v == "0") || !dspark_gpu_on()
7306    {
7307        return;
7308    }
7309    let dt = |q2: bool| {
7310        if q2 {
7311            cortiq_core::TensorDtype::Q2TiledP
7312        } else {
7313            cortiq_core::TensorDtype::Q4TiledP
7314        }
7315    };
7316    let gu_q2 = mtp[0]
7317        .layer
7318        .experts
7319        .first()
7320        .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
7321    let dn_q2 = mtp[0]
7322        .layer
7323        .experts
7324        .first()
7325        .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
7326    let gu = cortiq_core::quant::expected_nbytes(dt(gu_q2), &[cfg.moe_inter, cfg.dim]).unwrap_or(0);
7327    let dn = cortiq_core::quant::expected_nbytes(dt(dn_q2), &[cfg.dim, cfg.moe_inter]).unwrap_or(0);
7328    let per = (2 * gu + dn) as u64;
7329    let n_res: usize = std::env::var("CMF_DSPARK_RESIDENT")
7330        .ok()
7331        .and_then(|v| v.parse().ok())
7332        // The default matches the measured acceptance plateau's low edge:
7333        // residency below it costs acceptance, above it only costs VRAM.
7334        .unwrap_or(40);
7335    // Routed residents per stage, plus each stage's shared expert.
7336    let bytes = per * (n_res * mtp.len() + mtp.len() + 1) as u64;
7337    // The trunk's own expert bytes, from the layers as they are.
7338    let trunk: u64 = layers
7339        .iter()
7340        .map(|l| {
7341            let Some(e) = l.experts.first() else { return 0 };
7342            let gu = cortiq_core::quant::expected_nbytes(
7343                dt(e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
7344                &[cfg.moe_inter, cfg.dim],
7345            )
7346            .unwrap_or(0);
7347            let dn = cortiq_core::quant::expected_nbytes(
7348                dt(e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)),
7349                &[cfg.dim, cfg.moe_inter],
7350            )
7351            .unwrap_or(0);
7352            ((2 * gu + dn) * (l.experts.len() + 1)) as u64
7353        })
7354        .sum();
7355    if let Some(budget) = crate::gpu_wgpu::dsv4_vram_budget() {
7356        if budget < trunk / 10 * 9 + bytes {
7357            return;
7358        }
7359    }
7360    crate::gpu_wgpu::DRAFT_RESERVE.store(bytes, std::sync::atomic::Ordering::Relaxed);
7361}
7362
7363#[cfg(not(feature = "gpu"))]
7364pub fn dspark_reserve_note(_mtp: &[Dsv4Mtp], _cfg: &Dsv4Cfg, _layers: &[Dsv4Layer]) {}
7365
7366#[cfg(feature = "gpu")]
7367pub fn dspark_pack_build(mtp: &[Dsv4Mtp], cfg: &Dsv4Cfg) -> Option<DsparkPack> {
7368    if mtp.is_empty() {
7369        return None;
7370    }
7371    let n_res: usize =
7372        std::env::var("CMF_DSPARK_RESIDENT")
7373            .ok()
7374            .and_then(|v| v.parse().ok())
7375            .unwrap_or_else(|| {
7376                // No knob: take what the card actually has left, whatever the
7377                // card is. The stages split the fit evenly after their shared
7378                // experts; the clamp keeps the band where drafting is known to
7379                // be worth the VRAM at the low end and past diminishing
7380                // returns at the high one.
7381                let native_q2 = mtp[0].layer.experts.first().is_some_and(|e| {
7382                    e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
7383                });
7384                let gu_q2 = native_q2 || DSPARK_Q2TP_ENCODE.get().is_some();
7385                let dn_q2 = mtp[0].layer.experts.first().is_some_and(|e| {
7386                    e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
7387                });
7388                let room = crate::gpu_wgpu::dsv4_draft_fit(cfg.moe_inter, cfg.dim, gu_q2, dn_q2);
7389                (room.saturating_sub(mtp.len() + 1) / mtp.len().max(1)).clamp(8, 64)
7390            });
7391    // Frequency tallies: lines of `stage<TAB>expert<TAB>count`. Named by
7392    // `CMF_DSPARK_PACK`, or found as `<model>.dspark.tsv` beside the model
7393    // file — ship the tally next to the checkpoint and no knob is needed.
7394    let mut freq: Vec<Vec<(u64, usize)>> = vec![Vec::new(); mtp.len()];
7395    let pack_path = std::env::var("CMF_DSPARK_PACK").ok().or_else(|| {
7396        let m = mtp[0].layer.experts.first()?.w1.model_arc()?;
7397        let mut s = m.path.as_os_str().to_os_string();
7398        s.push(".dspark.tsv");
7399        let p = std::path::PathBuf::from(s);
7400        p.exists().then(|| p.to_string_lossy().into_owned())
7401    });
7402    if let Some(path) = pack_path {
7403        if let Ok(text) = std::fs::read_to_string(&path) {
7404            for line in text.lines() {
7405                let mut it = line.split_whitespace();
7406                if let (Some(s), Some(e), Some(n)) = (it.next(), it.next(), it.next()) {
7407                    if let (Ok(s), Ok(e), Ok(n)) =
7408                        (s.parse::<usize>(), e.parse::<usize>(), n.parse::<u64>())
7409                    {
7410                        if s < freq.len() {
7411                            freq[s].push((n, e));
7412                        }
7413                    }
7414                }
7415            }
7416        }
7417    }
7418    let mut stages = Vec::with_capacity(mtp.len());
7419    let mut routers = Vec::with_capacity(mtp.len());
7420    let mut biases = Vec::with_capacity(mtp.len());
7421    for (si, m) in mtp.iter().enumerate() {
7422        let l = &m.layer;
7423        let n = l.experts.len();
7424        // Frequency order, then the untallied ids — a cold start still
7425        // packs SOMETHING deterministic.
7426        let mut order: Vec<usize> = {
7427            let mut f = freq[si].clone();
7428            f.sort_by(|a, b| b.0.cmp(&a.0));
7429            let mut seen = vec![false; n];
7430            let mut o: Vec<usize> = f
7431                .into_iter()
7432                .map(|(_, e)| e)
7433                .filter(|&e| {
7434                    if e < n && !seen[e] {
7435                        seen[e] = true;
7436                        true
7437                    } else {
7438                        false
7439                    }
7440                })
7441                .collect();
7442            o.extend((0..n).filter(|&e| !seen[e]));
7443            o
7444        };
7445        order.truncate(n_res.min(n));
7446        let mut mask = vec![false; n];
7447        let mut to_slot = vec![usize::MAX; n];
7448        let mut tensors = Vec::with_capacity(order.len() + 1);
7449        for (slot, &e) in order.iter().enumerate() {
7450            let ex = &l.experts[e];
7451            let (Some(w1), Some(w3), Some(w2)) =
7452                (ex.w1.model_idx(), ex.w3.model_idx(), ex.w2.model_idx())
7453            else {
7454                return None;
7455            };
7456            mask[e] = true;
7457            to_slot[e] = slot;
7458            tensors.push((w1, w3, w2));
7459        }
7460        let (Some(s1), Some(s3), Some(s2)) = (
7461            l.shared.w1.model_idx(),
7462            l.shared.w3.model_idx(),
7463            l.shared.w2.model_idx(),
7464        ) else {
7465            return None;
7466        };
7467        tensors.push((s1, s3, s2));
7468        // The router and bias, dequantized once.
7469        let mut router = vec![0.0f32; n * cfg.dim];
7470        for (r, row) in (0..n).zip(router.chunks_mut(cfg.dim)) {
7471            l.gate.row_f32(r, row);
7472        }
7473        routers.push(router);
7474        biases.push(l.gate_bias.clone());
7475        let mask_u32: Vec<u32> = mask.iter().map(|&m| m as u32).collect();
7476        let map_u32: Vec<u32> = to_slot
7477            .iter()
7478            .map(|&x| if x == usize::MAX { u32::MAX } else { x as u32 })
7479            .collect();
7480        stages.push(DsparkStagePack {
7481            mask,
7482            to_slot,
7483            mask_u32,
7484            map_u32,
7485            tensors,
7486            n_resident: order.len(),
7487        });
7488    }
7489    // ── upload: the small skeleton FIRST, the expert stacks after — the
7490    //    documented admission order (experts fill the card and the skeleton
7491    //    then misses). ──
7492    let model = mtp[0]
7493        .layer
7494        .experts
7495        .first()
7496        .and_then(|e| e.w1.model_arc())?;
7497    let mut skeleton = Vec::new();
7498    for m in mtp {
7499        let l = &m.layer;
7500        for t in [&l.wq_a, &l.wq_b, &l.wkv, &l.wo_a, &l.wo_b] {
7501            skeleton.push(t.model_idx()?);
7502        }
7503    }
7504    if let Some(mp) = mtp[0].main_proj.as_ref() {
7505        skeleton.push(mp.model_idx()?);
7506    }
7507    for &idx in &skeleton {
7508        if !crate::gpu_wgpu::dsv4_weight_ready(&model, idx) {
7509            eprintln!("DSpark: скелет драфта не влез в VRAM — GPU-черновик выключен");
7510            return None;
7511        }
7512    }
7513    // The dtype in the FILE decides: a properly converted CMF stores the
7514    // draft's gate/up as q2tp and uploads through the same path as the
7515    // trunk's 2-bit experts. The at-upload requant is only the fallback for
7516    // files published before the converter's q2tp profile covered the MTP
7517    // stack (and only when the binary registered an encoder).
7518    let native_q2 = mtp[0]
7519        .layer
7520        .experts
7521        .first()
7522        .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
7523    let gu_q2 = native_q2 || crate::dsv4::DSPARK_Q2TP_ENCODE.get().is_some();
7524    let dn_native = mtp[0]
7525        .layer
7526        .experts
7527        .first()
7528        .is_some_and(|e| e.w2.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
7529    for (si, sp) in stages.iter().enumerate() {
7530        let ok = if native_q2 {
7531            crate::gpu_wgpu::dsv4_experts_ready(
7532                &model,
7533                &sp.tensors,
7534                cfg.moe_inter,
7535                cfg.dim,
7536                true,
7537                dn_native,
7538            )
7539        } else if gu_q2 {
7540            crate::gpu_wgpu::moe_expert_bufs_requant_gu(&model, &sp.tensors, cfg.moe_inter, cfg.dim)
7541                .is_some()
7542        } else {
7543            crate::gpu_wgpu::dsv4_experts_ready(
7544                &model,
7545                &sp.tensors,
7546                cfg.moe_inter,
7547                cfg.dim,
7548                false,
7549                false,
7550            )
7551        };
7552        if !ok {
7553            eprintln!(
7554                "DSpark: эксперты стадии {si} ({} + shared) не влезли в VRAM — GPU-черновик выключен",
7555                sp.n_resident
7556            );
7557            return None;
7558        }
7559    }
7560    let _ = crate::gpu_wgpu::pin_weights(&model, &skeleton);
7561    eprintln!(
7562        "DSpark: пак драфта на карте — {} стадии по {} экспертов + shared",
7563        stages.len(),
7564        stages
7565            .iter()
7566            .map(|s| s.n_resident.to_string())
7567            .collect::<Vec<_>>()
7568            .join("/")
7569    );
7570    Some(DsparkPack {
7571        stages,
7572        gu_q2,
7573        dn_q2: dn_native,
7574        routers,
7575        biases,
7576    })
7577}
7578
7579/// Append one real position's entry to every stage's KV ring, from the
7580/// trunk captures in `ds.main_hidden`. The draft does this for the position
7581/// it drafts at; a speculative decode also owes an entry for every accepted
7582/// position it never drafted from — a hole in the ring silently starves
7583/// later blocks of context, which reads as "acceptance decayed" and not as
7584/// a bug.
7585pub fn dspark_ring_append(
7586    g: &Dsv4Globals,
7587    mtp: &[Dsv4Mtp],
7588    cfg: &Dsv4Cfg,
7589    ds: &mut DsparkState,
7590    pos: usize,
7591    pool: Option<&crate::pool::Pool>,
7592) {
7593    let (dim, hd, rd) = (cfg.dim, cfg.head_dim, cfg.rope_head_dim);
7594    let inv_freq = &g.inv_freq_window;
7595    let Some(stage0) = mtp.first() else { return };
7596    let (Some(mp), Some(mn)) = (stage0.main_proj.as_ref(), stage0.main_norm.as_ref()) else {
7597        return;
7598    };
7599    let mut main_x = vec![0.0f32; dim];
7600    mp.matvec(&ds.main_hidden, &mut main_x, pool);
7601    rms_weighted(&mut main_x, mn, cfg.norm_eps);
7602    for (si, m) in mtp.iter().enumerate() {
7603        let kvw = m.layer.wkv.rows();
7604        if ds.win[si].len() < cfg.window * kvw {
7605            ds.win[si].resize(cfg.window * kvw, 0.0);
7606        }
7607        let mut kv = vec![0.0f32; kvw];
7608        m.layer.wkv.matvec(&main_x, &mut kv, pool);
7609        rms_weighted(&mut kv, &m.layer.kv_norm, cfg.norm_eps);
7610        rope_tail(&mut kv[kvw - hd..], inv_freq, pos, rd, false);
7611        let slot = pos % cfg.window;
7612        ds.win[si][slot * kvw..(slot + 1) * kvw].copy_from_slice(&kv);
7613        ds.filled[si] = (pos + 1).min(cfg.window);
7614    }
7615}
7616
7617/// The draft block on the card: one submission for all three stages and
7618/// five positions, states home in one fence, the head on the host. The
7619/// markov bias is skipped (its per-position chain through the previous
7620/// PROPOSAL is the one part a single graph cannot batch) — compare against
7621/// the CPU draft under `CMF_DSPARK_NO_MARKOV=1`.
7622#[cfg(feature = "gpu")]
7623#[allow(clippy::too_many_arguments)]
7624pub fn dspark_draft_gpu(
7625    g: &Dsv4Globals,
7626    mtp: &[Dsv4Mtp],
7627    cfg: &Dsv4Cfg,
7628    ds: &mut DsparkState,
7629    pack: &DsparkPack,
7630    kv_id: u64,
7631    last_token: u32,
7632    pos: usize,
7633    pool: Option<&crate::pool::Pool>,
7634    out_conf: &mut Vec<f32>,
7635) -> Vec<u32> {
7636    let (hc, dim) = (cfg.hc_mult, cfg.dim);
7637    let block = dspark_block();
7638    let Some(model) = mtp[0].layer.experts.first().and_then(|e| e.w1.model_arc()) else {
7639        return Vec::new();
7640    };
7641    let (Some(mp), Some(mn)) = (mtp[0].main_proj.as_ref(), mtp[0].main_norm.as_ref()) else {
7642        return Vec::new();
7643    };
7644    let Some(mp_idx) = mp.model_idx() else {
7645        return Vec::new();
7646    };
7647    let mut stages = Vec::with_capacity(mtp.len());
7648    for (si, m) in mtp.iter().enumerate() {
7649        let l = &m.layer;
7650        let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b), Some(wkv)) = (
7651            l.wq_a.model_idx(),
7652            l.wq_b.model_idx(),
7653            l.wo_a.model_idx(),
7654            l.wo_b.model_idx(),
7655            l.wkv.model_idx(),
7656        ) else {
7657            return Vec::new();
7658        };
7659        let sp = &pack.stages[si];
7660        stages.push(crate::gpu_wgpu::DsparkStageW {
7661            wq_a,
7662            wq_b,
7663            wo_a,
7664            wo_b,
7665            wkv,
7666            q_norm: &l.q_norm,
7667            kv_norm: &l.kv_norm,
7668            attn_norm: &l.attn_norm,
7669            ffn_norm: &l.ffn_norm,
7670            sink: &l.attn_sink,
7671            hc_attn_fn: &l.hc_attn_fn,
7672            hc_attn_scale: &l.hc_attn_scale,
7673            hc_attn_base: &l.hc_attn_base,
7674            hc_ffn_fn: &l.hc_ffn_fn,
7675            hc_ffn_scale: &l.hc_ffn_scale,
7676            hc_ffn_base: &l.hc_ffn_base,
7677            router: &pack.routers[si],
7678            bias: pack.biases[si].as_deref(),
7679            experts: &sp.tensors,
7680            mask_u32: &sp.mask_u32,
7681            map_u32: &sp.map_u32,
7682        });
7683    }
7684    let geom = crate::gpu_wgpu::DsparkGeom {
7685        dim,
7686        hc,
7687        nh: cfg.n_heads,
7688        hd: cfg.head_dim,
7689        rd: cfg.rope_head_dim,
7690        q_lora: cfg.q_lora_rank,
7691        o_lora: cfg.o_lora_rank,
7692        o_groups: cfg.o_groups,
7693        inter: cfg.moe_inter,
7694        n_experts: cfg.n_routed_experts,
7695        top_k: cfg.top_k,
7696        window: cfg.window,
7697        eps: cfg.norm_eps,
7698        hc_eps: cfg.hc_eps,
7699        sinkhorn_iters: cfg.hc_sinkhorn_iters,
7700        route_scale: cfg.route_scale,
7701        swiglu_limit: cfg.swiglu_limit,
7702        scale: (cfg.head_dim as f32).powf(-0.5),
7703        gu_q2: pack.gu_q2,
7704        dn_q2: pack.dn_q2,
7705    };
7706    // ── seed states: the real token, then noise, replicated over copies ──
7707    let ids: Vec<u32> = (0..block)
7708        .map(|i| {
7709            if i == 0 {
7710                last_token
7711            } else {
7712                DSPARK_NOISE_TOKEN
7713            }
7714        })
7715        .collect();
7716    let mut states0 = vec![0.0f32; block * hc * dim];
7717    let mut emb = vec![0.0f32; dim];
7718    for (i, &id) in ids.iter().enumerate() {
7719        g.embed.row_f32(id as usize, &mut emb);
7720        for j in 0..hc {
7721            states0[(i * hc + j) * dim..(i * hc + j + 1) * dim].copy_from_slice(&emb);
7722        }
7723    }
7724    let dspark_time = {
7725        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7726        *ON.get_or_init(|| std::env::var("CMF_DSPARK_TIME").is_ok_and(|v| v != "0"))
7727    };
7728    let t0 = std::time::Instant::now();
7729    let filled = (pos + 1).min(cfg.window);
7730    let mut states = vec![0.0f32; block * hc * dim];
7731    if !crate::gpu_wgpu::dspark_graph(
7732        &model,
7733        &stages,
7734        geom,
7735        kv_id,
7736        mp_idx,
7737        mn,
7738        &ds.main_hidden,
7739        &states0,
7740        pos,
7741        filled,
7742        &g.inv_freq_window,
7743        block,
7744        &mut states,
7745    ) {
7746        return Vec::new();
7747    }
7748    for si in 0..mtp.len() {
7749        ds.filled[si] = filled;
7750    }
7751    let t_graph = t0.elapsed();
7752
7753    // ── head, on the host: fold, norm, one B-wide matmat, argmax ──
7754    let last = &mtp[mtp.len() - 1];
7755    let (Some(hfn), Some(hbase), Some(hscale), Some(hnorm)) = (
7756        last.hc_head_fn.as_ref(),
7757        last.hc_head_base.as_ref(),
7758        last.hc_head_scale,
7759        last.norm.as_ref(),
7760    ) else {
7761        return Vec::new();
7762    };
7763    let mut head_in = vec![0.0f32; block * dim];
7764    let mut pre_norms = vec![vec![0.0f32; dim]; block];
7765    for i in 0..block {
7766        hc_head_fold(
7767            &states[i * hc * dim..(i + 1) * hc * dim],
7768            hfn,
7769            hscale,
7770            hbase,
7771            cfg,
7772            pool,
7773            &mut head_in[i * dim..(i + 1) * dim],
7774        );
7775        pre_norms[i].copy_from_slice(&head_in[i * dim..(i + 1) * dim]);
7776        rms_weighted(&mut head_in[i * dim..(i + 1) * dim], hnorm, cfg.norm_eps);
7777    }
7778    let t_fold = t0.elapsed();
7779    let mut logits = vec![0.0f32; block * cfg.vocab];
7780    // The B-axis q4tp kernel, one submission: `matmat` at B=5 falls to the
7781    // CPU tile path and measured 46 ms of a 60 ms draft.
7782    let head_gpu = g.head.model_idx().is_some_and(|hi| {
7783        crate::gpu_wgpu::q4tp_matvec_batch_for_test(
7784            &model,
7785            hi,
7786            &head_in,
7787            block,
7788            cfg.vocab,
7789            dim,
7790            &mut logits,
7791        )
7792    });
7793    if !head_gpu {
7794        g.head.matmat(&head_in, block, &mut logits, pool);
7795    }
7796    let t_head = t0.elapsed();
7797    // The markov bigram is not optional: without it acceptance fell 1.02 →
7798    // 0.42 on natural text. Its chain runs through the previous PROPOSAL,
7799    // so it stays position-by-position; the w2 matvec is big enough that
7800    // the QTensor route puts it on the card by itself.
7801    let mut proposals = Vec::with_capacity(block);
7802    out_conf.clear();
7803    let mut prev = last_token;
7804    let mut mk_embed = vec![0.0f32; last.markov_w1.as_ref().map_or(0, |t| t.cols())];
7805    let mut bias = vec![0.0f32; cfg.vocab];
7806    for i in 0..block {
7807        let row = &mut logits[i * cfg.vocab..(i + 1) * cfg.vocab];
7808        if let (Some(w1), Some(w2)) = (last.markov_w1.as_ref(), last.markov_w2.as_ref()) {
7809            w1.row_f32(prev as usize, &mut mk_embed);
7810            w2.matvec(&mk_embed, &mut bias, pool);
7811            for (a, b) in row.iter_mut().zip(&bias) {
7812                *a += *b;
7813            }
7814        }
7815        let mut best = 0usize;
7816        for v in 1..row.len() {
7817            if row[v] > row[best] {
7818                best = v;
7819            }
7820        }
7821        if let Some(cf) = last.confidence.as_ref() {
7822            let mut cat = pre_norms[i].clone();
7823            cat.extend_from_slice(&mk_embed);
7824            let mut sc = [0.0f32; 1];
7825            if cat.len() == cf.cols() {
7826                cf.matvec(&cat, &mut sc, pool);
7827            }
7828            out_conf.push(sc[0]);
7829        }
7830        proposals.push(best as u32);
7831        prev = best as u32;
7832    }
7833    if dspark_time {
7834        eprintln!(
7835            "DSpark GPU: граф {:.1} мс, фолды {:.1}, голова {:.1}, марков+argmax {:.1}",
7836            t_graph.as_secs_f64() * 1e3,
7837            (t_fold - t_graph).as_secs_f64() * 1e3,
7838            (t_head - t_fold).as_secs_f64() * 1e3,
7839            (t0.elapsed() - t_head).as_secs_f64() * 1e3,
7840        );
7841    }
7842    proposals
7843}
7844
7845/// One draft: `DSPARK_BLOCK` proposed tokens and a confidence per position.
7846///
7847/// `pos` is the position of `last_token` — the block predicts `pos+1 ..
7848/// pos+BLOCK`. Returns the proposals in order; `out_conf` takes the
7849/// confidence head's score where the last stage carries one.
7850#[allow(clippy::too_many_arguments)]
7851pub fn dspark_draft(
7852    g: &Dsv4Globals,
7853    mtp: &[Dsv4Mtp],
7854    cfg: &Dsv4Cfg,
7855    ds: &mut DsparkState,
7856    last_token: u32,
7857    pos: usize,
7858    pool: Option<&crate::pool::Pool>,
7859    out_conf: &mut Vec<f32>,
7860) -> Vec<u32> {
7861    let (hc, dim, hd, rd) = (cfg.hc_mult, cfg.dim, cfg.head_dim, cfg.rope_head_dim);
7862    let block = dspark_block();
7863    let inv_freq = &g.inv_freq_window;
7864
7865    // ── the block's input: main_norm(main_proj(captured hiddens)) ──
7866    let Some(stage0) = mtp.first() else {
7867        return Vec::new();
7868    };
7869    let (Some(mp), Some(mn)) = (stage0.main_proj.as_ref(), stage0.main_norm.as_ref()) else {
7870        return Vec::new();
7871    };
7872    dspark_ring_append(g, mtp, cfg, ds, pos, pool);
7873
7874    // ── the block: the real token, then noise ──
7875    let ids: Vec<u32> = (0..block)
7876        .map(|i| {
7877            if i == 0 {
7878                last_token
7879            } else {
7880                DSPARK_NOISE_TOKEN
7881            }
7882        })
7883        .collect();
7884    let mut states = vec![vec![0.0f32; hc * dim]; block];
7885    let mut emb = vec![0.0f32; dim];
7886    for (i, &id) in ids.iter().enumerate() {
7887        g.embed.row_f32(id as usize, &mut emb);
7888        for j in 0..hc {
7889            states[i][j * dim..(j + 1) * dim].copy_from_slice(&emb);
7890        }
7891    }
7892
7893    let mut scratch = HcScratch::new(cfg);
7894    for (si, m) in mtp.iter().enumerate() {
7895        let l = &m.layer;
7896        let kvw = l.wkv.rows();
7897        // ── attention half: fold every position first, because each one's
7898        //    keys are visible to all the others. ──
7899        let mut post = vec![vec![0.0f32; hc]; block];
7900        let mut comb = vec![vec![0.0f32; hc * hc]; block];
7901        let mut resid = vec![vec![0.0f32; hc * dim]; block];
7902        let mut folded = vec![vec![0.0f32; dim]; block];
7903        let mix_hc = (2 + hc) * hc;
7904        for i in 0..block {
7905            hc_mixes(
7906                &states[i],
7907                &l.hc_attn_fn,
7908                mix_hc,
7909                cfg.norm_eps,
7910                pool,
7911                &mut scratch.mixes,
7912            );
7913            hc_split_sinkhorn(
7914                &scratch.mixes,
7915                &l.hc_attn_scale,
7916                &l.hc_attn_base,
7917                hc,
7918                cfg.hc_sinkhorn_iters,
7919                cfg.hc_eps,
7920                &mut scratch.pre,
7921                &mut post[i],
7922                &mut comb[i],
7923            );
7924            hc_fold(&states[i], &scratch.pre, hc, dim, &mut folded[i]);
7925            rms_weighted(&mut folded[i], &l.attn_norm, cfg.norm_eps);
7926            resid[i].copy_from_slice(&states[i]);
7927        }
7928        // Keys and values of the block itself — kept for this block only.
7929        let folded_all: Vec<f32> = folded.iter().flatten().copied().collect();
7930        let mut blk_kv = vec![0.0f32; block * kvw];
7931        l.wkv.matmat(&folded_all, block, &mut blk_kv, pool);
7932        for i in 0..block {
7933            let dst = &mut blk_kv[i * kvw..(i + 1) * kvw];
7934            rms_weighted(dst, &l.kv_norm, cfg.norm_eps);
7935            rope_tail(&mut dst[kvw - hd..], inv_freq, pos + 1 + i, rd, false);
7936        }
7937        // The attended set: every cached real position, then the whole block.
7938        let win_len = ds.filled[si];
7939        let mut cache = Vec::with_capacity((win_len + block) * hd);
7940        for p in 0..win_len {
7941            let e = &ds.win[si][p * kvw..(p + 1) * kvw];
7942            cache.extend_from_slice(&e[kvw - hd..]);
7943        }
7944        for i in 0..block {
7945            let e = &blk_kv[i * kvw..(i + 1) * kvw];
7946            cache.extend_from_slice(&e[kvw - hd..]);
7947        }
7948        let idxs: Vec<usize> = (0..win_len + block).collect();
7949        let scale = (hd as f32).powf(-0.5);
7950        let qrank = l.wq_a.rows();
7951        let qdim = cfg.n_heads * hd;
7952        let mut qr = vec![0.0f32; block * qrank];
7953        l.wq_a.matmat(&folded_all, block, &mut qr, pool);
7954        for i in 0..block {
7955            rms_weighted(&mut qr[i * qrank..(i + 1) * qrank], &l.q_norm, cfg.norm_eps);
7956        }
7957        let mut q = vec![0.0f32; block * qdim];
7958        l.wq_b.matmat(&qr, block, &mut q, pool);
7959        let mut attn = vec![0.0f32; block * qdim];
7960        for i in 0..block {
7961            let qi = &mut q[i * qdim..(i + 1) * qdim];
7962            let ai = &mut attn[i * qdim..(i + 1) * qdim];
7963            let qpos = pos + 1 + i;
7964            for h in 0..cfg.n_heads {
7965                let head = &mut qi[h * hd..(h + 1) * hd];
7966                rms_inplace(head, cfg.norm_eps);
7967                rope_tail(head, inv_freq, qpos, rd, false);
7968            }
7969            for h in 0..cfg.n_heads {
7970                let qh = &qi[h * hd..(h + 1) * hd];
7971                let oh = &mut ai[h * hd..(h + 1) * hd];
7972                sparse_attend(qh, &cache, &idxs, l.attn_sink[h], scale, hd, oh);
7973                rope_tail(oh, inv_freq, qpos, rd, true);
7974            }
7975        }
7976        let mut blk_out = vec![0.0f32; block * dim];
7977        o_project_block(
7978            &attn,
7979            block,
7980            &l.wo_a,
7981            &l.wo_b,
7982            cfg.o_groups,
7983            cfg.o_lora_rank,
7984            pool,
7985            &mut blk_out,
7986        );
7987        for i in 0..block {
7988            let mut next = vec![0.0f32; hc * dim];
7989            hc_expand(
7990                &blk_out[i * dim..(i + 1) * dim],
7991                &resid[i],
7992                &post[i],
7993                &comb[i],
7994                hc,
7995                dim,
7996                &mut next,
7997            );
7998            states[i] = next;
7999        }
8000        // ── MoE: fold every position, group equal experts, then expand in
8001        //    the original per-position route order. ──
8002        let mut ffn_fold = vec![0.0f32; block * dim];
8003        let mut ffn_post = vec![vec![0.0f32; hc]; block];
8004        let mut ffn_comb = vec![vec![0.0f32; hc * hc]; block];
8005        let mut ffn_resid = vec![vec![0.0f32; hc * dim]; block];
8006        for i in 0..block {
8007            hc_mixes(
8008                &states[i],
8009                &l.hc_ffn_fn,
8010                mix_hc,
8011                cfg.norm_eps,
8012                pool,
8013                &mut scratch.mixes,
8014            );
8015            hc_split_sinkhorn(
8016                &scratch.mixes,
8017                &l.hc_ffn_scale,
8018                &l.hc_ffn_base,
8019                hc,
8020                cfg.hc_sinkhorn_iters,
8021                cfg.hc_eps,
8022                &mut scratch.pre,
8023                &mut ffn_post[i],
8024                &mut ffn_comb[i],
8025            );
8026            hc_fold(
8027                &states[i],
8028                &scratch.pre,
8029                hc,
8030                dim,
8031                &mut ffn_fold[i * dim..(i + 1) * dim],
8032            );
8033            rms_weighted(
8034                &mut ffn_fold[i * dim..(i + 1) * dim],
8035                &l.ffn_norm,
8036                cfg.norm_eps,
8037            );
8038            ffn_resid[i].copy_from_slice(&states[i]);
8039        }
8040        let mut moe_out = vec![0.0f32; block * dim];
8041        moe_step_block(&ffn_fold, block, l, cfg, &ids, si, pool, &mut moe_out);
8042        for i in 0..block {
8043            let mut next = vec![0.0f32; hc * dim];
8044            hc_expand(
8045                &moe_out[i * dim..(i + 1) * dim],
8046                &ffn_resid[i],
8047                &ffn_post[i],
8048                &ffn_comb[i],
8049                hc,
8050                dim,
8051                &mut next,
8052            );
8053            states[i] = next;
8054        }
8055    }
8056
8057    // ── head: the last stage's fold, the trunk's own head ──
8058    let last = &mtp[mtp.len() - 1];
8059    let (Some(hfn), Some(hbase), Some(hscale), Some(hnorm)) = (
8060        last.hc_head_fn.as_ref(),
8061        last.hc_head_base.as_ref(),
8062        last.hc_head_scale,
8063        last.norm.as_ref(),
8064    ) else {
8065        return Vec::new();
8066    };
8067    let mut proposals = Vec::with_capacity(block);
8068    out_conf.clear();
8069    let mut prev = last_token;
8070    let mut head_in = vec![0.0f32; block * dim];
8071    let mut pre_norms = vec![vec![0.0f32; dim]; block];
8072    for i in 0..block {
8073        hc_head_fold(
8074            &states[i],
8075            hfn,
8076            hscale,
8077            hbase,
8078            cfg,
8079            pool,
8080            &mut head_in[i * dim..(i + 1) * dim],
8081        );
8082        pre_norms[i].copy_from_slice(&head_in[i * dim..(i + 1) * dim]);
8083        rms_weighted(&mut head_in[i * dim..(i + 1) * dim], hnorm, cfg.norm_eps);
8084    }
8085    let mut logits = vec![0.0f32; block * cfg.vocab];
8086    g.head.matmat(&head_in, block, &mut logits, pool);
8087    let mut mk_embed = vec![0.0f32; last.markov_w1.as_ref().map_or(0, |t| t.cols())];
8088    for i in 0..block {
8089        let logits_i = &mut logits[i * cfg.vocab..(i + 1) * cfg.vocab];
8090        // The markov head biases the logits from the PREVIOUS token — a
8091        // rank-256 bigram the draft samples through position by position,
8092        // while the network itself ran the whole block at once.
8093        // `CMF_DSPARK_NO_MARKOV=1` drops it: the bias is sequential through
8094        // the block (each position needs the previous PROPOSAL), which is
8095        // the one part of the draft a single device graph cannot batch — so
8096        // its acceptance value has to be known before it earns that
8097        // complexity.
8098        let no_markov = {
8099            static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8100            *ON.get_or_init(|| std::env::var("CMF_DSPARK_NO_MARKOV").is_ok_and(|v| v != "0"))
8101        };
8102        if no_markov {
8103            // Still feed the confidence head's embedding slot below.
8104            if let Some(w1) = last.markov_w1.as_ref() {
8105                w1.row_f32(prev as usize, &mut mk_embed);
8106            }
8107        } else if let (Some(w1), Some(w2)) = (last.markov_w1.as_ref(), last.markov_w2.as_ref()) {
8108            w1.row_f32(prev as usize, &mut mk_embed);
8109            let mut bias = vec![0.0f32; cfg.vocab];
8110            w2.matvec(&mk_embed, &mut bias, pool);
8111            for (a, b) in logits_i.iter_mut().zip(&bias) {
8112                *a += *b;
8113            }
8114        }
8115        let mut best = 0usize;
8116        for v in 1..logits_i.len() {
8117            if logits_i[v] > logits_i[best] {
8118                best = v;
8119            }
8120        }
8121        if let Some(cf) = last.confidence.as_ref() {
8122            let mut cat = pre_norms[i].clone();
8123            cat.extend_from_slice(&mk_embed);
8124            let mut s = [0.0f32; 1];
8125            if cat.len() == cf.cols() {
8126                cf.matvec(&cat, &mut s, pool);
8127            }
8128            out_conf.push(s[0]);
8129        }
8130        proposals.push(best as u32);
8131        prev = best as u32;
8132    }
8133    proposals
8134}