1use alloc::vec;
7use alloc::vec::Vec;
8use burn_backend::Scalar;
9use burn_backend::ops::{ActivationOps, FloatTensorOps};
10use burn_backend::tensor::FloatTensor;
11use burn_backend::{DType, TensorMetadata};
12use burn_std::{Bytes, bf16, f16};
13#[cfg(not(feature = "std"))]
14#[allow(unused_imports)]
15use num_traits::Float;
16use num_traits::ToPrimitive;
17
18use crate::ops::binary::binary_op;
19use crate::ops::unary::unary_op;
20use crate::{Flex, FlexTensor, Layout};
21
22impl ActivationOps<Flex> for Flex {
23 fn relu(tensor: FloatTensor<Flex>) -> FloatTensor<Flex> {
24 unary_op(
29 tensor,
30 |x: f32| if x.is_nan() || x > 0.0 { x } else { 0.0 },
31 |x: f64| if x.is_nan() || x > 0.0 { x } else { 0.0 },
32 )
33 }
34
35 fn relu_backward(output: FloatTensor<Flex>, grad: FloatTensor<Flex>) -> FloatTensor<Flex> {
36 binary_op(
40 output,
41 grad,
42 |out: f32, g| if out.is_nan() || out > 0.0 { g } else { 0.0 },
43 |out: f64, g| if out.is_nan() || out > 0.0 { g } else { 0.0 },
44 None,
45 )
46 }
47
48 fn leaky_relu(tensor: FloatTensor<Flex>, negative_slope: Scalar) -> FloatTensor<Flex> {
49 let ns32 = negative_slope.to_f32().unwrap();
50 let ns64 = negative_slope.to_f64().unwrap();
51 unary_op(
52 tensor,
53 move |x: f32| if x >= 0.0 { x } else { ns32 * x },
54 move |x: f64| if x >= 0.0 { x } else { ns64 * x },
55 )
56 }
57
58 fn prelu(tensor: FloatTensor<Flex>, alpha: FloatTensor<Flex>) -> FloatTensor<Flex> {
59 binary_op(
61 tensor,
62 alpha,
63 |x: f32, a| if x >= 0.0 { x } else { a * x },
64 |x: f64, a| if x >= 0.0 { x } else { a * x },
65 None,
66 )
67 }
68
69 fn gelu(tensor: FloatTensor<Flex>) -> FloatTensor<Flex> {
70 use crate::ops::unary::{erf_f32, erf_f64};
72 let sqrt2_f32: f32 = core::f32::consts::SQRT_2;
73 let sqrt2_f64: f64 = core::f64::consts::SQRT_2;
74 unary_op(
75 tensor,
76 move |x: f32| 0.5 * x * (1.0 + erf_f32(x / sqrt2_f32)),
77 move |x: f64| 0.5 * x * (1.0 + erf_f64(x / sqrt2_f64)),
78 )
79 }
80
81 fn gelu_backward(x: FloatTensor<Flex>, grad: FloatTensor<Flex>) -> FloatTensor<Flex> {
82 use crate::ops::unary::{erf_f32, erf_f64};
84 let sqrt2_f32: f32 = core::f32::consts::SQRT_2;
85 let sqrt2_f64: f64 = core::f64::consts::SQRT_2;
86 let inv_sqrt_2pi_f32: f32 = 1.0 / (2.0 * core::f32::consts::PI).sqrt();
87 let inv_sqrt_2pi_f64: f64 = 1.0 / (2.0 * core::f64::consts::PI).sqrt();
88 binary_op(
89 x,
90 grad,
91 move |x: f32, g| {
92 let cdf = 0.5 * (1.0 + erf_f32(x / sqrt2_f32));
93 let pdf = inv_sqrt_2pi_f32 * (-0.5 * x * x).exp();
94 g * (cdf + x * pdf)
95 },
96 move |x: f64, g| {
97 let cdf = 0.5 * (1.0 + erf_f64(x / sqrt2_f64));
98 let pdf = inv_sqrt_2pi_f64 * (-0.5 * x * x).exp();
99 g * (cdf + x * pdf)
100 },
101 None,
102 )
103 }
104
105 fn sigmoid(tensor: FloatTensor<Flex>) -> FloatTensor<Flex> {
106 unary_op(tensor, sigmoid_f32, sigmoid_f64)
107 }
108
109 fn sigmoid_backward(output: FloatTensor<Flex>, grad: FloatTensor<Flex>) -> FloatTensor<Flex> {
110 binary_op(
112 output,
113 grad,
114 |s: f32, g| g * s * (1.0 - s),
115 |s: f64, g| g * s * (1.0 - s),
116 None,
117 )
118 }
119
120 fn hard_sigmoid(tensor: FloatTensor<Flex>, alpha: Scalar, beta: Scalar) -> FloatTensor<Flex> {
121 let alpha32 = alpha.to_f32().unwrap();
122 let beta32 = beta.to_f32().unwrap();
123 let alpha64 = alpha.to_f64().unwrap();
124 let beta64 = beta.to_f64().unwrap();
125 unary_op(
126 tensor,
127 move |x: f32| (alpha32 * x + beta32).clamp(0.0, 1.0),
128 move |x: f64| (alpha64 * x + beta64).clamp(0.0, 1.0),
129 )
130 }
131
132 fn log_sigmoid(tensor: FloatTensor<Flex>) -> FloatTensor<Flex> {
133 unary_op(
137 tensor,
138 |x: f32| {
139 if x >= 0.0 {
140 -((-x).exp().ln_1p())
141 } else {
142 x - x.exp().ln_1p()
143 }
144 },
145 |x: f64| {
146 if x >= 0.0 {
147 -((-x).exp().ln_1p())
148 } else {
149 x - x.exp().ln_1p()
150 }
151 },
152 )
153 }
154
155 fn log_sigmoid_backward(x: FloatTensor<Flex>, grad: FloatTensor<Flex>) -> FloatTensor<Flex> {
156 binary_op(
159 x,
160 grad,
161 |x: f32, g| g * sigmoid_f32(-x),
162 |x: f64, g| g * sigmoid_f64(-x),
163 None,
164 )
165 }
166
167 fn softmax(tensor: FloatTensor<Flex>, dim: usize) -> FloatTensor<Flex> {
168 softmax(tensor, dim)
169 }
170}
171
172#[inline]
173fn sigmoid_f32(x: f32) -> f32 {
174 if x >= 0.0 {
175 1.0 / (1.0 + (-x).exp())
176 } else {
177 let e = x.exp();
178 e / (1.0 + e)
179 }
180}
181
182#[inline]
183fn sigmoid_f64(x: f64) -> f64 {
184 if x >= 0.0 {
185 1.0 / (1.0 + (-x).exp())
186 } else {
187 let e = x.exp();
188 e / (1.0 + e)
189 }
190}
191
192pub fn softmax(tensor: FloatTensor<Flex>, dim: usize) -> FloatTensor<Flex> {
213 let rank = tensor.shape().num_dims();
214 assert!(
215 dim < rank,
216 "softmax dim {} out of range for rank {}",
217 dim,
218 rank
219 );
220
221 if dim != rank - 1 {
222 let swapped = Flex::float_swap_dims(tensor, dim, rank - 1);
223 let normed = softmax_last(swapped);
224 return Flex::float_swap_dims(normed, dim, rank - 1);
225 }
226
227 softmax_last(tensor)
228}
229
230fn softmax_last(tensor: FloatTensor<Flex>) -> FloatTensor<Flex> {
231 let tensor = tensor.to_contiguous();
232 match tensor.dtype() {
233 DType::F32 => softmax_last_f32(tensor),
234 DType::F64 => softmax_last_f64(tensor),
235 DType::F16 => softmax_last_f16(tensor),
236 DType::BF16 => softmax_last_bf16(tensor),
237 dtype => panic!("softmax: unsupported dtype {:?}", dtype),
238 }
239}
240
241fn softmax_last_f32(tensor: FlexTensor) -> FlexTensor {
242 let shape = tensor.layout().shape().clone();
243 let last = *shape.last().expect("softmax: empty shape");
244 if last == 0 {
245 return tensor;
246 }
247 let input: &[f32] = tensor.storage();
248 let n = input.len();
249
250 let mut output: Vec<f32> = vec![0.0; n];
262 let out_slice = output.as_mut_slice();
263
264 #[cfg(feature = "rayon")]
267 {
268 use rayon::prelude::*;
269 const ROWS_PER_TASK: usize = 64;
270 let chunk_elems = ROWS_PER_TASK * last;
271 out_slice
272 .par_chunks_mut(chunk_elems)
273 .zip(input.par_chunks(chunk_elems))
274 .for_each(|(o, i)| softmax_rows_f32(i, o, last));
275 }
276 #[cfg(not(feature = "rayon"))]
277 {
278 softmax_rows_f32(input, out_slice, last);
279 }
280
281 FlexTensor::new(
282 Bytes::from_elems(output),
283 Layout::contiguous(shape),
284 DType::F32,
285 )
286}
287
288#[inline]
293fn softmax_rows_f32(input: &[f32], output: &mut [f32], row_len: usize) {
294 assert_eq!(input.len(), output.len());
301 assert_eq!(input.len() % row_len, 0);
302 #[cfg(feature = "simd")]
303 softmax_rows_f32_simd(input, output, row_len);
304 #[cfg(not(feature = "simd"))]
305 {
306 for (in_row, out_row) in input.chunks(row_len).zip(output.chunks_mut(row_len)) {
307 softmax_row_f32_scalar(in_row, out_row);
308 }
309 }
310}
311
312#[cfg(feature = "simd")]
313#[macerator::with_simd]
314fn softmax_rows_f32_simd<S: macerator::Simd>(input: &[f32], output: &mut [f32], row_len: usize) {
315 debug_assert_eq!(input.len(), output.len());
316 debug_assert_eq!(input.len() % row_len, 0);
317 for (in_row, out_row) in input.chunks(row_len).zip(output.chunks_mut(row_len)) {
318 softmax_row_f32_simd::<S>(in_row, out_row);
319 }
320}
321
322#[cfg(not(feature = "simd"))]
326#[inline]
327fn softmax_row_f32_scalar(input: &[f32], output: &mut [f32]) {
328 let mut max_val = f32::NEG_INFINITY;
329 for &x in input {
330 if x > max_val {
331 max_val = x;
332 }
333 }
334 let mut sum = 0.0f32;
335 for (i, &x) in input.iter().enumerate() {
336 let e = (x - max_val).exp();
337 output[i] = e;
338 sum += e;
339 }
340 let inv = 1.0f32 / sum;
341 for x in output.iter_mut() {
342 *x *= inv;
343 }
344}
345
346#[cfg(feature = "simd")]
350#[inline(always)]
351fn softmax_row_f32_simd<S: macerator::Simd>(input: &[f32], output: &mut [f32]) {
352 use macerator::{Scalar, vload_unaligned, vstore_unaligned};
353 let lanes = <f32 as Scalar>::lanes::<S>();
354 let len = input.len();
355 let simd_len = len / lanes * lanes;
356
357 let (mut max_val, tail_start) = if simd_len >= lanes {
360 let mut max_vec = unsafe { vload_unaligned::<S, _>(input.as_ptr()) };
361 let mut j = lanes;
362 while j < simd_len {
363 let v = unsafe { vload_unaligned::<S, _>(input.as_ptr().add(j)) };
364 max_vec = max_vec.max(v);
365 j += lanes;
366 }
367 (max_vec.reduce_max(), simd_len)
368 } else {
369 (f32::NEG_INFINITY, 0)
370 };
371 for &x in &input[tail_start..] {
372 if x > max_val {
373 max_val = x;
374 }
375 }
376
377 let mut sum = 0.0f32;
382 for idx in 0..len {
383 let e = (input[idx] - max_val).exp();
384 output[idx] = e;
385 sum += e;
386 }
387
388 let inv = 1.0f32 / sum;
391 let inv_vec = inv.splat::<S>();
392 let mut i = 0;
393 while i < simd_len {
394 unsafe {
395 let v = vload_unaligned::<S, _>(output.as_ptr().add(i));
396 vstore_unaligned::<S, _>(output.as_mut_ptr().add(i), v * inv_vec);
397 }
398 i += lanes;
399 }
400 for x in &mut output[i..] {
401 *x *= inv;
402 }
403}
404
405macro_rules! softmax_last_dtype {
411 ($fn_name:ident, $T:ty, $zero:expr, $dtype:expr, $row_fn:ident) => {
412 fn $fn_name(tensor: FlexTensor) -> FlexTensor {
413 let shape = tensor.layout().shape().clone();
414 let last = *shape.last().expect("softmax: empty shape");
415 if last == 0 {
416 return tensor;
417 }
418 let input: &[$T] = tensor.storage();
419 let mut output: Vec<$T> = vec![$zero; input.len()];
420
421 #[cfg(feature = "rayon")]
422 {
423 use rayon::prelude::*;
424 const ROWS_PER_TASK: usize = 64;
425 let chunk_elems = ROWS_PER_TASK * last;
426 output
427 .par_chunks_mut(chunk_elems)
428 .zip(input.par_chunks(chunk_elems))
429 .for_each(|(o_chunk, i_chunk)| {
430 for (i, o) in i_chunk
431 .chunks_exact(last)
432 .zip(o_chunk.chunks_exact_mut(last))
433 {
434 $row_fn(i, o);
435 }
436 });
437 }
438 #[cfg(not(feature = "rayon"))]
439 {
440 for (i, o) in input.chunks_exact(last).zip(output.chunks_exact_mut(last)) {
441 $row_fn(i, o);
442 }
443 }
444
445 FlexTensor::new(Bytes::from_elems(output), Layout::contiguous(shape), $dtype)
446 }
447 };
448}
449
450macro_rules! softmax_row_half {
456 ($fn_name:ident, $T:ty) => {
457 #[inline]
458 fn $fn_name(input: &[$T], output: &mut [$T]) {
459 let mut max_val = f32::NEG_INFINITY;
460 for &x in input {
461 let xf = x.to_f32();
462 if xf > max_val {
463 max_val = xf;
464 }
465 }
466 let mut sum = 0.0f32;
467 for (i, &x) in input.iter().enumerate() {
468 let e = (x.to_f32() - max_val).exp();
469 output[i] = <$T>::from_f32(e);
470 sum += e;
471 }
472 let inv = 1.0f32 / sum;
473 for x in output.iter_mut() {
474 *x = <$T>::from_f32(x.to_f32() * inv);
475 }
476 }
477 };
478}
479
480#[inline]
481fn softmax_row_f64(input: &[f64], output: &mut [f64]) {
482 let mut max_val = f64::NEG_INFINITY;
483 for &x in input {
484 if x > max_val {
485 max_val = x;
486 }
487 }
488 let mut sum = 0.0f64;
489 for (i, &x) in input.iter().enumerate() {
490 let e = (x - max_val).exp();
491 output[i] = e;
492 sum += e;
493 }
494 let inv = 1.0f64 / sum;
495 for x in output.iter_mut() {
496 *x *= inv;
497 }
498}
499
500softmax_row_half!(softmax_row_f16, f16);
501softmax_row_half!(softmax_row_bf16, bf16);
502
503softmax_last_dtype!(softmax_last_f64, f64, 0.0f64, DType::F64, softmax_row_f64);
504softmax_last_dtype!(
505 softmax_last_f16,
506 f16,
507 f16::from_f32(0.0),
508 DType::F16,
509 softmax_row_f16
510);
511softmax_last_dtype!(
512 softmax_last_bf16,
513 bf16,
514 bf16::from_f32(0.0),
515 DType::BF16,
516 softmax_row_bf16
517);
518
519pub fn layer_norm(
551 input: FloatTensor<Flex>,
552 gamma: FloatTensor<Flex>,
553 beta: Option<FloatTensor<Flex>>,
554 epsilon: f64,
555) -> FloatTensor<Flex> {
556 let rank = input.shape().num_dims();
557 assert!(rank >= 1, "layer_norm: input must have at least one dim");
558 assert_eq!(
563 gamma.dtype(),
564 input.dtype(),
565 "layer_norm: gamma dtype {:?} does not match input dtype {:?}",
566 gamma.dtype(),
567 input.dtype(),
568 );
569 if let Some(ref b) = beta {
570 assert_eq!(
571 b.dtype(),
572 input.dtype(),
573 "layer_norm: beta dtype {:?} does not match input dtype {:?}",
574 b.dtype(),
575 input.dtype(),
576 );
577 }
578 let input = input.to_contiguous();
579 let gamma = gamma.to_contiguous();
580 let beta = beta.map(|b| b.to_contiguous());
581
582 let d_model = *input
583 .layout()
584 .shape()
585 .last()
586 .expect("layer_norm: empty shape");
587 let gamma_shape = gamma.layout().shape();
592 assert!(
593 gamma_shape.len() == 1 && gamma_shape[0] == d_model,
594 "layer_norm: gamma must be a 1-D tensor of length equal to last dim of input \
595 (got shape {:?}, expected [{}])",
596 gamma_shape,
597 d_model,
598 );
599 if let Some(ref b) = beta {
600 let beta_shape = b.layout().shape();
601 assert!(
602 beta_shape.len() == 1 && beta_shape[0] == d_model,
603 "layer_norm: beta must be a 1-D tensor of length equal to last dim of input \
604 (got shape {:?}, expected [{}])",
605 beta_shape,
606 d_model,
607 );
608 }
609
610 match input.dtype() {
611 DType::F32 => layer_norm_f32(input, gamma, beta, epsilon as f32),
612 DType::F64 => layer_norm_f64(input, gamma, beta, epsilon),
613 DType::F16 => {
614 layer_norm_via_f32::<f16>(input, gamma, beta, epsilon, f16::to_f32, f16::from_f32)
615 }
616 DType::BF16 => {
617 layer_norm_via_f32::<bf16>(input, gamma, beta, epsilon, bf16::to_f32, bf16::from_f32)
618 }
619 dtype => panic!("burn_flex::layer_norm: unsupported dtype {:?}", dtype),
620 }
621}
622
623fn layer_norm_via_f32<E: burn_backend::Element + bytemuck::Pod + Copy>(
624 input: FlexTensor,
625 gamma: FlexTensor,
626 beta: Option<FlexTensor>,
627 epsilon: f64,
628 to_f32: fn(E) -> f32,
629 from_f32: fn(f32) -> E,
630) -> FlexTensor {
631 let input_f32 = crate::ops::module::cast_to_f32::<E>(input, to_f32);
632 let gamma_f32 = crate::ops::module::cast_to_f32::<E>(gamma, to_f32);
633 let beta_f32 = beta.map(|b| crate::ops::module::cast_to_f32::<E>(b, to_f32));
634 let out = layer_norm_f32(input_f32, gamma_f32, beta_f32, epsilon as f32);
635 crate::ops::module::cast_from_f32::<E>(out, from_f32)
636}
637
638fn layer_norm_f64(
643 input: FlexTensor,
644 gamma: FlexTensor,
645 beta: Option<FlexTensor>,
646 epsilon: f64,
647) -> FlexTensor {
648 let shape = input.layout().shape().clone();
649 let d_model = *shape.last().expect("layer_norm: empty shape");
650 if d_model == 0 {
651 return input;
652 }
653 let input_data: &[f64] = input.storage();
654 let gamma_data: &[f64] = gamma.storage();
655 let beta_data: Option<&[f64]> = beta.as_ref().map(|b| b.storage());
656 let mut output: Vec<f64> = vec![0.0; input_data.len()];
657
658 #[cfg(feature = "rayon")]
659 {
660 use rayon::prelude::*;
661 const ROWS_PER_TASK: usize = 64;
662 let chunk_elems = ROWS_PER_TASK * d_model;
663 match beta_data {
664 Some(beta_slice) => {
665 output
666 .par_chunks_mut(chunk_elems)
667 .zip(input_data.par_chunks(chunk_elems))
668 .for_each(|(o, i)| {
669 layer_norm_rows_f64_with_beta(
670 i, o, gamma_data, beta_slice, d_model, epsilon,
671 );
672 });
673 }
674 None => {
675 output
676 .par_chunks_mut(chunk_elems)
677 .zip(input_data.par_chunks(chunk_elems))
678 .for_each(|(o, i)| {
679 layer_norm_rows_f64_no_beta(i, o, gamma_data, d_model, epsilon);
680 });
681 }
682 }
683 }
684 #[cfg(not(feature = "rayon"))]
685 {
686 match beta_data {
687 Some(beta_slice) => layer_norm_rows_f64_with_beta(
688 input_data,
689 output.as_mut_slice(),
690 gamma_data,
691 beta_slice,
692 d_model,
693 epsilon,
694 ),
695 None => layer_norm_rows_f64_no_beta(
696 input_data,
697 output.as_mut_slice(),
698 gamma_data,
699 d_model,
700 epsilon,
701 ),
702 }
703 }
704
705 FlexTensor::new(
706 Bytes::from_elems(output),
707 Layout::contiguous(shape),
708 DType::F64,
709 )
710}
711
712#[inline]
713fn layer_norm_rows_f64_with_beta(
714 input: &[f64],
715 output: &mut [f64],
716 gamma: &[f64],
717 beta: &[f64],
718 d_model: usize,
719 epsilon: f64,
720) {
721 for (in_row, out_row) in input.chunks(d_model).zip(output.chunks_mut(d_model)) {
722 let (mean, inv_std) = welford_f64(in_row, epsilon);
723 for (i, &x) in in_row.iter().enumerate() {
724 out_row[i] = (x - mean) * (inv_std * gamma[i]) + beta[i];
725 }
726 }
727}
728
729#[inline]
730fn layer_norm_rows_f64_no_beta(
731 input: &[f64],
732 output: &mut [f64],
733 gamma: &[f64],
734 d_model: usize,
735 epsilon: f64,
736) {
737 for (in_row, out_row) in input.chunks(d_model).zip(output.chunks_mut(d_model)) {
738 let (mean, inv_std) = welford_f64(in_row, epsilon);
739 for (i, &x) in in_row.iter().enumerate() {
740 out_row[i] = (x - mean) * (inv_std * gamma[i]);
741 }
742 }
743}
744
745#[inline]
746fn welford_f64(row: &[f64], epsilon: f64) -> (f64, f64) {
747 let mut mean = 0.0f64;
748 let mut m2 = 0.0f64;
749 for (k, &x) in row.iter().enumerate() {
750 let n_k = (k + 1) as f64;
751 let delta = x - mean;
752 mean += delta / n_k;
753 m2 += delta * (x - mean);
754 }
755 let var = m2 / row.len() as f64;
756 (mean, 1.0f64 / (var + epsilon).sqrt())
757}
758
759fn layer_norm_f32(
760 input: FlexTensor,
761 gamma: FlexTensor,
762 beta: Option<FlexTensor>,
763 epsilon: f32,
764) -> FlexTensor {
765 let shape = input.layout().shape().clone();
766 let d_model = *shape.last().expect("layer_norm: empty shape");
767 if d_model == 0 {
768 return input;
769 }
770
771 let input_data: &[f32] = input.storage();
772 let gamma_data: &[f32] = gamma.storage();
773 let beta_data: Option<&[f32]> = beta.as_ref().map(|b| b.storage());
774
775 let n = input_data.len();
776 let mut output: Vec<f32> = vec![0.0; n];
781 let out_slice = output.as_mut_slice();
782
783 #[cfg(feature = "rayon")]
788 {
789 use rayon::prelude::*;
790 const ROWS_PER_TASK: usize = 64;
791 let chunk_elems = ROWS_PER_TASK * d_model;
792 match beta_data {
793 Some(beta_slice) => {
794 out_slice
795 .par_chunks_mut(chunk_elems)
796 .zip(input_data.par_chunks(chunk_elems))
797 .for_each(|(o, i)| {
798 layer_norm_rows_f32_with_beta(
799 i, o, gamma_data, beta_slice, d_model, epsilon,
800 );
801 });
802 }
803 None => {
804 out_slice
805 .par_chunks_mut(chunk_elems)
806 .zip(input_data.par_chunks(chunk_elems))
807 .for_each(|(o, i)| {
808 layer_norm_rows_f32_no_beta(i, o, gamma_data, d_model, epsilon);
809 });
810 }
811 }
812 }
813 #[cfg(not(feature = "rayon"))]
814 {
815 match beta_data {
816 Some(beta_slice) => layer_norm_rows_f32_with_beta(
817 input_data, out_slice, gamma_data, beta_slice, d_model, epsilon,
818 ),
819 None => {
820 layer_norm_rows_f32_no_beta(input_data, out_slice, gamma_data, d_model, epsilon)
821 }
822 }
823 }
824
825 FlexTensor::new(
826 Bytes::from_elems(output),
827 Layout::contiguous(shape),
828 DType::F32,
829 )
830}
831
832#[inline]
835fn layer_norm_rows_f32_with_beta(
836 input: &[f32],
837 output: &mut [f32],
838 gamma: &[f32],
839 beta: &[f32],
840 d_model: usize,
841 epsilon: f32,
842) {
843 assert_eq!(input.len(), output.len());
845 assert_eq!(input.len() % d_model, 0);
846 assert_eq!(gamma.len(), d_model);
847 assert_eq!(beta.len(), d_model);
848 #[cfg(feature = "simd")]
849 layer_norm_rows_f32_with_beta_simd(input, output, gamma, beta, d_model, epsilon);
850 #[cfg(not(feature = "simd"))]
851 {
852 for (in_row, out_row) in input.chunks(d_model).zip(output.chunks_mut(d_model)) {
853 layer_norm_row_f32_scalar(in_row, out_row, gamma, Some(beta), epsilon);
854 }
855 }
856}
857
858#[inline]
860fn layer_norm_rows_f32_no_beta(
861 input: &[f32],
862 output: &mut [f32],
863 gamma: &[f32],
864 d_model: usize,
865 epsilon: f32,
866) {
867 assert_eq!(input.len(), output.len());
869 assert_eq!(input.len() % d_model, 0);
870 assert_eq!(gamma.len(), d_model);
871 #[cfg(feature = "simd")]
872 layer_norm_rows_f32_no_beta_simd(input, output, gamma, d_model, epsilon);
873 #[cfg(not(feature = "simd"))]
874 {
875 for (in_row, out_row) in input.chunks(d_model).zip(output.chunks_mut(d_model)) {
876 layer_norm_row_f32_scalar(in_row, out_row, gamma, None, epsilon);
877 }
878 }
879}
880
881#[cfg(not(feature = "simd"))]
885#[inline]
886fn layer_norm_row_f32_scalar(
887 input: &[f32],
888 output: &mut [f32],
889 gamma: &[f32],
890 beta: Option<&[f32]>,
891 epsilon: f32,
892) {
893 let len = input.len();
902 let mut mean = 0.0f32;
903 let mut m2 = 0.0f32;
904 for (k, &x) in input.iter().enumerate() {
905 let n_k = (k + 1) as f32;
906 let delta = x - mean;
907 mean += delta / n_k;
908 let delta2 = x - mean;
909 m2 += delta * delta2;
910 }
911 let var = m2 / len as f32;
912 let inv_std = 1.0f32 / (var + epsilon).sqrt();
913 for (i, &x) in input.iter().enumerate() {
914 let scale = inv_std * gamma[i];
915 let normed = (x - mean) * scale;
916 output[i] = match beta {
917 Some(b) => normed + b[i],
918 None => normed,
919 };
920 }
921}
922
923#[cfg(feature = "simd")]
926#[macerator::with_simd]
927fn layer_norm_rows_f32_with_beta_simd<S: macerator::Simd>(
928 input: &[f32],
929 output: &mut [f32],
930 gamma: &[f32],
931 beta: &[f32],
932 d_model: usize,
933 epsilon: f32,
934) {
935 debug_assert_eq!(input.len(), output.len());
936 debug_assert_eq!(input.len() % d_model, 0);
937 debug_assert_eq!(gamma.len(), d_model);
938 debug_assert_eq!(beta.len(), d_model);
939 for (in_row, out_row) in input.chunks(d_model).zip(output.chunks_mut(d_model)) {
940 layer_norm_row_f32_simd::<S>(in_row, out_row, gamma, Some(beta), epsilon);
941 }
942}
943
944#[cfg(feature = "simd")]
946#[macerator::with_simd]
947fn layer_norm_rows_f32_no_beta_simd<S: macerator::Simd>(
948 input: &[f32],
949 output: &mut [f32],
950 gamma: &[f32],
951 d_model: usize,
952 epsilon: f32,
953) {
954 debug_assert_eq!(input.len(), output.len());
955 debug_assert_eq!(input.len() % d_model, 0);
956 debug_assert_eq!(gamma.len(), d_model);
957 for (in_row, out_row) in input.chunks(d_model).zip(output.chunks_mut(d_model)) {
958 layer_norm_row_f32_simd::<S>(in_row, out_row, gamma, None, epsilon);
959 }
960}
961
962#[cfg(feature = "simd")]
964#[inline(always)]
965fn layer_norm_row_f32_simd<S: macerator::Simd>(
966 input: &[f32],
967 output: &mut [f32],
968 gamma: &[f32],
969 beta: Option<&[f32]>,
970 epsilon: f32,
971) {
972 use macerator::{Scalar, vload_unaligned, vstore_unaligned};
973 let lanes = <f32 as Scalar>::lanes::<S>();
974 let len = input.len();
975 let simd_len = len / lanes * lanes;
976
977 let (sum, sumsq) = if simd_len >= lanes {
981 let mut acc_sum = 0.0f32.splat::<S>();
982 let mut acc_sumsq = 0.0f32.splat::<S>();
983 let mut i = 0;
984 while i < simd_len {
985 unsafe {
986 let v = vload_unaligned::<S, _>(input.as_ptr().add(i));
987 acc_sum += v;
988 acc_sumsq = v.mul_add(v, acc_sumsq);
991 }
992 i += lanes;
993 }
994 let mut s = acc_sum.reduce_add();
995 let mut sq = acc_sumsq.reduce_add();
996 for &x in &input[simd_len..] {
997 s += x;
998 sq += x * x;
999 }
1000 (s, sq)
1001 } else {
1002 let mut s = 0.0f32;
1003 let mut sq = 0.0f32;
1004 for &x in input {
1005 s += x;
1006 sq += x * x;
1007 }
1008 (s, sq)
1009 };
1010
1011 let n = len as f32;
1012 let mean = sum / n;
1013 let var = (sumsq / n) - mean * mean;
1017 let inv_std = 1.0f32 / (var + epsilon).sqrt();
1018
1019 let mean_vec = mean.splat::<S>();
1025 let inv_std_vec = inv_std.splat::<S>();
1026 let mut i = 0;
1027 while i < simd_len {
1028 unsafe {
1029 let x = vload_unaligned::<S, _>(input.as_ptr().add(i));
1030 let g = vload_unaligned::<S, _>(gamma.as_ptr().add(i));
1031 let scale = inv_std_vec * g;
1033 let centered = x - mean_vec;
1035 let normed = centered * scale;
1037 let out = if let Some(b) = beta {
1038 let b_vec = vload_unaligned::<S, _>(b.as_ptr().add(i));
1039 normed + b_vec
1040 } else {
1041 normed
1042 };
1043 vstore_unaligned::<S, _>(output.as_mut_ptr().add(i), out);
1044 }
1045 i += lanes;
1046 }
1047 while i < len {
1049 let centered = input[i] - mean;
1050 let normed = centered * inv_std * gamma[i];
1051 output[i] = match beta {
1052 Some(b) => normed + b[i],
1053 None => normed,
1054 };
1055 i += 1;
1056 }
1057}
1058
1059#[cfg(test)]
1067mod tests {
1068 use alloc::vec;
1069 use burn_backend::{DType, TensorData, TensorMetadata, Tolerance};
1070 use burn_std::{bf16, f16};
1071 use num_traits::Float;
1072
1073 use crate::FlexTensor;
1074
1075 fn softmax_row<T: Float>(row_in: &[T], row_out: &mut [T]) {
1085 let max = row_in
1086 .iter()
1087 .copied()
1088 .fold(T::neg_infinity(), |a, b| if a > b { a } else { b });
1089 let mut sum = T::zero();
1090 for (i, &x) in row_in.iter().enumerate() {
1091 let e = (x - max).exp();
1092 row_out[i] = e;
1093 sum = sum + e;
1094 }
1095 for v in row_out.iter_mut() {
1096 *v = *v / sum;
1097 }
1098 }
1099
1100 fn softmax_last_ref<T: Float>(data: &[T], row_len: usize) -> Vec<T> {
1101 let mut out = vec![T::zero(); data.len()];
1102 for (i, o) in data.chunks(row_len).zip(out.chunks_mut(row_len)) {
1103 softmax_row(i, o);
1104 }
1105 out
1106 }
1107
1108 fn layer_norm_row<T: Float>(
1109 row_in: &[T],
1110 gamma: &[T],
1111 beta: Option<&[T]>,
1112 eps: T,
1113 row_out: &mut [T],
1114 ) {
1115 let n = T::from(row_in.len()).unwrap();
1116 let mean = row_in.iter().copied().fold(T::zero(), |a, b| a + b) / n;
1117 let var = row_in
1118 .iter()
1119 .map(|&x| (x - mean) * (x - mean))
1120 .fold(T::zero(), |a, b| a + b)
1121 / n;
1122 let inv_std = T::one() / (var + eps).sqrt();
1123 for (i, &x) in row_in.iter().enumerate() {
1124 let normed = (x - mean) * inv_std;
1125 let scaled = normed * gamma[i];
1126 row_out[i] = match beta {
1127 Some(b) => scaled + b[i],
1128 None => scaled,
1129 };
1130 }
1131 }
1132
1133 fn layer_norm_last_ref<T: Float>(
1134 data: &[T],
1135 gamma: &[T],
1136 beta: Option<&[T]>,
1137 eps: T,
1138 row_len: usize,
1139 ) -> Vec<T> {
1140 let mut out = vec![T::zero(); data.len()];
1141 for (i, o) in data.chunks(row_len).zip(out.chunks_mut(row_len)) {
1142 layer_norm_row(i, gamma, beta, eps, o);
1143 }
1144 out
1145 }
1146
1147 fn flex_f32(data: Vec<f32>, shape: &[usize]) -> FlexTensor {
1152 FlexTensor::from_data(TensorData::new(data, shape.to_vec()))
1153 }
1154
1155 fn flex_f64(data: Vec<f64>, shape: &[usize]) -> FlexTensor {
1156 FlexTensor::from_data(TensorData::new(data, shape.to_vec()))
1157 }
1158
1159 fn flex_half<T: burn_backend::Element>(data: Vec<T>, shape: &[usize]) -> FlexTensor {
1160 FlexTensor::from_data(TensorData::new(data, shape.to_vec()))
1161 }
1162
1163 #[test]
1168 fn test_layer_norm_2d_with_beta() {
1169 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], &[2, 4]);
1170 let gamma = flex_f32(vec![1.0; 4], &[4]);
1171 let beta = flex_f32(vec![0.0; 4], &[4]);
1172 let out = crate::ops::activation::layer_norm(t, gamma, Some(beta), 1e-5);
1173
1174 let expected: Vec<f32> = vec![
1175 -1.3416408, -0.4472136, 0.4472136, 1.3416408, -1.3416408, -0.4472136, 0.4472136,
1176 1.3416408,
1177 ];
1178 out.into_data().assert_approx_eq::<f32>(
1179 &TensorData::new(expected, vec![2, 4]),
1180 Tolerance::absolute(1e-4),
1181 );
1182 }
1183
1184 #[test]
1185 fn test_layer_norm_with_affine() {
1186 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 4]);
1187 let gamma = flex_f32(vec![2.0, 0.5, 1.0, 3.0], &[4]);
1188 let beta = flex_f32(vec![1.0, -1.0, 0.0, 2.0], &[4]);
1189 let out = crate::ops::activation::layer_norm(t, gamma, Some(beta), 1e-5);
1190
1191 out.into_data().assert_approx_eq::<f32>(
1194 &TensorData::new(vec![-1.6833, -1.2236, 0.4472, 6.0249], vec![1, 4]),
1195 Tolerance::absolute(1e-3),
1196 );
1197 }
1198
1199 #[test]
1200 fn test_layer_norm_no_beta() {
1201 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 4]);
1202 let gamma = flex_f32(vec![1.0; 4], &[4]);
1203 let out = crate::ops::activation::layer_norm(t, gamma, None, 1e-5);
1204
1205 out.into_data().assert_approx_eq::<f32>(
1206 &TensorData::new(
1207 vec![-1.3416408, -0.4472136, 0.4472136, 1.3416408],
1208 vec![1, 4],
1209 ),
1210 Tolerance::absolute(1e-4),
1211 );
1212 }
1213
1214 #[test]
1219 fn test_softmax_simd_body_row() {
1220 let data: Vec<f32> = (0..32).map(|i| i as f32 * 0.1).collect();
1223 let expected = softmax_last_ref(&data, 32);
1224 let fused = crate::ops::activation::softmax(flex_f32(data, &[1, 32]), 1);
1225 fused.into_data().assert_approx_eq::<f32>(
1226 &TensorData::new(expected, vec![1, 32]),
1227 Tolerance::absolute(1e-5),
1228 );
1229 }
1230
1231 #[test]
1232 fn test_softmax_multi_chunk_rayon() {
1233 let data: Vec<f32> = (0..100 * 16).map(|i| ((i % 17) as f32) * 0.05).collect();
1235 let expected = softmax_last_ref(&data, 16);
1236 let fused = crate::ops::activation::softmax(flex_f32(data, &[100, 16]), 1);
1237 fused.into_data().assert_approx_eq::<f32>(
1238 &TensorData::new(expected, vec![100, 16]),
1239 Tolerance::absolute(1e-5),
1240 );
1241 }
1242
1243 #[test]
1244 fn test_softmax_f64() {
1245 let data: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1247 let expected = softmax_last_ref(&data, 4);
1248 let fused = crate::ops::activation::softmax(flex_f64(data, &[2, 4]), 1);
1249 fused.into_data().assert_approx_eq::<f64>(
1250 &TensorData::new(expected, vec![2, 4]),
1251 Tolerance::absolute(1e-10),
1252 );
1253 }
1254
1255 #[test]
1256 fn test_softmax_f16() {
1257 let source: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 0.5, 0.5, 0.5, 0.5];
1259 let data: Vec<f16> = source.iter().map(|&x| f16::from_f32(x)).collect();
1260 let expected = softmax_last_ref(&data, 4);
1261 let fused = crate::ops::activation::softmax(flex_half(data, &[2, 4]), 1);
1262 fused.into_data().assert_approx_eq::<f16>(
1263 &TensorData::new(expected, vec![2, 4]),
1264 Tolerance::absolute(1e-2),
1265 );
1266 }
1267
1268 #[test]
1269 fn test_softmax_bf16() {
1270 let source: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 0.5, 0.5, 0.5, 0.5];
1272 let data: Vec<bf16> = source.iter().map(|&x| bf16::from_f32(x)).collect();
1273 let expected = softmax_last_ref(&data, 4);
1274 let fused = crate::ops::activation::softmax(flex_half(data, &[2, 4]), 1);
1275 fused.into_data().assert_approx_eq::<bf16>(
1276 &TensorData::new(expected, vec![2, 4]),
1277 Tolerance::absolute(5e-2),
1278 );
1279 }
1280
1281 #[test]
1282 fn test_softmax_multi_chunk_f64() {
1283 let n_rows = 150;
1285 let d_cols = 8;
1286 let data: Vec<f64> = (0..n_rows * d_cols)
1287 .map(|i| ((i % 11) as f64) * 0.1 - 0.5)
1288 .collect();
1289 let expected = softmax_last_ref(&data, d_cols);
1290 let fused = crate::ops::activation::softmax(flex_f64(data, &[n_rows, d_cols]), 1);
1291 fused.into_data().assert_approx_eq::<f64>(
1292 &TensorData::new(expected, vec![n_rows, d_cols]),
1293 Tolerance::absolute(1e-10),
1294 );
1295 }
1296
1297 #[test]
1298 fn test_softmax_multi_chunk_f16() {
1299 let n_rows = 150;
1301 let d_cols = 8;
1302 let source: Vec<f32> = (0..n_rows * d_cols)
1303 .map(|i| ((i % 11) as f32) * 0.1 - 0.5)
1304 .collect();
1305 let data: Vec<f16> = source.iter().map(|&x| f16::from_f32(x)).collect();
1306 let expected = softmax_last_ref(&data, d_cols);
1307 let fused = crate::ops::activation::softmax(flex_half(data, &[n_rows, d_cols]), 1);
1308 fused.into_data().assert_approx_eq::<f16>(
1309 &TensorData::new(expected, vec![n_rows, d_cols]),
1310 Tolerance::absolute(1e-2),
1311 );
1312 }
1313
1314 #[test]
1315 fn test_softmax_multi_chunk_bf16() {
1316 let n_rows = 150;
1318 let d_cols = 8;
1319 let source: Vec<f32> = (0..n_rows * d_cols)
1320 .map(|i| ((i % 11) as f32) * 0.1 - 0.5)
1321 .collect();
1322 let data: Vec<bf16> = source.iter().map(|&x| bf16::from_f32(x)).collect();
1323 let expected = softmax_last_ref(&data, d_cols);
1324 let fused = crate::ops::activation::softmax(flex_half(data, &[n_rows, d_cols]), 1);
1325 fused.into_data().assert_approx_eq::<bf16>(
1326 &TensorData::new(expected, vec![n_rows, d_cols]),
1327 Tolerance::absolute(5e-2),
1328 );
1329 }
1330
1331 #[test]
1332 fn test_layer_norm_multi_chunk_rayon() {
1333 let data: Vec<f32> = (0..128 * 16).map(|i| ((i % 19) as f32) * 0.03).collect();
1335 let gamma_data: Vec<f32> = vec![1.0; 16];
1336 let beta_data: Vec<f32> = vec![0.0; 16];
1337 let expected = layer_norm_last_ref(&data, &gamma_data, Some(&beta_data), 1e-5f32, 16);
1338 let fused = crate::ops::activation::layer_norm(
1339 flex_f32(data, &[128, 16]),
1340 flex_f32(gamma_data, &[16]),
1341 Some(flex_f32(beta_data, &[16])),
1342 1e-5,
1343 );
1344 fused.into_data().assert_approx_eq::<f32>(
1345 &TensorData::new(expected, vec![128, 16]),
1346 Tolerance::absolute(1e-4),
1347 );
1348 }
1349
1350 #[test]
1351 fn test_softmax_empty_last_dim_returns_input() {
1352 let t = flex_f32(Vec::<f32>::new(), &[2, 0]);
1355 let result = crate::ops::activation::softmax(t, 1);
1356 assert_eq!(result.shape().as_slice(), &[2, 0]);
1357 }
1358
1359 #[test]
1360 fn test_layer_norm_empty_last_dim_returns_input() {
1361 let t = flex_f32(Vec::<f32>::new(), &[3, 0]);
1362 let gamma = flex_f32(Vec::<f32>::new(), &[0]);
1363 let beta = flex_f32(Vec::<f32>::new(), &[0]);
1364 let result = crate::ops::activation::layer_norm(t, gamma, Some(beta), 1e-5);
1365 assert_eq!(result.shape().as_slice(), &[3, 0]);
1366 }
1367
1368 #[test]
1369 #[should_panic(expected = "gamma must be a 1-D tensor")]
1370 fn test_layer_norm_gamma_length_mismatch_panics() {
1371 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 4]);
1372 let gamma = flex_f32(vec![1.0, 1.0, 1.0], &[3]);
1373 let _ = crate::ops::activation::layer_norm(t, gamma, None, 1e-5);
1374 }
1375
1376 #[test]
1377 #[should_panic(expected = "beta must be a 1-D tensor")]
1378 fn test_layer_norm_beta_length_mismatch_panics() {
1379 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 4]);
1380 let gamma = flex_f32(vec![1.0, 1.0, 1.0, 1.0], &[4]);
1381 let beta = flex_f32(vec![0.0, 0.0, 0.0], &[3]);
1382 let _ = crate::ops::activation::layer_norm(t, gamma, Some(beta), 1e-5);
1383 }
1384
1385 #[test]
1386 #[should_panic(expected = "gamma must be a 1-D tensor")]
1387 fn test_layer_norm_gamma_rank_mismatch_panics() {
1388 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 4]);
1391 let gamma = flex_f32(vec![1.0; 8], &[2, 4]);
1392 let _ = crate::ops::activation::layer_norm(t, gamma, None, 1e-5);
1393 }
1394
1395 #[test]
1401 fn test_softmax_simd_body_plus_scalar_tail() {
1402 let data: Vec<f32> = (0..34).map(|i| (i as f32 * 0.137) - 2.3).collect();
1403 let expected = softmax_last_ref(&data, 17);
1404 let fused = crate::ops::activation::softmax(flex_f32(data, &[2, 17]), 1);
1405 fused.into_data().assert_approx_eq::<f32>(
1406 &TensorData::new(expected, vec![2, 17]),
1407 Tolerance::absolute(1e-5),
1408 );
1409 }
1410
1411 #[test]
1412 fn test_layer_norm_simd_body_plus_scalar_tail() {
1413 let data: Vec<f32> = (0..34).map(|i| (i as f32 * 0.137) - 2.3).collect();
1414 let gamma_data: Vec<f32> = (0..17).map(|i| 1.0 + i as f32 * 0.05).collect();
1415 let beta_data: Vec<f32> = (0..17).map(|i| i as f32 * 0.01).collect();
1416 let expected = layer_norm_last_ref(&data, &gamma_data, Some(&beta_data), 1e-5f32, 17);
1417 let fused = crate::ops::activation::layer_norm(
1418 flex_f32(data, &[2, 17]),
1419 flex_f32(gamma_data, &[17]),
1420 Some(flex_f32(beta_data, &[17])),
1421 1e-5,
1422 );
1423 fused.into_data().assert_approx_eq::<f32>(
1424 &TensorData::new(expected, vec![2, 17]),
1425 Tolerance::absolute(1e-5),
1426 );
1427 }
1428
1429 #[test]
1430 fn test_layer_norm_f64_with_beta_multi_chunk() {
1431 let d_model = 16;
1433 let n_rows = 80;
1434 let data: Vec<f64> = (0..n_rows * d_model)
1435 .map(|i| ((i % 13) as f64) * 0.07 - 0.3)
1436 .collect();
1437 let gamma_data: Vec<f64> = vec![0.9; d_model];
1438 let beta_data: Vec<f64> = vec![0.05; d_model];
1439 let eps = 1e-5f64;
1440 let expected = layer_norm_last_ref(&data, &gamma_data, Some(&beta_data), eps, d_model);
1441 let fused = crate::ops::activation::layer_norm(
1442 flex_f64(data, &[n_rows, d_model]),
1443 flex_f64(gamma_data, &[d_model]),
1444 Some(flex_f64(beta_data, &[d_model])),
1445 eps,
1446 );
1447 fused.into_data().assert_approx_eq::<f64>(
1448 &TensorData::new(expected, vec![n_rows, d_model]),
1449 Tolerance::absolute(1e-10),
1450 );
1451 }
1452
1453 #[test]
1454 fn test_layer_norm_f64_no_beta() {
1455 let data: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, -1.0, 0.5, 1.5, -0.5];
1456 let gamma_data: Vec<f64> = vec![1.0; 4];
1457 let eps = 1e-5f64;
1458 let expected = layer_norm_last_ref(&data, &gamma_data, None, eps, 4);
1459 let fused = crate::ops::activation::layer_norm(
1460 flex_f64(data, &[2, 4]),
1461 flex_f64(gamma_data, &[4]),
1462 None,
1463 eps,
1464 );
1465 fused.into_data().assert_approx_eq::<f64>(
1466 &TensorData::new(expected, vec![2, 4]),
1467 Tolerance::absolute(1e-10),
1468 );
1469 }
1470
1471 fn check_layer_norm_half_precision<E>(from_f32: fn(f32) -> E, dtype: DType)
1475 where
1476 E: burn_backend::Element + Float,
1477 {
1478 let rows_f32: [f32; 12] = [
1479 1.0, 2.0, 3.0, 4.0, -1.0, 0.0, 1.0, 2.0, 0.5, -0.5, 1.5, -1.5,
1480 ];
1481 let gamma_f32: [f32; 4] = [1.0, 0.5, 1.5, 1.0];
1482 let beta_f32: [f32; 4] = [0.1, -0.1, 0.0, 0.2];
1483 let eps = 1e-5f32;
1484
1485 let expected_f32 = layer_norm_last_ref(&rows_f32, &gamma_f32, Some(&beta_f32), eps, 4);
1486
1487 let data: Vec<E> = rows_f32.iter().map(|&x| from_f32(x)).collect();
1488 let gamma_data: Vec<E> = gamma_f32.iter().map(|&x| from_f32(x)).collect();
1489 let beta_data: Vec<E> = beta_f32.iter().map(|&x| from_f32(x)).collect();
1490 assert_eq!(E::dtype(), dtype);
1491
1492 let fused = crate::ops::activation::layer_norm(
1493 flex_half(data, &[3, 4]),
1494 flex_half(gamma_data, &[4]),
1495 Some(flex_half(beta_data, &[4])),
1496 eps as f64,
1497 );
1498 fused.into_data().assert_approx_eq::<f32>(
1499 &TensorData::new(expected_f32, vec![3, 4]),
1500 Tolerance::absolute(3e-2),
1501 );
1502 }
1503
1504 #[test]
1505 fn test_layer_norm_f16_via_f32_cast() {
1506 check_layer_norm_half_precision::<f16>(f16::from_f32, DType::F16);
1507 }
1508
1509 #[test]
1510 fn test_layer_norm_bf16_via_f32_cast() {
1511 check_layer_norm_half_precision::<bf16>(bf16::from_f32, DType::BF16);
1512 }
1513
1514 #[test]
1515 #[should_panic(expected = "softmax dim")]
1516 fn test_softmax_dim_out_of_range_panics() {
1517 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
1518 let _ = crate::ops::activation::softmax(t, 2);
1519 }
1520
1521 #[test]
1522 #[should_panic(expected = "gamma dtype")]
1523 fn test_layer_norm_gamma_dtype_mismatch_panics() {
1524 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 4]);
1527 let gamma = flex_f64(vec![1.0; 4], &[4]);
1528 let _ = crate::ops::activation::layer_norm(t, gamma, None, 1e-5);
1529 }
1530}