Skip to main content

memra_engine/
mla.rs

1//! MLA (multi-head latent attention, DeepSeek lineage / GLM-5 "MLA-256") — CPU f32 reference.
2//!
3//! Increment 1 of the GLM-5.2 bring-up lane (`research/mla-bringup-20260801/DESIGN.md`).
4//! This module pins the decode-path math BEFORE any kernel work: both the naive form
5//! (decompress the latent cache to per-head K/V, then attend — vLLM "forward_mha") and the
6//! absorbed form (fold W_UK into the query, attend in latent space as MQA, decompress the
7//! output through W_UV — vLLM "forward_mqa", llama.cpp glm-dsa.cpp). The unit tests prove the
8//! two forms agree to f32 tolerance on random inputs across shapes (t=1 decode and small
9//! causal prefill), including full GLM-5.2 dims (64 heads, nope 192, rope 64, v 256, rank 512).
10//!
11//! Also pinned here: the interleaved ("NORM", `rope_interleave: true`) vs NEOX rope pairing and
12//! the load-time permutation that maps one onto the other (DESIGN.md §1.4) — memra only ships a
13//! NEOX kernel, GLM-5.2 needs NORM, and the permutation trick lets the existing kernel serve.
14//!
15//! Everything is plain CPU f32, no CUDA, no engine deps: this is the permanent oracle for the
16//! MLA kernel family's maxdiff gates.
17
18/// MLA head geometry. GLM-5.2: n_head=64, d_nope=192, d_rope=64, d_v=256, kv_rank=512.
19#[derive(Clone, Copy, Debug)]
20pub struct MlaDims {
21    pub n_head: usize,
22    /// qk nope head dim (P)
23    pub d_nope: usize,
24    /// qk rope head dim (R); latent cache row = kv_rank + d_rope
25    pub d_rope: usize,
26    /// v head dim (V)
27    pub d_v: usize,
28    /// kv lora rank (Lkv)
29    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    /// glm5_next (GLM-5.3-Flash) MLA geometry: NoPE — `rope_head_dim` is 0, so there is no
42    /// decoupled rope plane at all and the latent cache row is `kv_rank` wide (not kv_rank+rope).
43    /// qk_head_dim is therefore ALL nope (256), and the softmax scale is 1/sqrt(256) = 1/16 —
44    /// numerically the same 1/16 as GLM-5.2 (192+64), reached by a different decomposition.
45    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    /// Softmax scale: 1/sqrt(d_nope + d_rope) — the ORIGINAL qk head dim (256 for GLM-5.2),
54    /// NOT the absorbed width (576). llama.cpp glm-dsa.cpp `kq_scale` with mscale=1 (no yarn).
55    pub fn scale(&self) -> f32 {
56        1.0 / ((self.d_nope + self.d_rope) as f32).sqrt()
57    }
58}
59
60/// Inputs shared by both forms. Rope is already applied to `q_pe`/`k_pe` (it happens upstream
61/// of the attention core and is identical in both forms). `c_kv` is already RMS-normed.
62///
63/// Layouts (row-major):
64///   q_nope: [t_q][n_head][d_nope]
65///   q_pe:   [t_q][n_head][d_rope]
66///   c_kv:   [t_kv][kv_rank]           — the latent KV cache (one row per token, all heads)
67///   k_pe:   [t_kv][d_rope]            — decoupled rope key (one per token, all heads)
68///   w_uk:   [n_head][d_nope][kv_rank] — k_nope_h = w_uk[h] · c_kv
69///   w_uv:   [n_head][d_v][kv_rank]    — v_h      = w_uv[h] · c_kv
70///
71/// The queries occupy the LAST `t_q` positions of the cache (decode/prefill convention:
72/// their own rows are already appended). Causal: query i attends to cache rows
73/// 0 ..= (t_kv - t_q + i).
74pub 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
95/// In-place softmax with max-subtraction over `s[..n]`.
96fn 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
109/// Naive form: decompress k_nope/v per head from the latent cache, attend at qk dim
110/// d_nope+d_rope, output [t_q][n_head][d_v]. Quadratic decompression cost — prefill-only
111/// shape in production; here it is the independent oracle.
112pub 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    // Decompress the whole cache per head: k_nope[t][dn], v[t][dv].
119    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; // causal horizon for query i
146            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
174/// Absorbed form (decode form): q̃_h = w_uk[h]ᵀ·q_nope_h (rank-space, kv_rank wide), scores are
175/// MQA dots against the raw latent rows [c_kv | k_pe] (kv_rank + d_rope wide), the attention
176/// output is accumulated in latent space (kv_rank wide) and decompressed once through w_uv.
177/// Identical result to `mla_attend_naive` by associativity + linearity (DESIGN.md §1.3).
178pub 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]; // absorbed query, rank space
185    let mut o_lat = vec![0.0f32; r]; // attention output, latent space
186    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            // absorb: q_lat[l] = sum_p q_nope[p] * w_uk[h][p][l]
195            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            // MQA scores against the 576-wide latent rows
204            #[allow(clippy::needless_range_loop)]
205            // allow: the explicit index loop keeps the offset arithmetic visible and aligned with the device-side indexing
206            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            // latent-space AV
220            o_lat.iter_mut().for_each(|v| *v = 0.0);
221            #[allow(clippy::needless_range_loop)]
222            // allow: the explicit index loop keeps the offset arithmetic visible and aligned with the device-side indexing
223            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            // decompress once: out[j] = sum_l w_uv[h][j][l] * o_lat[l]
231            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
245// ---------------------------------------------------------------------------
246// RoPE: GLM-5.2 is `rope_interleave: true` == llama.cpp LLAMA_ROPE_TYPE_NORM.
247// memra ships only NEOX pairing; the permutation below maps NORM onto NEOX at
248// weight-load time (DESIGN.md §1.4). Both variants + the permutation live here
249// so the equivalence is a pinned, tested fact.
250// ---------------------------------------------------------------------------
251
252/// Interleaved ("NORM") rope over the first `n_dims` of `x`: pair (x[2j], x[2j+1]) rotated by
253/// theta_j = pos * base^(-2j/n_dims). Matches ggml GGML_ROPE_TYPE_NORM / HF interleaved.
254pub 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
268/// NEOX rope over the first `n_dims` of `x`: pair (x[j], x[j+half]) rotated by the same
269/// theta_j sequence. Matches memra's `rope_neox_f32` (kernels.cu) angle recurrence.
270pub 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
284/// The load-time permutation: source (interleaved-layout) index -> NEOX-layout index.
285/// pi(2j) = j, pi(2j+1) = j + n_dims/2. Applied to the rope rows of wq_b / wkv_a_mqa at load,
286/// it makes the existing NEOX kernel compute exactly the interleaved rotation (dot-product
287/// consumers only — which is all of them).
288pub 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    /// xorshift64* — deterministic, no external crates.
303    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 // uniform [-1, 1)
311        }
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    /// Build random inputs at unit-ish scale: weights ~ 1/sqrt(rank) so decompressed values and
329    /// scores stay O(1) and the f32 tolerance is meaningful.
330    #[allow(clippy::type_complexity)] // allow: one-shot composite type; naming it would hide the shape that matters at the call site
331    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        // sanity: outputs are finite and not trivially zero
372        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        // t=1 decode against a populated cache, several synthetic shapes + seeds.
379        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            // GLM-5.2 ratio at 1/8 scale: nope 24, rope 8, v 32, rank 64 handled above;
395            // an asymmetric case where d_v > d_nope (the GLM-5.2 signature, v 256 > nope 192):
396            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        // small prefill: t_q new tokens over t_kv-t_q past tokens, causal horizon per query.
414        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); // pure prefill, no past
423        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        // Full GLM-5.2 geometry (64 heads, 192/64/256, rank 512) — decode t=1, T=8.
436        // Wider accumulations (576-dot, rank-512 decompress) ⇒ slightly looser f32 tolerance.
437        run_case(&MlaDims::GLM52, 1, 8, 20260801, 1e-4);
438    }
439
440    /// NoPE pin (glm5_next / GLM-5.3-Flash): `d_rope == 0` — no decoupled rope plane, the
441    /// latent row is kv_rank wide, and every rope loop in both forms is empty. The oracle had
442    /// never been run at this geometry; it is the GPU arm's truth for the NoPE door, so it is
443    /// pinned as a tested fact here before any kernel compares against it.
444    #[test]
445    fn naive_equals_absorbed_nope_rope_zero() {
446        // shrunk NoPE shapes first (cheap, several seeds), then full glm5_next dims.
447        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); // decode
466                run_case(d, 5, 5, seed + 7 + i as u64, 1e-5); // pure prefill
467                run_case(d, 3, 9, seed + 13 + i as u64, 1e-5); // chunked
468            }
469        }
470        // Full glm5_next geometry (64 heads, nope 256, rope 0, v 256, rank 512).
471        run_case(&MlaDims::GLM5_NEXT, 1, 8, 20260827, 1e-4);
472        run_case(&MlaDims::GLM5_NEXT, 4, 4, 20260828, 1e-4);
473    }
474
475    /// The NoPE scale is 1/sqrt(qk_head_dim) with qk_head_dim == d_nope (rope contributes 0),
476    /// NOT 1/sqrt(kv_rank) — the absorbed width (512) must never reach the softmax.
477    #[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        // DESIGN.md §1.4: permuting the rope dims at load time (pi(2j)=j, pi(2j+1)=j+half)
488        // makes the NEOX kernel compute the interleaved ("NORM") rotation. Verify:
489        //   permute(rope_interleaved(x)) == rope_neox(permute(x))
490        // for the GLM-5.2 rope width (64) at several positions, and that dot products between
491        // two identically-permuted roped vectors match the un-permuted interleaved dots.
492        let n_dims = 64;
493        let base = 8_000_000.0f32; // GLM-5.2 rope_theta
494        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            // path A: interleaved rope, then permute
501            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            // path B: permute, then neox rope
508            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            // dot-product invariance (what attention actually consumes)
520            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}