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 = MlaDims {
34 n_head: 64,
35 d_nope: 192,
36 d_rope: 64,
37 d_v: 256,
38 kv_rank: 512,
39 };
40
41 pub const GLM5_NEXT: MlaDims = MlaDims {
46 n_head: 64,
47 d_nope: 256,
48 d_rope: 0,
49 d_v: 256,
50 kv_rank: 512,
51 };
52
53 pub fn scale(&self) -> f32 {
56 1.0 / ((self.d_nope + self.d_rope) as f32).sqrt()
57 }
58}
59
60pub struct MlaInputs<'a> {
75 pub q_nope: &'a [f32],
76 pub q_pe: &'a [f32],
77 pub c_kv: &'a [f32],
78 pub k_pe: &'a [f32],
79 pub w_uk: &'a [f32],
80 pub w_uv: &'a [f32],
81 pub t_q: usize,
82 pub t_kv: usize,
83}
84
85fn check_shapes(d: &MlaDims, x: &MlaInputs) {
86 assert_eq!(x.q_nope.len(), x.t_q * d.n_head * d.d_nope, "q_nope shape");
87 assert_eq!(x.q_pe.len(), x.t_q * d.n_head * d.d_rope, "q_pe shape");
88 assert_eq!(x.c_kv.len(), x.t_kv * d.kv_rank, "c_kv shape");
89 assert_eq!(x.k_pe.len(), x.t_kv * d.d_rope, "k_pe shape");
90 assert_eq!(x.w_uk.len(), d.n_head * d.d_nope * d.kv_rank, "w_uk shape");
91 assert_eq!(x.w_uv.len(), d.n_head * d.d_v * d.kv_rank, "w_uv shape");
92 assert!(x.t_q <= x.t_kv, "queries must be a suffix of the cache");
93}
94
95fn softmax(s: &mut [f32]) {
97 let m = s.iter().copied().fold(f32::NEG_INFINITY, f32::max);
98 let mut sum = 0.0f32;
99 for v in s.iter_mut() {
100 *v = (*v - m).exp();
101 sum += *v;
102 }
103 let inv = 1.0 / sum;
104 for v in s.iter_mut() {
105 *v *= inv;
106 }
107}
108
109pub fn mla_attend_naive(d: &MlaDims, x: &MlaInputs) -> Vec<f32> {
113 check_shapes(d, x);
114 let (nh, dn, dr, dv, r) = (d.n_head, d.d_nope, d.d_rope, d.d_v, d.kv_rank);
115 let scale = d.scale();
116 let mut out = vec![0.0f32; x.t_q * nh * dv];
117
118 let mut k_nope = vec![0.0f32; x.t_kv * dn];
120 let mut v = vec![0.0f32; x.t_kv * dv];
121 let mut scores = vec![0.0f32; x.t_kv];
122 for h in 0..nh {
123 let wuk = &x.w_uk[h * dn * r..(h + 1) * dn * r];
124 let wuv = &x.w_uv[h * dv * r..(h + 1) * dv * r];
125 for t in 0..x.t_kv {
126 let c = &x.c_kv[t * r..(t + 1) * r];
127 for p in 0..dn {
128 let row = &wuk[p * r..(p + 1) * r];
129 let mut acc = 0.0f32;
130 for l in 0..r {
131 acc += row[l] * c[l];
132 }
133 k_nope[t * dn + p] = acc;
134 }
135 for j in 0..dv {
136 let row = &wuv[j * r..(j + 1) * r];
137 let mut acc = 0.0f32;
138 for l in 0..r {
139 acc += row[l] * c[l];
140 }
141 v[t * dv + j] = acc;
142 }
143 }
144 for i in 0..x.t_q {
145 let visible = x.t_kv - x.t_q + i + 1; let qn = &x.q_nope[(i * nh + h) * dn..(i * nh + h + 1) * dn];
147 let qp = &x.q_pe[(i * nh + h) * dr..(i * nh + h + 1) * dr];
148 for t in 0..visible {
149 let mut s = 0.0f32;
150 let kn = &k_nope[t * dn..(t + 1) * dn];
151 for p in 0..dn {
152 s += qn[p] * kn[p];
153 }
154 let kp = &x.k_pe[t * dr..(t + 1) * dr];
155 for p in 0..dr {
156 s += qp[p] * kp[p];
157 }
158 scores[t] = s * scale;
159 }
160 softmax(&mut scores[..visible]);
161 let o = &mut out[(i * nh + h) * dv..(i * nh + h + 1) * dv];
162 for t in 0..visible {
163 let p = scores[t];
164 let vt = &v[t * dv..(t + 1) * dv];
165 for j in 0..dv {
166 o[j] += p * vt[j];
167 }
168 }
169 }
170 }
171 out
172}
173
174pub fn mla_attend_absorbed(d: &MlaDims, x: &MlaInputs) -> Vec<f32> {
179 check_shapes(d, x);
180 let (nh, dn, dr, dv, r) = (d.n_head, d.d_nope, d.d_rope, d.d_v, d.kv_rank);
181 let scale = d.scale();
182 let mut out = vec![0.0f32; x.t_q * nh * dv];
183
184 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];
187 for h in 0..nh {
188 let wuk = &x.w_uk[h * dn * r..(h + 1) * dn * r];
189 let wuv = &x.w_uv[h * dv * r..(h + 1) * dv * r];
190 for i in 0..x.t_q {
191 let visible = x.t_kv - x.t_q + i + 1;
192 let qn = &x.q_nope[(i * nh + h) * dn..(i * nh + h + 1) * dn];
193 let qp = &x.q_pe[(i * nh + h) * dr..(i * nh + h + 1) * dr];
194 q_lat.iter_mut().for_each(|v| *v = 0.0);
196 for p in 0..dn {
197 let row = &wuk[p * r..(p + 1) * r];
198 let qv = qn[p];
199 for l in 0..r {
200 q_lat[l] += qv * row[l];
201 }
202 }
203 #[allow(clippy::needless_range_loop)]
205 for t in 0..visible {
207 let c = &x.c_kv[t * r..(t + 1) * r];
208 let mut s = 0.0f32;
209 for l in 0..r {
210 s += q_lat[l] * c[l];
211 }
212 let kp = &x.k_pe[t * dr..(t + 1) * dr];
213 for p in 0..dr {
214 s += qp[p] * kp[p];
215 }
216 scores[t] = s * scale;
217 }
218 softmax(&mut scores[..visible]);
219 o_lat.iter_mut().for_each(|v| *v = 0.0);
221 #[allow(clippy::needless_range_loop)]
222 for t in 0..visible {
224 let p = scores[t];
225 let c = &x.c_kv[t * r..(t + 1) * r];
226 for l in 0..r {
227 o_lat[l] += p * c[l];
228 }
229 }
230 let o = &mut out[(i * nh + h) * dv..(i * nh + h + 1) * dv];
232 for j in 0..dv {
233 let row = &wuv[j * r..(j + 1) * r];
234 let mut acc = 0.0f32;
235 for l in 0..r {
236 acc += row[l] * o_lat[l];
237 }
238 o[j] = acc;
239 }
240 }
241 }
242 out
243}
244
245pub fn rope_interleaved(x: &mut [f32], n_dims: usize, pos: f32, base: f32) {
255 let half = n_dims / 2;
256 let theta_scale = base.powf(-2.0 / n_dims as f32);
257 let mut theta = pos;
258 for j in 0..half {
259 let (sin, cos) = theta.sin_cos();
260 let a = x[2 * j];
261 let b = x[2 * j + 1];
262 x[2 * j] = a * cos - b * sin;
263 x[2 * j + 1] = a * sin + b * cos;
264 theta *= theta_scale;
265 }
266}
267
268pub fn rope_neox(x: &mut [f32], n_dims: usize, pos: f32, base: f32) {
271 let half = n_dims / 2;
272 let theta_scale = base.powf(-2.0 / n_dims as f32);
273 let mut theta = pos;
274 for j in 0..half {
275 let (sin, cos) = theta.sin_cos();
276 let a = x[j];
277 let b = x[j + half];
278 x[j] = a * cos - b * sin;
279 x[j + half] = a * sin + b * cos;
280 theta *= theta_scale;
281 }
282}
283
284pub fn norm_to_neox_perm(n_dims: usize) -> Vec<usize> {
289 let half = n_dims / 2;
290 let mut p = vec![0usize; n_dims];
291 for j in 0..half {
292 p[2 * j] = j;
293 p[2 * j + 1] = j + half;
294 }
295 p
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 struct Rng(u64);
304 impl Rng {
305 fn next_f32(&mut self) -> f32 {
306 self.0 ^= self.0 << 13;
307 self.0 ^= self.0 >> 7;
308 self.0 ^= self.0 << 17;
309 let v = (self.0.wrapping_mul(0x2545F4914F6CDD1D) >> 40) as u32;
310 (v as f32 / (1u32 << 24) as f32) * 2.0 - 1.0 }
312 fn fill(&mut self, n: usize, scale: f32) -> Vec<f32> {
313 (0..n).map(|_| self.next_f32() * scale).collect()
314 }
315 }
316
317 fn maxdiff(a: &[f32], b: &[f32]) -> f32 {
318 assert_eq!(a.len(), b.len());
319 a.iter()
320 .zip(b)
321 .map(|(x, y)| (x - y).abs())
322 .fold(0.0f32, f32::max)
323 }
324 fn maxabs(a: &[f32]) -> f32 {
325 a.iter().map(|x| x.abs()).fold(0.0f32, f32::max)
326 }
327
328 #[allow(clippy::type_complexity)] fn random_case(
332 d: &MlaDims,
333 t_q: usize,
334 t_kv: usize,
335 seed: u64,
336 ) -> (Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>) {
337 let mut rng = Rng(seed | 1);
338 let ws = 1.0 / (d.kv_rank as f32).sqrt();
339 (
340 rng.fill(t_q * d.n_head * d.d_nope, 1.0),
341 rng.fill(t_q * d.n_head * d.d_rope, 1.0),
342 rng.fill(t_kv * d.kv_rank, 1.0),
343 rng.fill(t_kv * d.d_rope, 1.0),
344 rng.fill(d.n_head * d.d_nope * d.kv_rank, ws),
345 rng.fill(d.n_head * d.d_v * d.kv_rank, ws),
346 )
347 }
348
349 fn run_case(d: &MlaDims, t_q: usize, t_kv: usize, seed: u64, tol: f32) {
350 let (q_nope, q_pe, c_kv, k_pe, w_uk, w_uv) = random_case(d, t_q, t_kv, seed);
351 let x = MlaInputs {
352 q_nope: &q_nope,
353 q_pe: &q_pe,
354 c_kv: &c_kv,
355 k_pe: &k_pe,
356 w_uk: &w_uk,
357 w_uv: &w_uv,
358 t_q,
359 t_kv,
360 };
361 let naive = mla_attend_naive(d, &x);
362 let absorbed = mla_attend_absorbed(d, &x);
363 let md = maxdiff(&naive, &absorbed);
364 let scale = maxabs(&naive).max(1.0);
365 assert!(
366 md <= tol * scale,
367 "naive vs absorbed disagree: maxdiff {md:.3e} (scale {scale:.3e}, rel {:.3e}) \
368 dims {d:?} t_q {t_q} t_kv {t_kv} seed {seed}",
369 md / scale
370 );
371 assert!(naive.iter().all(|v| v.is_finite()));
373 assert!(maxabs(&naive) > 1e-6);
374 }
375
376 #[test]
377 fn naive_equals_absorbed_decode_t1() {
378 let shapes = [
380 MlaDims {
381 n_head: 4,
382 d_nope: 24,
383 d_rope: 8,
384 d_v: 32,
385 kv_rank: 64,
386 },
387 MlaDims {
388 n_head: 2,
389 d_nope: 16,
390 d_rope: 16,
391 d_v: 16,
392 kv_rank: 32,
393 },
394 MlaDims {
397 n_head: 3,
398 d_nope: 12,
399 d_rope: 4,
400 d_v: 20,
401 kv_rank: 48,
402 },
403 ];
404 for (i, d) in shapes.iter().enumerate() {
405 for seed in [7, 1234, 0xB1E55ED] {
406 run_case(d, 1, 17, seed + i as u64, 1e-5);
407 }
408 }
409 }
410
411 #[test]
412 fn naive_equals_absorbed_prefill_causal() {
413 let d = MlaDims {
415 n_head: 4,
416 d_nope: 24,
417 d_rope: 8,
418 d_v: 32,
419 kv_rank: 64,
420 };
421 run_case(&d, 5, 9, 42, 1e-5);
422 run_case(&d, 8, 8, 43, 1e-5); let d2 = MlaDims {
424 n_head: 2,
425 d_nope: 16,
426 d_rope: 16,
427 d_v: 16,
428 kv_rank: 32,
429 };
430 run_case(&d2, 3, 11, 44, 1e-5);
431 }
432
433 #[test]
434 fn naive_equals_absorbed_glm52_full_dims() {
435 run_case(&MlaDims::GLM52, 1, 8, 20260801, 1e-4);
438 }
439
440 #[test]
445 fn naive_equals_absorbed_nope_rope_zero() {
446 let shapes = [
448 MlaDims {
449 n_head: 4,
450 d_nope: 32,
451 d_rope: 0,
452 d_v: 32,
453 kv_rank: 64,
454 },
455 MlaDims {
456 n_head: 3,
457 d_nope: 16,
458 d_rope: 0,
459 d_v: 24,
460 kv_rank: 48,
461 },
462 ];
463 for (i, d) in shapes.iter().enumerate() {
464 for seed in [11, 2026, 0x5EED] {
465 run_case(d, 1, 13, seed + i as u64, 1e-5); run_case(d, 5, 5, seed + 7 + i as u64, 1e-5); run_case(d, 3, 9, seed + 13 + i as u64, 1e-5); }
469 }
470 run_case(&MlaDims::GLM5_NEXT, 1, 8, 20260827, 1e-4);
472 run_case(&MlaDims::GLM5_NEXT, 4, 4, 20260828, 1e-4);
473 }
474
475 #[test]
478 fn nope_scale_is_qk_head_dim() {
479 let d = MlaDims::GLM5_NEXT;
480 assert_eq!(d.d_nope + d.d_rope, 256);
481 assert!((d.scale() - 1.0 / 16.0).abs() <= 1e-9);
482 assert!((MlaDims::GLM52.scale() - d.scale()).abs() <= 1e-9);
483 }
484
485 #[test]
486 fn rope_norm_equals_permuted_neox() {
487 let n_dims = 64;
493 let base = 8_000_000.0f32; let perm = norm_to_neox_perm(n_dims);
495 let mut rng = Rng(99);
496 for pos in [0.0f32, 1.0, 17.0, 4096.0, 1_000_000.0] {
497 let x0: Vec<f32> = (0..n_dims).map(|_| rng.next_f32()).collect();
498 let y0: Vec<f32> = (0..n_dims).map(|_| rng.next_f32()).collect();
499
500 let mut xa = x0.clone();
502 rope_interleaved(&mut xa, n_dims, pos, base);
503 let mut xa_p = vec![0.0f32; n_dims];
504 for (src, &dst) in perm.iter().enumerate() {
505 xa_p[dst] = xa[src];
506 }
507 let mut xb = vec![0.0f32; n_dims];
509 for (src, &dst) in perm.iter().enumerate() {
510 xb[dst] = x0[src];
511 }
512 rope_neox(&mut xb, n_dims, pos, base);
513
514 assert!(
515 maxdiff(&xa_p, &xb) <= 1e-6,
516 "perm/rope orders disagree at pos {pos}"
517 );
518
519 let mut ya = y0.clone();
521 rope_interleaved(&mut ya, n_dims, pos, base);
522 let dot_norm: f32 = xa.iter().zip(&ya).map(|(a, b)| a * b).sum();
523
524 let mut yb = vec![0.0f32; n_dims];
525 for (src, &dst) in perm.iter().enumerate() {
526 yb[dst] = y0[src];
527 }
528 rope_neox(&mut yb, n_dims, pos, base);
529 let dot_neox: f32 = xb.iter().zip(&yb).map(|(a, b)| a * b).sum();
530
531 assert!(
532 (dot_norm - dot_neox).abs() <= 1e-4 * dot_norm.abs().max(1.0),
533 "roped dot products diverge at pos {pos}: {dot_norm} vs {dot_neox}"
534 );
535 }
536 }
537}