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 #[cfg(target_arch = "wasm32")]
323 {
324 8
325 }
326 #[cfg(not(target_arch = "wasm32"))]
327 {
328 1
329 }
330 }
331 F32LinearAccumulation::Scalar | F32LinearAccumulation::WidenedF64 => {
332 unreachable!("scalar and widened orders are handled above")
333 }
334 };
335 let mut partial = [0.0f32; 8];
336 for index in 0..x.len() {
337 let lane = index % lanes;
338 partial[lane] = match accumulation {
339 F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
340 x[index].mul_add(weight[index], partial[lane])
341 }
342 F32LinearAccumulation::Scalar
343 | F32LinearAccumulation::Lanes4
344 | F32LinearAccumulation::Lanes8
345 | F32LinearAccumulation::Accelerate
346 | F32LinearAccumulation::AccelerateRowInvariant
347 | F32LinearAccumulation::AccelerateBiasSeeded
348 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
349 | F32LinearAccumulation::WidenedF64 => partial[lane] + x[index] * weight[index],
350 };
351 }
352 let mut sum = 0.0f32;
353 for value in &partial[..lanes] {
354 sum += *value;
355 }
356 sum
357 }
358 }
359}
360
361#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
367#[allow(clippy::too_many_arguments)]
368fn accelerate_sgemm(
369 x: &[f32],
370 weight: &[f32],
371 m: usize,
372 k: usize,
373 n: usize,
374 beta: f32,
375 row_invariant: bool,
376 out: &mut [f32],
377) -> bool {
378 if row_invariant && m == 1 {
387 let mut doubled_x = Vec::with_capacity(2 * k);
388 doubled_x.extend_from_slice(x);
389 doubled_x.extend_from_slice(x);
390 let mut doubled_out = Vec::with_capacity(2 * n);
391 doubled_out.extend_from_slice(out);
392 doubled_out.extend_from_slice(out);
393 if !accelerate_sgemm(&doubled_x, weight, 2, k, n, beta, false, &mut doubled_out) {
394 return false;
395 }
396 out.copy_from_slice(&doubled_out[..n]);
397 return true;
398 }
399 let m = i32::try_from(m).expect("SGEMM rows fit CBLAS i32 dimensions");
400 let k = i32::try_from(k).expect("SGEMM reduction fits CBLAS i32 dimensions");
401 let n = i32::try_from(n).expect("SGEMM columns fit CBLAS i32 dimensions");
402 unsafe {
406 cblas_sgemm(
407 CBLAS_ROW_MAJOR,
408 CBLAS_NO_TRANSPOSE,
409 CBLAS_TRANSPOSE,
410 m,
411 n,
412 k,
413 1.0,
414 x.as_ptr(),
415 k,
416 weight.as_ptr(),
417 k,
418 beta,
419 out.as_mut_ptr(),
420 n,
421 );
422 }
423 true
424}
425
426#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
427fn accelerate_sgemm(
428 _x: &[f32],
429 _weight: &[f32],
430 _m: usize,
431 _k: usize,
432 _n: usize,
433 _beta: f32,
434 _row_invariant: bool,
435 _out: &mut [f32],
436) -> bool {
437 false
438}
439
440#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
441const CBLAS_ROW_MAJOR: i32 = 101;
442#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
443const CBLAS_NO_TRANSPOSE: i32 = 111;
444#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
445const CBLAS_TRANSPOSE: i32 = 112;
446
447#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
448#[link(name = "Accelerate", kind = "framework")]
449unsafe extern "C" {
450 fn cblas_sgemm(
451 order: i32,
452 trans_a: i32,
453 trans_b: i32,
454 m: i32,
455 n: i32,
456 k: i32,
457 alpha: f32,
458 a: *const f32,
459 lda: i32,
460 b: *const f32,
461 ldb: i32,
462 beta: f32,
463 c: *mut f32,
464 ldc: i32,
465 );
466}
467
468#[derive(Clone, Copy, Debug, Eq, PartialEq)]
477pub enum F32Transcendental {
478 ScalarLibm,
480 AccelerateVForce,
484 SleefU10,
490}
491
492pub fn sin_with(x: &[f32], implementation: F32Transcendental, out: &mut [f32]) {
498 assert_eq!(x.len(), out.len(), "sin output must match its input");
499 if implementation == F32Transcendental::AccelerateVForce && vforce_sin(x, out) {
500 return;
501 }
502 if implementation == F32Transcendental::SleefU10 {
503 for (value, target) in x.iter().zip(out.iter_mut()) {
504 *target = crate::sleef::sinf_u10(*value);
505 }
506 return;
507 }
508 for (value, target) in x.iter().zip(out.iter_mut()) {
509 *target = value.sin();
510 }
511}
512
513pub fn exp_with(x: &[f32], implementation: F32Transcendental, out: &mut [f32]) {
519 assert_eq!(x.len(), out.len(), "exp output must match its input");
520 if implementation == F32Transcendental::AccelerateVForce && vforce_exp(x, out) {
521 return;
522 }
523 if implementation == F32Transcendental::SleefU10 {
524 for (value, target) in x.iter().zip(out.iter_mut()) {
525 *target = crate::sleef::expf_u10(*value);
526 }
527 return;
528 }
529 for (value, target) in x.iter().zip(out.iter_mut()) {
530 *target = value.exp();
531 }
532}
533
534#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
535fn vforce_sin(x: &[f32], out: &mut [f32]) -> bool {
536 let count = i32::try_from(x.len()).expect("vForce length fits i32");
537 unsafe { vvsinf(out.as_mut_ptr(), x.as_ptr(), &raw const count) };
540 true
541}
542
543#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
544fn vforce_exp(x: &[f32], out: &mut [f32]) -> bool {
545 let count = i32::try_from(x.len()).expect("vForce length fits i32");
546 unsafe { vvexpf(out.as_mut_ptr(), x.as_ptr(), &raw const count) };
548 true
549}
550
551#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
552fn vforce_sin(_x: &[f32], _out: &mut [f32]) -> bool {
553 false
554}
555
556#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
557fn vforce_exp(_x: &[f32], _out: &mut [f32]) -> bool {
558 false
559}
560
561#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
562#[link(name = "Accelerate", kind = "framework")]
563unsafe extern "C" {
564 fn vvsinf(out: *mut f32, x: *const f32, count: *const i32);
565 fn vvexpf(out: *mut f32, x: *const f32, count: *const i32);
566}
567
568pub fn rms_norm(x: &[f32], weight: &[f32], eps: f32, rows: usize, dim: usize, out: &mut [f32]) {
574 rms_norm_with_arithmetic(
575 x,
576 weight,
577 eps,
578 rows,
579 dim,
580 F32RmsNormArithmetic::ScalarReciprocalSqrt,
581 out,
582 );
583}
584
585pub fn rms_norm_with_arithmetic(
590 x: &[f32],
591 weight: &[f32],
592 eps: f32,
593 rows: usize,
594 dim: usize,
595 arithmetic: F32RmsNormArithmetic,
596 out: &mut [f32],
597) {
598 assert_eq!(x.len(), rows * dim, "x must be [rows, dim]");
599 assert_eq!(weight.len(), dim, "weight must be [dim]");
600 assert_eq!(out.len(), rows * dim, "out must be [rows, dim]");
601
602 for row in 0..rows {
603 let src = &x[row * dim..row * dim + dim];
604 let scale = rms_scale(src, eps, arithmetic);
605 for index in 0..dim {
606 out[row * dim + index] = src[index] * scale * weight[index];
607 }
608 }
609}
610
611fn rms_scale(src: &[f32], eps: f32, arithmetic: F32RmsNormArithmetic) -> f32 {
612 match arithmetic {
613 F32RmsNormArithmetic::ScalarReciprocalSqrt => {
614 let sum = sum_squares_f32(src, 1);
615 (sum / src.len() as f32 + eps).sqrt().recip()
616 }
617 F32RmsNormArithmetic::ScalarDivideSqrt => {
618 let sum = sum_squares_f32(src, 1);
619 1.0f32 / (sum / src.len() as f32 + eps).sqrt()
620 }
621 F32RmsNormArithmetic::Lanes4ReciprocalSqrt => {
622 let sum = sum_squares_f32(src, 4);
623 (sum / src.len() as f32 + eps).sqrt().recip()
624 }
625 F32RmsNormArithmetic::Lanes8ReciprocalSqrt => {
626 let sum = sum_squares_f32(src, 8);
627 (sum / src.len() as f32 + eps).sqrt().recip()
628 }
629 F32RmsNormArithmetic::Lanes16ReciprocalSqrt => {
630 let sum = sum_squares_f32(src, 16);
631 (sum / src.len() as f32 + eps).sqrt().recip()
632 }
633 F32RmsNormArithmetic::Lanes32ReciprocalSqrt => {
634 let sum = sum_squares_f32(src, 32);
635 (sum / src.len() as f32 + eps).sqrt().recip()
636 }
637 F32RmsNormArithmetic::TorchCascade4ReciprocalSqrt => {
638 let sum = torch_cascade_sum(src, 4, |value| value * value);
639 (sum / src.len() as f32 + eps).sqrt().recip()
640 }
641 F32RmsNormArithmetic::TorchCascade8ReciprocalSqrt => {
642 let sum = torch_cascade_sum(src, 8, |value| value * value);
643 (sum / src.len() as f32 + eps).sqrt().recip()
644 }
645 F32RmsNormArithmetic::F64ReciprocalSqrt => {
646 let mut sum = 0.0f64;
647 for value in src {
648 let value = f64::from(*value);
649 sum += value * value;
650 }
651 (sum / src.len() as f64 + f64::from(eps)).sqrt().recip() as f32
652 }
653 }
654}
655
656fn sum_squares_f32(src: &[f32], lanes: usize) -> f32 {
657 let mut partial = [0.0f32; 32];
658 for (index, value) in src.iter().enumerate() {
659 partial[index % lanes] += *value * *value;
660 }
661 let mut sum = 0.0f32;
662 for value in &partial[..lanes] {
663 sum += *value;
664 }
665 sum
666}
667
668#[allow(clippy::needless_range_loop)]
701pub fn torch_cascade_sum(src: &[f32], width: usize, transform: impl Fn(f32) -> f32) -> f32 {
702 assert!(width > 0, "vector width must be positive");
703 const ILP: usize = 4;
704 const LEVELS: usize = 4;
705
706 let vector_count = src.len() / width;
707 let vector = |index: usize, lane: usize| transform(src[index * width + lane]);
708
709 let size = vector_count / ILP;
711 let level_power = ceil_log2(size).div_euclid(LEVELS).max(4);
712 let level_step = 1usize << level_power;
713 let level_mask = level_step - 1;
714
715 let mut acc = vec![[0.0f32; ILP].map(|_| vec![0.0f32; width]); LEVELS];
716 let mut index = 0usize;
717 while index + level_step <= size {
718 for _ in 0..level_step {
719 for chain in 0..ILP {
720 for lane in 0..width {
721 acc[0][chain][lane] += vector(index * ILP + chain, lane);
722 }
723 }
724 index += 1;
725 }
726 for level in 1..LEVELS {
727 for chain in 0..ILP {
728 for lane in 0..width {
729 acc[level][chain][lane] += acc[level - 1][chain][lane];
730 acc[level - 1][chain][lane] = 0.0;
731 }
732 }
733 if index & (level_mask << (level * level_power)) != 0 {
734 break;
735 }
736 }
737 }
738 while index < size {
739 for chain in 0..ILP {
740 for lane in 0..width {
741 acc[0][chain][lane] += vector(index * ILP + chain, lane);
742 }
743 }
744 index += 1;
745 }
746 for level in 1..LEVELS {
747 for chain in 0..ILP {
748 for lane in 0..width {
749 acc[level][chain][lane] += acc[level - 1][chain][lane];
750 }
751 }
752 }
753
754 let mut partial = acc.swap_remove(LEVELS - 1);
756 for leftover in size * ILP..vector_count {
757 for lane in 0..width {
758 partial[0][lane] += vector(leftover, lane);
759 }
760 }
761 for chain in 1..ILP {
762 for lane in 0..width {
763 partial[0][lane] += partial[chain][lane];
764 }
765 }
766
767 let mut sum = 0.0f32;
770 for index in vector_count * width..src.len() {
771 sum += transform(src[index]);
772 }
773 for lane in 0..width {
774 sum += partial[0][lane];
775 }
776 sum
777}
778
779fn ceil_log2(value: usize) -> usize {
781 if value <= 1 {
782 return 0;
783 }
784 usize::BITS as usize - (value - 1).leading_zeros() as usize
785}
786
787pub fn silu_mul_in_place(gate: &mut [f32], up: &[f32]) {
789 silu_mul_in_place_with_arithmetic(gate, up, F32SiluArithmetic::Divide);
790}
791
792pub fn silu_mul_in_place_with_arithmetic(
794 gate: &mut [f32],
795 up: &[f32],
796 arithmetic: F32SiluArithmetic,
797) {
798 assert_eq!(gate.len(), up.len(), "gate and up must match");
799 for (g, u) in gate.iter_mut().zip(up) {
800 let x = *g;
801 if arithmetic == F32SiluArithmetic::WidenedF64 {
802 let wide = f64::from(x);
803 *g = (wide / (1.0 + (-wide).exp()) * f64::from(*u)) as f32;
804 continue;
805 }
806 let denominator = 1.0 + (-x).exp();
807 let silu = match arithmetic {
808 F32SiluArithmetic::Divide => x / denominator,
809 F32SiluArithmetic::MultiplyReciprocal => x * denominator.recip(),
810 F32SiluArithmetic::WidenedF64 => unreachable!("handled above"),
811 };
812 *g = silu * u;
813 }
814}
815
816pub fn softmax_rows(x: &mut [f32], rows: usize, cols: usize) {
818 softmax_rows_with_arithmetic(x, rows, cols, F32SoftmaxArithmetic::ReciprocalMultiply);
819}
820
821pub fn softmax_rows_with_arithmetic(
823 x: &mut [f32],
824 rows: usize,
825 cols: usize,
826 arithmetic: F32SoftmaxArithmetic,
827) {
828 assert_eq!(x.len(), rows * cols, "x must be [rows, cols]");
829 for row in 0..rows {
830 let slice = &mut x[row * cols..row * cols + cols];
831 let mut max = f32::NEG_INFINITY;
832 for value in slice.iter() {
833 if *value > max {
834 max = *value;
835 }
836 }
837 if arithmetic == F32SoftmaxArithmetic::WidenedF64 {
838 let max = f64::from(max);
839 let mut wide = Vec::with_capacity(slice.len());
840 let mut sum = 0.0f64;
841 for value in slice.iter() {
842 let exponent = (f64::from(*value) - max).exp();
843 sum += exponent;
844 wide.push(exponent);
845 }
846 for (value, exponent) in slice.iter_mut().zip(wide) {
847 *value = (exponent / sum) as f32;
848 }
849 continue;
850 }
851 let mut sum = 0.0f32;
852 for value in slice.iter_mut() {
853 *value = (*value - max).exp();
854 sum += *value;
855 }
856 for value in slice.iter_mut() {
857 *value = match arithmetic {
858 F32SoftmaxArithmetic::ReciprocalMultiply => *value * sum.recip(),
859 F32SoftmaxArithmetic::Divide => *value / sum,
860 F32SoftmaxArithmetic::WidenedF64 => unreachable!("handled above"),
861 };
862 }
863 }
864}
865
866#[allow(clippy::too_many_arguments)]
878pub fn gqa_attention(
879 queries: &[f32],
880 keys: &[f32],
881 values: &[f32],
882 additive_mask: &[f32],
883 query_positions: usize,
884 key_positions: usize,
885 q_heads: usize,
886 kv_heads: usize,
887 head_dim: usize,
888 out: &mut [f32],
889) {
890 gqa_attention_with_softmax(
891 queries,
892 keys,
893 values,
894 additive_mask,
895 query_positions,
896 key_positions,
897 q_heads,
898 kv_heads,
899 head_dim,
900 F32SoftmaxArithmetic::ReciprocalMultiply,
901 out,
902 );
903}
904
905#[allow(clippy::too_many_arguments)]
907pub fn gqa_attention_with_softmax(
908 queries: &[f32],
909 keys: &[f32],
910 values: &[f32],
911 additive_mask: &[f32],
912 query_positions: usize,
913 key_positions: usize,
914 q_heads: usize,
915 kv_heads: usize,
916 head_dim: usize,
917 softmax_arithmetic: F32SoftmaxArithmetic,
918 out: &mut [f32],
919) {
920 gqa_attention_with_arithmetic(
921 queries,
922 keys,
923 values,
924 additive_mask,
925 query_positions,
926 key_positions,
927 q_heads,
928 kv_heads,
929 head_dim,
930 softmax_arithmetic,
931 F32LinearAccumulation::Scalar,
932 out,
933 );
934}
935
936#[allow(clippy::too_many_arguments)]
938pub fn gqa_attention_with_arithmetic(
939 queries: &[f32],
940 keys: &[f32],
941 values: &[f32],
942 additive_mask: &[f32],
943 query_positions: usize,
944 key_positions: usize,
945 q_heads: usize,
946 kv_heads: usize,
947 head_dim: usize,
948 softmax_arithmetic: F32SoftmaxArithmetic,
949 accumulation: F32LinearAccumulation,
950 out: &mut [f32],
951) {
952 assert!(kv_heads > 0, "at least one KV head is required");
953 assert_eq!(
954 q_heads % kv_heads,
955 0,
956 "query heads must divide evenly into KV groups"
957 );
958 assert_eq!(
959 queries.len(),
960 query_positions * q_heads * head_dim,
961 "queries must be [query_positions, q_heads, head_dim]"
962 );
963 assert_eq!(
964 keys.len(),
965 key_positions * kv_heads * head_dim,
966 "keys must be [key_positions, kv_heads, head_dim]"
967 );
968 assert_eq!(
969 values.len(),
970 key_positions * kv_heads * head_dim,
971 "values must be [key_positions, kv_heads, head_dim]"
972 );
973 assert_eq!(
974 additive_mask.len(),
975 query_positions * key_positions,
976 "mask must be [query_positions, key_positions]"
977 );
978 assert_eq!(
979 out.len(),
980 query_positions * q_heads * head_dim,
981 "out must be [query_positions, q_heads, head_dim]"
982 );
983
984 if accumulation == F32LinearAccumulation::Accelerate
985 && accelerate_gqa_attention(
986 queries,
987 keys,
988 values,
989 additive_mask,
990 query_positions,
991 key_positions,
992 q_heads,
993 kv_heads,
994 head_dim,
995 softmax_arithmetic,
996 out,
997 )
998 {
999 return;
1000 }
1001
1002 gqa_attention_head_range_with_arithmetic(
1003 queries,
1004 keys,
1005 values,
1006 additive_mask,
1007 query_positions,
1008 key_positions,
1009 q_heads,
1010 kv_heads,
1011 head_dim,
1012 softmax_arithmetic,
1013 accumulation,
1014 0..q_heads,
1015 out,
1016 );
1017}
1018
1019#[allow(clippy::too_many_arguments)]
1031pub fn gqa_attention_head_range_with_arithmetic(
1032 queries: &[f32],
1033 keys: &[f32],
1034 values: &[f32],
1035 additive_mask: &[f32],
1036 query_positions: usize,
1037 key_positions: usize,
1038 q_heads: usize,
1039 kv_heads: usize,
1040 head_dim: usize,
1041 softmax_arithmetic: F32SoftmaxArithmetic,
1042 accumulation: F32LinearAccumulation,
1043 q_head_range: std::ops::Range<usize>,
1044 out: &mut [f32],
1045) {
1046 assert!(
1047 out.len() >= query_positions * q_heads * head_dim,
1048 "attention output must hold [query_positions, q_heads, head_dim]"
1049 );
1050 let out = out.as_mut_ptr();
1051 unsafe {
1054 gqa_attention_head_range_into(
1055 queries,
1056 keys,
1057 values,
1058 additive_mask,
1059 query_positions,
1060 key_positions,
1061 q_heads,
1062 kv_heads,
1063 head_dim,
1064 softmax_arithmetic,
1065 accumulation,
1066 q_head_range,
1067 out,
1068 );
1069 }
1070}
1071
1072#[allow(clippy::too_many_arguments)]
1089pub(crate) unsafe fn gqa_attention_head_range_into(
1090 queries: &[f32],
1091 keys: &[f32],
1092 values: &[f32],
1093 additive_mask: &[f32],
1094 query_positions: usize,
1095 key_positions: usize,
1096 q_heads: usize,
1097 kv_heads: usize,
1098 head_dim: usize,
1099 softmax_arithmetic: F32SoftmaxArithmetic,
1100 accumulation: F32LinearAccumulation,
1101 q_head_range: std::ops::Range<usize>,
1102 out: *mut f32,
1103) {
1104 assert!(q_head_range.end <= q_heads, "head range exceeds q_heads");
1105 let scale = (head_dim as f32).sqrt().recip();
1106 let kv_group = q_heads / kv_heads;
1107 let mut scores = vec![0.0f32; key_positions];
1108
1109 for query_position in 0..query_positions {
1110 let mask =
1111 &additive_mask[query_position * key_positions..(query_position + 1) * key_positions];
1112 for q_head in q_head_range.clone() {
1113 let kv_head = q_head / kv_group;
1114 let query_base = (query_position * q_heads + q_head) * head_dim;
1115 let query = &queries[query_base..query_base + head_dim];
1116 for (key_position, score) in scores.iter_mut().enumerate() {
1117 let key_base = (key_position * kv_heads + kv_head) * head_dim;
1118 let key = &keys[key_base..key_base + head_dim];
1119 let dot = dot_with_accumulation(query, key, accumulation);
1120 *score = dot * scale + mask[key_position];
1121 }
1122 softmax_rows_with_arithmetic(&mut scores, 1, key_positions, softmax_arithmetic);
1123
1124 let head_out = unsafe { std::slice::from_raw_parts_mut(out.add(query_base), head_dim) };
1128 attention_weighted_sum(
1129 &scores,
1130 values,
1131 kv_head,
1132 kv_heads,
1133 head_dim,
1134 accumulation,
1135 head_out,
1136 );
1137 }
1138 }
1139}
1140
1141#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
1147#[allow(clippy::too_many_arguments)]
1148fn accelerate_gqa_attention(
1149 queries: &[f32],
1150 keys: &[f32],
1151 values: &[f32],
1152 additive_mask: &[f32],
1153 query_positions: usize,
1154 key_positions: usize,
1155 q_heads: usize,
1156 kv_heads: usize,
1157 head_dim: usize,
1158 softmax_arithmetic: F32SoftmaxArithmetic,
1159 out: &mut [f32],
1160) -> bool {
1161 let scale = (head_dim as f32).sqrt().recip();
1162 let kv_group = q_heads / kv_heads;
1163 let mut query_matrix = vec![0.0f32; query_positions * head_dim];
1164 let mut key_matrix = vec![0.0f32; key_positions * head_dim];
1165 let mut value_transpose = vec![0.0f32; head_dim * key_positions];
1166 let mut scores = vec![0.0f32; query_positions * key_positions];
1167 let mut context = vec![0.0f32; query_positions * head_dim];
1168
1169 for q_head in 0..q_heads {
1170 let kv_head = q_head / kv_group;
1171 for query_position in 0..query_positions {
1172 let query_base = (query_position * q_heads + q_head) * head_dim;
1173 query_matrix[query_position * head_dim..(query_position + 1) * head_dim]
1174 .copy_from_slice(&queries[query_base..query_base + head_dim]);
1175 }
1176 for key_position in 0..key_positions {
1177 let key_base = (key_position * kv_heads + kv_head) * head_dim;
1178 key_matrix[key_position * head_dim..(key_position + 1) * head_dim]
1179 .copy_from_slice(&keys[key_base..key_base + head_dim]);
1180 for lane in 0..head_dim {
1181 value_transpose[lane * key_positions + key_position] = values[key_base + lane];
1182 }
1183 }
1184
1185 if !accelerate_sgemm(
1186 &query_matrix,
1187 &key_matrix,
1188 query_positions,
1189 head_dim,
1190 key_positions,
1191 0.0,
1192 false,
1193 &mut scores,
1194 ) {
1195 return false;
1196 }
1197 for query_position in 0..query_positions {
1198 let score_row =
1199 &mut scores[query_position * key_positions..(query_position + 1) * key_positions];
1200 let mask = &additive_mask
1201 [query_position * key_positions..(query_position + 1) * key_positions];
1202 for (score, mask_value) in score_row.iter_mut().zip(mask) {
1203 *score = *score * scale + mask_value;
1204 }
1205 softmax_rows_with_arithmetic(score_row, 1, key_positions, softmax_arithmetic);
1206 }
1207 if !accelerate_sgemm(
1208 &scores,
1209 &value_transpose,
1210 query_positions,
1211 key_positions,
1212 head_dim,
1213 0.0,
1214 false,
1215 &mut context,
1216 ) {
1217 return false;
1218 }
1219 for query_position in 0..query_positions {
1220 let out_base = (query_position * q_heads + q_head) * head_dim;
1221 out[out_base..out_base + head_dim].copy_from_slice(
1222 &context[query_position * head_dim..(query_position + 1) * head_dim],
1223 );
1224 }
1225 }
1226 true
1227}
1228
1229#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
1230#[allow(clippy::too_many_arguments)]
1231fn accelerate_gqa_attention(
1232 _queries: &[f32],
1233 _keys: &[f32],
1234 _values: &[f32],
1235 _additive_mask: &[f32],
1236 _query_positions: usize,
1237 _key_positions: usize,
1238 _q_heads: usize,
1239 _kv_heads: usize,
1240 _head_dim: usize,
1241 _softmax_arithmetic: F32SoftmaxArithmetic,
1242 _out: &mut [f32],
1243) -> bool {
1244 false
1245}
1246
1247#[allow(clippy::too_many_arguments)]
1248fn attention_weighted_sum(
1249 scores: &[f32],
1250 values: &[f32],
1251 kv_head: usize,
1252 kv_heads: usize,
1253 head_dim: usize,
1254 accumulation: F32LinearAccumulation,
1255 out: &mut [f32],
1256) {
1257 if accumulation == F32LinearAccumulation::WidenedF64 {
1258 for lane in 0..head_dim {
1259 let mut sum = 0.0f64;
1260 for (key_position, weight) in scores.iter().copied().enumerate() {
1261 let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1262 sum += f64::from(weight) * f64::from(value);
1263 }
1264 out[lane] = sum as f32;
1265 }
1266 return;
1267 }
1268 let lanes = match accumulation {
1269 F32LinearAccumulation::Scalar => 1,
1270 F32LinearAccumulation::Lanes4 | F32LinearAccumulation::FusedLanes4 => 4,
1271 F32LinearAccumulation::Lanes8 | F32LinearAccumulation::FusedLanes8 => 8,
1272 F32LinearAccumulation::Accelerate
1273 | F32LinearAccumulation::AccelerateRowInvariant
1274 | F32LinearAccumulation::AccelerateBiasSeeded
1275 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => 1,
1276 F32LinearAccumulation::WidenedF64 => unreachable!("handled above"),
1277 };
1278 for lane in 0..head_dim {
1279 let mut partial = [0.0f32; 8];
1280 for (key_position, weight) in scores.iter().copied().enumerate() {
1281 let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1282 let partial_index = key_position % lanes;
1283 partial[partial_index] = match accumulation {
1284 F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
1285 weight.mul_add(value, partial[partial_index])
1286 }
1287 F32LinearAccumulation::Scalar
1288 | F32LinearAccumulation::Lanes4
1289 | F32LinearAccumulation::Lanes8
1290 | F32LinearAccumulation::Accelerate
1291 | F32LinearAccumulation::AccelerateRowInvariant
1292 | F32LinearAccumulation::AccelerateBiasSeeded
1293 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
1294 | F32LinearAccumulation::WidenedF64 => partial[partial_index] + weight * value,
1295 };
1296 }
1297 let mut sum = 0.0f32;
1298 for value in &partial[..lanes] {
1299 sum += *value;
1300 }
1301 out[lane] = sum;
1302 }
1303}
1304
1305pub fn mrope_interleave(axes: [&[f32]; 3], sections: [usize; 3], out: &mut [f32]) {
1320 let half = out.len();
1321 for axis in axes {
1322 assert!(
1323 axis.len() >= half,
1324 "axis row shorter than the half-dimension"
1325 );
1326 }
1327
1328 out.copy_from_slice(&axes[0][..half]);
1331 let modality_num = 3usize;
1332 for (axis_index, section) in sections.iter().enumerate().skip(1) {
1333 let end = section * modality_num;
1334 let mut lane = axis_index;
1335 while lane < end && lane < half {
1336 out[lane] = axes[axis_index][lane];
1337 lane += modality_num;
1338 }
1339 }
1340}
1341
1342pub fn apply_rope_in_place(row: &mut [f32], cos: &[f32], sin: &[f32]) {
1352 let dim = row.len();
1353 assert_eq!(cos.len(), dim, "cos must match head_dim");
1354 assert_eq!(sin.len(), dim, "sin must match head_dim");
1355 assert!(dim.is_multiple_of(2), "head_dim must be even");
1356
1357 let half = dim / 2;
1358 let original: Vec<f32> = row.to_vec();
1359 for index in 0..dim {
1360 let rotated = if index < half {
1361 -original[index + half]
1362 } else {
1363 original[index - half]
1364 };
1365 row[index] = original[index] * cos[index] + rotated * sin[index];
1366 }
1367}
1368
1369#[cfg(test)]
1370mod tests {
1371 use super::*;
1372
1373 #[test]
1374 fn linear_matches_a_hand_computed_product() {
1375 let x = [1.0, 2.0, 3.0];
1377 let weight = [1.0, 0.0, -1.0, 2.0, 2.0, 2.0];
1378 let mut out = [0.0; 2];
1379 linear(&x, &weight, None, 1, 3, 2, &mut out);
1380 assert_eq!(out, [-2.0, 12.0]);
1381
1382 let mut biased = [0.0; 2];
1383 linear(&x, &weight, Some(&[10.0, -12.0]), 1, 3, 2, &mut biased);
1384 assert_eq!(biased, [8.0, 0.0]);
1385 }
1386
1387 #[test]
1388 fn torch_cascade_sum_agrees_with_the_flat_sum_when_rounding_cannot_intervene() {
1389 for length in [1usize, 7, 8, 15, 16, 31, 128, 1024, 3072] {
1393 for width in [4usize, 8] {
1394 let values: Vec<f32> = (0..length).map(|index| (index % 8) as f32).collect();
1395 let flat: f32 = values.iter().sum();
1396 assert_eq!(
1397 torch_cascade_sum(&values, width, |value| value),
1398 flat,
1399 "length {length}, width {width}"
1400 );
1401 }
1402 }
1403 }
1404
1405 #[test]
1406 fn torch_cascade_sum_applies_its_transform_before_accumulating() {
1407 let values = [1.0f32, 2.0, 3.0, 4.0, 5.0];
1408 assert_eq!(torch_cascade_sum(&values, 4, |value| value * value), 55.0);
1409 }
1410
1411 #[test]
1412 fn torch_cascade_sum_differs_from_a_flat_sum_once_rounding_matters() {
1413 let mut values = vec![1.0f32; 1024];
1418 values[0] = 1.0e8;
1419 let flat = values.iter().fold(0.0f32, |sum, value| sum + value);
1420 assert_ne!(torch_cascade_sum(&values, 8, |value| value), flat);
1421 }
1422
1423 #[test]
1424 fn ceil_log2_matches_its_definition() {
1425 assert_eq!(ceil_log2(0), 0);
1426 assert_eq!(ceil_log2(1), 0);
1427 assert_eq!(ceil_log2(2), 1);
1428 assert_eq!(ceil_log2(3), 2);
1429 assert_eq!(ceil_log2(32), 5);
1430 assert_eq!(ceil_log2(33), 6);
1431 }
1432
1433 #[test]
1434 fn rms_norm_normalizes_and_scales() {
1435 let x = [3.0f32, 4.0];
1437 let weight = [1.0f32, 1.0];
1438 let mut out = [0.0; 2];
1439 rms_norm(&x, &weight, 0.0, 1, 2, &mut out);
1440 let expected = 12.5f32.sqrt().recip();
1441 assert!((out[0] - 3.0 * expected).abs() < 1e-6);
1442 assert!((out[1] - 4.0 * expected).abs() < 1e-6);
1443
1444 let mut weighted = [0.0; 2];
1446 rms_norm(&x, &[2.0, 0.5], 0.0, 1, 2, &mut weighted);
1447 assert!((weighted[0] - 3.0 * expected * 2.0).abs() < 1e-6);
1448 assert!((weighted[1] - 4.0 * expected * 0.5).abs() < 1e-6);
1449 }
1450
1451 #[test]
1452 fn silu_mul_matches_the_definition() {
1453 let mut gate = [0.0f32, 1.0, -1.0];
1454 let up = [1.0f32, 2.0, 3.0];
1455 silu_mul_in_place(&mut gate, &up);
1456 assert_eq!(gate[0], 0.0);
1457 let silu_one = 1.0f32 / (1.0 + (-1.0f32).exp());
1458 assert!((gate[1] - silu_one * 2.0).abs() < 1e-6);
1459 let silu_neg = -1.0f32 / (1.0 + 1.0f32.exp());
1460 assert!((gate[2] - silu_neg * 3.0).abs() < 1e-6);
1461 }
1462
1463 #[test]
1464 fn softmax_rows_sums_to_one_and_is_shift_invariant() {
1465 let mut x = [1.0f32, 2.0, 3.0, 101.0, 102.0, 103.0];
1466 softmax_rows(&mut x, 2, 3);
1467 let first: f32 = x[..3].iter().sum();
1468 let second: f32 = x[3..].iter().sum();
1469 assert!((first - 1.0).abs() < 1e-6);
1470 assert!((second - 1.0).abs() < 1e-6);
1471 for index in 0..3 {
1473 assert!((x[index] - x[index + 3]).abs() < 1e-6);
1474 }
1475 }
1476
1477 #[test]
1478 fn gqa_maps_each_query_head_to_its_kv_group() {
1479 let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (1, 1, 4, 2, 2);
1480 let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1481 let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1482 let values = [10.0f32, 11.0, 20.0, 21.0];
1483 let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1484
1485 gqa_attention(
1486 &queries,
1487 &keys,
1488 &values,
1489 &[0.0],
1490 query_positions,
1491 key_positions,
1492 q_heads,
1493 kv_heads,
1494 head_dim,
1495 &mut out,
1496 );
1497
1498 assert_eq!(&out[0..2], &[10.0, 11.0]);
1499 assert_eq!(&out[2..4], &[10.0, 11.0]);
1500 assert_eq!(&out[4..6], &[20.0, 21.0]);
1501 assert_eq!(&out[6..8], &[20.0, 21.0]);
1502 }
1503
1504 #[test]
1505 fn gqa_honors_the_additive_causal_mask() {
1506 let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (2, 2, 1, 1, 2);
1507 let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1508 let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1509 let values = [2.0f32, 4.0, 10.0, 20.0];
1510 let mask = [0.0f32, f32::NEG_INFINITY, 0.0, 0.0];
1511 let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1512
1513 gqa_attention(
1514 &queries,
1515 &keys,
1516 &values,
1517 &mask,
1518 query_positions,
1519 key_positions,
1520 q_heads,
1521 kv_heads,
1522 head_dim,
1523 &mut out,
1524 );
1525
1526 assert_eq!(&out[0..2], &[2.0, 4.0]);
1527 assert_eq!(&out[2..4], &[6.0, 12.0]);
1528 }
1529
1530 #[test]
1531 fn rope_rotates_a_known_pair() {
1532 let mut row = [3.0f32, 5.0];
1534 apply_rope_in_place(&mut row, &[0.0, 0.0], &[1.0, 1.0]);
1535 assert_eq!(row, [-5.0, 3.0]);
1536
1537 let mut same = [3.0f32, 5.0];
1539 apply_rope_in_place(&mut same, &[1.0, 1.0], &[0.0, 0.0]);
1540 assert_eq!(same, [3.0, 5.0]);
1541 }
1542
1543 #[test]
1544 fn mrope_interleave_is_identity_when_all_axes_agree() {
1545 let axis: Vec<f32> = (0..64).map(|value| value as f32).collect();
1548 let mut out = vec![0.0f32; 64];
1549 mrope_interleave([&axis, &axis, &axis], [24, 20, 20], &mut out);
1550 assert_eq!(out, axis);
1551 }
1552
1553 #[test]
1554 fn mrope_interleave_selects_the_documented_lanes() {
1555 let zeros = vec![0.0f32; 64];
1556 let ones = vec![1.0f32; 64];
1557 let twos = vec![2.0f32; 64];
1558 let mut out = vec![0.0f32; 64];
1559 mrope_interleave([&zeros, &ones, &twos], [24, 20, 20], &mut out);
1560
1561 for (lane, value) in out.iter().enumerate() {
1564 let expected = if lane < 60 && lane % 3 == 1 {
1565 1.0
1566 } else if lane < 60 && lane % 3 == 2 {
1567 2.0
1568 } else {
1569 0.0
1570 };
1571 assert_eq!(*value, expected, "lane {lane}");
1572 }
1573 }
1574}