use kopitiam_core::{DType, Error, Result};
use crate::storage::Storage;
use super::Tensor;
impl Tensor {
pub fn rms_norm(&self, weight: &Tensor, eps: f32) -> Result<Tensor> {
self.require_dtype(DType::F32)?;
let hidden = self.last_dim()?;
let w = self.matching_weight(weight, hidden)?;
let Storage::F32(data) = self.storage.as_ref() else { unreachable!() };
let contiguous: Vec<f32> = self.logical_offsets().map(|i| data[i]).collect();
let mut out = vec![0f32; contiguous.len()];
for (row_out, row_in) in out.chunks_mut(hidden).zip(contiguous.chunks(hidden)) {
let mean_square = row_in.iter().map(|v| v * v).sum::<f32>() / hidden as f32;
let inv_rms = 1.0 / (mean_square + eps).sqrt();
for (o, (&x, &w)) in row_out.iter_mut().zip(row_in.iter().zip(&w)) {
*o = x * inv_rms * w;
}
}
Tensor::from_f32(out, self.shape.clone())
}
pub fn layer_norm(&self, weight: &Tensor, bias: &Tensor, eps: f32) -> Result<Tensor> {
self.require_dtype(DType::F32)?;
let hidden = self.last_dim()?;
let w = self.matching_weight(weight, hidden)?;
let b = self.matching_weight(bias, hidden)?;
let Storage::F32(data) = self.storage.as_ref() else { unreachable!() };
let contiguous: Vec<f32> = self.logical_offsets().map(|i| data[i]).collect();
let mut out = vec![0f32; contiguous.len()];
for (row_out, row_in) in out.chunks_mut(hidden).zip(contiguous.chunks(hidden)) {
let mean = row_in.iter().sum::<f32>() / hidden as f32;
let variance = row_in.iter().map(|v| (v - mean) * (v - mean)).sum::<f32>() / hidden as f32;
let inv_std = 1.0 / (variance + eps).sqrt();
for (i, o) in row_out.iter_mut().enumerate() {
*o = (row_in[i] - mean) * inv_std * w[i] + b[i];
}
}
Tensor::from_f32(out, self.shape.clone())
}
fn last_dim(&self) -> Result<usize> {
if self.rank() == 0 {
return Err(Error::ShapeMismatch { expected: self.shape.clone(), actual: self.shape.clone() });
}
Ok(self.shape.dims()[self.rank() - 1])
}
fn matching_weight(&self, weight: &Tensor, hidden: usize) -> Result<Vec<f32>> {
weight.require_dtype(DType::F32)?;
if weight.elem_count() != hidden {
return Err(Error::ShapeMismatch { expected: self.shape.clone(), actual: weight.shape.clone() });
}
weight.to_vec_f32()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rms_norm_matches_hand_computation() {
let x = Tensor::from_f32(vec![3.0, 4.0], [1, 2]).unwrap();
let w = Tensor::from_f32(vec![1.0, 1.0], [2]).unwrap();
let out = x.rms_norm(&w, 0.0).unwrap().to_vec_f32().unwrap();
let expected_scale = 1.0 / 12.5f32.sqrt();
assert!((out[0] - 3.0 * expected_scale).abs() < 1e-6);
assert!((out[1] - 4.0 * expected_scale).abs() < 1e-6);
}
#[test]
fn rms_norm_applies_the_per_channel_weight() {
let x = Tensor::from_f32(vec![1.0, 1.0], [1, 2]).unwrap();
let w = Tensor::from_f32(vec![2.0, 3.0], [2]).unwrap();
let out = x.rms_norm(&w, 0.0).unwrap().to_vec_f32().unwrap();
assert!((out[0] - 2.0).abs() < 1e-6);
assert!((out[1] - 3.0).abs() < 1e-6);
}
#[test]
fn layer_norm_matches_hand_computation() {
let x = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0], [1, 4]).unwrap();
let w = Tensor::from_f32(vec![1.0; 4], [4]).unwrap();
let b = Tensor::from_f32(vec![0.0; 4], [4]).unwrap();
let out = x.layer_norm(&w, &b, 0.0).unwrap().to_vec_f32().unwrap();
let inv_std = 1.0 / 1.25f32.sqrt();
let expected = [
(1.0 - 2.5) * inv_std,
(2.0 - 2.5) * inv_std,
(3.0 - 2.5) * inv_std,
(4.0 - 2.5) * inv_std,
];
for (o, e) in out.iter().zip(expected) {
assert!((o - e).abs() < 1e-5, "got {o}, expected {e}");
}
assert!(out.iter().sum::<f32>().abs() < 1e-5);
}
#[test]
fn layer_norm_applies_weight_and_bias_after_normalizing() {
let x = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0], [1, 4]).unwrap();
let unit_w = Tensor::from_f32(vec![1.0; 4], [4]).unwrap();
let zero_b = Tensor::from_f32(vec![0.0; 4], [4]).unwrap();
let normalized = x.layer_norm(&unit_w, &zero_b, 0.0).unwrap().to_vec_f32().unwrap();
let w = Tensor::from_f32(vec![2.0; 4], [4]).unwrap();
let b = Tensor::from_f32(vec![10.0; 4], [4]).unwrap();
let out = x.layer_norm(&w, &b, 0.0).unwrap().to_vec_f32().unwrap();
for (o, n) in out.iter().zip(&normalized) {
assert!((o - (n * 2.0 + 10.0)).abs() < 1e-5, "got {o}, expected {}", n * 2.0 + 10.0);
}
}
#[test]
fn norm_ops_reject_a_mismatched_weight_length() {
let x = Tensor::from_f32(vec![1.0, 2.0], [1, 2]).unwrap();
let w = Tensor::from_f32(vec![1.0, 1.0, 1.0], [3]).unwrap();
assert!(matches!(x.rms_norm(&w, 1e-5), Err(Error::ShapeMismatch { .. })));
}
}