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 let lg: Vec<f32> = if subset {
2110 logits.to_vec()
2111 } else {
2112 pk.globals.iter().map(|&g| logits[g]).collect()
2113 };
2114 let bias: Option<Vec<f32>> = l.gate_bias.as_deref().map(|b| {
2115 if subset {
2116 b.to_vec()
2117 } else {
2118 pk.globals.iter().map(|&g| b[g]).collect()
2119 }
2120 });
2121 let w = crate::gpu_wgpu::Dsv4MoeW {
2122 experts: &pk.tensors,
2123 logits: &lg,
2124 bias: bias.as_deref(),
2125 forced: fpack.as_deref(),
2126 remap: if subset { Some(&pk.remap) } else { None },
2127 };
2128 let g = crate::gpu_wgpu::Dsv4MoeGeom {
2129 hidden: cfg.dim,
2130 inter: cfg.moe_inter,
2131 top_k: cfg.top_k,
2132 route_scale: cfg.route_scale,
2133 swiglu_limit: cfg.swiglu_limit,
2134 gu_q2: l.experts.first().is_some_and(|e| {
2135 e.w1.model_dtype() == Some(cortiq_core::TensorDtype::Q2TiledP)
2136 }),
2137 };
2138 let mut cold = Vec::new();
2139 if !crate::gpu_wgpu::dsv4_moe_frame(&model, &w, g, hidden, &mut cold, out) {
2140 return false;
2141 }
2142 if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
2145 let csum: f32 = cold.iter().map(|c| c.1).sum();
2146 eprintln!(
2147 "[холодные] слой {li}: вернулось {} из {} | сумма холодных {csum:.4} | \
2148 route_scale {:.4} | {:?}",
2149 cold.len(),
2150 cfg.top_k,
2151 cfg.route_scale,
2152 &cold[..cold.len().min(3)]
2153 );
2154 }
2155 let mut acc = vec![0.0f32; cfg.dim];
2156 for &(gi, wt) in &cold {
2157 let Some(exp) = l.experts.get(gi) else { continue };
2158 run_expert(hidden, exp, cfg, wt, pool, &mut acc);
2159 for (o, a) in out.iter_mut().zip(&acc) {
2160 *o += a;
2161 }
2162 }
2163 true
2164}
2165
2166#[cfg(feature = "gpu")]
2172fn last_grew(now: u64) -> u64 {
2173 use std::sync::atomic::{AtomicU64, Ordering};
2174 static SEEN: AtomicU64 = AtomicU64::new(0);
2175 let was = SEEN.load(Ordering::Relaxed);
2176 if was != now {
2177 SEEN.store(now, Ordering::Relaxed);
2178 compressed_map().lock().unwrap().clear();
2179 return u64::MAX; }
2181 now
2182}
2183
2184#[cfg(feature = "gpu")]
2185fn compressed_map() -> &'static std::sync::Mutex<std::collections::HashMap<(u64, usize), usize>> {
2186 use std::collections::HashMap;
2187 use std::sync::{Mutex, OnceLock};
2188 static W: OnceLock<Mutex<HashMap<(u64, usize), usize>>> = OnceLock::new();
2189 W.get_or_init(|| Mutex::new(HashMap::new()))
2190}
2191
2192#[cfg(feature = "gpu")]
2193fn compressed_written(kv_id: u64, li: usize) -> usize {
2194 compressed_map()
2195 .lock()
2196 .unwrap()
2197 .get(&(kv_id, li))
2198 .copied()
2199 .unwrap_or(0)
2200}
2201
2202#[cfg(feature = "gpu")]
2205fn note_compressed(kv_id: u64, li: usize, n: usize) {
2206 compressed_map().lock().unwrap().insert((kv_id, li), n);
2207}
2208
2209#[cfg(feature = "gpu")]
2210fn gpu_moe2_enabled() -> bool {
2211 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2212 *ON.get_or_init(|| {
2213 std::env::var("CMF_DSV4_GPU_MOE2").is_ok_and(|v| v != "0")
2214 && crate::gpu::backend_available()
2215 })
2216}
2217
2218pub fn moe_step(
2219 hidden: &[f32],
2220 l: &Dsv4Layer,
2221 cfg: &Dsv4Cfg,
2222 token_id: u32,
2223 li: usize,
2225 pool: Option<&crate::pool::Pool>,
2226 out: &mut [f32],
2227) {
2228 let _t0 = prof::on().then(std::time::Instant::now);
2229 let _guard = scopeguard_moe(_t0, li);
2230 let mut logits = vec![0.0f32; cfg.n_routed_experts];
2231 l.gate.matvec(hidden, &mut logits, pool);
2232 let (mut idx, mut w) = (Vec::new(), Vec::new());
2233 route(
2234 &logits,
2235 l.gate_bias.as_deref(),
2236 cfg.top_k,
2237 cfg.route_scale,
2238 l.tid2eid
2239 .as_ref()
2240 .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id))
2241 .as_deref(),
2242 l.mask.as_deref(),
2243 &mut idx,
2244 &mut w,
2245 );
2246 if route_stats_on() {
2247 record_route(li, 0, cfg.n_routed_experts, &idx);
2248 }
2249 #[cfg(feature = "gpu")]
2253 if gpu_moe2_enabled() {
2254 let forced = l
2255 .tid2eid
2256 .as_ref()
2257 .map(|tbl| hash_route(tbl, cfg.vocab, cfg.top_k, token_id));
2258 if moe_frame(hidden, l, cfg, li, &logits, forced.as_deref(), pool, out) {
2259 if std::env::var("CMF_DSV4_MOE_CHECK").is_ok() {
2265 let mut want = vec![0.0f32; out.len()];
2266 let mut acc = vec![0.0f32; cfg.dim];
2267 for (e, &ei) in idx.iter().enumerate() {
2268 let Some(exp) = l.experts.get(ei) else { continue };
2269 run_expert(hidden, exp, cfg, w.get(e).copied().unwrap_or(0.0), pool, &mut acc);
2270 for (o, a) in want.iter_mut().zip(&acc) {
2271 *o += a;
2272 }
2273 }
2274 run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
2275 for (o, a) in want.iter_mut().zip(&acc) {
2276 *o += a;
2277 }
2278 let num: f32 = want.iter().zip(out.iter()).map(|(a, b)| (a - b) * (a - b)).sum();
2279 let den: f32 = want.iter().map(|a| a * a).sum::<f32>().max(1e-20);
2280 let rel = (num / den).sqrt();
2281 if rel > 1e-3 {
2282 let packed = pack_for(l, cfg, li).map_or(0, |p| p.globals.len());
2283 eprintln!(
2284 "[кадр MoE] слой {li}: расхождение {rel:.3e} | выбрано {} | \
2285 упаковано {packed} из {} | хеш={} | смещение={}",
2286 idx.len(),
2287 cfg.n_routed_experts,
2288 l.tid2eid.is_some(),
2289 l.gate_bias.is_some()
2290 );
2291 }
2292 }
2293 return;
2294 }
2295 }
2296 if dump_path().is_some() {
2297 PICKED.with(|p| {
2298 let mut p = p.borrow_mut();
2299 if p.len() <= li {
2300 p.resize(li + 1, Vec::new());
2301 }
2302 p[li] = idx.clone();
2303 });
2304 }
2305 fn gpu_moe_on() -> bool {
2324 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2325 *ON.get_or_init(|| std::env::var("CMF_DSV4_GPU_MOE").is_ok_and(|v| v != "0"))
2326 }
2327 if gpu_moe_on() && crate::gpu::enabled_here() {
2328 let mut jobs = Vec::with_capacity(idx.len() + 1);
2329 let mut model_ref = None;
2330 let mut ok = true;
2331 for (e, &ei) in idx.iter().enumerate() {
2332 let Some(exp) = l.experts.get(ei) else { continue };
2333 ok &= crate::pipeline::moe_push_job_parts(
2334 &exp.w1,
2335 &exp.w3,
2336 &exp.w2,
2337 hidden,
2338 w.get(e).copied().unwrap_or(0.0),
2339 cfg.swiglu_limit,
2340 &mut jobs,
2341 &mut model_ref,
2342 )
2343 .is_some();
2344 }
2345 ok &= crate::pipeline::moe_push_job_parts(
2346 &l.shared.w1,
2347 &l.shared.w3,
2348 &l.shared.w2,
2349 hidden,
2350 1.0,
2351 cfg.swiglu_limit,
2352 &mut jobs,
2353 &mut model_ref,
2354 )
2355 .is_some();
2356 if ok {
2357 if let Some(m) = model_ref.as_ref() {
2358 if crate::gpu::moe_block(m, &jobs, out) {
2359 if std::env::var("CMF_DSV4_GPU_CHECK").is_ok() {
2364 let mut want = vec![0.0f32; out.len()];
2365 let mut acc = vec![0.0f32; cfg.dim];
2366 for (e, &ei) in idx.iter().enumerate() {
2367 let Some(exp) = l.experts.get(ei) else { continue };
2368 run_expert(
2369 hidden, exp, cfg,
2370 w.get(e).copied().unwrap_or(0.0), pool, &mut acc,
2371 );
2372 for (o, a) in want.iter_mut().zip(&acc) {
2373 *o += a;
2374 }
2375 }
2376 run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
2377 for (o, a) in want.iter_mut().zip(&acc) {
2378 *o += a;
2379 }
2380 let num: f32 = want
2381 .iter()
2382 .zip(out.iter())
2383 .map(|(a, b)| (a - b) * (a - b))
2384 .sum();
2385 let den: f32 = want.iter().map(|a| a * a).sum::<f32>().max(1e-20);
2386 eprintln!(
2387 "[dsv4-gpu] слой {li}: расхождение {:.3e} | |CPU|={:.5} |GPU|={:.5} | экспертов {}",
2388 (num / den).sqrt(),
2389 den.sqrt(),
2390 out.iter().map(|x| x * x).sum::<f32>().sqrt(),
2391 jobs.len()
2392 );
2393 }
2394 return;
2395 }
2396 }
2397 }
2398 }
2399 out.fill(0.0);
2400 let mut acc = vec![0.0f32; cfg.dim];
2401 for (e, &ei) in idx.iter().enumerate() {
2402 let Some(exp) = l.experts.get(ei) else {
2403 continue;
2404 };
2405 run_expert(
2406 hidden,
2407 exp,
2408 cfg,
2409 w.get(e).copied().unwrap_or(0.0),
2410 pool,
2411 &mut acc,
2412 );
2413 for (o, a) in out.iter_mut().zip(&acc) {
2414 *o += a;
2415 }
2416 }
2417 run_expert(hidden, &l.shared, cfg, 1.0, pool, &mut acc);
2419 for (o, a) in out.iter_mut().zip(&acc) {
2420 *o += a;
2421 }
2422}
2423
2424fn run_expert(
2427 x: &[f32],
2428 e: &Dsv4Expert,
2429 cfg: &Dsv4Cfg,
2430 weight: f32,
2431 pool: Option<&crate::pool::Pool>,
2432 out: &mut [f32],
2433) {
2434 expert_swiglu(
2435 x,
2436 &|src, dst| e.w1.matvec(src, dst, pool),
2437 &|src, dst| e.w3.matvec(src, dst, pool),
2438 &|src, dst| e.w2.matvec(src, dst, pool),
2439 cfg.moe_inter,
2440 weight,
2441 cfg.swiglu_limit,
2442 out,
2443 );
2444}
2445
2446fn no_compressed() -> bool {
2452 static OFF: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2453 *OFF.get_or_init(|| std::env::var("CMF_DSV4_NO_COMPRESSED").is_ok_and(|v| v != "0"))
2454}
2455
2456fn trace_on() -> bool {
2457 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2458 *ON.get_or_init(|| std::env::var("CMF_DSV4_TRACE").is_ok_and(|v| v != "0"))
2459}
2460
2461fn rms_of(v: &[f32]) -> f32 {
2462 if v.is_empty() {
2463 return 0.0;
2464 }
2465 (v.iter().map(|x| x * x).sum::<f32>() / v.len() as f32).sqrt()
2466}
2467
2468thread_local! {
2474 static BODY: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
2476 static PICKED: std::cell::RefCell<Vec<Vec<usize>>> =
2480 const { std::cell::RefCell::new(Vec::new()) };
2481}
2482
2483fn dump_path() -> Option<&'static str> {
2484 static P: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
2485 P.get_or_init(|| std::env::var("CMF_DSV4_DUMP").ok())
2486 .as_deref()
2487}
2488
2489fn dump_line(json: &str) {
2490 if let Some(p) = dump_path() {
2491 use std::io::Write as _;
2492 if let Ok(mut f) = std::fs::OpenOptions::new()
2493 .create(true)
2494 .append(true)
2495 .open(p)
2496 {
2497 let _ = writeln!(f, "{json}");
2498 }
2499 }
2500}
2501
2502fn vec_json(v: &[f32]) -> String {
2503 let mut s = String::with_capacity(v.len() * 9);
2504 s.push('[');
2505 for (i, x) in v.iter().enumerate() {
2506 if i > 0 {
2507 s.push(',');
2508 }
2509 s.push_str(&format!("{x:.6e}"));
2510 }
2511 s.push(']');
2512 s
2513}
2514
2515#[allow(clippy::too_many_arguments)]
2523pub fn forward_token(
2524 g: &Dsv4Globals,
2525 layers: &[Dsv4Layer],
2526 cfg: &Dsv4Cfg,
2527 st: &mut Dsv4State,
2528 token_id: u32,
2529 inv_freq: &[f32],
2530 pool: Option<&crate::pool::Pool>,
2531 logits: &mut Vec<f32>,
2532) {
2533 let _t_all = prof::on().then(std::time::Instant::now);
2534 let _all_guard = Charge(_t_all, &prof::ALL_NS);
2535 let (hc, dim) = (cfg.hc_mult, cfg.dim);
2536
2537 let mut emb = vec![0.0f32; dim];
2539 g.embed.row_f32(token_id as usize, &mut emb);
2540 let mut state = vec![0.0f32; hc * dim];
2541 for j in 0..hc {
2542 state[j * dim..(j + 1) * dim].copy_from_slice(&emb);
2543 }
2544
2545 let mut scratch = HcScratch::new(cfg);
2546 let mut dump: Vec<String> = Vec::new();
2547 if dump_path().is_some() {
2548 dump.push(format!("\"embed\":{}", vec_json(&emb)));
2549 PICKED.with(|p| p.borrow_mut().clear());
2550 BODY.with(|b| b.borrow_mut().clear());
2551 dump.push(",\"layers\":[".into());
2552 }
2553 if trace_on() {
2554 eprintln!(
2555 "[dsv4] tok={token_id} pos={} embed rms={:.5}",
2556 st.pos,
2557 rms_of(&emb)
2558 );
2559 }
2560 #[cfg(feature = "gpu")]
2562 let layer_frames = gpu_layer_enabled()
2563 && dsv4_layer_loop(
2564 &mut state, layers, g, cfg, st, token_id, inv_freq, pool, &mut scratch,
2565 );
2566 #[cfg(not(feature = "gpu"))]
2567 let layer_frames = false;
2568
2569 for (li, l) in layers.iter().enumerate() {
2570 if layer_frames {
2571 break;
2572 }
2573 hc_block(
2575 &mut state,
2576 &l.hc_attn_fn,
2577 &l.hc_attn_scale,
2578 &l.hc_attn_base,
2579 &l.attn_norm,
2580 cfg,
2581 &mut scratch,
2582 pool,
2583 |folded, out| {
2584 if dump_path().is_some() {
2585 BODY.with(|b| b.borrow_mut().push(vec_json(folded)));
2588 }
2589 let freqs = if l.compressor.is_some() {
2591 &g.inv_freq_compress
2592 } else {
2593 &g.inv_freq_window
2594 };
2595 let freqs = if freqs.is_empty() {
2596 inv_freq
2597 } else {
2598 freqs.as_slice()
2599 };
2600 attention_step(folded, l, cfg, st, li, freqs, pool, None, out);
2601 if dump_path().is_some() {
2602 BODY.with(|b| b.borrow_mut().push(vec_json(out)));
2603 }
2604 },
2605 );
2606 if dump_path().is_some() {
2607 dump.push(format!(
2610 "{}{}",
2611 if li == 0 { "" } else { "," },
2612 vec_json(&state)
2613 ));
2614 }
2615 let _t_hc2 = prof::on().then(std::time::Instant::now);
2617 hc_block(
2618 &mut state,
2619 &l.hc_ffn_fn,
2620 &l.hc_ffn_scale,
2621 &l.hc_ffn_base,
2622 &l.ffn_norm,
2623 cfg,
2624 &mut scratch,
2625 pool,
2626 |folded, out| moe_step(folded, l, cfg, token_id, li, pool, out),
2627 );
2628 if let Some(t) = _t_hc2 {
2629 prof::HC_NS.fetch_add(
2632 t.elapsed().as_nanos() as u64,
2633 std::sync::atomic::Ordering::Relaxed,
2634 );
2635 }
2636 if dump_path().is_some() {
2637 dump.push(format!(",{}", vec_json(&state)));
2638 }
2639 if trace_on() && (st.pos % 64 == 0 || st.pos == 199) {
2640 eprintln!(
2641 "[dsv4] кеши слоя {li}: окно={} сжатых={} индекс={} (ratio={:?})",
2642 st.window[li].len() / cfg.head_dim.max(1),
2643 st.compressed[li].len() / cfg.head_dim.max(1),
2644 st.index_kv[li].len().max(1) / 128,
2645 l.compressor.as_ref().map(|c| (c.ratio, c.overlap)),
2646 );
2647 }
2648 if trace_on() {
2649 let bad = state.iter().filter(|v| !v.is_finite()).count();
2650 eprintln!(
2651 "[dsv4] layer {li:>2}: rms={:.5}{}",
2652 rms_of(&state),
2653 if bad > 0 {
2654 format!(" NON-FINITE x{bad}")
2655 } else {
2656 String::new()
2657 }
2658 );
2659 }
2660 }
2661 st.pos += 1;
2662
2663 let mut h = vec![0.0f32; dim];
2665 hc_head_fold(
2666 &state,
2667 &g.hc_head_fn,
2668 g.hc_head_scale,
2669 &g.hc_head_base,
2670 cfg,
2671 pool,
2672 &mut h,
2673 );
2674 let _t_head = prof::on().then(std::time::Instant::now);
2675 rms_weighted(&mut h, &g.norm, cfg.norm_eps);
2676 logits.clear();
2677 logits.resize(g.head.rows(), 0.0);
2678 g.head.matvec(&h, logits, pool);
2679 if let Some(t) = _t_head {
2680 prof::HEAD_NS.fetch_add(
2681 t.elapsed().as_nanos() as u64,
2682 std::sync::atomic::Ordering::Relaxed,
2683 );
2684 }
2685 if dump_path().is_some() {
2686 dump.push("]".into());
2687 let picked = PICKED.with(|p| {
2688 p.borrow()
2689 .iter()
2690 .map(|v| {
2691 format!(
2692 "[{}]",
2693 v.iter()
2694 .map(|e| e.to_string())
2695 .collect::<Vec<_>>()
2696 .join(",")
2697 )
2698 })
2699 .collect::<Vec<_>>()
2700 .join(",")
2701 });
2702 dump.push(format!(",\"experts\":[{picked}]"));
2703 let body = BODY.with(|b| b.borrow().join(","));
2704 dump.push(format!(",\"attn_io\":[{body}]"));
2705 dump_line(&format!(
2706 "{{\"tok\":{token_id},\"pos\":{},{},\"head\":{},\"logits\":{}}}",
2707 st.pos - 1,
2708 dump.join(""),
2709 vec_json(&h),
2710 vec_json(logits)
2711 ));
2712 }
2713 if trace_on() {
2714 let (mut top, mut best) = (0usize, f32::NEG_INFINITY);
2715 for (i, &v) in logits.iter().enumerate() {
2716 if v > best {
2717 best = v;
2718 top = i;
2719 }
2720 }
2721 let lo = logits.iter().cloned().fold(f32::MAX, f32::min);
2722 eprintln!(
2723 "[dsv4] head: rms={:.5} logits[{}..{:.3}] argmax={top}",
2724 rms_of(&h),
2725 format_args!("{lo:.3}"),
2726 best
2727 );
2728 }
2729}
2730
2731pub fn load(
2738 model: &std::sync::Arc<cortiq_core::CmfModel>,
2739 cfg: &Dsv4Cfg,
2740 n_layers: usize,
2741) -> Result<(Dsv4Globals, Vec<Dsv4Layer>), String> {
2742 let q = |name: &str| -> Result<crate::qtensor::QTensor, String> {
2743 crate::qtensor::QTensor::from_model(model, name)
2744 };
2745 let f = |name: &str| -> Result<Vec<f32>, String> {
2749 crate::loader::load_f32(model, name, &crate::loader::Overlay::None)
2750 };
2751 let opt_f = |name: &str| -> Option<Vec<f32>> { f(name).ok() };
2752
2753 let rope_of = |base: f32, yarn: bool| -> Vec<f32> {
2758 if yarn {
2759 crate::attention::yarn_inv_freq(cfg.rope_head_dim, base, 16.0, 65536, 32.0, 1.0)
2760 } else {
2761 crate::attention::rope_inv_freq(cfg.rope_head_dim, base)
2762 }
2763 };
2764 let globals = Dsv4Globals {
2765 inv_freq_compress: rope_of(160_000.0, true),
2766 inv_freq_window: rope_of(10_000.0, false),
2767 embed: q("model.embed_tokens.weight")?,
2768 norm: f("model.norm.weight")?,
2769 head: q("lm_head.weight")?,
2770 hc_head_fn: f("model.hc_head_fn")?,
2771 hc_head_base: f("model.hc_head_base")?,
2772 hc_head_scale: *f("model.hc_head_scale")?
2773 .first()
2774 .ok_or("dsv4: empty hc_head_scale")?,
2775 };
2776
2777 let mut layers = Vec::with_capacity(n_layers);
2778 for li in 0..n_layers {
2779 let p = format!("model.layers.{li}");
2780 let scale3 = |name: &str| -> Result<[f32; 3], String> {
2781 let v = f(name)?;
2782 if v.len() < 3 {
2783 return Err(format!("{name}: expected 3 scales, got {}", v.len()));
2784 }
2785 Ok([v[0], v[1], v[2]])
2786 };
2787 let compressor = match q(&format!("{p}.self_attn.compressor.wkv.weight")) {
2790 Ok(wkv) => {
2791 let ape = f(&format!("{p}.self_attn.compressor.ape"))?;
2792 let width = wkv.rows();
2795 let ratio = (ape.len() / width.max(1)).max(1);
2796 Some(Dsv4Compressor {
2797 wkv,
2798 wgate: q(&format!("{p}.self_attn.compressor.wgate.weight"))?,
2799 norm: f(&format!("{p}.self_attn.compressor.norm.weight"))?,
2800 ape,
2801 ratio,
2802 overlap: ratio == 4,
2803 })
2804 }
2805 Err(_) => None,
2806 };
2807 let indexer = match q(&format!("{p}.self_attn.indexer.wq_b.weight")) {
2808 Ok(wq_b) => {
2809 let ape = f(&format!("{p}.self_attn.indexer.compressor.ape"))?;
2810 let cwkv = q(&format!("{p}.self_attn.indexer.compressor.wkv.weight"))?;
2811 let width = cwkv.rows();
2812 let ratio = (ape.len() / width.max(1)).max(1);
2813 Some(Dsv4Indexer {
2814 wq_b,
2815 weights_proj: q(&format!("{p}.self_attn.indexer.weights_proj.weight"))?,
2816 compressor: Dsv4Compressor {
2817 wkv: cwkv,
2818 wgate: q(&format!("{p}.self_attn.indexer.compressor.wgate.weight"))?,
2819 norm: f(&format!("{p}.self_attn.indexer.compressor.norm.weight"))?,
2820 ape,
2821 ratio,
2822 overlap: ratio == 4,
2823 },
2824 })
2825 }
2826 Err(_) => None,
2827 };
2828
2829 let mut experts = Vec::with_capacity(cfg.n_routed_experts);
2830 for e in 0..cfg.n_routed_experts {
2831 let ep = format!("{p}.mlp.experts.{e}");
2832 experts.push(Dsv4Expert {
2833 w1: q(&format!("{ep}.gate_proj.weight"))?,
2834 w2: q(&format!("{ep}.down_proj.weight"))?,
2835 w3: q(&format!("{ep}.up_proj.weight"))?,
2836 });
2837 }
2838
2839 layers.push(Dsv4Layer {
2840 attn_norm: f(&format!("{p}.input_layernorm.weight"))?,
2841 ffn_norm: f(&format!("{p}.post_attention_layernorm.weight"))?,
2842 wq_a: q(&format!("{p}.self_attn.wq_a.weight"))?,
2843 q_norm: f(&format!("{p}.self_attn.q_norm.weight"))?,
2844 wq_b: q(&format!("{p}.self_attn.wq_b.weight"))?,
2845 wkv: q(&format!("{p}.self_attn.wkv.weight"))?,
2846 kv_norm: f(&format!("{p}.self_attn.kv_norm.weight"))?,
2847 wo_a: q(&format!("{p}.self_attn.wo_a.weight"))?,
2848 wo_b: q(&format!("{p}.self_attn.wo_b.weight"))?,
2849 attn_sink: f(&format!("{p}.self_attn.attn_sink"))?,
2850 compressor,
2851 indexer,
2852 hc_attn_fn: f(&format!("{p}.hc_attn_fn"))?,
2853 hc_attn_base: f(&format!("{p}.hc_attn_base"))?,
2854 hc_attn_scale: scale3(&format!("{p}.hc_attn_scale"))?,
2855 hc_ffn_fn: f(&format!("{p}.hc_ffn_fn"))?,
2856 hc_ffn_base: f(&format!("{p}.hc_ffn_base"))?,
2857 hc_ffn_scale: scale3(&format!("{p}.hc_ffn_scale"))?,
2858 gate: q(&format!("{p}.mlp.gate.weight"))?,
2859 gate_bias: opt_f(&format!("{p}.mlp.expert_bias")),
2862 tid2eid: opt_f(&format!("{p}.mlp.tid2eid")),
2863 experts,
2864 mask: if model.tensor(&format!("{p}.mlp.tid2eid")).is_some() {
2865 None
2866 } else {
2867 crate::loader::moe_task_mask(&format!("{p}."), cfg.n_routed_experts)
2868 },
2869 shared: Dsv4Expert {
2870 w1: q(&format!("{p}.mlp.shared_expert.gate_proj.weight"))?,
2871 w2: q(&format!("{p}.mlp.shared_expert.down_proj.weight"))?,
2872 w3: q(&format!("{p}.mlp.shared_expert.up_proj.weight"))?,
2873 },
2874 });
2875 }
2876 Ok((globals, layers))
2877}
2878
2879#[cfg(test)]
2880mod tests {
2881 use super::*;
2882
2883 fn toy() -> (Dsv4Globals, Vec<Dsv4Layer>, Dsv4Cfg) {
2888 use crate::qtensor::QTensor;
2889 let cfg = Dsv4Cfg {
2890 dim: 32,
2891 n_heads: 4,
2892 head_dim: 8,
2893 rope_head_dim: 4,
2894 q_lora_rank: 16,
2895 o_lora_rank: 16,
2896 o_groups: 2,
2897 hc_mult: 4,
2898 hc_sinkhorn_iters: 20,
2899 hc_eps: 1e-6,
2900 norm_eps: 1e-6,
2901 n_routed_experts: 8,
2902 top_k: 2,
2903 moe_inter: 16,
2904 route_scale: 1.0,
2905 swiglu_limit: 10.0,
2906 window: 6,
2907 index_topk: 8,
2908 vocab: 24,
2909 };
2910 let w = |n: usize, seed: usize| -> Vec<f32> {
2913 (0..n)
2914 .map(|i| (((i * 7 + seed * 13) % 101) as f32 / 101.0 - 0.5) * 0.3)
2915 .collect()
2916 };
2917 let t = |rows: usize, cols: usize, seed: usize| {
2918 QTensor::from_f32(w(rows * cols, seed), rows, cols)
2919 };
2920 let ones = |n: usize| vec![1.0f32; n];
2921
2922 let (dim, hc) = (cfg.dim, cfg.hc_mult);
2923 let q_width = cfg.n_heads * cfg.head_dim;
2926 let kv_width = cfg.head_dim;
2927 let o_per_group = q_width / cfg.o_groups;
2928 let mut layers = Vec::new();
2929 for li in 0..2 {
2930 let experts: Vec<Dsv4Expert> = (0..cfg.n_routed_experts)
2931 .map(|e| Dsv4Expert {
2932 w1: t(cfg.moe_inter, dim, 40 + e + li * 8),
2933 w2: t(dim, cfg.moe_inter, 60 + e + li * 8),
2934 w3: t(cfg.moe_inter, dim, 80 + e + li * 8),
2935 })
2936 .collect();
2937 layers.push(Dsv4Layer {
2940 attn_norm: ones(dim),
2941 ffn_norm: ones(dim),
2942 wq_a: t(cfg.q_lora_rank, dim, 1 + li),
2943 q_norm: ones(cfg.q_lora_rank),
2944 wq_b: t(q_width, cfg.q_lora_rank, 3 + li),
2945 wkv: t(kv_width, dim, 5 + li),
2946 kv_norm: ones(kv_width),
2947 wo_a: t(cfg.o_groups * cfg.o_lora_rank, o_per_group, 7 + li),
2948 wo_b: t(dim, cfg.o_groups * cfg.o_lora_rank, 9 + li),
2949 attn_sink: vec![0.1; cfg.n_heads],
2950 compressor: if li == 1 {
2953 Some(Dsv4Compressor {
2954 wkv: t(2 * kv_width, dim, 11),
2955 wgate: t(2 * kv_width, dim, 13),
2956 norm: ones(kv_width),
2957 ape: vec![0.01; 4 * 2 * kv_width],
2958 ratio: 4,
2959 overlap: true,
2960 })
2961 } else {
2962 None
2963 },
2964 indexer: if li == 1 {
2965 Some(Dsv4Indexer {
2966 wq_b: t(2 * 16, cfg.q_lora_rank, 41),
2967 weights_proj: t(2, dim, 43),
2968 compressor: Dsv4Compressor {
2969 wkv: t(2 * 16, dim, 45),
2970 wgate: t(2 * 16, dim, 47),
2971 norm: ones(16),
2972 ape: vec![0.01; 4 * 2 * 16],
2973 ratio: 4,
2974 overlap: true,
2975 },
2976 })
2977 } else {
2978 None
2979 },
2980 hc_attn_fn: w((2 + hc) * hc * hc * dim, 15 + li),
2981 hc_attn_base: w((2 + hc) * hc, 17 + li),
2982 hc_attn_scale: [1.0, 1.0, 1.0],
2983 hc_ffn_fn: w((2 + hc) * hc * hc * dim, 19 + li),
2984 hc_ffn_base: w((2 + hc) * hc, 21 + li),
2985 hc_ffn_scale: [1.0, 1.0, 1.0],
2986 gate: t(cfg.n_routed_experts, dim, 23 + li),
2987 gate_bias: if li == 1 {
2988 Some(vec![0.0; cfg.n_routed_experts])
2989 } else {
2990 None
2991 },
2992 tid2eid: if li == 0 {
2993 Some(
2994 (0..cfg.vocab * cfg.top_k)
2995 .map(|i| (i % cfg.n_routed_experts) as f32)
2996 .collect(),
2997 )
2998 } else {
2999 None
3000 },
3001 experts,
3002 mask: None,
3003 shared: Dsv4Expert {
3004 w1: t(cfg.moe_inter, dim, 25 + li),
3005 w2: t(dim, cfg.moe_inter, 27 + li),
3006 w3: t(cfg.moe_inter, dim, 29 + li),
3007 },
3008 });
3009 }
3010 let inv = |base: f32| -> Vec<f32> {
3011 (0..cfg.rope_head_dim / 2)
3012 .map(|i| 1.0 / base.powf(2.0 * i as f32 / cfg.rope_head_dim as f32))
3013 .collect()
3014 };
3015 let g = Dsv4Globals {
3016 inv_freq_compress: inv(160000.0),
3017 inv_freq_window: inv(10000.0),
3018 embed: t(cfg.vocab, dim, 31),
3019 norm: ones(dim),
3020 head: t(cfg.vocab, dim, 33),
3021 hc_head_fn: w(hc * hc * dim, 35),
3022 hc_head_base: w(hc, 37),
3023 hc_head_scale: 1.0,
3024 };
3025 (g, layers, cfg)
3026 }
3027
3028 #[test]
3033 fn forward_token_decodes_a_sequence_without_falling_over() {
3034 let (g, layers, cfg) = toy();
3035 let mut st = Dsv4State::new(layers.len());
3036 let inv_freq: Vec<f32> = (0..cfg.rope_head_dim / 2)
3037 .map(|i| 1.0 / 10000f32.powf(2.0 * i as f32 / cfg.rope_head_dim as f32))
3038 .collect();
3039 let mut logits = Vec::new();
3040
3041 let mut first: Option<Vec<f32>> = None;
3044 for (step, tok) in [3u32, 7, 1, 9, 4, 2, 8, 5, 6, 0].into_iter().enumerate() {
3045 forward_token(
3046 &g,
3047 &layers,
3048 &cfg,
3049 &mut st,
3050 tok,
3051 &inv_freq,
3052 None,
3053 &mut logits,
3054 );
3055 assert_eq!(logits.len(), cfg.vocab, "step {step}: logit count");
3056 assert!(
3057 logits.iter().all(|v| v.is_finite()),
3058 "step {step}: non-finite logit — {logits:?}"
3059 );
3060 let spread = logits.iter().cloned().fold(f32::MIN, f32::max)
3063 - logits.iter().cloned().fold(f32::MAX, f32::min);
3064 assert!(spread > 1e-6, "step {step}: logits are flat ({spread})");
3065 if step == 0 {
3066 first = Some(logits.clone());
3067 }
3068 assert_eq!(st.pos, step + 1, "position bookkeeping");
3069 }
3070
3071 assert!(!st.window[0].is_empty(), "sliding window never filled");
3074 for (li, w) in st.window.iter().enumerate() {
3076 assert!(
3077 w.len() / cfg.head_dim <= cfg.window,
3078 "layer {li}: window holds {} positions, cap is {}",
3079 w.len() / cfg.head_dim,
3080 cfg.window
3081 );
3082 }
3083 assert!(
3084 !st.compressed[1].is_empty(),
3085 "compressor layer produced no compressed KV in 10 tokens"
3086 );
3087 assert_eq!(
3091 st.compressed[1].len() / cfg.head_dim,
3092 2,
3093 "expected two folds in ten tokens at ratio 4"
3094 );
3095 assert!(
3096 !st.prev_kv[1].is_empty(),
3097 "the overlapping compressor never kept a previous window"
3098 );
3099 for (li, l) in layers.iter().enumerate() {
3104 if l.indexer.is_some() {
3105 assert!(
3106 !st.index_kv[li].is_empty(),
3107 "layer {li} has an indexer but its cache stayed empty"
3108 );
3109 }
3110 }
3111
3112 let mut fresh = Dsv4State::new(layers.len());
3115 let mut relogits = Vec::new();
3116 forward_token(
3117 &g,
3118 &layers,
3119 &cfg,
3120 &mut fresh,
3121 3,
3122 &inv_freq,
3123 None,
3124 &mut relogits,
3125 );
3126 assert_eq!(
3127 relogits,
3128 first.unwrap(),
3129 "the same token from a fresh state must reproduce exactly"
3130 );
3131 }
3132
3133 #[test]
3137 fn swiglu_limit_clamps_up_both_ways_and_gate_only_from_above() {
3138 let inter = 4;
3139 let gate_src = [-50.0f32, 50.0, 1.0, -1.0];
3141 let up_src = [50.0f32, -50.0, 1.0, -1.0];
3142 let limit = 10.0f32;
3143 let mut got = vec![0.0f32; inter];
3144 expert_swiglu(
3145 &[0.0],
3146 &|_, d| d.copy_from_slice(&gate_src),
3147 &|_, d| d.copy_from_slice(&up_src),
3148 &|src, d| d.copy_from_slice(src),
3149 inter,
3150 1.0,
3151 limit,
3152 &mut got,
3153 );
3154 let silu = |g: f32| g / (1.0 + (-g).exp());
3155 let want = [
3157 silu(-50.0) * limit,
3158 silu(limit) * -limit,
3159 silu(1.0) * 1.0,
3160 silu(-1.0) * -1.0,
3161 ];
3162 for (i, w) in want.iter().enumerate() {
3163 assert!(
3164 (got[i] - w).abs() < 1e-5,
3165 "lane {i}: got {} want {w}",
3166 got[i]
3167 );
3168 }
3169 let mut raw = vec![0.0f32; inter];
3171 expert_swiglu(
3172 &[0.0],
3173 &|_, d| d.copy_from_slice(&gate_src),
3174 &|_, d| d.copy_from_slice(&up_src),
3175 &|src, d| d.copy_from_slice(src),
3176 inter,
3177 1.0,
3178 0.0,
3179 &mut raw,
3180 );
3181 assert!(
3182 (raw[1] - silu(50.0) * -50.0).abs() < 1e-3,
3183 "limit 0 must not clamp"
3184 );
3185 }
3186
3187 #[test]
3192 fn grouped_projection_is_identical_with_and_without_a_pool() {
3193 let (groups, lora, per_group, dim) = (4usize, 128usize, 64usize, 32usize);
3194 let attn: Vec<f32> = (0..groups * per_group)
3195 .map(|i| ((i * 13) as f32 * 0.021).sin())
3196 .collect();
3197 let wo_a: Vec<f32> = (0..groups * lora * per_group)
3198 .map(|i| ((i * 7) as f32 * 0.011).cos())
3199 .collect();
3200 let wo_b: Vec<f32> = (0..dim * groups * lora)
3201 .map(|i| ((i * 5) as f32 * 0.009).sin())
3202 .collect();
3203 let row = |r: usize, x: &[f32], _sc: &mut [f32]| -> f32 {
3204 wo_a[r * per_group..(r + 1) * per_group]
3205 .iter()
3206 .zip(x)
3207 .map(|(a, b)| a * b)
3208 .sum()
3209 };
3210 let project = |mid: &[f32], dst: &mut [f32]| {
3211 for (d, o) in dst.iter_mut().enumerate() {
3212 *o = wo_b[d * mid.len()..(d + 1) * mid.len()]
3213 .iter()
3214 .zip(mid)
3215 .map(|(a, b)| a * b)
3216 .sum();
3217 }
3218 };
3219
3220 let mut serial = vec![0.0f32; dim];
3221 o_project(
3222 &attn,
3223 &row,
3224 per_group,
3225 &project,
3226 groups,
3227 lora,
3228 None,
3229 &mut serial,
3230 );
3231
3232 let pool = crate::pool::Pool::new(4);
3233 let mut pooled = vec![0.0f32; dim];
3234 o_project(
3235 &attn,
3236 &row,
3237 per_group,
3238 &project,
3239 groups,
3240 lora,
3241 Some(&pool),
3242 &mut pooled,
3243 );
3244 assert_eq!(serial, pooled, "the pooled projection diverged");
3245 assert!(
3246 serial.iter().any(|v| v.abs() > 1e-6),
3247 "test data is degenerate"
3248 );
3249 }
3250
3251 #[test]
3257 fn overlapping_compressor_folds_both_windows() {
3258 let (ratio, d) = (2usize, 3usize);
3259 let cur_kv: Vec<f32> = vec![
3262 1.0, 1.0, 1.0, 10.0, 20.0, 30.0, 2.0, 2.0, 2.0, 40.0, 50.0, 60.0, ];
3265 let cur_sc: Vec<f32> = vec![
3267 0.0, 0.0, 0.0, 0.0, 0.0, 100.0, 0.0, 0.0, 0.0, 100.0, 100.0, 0.0,
3269 ];
3270 let prev_kv: Vec<f32> = vec![
3272 7.0, 8.0, 9.0, 0.0, 0.0, 0.0, 5.0, 6.0, 7.0, 0.0, 0.0, 0.0,
3274 ];
3275 let prev_sc = vec![0.0f32; ratio * 2 * d];
3276
3277 let mut out = vec![0.0f32; d];
3278 compress_window_overlap(&prev_kv, &prev_sc, &cur_kv, &cur_sc, ratio, d, &mut out);
3279 assert!((out[0] - 40.0).abs() < 1e-3, "dim0 = {}", out[0]);
3281 assert!((out[1] - 50.0).abs() < 1e-3, "dim1 = {}", out[1]);
3282 assert!((out[2] - 30.0).abs() < 1e-3, "dim2 = {}", out[2]);
3284
3285 let mut first = vec![0.0f32; d];
3288 compress_window_overlap(&[], &[], &cur_kv, &cur_sc, ratio, d, &mut first);
3289 assert!(
3290 first.iter().all(|v| v.is_finite()),
3291 "first window: {first:?}"
3292 );
3293 assert!((first[0] - 40.0).abs() < 1e-3, "first dim0 = {}", first[0]);
3294
3295 let mut both = vec![0.0f32; d];
3297 let strong_prev = vec![100.0f32; ratio * 2 * d];
3298 compress_window_overlap(
3299 &prev_kv,
3300 &strong_prev,
3301 &cur_kv,
3302 &cur_sc,
3303 ratio,
3304 d,
3305 &mut both,
3306 );
3307 assert!(
3308 (both[0] - 40.0).abs() > 1.0,
3309 "a scored previous window must move the fold, got {}",
3310 both[0]
3311 );
3312 }
3313
3314 #[test]
3320 fn sinkhorn_matches_the_reference_numbers() {
3321 let hc = 4;
3322 let mixes: Vec<f32> = (0..24).map(|i| (i as f32 * 0.37).sin() * 3.0).collect();
3323 let base: Vec<f32> = (0..24).map(|i| (i as f32 * 0.11).cos()).collect();
3324 let (mut pre, mut post, mut comb) = (vec![0.0; hc], vec![0.0; hc], vec![0.0; hc * hc]);
3325 hc_split_sinkhorn(
3326 &mixes,
3327 &[1.0, 1.0, 1.0],
3328 &base,
3329 hc,
3330 20,
3331 1e-6,
3332 &mut pre,
3333 &mut post,
3334 &mut comb,
3335 );
3336 let want_pre = [0.7310596, 0.8888268, 0.9525191, 0.97424865];
3337 let want_post = [1.9600224, 1.9534285, 1.9201256, 1.8160983];
3338 let want_comb = [
3339 0.5996052,
3340 0.28253591,
3341 0.09218107,
3342 0.025676856,
3343 0.17564717,
3344 0.22228767,
3345 0.27174541,
3346 0.33031881,
3347 0.029528176,
3348 0.12206022,
3349 0.32619134,
3350 0.5222193,
3351 0.19521846,
3352 0.37311527,
3353 0.30988118,
3354 0.12178412,
3355 ];
3356 for (i, w) in want_pre.iter().enumerate() {
3357 assert!((pre[i] - w).abs() < 1e-5, "pre[{i}]: {} vs {w}", pre[i]);
3358 }
3359 for (i, w) in want_post.iter().enumerate() {
3360 assert!((post[i] - w).abs() < 1e-5, "post[{i}]: {} vs {w}", post[i]);
3361 }
3362 for (i, w) in want_comb.iter().enumerate() {
3363 assert!((comb[i] - w).abs() < 1e-4, "comb[{i}]: {} vs {w}", comb[i]);
3364 }
3365 }
3366
3367 #[test]
3372 fn sinkhorn_leaves_the_mixing_matrix_doubly_stochastic() {
3373 let hc = 4;
3374 let mix_hc = (2 + hc) * hc;
3375 let mixes: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.37).sin() * 3.0).collect();
3377 let base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.11).cos()).collect();
3378 let (mut pre, mut post, mut comb) = (vec![0.0; hc], vec![0.0; hc], vec![0.0; hc * hc]);
3379 hc_split_sinkhorn(
3380 &mixes,
3381 &[1.0, 1.0, 1.0],
3382 &base,
3383 hc,
3384 20,
3385 1e-6,
3386 &mut pre,
3387 &mut post,
3388 &mut comb,
3389 );
3390 for j in 0..hc {
3391 let r: f32 = comb[j * hc..(j + 1) * hc].iter().sum();
3392 assert!((r - 1.0).abs() < 2e-3, "row {j} sums to {r}");
3393 let c: f32 = (0..hc).map(|k| comb[k * hc + j]).sum();
3394 assert!((c - 1.0).abs() < 2e-3, "col {j} sums to {c}");
3395 }
3396 assert!(pre.iter().all(|&v| v > 0.0 && v < 1.001));
3398 assert!(post.iter().all(|&v| v >= 0.0 && v <= 2.0));
3399 }
3400
3401 #[test]
3405 fn expand_of_identical_copies_is_a_fixed_point() {
3406 let (hc, dim) = (4usize, 3usize);
3407 let residual: Vec<f32> = std::iter::repeat([1.5f32, -2.0, 0.25])
3408 .take(hc)
3409 .flatten()
3410 .collect();
3411 let comb = {
3412 vec![0.25f32; hc * hc]
3414 };
3415 let post = vec![0.0f32; hc];
3416 let mut out = vec![0.0f32; hc * dim];
3417 hc_expand(&[0.0; 3], &residual, &post, &comb, hc, dim, &mut out);
3418 for (o, r) in out.iter().zip(&residual) {
3419 assert!((o - r).abs() < 1e-6, "{o} vs {r}");
3420 }
3421 }
3422
3423 #[test]
3427 fn selection_bias_steers_the_choice_but_not_the_weights() {
3428 let scores = [3.0f32, 0.1, 2.0, 0.05];
3429 let bias = [0.0f32, 10.0, 0.0, 0.0];
3430 let (mut idx, mut w) = (Vec::new(), Vec::new());
3431 route(&scores, Some(&bias), 2, 1.5, None, None, &mut idx, &mut w);
3432 assert_eq!(idx[0], 1, "the biased expert must win selection");
3433 assert_eq!(idx[1], 0);
3434 assert!(w[0] < w[1], "biased expert kept its own (small) weight");
3437 let sum: f32 = w.iter().sum();
3438 assert!((sum - 1.5).abs() < 1e-5, "weights renormalize then scale");
3439 }
3440
3441 #[test]
3445 fn attention_sink_drains_weight_without_contributing_output() {
3446 let hd = 2;
3447 let q = [1.0f32, 0.0];
3448 let kv = [1.0f32, 0.0, 0.0, 1.0];
3449 let mut out = vec![0.0f32; hd];
3450 sparse_attend(&q, &kv, &[0, 1], f32::NEG_INFINITY, 1.0, hd, &mut out);
3451 let plain = out.clone();
3452 assert!(plain[0] > plain[1], "the aligned key must dominate");
3453 sparse_attend(&q, &kv, &[0, 1], 20.0, 1.0, hd, &mut out);
3454 assert!(
3455 out[0] < plain[0] * 0.01 && out[1] < plain[1] * 0.01,
3456 "a large sink must drain nearly all the mass: {out:?}"
3457 );
3458 }
3459
3460 #[test]
3463 fn masked_positions_leave_the_denominator_alone() {
3464 let hd = 2;
3465 let q = [1.0f32, 0.0];
3466 let kv = [1.0f32, 0.0, 0.0, 1.0];
3467 let (mut a, mut b) = (vec![0.0f32; hd], vec![0.0f32; hd]);
3468 sparse_attend(&q, &kv, &[0], f32::NEG_INFINITY, 1.0, hd, &mut a);
3469 sparse_attend(
3470 &q,
3471 &kv,
3472 &[0, usize::MAX],
3473 f32::NEG_INFINITY,
3474 1.0,
3475 hd,
3476 &mut b,
3477 );
3478 for (x, y) in a.iter().zip(&b) {
3479 assert!((x - y).abs() < 1e-6, "{x} vs {y}");
3480 }
3481 }
3482
3483 #[test]
3486 fn rope_tail_inverts_itself() {
3487 let inv_freq = [1.0f32, 0.5];
3488 let orig = [9.0f32, 8.0, 1.0, 2.0, 3.0, 4.0];
3489 let mut v = orig;
3490 rope_tail(&mut v, &inv_freq, 7, 4, false);
3491 assert!(v[..2] == orig[..2], "the non-rope head must not move");
3492 assert!(v[2..] != orig[2..], "the tail must actually rotate");
3493 rope_tail(&mut v, &inv_freq, 7, 4, true);
3494 for (a, b) in v.iter().zip(&orig) {
3495 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
3496 }
3497 }
3498
3499 #[test]
3502 fn compressor_pools_the_window_per_dimension() {
3503 let (ratio, width) = (2usize, 2usize);
3504 let kv = [1.0f32, 10.0, 3.0, 20.0];
3505 let score = [0.0f32, 0.0, 0.0, 50.0];
3507 let ape = vec![0.0f32; ratio * width];
3508 let mut out = vec![0.0f32; width];
3509 compress_window(&kv, &score, &ape, ratio, width, &mut out);
3510 assert!(
3511 (out[0] - 2.0).abs() < 1e-5,
3512 "equal scores average: {}",
3513 out[0]
3514 );
3515 assert!(
3516 (out[1] - 20.0).abs() < 1e-3,
3517 "a dominant score wins: {}",
3518 out[1]
3519 );
3520 }
3521
3522 #[test]
3525 fn index_scores_relu_before_weighting() {
3526 let (nh, hd) = (2usize, 2usize);
3527 let q = [1.0f32, 0.0, -1.0, 0.0];
3529 let kv = [1.0f32, 0.0, 0.0, 1.0];
3530 let w = [1.0f32, 1.0];
3531 let mut sc = Vec::new();
3532 index_scores(&q, &kv, &w, nh, hd, 2, 2, None, &mut sc);
3533 assert!(sc[0] > 0.9, "abstention, not veto: {:?}", sc);
3535 }
3536
3537 #[test]
3538 fn index_scores_mask_the_future() {
3539 let (nh, hd) = (1usize, 2usize);
3540 let q = [1.0f32, 0.0];
3541 let kv = [1.0f32, 0.0, 1.0, 0.0, 1.0, 0.0];
3542 let w = [1.0f32];
3543 let mut sc = Vec::new();
3544 index_scores(&q, &kv, &w, nh, hd, 3, 2, None, &mut sc);
3545 assert!(sc[0].is_finite() && sc[1].is_finite());
3546 assert!(sc[2] == f32::NEG_INFINITY, "position 2 is in the future");
3547 let mut idx = Vec::new();
3548 top_k_positions(&sc, 3, &mut idx);
3549 assert_eq!(idx, vec![0, 1], "a masked slot never wins a slot");
3550 }
3551
3552 #[test]
3553 fn top_k_is_deterministic_on_ties() {
3554 let sc = [1.0f32, 1.0, 1.0, 0.0];
3555 let mut idx = Vec::new();
3556 top_k_positions(&sc, 2, &mut idx);
3557 assert_eq!(idx, vec![0, 1], "ties resolve to the lower index");
3558 }
3559
3560 #[test]
3564 fn hc_block_preserves_the_copy_structure_and_applies_the_block() {
3565 let cfg = Dsv4Cfg {
3566 dim: 4,
3567 n_heads: 1,
3568 head_dim: 4,
3569 rope_head_dim: 2,
3570 q_lora_rank: 4,
3571 o_lora_rank: 2,
3572 o_groups: 1,
3573 hc_mult: 4,
3574 hc_sinkhorn_iters: 20,
3575 hc_eps: 1e-6,
3576 norm_eps: 1e-6,
3577 n_routed_experts: 2,
3578 top_k: 1,
3579 moe_inter: 4,
3580 route_scale: 1.0,
3581 swiglu_limit: 10.0,
3582 window: 128,
3583 index_topk: 4,
3584 vocab: 8,
3585 };
3586 let (hc, dim) = (cfg.hc_mult, cfg.dim);
3587 let mix_hc = (2 + hc) * hc;
3588 let hc_fn: Vec<f32> = (0..mix_hc * hc * dim)
3589 .map(|i| ((i % 13) as f32 - 6.0) * 0.05)
3590 .collect();
3591 let hc_base: Vec<f32> = (0..mix_hc).map(|i| (i as f32 * 0.2).sin()).collect();
3592 let norm_w = vec![1.0f32; dim];
3593 let mut state: Vec<f32> = (0..hc * dim).map(|i| (i as f32 * 0.3).cos()).collect();
3594 let before = state.clone();
3595 let mut scratch = HcScratch::new(&cfg);
3596 hc_block(
3597 &mut state,
3598 &hc_fn,
3599 &[1.0, 1.0, 1.0],
3600 &hc_base,
3601 &norm_w,
3602 &cfg,
3603 &mut scratch,
3604 None,
3605 |_folded, out: &mut [f32]| out.iter_mut().for_each(|o| *o = 1.0),
3606 );
3607 assert_eq!(state.len(), before.len(), "copy structure must survive");
3608 assert!(state.iter().all(|v| v.is_finite()), "{state:?}");
3609 assert!(
3610 state.iter().zip(&before).any(|(a, b)| (a - b).abs() > 1e-4),
3611 "the block's output has to reach the state"
3612 );
3613 }
3614
3615 #[test]
3616 fn hash_route_reads_the_table_row() {
3617 let table = [7.0f32, 9.0, 1.0, 2.0, 5.0, 6.0];
3619 assert_eq!(hash_route(&table, 3, 2, 0), vec![7, 9]);
3620 assert_eq!(hash_route(&table, 3, 2, 2), vec![5, 6]);
3621 assert_eq!(hash_route(&table, 3, 2, 99), vec![5, 6]);
3623 }
3624
3625 #[test]
3630 fn a_task_mask_restricts_selection_and_renormalizes() {
3631 let scores = [0.1f32, 4.0, 1.0, 9.0];
3633 let (mut idx, mut w) = (Vec::new(), Vec::new());
3634 route(&scores, None, 2, 1.0, None, None, &mut idx, &mut w);
3635 assert_eq!(idx, vec![3, 1], "unmasked: the two best win");
3636 let sum: f32 = w.iter().sum();
3637 assert!((sum - 1.0).abs() < 1e-5, "weights must sum to route_scale");
3638
3639 let mask = [true, false, true, true];
3642 let (mut i2, mut w2) = (Vec::new(), Vec::new());
3643 route(&scores, None, 2, 1.0, None, Some(&mask), &mut i2, &mut w2);
3644 assert_eq!(i2, vec![3, 2], "masked expert must not be selected");
3645 let sum2: f32 = w2.iter().sum();
3646 assert!((sum2 - 1.0).abs() < 1e-5, "masked weights must renormalize");
3647
3648 let tight = [false, false, false, true];
3650 let (mut i3, mut w3) = (Vec::new(), Vec::new());
3651 route(&scores, None, 2, 1.0, None, Some(&tight), &mut i3, &mut w3);
3652 assert_eq!(i3, vec![3]);
3653 assert_eq!(w3.len(), 1);
3654 }
3655
3656 #[test]
3661 fn hash_layers_weight_the_experts_the_table_names() {
3662 let scores = [0.1f32, 0.4, 0.2, 5.0];
3664 let table = vec![0.0f32, 1.0];
3665 let idx_forced = hash_route(&table, 1, 2, 0);
3666 assert_eq!(idx_forced, vec![0, 1]);
3667
3668 let (mut idx, mut w) = (Vec::new(), Vec::new());
3669 route(
3670 &scores,
3671 None,
3672 2,
3673 1.0,
3674 Some(&idx_forced),
3675 None,
3676 &mut idx,
3677 &mut w,
3678 );
3679 assert_eq!(idx, vec![0, 1], "the table must decide the experts");
3680
3681 let sp = |x: f32| (1.0 + x.exp()).ln().sqrt();
3683 let (s0, s1) = (sp(scores[0]), sp(scores[1]));
3684 let tot = s0 + s1;
3685 assert!(
3686 (w[0] - s0 / tot).abs() < 1e-6,
3687 "w[0]={} want {}",
3688 w[0],
3689 s0 / tot
3690 );
3691 assert!(
3692 (w[1] - s1 / tot).abs() < 1e-6,
3693 "w[1]={} want {}",
3694 w[1],
3695 s1 / tot
3696 );
3697
3698 let (mut idx2, mut w2) = (Vec::new(), Vec::new());
3700 route(&scores, None, 2, 1.0, None, None, &mut idx2, &mut w2);
3701 assert_eq!(idx2[0], 3, "without a table the highest score still wins");
3702 }
3703}
3704