1use kime_tensor::Epilogue;
16
17use crate::ops::gelu;
18use crate::par::Shared;
19
20const KG: usize = 8;
22const TILE: usize = 8;
24const MB: usize = 64;
26
27#[derive(Debug, Clone, Default, PartialEq)]
29pub struct QMatrix {
30 pub rows: usize,
32 pub k: usize,
34 pub q: Vec<i8>,
36 pub scale: Vec<f32>,
38}
39
40impl QMatrix {
41 #[must_use]
47 pub fn quantize(x: &[f32], rows: usize, k: usize) -> Self {
48 assert_eq!(x.len(), rows * k, "x is not [rows, k]");
49 let mut q = vec![0; tiled_len(rows, k)];
50 let mut scale = vec![0.0; rows];
51 for (r, row) in x.chunks_exact(k.max(1)).take(rows).enumerate() {
52 scale[r] = quantize_row(row, r, &mut q);
53 }
54 Self { rows, k, q, scale }
55 }
56
57 #[must_use]
59 pub fn dequantize(&self) -> Vec<f32> {
60 let mut out = vec![0.0; self.rows * self.k];
61 for r in 0..self.rows {
62 for p in 0..self.k {
63 out[r * self.k + p] = f32::from(self.q[at(r, p, self.k)]) * self.scale[r];
64 }
65 }
66 out
67 }
68}
69
70#[must_use]
72pub fn tiled_len(rows: usize, k: usize) -> usize {
73 rows.div_ceil(TILE) * TILE * k.div_ceil(KG) * KG
74}
75
76#[inline(always)]
78fn at(r: usize, p: usize, k: usize) -> usize {
79 let (tile, pair, half) = (r / TILE, r % TILE / 2, r % 2);
80 tile * k.div_ceil(KG) * TILE * KG + p / KG * TILE * KG + pair * 2 * KG + half * KG + p % KG
81}
82
83fn quantize_row(row: &[f32], r: usize, q: &mut [i8]) -> f32 {
85 let k = row.len();
86 let amax = row.iter().fold(0f32, |m, v| m.max(v.abs()));
87 if amax == 0.0 || !amax.is_finite() {
88 for p in 0..k {
89 q[at(r, p, k)] = 0;
90 }
91 return if amax == 0.0 { 0.0 } else { f32::NAN };
92 }
93 let inv = 127.0 / amax;
94 let base = at(r, 0, k);
95 for (g, chunk) in row.chunks(KG).enumerate() {
96 let o = base + g * TILE * KG;
97 for (e, &v) in chunk.iter().enumerate() {
98 q[o + e] = (v * inv).round() as i8;
100 }
101 }
102 amax / 127.0
103}
104
105#[must_use]
107pub fn scratch_len(k: usize) -> usize {
108 tiled_len(MB, k).div_ceil(4) + MB
109}
110
111#[derive(Debug, Clone, Copy)]
114pub struct QGemm<'a> {
115 pub x: &'a [f32],
117 pub m: usize,
119 pub w: &'a QMatrix,
121 pub b: Option<&'a [f32]>,
123 pub ep: Epilogue,
125}
126
127impl QGemm<'_> {
128 pub fn run(
136 &self,
137 y: &mut [f32],
138 threads: usize,
139 spawn: impl FnOnce(usize, &(dyn Fn(usize, &mut [f32]) + Sync)),
140 ) {
141 let Self { x, m, w, b, ep } = *self;
142 let (k, n) = (w.k, w.rows);
143 assert_eq!(x.len(), m * k, "x is not [m, k]");
144 assert_eq!(y.len(), m * n, "y is not [m, n]");
145 if let Some(b) = b {
146 assert_eq!(b.len(), n, "b is not [n]");
147 }
148 if m == 0 || n == 0 {
149 return;
150 }
151 let y = Shared::new(y);
152 let mt = m.div_ceil(MB);
153 let tiles = n.div_ceil(TILE);
154 let per = tiles.div_ceil((3 * threads.max(1)).div_ceil(mt)).max(1);
156 let nt = tiles.div_ceil(per);
157 let kernel = pick();
158 spawn(mt * nt, &|t, scratch| {
159 let (bi, bj) = (t % mt, t / mt);
160 let (r0, rows) = (bi * MB, MB.min(m - bi * MB));
161 let (scale, bytes) = scratch[..scratch_len(k)].split_at_mut(MB);
162 let (_, bytes, _) = unsafe { bytes.align_to_mut::<i8>() };
164 let xq = &mut bytes[..tiled_len(MB, k)];
165 for r in 0..rows {
166 scale[r] = quantize_row(&x[(r0 + r) * k..(r0 + r + 1) * k], r, xq);
167 }
168 let tile_bytes = k.div_ceil(KG) * TILE * KG;
172 for tj in bj * per..((bj + 1) * per).min(tiles) {
173 let wt = &w.q[tj * tile_bytes..(tj + 1) * tile_bytes];
174 for ti in 0..rows.div_ceil(TILE) {
175 let xt = &xq[ti * tile_bytes..(ti + 1) * tile_bytes];
176 let sums = kernel(xt, wt);
177 for (i, row) in sums.iter().enumerate().take(rows - ti * TILE) {
178 let (r, j0) = (ti * TILE + i, tj * TILE);
179 for (jj, &s) in row.iter().enumerate().take(n - j0) {
180 let j = j0 + jj;
181 let v = s as f32 * (scale[r] * w.scale[j]);
182 let v = match b {
183 Some(b) => v + b[j],
184 None => v,
185 };
186 let at = (r0 + r) * n + j;
187 unsafe {
189 y.set(
190 at,
191 match ep {
192 Epilogue::None => v,
193 Epilogue::Gelu => gelu(v),
194 Epilogue::Relu => v.max(0.0),
195 Epilogue::Accumulate => y.get(at) + v,
196 },
197 );
198 }
199 }
200 }
201 }
202 }
203 });
204 }
205}
206
207type Kernel = fn(&[i8], &[i8]) -> [[i32; TILE]; TILE];
209
210fn pick() -> Kernel {
211 #[cfg(target_arch = "aarch64")]
212 if std::arch::is_aarch64_feature_detected!("i8mm") {
213 return |x, w| {
214 unsafe { i8mm::tile(x, w) }
216 };
217 }
218 tile_scalar
219}
220
221fn tile_scalar(x: &[i8], w: &[i8]) -> [[i32; TILE]; TILE] {
223 assert_eq!(x.len(), w.len());
224 let mut out = [[0i32; TILE]; TILE];
225 for (xg, wg) in x.as_chunks::<{ TILE * KG }>().0.iter().zip(w.as_chunks::<{ TILE * KG }>().0) {
226 for (i, row) in out.iter_mut().enumerate() {
227 let xr = &xg[i / 2 * 2 * KG + i % 2 * KG..][..KG];
228 for (j, o) in row.iter_mut().enumerate() {
229 let wr = &wg[j / 2 * 2 * KG + j % 2 * KG..][..KG];
230 *o += xr.iter().zip(wr).map(|(&a, &b)| i32::from(a) * i32::from(b)).sum::<i32>();
231 }
232 }
233 }
234 out
235}
236
237#[cfg(target_arch = "aarch64")]
238mod i8mm {
239 use std::arch::aarch64::{int8x16_t, int32x4_t, vdupq_n_s32, vld1q_s8, vst1q_s32};
240 use std::arch::asm;
241
242 use super::{KG, TILE};
243
244 #[inline(always)]
247 fn mmla(acc: int32x4_t, a: int8x16_t, b: int8x16_t) -> int32x4_t {
248 let mut acc = acc;
249 unsafe {
251 asm!(
252 "smmla {d:v}.4s, {a:v}.16b, {b:v}.16b",
253 d = inout(vreg) acc,
254 a = in(vreg) a,
255 b = in(vreg) b,
256 options(pure, nomem, nostack),
257 );
258 }
259 acc
260 }
261
262 #[target_feature(enable = "neon,i8mm")]
266 pub(super) unsafe fn tile(x: &[i8], w: &[i8]) -> [[i32; TILE]; TILE] {
267 assert!(x.len() == w.len() && x.len().is_multiple_of(TILE * KG));
268 let mut acc = [[vdupq_n_s32(0); 4]; 4];
269 for (xg, wg) in
270 x.as_chunks::<{ TILE * KG }>().0.iter().zip(w.as_chunks::<{ TILE * KG }>().0)
271 {
272 let wv = unsafe { [0, 1, 2, 3].map(|b| vld1q_s8(wg.as_ptr().add(16 * b))) };
274 for (a, acc) in acc.iter_mut().enumerate() {
275 let xa = unsafe { vld1q_s8(xg.as_ptr().add(16 * a)) };
277 for (acc, &wb) in acc.iter_mut().zip(&wv) {
278 *acc = mmla(*acc, xa, wb);
279 }
280 }
281 }
282 let mut out = [[0i32; TILE]; TILE];
283 for (a, acc) in acc.iter().enumerate() {
284 for (b, &v) in acc.iter().enumerate() {
285 let mut s = [0i32; 4];
286 unsafe { vst1q_s32(s.as_mut_ptr(), v) };
288 out[2 * a][2 * b] = s[0];
289 out[2 * a][2 * b + 1] = s[1];
290 out[2 * a + 1][2 * b] = s[2];
291 out[2 * a + 1][2 * b + 1] = s[3];
292 }
293 }
294 out
295 }
296}
297
298#[allow(clippy::too_many_arguments)]
304pub fn linear(
305 x: &[f32],
306 m: usize,
307 k: usize,
308 w: &[f32],
309 n: usize,
310 b: Option<&[f32]>,
311 y: &mut [f32],
312 threads: usize,
313) {
314 let w = QMatrix::quantize(w, n, k);
315 let g = QGemm { x, m, w: &w, b, ep: Epilogue::None };
316 let len = scratch_len(k);
317 g.run(y, threads, |tasks, f| {
318 crate::par::for_each(tasks, threads, |t| f(t, &mut vec![0.0; len]));
319 });
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::testing::Rng;
326
327 #[test]
328 fn quantize_round_trips_within_half_a_step() {
329 let mut rng = Rng(3);
330 for (rows, k) in [(1, 1), (3, 7), (9, 17), (16, 64), (5, 0)] {
331 let x = rng.vec(rows * k);
332 let q = QMatrix::quantize(&x, rows, k);
333 let back = q.dequantize();
334 for r in 0..rows {
335 for p in 0..k {
336 let (a, b) = (x[r * k + p], back[r * k + p]);
337 assert!((a - b).abs() <= q.scale[r] * 0.5 + 1e-7, "{rows}x{k} {r},{p}");
338 }
339 }
340 }
341 let q = QMatrix::quantize(&[0.0, 0.0, 2.0, -1.0], 2, 2);
342 assert_eq!((q.scale[0], q.q[at(1, 0, 2)], q.q[at(1, 1, 2)]), (0.0, 127, -64));
343 }
344
345 fn want(x: &QMatrix, w: &QMatrix, b: Option<&[f32]>) -> Vec<f32> {
347 let k = x.k;
348 let mut y = vec![0.0; x.rows * w.rows];
349 for i in 0..x.rows {
350 for j in 0..w.rows {
351 let s: i64 =
352 (0..k).map(|p| i64::from(x.q[at(i, p, k)]) * i64::from(w.q[at(j, p, k)])).sum();
353 let v = s as f32 * (x.scale[i] * w.scale[j]);
354 y[i * w.rows + j] = b.map_or(v, |b| v + b[j]);
355 }
356 }
357 y
358 }
359
360 #[test]
361 fn exact_on_awkward_shapes() {
362 let mut rng = Rng(7);
363 for (m, k, n) in
364 [(1, 1, 1), (3, 5, 2), (9, 17, 9), (64, 64, 64), (70, 300, 130), (130, 8, 7)]
365 {
366 let (x, w, b) = (rng.vec(m * k), rng.vec(n * k), rng.vec(n));
367 let (xq, wq) = (QMatrix::quantize(&x, m, k), QMatrix::quantize(&w, n, k));
368 for bias in [None, Some(&b[..])] {
369 let want = want(&xq, &wq, bias);
370 for threads in [1, 3, 16] {
371 let mut y = vec![f32::NAN; m * n];
372 linear(&x, m, k, &w, n, bias, &mut y, threads);
373 assert!(
374 y.iter().zip(&want).all(|(a, b)| a.to_bits() == b.to_bits()),
375 "{m}x{k}x{n} on {threads}"
376 );
377 }
378 }
379 }
380 }
381
382 #[test]
383 fn kernels_agree() {
384 let mut rng = Rng(9);
385 for k in [8, 64, 1024] {
386 let (x, w) = (rng.vec(TILE * k), rng.vec(TILE * k));
387 let (x, w) = (QMatrix::quantize(&x, TILE, k), QMatrix::quantize(&w, TILE, k));
388 assert_eq!(pick()(&x.q, &w.q), tile_scalar(&x.q, &w.q), "k {k}");
389 }
390 }
391
392 #[test]
393 fn close_to_f32() {
394 let mut rng = Rng(11);
395 let (m, k, n) = (20, 1024, 96);
396 let (x, w) = (rng.vec(m * k), rng.vec(n * k));
397 let mut y = vec![0.0; m * n];
398 linear(&x, m, k, &w, n, None, &mut y, 4);
399 let mut exact = vec![0.0; m * n];
400 crate::gemm::linear(&x, m, k, &w, n, None, &mut exact, 4);
401 let spread = (k as f32 / 9.0).sqrt();
404 let err = y.iter().zip(&exact).fold(0f32, |e, (a, b)| e.max((a - b).abs()));
405 assert!(err < 0.05 * spread, "error {err} against a spread of {spread}");
406 }
407
408 #[test]
409 fn epilogues() {
410 let mut rng = Rng(5);
411 let (m, k, n) = (70, 40, 90);
412 let (x, w, b, y0) = (rng.vec(m * k), rng.vec(n * k), rng.vec(n), rng.vec(m * n));
413 let (xq, wq) = (QMatrix::quantize(&x, m, k), QMatrix::quantize(&w, n, k));
414 let lin = want(&xq, &wq, Some(&b));
415 for ep in [Epilogue::None, Epilogue::Gelu, Epilogue::Relu, Epilogue::Accumulate] {
416 let mut y = y0.clone();
417 let g = QGemm { x: &x, m, w: &wq, b: Some(&b), ep };
418 g.run(&mut y, 4, |tasks, f| {
419 crate::par::for_each(tasks, 4, |t| f(t, &mut vec![0.0; scratch_len(k)]));
420 });
421 for (i, (&got, (&v, &y))) in y.iter().zip(lin.iter().zip(&y0)).enumerate() {
422 let want = match ep {
423 Epilogue::None => v,
424 Epilogue::Gelu => gelu(v),
425 Epilogue::Relu => v.max(0.0),
426 Epilogue::Accumulate => y + v,
427 };
428 assert_eq!(got.to_bits(), want.to_bits(), "{ep:?} at {i}");
429 }
430 }
431 }
432}