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"))]
884#[inline]
885fn layer_norm_row_f32_scalar(
886 input: &[f32],
887 output: &mut [f32],
888 gamma: &[f32],
889 beta: Option<&[f32]>,
890 epsilon: f32,
891) {
892 let len = input.len();
896 let mut mean = 0.0f32;
897 let mut m2 = 0.0f32;
898 for (k, &x) in input.iter().enumerate() {
899 let n_k = (k + 1) as f32;
900 let delta = x - mean;
901 mean += delta / n_k;
902 let delta2 = x - mean;
903 m2 += delta * delta2;
904 }
905 let var = m2 / len as f32;
906 let inv_std = 1.0f32 / (var + epsilon).sqrt();
907 for (i, &x) in input.iter().enumerate() {
908 let scale = inv_std * gamma[i];
909 let normed = (x - mean) * scale;
910 output[i] = match beta {
911 Some(b) => normed + b[i],
912 None => normed,
913 };
914 }
915}
916
917#[cfg(feature = "simd")]
920#[macerator::with_simd]
921fn layer_norm_rows_f32_with_beta_simd<S: macerator::Simd>(
922 input: &[f32],
923 output: &mut [f32],
924 gamma: &[f32],
925 beta: &[f32],
926 d_model: usize,
927 epsilon: f32,
928) {
929 debug_assert_eq!(input.len(), output.len());
930 debug_assert_eq!(input.len() % d_model, 0);
931 debug_assert_eq!(gamma.len(), d_model);
932 debug_assert_eq!(beta.len(), d_model);
933 for (in_row, out_row) in input.chunks(d_model).zip(output.chunks_mut(d_model)) {
934 layer_norm_row_f32_simd::<S>(in_row, out_row, gamma, Some(beta), epsilon);
935 }
936}
937
938#[cfg(feature = "simd")]
940#[macerator::with_simd]
941fn layer_norm_rows_f32_no_beta_simd<S: macerator::Simd>(
942 input: &[f32],
943 output: &mut [f32],
944 gamma: &[f32],
945 d_model: usize,
946 epsilon: f32,
947) {
948 debug_assert_eq!(input.len(), output.len());
949 debug_assert_eq!(input.len() % d_model, 0);
950 debug_assert_eq!(gamma.len(), d_model);
951 for (in_row, out_row) in input.chunks(d_model).zip(output.chunks_mut(d_model)) {
952 layer_norm_row_f32_simd::<S>(in_row, out_row, gamma, None, epsilon);
953 }
954}
955
956#[cfg(feature = "simd")]
959#[inline(always)]
960fn layer_norm_row_f32_simd<S: macerator::Simd>(
961 input: &[f32],
962 output: &mut [f32],
963 gamma: &[f32],
964 beta: Option<&[f32]>,
965 epsilon: f32,
966) {
967 use macerator::{Scalar, vload_unaligned, vstore_unaligned};
968 let lanes = <f32 as Scalar>::lanes::<S>();
969 let len = input.len();
970 let simd_len = len / lanes * lanes;
971 let n = len as f32;
972
973 let mut acc = 0.0f32.splat::<S>();
975 let mut i = 0;
976 while i < simd_len {
977 unsafe {
978 acc += vload_unaligned::<S, _>(input.as_ptr().add(i));
979 }
980 i += lanes;
981 }
982 let mut sum = acc.reduce_add();
983 for &x in &input[simd_len..] {
984 sum += x;
985 }
986 let mean = sum / n;
987 let mean_vec = mean.splat::<S>();
988
989 let mut acc = 0.0f32.splat::<S>();
992 let mut i = 0;
993 while i < simd_len {
994 unsafe {
995 let d = vload_unaligned::<S, _>(input.as_ptr().add(i)) - mean_vec;
996 acc = d.mul_add(d, acc);
998 }
999 i += lanes;
1000 }
1001 let mut sumsq = acc.reduce_add();
1002 for &x in &input[simd_len..] {
1003 let d = x - mean;
1004 sumsq += d * d;
1005 }
1006 let var = sumsq / n;
1007 let inv_std = 1.0f32 / (var + epsilon).sqrt();
1008
1009 let inv_std_vec = inv_std.splat::<S>();
1015 let mut i = 0;
1016 while i < simd_len {
1017 unsafe {
1018 let x = vload_unaligned::<S, _>(input.as_ptr().add(i));
1019 let g = vload_unaligned::<S, _>(gamma.as_ptr().add(i));
1020 let scale = inv_std_vec * g;
1022 let centered = x - mean_vec;
1024 let normed = centered * scale;
1026 let out = if let Some(b) = beta {
1027 let b_vec = vload_unaligned::<S, _>(b.as_ptr().add(i));
1028 normed + b_vec
1029 } else {
1030 normed
1031 };
1032 vstore_unaligned::<S, _>(output.as_mut_ptr().add(i), out);
1033 }
1034 i += lanes;
1035 }
1036 while i < len {
1038 let centered = input[i] - mean;
1039 let normed = centered * inv_std * gamma[i];
1040 output[i] = match beta {
1041 Some(b) => normed + b[i],
1042 None => normed,
1043 };
1044 i += 1;
1045 }
1046}
1047
1048#[cfg(test)]
1056mod tests {
1057 use alloc::vec;
1058 use burn_backend::{DType, TensorData, TensorMetadata, Tolerance};
1059 use burn_std::{bf16, f16};
1060 use num_traits::Float;
1061
1062 use crate::FlexTensor;
1063
1064 fn softmax_row<T: Float>(row_in: &[T], row_out: &mut [T]) {
1074 let max = row_in
1075 .iter()
1076 .copied()
1077 .fold(T::neg_infinity(), |a, b| if a > b { a } else { b });
1078 let mut sum = T::zero();
1079 for (i, &x) in row_in.iter().enumerate() {
1080 let e = (x - max).exp();
1081 row_out[i] = e;
1082 sum = sum + e;
1083 }
1084 for v in row_out.iter_mut() {
1085 *v = *v / sum;
1086 }
1087 }
1088
1089 fn softmax_last_ref<T: Float>(data: &[T], row_len: usize) -> Vec<T> {
1090 let mut out = vec![T::zero(); data.len()];
1091 for (i, o) in data.chunks(row_len).zip(out.chunks_mut(row_len)) {
1092 softmax_row(i, o);
1093 }
1094 out
1095 }
1096
1097 fn layer_norm_row<T: Float>(
1098 row_in: &[T],
1099 gamma: &[T],
1100 beta: Option<&[T]>,
1101 eps: T,
1102 row_out: &mut [T],
1103 ) {
1104 let n = T::from(row_in.len()).unwrap();
1105 let mean = row_in.iter().copied().fold(T::zero(), |a, b| a + b) / n;
1106 let var = row_in
1107 .iter()
1108 .map(|&x| (x - mean) * (x - mean))
1109 .fold(T::zero(), |a, b| a + b)
1110 / n;
1111 let inv_std = T::one() / (var + eps).sqrt();
1112 for (i, &x) in row_in.iter().enumerate() {
1113 let normed = (x - mean) * inv_std;
1114 let scaled = normed * gamma[i];
1115 row_out[i] = match beta {
1116 Some(b) => scaled + b[i],
1117 None => scaled,
1118 };
1119 }
1120 }
1121
1122 fn layer_norm_last_ref<T: Float>(
1123 data: &[T],
1124 gamma: &[T],
1125 beta: Option<&[T]>,
1126 eps: T,
1127 row_len: usize,
1128 ) -> Vec<T> {
1129 let mut out = vec![T::zero(); data.len()];
1130 for (i, o) in data.chunks(row_len).zip(out.chunks_mut(row_len)) {
1131 layer_norm_row(i, gamma, beta, eps, o);
1132 }
1133 out
1134 }
1135
1136 fn flex_f32(data: Vec<f32>, shape: &[usize]) -> FlexTensor {
1141 FlexTensor::from_data(TensorData::new(data, shape.to_vec()))
1142 }
1143
1144 fn flex_f64(data: Vec<f64>, shape: &[usize]) -> FlexTensor {
1145 FlexTensor::from_data(TensorData::new(data, shape.to_vec()))
1146 }
1147
1148 fn flex_half<T: burn_backend::Element>(data: Vec<T>, shape: &[usize]) -> FlexTensor {
1149 FlexTensor::from_data(TensorData::new(data, shape.to_vec()))
1150 }
1151
1152 #[test]
1157 fn test_layer_norm_2d_with_beta() {
1158 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], &[2, 4]);
1159 let gamma = flex_f32(vec![1.0; 4], &[4]);
1160 let beta = flex_f32(vec![0.0; 4], &[4]);
1161 let out = crate::ops::activation::layer_norm(t, gamma, Some(beta), 1e-5);
1162
1163 let expected: Vec<f32> = vec![
1164 -1.3416408, -0.4472136, 0.4472136, 1.3416408, -1.3416408, -0.4472136, 0.4472136,
1165 1.3416408,
1166 ];
1167 out.into_data().assert_approx_eq::<f32>(
1168 &TensorData::new(expected, vec![2, 4]),
1169 Tolerance::absolute(1e-4),
1170 );
1171 }
1172
1173 #[test]
1174 fn test_layer_norm_with_affine() {
1175 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 4]);
1176 let gamma = flex_f32(vec![2.0, 0.5, 1.0, 3.0], &[4]);
1177 let beta = flex_f32(vec![1.0, -1.0, 0.0, 2.0], &[4]);
1178 let out = crate::ops::activation::layer_norm(t, gamma, Some(beta), 1e-5);
1179
1180 out.into_data().assert_approx_eq::<f32>(
1183 &TensorData::new(vec![-1.6833, -1.2236, 0.4472, 6.0249], vec![1, 4]),
1184 Tolerance::absolute(1e-3),
1185 );
1186 }
1187
1188 #[test]
1189 fn test_layer_norm_no_beta() {
1190 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 4]);
1191 let gamma = flex_f32(vec![1.0; 4], &[4]);
1192 let out = crate::ops::activation::layer_norm(t, gamma, None, 1e-5);
1193
1194 out.into_data().assert_approx_eq::<f32>(
1195 &TensorData::new(
1196 vec![-1.3416408, -0.4472136, 0.4472136, 1.3416408],
1197 vec![1, 4],
1198 ),
1199 Tolerance::absolute(1e-4),
1200 );
1201 }
1202
1203 #[test]
1208 fn test_softmax_simd_body_row() {
1209 let data: Vec<f32> = (0..32).map(|i| i as f32 * 0.1).collect();
1212 let expected = softmax_last_ref(&data, 32);
1213 let fused = crate::ops::activation::softmax(flex_f32(data, &[1, 32]), 1);
1214 fused.into_data().assert_approx_eq::<f32>(
1215 &TensorData::new(expected, vec![1, 32]),
1216 Tolerance::absolute(1e-5),
1217 );
1218 }
1219
1220 #[test]
1221 fn test_softmax_multi_chunk_rayon() {
1222 let data: Vec<f32> = (0..100 * 16).map(|i| ((i % 17) as f32) * 0.05).collect();
1224 let expected = softmax_last_ref(&data, 16);
1225 let fused = crate::ops::activation::softmax(flex_f32(data, &[100, 16]), 1);
1226 fused.into_data().assert_approx_eq::<f32>(
1227 &TensorData::new(expected, vec![100, 16]),
1228 Tolerance::absolute(1e-5),
1229 );
1230 }
1231
1232 #[test]
1233 fn test_softmax_f64() {
1234 let data: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1236 let expected = softmax_last_ref(&data, 4);
1237 let fused = crate::ops::activation::softmax(flex_f64(data, &[2, 4]), 1);
1238 fused.into_data().assert_approx_eq::<f64>(
1239 &TensorData::new(expected, vec![2, 4]),
1240 Tolerance::absolute(1e-10),
1241 );
1242 }
1243
1244 #[test]
1245 fn test_softmax_f16() {
1246 let source: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 0.5, 0.5, 0.5, 0.5];
1248 let data: Vec<f16> = source.iter().map(|&x| f16::from_f32(x)).collect();
1249 let expected = softmax_last_ref(&data, 4);
1250 let fused = crate::ops::activation::softmax(flex_half(data, &[2, 4]), 1);
1251 fused.into_data().assert_approx_eq::<f16>(
1252 &TensorData::new(expected, vec![2, 4]),
1253 Tolerance::absolute(1e-2),
1254 );
1255 }
1256
1257 #[test]
1258 fn test_softmax_bf16() {
1259 let source: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 0.5, 0.5, 0.5, 0.5];
1261 let data: Vec<bf16> = source.iter().map(|&x| bf16::from_f32(x)).collect();
1262 let expected = softmax_last_ref(&data, 4);
1263 let fused = crate::ops::activation::softmax(flex_half(data, &[2, 4]), 1);
1264 fused.into_data().assert_approx_eq::<bf16>(
1265 &TensorData::new(expected, vec![2, 4]),
1266 Tolerance::absolute(5e-2),
1267 );
1268 }
1269
1270 #[test]
1271 fn test_softmax_multi_chunk_f64() {
1272 let n_rows = 150;
1274 let d_cols = 8;
1275 let data: Vec<f64> = (0..n_rows * d_cols)
1276 .map(|i| ((i % 11) as f64) * 0.1 - 0.5)
1277 .collect();
1278 let expected = softmax_last_ref(&data, d_cols);
1279 let fused = crate::ops::activation::softmax(flex_f64(data, &[n_rows, d_cols]), 1);
1280 fused.into_data().assert_approx_eq::<f64>(
1281 &TensorData::new(expected, vec![n_rows, d_cols]),
1282 Tolerance::absolute(1e-10),
1283 );
1284 }
1285
1286 #[test]
1287 fn test_softmax_multi_chunk_f16() {
1288 let n_rows = 150;
1290 let d_cols = 8;
1291 let source: Vec<f32> = (0..n_rows * d_cols)
1292 .map(|i| ((i % 11) as f32) * 0.1 - 0.5)
1293 .collect();
1294 let data: Vec<f16> = source.iter().map(|&x| f16::from_f32(x)).collect();
1295 let expected = softmax_last_ref(&data, d_cols);
1296 let fused = crate::ops::activation::softmax(flex_half(data, &[n_rows, d_cols]), 1);
1297 fused.into_data().assert_approx_eq::<f16>(
1298 &TensorData::new(expected, vec![n_rows, d_cols]),
1299 Tolerance::absolute(1e-2),
1300 );
1301 }
1302
1303 #[test]
1304 fn test_softmax_multi_chunk_bf16() {
1305 let n_rows = 150;
1307 let d_cols = 8;
1308 let source: Vec<f32> = (0..n_rows * d_cols)
1309 .map(|i| ((i % 11) as f32) * 0.1 - 0.5)
1310 .collect();
1311 let data: Vec<bf16> = source.iter().map(|&x| bf16::from_f32(x)).collect();
1312 let expected = softmax_last_ref(&data, d_cols);
1313 let fused = crate::ops::activation::softmax(flex_half(data, &[n_rows, d_cols]), 1);
1314 fused.into_data().assert_approx_eq::<bf16>(
1315 &TensorData::new(expected, vec![n_rows, d_cols]),
1316 Tolerance::absolute(5e-2),
1317 );
1318 }
1319
1320 #[test]
1321 fn test_layer_norm_large_mean_small_spread() {
1322 let data: Vec<f32> = (0..17).map(|i| 100.0 + i as f32 * 1e-3).collect();
1327 let data_f64: Vec<f64> = data.iter().map(|&x| x as f64).collect();
1328 let expected: Vec<f32> = layer_norm_last_ref(&data_f64, &[1.0; 17], None, 1e-5, 17)
1329 .into_iter()
1330 .map(|x| x as f32)
1331 .collect();
1332 let out = crate::ops::activation::layer_norm(
1333 flex_f32(data, &[1, 17]),
1334 flex_f32(vec![1.0; 17], &[17]),
1335 None,
1336 1e-5,
1337 );
1338 out.into_data().assert_approx_eq::<f32>(
1339 &TensorData::new(expected, vec![1, 17]),
1340 Tolerance::absolute(1e-2),
1341 );
1342 }
1343
1344 #[test]
1345 fn test_layer_norm_multi_chunk_rayon() {
1346 let data: Vec<f32> = (0..128 * 16).map(|i| ((i % 19) as f32) * 0.03).collect();
1348 let gamma_data: Vec<f32> = vec![1.0; 16];
1349 let beta_data: Vec<f32> = vec![0.0; 16];
1350 let expected = layer_norm_last_ref(&data, &gamma_data, Some(&beta_data), 1e-5f32, 16);
1351 let fused = crate::ops::activation::layer_norm(
1352 flex_f32(data, &[128, 16]),
1353 flex_f32(gamma_data, &[16]),
1354 Some(flex_f32(beta_data, &[16])),
1355 1e-5,
1356 );
1357 fused.into_data().assert_approx_eq::<f32>(
1358 &TensorData::new(expected, vec![128, 16]),
1359 Tolerance::absolute(1e-4),
1360 );
1361 }
1362
1363 #[test]
1364 fn test_softmax_empty_last_dim_returns_input() {
1365 let t = flex_f32(Vec::<f32>::new(), &[2, 0]);
1368 let result = crate::ops::activation::softmax(t, 1);
1369 assert_eq!(result.shape().as_slice(), &[2, 0]);
1370 }
1371
1372 #[test]
1373 fn test_layer_norm_empty_last_dim_returns_input() {
1374 let t = flex_f32(Vec::<f32>::new(), &[3, 0]);
1375 let gamma = flex_f32(Vec::<f32>::new(), &[0]);
1376 let beta = flex_f32(Vec::<f32>::new(), &[0]);
1377 let result = crate::ops::activation::layer_norm(t, gamma, Some(beta), 1e-5);
1378 assert_eq!(result.shape().as_slice(), &[3, 0]);
1379 }
1380
1381 #[test]
1382 #[should_panic(expected = "gamma must be a 1-D tensor")]
1383 fn test_layer_norm_gamma_length_mismatch_panics() {
1384 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 4]);
1385 let gamma = flex_f32(vec![1.0, 1.0, 1.0], &[3]);
1386 let _ = crate::ops::activation::layer_norm(t, gamma, None, 1e-5);
1387 }
1388
1389 #[test]
1390 #[should_panic(expected = "beta must be a 1-D tensor")]
1391 fn test_layer_norm_beta_length_mismatch_panics() {
1392 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 4]);
1393 let gamma = flex_f32(vec![1.0, 1.0, 1.0, 1.0], &[4]);
1394 let beta = flex_f32(vec![0.0, 0.0, 0.0], &[3]);
1395 let _ = crate::ops::activation::layer_norm(t, gamma, Some(beta), 1e-5);
1396 }
1397
1398 #[test]
1399 #[should_panic(expected = "gamma must be a 1-D tensor")]
1400 fn test_layer_norm_gamma_rank_mismatch_panics() {
1401 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 4]);
1404 let gamma = flex_f32(vec![1.0; 8], &[2, 4]);
1405 let _ = crate::ops::activation::layer_norm(t, gamma, None, 1e-5);
1406 }
1407
1408 #[test]
1414 fn test_softmax_simd_body_plus_scalar_tail() {
1415 let data: Vec<f32> = (0..34).map(|i| (i as f32 * 0.137) - 2.3).collect();
1416 let expected = softmax_last_ref(&data, 17);
1417 let fused = crate::ops::activation::softmax(flex_f32(data, &[2, 17]), 1);
1418 fused.into_data().assert_approx_eq::<f32>(
1419 &TensorData::new(expected, vec![2, 17]),
1420 Tolerance::absolute(1e-5),
1421 );
1422 }
1423
1424 #[test]
1425 fn test_layer_norm_simd_body_plus_scalar_tail() {
1426 let data: Vec<f32> = (0..34).map(|i| (i as f32 * 0.137) - 2.3).collect();
1427 let gamma_data: Vec<f32> = (0..17).map(|i| 1.0 + i as f32 * 0.05).collect();
1428 let beta_data: Vec<f32> = (0..17).map(|i| i as f32 * 0.01).collect();
1429 let expected = layer_norm_last_ref(&data, &gamma_data, Some(&beta_data), 1e-5f32, 17);
1430 let fused = crate::ops::activation::layer_norm(
1431 flex_f32(data, &[2, 17]),
1432 flex_f32(gamma_data, &[17]),
1433 Some(flex_f32(beta_data, &[17])),
1434 1e-5,
1435 );
1436 fused.into_data().assert_approx_eq::<f32>(
1437 &TensorData::new(expected, vec![2, 17]),
1438 Tolerance::absolute(1e-5),
1439 );
1440 }
1441
1442 #[test]
1443 fn test_layer_norm_f64_with_beta_multi_chunk() {
1444 let d_model = 16;
1446 let n_rows = 80;
1447 let data: Vec<f64> = (0..n_rows * d_model)
1448 .map(|i| ((i % 13) as f64) * 0.07 - 0.3)
1449 .collect();
1450 let gamma_data: Vec<f64> = vec![0.9; d_model];
1451 let beta_data: Vec<f64> = vec![0.05; d_model];
1452 let eps = 1e-5f64;
1453 let expected = layer_norm_last_ref(&data, &gamma_data, Some(&beta_data), eps, d_model);
1454 let fused = crate::ops::activation::layer_norm(
1455 flex_f64(data, &[n_rows, d_model]),
1456 flex_f64(gamma_data, &[d_model]),
1457 Some(flex_f64(beta_data, &[d_model])),
1458 eps,
1459 );
1460 fused.into_data().assert_approx_eq::<f64>(
1461 &TensorData::new(expected, vec![n_rows, d_model]),
1462 Tolerance::absolute(1e-10),
1463 );
1464 }
1465
1466 #[test]
1467 fn test_layer_norm_f64_no_beta() {
1468 let data: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, -1.0, 0.5, 1.5, -0.5];
1469 let gamma_data: Vec<f64> = vec![1.0; 4];
1470 let eps = 1e-5f64;
1471 let expected = layer_norm_last_ref(&data, &gamma_data, None, eps, 4);
1472 let fused = crate::ops::activation::layer_norm(
1473 flex_f64(data, &[2, 4]),
1474 flex_f64(gamma_data, &[4]),
1475 None,
1476 eps,
1477 );
1478 fused.into_data().assert_approx_eq::<f64>(
1479 &TensorData::new(expected, vec![2, 4]),
1480 Tolerance::absolute(1e-10),
1481 );
1482 }
1483
1484 fn check_layer_norm_half_precision<E>(from_f32: fn(f32) -> E, dtype: DType)
1488 where
1489 E: burn_backend::Element + Float,
1490 {
1491 let rows_f32: [f32; 12] = [
1492 1.0, 2.0, 3.0, 4.0, -1.0, 0.0, 1.0, 2.0, 0.5, -0.5, 1.5, -1.5,
1493 ];
1494 let gamma_f32: [f32; 4] = [1.0, 0.5, 1.5, 1.0];
1495 let beta_f32: [f32; 4] = [0.1, -0.1, 0.0, 0.2];
1496 let eps = 1e-5f32;
1497
1498 let expected_f32 = layer_norm_last_ref(&rows_f32, &gamma_f32, Some(&beta_f32), eps, 4);
1499
1500 let data: Vec<E> = rows_f32.iter().map(|&x| from_f32(x)).collect();
1501 let gamma_data: Vec<E> = gamma_f32.iter().map(|&x| from_f32(x)).collect();
1502 let beta_data: Vec<E> = beta_f32.iter().map(|&x| from_f32(x)).collect();
1503 assert_eq!(E::dtype(), dtype);
1504
1505 let fused = crate::ops::activation::layer_norm(
1506 flex_half(data, &[3, 4]),
1507 flex_half(gamma_data, &[4]),
1508 Some(flex_half(beta_data, &[4])),
1509 eps as f64,
1510 );
1511 fused.into_data().assert_approx_eq::<f32>(
1512 &TensorData::new(expected_f32, vec![3, 4]),
1513 Tolerance::absolute(3e-2),
1514 );
1515 }
1516
1517 #[test]
1518 fn test_layer_norm_f16_via_f32_cast() {
1519 check_layer_norm_half_precision::<f16>(f16::from_f32, DType::F16);
1520 }
1521
1522 #[test]
1523 fn test_layer_norm_bf16_via_f32_cast() {
1524 check_layer_norm_half_precision::<bf16>(bf16::from_f32, DType::BF16);
1525 }
1526
1527 #[test]
1528 #[should_panic(expected = "softmax dim")]
1529 fn test_softmax_dim_out_of_range_panics() {
1530 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
1531 let _ = crate::ops::activation::softmax(t, 2);
1532 }
1533
1534 #[test]
1535 #[should_panic(expected = "gamma dtype")]
1536 fn test_layer_norm_gamma_dtype_mismatch_panics() {
1537 let t = flex_f32(vec![1.0, 2.0, 3.0, 4.0], &[1, 4]);
1540 let gamma = flex_f64(vec![1.0; 4], &[4]);
1541 let _ = crate::ops::activation::layer_norm(t, gamma, None, 1e-5);
1542 }
1543}