1use crate::cache::PagedKvStore;
13
14fn rope_freqs(theta: f32, dim: usize) -> std::rc::Rc<Vec<f32>> {
31 type FreqTables = std::collections::HashMap<(u32, usize), std::rc::Rc<Vec<f32>>>;
33 thread_local! {
34 static CACHE: std::cell::RefCell<FreqTables> =
35 std::cell::RefCell::new(FreqTables::new());
36 }
37 let key = (theta.to_bits(), dim);
40 CACHE.with(|c| {
41 if let Some(hit) = c.borrow().get(&key) {
42 return std::rc::Rc::clone(hit);
43 }
44 let table: Vec<f32> = (0..dim / 2)
45 .map(|i| 1.0 / theta.powf((2 * i) as f32 / dim as f32))
46 .collect();
47 let table = std::rc::Rc::new(table);
48 c.borrow_mut().insert(key, std::rc::Rc::clone(&table));
49 table
50 })
51}
52
53pub fn apply_rope(vec: &mut [f32], pos: usize, theta: f32) {
60 let dim = vec.len();
61 let half = dim / 2;
62 let freqs = rope_freqs(theta, dim);
63 for i in 0..half {
64 let freq = freqs[i];
65 let angle = pos as f32 * freq;
66 let (sin, cos) = angle.sin_cos();
67 let a = vec[i];
68 let b = vec[i + half];
69 vec[i] = a * cos - b * sin;
70 vec[i + half] = a * sin + b * cos;
71 }
72}
73
74pub fn apply_rope_back(vec: &mut [f32], pos: usize, theta: f32) {
79 let dim = vec.len();
80 let half = dim / 2;
81 let freqs = rope_freqs(theta, dim);
82 for i in 0..half {
83 let freq = freqs[i];
84 let angle = pos as f32 * freq;
85 let (sin, cos) = angle.sin_cos();
86 let a = vec[i];
87 let b = vec[i + half];
88 vec[i] = a * cos + b * sin;
90 vec[i + half] = -a * sin + b * cos;
91 }
92}
93
94pub fn apply_rope_interleaved_back(vec: &mut [f32], pos: usize, theta: f32) {
96 let dim = vec.len();
97 let half = dim / 2;
98 let freqs = rope_freqs(theta, dim);
99 for i in 0..half {
100 let freq = freqs[i];
101 let angle = pos as f32 * freq;
102 let (sin, cos) = angle.sin_cos();
103 let a = vec[2 * i];
104 let b = vec[2 * i + 1];
105 vec[2 * i] = a * cos + b * sin;
106 vec[2 * i + 1] = -a * sin + b * cos;
107 }
108}
109
110#[inline]
116fn dot_f32(a: &[f32], b: &[f32]) -> f32 {
117 debug_assert_eq!(a.len(), b.len());
118 #[cfg(target_arch = "aarch64")]
119 {
120 if std::arch::is_aarch64_feature_detected!("neon") {
121 return unsafe { dot_f32_neon(a, b) };
122 }
123 }
124 #[cfg(target_arch = "x86_64")]
125 {
126 if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
127 return unsafe { dot_f32_avx2(a, b) };
128 }
129 }
130 a.iter().zip(b).map(|(x, y)| x * y).sum()
131}
132
133#[cfg(target_arch = "aarch64")]
134#[target_feature(enable = "neon")]
135unsafe fn dot_f32_neon(a: &[f32], b: &[f32]) -> f32 {
136 use std::arch::aarch64::*;
137 let n = a.len();
138 let mut acc = vdupq_n_f32(0.0);
139 let mut i = 0;
140 while i + 4 <= n {
141 let va = vld1q_f32(a.as_ptr().add(i));
142 let vb = vld1q_f32(b.as_ptr().add(i));
143 acc = vfmaq_f32(acc, va, vb);
144 i += 4;
145 }
146 let mut sum = vaddvq_f32(acc);
147 while i < n {
148 sum += a[i] * b[i];
149 i += 1;
150 }
151 sum
152}
153
154#[cfg(target_arch = "x86_64")]
155#[target_feature(enable = "avx2,fma")]
156unsafe fn dot_f32_avx2(a: &[f32], b: &[f32]) -> f32 {
157 use std::arch::x86_64::*;
158 let n = a.len();
159 let mut acc = _mm256_setzero_ps();
160 let mut i = 0;
161 while i + 8 <= n {
162 let va = _mm256_loadu_ps(a.as_ptr().add(i));
163 let vb = _mm256_loadu_ps(b.as_ptr().add(i));
164 acc = _mm256_fmadd_ps(va, vb, acc);
165 i += 8;
166 }
167 let mut sum = hsum256_ps(acc);
168 while i < n {
169 sum += a[i] * b[i];
170 i += 1;
171 }
172 sum
173}
174
175#[cfg(target_arch = "x86_64")]
180#[target_feature(enable = "avx2")]
181unsafe fn hsum256_ps(acc: std::arch::x86_64::__m256) -> f32 {
182 use std::arch::x86_64::*;
183 let lo = _mm256_castps256_ps128(acc);
184 let hi = _mm256_extractf128_ps(acc, 1);
185 let mut s128 = _mm_add_ps(lo, hi);
186 s128 = _mm_add_ps(s128, _mm_movehl_ps(s128, s128));
187 s128 = _mm_add_ss(s128, _mm_shuffle_ps(s128, s128, 0x55));
188 _mm_cvtss_f32(s128)
189}
190
191fn online_attn_accumulate(
217 q_h: &[f32],
218 scale: f32,
219 head_dim: usize,
220 out_h: &mut [f32],
221 attn_softcap: Option<f32>,
222 sink: Option<f32>,
223 mut for_each_kv: impl FnMut(&mut dyn FnMut(&[f32], &[f32])),
224) {
225 debug_assert_eq!(q_h.len(), head_dim);
226 debug_assert_eq!(out_h.len(), head_dim);
227 let mut m = f32::NEG_INFINITY;
228 let mut l = 0f32;
229 out_h.fill(0.0);
230 for_each_kv(&mut |k_t, v_t| {
231 let mut s = dot_f32(q_h, k_t) * scale;
232 if let Some(sc) = attn_softcap.filter(|&c| c > 0.0) {
233 s = sc * (s / sc).tanh();
234 }
235 let m_new = m.max(s);
236 let alpha = (m - m_new).exp();
237 let p = (s - m_new).exp();
238 l = l * alpha + p;
239 axpy_scale(out_h, alpha, v_t, p);
240 m = m_new;
241 });
242 if let Some(s) = sink {
243 let m_new = m.max(s);
244 let alpha = (m - m_new).exp();
245 l = l * alpha + (s - m_new).exp();
246 scale_inplace(out_h, alpha);
247 }
248 if l > 0.0 {
249 let inv = 1.0 / l;
250 scale_inplace(out_h, inv);
251 }
252}
253
254#[inline]
256fn axpy_scale(out: &mut [f32], alpha: f32, v: &[f32], p: f32) {
257 debug_assert_eq!(out.len(), v.len());
258 #[cfg(target_arch = "aarch64")]
259 {
260 if std::arch::is_aarch64_feature_detected!("neon") {
261 unsafe { axpy_scale_neon(out, alpha, v, p) };
262 return;
263 }
264 }
265 #[cfg(target_arch = "x86_64")]
266 {
267 if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
268 unsafe { axpy_scale_avx2(out, alpha, v, p) };
269 return;
270 }
271 }
272 for (o, &vv) in out.iter_mut().zip(v) {
273 *o = *o * alpha + p * vv;
274 }
275}
276
277#[inline]
280fn axpy(out: &mut [f32], v: &[f32], p: f32) {
281 debug_assert_eq!(out.len(), v.len());
282 #[cfg(target_arch = "aarch64")]
283 {
284 if std::arch::is_aarch64_feature_detected!("neon") {
285 unsafe { axpy_neon(out, v, p) };
286 return;
287 }
288 }
289 #[cfg(target_arch = "x86_64")]
290 {
291 if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
292 unsafe { axpy_avx2(out, v, p) };
293 return;
294 }
295 }
296 for (o, &vv) in out.iter_mut().zip(v) {
297 *o += p * vv;
298 }
299}
300
301#[cfg(target_arch = "aarch64")]
302#[target_feature(enable = "neon")]
303unsafe fn axpy_neon(out: &mut [f32], v: &[f32], p: f32) {
304 use std::arch::aarch64::*;
305 let n = out.len();
306 let vp = vdupq_n_f32(p);
307 let mut i = 0;
308 while i + 4 <= n {
309 let o = vld1q_f32(out.as_ptr().add(i));
310 let vv = vld1q_f32(v.as_ptr().add(i));
311 vst1q_f32(out.as_mut_ptr().add(i), vfmaq_f32(o, vv, vp));
312 i += 4;
313 }
314 while i < n {
315 out[i] += p * v[i];
316 i += 1;
317 }
318}
319
320#[cfg(target_arch = "x86_64")]
321#[target_feature(enable = "avx2,fma")]
322unsafe fn axpy_avx2(out: &mut [f32], v: &[f32], p: f32) {
323 use std::arch::x86_64::*;
324 let n = out.len();
325 let vp = _mm256_set1_ps(p);
326 let mut i = 0;
327 while i + 8 <= n {
328 let o = _mm256_loadu_ps(out.as_ptr().add(i));
329 let vv = _mm256_loadu_ps(v.as_ptr().add(i));
330 _mm256_storeu_ps(out.as_mut_ptr().add(i), _mm256_fmadd_ps(vv, vp, o));
331 i += 8;
332 }
333 while i < n {
334 out[i] += p * v[i];
335 i += 1;
336 }
337}
338
339#[inline]
340fn scale_inplace(x: &mut [f32], s: f32) {
341 #[cfg(target_arch = "aarch64")]
342 {
343 if std::arch::is_aarch64_feature_detected!("neon") {
344 unsafe { scale_inplace_neon(x, s) };
345 return;
346 }
347 }
348 #[cfg(target_arch = "x86_64")]
349 {
350 if std::is_x86_feature_detected!("avx2") {
351 unsafe { scale_inplace_avx2(x, s) };
352 return;
353 }
354 }
355 for v in x.iter_mut() {
356 *v *= s;
357 }
358}
359
360#[cfg(target_arch = "aarch64")]
361#[target_feature(enable = "neon")]
362unsafe fn axpy_scale_neon(out: &mut [f32], alpha: f32, v: &[f32], p: f32) {
363 use std::arch::aarch64::*;
364 let n = out.len();
365 let va = vdupq_n_f32(alpha);
366 let vp = vdupq_n_f32(p);
367 let mut i = 0;
368 while i + 4 <= n {
369 let o = vld1q_f32(out.as_ptr().add(i));
370 let vv = vld1q_f32(v.as_ptr().add(i));
371 let r = vfmaq_f32(vmulq_f32(o, va), vv, vp);
372 vst1q_f32(out.as_mut_ptr().add(i), r);
373 i += 4;
374 }
375 while i < n {
376 out[i] = out[i] * alpha + p * v[i];
377 i += 1;
378 }
379}
380
381#[cfg(target_arch = "aarch64")]
382#[target_feature(enable = "neon")]
383unsafe fn scale_inplace_neon(x: &mut [f32], s: f32) {
384 use std::arch::aarch64::*;
385 let n = x.len();
386 let vs = vdupq_n_f32(s);
387 let mut i = 0;
388 while i + 4 <= n {
389 let v = vld1q_f32(x.as_ptr().add(i));
390 vst1q_f32(x.as_mut_ptr().add(i), vmulq_f32(v, vs));
391 i += 4;
392 }
393 while i < n {
394 x[i] *= s;
395 i += 1;
396 }
397}
398
399#[cfg(target_arch = "x86_64")]
400#[target_feature(enable = "avx2,fma")]
401unsafe fn axpy_scale_avx2(out: &mut [f32], alpha: f32, v: &[f32], p: f32) {
402 use std::arch::x86_64::*;
403 let n = out.len();
404 let va = _mm256_set1_ps(alpha);
405 let vp = _mm256_set1_ps(p);
406 let mut i = 0;
407 while i + 8 <= n {
408 let o = _mm256_loadu_ps(out.as_ptr().add(i));
409 let vv = _mm256_loadu_ps(v.as_ptr().add(i));
410 let r = _mm256_fmadd_ps(vv, vp, _mm256_mul_ps(o, va));
411 _mm256_storeu_ps(out.as_mut_ptr().add(i), r);
412 i += 8;
413 }
414 while i < n {
415 out[i] = out[i] * alpha + p * v[i];
416 i += 1;
417 }
418}
419
420#[cfg(target_arch = "x86_64")]
421#[target_feature(enable = "avx2")]
422unsafe fn scale_inplace_avx2(x: &mut [f32], s: f32) {
423 use std::arch::x86_64::*;
424 let n = x.len();
425 let vs = _mm256_set1_ps(s);
426 let mut i = 0;
427 while i + 8 <= n {
428 let v = _mm256_loadu_ps(x.as_ptr().add(i));
429 _mm256_storeu_ps(x.as_mut_ptr().add(i), _mm256_mul_ps(v, vs));
430 i += 8;
431 }
432 while i < n {
433 x[i] *= s;
434 i += 1;
435 }
436}
437
438pub fn apply_rope_with_freq_factors(vec: &mut [f32], pos: usize, theta: f32, freq_factors: &[f32]) {
459 let dim = vec.len();
460 let half = dim / 2;
461 assert_eq!(
462 freq_factors.len(),
463 half,
464 "freq_factors must have one entry per rotation band (dim/2)"
465 );
466 let freqs = rope_freqs(theta, dim);
467 for i in 0..half {
468 let freq = freqs[i];
469 let angle = pos as f32 * freq / freq_factors[i];
470 let (sin, cos) = angle.sin_cos();
471 let a = vec[i];
472 let b = vec[i + half];
473 vec[i] = a * cos - b * sin;
474 vec[i + half] = a * sin + b * cos;
475 }
476}
477
478pub fn apply_rope_interleaved(vec: &mut [f32], pos: usize, theta: f32) {
488 let dim = vec.len();
489 let half = dim / 2;
490 let freqs = rope_freqs(theta, dim);
491 for i in 0..half {
492 let freq = freqs[i];
493 let angle = pos as f32 * freq;
494 let (sin, cos) = angle.sin_cos();
495 let a = vec[2 * i];
496 let b = vec[2 * i + 1];
497 vec[2 * i] = a * cos - b * sin;
498 vec[2 * i + 1] = a * sin + b * cos;
499 }
500}
501
502pub fn apply_rope_interleaved_with_freq_factors(
512 vec: &mut [f32],
513 pos: usize,
514 theta: f32,
515 freq_factors: &[f32],
516) {
517 let dim = vec.len();
518 let half = dim / 2;
519 assert_eq!(
520 freq_factors.len(),
521 half,
522 "freq_factors must have one entry per rotation band (dim/2)"
523 );
524 let freqs = rope_freqs(theta, dim);
525 for i in 0..half {
526 let freq = freqs[i];
527 let angle = pos as f32 * freq / freq_factors[i];
528 let (sin, cos) = angle.sin_cos();
529 let a = vec[2 * i];
530 let b = vec[2 * i + 1];
531 vec[2 * i] = a * cos - b * sin;
532 vec[2 * i + 1] = a * sin + b * cos;
533 }
534}
535
536#[derive(Debug, Clone, Copy, PartialEq)]
549pub struct YarnScaling {
550 pub factor: f32,
554 pub beta_fast: f32,
558 pub beta_slow: f32,
562 pub orig_max_pos: usize,
566 pub truncate: bool,
572}
573
574impl YarnScaling {
575 pub fn new(factor: f32, orig_max_pos: usize) -> Self {
581 YarnScaling {
582 factor,
583 beta_fast: 32.0,
584 beta_slow: 1.0,
585 orig_max_pos,
586 truncate: true,
587 }
588 }
589}
590
591fn yarn_correction_dim(
601 num_rotations: f64,
602 rotary_dim: usize,
603 base: f64,
604 orig_max_pos: usize,
605) -> f64 {
606 rotary_dim as f64 * (orig_max_pos as f64 / (num_rotations * 2.0 * std::f64::consts::PI)).ln()
607 / (2.0 * base.ln())
608}
609
610pub fn yarn_correction_range(scaling: YarnScaling, rotary_dim: usize, base: f32) -> (f64, f64) {
633 let base = base as f64;
634 let mut low = yarn_correction_dim(
635 scaling.beta_fast as f64,
636 rotary_dim,
637 base,
638 scaling.orig_max_pos,
639 );
640 let mut high = yarn_correction_dim(
641 scaling.beta_slow as f64,
642 rotary_dim,
643 base,
644 scaling.orig_max_pos,
645 );
646 if scaling.truncate {
647 low = low.floor();
648 high = high.ceil();
649 }
650 low = low.max(0.0);
651 high = high.min(rotary_dim as f64 - 1.0);
652 if low == high {
653 high += 0.001;
654 }
655 (low, high)
656}
657
658pub fn yarn_freq_factors(scaling: YarnScaling, rotary_dim: usize, base: f32) -> Vec<f32> {
689 assert!(
690 rotary_dim > 0 && rotary_dim.is_multiple_of(2),
691 "rotary_dim must be a positive even number of channels, got {rotary_dim}"
692 );
693 assert!(
694 scaling.factor > 0.0,
695 "YaRN factor must be positive, got {}",
696 scaling.factor
697 );
698 let (low, high) = yarn_correction_range(scaling, rotary_dim, base);
699 let factor = scaling.factor as f64;
700 (0..rotary_dim / 2)
701 .map(|band| {
702 let ramp = ((band as f64 - low) / (high - low)).clamp(0.0, 1.0);
703 let freq_scale = ramp / factor + (1.0 - ramp);
705 (1.0 / freq_scale) as f32
706 })
707 .collect()
708}
709
710pub fn proportional_freq_factors(head_size: usize, rotary_dim: usize, base: f32) -> Vec<f32> {
731 assert!(
732 rotary_dim > 0 && rotary_dim.is_multiple_of(2) && rotary_dim <= head_size,
733 "rotary_dim {rotary_dim} must be positive, even, and no wider than head_size {head_size}"
734 );
735 let base = base as f64;
736 (0..rotary_dim / 2)
737 .map(|band| {
738 let exponent =
739 (2 * band) as f64 / head_size as f64 - (2 * band) as f64 / rotary_dim as f64;
740 base.powf(exponent) as f32
741 })
742 .collect()
743}
744
745pub fn causal_gqa_attention(
754 q: &[f32],
755 k_cache: &[f32],
756 v_cache: &[f32],
757 n_heads: usize,
758 n_kv_heads: usize,
759 head_dim: usize,
760 seq_len: usize,
761) -> Vec<f32> {
762 causal_gqa_attention_softcap(
763 q, k_cache, v_cache, n_heads, n_kv_heads, head_dim, seq_len, None,
764 )
765}
766
767#[allow(clippy::too_many_arguments)]
769pub fn causal_gqa_attention_softcap(
770 q: &[f32],
771 k_cache: &[f32],
772 v_cache: &[f32],
773 n_heads: usize,
774 n_kv_heads: usize,
775 head_dim: usize,
776 seq_len: usize,
777 attn_softcap: Option<f32>,
778) -> Vec<f32> {
779 assert_eq!(q.len(), n_heads * head_dim);
780 assert_eq!(k_cache.len(), seq_len * n_kv_heads * head_dim);
781 assert_eq!(v_cache.len(), seq_len * n_kv_heads * head_dim);
782
783 let group_size = n_heads / n_kv_heads.max(1);
784 let scale = 1.0 / (head_dim as f32).sqrt();
785 let mut out = vec![0f32; n_heads * head_dim];
786
787 for h in 0..n_heads {
788 let kv_h = h / group_size.max(1);
789 let q_h = &q[h * head_dim..(h + 1) * head_dim];
790 let out_h = &mut out[h * head_dim..(h + 1) * head_dim];
791 online_attn_accumulate(q_h, scale, head_dim, out_h, attn_softcap, None, |visit| {
792 for t in 0..seq_len {
793 let k_t = &k_cache
794 [(t * n_kv_heads + kv_h) * head_dim..(t * n_kv_heads + kv_h + 1) * head_dim];
795 let v_t = &v_cache
796 [(t * n_kv_heads + kv_h) * head_dim..(t * n_kv_heads + kv_h + 1) * head_dim];
797 visit(k_t, v_t);
798 }
799 });
800 }
801
802 out
803}
804
805#[allow(clippy::too_many_arguments)]
818pub fn causal_gqa_attention_windowed(
819 q: &[f32],
820 k_cache: &[f32],
821 v_cache: &[f32],
822 n_heads: usize,
823 n_kv_heads: usize,
824 head_dim: usize,
825 seq_len: usize,
826 window: usize,
827) -> Vec<f32> {
828 causal_gqa_attention_windowed_softcap(
829 q, k_cache, v_cache, n_heads, n_kv_heads, head_dim, seq_len, window, None,
830 )
831}
832
833#[allow(clippy::too_many_arguments)]
835pub fn causal_gqa_attention_windowed_softcap(
836 q: &[f32],
837 k_cache: &[f32],
838 v_cache: &[f32],
839 n_heads: usize,
840 n_kv_heads: usize,
841 head_dim: usize,
842 seq_len: usize,
843 window: usize,
844 attn_softcap: Option<f32>,
845) -> Vec<f32> {
846 assert_eq!(q.len(), n_heads * head_dim);
847 assert_eq!(k_cache.len(), seq_len * n_kv_heads * head_dim);
848 assert_eq!(v_cache.len(), seq_len * n_kv_heads * head_dim);
849 assert!(window > 0, "window must be positive");
850
851 let group_size = n_heads / n_kv_heads.max(1);
852 let scale = 1.0 / (head_dim as f32).sqrt();
853 let mut out = vec![0f32; n_heads * head_dim];
854 let window_start = seq_len.saturating_sub(window);
858
859 for h in 0..n_heads {
860 let kv_h = h / group_size.max(1);
861 let q_h = &q[h * head_dim..(h + 1) * head_dim];
862 let out_h = &mut out[h * head_dim..(h + 1) * head_dim];
863 online_attn_accumulate(q_h, scale, head_dim, out_h, attn_softcap, None, |visit| {
864 for t in window_start..seq_len {
865 let k_t = &k_cache
866 [(t * n_kv_heads + kv_h) * head_dim..(t * n_kv_heads + kv_h + 1) * head_dim];
867 let v_t = &v_cache
868 [(t * n_kv_heads + kv_h) * head_dim..(t * n_kv_heads + kv_h + 1) * head_dim];
869 visit(k_t, v_t);
870 }
871 });
872 }
873
874 out
875}
876
877#[allow(clippy::too_many_arguments)]
898pub fn causal_gqa_attention_sinks(
899 q: &[f32],
900 k_cache: &[f32],
901 v_cache: &[f32],
902 n_heads: usize,
903 n_kv_heads: usize,
904 head_dim: usize,
905 seq_len: usize,
906 window: Option<usize>,
907 sinks: &[f32],
908) -> Vec<f32> {
909 assert_eq!(q.len(), n_heads * head_dim);
910 assert_eq!(k_cache.len(), seq_len * n_kv_heads * head_dim);
911 assert_eq!(v_cache.len(), seq_len * n_kv_heads * head_dim);
912 assert_eq!(
913 sinks.len(),
914 n_heads,
915 "attention sinks are per query head (llama.cpp `attn_sinks` is {{n_head}})"
916 );
917
918 let group_size = n_heads / n_kv_heads.max(1);
919 let scale = 1.0 / (head_dim as f32).sqrt();
920 let mut out = vec![0f32; n_heads * head_dim];
921 let start = match window {
924 Some(w) => {
925 assert!(w > 0, "window must be positive");
926 seq_len.saturating_sub(w)
927 }
928 None => 0,
929 };
930
931 for h in 0..n_heads {
932 let kv_h = h / group_size.max(1);
933 let q_h = &q[h * head_dim..(h + 1) * head_dim];
934 let sink = sinks[h];
935 let out_h = &mut out[h * head_dim..(h + 1) * head_dim];
936 online_attn_accumulate(q_h, scale, head_dim, out_h, None, Some(sink), |visit| {
937 for t in start..seq_len {
938 let k_t = &k_cache
939 [(t * n_kv_heads + kv_h) * head_dim..(t * n_kv_heads + kv_h + 1) * head_dim];
940 let v_t = &v_cache
941 [(t * n_kv_heads + kv_h) * head_dim..(t * n_kv_heads + kv_h + 1) * head_dim];
942 visit(k_t, v_t);
943 }
944 });
945 }
946
947 out
948}
949
950pub fn causal_gqa_attention_prefill(
956 q: &[f32],
957 k_cache: &[f32],
958 v_cache: &[f32],
959 n_heads: usize,
960 n_kv_heads: usize,
961 head_dim: usize,
962 seq_len: usize,
963) -> Vec<f32> {
964 assert_eq!(q.len(), seq_len * n_heads * head_dim);
965 assert_eq!(k_cache.len(), seq_len * n_kv_heads * head_dim);
966 assert_eq!(v_cache.len(), seq_len * n_kv_heads * head_dim);
967
968 let q_stride = n_heads * head_dim;
969 let kv_stride = n_kv_heads * head_dim;
970 let mut out = vec![0f32; seq_len * q_stride];
971 for t in 0..seq_len {
972 let q_t = &q[t * q_stride..(t + 1) * q_stride];
973 let k_prefix = &k_cache[..(t + 1) * kv_stride];
974 let v_prefix = &v_cache[..(t + 1) * kv_stride];
975 let attn = causal_gqa_attention(
976 q_t,
977 k_prefix,
978 v_prefix,
979 n_heads,
980 n_kv_heads,
981 head_dim,
982 t + 1,
983 );
984 out[t * q_stride..(t + 1) * q_stride].copy_from_slice(&attn);
985 }
986 out
987}
988
989#[allow(clippy::too_many_arguments)]
994pub fn causal_gqa_attention_prefill_shared_kv(
995 q: &[f32],
996 k_cache: &[f32],
997 v_cache: &[f32],
998 n_heads: usize,
999 n_kv_heads: usize,
1000 head_dim: usize,
1001 n_q: usize,
1002 kv_prefix: usize,
1003 attn_softcap: Option<f32>,
1004) -> Vec<f32> {
1005 causal_gqa_attention_prefill_shared_kv_windowed(
1006 q,
1007 k_cache,
1008 v_cache,
1009 n_heads,
1010 n_kv_heads,
1011 head_dim,
1012 n_q,
1013 kv_prefix,
1014 attn_softcap,
1015 None,
1016 )
1017}
1018
1019#[allow(clippy::too_many_arguments)]
1031pub fn causal_gqa_attention_prefill_shared_kv_windowed(
1032 q: &[f32],
1033 k_cache: &[f32],
1034 v_cache: &[f32],
1035 n_heads: usize,
1036 n_kv_heads: usize,
1037 head_dim: usize,
1038 n_q: usize,
1039 kv_prefix: usize,
1040 attn_softcap: Option<f32>,
1041 window: Option<usize>,
1042) -> Vec<f32> {
1043 let q_stride = n_heads * head_dim;
1044 let kv_stride = n_kv_heads * head_dim;
1045 assert_eq!(q.len(), n_q * q_stride);
1046 let kv_len = kv_prefix + n_q;
1047 assert!(k_cache.len() >= kv_len * kv_stride);
1048 assert!(v_cache.len() >= kv_len * kv_stride);
1049
1050 let group_size = n_heads / n_kv_heads.max(1);
1051 let scale = 1.0 / (head_dim as f32).sqrt();
1052 let mut out = vec![0f32; n_q * q_stride];
1053
1054 struct OutPtr(*mut f32);
1061 unsafe impl Send for OutPtr {}
1062 unsafe impl Sync for OutPtr {}
1063 impl OutPtr {
1064 #[inline]
1066 unsafe fn write(&self, off: usize, src: &[f32]) {
1067 std::ptr::copy_nonoverlapping(src.as_ptr(), self.0.add(off), src.len());
1068 }
1069 }
1070
1071 #[derive(Default)]
1077 struct Scratch {
1078 q_tile: Vec<f32>,
1079 scores: Vec<f32>,
1080 acc: Vec<f32>,
1081 }
1082
1083 const Q_BLOCK: usize = 8;
1084 let n_blocks = n_q.div_ceil(Q_BLOCK);
1085 let out_w = OutPtr(out.as_mut_ptr());
1086 let softcap = attn_softcap.filter(|&c| c > 0.0);
1087
1088 crate::par::indices_init(n_blocks * n_heads, 1, Scratch::default, |scratch, task| {
1089 let Scratch {
1090 q_tile,
1091 scores,
1092 acc,
1093 } = scratch;
1094 let blk = task / n_heads;
1095 let h = task % n_heads;
1096 let kv_h = h / group_size.max(1);
1097 let b_start = blk * Q_BLOCK;
1098 let b_end = (b_start + Q_BLOCK).min(n_q);
1099 let n_b = b_end - b_start;
1100
1101 let t_hi = kv_prefix + b_end;
1105 let t_lo = match window {
1106 Some(w) => (kv_prefix + b_start + 1).saturating_sub(w),
1107 None => 0,
1108 };
1109 let span = t_hi - t_lo;
1110 let kv_off = t_lo * kv_stride + kv_h * head_dim;
1111
1112 q_tile.clear();
1118 for b in b_start..b_end {
1119 q_tile.extend_from_slice(&q[b * q_stride + h * head_dim..][..head_dim]);
1120 }
1121
1122 scores.resize(n_b * span, 0.0);
1130 qk_tile(
1131 q_tile, n_b, head_dim, k_cache, kv_off, kv_stride, span, scale, scores,
1132 );
1133
1134 let mut norms = [0f32; Q_BLOCK];
1140 for b in b_start..b_end {
1141 let causal_len = kv_prefix + b + 1;
1142 let t_start = match window {
1145 Some(w) => causal_len.saturating_sub(w),
1146 None => 0,
1147 };
1148 let row = &mut scores[(b - b_start) * span..][..span];
1149 let lo = t_start - t_lo;
1150 let hi = causal_len - t_lo;
1151 row[..lo].fill(0.0);
1152 row[hi..].fill(0.0);
1153 let live = &mut row[lo..hi];
1154 if let Some(sc) = softcap {
1155 for s in live.iter_mut() {
1156 *s = sc * (*s / sc).tanh();
1157 }
1158 }
1159 norms[b - b_start] = softmax_row_exp_sum(live);
1160 }
1161
1162 acc.resize(n_b * head_dim, 0.0);
1167 acc.fill(0.0);
1168 pv_tile(scores, n_b, span, v_cache, kv_off, kv_stride, head_dim, acc);
1169
1170 for b in b_start..b_end {
1171 let out_h = &mut acc[(b - b_start) * head_dim..][..head_dim];
1172 let l = norms[b - b_start];
1173 if l > 0.0 {
1174 scale_inplace(out_h, 1.0 / l);
1175 }
1176 unsafe {
1177 out_w.write(b * q_stride + h * head_dim, out_h);
1178 }
1179 }
1180 });
1181
1182 out
1183}
1184
1185#[inline]
1221fn softmax_row_exp_sum(x: &mut [f32]) -> f32 {
1222 if x.is_empty() {
1223 return 0.0;
1227 }
1228 #[cfg(target_arch = "aarch64")]
1229 {
1230 if std::arch::is_aarch64_feature_detected!("neon") {
1231 return unsafe { softmax_row_exp_sum_neon(x) };
1232 }
1233 }
1234 #[cfg(target_arch = "x86_64")]
1235 {
1236 if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
1237 return unsafe { softmax_row_exp_sum_avx2(x) };
1238 }
1239 }
1240 softmax_row_exp_sum_scalar(x)
1241}
1242
1243fn softmax_row_exp_sum_scalar(x: &mut [f32]) -> f32 {
1246 let m = x.iter().fold(f32::NEG_INFINITY, |a, &s| a.max(s));
1247 let mut l = 0f32;
1248 for s in x.iter_mut() {
1249 *s = (*s - m).exp();
1250 l += *s;
1251 }
1252 l
1253}
1254
1255#[cfg(target_arch = "aarch64")]
1278#[target_feature(enable = "neon")]
1279#[inline]
1280unsafe fn expf_neon(x: std::arch::aarch64::float32x4_t) -> std::arch::aarch64::float32x4_t {
1281 use std::arch::aarch64::*;
1282 let x = vmaxq_f32(x, vdupq_n_f32(EXP_MIN_ARG));
1283 let r = vdupq_n_f32(EXP_SHIFT);
1284 let z = vfmaq_f32(r, x, vdupq_n_f32(EXP_LOG2E));
1285 let n = vsubq_f32(z, r);
1286 let b = vfmsq_f32(
1288 vfmsq_f32(x, n, vdupq_n_f32(EXP_LN2_HI)),
1289 n,
1290 vdupq_n_f32(EXP_LN2_LO),
1291 );
1292 let e = vshlq_n_u32::<23>(vreinterpretq_u32_f32(z));
1295 let k = vreinterpretq_f32_u32(vaddq_u32(e, vreinterpretq_u32_f32(vdupq_n_f32(1.0))));
1296 let u = vmulq_f32(b, b);
1297 let j = vfmaq_f32(
1298 vmulq_f32(vdupq_n_f32(EXP_C0), b),
1299 vfmaq_f32(
1300 vfmaq_f32(vdupq_n_f32(EXP_C1), vdupq_n_f32(EXP_C2), b),
1301 vfmaq_f32(vdupq_n_f32(EXP_C3), vdupq_n_f32(EXP_C4), b),
1302 u,
1303 ),
1304 u,
1305 );
1306 vfmaq_f32(k, j, k)
1307}
1308
1309#[cfg(target_arch = "x86_64")]
1312#[target_feature(enable = "avx2,fma")]
1313#[inline]
1314unsafe fn expf_avx2(x: std::arch::x86_64::__m256) -> std::arch::x86_64::__m256 {
1315 use std::arch::x86_64::*;
1316 let x = _mm256_max_ps(x, _mm256_set1_ps(EXP_MIN_ARG));
1317 let r = _mm256_set1_ps(EXP_SHIFT);
1318 let z = _mm256_fmadd_ps(x, _mm256_set1_ps(EXP_LOG2E), r);
1319 let n = _mm256_sub_ps(z, r);
1320 let b = _mm256_fnmadd_ps(
1321 n,
1322 _mm256_set1_ps(EXP_LN2_LO),
1323 _mm256_fnmadd_ps(n, _mm256_set1_ps(EXP_LN2_HI), x),
1324 );
1325 let e = _mm256_slli_epi32::<23>(_mm256_castps_si256(z));
1326 let k = _mm256_castsi256_ps(_mm256_add_epi32(
1327 e,
1328 _mm256_castps_si256(_mm256_set1_ps(1.0)),
1329 ));
1330 let u = _mm256_mul_ps(b, b);
1331 let j = _mm256_fmadd_ps(
1332 _mm256_fmadd_ps(
1333 _mm256_fmadd_ps(_mm256_set1_ps(EXP_C4), b, _mm256_set1_ps(EXP_C3)),
1334 u,
1335 _mm256_fmadd_ps(_mm256_set1_ps(EXP_C2), b, _mm256_set1_ps(EXP_C1)),
1336 ),
1337 u,
1338 _mm256_mul_ps(_mm256_set1_ps(EXP_C0), b),
1339 );
1340 _mm256_fmadd_ps(j, k, k)
1341}
1342
1343#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
1347mod exp_consts {
1348 pub use crate::vexp::*;
1354
1355 pub const EXP_MIN_ARG: f32 = -87.0;
1358}
1359#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
1360use exp_consts::*;
1361
1362#[cfg(target_arch = "aarch64")]
1363#[target_feature(enable = "neon")]
1364unsafe fn softmax_row_exp_sum_neon(x: &mut [f32]) -> f32 {
1365 use std::arch::aarch64::*;
1366 let n = x.len();
1367 let p = x.as_mut_ptr();
1368 let nv = n & !3;
1369
1370 let mut mv = vdupq_n_f32(f32::NEG_INFINITY);
1371 let mut i = 0;
1372 while i < nv {
1373 mv = vmaxq_f32(mv, vld1q_f32(p.add(i)));
1374 i += 4;
1375 }
1376 let mut m = if nv == 0 {
1377 f32::NEG_INFINITY
1378 } else {
1379 vmaxvq_f32(mv)
1380 };
1381 for j in nv..n {
1382 m = m.max(*p.add(j));
1383 }
1384
1385 let mvec = vdupq_n_f32(m);
1386 let mut sv = vdupq_n_f32(0.0);
1387 let mut i = 0;
1388 while i < nv {
1389 let e = expf_neon(vsubq_f32(vld1q_f32(p.add(i)), mvec));
1390 vst1q_f32(p.add(i), e);
1391 sv = vaddq_f32(sv, e);
1392 i += 4;
1393 }
1394 let mut l = vaddvq_f32(sv);
1395 if nv < n {
1396 let mut buf = [0f32; 4];
1401 for (j, slot) in (nv..n).zip(buf.iter_mut()) {
1402 *slot = *p.add(j) - m;
1403 }
1404 vst1q_f32(buf.as_mut_ptr(), expf_neon(vld1q_f32(buf.as_ptr())));
1405 for (j, &e) in (nv..n).zip(buf.iter()) {
1406 *p.add(j) = e;
1407 l += e;
1408 }
1409 }
1410 l
1411}
1412
1413#[cfg(target_arch = "x86_64")]
1414#[target_feature(enable = "avx2,fma")]
1415unsafe fn softmax_row_exp_sum_avx2(x: &mut [f32]) -> f32 {
1416 use std::arch::x86_64::*;
1417 let n = x.len();
1418 let p = x.as_mut_ptr();
1419 let nv = n & !7;
1420
1421 let mut mv = _mm256_set1_ps(f32::NEG_INFINITY);
1422 let mut i = 0;
1423 while i < nv {
1424 mv = _mm256_max_ps(mv, _mm256_loadu_ps(p.add(i)));
1425 i += 8;
1426 }
1427 let mut m = if nv == 0 {
1428 f32::NEG_INFINITY
1429 } else {
1430 let mut lanes = [0f32; 8];
1431 _mm256_storeu_ps(lanes.as_mut_ptr(), mv);
1432 lanes.iter().fold(f32::NEG_INFINITY, |a, &s| a.max(s))
1433 };
1434 for j in nv..n {
1435 m = m.max(*p.add(j));
1436 }
1437
1438 let mvec = _mm256_set1_ps(m);
1439 let mut sv = _mm256_setzero_ps();
1440 let mut i = 0;
1441 while i < nv {
1442 let e = expf_avx2(_mm256_sub_ps(_mm256_loadu_ps(p.add(i)), mvec));
1443 _mm256_storeu_ps(p.add(i), e);
1444 sv = _mm256_add_ps(sv, e);
1445 i += 8;
1446 }
1447 let mut l = hsum256_ps(sv);
1448 if nv < n {
1449 let mut buf = [0f32; 8];
1450 for (j, slot) in (nv..n).zip(buf.iter_mut()) {
1451 *slot = *p.add(j) - m;
1452 }
1453 _mm256_storeu_ps(buf.as_mut_ptr(), expf_avx2(_mm256_loadu_ps(buf.as_ptr())));
1454 for (j, &e) in (nv..n).zip(buf.iter()) {
1455 *p.add(j) = e;
1456 l += e;
1457 }
1458 }
1459 l
1460}
1461
1462#[inline(never)]
1488#[allow(clippy::too_many_arguments)]
1489fn qk_tile(
1490 q_tile: &[f32],
1491 n_b: usize,
1492 head_dim: usize,
1493 k: &[f32],
1494 k_off: usize,
1495 k_stride: usize,
1496 span: usize,
1497 scale: f32,
1498 scores: &mut [f32],
1499) {
1500 debug_assert_eq!(q_tile.len(), n_b * head_dim);
1501 debug_assert_eq!(scores.len(), n_b * span);
1502 #[cfg(target_arch = "aarch64")]
1503 {
1504 if std::arch::is_aarch64_feature_detected!("neon") {
1505 unsafe {
1506 qk_tile_neon(
1507 q_tile, n_b, head_dim, k, k_off, k_stride, span, scale, scores,
1508 )
1509 };
1510 return;
1511 }
1512 }
1513 #[cfg(target_arch = "x86_64")]
1514 {
1515 if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
1516 unsafe {
1517 qk_tile_avx2(
1518 q_tile, n_b, head_dim, k, k_off, k_stride, span, scale, scores,
1519 )
1520 };
1521 return;
1522 }
1523 }
1524 qk_rows(
1525 q_tile,
1526 head_dim,
1527 k,
1528 k_off,
1529 k_stride,
1530 span,
1531 scale,
1532 scores,
1533 0..n_b,
1534 0..span,
1535 );
1536}
1537
1538#[allow(clippy::too_many_arguments)]
1542fn qk_rows(
1543 q_tile: &[f32],
1544 head_dim: usize,
1545 k: &[f32],
1546 k_off: usize,
1547 k_stride: usize,
1548 span: usize,
1549 scale: f32,
1550 scores: &mut [f32],
1551 rows: std::ops::Range<usize>,
1552 cols: std::ops::Range<usize>,
1553) {
1554 for b in rows {
1555 let q_b = &q_tile[b * head_dim..][..head_dim];
1556 for t in cols.clone() {
1557 let k_t = &k[k_off + t * k_stride..][..head_dim];
1558 scores[b * span + t] = dot_f32(q_b, k_t) * scale;
1559 }
1560 }
1561}
1562
1563#[cfg(target_arch = "aarch64")]
1564#[target_feature(enable = "neon")]
1565#[allow(clippy::too_many_arguments)]
1566unsafe fn qk_tile_neon(
1567 q_tile: &[f32],
1568 n_b: usize,
1569 head_dim: usize,
1570 k: &[f32],
1571 k_off: usize,
1572 k_stride: usize,
1573 span: usize,
1574 scale: f32,
1575 scores: &mut [f32],
1576) {
1577 use std::arch::aarch64::*;
1578 let qp = q_tile.as_ptr();
1579 let kp = k.as_ptr().add(k_off);
1580 let sp = scores.as_mut_ptr();
1581 let bt = n_b & !3;
1585 let tt = span & !3;
1586 let dv = head_dim & !3;
1587
1588 let mut t0 = 0;
1592 while t0 < tt {
1593 let k0 = kp.add(t0 * k_stride);
1594 let k1 = k0.add(k_stride);
1595 let k2 = k1.add(k_stride);
1596 let k3 = k2.add(k_stride);
1597 let mut b0 = 0;
1598 while b0 < bt {
1599 let a0 = qp.add(b0 * head_dim);
1600 let a1 = a0.add(head_dim);
1601 let a2 = a1.add(head_dim);
1602 let a3 = a2.add(head_dim);
1603 let z = vdupq_n_f32(0.0);
1604 let (mut c00, mut c01, mut c02, mut c03) = (z, z, z, z);
1606 let (mut c10, mut c11, mut c12, mut c13) = (z, z, z, z);
1607 let (mut c20, mut c21, mut c22, mut c23) = (z, z, z, z);
1608 let (mut c30, mut c31, mut c32, mut c33) = (z, z, z, z);
1609 let mut d = 0;
1610 while d < dv {
1611 let av0 = vld1q_f32(a0.add(d));
1612 let av1 = vld1q_f32(a1.add(d));
1613 let av2 = vld1q_f32(a2.add(d));
1614 let av3 = vld1q_f32(a3.add(d));
1615 let kv0 = vld1q_f32(k0.add(d));
1616 c00 = vfmaq_f32(c00, av0, kv0);
1617 c10 = vfmaq_f32(c10, av1, kv0);
1618 c20 = vfmaq_f32(c20, av2, kv0);
1619 c30 = vfmaq_f32(c30, av3, kv0);
1620 let kv1 = vld1q_f32(k1.add(d));
1621 c01 = vfmaq_f32(c01, av0, kv1);
1622 c11 = vfmaq_f32(c11, av1, kv1);
1623 c21 = vfmaq_f32(c21, av2, kv1);
1624 c31 = vfmaq_f32(c31, av3, kv1);
1625 let kv2 = vld1q_f32(k2.add(d));
1626 c02 = vfmaq_f32(c02, av0, kv2);
1627 c12 = vfmaq_f32(c12, av1, kv2);
1628 c22 = vfmaq_f32(c22, av2, kv2);
1629 c32 = vfmaq_f32(c32, av3, kv2);
1630 let kv3 = vld1q_f32(k3.add(d));
1631 c03 = vfmaq_f32(c03, av0, kv3);
1632 c13 = vfmaq_f32(c13, av1, kv3);
1633 c23 = vfmaq_f32(c23, av2, kv3);
1634 c33 = vfmaq_f32(c33, av3, kv3);
1635 d += 4;
1636 }
1637 let mut r = [
1638 [
1639 vaddvq_f32(c00),
1640 vaddvq_f32(c01),
1641 vaddvq_f32(c02),
1642 vaddvq_f32(c03),
1643 ],
1644 [
1645 vaddvq_f32(c10),
1646 vaddvq_f32(c11),
1647 vaddvq_f32(c12),
1648 vaddvq_f32(c13),
1649 ],
1650 [
1651 vaddvq_f32(c20),
1652 vaddvq_f32(c21),
1653 vaddvq_f32(c22),
1654 vaddvq_f32(c23),
1655 ],
1656 [
1657 vaddvq_f32(c30),
1658 vaddvq_f32(c31),
1659 vaddvq_f32(c32),
1660 vaddvq_f32(c33),
1661 ],
1662 ];
1663 let arow = [a0, a1, a2, a3];
1666 let krow = [k0, k1, k2, k3];
1667 for d in dv..head_dim {
1668 for (i, ai) in arow.iter().enumerate() {
1669 let av = *ai.add(d);
1670 for (j, kj) in krow.iter().enumerate() {
1671 r[i][j] += av * *kj.add(d);
1672 }
1673 }
1674 }
1675 for (i, ri) in r.iter().enumerate() {
1676 for (j, v) in ri.iter().enumerate() {
1677 *sp.add((b0 + i) * span + t0 + j) = v * scale;
1678 }
1679 }
1680 b0 += 4;
1681 }
1682 t0 += 4;
1683 }
1684 qk_rows(
1685 q_tile,
1686 head_dim,
1687 k,
1688 k_off,
1689 k_stride,
1690 span,
1691 scale,
1692 scores,
1693 0..bt,
1694 tt..span,
1695 );
1696 qk_rows(
1697 q_tile,
1698 head_dim,
1699 k,
1700 k_off,
1701 k_stride,
1702 span,
1703 scale,
1704 scores,
1705 bt..n_b,
1706 0..span,
1707 );
1708}
1709
1710#[cfg(target_arch = "x86_64")]
1714#[target_feature(enable = "avx2,fma")]
1715#[allow(clippy::too_many_arguments)]
1716unsafe fn qk_tile_avx2(
1717 q_tile: &[f32],
1718 n_b: usize,
1719 head_dim: usize,
1720 k: &[f32],
1721 k_off: usize,
1722 k_stride: usize,
1723 span: usize,
1724 scale: f32,
1725 scores: &mut [f32],
1726) {
1727 use std::arch::x86_64::*;
1728 let qp = q_tile.as_ptr();
1729 let kp = k.as_ptr().add(k_off);
1730 let sp = scores.as_mut_ptr();
1731 let bt = n_b & !3;
1732 let tt = span & !1;
1733 let dv = head_dim & !7;
1734
1735 let mut t0 = 0;
1736 while t0 < tt {
1737 let k0 = kp.add(t0 * k_stride);
1738 let k1 = k0.add(k_stride);
1739 let mut b0 = 0;
1740 while b0 < bt {
1741 let a0 = qp.add(b0 * head_dim);
1742 let a1 = a0.add(head_dim);
1743 let a2 = a1.add(head_dim);
1744 let a3 = a2.add(head_dim);
1745 let z = _mm256_setzero_ps();
1746 let (mut c00, mut c01) = (z, z);
1747 let (mut c10, mut c11) = (z, z);
1748 let (mut c20, mut c21) = (z, z);
1749 let (mut c30, mut c31) = (z, z);
1750 let mut d = 0;
1751 while d < dv {
1752 let av0 = _mm256_loadu_ps(a0.add(d));
1753 let av1 = _mm256_loadu_ps(a1.add(d));
1754 let av2 = _mm256_loadu_ps(a2.add(d));
1755 let av3 = _mm256_loadu_ps(a3.add(d));
1756 let kv0 = _mm256_loadu_ps(k0.add(d));
1757 c00 = _mm256_fmadd_ps(av0, kv0, c00);
1758 c10 = _mm256_fmadd_ps(av1, kv0, c10);
1759 c20 = _mm256_fmadd_ps(av2, kv0, c20);
1760 c30 = _mm256_fmadd_ps(av3, kv0, c30);
1761 let kv1 = _mm256_loadu_ps(k1.add(d));
1762 c01 = _mm256_fmadd_ps(av0, kv1, c01);
1763 c11 = _mm256_fmadd_ps(av1, kv1, c11);
1764 c21 = _mm256_fmadd_ps(av2, kv1, c21);
1765 c31 = _mm256_fmadd_ps(av3, kv1, c31);
1766 d += 8;
1767 }
1768 let mut r = [
1769 [hsum256_ps(c00), hsum256_ps(c01)],
1770 [hsum256_ps(c10), hsum256_ps(c11)],
1771 [hsum256_ps(c20), hsum256_ps(c21)],
1772 [hsum256_ps(c30), hsum256_ps(c31)],
1773 ];
1774 let arow = [a0, a1, a2, a3];
1775 let krow = [k0, k1];
1776 for d in dv..head_dim {
1777 for (i, ai) in arow.iter().enumerate() {
1778 let av = *ai.add(d);
1779 for (j, kj) in krow.iter().enumerate() {
1780 r[i][j] += av * *kj.add(d);
1781 }
1782 }
1783 }
1784 for (i, ri) in r.iter().enumerate() {
1785 for (j, v) in ri.iter().enumerate() {
1786 *sp.add((b0 + i) * span + t0 + j) = v * scale;
1787 }
1788 }
1789 b0 += 4;
1790 }
1791 t0 += 2;
1792 }
1793 qk_rows(
1794 q_tile,
1795 head_dim,
1796 k,
1797 k_off,
1798 k_stride,
1799 span,
1800 scale,
1801 scores,
1802 0..bt,
1803 tt..span,
1804 );
1805 qk_rows(
1806 q_tile,
1807 head_dim,
1808 k,
1809 k_off,
1810 k_stride,
1811 span,
1812 scale,
1813 scores,
1814 bt..n_b,
1815 0..span,
1816 );
1817}
1818
1819#[inline(never)]
1840#[allow(clippy::too_many_arguments)]
1841fn pv_tile(
1842 p: &[f32],
1843 n_b: usize,
1844 span: usize,
1845 v: &[f32],
1846 v_off: usize,
1847 v_stride: usize,
1848 head_dim: usize,
1849 acc: &mut [f32],
1850) {
1851 debug_assert_eq!(p.len(), n_b * span);
1852 debug_assert_eq!(acc.len(), n_b * head_dim);
1853 #[cfg(target_arch = "aarch64")]
1854 {
1855 if std::arch::is_aarch64_feature_detected!("neon") {
1856 unsafe { pv_tile_neon(p, n_b, span, v, v_off, v_stride, head_dim, acc) };
1857 return;
1858 }
1859 }
1860 #[cfg(target_arch = "x86_64")]
1861 {
1862 if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
1863 unsafe { pv_tile_avx2(p, n_b, span, v, v_off, v_stride, head_dim, acc) };
1864 return;
1865 }
1866 }
1867 pv_rows(p, span, v, v_off, v_stride, head_dim, acc, 0..n_b);
1868}
1869
1870#[allow(clippy::too_many_arguments)]
1875fn pv_rows(
1876 p: &[f32],
1877 span: usize,
1878 v: &[f32],
1879 v_off: usize,
1880 v_stride: usize,
1881 head_dim: usize,
1882 acc: &mut [f32],
1883 rows: std::ops::Range<usize>,
1884) {
1885 for b in rows {
1886 let out_b = &mut acc[b * head_dim..][..head_dim];
1887 for t in 0..span {
1888 let w = p[b * span + t];
1889 if w == 0.0 {
1890 continue;
1891 }
1892 axpy(out_b, &v[v_off + t * v_stride..][..head_dim], w);
1893 }
1894 }
1895}
1896
1897#[cfg(target_arch = "aarch64")]
1898#[target_feature(enable = "neon")]
1899#[allow(clippy::too_many_arguments)]
1900unsafe fn pv_tile_neon(
1901 p: &[f32],
1902 n_b: usize,
1903 span: usize,
1904 v: &[f32],
1905 v_off: usize,
1906 v_stride: usize,
1907 head_dim: usize,
1908 acc: &mut [f32],
1909) {
1910 use std::arch::aarch64::*;
1911 let vp = v.as_ptr().add(v_off);
1912 let pp = p.as_ptr();
1913 let ap = acc.as_mut_ptr();
1914 let bt = n_b & !7;
1915 let dv = head_dim & !7;
1916 let dv4 = head_dim & !3;
1919
1920 let mut b0 = 0;
1921 while b0 < bt {
1922 let mut d0 = 0;
1923 while d0 < dv {
1924 let mut c0l = vld1q_f32(ap.add(b0 * head_dim + d0));
1925 let mut c0h = vld1q_f32(ap.add(b0 * head_dim + d0 + 4));
1926 let mut c1l = vld1q_f32(ap.add((b0 + 1) * head_dim + d0));
1927 let mut c1h = vld1q_f32(ap.add((b0 + 1) * head_dim + d0 + 4));
1928 let mut c2l = vld1q_f32(ap.add((b0 + 2) * head_dim + d0));
1929 let mut c2h = vld1q_f32(ap.add((b0 + 2) * head_dim + d0 + 4));
1930 let mut c3l = vld1q_f32(ap.add((b0 + 3) * head_dim + d0));
1931 let mut c3h = vld1q_f32(ap.add((b0 + 3) * head_dim + d0 + 4));
1932 let mut c4l = vld1q_f32(ap.add((b0 + 4) * head_dim + d0));
1933 let mut c4h = vld1q_f32(ap.add((b0 + 4) * head_dim + d0 + 4));
1934 let mut c5l = vld1q_f32(ap.add((b0 + 5) * head_dim + d0));
1935 let mut c5h = vld1q_f32(ap.add((b0 + 5) * head_dim + d0 + 4));
1936 let mut c6l = vld1q_f32(ap.add((b0 + 6) * head_dim + d0));
1937 let mut c6h = vld1q_f32(ap.add((b0 + 6) * head_dim + d0 + 4));
1938 let mut c7l = vld1q_f32(ap.add((b0 + 7) * head_dim + d0));
1939 let mut c7h = vld1q_f32(ap.add((b0 + 7) * head_dim + d0 + 4));
1940 for t in 0..span {
1941 let vr = vp.add(t * v_stride + d0);
1942 let v0 = vld1q_f32(vr);
1943 let v1 = vld1q_f32(vr.add(4));
1944 let s0 = vdupq_n_f32(*pp.add(b0 * span + t));
1945 c0l = vfmaq_f32(c0l, v0, s0);
1946 c0h = vfmaq_f32(c0h, v1, s0);
1947 let s1 = vdupq_n_f32(*pp.add((b0 + 1) * span + t));
1948 c1l = vfmaq_f32(c1l, v0, s1);
1949 c1h = vfmaq_f32(c1h, v1, s1);
1950 let s2 = vdupq_n_f32(*pp.add((b0 + 2) * span + t));
1951 c2l = vfmaq_f32(c2l, v0, s2);
1952 c2h = vfmaq_f32(c2h, v1, s2);
1953 let s3 = vdupq_n_f32(*pp.add((b0 + 3) * span + t));
1954 c3l = vfmaq_f32(c3l, v0, s3);
1955 c3h = vfmaq_f32(c3h, v1, s3);
1956 let s4 = vdupq_n_f32(*pp.add((b0 + 4) * span + t));
1957 c4l = vfmaq_f32(c4l, v0, s4);
1958 c4h = vfmaq_f32(c4h, v1, s4);
1959 let s5 = vdupq_n_f32(*pp.add((b0 + 5) * span + t));
1960 c5l = vfmaq_f32(c5l, v0, s5);
1961 c5h = vfmaq_f32(c5h, v1, s5);
1962 let s6 = vdupq_n_f32(*pp.add((b0 + 6) * span + t));
1963 c6l = vfmaq_f32(c6l, v0, s6);
1964 c6h = vfmaq_f32(c6h, v1, s6);
1965 let s7 = vdupq_n_f32(*pp.add((b0 + 7) * span + t));
1966 c7l = vfmaq_f32(c7l, v0, s7);
1967 c7h = vfmaq_f32(c7h, v1, s7);
1968 }
1969 vst1q_f32(ap.add(b0 * head_dim + d0), c0l);
1970 vst1q_f32(ap.add(b0 * head_dim + d0 + 4), c0h);
1971 vst1q_f32(ap.add((b0 + 1) * head_dim + d0), c1l);
1972 vst1q_f32(ap.add((b0 + 1) * head_dim + d0 + 4), c1h);
1973 vst1q_f32(ap.add((b0 + 2) * head_dim + d0), c2l);
1974 vst1q_f32(ap.add((b0 + 2) * head_dim + d0 + 4), c2h);
1975 vst1q_f32(ap.add((b0 + 3) * head_dim + d0), c3l);
1976 vst1q_f32(ap.add((b0 + 3) * head_dim + d0 + 4), c3h);
1977 vst1q_f32(ap.add((b0 + 4) * head_dim + d0), c4l);
1978 vst1q_f32(ap.add((b0 + 4) * head_dim + d0 + 4), c4h);
1979 vst1q_f32(ap.add((b0 + 5) * head_dim + d0), c5l);
1980 vst1q_f32(ap.add((b0 + 5) * head_dim + d0 + 4), c5h);
1981 vst1q_f32(ap.add((b0 + 6) * head_dim + d0), c6l);
1982 vst1q_f32(ap.add((b0 + 6) * head_dim + d0 + 4), c6h);
1983 vst1q_f32(ap.add((b0 + 7) * head_dim + d0), c7l);
1984 vst1q_f32(ap.add((b0 + 7) * head_dim + d0 + 4), c7h);
1985 d0 += 8;
1986 }
1987 if dv < head_dim {
1991 for t in 0..span {
1992 for i in 0..8 {
1993 let w = *pp.add((b0 + i) * span + t);
1994 if w == 0.0 {
1995 continue;
1996 }
1997 let row = ap.add((b0 + i) * head_dim);
1998 for d in dv..dv4 {
1999 *row.add(d) = f32::mul_add(w, *vp.add(t * v_stride + d), *row.add(d));
2000 }
2001 for d in dv4..head_dim {
2002 *row.add(d) += w * *vp.add(t * v_stride + d);
2003 }
2004 }
2005 }
2006 }
2007 b0 += 8;
2008 }
2009 pv_rows(p, span, v, v_off, v_stride, head_dim, acc, bt..n_b);
2010}
2011
2012#[cfg(target_arch = "x86_64")]
2015#[target_feature(enable = "avx2,fma")]
2016#[allow(clippy::too_many_arguments)]
2017unsafe fn pv_tile_avx2(
2018 p: &[f32],
2019 n_b: usize,
2020 span: usize,
2021 v: &[f32],
2022 v_off: usize,
2023 v_stride: usize,
2024 head_dim: usize,
2025 acc: &mut [f32],
2026) {
2027 use std::arch::x86_64::*;
2028 let vp = v.as_ptr().add(v_off);
2029 let pp = p.as_ptr();
2030 let ap = acc.as_mut_ptr();
2031 let bt = n_b & !7;
2032 let dv = head_dim & !7;
2035
2036 let mut b0 = 0;
2037 while b0 < bt {
2038 let mut d0 = 0;
2039 while d0 < dv {
2040 let mut c0 = _mm256_loadu_ps(ap.add(b0 * head_dim + d0));
2041 let mut c1 = _mm256_loadu_ps(ap.add((b0 + 1) * head_dim + d0));
2042 let mut c2 = _mm256_loadu_ps(ap.add((b0 + 2) * head_dim + d0));
2043 let mut c3 = _mm256_loadu_ps(ap.add((b0 + 3) * head_dim + d0));
2044 let mut c4 = _mm256_loadu_ps(ap.add((b0 + 4) * head_dim + d0));
2045 let mut c5 = _mm256_loadu_ps(ap.add((b0 + 5) * head_dim + d0));
2046 let mut c6 = _mm256_loadu_ps(ap.add((b0 + 6) * head_dim + d0));
2047 let mut c7 = _mm256_loadu_ps(ap.add((b0 + 7) * head_dim + d0));
2048 for t in 0..span {
2049 let vv = _mm256_loadu_ps(vp.add(t * v_stride + d0));
2050 c0 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add(b0 * span + t)), c0);
2051 c1 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 1) * span + t)), c1);
2052 c2 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 2) * span + t)), c2);
2053 c3 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 3) * span + t)), c3);
2054 c4 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 4) * span + t)), c4);
2055 c5 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 5) * span + t)), c5);
2056 c6 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 6) * span + t)), c6);
2057 c7 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 7) * span + t)), c7);
2058 }
2059 _mm256_storeu_ps(ap.add(b0 * head_dim + d0), c0);
2060 _mm256_storeu_ps(ap.add((b0 + 1) * head_dim + d0), c1);
2061 _mm256_storeu_ps(ap.add((b0 + 2) * head_dim + d0), c2);
2062 _mm256_storeu_ps(ap.add((b0 + 3) * head_dim + d0), c3);
2063 _mm256_storeu_ps(ap.add((b0 + 4) * head_dim + d0), c4);
2064 _mm256_storeu_ps(ap.add((b0 + 5) * head_dim + d0), c5);
2065 _mm256_storeu_ps(ap.add((b0 + 6) * head_dim + d0), c6);
2066 _mm256_storeu_ps(ap.add((b0 + 7) * head_dim + d0), c7);
2067 d0 += 8;
2068 }
2069 if dv < head_dim {
2070 for t in 0..span {
2071 for i in 0..8 {
2072 let w = *pp.add((b0 + i) * span + t);
2073 if w == 0.0 {
2074 continue;
2075 }
2076 let row = ap.add((b0 + i) * head_dim);
2077 for d in dv..head_dim {
2078 *row.add(d) += w * *vp.add(t * v_stride + d);
2079 }
2080 }
2081 }
2082 }
2083 b0 += 8;
2084 }
2085 pv_rows(p, span, v, v_off, v_stride, head_dim, acc, bt..n_b);
2086}
2087
2088pub fn causal_gqa_attention_paged(
2096 q: &[f32],
2097 store: &PagedKvStore,
2098 block_table: &[usize],
2099 n_heads: usize,
2100 n_kv_heads: usize,
2101 head_dim: usize,
2102 seq_len: usize,
2103) -> Vec<f32> {
2104 assert_eq!(q.len(), n_heads * head_dim);
2105 let block_size = store.block_size();
2106 assert!(
2107 block_table.len() * block_size >= seq_len,
2108 "block table too short for seq_len"
2109 );
2110
2111 let group_size = n_heads / n_kv_heads.max(1);
2112 let scale = 1.0 / (head_dim as f32).sqrt();
2113 let mut out = vec![0f32; n_heads * head_dim];
2114
2115 for h in 0..n_heads {
2116 let kv_h = h / group_size.max(1);
2117 let q_h = &q[h * head_dim..(h + 1) * head_dim];
2118 let out_h = &mut out[h * head_dim..(h + 1) * head_dim];
2119 online_attn_accumulate(q_h, scale, head_dim, out_h, None, None, |visit| {
2120 for t in 0..seq_len {
2121 let block_id = block_table[t / block_size];
2122 let offset = t % block_size;
2123 let k_row = store.k_row(block_id, offset);
2124 let v_row = store.v_row(block_id, offset);
2125 let k_t = &k_row[kv_h * head_dim..(kv_h + 1) * head_dim];
2126 let v_t = &v_row[kv_h * head_dim..(kv_h + 1) * head_dim];
2127 visit(k_t, v_t);
2128 }
2129 });
2130 }
2131
2132 out
2133}
2134
2135#[allow(clippy::too_many_arguments)]
2166pub fn causal_gqa_attention_paged_sinks(
2167 q: &[f32],
2168 store: &PagedKvStore,
2169 block_table: &[usize],
2170 n_heads: usize,
2171 n_kv_heads: usize,
2172 head_dim: usize,
2173 seq_len: usize,
2174 window: Option<usize>,
2175 sinks: Option<&[f32]>,
2176 attn_softcap: Option<f32>,
2177) -> Vec<f32> {
2178 assert_eq!(q.len(), n_heads * head_dim);
2179 let block_size = store.block_size();
2180 assert!(
2181 block_table.len() * block_size >= seq_len,
2182 "block table too short for seq_len"
2183 );
2184 if let Some(sinks) = sinks {
2185 assert_eq!(
2186 sinks.len(),
2187 n_heads,
2188 "attention sinks are per query head (llama.cpp `attn_sinks` is {{n_head}})"
2189 );
2190 }
2191
2192 let group_size = n_heads / n_kv_heads.max(1);
2193 let scale = 1.0 / (head_dim as f32).sqrt();
2194 let mut out = vec![0f32; n_heads * head_dim];
2195 let start = match window {
2200 Some(w) => {
2201 assert!(w > 0, "window must be positive");
2202 seq_len.saturating_sub(w)
2203 }
2204 None => 0,
2205 };
2206
2207 for h in 0..n_heads {
2208 let kv_h = h / group_size.max(1);
2209 let q_h = &q[h * head_dim..(h + 1) * head_dim];
2210 let sink = sinks.map(|s| s[h]);
2211 let out_h = &mut out[h * head_dim..(h + 1) * head_dim];
2212 online_attn_accumulate(q_h, scale, head_dim, out_h, attn_softcap, sink, |visit| {
2213 for t in start..seq_len {
2214 let block_id = block_table[t / block_size];
2215 let offset = t % block_size;
2216 let k_row = store.k_row(block_id, offset);
2217 let v_row = store.v_row(block_id, offset);
2218 let k_t = &k_row[kv_h * head_dim..(kv_h + 1) * head_dim];
2219 let v_t = &v_row[kv_h * head_dim..(kv_h + 1) * head_dim];
2220 visit(k_t, v_t);
2221 }
2222 });
2223 }
2224
2225 out
2226}
2227
2228pub fn causal_mla_attention(
2242 q: &[f32],
2243 k_cache: &[f32],
2244 v_cache: &[f32],
2245 n_heads: usize,
2246 qk_head_dim: usize,
2247 v_head_dim: usize,
2248 seq_len: usize,
2249) -> Vec<f32> {
2250 mla_attention_inner(
2251 q,
2252 k_cache,
2253 v_cache,
2254 n_heads,
2255 qk_head_dim,
2256 v_head_dim,
2257 seq_len,
2258 None,
2259 None,
2260 )
2261}
2262
2263#[allow(clippy::too_many_arguments)]
2285fn mla_attention_inner(
2286 q: &[f32],
2287 k_cache: &[f32],
2288 v_cache: &[f32],
2289 n_heads: usize,
2290 qk_head_dim: usize,
2291 v_head_dim: usize,
2292 seq_len: usize,
2293 visible: Option<&[usize]>,
2294 sinks: Option<&[f32]>,
2295) -> Vec<f32> {
2296 assert_eq!(q.len(), n_heads * qk_head_dim);
2297 assert_eq!(k_cache.len(), seq_len * n_heads * qk_head_dim);
2298 assert_eq!(v_cache.len(), seq_len * n_heads * v_head_dim);
2299 if let Some(visible) = visible {
2300 assert!(
2301 visible.iter().all(|&t| t < seq_len),
2302 "visible positions must be within seq_len"
2303 );
2304 }
2305 if let Some(sinks) = sinks {
2306 assert_eq!(
2307 sinks.len(),
2308 n_heads,
2309 "one sink logit per query head, or none at all"
2310 );
2311 }
2312
2313 let n_positions = visible.map_or(seq_len, |v| v.len());
2318 let position_at = |i: usize| visible.map_or(i, |v| v[i]);
2319
2320 let scale = 1.0 / (qk_head_dim as f32).sqrt();
2321 let mut out = vec![0f32; n_heads * v_head_dim];
2322
2323 for h in 0..n_heads {
2324 let q_h = &q[h * qk_head_dim..(h + 1) * qk_head_dim];
2325
2326 let mut scores = vec![0f32; n_positions];
2327 for (i, score) in scores.iter_mut().enumerate() {
2328 let t = position_at(i);
2329 let k_t =
2330 &k_cache[(t * n_heads + h) * qk_head_dim..(t * n_heads + h + 1) * qk_head_dim];
2331 let mut dot = 0f32;
2332 for d in 0..qk_head_dim {
2333 dot += q_h[d] * k_t[d];
2334 }
2335 *score = dot * scale;
2336 }
2337
2338 let sink = sinks.map(|s| s[h]);
2339 let mut max = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2340 if let Some(s) = sink {
2341 max = max.max(s);
2342 }
2343 let mut sum = 0f32;
2344 for s in scores.iter_mut() {
2345 *s = (*s - max).exp();
2346 sum += *s;
2347 }
2348 if let Some(s) = sink {
2352 sum += (s - max).exp();
2353 }
2354 if sum > 0.0 {
2355 for s in scores.iter_mut() {
2356 *s /= sum;
2357 }
2358 }
2359
2360 let out_h = &mut out[h * v_head_dim..(h + 1) * v_head_dim];
2361 for (i, &w) in scores.iter().enumerate() {
2362 let t = position_at(i);
2363 let v_t = &v_cache[(t * n_heads + h) * v_head_dim..(t * n_heads + h + 1) * v_head_dim];
2364 for d in 0..v_head_dim {
2365 out_h[d] += w * v_t[d];
2366 }
2367 }
2368 }
2369
2370 out
2371}
2372
2373#[allow(clippy::too_many_arguments)]
2379pub fn causal_mla_attention_sinks(
2380 q: &[f32],
2381 k_cache: &[f32],
2382 v_cache: &[f32],
2383 n_heads: usize,
2384 qk_head_dim: usize,
2385 v_head_dim: usize,
2386 seq_len: usize,
2387 sinks: Option<&[f32]>,
2388) -> Vec<f32> {
2389 mla_attention_inner(
2390 q,
2391 k_cache,
2392 v_cache,
2393 n_heads,
2394 qk_head_dim,
2395 v_head_dim,
2396 seq_len,
2397 None,
2398 sinks,
2399 )
2400}
2401
2402#[allow(clippy::too_many_arguments)]
2409pub fn causal_mla_attention_sparse_sinks(
2410 q: &[f32],
2411 k_cache: &[f32],
2412 v_cache: &[f32],
2413 n_heads: usize,
2414 qk_head_dim: usize,
2415 v_head_dim: usize,
2416 seq_len: usize,
2417 visible: &[usize],
2418 sinks: Option<&[f32]>,
2419) -> Vec<f32> {
2420 mla_attention_inner(
2421 q,
2422 k_cache,
2423 v_cache,
2424 n_heads,
2425 qk_head_dim,
2426 v_head_dim,
2427 seq_len,
2428 Some(visible),
2429 sinks,
2430 )
2431}
2432
2433pub fn lightning_indexer_topk(
2453 indexer_q: &[Vec<f32>],
2454 indexer_keys: &[Vec<f32>],
2455 indexer_weights: &[f32],
2456 top_k: usize,
2457) -> Vec<usize> {
2458 let n_heads = indexer_q.len();
2459 assert_eq!(indexer_weights.len(), n_heads);
2460 let index_head_dim = indexer_q.first().map_or(0, |q| q.len());
2461 let scale = 1.0 / ((index_head_dim * n_heads) as f32).sqrt();
2462
2463 let mut scored: Vec<(usize, f32)> = indexer_keys
2464 .iter()
2465 .enumerate()
2466 .map(|(j, k)| {
2467 let score: f32 = indexer_q
2468 .iter()
2469 .zip(indexer_weights.iter())
2470 .map(|(q, w)| {
2471 let dot: f32 = q.iter().zip(k.iter()).map(|(a, b)| a * b).sum();
2472 dot.max(0.0) * w * scale
2473 })
2474 .sum();
2475 (j, score)
2476 })
2477 .collect();
2478
2479 let keep = top_k.min(scored.len());
2480 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
2481 let mut kept: Vec<usize> = scored.into_iter().take(keep).map(|(j, _)| j).collect();
2482 kept.sort_unstable();
2483 kept
2484}
2485
2486#[allow(clippy::too_many_arguments)]
2492pub fn causal_mla_attention_sparse(
2493 q: &[f32],
2494 k_cache: &[f32],
2495 v_cache: &[f32],
2496 n_heads: usize,
2497 qk_head_dim: usize,
2498 v_head_dim: usize,
2499 seq_len: usize,
2500 visible: &[usize],
2501) -> Vec<f32> {
2502 mla_attention_inner(
2503 q,
2504 k_cache,
2505 v_cache,
2506 n_heads,
2507 qk_head_dim,
2508 v_head_dim,
2509 seq_len,
2510 Some(visible),
2511 None,
2512 )
2513}
2514
2515#[cfg(test)]
2516mod tests {
2517
2518 #[test]
2524 fn an_mla_sink_removes_weight_from_the_real_keys_instead_of_moving_it() {
2525 let (n_heads, qk, vd, seq) = (2, 2, 2, 2);
2526 let q = vec![1.0, 0.0, 0.0, 1.0];
2527 let k = vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0];
2528 let v = vec![4.0, 8.0, 1.0, 2.0, 4.0, 8.0, 1.0, 2.0];
2529
2530 let plain = super::causal_mla_attention(&q, &k, &v, n_heads, qk, vd, seq);
2531 let none = super::causal_mla_attention_sinks(&q, &k, &v, n_heads, qk, vd, seq, None);
2532 assert_eq!(plain, none, "no sink must be exactly the old path");
2533
2534 let big = super::causal_mla_attention_sinks(
2536 &q,
2537 &k,
2538 &v,
2539 n_heads,
2540 qk,
2541 vd,
2542 seq,
2543 Some(&[40.0, 40.0]),
2544 );
2545 for (b, p) in big.iter().zip(plain.iter()) {
2546 assert!(b.abs() < 1e-6, "a dominant sink leaves ~0, got {b} vs {p}");
2547 }
2548
2549 let tiny = super::causal_mla_attention_sinks(
2551 &q,
2552 &k,
2553 &v,
2554 n_heads,
2555 qk,
2556 vd,
2557 seq,
2558 Some(&[-40.0, -40.0]),
2559 );
2560 for (s, p) in tiny.iter().zip(plain.iter()) {
2561 assert!((s - p).abs() < 1e-5, "negligible sink: {s} vs {p}");
2562 }
2563 }
2564
2565 #[test]
2569 fn each_head_gets_its_own_mla_sink() {
2570 let (n_heads, qk, vd, seq) = (2, 1, 1, 1);
2571 let q = vec![1.0, 1.0];
2572 let k = vec![1.0, 1.0];
2573 let v = vec![5.0, 5.0];
2574
2575 let out = super::causal_mla_attention_sinks(
2576 &q,
2577 &k,
2578 &v,
2579 n_heads,
2580 qk,
2581 vd,
2582 seq,
2583 Some(&[40.0, -40.0]),
2584 );
2585 assert!(out[0].abs() < 1e-6, "head 0 declined: {}", out[0]);
2586 assert!(
2587 (out[1] - 5.0).abs() < 1e-5,
2588 "head 1 attended normally: {}",
2589 out[1]
2590 );
2591 }
2592
2593 #[test]
2598 fn the_mla_sink_logit_is_not_scaled_by_the_head_width() {
2599 let sink = 0.0f32;
2602 let mut outs = Vec::new();
2603 for qk in [1usize, 4, 16] {
2604 let q = vec![0.0; qk];
2605 let k = vec![0.0; qk];
2606 let v = vec![10.0];
2607 outs.push(super::causal_mla_attention_sinks(&q, &k, &v, 1, qk, 1, 1, Some(&[sink]))[0]);
2608 }
2609 for o in &outs {
2611 assert!((o - 5.0).abs() < 1e-5, "expected 5.0, got {o}");
2612 }
2613 }
2614
2615 #[test]
2619 fn the_sparse_mla_path_honours_a_sink_over_the_selected_positions() {
2620 let (n_heads, qk, vd, seq) = (1, 1, 1, 3);
2621 let q = vec![1.0];
2622 let k = vec![1.0, 1.0, 1.0];
2623 let v = vec![2.0, 4.0, 6.0];
2624 let visible = [0usize, 2];
2625
2626 let plain = super::causal_mla_attention_sparse(&q, &k, &v, n_heads, qk, vd, seq, &visible);
2627 let none = super::causal_mla_attention_sparse_sinks(
2628 &q, &k, &v, n_heads, qk, vd, seq, &visible, None,
2629 );
2630 assert_eq!(plain, none);
2631 assert!((plain[0] - 4.0).abs() < 1e-5, "mean of 2 and 6");
2632
2633 let sunk = super::causal_mla_attention_sparse_sinks(
2634 &q,
2635 &k,
2636 &v,
2637 n_heads,
2638 qk,
2639 vd,
2640 seq,
2641 &visible,
2642 Some(&[40.0]),
2643 );
2644 assert!(sunk[0].abs() < 1e-6, "a dominant sink leaves ~0");
2645 }
2646 #[test]
2647 fn prefill_shared_kv_matches_per_query_reference() {
2648 let n_heads = 6;
2654 let n_kv_heads = 2;
2655 let head_dim = 16;
2656 let n_q = 19;
2657 let kv_prefix = 5;
2658 let kv_len = kv_prefix + n_q;
2659 let q_stride = n_heads * head_dim;
2660 let kv_stride = n_kv_heads * head_dim;
2661
2662 let q: Vec<f32> = (0..n_q * q_stride)
2663 .map(|i| ((i as f32) * 0.013 - 0.7).sin() * 1.3)
2664 .collect();
2665 let k_cache: Vec<f32> = (0..kv_len * kv_stride)
2666 .map(|i| ((i as f32) * 0.017 - 0.3).cos() * 1.1)
2667 .collect();
2668 let v_cache: Vec<f32> = (0..kv_len * kv_stride)
2669 .map(|i| ((i as f32) * 0.011 + 0.2).sin() * 0.9)
2670 .collect();
2671
2672 for softcap in [None, Some(30.0)] {
2673 let got = super::causal_gqa_attention_prefill_shared_kv(
2674 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, n_q, kv_prefix, softcap,
2675 );
2676 assert_eq!(got.len(), n_q * q_stride);
2677 for b in 0..n_q {
2678 let causal_len = kv_prefix + b + 1;
2679 let want = super::causal_gqa_attention_softcap(
2680 &q[b * q_stride..(b + 1) * q_stride],
2681 &k_cache[..causal_len * kv_stride],
2682 &v_cache[..causal_len * kv_stride],
2683 n_heads,
2684 n_kv_heads,
2685 head_dim,
2686 causal_len,
2687 softcap,
2688 );
2689 for (i, (g, w)) in got[b * q_stride..(b + 1) * q_stride]
2690 .iter()
2691 .zip(want.iter())
2692 .enumerate()
2693 {
2694 assert!(
2695 (g - w).abs() < 1e-5,
2696 "softcap {softcap:?} query {b} slot {i}: blocked {g} vs online {w}"
2697 );
2698 }
2699 }
2700 }
2701 }
2702
2703 #[test]
2704 fn windowed_prefill_shared_kv_matches_the_per_query_windowed_reference() {
2705 let n_heads = 6;
2713 let n_kv_heads = 2;
2714 let head_dim = 16;
2715 let n_q = 19;
2716 let kv_prefix = 5;
2717 let kv_len = kv_prefix + n_q;
2718 let q_stride = n_heads * head_dim;
2719 let kv_stride = n_kv_heads * head_dim;
2720
2721 let q: Vec<f32> = (0..n_q * q_stride)
2722 .map(|i| ((i as f32) * 0.013 - 0.7).sin() * 1.3)
2723 .collect();
2724 let k_cache: Vec<f32> = (0..kv_len * kv_stride)
2725 .map(|i| ((i as f32) * 0.017 - 0.3).cos() * 1.1)
2726 .collect();
2727 let v_cache: Vec<f32> = (0..kv_len * kv_stride)
2728 .map(|i| ((i as f32) * 0.011 + 0.2).sin() * 0.9)
2729 .collect();
2730
2731 for window in [1usize, 3, 7, kv_prefix, kv_len, kv_len + 8] {
2732 for softcap in [None, Some(30.0)] {
2733 let got = super::causal_gqa_attention_prefill_shared_kv_windowed(
2734 &q,
2735 &k_cache,
2736 &v_cache,
2737 n_heads,
2738 n_kv_heads,
2739 head_dim,
2740 n_q,
2741 kv_prefix,
2742 softcap,
2743 Some(window),
2744 );
2745 assert_eq!(got.len(), n_q * q_stride);
2746 for b in 0..n_q {
2747 let causal_len = kv_prefix + b + 1;
2748 let want = super::causal_gqa_attention_windowed_softcap(
2749 &q[b * q_stride..(b + 1) * q_stride],
2750 &k_cache[..causal_len * kv_stride],
2751 &v_cache[..causal_len * kv_stride],
2752 n_heads,
2753 n_kv_heads,
2754 head_dim,
2755 causal_len,
2756 window,
2757 softcap,
2758 );
2759 for (i, (g, w)) in got[b * q_stride..(b + 1) * q_stride]
2760 .iter()
2761 .zip(want.iter())
2762 .enumerate()
2763 {
2764 assert!(
2765 (g - w).abs() < 1e-5,
2766 "window {window} softcap {softcap:?} query {b} slot {i}: \
2767 blocked {g} vs per-query {w}"
2768 );
2769 }
2770 }
2771 }
2772 }
2773
2774 let windowed = super::causal_gqa_attention_prefill_shared_kv_windowed(
2776 &q,
2777 &k_cache,
2778 &v_cache,
2779 n_heads,
2780 n_kv_heads,
2781 head_dim,
2782 n_q,
2783 kv_prefix,
2784 None,
2785 Some(kv_len + 8),
2786 );
2787 let full = super::causal_gqa_attention_prefill_shared_kv(
2788 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, n_q, kv_prefix, None,
2789 );
2790 assert_eq!(windowed, full);
2791 }
2792
2793 #[allow(clippy::too_many_arguments)]
2804 fn prefill_query_outer_reference(
2805 q: &[f32],
2806 k_cache: &[f32],
2807 v_cache: &[f32],
2808 n_heads: usize,
2809 n_kv_heads: usize,
2810 head_dim: usize,
2811 n_q: usize,
2812 kv_prefix: usize,
2813 attn_softcap: Option<f32>,
2814 window: Option<usize>,
2815 ) -> Vec<f32> {
2816 let q_stride = n_heads * head_dim;
2817 let group_size = n_heads / n_kv_heads.max(1);
2818 let scale = 1.0 / (head_dim as f32).sqrt();
2819 let softcap = attn_softcap.filter(|&c| c > 0.0);
2820 let mut out = vec![0f32; n_q * q_stride];
2821 let mut acc = vec![0f32; head_dim];
2822 for h in 0..n_heads {
2823 let kv_h = h / group_size.max(1);
2824 for b in 0..n_q {
2825 let causal_len = kv_prefix + b + 1;
2826 let t_start = match window {
2827 Some(w) => causal_len.saturating_sub(w),
2828 None => 0,
2829 };
2830 let q_h = &q[b * q_stride + h * head_dim..][..head_dim];
2831 let mut scores = vec![0f32; causal_len - t_start];
2832 for (i, s) in scores.iter_mut().enumerate() {
2833 let base = ((t_start + i) * n_kv_heads + kv_h) * head_dim;
2834 let mut v = super::dot_f32(q_h, &k_cache[base..base + head_dim]) * scale;
2835 if let Some(sc) = softcap {
2836 v = sc * (v / sc).tanh();
2837 }
2838 *s = v;
2839 }
2840 let l = super::softmax_row_exp_sum(&mut scores);
2841 acc.fill(0.0);
2842 for (i, &p) in scores.iter().enumerate() {
2843 let base = ((t_start + i) * n_kv_heads + kv_h) * head_dim;
2844 super::axpy(&mut acc, &v_cache[base..base + head_dim], p);
2845 }
2846 if l > 0.0 {
2847 super::scale_inplace(&mut acc, 1.0 / l);
2848 }
2849 out[b * q_stride + h * head_dim..][..head_dim].copy_from_slice(&acc);
2850 }
2851 }
2852 out
2853 }
2854
2855 #[test]
2856 fn position_outer_prefill_is_bit_identical_to_the_query_outer_form() {
2857 let n_heads = 6;
2862 let n_kv_heads = 2;
2863 let head_dim = 16;
2864 let n_q = 19;
2865 let kv_prefix = 5;
2866 let kv_len = kv_prefix + n_q;
2867 let q_stride = n_heads * head_dim;
2868 let kv_stride = n_kv_heads * head_dim;
2869
2870 let q: Vec<f32> = (0..n_q * q_stride)
2871 .map(|i| ((i as f32) * 0.013 - 0.7).sin() * 1.3)
2872 .collect();
2873 let k_cache: Vec<f32> = (0..kv_len * kv_stride)
2874 .map(|i| ((i as f32) * 0.017 - 0.3).cos() * 1.1)
2875 .collect();
2876 let v_cache: Vec<f32> = (0..kv_len * kv_stride)
2877 .map(|i| ((i as f32) * 0.011 + 0.2).sin() * 0.9)
2878 .collect();
2879
2880 for window in [None, Some(1), Some(3), Some(8), Some(9), Some(kv_len + 4)] {
2881 for softcap in [None, Some(30.0)] {
2882 let got = super::causal_gqa_attention_prefill_shared_kv_windowed(
2883 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, n_q, kv_prefix, softcap,
2884 window,
2885 );
2886 let want = prefill_query_outer_reference(
2887 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, n_q, kv_prefix, softcap,
2888 window,
2889 );
2890 assert_eq!(got, want, "window {window:?} softcap {softcap:?}");
2891 }
2892 }
2893 }
2894
2895 #[test]
2896 fn tiled_prefill_gemm_is_bit_identical_across_awkward_shapes() {
2897 let shapes = [
2912 (4usize, 4usize, 64usize),
2913 (6, 2, 64),
2914 (5, 1, 80),
2915 (3, 3, 128),
2916 (2, 1, 256),
2917 ];
2918 let batches = [(19usize, 5usize), (8, 0), (3, 7), (16, 1), (7, 0)];
2919 for &(n_heads, n_kv_heads, head_dim) in &shapes {
2920 let q_stride = n_heads * head_dim;
2921 let kv_stride = n_kv_heads * head_dim;
2922 for &(n_q, kv_prefix) in &batches {
2923 let kv_len = kv_prefix + n_q;
2924 let q: Vec<f32> = (0..n_q * q_stride)
2925 .map(|i| ((i as f32) * 0.013 - 0.7).sin() * 1.3)
2926 .collect();
2927 let k_cache: Vec<f32> = (0..kv_len * kv_stride)
2928 .map(|i| ((i as f32) * 0.017 - 0.3).cos() * 1.1)
2929 .collect();
2930 let v_cache: Vec<f32> = (0..kv_len * kv_stride)
2931 .map(|i| ((i as f32) * 0.011 + 0.2).sin() * 0.9)
2932 .collect();
2933 for window in [None, Some(2), Some(5), Some(9), Some(kv_len + 3)] {
2934 for softcap in [None, Some(30.0)] {
2935 let got = super::causal_gqa_attention_prefill_shared_kv_windowed(
2936 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, n_q, kv_prefix,
2937 softcap, window,
2938 );
2939 let want = prefill_query_outer_reference(
2940 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, n_q, kv_prefix,
2941 softcap, window,
2942 );
2943 assert_eq!(
2944 got, want,
2945 "heads {n_heads}/{n_kv_heads} head_dim {head_dim} n_q {n_q} \
2946 kv_prefix {kv_prefix} window {window:?} softcap {softcap:?}"
2947 );
2948 }
2949 }
2950 }
2951 }
2952 }
2953
2954 #[test]
2968 fn vectorised_softmax_row_matches_the_scalar_libm_form() {
2969 fn libm_reference(x: &[f32]) -> (Vec<f32>, f32) {
2970 let m = x.iter().fold(f32::NEG_INFINITY, |a, &s| a.max(s));
2971 let out: Vec<f32> = x.iter().map(|s| (s - m).exp()).collect();
2972 let mut l = 0f32;
2973 for &e in out.iter() {
2974 l += e;
2975 }
2976 (out, l)
2977 }
2978
2979 for spread in [1.0f32, 8.0, 200.0, 0.0] {
2982 for n in (0..=17).chain([31, 32, 33, 64, 127, 512]) {
2983 let row: Vec<f32> = (0..n)
2984 .map(|i| ((i as f32) * 0.37 - 1.1).sin() * spread)
2985 .collect();
2986 let (want, want_l) = libm_reference(&row);
2987
2988 let mut got = row.clone();
2989 let got_l = super::softmax_row_exp_sum(&mut got);
2990
2991 for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() {
2992 assert!(
2993 (g - w).abs() <= 1e-6 * w + 1e-30,
2994 "spread {spread} n {n} slot {i}: vector {g} vs libm {w}"
2995 );
2996 }
2997 assert!(
2998 (got_l - want_l).abs() <= 1e-5 * want_l.max(1.0),
2999 "spread {spread} n {n}: sum {got_l} vs libm {want_l}"
3000 );
3001 if n == 0 {
3002 assert_eq!(got_l, 0.0, "an empty visible range normalises to nothing");
3003 }
3004 if spread == 0.0 && n > 0 {
3005 for (i, g) in got.iter().enumerate() {
3010 assert_eq!(*g, 1.0, "n {n} slot {i}: exp(0) must be exact");
3011 }
3012 }
3013 }
3014 }
3015 }
3016
3017 #[test]
3033 fn the_vectorised_softmax_is_no_less_accurate_than_the_scalar_one() {
3034 for spread in [1.0f64, 6.0, 20.0] {
3035 for n in [64usize, 253, 512] {
3036 let row: Vec<f64> = (0..n)
3037 .map(|i| ((i as f64) * 0.37 - 1.1).sin() * spread)
3038 .collect();
3039
3040 let m64 = row.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
3042 let truth: Vec<f64> = row.iter().map(|s| (s - m64).exp()).collect();
3043 let truth_l: f64 = truth.iter().sum();
3044
3045 let f32_row: Vec<f32> = row.iter().map(|&s| s as f32).collect();
3046
3047 let mut vector = f32_row.clone();
3048 let vector_l = super::softmax_row_exp_sum(&mut vector);
3049 let mut scalar = f32_row.clone();
3050 let scalar_l = super::softmax_row_exp_sum_scalar(&mut scalar);
3051
3052 let worst = |got: &[f32]| -> f64 {
3053 got.iter()
3054 .zip(truth.iter())
3055 .map(|(&g, &t)| ((g as f64) - t).abs() / t)
3056 .fold(0.0, f64::max)
3057 };
3058 let (ev, es) = (worst(&vector), worst(&scalar));
3059 let lv = ((vector_l as f64) - truth_l).abs() / truth_l;
3060 let ls = ((scalar_l as f64) - truth_l).abs() / truth_l;
3061 let eps = f64::from(f32::EPSILON);
3062
3063 assert!(
3069 ev <= 2.0 * es.max(eps) && ev <= 64.0 * eps,
3070 "spread {spread} n {n}: vector probabilities err {ev:e} \
3071 against scalar {es:e}"
3072 );
3073
3074 assert!(
3078 lv <= ls.max(eps),
3079 "spread {spread} n {n}: vector normaliser err {lv:e} \
3080 against scalar {ls:e}"
3081 );
3082 }
3083 }
3084 }
3085
3086 #[test]
3090 fn softmax_row_finds_its_maximum_in_every_lane_position() {
3091 for n in 1usize..=20 {
3092 for peak in 0..n {
3093 let mut row: Vec<f32> = (0..n).map(|i| -(i as f32) - 3.0).collect();
3094 row[peak] = 12.5;
3095 let l = super::softmax_row_exp_sum(&mut row);
3096 assert_eq!(row[peak], 1.0, "n {n} peak {peak}: the max term is exp(0)");
3097 for (i, &p) in row.iter().enumerate() {
3098 assert!(p <= 1.0, "n {n} peak {peak} slot {i}: {p} exceeds the max");
3099 }
3100 assert!(l >= 1.0, "n {n} peak {peak}: sum {l} must include the max");
3101 }
3102 }
3103 }
3104
3105 use super::*;
3106
3107 #[test]
3108 fn simd_dot_f32_matches_scalar_across_lengths() {
3109 for n in [1usize, 3, 4, 7, 8, 15, 16, 63, 128, 129] {
3111 let a: Vec<f32> = (0..n).map(|i| ((i as f32) * 0.31 - 2.0).sin()).collect();
3112 let b: Vec<f32> = (0..n).map(|i| ((i as f32) * 0.17 + 1.0).cos()).collect();
3113 let simd = dot_f32(&a, &b);
3114 let scalar: f32 = a.iter().zip(&b).map(|(x, y)| x * y).sum();
3115 assert!(
3116 (simd - scalar).abs() <= 1e-4 * scalar.abs().max(1.0),
3117 "n={n} simd={simd} scalar={scalar}"
3118 );
3119 }
3120 }
3121
3122 #[test]
3123 fn rope_preserves_vector_norm() {
3124 let mut v = vec![1.0, 2.0, 3.0, 4.0];
3125 let norm_before: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3126 apply_rope(&mut v, 5, 10000.0);
3127 let norm_after: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3128 assert!(
3129 (norm_before - norm_after).abs() < 1e-4,
3130 "RoPE is a rotation and must preserve norm"
3131 );
3132 }
3133
3134 #[test]
3135 fn rope_back_inverts_rope() {
3136 let original = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3137 let mut v = original.clone();
3138 apply_rope(&mut v, 11, 10000.0);
3139 apply_rope_back(&mut v, 11, 10000.0);
3140 for (a, b) in v.iter().zip(original.iter()) {
3141 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
3142 }
3143 }
3144
3145 #[test]
3146 fn rope_interleaved_back_inverts_interleaved() {
3147 let original = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3148 let mut v = original.clone();
3149 apply_rope_interleaved(&mut v, 11, 10000.0);
3150 apply_rope_interleaved_back(&mut v, 11, 10000.0);
3151 for (a, b) in v.iter().zip(original.iter()) {
3152 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
3153 }
3154 }
3155
3156 #[test]
3157 fn rope_at_position_zero_is_identity() {
3158 let mut v = vec![1.0, 2.0, 3.0, 4.0];
3159 let original = v.clone();
3160 apply_rope(&mut v, 0, 10000.0);
3161 for (a, b) in v.iter().zip(original.iter()) {
3162 assert!((a - b).abs() < 1e-5);
3163 }
3164 }
3165
3166 #[test]
3167 fn rope_with_all_ones_freq_factors_matches_plain_rope() {
3168 let mut with_factors = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3169 let mut plain = with_factors.clone();
3170 let ones = vec![1.0; 3];
3171 apply_rope_with_freq_factors(&mut with_factors, 7, 10000.0, &ones);
3172 apply_rope(&mut plain, 7, 10000.0);
3173 for (a, b) in with_factors.iter().zip(plain.iter()) {
3174 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
3175 }
3176 }
3177
3178 #[test]
3179 fn rope_with_freq_factors_diverges_from_plain_rope_when_factors_are_not_one() {
3180 let mut with_factors = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3181 let mut plain = with_factors.clone();
3182 let factors = vec![0.5, 2.0, 1.0];
3183 apply_rope_with_freq_factors(&mut with_factors, 7, 10000.0, &factors);
3184 apply_rope(&mut plain, 7, 10000.0);
3185 let differs = with_factors
3186 .iter()
3187 .zip(plain.iter())
3188 .any(|(a, b)| (a - b).abs() > 1e-4);
3189 assert!(differs, "non-1.0 freq_factors must change the rotation");
3190 }
3191
3192 #[test]
3193 fn rope_with_freq_factors_preserves_vector_norm() {
3194 let mut v = vec![1.0, 2.0, 3.0, 4.0];
3195 let norm_before: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3196 apply_rope_with_freq_factors(&mut v, 5, 10000.0, &[0.8, 1.3]);
3197 let norm_after: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3198 assert!((norm_before - norm_after).abs() < 1e-4);
3199 }
3200
3201 #[test]
3215 fn yarn_high_is_clamped_to_rotary_dim_minus_one_not_half_minus_one() {
3216 let scaling = YarnScaling::new(8.0, 131_072);
3217 let (low, high) = yarn_correction_range(scaling, 64, 10_000.0);
3218 assert!((low - 22.0).abs() < 1e-9, "low was {low}");
3219 assert!((high - 35.0).abs() < 1e-9, "high was {high}");
3220
3221 let factors = yarn_freq_factors(scaling, 64, 10_000.0);
3222 let last = factors[31];
3223 let ramp = (31.0 - 22.0) / (35.0 - 22.0);
3224 let want = 1.0 / (ramp / 8.0 + (1.0 - ramp));
3225 assert!(
3226 (last - want).abs() < 1e-4,
3227 "last band divisor {last} must be the reference's {want}"
3228 );
3229 assert!(
3230 (last - 8.0).abs() > 1.0,
3231 "clamping high to rotary_dim/2 - 1 would fully interpolate this band \
3232 (divisor 8.0, the whole factor); got {last}"
3233 );
3234 }
3235
3236 #[test]
3242 fn yarn_freq_factors_match_the_reference_ramp_formula_band_by_band() {
3243 let scaling = YarnScaling::new(8.0, 131_072);
3244 let factors = yarn_freq_factors(scaling, 64, 10_000.0);
3245 assert_eq!(factors.len(), 32, "one divisor per rotation band");
3246 for band in [0usize, 10, 22] {
3247 assert!(
3248 (factors[band] - 1.0).abs() < 1e-6,
3249 "band {band} is at or below low=22 and must be left extrapolated, \
3250 got {}",
3251 factors[band]
3252 );
3253 }
3254 for band in [23usize, 27, 31] {
3255 let ramp = (band as f32 - 22.0) / (35.0 - 22.0);
3256 let want = 1.0 / (ramp / 8.0 + (1.0 - ramp));
3257 assert!(
3258 (factors[band] - want).abs() < 1e-4,
3259 "band {band}: got {}, reference {want}",
3260 factors[band]
3261 );
3262 }
3263 }
3264
3265 #[test]
3276 fn yarn_nudges_a_collapsed_correction_range_instead_of_flooring_the_gap_at_one() {
3277 let scaling = YarnScaling {
3278 beta_slow: 32.0,
3279 truncate: false,
3280 ..YarnScaling::new(8.0, 131_072)
3281 };
3282 let (low, high) = yarn_correction_range(scaling, 64, 10_000.0);
3283 assert!((low - 22.513_44).abs() < 1e-4, "low was {low}");
3285 assert!(
3286 (high - low - 0.001).abs() < 1e-9,
3287 "high must be low + 0.001, got {high}"
3288 );
3289
3290 let factors = yarn_freq_factors(scaling, 64, 10_000.0);
3291 assert!(
3292 (factors[22] - 1.0).abs() < 1e-6,
3293 "band below the step must be untouched, got {}",
3294 factors[22]
3295 );
3296 assert!(
3297 (factors[23] - 8.0).abs() < 1e-4,
3298 "band above the step must take the whole factor (a gap of 1 would \
3299 give 1.7414); got {}",
3300 factors[23]
3301 );
3302 }
3303
3304 #[test]
3309 fn yarn_with_a_factor_of_one_leaves_every_band_untouched() {
3310 let factors = yarn_freq_factors(YarnScaling::new(1.0, 4096), 32, 10_000.0);
3311 for (band, f) in factors.iter().enumerate() {
3312 assert!((f - 1.0).abs() < 1e-6, "band {band} moved to {f}");
3313 }
3314 }
3315
3316 #[test]
3323 fn yarn_divisors_reproduce_the_references_rewritten_frequencies() {
3324 let scaling = YarnScaling::new(8.0, 131_072);
3325 let factors = yarn_freq_factors(scaling, 64, 10_000.0);
3326
3327 let band = 31usize;
3328 let pos = 1024usize;
3329 let mut v = vec![0.0f32; 64];
3330 v[band] = 1.0;
3331 apply_rope_with_freq_factors(&mut v, pos, 10_000.0, &factors);
3332
3333 let ramp = (band as f64 - 22.0) / (35.0 - 22.0);
3334 let inv_freq = 1.0 / 10_000f64.powf((2 * band) as f64 / 64.0);
3335 let inv_freq_new = inv_freq * (ramp / 8.0 + (1.0 - ramp));
3336 let angle = pos as f64 * inv_freq_new;
3337 assert!(
3338 (v[band] as f64 - angle.cos()).abs() < 1e-5,
3339 "cos: {} vs {}",
3340 v[band],
3341 angle.cos()
3342 );
3343 assert!(
3344 (v[band + 32] as f64 - angle.sin()).abs() < 1e-5,
3345 "sin: {} vs {}",
3346 v[band + 32],
3347 angle.sin()
3348 );
3349 }
3350
3351 #[test]
3358 fn proportional_freq_factors_respace_frequencies_over_the_full_head() {
3359 let factors = proportional_freq_factors(128, 96, 10_000.0);
3360 assert_eq!(factors.len(), 48, "one divisor per rotated band");
3361 assert!((factors[0] - 1.0).abs() < 1e-6, "band 0 is 1/1");
3362 assert!(
3364 (factors[1] - 0.953_161_9).abs() < 1e-5,
3365 "band 1 was {}",
3366 factors[1]
3367 );
3368 assert!(
3370 (factors[47] - 0.104_913_97).abs() < 1e-5,
3371 "last band was {}",
3372 factors[47]
3373 );
3374 }
3375
3376 #[test]
3380 fn proportional_freq_factors_are_all_ones_when_the_whole_head_rotates() {
3381 for f in proportional_freq_factors(128, 128, 500_000.0) {
3382 assert!((f - 1.0).abs() < 1e-6, "full-width band moved to {f}");
3383 }
3384 }
3385
3386 #[test]
3387 fn rope_interleaved_preserves_vector_norm() {
3388 let mut v = vec![1.0, 2.0, 3.0, 4.0];
3389 let norm_before: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3390 apply_rope_interleaved(&mut v, 5, 10000.0);
3391 let norm_after: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3392 assert!(
3393 (norm_before - norm_after).abs() < 1e-4,
3394 "RoPE is a rotation and must preserve norm"
3395 );
3396 }
3397
3398 #[test]
3399 fn rope_interleaved_at_position_zero_is_identity() {
3400 let mut v = vec![1.0, 2.0, 3.0, 4.0];
3401 let original = v.clone();
3402 apply_rope_interleaved(&mut v, 0, 10000.0);
3403 for (a, b) in v.iter().zip(original.iter()) {
3404 assert!((a - b).abs() < 1e-5);
3405 }
3406 }
3407
3408 #[test]
3409 fn rope_interleaved_rotates_adjacent_pairs_not_split_halves() {
3410 let mut interleaved = vec![1.0, 0.0, 0.0, 1.0];
3417 let mut split_half = interleaved.clone();
3418 apply_rope_interleaved(&mut interleaved, 3, 10000.0);
3419 apply_rope(&mut split_half, 3, 10000.0);
3420 let differs = interleaved
3425 .iter()
3426 .zip(split_half.iter())
3427 .any(|(a, b)| (a - b).abs() > 1e-4);
3428 assert!(differs, "the two RoPE conventions must not coincide here");
3429 }
3430
3431 #[test]
3432 fn rope_interleaved_with_all_ones_freq_factors_matches_plain_interleaved() {
3433 let mut with_factors = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3434 let mut plain = with_factors.clone();
3435 let ones = vec![1.0; 3];
3436 apply_rope_interleaved_with_freq_factors(&mut with_factors, 7, 10000.0, &ones);
3437 apply_rope_interleaved(&mut plain, 7, 10000.0);
3438 for (a, b) in with_factors.iter().zip(plain.iter()) {
3439 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
3440 }
3441 }
3442
3443 #[test]
3444 fn rope_interleaved_with_freq_factors_diverges_when_factors_are_not_one() {
3445 let mut with_factors = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3446 let mut plain = with_factors.clone();
3447 let factors = vec![0.5, 2.0, 1.0];
3448 apply_rope_interleaved_with_freq_factors(&mut with_factors, 7, 10000.0, &factors);
3449 apply_rope_interleaved(&mut plain, 7, 10000.0);
3450 let differs = with_factors
3451 .iter()
3452 .zip(plain.iter())
3453 .any(|(a, b)| (a - b).abs() > 1e-4);
3454 assert!(differs, "non-1.0 freq_factors must change the rotation");
3455 }
3456
3457 #[test]
3458 fn rope_interleaved_with_freq_factors_preserves_vector_norm() {
3459 let mut v = vec![1.0, 2.0, 3.0, 4.0];
3460 let norm_before: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3461 apply_rope_interleaved_with_freq_factors(&mut v, 5, 10000.0, &[0.8, 1.3]);
3462 let norm_after: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3463 assert!((norm_before - norm_after).abs() < 1e-4);
3464 }
3465
3466 #[test]
3467 fn attention_with_single_position_returns_that_value() {
3468 let q = vec![1.0, 0.0]; let k_cache = vec![0.5, 0.5]; let v_cache = vec![9.0, -3.0];
3473 let out = causal_gqa_attention(&q, &k_cache, &v_cache, 1, 1, 2, 1);
3474 assert!((out[0] - 9.0).abs() < 1e-4);
3475 assert!((out[1] - (-3.0)).abs() < 1e-4);
3476 }
3477
3478 #[test]
3479 fn gqa_group_mapping_shares_kv_heads_correctly() {
3480 let head_dim = 2;
3482 let q = vec![
3483 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, ];
3488 let k_cache = vec![1.0, 0.0, 1.0, 0.0];
3490 let v_cache = vec![100.0, 100.0, 200.0, 200.0];
3491 let out = causal_gqa_attention(&q, &k_cache, &v_cache, 4, 2, head_dim, 1);
3492 assert_eq!(&out[0..2], &[100.0, 100.0][..]);
3494 assert_eq!(&out[2..4], &[100.0, 100.0][..]);
3495 assert_eq!(&out[4..6], &[200.0, 200.0][..]);
3496 assert_eq!(&out[6..8], &[200.0, 200.0][..]);
3497 }
3498
3499 #[test]
3500 fn prefill_gqa_matches_per_token_causal() {
3501 let n_heads = 4;
3502 let n_kv_heads = 2;
3503 let head_dim = 4;
3504 let seq_len = 5;
3505 let q: Vec<f32> = (0..seq_len * n_heads * head_dim)
3506 .map(|i| (i as f32 * 0.13).sin())
3507 .collect();
3508 let k: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3509 .map(|i| (i as f32 * 0.19).cos())
3510 .collect();
3511 let v: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3512 .map(|i| (i as f32 * 0.07).sin())
3513 .collect();
3514 let batched =
3515 causal_gqa_attention_prefill(&q, &k, &v, n_heads, n_kv_heads, head_dim, seq_len);
3516 let q_stride = n_heads * head_dim;
3517 let kv_stride = n_kv_heads * head_dim;
3518 for t in 0..seq_len {
3519 let expect = causal_gqa_attention(
3520 &q[t * q_stride..(t + 1) * q_stride],
3521 &k[..(t + 1) * kv_stride],
3522 &v[..(t + 1) * kv_stride],
3523 n_heads,
3524 n_kv_heads,
3525 head_dim,
3526 t + 1,
3527 );
3528 let got = &batched[t * q_stride..(t + 1) * q_stride];
3529 for (a, b) in got.iter().zip(expect.iter()) {
3530 assert!((a - b).abs() < 1e-5, "t={t}: {a} vs {b}");
3531 }
3532 }
3533 }
3534
3535 #[test]
3536 fn windowed_attention_with_window_covering_full_history_matches_full_causal() {
3537 let n_heads = 2;
3538 let n_kv_heads = 1;
3539 let head_dim = 3;
3540 let seq_len = 4;
3541
3542 let q: Vec<f32> = (0..n_heads * head_dim)
3543 .map(|i| (i as f32 * 0.3).sin())
3544 .collect();
3545 let k_cache: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3546 .map(|i| (i as f32 * 0.17).cos())
3547 .collect();
3548 let v_cache: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3549 .map(|i| (i as f32 * 0.11).sin())
3550 .collect();
3551
3552 let full = causal_gqa_attention(
3553 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, seq_len,
3554 );
3555 let windowed = causal_gqa_attention_windowed(
3556 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, seq_len, seq_len,
3557 );
3558 assert_eq!(full.len(), windowed.len());
3559 for (a, b) in full.iter().zip(windowed.iter()) {
3560 assert_eq!(
3561 a.to_bits(),
3562 b.to_bits(),
3563 "window >= seq_len must be bit-identical to full causal"
3564 );
3565 }
3566 }
3567
3568 #[test]
3569 fn windowed_attention_ignores_positions_outside_the_window() {
3570 let head_dim = 2;
3576 let q = vec![1.0, 0.0];
3577 let k_cache = vec![9.0, -9.0, 0.5, 0.5, -3.0, 7.0]; let v_cache = vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
3579 let out = causal_gqa_attention_windowed(&q, &k_cache, &v_cache, 1, 1, head_dim, 3, 1);
3580 assert!((out[0] - 50.0).abs() < 1e-4);
3581 assert!((out[1] - 60.0).abs() < 1e-4);
3582 }
3583
3584 #[test]
3585 fn paged_attention_matches_contiguous_attention_bit_identical() {
3586 use crate::cache::{PagedKvCache, PagedKvStore};
3587
3588 let n_heads = 4;
3589 let n_kv_heads = 2;
3590 let head_dim = 3;
3591 let block_size = 2;
3592 let seq_len = 5;
3593
3594 let k_flat: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3596 .map(|i| ((i * 7 + 1) % 13) as f32 * 0.1)
3597 .collect();
3598 let v_flat: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3599 .map(|i| ((i * 5 + 3) % 11) as f32 * 0.1)
3600 .collect();
3601 let q: Vec<f32> = (0..n_heads * head_dim)
3602 .map(|i| ((i * 3 + 2) % 9) as f32 * 0.1)
3603 .collect();
3604
3605 let contiguous =
3606 causal_gqa_attention(&q, &k_flat, &v_flat, n_heads, n_kv_heads, head_dim, seq_len);
3607
3608 let mut store = PagedKvStore::new(block_size, seq_len, n_kv_heads, head_dim);
3609 let mut cache = PagedKvCache::new();
3610 for t in 0..seq_len {
3611 let start = t * n_kv_heads * head_dim;
3612 let end = start + n_kv_heads * head_dim;
3613 cache
3614 .push(&mut store, &k_flat[start..end], &v_flat[start..end])
3615 .expect("store sized for seq_len blocks, must not exhaust");
3616 }
3617
3618 let paged = causal_gqa_attention_paged(
3619 &q,
3620 &store,
3621 cache.block_table(),
3622 n_heads,
3623 n_kv_heads,
3624 head_dim,
3625 seq_len,
3626 );
3627
3628 assert_eq!(contiguous.len(), paged.len());
3629 for (a, b) in contiguous.iter().zip(paged.iter()) {
3630 assert_eq!(a.to_bits(), b.to_bits(), "paged path must be bit-identical");
3631 }
3632 }
3633
3634 fn paged_fixture(
3638 seq_len: usize,
3639 n_kv_heads: usize,
3640 head_dim: usize,
3641 block_size: usize,
3642 ) -> (Vec<f32>, Vec<f32>, crate::cache::PagedKvStore, Vec<usize>) {
3643 use crate::cache::{PagedKvCache, PagedKvStore};
3644 let k_flat: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3645 .map(|i| ((i * 7 + 1) % 13) as f32 * 0.1)
3646 .collect();
3647 let v_flat: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3648 .map(|i| ((i * 5 + 3) % 11) as f32 * 0.1)
3649 .collect();
3650 let mut store = PagedKvStore::new(block_size, seq_len, n_kv_heads, head_dim);
3651 let mut cache = PagedKvCache::new();
3652 for t in 0..seq_len {
3653 let start = t * n_kv_heads * head_dim;
3654 let end = start + n_kv_heads * head_dim;
3655 cache
3656 .push(&mut store, &k_flat[start..end], &v_flat[start..end])
3657 .expect("store sized for seq_len blocks, must not exhaust");
3658 }
3659 let table = cache.block_table().to_vec();
3660 (k_flat, v_flat, store, table)
3661 }
3662
3663 #[test]
3674 fn the_paged_sink_term_is_bit_identical_to_the_contiguous_one() {
3675 let (n_heads, n_kv_heads, head_dim, seq_len, block_size) = (4, 2, 3, 5, 2);
3676 let (k_flat, v_flat, store, table) =
3677 paged_fixture(seq_len, n_kv_heads, head_dim, block_size);
3678 let q: Vec<f32> = (0..n_heads * head_dim)
3679 .map(|i| ((i * 3 + 2) % 9) as f32 * 0.1)
3680 .collect();
3681
3682 let sinks = vec![-30.0f32, 0.0, 1.5, 30.0];
3686 let contiguous = causal_gqa_attention_sinks(
3687 &q, &k_flat, &v_flat, n_heads, n_kv_heads, head_dim, seq_len, None, &sinks,
3688 );
3689 let paged = causal_gqa_attention_paged_sinks(
3690 &q,
3691 &store,
3692 &table,
3693 n_heads,
3694 n_kv_heads,
3695 head_dim,
3696 seq_len,
3697 None,
3698 Some(&sinks),
3699 None,
3700 );
3701 assert_eq!(contiguous.len(), paged.len());
3702 for (i, (a, b)) in contiguous.iter().zip(paged.iter()).enumerate() {
3703 assert_eq!(a.to_bits(), b.to_bits(), "element {i}: {a} vs {b}");
3704 }
3705 }
3706
3707 #[test]
3711 fn the_paged_window_arm_is_bit_identical_to_the_contiguous_one() {
3712 let (n_heads, n_kv_heads, head_dim, seq_len, block_size) = (4, 2, 3, 7, 2);
3713 let (k_flat, v_flat, store, table) =
3714 paged_fixture(seq_len, n_kv_heads, head_dim, block_size);
3715 let q: Vec<f32> = (0..n_heads * head_dim)
3716 .map(|i| ((i * 3 + 2) % 9) as f32 * 0.1)
3717 .collect();
3718 let sinks = vec![0.5f32; n_heads];
3719
3720 for window in [1usize, 2, 3, 6, 7, 99] {
3721 let contiguous = causal_gqa_attention_sinks(
3722 &q,
3723 &k_flat,
3724 &v_flat,
3725 n_heads,
3726 n_kv_heads,
3727 head_dim,
3728 seq_len,
3729 Some(window),
3730 &sinks,
3731 );
3732 let paged = causal_gqa_attention_paged_sinks(
3733 &q,
3734 &store,
3735 &table,
3736 n_heads,
3737 n_kv_heads,
3738 head_dim,
3739 seq_len,
3740 Some(window),
3741 Some(&sinks),
3742 None,
3743 );
3744 for (i, (a, b)) in contiguous.iter().zip(paged.iter()).enumerate() {
3745 assert_eq!(
3746 a.to_bits(),
3747 b.to_bits(),
3748 "window {window} element {i}: {a} vs {b}"
3749 );
3750 }
3751 }
3752 }
3753
3754 #[test]
3758 fn the_paged_sink_kernel_without_sinks_or_window_is_the_plain_paged_kernel() {
3759 let (n_heads, n_kv_heads, head_dim, seq_len, block_size) = (4, 2, 3, 5, 2);
3760 let (_k, _v, store, table) = paged_fixture(seq_len, n_kv_heads, head_dim, block_size);
3761 let q: Vec<f32> = (0..n_heads * head_dim)
3762 .map(|i| ((i * 3 + 2) % 9) as f32 * 0.1)
3763 .collect();
3764
3765 let plain =
3766 causal_gqa_attention_paged(&q, &store, &table, n_heads, n_kv_heads, head_dim, seq_len);
3767 let via_sinks = causal_gqa_attention_paged_sinks(
3768 &q, &store, &table, n_heads, n_kv_heads, head_dim, seq_len, None, None, None,
3769 );
3770 for (i, (a, b)) in plain.iter().zip(via_sinks.iter()).enumerate() {
3771 assert_eq!(a.to_bits(), b.to_bits(), "element {i}");
3772 }
3773 }
3774
3775 #[test]
3776 fn mla_attention_with_single_position_returns_that_value() {
3777 let q = vec![1.0, 0.0, 0.0, 0.0, 0.0]; let k_cache = vec![0.2, 0.2, 0.2, 0.2, 0.2]; let v_cache = vec![9.0, -3.0, 1.0]; let out = causal_mla_attention(&q, &k_cache, &v_cache, 1, 5, 3, 1);
3784 assert_eq!(out.len(), 3);
3785 assert!((out[0] - 9.0).abs() < 1e-4);
3786 assert!((out[1] - (-3.0)).abs() < 1e-4);
3787 assert!((out[2] - 1.0).abs() < 1e-4);
3788 }
3789
3790 #[test]
3791 fn mla_attention_every_head_gets_its_own_kv_no_grouping() {
3792 let qk_head_dim = 2;
3796 let v_head_dim = 2;
3797 let q = vec![1.0, 0.0, 1.0, 0.0]; let k_cache = vec![1.0, 0.0, 1.0, 0.0]; let v_cache = vec![100.0, 100.0, 200.0, 200.0];
3800 let out = causal_mla_attention(&q, &k_cache, &v_cache, 2, qk_head_dim, v_head_dim, 1);
3801 assert_eq!(&out[0..2], &[100.0, 100.0][..]);
3802 assert_eq!(&out[2..4], &[200.0, 200.0][..]);
3803 }
3804
3805 #[test]
3806 fn lightning_indexer_topk_keeps_all_positions_when_top_k_covers_them() {
3807 let indexer_q = vec![vec![1.0, 0.0]];
3808 let indexer_keys = vec![vec![1.0, 0.0], vec![0.5, 0.5], vec![0.1, 0.9]];
3809 let indexer_weights = vec![1.0];
3810 let kept = lightning_indexer_topk(&indexer_q, &indexer_keys, &indexer_weights, 10);
3811 assert_eq!(kept, vec![0, 1, 2]);
3812 }
3813
3814 #[test]
3815 fn lightning_indexer_topk_selects_highest_scoring_positions() {
3816 let indexer_q = vec![vec![1.0, 0.0]];
3819 let indexer_keys = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![0.9, 0.1]];
3820 let indexer_weights = vec![1.0];
3821 let kept = lightning_indexer_topk(&indexer_q, &indexer_keys, &indexer_weights, 2);
3822 assert_eq!(kept, vec![0, 2]);
3823 }
3824
3825 #[test]
3826 fn lightning_indexer_topk_relu_zeroes_negative_dot_products() {
3827 let indexer_q = vec![vec![1.0, 0.0]];
3830 let indexer_keys = vec![vec![0.3, 0.0], vec![-1.0, 0.0]];
3831 let indexer_weights = vec![1.0];
3832 let kept = lightning_indexer_topk(&indexer_q, &indexer_keys, &indexer_weights, 1);
3833 assert_eq!(kept, vec![0]);
3834 }
3835
3836 #[test]
3837 fn mla_attention_sparse_with_all_positions_visible_matches_full_causal() {
3838 let qk_head_dim = 3;
3839 let v_head_dim = 2;
3840 let seq_len = 4;
3841 let n_heads = 2;
3842 let q: Vec<f32> = (0..n_heads * qk_head_dim).map(|i| i as f32 * 0.1).collect();
3843 let k_cache: Vec<f32> = (0..seq_len * n_heads * qk_head_dim)
3844 .map(|i| (i as f32 * 0.05).sin())
3845 .collect();
3846 let v_cache: Vec<f32> = (0..seq_len * n_heads * v_head_dim)
3847 .map(|i| (i as f32 * 0.05).cos())
3848 .collect();
3849
3850 let full = causal_mla_attention(
3851 &q,
3852 &k_cache,
3853 &v_cache,
3854 n_heads,
3855 qk_head_dim,
3856 v_head_dim,
3857 seq_len,
3858 );
3859 let visible: Vec<usize> = (0..seq_len).collect();
3860 let sparse = causal_mla_attention_sparse(
3861 &q,
3862 &k_cache,
3863 &v_cache,
3864 n_heads,
3865 qk_head_dim,
3866 v_head_dim,
3867 seq_len,
3868 &visible,
3869 );
3870
3871 assert_eq!(full.len(), sparse.len());
3872 for (a, b) in full.iter().zip(sparse.iter()) {
3873 assert!((a - b).abs() < 1e-6, "full={a} sparse={b}");
3874 }
3875 }
3876
3877 #[test]
3878 fn mla_attention_sparse_ignores_positions_outside_visible_set() {
3879 let qk_head_dim = 2;
3882 let v_head_dim = 1;
3883 let q = vec![1.0, 0.0];
3884 let k_cache = vec![1.0, 0.0, 1.0, 0.0]; let v_cache = vec![5.0, 999.0]; let out = causal_mla_attention_sparse(
3887 &q,
3888 &k_cache,
3889 &v_cache,
3890 1,
3891 qk_head_dim,
3892 v_head_dim,
3893 2,
3894 &[0],
3895 );
3896 assert_eq!(out.len(), 1);
3897 assert!((out[0] - 5.0).abs() < 1e-6);
3898 }
3899
3900 #[test]
3901 fn attn_logit_softcap_changes_output_vs_uncapped() {
3902 let n_heads = 2;
3905 let n_kv_heads = 1;
3906 let head_dim = 4;
3907 let seq_len = 3;
3908 let q: Vec<f32> = (0..n_heads * head_dim)
3909 .map(|i| (i as f32 + 1.0) * 2.5)
3910 .collect();
3911 let k: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3912 .map(|i| (i as f32 * 0.7).sin() * 3.0)
3913 .collect();
3914 let v: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3915 .map(|i| (i as f32 * 0.3).cos())
3916 .collect();
3917 let plain = causal_gqa_attention(&q, &k, &v, n_heads, n_kv_heads, head_dim, seq_len);
3918 let capped = causal_gqa_attention_softcap(
3919 &q,
3920 &k,
3921 &v,
3922 n_heads,
3923 n_kv_heads,
3924 head_dim,
3925 seq_len,
3926 Some(30.0),
3927 );
3928 assert_eq!(plain.len(), capped.len());
3929 let differs = plain
3930 .iter()
3931 .zip(capped.iter())
3932 .any(|(a, b)| (a - b).abs() > 1e-5);
3933 assert!(differs, "softcap must change attention output");
3934 let none =
3936 causal_gqa_attention_softcap(&q, &k, &v, n_heads, n_kv_heads, head_dim, seq_len, None);
3937 for (a, b) in plain.iter().zip(none.iter()) {
3938 assert!((a - b).abs() < 1e-6);
3939 }
3940 }
3941}