use crate::error::{Result, RustyGradientsError};
use ndarray::{ArrayD, Axis, IxDyn};
pub fn layer_norm_fused(
x: &ArrayD<f32>,
gamma: &ArrayD<f32>,
beta: &ArrayD<f32>,
epsilon: f32,
) -> Result<ArrayD<f32>> {
let _last_axis = Axis(x.ndim() - 1);
let normalized_dim = x.shape()[x.ndim() - 1];
#[allow(unused_mut, unused_assignments)]
let mut output = ArrayD::zeros(x.raw_dim());
let mut param_shape = x.shape().to_vec();
param_shape[x.ndim() - 1] = 1;
let num_slices = x.len() / normalized_dim;
#[cfg(feature = "cpu")]
{
use rayon::prelude::*;
if x.as_slice().is_none() {
return layer_norm_naive_fallback(x, gamma, beta, epsilon);
}
let x_slice = x.as_slice().unwrap();
let input_slices: Vec<_> = (0..num_slices)
.map(|i| {
let start = i * normalized_dim;
let end = start + normalized_dim;
&x_slice[start..end]
})
.collect();
let output_vecs: Vec<Vec<f32>> = input_slices
.par_iter()
.enumerate()
.map(|(_slice_idx, &slice)| {
let mut mean = 0.0f32;
let mut m2 = 0.0f32;
let n = slice.len() as f32;
for (i, &value) in slice.iter().enumerate() {
let delta = value - mean;
mean += delta / (i + 1) as f32;
let delta2 = value - mean;
m2 += delta * delta2;
}
let variance = m2 / n;
let std_inv = 1.0 / (variance + epsilon).sqrt();
let gamma_slice = gamma.as_slice().unwrap();
let beta_slice = beta.as_slice().unwrap();
slice
.iter()
.enumerate()
.map(|(i, &x_val)| {
let normalized = (x_val - mean) * std_inv;
gamma_slice[i] * normalized + beta_slice[i]
})
.collect()
})
.collect();
let mut output_data = Vec::with_capacity(x.len());
for vec in output_vecs {
output_data.extend(vec);
}
output = ArrayD::from_shape_vec(x.raw_dim(), output_data)
.map_err(|e| RustyGradientsError::ShapeError(e.to_string()))?;
}
#[cfg(not(feature = "cpu"))]
{
for slice_idx in 0..num_slices {
let start = slice_idx * normalized_dim;
let end = start + normalized_dim;
let slice = &x.as_slice().unwrap()[start..end];
let mut mean = 0.0f32;
let mut m2 = 0.0f32;
let n = slice.len() as f32;
for (i, &value) in slice.iter().enumerate() {
let delta = value - mean;
mean += delta / (i + 1) as f32;
let delta2 = value - mean;
m2 += delta * delta2;
}
let variance = m2 / n;
let std_inv = 1.0 / (variance + epsilon).sqrt();
let gamma_slice = gamma.as_slice().unwrap();
let beta_slice = beta.as_slice().unwrap();
let output_slice = &mut output.as_slice_mut().unwrap()[start..end];
for (i, &x_val) in slice.iter().enumerate() {
let normalized = (x_val - mean) * std_inv;
output_slice[i] = gamma_slice[i] * normalized + beta_slice[i];
}
}
}
Ok(output)
}
pub fn gelu_fused(x: &ArrayD<f32>) -> Result<ArrayD<f32>> {
const SQRT_2_OVER_PI: f32 = 0.7978845608; const COEFF: f32 = 0.044715;
#[cfg(feature = "cpu")]
{
use rayon::prelude::*;
if let Some(slice) = x.as_slice() {
let data: Vec<f32> = slice
.par_iter()
.map(|&val| {
let inner = SQRT_2_OVER_PI * (val + COEFF * val.powi(3));
0.5 * val * (1.0 + inner.tanh())
})
.collect();
return Ok(ArrayD::from_shape_vec(x.raw_dim(), data).unwrap());
}
}
Ok(x.mapv(|val| {
let inner = SQRT_2_OVER_PI * (val + COEFF * val.powi(3));
0.5 * val * (1.0 + inner.tanh())
}))
}
fn layer_norm_naive_fallback(
x: &ArrayD<f32>,
gamma: &ArrayD<f32>,
beta: &ArrayD<f32>,
epsilon: f32,
) -> Result<ArrayD<f32>> {
let last_axis = Axis(x.ndim() - 1);
let mean = x
.mean_axis(last_axis)
.ok_or_else(|| RustyGradientsError::ShapeError("Failed to compute mean".to_string()))?;
let mut mean_shape = x.shape().to_vec();
mean_shape[x.ndim() - 1] = 1;
let mean_reshaped = mean
.into_shape_with_order(IxDyn(&mean_shape))
.map_err(|e| RustyGradientsError::ShapeError(e.to_string()))?;
let x_minus_mean = x - &mean_reshaped;
let variance = x_minus_mean
.mapv(|v| v.powi(2))
.mean_axis(last_axis)
.ok_or_else(|| RustyGradientsError::ShapeError("Failed to compute variance".to_string()))?;
let variance_reshaped = variance
.into_shape_with_order(IxDyn(&mean_shape))
.map_err(|e| RustyGradientsError::ShapeError(e.to_string()))?;
let std_dev_inv = (&variance_reshaped + epsilon).mapv(|v| 1.0 / v.sqrt());
let x_normalized = &x_minus_mean * &std_dev_inv;
let gamma_reshaped = gamma
.clone()
.into_shape_with_order(IxDyn(&mean_shape))
.map_err(|e| RustyGradientsError::ShapeError(e.to_string()))?;
let beta_reshaped = beta
.clone()
.into_shape_with_order(IxDyn(&mean_shape))
.map_err(|e| RustyGradientsError::ShapeError(e.to_string()))?;
Ok(&x_normalized * &gamma_reshaped + &beta_reshaped)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_layer_norm_fused() {
let x = ArrayD::from_shape_vec(vec![1, 4], vec![1.0, 2.0, 3.0, 4.0]).unwrap();
let gamma = ArrayD::from_shape_vec(vec![4], vec![1.0, 1.0, 1.0, 1.0]).unwrap();
let beta = ArrayD::from_shape_vec(vec![4], vec![0.0, 0.0, 0.0, 0.0]).unwrap();
let result = layer_norm_fused(&x, &gamma, &beta, 1e-5).unwrap();
let mean = result.mean().unwrap();
assert!((mean - 0.0).abs() < 1e-3, "Mean should be ~0 after normalization");
let variance = result.mapv(|v| v.powi(2)).mean().unwrap();
assert!(
(variance - 1.0).abs() < 0.1,
"Variance should be ~1 after normalization"
);
}
#[test]
fn test_gelu_fused() {
let x = ArrayD::from_shape_vec(vec![3], vec![-1.0, 0.0, 1.0]).unwrap();
let result = gelu_fused(&x).unwrap();
assert!((result[[1]] - 0.0).abs() < 0.01);
assert!(result[[2]] > 0.8 && result[[2]] < 0.9);
}
}