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}
894
895impl Dsv4State {
896    pub fn new(layers: usize) -> Self {
897        use std::sync::atomic::{AtomicU64, Ordering};
898        static NEXT: AtomicU64 = AtomicU64::new(1);
899        Self {
900            kv_id: NEXT.fetch_add(1, Ordering::Relaxed),
901            window: vec![Vec::new(); layers],
902            compressed: vec![Vec::new(); layers],
903            index_kv: vec![Vec::new(); layers],
904            pending_kv: vec![Vec::new(); layers],
905            pending_score: vec![Vec::new(); layers],
906            prev_kv: vec![Vec::new(); layers],
907            prev_score: vec![Vec::new(); layers],
908            pending_ix_kv: vec![Vec::new(); layers],
909            pending_ix_score: vec![Vec::new(); layers],
910            prev_ix_kv: vec![Vec::new(); layers],
911            prev_ix_score: vec![Vec::new(); layers],
912            pos: 0,
913        }
914    }
915}
916
917/// One attention block for a single position. `hidden` is the folded,
918/// normalized vector `hc_block` hands over; the result goes back to it.
919///
920/// The order matters and is the reference's: q through the LoRA pair with
921/// a normalization at each end, kv compressed to one head's width, rope on
922/// the tails, the window and the compressed positions concatenated into
923/// one index list, sparse attention with the sink, the INVERSE rope on the
924/// output, then the grouped low-rank projection.
925#[allow(clippy::too_many_arguments)]
926/// Advance one compressor by a token and return its folded entry when the
927/// window closes. Both the attention compressor and the indexer's own run
928/// through here — the indexer's was simply never called, so its cache stayed
929/// empty and every layer that has an indexer selected ZERO compressed
930/// positions, discarding a correctly-built long-range memory.
931#[allow(clippy::too_many_arguments)]
932fn compressor_step(
933    cp: &Dsv4Compressor,
934    hidden: &[f32],
935    pos: usize,
936    rd: usize,
937    norm_eps: f32,
938    inv_freq: &[f32],
939    pool: Option<&crate::pool::Pool>,
940    pending_kv: &mut Vec<f32>,
941    pending_score: &mut Vec<f32>,
942    prev_kv: &mut Vec<f32>,
943    prev_score: &mut Vec<f32>,
944) -> Option<Vec<f32>> {
945    let width = cp.wkv.rows();
946    let ew = if cp.overlap { width / 2 } else { width };
947    let mut ckv = vec![0.0f32; width];
948    let mut cscore = vec![0.0f32; width];
949    cp.wkv.matvec(hidden, &mut ckv, pool);
950    cp.wgate.matvec(hidden, &mut cscore, pool);
951    if cp.overlap {
952        // The reference biases the score as the token arrives and keeps it
953        // biased across the shift, so ape is added ONCE, here.
954        let slot = pos % cp.ratio;
955        for (c, a) in cscore
956            .iter_mut()
957            .zip(&cp.ape[slot * width..(slot + 1) * width])
958        {
959            *c += a;
960        }
961    }
962    pending_kv.extend_from_slice(&ckv);
963    pending_score.extend_from_slice(&cscore);
964    if pending_kv.len() / width < cp.ratio {
965        return None;
966    }
967    let mut folded = vec![0.0f32; ew];
968    if cp.overlap {
969        compress_window_overlap(
970            prev_kv,
971            prev_score,
972            pending_kv,
973            pending_score,
974            cp.ratio,
975            ew,
976            &mut folded,
977        );
978        *prev_kv = std::mem::take(pending_kv);
979        *prev_score = std::mem::take(pending_score);
980    } else {
981        compress_window(
982            pending_kv,
983            pending_score,
984            &cp.ape,
985            cp.ratio,
986            width,
987            &mut folded,
988        );
989    }
990    rms_weighted(&mut folded, &cp.norm, norm_eps);
991    // The entry carries the same rope-tagged tail as a window key, at the
992    // position of the window's first token.
993    rope_tail(&mut folded, inv_freq, pos + 1 - cp.ratio, rd, false);
994    pending_kv.clear();
995    pending_score.clear();
996    Some(folded)
997}
998
999/// `CMF_DSV4_PROFILE=1` accumulates wall time per stage and prints the split
1000/// when the process ends. Guessing which half of a layer costs what is how
1001/// one ends up optimising the cheap one: the fused attention block came out a
1002/// wash on the release checkpoint, and no amount of reasoning about MAC
1003/// counts settles whether that is because attention was already cheap or
1004/// because the device arm was slow.
1005pub(crate) mod prof {
1006    use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
1007
1008    pub static ATTN_NS: AtomicU64 = AtomicU64::new(0);
1009    pub static MOE_NS: AtomicU64 = AtomicU64::new(0);
1010    pub static CALLS: AtomicU64 = AtomicU64::new(0);
1011    /// Everything in a layer that is neither attention nor the experts: the
1012    /// hyper-connection fold and expand, the two norms, the residual.
1013    pub static HC_NS: AtomicU64 = AtomicU64::new(0);
1014    /// The head: final norm plus lm_head over 129280 rows.
1015    pub static HEAD_NS: AtomicU64 = AtomicU64::new(0);
1016    /// The whole forward, so the buckets can be checked against a total
1017    /// instead of against a guess. 78 ms of measured work in a 108 ms token
1018    /// left 30 ms that no counter had ever looked at.
1019    pub static ALL_NS: AtomicU64 = AtomicU64::new(0);
1020    pub static TOKENS: AtomicU64 = AtomicU64::new(0);
1021
1022    /// One token = one visit to layer zero. Counting `moe_step` calls instead
1023    /// counts layers.
1024    pub fn note_layer(li: usize) {
1025        CALLS.fetch_add(1, Ordering::Relaxed);
1026        if li == 0 {
1027            TOKENS.fetch_add(1, Ordering::Relaxed);
1028        }
1029    }
1030    static REPORT: AtomicBool = AtomicBool::new(false);
1031
1032    pub fn on() -> bool {
1033        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1034        *ON.get_or_init(|| std::env::var("CMF_DSV4_PROFILE").is_ok_and(|v| v != "0"))
1035    }
1036
1037    /// Print once, from wherever the last caller happens to be — a process
1038    /// that exits through several paths would otherwise report zero or twice.
1039    pub fn report() {
1040        if !on() || REPORT.swap(true, Ordering::Relaxed) {
1041            return;
1042        }
1043        // CALLS counts layer visits, not tokens — dividing by it and calling
1044        // the result "per token" is off by the layer count, which is 43 on
1045        // the release and reads as a plausible number either way.
1046        let calls = CALLS.load(Ordering::Relaxed).max(1);
1047        let toks = TOKENS.load(Ordering::Relaxed).max(1);
1048        let (a, m) = (
1049            ATTN_NS.load(Ordering::Relaxed) as f64 / 1e6,
1050            MOE_NS.load(Ordering::Relaxed) as f64 / 1e6,
1051        );
1052        let all = ALL_NS.load(Ordering::Relaxed) as f64 / 1e6;
1053        let hc = HC_NS.load(Ordering::Relaxed) as f64 / 1e6;
1054        let hd = HEAD_NS.load(Ordering::Relaxed) as f64 / 1e6;
1055        eprintln!(
1056            "[dsv4-профиль] {calls} вызовов слоя за {toks} токенов | \
1057             на токен: внимание {:.0} мс, MoE {:.0} мс, гипер-связи+нормы {:.0} мс, \
1058             голова {:.0} мс | на вызов: внимание {:.2}, MoE {:.2}, связи {:.2}",
1059            a / toks as f64,
1060            m / toks as f64,
1061            hc / toks as f64,
1062            hd / toks as f64,
1063            a / calls as f64,
1064            m / calls as f64,
1065            hc / calls as f64,
1066        );
1067        eprintln!(
1068            "[dsv4-профиль] весь проход {:.0} мс на токен; вне счётчиков {:.0} мс",
1069            all / toks as f64,
1070            (all - a - m - hd) / toks as f64,
1071        );
1072        #[cfg(feature = "gpu")]
1073        {
1074            let ae = crate::gpu_wgpu::ATT_ENC_NS.load(Ordering::Relaxed) as f64 / 1e6;
1075            let aw = crate::gpu_wgpu::ATT_WAIT_NS.load(Ordering::Relaxed) as f64 / 1e6;
1076            if ae + aw > 0.0 {
1077                eprintln!(
1078                    "[dsv4-профиль] кадр внимания на вызов: кодирование {:.2} мс, \
1079                     отправка и ожидание {:.2} мс",
1080                    ae / calls as f64,
1081                    aw / calls as f64,
1082                );
1083            }
1084            let e = crate::gpu_wgpu::MOE_ENC_NS.load(Ordering::Relaxed) as f64 / 1e6;
1085            let wt = crate::gpu_wgpu::MOE_WAIT_NS.load(Ordering::Relaxed) as f64 / 1e6;
1086            if e + wt > 0.0 {
1087                eprintln!(
1088                    "[dsv4-профиль] кадр MoE на вызов: кодирование {:.2} мс, \
1089                     отправка и ожидание {:.2} мс",
1090                    e / calls as f64,
1091                    wt / calls as f64,
1092                );
1093            }
1094        }
1095    }
1096}
1097
1098/// Print the per-token split, if `CMF_DSV4_PROFILE` asked for one.
1099pub fn profile_report() {
1100    prof::report();
1101}
1102
1103/// `CMF_DSV4_GPU_ATTN=1` moves the attention block onto the device as one
1104/// submission. Off by default: it needs every attention weight in q4tp and a
1105/// working wgpu context, and a frame that declines mid-layer after the state
1106/// has been advanced would be worse than one that never ran.
1107fn gpu_attn_enabled() -> bool {
1108    #[cfg(feature = "gpu")]
1109    {
1110        use std::sync::OnceLock;
1111        static ON: OnceLock<bool> = OnceLock::new();
1112        *ON.get_or_init(|| {
1113            let want = std::env::var("CMF_DSV4_GPU_ATTN")
1114                .map(|v| v != "0")
1115                .unwrap_or(false);
1116            let have = want && crate::gpu::backend_available();
1117            if want && !have {
1118                tracing::warn!(
1119                    "CMF_DSV4_GPU_ATTN задан, но устройства нет — блок внимания                      остаётся на CPU. Проверьте CMF_GPU=wgpu и Vulkan-ICD."
1120                );
1121            }
1122            if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
1123                eprintln!("кадр dsv4: запрошен={want} доступен={have}");
1124            }
1125            have
1126        })
1127    }
1128    #[cfg(not(feature = "gpu"))]
1129    {
1130        false
1131    }
1132}
1133
1134/// The device half of `attention_step`. Returns false — having changed
1135/// nothing — whenever it cannot do the whole block, so the caller's CPU path
1136/// is still correct to run.
1137#[cfg(feature = "gpu")]
1138#[allow(clippy::too_many_arguments)]
1139fn attn_frame(
1140    l: &Dsv4Layer,
1141    cfg: &Dsv4Cfg,
1142    st: &Dsv4State,
1143    li: usize,
1144    qn: &[f32],
1145    idxs: &[usize],
1146    inv_freq: &[f32],
1147    pos: usize,
1148    win_len: usize,
1149    scale: f32,
1150    out: &mut [f32],
1151) -> bool {
1152    let hd = cfg.head_dim;
1153    let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b)) = (
1154        l.wq_a.model_idx(),
1155        l.wq_b.model_idx(),
1156        l.wo_a.model_idx(),
1157        l.wo_b.model_idx(),
1158    ) else {
1159        return false;
1160    };
1161    let Some(model) = l.wq_b.model_arc() else {
1162        return false;
1163    };
1164    // Fixed window region, then the compressed tail — so a token writes one
1165    // window slot's worth of movement and whatever the compressor just added,
1166    // not the whole cache. `cap` has to cover the longest run this sequence
1167    // will reach; the compressed axis grows by one entry per `ratio` tokens.
1168    let n_comp = st.compressed[li].len() / hd;
1169    let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
1170    let kv_id = st.kv_id;
1171    // The window is rewritten whole. A ring would write one slot instead of
1172    // 128 — 2 KB against 256 — and was tried: it bought NOTHING (the cost is
1173    // per-dispatch driver bookkeeping, not the copy) and moved perplexity by
1174    // 6e-5 because the attended positions arrive in a different order and the
1175    // softmax accumulates differently. Not a trade worth making.
1176    if !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, 0, &st.window[li], cap) {
1177        return false;
1178    }
1179    // The compressed axis only ever grows, so write the TAIL. Rewriting it
1180    // whole was 22 MB a token at 1024 positions — the cache write, not the
1181    // arithmetic, was what the attention block had left to pay.
1182    // The compressed tail is written WHOLE every token. Writing only the new
1183    // part was tried and gave nothing measurable, and the bookkeeping it
1184    // needs — a per-layer tail count invalidated by every buffer growth — is
1185    // exactly the kind of state that drifts silently and shows up as a model
1186    // that stops early. Not worth carrying for zero.
1187    if n_comp > 0
1188        && !crate::gpu_wgpu::dsv4_cache_write(
1189            kv_id,
1190            li,
1191            cfg.window * hd,
1192            &st.compressed[li],
1193            cap,
1194        )
1195    {
1196        return false;
1197    }
1198    let idx32: Vec<u32> = idxs
1199        .iter()
1200        .map(|&p| {
1201            if p < win_len {
1202                p as u32
1203            } else {
1204                (cfg.window + (p - win_len)) as u32
1205            }
1206        })
1207        .collect();
1208    let w = crate::gpu_wgpu::Dsv4AttnW {
1209        wq_a,
1210        wq_b,
1211        wo_a,
1212        wo_b,
1213        q_norm: &l.q_norm,
1214        sink: &l.attn_sink,
1215    };
1216    let g = crate::gpu_wgpu::Dsv4AttnGeom {
1217        dim: cfg.dim,
1218        nh: cfg.n_heads,
1219        hd,
1220        rd: cfg.rope_head_dim,
1221        q_lora: cfg.q_lora_rank,
1222        o_lora: cfg.o_lora_rank,
1223        o_groups: cfg.o_groups,
1224        eps: cfg.norm_eps,
1225        scale,
1226    };
1227    crate::gpu_wgpu::dsv4_attn_frame(
1228        &model,
1229        &w,
1230        g,
1231        &[],
1232        Some(qn),
1233        kv_id,
1234        li,
1235        &idx32,
1236        inv_freq,
1237        pos,
1238        out,
1239    )
1240}
1241
1242/// What the host still owes the device before a layer frame can run: the
1243/// shared LoRA vector the indexer reads, and the attended position list.
1244#[derive(Default)]
1245pub struct AttnPrep {
1246    pub qr: Vec<f32>,
1247    pub idxs: Vec<usize>,
1248    pub win_len: usize,
1249}
1250
1251#[allow(clippy::too_many_arguments)]
1252pub fn attention_step(
1253    hidden: &[f32],
1254    l: &Dsv4Layer,
1255    cfg: &Dsv4Cfg,
1256    st: &mut Dsv4State,
1257    li: usize,
1258    // Chosen by the caller from the layer's kind — see Dsv4Globals.
1259    inv_freq: &[f32],
1260    pool: Option<&crate::pool::Pool>,
1261    // When set, stop once the caches are advanced and the index list is
1262    // built, and hand those back instead of running attention: the layer
1263    // frame does the rest on the device.
1264    prep_out: Option<&mut AttnPrep>,
1265    out: &mut [f32],
1266) {
1267    let _t0 = prof::on().then(std::time::Instant::now);
1268    let _guard = scopeguard_attn(_t0);
1269    let (hd, rd) = (cfg.head_dim, cfg.rope_head_dim);
1270    let pos = st.pos;
1271    if std::env::var("CMF_FREQ_DEBUG").is_ok() && li == 0 && pos == 0 {
1272        eprintln!(
1273            "    [порт] rd={rd} частот={} inv_freq[0..4]={:?}",
1274            inv_freq.len(),
1275            &inv_freq[..4.min(inv_freq.len())]
1276        );
1277    }
1278
1279    // ── q: wq_a → q_norm → wq_b → per-head norm → rope tail ──
1280    let mut qr = vec![0.0f32; cfg.q_lora_rank];
1281    l.wq_a.matvec(hidden, &mut qr, pool);
1282    rms_weighted(&mut qr, &l.q_norm, cfg.norm_eps);
1283    // The queries are built further down, after the frame has had its chance
1284    // at the whole block. `qr` is needed either way: the indexer reads it.
1285    let on_gpu = gpu_attn_enabled();
1286
1287    // ── kv: one head's width, shared by every query head ──
1288    let mut kv = vec![0.0f32; hd];
1289    l.wkv.matvec(hidden, &mut kv, pool);
1290    rms_weighted(&mut kv, &l.kv_norm, cfg.norm_eps);
1291    rope_tail(&mut kv, inv_freq, pos, rd, false);
1292
1293    // ── the compressor: accumulate `ratio` tokens, then fold them into
1294    // one compressed entry. The reference fires when (pos+1) % ratio == 0,
1295    // so a partial window simply waits — which is why the state carries
1296    // the pending streams across tokens.
1297    if let Some(cp) = &l.compressor {
1298        let mut pk = std::mem::take(&mut st.pending_kv[li]);
1299        let mut ps = std::mem::take(&mut st.pending_score[li]);
1300        let mut qk = std::mem::take(&mut st.prev_kv[li]);
1301        let mut qs = std::mem::take(&mut st.prev_score[li]);
1302        let entry = compressor_step(
1303            cp,
1304            hidden,
1305            pos,
1306            rd,
1307            cfg.norm_eps,
1308            inv_freq,
1309            pool,
1310            &mut pk,
1311            &mut ps,
1312            &mut qk,
1313            &mut qs,
1314        );
1315        st.pending_kv[li] = pk;
1316        st.pending_score[li] = ps;
1317        st.prev_kv[li] = qk;
1318        st.prev_score[li] = qs;
1319        if let Some(e) = entry {
1320            st.compressed[li].extend_from_slice(&e);
1321        }
1322    }
1323    // The indexer scores against ITS OWN compressed cache, built by its own
1324    // compressor. Without this the cache is empty, `n_ix` is zero, and every
1325    // indexer layer picks no compressed positions at all — the long-range
1326    // memory is built and then never read.
1327    if let Some(ix) = &l.indexer {
1328        let mut pk = std::mem::take(&mut st.pending_ix_kv[li]);
1329        let mut ps = std::mem::take(&mut st.pending_ix_score[li]);
1330        let mut qk = std::mem::take(&mut st.prev_ix_kv[li]);
1331        let mut qs = std::mem::take(&mut st.prev_ix_score[li]);
1332        let entry = compressor_step(
1333            &ix.compressor,
1334            hidden,
1335            pos,
1336            rd,
1337            cfg.norm_eps,
1338            inv_freq,
1339            pool,
1340            &mut pk,
1341            &mut ps,
1342            &mut qk,
1343            &mut qs,
1344        );
1345        st.pending_ix_kv[li] = pk;
1346        st.pending_ix_score[li] = ps;
1347        st.prev_ix_kv[li] = qk;
1348        st.prev_ix_score[li] = qs;
1349        if let Some(e) = entry {
1350            st.index_kv[li].extend_from_slice(&e);
1351        }
1352    }
1353
1354    st.window[li].extend_from_slice(&kv);
1355    // The reference keeps the window in a ring of `window_size`; holding the
1356    // last N in order is the same set, and without this the "window" grows
1357    // for the whole generation — wrong attention AND unbounded memory.
1358    let cap = cfg.window * hd;
1359    if st.window[li].len() > cap {
1360        let drop = st.window[li].len() - cap;
1361        st.window[li].drain(..drop);
1362    }
1363    let win_len = st.window[li].len() / hd;
1364    let n_pos = win_len + st.compressed[li].len() / hd;
1365
1366    // Index list: every window position, plus whatever the indexer picked
1367    // (or, without an indexer, every compressed position).
1368    //
1369    // CMF_DSV4_NO_COMPRESSED=1 attends to the sliding window ALONE. That is
1370    // not a mode anyone should serve — it drops the model's long-range
1371    // memory — but it separates two failure modes that look identical from
1372    // the outside: output that degrades because the compressed path is
1373    // wrong, and output that degrades because the weights are too coarse.
1374    let mut idxs: Vec<usize> = (0..win_len).collect();
1375    if !st.compressed[li].is_empty() && !no_compressed() {
1376        let n_comp = st.compressed[li].len() / hd;
1377        match &l.indexer {
1378            Some(ix) => {
1379                // The indexer scores from the SHARED LoRA output through
1380                // its own wq_b — not from attention's queries — and its
1381                // per-head weights are a projection of the hidden state,
1382                // scaled by head_dim^-0.5 * n_heads^-0.5 as the reference
1383                // folds into `weights_proj`'s output.
1384                //
1385                // The reference also applies a randomized Hadamard rotation
1386                // to the queries here and to the keys in the indexer's
1387                // compressor, then simulates FP4 on both. That transform is
1388                // orthogonal (`hadamard_transform` scaled by d^-0.5) and it
1389                // hits BOTH sides of the same dot product, so it cancels:
1390                // its purpose is to condition the FP4 quantization, which we
1391                // do not do either. Omitting the pair is exact, and keeping
1392                // f32 is strictly more accurate than the reference — not an
1393                // approximation to be fixed later.
1394                let ih = ix.weights_proj.rows();
1395                let idim = ix.wq_b.rows() / ih.max(1);
1396                let mut qi = vec![0.0f32; ix.wq_b.rows()];
1397                ix.wq_b.matvec(&qr, &mut qi, pool);
1398                for h in 0..ih {
1399                    rope_tail(&mut qi[h * idim..(h + 1) * idim], inv_freq, pos, rd, false);
1400                }
1401                let mut hw = vec![0.0f32; ih];
1402                ix.weights_proj.matvec(hidden, &mut hw, pool);
1403                let sc_factor = (idim as f32).powf(-0.5) * (ih as f32).powf(-0.5);
1404                for w in hw.iter_mut() {
1405                    *w *= sc_factor;
1406                }
1407                let n_ix = st.index_kv[li].len() / idim.max(1);
1408                let mut sc = Vec::new();
1409                index_scores(
1410                    &qi,
1411                    &st.index_kv[li],
1412                    &hw,
1413                    ih,
1414                    idim,
1415                    n_ix.min(n_comp),
1416                    n_ix.min(n_comp),
1417                    pool,
1418                    &mut sc,
1419                );
1420                let mut picked = Vec::new();
1421                top_k_positions(&sc, cfg.index_topk, &mut picked);
1422                idxs.extend(picked.into_iter().map(|p| win_len + p));
1423            }
1424            None => idxs.extend((0..n_comp).map(|p| win_len + p)),
1425        }
1426    }
1427    debug_assert!(idxs.iter().all(|&p| p < n_pos));
1428    if let Some(p) = prep_out {
1429        p.qr = qr;
1430        p.idxs = idxs;
1431        p.win_len = win_len;
1432        return;
1433    }
1434
1435    // ── the whole block on the device, or nothing ──
1436    let scale = (hd as f32).powf(-0.5);
1437    #[cfg(feature = "gpu")]
1438    if on_gpu
1439        && attn_frame(
1440            l, cfg, st, li, &qr, &idxs, inv_freq, pos, win_len, scale, out,
1441        )
1442    {
1443        return;
1444    }
1445
1446    // ── queries: wq_b, then a norm and the rope tail per head ──
1447    let mut q = vec![0.0f32; cfg.n_heads * hd];
1448    l.wq_b.matvec(&qr, &mut q, pool);
1449    for h in 0..cfg.n_heads {
1450        let head = &mut q[h * hd..(h + 1) * hd];
1451        rms_inplace(head, cfg.norm_eps);
1452        rope_tail(head, inv_freq, pos, rd, false);
1453    }
1454    let mut cache: Vec<f32> = st.window[li].clone();
1455    cache.extend_from_slice(&st.compressed[li]);
1456
1457    // ── sparse attention per head, then the inverse rope ──
1458    let mut attn = vec![0.0f32; cfg.n_heads * hd];
1459    for h in 0..cfg.n_heads {
1460        let qh = &q[h * hd..(h + 1) * hd];
1461        let mut oh = vec![0.0f32; hd];
1462        sparse_attend(qh, &cache, &idxs, l.attn_sink[h], scale, hd, &mut oh);
1463        rope_tail(&mut oh, inv_freq, pos, rd, true);
1464        attn[h * hd..(h + 1) * hd].copy_from_slice(&oh);
1465    }
1466
1467    // ── grouped low-rank output ──
1468    // Read the two blocks through the quantized readers. Materializing them
1469    // here instead costs ~270 MB of dequantization per layer per token on
1470    // the release checkpoint (wo_a and wo_b are 33M weights each), which is
1471    // the difference between decoding and not.
1472    o_project(
1473        &attn,
1474        &|r, x, sc| l.wo_a.row_dot(r, x, sc),
1475        l.wo_a.cols(),
1476        &|mid, dst| l.wo_b.matvec(mid, dst, pool),
1477        cfg.o_groups,
1478        cfg.o_lora_rank,
1479        pool,
1480        out,
1481    );
1482}
1483
1484/// RMSNorm with a learned weight, in place.
1485pub fn rms_weighted(v: &mut [f32], w: &[f32], eps: f32) {
1486    let ms = v.iter().map(|x| x * x).sum::<f32>() / v.len() as f32;
1487    let inv = 1.0 / (ms + eps).sqrt();
1488    for (x, g) in v.iter_mut().zip(w) {
1489        *x = *x * inv * g;
1490    }
1491}
1492
1493/// The MoE half of a block: route, run the chosen experts plus the shared
1494/// one, and sum. `token_id` is only read on the hash layers.
1495/// Per-layer expert-selection counts, the routing field a task-conditional
1496/// expert set is derived from (`CMF_MOE_STATS`). The generic MoE path keeps
1497/// these on its `MoeFfn`; this architecture has its own experts and never
1498/// touches that struct, so without this the field cannot be recorded for
1499/// DeepSeek-V4 at all — and its hash layers already make defrag useless, so
1500/// the only interesting question is what the OTHER forty layers do.
1501///
1502/// Decode drives this from one thread; the pool parallelizes inside the
1503/// matvecs, below this point.
1504thread_local! {
1505    static ROUTE_COUNTS: std::cell::RefCell<Vec<Vec<u64>>> =
1506        const { std::cell::RefCell::new(Vec::new()) };
1507}
1508
1509fn route_stats_on() -> bool {
1510    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1511    *ON.get_or_init(|| std::env::var("CMF_MOE_STATS").is_ok())
1512}
1513
1514fn record_route(li: usize, n_layers_hint: usize, n_experts: usize, idx: &[usize]) {
1515    ROUTE_COUNTS.with(|c| {
1516        let mut c = c.borrow_mut();
1517        if c.len() <= li.max(n_layers_hint) {
1518            c.resize(li.max(n_layers_hint) + 1, Vec::new());
1519        }
1520        let row = &mut c[li];
1521        if row.len() < n_experts {
1522            row.resize(n_experts, 0);
1523        }
1524        for &e in idx {
1525            if e < row.len() {
1526                row[e] += 1;
1527            }
1528        }
1529    });
1530}
1531
1532/// Take the recorded routing field, leaving the counters empty.
1533pub fn take_route_counts() -> Vec<Vec<u64>> {
1534    ROUTE_COUNTS.with(|c| std::mem::take(&mut *c.borrow_mut()))
1535}
1536
1537/// Charge elapsed time to a counter when it goes out of scope — the two
1538/// steps have several early returns each, and a timer that only stops on the
1539/// long path measures the short one as free.
1540struct Charge(Option<std::time::Instant>, &'static std::sync::atomic::AtomicU64);
1541impl Drop for Charge {
1542    fn drop(&mut self) {
1543        if let Some(t) = self.0 {
1544            self.1.fetch_add(
1545                t.elapsed().as_nanos() as u64,
1546                std::sync::atomic::Ordering::Relaxed,
1547            );
1548        }
1549    }
1550}
1551fn scopeguard_attn(t: Option<std::time::Instant>) -> Charge {
1552    Charge(t, &prof::ATTN_NS)
1553}
1554fn scopeguard_moe(t: Option<std::time::Instant>, li: usize) -> Charge {
1555    if t.is_some() {
1556        prof::note_layer(li);
1557    }
1558    Charge(t, &prof::MOE_NS)
1559}
1560
1561/// The whole token, one submission per layer. Returns false having changed
1562/// nothing if the device declines any layer — the caller's loop is then still
1563/// correct to run.
1564#[cfg(feature = "gpu")]
1565#[allow(clippy::too_many_arguments)]
1566fn dsv4_layer_loop(
1567    state: &mut [f32],
1568    layers: &[Dsv4Layer],
1569    g: &Dsv4Globals,
1570    cfg: &Dsv4Cfg,
1571    st: &mut Dsv4State,
1572    token_id: u32,
1573    inv_freq: &[f32],
1574    pool: Option<&crate::pool::Pool>,
1575    scratch: &mut HcScratch,
1576) -> bool {
1577    let dim = cfg.dim;
1578    let freqs_of = |l: &Dsv4Layer| -> &[f32] {
1579        let f = if l.compressor.is_some() {
1580            &g.inv_freq_compress
1581        } else {
1582            &g.inv_freq_window
1583        };
1584        if f.is_empty() { inv_freq } else { f.as_slice() }
1585    };
1586    // PRE-FLIGHT. The prep inside the loop advances the window and the
1587    // compressor caches, so a refusal halfway leaves state that the CPU
1588    // fallback would advance a SECOND time — which is not a slow answer but a
1589    // wrong one. Everything that can decline is therefore asked before the
1590    // first byte of state moves. The expert upload happens here too, which is
1591    // where it belonged anyway.
1592    let mut on_dev = vec![false; layers.len()];
1593    for (li, l) in layers.iter().enumerate() {
1594        let Some(pk) = pack_for(l, cfg, li) else {
1595            return false;
1596        };
1597        if l.wq_a.model_idx().is_none()
1598            || l.wq_b.model_idx().is_none()
1599            || l.wo_a.model_idx().is_none()
1600            || l.wo_b.model_idx().is_none()
1601        {
1602            return false;
1603        }
1604        let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
1605            return false;
1606        };
1607        let gu_q2 = l
1608            .experts
1609            .first()
1610            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
1611        // A layer whose experts do not fit is not a reason to abandon the
1612        // token: 100 GB of experts against a 98 GB card means SOME layer will
1613        // always miss. Those run on the host, with the state fetched and put
1614        // back around them — two transfers for the few that need it.
1615        // The attention weights have to be asked for too. Experts fill the
1616        // card first, and a wo_b that misses at layer 11 used to surface as a
1617        // mid-loop refusal — after the caches had advanced, which the CPU
1618        // fallback then advanced again.
1619        let attn_ok = [
1620            l.wq_a.model_idx(),
1621            l.wq_b.model_idx(),
1622            l.wo_a.model_idx(),
1623            l.wo_b.model_idx(),
1624        ]
1625        .into_iter()
1626        .flatten()
1627        .all(|i| crate::gpu_wgpu::dsv4_weight_ready(&model, i));
1628        // The layer frame's router has no remap yet, so a PARTIAL packing
1629        // there would silently become a mask — the one thing that is known to
1630        // wreck this model. Such a layer goes to the host until the frame
1631        // learns to hand cold picks back the way the two-frame path does.
1632        on_dev[li] = attn_ok
1633            && pk.globals.len() == cfg.n_routed_experts
1634            && crate::gpu_wgpu::dsv4_experts_ready(&model, &pk.tensors, cfg.moe_inter, dim, gu_q2);
1635    }
1636    if !on_dev.iter().any(|&x| x) {
1637        return false;
1638    }
1639
1640    // Layer zero's opening fold has no frame before it to have prepared it.
1641    let (mut folded, post0, comb0) = hc_fold_norm(
1642        state,
1643        &layers[0].hc_attn_fn,
1644        &layers[0].hc_attn_scale,
1645        &layers[0].hc_attn_base,
1646        &layers[0].attn_norm,
1647        cfg,
1648        pool,
1649    );
1650    if !crate::gpu_wgpu::dsv4_state_write(state)
1651        || !crate::gpu_wgpu::dsv4_hc_write(&post0, &comb0)
1652    {
1653        return false;
1654    }
1655    let mut sink_out = vec![0.0f32; dim];
1656    for (li, l) in layers.iter().enumerate() {
1657        if !on_dev[li] {
1658            if !crate::gpu_wgpu::dsv4_state_read(state) {
1659                return false;
1660            }
1661            let freqs = freqs_of(l);
1662            hc_block(
1663                state,
1664                &l.hc_attn_fn,
1665                &l.hc_attn_scale,
1666                &l.hc_attn_base,
1667                &l.attn_norm,
1668                cfg,
1669                scratch,
1670                pool,
1671                |f, o| attention_step(f, l, cfg, st, li, freqs, pool, None, o),
1672            );
1673            hc_block(
1674                state,
1675                &l.hc_ffn_fn,
1676                &l.hc_ffn_scale,
1677                &l.hc_ffn_base,
1678                &l.ffn_norm,
1679                cfg,
1680                scratch,
1681                pool,
1682                |f, o| moe_step(f, l, cfg, token_id, li, pool, o),
1683            );
1684            if let Some(n) = layers.get(li + 1) {
1685                let (f, p2, c2) = hc_fold_norm(
1686                    state,
1687                    &n.hc_attn_fn,
1688                    &n.hc_attn_scale,
1689                    &n.hc_attn_base,
1690                    &n.attn_norm,
1691                    cfg,
1692                    pool,
1693                );
1694                folded = f;
1695                if !crate::gpu_wgpu::dsv4_hc_write(&p2, &c2) {
1696                    return false;
1697                }
1698            }
1699            if !crate::gpu_wgpu::dsv4_state_write(state) {
1700                return false;
1701            }
1702            continue;
1703        }
1704        let mut prep = AttnPrep::default();
1705        attention_step(
1706            &folded,
1707            l,
1708            cfg,
1709            st,
1710            li,
1711            freqs_of(l),
1712            pool,
1713            Some(&mut prep),
1714            &mut sink_out,
1715        );
1716        // The caches the frame will read.
1717        let hd = cfg.head_dim;
1718        let n_comp = st.compressed[li].len() / hd;
1719        let cap = (cfg.window + n_comp.next_power_of_two().max(64)) * hd;
1720        let kv_id = st.kv_id;
1721        if !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, 0, &st.window[li], cap)
1722            || (n_comp > 0
1723                && !crate::gpu_wgpu::dsv4_cache_write(
1724                    kv_id,
1725                    li,
1726                    cfg.window * hd,
1727                    &st.compressed[li],
1728                    cap,
1729                ))
1730        {
1731            return false;
1732        }
1733        let idx32: Vec<u32> = prep
1734            .idxs
1735            .iter()
1736            .map(|&p| {
1737                if p < prep.win_len {
1738                    p as u32
1739                } else {
1740                    (cfg.window + (p - prep.win_len)) as u32
1741                }
1742            })
1743            .collect();
1744        let Some(pk) = pack_for(l, cfg, li) else {
1745            return false;
1746        };
1747        let (Some(wq_a), Some(wq_b), Some(wo_a), Some(wo_b)) = (
1748            l.wq_a.model_idx(),
1749            l.wq_b.model_idx(),
1750            l.wo_a.model_idx(),
1751            l.wo_b.model_idx(),
1752        ) else {
1753            return false;
1754        };
1755        let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
1756            return false;
1757        };
1758        let forced: Option<Vec<usize>> = l.tid2eid.as_ref().and_then(|tbl| {
1759            let v: Vec<usize> = hash_route(tbl, cfg.vocab, cfg.top_k, token_id)
1760                .into_iter()
1761                .map(|gi| pk.to_slot[gi])
1762                .collect();
1763            if v.iter().any(|&x| x == usize::MAX) {
1764                None
1765            } else {
1766                Some(v)
1767            }
1768        });
1769        if l.tid2eid.is_some() && forced.is_none() {
1770            return false;
1771        }
1772        let bias: Option<Vec<f32>> = l
1773            .gate_bias
1774            .as_deref()
1775            .map(|b| pk.globals.iter().map(|&gi| b[gi]).collect());
1776        let nxt = layers.get(li + 1);
1777        let w = crate::gpu_wgpu::Dsv4LayerW {
1778            attn: crate::gpu_wgpu::Dsv4AttnW {
1779                wq_a,
1780                wq_b,
1781                wo_a,
1782                wo_b,
1783                q_norm: &l.q_norm,
1784                sink: &l.attn_sink,
1785            },
1786            moe: crate::gpu_wgpu::Dsv4MoeW {
1787                experts: &pk.tensors,
1788                logits: &[],
1789                bias: bias.as_deref(),
1790                forced: forced.as_deref(),
1791                remap: None,
1792            },
1793            hc_ffn_fn: &l.hc_ffn_fn,
1794            hc_ffn_scale: &l.hc_ffn_scale,
1795            hc_ffn_base: &l.hc_ffn_base,
1796            hc_next_fn: nxt.map(|n| n.hc_attn_fn.as_slice()),
1797            hc_next_scale: nxt.map_or(&l.hc_attn_scale, |n| &n.hc_attn_scale),
1798            hc_next_base: nxt.map_or(&l.hc_attn_base, |n| n.hc_attn_base.as_slice()),
1799            ffn_norm: &l.ffn_norm,
1800            next_norm: nxt.map_or(&l.attn_norm, |n| n.attn_norm.as_slice()),
1801            next_q_norm: nxt.map_or(&l.q_norm, |n| n.q_norm.as_slice()),
1802            next_wq_a: nxt.and_then(|n| n.wq_a.model_idx()),
1803            router: &pk.router,
1804        };
1805        let geom = crate::gpu_wgpu::Dsv4LayerGeom {
1806            attn: crate::gpu_wgpu::Dsv4AttnGeom {
1807                dim,
1808                nh: cfg.n_heads,
1809                hd,
1810                rd: cfg.rope_head_dim,
1811                q_lora: cfg.q_lora_rank,
1812                o_lora: cfg.o_lora_rank,
1813                o_groups: cfg.o_groups,
1814                eps: cfg.norm_eps,
1815                scale: (hd as f32).powf(-0.5),
1816            },
1817            moe: crate::gpu_wgpu::Dsv4MoeGeom {
1818                hidden: dim,
1819                inter: cfg.moe_inter,
1820                top_k: cfg.top_k,
1821                route_scale: cfg.route_scale,
1822                swiglu_limit: cfg.swiglu_limit,
1823                gu_q2: l.experts.first().is_some_and(|e| {
1824                    e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
1825                }),
1826            },
1827            hc: cfg.hc_mult,
1828            hc_eps: cfg.hc_eps,
1829            sinkhorn_iters: cfg.hc_sinkhorn_iters,
1830        };
1831        let mut next = vec![0.0f32; dim];
1832        if !crate::gpu_wgpu::dsv4_layer_frame(
1833            &model,
1834            &w,
1835            geom,
1836            kv_id,
1837            li,
1838            Some(&prep.qr),
1839            &idx32,
1840            freqs_of(l),
1841            st.pos,
1842            &mut next,
1843        ) {
1844            return false;
1845        }
1846        folded = next;
1847    }
1848    crate::gpu_wgpu::dsv4_state_read(state)
1849}
1850
1851/// The host half of one hyper-connection block: mixes, Sinkhorn, fold, norm.
1852/// The device does this for every layer but the first, whose state it has not
1853/// seen yet.
1854#[cfg(feature = "gpu")]
1855#[allow(clippy::too_many_arguments)]
1856fn hc_fold_norm(
1857    state: &[f32],
1858    hc_fn: &[f32],
1859    hc_scale: &[f32; 3],
1860    hc_base: &[f32],
1861    norm_w: &[f32],
1862    cfg: &Dsv4Cfg,
1863    pool: Option<&crate::pool::Pool>,
1864) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
1865    let (hc, dim) = (cfg.hc_mult, cfg.dim);
1866    let mix_hc = (2 + hc) * hc;
1867    let mut mixes = vec![0.0f32; mix_hc];
1868    hc_mixes(state, hc_fn, mix_hc, cfg.norm_eps, pool, &mut mixes);
1869    let mut pre = vec![0.0f32; hc];
1870    let mut post = vec![0.0f32; hc];
1871    let mut comb = vec![0.0f32; hc * hc];
1872    hc_split_sinkhorn(
1873        &mixes,
1874        hc_scale,
1875        hc_base,
1876        hc,
1877        cfg.hc_sinkhorn_iters,
1878        cfg.hc_eps,
1879        &mut pre,
1880        &mut post,
1881        &mut comb,
1882    );
1883    let mut folded = vec![0.0f32; dim];
1884    hc_fold(state, &pre, hc, dim, &mut folded);
1885    rms_weighted(&mut folded, norm_w, cfg.norm_eps);
1886    // post and comb travel with the fold: the frame's opening expand needs
1887    // exactly those, and they are not recoverable from the state alone.
1888    (folded, post, comb)
1889}
1890
1891/// `CMF_DSV4_GPU_LAYER=1`: one submission per layer instead of two, with the
1892/// hyper-connection glue and the router on the device.
1893///
1894/// CORRECT — perplexity 5.211 against the CPU's 5.211 on the release, 128.576
1895/// against 128.576 on the toy — and SLOWER on this hardware: 6.0 tok/s where
1896/// the two-frame path gets 9.3. The reason is not the frame, it is the
1897/// all-or-nothing granularity underneath it. A layer whose experts miss VRAM
1898/// runs entirely on the host, attention included (6.5 ms a call against 0.9),
1899/// and with 100 GB of experts against a 98 GB card a fifth of the layers
1900/// miss. The two-frame path only loses the MoE half of those layers.
1901///
1902/// So the barrier it saves is real and the fallback it forces costs more. The
1903/// fix is the granularity: pack the experts that FIT, route over all of them
1904/// anyway, and run the few cold picks of a token on the host — per EXPERT,
1905/// not per layer. Then no layer ever leaves the device and this frame wins by
1906/// the 15 ms a token it was built to save.
1907#[cfg(feature = "gpu")]
1908fn gpu_layer_enabled() -> bool {
1909    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1910    *ON.get_or_init(|| {
1911        std::env::var("CMF_DSV4_GPU_LAYER").is_ok_and(|v| v != "0")
1912            && crate::gpu::backend_available()
1913    })
1914}
1915
1916/// The packed expert set of one layer: which globals made it in, and their
1917/// directory indices in packing order with the shared expert last. Built once
1918/// — the mask does not change during a run — and keyed by layer.
1919#[cfg(feature = "gpu")]
1920struct Pack {
1921    /// The router as dense f32, expanded once. It is 4 MB a layer against a
1922    /// 112 GB model, it lives as long as the process — so the address-keyed
1923    /// device cache is sound for it, unlike anything built per call.
1924    router: Vec<f32>,
1925    /// global expert id -> packed slot, `usize::MAX` for the ones left out.
1926    to_slot: Vec<usize>,
1927    /// The same, as the u32 table the router reads.
1928    remap: Vec<u32>,
1929    /// packed order, globals only (shared is not in here).
1930    globals: Vec<usize>,
1931    tensors: Vec<(usize, usize, usize)>,
1932}
1933
1934#[cfg(feature = "gpu")]
1935fn pack_for(l: &Dsv4Layer, cfg: &Dsv4Cfg, li: usize) -> Option<std::sync::Arc<Pack>> {
1936    use std::collections::HashMap;
1937    use std::sync::{Arc, Mutex, OnceLock};
1938    static CACHE: OnceLock<Mutex<HashMap<usize, Option<Arc<Pack>>>>> = OnceLock::new();
1939    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
1940    if let Some(v) = cache.lock().unwrap().get(&li) {
1941        return v.clone();
1942    }
1943    let build = || -> Option<Arc<Pack>> {
1944        let mut to_slot = vec![usize::MAX; cfg.n_routed_experts];
1945        let mut globals = Vec::new();
1946        let mut tensors = Vec::new();
1947        let idx3 = |e: &Dsv4Expert| -> Option<(usize, usize, usize)> {
1948            Some((e.w1.model_idx()?, e.w3.model_idx()?, e.w2.model_idx()?))
1949        };
1950        // How many experts the card still has room for, minus one for the
1951        // shared expert, which always rides. Everything past that stays on the
1952        // host and is reached through the remap — the router still ranges over
1953        // all of them, so this costs speed and not a single bit of quality.
1954        let gu_q2 = l
1955            .experts
1956            .first()
1957            .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
1958        // `CMF_DSV4_COLD_CPU=1` packs only what fits and leaves the rest to
1959        // the host. Exact on the toy at any budget, still 5.379 against the
1960        // CPU's 5.211 on the release — so it is opt-in until that is closed.
1961        // Off, a layer that does not fit whole declines and runs on the host,
1962        // which is slower and right.
1963        // `CMF_DSV4_PACK_MAX=N` caps the packing directly, so a toy can
1964        // reproduce the subset path without needing a card that runs out.
1965        if let Some(n) = std::env::var("CMF_DSV4_PACK_MAX")
1966            .ok()
1967            .and_then(|v| v.parse::<usize>().ok())
1968        {
1969            let mut to_slot = vec![usize::MAX; cfg.n_routed_experts];
1970            let mut globals = Vec::new();
1971            let mut tensors = Vec::new();
1972            for (gi, e) in l.experts.iter().enumerate().take(n) {
1973                to_slot[gi] = globals.len();
1974                globals.push(gi);
1975                tensors.push(idx3(e)?);
1976            }
1977            tensors.push(idx3(&l.shared)?);
1978            let (rows, cols) = (l.gate.rows(), l.gate.cols());
1979            let mut router = vec![0.0f32; rows * cols];
1980            for r in 0..rows {
1981                l.gate.row_f32(r, &mut router[r * cols..(r + 1) * cols]);
1982            }
1983            let remap: Vec<u32> = to_slot
1984                .iter()
1985                .map(|&sl| if sl == usize::MAX { u32::MAX } else { sl as u32 })
1986                .collect();
1987            return Some(Arc::new(Pack {
1988                router,
1989                to_slot,
1990                remap,
1991                globals,
1992                tensors,
1993            }));
1994        }
1995        let room = if std::env::var("CMF_DSV4_COLD_CPU").is_ok_and(|v| v != "0") {
1996            crate::gpu_wgpu::dsv4_experts_fit(cfg.moe_inter, cfg.dim, gu_q2).saturating_sub(1)
1997        } else {
1998            usize::MAX
1999        };
2000        for (gi, e) in l.experts.iter().enumerate() {
2001            if l.mask.as_deref().is_some_and(|m| !m.get(gi).copied().unwrap_or(true)) {
2002                continue;
2003            }
2004            if globals.len() >= room {
2005                break;
2006            }
2007            to_slot[gi] = globals.len();
2008            globals.push(gi);
2009            match idx3(e) {
2010                Some(t) => tensors.push(t),
2011                None => {
2012                    if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
2013                        eprintln!("слой {li}: эксперт {gi} без индексов в каталоге");
2014                    }
2015                    return None;
2016                }
2017            }
2018        }
2019        if globals.is_empty() {
2020            tracing::warn!("слой {li}: маска не оставила ни одного эксперта");
2021            return None;
2022        }
2023        tensors.push(idx3(&l.shared)?); // shared rides last, as the kernels expect
2024        let (rows, cols) = (l.gate.rows(), l.gate.cols());
2025        let mut router = vec![0.0f32; rows * cols];
2026        for r in 0..rows {
2027            l.gate.row_f32(r, &mut router[r * cols..(r + 1) * cols]);
2028        }
2029        let remap: Vec<u32> = to_slot
2030            .iter()
2031            .map(|&sl| if sl == usize::MAX { u32::MAX } else { sl as u32 })
2032            .collect();
2033        Some(Arc::new(Pack {
2034            router,
2035            to_slot,
2036            remap,
2037            globals,
2038            tensors,
2039        }))
2040    };
2041    let v = build();
2042    cache.lock().unwrap().insert(li, v.clone());
2043    v
2044}
2045
2046/// `CMF_DSV4_GPU_MOE2=1`: the whole MoE block in one submission, experts
2047/// resident. Returns false having changed nothing if it cannot.
2048///
2049/// NOT CORRECT YET — off by default and it must stay off. On the release
2050/// checkpoint it diverges from the CPU by up to 0.44 relative on most MoE
2051/// layers (`CMF_DSV4_MOE_CHECK=1` prints them), and perplexity lands at 5.162
2052/// against the CPU's 5.211 on the exact contract.
2053///
2054/// Ruled out so far, so the next attempt need not redo it:
2055///   * the routing kernel — `gpu_route_parity` matches exactly at 256 experts
2056///     with bias, mask, a hash row and an all-closed mask;
2057///   * the 2-bit expert layout — a q2tp toy (gate/up Q2TiledP, down Q4TiledP,
2058///     confirmed identical to the release by `expert_dtypes`) agrees bit for
2059///     bit, PPL 143.512 both ways;
2060///   * missing storage barriers in `moe_route` — added, no change.
2061///
2062/// So it is scale-dependent: the toy runs 8 experts, top_k 2, inter 64,
2063/// hidden 128; the release runs 256 / 6 / 2048 / 4096. The next thing to try
2064/// is a toy at release proportions, which will either reproduce it or narrow
2065/// it to something only the real weights do.
2066#[cfg(feature = "gpu")]
2067fn moe_frame(
2068    hidden: &[f32],
2069    l: &Dsv4Layer,
2070    cfg: &Dsv4Cfg,
2071    li: usize,
2072    logits: &[f32],
2073    forced: Option<&[usize]>,
2074    pool: Option<&crate::pool::Pool>,
2075    out: &mut [f32],
2076) -> bool {
2077    macro_rules! no {
2078        ($($t:tt)*) => {{
2079            if std::env::var("CMF_DSV4_FRAME_DEBUG").is_ok() {
2080                eprintln!("кадр MoE отклонён: {}", format_args!($($t)*));
2081            }
2082            return false;
2083        }};
2084    }
2085    let Some(pk) = pack_for(l, cfg, li) else {
2086        no!("слой {li}: упаковка экспертов не построена");
2087    };
2088    // The router is a small f32 tensor and is usually NOT mapped; the handle
2089    // has to come from something that is.
2090    let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
2091        no!("слой {li}: эксперты не отображены из файла");
2092    };
2093    // A forced expert outside the packing has nowhere to go; the hash layers
2094    // name specific experts and a mask that drops one of them is a mask this
2095    // layer cannot use.
2096    let fpack: Option<Vec<usize>> = match forced {
2097        Some(f) => {
2098            let v: Vec<usize> = f.iter().map(|&g| pk.to_slot[g]).collect();
2099            if v.iter().any(|&s| s == usize::MAX) {
2100                no!("слой {li}: хеш-слой называет эксперта вне упаковки");
2101            }
2102            Some(v)
2103        }
2104        None => None,
2105    };
2106    // Routing ranges over EVERY expert; the remap turns a winner into a slot
2107    // or marks it cold. Nothing is masked, so nothing is lost.
2108    let subset = pk.globals.len() < cfg.n_routed_experts;
2109    let lg: Vec<f32> = if subset {
2110        logits.to_vec()
2111    } else {
2112        pk.globals.iter().map(|&g| logits[g]).collect()
2113    };
2114    let bias: Option<Vec<f32>> = l.gate_bias.as_deref().map(|b| {
2115        if subset {
2116            b.to_vec()
2117        } else {
2118            pk.globals.iter().map(|&g| b[g]).collect()
2119        }
2120    });
2121    let w = crate::gpu_wgpu::Dsv4MoeW {
2122        experts: &pk.tensors,
2123        logits: &lg,
2124        bias: bias.as_deref(),
2125        forced: fpack.as_deref(),
2126        remap: if subset { Some(&pk.remap) } else { None },
2127    };
2128    let g = crate::gpu_wgpu::Dsv4MoeGeom {
2129        hidden: cfg.dim,
2130        inter: cfg.moe_inter,
2131        top_k: cfg.top_k,
2132        route_scale: cfg.route_scale,
2133        swiglu_limit: cfg.swiglu_limit,
2134        gu_q2: l.experts.first().is_some_and(|e| {
2135            e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
2136        }),
2137    };
2138    let mut cold = Vec::new();
2139    if !crate::gpu_wgpu::dsv4_moe_frame(&model, &w, g, hidden, &mut cold, out) {
2140        return false;
2141    }
2142    // The picks the card had no room for, finished here and added in. Their
2143    // weights already carry the top-k normalisation the device applied.
2144    if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
2145        let csum: f32 = cold.iter().map(|c| c.1).sum();
2146        eprintln!(
2147            "[холодные] слой {li}: вернулось {} из {} | сумма холодных {csum:.4} | \
2148             route_scale {:.4} | {:?}",
2149            cold.len(),
2150            cfg.top_k,
2151            cfg.route_scale,
2152            &cold[..cold.len().min(3)]
2153        );
2154    }
2155    let mut acc = vec![0.0f32; cfg.dim];
2156    for &(gi, wt) in &cold {
2157        let Some(exp) = l.experts.get(gi) else { continue };
2158        run_expert(hidden, exp, cfg, wt, pool, &mut acc);
2159        for (o, a) in out.iter_mut().zip(&acc) {
2160            *o += a;
2161        }
2162    }
2163    true
2164}
2165
2166/// How much of each layer's compressed cache already sits on the card. ONE
2167/// map: a reader and a writer with a `static` each are two maps, and the
2168/// reader would never see a thing the writer put down.
2169/// The reallocation counter as of the last successful tail write. Any change
2170/// means some buffer was rebuilt and every tail count is stale.
2171#[cfg(feature = "gpu")]
2172fn last_grew(now: u64) -> u64 {
2173    use std::sync::atomic::{AtomicU64, Ordering};
2174    static SEEN: AtomicU64 = AtomicU64::new(0);
2175    let was = SEEN.load(Ordering::Relaxed);
2176    if was != now {
2177        SEEN.store(now, Ordering::Relaxed);
2178        compressed_map().lock().unwrap().clear();
2179        return u64::MAX; // force a full write this round
2180    }
2181    now
2182}
2183
2184#[cfg(feature = "gpu")]
2185fn compressed_map() -> &'static std::sync::Mutex<std::collections::HashMap<(u64, usize), usize>> {
2186    use std::collections::HashMap;
2187    use std::sync::{Mutex, OnceLock};
2188    static W: OnceLock<Mutex<HashMap<(u64, usize), usize>>> = OnceLock::new();
2189    W.get_or_init(|| Mutex::new(HashMap::new()))
2190}
2191
2192#[cfg(feature = "gpu")]
2193fn compressed_written(kv_id: u64, li: usize) -> usize {
2194    compressed_map()
2195        .lock()
2196        .unwrap()
2197        .get(&(kv_id, li))
2198        .copied()
2199        .unwrap_or(0)
2200}
2201
2202/// Reset to zero whenever a write fails or the buffer grows — a grown buffer
2203/// keeps none of its contents.
2204#[cfg(feature = "gpu")]
2205fn note_compressed(kv_id: u64, li: usize, n: usize) {
2206    compressed_map().lock().unwrap().insert((kv_id, li), n);
2207}
2208
2209#[cfg(feature = "gpu")]
2210fn gpu_moe2_enabled() -> bool {
2211    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2212    *ON.get_or_init(|| {
2213        std::env::var("CMF_DSV4_GPU_MOE2").is_ok_and(|v| v != "0")
2214            && crate::gpu::backend_available()
2215    })
2216}
2217
2218pub fn moe_step(
2219    hidden: &[f32],
2220    l: &Dsv4Layer,
2221    cfg: &Dsv4Cfg,
2222    token_id: u32,
2223    // Layer index — only used to bucket routing statistics.
2224    li: usize,
2225    pool: Option<&crate::pool::Pool>,
2226    out: &mut [f32],
2227) {
2228    let _t0 = prof::on().then(std::time::Instant::now);
2229    let _guard = scopeguard_moe(_t0, li);
2230    let mut logits = vec![0.0f32; cfg.n_routed_experts];
2231    l.gate.matvec(hidden, &mut logits, pool);
2232    let (mut idx, mut w) = (Vec::new(), Vec::new());
2233    route(
2234        &logits,
2235        l.gate_bias.as_deref(),
2236        cfg.top_k,
2237        cfg.route_scale,
2238        l.tid2eid
2239            .as_ref()
2240            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id))
2241            .as_deref(),
2242        l.mask.as_deref(),
2243        &mut idx,
2244        &mut w,
2245    );
2246    if route_stats_on() {
2247        record_route(li, 0, cfg.n_routed_experts, &idx);
2248    }
2249    // The whole block on the device, in one submission, or nothing. Routing
2250    // happens there too — the logits above are what it starts from, so the
2251    // CPU's own choice is discarded rather than second-guessed.
2252    #[cfg(feature = "gpu")]
2253    if gpu_moe2_enabled() {
2254        let forced = l
2255            .tid2eid
2256            .as_ref()
2257            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
2258        if moe_frame(hidden, l, cfg, li, &logits, forced.as_deref(), pool, out) {
2259            // CMF_DSV4_MOE_CHECK=1 recomputes the same block on the CPU and
2260            // reports where they part. A wrong MoE does not fail — it answers
2261            // differently — and the toy agreed bit for bit while the release
2262            // did not, so the difference lives in something the toy has no
2263            // instance of. Only a per-layer number will say which.
2264            if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
2265                let mut want = vec![0.0f32; out.len()];
2266                let mut acc = vec![0.0f32; cfg.dim];
2267                for (e, &ei) in idx.iter().enumerate() {
2268                    let Some(exp) = l.experts.get(ei) else { continue };
2269                    run_expert(hidden, exp, cfg, w.get(e).copied().unwrap_or(0.0), pool, &mut acc);
2270                    for (o, a) in want.iter_mut().zip(&acc) {
2271                        *o += a;
2272                    }
2273                }
2274                run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
2275                for (o, a) in want.iter_mut().zip(&acc) {
2276                    *o += a;
2277                }
2278                let num: f32 = want.iter().zip(out.iter()).map(|(a, b)| (a - b) * (a - b)).sum();
2279                let den: f32 = want.iter().map(|a| a * a).sum::<f32>().max(1e-20);
2280                let rel = (num / den).sqrt();
2281                if rel > 1e-3 {
2282                    let packed = pack_for(l, cfg, li).map_or(0, |p| p.globals.len());
2283                    eprintln!(
2284                        "[кадр MoE] слой {li}: расхождение {rel:.3e} | выбрано {} | \
2285                         упаковано {packed} из {} | хеш={} | смещение={}",
2286                        idx.len(),
2287                        cfg.n_routed_experts,
2288                        l.tid2eid.is_some(),
2289                        l.gate_bias.is_some()
2290                    );
2291                }
2292            }
2293            return;
2294        }
2295    }
2296    if dump_path().is_some() {
2297        PICKED.with(|p| {
2298            let mut p = p.borrow_mut();
2299            if p.len() <= li {
2300                p.resize(li + 1, Vec::new());
2301            }
2302            p[li] = idx.clone();
2303        });
2304    }
2305    // One submission for the whole block — the chosen experts plus the
2306    // shared one. Per-expert dispatches are what made MoE slow elsewhere,
2307    // and the device keeps the weights across tokens, so the cost is the
2308    // arithmetic rather than the traffic. A refusal (missing kernel, mixed
2309    // layouts, weights that do not fit the budget) falls to the CPU whole,
2310    // never half.
2311    // CORRECT but SLOWER, so off by default. Parity holds on real weights
2312    // (perplexity 6.808 → 6.839 at 64 tokens, 5.102 → 5.146 at 200), and an
2313    // honest alternating A/B says 1.0 tok/s against the CPU's 2.2. The first
2314    // measurement claimed the opposite — 0.7 → 2.0 — because the CPU arm ran
2315    // first and paged in 158 GB for the GPU arm to inherit.
2316    //
2317    // The cost is not arithmetic, it is round trips: this submits and reads
2318    // back once per layer, forty-three times a token, and a discrete card
2319    // charges milliseconds for each. Fixing it means one submission per
2320    // token — the whole-token graph — not a faster kernel.
2321    //
2322    // `CMF_DSV4_GPU_MOE=1` opts in.
2323    fn gpu_moe_on() -> bool {
2324        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2325        *ON.get_or_init(|| std::env::var("CMF_DSV4_GPU_MOE").is_ok_and(|v| v != "0"))
2326    }
2327    if gpu_moe_on() && crate::gpu::enabled_here() {
2328        let mut jobs = Vec::with_capacity(idx.len() + 1);
2329        let mut model_ref = None;
2330        let mut ok = true;
2331        for (e, &ei) in idx.iter().enumerate() {
2332            let Some(exp) = l.experts.get(ei) else { continue };
2333            ok &= crate::pipeline::moe_push_job_parts(
2334                &exp.w1,
2335                &exp.w3,
2336                &exp.w2,
2337                hidden,
2338                w.get(e).copied().unwrap_or(0.0),
2339                cfg.swiglu_limit,
2340                &mut jobs,
2341                &mut model_ref,
2342            )
2343            .is_some();
2344        }
2345        ok &= crate::pipeline::moe_push_job_parts(
2346            &l.shared.w1,
2347            &l.shared.w3,
2348            &l.shared.w2,
2349            hidden,
2350            1.0,
2351            cfg.swiglu_limit,
2352            &mut jobs,
2353            &mut model_ref,
2354        )
2355        .is_some();
2356        if ok {
2357            if let Some(m) = model_ref.as_ref() {
2358                if crate::gpu::moe_block(m, &jobs, out) {
2359                    // CMF_DSV4_GPU_CHECK=1 recomputes the same block on the
2360                    // CPU and reports the divergence. A GPU MoE that is wrong
2361                    // does not fail — it answers differently — so the only way
2362                    // to know is to ask both.
2363                    if std::env::var("CMF_DSV4_GPU_CHECK").is_ok() {
2364                        let mut want = vec![0.0f32; out.len()];
2365                        let mut acc = vec![0.0f32; cfg.dim];
2366                        for (e, &ei) in idx.iter().enumerate() {
2367                            let Some(exp) = l.experts.get(ei) else { continue };
2368                            run_expert(
2369                                hidden, exp, cfg,
2370                                w.get(e).copied().unwrap_or(0.0), pool, &mut acc,
2371                            );
2372                            for (o, a) in want.iter_mut().zip(&acc) {
2373                                *o += a;
2374                            }
2375                        }
2376                        run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
2377                        for (o, a) in want.iter_mut().zip(&acc) {
2378                            *o += a;
2379                        }
2380                        let num: f32 = want
2381                            .iter()
2382                            .zip(out.iter())
2383                            .map(|(a, b)| (a - b) * (a - b))
2384                            .sum();
2385                        let den: f32 = want.iter().map(|a| a * a).sum::<f32>().max(1e-20);
2386                        eprintln!(
2387                            "[dsv4-gpu] слой {li}: расхождение {:.3e} | |CPU|={:.5} |GPU|={:.5} | экспертов {}",
2388                            (num / den).sqrt(),
2389                            den.sqrt(),
2390                            out.iter().map(|x| x * x).sum::<f32>().sqrt(),
2391                            jobs.len()
2392                        );
2393                    }
2394                    return;
2395                }
2396            }
2397        }
2398    }
2399    out.fill(0.0);
2400    let mut acc = vec![0.0f32; cfg.dim];
2401    for (e, &ei) in idx.iter().enumerate() {
2402        let Some(exp) = l.experts.get(ei) else {
2403            continue;
2404        };
2405        run_expert(
2406            hidden,
2407            exp,
2408            cfg,
2409            w.get(e).copied().unwrap_or(0.0),
2410            pool,
2411            &mut acc,
2412        );
2413        for (o, a) in out.iter_mut().zip(&acc) {
2414            *o += a;
2415        }
2416    }
2417    // The shared expert always runs, at weight 1.
2418    run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
2419    for (o, a) in out.iter_mut().zip(&acc) {
2420        *o += a;
2421    }
2422}
2423
2424/// The routed and shared experts both come through here, so the clamp and
2425/// the weight folding have exactly one implementation — `expert_swiglu`.
2426fn run_expert(
2427    x: &[f32],
2428    e: &Dsv4Expert,
2429    cfg: &Dsv4Cfg,
2430    weight: f32,
2431    pool: Option<&crate::pool::Pool>,
2432    out: &mut [f32],
2433) {
2434    expert_swiglu(
2435        x,
2436        &|src, dst| e.w1.matvec(src, dst, pool),
2437        &|src, dst| e.w3.matvec(src, dst, pool),
2438        &|src, dst| e.w2.matvec(src, dst, pool),
2439        cfg.moe_inter,
2440        weight,
2441        cfg.swiglu_limit,
2442        out,
2443    );
2444}
2445
2446/// `CMF_DSV4_TRACE=1` prints the hidden state's RMS after each half-block and
2447/// the logits' shape at the end. A 300B model that decodes nonsense gives no
2448/// other handle: this says whether the state grew, collapsed or went
2449/// non-finite, and at which layer — before anyone reaches for a debugger on a
2450/// hundred-gigabyte file.
2451fn no_compressed() -> bool {
2452    static OFF: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2453    *OFF.get_or_init(|| std::env::var("CMF_DSV4_NO_COMPRESSED").is_ok_and(|v| v != "0"))
2454}
2455
2456fn trace_on() -> bool {
2457    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2458    *ON.get_or_init(|| std::env::var("CMF_DSV4_TRACE").is_ok_and(|v| v != "0"))
2459}
2460
2461fn rms_of(v: &[f32]) -> f32 {
2462    if v.is_empty() {
2463        return 0.0;
2464    }
2465    (v.iter().map(|x| x * x).sum::<f32>() / v.len() as f32).sqrt()
2466}
2467
2468/// `CMF_DSV4_DUMP=<path>` appends one JSON line per token: the embedding, the
2469/// hyper-connection state after every layer, the folded-and-normed head input
2470/// and the logits. It exists to be diffed against the reference forward on
2471/// the same weights — the numerical parity this port has never had, which at
2472/// toy scale is a few thousand floats and entirely tractable.
2473thread_local! {
2474    /// The attention body's input and output per layer, interleaved.
2475    static BODY: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
2476    /// Experts chosen per layer for the token being decoded — the dump needs
2477    /// them, because two implementations that pick DIFFERENT experts diverge
2478    /// hugely for a reason that is not a bug in either.
2479    static PICKED: std::cell::RefCell<Vec<Vec<usize>>> =
2480        const { std::cell::RefCell::new(Vec::new()) };
2481}
2482
2483fn dump_path() -> Option<&'static str> {
2484    static P: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
2485    P.get_or_init(|| std::env::var("CMF_DSV4_DUMP").ok())
2486        .as_deref()
2487}
2488
2489fn dump_line(json: &str) {
2490    if let Some(p) = dump_path() {
2491        use std::io::Write as _;
2492        if let Ok(mut f) = std::fs::OpenOptions::new()
2493            .create(true)
2494            .append(true)
2495            .open(p)
2496        {
2497            let _ = writeln!(f, "{json}");
2498        }
2499    }
2500}
2501
2502fn vec_json(v: &[f32]) -> String {
2503    let mut s = String::with_capacity(v.len() * 9);
2504    s.push('[');
2505    for (i, x) in v.iter().enumerate() {
2506        if i > 0 {
2507            s.push(',');
2508        }
2509        s.push_str(&format!("{x:.6e}"));
2510    }
2511    s.push(']');
2512    s
2513}
2514
2515/// One token through the whole stack.
2516///
2517/// The hidden state is `hc_mult` copies of a `dim`-vector from the very
2518/// first line to the very last: the embedding is replicated, every layer
2519/// folds/expands around its two halves, and only `hc_head_fold` collapses
2520/// it before the output norm and the head. There is no point in this
2521/// function where an ordinary residual would fit.
2522#[allow(clippy::too_many_arguments)]
2523pub fn forward_token(
2524    g: &Dsv4Globals,
2525    layers: &[Dsv4Layer],
2526    cfg: &Dsv4Cfg,
2527    st: &mut Dsv4State,
2528    token_id: u32,
2529    inv_freq: &[f32],
2530    pool: Option<&crate::pool::Pool>,
2531    logits: &mut Vec<f32>,
2532) {
2533    let _t_all = prof::on().then(std::time::Instant::now);
2534    let _all_guard = Charge(_t_all, &prof::ALL_NS);
2535    let (hc, dim) = (cfg.hc_mult, cfg.dim);
2536
2537    // Embedding, replicated into the copies.
2538    let mut emb = vec![0.0f32; dim];
2539    g.embed.row_f32(token_id as usize, &mut emb);
2540    let mut state = vec![0.0f32; hc * dim];
2541    for j in 0..hc {
2542        state[j * dim..(j + 1) * dim].copy_from_slice(&emb);
2543    }
2544
2545    let mut scratch = HcScratch::new(cfg);
2546    let mut dump: Vec<String> = Vec::new();
2547    if dump_path().is_some() {
2548        dump.push(format!("\"embed\":{}", vec_json(&emb)));
2549        PICKED.with(|p| p.borrow_mut().clear());
2550        BODY.with(|b| b.borrow_mut().clear());
2551        dump.push(",\"layers\":[".into());
2552    }
2553    if trace_on() {
2554        eprintln!(
2555            "[dsv4] tok={token_id} pos={} embed rms={:.5}",
2556            st.pos,
2557            rms_of(&emb)
2558        );
2559    }
2560    // ── one submission per layer, when the device will take it ──
2561    #[cfg(feature = "gpu")]
2562    let layer_frames = gpu_layer_enabled()
2563        && dsv4_layer_loop(
2564            &mut state, layers, g, cfg, st, token_id, inv_freq, pool, &mut scratch,
2565        );
2566    #[cfg(not(feature = "gpu"))]
2567    let layer_frames = false;
2568
2569    for (li, l) in layers.iter().enumerate() {
2570        if layer_frames {
2571            break;
2572        }
2573        // attention half
2574        hc_block(
2575            &mut state,
2576            &l.hc_attn_fn,
2577            &l.hc_attn_scale,
2578            &l.hc_attn_base,
2579            &l.attn_norm,
2580            cfg,
2581            &mut scratch,
2582            pool,
2583            |folded, out| {
2584                if dump_path().is_some() {
2585                    // The body's own input and output, so the reference can be
2586                    // fed the port's input: then only the body can differ.
2587                    BODY.with(|b| b.borrow_mut().push(vec_json(folded)));
2588                }
2589                // The layer's kind decides its frequencies, not the model's.
2590                let freqs = if l.compressor.is_some() {
2591                    &g.inv_freq_compress
2592                } else {
2593                    &g.inv_freq_window
2594                };
2595                let freqs = if freqs.is_empty() {
2596                    inv_freq
2597                } else {
2598                    freqs.as_slice()
2599                };
2600                attention_step(folded, l, cfg, st, li, freqs, pool, None, out);
2601                if dump_path().is_some() {
2602                    BODY.with(|b| b.borrow_mut().push(vec_json(out)));
2603                }
2604            },
2605        );
2606        if dump_path().is_some() {
2607            // After the attention half only — this is what separates an
2608            // attention discrepancy from an expert one.
2609            dump.push(format!(
2610                "{}{}",
2611                if li == 0 { "" } else { "," },
2612                vec_json(&state)
2613            ));
2614        }
2615        // FFN half
2616        let _t_hc2 = prof::on().then(std::time::Instant::now);
2617        hc_block(
2618            &mut state,
2619            &l.hc_ffn_fn,
2620            &l.hc_ffn_scale,
2621            &l.hc_ffn_base,
2622            &l.ffn_norm,
2623            cfg,
2624            &mut scratch,
2625            pool,
2626            |folded, out| moe_step(folded, l, cfg, token_id, li, pool, out),
2627        );
2628        if let Some(t) = _t_hc2 {
2629            // The block's own time minus the expert step inside it — what the
2630            // fold, the norm and the expand cost on their own.
2631            prof::HC_NS.fetch_add(
2632                t.elapsed().as_nanos() as u64,
2633                std::sync::atomic::Ordering::Relaxed,
2634            );
2635        }
2636        if dump_path().is_some() {
2637            dump.push(format!(",{}", vec_json(&state)));
2638        }
2639        if trace_on() && (st.pos % 64 == 0 || st.pos == 199) {
2640            eprintln!(
2641                "[dsv4]  кеши слоя {li}: окно={} сжатых={} индекс={} (ratio={:?})",
2642                st.window[li].len() / cfg.head_dim.max(1),
2643                st.compressed[li].len() / cfg.head_dim.max(1),
2644                st.index_kv[li].len().max(1) / 128,
2645                l.compressor.as_ref().map(|c| (c.ratio, c.overlap)),
2646            );
2647        }
2648        if trace_on() {
2649            let bad = state.iter().filter(|v| !v.is_finite()).count();
2650            eprintln!(
2651                "[dsv4]  layer {li:>2}: rms={:.5}{}",
2652                rms_of(&state),
2653                if bad > 0 {
2654                    format!("  NON-FINITE x{bad}")
2655                } else {
2656                    String::new()
2657                }
2658            );
2659        }
2660    }
2661    st.pos += 1;
2662
2663    // Collapse the copies, normalize, project to the vocabulary.
2664    let mut h = vec![0.0f32; dim];
2665    hc_head_fold(
2666        &state,
2667        &g.hc_head_fn,
2668        g.hc_head_scale,
2669        &g.hc_head_base,
2670        cfg,
2671        pool,
2672        &mut h,
2673    );
2674    let _t_head = prof::on().then(std::time::Instant::now);
2675    rms_weighted(&mut h, &g.norm, cfg.norm_eps);
2676    logits.clear();
2677    logits.resize(g.head.rows(), 0.0);
2678    g.head.matvec(&h, logits, pool);
2679    if let Some(t) = _t_head {
2680        prof::HEAD_NS.fetch_add(
2681            t.elapsed().as_nanos() as u64,
2682            std::sync::atomic::Ordering::Relaxed,
2683        );
2684    }
2685    if dump_path().is_some() {
2686        dump.push("]".into());
2687        let picked = PICKED.with(|p| {
2688            p.borrow()
2689                .iter()
2690                .map(|v| {
2691                    format!(
2692                        "[{}]",
2693                        v.iter()
2694                            .map(|e| e.to_string())
2695                            .collect::<Vec<_>>()
2696                            .join(",")
2697                    )
2698                })
2699                .collect::<Vec<_>>()
2700                .join(",")
2701        });
2702        dump.push(format!(",\"experts\":[{picked}]"));
2703        let body = BODY.with(|b| b.borrow().join(","));
2704        dump.push(format!(",\"attn_io\":[{body}]"));
2705        dump_line(&format!(
2706            "{{\"tok\":{token_id},\"pos\":{},{},\"head\":{},\"logits\":{}}}",
2707            st.pos - 1,
2708            dump.join(""),
2709            vec_json(&h),
2710            vec_json(logits)
2711        ));
2712    }
2713    if trace_on() {
2714        let (mut top, mut best) = (0usize, f32::NEG_INFINITY);
2715        for (i, &v) in logits.iter().enumerate() {
2716            if v > best {
2717                best = v;
2718                top = i;
2719            }
2720        }
2721        let lo = logits.iter().cloned().fold(f32::MAX, f32::min);
2722        eprintln!(
2723            "[dsv4]  head: rms={:.5} logits[{}..{:.3}] argmax={top}",
2724            rms_of(&h),
2725            format_args!("{lo:.3}"),
2726            best
2727        );
2728    }
2729}
2730
2731/// Build the runtime weights from a converted `.cmf`.
2732///
2733/// Names are the converter's output (see `canon_name`'s deepseek_v4 arm):
2734/// attention keeps DeepSeek's own spelling under `self_attn.`, the MoE is
2735/// rewritten into the layout every other MoE here uses, and the hyper-
2736/// connection tensors ride under the layer prefix.
2737pub fn load(
2738    model: &std::sync::Arc<cortiq_core::CmfModel>,
2739    cfg: &Dsv4Cfg,
2740    n_layers: usize,
2741) -> Result<(Dsv4Globals, Vec<Dsv4Layer>), String> {
2742    let q = |name: &str| -> Result<crate::qtensor::QTensor, String> {
2743        crate::qtensor::QTensor::from_model(model, name)
2744    };
2745    // The small pieces — norms, the sink, ape, the hyper-connection
2746    // projections — are read as plain f32. They are not all 2-D (a norm is a
2747    // vector), so this cannot go through QTensor, which requires a matrix.
2748    let f = |name: &str| -> Result<Vec<f32>, String> {
2749        crate::loader::load_f32(model, name, &crate::loader::Overlay::None)
2750    };
2751    let opt_f = |name: &str| -> Option<Vec<f32>> { f(name).ok() };
2752
2753    // Two frequency tables, chosen per layer by whether it compresses. The
2754    // release's compress_rope_theta (160 000) is not in config.json — it
2755    // lives in inference/config.json — so it is pinned here with the other
2756    // constants the header cannot carry.
2757    let rope_of = |base: f32, yarn: bool| -> Vec<f32> {
2758        if yarn {
2759            crate::attention::yarn_inv_freq(cfg.rope_head_dim, base, 16.0, 65536, 32.0, 1.0)
2760        } else {
2761            crate::attention::rope_inv_freq(cfg.rope_head_dim, base)
2762        }
2763    };
2764    let globals = Dsv4Globals {
2765        inv_freq_compress: rope_of(160_000.0, true),
2766        inv_freq_window: rope_of(10_000.0, false),
2767        embed: q("model.embed_tokens.weight")?,
2768        norm: f("model.norm.weight")?,
2769        head: q("lm_head.weight")?,
2770        hc_head_fn: f("model.hc_head_fn")?,
2771        hc_head_base: f("model.hc_head_base")?,
2772        hc_head_scale: *f("model.hc_head_scale")?
2773            .first()
2774            .ok_or("dsv4: empty hc_head_scale")?,
2775    };
2776
2777    let mut layers = Vec::with_capacity(n_layers);
2778    for li in 0..n_layers {
2779        let p = format!("model.layers.{li}");
2780        let scale3 = |name: &str| -> Result<[f32; 3], String> {
2781            let v = f(name)?;
2782            if v.len() < 3 {
2783                return Err(format!("{name}: expected 3 scales, got {}", v.len()));
2784            }
2785            Ok([v[0], v[1], v[2]])
2786        };
2787        // The compressor exists on every layer whose ratio is non-zero;
2788        // its presence in the file is the only signal we need.
2789        let compressor = match q(&format!("{p}.self_attn.compressor.wkv.weight")) {
2790            Ok(wkv) => {
2791                let ape = f(&format!("{p}.self_attn.compressor.ape"))?;
2792                // ape is [ratio, coff*head_dim]; coff is 2 when the windows
2793                // overlap, which the release does at ratio 4.
2794                let width = wkv.rows();
2795                let ratio = (ape.len() / width.max(1)).max(1);
2796                Some(Dsv4Compressor {
2797                    wkv,
2798                    wgate: q(&format!("{p}.self_attn.compressor.wgate.weight"))?,
2799                    norm: f(&format!("{p}.self_attn.compressor.norm.weight"))?,
2800                    ape,
2801                    ratio,
2802                    overlap: ratio == 4,
2803                })
2804            }
2805            Err(_) => None,
2806        };
2807        let indexer = match q(&format!("{p}.self_attn.indexer.wq_b.weight")) {
2808            Ok(wq_b) => {
2809                let ape = f(&format!("{p}.self_attn.indexer.compressor.ape"))?;
2810                let cwkv = q(&format!("{p}.self_attn.indexer.compressor.wkv.weight"))?;
2811                let width = cwkv.rows();
2812                let ratio = (ape.len() / width.max(1)).max(1);
2813                Some(Dsv4Indexer {
2814                    wq_b,
2815                    weights_proj: q(&format!("{p}.self_attn.indexer.weights_proj.weight"))?,
2816                    compressor: Dsv4Compressor {
2817                        wkv: cwkv,
2818                        wgate: q(&format!("{p}.self_attn.indexer.compressor.wgate.weight"))?,
2819                        norm: f(&format!("{p}.self_attn.indexer.compressor.norm.weight"))?,
2820                        ape,
2821                        ratio,
2822                        overlap: ratio == 4,
2823                    },
2824                })
2825            }
2826            Err(_) => None,
2827        };
2828
2829        let mut experts = Vec::with_capacity(cfg.n_routed_experts);
2830        for e in 0..cfg.n_routed_experts {
2831            let ep = format!("{p}.mlp.experts.{e}");
2832            experts.push(Dsv4Expert {
2833                w1: q(&format!("{ep}.gate_proj.weight"))?,
2834                w2: q(&format!("{ep}.down_proj.weight"))?,
2835                w3: q(&format!("{ep}.up_proj.weight"))?,
2836            });
2837        }
2838
2839        layers.push(Dsv4Layer {
2840            attn_norm: f(&format!("{p}.input_layernorm.weight"))?,
2841            ffn_norm: f(&format!("{p}.post_attention_layernorm.weight"))?,
2842            wq_a: q(&format!("{p}.self_attn.wq_a.weight"))?,
2843            q_norm: f(&format!("{p}.self_attn.q_norm.weight"))?,
2844            wq_b: q(&format!("{p}.self_attn.wq_b.weight"))?,
2845            wkv: q(&format!("{p}.self_attn.wkv.weight"))?,
2846            kv_norm: f(&format!("{p}.self_attn.kv_norm.weight"))?,
2847            wo_a: q(&format!("{p}.self_attn.wo_a.weight"))?,
2848            wo_b: q(&format!("{p}.self_attn.wo_b.weight"))?,
2849            attn_sink: f(&format!("{p}.self_attn.attn_sink"))?,
2850            compressor,
2851            indexer,
2852            hc_attn_fn: f(&format!("{p}.hc_attn_fn"))?,
2853            hc_attn_base: f(&format!("{p}.hc_attn_base"))?,
2854            hc_attn_scale: scale3(&format!("{p}.hc_attn_scale"))?,
2855            hc_ffn_fn: f(&format!("{p}.hc_ffn_fn"))?,
2856            hc_ffn_base: f(&format!("{p}.hc_ffn_base"))?,
2857            hc_ffn_scale: scale3(&format!("{p}.hc_ffn_scale"))?,
2858            gate: q(&format!("{p}.mlp.gate.weight"))?,
2859            // The bias is absent exactly on the hash layers, and the table
2860            // is present exactly there — the file itself says which is which.
2861            gate_bias: opt_f(&format!("{p}.mlp.expert_bias")),
2862            tid2eid: opt_f(&format!("{p}.mlp.tid2eid")),
2863            experts,
2864            mask: if model.tensor(&format!("{p}.mlp.tid2eid")).is_some() {
2865                None
2866            } else {
2867                crate::loader::moe_task_mask(&format!("{p}."), cfg.n_routed_experts)
2868            },
2869            shared: Dsv4Expert {
2870                w1: q(&format!("{p}.mlp.shared_expert.gate_proj.weight"))?,
2871                w2: q(&format!("{p}.mlp.shared_expert.down_proj.weight"))?,
2872                w3: q(&format!("{p}.mlp.shared_expert.up_proj.weight"))?,
2873            },
2874        });
2875    }
2876    Ok((globals, layers))
2877}
2878
2879#[cfg(test)]
2880mod tests {
2881    use super::*;
2882
2883    // A whole model, small enough to reason about: 2 layers, 4 heads, 8
2884    // experts. Weights are deterministic and tiny, which is the point —
2885    // this test is about shapes, indexing and cache bookkeeping, the things
2886    // that a 138 GB file would surface only after an hour of loading.
2887    fn toy() -> (Dsv4Globals, Vec<Dsv4Layer>, Dsv4Cfg) {
2888        use crate::qtensor::QTensor;
2889        let cfg = Dsv4Cfg {
2890            dim: 32,
2891            n_heads: 4,
2892            head_dim: 8,
2893            rope_head_dim: 4,
2894            q_lora_rank: 16,
2895            o_lora_rank: 16,
2896            o_groups: 2,
2897            hc_mult: 4,
2898            hc_sinkhorn_iters: 20,
2899            hc_eps: 1e-6,
2900            norm_eps: 1e-6,
2901            n_routed_experts: 8,
2902            top_k: 2,
2903            moe_inter: 16,
2904            route_scale: 1.0,
2905            swiglu_limit: 10.0,
2906            window: 6,
2907            index_topk: 8,
2908            vocab: 24,
2909        };
2910        // Deterministic pseudo-random in a narrow band: big enough to move
2911        // the state, small enough that nothing saturates.
2912        let w = |n: usize, seed: usize| -> Vec<f32> {
2913            (0..n)
2914                .map(|i| (((i * 7 + seed * 13) % 101) as f32 / 101.0 - 0.5) * 0.3)
2915                .collect()
2916        };
2917        let t = |rows: usize, cols: usize, seed: usize| {
2918            QTensor::from_f32(w(rows * cols, seed), rows, cols)
2919        };
2920        let ones = |n: usize| vec![1.0f32; n];
2921
2922        let (dim, hc) = (cfg.dim, cfg.hc_mult);
2923        // q is n_heads*head_dim wide and kv is one head wide; rope rides the
2924        // tail of each rather than widening anything.
2925        let q_width = cfg.n_heads * cfg.head_dim;
2926        let kv_width = cfg.head_dim;
2927        let o_per_group = q_width / cfg.o_groups;
2928        let mut layers = Vec::new();
2929        for li in 0..2 {
2930            let experts: Vec<Dsv4Expert> = (0..cfg.n_routed_experts)
2931                .map(|e| Dsv4Expert {
2932                    w1: t(cfg.moe_inter, dim, 40 + e + li * 8),
2933                    w2: t(dim, cfg.moe_inter, 60 + e + li * 8),
2934                    w3: t(cfg.moe_inter, dim, 80 + e + li * 8),
2935                })
2936                .collect();
2937            // Layer 0 is a hash layer (table-routed); layer 1 routes normally
2938            // and carries the compressor — both paths get exercised.
2939            layers.push(Dsv4Layer {
2940                attn_norm: ones(dim),
2941                ffn_norm: ones(dim),
2942                wq_a: t(cfg.q_lora_rank, dim, 1 + li),
2943                q_norm: ones(cfg.q_lora_rank),
2944                wq_b: t(q_width, cfg.q_lora_rank, 3 + li),
2945                wkv: t(kv_width, dim, 5 + li),
2946                kv_norm: ones(kv_width),
2947                wo_a: t(cfg.o_groups * cfg.o_lora_rank, o_per_group, 7 + li),
2948                wo_b: t(dim, cfg.o_groups * cfg.o_lora_rank, 9 + li),
2949                attn_sink: vec![0.1; cfg.n_heads],
2950                // Layer 1 carries the OVERLAPPING compressor, as the release
2951                // does at ratio 4: the projection is twice the entry width.
2952                compressor: if li == 1 {
2953                    Some(Dsv4Compressor {
2954                        wkv: t(2 * kv_width, dim, 11),
2955                        wgate: t(2 * kv_width, dim, 13),
2956                        norm: ones(kv_width),
2957                        ape: vec![0.01; 4 * 2 * kv_width],
2958                        ratio: 4,
2959                        overlap: true,
2960                    })
2961                } else {
2962                    None
2963                },
2964                indexer: if li == 1 {
2965                    Some(Dsv4Indexer {
2966                        wq_b: t(2 * 16, cfg.q_lora_rank, 41),
2967                        weights_proj: t(2, dim, 43),
2968                        compressor: Dsv4Compressor {
2969                            wkv: t(2 * 16, dim, 45),
2970                            wgate: t(2 * 16, dim, 47),
2971                            norm: ones(16),
2972                            ape: vec![0.01; 4 * 2 * 16],
2973                            ratio: 4,
2974                            overlap: true,
2975                        },
2976                    })
2977                } else {
2978                    None
2979                },
2980                hc_attn_fn: w((2 + hc) * hc * hc * dim, 15 + li),
2981                hc_attn_base: w((2 + hc) * hc, 17 + li),
2982                hc_attn_scale: [1.0, 1.0, 1.0],
2983                hc_ffn_fn: w((2 + hc) * hc * hc * dim, 19 + li),
2984                hc_ffn_base: w((2 + hc) * hc, 21 + li),
2985                hc_ffn_scale: [1.0, 1.0, 1.0],
2986                gate: t(cfg.n_routed_experts, dim, 23 + li),
2987                gate_bias: if li == 1 {
2988                    Some(vec![0.0; cfg.n_routed_experts])
2989                } else {
2990                    None
2991                },
2992                tid2eid: if li == 0 {
2993                    Some(
2994                        (0..cfg.vocab * cfg.top_k)
2995                            .map(|i| (i % cfg.n_routed_experts) as f32)
2996                            .collect(),
2997                    )
2998                } else {
2999                    None
3000                },
3001                experts,
3002                mask: None,
3003                shared: Dsv4Expert {
3004                    w1: t(cfg.moe_inter, dim, 25 + li),
3005                    w2: t(dim, cfg.moe_inter, 27 + li),
3006                    w3: t(cfg.moe_inter, dim, 29 + li),
3007                },
3008            });
3009        }
3010        let inv = |base: f32| -> Vec<f32> {
3011            (0..cfg.rope_head_dim / 2)
3012                .map(|i| 1.0 / base.powf(2.0 * i as f32 / cfg.rope_head_dim as f32))
3013                .collect()
3014        };
3015        let g = Dsv4Globals {
3016            inv_freq_compress: inv(160000.0),
3017            inv_freq_window: inv(10000.0),
3018            embed: t(cfg.vocab, dim, 31),
3019            norm: ones(dim),
3020            head: t(cfg.vocab, dim, 33),
3021            hc_head_fn: w(hc * hc * dim, 35),
3022            hc_head_base: w(hc, 37),
3023            hc_head_scale: 1.0,
3024        };
3025        (g, layers, cfg)
3026    }
3027
3028    /// The whole stack, decoding a sequence. Every block is on the path:
3029    /// hyper-connections, the double-LoRA attention with its sink, the KV
3030    /// compressor firing on its ratio boundary, hash routing on one layer
3031    /// and score routing on the other.
3032    #[test]
3033    fn forward_token_decodes_a_sequence_without_falling_over() {
3034        let (g, layers, cfg) = toy();
3035        let mut st = Dsv4State::new(layers.len());
3036        let inv_freq: Vec<f32> = (0..cfg.rope_head_dim / 2)
3037            .map(|i| 1.0 / 10000f32.powf(2.0 * i as f32 / cfg.rope_head_dim as f32))
3038            .collect();
3039        let mut logits = Vec::new();
3040
3041        // Ten tokens: more than twice the compressor's ratio, so the
3042        // compressed cache is written on a boundary and read afterwards.
3043        let mut first: Option<Vec<f32>> = None;
3044        for (step, tok) in [3u32, 7, 1, 9, 4, 2, 8, 5, 6, 0].into_iter().enumerate() {
3045            forward_token(
3046                &g,
3047                &layers,
3048                &cfg,
3049                &mut st,
3050                tok,
3051                &inv_freq,
3052                None,
3053                &mut logits,
3054            );
3055            assert_eq!(logits.len(), cfg.vocab, "step {step}: logit count");
3056            assert!(
3057                logits.iter().all(|v| v.is_finite()),
3058                "step {step}: non-finite logit — {logits:?}"
3059            );
3060            // A model that has collapsed returns the same distribution
3061            // regardless of input; that is the failure this catches.
3062            let spread = logits.iter().cloned().fold(f32::MIN, f32::max)
3063                - logits.iter().cloned().fold(f32::MAX, f32::min);
3064            assert!(spread > 1e-6, "step {step}: logits are flat ({spread})");
3065            if step == 0 {
3066                first = Some(logits.clone());
3067            }
3068            assert_eq!(st.pos, step + 1, "position bookkeeping");
3069        }
3070
3071        // The cache has to have grown, and the compressor layer must have
3072        // emitted compressed entries (10 tokens / ratio 4 = 2 windows).
3073        assert!(!st.window[0].is_empty(), "sliding window never filled");
3074        // Ten tokens through a window of six: it must have slid, not grown.
3075        for (li, w) in st.window.iter().enumerate() {
3076            assert!(
3077                w.len() / cfg.head_dim <= cfg.window,
3078                "layer {li}: window holds {} positions, cap is {}",
3079                w.len() / cfg.head_dim,
3080                cfg.window
3081            );
3082        }
3083        assert!(
3084            !st.compressed[1].is_empty(),
3085            "compressor layer produced no compressed KV in 10 tokens"
3086        );
3087        // Ten tokens at ratio 4 fold twice, and the entries must be one head
3088        // wide — the overlapping projection is 2x that, so a width mistake
3089        // shows up here rather than as quiet nonsense.
3090        assert_eq!(
3091            st.compressed[1].len() / cfg.head_dim,
3092            2,
3093            "expected two folds in ten tokens at ratio 4"
3094        );
3095        assert!(
3096            !st.prev_kv[1].is_empty(),
3097            "the overlapping compressor never kept a previous window"
3098        );
3099        // Every layer that HAS an indexer must have filled the indexer's own
3100        // cache: it is what decides which compressed positions attention
3101        // reads, and an empty one silently discards the whole long-range
3102        // memory rather than failing.
3103        for (li, l) in layers.iter().enumerate() {
3104            if l.indexer.is_some() {
3105                assert!(
3106                    !st.index_kv[li].is_empty(),
3107                    "layer {li} has an indexer but its cache stayed empty"
3108                );
3109            }
3110        }
3111
3112        // Context must matter: the same token at position 0 of a fresh state
3113        // and at the end of a filled one cannot give identical logits.
3114        let mut fresh = Dsv4State::new(layers.len());
3115        let mut relogits = Vec::new();
3116        forward_token(
3117            &g,
3118            &layers,
3119            &cfg,
3120            &mut fresh,
3121            3,
3122            &inv_freq,
3123            None,
3124            &mut relogits,
3125        );
3126        assert_eq!(
3127            relogits,
3128            first.unwrap(),
3129            "the same token from a fresh state must reproduce exactly"
3130        );
3131    }
3132
3133    /// The reference clamps `up` on both sides but `gate` only from above.
3134    /// Getting that symmetric would quietly change every expert's output on
3135    /// the tokens that saturate, which is the hardest kind of bug to see.
3136    #[test]
3137    fn swiglu_limit_clamps_up_both_ways_and_gate_only_from_above() {
3138        let inter = 4;
3139        // gate = [-50, 50, 1, -1], up = [50, -50, 1, -1]
3140        let gate_src = [-50.0f32, 50.0, 1.0, -1.0];
3141        let up_src = [50.0f32, -50.0, 1.0, -1.0];
3142        let limit = 10.0f32;
3143        let mut got = vec![0.0f32; inter];
3144        expert_swiglu(
3145            &[0.0],
3146            &|_, d| d.copy_from_slice(&gate_src),
3147            &|_, d| d.copy_from_slice(&up_src),
3148            &|src, d| d.copy_from_slice(src),
3149            inter,
3150            1.0,
3151            limit,
3152            &mut got,
3153        );
3154        let silu = |g: f32| g / (1.0 + (-g).exp());
3155        // gate: only the +50 is cut, the -50 rides through silu untouched.
3156        let want = [
3157            silu(-50.0) * limit,
3158            silu(limit) * -limit,
3159            silu(1.0) * 1.0,
3160            silu(-1.0) * -1.0,
3161        ];
3162        for (i, w) in want.iter().enumerate() {
3163            assert!(
3164                (got[i] - w).abs() < 1e-5,
3165                "lane {i}: got {} want {w}",
3166                got[i]
3167            );
3168        }
3169        // And with the clamp off nothing is touched.
3170        let mut raw = vec![0.0f32; inter];
3171        expert_swiglu(
3172            &[0.0],
3173            &|_, d| d.copy_from_slice(&gate_src),
3174            &|_, d| d.copy_from_slice(&up_src),
3175            &|src, d| d.copy_from_slice(src),
3176            inter,
3177            1.0,
3178            0.0,
3179            &mut raw,
3180        );
3181        assert!(
3182            (raw[1] - silu(50.0) * -50.0).abs() < 1e-3,
3183            "limit 0 must not clamp"
3184        );
3185    }
3186
3187    /// The grouped projection writes its intermediate from several threads
3188    /// at once. Disjoint indices are the whole argument for that being safe,
3189    /// so the pooled result has to equal the serial one exactly — a race
3190    /// here would show up as occasional wrong tokens, not as a crash.
3191    #[test]
3192    fn grouped_projection_is_identical_with_and_without_a_pool() {
3193        let (groups, lora, per_group, dim) = (4usize, 128usize, 64usize, 32usize);
3194        let attn: Vec<f32> = (0..groups * per_group)
3195            .map(|i| ((i * 13) as f32 * 0.021).sin())
3196            .collect();
3197        let wo_a: Vec<f32> = (0..groups * lora * per_group)
3198            .map(|i| ((i * 7) as f32 * 0.011).cos())
3199            .collect();
3200        let wo_b: Vec<f32> = (0..dim * groups * lora)
3201            .map(|i| ((i * 5) as f32 * 0.009).sin())
3202            .collect();
3203        let row = |r: usize, x: &[f32], _sc: &mut [f32]| -> f32 {
3204            wo_a[r * per_group..(r + 1) * per_group]
3205                .iter()
3206                .zip(x)
3207                .map(|(a, b)| a * b)
3208                .sum()
3209        };
3210        let project = |mid: &[f32], dst: &mut [f32]| {
3211            for (d, o) in dst.iter_mut().enumerate() {
3212                *o = wo_b[d * mid.len()..(d + 1) * mid.len()]
3213                    .iter()
3214                    .zip(mid)
3215                    .map(|(a, b)| a * b)
3216                    .sum();
3217            }
3218        };
3219
3220        let mut serial = vec![0.0f32; dim];
3221        o_project(
3222            &attn,
3223            &row,
3224            per_group,
3225            &project,
3226            groups,
3227            lora,
3228            None,
3229            &mut serial,
3230        );
3231
3232        let pool = crate::pool::Pool::new(4);
3233        let mut pooled = vec![0.0f32; dim];
3234        o_project(
3235            &attn,
3236            &row,
3237            per_group,
3238            &project,
3239            groups,
3240            lora,
3241            Some(&pool),
3242            &mut pooled,
3243        );
3244        assert_eq!(serial, pooled, "the pooled projection diverged");
3245        assert!(
3246            serial.iter().any(|v| v.abs() > 1e-6),
3247            "test data is degenerate"
3248        );
3249    }
3250
3251    /// The overlapping compressor folds 2*ratio slots, not ratio: the
3252    /// previous window contributes its first half of dimensions and the
3253    /// current one its second half. Treating it as a plain compressor makes
3254    /// the entry twice as wide as the cache expects, which lands the whole
3255    /// thing in the wrong store rather than raising anything.
3256    #[test]
3257    fn overlapping_compressor_folds_both_windows() {
3258        let (ratio, d) = (2usize, 3usize);
3259        // Current window: two tokens, 2*d wide each. Second half is what the
3260        // current window contributes.
3261        let cur_kv: Vec<f32> = vec![
3262            1.0, 1.0, 1.0, /*|*/ 10.0, 20.0, 30.0, // token 0
3263            2.0, 2.0, 2.0, /*|*/ 40.0, 50.0, 60.0, // token 1
3264        ];
3265        // Make the current window's second-half scores dominate everywhere.
3266        let cur_sc: Vec<f32> = vec![
3267            0.0, 0.0, 0.0, /*|*/ 0.0, 0.0, 100.0, //
3268            0.0, 0.0, 0.0, /*|*/ 100.0, 100.0, 0.0,
3269        ];
3270        // Previous window: its FIRST half is what it contributes.
3271        let prev_kv: Vec<f32> = vec![
3272            7.0, 8.0, 9.0, /*|*/ 0.0, 0.0, 0.0, //
3273            5.0, 6.0, 7.0, /*|*/ 0.0, 0.0, 0.0,
3274        ];
3275        let prev_sc = vec![0.0f32; ratio * 2 * d];
3276
3277        let mut out = vec![0.0f32; d];
3278        compress_window_overlap(&prev_kv, &prev_sc, &cur_kv, &cur_sc, ratio, d, &mut out);
3279        // dim 0 and 1: token 1's second half wins (score 100)
3280        assert!((out[0] - 40.0).abs() < 1e-3, "dim0 = {}", out[0]);
3281        assert!((out[1] - 50.0).abs() < 1e-3, "dim1 = {}", out[1]);
3282        // dim 2: token 0's second half wins
3283        assert!((out[2] - 30.0).abs() < 1e-3, "dim2 = {}", out[2]);
3284
3285        // With no previous window the fold still works and uses only the
3286        // current one — this is the very first window of a generation.
3287        let mut first = vec![0.0f32; d];
3288        compress_window_overlap(&[], &[], &cur_kv, &cur_sc, ratio, d, &mut first);
3289        assert!(
3290            first.iter().all(|v| v.is_finite()),
3291            "first window: {first:?}"
3292        );
3293        assert!((first[0] - 40.0).abs() < 1e-3, "first dim0 = {}", first[0]);
3294
3295        // And a previous window with real scores does pull the result.
3296        let mut both = vec![0.0f32; d];
3297        let strong_prev = vec![100.0f32; ratio * 2 * d];
3298        compress_window_overlap(
3299            &prev_kv,
3300            &strong_prev,
3301            &cur_kv,
3302            &cur_sc,
3303            ratio,
3304            d,
3305            &mut both,
3306        );
3307        assert!(
3308            (both[0] - 40.0).abs() > 1.0,
3309            "a scored previous window must move the fold, got {}",
3310            both[0]
3311        );
3312    }
3313
3314    /// Numerical parity with the reference. The vectors below come from
3315    /// running `kernel.py::hc_split_sinkhorn`'s own formula on a fixed
3316    /// input; matching them pins the exponent order, the eps placement and
3317    /// the off-by-one in the iteration count all at once — a property test
3318    /// alone would pass with any of those wrong.
3319    #[test]
3320    fn sinkhorn_matches_the_reference_numbers() {
3321        let hc = 4;
3322        let mixes: Vec<f32> = (0..24).map(|i| (i as f32 * 0.37).sin() * 3.0).collect();
3323        let base: Vec<f32> = (0..24).map(|i| (i as f32 * 0.11).cos()).collect();
3324        let (mut pre, mut post, mut comb) = (vec![0.0; hc], vec![0.0; hc], vec![0.0; hc * hc]);
3325        hc_split_sinkhorn(
3326            &mixes,
3327            &[1.0, 1.0, 1.0],
3328            &base,
3329            hc,
3330            20,
3331            1e-6,
3332            &mut pre,
3333            &mut post,
3334            &mut comb,
3335        );
3336        let want_pre = [0.7310596, 0.8888268, 0.9525191, 0.97424865];
3337        let want_post = [1.9600224, 1.9534285, 1.9201256, 1.8160983];
3338        let want_comb = [
3339            0.5996052,
3340            0.28253591,
3341            0.09218107,
3342            0.025676856,
3343            0.17564717,
3344            0.22228767,
3345            0.27174541,
3346            0.33031881,
3347            0.029528176,
3348            0.12206022,
3349            0.32619134,
3350            0.5222193,
3351            0.19521846,
3352            0.37311527,
3353            0.30988118,
3354            0.12178412,
3355        ];
3356        for (i, w) in want_pre.iter().enumerate() {
3357            assert!((pre[i] - w).abs() < 1e-5, "pre[{i}]: {} vs {w}", pre[i]);
3358        }
3359        for (i, w) in want_post.iter().enumerate() {
3360            assert!((post[i] - w).abs() < 1e-5, "post[{i}]: {} vs {w}", post[i]);
3361        }
3362        for (i, w) in want_comb.iter().enumerate() {
3363            assert!((comb[i] - w).abs() < 1e-4, "comb[{i}]: {} vs {w}", comb[i]);
3364        }
3365    }
3366
3367    /// Sinkhorn's whole point is a doubly stochastic matrix: every row and
3368    /// every column sums to one. If the alternating normalization is wrong
3369    /// (or the loop count is off by one) the sums drift, and the residual
3370    /// mixing quietly gains or loses mass on every layer.
3371    #[test]
3372    fn sinkhorn_leaves_the_mixing_matrix_doubly_stochastic() {
3373        let hc = 4;
3374        let mix_hc = (2 + hc) * hc;
3375        // a deliberately lopsided projection
3376        let mixes: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.37).sin() * 3.0).collect();
3377        let base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.11).cos()).collect();
3378        let (mut pre, mut post, mut comb) = (vec![0.0; hc], vec![0.0; hc], vec![0.0; hc * hc]);
3379        hc_split_sinkhorn(
3380            &mixes,
3381            &[1.0, 1.0, 1.0],
3382            &base,
3383            hc,
3384            20,
3385            1e-6,
3386            &mut pre,
3387            &mut post,
3388            &mut comb,
3389        );
3390        for j in 0..hc {
3391            let r: f32 = comb[j * hc..(j + 1) * hc].iter().sum();
3392            assert!((r - 1.0).abs() < 2e-3, "row {j} sums to {r}");
3393            let c: f32 = (0..hc).map(|k| comb[k * hc + j]).sum();
3394            assert!((c - 1.0).abs() < 2e-3, "col {j} sums to {c}");
3395        }
3396        // pre is a gate in (eps, 1+eps); post carries the factor 2
3397        assert!(pre.iter().all(|&v| v > 0.0 && v < 1.001));
3398        assert!(post.iter().all(|&v| v >= 0.0 && v <= 2.0));
3399    }
3400
3401    /// Folding four copies and expanding them back must preserve a constant
3402    /// state exactly when the block contributes nothing: with post = 0 the
3403    /// expansion is a doubly stochastic mix of identical copies, i.e. itself.
3404    #[test]
3405    fn expand_of_identical_copies_is_a_fixed_point() {
3406        let (hc, dim) = (4usize, 3usize);
3407        let residual: Vec<f32> = std::iter::repeat([1.5f32, -2.0, 0.25])
3408            .take(hc)
3409            .flatten()
3410            .collect();
3411        let comb = {
3412            // exactly doubly stochastic: uniform
3413            vec![0.25f32; hc * hc]
3414        };
3415        let post = vec![0.0f32; hc];
3416        let mut out = vec![0.0f32; hc * dim];
3417        hc_expand(&[0.0; 3], &residual, &post, &comb, hc, dim, &mut out);
3418        for (o, r) in out.iter().zip(&residual) {
3419            assert!((o - r).abs() < 1e-6, "{o} vs {r}");
3420        }
3421    }
3422
3423    /// The bias must move the SELECTION without touching the weights: with a
3424    /// large bias on a low-scoring expert it gets picked, but its weight is
3425    /// still its own (small) score, renormalized.
3426    #[test]
3427    fn selection_bias_steers_the_choice_but_not_the_weights() {
3428        let scores = [3.0f32, 0.1, 2.0, 0.05];
3429        let bias = [0.0f32, 10.0, 0.0, 0.0];
3430        let (mut idx, mut w) = (Vec::new(), Vec::new());
3431        route(&scores, Some(&bias), 2, 1.5, None, None, &mut idx, &mut w);
3432        assert_eq!(idx[0], 1, "the biased expert must win selection");
3433        assert_eq!(idx[1], 0);
3434        // weights come from sqrt(softplus(score)) BEFORE the bias, so the
3435        // biased expert's share must be the smaller of the two
3436        assert!(w[0] < w[1], "biased expert kept its own (small) weight");
3437        let sum: f32 = w.iter().sum();
3438        assert!((sum - 1.5).abs() < 1e-5, "weights renormalize then scale");
3439    }
3440
3441    /// The sink is an extra logit with no value: it must lower every
3442    /// weight without adding output. With a huge sink the head should
3443    /// attend to almost nothing.
3444    #[test]
3445    fn attention_sink_drains_weight_without_contributing_output() {
3446        let hd = 2;
3447        let q = [1.0f32, 0.0];
3448        let kv = [1.0f32, 0.0, 0.0, 1.0];
3449        let mut out = vec![0.0f32; hd];
3450        sparse_attend(&q, &kv, &[0, 1], f32::NEG_INFINITY, 1.0, hd, &mut out);
3451        let plain = out.clone();
3452        assert!(plain[0] > plain[1], "the aligned key must dominate");
3453        sparse_attend(&q, &kv, &[0, 1], 20.0, 1.0, hd, &mut out);
3454        assert!(
3455            out[0] < plain[0] * 0.01 && out[1] < plain[1] * 0.01,
3456            "a large sink must drain nearly all the mass: {out:?}"
3457        );
3458    }
3459
3460    /// A masked slot must be ignored entirely — not folded in as a zero
3461    /// key, which would still add exp(0) to the denominator.
3462    #[test]
3463    fn masked_positions_leave_the_denominator_alone() {
3464        let hd = 2;
3465        let q = [1.0f32, 0.0];
3466        let kv = [1.0f32, 0.0, 0.0, 1.0];
3467        let (mut a, mut b) = (vec![0.0f32; hd], vec![0.0f32; hd]);
3468        sparse_attend(&q, &kv, &[0], f32::NEG_INFINITY, 1.0, hd, &mut a);
3469        sparse_attend(
3470            &q,
3471            &kv,
3472            &[0, usize::MAX],
3473            f32::NEG_INFINITY,
3474            1.0,
3475            hd,
3476            &mut b,
3477        );
3478        for (x, y) in a.iter().zip(&b) {
3479            assert!((x - y).abs() < 1e-6, "{x} vs {y}");
3480        }
3481    }
3482
3483    /// Forward then inverse rotation is the identity — the property the
3484    /// output path depends on.
3485    #[test]
3486    fn rope_tail_inverts_itself() {
3487        let inv_freq = [1.0f32, 0.5];
3488        let orig = [9.0f32, 8.0, 1.0, 2.0, 3.0, 4.0];
3489        let mut v = orig;
3490        rope_tail(&mut v, &inv_freq, 7, 4, false);
3491        assert!(v[..2] == orig[..2], "the non-rope head must not move");
3492        assert!(v[2..] != orig[2..], "the tail must actually rotate");
3493        rope_tail(&mut v, &inv_freq, 7, 4, true);
3494        for (a, b) in v.iter().zip(&orig) {
3495            assert!((a - b).abs() < 1e-5, "{a} vs {b}");
3496        }
3497    }
3498
3499    /// The window pooling is a softmax per DIMENSION over the ratio, with
3500    /// the position bias inside the exponent.
3501    #[test]
3502    fn compressor_pools_the_window_per_dimension() {
3503        let (ratio, width) = (2usize, 2usize);
3504        let kv = [1.0f32, 10.0, 3.0, 20.0];
3505        // dim 0: equal scores → mean; dim 1: second token wins by a mile
3506        let score = [0.0f32, 0.0, 0.0, 50.0];
3507        let ape = vec![0.0f32; ratio * width];
3508        let mut out = vec![0.0f32; width];
3509        compress_window(&kv, &score, &ape, ratio, width, &mut out);
3510        assert!(
3511            (out[0] - 2.0).abs() < 1e-5,
3512            "equal scores average: {}",
3513            out[0]
3514        );
3515        assert!(
3516            (out[1] - 20.0).abs() < 1e-3,
3517            "a dominant score wins: {}",
3518            out[1]
3519        );
3520    }
3521
3522    /// A negative dot product must not drag a position down: the relu
3523    /// means heads abstain rather than veto.
3524    #[test]
3525    fn index_scores_relu_before_weighting() {
3526        let (nh, hd) = (2usize, 2usize);
3527        // head 0 aligns with position 0, head 1 anti-aligns with it
3528        let q = [1.0f32, 0.0, -1.0, 0.0];
3529        let kv = [1.0f32, 0.0, 0.0, 1.0];
3530        let w = [1.0f32, 1.0];
3531        let mut sc = Vec::new();
3532        index_scores(&q, &kv, &w, nh, hd, 2, 2, None, &mut sc);
3533        // without the relu the anti-aligned head would cancel head 0 to zero
3534        assert!(sc[0] > 0.9, "abstention, not veto: {:?}", sc);
3535    }
3536
3537    #[test]
3538    fn index_scores_mask_the_future() {
3539        let (nh, hd) = (1usize, 2usize);
3540        let q = [1.0f32, 0.0];
3541        let kv = [1.0f32, 0.0, 1.0, 0.0, 1.0, 0.0];
3542        let w = [1.0f32];
3543        let mut sc = Vec::new();
3544        index_scores(&q, &kv, &w, nh, hd, 3, 2, None, &mut sc);
3545        assert!(sc[0].is_finite() && sc[1].is_finite());
3546        assert!(sc[2] == f32::NEG_INFINITY, "position 2 is in the future");
3547        let mut idx = Vec::new();
3548        top_k_positions(&sc, 3, &mut idx);
3549        assert_eq!(idx, vec![0, 1], "a masked slot never wins a slot");
3550    }
3551
3552    #[test]
3553    fn top_k_is_deterministic_on_ties() {
3554        let sc = [1.0f32, 1.0, 1.0, 0.0];
3555        let mut idx = Vec::new();
3556        top_k_positions(&sc, 2, &mut idx);
3557        assert_eq!(idx, vec![0, 1], "ties resolve to the lower index");
3558    }
3559
3560    /// The block cycle must leave the state's SHAPE intact (hc copies in,
3561    /// hc copies out) and must actually route the block's output back in:
3562    /// a block that writes a constant has to move every copy.
3563    #[test]
3564    fn hc_block_preserves_the_copy_structure_and_applies_the_block() {
3565        let cfg = Dsv4Cfg {
3566            dim: 4,
3567            n_heads: 1,
3568            head_dim: 4,
3569            rope_head_dim: 2,
3570            q_lora_rank: 4,
3571            o_lora_rank: 2,
3572            o_groups: 1,
3573            hc_mult: 4,
3574            hc_sinkhorn_iters: 20,
3575            hc_eps: 1e-6,
3576            norm_eps: 1e-6,
3577            n_routed_experts: 2,
3578            top_k: 1,
3579            moe_inter: 4,
3580            route_scale: 1.0,
3581            swiglu_limit: 10.0,
3582            window: 128,
3583            index_topk: 4,
3584            vocab: 8,
3585        };
3586        let (hc, dim) = (cfg.hc_mult, cfg.dim);
3587        let mix_hc = (2 + hc) * hc;
3588        let hc_fn: Vec<f32> = (0..mix_hc * hc * dim)
3589            .map(|i| ((i % 13) as f32 - 6.0) * 0.05)
3590            .collect();
3591        let hc_base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.2).sin()).collect();
3592        let norm_w = vec![1.0f32; dim];
3593        let mut state: Vec<f32> = (0..hc * dim).map(|i| (i as f32 * 0.3).cos()).collect();
3594        let before = state.clone();
3595        let mut scratch = HcScratch::new(&cfg);
3596        hc_block(
3597            &mut state,
3598            &hc_fn,
3599            &[1.0, 1.0, 1.0],
3600            &hc_base,
3601            &norm_w,
3602            &cfg,
3603            &mut scratch,
3604            None,
3605            |_folded, out: &mut [f32]| out.iter_mut().for_each(|o| *o = 1.0),
3606        );
3607        assert_eq!(state.len(), before.len(), "copy structure must survive");
3608        assert!(state.iter().all(|v| v.is_finite()), "{state:?}");
3609        assert!(
3610            state.iter().zip(&before).any(|(a, b)| (a - b).abs() > 1e-4),
3611            "the block's output has to reach the state"
3612        );
3613    }
3614
3615    #[test]
3616    fn hash_route_reads_the_table_row() {
3617        // vocab 3, top_k 2
3618        let table = [7.0f32, 9.0, 1.0, 2.0, 5.0, 6.0];
3619        assert_eq!(hash_route(&table, 3, 2, 0), vec![7, 9]);
3620        assert_eq!(hash_route(&table, 3, 2, 2), vec![5, 6]);
3621        // out-of-range ids clamp instead of panicking
3622        assert_eq!(hash_route(&table, 3, 2, 99), vec![5, 6]);
3623    }
3624
3625    /// A task mask restricts SELECTION and nothing else: the weights still
3626    /// come from the pre-bias scores and still renormalize, now over what
3627    /// survives. Masking must never reroute — an expert the mask forbids has
3628    /// to be absent, not replaced by a neighbour with the wrong weight.
3629    #[test]
3630    fn a_task_mask_restricts_selection_and_renormalizes() {
3631        // Expert 3 scores highest, then 1, then 2, then 0.
3632        let scores = [0.1f32, 4.0, 1.0, 9.0];
3633        let (mut idx, mut w) = (Vec::new(), Vec::new());
3634        route(&scores, None, 2, 1.0, None, None, &mut idx, &mut w);
3635        assert_eq!(idx, vec![3, 1], "unmasked: the two best win");
3636        let sum: f32 = w.iter().sum();
3637        assert!((sum - 1.0).abs() < 1e-5, "weights must sum to route_scale");
3638
3639        // Forbid the winner: the next two take its place and the weights
3640        // renormalize over them.
3641        let mask = [true, false, true, true];
3642        let (mut i2, mut w2) = (Vec::new(), Vec::new());
3643        route(&scores, None, 2, 1.0, None, Some(&mask), &mut i2, &mut w2);
3644        assert_eq!(i2, vec![3, 2], "masked expert must not be selected");
3645        let sum2: f32 = w2.iter().sum();
3646        assert!((sum2 - 1.0).abs() < 1e-5, "masked weights must renormalize");
3647
3648        // A mask leaving fewer than top_k experts yields fewer, not garbage.
3649        let tight = [false, false, false, true];
3650        let (mut i3, mut w3) = (Vec::new(), Vec::new());
3651        route(&scores, None, 2, 1.0, None, Some(&tight), &mut i3, &mut w3);
3652        assert_eq!(i3, vec![3]);
3653        assert_eq!(w3.len(), 1);
3654    }
3655
3656    /// On a hash layer the reference gathers the scores AT THE TABLE's
3657    /// experts. Choosing top-k first and swapping the indices afterwards
3658    /// leaves every weight attached to a different expert than the one it
3659    /// scales — silently, since both lists are the right length.
3660    #[test]
3661    fn hash_layers_weight_the_experts_the_table_names() {
3662        // Expert 3 scores highest, expert 0 lowest; the table names 0 and 1.
3663        let scores = [0.1f32, 0.4, 0.2, 5.0];
3664        let table = vec![0.0f32, 1.0];
3665        let idx_forced = hash_route(&table, 1, 2, 0);
3666        assert_eq!(idx_forced, vec![0, 1]);
3667
3668        let (mut idx, mut w) = (Vec::new(), Vec::new());
3669        route(
3670            &scores,
3671            None,
3672            2,
3673            1.0,
3674            Some(&idx_forced),
3675            None,
3676            &mut idx,
3677            &mut w,
3678        );
3679        assert_eq!(idx, vec![0, 1], "the table must decide the experts");
3680
3681        // The weights must be the table experts' own scores, normalized.
3682        let sp = |x: f32| (1.0 + x.exp()).ln().sqrt();
3683        let (s0, s1) = (sp(scores[0]), sp(scores[1]));
3684        let tot = s0 + s1;
3685        assert!(
3686            (w[0] - s0 / tot).abs() < 1e-6,
3687            "w[0]={} want {}",
3688            w[0],
3689            s0 / tot
3690        );
3691        assert!(
3692            (w[1] - s1 / tot).abs() < 1e-6,
3693            "w[1]={} want {}",
3694            w[1],
3695            s1 / tot
3696        );
3697
3698        // And the top-k path is untouched: expert 3 still wins there.
3699        let (mut idx2, mut w2) = (Vec::new(), Vec::new());
3700        route(&scores, None, 2, 1.0, None, None, &mut idx2, &mut w2);
3701        assert_eq!(idx2[0], 3, "without a table the highest score still wins");
3702    }
3703}
3704