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 for row in 0..m {
212 let x_row = &x[row * k..row * k + k];
213 for col in 0..n {
214 let w_row = &weight[col * k..col * k + k];
215 let sum = dot_with_accumulation(x_row, w_row, accumulation);
216 out[row * n + col] = bias.map_or(sum, |b| sum + b[col]);
217 }
218 }
219}
220
221fn dot_with_accumulation(x: &[f32], weight: &[f32], accumulation: F32LinearAccumulation) -> f32 {
222 assert_eq!(x.len(), weight.len(), "dot-product inputs must match");
223 match accumulation {
224 F32LinearAccumulation::Scalar => {
225 let mut sum = 0.0f32;
226 for index in 0..x.len() {
227 sum += x[index] * weight[index];
228 }
229 sum
230 }
231 F32LinearAccumulation::WidenedF64 => {
232 let mut sum = 0.0f64;
233 for index in 0..x.len() {
234 sum += f64::from(x[index]) * f64::from(weight[index]);
235 }
236 sum as f32
237 }
238 F32LinearAccumulation::Lanes4
239 | F32LinearAccumulation::Lanes8
240 | F32LinearAccumulation::FusedLanes4
241 | F32LinearAccumulation::FusedLanes8
242 | F32LinearAccumulation::Accelerate
243 | F32LinearAccumulation::AccelerateRowInvariant
244 | F32LinearAccumulation::AccelerateBiasSeeded
245 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => {
246 let lanes = match accumulation {
247 F32LinearAccumulation::Lanes4 => 4,
248 F32LinearAccumulation::Lanes8 => 8,
249 F32LinearAccumulation::FusedLanes4 => 4,
250 F32LinearAccumulation::FusedLanes8 => 8,
251 F32LinearAccumulation::Accelerate
252 | F32LinearAccumulation::AccelerateRowInvariant
253 | F32LinearAccumulation::AccelerateBiasSeeded
254 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => 1,
255 F32LinearAccumulation::Scalar | F32LinearAccumulation::WidenedF64 => {
256 unreachable!("scalar and widened orders are handled above")
257 }
258 };
259 let mut partial = [0.0f32; 8];
260 for index in 0..x.len() {
261 let lane = index % lanes;
262 partial[lane] = match accumulation {
263 F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
264 x[index].mul_add(weight[index], partial[lane])
265 }
266 F32LinearAccumulation::Scalar
267 | F32LinearAccumulation::Lanes4
268 | F32LinearAccumulation::Lanes8
269 | F32LinearAccumulation::Accelerate
270 | F32LinearAccumulation::AccelerateRowInvariant
271 | F32LinearAccumulation::AccelerateBiasSeeded
272 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
273 | F32LinearAccumulation::WidenedF64 => partial[lane] + x[index] * weight[index],
274 };
275 }
276 let mut sum = 0.0f32;
277 for value in &partial[..lanes] {
278 sum += *value;
279 }
280 sum
281 }
282 }
283}
284
285#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
291#[allow(clippy::too_many_arguments)]
292fn accelerate_sgemm(
293 x: &[f32],
294 weight: &[f32],
295 m: usize,
296 k: usize,
297 n: usize,
298 beta: f32,
299 row_invariant: bool,
300 out: &mut [f32],
301) -> bool {
302 if row_invariant && m == 1 {
311 let mut doubled_x = Vec::with_capacity(2 * k);
312 doubled_x.extend_from_slice(x);
313 doubled_x.extend_from_slice(x);
314 let mut doubled_out = Vec::with_capacity(2 * n);
315 doubled_out.extend_from_slice(out);
316 doubled_out.extend_from_slice(out);
317 if !accelerate_sgemm(&doubled_x, weight, 2, k, n, beta, false, &mut doubled_out) {
318 return false;
319 }
320 out.copy_from_slice(&doubled_out[..n]);
321 return true;
322 }
323 let m = i32::try_from(m).expect("SGEMM rows fit CBLAS i32 dimensions");
324 let k = i32::try_from(k).expect("SGEMM reduction fits CBLAS i32 dimensions");
325 let n = i32::try_from(n).expect("SGEMM columns fit CBLAS i32 dimensions");
326 unsafe {
330 cblas_sgemm(
331 CBLAS_ROW_MAJOR,
332 CBLAS_NO_TRANSPOSE,
333 CBLAS_TRANSPOSE,
334 m,
335 n,
336 k,
337 1.0,
338 x.as_ptr(),
339 k,
340 weight.as_ptr(),
341 k,
342 beta,
343 out.as_mut_ptr(),
344 n,
345 );
346 }
347 true
348}
349
350#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
351fn accelerate_sgemm(
352 _x: &[f32],
353 _weight: &[f32],
354 _m: usize,
355 _k: usize,
356 _n: usize,
357 _beta: f32,
358 _row_invariant: bool,
359 _out: &mut [f32],
360) -> bool {
361 false
362}
363
364#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
365const CBLAS_ROW_MAJOR: i32 = 101;
366#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
367const CBLAS_NO_TRANSPOSE: i32 = 111;
368#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
369const CBLAS_TRANSPOSE: i32 = 112;
370
371#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
372#[link(name = "Accelerate", kind = "framework")]
373unsafe extern "C" {
374 fn cblas_sgemm(
375 order: i32,
376 trans_a: i32,
377 trans_b: i32,
378 m: i32,
379 n: i32,
380 k: i32,
381 alpha: f32,
382 a: *const f32,
383 lda: i32,
384 b: *const f32,
385 ldb: i32,
386 beta: f32,
387 c: *mut f32,
388 ldc: i32,
389 );
390}
391
392#[derive(Clone, Copy, Debug, Eq, PartialEq)]
401pub enum F32Transcendental {
402 ScalarLibm,
404 AccelerateVForce,
408 SleefU10,
414}
415
416pub fn sin_with(x: &[f32], implementation: F32Transcendental, out: &mut [f32]) {
422 assert_eq!(x.len(), out.len(), "sin output must match its input");
423 if implementation == F32Transcendental::AccelerateVForce && vforce_sin(x, out) {
424 return;
425 }
426 if implementation == F32Transcendental::SleefU10 {
427 for (value, target) in x.iter().zip(out.iter_mut()) {
428 *target = crate::sleef::sinf_u10(*value);
429 }
430 return;
431 }
432 for (value, target) in x.iter().zip(out.iter_mut()) {
433 *target = value.sin();
434 }
435}
436
437pub fn exp_with(x: &[f32], implementation: F32Transcendental, out: &mut [f32]) {
443 assert_eq!(x.len(), out.len(), "exp output must match its input");
444 if implementation == F32Transcendental::AccelerateVForce && vforce_exp(x, out) {
445 return;
446 }
447 if implementation == F32Transcendental::SleefU10 {
448 for (value, target) in x.iter().zip(out.iter_mut()) {
449 *target = crate::sleef::expf_u10(*value);
450 }
451 return;
452 }
453 for (value, target) in x.iter().zip(out.iter_mut()) {
454 *target = value.exp();
455 }
456}
457
458#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
459fn vforce_sin(x: &[f32], out: &mut [f32]) -> bool {
460 let count = i32::try_from(x.len()).expect("vForce length fits i32");
461 unsafe { vvsinf(out.as_mut_ptr(), x.as_ptr(), &raw const count) };
464 true
465}
466
467#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
468fn vforce_exp(x: &[f32], out: &mut [f32]) -> bool {
469 let count = i32::try_from(x.len()).expect("vForce length fits i32");
470 unsafe { vvexpf(out.as_mut_ptr(), x.as_ptr(), &raw const count) };
472 true
473}
474
475#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
476fn vforce_sin(_x: &[f32], _out: &mut [f32]) -> bool {
477 false
478}
479
480#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
481fn vforce_exp(_x: &[f32], _out: &mut [f32]) -> bool {
482 false
483}
484
485#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
486#[link(name = "Accelerate", kind = "framework")]
487unsafe extern "C" {
488 fn vvsinf(out: *mut f32, x: *const f32, count: *const i32);
489 fn vvexpf(out: *mut f32, x: *const f32, count: *const i32);
490}
491
492pub fn rms_norm(x: &[f32], weight: &[f32], eps: f32, rows: usize, dim: usize, out: &mut [f32]) {
498 rms_norm_with_arithmetic(
499 x,
500 weight,
501 eps,
502 rows,
503 dim,
504 F32RmsNormArithmetic::ScalarReciprocalSqrt,
505 out,
506 );
507}
508
509pub fn rms_norm_with_arithmetic(
514 x: &[f32],
515 weight: &[f32],
516 eps: f32,
517 rows: usize,
518 dim: usize,
519 arithmetic: F32RmsNormArithmetic,
520 out: &mut [f32],
521) {
522 assert_eq!(x.len(), rows * dim, "x must be [rows, dim]");
523 assert_eq!(weight.len(), dim, "weight must be [dim]");
524 assert_eq!(out.len(), rows * dim, "out must be [rows, dim]");
525
526 for row in 0..rows {
527 let src = &x[row * dim..row * dim + dim];
528 let scale = rms_scale(src, eps, arithmetic);
529 for index in 0..dim {
530 out[row * dim + index] = src[index] * scale * weight[index];
531 }
532 }
533}
534
535fn rms_scale(src: &[f32], eps: f32, arithmetic: F32RmsNormArithmetic) -> f32 {
536 match arithmetic {
537 F32RmsNormArithmetic::ScalarReciprocalSqrt => {
538 let sum = sum_squares_f32(src, 1);
539 (sum / src.len() as f32 + eps).sqrt().recip()
540 }
541 F32RmsNormArithmetic::ScalarDivideSqrt => {
542 let sum = sum_squares_f32(src, 1);
543 1.0f32 / (sum / src.len() as f32 + eps).sqrt()
544 }
545 F32RmsNormArithmetic::Lanes4ReciprocalSqrt => {
546 let sum = sum_squares_f32(src, 4);
547 (sum / src.len() as f32 + eps).sqrt().recip()
548 }
549 F32RmsNormArithmetic::Lanes8ReciprocalSqrt => {
550 let sum = sum_squares_f32(src, 8);
551 (sum / src.len() as f32 + eps).sqrt().recip()
552 }
553 F32RmsNormArithmetic::Lanes16ReciprocalSqrt => {
554 let sum = sum_squares_f32(src, 16);
555 (sum / src.len() as f32 + eps).sqrt().recip()
556 }
557 F32RmsNormArithmetic::Lanes32ReciprocalSqrt => {
558 let sum = sum_squares_f32(src, 32);
559 (sum / src.len() as f32 + eps).sqrt().recip()
560 }
561 F32RmsNormArithmetic::TorchCascade4ReciprocalSqrt => {
562 let sum = torch_cascade_sum(src, 4, |value| value * value);
563 (sum / src.len() as f32 + eps).sqrt().recip()
564 }
565 F32RmsNormArithmetic::TorchCascade8ReciprocalSqrt => {
566 let sum = torch_cascade_sum(src, 8, |value| value * value);
567 (sum / src.len() as f32 + eps).sqrt().recip()
568 }
569 F32RmsNormArithmetic::F64ReciprocalSqrt => {
570 let mut sum = 0.0f64;
571 for value in src {
572 let value = f64::from(*value);
573 sum += value * value;
574 }
575 (sum / src.len() as f64 + f64::from(eps)).sqrt().recip() as f32
576 }
577 }
578}
579
580fn sum_squares_f32(src: &[f32], lanes: usize) -> f32 {
581 let mut partial = [0.0f32; 32];
582 for (index, value) in src.iter().enumerate() {
583 partial[index % lanes] += *value * *value;
584 }
585 let mut sum = 0.0f32;
586 for value in &partial[..lanes] {
587 sum += *value;
588 }
589 sum
590}
591
592#[allow(clippy::needless_range_loop)]
625pub fn torch_cascade_sum(src: &[f32], width: usize, transform: impl Fn(f32) -> f32) -> f32 {
626 assert!(width > 0, "vector width must be positive");
627 const ILP: usize = 4;
628 const LEVELS: usize = 4;
629
630 let vector_count = src.len() / width;
631 let vector = |index: usize, lane: usize| transform(src[index * width + lane]);
632
633 let size = vector_count / ILP;
635 let level_power = ceil_log2(size).div_euclid(LEVELS).max(4);
636 let level_step = 1usize << level_power;
637 let level_mask = level_step - 1;
638
639 let mut acc = vec![[0.0f32; ILP].map(|_| vec![0.0f32; width]); LEVELS];
640 let mut index = 0usize;
641 while index + level_step <= size {
642 for _ in 0..level_step {
643 for chain in 0..ILP {
644 for lane in 0..width {
645 acc[0][chain][lane] += vector(index * ILP + chain, lane);
646 }
647 }
648 index += 1;
649 }
650 for level in 1..LEVELS {
651 for chain in 0..ILP {
652 for lane in 0..width {
653 acc[level][chain][lane] += acc[level - 1][chain][lane];
654 acc[level - 1][chain][lane] = 0.0;
655 }
656 }
657 if index & (level_mask << (level * level_power)) != 0 {
658 break;
659 }
660 }
661 }
662 while index < size {
663 for chain in 0..ILP {
664 for lane in 0..width {
665 acc[0][chain][lane] += vector(index * ILP + chain, lane);
666 }
667 }
668 index += 1;
669 }
670 for level in 1..LEVELS {
671 for chain in 0..ILP {
672 for lane in 0..width {
673 acc[level][chain][lane] += acc[level - 1][chain][lane];
674 }
675 }
676 }
677
678 let mut partial = acc.swap_remove(LEVELS - 1);
680 for leftover in size * ILP..vector_count {
681 for lane in 0..width {
682 partial[0][lane] += vector(leftover, lane);
683 }
684 }
685 for chain in 1..ILP {
686 for lane in 0..width {
687 partial[0][lane] += partial[chain][lane];
688 }
689 }
690
691 let mut sum = 0.0f32;
694 for index in vector_count * width..src.len() {
695 sum += transform(src[index]);
696 }
697 for lane in 0..width {
698 sum += partial[0][lane];
699 }
700 sum
701}
702
703fn ceil_log2(value: usize) -> usize {
705 if value <= 1 {
706 return 0;
707 }
708 usize::BITS as usize - (value - 1).leading_zeros() as usize
709}
710
711pub fn silu_mul_in_place(gate: &mut [f32], up: &[f32]) {
713 silu_mul_in_place_with_arithmetic(gate, up, F32SiluArithmetic::Divide);
714}
715
716pub fn silu_mul_in_place_with_arithmetic(
718 gate: &mut [f32],
719 up: &[f32],
720 arithmetic: F32SiluArithmetic,
721) {
722 assert_eq!(gate.len(), up.len(), "gate and up must match");
723 for (g, u) in gate.iter_mut().zip(up) {
724 let x = *g;
725 if arithmetic == F32SiluArithmetic::WidenedF64 {
726 let wide = f64::from(x);
727 *g = (wide / (1.0 + (-wide).exp()) * f64::from(*u)) as f32;
728 continue;
729 }
730 let denominator = 1.0 + (-x).exp();
731 let silu = match arithmetic {
732 F32SiluArithmetic::Divide => x / denominator,
733 F32SiluArithmetic::MultiplyReciprocal => x * denominator.recip(),
734 F32SiluArithmetic::WidenedF64 => unreachable!("handled above"),
735 };
736 *g = silu * u;
737 }
738}
739
740pub fn softmax_rows(x: &mut [f32], rows: usize, cols: usize) {
742 softmax_rows_with_arithmetic(x, rows, cols, F32SoftmaxArithmetic::ReciprocalMultiply);
743}
744
745pub fn softmax_rows_with_arithmetic(
747 x: &mut [f32],
748 rows: usize,
749 cols: usize,
750 arithmetic: F32SoftmaxArithmetic,
751) {
752 assert_eq!(x.len(), rows * cols, "x must be [rows, cols]");
753 for row in 0..rows {
754 let slice = &mut x[row * cols..row * cols + cols];
755 let mut max = f32::NEG_INFINITY;
756 for value in slice.iter() {
757 if *value > max {
758 max = *value;
759 }
760 }
761 if arithmetic == F32SoftmaxArithmetic::WidenedF64 {
762 let max = f64::from(max);
763 let mut wide = Vec::with_capacity(slice.len());
764 let mut sum = 0.0f64;
765 for value in slice.iter() {
766 let exponent = (f64::from(*value) - max).exp();
767 sum += exponent;
768 wide.push(exponent);
769 }
770 for (value, exponent) in slice.iter_mut().zip(wide) {
771 *value = (exponent / sum) as f32;
772 }
773 continue;
774 }
775 let mut sum = 0.0f32;
776 for value in slice.iter_mut() {
777 *value = (*value - max).exp();
778 sum += *value;
779 }
780 for value in slice.iter_mut() {
781 *value = match arithmetic {
782 F32SoftmaxArithmetic::ReciprocalMultiply => *value * sum.recip(),
783 F32SoftmaxArithmetic::Divide => *value / sum,
784 F32SoftmaxArithmetic::WidenedF64 => unreachable!("handled above"),
785 };
786 }
787 }
788}
789
790#[allow(clippy::too_many_arguments)]
802pub fn gqa_attention(
803 queries: &[f32],
804 keys: &[f32],
805 values: &[f32],
806 additive_mask: &[f32],
807 query_positions: usize,
808 key_positions: usize,
809 q_heads: usize,
810 kv_heads: usize,
811 head_dim: usize,
812 out: &mut [f32],
813) {
814 gqa_attention_with_softmax(
815 queries,
816 keys,
817 values,
818 additive_mask,
819 query_positions,
820 key_positions,
821 q_heads,
822 kv_heads,
823 head_dim,
824 F32SoftmaxArithmetic::ReciprocalMultiply,
825 out,
826 );
827}
828
829#[allow(clippy::too_many_arguments)]
831pub fn gqa_attention_with_softmax(
832 queries: &[f32],
833 keys: &[f32],
834 values: &[f32],
835 additive_mask: &[f32],
836 query_positions: usize,
837 key_positions: usize,
838 q_heads: usize,
839 kv_heads: usize,
840 head_dim: usize,
841 softmax_arithmetic: F32SoftmaxArithmetic,
842 out: &mut [f32],
843) {
844 gqa_attention_with_arithmetic(
845 queries,
846 keys,
847 values,
848 additive_mask,
849 query_positions,
850 key_positions,
851 q_heads,
852 kv_heads,
853 head_dim,
854 softmax_arithmetic,
855 F32LinearAccumulation::Scalar,
856 out,
857 );
858}
859
860#[allow(clippy::too_many_arguments)]
862pub fn gqa_attention_with_arithmetic(
863 queries: &[f32],
864 keys: &[f32],
865 values: &[f32],
866 additive_mask: &[f32],
867 query_positions: usize,
868 key_positions: usize,
869 q_heads: usize,
870 kv_heads: usize,
871 head_dim: usize,
872 softmax_arithmetic: F32SoftmaxArithmetic,
873 accumulation: F32LinearAccumulation,
874 out: &mut [f32],
875) {
876 assert!(kv_heads > 0, "at least one KV head is required");
877 assert_eq!(
878 q_heads % kv_heads,
879 0,
880 "query heads must divide evenly into KV groups"
881 );
882 assert_eq!(
883 queries.len(),
884 query_positions * q_heads * head_dim,
885 "queries must be [query_positions, q_heads, head_dim]"
886 );
887 assert_eq!(
888 keys.len(),
889 key_positions * kv_heads * head_dim,
890 "keys must be [key_positions, kv_heads, head_dim]"
891 );
892 assert_eq!(
893 values.len(),
894 key_positions * kv_heads * head_dim,
895 "values must be [key_positions, kv_heads, head_dim]"
896 );
897 assert_eq!(
898 additive_mask.len(),
899 query_positions * key_positions,
900 "mask must be [query_positions, key_positions]"
901 );
902 assert_eq!(
903 out.len(),
904 query_positions * q_heads * head_dim,
905 "out must be [query_positions, q_heads, head_dim]"
906 );
907
908 if accumulation == F32LinearAccumulation::Accelerate
909 && accelerate_gqa_attention(
910 queries,
911 keys,
912 values,
913 additive_mask,
914 query_positions,
915 key_positions,
916 q_heads,
917 kv_heads,
918 head_dim,
919 softmax_arithmetic,
920 out,
921 )
922 {
923 return;
924 }
925
926 let scale = (head_dim as f32).sqrt().recip();
927 let kv_group = q_heads / kv_heads;
928 let mut scores = vec![0.0f32; key_positions];
929
930 for query_position in 0..query_positions {
931 let mask =
932 &additive_mask[query_position * key_positions..(query_position + 1) * key_positions];
933 for q_head in 0..q_heads {
934 let kv_head = q_head / kv_group;
935 let query_base = (query_position * q_heads + q_head) * head_dim;
936 let query = &queries[query_base..query_base + head_dim];
937
938 for (key_position, score) in scores.iter_mut().enumerate() {
939 let key_base = (key_position * kv_heads + kv_head) * head_dim;
940 let key = &keys[key_base..key_base + head_dim];
941 let dot = dot_with_accumulation(query, key, accumulation);
942 *score = dot * scale + mask[key_position];
943 }
944 softmax_rows_with_arithmetic(&mut scores, 1, key_positions, softmax_arithmetic);
945
946 let out_base = query_base;
947 attention_weighted_sum(
948 &scores,
949 values,
950 kv_head,
951 kv_heads,
952 head_dim,
953 accumulation,
954 &mut out[out_base..out_base + head_dim],
955 );
956 }
957 }
958}
959
960#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
966#[allow(clippy::too_many_arguments)]
967fn accelerate_gqa_attention(
968 queries: &[f32],
969 keys: &[f32],
970 values: &[f32],
971 additive_mask: &[f32],
972 query_positions: usize,
973 key_positions: usize,
974 q_heads: usize,
975 kv_heads: usize,
976 head_dim: usize,
977 softmax_arithmetic: F32SoftmaxArithmetic,
978 out: &mut [f32],
979) -> bool {
980 let scale = (head_dim as f32).sqrt().recip();
981 let kv_group = q_heads / kv_heads;
982 let mut query_matrix = vec![0.0f32; query_positions * head_dim];
983 let mut key_matrix = vec![0.0f32; key_positions * head_dim];
984 let mut value_transpose = vec![0.0f32; head_dim * key_positions];
985 let mut scores = vec![0.0f32; query_positions * key_positions];
986 let mut context = vec![0.0f32; query_positions * head_dim];
987
988 for q_head in 0..q_heads {
989 let kv_head = q_head / kv_group;
990 for query_position in 0..query_positions {
991 let query_base = (query_position * q_heads + q_head) * head_dim;
992 query_matrix[query_position * head_dim..(query_position + 1) * head_dim]
993 .copy_from_slice(&queries[query_base..query_base + head_dim]);
994 }
995 for key_position in 0..key_positions {
996 let key_base = (key_position * kv_heads + kv_head) * head_dim;
997 key_matrix[key_position * head_dim..(key_position + 1) * head_dim]
998 .copy_from_slice(&keys[key_base..key_base + head_dim]);
999 for lane in 0..head_dim {
1000 value_transpose[lane * key_positions + key_position] = values[key_base + lane];
1001 }
1002 }
1003
1004 if !accelerate_sgemm(
1005 &query_matrix,
1006 &key_matrix,
1007 query_positions,
1008 head_dim,
1009 key_positions,
1010 0.0,
1011 false,
1012 &mut scores,
1013 ) {
1014 return false;
1015 }
1016 for query_position in 0..query_positions {
1017 let score_row =
1018 &mut scores[query_position * key_positions..(query_position + 1) * key_positions];
1019 let mask = &additive_mask
1020 [query_position * key_positions..(query_position + 1) * key_positions];
1021 for (score, mask_value) in score_row.iter_mut().zip(mask) {
1022 *score = *score * scale + mask_value;
1023 }
1024 softmax_rows_with_arithmetic(score_row, 1, key_positions, softmax_arithmetic);
1025 }
1026 if !accelerate_sgemm(
1027 &scores,
1028 &value_transpose,
1029 query_positions,
1030 key_positions,
1031 head_dim,
1032 0.0,
1033 false,
1034 &mut context,
1035 ) {
1036 return false;
1037 }
1038 for query_position in 0..query_positions {
1039 let out_base = (query_position * q_heads + q_head) * head_dim;
1040 out[out_base..out_base + head_dim].copy_from_slice(
1041 &context[query_position * head_dim..(query_position + 1) * head_dim],
1042 );
1043 }
1044 }
1045 true
1046}
1047
1048#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
1049#[allow(clippy::too_many_arguments)]
1050fn accelerate_gqa_attention(
1051 _queries: &[f32],
1052 _keys: &[f32],
1053 _values: &[f32],
1054 _additive_mask: &[f32],
1055 _query_positions: usize,
1056 _key_positions: usize,
1057 _q_heads: usize,
1058 _kv_heads: usize,
1059 _head_dim: usize,
1060 _softmax_arithmetic: F32SoftmaxArithmetic,
1061 _out: &mut [f32],
1062) -> bool {
1063 false
1064}
1065
1066#[allow(clippy::too_many_arguments)]
1067fn attention_weighted_sum(
1068 scores: &[f32],
1069 values: &[f32],
1070 kv_head: usize,
1071 kv_heads: usize,
1072 head_dim: usize,
1073 accumulation: F32LinearAccumulation,
1074 out: &mut [f32],
1075) {
1076 if accumulation == F32LinearAccumulation::WidenedF64 {
1077 for lane in 0..head_dim {
1078 let mut sum = 0.0f64;
1079 for (key_position, weight) in scores.iter().copied().enumerate() {
1080 let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1081 sum += f64::from(weight) * f64::from(value);
1082 }
1083 out[lane] = sum as f32;
1084 }
1085 return;
1086 }
1087 let lanes = match accumulation {
1088 F32LinearAccumulation::Scalar => 1,
1089 F32LinearAccumulation::Lanes4 | F32LinearAccumulation::FusedLanes4 => 4,
1090 F32LinearAccumulation::Lanes8 | F32LinearAccumulation::FusedLanes8 => 8,
1091 F32LinearAccumulation::Accelerate
1092 | F32LinearAccumulation::AccelerateRowInvariant
1093 | F32LinearAccumulation::AccelerateBiasSeeded
1094 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => 1,
1095 F32LinearAccumulation::WidenedF64 => unreachable!("handled above"),
1096 };
1097 for lane in 0..head_dim {
1098 let mut partial = [0.0f32; 8];
1099 for (key_position, weight) in scores.iter().copied().enumerate() {
1100 let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1101 let partial_index = key_position % lanes;
1102 partial[partial_index] = match accumulation {
1103 F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
1104 weight.mul_add(value, partial[partial_index])
1105 }
1106 F32LinearAccumulation::Scalar
1107 | F32LinearAccumulation::Lanes4
1108 | F32LinearAccumulation::Lanes8
1109 | F32LinearAccumulation::Accelerate
1110 | F32LinearAccumulation::AccelerateRowInvariant
1111 | F32LinearAccumulation::AccelerateBiasSeeded
1112 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
1113 | F32LinearAccumulation::WidenedF64 => partial[partial_index] + weight * value,
1114 };
1115 }
1116 let mut sum = 0.0f32;
1117 for value in &partial[..lanes] {
1118 sum += *value;
1119 }
1120 out[lane] = sum;
1121 }
1122}
1123
1124pub fn mrope_interleave(axes: [&[f32]; 3], sections: [usize; 3], out: &mut [f32]) {
1139 let half = out.len();
1140 for axis in axes {
1141 assert!(
1142 axis.len() >= half,
1143 "axis row shorter than the half-dimension"
1144 );
1145 }
1146
1147 out.copy_from_slice(&axes[0][..half]);
1150 let modality_num = 3usize;
1151 for (axis_index, section) in sections.iter().enumerate().skip(1) {
1152 let end = section * modality_num;
1153 let mut lane = axis_index;
1154 while lane < end && lane < half {
1155 out[lane] = axes[axis_index][lane];
1156 lane += modality_num;
1157 }
1158 }
1159}
1160
1161pub fn apply_rope_in_place(row: &mut [f32], cos: &[f32], sin: &[f32]) {
1171 let dim = row.len();
1172 assert_eq!(cos.len(), dim, "cos must match head_dim");
1173 assert_eq!(sin.len(), dim, "sin must match head_dim");
1174 assert!(dim.is_multiple_of(2), "head_dim must be even");
1175
1176 let half = dim / 2;
1177 let original: Vec<f32> = row.to_vec();
1178 for index in 0..dim {
1179 let rotated = if index < half {
1180 -original[index + half]
1181 } else {
1182 original[index - half]
1183 };
1184 row[index] = original[index] * cos[index] + rotated * sin[index];
1185 }
1186}
1187
1188#[cfg(test)]
1189mod tests {
1190 use super::*;
1191
1192 #[test]
1193 fn linear_matches_a_hand_computed_product() {
1194 let x = [1.0, 2.0, 3.0];
1196 let weight = [1.0, 0.0, -1.0, 2.0, 2.0, 2.0];
1197 let mut out = [0.0; 2];
1198 linear(&x, &weight, None, 1, 3, 2, &mut out);
1199 assert_eq!(out, [-2.0, 12.0]);
1200
1201 let mut biased = [0.0; 2];
1202 linear(&x, &weight, Some(&[10.0, -12.0]), 1, 3, 2, &mut biased);
1203 assert_eq!(biased, [8.0, 0.0]);
1204 }
1205
1206 #[test]
1207 fn torch_cascade_sum_agrees_with_the_flat_sum_when_rounding_cannot_intervene() {
1208 for length in [1usize, 7, 8, 15, 16, 31, 128, 1024, 3072] {
1212 for width in [4usize, 8] {
1213 let values: Vec<f32> = (0..length).map(|index| (index % 8) as f32).collect();
1214 let flat: f32 = values.iter().sum();
1215 assert_eq!(
1216 torch_cascade_sum(&values, width, |value| value),
1217 flat,
1218 "length {length}, width {width}"
1219 );
1220 }
1221 }
1222 }
1223
1224 #[test]
1225 fn torch_cascade_sum_applies_its_transform_before_accumulating() {
1226 let values = [1.0f32, 2.0, 3.0, 4.0, 5.0];
1227 assert_eq!(torch_cascade_sum(&values, 4, |value| value * value), 55.0);
1228 }
1229
1230 #[test]
1231 fn torch_cascade_sum_differs_from_a_flat_sum_once_rounding_matters() {
1232 let mut values = vec![1.0f32; 1024];
1237 values[0] = 1.0e8;
1238 let flat = values.iter().fold(0.0f32, |sum, value| sum + value);
1239 assert_ne!(torch_cascade_sum(&values, 8, |value| value), flat);
1240 }
1241
1242 #[test]
1243 fn ceil_log2_matches_its_definition() {
1244 assert_eq!(ceil_log2(0), 0);
1245 assert_eq!(ceil_log2(1), 0);
1246 assert_eq!(ceil_log2(2), 1);
1247 assert_eq!(ceil_log2(3), 2);
1248 assert_eq!(ceil_log2(32), 5);
1249 assert_eq!(ceil_log2(33), 6);
1250 }
1251
1252 #[test]
1253 fn rms_norm_normalizes_and_scales() {
1254 let x = [3.0f32, 4.0];
1256 let weight = [1.0f32, 1.0];
1257 let mut out = [0.0; 2];
1258 rms_norm(&x, &weight, 0.0, 1, 2, &mut out);
1259 let expected = 12.5f32.sqrt().recip();
1260 assert!((out[0] - 3.0 * expected).abs() < 1e-6);
1261 assert!((out[1] - 4.0 * expected).abs() < 1e-6);
1262
1263 let mut weighted = [0.0; 2];
1265 rms_norm(&x, &[2.0, 0.5], 0.0, 1, 2, &mut weighted);
1266 assert!((weighted[0] - 3.0 * expected * 2.0).abs() < 1e-6);
1267 assert!((weighted[1] - 4.0 * expected * 0.5).abs() < 1e-6);
1268 }
1269
1270 #[test]
1271 fn silu_mul_matches_the_definition() {
1272 let mut gate = [0.0f32, 1.0, -1.0];
1273 let up = [1.0f32, 2.0, 3.0];
1274 silu_mul_in_place(&mut gate, &up);
1275 assert_eq!(gate[0], 0.0);
1276 let silu_one = 1.0f32 / (1.0 + (-1.0f32).exp());
1277 assert!((gate[1] - silu_one * 2.0).abs() < 1e-6);
1278 let silu_neg = -1.0f32 / (1.0 + 1.0f32.exp());
1279 assert!((gate[2] - silu_neg * 3.0).abs() < 1e-6);
1280 }
1281
1282 #[test]
1283 fn softmax_rows_sums_to_one_and_is_shift_invariant() {
1284 let mut x = [1.0f32, 2.0, 3.0, 101.0, 102.0, 103.0];
1285 softmax_rows(&mut x, 2, 3);
1286 let first: f32 = x[..3].iter().sum();
1287 let second: f32 = x[3..].iter().sum();
1288 assert!((first - 1.0).abs() < 1e-6);
1289 assert!((second - 1.0).abs() < 1e-6);
1290 for index in 0..3 {
1292 assert!((x[index] - x[index + 3]).abs() < 1e-6);
1293 }
1294 }
1295
1296 #[test]
1297 fn gqa_maps_each_query_head_to_its_kv_group() {
1298 let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (1, 1, 4, 2, 2);
1299 let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1300 let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1301 let values = [10.0f32, 11.0, 20.0, 21.0];
1302 let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1303
1304 gqa_attention(
1305 &queries,
1306 &keys,
1307 &values,
1308 &[0.0],
1309 query_positions,
1310 key_positions,
1311 q_heads,
1312 kv_heads,
1313 head_dim,
1314 &mut out,
1315 );
1316
1317 assert_eq!(&out[0..2], &[10.0, 11.0]);
1318 assert_eq!(&out[2..4], &[10.0, 11.0]);
1319 assert_eq!(&out[4..6], &[20.0, 21.0]);
1320 assert_eq!(&out[6..8], &[20.0, 21.0]);
1321 }
1322
1323 #[test]
1324 fn gqa_honors_the_additive_causal_mask() {
1325 let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (2, 2, 1, 1, 2);
1326 let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1327 let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1328 let values = [2.0f32, 4.0, 10.0, 20.0];
1329 let mask = [0.0f32, f32::NEG_INFINITY, 0.0, 0.0];
1330 let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1331
1332 gqa_attention(
1333 &queries,
1334 &keys,
1335 &values,
1336 &mask,
1337 query_positions,
1338 key_positions,
1339 q_heads,
1340 kv_heads,
1341 head_dim,
1342 &mut out,
1343 );
1344
1345 assert_eq!(&out[0..2], &[2.0, 4.0]);
1346 assert_eq!(&out[2..4], &[6.0, 12.0]);
1347 }
1348
1349 #[test]
1350 fn rope_rotates_a_known_pair() {
1351 let mut row = [3.0f32, 5.0];
1353 apply_rope_in_place(&mut row, &[0.0, 0.0], &[1.0, 1.0]);
1354 assert_eq!(row, [-5.0, 3.0]);
1355
1356 let mut same = [3.0f32, 5.0];
1358 apply_rope_in_place(&mut same, &[1.0, 1.0], &[0.0, 0.0]);
1359 assert_eq!(same, [3.0, 5.0]);
1360 }
1361
1362 #[test]
1363 fn mrope_interleave_is_identity_when_all_axes_agree() {
1364 let axis: Vec<f32> = (0..64).map(|value| value as f32).collect();
1367 let mut out = vec![0.0f32; 64];
1368 mrope_interleave([&axis, &axis, &axis], [24, 20, 20], &mut out);
1369 assert_eq!(out, axis);
1370 }
1371
1372 #[test]
1373 fn mrope_interleave_selects_the_documented_lanes() {
1374 let zeros = vec![0.0f32; 64];
1375 let ones = vec![1.0f32; 64];
1376 let twos = vec![2.0f32; 64];
1377 let mut out = vec![0.0f32; 64];
1378 mrope_interleave([&zeros, &ones, &twos], [24, 20, 20], &mut out);
1379
1380 for (lane, value) in out.iter().enumerate() {
1383 let expected = if lane < 60 && lane % 3 == 1 {
1384 1.0
1385 } else if lane < 60 && lane % 3 == 2 {
1386 2.0
1387 } else {
1388 0.0
1389 };
1390 assert_eq!(*value, expected, "lane {lane}");
1391 }
1392 }
1393}