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 use rayon::prelude::*;
1044 let q_stride = n_heads * head_dim;
1045 let kv_stride = n_kv_heads * head_dim;
1046 assert_eq!(q.len(), n_q * q_stride);
1047 let kv_len = kv_prefix + n_q;
1048 assert!(k_cache.len() >= kv_len * kv_stride);
1049 assert!(v_cache.len() >= kv_len * kv_stride);
1050
1051 let group_size = n_heads / n_kv_heads.max(1);
1052 let scale = 1.0 / (head_dim as f32).sqrt();
1053 let mut out = vec![0f32; n_q * q_stride];
1054
1055 struct OutPtr(*mut f32);
1062 unsafe impl Send for OutPtr {}
1063 unsafe impl Sync for OutPtr {}
1064 impl OutPtr {
1065 #[inline]
1067 unsafe fn write(&self, off: usize, src: &[f32]) {
1068 std::ptr::copy_nonoverlapping(src.as_ptr(), self.0.add(off), src.len());
1069 }
1070 }
1071
1072 #[derive(Default)]
1078 struct Scratch {
1079 q_tile: Vec<f32>,
1080 scores: Vec<f32>,
1081 acc: Vec<f32>,
1082 }
1083
1084 const Q_BLOCK: usize = 8;
1085 let n_blocks = n_q.div_ceil(Q_BLOCK);
1086 let out_w = OutPtr(out.as_mut_ptr());
1087 let softcap = attn_softcap.filter(|&c| c > 0.0);
1088
1089 (0..n_blocks * n_heads)
1090 .into_par_iter()
1091 .with_min_len(1)
1092 .for_each_init(Scratch::default, |scratch, task| {
1093 let Scratch {
1094 q_tile,
1095 scores,
1096 acc,
1097 } = scratch;
1098 let blk = task / n_heads;
1099 let h = task % n_heads;
1100 let kv_h = h / group_size.max(1);
1101 let b_start = blk * Q_BLOCK;
1102 let b_end = (b_start + Q_BLOCK).min(n_q);
1103 let n_b = b_end - b_start;
1104
1105 let t_hi = kv_prefix + b_end;
1109 let t_lo = match window {
1110 Some(w) => (kv_prefix + b_start + 1).saturating_sub(w),
1111 None => 0,
1112 };
1113 let span = t_hi - t_lo;
1114 let kv_off = t_lo * kv_stride + kv_h * head_dim;
1115
1116 q_tile.clear();
1122 for b in b_start..b_end {
1123 q_tile.extend_from_slice(&q[b * q_stride + h * head_dim..][..head_dim]);
1124 }
1125
1126 scores.resize(n_b * span, 0.0);
1134 qk_tile(
1135 q_tile, n_b, head_dim, k_cache, kv_off, kv_stride, span, scale, scores,
1136 );
1137
1138 let mut norms = [0f32; Q_BLOCK];
1144 for b in b_start..b_end {
1145 let causal_len = kv_prefix + b + 1;
1146 let t_start = match window {
1149 Some(w) => causal_len.saturating_sub(w),
1150 None => 0,
1151 };
1152 let row = &mut scores[(b - b_start) * span..][..span];
1153 let lo = t_start - t_lo;
1154 let hi = causal_len - t_lo;
1155 row[..lo].fill(0.0);
1156 row[hi..].fill(0.0);
1157 let live = &mut row[lo..hi];
1158 if let Some(sc) = softcap {
1159 for s in live.iter_mut() {
1160 *s = sc * (*s / sc).tanh();
1161 }
1162 }
1163 norms[b - b_start] = softmax_row_exp_sum(live);
1164 }
1165
1166 acc.resize(n_b * head_dim, 0.0);
1171 acc.fill(0.0);
1172 pv_tile(scores, n_b, span, v_cache, kv_off, kv_stride, head_dim, acc);
1173
1174 for b in b_start..b_end {
1175 let out_h = &mut acc[(b - b_start) * head_dim..][..head_dim];
1176 let l = norms[b - b_start];
1177 if l > 0.0 {
1178 scale_inplace(out_h, 1.0 / l);
1179 }
1180 unsafe {
1181 out_w.write(b * q_stride + h * head_dim, out_h);
1182 }
1183 }
1184 });
1185
1186 out
1187}
1188
1189#[inline]
1225fn softmax_row_exp_sum(x: &mut [f32]) -> f32 {
1226 if x.is_empty() {
1227 return 0.0;
1231 }
1232 #[cfg(target_arch = "aarch64")]
1233 {
1234 if std::arch::is_aarch64_feature_detected!("neon") {
1235 return unsafe { softmax_row_exp_sum_neon(x) };
1236 }
1237 }
1238 #[cfg(target_arch = "x86_64")]
1239 {
1240 if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
1241 return unsafe { softmax_row_exp_sum_avx2(x) };
1242 }
1243 }
1244 softmax_row_exp_sum_scalar(x)
1245}
1246
1247fn softmax_row_exp_sum_scalar(x: &mut [f32]) -> f32 {
1250 let m = x.iter().fold(f32::NEG_INFINITY, |a, &s| a.max(s));
1251 let mut l = 0f32;
1252 for s in x.iter_mut() {
1253 *s = (*s - m).exp();
1254 l += *s;
1255 }
1256 l
1257}
1258
1259#[cfg(target_arch = "aarch64")]
1282#[target_feature(enable = "neon")]
1283#[inline]
1284unsafe fn expf_neon(x: std::arch::aarch64::float32x4_t) -> std::arch::aarch64::float32x4_t {
1285 use std::arch::aarch64::*;
1286 let x = vmaxq_f32(x, vdupq_n_f32(EXP_MIN_ARG));
1287 let r = vdupq_n_f32(EXP_SHIFT);
1288 let z = vfmaq_f32(r, x, vdupq_n_f32(EXP_LOG2E));
1289 let n = vsubq_f32(z, r);
1290 let b = vfmsq_f32(
1292 vfmsq_f32(x, n, vdupq_n_f32(EXP_LN2_HI)),
1293 n,
1294 vdupq_n_f32(EXP_LN2_LO),
1295 );
1296 let e = vshlq_n_u32::<23>(vreinterpretq_u32_f32(z));
1299 let k = vreinterpretq_f32_u32(vaddq_u32(e, vreinterpretq_u32_f32(vdupq_n_f32(1.0))));
1300 let u = vmulq_f32(b, b);
1301 let j = vfmaq_f32(
1302 vmulq_f32(vdupq_n_f32(EXP_C0), b),
1303 vfmaq_f32(
1304 vfmaq_f32(vdupq_n_f32(EXP_C1), vdupq_n_f32(EXP_C2), b),
1305 vfmaq_f32(vdupq_n_f32(EXP_C3), vdupq_n_f32(EXP_C4), b),
1306 u,
1307 ),
1308 u,
1309 );
1310 vfmaq_f32(k, j, k)
1311}
1312
1313#[cfg(target_arch = "x86_64")]
1316#[target_feature(enable = "avx2,fma")]
1317#[inline]
1318unsafe fn expf_avx2(x: std::arch::x86_64::__m256) -> std::arch::x86_64::__m256 {
1319 use std::arch::x86_64::*;
1320 let x = _mm256_max_ps(x, _mm256_set1_ps(EXP_MIN_ARG));
1321 let r = _mm256_set1_ps(EXP_SHIFT);
1322 let z = _mm256_fmadd_ps(x, _mm256_set1_ps(EXP_LOG2E), r);
1323 let n = _mm256_sub_ps(z, r);
1324 let b = _mm256_fnmadd_ps(
1325 n,
1326 _mm256_set1_ps(EXP_LN2_LO),
1327 _mm256_fnmadd_ps(n, _mm256_set1_ps(EXP_LN2_HI), x),
1328 );
1329 let e = _mm256_slli_epi32::<23>(_mm256_castps_si256(z));
1330 let k = _mm256_castsi256_ps(_mm256_add_epi32(
1331 e,
1332 _mm256_castps_si256(_mm256_set1_ps(1.0)),
1333 ));
1334 let u = _mm256_mul_ps(b, b);
1335 let j = _mm256_fmadd_ps(
1336 _mm256_fmadd_ps(
1337 _mm256_fmadd_ps(_mm256_set1_ps(EXP_C4), b, _mm256_set1_ps(EXP_C3)),
1338 u,
1339 _mm256_fmadd_ps(_mm256_set1_ps(EXP_C2), b, _mm256_set1_ps(EXP_C1)),
1340 ),
1341 u,
1342 _mm256_mul_ps(_mm256_set1_ps(EXP_C0), b),
1343 );
1344 _mm256_fmadd_ps(j, k, k)
1345}
1346
1347#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
1351mod exp_consts {
1352 pub use crate::vexp::*;
1358
1359 pub const EXP_MIN_ARG: f32 = -87.0;
1362}
1363#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
1364use exp_consts::*;
1365
1366#[cfg(target_arch = "aarch64")]
1367#[target_feature(enable = "neon")]
1368unsafe fn softmax_row_exp_sum_neon(x: &mut [f32]) -> f32 {
1369 use std::arch::aarch64::*;
1370 let n = x.len();
1371 let p = x.as_mut_ptr();
1372 let nv = n & !3;
1373
1374 let mut mv = vdupq_n_f32(f32::NEG_INFINITY);
1375 let mut i = 0;
1376 while i < nv {
1377 mv = vmaxq_f32(mv, vld1q_f32(p.add(i)));
1378 i += 4;
1379 }
1380 let mut m = if nv == 0 {
1381 f32::NEG_INFINITY
1382 } else {
1383 vmaxvq_f32(mv)
1384 };
1385 for j in nv..n {
1386 m = m.max(*p.add(j));
1387 }
1388
1389 let mvec = vdupq_n_f32(m);
1390 let mut sv = vdupq_n_f32(0.0);
1391 let mut i = 0;
1392 while i < nv {
1393 let e = expf_neon(vsubq_f32(vld1q_f32(p.add(i)), mvec));
1394 vst1q_f32(p.add(i), e);
1395 sv = vaddq_f32(sv, e);
1396 i += 4;
1397 }
1398 let mut l = vaddvq_f32(sv);
1399 if nv < n {
1400 let mut buf = [0f32; 4];
1405 for (j, slot) in (nv..n).zip(buf.iter_mut()) {
1406 *slot = *p.add(j) - m;
1407 }
1408 vst1q_f32(buf.as_mut_ptr(), expf_neon(vld1q_f32(buf.as_ptr())));
1409 for (j, &e) in (nv..n).zip(buf.iter()) {
1410 *p.add(j) = e;
1411 l += e;
1412 }
1413 }
1414 l
1415}
1416
1417#[cfg(target_arch = "x86_64")]
1418#[target_feature(enable = "avx2,fma")]
1419unsafe fn softmax_row_exp_sum_avx2(x: &mut [f32]) -> f32 {
1420 use std::arch::x86_64::*;
1421 let n = x.len();
1422 let p = x.as_mut_ptr();
1423 let nv = n & !7;
1424
1425 let mut mv = _mm256_set1_ps(f32::NEG_INFINITY);
1426 let mut i = 0;
1427 while i < nv {
1428 mv = _mm256_max_ps(mv, _mm256_loadu_ps(p.add(i)));
1429 i += 8;
1430 }
1431 let mut m = if nv == 0 {
1432 f32::NEG_INFINITY
1433 } else {
1434 let mut lanes = [0f32; 8];
1435 _mm256_storeu_ps(lanes.as_mut_ptr(), mv);
1436 lanes.iter().fold(f32::NEG_INFINITY, |a, &s| a.max(s))
1437 };
1438 for j in nv..n {
1439 m = m.max(*p.add(j));
1440 }
1441
1442 let mvec = _mm256_set1_ps(m);
1443 let mut sv = _mm256_setzero_ps();
1444 let mut i = 0;
1445 while i < nv {
1446 let e = expf_avx2(_mm256_sub_ps(_mm256_loadu_ps(p.add(i)), mvec));
1447 _mm256_storeu_ps(p.add(i), e);
1448 sv = _mm256_add_ps(sv, e);
1449 i += 8;
1450 }
1451 let mut l = hsum256_ps(sv);
1452 if nv < n {
1453 let mut buf = [0f32; 8];
1454 for (j, slot) in (nv..n).zip(buf.iter_mut()) {
1455 *slot = *p.add(j) - m;
1456 }
1457 _mm256_storeu_ps(buf.as_mut_ptr(), expf_avx2(_mm256_loadu_ps(buf.as_ptr())));
1458 for (j, &e) in (nv..n).zip(buf.iter()) {
1459 *p.add(j) = e;
1460 l += e;
1461 }
1462 }
1463 l
1464}
1465
1466#[inline(never)]
1492#[allow(clippy::too_many_arguments)]
1493fn qk_tile(
1494 q_tile: &[f32],
1495 n_b: usize,
1496 head_dim: usize,
1497 k: &[f32],
1498 k_off: usize,
1499 k_stride: usize,
1500 span: usize,
1501 scale: f32,
1502 scores: &mut [f32],
1503) {
1504 debug_assert_eq!(q_tile.len(), n_b * head_dim);
1505 debug_assert_eq!(scores.len(), n_b * span);
1506 #[cfg(target_arch = "aarch64")]
1507 {
1508 if std::arch::is_aarch64_feature_detected!("neon") {
1509 unsafe {
1510 qk_tile_neon(
1511 q_tile, n_b, head_dim, k, k_off, k_stride, span, scale, scores,
1512 )
1513 };
1514 return;
1515 }
1516 }
1517 #[cfg(target_arch = "x86_64")]
1518 {
1519 if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
1520 unsafe {
1521 qk_tile_avx2(
1522 q_tile, n_b, head_dim, k, k_off, k_stride, span, scale, scores,
1523 )
1524 };
1525 return;
1526 }
1527 }
1528 qk_rows(
1529 q_tile,
1530 head_dim,
1531 k,
1532 k_off,
1533 k_stride,
1534 span,
1535 scale,
1536 scores,
1537 0..n_b,
1538 0..span,
1539 );
1540}
1541
1542#[allow(clippy::too_many_arguments)]
1546fn qk_rows(
1547 q_tile: &[f32],
1548 head_dim: usize,
1549 k: &[f32],
1550 k_off: usize,
1551 k_stride: usize,
1552 span: usize,
1553 scale: f32,
1554 scores: &mut [f32],
1555 rows: std::ops::Range<usize>,
1556 cols: std::ops::Range<usize>,
1557) {
1558 for b in rows {
1559 let q_b = &q_tile[b * head_dim..][..head_dim];
1560 for t in cols.clone() {
1561 let k_t = &k[k_off + t * k_stride..][..head_dim];
1562 scores[b * span + t] = dot_f32(q_b, k_t) * scale;
1563 }
1564 }
1565}
1566
1567#[cfg(target_arch = "aarch64")]
1568#[target_feature(enable = "neon")]
1569#[allow(clippy::too_many_arguments)]
1570unsafe fn qk_tile_neon(
1571 q_tile: &[f32],
1572 n_b: usize,
1573 head_dim: usize,
1574 k: &[f32],
1575 k_off: usize,
1576 k_stride: usize,
1577 span: usize,
1578 scale: f32,
1579 scores: &mut [f32],
1580) {
1581 use std::arch::aarch64::*;
1582 let qp = q_tile.as_ptr();
1583 let kp = k.as_ptr().add(k_off);
1584 let sp = scores.as_mut_ptr();
1585 let bt = n_b & !3;
1589 let tt = span & !3;
1590 let dv = head_dim & !3;
1591
1592 let mut t0 = 0;
1596 while t0 < tt {
1597 let k0 = kp.add(t0 * k_stride);
1598 let k1 = k0.add(k_stride);
1599 let k2 = k1.add(k_stride);
1600 let k3 = k2.add(k_stride);
1601 let mut b0 = 0;
1602 while b0 < bt {
1603 let a0 = qp.add(b0 * head_dim);
1604 let a1 = a0.add(head_dim);
1605 let a2 = a1.add(head_dim);
1606 let a3 = a2.add(head_dim);
1607 let z = vdupq_n_f32(0.0);
1608 let (mut c00, mut c01, mut c02, mut c03) = (z, z, z, z);
1610 let (mut c10, mut c11, mut c12, mut c13) = (z, z, z, z);
1611 let (mut c20, mut c21, mut c22, mut c23) = (z, z, z, z);
1612 let (mut c30, mut c31, mut c32, mut c33) = (z, z, z, z);
1613 let mut d = 0;
1614 while d < dv {
1615 let av0 = vld1q_f32(a0.add(d));
1616 let av1 = vld1q_f32(a1.add(d));
1617 let av2 = vld1q_f32(a2.add(d));
1618 let av3 = vld1q_f32(a3.add(d));
1619 let kv0 = vld1q_f32(k0.add(d));
1620 c00 = vfmaq_f32(c00, av0, kv0);
1621 c10 = vfmaq_f32(c10, av1, kv0);
1622 c20 = vfmaq_f32(c20, av2, kv0);
1623 c30 = vfmaq_f32(c30, av3, kv0);
1624 let kv1 = vld1q_f32(k1.add(d));
1625 c01 = vfmaq_f32(c01, av0, kv1);
1626 c11 = vfmaq_f32(c11, av1, kv1);
1627 c21 = vfmaq_f32(c21, av2, kv1);
1628 c31 = vfmaq_f32(c31, av3, kv1);
1629 let kv2 = vld1q_f32(k2.add(d));
1630 c02 = vfmaq_f32(c02, av0, kv2);
1631 c12 = vfmaq_f32(c12, av1, kv2);
1632 c22 = vfmaq_f32(c22, av2, kv2);
1633 c32 = vfmaq_f32(c32, av3, kv2);
1634 let kv3 = vld1q_f32(k3.add(d));
1635 c03 = vfmaq_f32(c03, av0, kv3);
1636 c13 = vfmaq_f32(c13, av1, kv3);
1637 c23 = vfmaq_f32(c23, av2, kv3);
1638 c33 = vfmaq_f32(c33, av3, kv3);
1639 d += 4;
1640 }
1641 let mut r = [
1642 [
1643 vaddvq_f32(c00),
1644 vaddvq_f32(c01),
1645 vaddvq_f32(c02),
1646 vaddvq_f32(c03),
1647 ],
1648 [
1649 vaddvq_f32(c10),
1650 vaddvq_f32(c11),
1651 vaddvq_f32(c12),
1652 vaddvq_f32(c13),
1653 ],
1654 [
1655 vaddvq_f32(c20),
1656 vaddvq_f32(c21),
1657 vaddvq_f32(c22),
1658 vaddvq_f32(c23),
1659 ],
1660 [
1661 vaddvq_f32(c30),
1662 vaddvq_f32(c31),
1663 vaddvq_f32(c32),
1664 vaddvq_f32(c33),
1665 ],
1666 ];
1667 let arow = [a0, a1, a2, a3];
1670 let krow = [k0, k1, k2, k3];
1671 for d in dv..head_dim {
1672 for (i, ai) in arow.iter().enumerate() {
1673 let av = *ai.add(d);
1674 for (j, kj) in krow.iter().enumerate() {
1675 r[i][j] += av * *kj.add(d);
1676 }
1677 }
1678 }
1679 for (i, ri) in r.iter().enumerate() {
1680 for (j, v) in ri.iter().enumerate() {
1681 *sp.add((b0 + i) * span + t0 + j) = v * scale;
1682 }
1683 }
1684 b0 += 4;
1685 }
1686 t0 += 4;
1687 }
1688 qk_rows(
1689 q_tile,
1690 head_dim,
1691 k,
1692 k_off,
1693 k_stride,
1694 span,
1695 scale,
1696 scores,
1697 0..bt,
1698 tt..span,
1699 );
1700 qk_rows(
1701 q_tile,
1702 head_dim,
1703 k,
1704 k_off,
1705 k_stride,
1706 span,
1707 scale,
1708 scores,
1709 bt..n_b,
1710 0..span,
1711 );
1712}
1713
1714#[cfg(target_arch = "x86_64")]
1718#[target_feature(enable = "avx2,fma")]
1719#[allow(clippy::too_many_arguments)]
1720unsafe fn qk_tile_avx2(
1721 q_tile: &[f32],
1722 n_b: usize,
1723 head_dim: usize,
1724 k: &[f32],
1725 k_off: usize,
1726 k_stride: usize,
1727 span: usize,
1728 scale: f32,
1729 scores: &mut [f32],
1730) {
1731 use std::arch::x86_64::*;
1732 let qp = q_tile.as_ptr();
1733 let kp = k.as_ptr().add(k_off);
1734 let sp = scores.as_mut_ptr();
1735 let bt = n_b & !3;
1736 let tt = span & !1;
1737 let dv = head_dim & !7;
1738
1739 let mut t0 = 0;
1740 while t0 < tt {
1741 let k0 = kp.add(t0 * k_stride);
1742 let k1 = k0.add(k_stride);
1743 let mut b0 = 0;
1744 while b0 < bt {
1745 let a0 = qp.add(b0 * head_dim);
1746 let a1 = a0.add(head_dim);
1747 let a2 = a1.add(head_dim);
1748 let a3 = a2.add(head_dim);
1749 let z = _mm256_setzero_ps();
1750 let (mut c00, mut c01) = (z, z);
1751 let (mut c10, mut c11) = (z, z);
1752 let (mut c20, mut c21) = (z, z);
1753 let (mut c30, mut c31) = (z, z);
1754 let mut d = 0;
1755 while d < dv {
1756 let av0 = _mm256_loadu_ps(a0.add(d));
1757 let av1 = _mm256_loadu_ps(a1.add(d));
1758 let av2 = _mm256_loadu_ps(a2.add(d));
1759 let av3 = _mm256_loadu_ps(a3.add(d));
1760 let kv0 = _mm256_loadu_ps(k0.add(d));
1761 c00 = _mm256_fmadd_ps(av0, kv0, c00);
1762 c10 = _mm256_fmadd_ps(av1, kv0, c10);
1763 c20 = _mm256_fmadd_ps(av2, kv0, c20);
1764 c30 = _mm256_fmadd_ps(av3, kv0, c30);
1765 let kv1 = _mm256_loadu_ps(k1.add(d));
1766 c01 = _mm256_fmadd_ps(av0, kv1, c01);
1767 c11 = _mm256_fmadd_ps(av1, kv1, c11);
1768 c21 = _mm256_fmadd_ps(av2, kv1, c21);
1769 c31 = _mm256_fmadd_ps(av3, kv1, c31);
1770 d += 8;
1771 }
1772 let mut r = [
1773 [hsum256_ps(c00), hsum256_ps(c01)],
1774 [hsum256_ps(c10), hsum256_ps(c11)],
1775 [hsum256_ps(c20), hsum256_ps(c21)],
1776 [hsum256_ps(c30), hsum256_ps(c31)],
1777 ];
1778 let arow = [a0, a1, a2, a3];
1779 let krow = [k0, k1];
1780 for d in dv..head_dim {
1781 for (i, ai) in arow.iter().enumerate() {
1782 let av = *ai.add(d);
1783 for (j, kj) in krow.iter().enumerate() {
1784 r[i][j] += av * *kj.add(d);
1785 }
1786 }
1787 }
1788 for (i, ri) in r.iter().enumerate() {
1789 for (j, v) in ri.iter().enumerate() {
1790 *sp.add((b0 + i) * span + t0 + j) = v * scale;
1791 }
1792 }
1793 b0 += 4;
1794 }
1795 t0 += 2;
1796 }
1797 qk_rows(
1798 q_tile,
1799 head_dim,
1800 k,
1801 k_off,
1802 k_stride,
1803 span,
1804 scale,
1805 scores,
1806 0..bt,
1807 tt..span,
1808 );
1809 qk_rows(
1810 q_tile,
1811 head_dim,
1812 k,
1813 k_off,
1814 k_stride,
1815 span,
1816 scale,
1817 scores,
1818 bt..n_b,
1819 0..span,
1820 );
1821}
1822
1823#[inline(never)]
1844#[allow(clippy::too_many_arguments)]
1845fn pv_tile(
1846 p: &[f32],
1847 n_b: usize,
1848 span: usize,
1849 v: &[f32],
1850 v_off: usize,
1851 v_stride: usize,
1852 head_dim: usize,
1853 acc: &mut [f32],
1854) {
1855 debug_assert_eq!(p.len(), n_b * span);
1856 debug_assert_eq!(acc.len(), n_b * head_dim);
1857 #[cfg(target_arch = "aarch64")]
1858 {
1859 if std::arch::is_aarch64_feature_detected!("neon") {
1860 unsafe { pv_tile_neon(p, n_b, span, v, v_off, v_stride, head_dim, acc) };
1861 return;
1862 }
1863 }
1864 #[cfg(target_arch = "x86_64")]
1865 {
1866 if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
1867 unsafe { pv_tile_avx2(p, n_b, span, v, v_off, v_stride, head_dim, acc) };
1868 return;
1869 }
1870 }
1871 pv_rows(p, span, v, v_off, v_stride, head_dim, acc, 0..n_b);
1872}
1873
1874#[allow(clippy::too_many_arguments)]
1879fn pv_rows(
1880 p: &[f32],
1881 span: usize,
1882 v: &[f32],
1883 v_off: usize,
1884 v_stride: usize,
1885 head_dim: usize,
1886 acc: &mut [f32],
1887 rows: std::ops::Range<usize>,
1888) {
1889 for b in rows {
1890 let out_b = &mut acc[b * head_dim..][..head_dim];
1891 for t in 0..span {
1892 let w = p[b * span + t];
1893 if w == 0.0 {
1894 continue;
1895 }
1896 axpy(out_b, &v[v_off + t * v_stride..][..head_dim], w);
1897 }
1898 }
1899}
1900
1901#[cfg(target_arch = "aarch64")]
1902#[target_feature(enable = "neon")]
1903#[allow(clippy::too_many_arguments)]
1904unsafe fn pv_tile_neon(
1905 p: &[f32],
1906 n_b: usize,
1907 span: usize,
1908 v: &[f32],
1909 v_off: usize,
1910 v_stride: usize,
1911 head_dim: usize,
1912 acc: &mut [f32],
1913) {
1914 use std::arch::aarch64::*;
1915 let vp = v.as_ptr().add(v_off);
1916 let pp = p.as_ptr();
1917 let ap = acc.as_mut_ptr();
1918 let bt = n_b & !7;
1919 let dv = head_dim & !7;
1920 let dv4 = head_dim & !3;
1923
1924 let mut b0 = 0;
1925 while b0 < bt {
1926 let mut d0 = 0;
1927 while d0 < dv {
1928 let mut c0l = vld1q_f32(ap.add(b0 * head_dim + d0));
1929 let mut c0h = vld1q_f32(ap.add(b0 * head_dim + d0 + 4));
1930 let mut c1l = vld1q_f32(ap.add((b0 + 1) * head_dim + d0));
1931 let mut c1h = vld1q_f32(ap.add((b0 + 1) * head_dim + d0 + 4));
1932 let mut c2l = vld1q_f32(ap.add((b0 + 2) * head_dim + d0));
1933 let mut c2h = vld1q_f32(ap.add((b0 + 2) * head_dim + d0 + 4));
1934 let mut c3l = vld1q_f32(ap.add((b0 + 3) * head_dim + d0));
1935 let mut c3h = vld1q_f32(ap.add((b0 + 3) * head_dim + d0 + 4));
1936 let mut c4l = vld1q_f32(ap.add((b0 + 4) * head_dim + d0));
1937 let mut c4h = vld1q_f32(ap.add((b0 + 4) * head_dim + d0 + 4));
1938 let mut c5l = vld1q_f32(ap.add((b0 + 5) * head_dim + d0));
1939 let mut c5h = vld1q_f32(ap.add((b0 + 5) * head_dim + d0 + 4));
1940 let mut c6l = vld1q_f32(ap.add((b0 + 6) * head_dim + d0));
1941 let mut c6h = vld1q_f32(ap.add((b0 + 6) * head_dim + d0 + 4));
1942 let mut c7l = vld1q_f32(ap.add((b0 + 7) * head_dim + d0));
1943 let mut c7h = vld1q_f32(ap.add((b0 + 7) * head_dim + d0 + 4));
1944 for t in 0..span {
1945 let vr = vp.add(t * v_stride + d0);
1946 let v0 = vld1q_f32(vr);
1947 let v1 = vld1q_f32(vr.add(4));
1948 let s0 = vdupq_n_f32(*pp.add(b0 * span + t));
1949 c0l = vfmaq_f32(c0l, v0, s0);
1950 c0h = vfmaq_f32(c0h, v1, s0);
1951 let s1 = vdupq_n_f32(*pp.add((b0 + 1) * span + t));
1952 c1l = vfmaq_f32(c1l, v0, s1);
1953 c1h = vfmaq_f32(c1h, v1, s1);
1954 let s2 = vdupq_n_f32(*pp.add((b0 + 2) * span + t));
1955 c2l = vfmaq_f32(c2l, v0, s2);
1956 c2h = vfmaq_f32(c2h, v1, s2);
1957 let s3 = vdupq_n_f32(*pp.add((b0 + 3) * span + t));
1958 c3l = vfmaq_f32(c3l, v0, s3);
1959 c3h = vfmaq_f32(c3h, v1, s3);
1960 let s4 = vdupq_n_f32(*pp.add((b0 + 4) * span + t));
1961 c4l = vfmaq_f32(c4l, v0, s4);
1962 c4h = vfmaq_f32(c4h, v1, s4);
1963 let s5 = vdupq_n_f32(*pp.add((b0 + 5) * span + t));
1964 c5l = vfmaq_f32(c5l, v0, s5);
1965 c5h = vfmaq_f32(c5h, v1, s5);
1966 let s6 = vdupq_n_f32(*pp.add((b0 + 6) * span + t));
1967 c6l = vfmaq_f32(c6l, v0, s6);
1968 c6h = vfmaq_f32(c6h, v1, s6);
1969 let s7 = vdupq_n_f32(*pp.add((b0 + 7) * span + t));
1970 c7l = vfmaq_f32(c7l, v0, s7);
1971 c7h = vfmaq_f32(c7h, v1, s7);
1972 }
1973 vst1q_f32(ap.add(b0 * head_dim + d0), c0l);
1974 vst1q_f32(ap.add(b0 * head_dim + d0 + 4), c0h);
1975 vst1q_f32(ap.add((b0 + 1) * head_dim + d0), c1l);
1976 vst1q_f32(ap.add((b0 + 1) * head_dim + d0 + 4), c1h);
1977 vst1q_f32(ap.add((b0 + 2) * head_dim + d0), c2l);
1978 vst1q_f32(ap.add((b0 + 2) * head_dim + d0 + 4), c2h);
1979 vst1q_f32(ap.add((b0 + 3) * head_dim + d0), c3l);
1980 vst1q_f32(ap.add((b0 + 3) * head_dim + d0 + 4), c3h);
1981 vst1q_f32(ap.add((b0 + 4) * head_dim + d0), c4l);
1982 vst1q_f32(ap.add((b0 + 4) * head_dim + d0 + 4), c4h);
1983 vst1q_f32(ap.add((b0 + 5) * head_dim + d0), c5l);
1984 vst1q_f32(ap.add((b0 + 5) * head_dim + d0 + 4), c5h);
1985 vst1q_f32(ap.add((b0 + 6) * head_dim + d0), c6l);
1986 vst1q_f32(ap.add((b0 + 6) * head_dim + d0 + 4), c6h);
1987 vst1q_f32(ap.add((b0 + 7) * head_dim + d0), c7l);
1988 vst1q_f32(ap.add((b0 + 7) * head_dim + d0 + 4), c7h);
1989 d0 += 8;
1990 }
1991 if dv < head_dim {
1995 for t in 0..span {
1996 for i in 0..8 {
1997 let w = *pp.add((b0 + i) * span + t);
1998 if w == 0.0 {
1999 continue;
2000 }
2001 let row = ap.add((b0 + i) * head_dim);
2002 for d in dv..dv4 {
2003 *row.add(d) = f32::mul_add(w, *vp.add(t * v_stride + d), *row.add(d));
2004 }
2005 for d in dv4..head_dim {
2006 *row.add(d) += w * *vp.add(t * v_stride + d);
2007 }
2008 }
2009 }
2010 }
2011 b0 += 8;
2012 }
2013 pv_rows(p, span, v, v_off, v_stride, head_dim, acc, bt..n_b);
2014}
2015
2016#[cfg(target_arch = "x86_64")]
2019#[target_feature(enable = "avx2,fma")]
2020#[allow(clippy::too_many_arguments)]
2021unsafe fn pv_tile_avx2(
2022 p: &[f32],
2023 n_b: usize,
2024 span: usize,
2025 v: &[f32],
2026 v_off: usize,
2027 v_stride: usize,
2028 head_dim: usize,
2029 acc: &mut [f32],
2030) {
2031 use std::arch::x86_64::*;
2032 let vp = v.as_ptr().add(v_off);
2033 let pp = p.as_ptr();
2034 let ap = acc.as_mut_ptr();
2035 let bt = n_b & !7;
2036 let dv = head_dim & !7;
2039
2040 let mut b0 = 0;
2041 while b0 < bt {
2042 let mut d0 = 0;
2043 while d0 < dv {
2044 let mut c0 = _mm256_loadu_ps(ap.add(b0 * head_dim + d0));
2045 let mut c1 = _mm256_loadu_ps(ap.add((b0 + 1) * head_dim + d0));
2046 let mut c2 = _mm256_loadu_ps(ap.add((b0 + 2) * head_dim + d0));
2047 let mut c3 = _mm256_loadu_ps(ap.add((b0 + 3) * head_dim + d0));
2048 let mut c4 = _mm256_loadu_ps(ap.add((b0 + 4) * head_dim + d0));
2049 let mut c5 = _mm256_loadu_ps(ap.add((b0 + 5) * head_dim + d0));
2050 let mut c6 = _mm256_loadu_ps(ap.add((b0 + 6) * head_dim + d0));
2051 let mut c7 = _mm256_loadu_ps(ap.add((b0 + 7) * head_dim + d0));
2052 for t in 0..span {
2053 let vv = _mm256_loadu_ps(vp.add(t * v_stride + d0));
2054 c0 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add(b0 * span + t)), c0);
2055 c1 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 1) * span + t)), c1);
2056 c2 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 2) * span + t)), c2);
2057 c3 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 3) * span + t)), c3);
2058 c4 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 4) * span + t)), c4);
2059 c5 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 5) * span + t)), c5);
2060 c6 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 6) * span + t)), c6);
2061 c7 = _mm256_fmadd_ps(vv, _mm256_set1_ps(*pp.add((b0 + 7) * span + t)), c7);
2062 }
2063 _mm256_storeu_ps(ap.add(b0 * head_dim + d0), c0);
2064 _mm256_storeu_ps(ap.add((b0 + 1) * head_dim + d0), c1);
2065 _mm256_storeu_ps(ap.add((b0 + 2) * head_dim + d0), c2);
2066 _mm256_storeu_ps(ap.add((b0 + 3) * head_dim + d0), c3);
2067 _mm256_storeu_ps(ap.add((b0 + 4) * head_dim + d0), c4);
2068 _mm256_storeu_ps(ap.add((b0 + 5) * head_dim + d0), c5);
2069 _mm256_storeu_ps(ap.add((b0 + 6) * head_dim + d0), c6);
2070 _mm256_storeu_ps(ap.add((b0 + 7) * head_dim + d0), c7);
2071 d0 += 8;
2072 }
2073 if dv < head_dim {
2074 for t in 0..span {
2075 for i in 0..8 {
2076 let w = *pp.add((b0 + i) * span + t);
2077 if w == 0.0 {
2078 continue;
2079 }
2080 let row = ap.add((b0 + i) * head_dim);
2081 for d in dv..head_dim {
2082 *row.add(d) += w * *vp.add(t * v_stride + d);
2083 }
2084 }
2085 }
2086 }
2087 b0 += 8;
2088 }
2089 pv_rows(p, span, v, v_off, v_stride, head_dim, acc, bt..n_b);
2090}
2091
2092pub fn causal_gqa_attention_paged(
2100 q: &[f32],
2101 store: &PagedKvStore,
2102 block_table: &[usize],
2103 n_heads: usize,
2104 n_kv_heads: usize,
2105 head_dim: usize,
2106 seq_len: usize,
2107) -> Vec<f32> {
2108 assert_eq!(q.len(), n_heads * head_dim);
2109 let block_size = store.block_size();
2110 assert!(
2111 block_table.len() * block_size >= seq_len,
2112 "block table too short for seq_len"
2113 );
2114
2115 let group_size = n_heads / n_kv_heads.max(1);
2116 let scale = 1.0 / (head_dim as f32).sqrt();
2117 let mut out = vec![0f32; n_heads * head_dim];
2118
2119 for h in 0..n_heads {
2120 let kv_h = h / group_size.max(1);
2121 let q_h = &q[h * head_dim..(h + 1) * head_dim];
2122 let out_h = &mut out[h * head_dim..(h + 1) * head_dim];
2123 online_attn_accumulate(q_h, scale, head_dim, out_h, None, None, |visit| {
2124 for t in 0..seq_len {
2125 let block_id = block_table[t / block_size];
2126 let offset = t % block_size;
2127 let k_row = store.k_row(block_id, offset);
2128 let v_row = store.v_row(block_id, offset);
2129 let k_t = &k_row[kv_h * head_dim..(kv_h + 1) * head_dim];
2130 let v_t = &v_row[kv_h * head_dim..(kv_h + 1) * head_dim];
2131 visit(k_t, v_t);
2132 }
2133 });
2134 }
2135
2136 out
2137}
2138
2139#[allow(clippy::too_many_arguments)]
2170pub fn causal_gqa_attention_paged_sinks(
2171 q: &[f32],
2172 store: &PagedKvStore,
2173 block_table: &[usize],
2174 n_heads: usize,
2175 n_kv_heads: usize,
2176 head_dim: usize,
2177 seq_len: usize,
2178 window: Option<usize>,
2179 sinks: Option<&[f32]>,
2180 attn_softcap: Option<f32>,
2181) -> Vec<f32> {
2182 assert_eq!(q.len(), n_heads * head_dim);
2183 let block_size = store.block_size();
2184 assert!(
2185 block_table.len() * block_size >= seq_len,
2186 "block table too short for seq_len"
2187 );
2188 if let Some(sinks) = sinks {
2189 assert_eq!(
2190 sinks.len(),
2191 n_heads,
2192 "attention sinks are per query head (llama.cpp `attn_sinks` is {{n_head}})"
2193 );
2194 }
2195
2196 let group_size = n_heads / n_kv_heads.max(1);
2197 let scale = 1.0 / (head_dim as f32).sqrt();
2198 let mut out = vec![0f32; n_heads * head_dim];
2199 let start = match window {
2204 Some(w) => {
2205 assert!(w > 0, "window must be positive");
2206 seq_len.saturating_sub(w)
2207 }
2208 None => 0,
2209 };
2210
2211 for h in 0..n_heads {
2212 let kv_h = h / group_size.max(1);
2213 let q_h = &q[h * head_dim..(h + 1) * head_dim];
2214 let sink = sinks.map(|s| s[h]);
2215 let out_h = &mut out[h * head_dim..(h + 1) * head_dim];
2216 online_attn_accumulate(q_h, scale, head_dim, out_h, attn_softcap, sink, |visit| {
2217 for t in start..seq_len {
2218 let block_id = block_table[t / block_size];
2219 let offset = t % block_size;
2220 let k_row = store.k_row(block_id, offset);
2221 let v_row = store.v_row(block_id, offset);
2222 let k_t = &k_row[kv_h * head_dim..(kv_h + 1) * head_dim];
2223 let v_t = &v_row[kv_h * head_dim..(kv_h + 1) * head_dim];
2224 visit(k_t, v_t);
2225 }
2226 });
2227 }
2228
2229 out
2230}
2231
2232pub fn causal_mla_attention(
2246 q: &[f32],
2247 k_cache: &[f32],
2248 v_cache: &[f32],
2249 n_heads: usize,
2250 qk_head_dim: usize,
2251 v_head_dim: usize,
2252 seq_len: usize,
2253) -> Vec<f32> {
2254 mla_attention_inner(
2255 q,
2256 k_cache,
2257 v_cache,
2258 n_heads,
2259 qk_head_dim,
2260 v_head_dim,
2261 seq_len,
2262 None,
2263 None,
2264 )
2265}
2266
2267#[allow(clippy::too_many_arguments)]
2289fn mla_attention_inner(
2290 q: &[f32],
2291 k_cache: &[f32],
2292 v_cache: &[f32],
2293 n_heads: usize,
2294 qk_head_dim: usize,
2295 v_head_dim: usize,
2296 seq_len: usize,
2297 visible: Option<&[usize]>,
2298 sinks: Option<&[f32]>,
2299) -> Vec<f32> {
2300 assert_eq!(q.len(), n_heads * qk_head_dim);
2301 assert_eq!(k_cache.len(), seq_len * n_heads * qk_head_dim);
2302 assert_eq!(v_cache.len(), seq_len * n_heads * v_head_dim);
2303 if let Some(visible) = visible {
2304 assert!(
2305 visible.iter().all(|&t| t < seq_len),
2306 "visible positions must be within seq_len"
2307 );
2308 }
2309 if let Some(sinks) = sinks {
2310 assert_eq!(
2311 sinks.len(),
2312 n_heads,
2313 "one sink logit per query head, or none at all"
2314 );
2315 }
2316
2317 let n_positions = visible.map_or(seq_len, |v| v.len());
2322 let position_at = |i: usize| visible.map_or(i, |v| v[i]);
2323
2324 let scale = 1.0 / (qk_head_dim as f32).sqrt();
2325 let mut out = vec![0f32; n_heads * v_head_dim];
2326
2327 for h in 0..n_heads {
2328 let q_h = &q[h * qk_head_dim..(h + 1) * qk_head_dim];
2329
2330 let mut scores = vec![0f32; n_positions];
2331 for (i, score) in scores.iter_mut().enumerate() {
2332 let t = position_at(i);
2333 let k_t =
2334 &k_cache[(t * n_heads + h) * qk_head_dim..(t * n_heads + h + 1) * qk_head_dim];
2335 let mut dot = 0f32;
2336 for d in 0..qk_head_dim {
2337 dot += q_h[d] * k_t[d];
2338 }
2339 *score = dot * scale;
2340 }
2341
2342 let sink = sinks.map(|s| s[h]);
2343 let mut max = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2344 if let Some(s) = sink {
2345 max = max.max(s);
2346 }
2347 let mut sum = 0f32;
2348 for s in scores.iter_mut() {
2349 *s = (*s - max).exp();
2350 sum += *s;
2351 }
2352 if let Some(s) = sink {
2356 sum += (s - max).exp();
2357 }
2358 if sum > 0.0 {
2359 for s in scores.iter_mut() {
2360 *s /= sum;
2361 }
2362 }
2363
2364 let out_h = &mut out[h * v_head_dim..(h + 1) * v_head_dim];
2365 for (i, &w) in scores.iter().enumerate() {
2366 let t = position_at(i);
2367 let v_t = &v_cache[(t * n_heads + h) * v_head_dim..(t * n_heads + h + 1) * v_head_dim];
2368 for d in 0..v_head_dim {
2369 out_h[d] += w * v_t[d];
2370 }
2371 }
2372 }
2373
2374 out
2375}
2376
2377#[allow(clippy::too_many_arguments)]
2383pub fn causal_mla_attention_sinks(
2384 q: &[f32],
2385 k_cache: &[f32],
2386 v_cache: &[f32],
2387 n_heads: usize,
2388 qk_head_dim: usize,
2389 v_head_dim: usize,
2390 seq_len: usize,
2391 sinks: Option<&[f32]>,
2392) -> Vec<f32> {
2393 mla_attention_inner(
2394 q,
2395 k_cache,
2396 v_cache,
2397 n_heads,
2398 qk_head_dim,
2399 v_head_dim,
2400 seq_len,
2401 None,
2402 sinks,
2403 )
2404}
2405
2406#[allow(clippy::too_many_arguments)]
2413pub fn causal_mla_attention_sparse_sinks(
2414 q: &[f32],
2415 k_cache: &[f32],
2416 v_cache: &[f32],
2417 n_heads: usize,
2418 qk_head_dim: usize,
2419 v_head_dim: usize,
2420 seq_len: usize,
2421 visible: &[usize],
2422 sinks: Option<&[f32]>,
2423) -> Vec<f32> {
2424 mla_attention_inner(
2425 q,
2426 k_cache,
2427 v_cache,
2428 n_heads,
2429 qk_head_dim,
2430 v_head_dim,
2431 seq_len,
2432 Some(visible),
2433 sinks,
2434 )
2435}
2436
2437pub fn lightning_indexer_topk(
2457 indexer_q: &[Vec<f32>],
2458 indexer_keys: &[Vec<f32>],
2459 indexer_weights: &[f32],
2460 top_k: usize,
2461) -> Vec<usize> {
2462 let n_heads = indexer_q.len();
2463 assert_eq!(indexer_weights.len(), n_heads);
2464 let index_head_dim = indexer_q.first().map_or(0, |q| q.len());
2465 let scale = 1.0 / ((index_head_dim * n_heads) as f32).sqrt();
2466
2467 let mut scored: Vec<(usize, f32)> = indexer_keys
2468 .iter()
2469 .enumerate()
2470 .map(|(j, k)| {
2471 let score: f32 = indexer_q
2472 .iter()
2473 .zip(indexer_weights.iter())
2474 .map(|(q, w)| {
2475 let dot: f32 = q.iter().zip(k.iter()).map(|(a, b)| a * b).sum();
2476 dot.max(0.0) * w * scale
2477 })
2478 .sum();
2479 (j, score)
2480 })
2481 .collect();
2482
2483 let keep = top_k.min(scored.len());
2484 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
2485 let mut kept: Vec<usize> = scored.into_iter().take(keep).map(|(j, _)| j).collect();
2486 kept.sort_unstable();
2487 kept
2488}
2489
2490#[allow(clippy::too_many_arguments)]
2496pub fn causal_mla_attention_sparse(
2497 q: &[f32],
2498 k_cache: &[f32],
2499 v_cache: &[f32],
2500 n_heads: usize,
2501 qk_head_dim: usize,
2502 v_head_dim: usize,
2503 seq_len: usize,
2504 visible: &[usize],
2505) -> Vec<f32> {
2506 mla_attention_inner(
2507 q,
2508 k_cache,
2509 v_cache,
2510 n_heads,
2511 qk_head_dim,
2512 v_head_dim,
2513 seq_len,
2514 Some(visible),
2515 None,
2516 )
2517}
2518
2519#[cfg(test)]
2520mod tests {
2521
2522 #[test]
2528 fn an_mla_sink_removes_weight_from_the_real_keys_instead_of_moving_it() {
2529 let (n_heads, qk, vd, seq) = (2, 2, 2, 2);
2530 let q = vec![1.0, 0.0, 0.0, 1.0];
2531 let k = vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0];
2532 let v = vec![4.0, 8.0, 1.0, 2.0, 4.0, 8.0, 1.0, 2.0];
2533
2534 let plain = super::causal_mla_attention(&q, &k, &v, n_heads, qk, vd, seq);
2535 let none = super::causal_mla_attention_sinks(&q, &k, &v, n_heads, qk, vd, seq, None);
2536 assert_eq!(plain, none, "no sink must be exactly the old path");
2537
2538 let big = super::causal_mla_attention_sinks(
2540 &q,
2541 &k,
2542 &v,
2543 n_heads,
2544 qk,
2545 vd,
2546 seq,
2547 Some(&[40.0, 40.0]),
2548 );
2549 for (b, p) in big.iter().zip(plain.iter()) {
2550 assert!(b.abs() < 1e-6, "a dominant sink leaves ~0, got {b} vs {p}");
2551 }
2552
2553 let tiny = super::causal_mla_attention_sinks(
2555 &q,
2556 &k,
2557 &v,
2558 n_heads,
2559 qk,
2560 vd,
2561 seq,
2562 Some(&[-40.0, -40.0]),
2563 );
2564 for (s, p) in tiny.iter().zip(plain.iter()) {
2565 assert!((s - p).abs() < 1e-5, "negligible sink: {s} vs {p}");
2566 }
2567 }
2568
2569 #[test]
2573 fn each_head_gets_its_own_mla_sink() {
2574 let (n_heads, qk, vd, seq) = (2, 1, 1, 1);
2575 let q = vec![1.0, 1.0];
2576 let k = vec![1.0, 1.0];
2577 let v = vec![5.0, 5.0];
2578
2579 let out = super::causal_mla_attention_sinks(
2580 &q,
2581 &k,
2582 &v,
2583 n_heads,
2584 qk,
2585 vd,
2586 seq,
2587 Some(&[40.0, -40.0]),
2588 );
2589 assert!(out[0].abs() < 1e-6, "head 0 declined: {}", out[0]);
2590 assert!(
2591 (out[1] - 5.0).abs() < 1e-5,
2592 "head 1 attended normally: {}",
2593 out[1]
2594 );
2595 }
2596
2597 #[test]
2602 fn the_mla_sink_logit_is_not_scaled_by_the_head_width() {
2603 let sink = 0.0f32;
2606 let mut outs = Vec::new();
2607 for qk in [1usize, 4, 16] {
2608 let q = vec![0.0; qk];
2609 let k = vec![0.0; qk];
2610 let v = vec![10.0];
2611 outs.push(super::causal_mla_attention_sinks(&q, &k, &v, 1, qk, 1, 1, Some(&[sink]))[0]);
2612 }
2613 for o in &outs {
2615 assert!((o - 5.0).abs() < 1e-5, "expected 5.0, got {o}");
2616 }
2617 }
2618
2619 #[test]
2623 fn the_sparse_mla_path_honours_a_sink_over_the_selected_positions() {
2624 let (n_heads, qk, vd, seq) = (1, 1, 1, 3);
2625 let q = vec![1.0];
2626 let k = vec![1.0, 1.0, 1.0];
2627 let v = vec![2.0, 4.0, 6.0];
2628 let visible = [0usize, 2];
2629
2630 let plain = super::causal_mla_attention_sparse(&q, &k, &v, n_heads, qk, vd, seq, &visible);
2631 let none = super::causal_mla_attention_sparse_sinks(
2632 &q, &k, &v, n_heads, qk, vd, seq, &visible, None,
2633 );
2634 assert_eq!(plain, none);
2635 assert!((plain[0] - 4.0).abs() < 1e-5, "mean of 2 and 6");
2636
2637 let sunk = super::causal_mla_attention_sparse_sinks(
2638 &q,
2639 &k,
2640 &v,
2641 n_heads,
2642 qk,
2643 vd,
2644 seq,
2645 &visible,
2646 Some(&[40.0]),
2647 );
2648 assert!(sunk[0].abs() < 1e-6, "a dominant sink leaves ~0");
2649 }
2650 #[test]
2651 fn prefill_shared_kv_matches_per_query_reference() {
2652 let n_heads = 6;
2658 let n_kv_heads = 2;
2659 let head_dim = 16;
2660 let n_q = 19;
2661 let kv_prefix = 5;
2662 let kv_len = kv_prefix + n_q;
2663 let q_stride = n_heads * head_dim;
2664 let kv_stride = n_kv_heads * head_dim;
2665
2666 let q: Vec<f32> = (0..n_q * q_stride)
2667 .map(|i| ((i as f32) * 0.013 - 0.7).sin() * 1.3)
2668 .collect();
2669 let k_cache: Vec<f32> = (0..kv_len * kv_stride)
2670 .map(|i| ((i as f32) * 0.017 - 0.3).cos() * 1.1)
2671 .collect();
2672 let v_cache: Vec<f32> = (0..kv_len * kv_stride)
2673 .map(|i| ((i as f32) * 0.011 + 0.2).sin() * 0.9)
2674 .collect();
2675
2676 for softcap in [None, Some(30.0)] {
2677 let got = super::causal_gqa_attention_prefill_shared_kv(
2678 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, n_q, kv_prefix, softcap,
2679 );
2680 assert_eq!(got.len(), n_q * q_stride);
2681 for b in 0..n_q {
2682 let causal_len = kv_prefix + b + 1;
2683 let want = super::causal_gqa_attention_softcap(
2684 &q[b * q_stride..(b + 1) * q_stride],
2685 &k_cache[..causal_len * kv_stride],
2686 &v_cache[..causal_len * kv_stride],
2687 n_heads,
2688 n_kv_heads,
2689 head_dim,
2690 causal_len,
2691 softcap,
2692 );
2693 for (i, (g, w)) in got[b * q_stride..(b + 1) * q_stride]
2694 .iter()
2695 .zip(want.iter())
2696 .enumerate()
2697 {
2698 assert!(
2699 (g - w).abs() < 1e-5,
2700 "softcap {softcap:?} query {b} slot {i}: blocked {g} vs online {w}"
2701 );
2702 }
2703 }
2704 }
2705 }
2706
2707 #[test]
2708 fn windowed_prefill_shared_kv_matches_the_per_query_windowed_reference() {
2709 let n_heads = 6;
2717 let n_kv_heads = 2;
2718 let head_dim = 16;
2719 let n_q = 19;
2720 let kv_prefix = 5;
2721 let kv_len = kv_prefix + n_q;
2722 let q_stride = n_heads * head_dim;
2723 let kv_stride = n_kv_heads * head_dim;
2724
2725 let q: Vec<f32> = (0..n_q * q_stride)
2726 .map(|i| ((i as f32) * 0.013 - 0.7).sin() * 1.3)
2727 .collect();
2728 let k_cache: Vec<f32> = (0..kv_len * kv_stride)
2729 .map(|i| ((i as f32) * 0.017 - 0.3).cos() * 1.1)
2730 .collect();
2731 let v_cache: Vec<f32> = (0..kv_len * kv_stride)
2732 .map(|i| ((i as f32) * 0.011 + 0.2).sin() * 0.9)
2733 .collect();
2734
2735 for window in [1usize, 3, 7, kv_prefix, kv_len, kv_len + 8] {
2736 for softcap in [None, Some(30.0)] {
2737 let got = super::causal_gqa_attention_prefill_shared_kv_windowed(
2738 &q,
2739 &k_cache,
2740 &v_cache,
2741 n_heads,
2742 n_kv_heads,
2743 head_dim,
2744 n_q,
2745 kv_prefix,
2746 softcap,
2747 Some(window),
2748 );
2749 assert_eq!(got.len(), n_q * q_stride);
2750 for b in 0..n_q {
2751 let causal_len = kv_prefix + b + 1;
2752 let want = super::causal_gqa_attention_windowed_softcap(
2753 &q[b * q_stride..(b + 1) * q_stride],
2754 &k_cache[..causal_len * kv_stride],
2755 &v_cache[..causal_len * kv_stride],
2756 n_heads,
2757 n_kv_heads,
2758 head_dim,
2759 causal_len,
2760 window,
2761 softcap,
2762 );
2763 for (i, (g, w)) in got[b * q_stride..(b + 1) * q_stride]
2764 .iter()
2765 .zip(want.iter())
2766 .enumerate()
2767 {
2768 assert!(
2769 (g - w).abs() < 1e-5,
2770 "window {window} softcap {softcap:?} query {b} slot {i}: \
2771 blocked {g} vs per-query {w}"
2772 );
2773 }
2774 }
2775 }
2776 }
2777
2778 let windowed = super::causal_gqa_attention_prefill_shared_kv_windowed(
2780 &q,
2781 &k_cache,
2782 &v_cache,
2783 n_heads,
2784 n_kv_heads,
2785 head_dim,
2786 n_q,
2787 kv_prefix,
2788 None,
2789 Some(kv_len + 8),
2790 );
2791 let full = super::causal_gqa_attention_prefill_shared_kv(
2792 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, n_q, kv_prefix, None,
2793 );
2794 assert_eq!(windowed, full);
2795 }
2796
2797 #[allow(clippy::too_many_arguments)]
2808 fn prefill_query_outer_reference(
2809 q: &[f32],
2810 k_cache: &[f32],
2811 v_cache: &[f32],
2812 n_heads: usize,
2813 n_kv_heads: usize,
2814 head_dim: usize,
2815 n_q: usize,
2816 kv_prefix: usize,
2817 attn_softcap: Option<f32>,
2818 window: Option<usize>,
2819 ) -> Vec<f32> {
2820 let q_stride = n_heads * head_dim;
2821 let group_size = n_heads / n_kv_heads.max(1);
2822 let scale = 1.0 / (head_dim as f32).sqrt();
2823 let softcap = attn_softcap.filter(|&c| c > 0.0);
2824 let mut out = vec![0f32; n_q * q_stride];
2825 let mut acc = vec![0f32; head_dim];
2826 for h in 0..n_heads {
2827 let kv_h = h / group_size.max(1);
2828 for b in 0..n_q {
2829 let causal_len = kv_prefix + b + 1;
2830 let t_start = match window {
2831 Some(w) => causal_len.saturating_sub(w),
2832 None => 0,
2833 };
2834 let q_h = &q[b * q_stride + h * head_dim..][..head_dim];
2835 let mut scores = vec![0f32; causal_len - t_start];
2836 for (i, s) in scores.iter_mut().enumerate() {
2837 let base = ((t_start + i) * n_kv_heads + kv_h) * head_dim;
2838 let mut v = super::dot_f32(q_h, &k_cache[base..base + head_dim]) * scale;
2839 if let Some(sc) = softcap {
2840 v = sc * (v / sc).tanh();
2841 }
2842 *s = v;
2843 }
2844 let l = super::softmax_row_exp_sum(&mut scores);
2845 acc.fill(0.0);
2846 for (i, &p) in scores.iter().enumerate() {
2847 let base = ((t_start + i) * n_kv_heads + kv_h) * head_dim;
2848 super::axpy(&mut acc, &v_cache[base..base + head_dim], p);
2849 }
2850 if l > 0.0 {
2851 super::scale_inplace(&mut acc, 1.0 / l);
2852 }
2853 out[b * q_stride + h * head_dim..][..head_dim].copy_from_slice(&acc);
2854 }
2855 }
2856 out
2857 }
2858
2859 #[test]
2860 fn position_outer_prefill_is_bit_identical_to_the_query_outer_form() {
2861 let n_heads = 6;
2866 let n_kv_heads = 2;
2867 let head_dim = 16;
2868 let n_q = 19;
2869 let kv_prefix = 5;
2870 let kv_len = kv_prefix + n_q;
2871 let q_stride = n_heads * head_dim;
2872 let kv_stride = n_kv_heads * head_dim;
2873
2874 let q: Vec<f32> = (0..n_q * q_stride)
2875 .map(|i| ((i as f32) * 0.013 - 0.7).sin() * 1.3)
2876 .collect();
2877 let k_cache: Vec<f32> = (0..kv_len * kv_stride)
2878 .map(|i| ((i as f32) * 0.017 - 0.3).cos() * 1.1)
2879 .collect();
2880 let v_cache: Vec<f32> = (0..kv_len * kv_stride)
2881 .map(|i| ((i as f32) * 0.011 + 0.2).sin() * 0.9)
2882 .collect();
2883
2884 for window in [None, Some(1), Some(3), Some(8), Some(9), Some(kv_len + 4)] {
2885 for softcap in [None, Some(30.0)] {
2886 let got = super::causal_gqa_attention_prefill_shared_kv_windowed(
2887 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, n_q, kv_prefix, softcap,
2888 window,
2889 );
2890 let want = prefill_query_outer_reference(
2891 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, n_q, kv_prefix, softcap,
2892 window,
2893 );
2894 assert_eq!(got, want, "window {window:?} softcap {softcap:?}");
2895 }
2896 }
2897 }
2898
2899 #[test]
2900 fn tiled_prefill_gemm_is_bit_identical_across_awkward_shapes() {
2901 let shapes = [
2916 (4usize, 4usize, 64usize),
2917 (6, 2, 64),
2918 (5, 1, 80),
2919 (3, 3, 128),
2920 (2, 1, 256),
2921 ];
2922 let batches = [(19usize, 5usize), (8, 0), (3, 7), (16, 1), (7, 0)];
2923 for &(n_heads, n_kv_heads, head_dim) in &shapes {
2924 let q_stride = n_heads * head_dim;
2925 let kv_stride = n_kv_heads * head_dim;
2926 for &(n_q, kv_prefix) in &batches {
2927 let kv_len = kv_prefix + n_q;
2928 let q: Vec<f32> = (0..n_q * q_stride)
2929 .map(|i| ((i as f32) * 0.013 - 0.7).sin() * 1.3)
2930 .collect();
2931 let k_cache: Vec<f32> = (0..kv_len * kv_stride)
2932 .map(|i| ((i as f32) * 0.017 - 0.3).cos() * 1.1)
2933 .collect();
2934 let v_cache: Vec<f32> = (0..kv_len * kv_stride)
2935 .map(|i| ((i as f32) * 0.011 + 0.2).sin() * 0.9)
2936 .collect();
2937 for window in [None, Some(2), Some(5), Some(9), Some(kv_len + 3)] {
2938 for softcap in [None, Some(30.0)] {
2939 let got = super::causal_gqa_attention_prefill_shared_kv_windowed(
2940 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, n_q, kv_prefix,
2941 softcap, window,
2942 );
2943 let want = prefill_query_outer_reference(
2944 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, n_q, kv_prefix,
2945 softcap, window,
2946 );
2947 assert_eq!(
2948 got, want,
2949 "heads {n_heads}/{n_kv_heads} head_dim {head_dim} n_q {n_q} \
2950 kv_prefix {kv_prefix} window {window:?} softcap {softcap:?}"
2951 );
2952 }
2953 }
2954 }
2955 }
2956 }
2957
2958 #[test]
2972 fn vectorised_softmax_row_matches_the_scalar_libm_form() {
2973 fn libm_reference(x: &[f32]) -> (Vec<f32>, f32) {
2974 let m = x.iter().fold(f32::NEG_INFINITY, |a, &s| a.max(s));
2975 let out: Vec<f32> = x.iter().map(|s| (s - m).exp()).collect();
2976 let mut l = 0f32;
2977 for &e in out.iter() {
2978 l += e;
2979 }
2980 (out, l)
2981 }
2982
2983 for spread in [1.0f32, 8.0, 200.0, 0.0] {
2986 for n in (0..=17).chain([31, 32, 33, 64, 127, 512]) {
2987 let row: Vec<f32> = (0..n)
2988 .map(|i| ((i as f32) * 0.37 - 1.1).sin() * spread)
2989 .collect();
2990 let (want, want_l) = libm_reference(&row);
2991
2992 let mut got = row.clone();
2993 let got_l = super::softmax_row_exp_sum(&mut got);
2994
2995 for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() {
2996 assert!(
2997 (g - w).abs() <= 1e-6 * w + 1e-30,
2998 "spread {spread} n {n} slot {i}: vector {g} vs libm {w}"
2999 );
3000 }
3001 assert!(
3002 (got_l - want_l).abs() <= 1e-5 * want_l.max(1.0),
3003 "spread {spread} n {n}: sum {got_l} vs libm {want_l}"
3004 );
3005 if n == 0 {
3006 assert_eq!(got_l, 0.0, "an empty visible range normalises to nothing");
3007 }
3008 if spread == 0.0 && n > 0 {
3009 for (i, g) in got.iter().enumerate() {
3014 assert_eq!(*g, 1.0, "n {n} slot {i}: exp(0) must be exact");
3015 }
3016 }
3017 }
3018 }
3019 }
3020
3021 #[test]
3037 fn the_vectorised_softmax_is_no_less_accurate_than_the_scalar_one() {
3038 for spread in [1.0f64, 6.0, 20.0] {
3039 for n in [64usize, 253, 512] {
3040 let row: Vec<f64> = (0..n)
3041 .map(|i| ((i as f64) * 0.37 - 1.1).sin() * spread)
3042 .collect();
3043
3044 let m64 = row.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
3046 let truth: Vec<f64> = row.iter().map(|s| (s - m64).exp()).collect();
3047 let truth_l: f64 = truth.iter().sum();
3048
3049 let f32_row: Vec<f32> = row.iter().map(|&s| s as f32).collect();
3050
3051 let mut vector = f32_row.clone();
3052 let vector_l = super::softmax_row_exp_sum(&mut vector);
3053 let mut scalar = f32_row.clone();
3054 let scalar_l = super::softmax_row_exp_sum_scalar(&mut scalar);
3055
3056 let worst = |got: &[f32]| -> f64 {
3057 got.iter()
3058 .zip(truth.iter())
3059 .map(|(&g, &t)| ((g as f64) - t).abs() / t)
3060 .fold(0.0, f64::max)
3061 };
3062 let (ev, es) = (worst(&vector), worst(&scalar));
3063 let lv = ((vector_l as f64) - truth_l).abs() / truth_l;
3064 let ls = ((scalar_l as f64) - truth_l).abs() / truth_l;
3065 let eps = f64::from(f32::EPSILON);
3066
3067 assert!(
3073 ev <= 2.0 * es.max(eps) && ev <= 64.0 * eps,
3074 "spread {spread} n {n}: vector probabilities err {ev:e} \
3075 against scalar {es:e}"
3076 );
3077
3078 assert!(
3082 lv <= ls.max(eps),
3083 "spread {spread} n {n}: vector normaliser err {lv:e} \
3084 against scalar {ls:e}"
3085 );
3086 }
3087 }
3088 }
3089
3090 #[test]
3094 fn softmax_row_finds_its_maximum_in_every_lane_position() {
3095 for n in 1usize..=20 {
3096 for peak in 0..n {
3097 let mut row: Vec<f32> = (0..n).map(|i| -(i as f32) - 3.0).collect();
3098 row[peak] = 12.5;
3099 let l = super::softmax_row_exp_sum(&mut row);
3100 assert_eq!(row[peak], 1.0, "n {n} peak {peak}: the max term is exp(0)");
3101 for (i, &p) in row.iter().enumerate() {
3102 assert!(p <= 1.0, "n {n} peak {peak} slot {i}: {p} exceeds the max");
3103 }
3104 assert!(l >= 1.0, "n {n} peak {peak}: sum {l} must include the max");
3105 }
3106 }
3107 }
3108
3109 use super::*;
3110
3111 #[test]
3112 fn simd_dot_f32_matches_scalar_across_lengths() {
3113 for n in [1usize, 3, 4, 7, 8, 15, 16, 63, 128, 129] {
3115 let a: Vec<f32> = (0..n).map(|i| ((i as f32) * 0.31 - 2.0).sin()).collect();
3116 let b: Vec<f32> = (0..n).map(|i| ((i as f32) * 0.17 + 1.0).cos()).collect();
3117 let simd = dot_f32(&a, &b);
3118 let scalar: f32 = a.iter().zip(&b).map(|(x, y)| x * y).sum();
3119 assert!(
3120 (simd - scalar).abs() <= 1e-4 * scalar.abs().max(1.0),
3121 "n={n} simd={simd} scalar={scalar}"
3122 );
3123 }
3124 }
3125
3126 #[test]
3127 fn rope_preserves_vector_norm() {
3128 let mut v = vec![1.0, 2.0, 3.0, 4.0];
3129 let norm_before: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3130 apply_rope(&mut v, 5, 10000.0);
3131 let norm_after: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3132 assert!(
3133 (norm_before - norm_after).abs() < 1e-4,
3134 "RoPE is a rotation and must preserve norm"
3135 );
3136 }
3137
3138 #[test]
3139 fn rope_back_inverts_rope() {
3140 let original = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3141 let mut v = original.clone();
3142 apply_rope(&mut v, 11, 10000.0);
3143 apply_rope_back(&mut v, 11, 10000.0);
3144 for (a, b) in v.iter().zip(original.iter()) {
3145 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
3146 }
3147 }
3148
3149 #[test]
3150 fn rope_interleaved_back_inverts_interleaved() {
3151 let original = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3152 let mut v = original.clone();
3153 apply_rope_interleaved(&mut v, 11, 10000.0);
3154 apply_rope_interleaved_back(&mut v, 11, 10000.0);
3155 for (a, b) in v.iter().zip(original.iter()) {
3156 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
3157 }
3158 }
3159
3160 #[test]
3161 fn rope_at_position_zero_is_identity() {
3162 let mut v = vec![1.0, 2.0, 3.0, 4.0];
3163 let original = v.clone();
3164 apply_rope(&mut v, 0, 10000.0);
3165 for (a, b) in v.iter().zip(original.iter()) {
3166 assert!((a - b).abs() < 1e-5);
3167 }
3168 }
3169
3170 #[test]
3171 fn rope_with_all_ones_freq_factors_matches_plain_rope() {
3172 let mut with_factors = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3173 let mut plain = with_factors.clone();
3174 let ones = vec![1.0; 3];
3175 apply_rope_with_freq_factors(&mut with_factors, 7, 10000.0, &ones);
3176 apply_rope(&mut plain, 7, 10000.0);
3177 for (a, b) in with_factors.iter().zip(plain.iter()) {
3178 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
3179 }
3180 }
3181
3182 #[test]
3183 fn rope_with_freq_factors_diverges_from_plain_rope_when_factors_are_not_one() {
3184 let mut with_factors = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3185 let mut plain = with_factors.clone();
3186 let factors = vec![0.5, 2.0, 1.0];
3187 apply_rope_with_freq_factors(&mut with_factors, 7, 10000.0, &factors);
3188 apply_rope(&mut plain, 7, 10000.0);
3189 let differs = with_factors
3190 .iter()
3191 .zip(plain.iter())
3192 .any(|(a, b)| (a - b).abs() > 1e-4);
3193 assert!(differs, "non-1.0 freq_factors must change the rotation");
3194 }
3195
3196 #[test]
3197 fn rope_with_freq_factors_preserves_vector_norm() {
3198 let mut v = vec![1.0, 2.0, 3.0, 4.0];
3199 let norm_before: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3200 apply_rope_with_freq_factors(&mut v, 5, 10000.0, &[0.8, 1.3]);
3201 let norm_after: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3202 assert!((norm_before - norm_after).abs() < 1e-4);
3203 }
3204
3205 #[test]
3219 fn yarn_high_is_clamped_to_rotary_dim_minus_one_not_half_minus_one() {
3220 let scaling = YarnScaling::new(8.0, 131_072);
3221 let (low, high) = yarn_correction_range(scaling, 64, 10_000.0);
3222 assert!((low - 22.0).abs() < 1e-9, "low was {low}");
3223 assert!((high - 35.0).abs() < 1e-9, "high was {high}");
3224
3225 let factors = yarn_freq_factors(scaling, 64, 10_000.0);
3226 let last = factors[31];
3227 let ramp = (31.0 - 22.0) / (35.0 - 22.0);
3228 let want = 1.0 / (ramp / 8.0 + (1.0 - ramp));
3229 assert!(
3230 (last - want).abs() < 1e-4,
3231 "last band divisor {last} must be the reference's {want}"
3232 );
3233 assert!(
3234 (last - 8.0).abs() > 1.0,
3235 "clamping high to rotary_dim/2 - 1 would fully interpolate this band \
3236 (divisor 8.0, the whole factor); got {last}"
3237 );
3238 }
3239
3240 #[test]
3246 fn yarn_freq_factors_match_the_reference_ramp_formula_band_by_band() {
3247 let scaling = YarnScaling::new(8.0, 131_072);
3248 let factors = yarn_freq_factors(scaling, 64, 10_000.0);
3249 assert_eq!(factors.len(), 32, "one divisor per rotation band");
3250 for band in [0usize, 10, 22] {
3251 assert!(
3252 (factors[band] - 1.0).abs() < 1e-6,
3253 "band {band} is at or below low=22 and must be left extrapolated, \
3254 got {}",
3255 factors[band]
3256 );
3257 }
3258 for band in [23usize, 27, 31] {
3259 let ramp = (band as f32 - 22.0) / (35.0 - 22.0);
3260 let want = 1.0 / (ramp / 8.0 + (1.0 - ramp));
3261 assert!(
3262 (factors[band] - want).abs() < 1e-4,
3263 "band {band}: got {}, reference {want}",
3264 factors[band]
3265 );
3266 }
3267 }
3268
3269 #[test]
3280 fn yarn_nudges_a_collapsed_correction_range_instead_of_flooring_the_gap_at_one() {
3281 let scaling = YarnScaling {
3282 beta_slow: 32.0,
3283 truncate: false,
3284 ..YarnScaling::new(8.0, 131_072)
3285 };
3286 let (low, high) = yarn_correction_range(scaling, 64, 10_000.0);
3287 assert!((low - 22.513_44).abs() < 1e-4, "low was {low}");
3289 assert!(
3290 (high - low - 0.001).abs() < 1e-9,
3291 "high must be low + 0.001, got {high}"
3292 );
3293
3294 let factors = yarn_freq_factors(scaling, 64, 10_000.0);
3295 assert!(
3296 (factors[22] - 1.0).abs() < 1e-6,
3297 "band below the step must be untouched, got {}",
3298 factors[22]
3299 );
3300 assert!(
3301 (factors[23] - 8.0).abs() < 1e-4,
3302 "band above the step must take the whole factor (a gap of 1 would \
3303 give 1.7414); got {}",
3304 factors[23]
3305 );
3306 }
3307
3308 #[test]
3313 fn yarn_with_a_factor_of_one_leaves_every_band_untouched() {
3314 let factors = yarn_freq_factors(YarnScaling::new(1.0, 4096), 32, 10_000.0);
3315 for (band, f) in factors.iter().enumerate() {
3316 assert!((f - 1.0).abs() < 1e-6, "band {band} moved to {f}");
3317 }
3318 }
3319
3320 #[test]
3327 fn yarn_divisors_reproduce_the_references_rewritten_frequencies() {
3328 let scaling = YarnScaling::new(8.0, 131_072);
3329 let factors = yarn_freq_factors(scaling, 64, 10_000.0);
3330
3331 let band = 31usize;
3332 let pos = 1024usize;
3333 let mut v = vec![0.0f32; 64];
3334 v[band] = 1.0;
3335 apply_rope_with_freq_factors(&mut v, pos, 10_000.0, &factors);
3336
3337 let ramp = (band as f64 - 22.0) / (35.0 - 22.0);
3338 let inv_freq = 1.0 / 10_000f64.powf((2 * band) as f64 / 64.0);
3339 let inv_freq_new = inv_freq * (ramp / 8.0 + (1.0 - ramp));
3340 let angle = pos as f64 * inv_freq_new;
3341 assert!(
3342 (v[band] as f64 - angle.cos()).abs() < 1e-5,
3343 "cos: {} vs {}",
3344 v[band],
3345 angle.cos()
3346 );
3347 assert!(
3348 (v[band + 32] as f64 - angle.sin()).abs() < 1e-5,
3349 "sin: {} vs {}",
3350 v[band + 32],
3351 angle.sin()
3352 );
3353 }
3354
3355 #[test]
3362 fn proportional_freq_factors_respace_frequencies_over_the_full_head() {
3363 let factors = proportional_freq_factors(128, 96, 10_000.0);
3364 assert_eq!(factors.len(), 48, "one divisor per rotated band");
3365 assert!((factors[0] - 1.0).abs() < 1e-6, "band 0 is 1/1");
3366 assert!(
3368 (factors[1] - 0.953_161_9).abs() < 1e-5,
3369 "band 1 was {}",
3370 factors[1]
3371 );
3372 assert!(
3374 (factors[47] - 0.104_913_97).abs() < 1e-5,
3375 "last band was {}",
3376 factors[47]
3377 );
3378 }
3379
3380 #[test]
3384 fn proportional_freq_factors_are_all_ones_when_the_whole_head_rotates() {
3385 for f in proportional_freq_factors(128, 128, 500_000.0) {
3386 assert!((f - 1.0).abs() < 1e-6, "full-width band moved to {f}");
3387 }
3388 }
3389
3390 #[test]
3391 fn rope_interleaved_preserves_vector_norm() {
3392 let mut v = vec![1.0, 2.0, 3.0, 4.0];
3393 let norm_before: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3394 apply_rope_interleaved(&mut v, 5, 10000.0);
3395 let norm_after: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3396 assert!(
3397 (norm_before - norm_after).abs() < 1e-4,
3398 "RoPE is a rotation and must preserve norm"
3399 );
3400 }
3401
3402 #[test]
3403 fn rope_interleaved_at_position_zero_is_identity() {
3404 let mut v = vec![1.0, 2.0, 3.0, 4.0];
3405 let original = v.clone();
3406 apply_rope_interleaved(&mut v, 0, 10000.0);
3407 for (a, b) in v.iter().zip(original.iter()) {
3408 assert!((a - b).abs() < 1e-5);
3409 }
3410 }
3411
3412 #[test]
3413 fn rope_interleaved_rotates_adjacent_pairs_not_split_halves() {
3414 let mut interleaved = vec![1.0, 0.0, 0.0, 1.0];
3421 let mut split_half = interleaved.clone();
3422 apply_rope_interleaved(&mut interleaved, 3, 10000.0);
3423 apply_rope(&mut split_half, 3, 10000.0);
3424 let differs = interleaved
3429 .iter()
3430 .zip(split_half.iter())
3431 .any(|(a, b)| (a - b).abs() > 1e-4);
3432 assert!(differs, "the two RoPE conventions must not coincide here");
3433 }
3434
3435 #[test]
3436 fn rope_interleaved_with_all_ones_freq_factors_matches_plain_interleaved() {
3437 let mut with_factors = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3438 let mut plain = with_factors.clone();
3439 let ones = vec![1.0; 3];
3440 apply_rope_interleaved_with_freq_factors(&mut with_factors, 7, 10000.0, &ones);
3441 apply_rope_interleaved(&mut plain, 7, 10000.0);
3442 for (a, b) in with_factors.iter().zip(plain.iter()) {
3443 assert!((a - b).abs() < 1e-5, "{a} vs {b}");
3444 }
3445 }
3446
3447 #[test]
3448 fn rope_interleaved_with_freq_factors_diverges_when_factors_are_not_one() {
3449 let mut with_factors = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
3450 let mut plain = with_factors.clone();
3451 let factors = vec![0.5, 2.0, 1.0];
3452 apply_rope_interleaved_with_freq_factors(&mut with_factors, 7, 10000.0, &factors);
3453 apply_rope_interleaved(&mut plain, 7, 10000.0);
3454 let differs = with_factors
3455 .iter()
3456 .zip(plain.iter())
3457 .any(|(a, b)| (a - b).abs() > 1e-4);
3458 assert!(differs, "non-1.0 freq_factors must change the rotation");
3459 }
3460
3461 #[test]
3462 fn rope_interleaved_with_freq_factors_preserves_vector_norm() {
3463 let mut v = vec![1.0, 2.0, 3.0, 4.0];
3464 let norm_before: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3465 apply_rope_interleaved_with_freq_factors(&mut v, 5, 10000.0, &[0.8, 1.3]);
3466 let norm_after: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
3467 assert!((norm_before - norm_after).abs() < 1e-4);
3468 }
3469
3470 #[test]
3471 fn attention_with_single_position_returns_that_value() {
3472 let q = vec![1.0, 0.0]; let k_cache = vec![0.5, 0.5]; let v_cache = vec![9.0, -3.0];
3477 let out = causal_gqa_attention(&q, &k_cache, &v_cache, 1, 1, 2, 1);
3478 assert!((out[0] - 9.0).abs() < 1e-4);
3479 assert!((out[1] - (-3.0)).abs() < 1e-4);
3480 }
3481
3482 #[test]
3483 fn gqa_group_mapping_shares_kv_heads_correctly() {
3484 let head_dim = 2;
3486 let q = vec![
3487 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, ];
3492 let k_cache = vec![1.0, 0.0, 1.0, 0.0];
3494 let v_cache = vec![100.0, 100.0, 200.0, 200.0];
3495 let out = causal_gqa_attention(&q, &k_cache, &v_cache, 4, 2, head_dim, 1);
3496 assert_eq!(&out[0..2], &[100.0, 100.0][..]);
3498 assert_eq!(&out[2..4], &[100.0, 100.0][..]);
3499 assert_eq!(&out[4..6], &[200.0, 200.0][..]);
3500 assert_eq!(&out[6..8], &[200.0, 200.0][..]);
3501 }
3502
3503 #[test]
3504 fn prefill_gqa_matches_per_token_causal() {
3505 let n_heads = 4;
3506 let n_kv_heads = 2;
3507 let head_dim = 4;
3508 let seq_len = 5;
3509 let q: Vec<f32> = (0..seq_len * n_heads * head_dim)
3510 .map(|i| (i as f32 * 0.13).sin())
3511 .collect();
3512 let k: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3513 .map(|i| (i as f32 * 0.19).cos())
3514 .collect();
3515 let v: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3516 .map(|i| (i as f32 * 0.07).sin())
3517 .collect();
3518 let batched =
3519 causal_gqa_attention_prefill(&q, &k, &v, n_heads, n_kv_heads, head_dim, seq_len);
3520 let q_stride = n_heads * head_dim;
3521 let kv_stride = n_kv_heads * head_dim;
3522 for t in 0..seq_len {
3523 let expect = causal_gqa_attention(
3524 &q[t * q_stride..(t + 1) * q_stride],
3525 &k[..(t + 1) * kv_stride],
3526 &v[..(t + 1) * kv_stride],
3527 n_heads,
3528 n_kv_heads,
3529 head_dim,
3530 t + 1,
3531 );
3532 let got = &batched[t * q_stride..(t + 1) * q_stride];
3533 for (a, b) in got.iter().zip(expect.iter()) {
3534 assert!((a - b).abs() < 1e-5, "t={t}: {a} vs {b}");
3535 }
3536 }
3537 }
3538
3539 #[test]
3540 fn windowed_attention_with_window_covering_full_history_matches_full_causal() {
3541 let n_heads = 2;
3542 let n_kv_heads = 1;
3543 let head_dim = 3;
3544 let seq_len = 4;
3545
3546 let q: Vec<f32> = (0..n_heads * head_dim)
3547 .map(|i| (i as f32 * 0.3).sin())
3548 .collect();
3549 let k_cache: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3550 .map(|i| (i as f32 * 0.17).cos())
3551 .collect();
3552 let v_cache: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3553 .map(|i| (i as f32 * 0.11).sin())
3554 .collect();
3555
3556 let full = causal_gqa_attention(
3557 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, seq_len,
3558 );
3559 let windowed = causal_gqa_attention_windowed(
3560 &q, &k_cache, &v_cache, n_heads, n_kv_heads, head_dim, seq_len, seq_len,
3561 );
3562 assert_eq!(full.len(), windowed.len());
3563 for (a, b) in full.iter().zip(windowed.iter()) {
3564 assert_eq!(
3565 a.to_bits(),
3566 b.to_bits(),
3567 "window >= seq_len must be bit-identical to full causal"
3568 );
3569 }
3570 }
3571
3572 #[test]
3573 fn windowed_attention_ignores_positions_outside_the_window() {
3574 let head_dim = 2;
3580 let q = vec![1.0, 0.0];
3581 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];
3583 let out = causal_gqa_attention_windowed(&q, &k_cache, &v_cache, 1, 1, head_dim, 3, 1);
3584 assert!((out[0] - 50.0).abs() < 1e-4);
3585 assert!((out[1] - 60.0).abs() < 1e-4);
3586 }
3587
3588 #[test]
3589 fn paged_attention_matches_contiguous_attention_bit_identical() {
3590 use crate::cache::{PagedKvCache, PagedKvStore};
3591
3592 let n_heads = 4;
3593 let n_kv_heads = 2;
3594 let head_dim = 3;
3595 let block_size = 2;
3596 let seq_len = 5;
3597
3598 let k_flat: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3600 .map(|i| ((i * 7 + 1) % 13) as f32 * 0.1)
3601 .collect();
3602 let v_flat: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3603 .map(|i| ((i * 5 + 3) % 11) as f32 * 0.1)
3604 .collect();
3605 let q: Vec<f32> = (0..n_heads * head_dim)
3606 .map(|i| ((i * 3 + 2) % 9) as f32 * 0.1)
3607 .collect();
3608
3609 let contiguous =
3610 causal_gqa_attention(&q, &k_flat, &v_flat, n_heads, n_kv_heads, head_dim, seq_len);
3611
3612 let mut store = PagedKvStore::new(block_size, seq_len, n_kv_heads, head_dim);
3613 let mut cache = PagedKvCache::new();
3614 for t in 0..seq_len {
3615 let start = t * n_kv_heads * head_dim;
3616 let end = start + n_kv_heads * head_dim;
3617 cache
3618 .push(&mut store, &k_flat[start..end], &v_flat[start..end])
3619 .expect("store sized for seq_len blocks, must not exhaust");
3620 }
3621
3622 let paged = causal_gqa_attention_paged(
3623 &q,
3624 &store,
3625 cache.block_table(),
3626 n_heads,
3627 n_kv_heads,
3628 head_dim,
3629 seq_len,
3630 );
3631
3632 assert_eq!(contiguous.len(), paged.len());
3633 for (a, b) in contiguous.iter().zip(paged.iter()) {
3634 assert_eq!(a.to_bits(), b.to_bits(), "paged path must be bit-identical");
3635 }
3636 }
3637
3638 fn paged_fixture(
3642 seq_len: usize,
3643 n_kv_heads: usize,
3644 head_dim: usize,
3645 block_size: usize,
3646 ) -> (Vec<f32>, Vec<f32>, crate::cache::PagedKvStore, Vec<usize>) {
3647 use crate::cache::{PagedKvCache, PagedKvStore};
3648 let k_flat: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3649 .map(|i| ((i * 7 + 1) % 13) as f32 * 0.1)
3650 .collect();
3651 let v_flat: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3652 .map(|i| ((i * 5 + 3) % 11) as f32 * 0.1)
3653 .collect();
3654 let mut store = PagedKvStore::new(block_size, seq_len, n_kv_heads, head_dim);
3655 let mut cache = PagedKvCache::new();
3656 for t in 0..seq_len {
3657 let start = t * n_kv_heads * head_dim;
3658 let end = start + n_kv_heads * head_dim;
3659 cache
3660 .push(&mut store, &k_flat[start..end], &v_flat[start..end])
3661 .expect("store sized for seq_len blocks, must not exhaust");
3662 }
3663 let table = cache.block_table().to_vec();
3664 (k_flat, v_flat, store, table)
3665 }
3666
3667 #[test]
3678 fn the_paged_sink_term_is_bit_identical_to_the_contiguous_one() {
3679 let (n_heads, n_kv_heads, head_dim, seq_len, block_size) = (4, 2, 3, 5, 2);
3680 let (k_flat, v_flat, store, table) =
3681 paged_fixture(seq_len, n_kv_heads, head_dim, block_size);
3682 let q: Vec<f32> = (0..n_heads * head_dim)
3683 .map(|i| ((i * 3 + 2) % 9) as f32 * 0.1)
3684 .collect();
3685
3686 let sinks = vec![-30.0f32, 0.0, 1.5, 30.0];
3690 let contiguous = causal_gqa_attention_sinks(
3691 &q, &k_flat, &v_flat, n_heads, n_kv_heads, head_dim, seq_len, None, &sinks,
3692 );
3693 let paged = causal_gqa_attention_paged_sinks(
3694 &q,
3695 &store,
3696 &table,
3697 n_heads,
3698 n_kv_heads,
3699 head_dim,
3700 seq_len,
3701 None,
3702 Some(&sinks),
3703 None,
3704 );
3705 assert_eq!(contiguous.len(), paged.len());
3706 for (i, (a, b)) in contiguous.iter().zip(paged.iter()).enumerate() {
3707 assert_eq!(a.to_bits(), b.to_bits(), "element {i}: {a} vs {b}");
3708 }
3709 }
3710
3711 #[test]
3715 fn the_paged_window_arm_is_bit_identical_to_the_contiguous_one() {
3716 let (n_heads, n_kv_heads, head_dim, seq_len, block_size) = (4, 2, 3, 7, 2);
3717 let (k_flat, v_flat, store, table) =
3718 paged_fixture(seq_len, n_kv_heads, head_dim, block_size);
3719 let q: Vec<f32> = (0..n_heads * head_dim)
3720 .map(|i| ((i * 3 + 2) % 9) as f32 * 0.1)
3721 .collect();
3722 let sinks = vec![0.5f32; n_heads];
3723
3724 for window in [1usize, 2, 3, 6, 7, 99] {
3725 let contiguous = causal_gqa_attention_sinks(
3726 &q,
3727 &k_flat,
3728 &v_flat,
3729 n_heads,
3730 n_kv_heads,
3731 head_dim,
3732 seq_len,
3733 Some(window),
3734 &sinks,
3735 );
3736 let paged = causal_gqa_attention_paged_sinks(
3737 &q,
3738 &store,
3739 &table,
3740 n_heads,
3741 n_kv_heads,
3742 head_dim,
3743 seq_len,
3744 Some(window),
3745 Some(&sinks),
3746 None,
3747 );
3748 for (i, (a, b)) in contiguous.iter().zip(paged.iter()).enumerate() {
3749 assert_eq!(
3750 a.to_bits(),
3751 b.to_bits(),
3752 "window {window} element {i}: {a} vs {b}"
3753 );
3754 }
3755 }
3756 }
3757
3758 #[test]
3762 fn the_paged_sink_kernel_without_sinks_or_window_is_the_plain_paged_kernel() {
3763 let (n_heads, n_kv_heads, head_dim, seq_len, block_size) = (4, 2, 3, 5, 2);
3764 let (_k, _v, store, table) = paged_fixture(seq_len, n_kv_heads, head_dim, block_size);
3765 let q: Vec<f32> = (0..n_heads * head_dim)
3766 .map(|i| ((i * 3 + 2) % 9) as f32 * 0.1)
3767 .collect();
3768
3769 let plain =
3770 causal_gqa_attention_paged(&q, &store, &table, n_heads, n_kv_heads, head_dim, seq_len);
3771 let via_sinks = causal_gqa_attention_paged_sinks(
3772 &q, &store, &table, n_heads, n_kv_heads, head_dim, seq_len, None, None, None,
3773 );
3774 for (i, (a, b)) in plain.iter().zip(via_sinks.iter()).enumerate() {
3775 assert_eq!(a.to_bits(), b.to_bits(), "element {i}");
3776 }
3777 }
3778
3779 #[test]
3780 fn mla_attention_with_single_position_returns_that_value() {
3781 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);
3788 assert_eq!(out.len(), 3);
3789 assert!((out[0] - 9.0).abs() < 1e-4);
3790 assert!((out[1] - (-3.0)).abs() < 1e-4);
3791 assert!((out[2] - 1.0).abs() < 1e-4);
3792 }
3793
3794 #[test]
3795 fn mla_attention_every_head_gets_its_own_kv_no_grouping() {
3796 let qk_head_dim = 2;
3800 let v_head_dim = 2;
3801 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];
3804 let out = causal_mla_attention(&q, &k_cache, &v_cache, 2, qk_head_dim, v_head_dim, 1);
3805 assert_eq!(&out[0..2], &[100.0, 100.0][..]);
3806 assert_eq!(&out[2..4], &[200.0, 200.0][..]);
3807 }
3808
3809 #[test]
3810 fn lightning_indexer_topk_keeps_all_positions_when_top_k_covers_them() {
3811 let indexer_q = vec![vec![1.0, 0.0]];
3812 let indexer_keys = vec![vec![1.0, 0.0], vec![0.5, 0.5], vec![0.1, 0.9]];
3813 let indexer_weights = vec![1.0];
3814 let kept = lightning_indexer_topk(&indexer_q, &indexer_keys, &indexer_weights, 10);
3815 assert_eq!(kept, vec![0, 1, 2]);
3816 }
3817
3818 #[test]
3819 fn lightning_indexer_topk_selects_highest_scoring_positions() {
3820 let indexer_q = vec![vec![1.0, 0.0]];
3823 let indexer_keys = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![0.9, 0.1]];
3824 let indexer_weights = vec![1.0];
3825 let kept = lightning_indexer_topk(&indexer_q, &indexer_keys, &indexer_weights, 2);
3826 assert_eq!(kept, vec![0, 2]);
3827 }
3828
3829 #[test]
3830 fn lightning_indexer_topk_relu_zeroes_negative_dot_products() {
3831 let indexer_q = vec![vec![1.0, 0.0]];
3834 let indexer_keys = vec![vec![0.3, 0.0], vec![-1.0, 0.0]];
3835 let indexer_weights = vec![1.0];
3836 let kept = lightning_indexer_topk(&indexer_q, &indexer_keys, &indexer_weights, 1);
3837 assert_eq!(kept, vec![0]);
3838 }
3839
3840 #[test]
3841 fn mla_attention_sparse_with_all_positions_visible_matches_full_causal() {
3842 let qk_head_dim = 3;
3843 let v_head_dim = 2;
3844 let seq_len = 4;
3845 let n_heads = 2;
3846 let q: Vec<f32> = (0..n_heads * qk_head_dim).map(|i| i as f32 * 0.1).collect();
3847 let k_cache: Vec<f32> = (0..seq_len * n_heads * qk_head_dim)
3848 .map(|i| (i as f32 * 0.05).sin())
3849 .collect();
3850 let v_cache: Vec<f32> = (0..seq_len * n_heads * v_head_dim)
3851 .map(|i| (i as f32 * 0.05).cos())
3852 .collect();
3853
3854 let full = causal_mla_attention(
3855 &q,
3856 &k_cache,
3857 &v_cache,
3858 n_heads,
3859 qk_head_dim,
3860 v_head_dim,
3861 seq_len,
3862 );
3863 let visible: Vec<usize> = (0..seq_len).collect();
3864 let sparse = causal_mla_attention_sparse(
3865 &q,
3866 &k_cache,
3867 &v_cache,
3868 n_heads,
3869 qk_head_dim,
3870 v_head_dim,
3871 seq_len,
3872 &visible,
3873 );
3874
3875 assert_eq!(full.len(), sparse.len());
3876 for (a, b) in full.iter().zip(sparse.iter()) {
3877 assert!((a - b).abs() < 1e-6, "full={a} sparse={b}");
3878 }
3879 }
3880
3881 #[test]
3882 fn mla_attention_sparse_ignores_positions_outside_visible_set() {
3883 let qk_head_dim = 2;
3886 let v_head_dim = 1;
3887 let q = vec![1.0, 0.0];
3888 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(
3891 &q,
3892 &k_cache,
3893 &v_cache,
3894 1,
3895 qk_head_dim,
3896 v_head_dim,
3897 2,
3898 &[0],
3899 );
3900 assert_eq!(out.len(), 1);
3901 assert!((out[0] - 5.0).abs() < 1e-6);
3902 }
3903
3904 #[test]
3905 fn attn_logit_softcap_changes_output_vs_uncapped() {
3906 let n_heads = 2;
3909 let n_kv_heads = 1;
3910 let head_dim = 4;
3911 let seq_len = 3;
3912 let q: Vec<f32> = (0..n_heads * head_dim)
3913 .map(|i| (i as f32 + 1.0) * 2.5)
3914 .collect();
3915 let k: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3916 .map(|i| (i as f32 * 0.7).sin() * 3.0)
3917 .collect();
3918 let v: Vec<f32> = (0..seq_len * n_kv_heads * head_dim)
3919 .map(|i| (i as f32 * 0.3).cos())
3920 .collect();
3921 let plain = causal_gqa_attention(&q, &k, &v, n_heads, n_kv_heads, head_dim, seq_len);
3922 let capped = causal_gqa_attention_softcap(
3923 &q,
3924 &k,
3925 &v,
3926 n_heads,
3927 n_kv_heads,
3928 head_dim,
3929 seq_len,
3930 Some(30.0),
3931 );
3932 assert_eq!(plain.len(), capped.len());
3933 let differs = plain
3934 .iter()
3935 .zip(capped.iter())
3936 .any(|(a, b)| (a - b).abs() > 1e-5);
3937 assert!(differs, "softcap must change attention output");
3938 let none =
3940 causal_gqa_attention_softcap(&q, &k, &v, n_heads, n_kv_heads, head_dim, seq_len, None);
3941 for (a, b) in plain.iter().zip(none.iter()) {
3942 assert!((a - b).abs() < 1e-6);
3943 }
3944 }
3945}