Skip to main content

kime_cpu/
ops.rs

1//! The elementwise and per row ops of the compat graph, in FP32.
2//!
3//! Statistics and transcendental functions are taken in f64 and rounded once, so each result is
4//! within an ulp or so of the exact value. PyTorch computes them in f32 with its own vectorized
5//! approximations, and the difference to it is at that level either way.
6
7/// LayerNorm over rows of `d`: `(x - mean) / sqrt(var + eps) * w + b`, with the biased variance
8/// PyTorch uses.
9///
10/// # Panics
11///
12/// If a length does not match.
13pub fn layer_norm(x: &[f32], d: usize, w: &[f32], b: Option<&[f32]>, eps: f64, out: &mut [f32]) {
14    assert_eq!(x.len(), out.len());
15    assert_eq!(w.len(), d);
16    if d == 0 {
17        return;
18    }
19    for (row, o) in x.chunks_exact(d).zip(out.chunks_exact_mut(d)) {
20        let mean = row.iter().map(|&v| f64::from(v)).sum::<f64>() / d as f64;
21        let var = row.iter().map(|&v| (f64::from(v) - mean).powi(2)).sum::<f64>() / d as f64;
22        let rstd = 1.0 / (var + eps).sqrt();
23        for i in 0..d {
24            let n = ((f64::from(row[i]) - mean) * rstd) as f32;
25            o[i] = match b {
26                Some(b) => n * w[i] + b[i],
27                None => n * w[i],
28            };
29        }
30    }
31}
32
33/// The exact GELU, `x/2 (1 + erf(x/√2))`, which is what `nn.GELU()` and ModernBERT's `"gelu"`
34/// compute.
35#[inline]
36#[must_use]
37pub fn gelu(x: f32) -> f32 {
38    let x = f64::from(x);
39    (0.5 * x * (1.0 + libm::erf(x * std::f64::consts::FRAC_1_SQRT_2))) as f32
40}
41
42/// ModernBERT's gated MLP input: `u` is rows of `2i`, the first half goes through GELU and is
43/// multiplied by the second half.
44///
45/// # Panics
46///
47/// If a length does not match.
48pub fn geglu(u: &[f32], inter: usize, out: &mut [f32]) {
49    assert_eq!(u.len(), 2 * out.len());
50    if inter == 0 {
51        return;
52    }
53    for (row, o) in u.chunks_exact(2 * inter).zip(out.chunks_exact_mut(inter)) {
54        let (a, g) = row.split_at(inter);
55        for i in 0..inter {
56            o[i] = gelu(a[i]) * g[i];
57        }
58    }
59}
60
61/// `x += y`.
62///
63/// # Panics
64///
65/// If the lengths differ.
66pub fn add(x: &mut [f32], y: &[f32]) {
67    assert_eq!(x.len(), y.len());
68    x.iter_mut().zip(y).for_each(|(a, b)| *a += b);
69}
70
71/// Rotary position tables for one base, as Hugging Face builds them for the default rope type.
72#[derive(Debug, Clone)]
73pub struct Rope {
74    half: usize,
75    cos: Vec<f32>,
76    sin: Vec<f32>,
77}
78
79impl Rope {
80    /// Tables for positions `0..len` over heads of `dim` (even).
81    ///
82    /// # Panics
83    ///
84    /// If `dim` is odd.
85    #[must_use]
86    pub fn new(theta: f64, dim: usize, len: usize) -> Self {
87        assert!(dim.is_multiple_of(2));
88        let half = dim / 2;
89        // inv_freq = 1 / theta ** (arange(0, dim, 2) / dim) in f32, then pos * inv_freq in f32.
90        let inv: Vec<f32> = (0..half)
91            .map(|i| {
92                let e = (2 * i) as f32 / dim as f32;
93                1.0 / (theta.powf(f64::from(e)) as f32)
94            })
95            .collect();
96        let mut cos = Vec::with_capacity(len * half);
97        let mut sin = Vec::with_capacity(len * half);
98        for p in 0..len {
99            for &f in &inv {
100                let a = f64::from(p as f32 * f);
101                cos.push(a.cos() as f32);
102                sin.push(a.sin() as f32);
103            }
104        }
105        Self { half, cos, sin }
106    }
107
108    /// Positions the tables cover.
109    #[must_use]
110    pub fn len(&self) -> usize {
111        self.cos.len() / self.half.max(1)
112    }
113
114    /// True when the tables cover no positions.
115    #[must_use]
116    pub fn is_empty(&self) -> bool {
117        self.cos.is_empty()
118    }
119
120    /// Rotates one head vector at position `pos` in place: `x cos + rotate_half(x) sin`.
121    ///
122    /// # Panics
123    ///
124    /// If `pos` is past the tables or `x` is not one head long.
125    #[inline]
126    pub fn apply(&self, x: &mut [f32], pos: usize) {
127        let h = self.half;
128        assert_eq!(x.len(), 2 * h);
129        let c = &self.cos[pos * h..(pos + 1) * h];
130        let s = &self.sin[pos * h..(pos + 1) * h];
131        for i in 0..h {
132            let (a, b) = (x[i], x[i + h]);
133            // Two products then a sum, not a fused multiply add, the rounding torch does.
134            x[i] = a * c[i] + -b * s[i];
135            x[i + h] = b * c[i] + a * s[i];
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::testing::{Rng, close};
144
145    #[test]
146    fn layer_norm_against_the_formula() {
147        let mut rng = Rng(3);
148        for (rows, d) in [(0, 4), (1, 1), (3, 7), (2, 1024)] {
149            let x = rng.vec(rows * d);
150            let w = rng.vec(d);
151            let b = rng.vec(d);
152            let mut out = vec![0f32; rows * d];
153            layer_norm(&x, d, &w, Some(&b), 1e-5, &mut out);
154            let mut want = vec![0f32; rows * d];
155            for r in 0..rows {
156                let row = &x[r * d..(r + 1) * d];
157                let mean: f32 = row.iter().sum::<f32>() / d as f32;
158                let var: f32 = row.iter().map(|v| (v - mean) * (v - mean)).sum::<f32>() / d as f32;
159                for i in 0..d {
160                    want[r * d + i] = (row[i] - mean) / (var + 1e-5).sqrt() * w[i] + b[i];
161                }
162            }
163            close(&out, &want, 1e-4, "layer_norm");
164        }
165    }
166
167    #[test]
168    fn gelu_values() {
169        // x/2 (1 + erf(x/√2)) from Python's math.erf.
170        let cases = [
171            (0.0, 0.0),
172            (1.0, 0.841_344_746_068_542_9),
173            (-1.0, -0.158_655_253_931_457_07),
174            (3.0, 2.995_950_305_905_11),
175            (-6.0, -5.919_525_869_479_969e-9),
176        ];
177        for (x, want) in cases {
178            let got = gelu(x as f32);
179            assert!((f64::from(got) - want).abs() <= want.abs() * 1e-6 + 1e-12, "{x}: {got}");
180        }
181        let mut out = [0f32; 2];
182        geglu(&[1.0, -1.0, 2.0, 3.0], 2, &mut out);
183        close(&out, &[gelu(1.0) * 2.0, gelu(-1.0) * 3.0], 0.0, "geglu");
184    }
185
186    #[test]
187    fn rope_rotates() {
188        let r = Rope::new(10000.0, 4, 3);
189        assert_eq!(r.len(), 3);
190        let mut x = [1.0, 2.0, 3.0, 4.0];
191        r.apply(&mut x, 0);
192        close(&x, &[1.0, 2.0, 3.0, 4.0], 0.0, "position 0");
193        // Position 1: pair 0 turns by 1 radian and pair 1 by 1/100.
194        let mut x = [1.0, 2.0, 3.0, 4.0];
195        r.apply(&mut x, 1);
196        let (c0, s0, c1, s1) = (1f32.cos(), 1f32.sin(), 0.01f32.cos(), 0.01f32.sin());
197        close(
198            &x,
199            &[c0 - 3.0 * s0, 2.0 * c1 - 4.0 * s1, 3.0 * c0 + s0, 4.0 * c1 + 2.0 * s1],
200            1e-6,
201            "rope",
202        );
203    }
204}