kopitiam_tensor/tensor/
norm.rs1use kopitiam_core::{DType, Error, Result};
10
11use crate::storage::Storage;
12
13use super::Tensor;
14
15impl Tensor {
16 pub fn rms_norm(&self, weight: &Tensor, eps: f32) -> Result<Tensor> {
23 self.require_dtype(DType::F32)?;
24 let hidden = self.last_dim()?;
25 let w = self.matching_weight(weight, hidden)?;
26
27 let Storage::F32(data) = self.storage.as_ref() else { unreachable!() };
28 let contiguous: Vec<f32> = self.logical_offsets().map(|i| data[i]).collect();
29
30 let mut out = vec![0f32; contiguous.len()];
31 for (row_out, row_in) in out.chunks_mut(hidden).zip(contiguous.chunks(hidden)) {
32 let mean_square = row_in.iter().map(|v| v * v).sum::<f32>() / hidden as f32;
33 let inv_rms = 1.0 / (mean_square + eps).sqrt();
34 for (o, (&x, &w)) in row_out.iter_mut().zip(row_in.iter().zip(&w)) {
35 *o = x * inv_rms * w;
36 }
37 }
38 Tensor::from_f32(out, self.shape.clone())
39 }
40
41 pub fn layer_norm(&self, weight: &Tensor, bias: &Tensor, eps: f32) -> Result<Tensor> {
46 self.require_dtype(DType::F32)?;
47 let hidden = self.last_dim()?;
48 let w = self.matching_weight(weight, hidden)?;
49 let b = self.matching_weight(bias, hidden)?;
50
51 let Storage::F32(data) = self.storage.as_ref() else { unreachable!() };
52 let contiguous: Vec<f32> = self.logical_offsets().map(|i| data[i]).collect();
53
54 let mut out = vec![0f32; contiguous.len()];
55 for (row_out, row_in) in out.chunks_mut(hidden).zip(contiguous.chunks(hidden)) {
56 let mean = row_in.iter().sum::<f32>() / hidden as f32;
57 let variance = row_in.iter().map(|v| (v - mean) * (v - mean)).sum::<f32>() / hidden as f32;
58 let inv_std = 1.0 / (variance + eps).sqrt();
59 for (i, o) in row_out.iter_mut().enumerate() {
60 *o = (row_in[i] - mean) * inv_std * w[i] + b[i];
61 }
62 }
63 Tensor::from_f32(out, self.shape.clone())
64 }
65
66 fn last_dim(&self) -> Result<usize> {
67 if self.rank() == 0 {
68 return Err(Error::ShapeMismatch { expected: self.shape.clone(), actual: self.shape.clone() });
69 }
70 Ok(self.shape.dims()[self.rank() - 1])
71 }
72
73 fn matching_weight(&self, weight: &Tensor, hidden: usize) -> Result<Vec<f32>> {
77 weight.require_dtype(DType::F32)?;
78 if weight.elem_count() != hidden {
79 return Err(Error::ShapeMismatch { expected: self.shape.clone(), actual: weight.shape.clone() });
80 }
81 weight.to_vec_f32()
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn rms_norm_matches_hand_computation() {
91 let x = Tensor::from_f32(vec![3.0, 4.0], [1, 2]).unwrap();
94 let w = Tensor::from_f32(vec![1.0, 1.0], [2]).unwrap();
95 let out = x.rms_norm(&w, 0.0).unwrap().to_vec_f32().unwrap();
96 let expected_scale = 1.0 / 12.5f32.sqrt();
97 assert!((out[0] - 3.0 * expected_scale).abs() < 1e-6);
98 assert!((out[1] - 4.0 * expected_scale).abs() < 1e-6);
99 }
100
101 #[test]
102 fn rms_norm_applies_the_per_channel_weight() {
103 let x = Tensor::from_f32(vec![1.0, 1.0], [1, 2]).unwrap();
104 let w = Tensor::from_f32(vec![2.0, 3.0], [2]).unwrap();
105 let out = x.rms_norm(&w, 0.0).unwrap().to_vec_f32().unwrap();
107 assert!((out[0] - 2.0).abs() < 1e-6);
108 assert!((out[1] - 3.0).abs() < 1e-6);
109 }
110
111 #[test]
112 fn layer_norm_matches_hand_computation() {
113 let x = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0], [1, 4]).unwrap();
115 let w = Tensor::from_f32(vec![1.0; 4], [4]).unwrap();
116 let b = Tensor::from_f32(vec![0.0; 4], [4]).unwrap();
117 let out = x.layer_norm(&w, &b, 0.0).unwrap().to_vec_f32().unwrap();
118 let inv_std = 1.0 / 1.25f32.sqrt();
119 let expected = [
120 (1.0 - 2.5) * inv_std,
121 (2.0 - 2.5) * inv_std,
122 (3.0 - 2.5) * inv_std,
123 (4.0 - 2.5) * inv_std,
124 ];
125 for (o, e) in out.iter().zip(expected) {
126 assert!((o - e).abs() < 1e-5, "got {o}, expected {e}");
127 }
128 assert!(out.iter().sum::<f32>().abs() < 1e-5);
130 }
131
132 #[test]
133 fn layer_norm_applies_weight_and_bias_after_normalizing() {
134 let x = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0], [1, 4]).unwrap();
135 let unit_w = Tensor::from_f32(vec![1.0; 4], [4]).unwrap();
136 let zero_b = Tensor::from_f32(vec![0.0; 4], [4]).unwrap();
137 let normalized = x.layer_norm(&unit_w, &zero_b, 0.0).unwrap().to_vec_f32().unwrap();
138
139 let w = Tensor::from_f32(vec![2.0; 4], [4]).unwrap();
140 let b = Tensor::from_f32(vec![10.0; 4], [4]).unwrap();
141 let out = x.layer_norm(&w, &b, 0.0).unwrap().to_vec_f32().unwrap();
142
143 for (o, n) in out.iter().zip(&normalized) {
144 assert!((o - (n * 2.0 + 10.0)).abs() < 1e-5, "got {o}, expected {}", n * 2.0 + 10.0);
145 }
146 }
147
148 #[test]
149 fn norm_ops_reject_a_mismatched_weight_length() {
150 let x = Tensor::from_f32(vec![1.0, 2.0], [1, 2]).unwrap();
151 let w = Tensor::from_f32(vec![1.0, 1.0, 1.0], [3]).unwrap();
152 assert!(matches!(x.rms_norm(&w, 1e-5), Err(Error::ShapeMismatch { .. })));
153 }
154}