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 gqa_attention_head_range_with_arithmetic(
927 queries,
928 keys,
929 values,
930 additive_mask,
931 query_positions,
932 key_positions,
933 q_heads,
934 kv_heads,
935 head_dim,
936 softmax_arithmetic,
937 accumulation,
938 0..q_heads,
939 out,
940 );
941}
942
943#[allow(clippy::too_many_arguments)]
955pub fn gqa_attention_head_range_with_arithmetic(
956 queries: &[f32],
957 keys: &[f32],
958 values: &[f32],
959 additive_mask: &[f32],
960 query_positions: usize,
961 key_positions: usize,
962 q_heads: usize,
963 kv_heads: usize,
964 head_dim: usize,
965 softmax_arithmetic: F32SoftmaxArithmetic,
966 accumulation: F32LinearAccumulation,
967 q_head_range: std::ops::Range<usize>,
968 out: &mut [f32],
969) {
970 assert!(
971 out.len() >= query_positions * q_heads * head_dim,
972 "attention output must hold [query_positions, q_heads, head_dim]"
973 );
974 let out = out.as_mut_ptr();
975 unsafe {
978 gqa_attention_head_range_into(
979 queries,
980 keys,
981 values,
982 additive_mask,
983 query_positions,
984 key_positions,
985 q_heads,
986 kv_heads,
987 head_dim,
988 softmax_arithmetic,
989 accumulation,
990 q_head_range,
991 out,
992 );
993 }
994}
995
996#[allow(clippy::too_many_arguments)]
1010pub(crate) unsafe fn gqa_attention_head_range_into(
1011 queries: &[f32],
1012 keys: &[f32],
1013 values: &[f32],
1014 additive_mask: &[f32],
1015 query_positions: usize,
1016 key_positions: usize,
1017 q_heads: usize,
1018 kv_heads: usize,
1019 head_dim: usize,
1020 softmax_arithmetic: F32SoftmaxArithmetic,
1021 accumulation: F32LinearAccumulation,
1022 q_head_range: std::ops::Range<usize>,
1023 out: *mut f32,
1024) {
1025 assert!(q_head_range.end <= q_heads, "head range exceeds q_heads");
1026 let scale = (head_dim as f32).sqrt().recip();
1027 let kv_group = q_heads / kv_heads;
1028 let mut scores = vec![0.0f32; key_positions];
1029
1030 for query_position in 0..query_positions {
1031 let mask =
1032 &additive_mask[query_position * key_positions..(query_position + 1) * key_positions];
1033 for q_head in q_head_range.clone() {
1034 let kv_head = q_head / kv_group;
1035 let query_base = (query_position * q_heads + q_head) * head_dim;
1036 let query = &queries[query_base..query_base + head_dim];
1037 for (key_position, score) in scores.iter_mut().enumerate() {
1038 let key_base = (key_position * kv_heads + kv_head) * head_dim;
1039 let key = &keys[key_base..key_base + head_dim];
1040 let dot = dot_with_accumulation(query, key, accumulation);
1041 *score = dot * scale + mask[key_position];
1042 }
1043 softmax_rows_with_arithmetic(&mut scores, 1, key_positions, softmax_arithmetic);
1044
1045 let head_out = unsafe { std::slice::from_raw_parts_mut(out.add(query_base), head_dim) };
1049 attention_weighted_sum(
1050 &scores,
1051 values,
1052 kv_head,
1053 kv_heads,
1054 head_dim,
1055 accumulation,
1056 head_out,
1057 );
1058 }
1059 }
1060}
1061
1062#[cfg(all(feature = "accelerate-sgemm", target_os = "macos"))]
1068#[allow(clippy::too_many_arguments)]
1069fn accelerate_gqa_attention(
1070 queries: &[f32],
1071 keys: &[f32],
1072 values: &[f32],
1073 additive_mask: &[f32],
1074 query_positions: usize,
1075 key_positions: usize,
1076 q_heads: usize,
1077 kv_heads: usize,
1078 head_dim: usize,
1079 softmax_arithmetic: F32SoftmaxArithmetic,
1080 out: &mut [f32],
1081) -> bool {
1082 let scale = (head_dim as f32).sqrt().recip();
1083 let kv_group = q_heads / kv_heads;
1084 let mut query_matrix = vec![0.0f32; query_positions * head_dim];
1085 let mut key_matrix = vec![0.0f32; key_positions * head_dim];
1086 let mut value_transpose = vec![0.0f32; head_dim * key_positions];
1087 let mut scores = vec![0.0f32; query_positions * key_positions];
1088 let mut context = vec![0.0f32; query_positions * head_dim];
1089
1090 for q_head in 0..q_heads {
1091 let kv_head = q_head / kv_group;
1092 for query_position in 0..query_positions {
1093 let query_base = (query_position * q_heads + q_head) * head_dim;
1094 query_matrix[query_position * head_dim..(query_position + 1) * head_dim]
1095 .copy_from_slice(&queries[query_base..query_base + head_dim]);
1096 }
1097 for key_position in 0..key_positions {
1098 let key_base = (key_position * kv_heads + kv_head) * head_dim;
1099 key_matrix[key_position * head_dim..(key_position + 1) * head_dim]
1100 .copy_from_slice(&keys[key_base..key_base + head_dim]);
1101 for lane in 0..head_dim {
1102 value_transpose[lane * key_positions + key_position] = values[key_base + lane];
1103 }
1104 }
1105
1106 if !accelerate_sgemm(
1107 &query_matrix,
1108 &key_matrix,
1109 query_positions,
1110 head_dim,
1111 key_positions,
1112 0.0,
1113 false,
1114 &mut scores,
1115 ) {
1116 return false;
1117 }
1118 for query_position in 0..query_positions {
1119 let score_row =
1120 &mut scores[query_position * key_positions..(query_position + 1) * key_positions];
1121 let mask = &additive_mask
1122 [query_position * key_positions..(query_position + 1) * key_positions];
1123 for (score, mask_value) in score_row.iter_mut().zip(mask) {
1124 *score = *score * scale + mask_value;
1125 }
1126 softmax_rows_with_arithmetic(score_row, 1, key_positions, softmax_arithmetic);
1127 }
1128 if !accelerate_sgemm(
1129 &scores,
1130 &value_transpose,
1131 query_positions,
1132 key_positions,
1133 head_dim,
1134 0.0,
1135 false,
1136 &mut context,
1137 ) {
1138 return false;
1139 }
1140 for query_position in 0..query_positions {
1141 let out_base = (query_position * q_heads + q_head) * head_dim;
1142 out[out_base..out_base + head_dim].copy_from_slice(
1143 &context[query_position * head_dim..(query_position + 1) * head_dim],
1144 );
1145 }
1146 }
1147 true
1148}
1149
1150#[cfg(not(all(feature = "accelerate-sgemm", target_os = "macos")))]
1151#[allow(clippy::too_many_arguments)]
1152fn accelerate_gqa_attention(
1153 _queries: &[f32],
1154 _keys: &[f32],
1155 _values: &[f32],
1156 _additive_mask: &[f32],
1157 _query_positions: usize,
1158 _key_positions: usize,
1159 _q_heads: usize,
1160 _kv_heads: usize,
1161 _head_dim: usize,
1162 _softmax_arithmetic: F32SoftmaxArithmetic,
1163 _out: &mut [f32],
1164) -> bool {
1165 false
1166}
1167
1168#[allow(clippy::too_many_arguments)]
1169fn attention_weighted_sum(
1170 scores: &[f32],
1171 values: &[f32],
1172 kv_head: usize,
1173 kv_heads: usize,
1174 head_dim: usize,
1175 accumulation: F32LinearAccumulation,
1176 out: &mut [f32],
1177) {
1178 if accumulation == F32LinearAccumulation::WidenedF64 {
1179 for lane in 0..head_dim {
1180 let mut sum = 0.0f64;
1181 for (key_position, weight) in scores.iter().copied().enumerate() {
1182 let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1183 sum += f64::from(weight) * f64::from(value);
1184 }
1185 out[lane] = sum as f32;
1186 }
1187 return;
1188 }
1189 let lanes = match accumulation {
1190 F32LinearAccumulation::Scalar => 1,
1191 F32LinearAccumulation::Lanes4 | F32LinearAccumulation::FusedLanes4 => 4,
1192 F32LinearAccumulation::Lanes8 | F32LinearAccumulation::FusedLanes8 => 8,
1193 F32LinearAccumulation::Accelerate
1194 | F32LinearAccumulation::AccelerateRowInvariant
1195 | F32LinearAccumulation::AccelerateBiasSeeded
1196 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant => 1,
1197 F32LinearAccumulation::WidenedF64 => unreachable!("handled above"),
1198 };
1199 for lane in 0..head_dim {
1200 let mut partial = [0.0f32; 8];
1201 for (key_position, weight) in scores.iter().copied().enumerate() {
1202 let value = values[(key_position * kv_heads + kv_head) * head_dim + lane];
1203 let partial_index = key_position % lanes;
1204 partial[partial_index] = match accumulation {
1205 F32LinearAccumulation::FusedLanes4 | F32LinearAccumulation::FusedLanes8 => {
1206 weight.mul_add(value, partial[partial_index])
1207 }
1208 F32LinearAccumulation::Scalar
1209 | F32LinearAccumulation::Lanes4
1210 | F32LinearAccumulation::Lanes8
1211 | F32LinearAccumulation::Accelerate
1212 | F32LinearAccumulation::AccelerateRowInvariant
1213 | F32LinearAccumulation::AccelerateBiasSeeded
1214 | F32LinearAccumulation::AccelerateBiasSeededRowInvariant
1215 | F32LinearAccumulation::WidenedF64 => partial[partial_index] + weight * value,
1216 };
1217 }
1218 let mut sum = 0.0f32;
1219 for value in &partial[..lanes] {
1220 sum += *value;
1221 }
1222 out[lane] = sum;
1223 }
1224}
1225
1226pub fn mrope_interleave(axes: [&[f32]; 3], sections: [usize; 3], out: &mut [f32]) {
1241 let half = out.len();
1242 for axis in axes {
1243 assert!(
1244 axis.len() >= half,
1245 "axis row shorter than the half-dimension"
1246 );
1247 }
1248
1249 out.copy_from_slice(&axes[0][..half]);
1252 let modality_num = 3usize;
1253 for (axis_index, section) in sections.iter().enumerate().skip(1) {
1254 let end = section * modality_num;
1255 let mut lane = axis_index;
1256 while lane < end && lane < half {
1257 out[lane] = axes[axis_index][lane];
1258 lane += modality_num;
1259 }
1260 }
1261}
1262
1263pub fn apply_rope_in_place(row: &mut [f32], cos: &[f32], sin: &[f32]) {
1273 let dim = row.len();
1274 assert_eq!(cos.len(), dim, "cos must match head_dim");
1275 assert_eq!(sin.len(), dim, "sin must match head_dim");
1276 assert!(dim.is_multiple_of(2), "head_dim must be even");
1277
1278 let half = dim / 2;
1279 let original: Vec<f32> = row.to_vec();
1280 for index in 0..dim {
1281 let rotated = if index < half {
1282 -original[index + half]
1283 } else {
1284 original[index - half]
1285 };
1286 row[index] = original[index] * cos[index] + rotated * sin[index];
1287 }
1288}
1289
1290#[cfg(test)]
1291mod tests {
1292 use super::*;
1293
1294 #[test]
1295 fn linear_matches_a_hand_computed_product() {
1296 let x = [1.0, 2.0, 3.0];
1298 let weight = [1.0, 0.0, -1.0, 2.0, 2.0, 2.0];
1299 let mut out = [0.0; 2];
1300 linear(&x, &weight, None, 1, 3, 2, &mut out);
1301 assert_eq!(out, [-2.0, 12.0]);
1302
1303 let mut biased = [0.0; 2];
1304 linear(&x, &weight, Some(&[10.0, -12.0]), 1, 3, 2, &mut biased);
1305 assert_eq!(biased, [8.0, 0.0]);
1306 }
1307
1308 #[test]
1309 fn torch_cascade_sum_agrees_with_the_flat_sum_when_rounding_cannot_intervene() {
1310 for length in [1usize, 7, 8, 15, 16, 31, 128, 1024, 3072] {
1314 for width in [4usize, 8] {
1315 let values: Vec<f32> = (0..length).map(|index| (index % 8) as f32).collect();
1316 let flat: f32 = values.iter().sum();
1317 assert_eq!(
1318 torch_cascade_sum(&values, width, |value| value),
1319 flat,
1320 "length {length}, width {width}"
1321 );
1322 }
1323 }
1324 }
1325
1326 #[test]
1327 fn torch_cascade_sum_applies_its_transform_before_accumulating() {
1328 let values = [1.0f32, 2.0, 3.0, 4.0, 5.0];
1329 assert_eq!(torch_cascade_sum(&values, 4, |value| value * value), 55.0);
1330 }
1331
1332 #[test]
1333 fn torch_cascade_sum_differs_from_a_flat_sum_once_rounding_matters() {
1334 let mut values = vec![1.0f32; 1024];
1339 values[0] = 1.0e8;
1340 let flat = values.iter().fold(0.0f32, |sum, value| sum + value);
1341 assert_ne!(torch_cascade_sum(&values, 8, |value| value), flat);
1342 }
1343
1344 #[test]
1345 fn ceil_log2_matches_its_definition() {
1346 assert_eq!(ceil_log2(0), 0);
1347 assert_eq!(ceil_log2(1), 0);
1348 assert_eq!(ceil_log2(2), 1);
1349 assert_eq!(ceil_log2(3), 2);
1350 assert_eq!(ceil_log2(32), 5);
1351 assert_eq!(ceil_log2(33), 6);
1352 }
1353
1354 #[test]
1355 fn rms_norm_normalizes_and_scales() {
1356 let x = [3.0f32, 4.0];
1358 let weight = [1.0f32, 1.0];
1359 let mut out = [0.0; 2];
1360 rms_norm(&x, &weight, 0.0, 1, 2, &mut out);
1361 let expected = 12.5f32.sqrt().recip();
1362 assert!((out[0] - 3.0 * expected).abs() < 1e-6);
1363 assert!((out[1] - 4.0 * expected).abs() < 1e-6);
1364
1365 let mut weighted = [0.0; 2];
1367 rms_norm(&x, &[2.0, 0.5], 0.0, 1, 2, &mut weighted);
1368 assert!((weighted[0] - 3.0 * expected * 2.0).abs() < 1e-6);
1369 assert!((weighted[1] - 4.0 * expected * 0.5).abs() < 1e-6);
1370 }
1371
1372 #[test]
1373 fn silu_mul_matches_the_definition() {
1374 let mut gate = [0.0f32, 1.0, -1.0];
1375 let up = [1.0f32, 2.0, 3.0];
1376 silu_mul_in_place(&mut gate, &up);
1377 assert_eq!(gate[0], 0.0);
1378 let silu_one = 1.0f32 / (1.0 + (-1.0f32).exp());
1379 assert!((gate[1] - silu_one * 2.0).abs() < 1e-6);
1380 let silu_neg = -1.0f32 / (1.0 + 1.0f32.exp());
1381 assert!((gate[2] - silu_neg * 3.0).abs() < 1e-6);
1382 }
1383
1384 #[test]
1385 fn softmax_rows_sums_to_one_and_is_shift_invariant() {
1386 let mut x = [1.0f32, 2.0, 3.0, 101.0, 102.0, 103.0];
1387 softmax_rows(&mut x, 2, 3);
1388 let first: f32 = x[..3].iter().sum();
1389 let second: f32 = x[3..].iter().sum();
1390 assert!((first - 1.0).abs() < 1e-6);
1391 assert!((second - 1.0).abs() < 1e-6);
1392 for index in 0..3 {
1394 assert!((x[index] - x[index + 3]).abs() < 1e-6);
1395 }
1396 }
1397
1398 #[test]
1399 fn gqa_maps_each_query_head_to_its_kv_group() {
1400 let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (1, 1, 4, 2, 2);
1401 let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1402 let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1403 let values = [10.0f32, 11.0, 20.0, 21.0];
1404 let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1405
1406 gqa_attention(
1407 &queries,
1408 &keys,
1409 &values,
1410 &[0.0],
1411 query_positions,
1412 key_positions,
1413 q_heads,
1414 kv_heads,
1415 head_dim,
1416 &mut out,
1417 );
1418
1419 assert_eq!(&out[0..2], &[10.0, 11.0]);
1420 assert_eq!(&out[2..4], &[10.0, 11.0]);
1421 assert_eq!(&out[4..6], &[20.0, 21.0]);
1422 assert_eq!(&out[6..8], &[20.0, 21.0]);
1423 }
1424
1425 #[test]
1426 fn gqa_honors_the_additive_causal_mask() {
1427 let (query_positions, key_positions, q_heads, kv_heads, head_dim) = (2, 2, 1, 1, 2);
1428 let queries = vec![0.0f32; query_positions * q_heads * head_dim];
1429 let keys = vec![0.0f32; key_positions * kv_heads * head_dim];
1430 let values = [2.0f32, 4.0, 10.0, 20.0];
1431 let mask = [0.0f32, f32::NEG_INFINITY, 0.0, 0.0];
1432 let mut out = vec![0.0f32; query_positions * q_heads * head_dim];
1433
1434 gqa_attention(
1435 &queries,
1436 &keys,
1437 &values,
1438 &mask,
1439 query_positions,
1440 key_positions,
1441 q_heads,
1442 kv_heads,
1443 head_dim,
1444 &mut out,
1445 );
1446
1447 assert_eq!(&out[0..2], &[2.0, 4.0]);
1448 assert_eq!(&out[2..4], &[6.0, 12.0]);
1449 }
1450
1451 #[test]
1452 fn rope_rotates_a_known_pair() {
1453 let mut row = [3.0f32, 5.0];
1455 apply_rope_in_place(&mut row, &[0.0, 0.0], &[1.0, 1.0]);
1456 assert_eq!(row, [-5.0, 3.0]);
1457
1458 let mut same = [3.0f32, 5.0];
1460 apply_rope_in_place(&mut same, &[1.0, 1.0], &[0.0, 0.0]);
1461 assert_eq!(same, [3.0, 5.0]);
1462 }
1463
1464 #[test]
1465 fn mrope_interleave_is_identity_when_all_axes_agree() {
1466 let axis: Vec<f32> = (0..64).map(|value| value as f32).collect();
1469 let mut out = vec![0.0f32; 64];
1470 mrope_interleave([&axis, &axis, &axis], [24, 20, 20], &mut out);
1471 assert_eq!(out, axis);
1472 }
1473
1474 #[test]
1475 fn mrope_interleave_selects_the_documented_lanes() {
1476 let zeros = vec![0.0f32; 64];
1477 let ones = vec![1.0f32; 64];
1478 let twos = vec![2.0f32; 64];
1479 let mut out = vec![0.0f32; 64];
1480 mrope_interleave([&zeros, &ones, &twos], [24, 20, 20], &mut out);
1481
1482 for (lane, value) in out.iter().enumerate() {
1485 let expected = if lane < 60 && lane % 3 == 1 {
1486 1.0
1487 } else if lane < 60 && lane % 3 == 2 {
1488 2.0
1489 } else {
1490 0.0
1491 };
1492 assert_eq!(*value, expected, "lane {lane}");
1493 }
1494 }
1495}