1#[derive(Clone, Copy, Debug)]
20pub struct MlaDims {
21 pub n_head: usize,
22 pub d_nope: usize,
24 pub d_rope: usize,
26 pub d_v: usize,
28 pub kv_rank: usize,
30}
31
32impl MlaDims {
33 pub const GLM52: MlaDims =
34 MlaDims { n_head: 64, d_nope: 192, d_rope: 64, d_v: 256, kv_rank: 512 };
35
36 pub fn scale(&self) -> f32 {
39 1.0 / ((self.d_nope + self.d_rope) as f32).sqrt()
40 }
41}
42
43pub struct MlaInputs<'a> {
58 pub q_nope: &'a [f32],
59 pub q_pe: &'a [f32],
60 pub c_kv: &'a [f32],
61 pub k_pe: &'a [f32],
62 pub w_uk: &'a [f32],
63 pub w_uv: &'a [f32],
64 pub t_q: usize,
65 pub t_kv: usize,
66}
67
68fn check_shapes(d: &MlaDims, x: &MlaInputs) {
69 assert_eq!(x.q_nope.len(), x.t_q * d.n_head * d.d_nope, "q_nope shape");
70 assert_eq!(x.q_pe.len(), x.t_q * d.n_head * d.d_rope, "q_pe shape");
71 assert_eq!(x.c_kv.len(), x.t_kv * d.kv_rank, "c_kv shape");
72 assert_eq!(x.k_pe.len(), x.t_kv * d.d_rope, "k_pe shape");
73 assert_eq!(x.w_uk.len(), d.n_head * d.d_nope * d.kv_rank, "w_uk shape");
74 assert_eq!(x.w_uv.len(), d.n_head * d.d_v * d.kv_rank, "w_uv shape");
75 assert!(x.t_q <= x.t_kv, "queries must be a suffix of the cache");
76}
77
78fn softmax(s: &mut [f32]) {
80 let m = s.iter().copied().fold(f32::NEG_INFINITY, f32::max);
81 let mut sum = 0.0f32;
82 for v in s.iter_mut() {
83 *v = (*v - m).exp();
84 sum += *v;
85 }
86 let inv = 1.0 / sum;
87 for v in s.iter_mut() {
88 *v *= inv;
89 }
90}
91
92pub fn mla_attend_naive(d: &MlaDims, x: &MlaInputs) -> Vec<f32> {
96 check_shapes(d, x);
97 let (nh, dn, dr, dv, r) = (d.n_head, d.d_nope, d.d_rope, d.d_v, d.kv_rank);
98 let scale = d.scale();
99 let mut out = vec![0.0f32; x.t_q * nh * dv];
100
101 let mut k_nope = vec![0.0f32; x.t_kv * dn];
103 let mut v = vec![0.0f32; x.t_kv * dv];
104 let mut scores = vec![0.0f32; x.t_kv];
105 for h in 0..nh {
106 let wuk = &x.w_uk[h * dn * r..(h + 1) * dn * r];
107 let wuv = &x.w_uv[h * dv * r..(h + 1) * dv * r];
108 for t in 0..x.t_kv {
109 let c = &x.c_kv[t * r..(t + 1) * r];
110 for p in 0..dn {
111 let row = &wuk[p * r..(p + 1) * r];
112 let mut acc = 0.0f32;
113 for l in 0..r {
114 acc += row[l] * c[l];
115 }
116 k_nope[t * dn + p] = acc;
117 }
118 for j in 0..dv {
119 let row = &wuv[j * r..(j + 1) * r];
120 let mut acc = 0.0f32;
121 for l in 0..r {
122 acc += row[l] * c[l];
123 }
124 v[t * dv + j] = acc;
125 }
126 }
127 for i in 0..x.t_q {
128 let visible = x.t_kv - x.t_q + i + 1; let qn = &x.q_nope[(i * nh + h) * dn..(i * nh + h + 1) * dn];
130 let qp = &x.q_pe[(i * nh + h) * dr..(i * nh + h + 1) * dr];
131 for t in 0..visible {
132 let mut s = 0.0f32;
133 let kn = &k_nope[t * dn..(t + 1) * dn];
134 for p in 0..dn {
135 s += qn[p] * kn[p];
136 }
137 let kp = &x.k_pe[t * dr..(t + 1) * dr];
138 for p in 0..dr {
139 s += qp[p] * kp[p];
140 }
141 scores[t] = s * scale;
142 }
143 softmax(&mut scores[..visible]);
144 let o = &mut out[(i * nh + h) * dv..(i * nh + h + 1) * dv];
145 for t in 0..visible {
146 let p = scores[t];
147 let vt = &v[t * dv..(t + 1) * dv];
148 for j in 0..dv {
149 o[j] += p * vt[j];
150 }
151 }
152 }
153 }
154 out
155}
156
157pub fn mla_attend_absorbed(d: &MlaDims, x: &MlaInputs) -> Vec<f32> {
162 check_shapes(d, x);
163 let (nh, dn, dr, dv, r) = (d.n_head, d.d_nope, d.d_rope, d.d_v, d.kv_rank);
164 let scale = d.scale();
165 let mut out = vec![0.0f32; x.t_q * nh * dv];
166
167 let mut q_lat = vec![0.0f32; r]; let mut o_lat = vec![0.0f32; r]; let mut scores = vec![0.0f32; x.t_kv];
170 for h in 0..nh {
171 let wuk = &x.w_uk[h * dn * r..(h + 1) * dn * r];
172 let wuv = &x.w_uv[h * dv * r..(h + 1) * dv * r];
173 for i in 0..x.t_q {
174 let visible = x.t_kv - x.t_q + i + 1;
175 let qn = &x.q_nope[(i * nh + h) * dn..(i * nh + h + 1) * dn];
176 let qp = &x.q_pe[(i * nh + h) * dr..(i * nh + h + 1) * dr];
177 q_lat.iter_mut().for_each(|v| *v = 0.0);
179 for p in 0..dn {
180 let row = &wuk[p * r..(p + 1) * r];
181 let qv = qn[p];
182 for l in 0..r {
183 q_lat[l] += qv * row[l];
184 }
185 }
186 for t in 0..visible {
188 let c = &x.c_kv[t * r..(t + 1) * r];
189 let mut s = 0.0f32;
190 for l in 0..r {
191 s += q_lat[l] * c[l];
192 }
193 let kp = &x.k_pe[t * dr..(t + 1) * dr];
194 for p in 0..dr {
195 s += qp[p] * kp[p];
196 }
197 scores[t] = s * scale;
198 }
199 softmax(&mut scores[..visible]);
200 o_lat.iter_mut().for_each(|v| *v = 0.0);
202 for t in 0..visible {
203 let p = scores[t];
204 let c = &x.c_kv[t * r..(t + 1) * r];
205 for l in 0..r {
206 o_lat[l] += p * c[l];
207 }
208 }
209 let o = &mut out[(i * nh + h) * dv..(i * nh + h + 1) * dv];
211 for j in 0..dv {
212 let row = &wuv[j * r..(j + 1) * r];
213 let mut acc = 0.0f32;
214 for l in 0..r {
215 acc += row[l] * o_lat[l];
216 }
217 o[j] = acc;
218 }
219 }
220 }
221 out
222}
223
224pub fn rope_interleaved(x: &mut [f32], n_dims: usize, pos: f32, base: f32) {
234 let half = n_dims / 2;
235 let theta_scale = base.powf(-2.0 / n_dims as f32);
236 let mut theta = pos;
237 for j in 0..half {
238 let (sin, cos) = theta.sin_cos();
239 let a = x[2 * j];
240 let b = x[2 * j + 1];
241 x[2 * j] = a * cos - b * sin;
242 x[2 * j + 1] = a * sin + b * cos;
243 theta *= theta_scale;
244 }
245}
246
247pub fn rope_neox(x: &mut [f32], n_dims: usize, pos: f32, base: f32) {
250 let half = n_dims / 2;
251 let theta_scale = base.powf(-2.0 / n_dims as f32);
252 let mut theta = pos;
253 for j in 0..half {
254 let (sin, cos) = theta.sin_cos();
255 let a = x[j];
256 let b = x[j + half];
257 x[j] = a * cos - b * sin;
258 x[j + half] = a * sin + b * cos;
259 theta *= theta_scale;
260 }
261}
262
263pub fn norm_to_neox_perm(n_dims: usize) -> Vec<usize> {
268 let half = n_dims / 2;
269 let mut p = vec![0usize; n_dims];
270 for j in 0..half {
271 p[2 * j] = j;
272 p[2 * j + 1] = j + half;
273 }
274 p
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 struct Rng(u64);
283 impl Rng {
284 fn next_f32(&mut self) -> f32 {
285 self.0 ^= self.0 << 13;
286 self.0 ^= self.0 >> 7;
287 self.0 ^= self.0 << 17;
288 let v = (self.0.wrapping_mul(0x2545F4914F6CDD1D) >> 40) as u32;
289 (v as f32 / (1u32 << 24) as f32) * 2.0 - 1.0 }
291 fn fill(&mut self, n: usize, scale: f32) -> Vec<f32> {
292 (0..n).map(|_| self.next_f32() * scale).collect()
293 }
294 }
295
296 fn maxdiff(a: &[f32], b: &[f32]) -> f32 {
297 assert_eq!(a.len(), b.len());
298 a.iter().zip(b).map(|(x, y)| (x - y).abs()).fold(0.0f32, f32::max)
299 }
300 fn maxabs(a: &[f32]) -> f32 {
301 a.iter().map(|x| x.abs()).fold(0.0f32, f32::max)
302 }
303
304 fn random_case(d: &MlaDims, t_q: usize, t_kv: usize, seed: u64) -> (Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>) {
307 let mut rng = Rng(seed | 1);
308 let ws = 1.0 / (d.kv_rank as f32).sqrt();
309 (
310 rng.fill(t_q * d.n_head * d.d_nope, 1.0),
311 rng.fill(t_q * d.n_head * d.d_rope, 1.0),
312 rng.fill(t_kv * d.kv_rank, 1.0),
313 rng.fill(t_kv * d.d_rope, 1.0),
314 rng.fill(d.n_head * d.d_nope * d.kv_rank, ws),
315 rng.fill(d.n_head * d.d_v * d.kv_rank, ws),
316 )
317 }
318
319 fn run_case(d: &MlaDims, t_q: usize, t_kv: usize, seed: u64, tol: f32) {
320 let (q_nope, q_pe, c_kv, k_pe, w_uk, w_uv) = random_case(d, t_q, t_kv, seed);
321 let x = MlaInputs {
322 q_nope: &q_nope, q_pe: &q_pe, c_kv: &c_kv, k_pe: &k_pe,
323 w_uk: &w_uk, w_uv: &w_uv, t_q, t_kv,
324 };
325 let naive = mla_attend_naive(d, &x);
326 let absorbed = mla_attend_absorbed(d, &x);
327 let md = maxdiff(&naive, &absorbed);
328 let scale = maxabs(&naive).max(1.0);
329 assert!(
330 md <= tol * scale,
331 "naive vs absorbed disagree: maxdiff {md:.3e} (scale {scale:.3e}, rel {:.3e}) \
332 dims {d:?} t_q {t_q} t_kv {t_kv} seed {seed}",
333 md / scale
334 );
335 assert!(naive.iter().all(|v| v.is_finite()));
337 assert!(maxabs(&naive) > 1e-6);
338 }
339
340 #[test]
341 fn naive_equals_absorbed_decode_t1() {
342 let shapes = [
344 MlaDims { n_head: 4, d_nope: 24, d_rope: 8, d_v: 32, kv_rank: 64 },
345 MlaDims { n_head: 2, d_nope: 16, d_rope: 16, d_v: 16, kv_rank: 32 },
346 MlaDims { n_head: 3, d_nope: 12, d_rope: 4, d_v: 20, kv_rank: 48 },
349 ];
350 for (i, d) in shapes.iter().enumerate() {
351 for seed in [7, 1234, 0xB1E55ED] {
352 run_case(d, 1, 17, seed + i as u64, 1e-5);
353 }
354 }
355 }
356
357 #[test]
358 fn naive_equals_absorbed_prefill_causal() {
359 let d = MlaDims { n_head: 4, d_nope: 24, d_rope: 8, d_v: 32, kv_rank: 64 };
361 run_case(&d, 5, 9, 42, 1e-5);
362 run_case(&d, 8, 8, 43, 1e-5); let d2 = MlaDims { n_head: 2, d_nope: 16, d_rope: 16, d_v: 16, kv_rank: 32 };
364 run_case(&d2, 3, 11, 44, 1e-5);
365 }
366
367 #[test]
368 fn naive_equals_absorbed_glm52_full_dims() {
369 run_case(&MlaDims::GLM52, 1, 8, 20260801, 1e-4);
372 }
373
374 #[test]
375 fn rope_norm_equals_permuted_neox() {
376 let n_dims = 64;
382 let base = 8_000_000.0f32; let perm = norm_to_neox_perm(n_dims);
384 let mut rng = Rng(99);
385 for pos in [0.0f32, 1.0, 17.0, 4096.0, 1_000_000.0] {
386 let x0: Vec<f32> = (0..n_dims).map(|_| rng.next_f32()).collect();
387 let y0: Vec<f32> = (0..n_dims).map(|_| rng.next_f32()).collect();
388
389 let mut xa = x0.clone();
391 rope_interleaved(&mut xa, n_dims, pos, base);
392 let mut xa_p = vec![0.0f32; n_dims];
393 for (src, &dst) in perm.iter().enumerate() {
394 xa_p[dst] = xa[src];
395 }
396 let mut xb = vec![0.0f32; n_dims];
398 for (src, &dst) in perm.iter().enumerate() {
399 xb[dst] = x0[src];
400 }
401 rope_neox(&mut xb, n_dims, pos, base);
402
403 assert!(
404 maxdiff(&xa_p, &xb) <= 1e-6,
405 "perm/rope orders disagree at pos {pos}"
406 );
407
408 let mut ya = y0.clone();
410 rope_interleaved(&mut ya, n_dims, pos, base);
411 let dot_norm: f32 = xa.iter().zip(&ya).map(|(a, b)| a * b).sum();
412
413 let mut yb = vec![0.0f32; n_dims];
414 for (src, &dst) in perm.iter().enumerate() {
415 yb[dst] = y0[src];
416 }
417 rope_neox(&mut yb, n_dims, pos, base);
418 let dot_neox: f32 = xb.iter().zip(&yb).map(|(a, b)| a * b).sum();
419
420 assert!(
421 (dot_norm - dot_neox).abs() <= 1e-4 * dot_norm.abs().max(1.0),
422 "roped dot products diverge at pos {pos}: {dot_norm} vs {dot_neox}"
423 );
424 }
425 }
426}