Skip to main content

needle_core/v3/
kernels.rs

1//! Needle 3 kernels that differ from v2.
2//!
3//! The important one is [`hadamard_mlp`]. Despite the name it is **not** a
4//! Hadamard transform in v3: the three transform stages are Kronecker products
5//! of learned 32x32 factors, initialised to Walsh matrices but trained away
6//! from them (measured drift on the shipped weights is 1.45 to 3.98 against
7//! entries of +/-1). So no fast Walsh transform applies, and the honest kernel
8//! is a pair of small dense matmuls per stage.
9//!
10//! That is still the cheap option. At `n = 1024` a dense map would be about
11//! 1.05M MACs per stage; the Kronecker factorisation costs
12//! `2 * ba * bb * ba = 65,536`, a 16x saving, and 32x32 blocks stay in L1 and
13//! vectorise cleanly — which is what makes v3 viable on constrained targets.
14
15extern crate alloc;
16use alloc::vec;
17use alloc::vec::Vec;
18
19use crate::math::exp;
20use crate::ops::silu;
21
22/// Kronecker block sizes for a transform width of `n`.
23///
24/// Mirrors upstream `_hada_blocks`: `ba = 1 << ((n - 1).bit_length() / 2)`,
25/// `bb = n / ba`. At `n = 1024` both are 32.
26pub fn hada_blocks(n: usize) -> (usize, usize) {
27    debug_assert!(
28        n.is_power_of_two(),
29        "transform width must be a power of two"
30    );
31    let bits = usize::BITS - (n - 1).leading_zeros();
32    let ba = 1usize << (bits / 2);
33    (ba, n / ba)
34}
35
36/// Apply `out = aᵀ · Z · b`, where `z` is `Z` flattened row-major as
37/// `(ba, bb)`.
38///
39/// This is upstream's `_kron_apply`, whose einsum is
40/// `"...ij,ik,jl->...kl"` — that is, `out[k, l] = Σ_ij z[i, j] a[i, k] b[j, l]`.
41pub fn kron_apply(z: &[f32], a: &[f32], b: &[f32], ba: usize, bb: usize, out: &mut [f32]) {
42    debug_assert_eq!(z.len(), ba * bb);
43    debug_assert_eq!(a.len(), ba * ba);
44    debug_assert_eq!(b.len(), bb * bb);
45    debug_assert_eq!(out.len(), ba * bb);
46
47    // Stage one: t[k, j] = Σ_i z[i, j] · a[i, k]. Walking `i` in the outer
48    // loop keeps `z`'s row and `a`'s row contiguous.
49    let mut t = vec![0.0f32; ba * bb];
50    for i in 0..ba {
51        let zi = &z[i * bb..(i + 1) * bb];
52        let ai = &a[i * ba..(i + 1) * ba];
53        for (k, &aik) in ai.iter().enumerate() {
54            if aik == 0.0 {
55                continue;
56            }
57            let tk = &mut t[k * bb..(k + 1) * bb];
58            for (tkj, &zij) in tk.iter_mut().zip(zi) {
59                *tkj += zij * aik;
60            }
61        }
62    }
63
64    // Stage two: out[k, l] = Σ_j t[k, j] · b[j, l].
65    for k in 0..ba {
66        let tk = &t[k * bb..(k + 1) * bb];
67        let ok = &mut out[k * bb..(k + 1) * bb];
68        ok.fill(0.0);
69        for (j, &tkj) in tk.iter().enumerate() {
70            if tkj == 0.0 {
71                continue;
72            }
73            let bj = &b[j * bb..(j + 1) * bb];
74            for (okl, &bjl) in ok.iter_mut().zip(bj) {
75                *okl += tkj * bjl;
76            }
77        }
78    }
79}
80
81/// The learned factors and diagonals of one layer's HadamardMLP.
82#[derive(Debug, Clone)]
83pub struct HadaMlp {
84    /// Diagonals, `hada_n` long each.
85    pub d1: Vec<f32>,
86    pub d2: Vec<f32>,
87    pub b2: Vec<f32>,
88    pub d3: Vec<f32>,
89    pub d4: Vec<f32>,
90    /// Kronecker factor pairs for the three stages, `(ba*ba, bb*bb)` each.
91    pub w1: (Vec<f32>, Vec<f32>),
92    pub w2: (Vec<f32>, Vec<f32>),
93    pub w3: (Vec<f32>, Vec<f32>),
94    /// Low-rank conditioning: `[d_model, rank]` then `[rank, hada_n]`.
95    pub cond_v: Vec<f32>,
96    pub cond_u: Vec<f32>,
97    pub cond_rank: usize,
98}
99
100/// Permutations applied after stages one and two. Stored in the container as
101/// FP32 rather than recomputed, because upstream derives them from a seeded
102/// RNG that a runtime has no way to reproduce.
103#[derive(Debug, Clone)]
104pub struct HadaPerms {
105    pub p1: Vec<u32>,
106    pub p2: Vec<u32>,
107}
108
109/// One position through HadamardMLP.
110///
111/// ```text
112/// cond = 1 + softmax(x · cond_v) · cond_u
113/// z    = pad(x, n)
114/// z    = kron(d1 ⊙ z,                 w1)[p1]
115/// z    = kron(silu(d2 ⊙ cond ⊙ z + b2), w2)[p2]
116/// z    = kron(d3 ⊙ z,                 w3)
117/// out  = (d4 ⊙ z)[..d_model]
118/// ```
119pub fn hadamard_mlp(x: &[f32], w: &HadaMlp, perms: &HadaPerms, hada_n: usize, out: &mut [f32]) {
120    let d_model = x.len();
121    debug_assert_eq!(out.len(), d_model);
122    let (ba, bb) = hada_blocks(hada_n);
123
124    // cond = 1 + softmax(x · cond_v) · cond_u
125    let r = w.cond_rank;
126    let mut proj = vec![0.0f32; r];
127    for (j, p) in proj.iter_mut().enumerate() {
128        let mut acc = 0.0f32;
129        for (i, &xi) in x.iter().enumerate() {
130            acc += xi * w.cond_v[i * r + j];
131        }
132        *p = acc;
133    }
134    let max = proj.iter().copied().fold(f32::NEG_INFINITY, f32::max);
135    let mut sum = 0.0f32;
136    for p in proj.iter_mut() {
137        *p = exp(*p - max);
138        sum += *p;
139    }
140    for p in proj.iter_mut() {
141        *p /= sum;
142    }
143    let mut cond = vec![1.0f32; hada_n];
144    for (j, &pj) in proj.iter().enumerate() {
145        if pj == 0.0 {
146            continue;
147        }
148        let row = &w.cond_u[j * hada_n..(j + 1) * hada_n];
149        for (c, &u) in cond.iter_mut().zip(row) {
150            *c += pj * u;
151        }
152    }
153
154    // z = pad(x, n), then the three stages.
155    let mut z = vec![0.0f32; hada_n];
156    z[..d_model].copy_from_slice(x);
157
158    let mut buf = vec![0.0f32; hada_n];
159
160    for (zi, &d) in z.iter_mut().zip(&w.d1) {
161        *zi *= d;
162    }
163    kron_apply(&z, &w.w1.0, &w.w1.1, ba, bb, &mut buf);
164    permute(&buf, &perms.p1, &mut z);
165
166    for i in 0..hada_n {
167        z[i] = silu(w.d2[i] * cond[i] * z[i] + w.b2[i]);
168    }
169    kron_apply(&z, &w.w2.0, &w.w2.1, ba, bb, &mut buf);
170    permute(&buf, &perms.p2, &mut z);
171
172    for (zi, &d) in z.iter_mut().zip(&w.d3) {
173        *zi *= d;
174    }
175    kron_apply(&z, &w.w3.0, &w.w3.1, ba, bb, &mut buf);
176
177    for (o, (&b, &d)) in out.iter_mut().zip(buf.iter().zip(&w.d4)) {
178        *o = b * d;
179    }
180}
181
182/// `out[i] = src[perm[i]]`, upstream's `z[..., p]` gather.
183fn permute(src: &[f32], perm: &[u32], out: &mut [f32]) {
184    debug_assert_eq!(src.len(), perm.len());
185    for (o, &p) in out.iter_mut().zip(perm) {
186        *o = src[p as usize];
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn block_sizes_match_upstream() {
196        assert_eq!(hada_blocks(1024), (32, 32));
197        assert_eq!(hada_blocks(512), (16, 32));
198        assert_eq!(hada_blocks(256), (16, 16));
199    }
200
201    #[test]
202    fn kron_matches_a_transpose_z_b() {
203        // 2x2 blocks, worked by hand: out = aᵀ · Z · b.
204        let (ba, bb) = (2usize, 2usize);
205        let z = [1.0f32, 2.0, 3.0, 4.0]; // Z = [[1,2],[3,4]]
206        let a = [1.0f32, 2.0, 3.0, 4.0]; // A = [[1,2],[3,4]]
207        let b = [0.0f32, 1.0, 1.0, 0.0]; // B swaps columns
208        let mut out = [0.0f32; 4];
209        kron_apply(&z, &a, &b, ba, bb, &mut out);
210        // AᵀZ = [[1,3],[2,4]]ᵀ… computed explicitly:
211        // (AᵀZ)[k][j] = Σ_i Z[i][j] A[i][k]
212        //   k=0: [1*1+3*3, 2*1+4*3] = [10, 14]
213        //   k=1: [1*2+3*4, 2*2+4*4] = [14, 20]
214        // then · B swaps each row: [14,10] and [20,14].
215        assert_eq!(out, [14.0, 10.0, 20.0, 14.0]);
216    }
217
218    #[test]
219    fn permute_gathers() {
220        let src = [10.0f32, 20.0, 30.0];
221        let perm = [2u32, 0, 1];
222        let mut out = [0.0f32; 3];
223        permute(&src, &perm, &mut out);
224        assert_eq!(out, [30.0, 10.0, 20.0]);
225    }
226}