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    if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
2110        eprintln!(
2111            "[упаковка] слой {li}: globals={} n_routed={} subset={subset} remap.len={}",
2112            pk.globals.len(),
2113            cfg.n_routed_experts,
2114            pk.remap.len()
2115        );
2116    }
2117    let lg: Vec<f32> = if subset {
2118        logits.to_vec()
2119    } else {
2120        pk.globals.iter().map(|&g| logits[g]).collect()
2121    };
2122    let bias: Option<Vec<f32>> = l.gate_bias.as_deref().map(|b| {
2123        if subset {
2124            b.to_vec()
2125        } else {
2126            pk.globals.iter().map(|&g| b[g]).collect()
2127        }
2128    });
2129    let w = crate::gpu_wgpu::Dsv4MoeW {
2130        experts: &pk.tensors,
2131        logits: &lg,
2132        bias: bias.as_deref(),
2133        forced: fpack.as_deref(),
2134        remap: if subset { Some(&pk.remap) } else { None },
2135    };
2136    let g = crate::gpu_wgpu::Dsv4MoeGeom {
2137        hidden: cfg.dim,
2138        inter: cfg.moe_inter,
2139        top_k: cfg.top_k,
2140        route_scale: cfg.route_scale,
2141        swiglu_limit: cfg.swiglu_limit,
2142        gu_q2: l.experts.first().is_some_and(|e| {
2143            e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
2144        }),
2145    };
2146    let mut cold = Vec::new();
2147    if !crate::gpu_wgpu::dsv4_moe_frame(&model, &w, g, hidden, &mut cold, out) {
2148        return false;
2149    }
2150    // The picks the card had no room for, finished here and added in. Their
2151    // weights already carry the top-k normalisation the device applied.
2152    if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
2153        let (mut ci, mut cw) = (Vec::new(), Vec::new());
2154        route(
2155            logits,
2156            l.gate_bias.as_deref(),
2157            cfg.top_k,
2158            cfg.route_scale,
2159            forced,
2160            None,
2161            &mut ci,
2162            &mut cw,
2163        );
2164        eprintln!("[выбор CPU] слой {li}: {ci:?} веса {cw:?}");
2165        let csum: f32 = cold.iter().map(|c| c.1).sum();
2166        eprintln!(
2167            "[холодные] слой {li}: вернулось {} из {} | сумма холодных {csum:.4} | \
2168             route_scale {:.4} | {:?}",
2169            cold.len(),
2170            cfg.top_k,
2171            cfg.route_scale,
2172            &cold[..cold.len().min(3)]
2173        );
2174    }
2175    let mut acc = vec![0.0f32; cfg.dim];
2176    for &(gi, wt) in &cold {
2177        let Some(exp) = l.experts.get(gi) else { continue };
2178        run_expert(hidden, exp, cfg, wt, pool, &mut acc);
2179        for (o, a) in out.iter_mut().zip(&acc) {
2180            *o += a;
2181        }
2182    }
2183    true
2184}
2185
2186/// How much of each layer's compressed cache already sits on the card. ONE
2187/// map: a reader and a writer with a `static` each are two maps, and the
2188/// reader would never see a thing the writer put down.
2189/// The reallocation counter as of the last successful tail write. Any change
2190/// means some buffer was rebuilt and every tail count is stale.
2191#[cfg(feature = "gpu")]
2192fn last_grew(now: u64) -> u64 {
2193    use std::sync::atomic::{AtomicU64, Ordering};
2194    static SEEN: AtomicU64 = AtomicU64::new(0);
2195    let was = SEEN.load(Ordering::Relaxed);
2196    if was != now {
2197        SEEN.store(now, Ordering::Relaxed);
2198        compressed_map().lock().unwrap().clear();
2199        return u64::MAX; // force a full write this round
2200    }
2201    now
2202}
2203
2204#[cfg(feature = "gpu")]
2205fn compressed_map() -> &'static std::sync::Mutex<std::collections::HashMap<(u64, usize), usize>> {
2206    use std::collections::HashMap;
2207    use std::sync::{Mutex, OnceLock};
2208    static W: OnceLock<Mutex<HashMap<(u64, usize), usize>>> = OnceLock::new();
2209    W.get_or_init(|| Mutex::new(HashMap::new()))
2210}
2211
2212#[cfg(feature = "gpu")]
2213fn compressed_written(kv_id: u64, li: usize) -> usize {
2214    compressed_map()
2215        .lock()
2216        .unwrap()
2217        .get(&(kv_id, li))
2218        .copied()
2219        .unwrap_or(0)
2220}
2221
2222/// Reset to zero whenever a write fails or the buffer grows — a grown buffer
2223/// keeps none of its contents.
2224#[cfg(feature = "gpu")]
2225fn note_compressed(kv_id: u64, li: usize, n: usize) {
2226    compressed_map().lock().unwrap().insert((kv_id, li), n);
2227}
2228
2229#[cfg(feature = "gpu")]
2230fn gpu_moe2_enabled() -> bool {
2231    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2232    *ON.get_or_init(|| {
2233        std::env::var("CMF_DSV4_GPU_MOE2").is_ok_and(|v| v != "0")
2234            && crate::gpu::backend_available()
2235    })
2236}
2237
2238pub fn moe_step(
2239    hidden: &[f32],
2240    l: &Dsv4Layer,
2241    cfg: &Dsv4Cfg,
2242    token_id: u32,
2243    // Layer index — only used to bucket routing statistics.
2244    li: usize,
2245    pool: Option<&crate::pool::Pool>,
2246    out: &mut [f32],
2247) {
2248    let _t0 = prof::on().then(std::time::Instant::now);
2249    let _guard = scopeguard_moe(_t0, li);
2250    let mut logits = vec![0.0f32; cfg.n_routed_experts];
2251    l.gate.matvec(hidden, &mut logits, pool);
2252    let (mut idx, mut w) = (Vec::new(), Vec::new());
2253    route(
2254        &logits,
2255        l.gate_bias.as_deref(),
2256        cfg.top_k,
2257        cfg.route_scale,
2258        l.tid2eid
2259            .as_ref()
2260            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id))
2261            .as_deref(),
2262        l.mask.as_deref(),
2263        &mut idx,
2264        &mut w,
2265    );
2266    if route_stats_on() {
2267        record_route(li, 0, cfg.n_routed_experts, &idx);
2268    }
2269    // The whole block on the device, in one submission, or nothing. Routing
2270    // happens there too — the logits above are what it starts from, so the
2271    // CPU's own choice is discarded rather than second-guessed.
2272    #[cfg(feature = "gpu")]
2273    if gpu_moe2_enabled() {
2274        let forced = l
2275            .tid2eid
2276            .as_ref()
2277            .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
2278        if moe_frame(hidden, l, cfg, li, &logits, forced.as_deref(), pool, out) {
2279            // CMF_DSV4_MOE_CHECK=1 recomputes the same block on the CPU and
2280            // reports where they part. A wrong MoE does not fail — it answers
2281            // differently — and the toy agreed bit for bit while the release
2282            // did not, so the difference lives in something the toy has no
2283            // instance of. Only a per-layer number will say which.
2284            if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
2285                let mut want = vec![0.0f32; out.len()];
2286                let mut acc = vec![0.0f32; cfg.dim];
2287                for (e, &ei) in idx.iter().enumerate() {
2288                    let Some(exp) = l.experts.get(ei) else { continue };
2289                    run_expert(hidden, exp, cfg, w.get(e).copied().unwrap_or(0.0), pool, &mut acc);
2290                    for (o, a) in want.iter_mut().zip(&acc) {
2291                        *o += a;
2292                    }
2293                }
2294                run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
2295                for (o, a) in want.iter_mut().zip(&acc) {
2296                    *o += a;
2297                }
2298                let num: f32 = want.iter().zip(out.iter()).map(|(a, b)| (a - b) * (a - b)).sum();
2299                let den: f32 = want.iter().map(|a| a * a).sum::<f32>().max(1e-20);
2300                let rel = (num / den).sqrt();
2301                if rel > 1e-3 {
2302                    let packed = pack_for(l, cfg, li).map_or(0, |p| p.globals.len());
2303                    eprintln!(
2304                        "[кадр MoE] слой {li}: расхождение {rel:.3e} | выбрано {} | \
2305                         упаковано {packed} из {} | хеш={} | смещение={}",
2306                        idx.len(),
2307                        cfg.n_routed_experts,
2308                        l.tid2eid.is_some(),
2309                        l.gate_bias.is_some()
2310                    );
2311                }
2312            }
2313            return;
2314        }
2315    }
2316    if dump_path().is_some() {
2317        PICKED.with(|p| {
2318            let mut p = p.borrow_mut();
2319            if p.len() <= li {
2320                p.resize(li + 1, Vec::new());
2321            }
2322            p[li] = idx.clone();
2323        });
2324    }
2325    // One submission for the whole block — the chosen experts plus the
2326    // shared one. Per-expert dispatches are what made MoE slow elsewhere,
2327    // and the device keeps the weights across tokens, so the cost is the
2328    // arithmetic rather than the traffic. A refusal (missing kernel, mixed
2329    // layouts, weights that do not fit the budget) falls to the CPU whole,
2330    // never half.
2331    // CORRECT but SLOWER, so off by default. Parity holds on real weights
2332    // (perplexity 6.808 → 6.839 at 64 tokens, 5.102 → 5.146 at 200), and an
2333    // honest alternating A/B says 1.0 tok/s against the CPU's 2.2. The first
2334    // measurement claimed the opposite — 0.7 → 2.0 — because the CPU arm ran
2335    // first and paged in 158 GB for the GPU arm to inherit.
2336    //
2337    // The cost is not arithmetic, it is round trips: this submits and reads
2338    // back once per layer, forty-three times a token, and a discrete card
2339    // charges milliseconds for each. Fixing it means one submission per
2340    // token — the whole-token graph — not a faster kernel.
2341    //
2342    // `CMF_DSV4_GPU_MOE=1` opts in.
2343    fn gpu_moe_on() -> bool {
2344        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2345        *ON.get_or_init(|| std::env::var("CMF_DSV4_GPU_MOE").is_ok_and(|v| v != "0"))
2346    }
2347    if gpu_moe_on() && crate::gpu::enabled_here() {
2348        let mut jobs = Vec::with_capacity(idx.len() + 1);
2349        let mut model_ref = None;
2350        let mut ok = true;
2351        for (e, &ei) in idx.iter().enumerate() {
2352            let Some(exp) = l.experts.get(ei) else { continue };
2353            ok &= crate::pipeline::moe_push_job_parts(
2354                &exp.w1,
2355                &exp.w3,
2356                &exp.w2,
2357                hidden,
2358                w.get(e).copied().unwrap_or(0.0),
2359                cfg.swiglu_limit,
2360                &mut jobs,
2361                &mut model_ref,
2362            )
2363            .is_some();
2364        }
2365        ok &= crate::pipeline::moe_push_job_parts(
2366            &l.shared.w1,
2367            &l.shared.w3,
2368            &l.shared.w2,
2369            hidden,
2370            1.0,
2371            cfg.swiglu_limit,
2372            &mut jobs,
2373            &mut model_ref,
2374        )
2375        .is_some();
2376        if ok {
2377            if let Some(m) = model_ref.as_ref() {
2378                if crate::gpu::moe_block(m, &jobs, out) {
2379                    // CMF_DSV4_GPU_CHECK=1 recomputes the same block on the
2380                    // CPU and reports the divergence. A GPU MoE that is wrong
2381                    // does not fail — it answers differently — so the only way
2382                    // to know is to ask both.
2383                    if std::env::var("CMF_DSV4_GPU_CHECK").is_ok() {
2384                        let mut want = vec![0.0f32; out.len()];
2385                        let mut acc = vec![0.0f32; cfg.dim];
2386                        for (e, &ei) in idx.iter().enumerate() {
2387                            let Some(exp) = l.experts.get(ei) else { continue };
2388                            run_expert(
2389                                hidden, exp, cfg,
2390                                w.get(e).copied().unwrap_or(0.0), pool, &mut acc,
2391                            );
2392                            for (o, a) in want.iter_mut().zip(&acc) {
2393                                *o += a;
2394                            }
2395                        }
2396                        run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
2397                        for (o, a) in want.iter_mut().zip(&acc) {
2398                            *o += a;
2399                        }
2400                        let num: f32 = want
2401                            .iter()
2402                            .zip(out.iter())
2403                            .map(|(a, b)| (a - b) * (a - b))
2404                            .sum();
2405                        let den: f32 = want.iter().map(|a| a * a).sum::<f32>().max(1e-20);
2406                        eprintln!(
2407                            "[dsv4-gpu] слой {li}: расхождение {:.3e} | |CPU|={:.5} |GPU|={:.5} | экспертов {}",
2408                            (num / den).sqrt(),
2409                            den.sqrt(),
2410                            out.iter().map(|x| x * x).sum::<f32>().sqrt(),
2411                            jobs.len()
2412                        );
2413                    }
2414                    return;
2415                }
2416            }
2417        }
2418    }
2419    out.fill(0.0);
2420    let mut acc = vec![0.0f32; cfg.dim];
2421    for (e, &ei) in idx.iter().enumerate() {
2422        let Some(exp) = l.experts.get(ei) else {
2423            continue;
2424        };
2425        run_expert(
2426            hidden,
2427            exp,
2428            cfg,
2429            w.get(e).copied().unwrap_or(0.0),
2430            pool,
2431            &mut acc,
2432        );
2433        for (o, a) in out.iter_mut().zip(&acc) {
2434            *o += a;
2435        }
2436    }
2437    // The shared expert always runs, at weight 1.
2438    run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
2439    for (o, a) in out.iter_mut().zip(&acc) {
2440        *o += a;
2441    }
2442}
2443
2444/// The routed and shared experts both come through here, so the clamp and
2445/// the weight folding have exactly one implementation — `expert_swiglu`.
2446fn run_expert(
2447    x: &[f32],
2448    e: &Dsv4Expert,
2449    cfg: &Dsv4Cfg,
2450    weight: f32,
2451    pool: Option<&crate::pool::Pool>,
2452    out: &mut [f32],
2453) {
2454    expert_swiglu(
2455        x,
2456        &|src, dst| e.w1.matvec(src, dst, pool),
2457        &|src, dst| e.w3.matvec(src, dst, pool),
2458        &|src, dst| e.w2.matvec(src, dst, pool),
2459        cfg.moe_inter,
2460        weight,
2461        cfg.swiglu_limit,
2462        out,
2463    );
2464}
2465
2466/// `CMF_DSV4_TRACE=1` prints the hidden state's RMS after each half-block and
2467/// the logits' shape at the end. A 300B model that decodes nonsense gives no
2468/// other handle: this says whether the state grew, collapsed or went
2469/// non-finite, and at which layer — before anyone reaches for a debugger on a
2470/// hundred-gigabyte file.
2471fn no_compressed() -> bool {
2472    static OFF: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2473    *OFF.get_or_init(|| std::env::var("CMF_DSV4_NO_COMPRESSED").is_ok_and(|v| v != "0"))
2474}
2475
2476fn trace_on() -> bool {
2477    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2478    *ON.get_or_init(|| std::env::var("CMF_DSV4_TRACE").is_ok_and(|v| v != "0"))
2479}
2480
2481fn rms_of(v: &[f32]) -> f32 {
2482    if v.is_empty() {
2483        return 0.0;
2484    }
2485    (v.iter().map(|x| x * x).sum::<f32>() / v.len() as f32).sqrt()
2486}
2487
2488/// `CMF_DSV4_DUMP=<path>` appends one JSON line per token: the embedding, the
2489/// hyper-connection state after every layer, the folded-and-normed head input
2490/// and the logits. It exists to be diffed against the reference forward on
2491/// the same weights — the numerical parity this port has never had, which at
2492/// toy scale is a few thousand floats and entirely tractable.
2493thread_local! {
2494    /// The attention body's input and output per layer, interleaved.
2495    static BODY: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
2496    /// Experts chosen per layer for the token being decoded — the dump needs
2497    /// them, because two implementations that pick DIFFERENT experts diverge
2498    /// hugely for a reason that is not a bug in either.
2499    static PICKED: std::cell::RefCell<Vec<Vec<usize>>> =
2500        const { std::cell::RefCell::new(Vec::new()) };
2501}
2502
2503fn dump_path() -> Option<&'static str> {
2504    static P: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
2505    P.get_or_init(|| std::env::var("CMF_DSV4_DUMP").ok())
2506        .as_deref()
2507}
2508
2509fn dump_line(json: &str) {
2510    if let Some(p) = dump_path() {
2511        use std::io::Write as _;
2512        if let Ok(mut f) = std::fs::OpenOptions::new()
2513            .create(true)
2514            .append(true)
2515            .open(p)
2516        {
2517            let _ = writeln!(f, "{json}");
2518        }
2519    }
2520}
2521
2522fn vec_json(v: &[f32]) -> String {
2523    let mut s = String::with_capacity(v.len() * 9);
2524    s.push('[');
2525    for (i, x) in v.iter().enumerate() {
2526        if i > 0 {
2527            s.push(',');
2528        }
2529        s.push_str(&format!("{x:.6e}"));
2530    }
2531    s.push(']');
2532    s
2533}
2534
2535/// One token through the whole stack.
2536///
2537/// The hidden state is `hc_mult` copies of a `dim`-vector from the very
2538/// first line to the very last: the embedding is replicated, every layer
2539/// folds/expands around its two halves, and only `hc_head_fold` collapses
2540/// it before the output norm and the head. There is no point in this
2541/// function where an ordinary residual would fit.
2542#[allow(clippy::too_many_arguments)]
2543pub fn forward_token(
2544    g: &Dsv4Globals,
2545    layers: &[Dsv4Layer],
2546    cfg: &Dsv4Cfg,
2547    st: &mut Dsv4State,
2548    token_id: u32,
2549    inv_freq: &[f32],
2550    pool: Option<&crate::pool::Pool>,
2551    logits: &mut Vec<f32>,
2552) {
2553    let _t_all = prof::on().then(std::time::Instant::now);
2554    let _all_guard = Charge(_t_all, &prof::ALL_NS);
2555    let (hc, dim) = (cfg.hc_mult, cfg.dim);
2556
2557    // Embedding, replicated into the copies.
2558    let mut emb = vec![0.0f32; dim];
2559    g.embed.row_f32(token_id as usize, &mut emb);
2560    let mut state = vec![0.0f32; hc * dim];
2561    for j in 0..hc {
2562        state[j * dim..(j + 1) * dim].copy_from_slice(&emb);
2563    }
2564
2565    let mut scratch = HcScratch::new(cfg);
2566    let mut dump: Vec<String> = Vec::new();
2567    if dump_path().is_some() {
2568        dump.push(format!("\"embed\":{}", vec_json(&emb)));
2569        PICKED.with(|p| p.borrow_mut().clear());
2570        BODY.with(|b| b.borrow_mut().clear());
2571        dump.push(",\"layers\":[".into());
2572    }
2573    if trace_on() {
2574        eprintln!(
2575            "[dsv4] tok={token_id} pos={} embed rms={:.5}",
2576            st.pos,
2577            rms_of(&emb)
2578        );
2579    }
2580    // ── one submission per layer, when the device will take it ──
2581    #[cfg(feature = "gpu")]
2582    let layer_frames = gpu_layer_enabled()
2583        && dsv4_layer_loop(
2584            &mut state, layers, g, cfg, st, token_id, inv_freq, pool, &mut scratch,
2585        );
2586    #[cfg(not(feature = "gpu"))]
2587    let layer_frames = false;
2588
2589    for (li, l) in layers.iter().enumerate() {
2590        if layer_frames {
2591            break;
2592        }
2593        // attention half
2594        hc_block(
2595            &mut state,
2596            &l.hc_attn_fn,
2597            &l.hc_attn_scale,
2598            &l.hc_attn_base,
2599            &l.attn_norm,
2600            cfg,
2601            &mut scratch,
2602            pool,
2603            |folded, out| {
2604                if dump_path().is_some() {
2605                    // The body's own input and output, so the reference can be
2606                    // fed the port's input: then only the body can differ.
2607                    BODY.with(|b| b.borrow_mut().push(vec_json(folded)));
2608                }
2609                // The layer's kind decides its frequencies, not the model's.
2610                let freqs = if l.compressor.is_some() {
2611                    &g.inv_freq_compress
2612                } else {
2613                    &g.inv_freq_window
2614                };
2615                let freqs = if freqs.is_empty() {
2616                    inv_freq
2617                } else {
2618                    freqs.as_slice()
2619                };
2620                attention_step(folded, l, cfg, st, li, freqs, pool, None, out);
2621                if dump_path().is_some() {
2622                    BODY.with(|b| b.borrow_mut().push(vec_json(out)));
2623                }
2624            },
2625        );
2626        if dump_path().is_some() {
2627            // After the attention half only — this is what separates an
2628            // attention discrepancy from an expert one.
2629            dump.push(format!(
2630                "{}{}",
2631                if li == 0 { "" } else { "," },
2632                vec_json(&state)
2633            ));
2634        }
2635        // FFN half
2636        let _t_hc2 = prof::on().then(std::time::Instant::now);
2637        hc_block(
2638            &mut state,
2639            &l.hc_ffn_fn,
2640            &l.hc_ffn_scale,
2641            &l.hc_ffn_base,
2642            &l.ffn_norm,
2643            cfg,
2644            &mut scratch,
2645            pool,
2646            |folded, out| moe_step(folded, l, cfg, token_id, li, pool, out),
2647        );
2648        if let Some(t) = _t_hc2 {
2649            // The block's own time minus the expert step inside it — what the
2650            // fold, the norm and the expand cost on their own.
2651            prof::HC_NS.fetch_add(
2652                t.elapsed().as_nanos() as u64,
2653                std::sync::atomic::Ordering::Relaxed,
2654            );
2655        }
2656        if dump_path().is_some() {
2657            dump.push(format!(",{}", vec_json(&state)));
2658        }
2659        if trace_on() && (st.pos % 64 == 0 || st.pos == 199) {
2660            eprintln!(
2661                "[dsv4]  кеши слоя {li}: окно={} сжатых={} индекс={} (ratio={:?})",
2662                st.window[li].len() / cfg.head_dim.max(1),
2663                st.compressed[li].len() / cfg.head_dim.max(1),
2664                st.index_kv[li].len().max(1) / 128,
2665                l.compressor.as_ref().map(|c| (c.ratio, c.overlap)),
2666            );
2667        }
2668        if trace_on() {
2669            let bad = state.iter().filter(|v| !v.is_finite()).count();
2670            eprintln!(
2671                "[dsv4]  layer {li:>2}: rms={:.5}{}",
2672                rms_of(&state),
2673                if bad > 0 {
2674                    format!("  NON-FINITE x{bad}")
2675                } else {
2676                    String::new()
2677                }
2678            );
2679        }
2680    }
2681    st.pos += 1;
2682
2683    // Collapse the copies, normalize, project to the vocabulary.
2684    let mut h = vec![0.0f32; dim];
2685    hc_head_fold(
2686        &state,
2687        &g.hc_head_fn,
2688        g.hc_head_scale,
2689        &g.hc_head_base,
2690        cfg,
2691        pool,
2692        &mut h,
2693    );
2694    let _t_head = prof::on().then(std::time::Instant::now);
2695    rms_weighted(&mut h, &g.norm, cfg.norm_eps);
2696    logits.clear();
2697    logits.resize(g.head.rows(), 0.0);
2698    g.head.matvec(&h, logits, pool);
2699    if let Some(t) = _t_head {
2700        prof::HEAD_NS.fetch_add(
2701            t.elapsed().as_nanos() as u64,
2702            std::sync::atomic::Ordering::Relaxed,
2703        );
2704    }
2705    if dump_path().is_some() {
2706        dump.push("]".into());
2707        let picked = PICKED.with(|p| {
2708            p.borrow()
2709                .iter()
2710                .map(|v| {
2711                    format!(
2712                        "[{}]",
2713                        v.iter()
2714                            .map(|e| e.to_string())
2715                            .collect::<Vec<_>>()
2716                            .join(",")
2717                    )
2718                })
2719                .collect::<Vec<_>>()
2720                .join(",")
2721        });
2722        dump.push(format!(",\"experts\":[{picked}]"));
2723        let body = BODY.with(|b| b.borrow().join(","));
2724        dump.push(format!(",\"attn_io\":[{body}]"));
2725        dump_line(&format!(
2726            "{{\"tok\":{token_id},\"pos\":{},{},\"head\":{},\"logits\":{}}}",
2727            st.pos - 1,
2728            dump.join(""),
2729            vec_json(&h),
2730            vec_json(logits)
2731        ));
2732    }
2733    if trace_on() {
2734        let (mut top, mut best) = (0usize, f32::NEG_INFINITY);
2735        for (i, &v) in logits.iter().enumerate() {
2736            if v > best {
2737                best = v;
2738                top = i;
2739            }
2740        }
2741        let lo = logits.iter().cloned().fold(f32::MAX, f32::min);
2742        eprintln!(
2743            "[dsv4]  head: rms={:.5} logits[{}..{:.3}] argmax={top}",
2744            rms_of(&h),
2745            format_args!("{lo:.3}"),
2746            best
2747        );
2748    }
2749}
2750
2751/// Build the runtime weights from a converted `.cmf`.
2752///
2753/// Names are the converter's output (see `canon_name`'s deepseek_v4 arm):
2754/// attention keeps DeepSeek's own spelling under `self_attn.`, the MoE is
2755/// rewritten into the layout every other MoE here uses, and the hyper-
2756/// connection tensors ride under the layer prefix.
2757pub fn load(
2758    model: &std::sync::Arc<cortiq_core::CmfModel>,
2759    cfg: &Dsv4Cfg,
2760    n_layers: usize,
2761) -> Result<(Dsv4Globals, Vec<Dsv4Layer>), String> {
2762    let q = |name: &str| -> Result<crate::qtensor::QTensor, String> {
2763        crate::qtensor::QTensor::from_model(model, name)
2764    };
2765    // The small pieces — norms, the sink, ape, the hyper-connection
2766    // projections — are read as plain f32. They are not all 2-D (a norm is a
2767    // vector), so this cannot go through QTensor, which requires a matrix.
2768    let f = |name: &str| -> Result<Vec<f32>, String> {
2769        crate::loader::load_f32(model, name, &crate::loader::Overlay::None)
2770    };
2771    let opt_f = |name: &str| -> Option<Vec<f32>> { f(name).ok() };
2772
2773    // Two frequency tables, chosen per layer by whether it compresses. The
2774    // release's compress_rope_theta (160 000) is not in config.json — it
2775    // lives in inference/config.json — so it is pinned here with the other
2776    // constants the header cannot carry.
2777    let rope_of = |base: f32, yarn: bool| -> Vec<f32> {
2778        if yarn {
2779            crate::attention::yarn_inv_freq(cfg.rope_head_dim, base, 16.0, 65536, 32.0, 1.0)
2780        } else {
2781            crate::attention::rope_inv_freq(cfg.rope_head_dim, base)
2782        }
2783    };
2784    let globals = Dsv4Globals {
2785        inv_freq_compress: rope_of(160_000.0, true),
2786        inv_freq_window: rope_of(10_000.0, false),
2787        embed: q("model.embed_tokens.weight")?,
2788        norm: f("model.norm.weight")?,
2789        head: q("lm_head.weight")?,
2790        hc_head_fn: f("model.hc_head_fn")?,
2791        hc_head_base: f("model.hc_head_base")?,
2792        hc_head_scale: *f("model.hc_head_scale")?
2793            .first()
2794            .ok_or("dsv4: empty hc_head_scale")?,
2795    };
2796
2797    let mut layers = Vec::with_capacity(n_layers);
2798    for li in 0..n_layers {
2799        let p = format!("model.layers.{li}");
2800        let scale3 = |name: &str| -> Result<[f32; 3], String> {
2801            let v = f(name)?;
2802            if v.len() < 3 {
2803                return Err(format!("{name}: expected 3 scales, got {}", v.len()));
2804            }
2805            Ok([v[0], v[1], v[2]])
2806        };
2807        // The compressor exists on every layer whose ratio is non-zero;
2808        // its presence in the file is the only signal we need.
2809        let compressor = match q(&format!("{p}.self_attn.compressor.wkv.weight")) {
2810            Ok(wkv) => {
2811                let ape = f(&format!("{p}.self_attn.compressor.ape"))?;
2812                // ape is [ratio, coff*head_dim]; coff is 2 when the windows
2813                // overlap, which the release does at ratio 4.
2814                let width = wkv.rows();
2815                let ratio = (ape.len() / width.max(1)).max(1);
2816                Some(Dsv4Compressor {
2817                    wkv,
2818                    wgate: q(&format!("{p}.self_attn.compressor.wgate.weight"))?,
2819                    norm: f(&format!("{p}.self_attn.compressor.norm.weight"))?,
2820                    ape,
2821                    ratio,
2822                    overlap: ratio == 4,
2823                })
2824            }
2825            Err(_) => None,
2826        };
2827        let indexer = match q(&format!("{p}.self_attn.indexer.wq_b.weight")) {
2828            Ok(wq_b) => {
2829                let ape = f(&format!("{p}.self_attn.indexer.compressor.ape"))?;
2830                let cwkv = q(&format!("{p}.self_attn.indexer.compressor.wkv.weight"))?;
2831                let width = cwkv.rows();
2832                let ratio = (ape.len() / width.max(1)).max(1);
2833                Some(Dsv4Indexer {
2834                    wq_b,
2835                    weights_proj: q(&format!("{p}.self_attn.indexer.weights_proj.weight"))?,
2836                    compressor: Dsv4Compressor {
2837                        wkv: cwkv,
2838                        wgate: q(&format!("{p}.self_attn.indexer.compressor.wgate.weight"))?,
2839                        norm: f(&format!("{p}.self_attn.indexer.compressor.norm.weight"))?,
2840                        ape,
2841                        ratio,
2842                        overlap: ratio == 4,
2843                    },
2844                })
2845            }
2846            Err(_) => None,
2847        };
2848
2849        let mut experts = Vec::with_capacity(cfg.n_routed_experts);
2850        for e in 0..cfg.n_routed_experts {
2851            let ep = format!("{p}.mlp.experts.{e}");
2852            experts.push(Dsv4Expert {
2853                w1: q(&format!("{ep}.gate_proj.weight"))?,
2854                w2: q(&format!("{ep}.down_proj.weight"))?,
2855                w3: q(&format!("{ep}.up_proj.weight"))?,
2856            });
2857        }
2858
2859        layers.push(Dsv4Layer {
2860            attn_norm: f(&format!("{p}.input_layernorm.weight"))?,
2861            ffn_norm: f(&format!("{p}.post_attention_layernorm.weight"))?,
2862            wq_a: q(&format!("{p}.self_attn.wq_a.weight"))?,
2863            q_norm: f(&format!("{p}.self_attn.q_norm.weight"))?,
2864            wq_b: q(&format!("{p}.self_attn.wq_b.weight"))?,
2865            wkv: q(&format!("{p}.self_attn.wkv.weight"))?,
2866            kv_norm: f(&format!("{p}.self_attn.kv_norm.weight"))?,
2867            wo_a: q(&format!("{p}.self_attn.wo_a.weight"))?,
2868            wo_b: q(&format!("{p}.self_attn.wo_b.weight"))?,
2869            attn_sink: f(&format!("{p}.self_attn.attn_sink"))?,
2870            compressor,
2871            indexer,
2872            hc_attn_fn: f(&format!("{p}.hc_attn_fn"))?,
2873            hc_attn_base: f(&format!("{p}.hc_attn_base"))?,
2874            hc_attn_scale: scale3(&format!("{p}.hc_attn_scale"))?,
2875            hc_ffn_fn: f(&format!("{p}.hc_ffn_fn"))?,
2876            hc_ffn_base: f(&format!("{p}.hc_ffn_base"))?,
2877            hc_ffn_scale: scale3(&format!("{p}.hc_ffn_scale"))?,
2878            gate: q(&format!("{p}.mlp.gate.weight"))?,
2879            // The bias is absent exactly on the hash layers, and the table
2880            // is present exactly there — the file itself says which is which.
2881            gate_bias: opt_f(&format!("{p}.mlp.expert_bias")),
2882            tid2eid: opt_f(&format!("{p}.mlp.tid2eid")),
2883            experts,
2884            mask: if model.tensor(&format!("{p}.mlp.tid2eid")).is_some() {
2885                None
2886            } else {
2887                crate::loader::moe_task_mask(&format!("{p}."), cfg.n_routed_experts)
2888            },
2889            shared: Dsv4Expert {
2890                w1: q(&format!("{p}.mlp.shared_expert.gate_proj.weight"))?,
2891                w2: q(&format!("{p}.mlp.shared_expert.down_proj.weight"))?,
2892                w3: q(&format!("{p}.mlp.shared_expert.up_proj.weight"))?,
2893            },
2894        });
2895    }
2896    Ok((globals, layers))
2897}
2898
2899#[cfg(test)]
2900mod tests {
2901    use super::*;
2902
2903    // A whole model, small enough to reason about: 2 layers, 4 heads, 8
2904    // experts. Weights are deterministic and tiny, which is the point —
2905    // this test is about shapes, indexing and cache bookkeeping, the things
2906    // that a 138 GB file would surface only after an hour of loading.
2907    fn toy() -> (Dsv4Globals, Vec<Dsv4Layer>, Dsv4Cfg) {
2908        use crate::qtensor::QTensor;
2909        let cfg = Dsv4Cfg {
2910            dim: 32,
2911            n_heads: 4,
2912            head_dim: 8,
2913            rope_head_dim: 4,
2914            q_lora_rank: 16,
2915            o_lora_rank: 16,
2916            o_groups: 2,
2917            hc_mult: 4,
2918            hc_sinkhorn_iters: 20,
2919            hc_eps: 1e-6,
2920            norm_eps: 1e-6,
2921            n_routed_experts: 8,
2922            top_k: 2,
2923            moe_inter: 16,
2924            route_scale: 1.0,
2925            swiglu_limit: 10.0,
2926            window: 6,
2927            index_topk: 8,
2928            vocab: 24,
2929        };
2930        // Deterministic pseudo-random in a narrow band: big enough to move
2931        // the state, small enough that nothing saturates.
2932        let w = |n: usize, seed: usize| -> Vec<f32> {
2933            (0..n)
2934                .map(|i| (((i * 7 + seed * 13) % 101) as f32 / 101.0 - 0.5) * 0.3)
2935                .collect()
2936        };
2937        let t = |rows: usize, cols: usize, seed: usize| {
2938            QTensor::from_f32(w(rows * cols, seed), rows, cols)
2939        };
2940        let ones = |n: usize| vec![1.0f32; n];
2941
2942        let (dim, hc) = (cfg.dim, cfg.hc_mult);
2943        // q is n_heads*head_dim wide and kv is one head wide; rope rides the
2944        // tail of each rather than widening anything.
2945        let q_width = cfg.n_heads * cfg.head_dim;
2946        let kv_width = cfg.head_dim;
2947        let o_per_group = q_width / cfg.o_groups;
2948        let mut layers = Vec::new();
2949        for li in 0..2 {
2950            let experts: Vec<Dsv4Expert> = (0..cfg.n_routed_experts)
2951                .map(|e| Dsv4Expert {
2952                    w1: t(cfg.moe_inter, dim, 40 + e + li * 8),
2953                    w2: t(dim, cfg.moe_inter, 60 + e + li * 8),
2954                    w3: t(cfg.moe_inter, dim, 80 + e + li * 8),
2955                })
2956                .collect();
2957            // Layer 0 is a hash layer (table-routed); layer 1 routes normally
2958            // and carries the compressor — both paths get exercised.
2959            layers.push(Dsv4Layer {
2960                attn_norm: ones(dim),
2961                ffn_norm: ones(dim),
2962                wq_a: t(cfg.q_lora_rank, dim, 1 + li),
2963                q_norm: ones(cfg.q_lora_rank),
2964                wq_b: t(q_width, cfg.q_lora_rank, 3 + li),
2965                wkv: t(kv_width, dim, 5 + li),
2966                kv_norm: ones(kv_width),
2967                wo_a: t(cfg.o_groups * cfg.o_lora_rank, o_per_group, 7 + li),
2968                wo_b: t(dim, cfg.o_groups * cfg.o_lora_rank, 9 + li),
2969                attn_sink: vec![0.1; cfg.n_heads],
2970                // Layer 1 carries the OVERLAPPING compressor, as the release
2971                // does at ratio 4: the projection is twice the entry width.
2972                compressor: if li == 1 {
2973                    Some(Dsv4Compressor {
2974                        wkv: t(2 * kv_width, dim, 11),
2975                        wgate: t(2 * kv_width, dim, 13),
2976                        norm: ones(kv_width),
2977                        ape: vec![0.01; 4 * 2 * kv_width],
2978                        ratio: 4,
2979                        overlap: true,
2980                    })
2981                } else {
2982                    None
2983                },
2984                indexer: if li == 1 {
2985                    Some(Dsv4Indexer {
2986                        wq_b: t(2 * 16, cfg.q_lora_rank, 41),
2987                        weights_proj: t(2, dim, 43),
2988                        compressor: Dsv4Compressor {
2989                            wkv: t(2 * 16, dim, 45),
2990                            wgate: t(2 * 16, dim, 47),
2991                            norm: ones(16),
2992                            ape: vec![0.01; 4 * 2 * 16],
2993                            ratio: 4,
2994                            overlap: true,
2995                        },
2996                    })
2997                } else {
2998                    None
2999                },
3000                hc_attn_fn: w((2 + hc) * hc * hc * dim, 15 + li),
3001                hc_attn_base: w((2 + hc) * hc, 17 + li),
3002                hc_attn_scale: [1.0, 1.0, 1.0],
3003                hc_ffn_fn: w((2 + hc) * hc * hc * dim, 19 + li),
3004                hc_ffn_base: w((2 + hc) * hc, 21 + li),
3005                hc_ffn_scale: [1.0, 1.0, 1.0],
3006                gate: t(cfg.n_routed_experts, dim, 23 + li),
3007                gate_bias: if li == 1 {
3008                    Some(vec![0.0; cfg.n_routed_experts])
3009                } else {
3010                    None
3011                },
3012                tid2eid: if li == 0 {
3013                    Some(
3014                        (0..cfg.vocab * cfg.top_k)
3015                            .map(|i| (i % cfg.n_routed_experts) as f32)
3016                            .collect(),
3017                    )
3018                } else {
3019                    None
3020                },
3021                experts,
3022                mask: None,
3023                shared: Dsv4Expert {
3024                    w1: t(cfg.moe_inter, dim, 25 + li),
3025                    w2: t(dim, cfg.moe_inter, 27 + li),
3026                    w3: t(cfg.moe_inter, dim, 29 + li),
3027                },
3028            });
3029        }
3030        let inv = |base: f32| -> Vec<f32> {
3031            (0..cfg.rope_head_dim / 2)
3032                .map(|i| 1.0 / base.powf(2.0 * i as f32 / cfg.rope_head_dim as f32))
3033                .collect()
3034        };
3035        let g = Dsv4Globals {
3036            inv_freq_compress: inv(160000.0),
3037            inv_freq_window: inv(10000.0),
3038            embed: t(cfg.vocab, dim, 31),
3039            norm: ones(dim),
3040            head: t(cfg.vocab, dim, 33),
3041            hc_head_fn: w(hc * hc * dim, 35),
3042            hc_head_base: w(hc, 37),
3043            hc_head_scale: 1.0,
3044        };
3045        (g, layers, cfg)
3046    }
3047
3048    /// The whole stack, decoding a sequence. Every block is on the path:
3049    /// hyper-connections, the double-LoRA attention with its sink, the KV
3050    /// compressor firing on its ratio boundary, hash routing on one layer
3051    /// and score routing on the other.
3052    #[test]
3053    fn forward_token_decodes_a_sequence_without_falling_over() {
3054        let (g, layers, cfg) = toy();
3055        let mut st = Dsv4State::new(layers.len());
3056        let inv_freq: Vec<f32> = (0..cfg.rope_head_dim / 2)
3057            .map(|i| 1.0 / 10000f32.powf(2.0 * i as f32 / cfg.rope_head_dim as f32))
3058            .collect();
3059        let mut logits = Vec::new();
3060
3061        // Ten tokens: more than twice the compressor's ratio, so the
3062        // compressed cache is written on a boundary and read afterwards.
3063        let mut first: Option<Vec<f32>> = None;
3064        for (step, tok) in [3u32, 7, 1, 9, 4, 2, 8, 5, 6, 0].into_iter().enumerate() {
3065            forward_token(
3066                &g,
3067                &layers,
3068                &cfg,
3069                &mut st,
3070                tok,
3071                &inv_freq,
3072                None,
3073                &mut logits,
3074            );
3075            assert_eq!(logits.len(), cfg.vocab, "step {step}: logit count");
3076            assert!(
3077                logits.iter().all(|v| v.is_finite()),
3078                "step {step}: non-finite logit — {logits:?}"
3079            );
3080            // A model that has collapsed returns the same distribution
3081            // regardless of input; that is the failure this catches.
3082            let spread = logits.iter().cloned().fold(f32::MIN, f32::max)
3083                - logits.iter().cloned().fold(f32::MAX, f32::min);
3084            assert!(spread > 1e-6, "step {step}: logits are flat ({spread})");
3085            if step == 0 {
3086                first = Some(logits.clone());
3087            }
3088            assert_eq!(st.pos, step + 1, "position bookkeeping");
3089        }
3090
3091        // The cache has to have grown, and the compressor layer must have
3092        // emitted compressed entries (10 tokens / ratio 4 = 2 windows).
3093        assert!(!st.window[0].is_empty(), "sliding window never filled");
3094        // Ten tokens through a window of six: it must have slid, not grown.
3095        for (li, w) in st.window.iter().enumerate() {
3096            assert!(
3097                w.len() / cfg.head_dim <= cfg.window,
3098                "layer {li}: window holds {} positions, cap is {}",
3099                w.len() / cfg.head_dim,
3100                cfg.window
3101            );
3102        }
3103        assert!(
3104            !st.compressed[1].is_empty(),
3105            "compressor layer produced no compressed KV in 10 tokens"
3106        );
3107        // Ten tokens at ratio 4 fold twice, and the entries must be one head
3108        // wide — the overlapping projection is 2x that, so a width mistake
3109        // shows up here rather than as quiet nonsense.
3110        assert_eq!(
3111            st.compressed[1].len() / cfg.head_dim,
3112            2,
3113            "expected two folds in ten tokens at ratio 4"
3114        );
3115        assert!(
3116            !st.prev_kv[1].is_empty(),
3117            "the overlapping compressor never kept a previous window"
3118        );
3119        // Every layer that HAS an indexer must have filled the indexer's own
3120        // cache: it is what decides which compressed positions attention
3121        // reads, and an empty one silently discards the whole long-range
3122        // memory rather than failing.
3123        for (li, l) in layers.iter().enumerate() {
3124            if l.indexer.is_some() {
3125                assert!(
3126                    !st.index_kv[li].is_empty(),
3127                    "layer {li} has an indexer but its cache stayed empty"
3128                );
3129            }
3130        }
3131
3132        // Context must matter: the same token at position 0 of a fresh state
3133        // and at the end of a filled one cannot give identical logits.
3134        let mut fresh = Dsv4State::new(layers.len());
3135        let mut relogits = Vec::new();
3136        forward_token(
3137            &g,
3138            &layers,
3139            &cfg,
3140            &mut fresh,
3141            3,
3142            &inv_freq,
3143            None,
3144            &mut relogits,
3145        );
3146        assert_eq!(
3147            relogits,
3148            first.unwrap(),
3149            "the same token from a fresh state must reproduce exactly"
3150        );
3151    }
3152
3153    /// The reference clamps `up` on both sides but `gate` only from above.
3154    /// Getting that symmetric would quietly change every expert's output on
3155    /// the tokens that saturate, which is the hardest kind of bug to see.
3156    #[test]
3157    fn swiglu_limit_clamps_up_both_ways_and_gate_only_from_above() {
3158        let inter = 4;
3159        // gate = [-50, 50, 1, -1], up = [50, -50, 1, -1]
3160        let gate_src = [-50.0f32, 50.0, 1.0, -1.0];
3161        let up_src = [50.0f32, -50.0, 1.0, -1.0];
3162        let limit = 10.0f32;
3163        let mut got = vec![0.0f32; inter];
3164        expert_swiglu(
3165            &[0.0],
3166            &|_, d| d.copy_from_slice(&gate_src),
3167            &|_, d| d.copy_from_slice(&up_src),
3168            &|src, d| d.copy_from_slice(src),
3169            inter,
3170            1.0,
3171            limit,
3172            &mut got,
3173        );
3174        let silu = |g: f32| g / (1.0 + (-g).exp());
3175        // gate: only the +50 is cut, the -50 rides through silu untouched.
3176        let want = [
3177            silu(-50.0) * limit,
3178            silu(limit) * -limit,
3179            silu(1.0) * 1.0,
3180            silu(-1.0) * -1.0,
3181        ];
3182        for (i, w) in want.iter().enumerate() {
3183            assert!(
3184                (got[i] - w).abs() < 1e-5,
3185                "lane {i}: got {} want {w}",
3186                got[i]
3187            );
3188        }
3189        // And with the clamp off nothing is touched.
3190        let mut raw = vec![0.0f32; inter];
3191        expert_swiglu(
3192            &[0.0],
3193            &|_, d| d.copy_from_slice(&gate_src),
3194            &|_, d| d.copy_from_slice(&up_src),
3195            &|src, d| d.copy_from_slice(src),
3196            inter,
3197            1.0,
3198            0.0,
3199            &mut raw,
3200        );
3201        assert!(
3202            (raw[1] - silu(50.0) * -50.0).abs() < 1e-3,
3203            "limit 0 must not clamp"
3204        );
3205    }
3206
3207    /// The grouped projection writes its intermediate from several threads
3208    /// at once. Disjoint indices are the whole argument for that being safe,
3209    /// so the pooled result has to equal the serial one exactly — a race
3210    /// here would show up as occasional wrong tokens, not as a crash.
3211    #[test]
3212    fn grouped_projection_is_identical_with_and_without_a_pool() {
3213        let (groups, lora, per_group, dim) = (4usize, 128usize, 64usize, 32usize);
3214        let attn: Vec<f32> = (0..groups * per_group)
3215            .map(|i| ((i * 13) as f32 * 0.021).sin())
3216            .collect();
3217        let wo_a: Vec<f32> = (0..groups * lora * per_group)
3218            .map(|i| ((i * 7) as f32 * 0.011).cos())
3219            .collect();
3220        let wo_b: Vec<f32> = (0..dim * groups * lora)
3221            .map(|i| ((i * 5) as f32 * 0.009).sin())
3222            .collect();
3223        let row = |r: usize, x: &[f32], _sc: &mut [f32]| -> f32 {
3224            wo_a[r * per_group..(r + 1) * per_group]
3225                .iter()
3226                .zip(x)
3227                .map(|(a, b)| a * b)
3228                .sum()
3229        };
3230        let project = |mid: &[f32], dst: &mut [f32]| {
3231            for (d, o) in dst.iter_mut().enumerate() {
3232                *o = wo_b[d * mid.len()..(d + 1) * mid.len()]
3233                    .iter()
3234                    .zip(mid)
3235                    .map(|(a, b)| a * b)
3236                    .sum();
3237            }
3238        };
3239
3240        let mut serial = vec![0.0f32; dim];
3241        o_project(
3242            &attn,
3243            &row,
3244            per_group,
3245            &project,
3246            groups,
3247            lora,
3248            None,
3249            &mut serial,
3250        );
3251
3252        let pool = crate::pool::Pool::new(4);
3253        let mut pooled = vec![0.0f32; dim];
3254        o_project(
3255            &attn,
3256            &row,
3257            per_group,
3258            &project,
3259            groups,
3260            lora,
3261            Some(&pool),
3262            &mut pooled,
3263        );
3264        assert_eq!(serial, pooled, "the pooled projection diverged");
3265        assert!(
3266            serial.iter().any(|v| v.abs() > 1e-6),
3267            "test data is degenerate"
3268        );
3269    }
3270
3271    /// The overlapping compressor folds 2*ratio slots, not ratio: the
3272    /// previous window contributes its first half of dimensions and the
3273    /// current one its second half. Treating it as a plain compressor makes
3274    /// the entry twice as wide as the cache expects, which lands the whole
3275    /// thing in the wrong store rather than raising anything.
3276    #[test]
3277    fn overlapping_compressor_folds_both_windows() {
3278        let (ratio, d) = (2usize, 3usize);
3279        // Current window: two tokens, 2*d wide each. Second half is what the
3280        // current window contributes.
3281        let cur_kv: Vec<f32> = vec![
3282            1.0, 1.0, 1.0, /*|*/ 10.0, 20.0, 30.0, // token 0
3283            2.0, 2.0, 2.0, /*|*/ 40.0, 50.0, 60.0, // token 1
3284        ];
3285        // Make the current window's second-half scores dominate everywhere.
3286        let cur_sc: Vec<f32> = vec![
3287            0.0, 0.0, 0.0, /*|*/ 0.0, 0.0, 100.0, //
3288            0.0, 0.0, 0.0, /*|*/ 100.0, 100.0, 0.0,
3289        ];
3290        // Previous window: its FIRST half is what it contributes.
3291        let prev_kv: Vec<f32> = vec![
3292            7.0, 8.0, 9.0, /*|*/ 0.0, 0.0, 0.0, //
3293            5.0, 6.0, 7.0, /*|*/ 0.0, 0.0, 0.0,
3294        ];
3295        let prev_sc = vec![0.0f32; ratio * 2 * d];
3296
3297        let mut out = vec![0.0f32; d];
3298        compress_window_overlap(&prev_kv, &prev_sc, &cur_kv, &cur_sc, ratio, d, &mut out);
3299        // dim 0 and 1: token 1's second half wins (score 100)
3300        assert!((out[0] - 40.0).abs() < 1e-3, "dim0 = {}", out[0]);
3301        assert!((out[1] - 50.0).abs() < 1e-3, "dim1 = {}", out[1]);
3302        // dim 2: token 0's second half wins
3303        assert!((out[2] - 30.0).abs() < 1e-3, "dim2 = {}", out[2]);
3304
3305        // With no previous window the fold still works and uses only the
3306        // current one — this is the very first window of a generation.
3307        let mut first = vec![0.0f32; d];
3308        compress_window_overlap(&[], &[], &cur_kv, &cur_sc, ratio, d, &mut first);
3309        assert!(
3310            first.iter().all(|v| v.is_finite()),
3311            "first window: {first:?}"
3312        );
3313        assert!((first[0] - 40.0).abs() < 1e-3, "first dim0 = {}", first[0]);
3314
3315        // And a previous window with real scores does pull the result.
3316        let mut both = vec![0.0f32; d];
3317        let strong_prev = vec![100.0f32; ratio * 2 * d];
3318        compress_window_overlap(
3319            &prev_kv,
3320            &strong_prev,
3321            &cur_kv,
3322            &cur_sc,
3323            ratio,
3324            d,
3325            &mut both,
3326        );
3327        assert!(
3328            (both[0] - 40.0).abs() > 1.0,
3329            "a scored previous window must move the fold, got {}",
3330            both[0]
3331        );
3332    }
3333
3334    /// Numerical parity with the reference. The vectors below come from
3335    /// running `kernel.py::hc_split_sinkhorn`'s own formula on a fixed
3336    /// input; matching them pins the exponent order, the eps placement and
3337    /// the off-by-one in the iteration count all at once — a property test
3338    /// alone would pass with any of those wrong.
3339    #[test]
3340    fn sinkhorn_matches_the_reference_numbers() {
3341        let hc = 4;
3342        let mixes: Vec<f32> = (0..24).map(|i| (i as f32 * 0.37).sin() * 3.0).collect();
3343        let base: Vec<f32> = (0..24).map(|i| (i as f32 * 0.11).cos()).collect();
3344        let (mut pre, mut post, mut comb) = (vec![0.0; hc], vec![0.0; hc], vec![0.0; hc * hc]);
3345        hc_split_sinkhorn(
3346            &mixes,
3347            &[1.0, 1.0, 1.0],
3348            &base,
3349            hc,
3350            20,
3351            1e-6,
3352            &mut pre,
3353            &mut post,
3354            &mut comb,
3355        );
3356        let want_pre = [0.7310596, 0.8888268, 0.9525191, 0.97424865];
3357        let want_post = [1.9600224, 1.9534285, 1.9201256, 1.8160983];
3358        let want_comb = [
3359            0.5996052,
3360            0.28253591,
3361            0.09218107,
3362            0.025676856,
3363            0.17564717,
3364            0.22228767,
3365            0.27174541,
3366            0.33031881,
3367            0.029528176,
3368            0.12206022,
3369            0.32619134,
3370            0.5222193,
3371            0.19521846,
3372            0.37311527,
3373            0.30988118,
3374            0.12178412,
3375        ];
3376        for (i, w) in want_pre.iter().enumerate() {
3377            assert!((pre[i] - w).abs() < 1e-5, "pre[{i}]: {} vs {w}", pre[i]);
3378        }
3379        for (i, w) in want_post.iter().enumerate() {
3380            assert!((post[i] - w).abs() < 1e-5, "post[{i}]: {} vs {w}", post[i]);
3381        }
3382        for (i, w) in want_comb.iter().enumerate() {
3383            assert!((comb[i] - w).abs() < 1e-4, "comb[{i}]: {} vs {w}", comb[i]);
3384        }
3385    }
3386
3387    /// Sinkhorn's whole point is a doubly stochastic matrix: every row and
3388    /// every column sums to one. If the alternating normalization is wrong
3389    /// (or the loop count is off by one) the sums drift, and the residual
3390    /// mixing quietly gains or loses mass on every layer.
3391    #[test]
3392    fn sinkhorn_leaves_the_mixing_matrix_doubly_stochastic() {
3393        let hc = 4;
3394        let mix_hc = (2 + hc) * hc;
3395        // a deliberately lopsided projection
3396        let mixes: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.37).sin() * 3.0).collect();
3397        let base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.11).cos()).collect();
3398        let (mut pre, mut post, mut comb) = (vec![0.0; hc], vec![0.0; hc], vec![0.0; hc * hc]);
3399        hc_split_sinkhorn(
3400            &mixes,
3401            &[1.0, 1.0, 1.0],
3402            &base,
3403            hc,
3404            20,
3405            1e-6,
3406            &mut pre,
3407            &mut post,
3408            &mut comb,
3409        );
3410        for j in 0..hc {
3411            let r: f32 = comb[j * hc..(j + 1) * hc].iter().sum();
3412            assert!((r - 1.0).abs() < 2e-3, "row {j} sums to {r}");
3413            let c: f32 = (0..hc).map(|k| comb[k * hc + j]).sum();
3414            assert!((c - 1.0).abs() < 2e-3, "col {j} sums to {c}");
3415        }
3416        // pre is a gate in (eps, 1+eps); post carries the factor 2
3417        assert!(pre.iter().all(|&v| v > 0.0 && v < 1.001));
3418        assert!(post.iter().all(|&v| v >= 0.0 && v <= 2.0));
3419    }
3420
3421    /// Folding four copies and expanding them back must preserve a constant
3422    /// state exactly when the block contributes nothing: with post = 0 the
3423    /// expansion is a doubly stochastic mix of identical copies, i.e. itself.
3424    #[test]
3425    fn expand_of_identical_copies_is_a_fixed_point() {
3426        let (hc, dim) = (4usize, 3usize);
3427        let residual: Vec<f32> = std::iter::repeat([1.5f32, -2.0, 0.25])
3428            .take(hc)
3429            .flatten()
3430            .collect();
3431        let comb = {
3432            // exactly doubly stochastic: uniform
3433            vec![0.25f32; hc * hc]
3434        };
3435        let post = vec![0.0f32; hc];
3436        let mut out = vec![0.0f32; hc * dim];
3437        hc_expand(&[0.0; 3], &residual, &post, &comb, hc, dim, &mut out);
3438        for (o, r) in out.iter().zip(&residual) {
3439            assert!((o - r).abs() < 1e-6, "{o} vs {r}");
3440        }
3441    }
3442
3443    /// The bias must move the SELECTION without touching the weights: with a
3444    /// large bias on a low-scoring expert it gets picked, but its weight is
3445    /// still its own (small) score, renormalized.
3446    #[test]
3447    fn selection_bias_steers_the_choice_but_not_the_weights() {
3448        let scores = [3.0f32, 0.1, 2.0, 0.05];
3449        let bias = [0.0f32, 10.0, 0.0, 0.0];
3450        let (mut idx, mut w) = (Vec::new(), Vec::new());
3451        route(&scores, Some(&bias), 2, 1.5, None, None, &mut idx, &mut w);
3452        assert_eq!(idx[0], 1, "the biased expert must win selection");
3453        assert_eq!(idx[1], 0);
3454        // weights come from sqrt(softplus(score)) BEFORE the bias, so the
3455        // biased expert's share must be the smaller of the two
3456        assert!(w[0] < w[1], "biased expert kept its own (small) weight");
3457        let sum: f32 = w.iter().sum();
3458        assert!((sum - 1.5).abs() < 1e-5, "weights renormalize then scale");
3459    }
3460
3461    /// The sink is an extra logit with no value: it must lower every
3462    /// weight without adding output. With a huge sink the head should
3463    /// attend to almost nothing.
3464    #[test]
3465    fn attention_sink_drains_weight_without_contributing_output() {
3466        let hd = 2;
3467        let q = [1.0f32, 0.0];
3468        let kv = [1.0f32, 0.0, 0.0, 1.0];
3469        let mut out = vec![0.0f32; hd];
3470        sparse_attend(&q, &kv, &[0, 1], f32::NEG_INFINITY, 1.0, hd, &mut out);
3471        let plain = out.clone();
3472        assert!(plain[0] > plain[1], "the aligned key must dominate");
3473        sparse_attend(&q, &kv, &[0, 1], 20.0, 1.0, hd, &mut out);
3474        assert!(
3475            out[0] < plain[0] * 0.01 && out[1] < plain[1] * 0.01,
3476            "a large sink must drain nearly all the mass: {out:?}"
3477        );
3478    }
3479
3480    /// A masked slot must be ignored entirely — not folded in as a zero
3481    /// key, which would still add exp(0) to the denominator.
3482    #[test]
3483    fn masked_positions_leave_the_denominator_alone() {
3484        let hd = 2;
3485        let q = [1.0f32, 0.0];
3486        let kv = [1.0f32, 0.0, 0.0, 1.0];
3487        let (mut a, mut b) = (vec![0.0f32; hd], vec![0.0f32; hd]);
3488        sparse_attend(&q, &kv, &[0], f32::NEG_INFINITY, 1.0, hd, &mut a);
3489        sparse_attend(
3490            &q,
3491            &kv,
3492            &[0, usize::MAX],
3493            f32::NEG_INFINITY,
3494            1.0,
3495            hd,
3496            &mut b,
3497        );
3498        for (x, y) in a.iter().zip(&b) {
3499            assert!((x - y).abs() < 1e-6, "{x} vs {y}");
3500        }
3501    }
3502
3503    /// Forward then inverse rotation is the identity — the property the
3504    /// output path depends on.
3505    #[test]
3506    fn rope_tail_inverts_itself() {
3507        let inv_freq = [1.0f32, 0.5];
3508        let orig = [9.0f32, 8.0, 1.0, 2.0, 3.0, 4.0];
3509        let mut v = orig;
3510        rope_tail(&mut v, &inv_freq, 7, 4, false);
3511        assert!(v[..2] == orig[..2], "the non-rope head must not move");
3512        assert!(v[2..] != orig[2..], "the tail must actually rotate");
3513        rope_tail(&mut v, &inv_freq, 7, 4, true);
3514        for (a, b) in v.iter().zip(&orig) {
3515            assert!((a - b).abs() < 1e-5, "{a} vs {b}");
3516        }
3517    }
3518
3519    /// The window pooling is a softmax per DIMENSION over the ratio, with
3520    /// the position bias inside the exponent.
3521    #[test]
3522    fn compressor_pools_the_window_per_dimension() {
3523        let (ratio, width) = (2usize, 2usize);
3524        let kv = [1.0f32, 10.0, 3.0, 20.0];
3525        // dim 0: equal scores → mean; dim 1: second token wins by a mile
3526        let score = [0.0f32, 0.0, 0.0, 50.0];
3527        let ape = vec![0.0f32; ratio * width];
3528        let mut out = vec![0.0f32; width];
3529        compress_window(&kv, &score, &ape, ratio, width, &mut out);
3530        assert!(
3531            (out[0] - 2.0).abs() < 1e-5,
3532            "equal scores average: {}",
3533            out[0]
3534        );
3535        assert!(
3536            (out[1] - 20.0).abs() < 1e-3,
3537            "a dominant score wins: {}",
3538            out[1]
3539        );
3540    }
3541
3542    /// A negative dot product must not drag a position down: the relu
3543    /// means heads abstain rather than veto.
3544    #[test]
3545    fn index_scores_relu_before_weighting() {
3546        let (nh, hd) = (2usize, 2usize);
3547        // head 0 aligns with position 0, head 1 anti-aligns with it
3548        let q = [1.0f32, 0.0, -1.0, 0.0];
3549        let kv = [1.0f32, 0.0, 0.0, 1.0];
3550        let w = [1.0f32, 1.0];
3551        let mut sc = Vec::new();
3552        index_scores(&q, &kv, &w, nh, hd, 2, 2, None, &mut sc);
3553        // without the relu the anti-aligned head would cancel head 0 to zero
3554        assert!(sc[0] > 0.9, "abstention, not veto: {:?}", sc);
3555    }
3556
3557    #[test]
3558    fn index_scores_mask_the_future() {
3559        let (nh, hd) = (1usize, 2usize);
3560        let q = [1.0f32, 0.0];
3561        let kv = [1.0f32, 0.0, 1.0, 0.0, 1.0, 0.0];
3562        let w = [1.0f32];
3563        let mut sc = Vec::new();
3564        index_scores(&q, &kv, &w, nh, hd, 3, 2, None, &mut sc);
3565        assert!(sc[0].is_finite() && sc[1].is_finite());
3566        assert!(sc[2] == f32::NEG_INFINITY, "position 2 is in the future");
3567        let mut idx = Vec::new();
3568        top_k_positions(&sc, 3, &mut idx);
3569        assert_eq!(idx, vec![0, 1], "a masked slot never wins a slot");
3570    }
3571
3572    #[test]
3573    fn top_k_is_deterministic_on_ties() {
3574        let sc = [1.0f32, 1.0, 1.0, 0.0];
3575        let mut idx = Vec::new();
3576        top_k_positions(&sc, 2, &mut idx);
3577        assert_eq!(idx, vec![0, 1], "ties resolve to the lower index");
3578    }
3579
3580    /// The block cycle must leave the state's SHAPE intact (hc copies in,
3581    /// hc copies out) and must actually route the block's output back in:
3582    /// a block that writes a constant has to move every copy.
3583    #[test]
3584    fn hc_block_preserves_the_copy_structure_and_applies_the_block() {
3585        let cfg = Dsv4Cfg {
3586            dim: 4,
3587            n_heads: 1,
3588            head_dim: 4,
3589            rope_head_dim: 2,
3590            q_lora_rank: 4,
3591            o_lora_rank: 2,
3592            o_groups: 1,
3593            hc_mult: 4,
3594            hc_sinkhorn_iters: 20,
3595            hc_eps: 1e-6,
3596            norm_eps: 1e-6,
3597            n_routed_experts: 2,
3598            top_k: 1,
3599            moe_inter: 4,
3600            route_scale: 1.0,
3601            swiglu_limit: 10.0,
3602            window: 128,
3603            index_topk: 4,
3604            vocab: 8,
3605        };
3606        let (hc, dim) = (cfg.hc_mult, cfg.dim);
3607        let mix_hc = (2 + hc) * hc;
3608        let hc_fn: Vec<f32> = (0..mix_hc * hc * dim)
3609            .map(|i| ((i % 13) as f32 - 6.0) * 0.05)
3610            .collect();
3611        let hc_base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.2).sin()).collect();
3612        let norm_w = vec![1.0f32; dim];
3613        let mut state: Vec<f32> = (0..hc * dim).map(|i| (i as f32 * 0.3).cos()).collect();
3614        let before = state.clone();
3615        let mut scratch = HcScratch::new(&cfg);
3616        hc_block(
3617            &mut state,
3618            &hc_fn,
3619            &[1.0, 1.0, 1.0],
3620            &hc_base,
3621            &norm_w,
3622            &cfg,
3623            &mut scratch,
3624            None,
3625            |_folded, out: &mut [f32]| out.iter_mut().for_each(|o| *o = 1.0),
3626        );
3627        assert_eq!(state.len(), before.len(), "copy structure must survive");
3628        assert!(state.iter().all(|v| v.is_finite()), "{state:?}");
3629        assert!(
3630            state.iter().zip(&before).any(|(a, b)| (a - b).abs() > 1e-4),
3631            "the block's output has to reach the state"
3632        );
3633    }
3634
3635    #[test]
3636    fn hash_route_reads_the_table_row() {
3637        // vocab 3, top_k 2
3638        let table = [7.0f32, 9.0, 1.0, 2.0, 5.0, 6.0];
3639        assert_eq!(hash_route(&table, 3, 2, 0), vec![7, 9]);
3640        assert_eq!(hash_route(&table, 3, 2, 2), vec![5, 6]);
3641        // out-of-range ids clamp instead of panicking
3642        assert_eq!(hash_route(&table, 3, 2, 99), vec![5, 6]);
3643    }
3644
3645    /// A task mask restricts SELECTION and nothing else: the weights still
3646    /// come from the pre-bias scores and still renormalize, now over what
3647    /// survives. Masking must never reroute — an expert the mask forbids has
3648    /// to be absent, not replaced by a neighbour with the wrong weight.
3649    #[test]
3650    fn a_task_mask_restricts_selection_and_renormalizes() {
3651        // Expert 3 scores highest, then 1, then 2, then 0.
3652        let scores = [0.1f32, 4.0, 1.0, 9.0];
3653        let (mut idx, mut w) = (Vec::new(), Vec::new());
3654        route(&scores, None, 2, 1.0, None, None, &mut idx, &mut w);
3655        assert_eq!(idx, vec![3, 1], "unmasked: the two best win");
3656        let sum: f32 = w.iter().sum();
3657        assert!((sum - 1.0).abs() < 1e-5, "weights must sum to route_scale");
3658
3659        // Forbid the winner: the next two take its place and the weights
3660        // renormalize over them.
3661        let mask = [true, false, true, true];
3662        let (mut i2, mut w2) = (Vec::new(), Vec::new());
3663        route(&scores, None, 2, 1.0, None, Some(&mask), &mut i2, &mut w2);
3664        assert_eq!(i2, vec![3, 2], "masked expert must not be selected");
3665        let sum2: f32 = w2.iter().sum();
3666        assert!((sum2 - 1.0).abs() < 1e-5, "masked weights must renormalize");
3667
3668        // A mask leaving fewer than top_k experts yields fewer, not garbage.
3669        let tight = [false, false, false, true];
3670        let (mut i3, mut w3) = (Vec::new(), Vec::new());
3671        route(&scores, None, 2, 1.0, None, Some(&tight), &mut i3, &mut w3);
3672        assert_eq!(i3, vec![3]);
3673        assert_eq!(w3.len(), 1);
3674    }
3675
3676    /// On a hash layer the reference gathers the scores AT THE TABLE's
3677    /// experts. Choosing top-k first and swapping the indices afterwards
3678    /// leaves every weight attached to a different expert than the one it
3679    /// scales — silently, since both lists are the right length.
3680    #[test]
3681    fn hash_layers_weight_the_experts_the_table_names() {
3682        // Expert 3 scores highest, expert 0 lowest; the table names 0 and 1.
3683        let scores = [0.1f32, 0.4, 0.2, 5.0];
3684        let table = vec![0.0f32, 1.0];
3685        let idx_forced = hash_route(&table, 1, 2, 0);
3686        assert_eq!(idx_forced, vec![0, 1]);
3687
3688        let (mut idx, mut w) = (Vec::new(), Vec::new());
3689        route(
3690            &scores,
3691            None,
3692            2,
3693            1.0,
3694            Some(&idx_forced),
3695            None,
3696            &mut idx,
3697            &mut w,
3698        );
3699        assert_eq!(idx, vec![0, 1], "the table must decide the experts");
3700
3701        // The weights must be the table experts' own scores, normalized.
3702        let sp = |x: f32| (1.0 + x.exp()).ln().sqrt();
3703        let (s0, s1) = (sp(scores[0]), sp(scores[1]));
3704        let tot = s0 + s1;
3705        assert!(
3706            (w[0] - s0 / tot).abs() < 1e-6,
3707            "w[0]={} want {}",
3708            w[0],
3709            s0 / tot
3710        );
3711        assert!(
3712            (w[1] - s1 / tot).abs() < 1e-6,
3713            "w[1]={} want {}",
3714            w[1],
3715            s1 / tot
3716        );
3717
3718        // And the top-k path is untouched: expert 3 still wins there.
3719        let (mut idx2, mut w2) = (Vec::new(), Vec::new());
3720        route(&scores, None, 2, 1.0, None, None, &mut idx2, &mut w2);
3721        assert_eq!(idx2[0], 3, "without a table the highest score still wins");
3722    }
3723}
3724