1use crate::pool::{matvec_rows, matvec_rows2, Pool};
15use cortiq_core::quant::{f16_to_f32, GROUP_SIZE};
16use cortiq_core::{CmfModel, TensorDtype};
17use std::sync::Arc;
18
19pub enum QTensor {
20 F32 {
21 data: Vec<f32>,
22 rows: usize,
23 cols: usize,
24 },
25 Mapped {
26 model: Arc<CmfModel>,
27 idx: usize,
29 dtype: TensorDtype,
30 rows: usize,
31 cols: usize,
32 row_scale: Vec<f32>,
34 col_field: Vec<f32>,
36 },
37}
38
39impl QTensor {
40 pub fn from_f32(data: Vec<f32>, rows: usize, cols: usize) -> Self {
41 debug_assert_eq!(data.len(), rows * cols);
42 Self::F32 { data, rows, cols }
43 }
44
45 pub fn from_model(model: &Arc<CmfModel>, name: &str) -> Result<Self, String> {
48 let idx = model
49 .tensors
50 .iter()
51 .position(|t| t.name == name)
52 .ok_or_else(|| format!("tensor '{name}' not found in CMF directory"))?;
53 let entry = &model.tensors[idx];
54 if entry.shape.len() != 2 {
55 return Err(format!("QTensor::from_model needs 2-D, got '{name}'"));
56 }
57 let (rows, cols) = (entry.shape[0], entry.shape[1]);
58 let bytes = model.entry_bytes(entry);
59
60 match entry.dtype {
61 TensorDtype::Q8Row | TensorDtype::Q8_2f => {
62 let n = rows * cols;
63 let scales_off = n;
64 let row_scale: Vec<f32> = (0..rows)
65 .map(|o| {
66 f16_to_f32(u16::from_le_bytes([
67 bytes[scales_off + o * 2],
68 bytes[scales_off + o * 2 + 1],
69 ]))
70 })
71 .collect();
72 let col_field: Vec<f32> = if entry.dtype == TensorDtype::Q8_2f {
73 let col_off = n + rows * 2;
74 (0..cols)
75 .map(|i| {
76 f16_to_f32(u16::from_le_bytes([
77 bytes[col_off + i * 2],
78 bytes[col_off + i * 2 + 1],
79 ]))
80 })
81 .collect()
82 } else {
83 Vec::new()
84 };
85 Ok(Self::Mapped {
86 model: model.clone(),
87 idx,
88 dtype: entry.dtype,
89 rows,
90 cols,
91 row_scale,
92 col_field,
93 })
94 }
95 TensorDtype::Vbit if cols % GROUP_SIZE == 0 => Ok(Self::Mapped {
97 model: model.clone(),
98 idx,
99 dtype: entry.dtype,
100 rows,
101 cols,
102 row_scale: Vec::new(),
103 col_field: Vec::new(),
104 }),
105 TensorDtype::Q4Block if cols % GROUP_SIZE == 0 => Ok(Self::Mapped {
108 model: model.clone(),
109 idx,
110 dtype: entry.dtype,
111 rows,
112 cols,
113 row_scale: Vec::new(),
114 col_field: Vec::new(),
115 }),
116 _ => {
118 let mut data = vec![0.0f32; rows * cols];
119 cortiq_core::quant::dequant_tensor(entry, bytes, &mut data)?;
120 Ok(Self::from_f32(data, rows, cols))
121 }
122 }
123 }
124
125 pub fn rows(&self) -> usize {
126 match self {
127 Self::F32 { rows, .. } | Self::Mapped { rows, .. } => *rows,
128 }
129 }
130
131 pub fn cols(&self) -> usize {
132 match self {
133 Self::F32 { cols, .. } | Self::Mapped { cols, .. } => *cols,
134 }
135 }
136
137 pub fn as_f32(&self) -> Option<&[f32]> {
140 match self {
141 Self::F32 { data, .. } => Some(data),
142 Self::Mapped { .. } => None,
143 }
144 }
145
146 fn quant_bytes(&self) -> &[u8] {
147 match self {
148 Self::Mapped { model, idx, .. } => model.entry_bytes(&model.tensors[*idx]),
149 Self::F32 { .. } => unreachable!("quant_bytes on F32"),
150 }
151 }
152
153 pub fn row_f32(&self, r: usize, dst: &mut [f32]) {
155 let cols = self.cols();
156 debug_assert_eq!(dst.len(), cols);
157 match self {
158 Self::F32 { data, .. } => dst.copy_from_slice(&data[r * cols..(r + 1) * cols]),
159 Self::Mapped {
160 dtype,
161 row_scale,
162 col_field,
163 ..
164 } => {
165 if *dtype == TensorDtype::Q4Block {
166 let (packed, scales) = q4_split(self.quant_bytes(), self.rows(), cols);
167 let gpr = cols / GROUP_SIZE;
168 for gi in 0..gpr {
169 let g = r * gpr + gi;
170 let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
171 for (k, &b) in packed[g * 16..(g + 1) * 16].iter().enumerate() {
172 dst[gi * GROUP_SIZE + k * 2] = ((b & 0x0F) as f32 - 8.0) * s;
173 dst[gi * GROUP_SIZE + k * 2 + 1] = (((b >> 4) & 0x0F) as f32 - 8.0) * s;
174 }
175 }
176 return;
177 }
178 if *dtype == TensorDtype::Vbit {
179 let bytes = self.quant_bytes();
180 let rows = self.rows();
181 let ng = cols / GROUP_SIZE;
182 let bits = &bytes[..rows];
183 let sc_off = rows;
184 let mut off = sc_off + rows * ng * 2;
185 for rr in 0..r {
186 off += (cols * bits[rr] as usize + 7) / 8;
187 }
188 let b = bits[r] as usize;
189 let l = ((1usize << (b - 1)) - 1) as f32;
190 let data = &bytes[off..];
191 let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
192 for (i, d) in dst.iter_mut().enumerate() {
193 while nbits < b {
194 acc = (acc << 8) | data[idx] as u64;
195 idx += 1;
196 nbits += 8;
197 }
198 let u = ((acc >> (nbits - b)) & ((1u64 << b) - 1)) as f32;
199 nbits -= b;
200 let so = (r * ng + i / GROUP_SIZE) * 2;
201 let sv = f16_to_f32(u16::from_le_bytes([
202 bytes[sc_off + so],
203 bytes[sc_off + so + 1],
204 ]));
205 *d = (u - l) * sv;
206 }
207 return;
208 }
209 let q = &self.quant_bytes()[r * cols..(r + 1) * cols];
210 let s = row_scale[r];
211 match dtype {
212 TensorDtype::Q8Row => {
213 for (d, &b) in dst.iter_mut().zip(q) {
214 *d = (b as i8) as f32 * s;
215 }
216 }
217 TensorDtype::Q8_2f => {
218 for (i, (d, &b)) in dst.iter_mut().zip(q).enumerate() {
219 *d = (b as i8) as f32 * s * col_field[i];
220 }
221 }
222 _ => unreachable!(),
223 }
224 }
225 }
226 }
227
228 pub fn sparse_col_ok(&self) -> bool {
233 match self {
234 Self::F32 { .. } => true,
235 Self::Mapped { dtype, .. } => {
236 matches!(dtype, TensorDtype::Q8Row | TensorDtype::Q8_2f)
237 }
238 }
239 }
240
241 pub fn add_col_scaled(&self, c: usize, w: f32, out: &mut [f32]) {
245 let inter = self.cols();
246 let hidden = self.rows();
247 debug_assert_eq!(out.len(), hidden);
248 match self {
249 Self::F32 { data, .. } => {
250 for (k, o) in out.iter_mut().enumerate() {
251 *o += w * data[k * inter + c];
252 }
253 }
254 Self::Mapped {
255 dtype, row_scale, col_field, ..
256 } => {
257 let q = self.quant_bytes();
258 let colf = if *dtype == TensorDtype::Q8_2f {
259 col_field[c]
260 } else {
261 1.0
262 };
263 let wc = w * colf;
264 for (k, o) in out.iter_mut().enumerate() {
265 let b = q[k * inter + c] as i8 as f32;
266 *o += wc * b * row_scale[k];
267 }
268 }
269 }
270 }
271
272 pub fn row_dot(&self, r: usize, x: &[f32], scratch: &mut [f32]) -> f32 {
276 let cols = self.cols();
277 match self {
278 Self::F32 { data, .. } => {
279 let row = &data[r * cols..(r + 1) * cols];
280 row.iter().zip(x).map(|(w, v)| w * v).sum()
281 }
282 Self::Mapped { dtype, row_scale, col_field, .. } => match dtype {
283 TensorDtype::Q8Row => {
284 let q = &self.quant_bytes()[r * cols..(r + 1) * cols];
285 dot_i8_f32(q, x) * row_scale[r]
286 }
287 TensorDtype::Q8_2f => {
288 let q = &self.quant_bytes()[r * cols..(r + 1) * cols];
289 dot_i8_col_f32(q, x, col_field) * row_scale[r]
290 }
291 _ => {
292 self.row_f32(r, scratch);
293 scratch.iter().zip(x).map(|(w, v)| w * v).sum()
294 }
295 },
296 }
297 }
298
299 pub fn matvec(&self, x: &[f32], out: &mut [f32], pool: Option<&Pool>) {
302 match self {
303 Self::F32 { data, .. } => matvec_rows(pool, data, x, out),
304 Self::Mapped {
305 model,
306 idx,
307 dtype,
308 rows,
309 cols,
310 row_scale,
311 col_field,
312 } => {
313 let _ = (model, idx);
314 if *dtype == TensorDtype::Q4Block {
315 q4matvec(self.quant_bytes(), x, *rows, *cols, out, pool);
316 return;
317 }
318 if *dtype == TensorDtype::Vbit {
319 vbitmatvec(self.quant_bytes(), x, *rows, *cols, out, pool);
320 return;
321 }
322 let xs = prescale(x, col_field, *dtype);
323 if *rows >= crate::gpu::min_rows()
328 && matches!(dtype, TensorDtype::Q8Row | TensorDtype::Q8_2f)
329 && std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0").unwrap_or(true)
330 && crate::gpu::enabled_here()
331 {
332 let frac = std::env::var("CMF_GPU_SPLIT")
333 .ok()
334 .and_then(|v| v.parse::<f32>().ok())
335 .unwrap_or(0.5)
336 .clamp(0.0, 1.0);
337 let cpu_rows = ((*rows as f32) * (1.0 - frac)) as usize;
338 let (out_cpu, out_gpu) = out.split_at_mut(cpu_rows);
339 let bytes = self.quant_bytes();
340 let ok = std::thread::scope(|sc| {
341 let g = sc.spawn(|| {
342 crate::gpu::q8_matvec_range(
343 model,
344 *idx,
345 cpu_rows,
346 &row_scale[cpu_rows..],
347 &xs,
348 *rows - cpu_rows,
349 *cols,
350 out_gpu,
351 )
352 });
353 if cpu_rows > 0 {
354 qmatvec(
355 &bytes[..cpu_rows * *cols],
356 &row_scale[..cpu_rows],
357 &xs,
358 cpu_rows,
359 *cols,
360 out_cpu,
361 pool,
362 );
363 }
364 g.join().unwrap_or(false)
365 });
366 if ok {
367 return;
368 }
369 qmatvec(
371 &bytes[cpu_rows * *cols..(*rows) * *cols],
372 &row_scale[cpu_rows..],
373 &xs,
374 *rows - cpu_rows,
375 *cols,
376 out_gpu,
377 pool,
378 );
379 return;
380 }
381 qmatvec(self.quant_bytes(), row_scale, &xs, *rows, *cols, out, pool);
382 }
383 }
384 }
385
386 pub fn matvec2(&self, x1: &[f32], x2: &[f32], o1: &mut [f32], o2: &mut [f32], pool: Option<&Pool>) {
388 match self {
389 Self::F32 { data, .. } => matvec_rows2(pool, data, x1, x2, o1, o2),
390 Self::Mapped {
391 dtype,
392 rows,
393 cols,
394 row_scale,
395 col_field,
396 ..
397 } => {
398 if *dtype == TensorDtype::Q4Block {
399 q4matvec(self.quant_bytes(), x1, *rows, *cols, o1, pool);
400 q4matvec(self.quant_bytes(), x2, *rows, *cols, o2, pool);
401 return;
402 }
403 if *dtype == TensorDtype::Vbit {
404 vbitmatvec(self.quant_bytes(), x1, *rows, *cols, o1, pool);
405 vbitmatvec(self.quant_bytes(), x2, *rows, *cols, o2, pool);
406 return;
407 }
408 let x1s = prescale(x1, col_field, *dtype);
409 let x2s = prescale(x2, col_field, *dtype);
410 qmatvec2(self.quant_bytes(), row_scale, &x1s, &x2s, *rows, *cols, o1, o2, pool);
411 }
412 }
413 }
414}
415
416impl QTensor {
417 pub fn matmat(&self, xs_all: &[f32], b: usize, out: &mut [f32], pool: Option<&Pool>) {
422 let cols = self.cols();
423 let rows = self.rows();
424 debug_assert_eq!(xs_all.len(), b * cols);
425 debug_assert_eq!(out.len(), b * rows);
426 match self {
427 Self::F32 { data, .. } => {
428 let out_addr = SendMut(out.as_mut_ptr());
429 let run = |start: usize, end: usize| {
430 for o in start..end {
431 let row = &data[o * cols..(o + 1) * cols];
432 for bi in 0..b {
433 let x = &xs_all[bi * cols..(bi + 1) * cols];
434 let mut acc = 0f32;
435 for j in 0..cols {
436 acc += row[j] * x[j];
437 }
438 unsafe { *out_addr.at(bi * rows + o) = acc };
439 }
440 }
441 };
442 dispatch_rows(pool, rows, &run);
443 }
444 Self::Mapped {
445 dtype,
446 row_scale,
447 col_field,
448 ..
449 } => {
450 if matches!(dtype, TensorDtype::Q4Block | TensorDtype::Vbit) {
451 for bi in 0..b {
453 let x = &xs_all[bi * cols..(bi + 1) * cols];
454 let (head, tail) = out[bi * rows..].split_at_mut(rows);
455 let _ = tail;
456 self.matvec(x, head, pool);
457 }
458 return;
459 }
460 let pre: Vec<std::borrow::Cow<'_, [f32]>> = (0..b)
461 .map(|bi| {
462 prescale(&xs_all[bi * cols..(bi + 1) * cols], col_field, *dtype)
463 })
464 .collect();
465 if b >= 8
468 && b * rows * cols >= 128_000_000
469 && crate::gpu::enabled_here()
470 {
471 if let Self::Mapped { model, idx, .. } = self {
472 let flat: Vec<f32> =
473 pre.iter().flat_map(|v| v.iter().copied()).collect();
474 if crate::gpu::q8_matmat(
475 model, *idx, row_scale, &flat, b, rows, cols, out)
476 {
477 return;
478 }
479 }
480 }
481 let q = self.quant_bytes();
482 qmatmat(q, row_scale, &pre, rows, cols, out, pool);
483 }
484 }
485 }
486}
487
488fn qmatmat(
491 q: &[u8],
492 row_scale: &[f32],
493 pre: &[std::borrow::Cow<'_, [f32]>],
494 rows: usize,
495 cols: usize,
496 out: &mut [f32],
497 pool: Option<&Pool>,
498) {
499 let b = pre.len();
500 debug_assert_eq!(out.len(), b * rows);
501 #[cfg(target_arch = "aarch64")]
502 if sdot_enabled() {
503 let acts: Vec<SplitAct> = pre.iter().map(|x| split_act(x)).collect();
504 let out_addr = SendMut(out.as_mut_ptr());
505 let run = |start: usize, end: usize| {
506 for o in start..end {
507 let row = &q[o * cols..(o + 1) * cols];
508 for (bi, act) in acts.iter().enumerate() {
509 let v = row_dot_sdot(row, act) * row_scale[o];
510 unsafe { *out_addr.at(bi * rows + o) = v };
511 }
512 }
513 };
514 dispatch_rows(pool, rows, &run);
515 return;
516 }
517 let out_addr = SendMut(out.as_mut_ptr());
518 let run = |start: usize, end: usize| {
519 for o in start..end {
520 let row = &q[o * cols..(o + 1) * cols];
521 for (bi, x) in pre.iter().enumerate() {
522 let mut acc = 0f32;
523 for j in 0..cols {
524 acc += (row[j] as i8) as f32 * x[j];
525 }
526 unsafe { *out_addr.at(bi * rows + o) = acc * row_scale[o] };
527 }
528 }
529 };
530 dispatch_rows(pool, rows, &run);
531}
532
533fn dispatch_rows(pool: Option<&Pool>, rows: usize, run: &(dyn Fn(usize, usize) + Sync)) {
536 match pool {
537 Some(pool) if rows >= 256 => pool.run_rows(rows, run),
538 _ => run(0, rows),
539 }
540}
541
542fn q4_split(bytes: &[u8], rows: usize, cols: usize) -> (&[u8], &[u8]) {
544 let groups = rows * cols / GROUP_SIZE;
545 bytes.split_at(groups * 16)
546}
547
548#[inline(always)]
553fn unpack8<const B: usize>(data: &[u8]) -> [i32; 8] {
554 let mut acc = 0u64;
555 for i in 0..B {
556 acc = (acc << 8) | data[i] as u64;
557 }
558 let mask = (1u64 << B) - 1;
559 let mut out = [0i32; 8];
560 for (k, o) in out.iter_mut().enumerate() {
561 *o = ((acc >> ((7 - k) * B)) & mask) as i32;
562 }
563 out
564}
565
566fn vbitmatvec(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
571 debug_assert_eq!(out.len(), rows);
572 let ng = cols / GROUP_SIZE;
573 let bits = &bytes[..rows];
574 let sc_off = rows;
575 let data_off = sc_off + rows * ng * 2;
576 let mut offsets = Vec::with_capacity(rows + 1);
577 let mut off = data_off;
578 for r in 0..rows {
579 offsets.push(off);
580 off += (cols * bits[r] as usize + 7) / 8;
581 }
582 offsets.push(off);
583
584 let offsets = &offsets;
585
586 #[cfg(target_arch = "aarch64")]
590 if sdot_enabled() {
591 let act = split_act(x);
592 let act = &act;
593 let row_dot = move |r: usize| -> f32 {
594 let b = bits[r] as usize;
595 let l = ((1i32 << (b - 1)) - 1) as i32;
596 let mask = (1u64 << b) - 1;
597 let data = &bytes[offsets[r]..offsets[r + 1]];
598 if b == 8 {
599 let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
601 let mut dot = 0f32;
602 for g in 0..ng {
603 let so = (r * ng + g) * 2;
604 let sgf = f16_to_f32(u16::from_le_bytes([
605 bytes[sc_off + so],
606 bytes[sc_off + so + 1],
607 ]));
608 let xg = &x[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
609 let mut gd = 0f32;
610 for &xv in xg.iter() {
611 if nbits < 8 {
612 acc = (acc << 8) | data[idx] as u64;
613 idx += 1;
614 nbits += 8;
615 }
616 let u = ((acc >> (nbits - 8)) & 0xFF) as i32;
617 nbits -= 8;
618 gd += (u - l) as f32 * xv;
619 }
620 dot += gd * sgf;
621 }
622 return dot;
623 }
624 thread_local! {
628 static VBIT_SCRATCH: std::cell::RefCell<Vec<u8>> =
629 const { std::cell::RefCell::new(Vec::new()) };
630 }
631 #[inline(always)]
632 fn fill<const B: usize>(data: &[u8], l: i32, buf: &mut [u8]) {
633 for (blk, chunk) in buf.chunks_exact_mut(8).enumerate() {
634 let u = unpack8::<B>(&data[blk * B..]);
635 for k in 0..8 {
636 chunk[k] = (u[k] - l) as i8 as u8;
637 }
638 }
639 }
640 let _ = mask;
641 VBIT_SCRATCH.with(|scratch| {
642 let mut buf = scratch.borrow_mut();
643 buf.resize(cols, 0);
644 match b {
645 3 => fill::<3>(data, l, &mut buf),
646 4 => fill::<4>(data, l, &mut buf),
647 5 => fill::<5>(data, l, &mut buf),
648 6 => fill::<6>(data, l, &mut buf),
649 _ => unreachable!(),
650 }
651 let mut dot = 0f32;
652 for g in 0..ng {
653 let so = (r * ng + g) * 2;
654 let s = f16_to_f32(u16::from_le_bytes([
655 bytes[sc_off + so],
656 bytes[sc_off + so + 1],
657 ]));
658 let d = unsafe {
659 dot_i8_sdot(
660 &buf[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
661 &act.xq[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
662 )
663 } as f32
664 * act.sx;
665 dot += d * s;
666 }
667 for &(j, xv) in &act.outliers {
668 let so = (r * ng + j / GROUP_SIZE) * 2;
669 let s = f16_to_f32(u16::from_le_bytes([
670 bytes[sc_off + so],
671 bytes[sc_off + so + 1],
672 ]));
673 dot += (buf[j] as i8) as f32 * s * xv;
675 }
676 dot
677 })
678 };
679 match pool {
680 Some(pool) if rows >= 256 => {
681 let out_addr = SendMut(out.as_mut_ptr());
682 pool.run(&move |widx, n| {
683 let chunk = rows.div_ceil(n);
684 let start = widx * chunk;
685 let end = (start + chunk).min(rows);
686 for r in start..end {
687 unsafe { *out_addr.at(r) = row_dot(r) };
689 }
690 });
691 }
692 _ => {
693 for (r, dst) in out.iter_mut().enumerate() {
694 *dst = row_dot(r);
695 }
696 }
697 }
698 return;
699 }
700
701 #[inline(always)]
705 fn dot_row<const B: usize>(
706 data: &[u8],
707 bytes: &[u8],
708 sc_off: usize,
709 r: usize,
710 ng: usize,
711 x: &[f32],
712 ) -> f32 {
713 let l = ((1i32 << (B - 1)) - 1) as f32;
714 let gbytes = GROUP_SIZE * B / 8;
715 let mut dot = 0f32;
716 for g in 0..ng {
717 let so = (r * ng + g) * 2;
718 let s = f16_to_f32(u16::from_le_bytes([bytes[sc_off + so], bytes[sc_off + so + 1]]));
719 let xg = &x[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
720 let gd0 = &data[g * gbytes..(g + 1) * gbytes];
721 let mut gd = 0f32;
722 for blk in 0..GROUP_SIZE / 8 {
723 let u = unpack8::<B>(&gd0[blk * B..]);
724 let xb = &xg[blk * 8..blk * 8 + 8];
725 for k in 0..8 {
726 gd += (u[k] as f32 - l) * xb[k];
727 }
728 }
729 dot += gd * s;
730 }
731 dot
732 }
733 let row_dot = move |r: usize| -> f32 {
734 let data = &bytes[offsets[r]..offsets[r + 1]];
735 match bits[r] {
736 3 => dot_row::<3>(data, bytes, sc_off, r, ng, x),
737 4 => dot_row::<4>(data, bytes, sc_off, r, ng, x),
738 5 => dot_row::<5>(data, bytes, sc_off, r, ng, x),
739 6 => dot_row::<6>(data, bytes, sc_off, r, ng, x),
740 8 => dot_row::<8>(data, bytes, sc_off, r, ng, x),
741 b => unreachable!("vbit bit-width {b} (validated at load)"),
742 }
743 };
744 match pool {
745 Some(pool) if rows >= 256 => {
746 let out_addr = SendMut(out.as_mut_ptr());
747 pool.run(&move |widx, n| {
748 let chunk = rows.div_ceil(n);
749 let start = widx * chunk;
750 let end = (start + chunk).min(rows);
751 for r in start..end {
752 unsafe { *out_addr.at(r) = row_dot(r) };
754 }
755 });
756 }
757 _ => {
758 for (r, dst) in out.iter_mut().enumerate() {
759 *dst = row_dot(r);
760 }
761 }
762 }
763}
764
765fn q4matvec(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
771 debug_assert_eq!(out.len(), rows);
772 let (packed, scales) = q4_split(bytes, rows, cols);
773 let gpr = cols / GROUP_SIZE;
774
775 #[cfg(target_arch = "aarch64")]
776 if sdot_enabled() {
777 let act = split_act(x);
778 let act = &act;
779 let row_dot = move |r: usize| -> f32 {
780 let mut acc =
781 unsafe { dot_q4_row_sdot(packed, scales, r * gpr, gpr, &act.xq) } * act.sx;
782 for &(j, xv) in &act.outliers {
784 let flat = r * cols + j;
785 let byte = packed[flat / 2];
786 let nib = if flat & 1 == 0 { byte & 0x0F } else { byte >> 4 };
787 let s = f16_to_f32(u16::from_le_bytes([
788 scales[(flat / GROUP_SIZE) * 2],
789 scales[(flat / GROUP_SIZE) * 2 + 1],
790 ]));
791 acc += ((nib as i32 - 8) as f32) * s * xv;
792 }
793 acc
794 };
795 match pool {
796 Some(pool) if rows >= 256 => {
797 let out_addr = SendMut(out.as_mut_ptr());
798 pool.run(&move |widx, n| {
799 let chunk = rows.div_ceil(n);
800 let start = widx * chunk;
801 let end = (start + chunk).min(rows);
802 for r in start..end {
803 unsafe { *out_addr.at(r) = row_dot(r) };
805 }
806 });
807 }
808 _ => {
809 for (r, dst) in out.iter_mut().enumerate() {
810 *dst = row_dot(r);
811 }
812 }
813 }
814 return;
815 }
816
817 let row_dot = |r: usize| -> f32 {
818 let mut acc = 0f32;
819 for gi in 0..gpr {
820 let g = r * gpr + gi;
821 let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
822 let pk = &packed[g * 16..(g + 1) * 16];
823 let xg = &x[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE];
824 let mut ga = 0f32;
825 for (k, &b) in pk.iter().enumerate() {
826 ga += ((b & 0x0F) as f32 - 8.0) * xg[k * 2]
827 + (((b >> 4) & 0x0F) as f32 - 8.0) * xg[k * 2 + 1];
828 }
829 acc += ga * s;
830 }
831 acc
832 };
833 match pool {
834 Some(pool) if rows >= 256 => {
835 let out_addr = SendMut(out.as_mut_ptr());
836 pool.run(&move |widx, n| {
837 let chunk = rows.div_ceil(n);
838 let start = widx * chunk;
839 let end = (start + chunk).min(rows);
840 for r in start..end {
841 unsafe { *out_addr.at(r) = row_dot(r) };
843 }
844 });
845 }
846 _ => {
847 for (r, dst) in out.iter_mut().enumerate() {
848 *dst = row_dot(r);
849 }
850 }
851 }
852}
853
854pub(crate) fn prescale<'a>(
858 x: &'a [f32],
859 col_field: &[f32],
860 dtype: TensorDtype,
861) -> std::borrow::Cow<'a, [f32]> {
862 if dtype == TensorDtype::Q8_2f {
863 x.iter().zip(col_field).map(|(a, c)| a * c).collect()
864 } else {
865 std::borrow::Cow::Borrowed(x)
866 }
867}
868
869fn sdot_enabled() -> bool {
874 use std::sync::OnceLock;
875 static ON: OnceLock<bool> = OnceLock::new();
876 *ON.get_or_init(|| {
877 let want = std::env::var("CMF_SDOT").map(|v| v != "0").unwrap_or(true);
878 #[cfg(target_arch = "aarch64")]
879 {
880 want && std::arch::is_aarch64_feature_detected!("dotprod")
881 }
882 #[cfg(not(target_arch = "aarch64"))]
883 {
884 let _ = want;
885 false
886 }
887 })
888}
889
890struct SplitAct {
895 xq: Vec<i8>,
896 sx: f32,
897 outliers: Vec<(usize, f32)>,
898}
899
900fn split_act(x: &[f32]) -> SplitAct {
901 let n = x.len();
902 let rms = (x.iter().map(|&v| (v * v) as f64).sum::<f64>() / n.max(1) as f64).sqrt() as f32;
903 let thr = 8.0 * rms;
904 let outliers: Vec<(usize, f32)> = (0..n)
905 .filter(|&j| x[j].abs() > thr)
906 .map(|j| (j, x[j]))
907 .collect();
908 let mut xb = x.to_vec();
909 for &(j, _) in &outliers {
910 xb[j] = 0.0;
911 }
912 let amax = xb.iter().fold(0f32, |m, &v| m.max(v.abs()));
913 let sx = if amax > 0.0 { amax / 127.0 } else { 1.0 };
914 let inv = 1.0 / sx;
915 let xq = xb
916 .iter()
917 .map(|&v| (v * inv).round().clamp(-127.0, 127.0) as i8)
918 .collect();
919 SplitAct { xq, sx, outliers }
920}
921
922#[cfg(target_arch = "aarch64")]
925#[target_feature(enable = "neon,dotprod")]
926unsafe fn dot_i8_sdot(w: &[u8], xq: &[i8]) -> i32 {
927 unsafe {
929 use core::arch::aarch64::*;
930 use core::arch::asm;
931 let wp = w.as_ptr() as *const i8;
932 let n = w.len();
933 let (mut a0, mut a1, mut a2, mut a3) =
934 (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
935 let mut i = 0;
936 while i + 64 <= n {
937 let (w0, x0) = (vld1q_s8(wp.add(i)), vld1q_s8(xq.as_ptr().add(i)));
938 let (w1, x1) = (vld1q_s8(wp.add(i + 16)), vld1q_s8(xq.as_ptr().add(i + 16)));
939 let (w2, x2) = (vld1q_s8(wp.add(i + 32)), vld1q_s8(xq.as_ptr().add(i + 32)));
940 let (w3, x3) = (vld1q_s8(wp.add(i + 48)), vld1q_s8(xq.as_ptr().add(i + 48)));
941 asm!(
942 "sdot {a0:v}.4s, {w0:v}.16b, {x0:v}.16b",
943 "sdot {a1:v}.4s, {w1:v}.16b, {x1:v}.16b",
944 "sdot {a2:v}.4s, {w2:v}.16b, {x2:v}.16b",
945 "sdot {a3:v}.4s, {w3:v}.16b, {x3:v}.16b",
946 a0 = inout(vreg) a0, a1 = inout(vreg) a1, a2 = inout(vreg) a2, a3 = inout(vreg) a3,
947 w0 = in(vreg) w0, x0 = in(vreg) x0, w1 = in(vreg) w1, x1 = in(vreg) x1,
948 w2 = in(vreg) w2, x2 = in(vreg) x2, w3 = in(vreg) w3, x3 = in(vreg) x3,
949 options(pure, nomem, nostack),
950 );
951 i += 64;
952 }
953 while i + 16 <= n {
954 let (wv, xv) = (vld1q_s8(wp.add(i)), vld1q_s8(xq.as_ptr().add(i)));
955 asm!("sdot {a:v}.4s, {w:v}.16b, {x:v}.16b",
956 a = inout(vreg) a0, w = in(vreg) wv, x = in(vreg) xv, options(pure, nomem, nostack));
957 i += 16;
958 }
959 let mut s = vaddvq_s32(vaddq_s32(vaddq_s32(a0, a1), vaddq_s32(a2, a3)));
960 while i < n {
961 s += (*wp.add(i)) as i32 * xq[i] as i32;
962 i += 1;
963 }
964 s
965}
966}
967
968#[cfg(target_arch = "aarch64")]
972#[target_feature(enable = "neon,dotprod")]
973unsafe fn dot_i8_sdot_4rows(w0: &[u8], w1: &[u8], w2: &[u8], w3: &[u8], xq: &[i8]) -> [i32; 4] {
974 unsafe {
976 use core::arch::aarch64::*;
977 use core::arch::asm;
978 let n = xq.len();
979 let px = xq.as_ptr();
980 let (p0, p1, p2, p3) = (
981 w0.as_ptr() as *const i8,
982 w1.as_ptr() as *const i8,
983 w2.as_ptr() as *const i8,
984 w3.as_ptr() as *const i8,
985 );
986 let (mut a0, mut a1, mut a2, mut a3) =
987 (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
988 let mut i = 0;
989 while i + 16 <= n {
990 let x = vld1q_s8(px.add(i));
991 let v0 = vld1q_s8(p0.add(i));
992 let v1 = vld1q_s8(p1.add(i));
993 let v2 = vld1q_s8(p2.add(i));
994 let v3 = vld1q_s8(p3.add(i));
995 asm!(
996 "sdot {a0:v}.4s, {v0:v}.16b, {x:v}.16b",
997 "sdot {a1:v}.4s, {v1:v}.16b, {x:v}.16b",
998 "sdot {a2:v}.4s, {v2:v}.16b, {x:v}.16b",
999 "sdot {a3:v}.4s, {v3:v}.16b, {x:v}.16b",
1000 a0 = inout(vreg) a0, a1 = inout(vreg) a1, a2 = inout(vreg) a2, a3 = inout(vreg) a3,
1001 v0 = in(vreg) v0, v1 = in(vreg) v1, v2 = in(vreg) v2, v3 = in(vreg) v3, x = in(vreg) x,
1002 options(pure, nomem, nostack),
1003 );
1004 i += 16;
1005 }
1006 let mut r = [vaddvq_s32(a0), vaddvq_s32(a1), vaddvq_s32(a2), vaddvq_s32(a3)];
1007 while i < n {
1008 let xi = *px.add(i) as i32;
1009 r[0] += (*p0.add(i)) as i32 * xi;
1010 r[1] += (*p1.add(i)) as i32 * xi;
1011 r[2] += (*p2.add(i)) as i32 * xi;
1012 r[3] += (*p3.add(i)) as i32 * xi;
1013 i += 1;
1014 }
1015 r
1016}
1017}
1018
1019#[cfg(target_arch = "aarch64")]
1022#[inline]
1023fn row_dot_sdot(row: &[u8], act: &SplitAct) -> f32 {
1024 let mut acc = unsafe { dot_i8_sdot(row, &act.xq) } as f32 * act.sx;
1025 for &(j, xv) in &act.outliers {
1026 acc += (row[j] as i8) as f32 * xv;
1027 }
1028 acc
1029}
1030
1031#[cfg(target_arch = "aarch64")]
1039#[target_feature(enable = "neon,dotprod")]
1040unsafe fn dot_q4_row_sdot(packed: &[u8], scales: &[u8], g0: usize, gpr: usize, xq: &[i8]) -> f32 {
1041 unsafe {
1044 use core::arch::aarch64::*;
1045 use core::arch::asm;
1046 let lomask = vdupq_n_u8(0x0F);
1047 let eight = vdupq_n_s8(8);
1048 let mut acc = 0f32;
1049 for gi in 0..gpr {
1050 let g = g0 + gi;
1051 let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
1052 let b = vld1q_u8(packed.as_ptr().add(g * 16));
1053 let lo = vandq_u8(b, lomask);
1054 let hi = vshrq_n_u8::<4>(b);
1055 let e0 = vsubq_s8(vreinterpretq_s8_u8(vzip1q_u8(lo, hi)), eight);
1056 let e1 = vsubq_s8(vreinterpretq_s8_u8(vzip2q_u8(lo, hi)), eight);
1057 let x0 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE));
1058 let x1 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE + 16));
1059 let (mut a0, mut a1) = (vdupq_n_s32(0), vdupq_n_s32(0));
1060 asm!(
1061 "sdot {a0:v}.4s, {e0:v}.16b, {x0:v}.16b",
1062 "sdot {a1:v}.4s, {e1:v}.16b, {x1:v}.16b",
1063 a0 = inout(vreg) a0, a1 = inout(vreg) a1,
1064 e0 = in(vreg) e0, x0 = in(vreg) x0, e1 = in(vreg) e1, x1 = in(vreg) x1,
1065 options(pure, nomem, nostack),
1066 );
1067 acc += vaddvq_s32(vaddq_s32(a0, a1)) as f32 * s;
1068 }
1069 acc
1070 }
1071}
1072
1073#[inline]
1078pub(crate) fn axpy_i8_f32(acc: &mut [f32], row: &[i8], w: f32) {
1079 #[cfg(target_arch = "aarch64")]
1080 unsafe {
1081 return axpy_i8_f32_neon(acc, row, w);
1082 }
1083 #[allow(unreachable_code)]
1084 {
1085 for (a, &b) in acc.iter_mut().zip(row) {
1086 *a += w * b as f32;
1087 }
1088 }
1089}
1090
1091#[cfg(target_arch = "aarch64")]
1092#[target_feature(enable = "neon")]
1093unsafe fn axpy_i8_f32_neon(acc: &mut [f32], row: &[i8], w: f32) {
1094 unsafe {
1096 use core::arch::aarch64::*;
1097 let n = acc.len().min(row.len());
1098 let ap = acc.as_mut_ptr();
1099 let rp = row.as_ptr();
1100 let wv = vdupq_n_f32(w);
1101 let mut j = 0usize;
1102 while j + 16 <= n {
1103 let rb = vld1q_s8(rp.add(j));
1104 let lo = vmovl_s8(vget_low_s8(rb));
1105 let hi = vmovl_s8(vget_high_s8(rb));
1106 for (off, half) in [(0, lo), (8, hi)] {
1107 let f0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(half)));
1108 let f1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(half)));
1109 let o = j + off;
1110 vst1q_f32(ap.add(o), vfmaq_f32(vld1q_f32(ap.add(o)), wv, f0));
1111 vst1q_f32(ap.add(o + 4), vfmaq_f32(vld1q_f32(ap.add(o + 4)), wv, f1));
1112 }
1113 j += 16;
1114 }
1115 while j < n {
1116 *ap.add(j) += w * (*rp.add(j)) as f32;
1117 j += 1;
1118 }
1119}
1120}
1121
1122#[inline]
1125pub(crate) fn dot_i8_f32(w: &[u8], x: &[f32]) -> f32 {
1126 #[cfg(target_arch = "aarch64")]
1127 unsafe {
1128 return dot_i8_f32_neon(w, x);
1129 }
1130 #[allow(unreachable_code)]
1131 {
1132 let mut sum = 0.0f32;
1133 for (j, &b) in w.iter().enumerate() {
1134 sum += (b as i8) as f32 * x[j];
1135 }
1136 sum
1137 }
1138}
1139
1140#[inline]
1144fn dot_i8_col_f32(w: &[u8], x: &[f32], col: &[f32]) -> f32 {
1145 #[cfg(target_arch = "aarch64")]
1146 unsafe {
1147 return dot_i8_col_f32_neon(w, x, col);
1148 }
1149 #[allow(unreachable_code)]
1150 {
1151 let mut sum = 0.0f32;
1152 for (j, &b) in w.iter().enumerate() {
1153 sum += (b as i8) as f32 * x[j] * col[j];
1154 }
1155 sum
1156 }
1157}
1158
1159#[cfg(target_arch = "aarch64")]
1160#[target_feature(enable = "neon")]
1161unsafe fn dot_i8_col_f32_neon(w: &[u8], x: &[f32], col: &[f32]) -> f32 {
1162 unsafe {
1164 use core::arch::aarch64::*;
1165 let n = x.len();
1166 let wp = w.as_ptr() as *const i8;
1167 let xp = x.as_ptr();
1168 let cp = col.as_ptr();
1169 let (mut a0, mut a1, mut a2, mut a3) =
1170 (vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0));
1171 let mut j = 0usize;
1172 while j + 16 <= n {
1173 let wb = vld1q_s8(wp.add(j));
1174 let lo = vmovl_s8(vget_low_s8(wb));
1175 let hi = vmovl_s8(vget_high_s8(wb));
1176 let w0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo)));
1177 let w1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo)));
1178 let w2 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi)));
1179 let w3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi)));
1180 a0 = vfmaq_f32(a0, w0, vmulq_f32(vld1q_f32(xp.add(j)), vld1q_f32(cp.add(j))));
1181 a1 = vfmaq_f32(a1, w1, vmulq_f32(vld1q_f32(xp.add(j + 4)), vld1q_f32(cp.add(j + 4))));
1182 a2 = vfmaq_f32(a2, w2, vmulq_f32(vld1q_f32(xp.add(j + 8)), vld1q_f32(cp.add(j + 8))));
1183 a3 = vfmaq_f32(a3, w3, vmulq_f32(vld1q_f32(xp.add(j + 12)), vld1q_f32(cp.add(j + 12))));
1184 j += 16;
1185 }
1186 let mut sum = vaddvq_f32(vaddq_f32(vaddq_f32(a0, a1), vaddq_f32(a2, a3)));
1187 while j < n {
1188 sum += (*wp.add(j)) as f32 * *xp.add(j) * *cp.add(j);
1189 j += 1;
1190 }
1191 sum
1192}
1193}
1194
1195#[cfg(target_arch = "aarch64")]
1196#[target_feature(enable = "neon")]
1197unsafe fn dot_i8_f32_neon(w: &[u8], x: &[f32]) -> f32 {
1198 unsafe {
1200 use core::arch::aarch64::*;
1201 let n = x.len();
1202 let wp = w.as_ptr() as *const i8;
1203 let xp = x.as_ptr();
1204 let (mut a0, mut a1, mut a2, mut a3) =
1205 (vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0));
1206 let mut j = 0usize;
1207 while j + 16 <= n {
1208 let wb = vld1q_s8(wp.add(j));
1209 let lo = vmovl_s8(vget_low_s8(wb));
1210 let hi = vmovl_s8(vget_high_s8(wb));
1211 let w0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo)));
1212 let w1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo)));
1213 let w2 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi)));
1214 let w3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi)));
1215 a0 = vfmaq_f32(a0, w0, vld1q_f32(xp.add(j)));
1216 a1 = vfmaq_f32(a1, w1, vld1q_f32(xp.add(j + 4)));
1217 a2 = vfmaq_f32(a2, w2, vld1q_f32(xp.add(j + 8)));
1218 a3 = vfmaq_f32(a3, w3, vld1q_f32(xp.add(j + 12)));
1219 j += 16;
1220 }
1221 let mut sum = vaddvq_f32(vaddq_f32(vaddq_f32(a0, a1), vaddq_f32(a2, a3)));
1222 while j < n {
1223 sum += (*wp.add(j)) as f32 * *xp.add(j);
1224 j += 1;
1225 }
1226 sum
1227}
1228}
1229
1230#[allow(clippy::too_many_arguments)]
1231fn qmatvec(
1232 q: &[u8],
1233 row_scale: &[f32],
1234 xs: &[f32],
1235 rows: usize,
1236 cols: usize,
1237 out: &mut [f32],
1238 pool: Option<&Pool>,
1239) {
1240 debug_assert_eq!(out.len(), rows);
1241
1242 #[cfg(target_arch = "aarch64")]
1243 if sdot_enabled() {
1244 let act = split_act(xs);
1245 let out_addr = SendMut(out.as_mut_ptr());
1246 let run_range = |start: usize, end: usize| {
1247 let mut o = start;
1248 while o + 4 <= end {
1249 let r = unsafe {
1250 dot_i8_sdot_4rows(
1251 &q[o * cols..(o + 1) * cols],
1252 &q[(o + 1) * cols..(o + 2) * cols],
1253 &q[(o + 2) * cols..(o + 3) * cols],
1254 &q[(o + 3) * cols..(o + 4) * cols],
1255 &act.xq,
1256 )
1257 };
1258 for k in 0..4 {
1259 let mut acc = r[k] as f32 * act.sx;
1260 for &(j, xv) in &act.outliers {
1261 acc += (q[(o + k) * cols + j] as i8) as f32 * xv;
1262 }
1263 unsafe { *out_addr.at(o + k) = acc * row_scale[o + k] };
1265 }
1266 o += 4;
1267 }
1268 while o < end {
1269 let v = row_dot_sdot(&q[o * cols..(o + 1) * cols], &act) * row_scale[o];
1270 unsafe { *out_addr.at(o) = v };
1271 o += 1;
1272 }
1273 };
1274 match pool {
1275 Some(pool) if rows >= 256 => pool.run_rows(rows, &run_range),
1276 _ => run_range(0, rows),
1277 }
1278 return;
1279 }
1280
1281 let row_dot = |o: usize| -> f32 { dot_i8_f32(&q[o * cols..(o + 1) * cols], xs) * row_scale[o] };
1282 match pool {
1283 Some(pool) if rows >= 256 => {
1284 let out_addr = SendMut(out.as_mut_ptr());
1285 pool.run(&move |widx, n| {
1286 let chunk = rows.div_ceil(n);
1287 let start = widx * chunk;
1288 let end = (start + chunk).min(rows);
1289 for o in start..end {
1290 unsafe { *out_addr.at(o) = row_dot(o) };
1292 }
1293 });
1294 }
1295 _ => {
1296 for (o, dst) in out.iter_mut().enumerate() {
1297 *dst = row_dot(o);
1298 }
1299 }
1300 }
1301}
1302
1303#[allow(clippy::too_many_arguments)]
1304fn qmatvec2(
1305 q: &[u8],
1306 row_scale: &[f32],
1307 x1: &[f32],
1308 x2: &[f32],
1309 rows: usize,
1310 cols: usize,
1311 o1: &mut [f32],
1312 o2: &mut [f32],
1313 pool: Option<&Pool>,
1314) {
1315 #[cfg(target_arch = "aarch64")]
1316 if sdot_enabled() {
1317 let a1s = split_act(x1);
1318 let a2s = split_act(x2);
1319 let p1 = SendMut(o1.as_mut_ptr());
1320 let p2 = SendMut(o2.as_mut_ptr());
1321 let run_range = |start: usize, end: usize| {
1322 for o in start..end {
1323 let row = &q[o * cols..(o + 1) * cols];
1324 unsafe {
1326 *p1.at(o) = row_dot_sdot(row, &a1s) * row_scale[o];
1327 *p2.at(o) = row_dot_sdot(row, &a2s) * row_scale[o];
1328 }
1329 }
1330 };
1331 match pool {
1332 Some(pool) if rows >= 256 => {
1333 pool.run(&|widx, n| {
1334 let chunk = rows.div_ceil(n);
1335 let start = widx * chunk;
1336 run_range(start, (start + chunk).min(rows));
1337 });
1338 }
1339 _ => run_range(0, rows),
1340 }
1341 return;
1342 }
1343
1344 let row_dots = |o: usize| -> (f32, f32) {
1345 let row = &q[o * cols..(o + 1) * cols];
1346 (dot_i8_f32(row, x1) * row_scale[o], dot_i8_f32(row, x2) * row_scale[o])
1347 };
1348 match pool {
1349 Some(pool) if rows >= 256 => {
1350 let p1 = SendMut(o1.as_mut_ptr());
1351 let p2 = SendMut(o2.as_mut_ptr());
1352 pool.run(&move |widx, n| {
1353 let chunk = rows.div_ceil(n);
1354 let start = widx * chunk;
1355 let end = (start + chunk).min(rows);
1356 for o in start..end {
1357 let (s1, s2) = row_dots(o);
1358 unsafe {
1360 *p1.at(o) = s1;
1361 *p2.at(o) = s2;
1362 }
1363 }
1364 });
1365 }
1366 _ => {
1367 for o in 0..rows {
1368 let (s1, s2) = row_dots(o);
1369 o1[o] = s1;
1370 o2[o] = s2;
1371 }
1372 }
1373 }
1374}
1375
1376#[derive(Clone, Copy)]
1377struct SendMut(*mut f32);
1378unsafe impl Send for SendMut {}
1379unsafe impl Sync for SendMut {}
1380
1381impl SendMut {
1382 #[inline]
1383 fn at(self, i: usize) -> *mut f32 {
1384 unsafe { self.0.add(i) }
1385 }
1386}
1387
1388#[cfg(test)]
1389mod tests {
1390 use super::*;
1391
1392 #[test]
1393 fn f32_matvec_matches_matvec_rows_bitexact() {
1394 let (rows, cols) = (300, 40);
1395 let w: Vec<f32> = (0..rows * cols).map(|i| (i as f32 * 0.017).sin()).collect();
1396 let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.05).cos()).collect();
1397 let qt = QTensor::from_f32(w.clone(), rows, cols);
1398
1399 let mut a = vec![0.0f32; rows];
1400 matvec_rows(None, &w, &x, &mut a);
1401 let mut b = vec![0.0f32; rows];
1402 qt.matvec(&x, &mut b, None);
1403 assert_eq!(a, b);
1404 }
1405
1406 #[test]
1407 fn sdot_kernel_exact_on_grid() {
1408 eprintln!("sdot_enabled = {}", sdot_enabled());
1413 let (rows, cols) = (9, 80); let w: Vec<u8> = (0..rows * cols)
1415 .map(|i| (((i * 37) % 251) as i32 - 125) as i8 as u8)
1416 .collect();
1417 let scales: Vec<f32> = (0..rows).map(|o| 0.005 + o as f32 * 0.001).collect();
1418 let x: Vec<f32> = (0..cols)
1419 .map(|i| match i % 3 {
1420 0 => 1.0,
1421 1 => -1.0,
1422 _ => 0.0,
1423 })
1424 .collect();
1425 let mut a = vec![0.0f32; rows];
1426 qmatvec(&w, &scales, &x, rows, cols, &mut a, None);
1427 for o in 0..rows {
1428 let mut acc = 0.0f32;
1429 for j in 0..cols {
1430 acc += (w[o * cols + j] as i8) as f32 * x[j];
1431 }
1432 let expect = acc * scales[o];
1433 assert!(
1434 (a[o] - expect).abs() < 1e-3 * expect.abs().max(1e-3),
1435 "row {o}: {} vs {expect}",
1436 a[o]
1437 );
1438 }
1439 }
1440
1441 #[test]
1442 fn sdot_a8w8_noise_is_bounded() {
1443 let (rows, cols) = (16, 512);
1447 let w: Vec<u8> = (0..rows * cols)
1448 .map(|i| (((i * 37) % 251) as i32 - 125) as i8 as u8)
1449 .collect();
1450 let scales = vec![0.01f32; rows];
1451 let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.21).sin()).collect();
1452 let mut a = vec![0.0f32; rows];
1453 qmatvec(&w, &scales, &x, rows, cols, &mut a, None);
1454 let (mut num, mut den) = (0f64, 0f64);
1455 for o in 0..rows {
1456 let mut acc = 0.0f32;
1457 for j in 0..cols {
1458 acc += (w[o * cols + j] as i8) as f32 * x[j];
1459 }
1460 let expect = acc * scales[o];
1461 num += ((a[o] - expect) as f64).powi(2);
1462 den += (expect as f64).powi(2);
1463 }
1464 let rel = (num / den.max(1e-12)).sqrt();
1465 assert!(rel < 0.05, "A8W8 relative L2 error too high: {rel}");
1466 }
1467
1468 #[test]
1469 fn i8_dot_neon_matches_scalar() {
1470 let n = 100;
1471 let w: Vec<u8> = (0..n).map(|i| ((i * 37 + 11) % 251) as u8).collect();
1472 let x: Vec<f32> = (0..n).map(|i| (i as f32 * 0.13).sin()).collect();
1473 let mut scalar = 0.0f32;
1474 for j in 0..n {
1475 scalar += (w[j] as i8) as f32 * x[j];
1476 }
1477 let fast = dot_i8_f32(&w, &x);
1478 assert!((scalar - fast).abs() < 1e-3 * scalar.abs().max(1.0));
1479 }
1480
1481 #[test]
1483 fn vbitmatvec_matches_full_dequant() {
1484 let (rows, cols) = (6, 64);
1485 let ng = cols / GROUP_SIZE;
1486 let bits: Vec<u8> = vec![3, 4, 5, 6, 8, 4];
1488 let mut bytes = bits.clone();
1489 for g in 0..rows * ng {
1490 let s = 0.02 + 0.001 * g as f32;
1491 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
1492 }
1493 for r in 0..rows {
1494 let b = bits[r] as usize;
1495 let (mut acc, mut nb) = (0u64, 0usize);
1496 let mut rowbytes = Vec::new();
1497 for i in 0..cols {
1498 let v = ((i * 7 + r * 13) % (1 << b)) as u64;
1499 acc = (acc << b) | v;
1500 nb += b;
1501 while nb >= 8 {
1502 nb -= 8;
1503 rowbytes.push(((acc >> nb) & 0xFF) as u8);
1504 }
1505 }
1506 if nb > 0 {
1507 rowbytes.push(((acc << (8 - nb)) & 0xFF) as u8);
1508 }
1509 bytes.extend_from_slice(&rowbytes);
1510 }
1511 let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.19).sin()).collect();
1512
1513 let mut reference = vec![0f32; rows * cols];
1514 cortiq_core::quant::dequant_vbit(&bytes, rows, cols, &mut reference).unwrap();
1515 let mut expect = vec![0f32; rows];
1516 for r in 0..rows {
1517 expect[r] = reference[r * cols..(r + 1) * cols]
1518 .iter()
1519 .zip(&x)
1520 .map(|(w, xv)| w * xv)
1521 .sum();
1522 }
1523 let mut got = vec![0f32; rows];
1524 vbitmatvec(&bytes, &x, rows, cols, &mut got, None);
1525 let tol = if sdot_enabled() { 6e-2 } else { 1e-4 };
1529 let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1e-6);
1530 for r in 0..rows {
1531 assert!(
1532 (got[r] - expect[r]).abs() < tol * scale,
1533 "row {r}: {} vs {}",
1534 got[r],
1535 expect[r]
1536 );
1537 }
1538 }
1539
1540 #[test]
1543 fn q4matvec_matches_full_dequant() {
1544 let (rows, cols) = (8, 64);
1545 let groups = rows * cols / GROUP_SIZE;
1546 let mut bytes = Vec::with_capacity(groups * 16 + groups * 2);
1548 for i in 0..groups * 16 {
1549 bytes.push((((i * 7 + 3) % 256) & 0xFF) as u8);
1550 }
1551 for g in 0..groups {
1552 let s = 0.01 + 0.003 * g as f32;
1553 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
1554 }
1555 let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.17).sin()).collect();
1556
1557 let mut reference = vec![0.0f32; rows * cols];
1558 cortiq_core::quant::dequant_q4_block(&bytes, &mut reference);
1559 let mut expect = vec![0.0f32; rows];
1560 for r in 0..rows {
1561 expect[r] = reference[r * cols..(r + 1) * cols]
1562 .iter()
1563 .zip(&x)
1564 .map(|(w, xv)| w * xv)
1565 .sum();
1566 }
1567
1568 let mut got = vec![0.0f32; rows];
1569 q4matvec(&bytes, &x, rows, cols, &mut got, None);
1570 let tol = if sdot_enabled() { 6e-2 } else { 1e-4 };
1574 let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1.0);
1575 for r in 0..rows {
1576 assert!(
1577 (got[r] - expect[r]).abs() < tol * scale,
1578 "row {r}: {} vs {}",
1579 got[r],
1580 expect[r]
1581 );
1582 }
1583 }
1584
1585 #[test]
1592 fn q4matvec_sdot_outlier_exact() {
1593 let (rows, cols) = (4, 128);
1594 let groups = rows * cols / GROUP_SIZE;
1595 let mut bytes = Vec::with_capacity(groups * 16 + groups * 2);
1596 for i in 0..groups * 16 {
1597 bytes.push(((i * 11 + 5) % 256) as u8);
1598 }
1599 for g in 0..groups {
1600 let s = 0.02 + 0.002 * g as f32;
1601 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
1602 }
1603 let mut x: Vec<f32> = (0..cols)
1604 .map(|i| match i % 3 {
1605 0 => 1.0,
1606 1 => -1.0,
1607 _ => 0.0,
1608 })
1609 .collect();
1610 x[17] = 300.0; let mut reference = vec![0.0f32; rows * cols];
1613 cortiq_core::quant::dequant_q4_block(&bytes, &mut reference);
1614 let mut expect = vec![0.0f32; rows];
1615 for r in 0..rows {
1616 expect[r] = reference[r * cols..(r + 1) * cols]
1617 .iter()
1618 .zip(&x)
1619 .map(|(w, xv)| w * xv)
1620 .sum();
1621 }
1622 let mut got = vec![0.0f32; rows];
1623 q4matvec(&bytes, &x, rows, cols, &mut got, None);
1624 let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1.0);
1625 for r in 0..rows {
1626 assert!(
1627 (got[r] - expect[r]).abs() < 2e-3 * scale,
1628 "row {r}: {} vs {} (outlier term must be exact)",
1629 got[r],
1630 expect[r]
1631 );
1632 }
1633 }
1634}