1pub 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 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 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 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
100pub 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 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
139pub 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
152pub 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
185pub 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
193pub 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 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 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
268pub 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
278pub 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 let rd = rd.min(n) & !1;
293 let base = n - rd;
294 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
313pub 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
324pub 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
380pub 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 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
443pub 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#[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 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 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#[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 acc += dot.max(0.0) * head_weights[h];
548 }
549 acc
550 };
551 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
571pub fn top_k_positions(scores: &[f32], k: usize, out: &mut Vec<usize>) {
575 out.clear();
576 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#[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#[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 pub swiglu_limit: f32,
669 pub window: usize,
671 pub index_topk: usize,
672 pub vocab: usize,
673}
674
675#[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 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
729pub 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
756pub 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
775pub struct Dsv4Layer {
781 pub attn_norm: Vec<f32>,
782 pub ffn_norm: Vec<f32>,
783 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 pub compressor: Option<Dsv4Compressor>,
794 pub indexer: Option<Dsv4Indexer>,
796 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 pub gate: crate::qtensor::QTensor,
805 pub gate_bias: Option<Vec<f32>>,
807 pub tid2eid: Option<Vec<f32>>,
809 pub experts: Vec<Dsv4Expert>,
810 pub shared: Dsv4Expert,
811 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 pub ape: Vec<f32>,
831 pub ratio: usize,
832 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
843pub struct Dsv4Globals {
846 pub inv_freq_compress: Vec<f32>,
849 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
865pub struct Dsv4State {
869 pub window: Vec<Vec<f32>>,
871 pub compressed: Vec<Vec<f32>>,
873 pub index_kv: Vec<Vec<f32>>,
875 pub pending_kv: Vec<Vec<f32>>,
877 pub pending_score: Vec<Vec<f32>>,
878 pub prev_kv: Vec<Vec<f32>>,
881 pub prev_score: Vec<Vec<f32>>,
882 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 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#[allow(clippy::too_many_arguments)]
926#[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 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 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
999pub(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 pub static HC_NS: AtomicU64 = AtomicU64::new(0);
1014 pub static HEAD_NS: AtomicU64 = AtomicU64::new(0);
1016 pub static ALL_NS: AtomicU64 = AtomicU64::new(0);
1020 pub static TOKENS: AtomicU64 = AtomicU64::new(0);
1021
1022 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 pub fn report() {
1040 if !on() || REPORT.swap(true, Ordering::Relaxed) {
1041 return;
1042 }
1043 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
1098pub fn profile_report() {
1100 prof::report();
1101}
1102
1103fn 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#[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 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 if !crate::gpu_wgpu::dsv4_cache_write(kv_id, li, 0, &st.window[li], cap) {
1177 return false;
1178 }
1179 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#[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 inv_freq: &[f32],
1260 pool: Option<&crate::pool::Pool>,
1261 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 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 let on_gpu = gpu_attn_enabled();
1286
1287 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 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 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 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 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 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 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 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 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 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
1484pub 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
1493thread_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
1532pub fn take_route_counts() -> Vec<Vec<u64>> {
1534 ROUTE_COUNTS.with(|c| std::mem::take(&mut *c.borrow_mut()))
1535}
1536
1537struct 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#[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 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 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 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 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 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#[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 (folded, post, comb)
1889}
1890
1891#[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#[cfg(feature = "gpu")]
1920struct Pack {
1921 router: Vec<f32>,
1925 to_slot: Vec<usize>,
1927 remap: Vec<u32>,
1929 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 let gu_q2 = l
1955 .experts
1956 .first()
1957 .is_some_and(|e| e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP));
1958 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)?); 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#[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 let Some(model) = l.experts.first().and_then(|e| e.w1.model_arc()) else {
2091 no!("слой {li}: эксперты не отображены из файла");
2092 };
2093 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 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 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#[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; }
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#[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 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 #[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 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 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 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 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
2444fn 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
2466fn 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
2488thread_local! {
2494 static BODY: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
2496 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#[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 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 #[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 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 BODY.with(|b| b.borrow_mut().push(vec_json(folded)));
2608 }
2609 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 dump.push(format!(
2630 "{}{}",
2631 if li == 0 { "" } else { "," },
2632 vec_json(&state)
2633 ));
2634 }
2635 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 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 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
2751pub 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 assert!(!st.window[0].is_empty(), "sliding window never filled");
3094 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 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 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 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 #[test]
3157 fn swiglu_limit_clamps_up_both_ways_and_gate_only_from_above() {
3158 let inter = 4;
3159 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 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 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 #[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 #[test]
3277 fn overlapping_compressor_folds_both_windows() {
3278 let (ratio, d) = (2usize, 3usize);
3279 let cur_kv: Vec<f32> = vec![
3282 1.0, 1.0, 1.0, 10.0, 20.0, 30.0, 2.0, 2.0, 2.0, 40.0, 50.0, 60.0, ];
3285 let cur_sc: Vec<f32> = vec![
3287 0.0, 0.0, 0.0, 0.0, 0.0, 100.0, 0.0, 0.0, 0.0, 100.0, 100.0, 0.0,
3289 ];
3290 let prev_kv: Vec<f32> = vec![
3292 7.0, 8.0, 9.0, 0.0, 0.0, 0.0, 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 assert!((out[0] - 40.0).abs() < 1e-3, "dim0 = {}", out[0]);
3301 assert!((out[1] - 50.0).abs() < 1e-3, "dim1 = {}", out[1]);
3302 assert!((out[2] - 30.0).abs() < 1e-3, "dim2 = {}", out[2]);
3304
3305 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 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 #[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 #[test]
3392 fn sinkhorn_leaves_the_mixing_matrix_doubly_stochastic() {
3393 let hc = 4;
3394 let mix_hc = (2 + hc) * hc;
3395 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 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 #[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 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 #[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 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 #[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 #[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 #[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 #[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 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 #[test]
3545 fn index_scores_relu_before_weighting() {
3546 let (nh, hd) = (2usize, 2usize);
3547 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 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 #[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 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 assert_eq!(hash_route(&table, 3, 2, 99), vec![5, 6]);
3643 }
3644
3645 #[test]
3650 fn a_task_mask_restricts_selection_and_renormalizes() {
3651 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 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 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 #[test]
3681 fn hash_layers_weight_the_experts_the_table_names() {
3682 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 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 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