1use crate::pool::Pool;
15use cortiq_core::CmfModel;
16use cortiq_core::blr::{BlrHeader, blr_f16s, blr_path_offsets, validate_blr};
17use std::sync::Arc;
18
19pub const BLR_GEMM_MIN: usize = 4;
21
22pub 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
29pub 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 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
86pub struct BlrCore {
88 pub rows: usize,
89 pub cols: usize,
90 pub paths: usize,
91 pub rank: usize,
92 pub h: Vec<Vec<f32>>,
94 pub g: Vec<Vec<f32>>,
95 pub l: Vec<Vec<f32>>,
96 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 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 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 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 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 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 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 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 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 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 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#[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#[inline]
319pub fn table_dot(row: &[u8], t: &[f32]) -> f32 {
320 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
341fn 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#[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 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#[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 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 #[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 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}