Skip to main content

cortiq_engine/
blr.rs

1//! Binary low-rank (`blr`, dtype 19) runtime: factored ±1 matvec / matmat
2//! straight from the mmap (docs/BLR_NVG.md §3). `W` is never materialised.
3//!
4//! Per token and path `p`:
5//! ```text
6//! z_p = x ⊙ g_p
7//! t_p[k] = ℓ_p[k] · Σ_j V_p(k,j) · z_p[j]          stage V: P·r binary dots of length d_in
8//! y[i]  = Σ_p h_p[i] · Σ_k U_p(i,k) · t_p[k]        stage U: d_out rows, P binary dots of length r
9//! ```
10//! A binary dot reads its signs four at a time through a 16-entry signed
11//! partial-sum table per group of four inputs. Batches of `BLR_GEMM_MIN` or
12//! more tokens unpack the signs to ±1 f32 and run two GEMMs.
13
14use crate::pool::Pool;
15use cortiq_core::CmfModel;
16use cortiq_core::blr::{BlrHeader, blr_f16s, blr_path_offsets, validate_blr};
17use std::sync::Arc;
18
19/// Below this many tokens a batch loops the table matvec.
20pub const BLR_GEMM_MIN: usize = 4;
21
22/// Whether the blr kernels may take the GPU when the device is up
23/// (`CMF_BLR_GPU=0` keeps them on the CPU).
24pub fn gpu_wanted() -> bool {
25    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
26    *ON.get_or_init(|| std::env::var("CMF_BLR_GPU").as_deref() != Ok("0"))
27}
28
29/// A mapped `blr` tensor: the model keeps the bytes, `core` the parsed scales.
30pub struct BlrTensor {
31    pub model: Arc<CmfModel>,
32    pub idx: usize,
33    pub rows: usize,
34    pub cols: usize,
35    pub core: BlrCore,
36}
37
38impl BlrTensor {
39    pub fn from_model(model: &Arc<CmfModel>, idx: usize) -> Result<Self, String> {
40        let e = &model.tensors[idx];
41        let core = BlrCore::parse(&e.shape, model.entry_bytes(e))
42            .map_err(|m| format!("{}: {m}", e.name))?;
43        Ok(BlrTensor {
44            model: model.clone(),
45            idx,
46            rows: core.rows,
47            cols: core.cols,
48            core,
49        })
50    }
51
52    #[inline]
53    pub fn bytes(&self) -> &[u8] {
54        self.model.entry_bytes(&self.model.tensors[self.idx])
55    }
56
57    pub fn name(&self) -> &str {
58        &self.model.tensors[self.idx].name
59    }
60
61    pub fn matvec(&self, x: &[f32], out: &mut [f32], pool: Option<&Pool>) {
62        self.core.matvec(self.bytes(), x, out, pool)
63    }
64
65    pub fn matmat(&self, xs: &[f32], b: usize, out: &mut [f32], pool: Option<&Pool>) {
66        self.core.matmat(self.bytes(), xs, b, out, pool)
67    }
68
69    pub fn row_f32(&self, i: usize, dst: &mut [f32]) {
70        self.core.row_f32(self.bytes(), i, dst)
71    }
72
73    pub fn add_col_scaled(&self, c: usize, w: f32, out: &mut [f32]) {
74        self.core.add_col_scaled(self.bytes(), c, w, out)
75    }
76
77    /// Dense f32 copy (tests, parity).
78    pub fn dequant(&self) -> Vec<f32> {
79        let mut w = vec![0f32; self.rows * self.cols];
80        cortiq_core::blr::dequant_blr(self.bytes(), self.rows, self.cols, &mut w)
81            .expect("validated at load");
82        w
83    }
84}
85
86/// Parsed header + f32 scales of a `blr` payload; kernels take the bytes.
87pub struct BlrCore {
88    pub rows: usize,
89    pub cols: usize,
90    pub paths: usize,
91    pub rank: usize,
92    /// Per path: h (rows), g (cols), ℓ (rank), f32.
93    pub h: Vec<Vec<f32>>,
94    pub g: Vec<Vec<f32>>,
95    pub l: Vec<Vec<f32>>,
96    /// Per path: byte offsets of U (rows × r/8) and V (r × cols/8) in the payload.
97    pub u_off: Vec<usize>,
98    pub v_off: Vec<usize>,
99}
100
101#[derive(Clone, Copy)]
102struct SendPtr(*mut f32);
103unsafe impl Send for SendPtr {}
104unsafe impl Sync for SendPtr {}
105
106impl BlrCore {
107    pub fn parse(shape: &[usize], bytes: &[u8]) -> Result<Self, String> {
108        let hdr: BlrHeader = validate_blr(shape, bytes)?;
109        let mut t = BlrCore {
110            rows: hdr.d_out,
111            cols: hdr.d_in,
112            paths: hdr.paths,
113            rank: hdr.rank,
114            h: Vec::new(),
115            g: Vec::new(),
116            l: Vec::new(),
117            u_off: Vec::new(),
118            v_off: Vec::new(),
119        };
120        for p in 0..hdr.paths {
121            let o = blr_path_offsets(&hdr, p);
122            t.h.push(blr_f16s(bytes, o.h, hdr.d_out));
123            t.g.push(blr_f16s(bytes, o.g, hdr.d_in));
124            t.l.push(blr_f16s(bytes, o.l, hdr.rank));
125            t.u_off.push(o.u);
126            t.v_off.push(o.v);
127        }
128        Ok(t)
129    }
130
131    /// Sign bits of path `p`: (U, V) byte slices.
132    pub fn planes<'a>(&self, b: &'a [u8], p: usize) -> (&'a [u8], &'a [u8]) {
133        let ub = self.rows * self.rank / 8;
134        let vb = self.rank * self.cols / 8;
135        (
136            &b[self.u_off[p]..self.u_off[p] + ub],
137            &b[self.v_off[p]..self.v_off[p] + vb],
138        )
139    }
140
141    /// Single-vector product `out[..rows] = W x`.
142    pub fn matvec(&self, bytes: &[u8], x: &[f32], out: &mut [f32], pool: Option<&Pool>) {
143        let (rows, cols, r, np) = (self.rows, self.cols, self.rank, self.paths);
144        assert!(
145            x.len() >= cols && out.len() >= rows,
146            "blr matvec {rows}x{cols}: x {} out {}",
147            x.len(),
148            out.len()
149        );
150        // stage V
151        let mut tv = vec![0f32; np * cols * 4];
152        let mut z = vec![0f32; cols];
153        for p in 0..np {
154            for j in 0..cols {
155                z[j] = x[j] * self.g[p][j];
156            }
157            build_table(&z, &mut tv[p * cols * 4..(p + 1) * cols * 4]);
158        }
159        let mut t = vec![0f32; np * r];
160        {
161            let tp = SendPtr(t.as_mut_ptr());
162            let tv = &tv;
163            let run = |s: usize, e: usize| {
164                let tp = tp;
165                let rb = cols / 8;
166                for row in s..e {
167                    let (p, k) = (row / r, row % r);
168                    let (_, vb) = self.planes(bytes, p);
169                    let d = table_dot(
170                        &vb[k * rb..(k + 1) * rb],
171                        &tv[p * cols * 4..(p + 1) * cols * 4],
172                    );
173                    unsafe { *tp.0.add(row) = d * self.l[p][k] };
174                }
175            };
176            dispatch(pool, np * r, np * r * cols, &run);
177        }
178        // stage U
179        let mut tu = vec![0f32; np * r * 4];
180        for p in 0..np {
181            build_table(&t[p * r..(p + 1) * r], &mut tu[p * r * 4..(p + 1) * r * 4]);
182        }
183        let op = SendPtr(out.as_mut_ptr());
184        let tu = &tu;
185        let run = |s: usize, e: usize| {
186            let op = op;
187            let rb = r / 8;
188            for i in s..e {
189                let mut y = 0f32;
190                for p in 0..np {
191                    let (ub, _) = self.planes(bytes, p);
192                    y += self.h[p][i]
193                        * table_dot(&ub[i * rb..(i + 1) * rb], &tu[p * r * 4..(p + 1) * r * 4]);
194                }
195                unsafe { *op.0.add(i) = y };
196            }
197        };
198        dispatch(pool, rows, rows * r * np, &run);
199    }
200
201    /// Batched product: `out[b·rows] = xs[b·cols] · Wᵀ`.
202    pub fn matmat(&self, bytes: &[u8], xs: &[f32], b: usize, out: &mut [f32], pool: Option<&Pool>) {
203        let (rows, cols, r, np) = (self.rows, self.cols, self.rank, self.paths);
204        assert!(xs.len() >= b * cols && out.len() >= b * rows);
205        if b < BLR_GEMM_MIN || std::env::var("CMF_BLR_GEMM").as_deref() == Ok("0") {
206            for t in 0..b {
207                self.matvec(
208                    bytes,
209                    &xs[t * cols..(t + 1) * cols],
210                    &mut out[t * rows..(t + 1) * rows],
211                    pool,
212                );
213            }
214            return;
215        }
216        out[..b * rows].iter_mut().for_each(|v| *v = 0.0);
217        let mut vf = vec![0f32; r * cols];
218        let mut uf = vec![0f32; rows * r];
219        let mut zp = vec![0f32; b * cols];
220        let mut tp = vec![0f32; b * r];
221        let mut yp = vec![0f32; b * rows];
222        for p in 0..np {
223            let (ub, vb) = self.planes(bytes, p);
224            unpack_signs(vb, &mut vf, pool);
225            unpack_signs(ub, &mut uf, pool);
226            for t in 0..b {
227                for j in 0..cols {
228                    zp[t * cols + j] = xs[t * cols + j] * self.g[p][j];
229                }
230            }
231            // T = Z · Vᵀ  (b × r), V stored [r, cols]
232            par_sgemm_bt(b, r, cols, &zp, cols, &vf, cols, &mut tp, r, pool);
233            for t in 0..b {
234                for k in 0..r {
235                    tp[t * r + k] *= self.l[p][k];
236                }
237            }
238            // Y_p = T · Uᵀ (b × rows), U stored [rows, r]
239            par_sgemm_bt(b, rows, r, &tp, r, &uf, r, &mut yp, rows, pool);
240            for t in 0..b {
241                let o = &mut out[t * rows..(t + 1) * rows];
242                let y = &yp[t * rows..(t + 1) * rows];
243                for i in 0..rows {
244                    o[i] += y[i] * self.h[p][i];
245                }
246            }
247        }
248    }
249
250    /// One row of `W` (embedding-style lookup; rare path).
251    pub fn row_f32(&self, bytes: &[u8], i: usize, dst: &mut [f32]) {
252        let (cols, r) = (self.cols, self.rank);
253        dst[..cols].iter_mut().for_each(|v| *v = 0.0);
254        for p in 0..self.paths {
255            let (ub, vb) = self.planes(bytes, p);
256            for k in 0..r {
257                let su = (ub[i * (r / 8) + k / 8] >> (k % 8)) & 1 == 1;
258                let c = self.h[p][i] * if su { self.l[p][k] } else { -self.l[p][k] };
259                let row = &vb[k * (cols / 8)..(k + 1) * (cols / 8)];
260                for j in 0..cols {
261                    let s = (row[j / 8] >> (j % 8)) & 1 == 1;
262                    let v = self.g[p][j] * c;
263                    dst[j] += if s { v } else { -v };
264                }
265            }
266        }
267    }
268
269    /// One column of `W`: `out[i] += w · W[i, c]`.
270    pub fn add_col_scaled(&self, bytes: &[u8], c: usize, w: f32, out: &mut [f32]) {
271        let (cols, r) = (self.cols, self.rank);
272        for p in 0..self.paths {
273            let (ub, vb) = self.planes(bytes, p);
274            // t[k] = ℓ_k · V(k,c) · g_c · w
275            let t: Vec<f32> = (0..r)
276                .map(|k| {
277                    let s = (vb[k * (cols / 8) + c / 8] >> (c % 8)) & 1 == 1;
278                    let v = self.l[p][k] * self.g[p][c] * w;
279                    if s { v } else { -v }
280                })
281                .collect();
282            for i in 0..self.rows {
283                let mut y = 0f32;
284                for k in 0..r {
285                    let s = (ub[i * (r / 8) + k / 8] >> (k % 8)) & 1 == 1;
286                    y += if s { t[k] } else { -t[k] };
287                }
288                out[i] += self.h[p][i] * y;
289            }
290        }
291    }
292}
293
294fn dispatch(pool: Option<&Pool>, rows: usize, work: usize, run: &(dyn Fn(usize, usize) + Sync)) {
295    match pool {
296        Some(p) if rows >= 64 && work >= 65_536 => p.run_rows(rows, run),
297        _ => run(0, rows),
298    }
299}
300
301/// 16 signed partial sums per group of four inputs:
302/// `T[16q + c] = Σ_{t<4} (bit t of c ? +1 : −1) · z[4q + t]`.
303#[inline]
304pub fn build_table(z: &[f32], t: &mut [f32]) {
305    debug_assert_eq!(z.len() % 4, 0);
306    debug_assert_eq!(t.len(), z.len() * 4);
307    for q in 0..z.len() / 4 {
308        let zz = [z[4 * q], z[4 * q + 1], z[4 * q + 2], z[4 * q + 3]];
309        let tt = &mut t[16 * q..16 * q + 16];
310        tt[0] = -(zz[0] + zz[1] + zz[2] + zz[3]);
311        for c in 1..16usize {
312            tt[c] = tt[c & (c - 1)] + 2.0 * zz[c.trailing_zeros() as usize];
313        }
314    }
315}
316
317/// Exact ±1 dot of one packed row (n bits) with the vector behind `t`.
318#[inline]
319pub fn table_dot(row: &[u8], t: &[f32]) -> f32 {
320    // Four independent accumulators: the adds are a dependency chain otherwise.
321    let mut acc = [0f32; 4];
322    let n2 = row.len() / 2;
323    for i in 0..n2 {
324        let b0 = row[2 * i] as usize;
325        let b1 = row[2 * i + 1] as usize;
326        let base = 64 * i;
327        acc[0] += t[base + (b0 & 15)];
328        acc[1] += t[base + 16 + (b0 >> 4)];
329        acc[2] += t[base + 32 + (b1 & 15)];
330        acc[3] += t[base + 48 + (b1 >> 4)];
331    }
332    if row.len() % 2 == 1 {
333        let i = row.len() - 1;
334        let b0 = row[i] as usize;
335        acc[0] += t[32 * i + (b0 & 15)];
336        acc[1] += t[32 * i + 16 + (b0 >> 4)];
337    }
338    (acc[0] + acc[1]) + (acc[2] + acc[3])
339}
340
341/// Packed sign rows → ±1.0 f32 (same row-major order, 8 values per byte).
342fn unpack_signs(bits: &[u8], dst: &mut [f32], pool: Option<&Pool>) {
343    debug_assert_eq!(dst.len(), bits.len() * 8);
344    let n = bits.len();
345    let dp = SendPtr(dst.as_mut_ptr());
346    let run = |s: usize, e: usize| {
347        let dp = dp;
348        for i in s..e {
349            let b = bits[i];
350            for t in 0..8 {
351                unsafe { *dp.0.add(8 * i + t) = if (b >> t) & 1 == 1 { 1.0 } else { -1.0 } };
352            }
353        }
354    };
355    dispatch(pool, n, n * 8, &run);
356}
357
358/// `C[m, n] = A[m, k] · B[n, k]ᵀ` (row-major, B stored by rows of length k),
359/// split over the larger output dimension across the pool.
360#[allow(clippy::too_many_arguments)]
361pub fn par_sgemm_bt(
362    m: usize,
363    n: usize,
364    k: usize,
365    a: &[f32],
366    lda: usize,
367    bm: &[f32],
368    ldb: usize,
369    c: &mut [f32],
370    ldc: usize,
371    pool: Option<&Pool>,
372) {
373    assert!(a.len() >= (m - 1) * lda + k && bm.len() >= (n - 1) * ldb + k);
374    assert!(c.len() >= (m - 1) * ldc + n);
375    let cp = SendPtr(c.as_mut_ptr());
376    let workers = pool.map(|p| p.n_workers() + 1).unwrap_or(1);
377    let big = m * n * k >= 1 << 20;
378    // Chunks of at least 32 so every block is a real GEMM (each re-packs
379    // its operand panel).
380    let split_m = m >= n;
381    let d = if split_m { m } else { n };
382    let cs = d.div_ceil(workers).max(32);
383    let nch = d.div_ceil(cs);
384    let run = |s: usize, e: usize| {
385        let cp = cp;
386        for ch in s..e {
387            let (lo, hi) = (ch * cs, ((ch + 1) * cs).min(d));
388            if hi <= lo {
389                continue;
390            }
391            unsafe {
392                if split_m {
393                    gemm_block(
394                        hi - lo,
395                        n,
396                        k,
397                        a.as_ptr().add(lo * lda),
398                        lda,
399                        bm.as_ptr(),
400                        ldb,
401                        cp.0.add(lo * ldc),
402                        ldc,
403                    );
404                } else {
405                    gemm_block(
406                        m,
407                        hi - lo,
408                        k,
409                        a.as_ptr(),
410                        lda,
411                        bm.as_ptr().add(lo * ldb),
412                        ldb,
413                        cp.0.add(lo),
414                        ldc,
415                    );
416                }
417            }
418        }
419    };
420    match pool {
421        Some(p) if big && nch > 1 => p.run_rows(nch, &run),
422        _ => run(0, nch),
423    }
424}
425
426/// One GEMM block: `C[m, nb] = A[m, k] · B[nb, k]ᵀ`.
427#[allow(clippy::too_many_arguments)]
428unsafe fn gemm_block(
429    m: usize,
430    nb: usize,
431    k: usize,
432    a: *const f32,
433    lda: usize,
434    b: *const f32,
435    ldb: usize,
436    c: *mut f32,
437    ldc: usize,
438) {
439    unsafe {
440        matrixmultiply::sgemm(
441            m,
442            k,
443            nb,
444            1.0,
445            a,
446            lda as isize,
447            1,
448            b,
449            1,
450            ldb as isize,
451            0.0,
452            c,
453            ldc as isize,
454            1,
455        );
456    }
457}
458
459#[cfg(test)]
460pub(crate) mod tests {
461    use super::*;
462    use cortiq_core::blr::{BlrPathIn, dequant_blr, encode_blr};
463
464    pub(crate) struct Rng(pub u64);
465    impl Rng {
466        pub fn next(&mut self) -> u64 {
467            self.0 = self.0.wrapping_add(0x9E3779B97F4A7C15);
468            let mut z = self.0;
469            z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
470            z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
471            z ^ (z >> 31)
472        }
473        pub fn f(&mut self) -> f32 {
474            (self.next() >> 40) as f32 / (1u64 << 24) as f32 - 0.5
475        }
476    }
477
478    #[test]
479    fn table_dot_is_exact_sign_dot() {
480        let n = 40usize;
481        let z: Vec<f32> = (0..n)
482            .map(|i| ((i * 37 % 11) as f32 - 5.0) * 0.25)
483            .collect();
484        let bits: Vec<u8> = (0..n / 8).map(|i| (i * 89 + 13) as u8).collect();
485        let mut t = vec![0f32; 4 * n];
486        build_table(&z, &mut t);
487        let want: f32 = (0..n)
488            .map(|j| {
489                if (bits[j / 8] >> (j % 8)) & 1 == 1 {
490                    z[j]
491                } else {
492                    -z[j]
493                }
494            })
495            .sum();
496        assert!((table_dot(&bits, &t) - want).abs() < 1e-5);
497    }
498
499    /// A random `blr` payload (P paths of rank r) for kernel tests.
500    pub(crate) fn make(rng: &mut Rng, d_out: usize, d_in: usize, r: usize, np: usize) -> Vec<u8> {
501        let ps: Vec<_> = (0..np)
502            .map(|_| {
503                let h: Vec<f32> = (0..d_out).map(|_| 1.0 + rng.f()).collect();
504                let g: Vec<f32> = (0..d_in).map(|_| 1.0 + rng.f()).collect();
505                let l: Vec<f32> = (0..r).map(|_| 0.05 * (1.0 + rng.f())).collect();
506                let su: Vec<bool> = (0..d_out * r).map(|_| rng.next() & 1 == 1).collect();
507                let sv: Vec<bool> = (0..d_in * r).map(|_| rng.next() & 1 == 1).collect();
508                (h, g, l, su, sv)
509            })
510            .collect();
511        let ins: Vec<BlrPathIn> = ps
512            .iter()
513            .map(|(h, g, l, su, sv)| BlrPathIn { h, g, l, su, sv })
514            .collect();
515        encode_blr(d_out, d_in, r, &ins)
516    }
517
518    /// matvec and matmat (b = 1, 3, 4, 17) against dequant + dense, P = 1 and
519    /// P = 2, r = 8 edge, with and without the pool.
520    #[test]
521    fn kernel_parity_with_dense() {
522        let mut rng = Rng(11);
523        let pool = Pool::new(3);
524        for &(d_out, d_in, r, np) in &[
525            (64usize, 96usize, 8usize, 1usize),
526            (96, 64, 24, 2),
527            (256, 512, 40, 2),
528            (512, 256, 64, 1),
529        ] {
530            let bytes = make(&mut rng, d_out, d_in, r, np);
531            let core = BlrCore::parse(&[d_out, d_in], &bytes).unwrap();
532            let mut w = vec![0f32; d_out * d_in];
533            dequant_blr(&bytes, d_out, d_in, &mut w).unwrap();
534            for &b in &[1usize, 3, 4, 17] {
535                let xs: Vec<f32> = (0..b * d_in).map(|_| rng.f()).collect();
536                let mut want = vec![0f64; b * d_out];
537                for t in 0..b {
538                    for i in 0..d_out {
539                        let mut s = 0f64;
540                        for j in 0..d_in {
541                            s += w[i * d_in + j] as f64 * xs[t * d_in + j] as f64;
542                        }
543                        want[t * d_out + i] = s;
544                    }
545                }
546                let scale = want.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-30);
547                for pl in [None, Some(&pool)] {
548                    let mut got = vec![0f32; b * d_out];
549                    core.matmat(&bytes, &xs, b, &mut got, pl);
550                    let err = got
551                        .iter()
552                        .zip(&want)
553                        .map(|(g, w)| (*g as f64 - w).powi(2))
554                        .sum::<f64>()
555                        .sqrt();
556                    assert!(
557                        err / scale < 1e-5,
558                        "{d_out}x{d_in} r{r} P{np} b{b}: rel {}",
559                        err / scale
560                    );
561                    if b == 1 {
562                        let mut mv = vec![0f32; d_out];
563                        core.matvec(&bytes, &xs, &mut mv, pl);
564                        let e = mv
565                            .iter()
566                            .zip(&want)
567                            .map(|(g, w)| (*g as f64 - w).powi(2))
568                            .sum::<f64>()
569                            .sqrt();
570                        assert!(e / scale < 1e-5);
571                    }
572                }
573            }
574            // rare paths: row and column reconstruction
575            let mut row = vec![0f32; d_in];
576            core.row_f32(&bytes, d_out / 2, &mut row);
577            for j in 0..d_in {
578                assert!((row[j] - w[(d_out / 2) * d_in + j]).abs() < 1e-4);
579            }
580            let mut col = vec![0f32; d_out];
581            core.add_col_scaled(&bytes, d_in / 3, 2.0, &mut col);
582            for i in 0..d_out {
583                assert!((col[i] - 2.0 * w[i * d_in + d_in / 3]).abs() < 2e-4);
584            }
585        }
586    }
587}