1#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum F32LinearAccumulation {
20 Scalar,
22 Lanes4,
24 Lanes8,
26 FusedLanes4,
28 FusedLanes8,
30 Accelerate,
34 AccelerateRowInvariant,
39 AccelerateBiasSeeded,
47 AccelerateBiasSeededRowInvariant,
56 WidenedF64,
63}
64
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub enum F32RmsNormArithmetic {
72 ScalarReciprocalSqrt,
74 ScalarDivideSqrt,
76 Lanes4ReciprocalSqrt,
78 Lanes8ReciprocalSqrt,
80 Lanes16ReciprocalSqrt,
82 Lanes32ReciprocalSqrt,
84 TorchCascade4ReciprocalSqrt,
87 TorchCascade8ReciprocalSqrt,
90 F64ReciprocalSqrt,
92}
93
94impl F32RmsNormArithmetic {
95 pub const WIDENED_F64: Self = Self::F64ReciprocalSqrt;
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub enum F32SiluArithmetic {
102 Divide,
104 MultiplyReciprocal,
106 WidenedF64,
109}
110
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub enum F32SoftmaxArithmetic {
114 ReciprocalMultiply,
116 Divide,
118 WidenedF64,
121}
122
123pub fn linear(
133 x: &[f32],
134 weight: &[f32],
135 bias: Option<&[f32]>,
136 m: usize,
137 k: usize,
138 n: usize,
139 out: &mut [f32],
140) {
141 linear_with_accumulation(x, weight, bias, m, k, n, F32LinearAccumulation::Scalar, out);
142}
143
144#[allow(clippy::too_many_arguments)]
149pub fn linear_with_accumulation(
150 x: &[f32],
151 weight: &[f32],
152 bias: Option<&[f32]>,
153 m: usize,
154 k: usize,
155 n: usize,
156 accumulation: F32LinearAccumulation,
157 out: &mut [f32],
158) {
159 assert_eq!(x.len(), m * k, "x must be [m, k]");
160 assert_eq!(weight.len(), n * k, "weight must be [n, k]");
161 assert_eq!(out.len(), m * n, "out must be [m, n]");
162 if let Some(bias) = bias {
163 assert_eq!(bias.len(), n, "bias must be [n]");
164 }
165
166 if matches!(
167 accumulation,
168 F32LinearAccumulation::AccelerateBiasSeeded
169 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
170 ) {
171 match bias {
174 Some(bias) => {
175 for row in out.chunks_exact_mut(n) {
176 row.copy_from_slice(bias);
177 }
178 }
179 None => out.fill(0.0),
180 }
181 let row_invariant = accumulation == F32LinearAccumulation::AccelerateBiasSeededRowInvariant;
182 if accelerate_sgemm(x, weight, m, k, n, 1.0, row_invariant, out) {
183 return;
184 }
185 out.fill(0.0);
186 }
187
188 if matches!(
189 accumulation,
190 F32LinearAccumulation::Accelerate | F32LinearAccumulation::AccelerateRowInvariant
191 ) && accelerate_sgemm(
192 x,
193 weight,
194 m,
195 k,
196 n,
197 0.0,
198 accumulation == F32LinearAccumulation::AccelerateRowInvariant,
199 out,
200 ) {
201 if let Some(bias) = bias {
202 for row in out.chunks_exact_mut(n) {
203 for (value, offset) in row.iter_mut().zip(bias) {
204 *value += offset;
205 }
206 }
207 }
208 return;
209 }
210
211 let packed_preserves_order = matches!(
244 accumulation,
245 F32LinearAccumulation::Scalar
246 | F32LinearAccumulation::Accelerate
247 | F32LinearAccumulation::AccelerateRowInvariant
248 | F32LinearAccumulation::AccelerateBiasSeeded
249 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
250 );
251 if m > 1 && packed_preserves_order {
252 const TEAM_FLOOR: usize = 64 * 1024;
260 if m * n >= TEAM_FLOOR
261 && !crate::team::thread_bypassed()
262 && let Some(team) = crate::team::armed()
263 {
264 team.linear_f32(x, weight, bias, m, k, n, out);
265 return;
266 }
267 crate::packed_gemm::linear_packed(x, weight, bias, m, k, n, out);
268 return;
269 }
270
271 for row in 0..m {
272 let x_row = &x[row * k..row * k + k];
273 for col in 0..n {
274 let w_row = &weight[col * k..col * k + k];
275 let sum = dot_with_accumulation(x_row, w_row, accumulation);
276 out[row * n + col] = bias.map_or(sum, |b| sum + b[col]);
277 }
278 }
279}
280
281fn dot_with_accumulation(x: &[f32], weight: &[f32], accumulation: F32LinearAccumulation) -> f32 {
282 assert_eq!(x.len(), weight.len(), "dot-product inputs must match");
283 match accumulation {
284 F32LinearAccumulation::Scalar => {
285 let mut sum = 0.0f32;
286 for index in 0..x.len() {
287 sum += x[index] * weight[index];
288 }
289 sum
290 }
291 F32LinearAccumulation::WidenedF64 => {
292 let mut sum = 0.0f64;
293 for index in 0..x.len() {
294 sum += f64::from(x[index]) * f64::from(weight[index]);
295 }
296 sum as f32
297 }
298 F32LinearAccumulation::Lanes4
299 | F32LinearAccumulation::Lanes8
300 | F32LinearAccumulation::FusedLanes4
301 | F32LinearAccumulation::FusedLanes8
302 | F32LinearAccumulation::Accelerate
303 | F32LinearAccumulation::AccelerateRowInvariant
304 | F32LinearAccumulation::AccelerateBiasSeeded
305 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => {
306 let lanes = match accumulation {
307 F32LinearAccumulation::Lanes4 => 4,
308 F32LinearAccumulation::Lanes8 => 8,
309 F32LinearAccumulation::FusedLanes4 => 4,
310 F32LinearAccumulation::FusedLanes8 => 8,
311 F32LinearAccumulation::Accelerate
312 | F32LinearAccumulation::AccelerateRowInvariant
313 | F32LinearAccumulation::AccelerateBiasSeeded
314 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => {
315 1
331 }
332 F32LinearAccumulation::Scalar | F32LinearAccumulation::WidenedF64 => {
333 unreachable!("scalar and widened orders are handled above")
334 }
335 };
336 let mut partial = [0.0f32; 8];
337 for index in 0..x.len() {
338 let lane = index % lanes;
339 partial[lane] = match accumulation {
340 F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
341 x[index].mul_add(weight[index], partial[lane])
342 }
343 F32LinearAccumulation::Scalar
344 | F32LinearAccumulation::Lanes4
345 | F32LinearAccumulation::Lanes8
346 | F32LinearAccumulation::Accelerate
347 | F32LinearAccumulation::AccelerateRowInvariant
348 | F32LinearAccumulation::AccelerateBiasSeeded
349 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
350 | F32LinearAccumulation::WidenedF64 => partial[lane] + x[index] * weight[index],
351 };
352 }
353 let mut sum = 0.0f32;
354 for value in &partial[..lanes] {
355 sum += *value;
356 }
357 sum
358 }
359 }
360}
361
362#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
368#[allow(clippy::too_many_arguments)]
369fn accelerate_sgemm(
370 x: &[f32],
371 weight: &[f32],
372 m: usize,
373 k: usize,
374 n: usize,
375 beta: f32,
376 row_invariant: bool,
377 out: &mut [f32],
378) -> bool {
379 if row_invariant && m == 1 {
388 let mut doubled_x = Vec::with_capacity(2 * k);
389 doubled_x.extend_from_slice(x);
390 doubled_x.extend_from_slice(x);
391 let mut doubled_out = Vec::with_capacity(2 * n);
392 doubled_out.extend_from_slice(out);
393 doubled_out.extend_from_slice(out);
394 if !accelerate_sgemm(&doubled_x, weight, 2, k, n, beta, false, &mut doubled_out) {
395 return false;
396 }
397 out.copy_from_slice(&doubled_out[..n]);
398 return true;
399 }
400 let m = i32::try_from(m).expect("SGEMM rows fit CBLAS i32 dimensions");
401 let k = i32::try_from(k).expect("SGEMM reduction fits CBLAS i32 dimensions");
402 let n = i32::try_from(n).expect("SGEMM columns fit CBLAS i32 dimensions");
403 unsafe {
407 cblas_sgemm(
408 CBLAS_ROW_MAJOR,
409 CBLAS_NO_TRANSPOSE,
410 CBLAS_TRANSPOSE,
411 m,
412 n,
413 k,
414 1.0,
415 x.as_ptr(),
416 k,
417 weight.as_ptr(),
418 k,
419 beta,
420 out.as_mut_ptr(),
421 n,
422 );
423 }
424 true
425}
426
427#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
428fn accelerate_sgemm(
429 _x: &[f32],
430 _weight: &[f32],
431 _m: usize,
432 _k: usize,
433 _n: usize,
434 _beta: f32,
435 _row_invariant: bool,
436 _out: &mut [f32],
437) -> bool {
438 false
439}
440
441#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
442const CBLAS_ROW_MAJOR: i32 = 101;
443#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
444const CBLAS_NO_TRANSPOSE: i32 = 111;
445#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
446const CBLAS_TRANSPOSE: i32 = 112;
447
448#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
449#[link(name = "Accelerate", kind = "framework")]
450unsafe extern "C" {
451 fn cblas_sgemm(
452 order: i32,
453 trans_a: i32,
454 trans_b: i32,
455 m: i32,
456 n: i32,
457 k: i32,
458 alpha: f32,
459 a: *const f32,
460 lda: i32,
461 b: *const f32,
462 ldb: i32,
463 beta: f32,
464 c: *mut f32,
465 ldc: i32,
466 );
467}
468
469#[derive(Clone, Copy, Debug, Eq, PartialEq)]
478pub enum F32Transcendental {
479 ScalarLibm,
481 AccelerateVForce,
485 SleefU10,
491}
492
493pub fn sin_with(x: &[f32], implementation: F32Transcendental, out: &mut [f32]) {
499 assert_eq!(x.len(), out.len(), "sin output must match its input");
500 if implementation == F32Transcendental::AccelerateVForce && vforce_sin(x, out) {
501 return;
502 }
503 if implementation == F32Transcendental::SleefU10 {
504 for (value, target) in x.iter().zip(out.iter_mut()) {
505 *target = crate::sleef::sinf_u10(*value);
506 }
507 return;
508 }
509 for (value, target) in x.iter().zip(out.iter_mut()) {
510 *target = value.sin();
511 }
512}
513
514pub fn exp_with(x: &[f32], implementation: F32Transcendental, out: &mut [f32]) {
520 assert_eq!(x.len(), out.len(), "exp output must match its input");
521 if implementation == F32Transcendental::AccelerateVForce && vforce_exp(x, out) {
522 return;
523 }
524 if implementation == F32Transcendental::SleefU10 {
525 for (value, target) in x.iter().zip(out.iter_mut()) {
526 *target = crate::sleef::expf_u10(*value);
527 }
528 return;
529 }
530 for (value, target) in x.iter().zip(out.iter_mut()) {
531 *target = value.exp();
532 }
533}
534
535#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
536fn vforce_sin(x: &[f32], out: &mut [f32]) -> bool {
537 let count = i32::try_from(x.len()).expect("vForce length fits i32");
538 unsafe { vvsinf(out.as_mut_ptr(), x.as_ptr(), &raw const count) };
541 true
542}
543
544#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
545fn vforce_exp(x: &[f32], out: &mut [f32]) -> bool {
546 let count = i32::try_from(x.len()).expect("vForce length fits i32");
547 unsafe { vvexpf(out.as_mut_ptr(), x.as_ptr(), &raw const count) };
549 true
550}
551
552#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
553fn vforce_sin(_x: &[f32], _out: &mut [f32]) -> bool {
554 false
555}
556
557#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
558fn vforce_exp(_x: &[f32], _out: &mut [f32]) -> bool {
559 false
560}
561
562#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
563#[link(name = "Accelerate", kind = "framework")]
564unsafe extern "C" {
565 fn vvsinf(out: *mut f32, x: *const f32, count: *const i32);
566 fn vvexpf(out: *mut f32, x: *const f32, count: *const i32);
567}
568
569pub fn rms_norm(x: &[f32], weight: &[f32], eps: f32, rows: usize, dim: usize, out: &mut [f32]) {
575 rms_norm_with_arithmetic(
576 x,
577 weight,
578 eps,
579 rows,
580 dim,
581 F32RmsNormArithmetic::ScalarReciprocalSqrt,
582 out,
583 );
584}
585
586pub fn rms_norm_with_arithmetic(
591 x: &[f32],
592 weight: &[f32],
593 eps: f32,
594 rows: usize,
595 dim: usize,
596 arithmetic: F32RmsNormArithmetic,
597 out: &mut [f32],
598) {
599 assert_eq!(x.len(), rows * dim, "x must be [rows, dim]");
600 assert_eq!(weight.len(), dim, "weight must be [dim]");
601 assert_eq!(out.len(), rows * dim, "out must be [rows, dim]");
602
603 for row in 0..rows {
604 let src = &x[row * dim..row * dim + dim];
605 let scale = rms_scale(src, eps, arithmetic);
606 for index in 0..dim {
607 out[row * dim + index] = src[index] * scale * weight[index];
608 }
609 }
610}
611
612fn rms_scale(src: &[f32], eps: f32, arithmetic: F32RmsNormArithmetic) -> f32 {
613 match arithmetic {
614 F32RmsNormArithmetic::ScalarReciprocalSqrt => {
615 let sum = sum_squares_f32(src, 1);
616 (sum / src.len() as f32 + eps).sqrt().recip()
617 }
618 F32RmsNormArithmetic::ScalarDivideSqrt => {
619 let sum = sum_squares_f32(src, 1);
620 1.0f32 / (sum / src.len() as f32 + eps).sqrt()
621 }
622 F32RmsNormArithmetic::Lanes4ReciprocalSqrt => {
623 let sum = sum_squares_f32(src, 4);
624 (sum / src.len() as f32 + eps).sqrt().recip()
625 }
626 F32RmsNormArithmetic::Lanes8ReciprocalSqrt => {
627 let sum = sum_squares_f32(src, 8);
628 (sum / src.len() as f32 + eps).sqrt().recip()
629 }
630 F32RmsNormArithmetic::Lanes16ReciprocalSqrt => {
631 let sum = sum_squares_f32(src, 16);
632 (sum / src.len() as f32 + eps).sqrt().recip()
633 }
634 F32RmsNormArithmetic::Lanes32ReciprocalSqrt => {
635 let sum = sum_squares_f32(src, 32);
636 (sum / src.len() as f32 + eps).sqrt().recip()
637 }
638 F32RmsNormArithmetic::TorchCascade4ReciprocalSqrt => {
639 let sum = torch_cascade_sum(src, 4, |value| value * value);
640 (sum / src.len() as f32 + eps).sqrt().recip()
641 }
642 F32RmsNormArithmetic::TorchCascade8ReciprocalSqrt => {
643 let sum = torch_cascade_sum(src, 8, |value| value * value);
644 (sum / src.len() as f32 + eps).sqrt().recip()
645 }
646 F32RmsNormArithmetic::F64ReciprocalSqrt => {
647 let mut sum = 0.0f64;
648 for value in src {
649 let value = f64::from(*value);
650 sum += value * value;
651 }
652 (sum / src.len() as f64 + f64::from(eps)).sqrt().recip() as f32
653 }
654 }
655}
656
657fn sum_squares_f32(src: &[f32], lanes: usize) -> f32 {
658 let mut partial = [0.0f32; 32];
659 for (index, value) in src.iter().enumerate() {
660 partial[index % lanes] += *value * *value;
661 }
662 let mut sum = 0.0f32;
663 for value in &partial[..lanes] {
664 sum += *value;
665 }
666 sum
667}
668
669#[allow(clippy::needless_range_loop)]
702pub fn torch_cascade_sum(src: &[f32], width: usize, transform: impl Fn(f32) -> f32) -> f32 {
703 assert!(width > 0, "vector width must be positive");
704 const ILP: usize = 4;
705 const LEVELS: usize = 4;
706
707 let vector_count = src.len() / width;
708 let vector = |index: usize, lane: usize| transform(src[index * width + lane]);
709
710 let size = vector_count / ILP;
712 let level_power = ceil_log2(size).div_euclid(LEVELS).max(4);
713 let level_step = 1usize << level_power;
714 let level_mask = level_step - 1;
715
716 let mut acc = vec![[0.0f32; ILP].map(|_| vec![0.0f32; width]); LEVELS];
717 let mut index = 0usize;
718 while index + level_step <= size {
719 for _ in 0..level_step {
720 for chain in 0..ILP {
721 for lane in 0..width {
722 acc[0][chain][lane] += vector(index * ILP + chain, lane);
723 }
724 }
725 index += 1;
726 }
727 for level in 1..LEVELS {
728 for chain in 0..ILP {
729 for lane in 0..width {
730 acc[level][chain][lane] += acc[level - 1][chain][lane];
731 acc[level - 1][chain][lane] = 0.0;
732 }
733 }
734 if index & (level_mask << (level * level_power)) != 0 {
735 break;
736 }
737 }
738 }
739 while index < size {
740 for chain in 0..ILP {
741 for lane in 0..width {
742 acc[0][chain][lane] += vector(index * ILP + chain, lane);
743 }
744 }
745 index += 1;
746 }
747 for level in 1..LEVELS {
748 for chain in 0..ILP {
749 for lane in 0..width {
750 acc[level][chain][lane] += acc[level - 1][chain][lane];
751 }
752 }
753 }
754
755 let mut partial = acc.swap_remove(LEVELS - 1);
757 for leftover in size * ILP..vector_count {
758 for lane in 0..width {
759 partial[0][lane] += vector(leftover, lane);
760 }
761 }
762 for chain in 1..ILP {
763 for lane in 0..width {
764 partial[0][lane] += partial[chain][lane];
765 }
766 }
767
768 let mut sum = 0.0f32;
771 for index in vector_count * width..src.len() {
772 sum += transform(src[index]);
773 }
774 for lane in 0..width {
775 sum += partial[0][lane];
776 }
777 sum
778}
779
780fn ceil_log2(value: usize) -> usize {
782 if value <= 1 {
783 return 0;
784 }
785 usize::BITS as usize - (value - 1).leading_zeros() as usize
786}
787
788pub fn silu_mul_in_place(gate: &mut [f32], up: &[f32]) {
790 silu_mul_in_place_with_arithmetic(gate, up, F32SiluArithmetic::Divide);
791}
792
793pub fn silu_mul_in_place_with_arithmetic(
795 gate: &mut [f32],
796 up: &[f32],
797 arithmetic: F32SiluArithmetic,
798) {
799 assert_eq!(gate.len(), up.len(), "gate and up must match");
800 for (g, u) in gate.iter_mut().zip(up) {
801 let x = *g;
802 if arithmetic == F32SiluArithmetic::WidenedF64 {
803 let wide = f64::from(x);
804 *g = (wide / (1.0 + (-wide).exp()) * f64::from(*u)) as f32;
805 continue;
806 }
807 let denominator = 1.0 + (-x).exp();
808 let silu = match arithmetic {
809 F32SiluArithmetic::Divide => x / denominator,
810 F32SiluArithmetic::MultiplyReciprocal => x * denominator.recip(),
811 F32SiluArithmetic::WidenedF64 => unreachable!("handled above"),
812 };
813 *g = silu * u;
814 }
815}
816
817pub fn softmax_rows(x: &mut [f32], rows: usize, cols: usize) {
819 softmax_rows_with_arithmetic(x, rows, cols, F32SoftmaxArithmetic::ReciprocalMultiply);
820}
821
822pub fn softmax_rows_with_arithmetic(
824 x: &mut [f32],
825 rows: usize,
826 cols: usize,
827 arithmetic: F32SoftmaxArithmetic,
828) {
829 assert_eq!(x.len(), rows * cols, "x must be [rows, cols]");
830 for row in 0..rows {
831 let slice = &mut x[row * cols..row * cols + cols];
832 let mut max = f32::NEG_INFINITY;
833 for value in slice.iter() {
834 if *value > max {
835 max = *value;
836 }
837 }
838 if arithmetic == F32SoftmaxArithmetic::WidenedF64 {
839 let max = f64::from(max);
840 let mut wide = Vec::with_capacity(slice.len());
841 let mut sum = 0.0f64;
842 for value in slice.iter() {
843 let exponent = (f64::from(*value) - max).exp();
844 sum += exponent;
845 wide.push(exponent);
846 }
847 for (value, exponent) in slice.iter_mut().zip(wide) {
848 *value = (exponent / sum) as f32;
849 }
850 continue;
851 }
852 let mut sum = 0.0f32;
853 for value in slice.iter_mut() {
854 *value = (*value - max).exp();
855 sum += *value;
856 }
857 for value in slice.iter_mut() {
858 *value = match arithmetic {
859 F32SoftmaxArithmetic::ReciprocalMultiply => *value * sum.recip(),
860 F32SoftmaxArithmetic::Divide => *value / sum,
861 F32SoftmaxArithmetic::WidenedF64 => unreachable!("handled above"),
862 };
863 }
864 }
865}
866
867#[allow(clippy::too_many_arguments)]
879pub fn gqa_attention(
880 queries: &[f32],
881 keys: &[f32],
882 values: &[f32],
883 additive_mask: &[f32],
884 query_positions: usize,
885 key_positions: usize,
886 q_heads: usize,
887 kv_heads: usize,
888 head_dim: usize,
889 out: &mut [f32],
890) {
891 gqa_attention_with_softmax(
892 queries,
893 keys,
894 values,
895 additive_mask,
896 query_positions,
897 key_positions,
898 q_heads,
899 kv_heads,
900 head_dim,
901 F32SoftmaxArithmetic::ReciprocalMultiply,
902 out,
903 );
904}
905
906#[allow(clippy::too_many_arguments)]
908pub fn gqa_attention_with_softmax(
909 queries: &[f32],
910 keys: &[f32],
911 values: &[f32],
912 additive_mask: &[f32],
913 query_positions: usize,
914 key_positions: usize,
915 q_heads: usize,
916 kv_heads: usize,
917 head_dim: usize,
918 softmax_arithmetic: F32SoftmaxArithmetic,
919 out: &mut [f32],
920) {
921 gqa_attention_with_arithmetic(
922 queries,
923 keys,
924 values,
925 additive_mask,
926 query_positions,
927 key_positions,
928 q_heads,
929 kv_heads,
930 head_dim,
931 softmax_arithmetic,
932 F32LinearAccumulation::Scalar,
933 out,
934 );
935}
936
937#[allow(clippy::too_many_arguments)]
939pub fn gqa_attention_with_arithmetic(
940 queries: &[f32],
941 keys: &[f32],
942 values: &[f32],
943 additive_mask: &[f32],
944 query_positions: usize,
945 key_positions: usize,
946 q_heads: usize,
947 kv_heads: usize,
948 head_dim: usize,
949 softmax_arithmetic: F32SoftmaxArithmetic,
950 accumulation: F32LinearAccumulation,
951 out: &mut [f32],
952) {
953 assert!(kv_heads > 0, "at least one KV head is required");
954 assert_eq!(
955 q_heads % kv_heads,
956 0,
957 "query heads must divide evenly into KV groups"
958 );
959 assert_eq!(
960 queries.len(),
961 query_positions * q_heads * head_dim,
962 "queries must be [query_positions, q_heads, head_dim]"
963 );
964 assert_eq!(
965 keys.len(),
966 key_positions * kv_heads * head_dim,
967 "keys must be [key_positions, kv_heads, head_dim]"
968 );
969 assert_eq!(
970 values.len(),
971 key_positions * kv_heads * head_dim,
972 "values must be [key_positions, kv_heads, head_dim]"
973 );
974 assert_eq!(
975 additive_mask.len(),
976 query_positions * key_positions,
977 "mask must be [query_positions, key_positions]"
978 );
979 assert_eq!(
980 out.len(),
981 query_positions * q_heads * head_dim,
982 "out must be [query_positions, q_heads, head_dim]"
983 );
984
985 if accumulation == F32LinearAccumulation::Accelerate
986 && accelerate_gqa_attention(
987 queries,
988 keys,
989 values,
990 additive_mask,
991 query_positions,
992 key_positions,
993 q_heads,
994 kv_heads,
995 head_dim,
996 softmax_arithmetic,
997 out,
998 )
999 {
1000 return;
1001 }
1002
1003 const TEAM_ATTENTION_FLOOR_MADDS: usize = 512 * 1024;
1012 if softmax_arithmetic == F32SoftmaxArithmetic::ReciprocalMultiply
1013 && accumulation == F32LinearAccumulation::Scalar
1014 && q_heads
1015 .saturating_mul(query_positions)
1016 .saturating_mul(key_positions)
1017 .saturating_mul(head_dim)
1018 >= TEAM_ATTENTION_FLOOR_MADDS
1019 && !crate::team::thread_bypassed()
1020 && let Some(team) = crate::team::armed()
1021 {
1022 team.gqa_attention(
1023 queries,
1024 keys,
1025 values,
1026 additive_mask,
1027 query_positions,
1028 key_positions,
1029 q_heads,
1030 kv_heads,
1031 head_dim,
1032 out,
1033 );
1034 return;
1035 }
1036
1037 gqa_attention_head_range_with_arithmetic(
1038 queries,
1039 keys,
1040 values,
1041 additive_mask,
1042 query_positions,
1043 key_positions,
1044 q_heads,
1045 kv_heads,
1046 head_dim,
1047 softmax_arithmetic,
1048 accumulation,
1049 0..q_heads,
1050 out,
1051 );
1052}
1053
1054#[allow(clippy::too_many_arguments)]
1066pub fn gqa_attention_head_range_with_arithmetic(
1067 queries: &[f32],
1068 keys: &[f32],
1069 values: &[f32],
1070 additive_mask: &[f32],
1071 query_positions: usize,
1072 key_positions: usize,
1073 q_heads: usize,
1074 kv_heads: usize,
1075 head_dim: usize,
1076 softmax_arithmetic: F32SoftmaxArithmetic,
1077 accumulation: F32LinearAccumulation,
1078 q_head_range: std::ops::Range<usize>,
1079 out: &mut [f32],
1080) {
1081 assert!(
1082 out.len() >= query_positions * q_heads * head_dim,
1083 "attention output must hold [query_positions, q_heads, head_dim]"
1084 );
1085 let out = out.as_mut_ptr();
1086 unsafe {
1089 gqa_attention_head_range_into(
1090 queries,
1091 keys,
1092 values,
1093 additive_mask,
1094 query_positions,
1095 key_positions,
1096 q_heads,
1097 kv_heads,
1098 head_dim,
1099 softmax_arithmetic,
1100 accumulation,
1101 q_head_range,
1102 out,
1103 );
1104 }
1105}
1106
1107#[allow(clippy::too_many_arguments)]
1124pub(crate) unsafe fn gqa_attention_head_range_into(
1125 queries: &[f32],
1126 keys: &[f32],
1127 values: &[f32],
1128 additive_mask: &[f32],
1129 query_positions: usize,
1130 key_positions: usize,
1131 q_heads: usize,
1132 kv_heads: usize,
1133 head_dim: usize,
1134 softmax_arithmetic: F32SoftmaxArithmetic,
1135 accumulation: F32LinearAccumulation,
1136 q_head_range: std::ops::Range<usize>,
1137 out: *mut f32,
1138) {
1139 assert!(q_head_range.end <= q_heads, "head range exceeds q_heads");
1140 let scale = (head_dim as f32).sqrt().recip();
1141 let kv_group = q_heads / kv_heads;
1142 thread_local! {
1146 static SCORES_SCRATCH: std::cell::RefCell<Vec<f32>> =
1147 const { std::cell::RefCell::new(Vec::new()) };
1148 }
1149 SCORES_SCRATCH.with(|scratch| {
1150 let mut scores_guard = scratch.borrow_mut();
1151 if scores_guard.len() < key_positions {
1152 scores_guard.resize(key_positions, 0.0);
1153 }
1154 let scores = &mut scores_guard[..key_positions];
1155
1156 for query_position in 0..query_positions {
1157 let mask = &additive_mask
1158 [query_position * key_positions..(query_position + 1) * key_positions];
1159 for q_head in q_head_range.clone() {
1160 let kv_head = q_head / kv_group;
1161 let query_base = (query_position * q_heads + q_head) * head_dim;
1162 let query = &queries[query_base..query_base + head_dim];
1163 for (key_position, score) in scores.iter_mut().enumerate() {
1164 let key_base = (key_position * kv_heads + kv_head) * head_dim;
1165 let key = &keys[key_base..key_base + head_dim];
1166 let dot = dot_with_accumulation(query, key, accumulation);
1167 *score = dot * scale + mask[key_position];
1168 }
1169 softmax_rows_with_arithmetic(scores, 1, key_positions, softmax_arithmetic);
1170
1171 let head_out =
1175 unsafe { std::slice::from_raw_parts_mut(out.add(query_base), head_dim) };
1176 attention_weighted_sum(
1177 scores,
1178 values,
1179 kv_head,
1180 kv_heads,
1181 head_dim,
1182 accumulation,
1183 head_out,
1184 );
1185 }
1186 }
1187 });
1188}
1189
1190#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
1196#[allow(clippy::too_many_arguments)]
1197fn accelerate_gqa_attention(
1198 queries: &[f32],
1199 keys: &[f32],
1200 values: &[f32],
1201 additive_mask: &[f32],
1202 query_positions: usize,
1203 key_positions: usize,
1204 q_heads: usize,
1205 kv_heads: usize,
1206 head_dim: usize,
1207 softmax_arithmetic: F32SoftmaxArithmetic,
1208 out: &mut [f32],
1209) -> bool {
1210 let scale = (head_dim as f32).sqrt().recip();
1211 let kv_group = q_heads / kv_heads;
1212 let mut query_matrix = vec![0.0f32; query_positions * head_dim];
1213 let mut key_matrix = vec![0.0f32; key_positions * head_dim];
1214 let mut value_transpose = vec![0.0f32; head_dim * key_positions];
1215 let mut scores = vec![0.0f32; query_positions * key_positions];
1216 let mut context = vec![0.0f32; query_positions * head_dim];
1217
1218 for q_head in 0..q_heads {
1219 let kv_head = q_head / kv_group;
1220 for query_position in 0..query_positions {
1221 let query_base = (query_position * q_heads + q_head) * head_dim;
1222 query_matrix[query_position * head_dim..(query_position + 1) * head_dim]
1223 .copy_from_slice(&queries[query_base..query_base + head_dim]);
1224 }
1225 for key_position in 0..key_positions {
1226 let key_base = (key_position * kv_heads + kv_head) * head_dim;
1227 key_matrix[key_position * head_dim..(key_position + 1) * head_dim]
1228 .copy_from_slice(&keys[key_base..key_base + head_dim]);
1229 for lane in 0..head_dim {
1230 value_transpose[lane * key_positions + key_position] = values[key_base + lane];
1231 }
1232 }
1233
1234 if !accelerate_sgemm(
1235 &query_matrix,
1236 &key_matrix,
1237 query_positions,
1238 head_dim,
1239 key_positions,
1240 0.0,
1241 false,
1242 &mut scores,
1243 ) {
1244 return false;
1245 }
1246 for query_position in 0..query_positions {
1247 let score_row =
1248 &mut scores[query_position * key_positions..(query_position + 1) * key_positions];
1249 let mask = &additive_mask
1250 [query_position * key_positions..(query_position + 1) * key_positions];
1251 for (score, mask_value) in score_row.iter_mut().zip(mask) {
1252 *score = *score * scale + mask_value;
1253 }
1254 softmax_rows_with_arithmetic(score_row, 1, key_positions, softmax_arithmetic);
1255 }
1256 if !accelerate_sgemm(
1257 &scores,
1258 &value_transpose,
1259 query_positions,
1260 key_positions,
1261 head_dim,
1262 0.0,
1263 false,
1264 &mut context,
1265 ) {
1266 return false;
1267 }
1268 for query_position in 0..query_positions {
1269 let out_base = (query_position * q_heads + q_head) * head_dim;
1270 out[out_base..out_base + head_dim].copy_from_slice(
1271 &context[query_position * head_dim..(query_position + 1) * head_dim],
1272 );
1273 }
1274 }
1275 true
1276}
1277
1278#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
1279#[allow(clippy::too_many_arguments)]
1280fn accelerate_gqa_attention(
1281 _queries: &[f32],
1282 _keys: &[f32],
1283 _values: &[f32],
1284 _additive_mask: &[f32],
1285 _query_positions: usize,
1286 _key_positions: usize,
1287 _q_heads: usize,
1288 _kv_heads: usize,
1289 _head_dim: usize,
1290 _softmax_arithmetic: F32SoftmaxArithmetic,
1291 _out: &mut [f32],
1292) -> bool {
1293 false
1294}
1295
1296#[allow(clippy::too_many_arguments)]
1297fn attention_weighted_sum(
1298 scores: &[f32],
1299 values: &[f32],
1300 kv_head: usize,
1301 kv_heads: usize,
1302 head_dim: usize,
1303 accumulation: F32LinearAccumulation,
1304 out: &mut [f32],
1305) {
1306 if accumulation == F32LinearAccumulation::WidenedF64 {
1307 for lane in 0..head_dim {
1308 let mut sum = 0.0f64;
1309 for (key_position, weight) in scores.iter().copied().enumerate() {
1310 let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1311 sum += f64::from(weight) * f64::from(value);
1312 }
1313 out[lane] = sum as f32;
1314 }
1315 return;
1316 }
1317 let lanes = match accumulation {
1318 F32LinearAccumulation::Scalar => 1,
1319 F32LinearAccumulation::Lanes4 | F32LinearAccumulation::FusedLanes4 => 4,
1320 F32LinearAccumulation::Lanes8 | F32LinearAccumulation::FusedLanes8 => 8,
1321 F32LinearAccumulation::Accelerate
1322 | F32LinearAccumulation::AccelerateRowInvariant
1323 | F32LinearAccumulation::AccelerateBiasSeeded
1324 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => 1,
1325 F32LinearAccumulation::WidenedF64 => unreachable!("handled above"),
1326 };
1327 for lane in 0..head_dim {
1328 let mut partial = [0.0f32; 8];
1329 for (key_position, weight) in scores.iter().copied().enumerate() {
1330 let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1331 let partial_index = key_position % lanes;
1332 partial[partial_index] = match accumulation {
1333 F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
1334 weight.mul_add(value, partial[partial_index])
1335 }
1336 F32LinearAccumulation::Scalar
1337 | F32LinearAccumulation::Lanes4
1338 | F32LinearAccumulation::Lanes8
1339 | F32LinearAccumulation::Accelerate
1340 | F32LinearAccumulation::AccelerateRowInvariant
1341 | F32LinearAccumulation::AccelerateBiasSeeded
1342 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
1343 | F32LinearAccumulation::WidenedF64 => partial[partial_index] + weight * value,
1344 };
1345 }
1346 let mut sum = 0.0f32;
1347 for value in &partial[..lanes] {
1348 sum += *value;
1349 }
1350 out[lane] = sum;
1351 }
1352}
1353
1354pub fn mrope_interleave(axes: [&[f32]; 3], sections: [usize; 3], out: &mut [f32]) {
1369 let half = out.len();
1370 for axis in axes {
1371 assert!(
1372 axis.len() >= half,
1373 "axis row shorter than the half-dimension"
1374 );
1375 }
1376
1377 out.copy_from_slice(&axes[0][..half]);
1380 let modality_num = 3usize;
1381 for (axis_index, section) in sections.iter().enumerate().skip(1) {
1382 let end = section * modality_num;
1383 let mut lane = axis_index;
1384 while lane < end && lane < half {
1385 out[lane] = axes[axis_index][lane];
1386 lane += modality_num;
1387 }
1388 }
1389}
1390
1391pub fn apply_rope_in_place(row: &mut [f32], cos: &[f32], sin: &[f32]) {
1401 let dim = row.len();
1402 assert_eq!(cos.len(), dim, "cos must match head_dim");
1403 assert_eq!(sin.len(), dim, "sin must match head_dim");
1404 assert!(dim.is_multiple_of(2), "head_dim must be even");
1405
1406 let half = dim / 2;
1407 let original: Vec<f32> = row.to_vec();
1408 for index in 0..dim {
1409 let rotated = if index < half {
1410 -original[index + half]
1411 } else {
1412 original[index - half]
1413 };
1414 row[index] = original[index] * cos[index] + rotated * sin[index];
1415 }
1416}
1417
1418#[cfg(test)]
1419mod tests {
1420 use super::*;
1421
1422 #[test]
1423 fn linear_matches_a_hand_computed_product() {
1424 let x = [1.0, 2.0, 3.0];
1426 let weight = [1.0, 0.0, -1.0, 2.0, 2.0, 2.0];
1427 let mut out = [0.0; 2];
1428 linear(&x, &weight, None, 1, 3, 2, &mut out);
1429 assert_eq!(out, [-2.0, 12.0]);
1430
1431 let mut biased = [0.0; 2];
1432 linear(&x, &weight, Some(&[10.0, -12.0]), 1, 3, 2, &mut biased);
1433 assert_eq!(biased, [8.0, 0.0]);
1434 }
1435
1436 #[test]
1437 fn torch_cascade_sum_agrees_with_the_flat_sum_when_rounding_cannot_intervene() {
1438 for length in [1usize, 7, 8, 15, 16, 31, 128, 1024, 3072] {
1442 for width in [4usize, 8] {
1443 let values: Vec<f32> = (0..length).map(|index| (index % 8) as f32).collect();
1444 let flat: f32 = values.iter().sum();
1445 assert_eq!(
1446 torch_cascade_sum(&values, width, |value| value),
1447 flat,
1448 "length {length}, width {width}"
1449 );
1450 }
1451 }
1452 }
1453
1454 #[test]
1455 fn torch_cascade_sum_applies_its_transform_before_accumulating() {
1456 let values = [1.0f32, 2.0, 3.0, 4.0, 5.0];
1457 assert_eq!(torch_cascade_sum(&values, 4, |value| value * value), 55.0);
1458 }
1459
1460 #[test]
1461 fn torch_cascade_sum_differs_from_a_flat_sum_once_rounding_matters() {
1462 let mut values = vec![1.0f32; 1024];
1467 values[0] = 1.0e8;
1468 let flat = values.iter().fold(0.0f32, |sum, value| sum + value);
1469 assert_ne!(torch_cascade_sum(&values, 8, |value| value), flat);
1470 }
1471
1472 #[test]
1473 fn ceil_log2_matches_its_definition() {
1474 assert_eq!(ceil_log2(0), 0);
1475 assert_eq!(ceil_log2(1), 0);
1476 assert_eq!(ceil_log2(2), 1);
1477 assert_eq!(ceil_log2(3), 2);
1478 assert_eq!(ceil_log2(32), 5);
1479 assert_eq!(ceil_log2(33), 6);
1480 }
1481
1482 #[test]
1483 fn rms_norm_normalizes_and_scales() {
1484 let x = [3.0f32, 4.0];
1486 let weight = [1.0f32, 1.0];
1487 let mut out = [0.0; 2];
1488 rms_norm(&x, &weight, 0.0, 1, 2, &mut out);
1489 let expected = 12.5f32.sqrt().recip();
1490 assert!((out[0] - 3.0 * expected).abs() < 1e-6);
1491 assert!((out[1] - 4.0 * expected).abs() < 1e-6);
1492
1493 let mut weighted = [0.0; 2];
1495 rms_norm(&x, &[2.0, 0.5], 0.0, 1, 2, &mut weighted);
1496 assert!((weighted[0] - 3.0 * expected * 2.0).abs() < 1e-6);
1497 assert!((weighted[1] - 4.0 * expected * 0.5).abs() < 1e-6);
1498 }
1499
1500 #[test]
1501 fn silu_mul_matches_the_definition() {
1502 let mut gate = [0.0f32, 1.0, -1.0];
1503 let up = [1.0f32, 2.0, 3.0];
1504 silu_mul_in_place(&mut gate, &up);
1505 assert_eq!(gate[0], 0.0);
1506 let silu_one = 1.0f32 / (1.0 + (-1.0f32).exp());
1507 assert!((gate[1] - silu_one * 2.0).abs() < 1e-6);
1508 let silu_neg = -1.0f32 / (1.0 + 1.0f32.exp());
1509 assert!((gate[2] - silu_neg * 3.0).abs() < 1e-6);
1510 }
1511
1512 #[test]
1513 fn softmax_rows_sums_to_one_and_is_shift_invariant() {
1514 let mut x = [1.0f32, 2.0, 3.0, 101.0, 102.0, 103.0];
1515 softmax_rows(&mut x, 2, 3);
1516 let first: f32 = x[..3].iter().sum();
1517 let second: f32 = x[3..].iter().sum();
1518 assert!((first - 1.0).abs() < 1e-6);
1519 assert!((second - 1.0).abs() < 1e-6);
1520 for index in 0..3 {
1522 assert!((x[index] - x[index + 3]).abs() < 1e-6);
1523 }
1524 }
1525
1526 #[test]
1527 fn gqa_maps_each_query_head_to_its_kv_group() {
1528 let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (1, 1, 4, 2, 2);
1529 let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1530 let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1531 let values = [10.0f32, 11.0, 20.0, 21.0];
1532 let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1533
1534 gqa_attention(
1535 &queries,
1536 &keys,
1537 &values,
1538 &[0.0],
1539 query_positions,
1540 key_positions,
1541 q_heads,
1542 kv_heads,
1543 head_dim,
1544 &mut out,
1545 );
1546
1547 assert_eq!(&out[0..2], &[10.0, 11.0]);
1548 assert_eq!(&out[2..4], &[10.0, 11.0]);
1549 assert_eq!(&out[4..6], &[20.0, 21.0]);
1550 assert_eq!(&out[6..8], &[20.0, 21.0]);
1551 }
1552
1553 #[test]
1554 fn gqa_honors_the_additive_causal_mask() {
1555 let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (2, 2, 1, 1, 2);
1556 let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1557 let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1558 let values = [2.0f32, 4.0, 10.0, 20.0];
1559 let mask = [0.0f32, f32::NEG_INFINITY, 0.0, 0.0];
1560 let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1561
1562 gqa_attention(
1563 &queries,
1564 &keys,
1565 &values,
1566 &mask,
1567 query_positions,
1568 key_positions,
1569 q_heads,
1570 kv_heads,
1571 head_dim,
1572 &mut out,
1573 );
1574
1575 assert_eq!(&out[0..2], &[2.0, 4.0]);
1576 assert_eq!(&out[2..4], &[6.0, 12.0]);
1577 }
1578
1579 #[test]
1580 fn rope_rotates_a_known_pair() {
1581 let mut row = [3.0f32, 5.0];
1583 apply_rope_in_place(&mut row, &[0.0, 0.0], &[1.0, 1.0]);
1584 assert_eq!(row, [-5.0, 3.0]);
1585
1586 let mut same = [3.0f32, 5.0];
1588 apply_rope_in_place(&mut same, &[1.0, 1.0], &[0.0, 0.0]);
1589 assert_eq!(same, [3.0, 5.0]);
1590 }
1591
1592 #[test]
1593 fn mrope_interleave_is_identity_when_all_axes_agree() {
1594 let axis: Vec<f32> = (0..64).map(|value| value as f32).collect();
1597 let mut out = vec![0.0f32; 64];
1598 mrope_interleave([&axis, &axis, &axis], [24, 20, 20], &mut out);
1599 assert_eq!(out, axis);
1600 }
1601
1602 #[test]
1603 fn mrope_interleave_selects_the_documented_lanes() {
1604 let zeros = vec![0.0f32; 64];
1605 let ones = vec![1.0f32; 64];
1606 let twos = vec![2.0f32; 64];
1607 let mut out = vec![0.0f32; 64];
1608 mrope_interleave([&zeros, &ones, &twos], [24, 20, 20], &mut out);
1609
1610 for (lane, value) in out.iter().enumerate() {
1613 let expected = if lane < 60 && lane % 3 == 1 {
1614 1.0
1615 } else if lane < 60 && lane % 3 == 2 {
1616 2.0
1617 } else {
1618 0.0
1619 };
1620 assert_eq!(*value, expected, "lane {lane}");
1621 }
1622 }
1623}