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)) {
535 match pool {
536 Some(pool) if rows >= 256 => {
537 pool.run(&|widx, n| {
538 let chunk = rows.div_ceil(n);
539 let start = widx * chunk;
540 let end = (start + chunk).min(rows);
541 if start < end {
542 run(start, end);
543 }
544 });
545 }
546 _ => run(0, rows),
547 }
548}
549
550fn q4_split(bytes: &[u8], rows: usize, cols: usize) -> (&[u8], &[u8]) {
552 let groups = rows * cols / GROUP_SIZE;
553 bytes.split_at(groups * 16)
554}
555
556#[inline(always)]
561fn unpack8<const B: usize>(data: &[u8]) -> [i32; 8] {
562 let mut acc = 0u64;
563 for i in 0..B {
564 acc = (acc << 8) | data[i] as u64;
565 }
566 let mask = (1u64 << B) - 1;
567 let mut out = [0i32; 8];
568 for (k, o) in out.iter_mut().enumerate() {
569 *o = ((acc >> ((7 - k) * B)) & mask) as i32;
570 }
571 out
572}
573
574fn vbitmatvec(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
579 debug_assert_eq!(out.len(), rows);
580 let ng = cols / GROUP_SIZE;
581 let bits = &bytes[..rows];
582 let sc_off = rows;
583 let data_off = sc_off + rows * ng * 2;
584 let mut offsets = Vec::with_capacity(rows + 1);
585 let mut off = data_off;
586 for r in 0..rows {
587 offsets.push(off);
588 off += (cols * bits[r] as usize + 7) / 8;
589 }
590 offsets.push(off);
591
592 let offsets = &offsets;
593
594 #[cfg(target_arch = "aarch64")]
598 if sdot_enabled() {
599 let act = split_act(x);
600 let act = &act;
601 let row_dot = move |r: usize| -> f32 {
602 let b = bits[r] as usize;
603 let l = ((1i32 << (b - 1)) - 1) as i32;
604 let mask = (1u64 << b) - 1;
605 let data = &bytes[offsets[r]..offsets[r + 1]];
606 if b == 8 {
607 let (mut acc, mut nbits, mut idx) = (0u64, 0usize, 0usize);
609 let mut dot = 0f32;
610 for g in 0..ng {
611 let so = (r * ng + g) * 2;
612 let sgf = f16_to_f32(u16::from_le_bytes([
613 bytes[sc_off + so],
614 bytes[sc_off + so + 1],
615 ]));
616 let xg = &x[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
617 let mut gd = 0f32;
618 for &xv in xg.iter() {
619 if nbits < 8 {
620 acc = (acc << 8) | data[idx] as u64;
621 idx += 1;
622 nbits += 8;
623 }
624 let u = ((acc >> (nbits - 8)) & 0xFF) as i32;
625 nbits -= 8;
626 gd += (u - l) as f32 * xv;
627 }
628 dot += gd * sgf;
629 }
630 return dot;
631 }
632 thread_local! {
636 static VBIT_SCRATCH: std::cell::RefCell<Vec<u8>> =
637 const { std::cell::RefCell::new(Vec::new()) };
638 }
639 #[inline(always)]
640 fn fill<const B: usize>(data: &[u8], l: i32, buf: &mut [u8]) {
641 for (blk, chunk) in buf.chunks_exact_mut(8).enumerate() {
642 let u = unpack8::<B>(&data[blk * B..]);
643 for k in 0..8 {
644 chunk[k] = (u[k] - l) as i8 as u8;
645 }
646 }
647 }
648 let _ = mask;
649 VBIT_SCRATCH.with(|scratch| {
650 let mut buf = scratch.borrow_mut();
651 buf.resize(cols, 0);
652 match b {
653 3 => fill::<3>(data, l, &mut buf),
654 4 => fill::<4>(data, l, &mut buf),
655 5 => fill::<5>(data, l, &mut buf),
656 6 => fill::<6>(data, l, &mut buf),
657 _ => unreachable!(),
658 }
659 let mut dot = 0f32;
660 for g in 0..ng {
661 let so = (r * ng + g) * 2;
662 let s = f16_to_f32(u16::from_le_bytes([
663 bytes[sc_off + so],
664 bytes[sc_off + so + 1],
665 ]));
666 let d = unsafe {
667 dot_i8_sdot(
668 &buf[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
669 &act.xq[g * GROUP_SIZE..(g + 1) * GROUP_SIZE],
670 )
671 } as f32
672 * act.sx;
673 dot += d * s;
674 }
675 for &(j, xv) in &act.outliers {
676 let so = (r * ng + j / GROUP_SIZE) * 2;
677 let s = f16_to_f32(u16::from_le_bytes([
678 bytes[sc_off + so],
679 bytes[sc_off + so + 1],
680 ]));
681 dot += (buf[j] as i8) as f32 * s * xv;
683 }
684 dot
685 })
686 };
687 match pool {
688 Some(pool) if rows >= 256 => {
689 let out_addr = SendMut(out.as_mut_ptr());
690 pool.run(&move |widx, n| {
691 let chunk = rows.div_ceil(n);
692 let start = widx * chunk;
693 let end = (start + chunk).min(rows);
694 for r in start..end {
695 unsafe { *out_addr.at(r) = row_dot(r) };
697 }
698 });
699 }
700 _ => {
701 for (r, dst) in out.iter_mut().enumerate() {
702 *dst = row_dot(r);
703 }
704 }
705 }
706 return;
707 }
708
709 #[inline(always)]
713 fn dot_row<const B: usize>(
714 data: &[u8],
715 bytes: &[u8],
716 sc_off: usize,
717 r: usize,
718 ng: usize,
719 x: &[f32],
720 ) -> f32 {
721 let l = ((1i32 << (B - 1)) - 1) as f32;
722 let gbytes = GROUP_SIZE * B / 8;
723 let mut dot = 0f32;
724 for g in 0..ng {
725 let so = (r * ng + g) * 2;
726 let s = f16_to_f32(u16::from_le_bytes([bytes[sc_off + so], bytes[sc_off + so + 1]]));
727 let xg = &x[g * GROUP_SIZE..(g + 1) * GROUP_SIZE];
728 let gd0 = &data[g * gbytes..(g + 1) * gbytes];
729 let mut gd = 0f32;
730 for blk in 0..GROUP_SIZE / 8 {
731 let u = unpack8::<B>(&gd0[blk * B..]);
732 let xb = &xg[blk * 8..blk * 8 + 8];
733 for k in 0..8 {
734 gd += (u[k] as f32 - l) * xb[k];
735 }
736 }
737 dot += gd * s;
738 }
739 dot
740 }
741 let row_dot = move |r: usize| -> f32 {
742 let data = &bytes[offsets[r]..offsets[r + 1]];
743 match bits[r] {
744 3 => dot_row::<3>(data, bytes, sc_off, r, ng, x),
745 4 => dot_row::<4>(data, bytes, sc_off, r, ng, x),
746 5 => dot_row::<5>(data, bytes, sc_off, r, ng, x),
747 6 => dot_row::<6>(data, bytes, sc_off, r, ng, x),
748 8 => dot_row::<8>(data, bytes, sc_off, r, ng, x),
749 b => unreachable!("vbit bit-width {b} (validated at load)"),
750 }
751 };
752 match pool {
753 Some(pool) if rows >= 256 => {
754 let out_addr = SendMut(out.as_mut_ptr());
755 pool.run(&move |widx, n| {
756 let chunk = rows.div_ceil(n);
757 let start = widx * chunk;
758 let end = (start + chunk).min(rows);
759 for r in start..end {
760 unsafe { *out_addr.at(r) = row_dot(r) };
762 }
763 });
764 }
765 _ => {
766 for (r, dst) in out.iter_mut().enumerate() {
767 *dst = row_dot(r);
768 }
769 }
770 }
771}
772
773fn q4matvec(bytes: &[u8], x: &[f32], rows: usize, cols: usize, out: &mut [f32], pool: Option<&Pool>) {
779 debug_assert_eq!(out.len(), rows);
780 let (packed, scales) = q4_split(bytes, rows, cols);
781 let gpr = cols / GROUP_SIZE;
782
783 #[cfg(target_arch = "aarch64")]
784 if sdot_enabled() {
785 let act = split_act(x);
786 let act = &act;
787 let row_dot = move |r: usize| -> f32 {
788 let mut acc =
789 unsafe { dot_q4_row_sdot(packed, scales, r * gpr, gpr, &act.xq) } * act.sx;
790 for &(j, xv) in &act.outliers {
792 let flat = r * cols + j;
793 let byte = packed[flat / 2];
794 let nib = if flat & 1 == 0 { byte & 0x0F } else { byte >> 4 };
795 let s = f16_to_f32(u16::from_le_bytes([
796 scales[(flat / GROUP_SIZE) * 2],
797 scales[(flat / GROUP_SIZE) * 2 + 1],
798 ]));
799 acc += ((nib as i32 - 8) as f32) * s * xv;
800 }
801 acc
802 };
803 match pool {
804 Some(pool) if rows >= 256 => {
805 let out_addr = SendMut(out.as_mut_ptr());
806 pool.run(&move |widx, n| {
807 let chunk = rows.div_ceil(n);
808 let start = widx * chunk;
809 let end = (start + chunk).min(rows);
810 for r in start..end {
811 unsafe { *out_addr.at(r) = row_dot(r) };
813 }
814 });
815 }
816 _ => {
817 for (r, dst) in out.iter_mut().enumerate() {
818 *dst = row_dot(r);
819 }
820 }
821 }
822 return;
823 }
824
825 let row_dot = |r: usize| -> f32 {
826 let mut acc = 0f32;
827 for gi in 0..gpr {
828 let g = r * gpr + gi;
829 let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
830 let pk = &packed[g * 16..(g + 1) * 16];
831 let xg = &x[gi * GROUP_SIZE..(gi + 1) * GROUP_SIZE];
832 let mut ga = 0f32;
833 for (k, &b) in pk.iter().enumerate() {
834 ga += ((b & 0x0F) as f32 - 8.0) * xg[k * 2]
835 + (((b >> 4) & 0x0F) as f32 - 8.0) * xg[k * 2 + 1];
836 }
837 acc += ga * s;
838 }
839 acc
840 };
841 match pool {
842 Some(pool) if rows >= 256 => {
843 let out_addr = SendMut(out.as_mut_ptr());
844 pool.run(&move |widx, n| {
845 let chunk = rows.div_ceil(n);
846 let start = widx * chunk;
847 let end = (start + chunk).min(rows);
848 for r in start..end {
849 unsafe { *out_addr.at(r) = row_dot(r) };
851 }
852 });
853 }
854 _ => {
855 for (r, dst) in out.iter_mut().enumerate() {
856 *dst = row_dot(r);
857 }
858 }
859 }
860}
861
862pub(crate) fn prescale<'a>(
866 x: &'a [f32],
867 col_field: &[f32],
868 dtype: TensorDtype,
869) -> std::borrow::Cow<'a, [f32]> {
870 if dtype == TensorDtype::Q8_2f {
871 x.iter().zip(col_field).map(|(a, c)| a * c).collect()
872 } else {
873 std::borrow::Cow::Borrowed(x)
874 }
875}
876
877fn sdot_enabled() -> bool {
882 use std::sync::OnceLock;
883 static ON: OnceLock<bool> = OnceLock::new();
884 *ON.get_or_init(|| {
885 let want = std::env::var("CMF_SDOT").map(|v| v != "0").unwrap_or(true);
886 #[cfg(target_arch = "aarch64")]
887 {
888 want && std::arch::is_aarch64_feature_detected!("dotprod")
889 }
890 #[cfg(not(target_arch = "aarch64"))]
891 {
892 let _ = want;
893 false
894 }
895 })
896}
897
898struct SplitAct {
903 xq: Vec<i8>,
904 sx: f32,
905 outliers: Vec<(usize, f32)>,
906}
907
908fn split_act(x: &[f32]) -> SplitAct {
909 let n = x.len();
910 let rms = (x.iter().map(|&v| (v * v) as f64).sum::<f64>() / n.max(1) as f64).sqrt() as f32;
911 let thr = 8.0 * rms;
912 let outliers: Vec<(usize, f32)> = (0..n)
913 .filter(|&j| x[j].abs() > thr)
914 .map(|j| (j, x[j]))
915 .collect();
916 let mut xb = x.to_vec();
917 for &(j, _) in &outliers {
918 xb[j] = 0.0;
919 }
920 let amax = xb.iter().fold(0f32, |m, &v| m.max(v.abs()));
921 let sx = if amax > 0.0 { amax / 127.0 } else { 1.0 };
922 let inv = 1.0 / sx;
923 let xq = xb
924 .iter()
925 .map(|&v| (v * inv).round().clamp(-127.0, 127.0) as i8)
926 .collect();
927 SplitAct { xq, sx, outliers }
928}
929
930#[cfg(target_arch = "aarch64")]
933#[target_feature(enable = "neon,dotprod")]
934unsafe fn dot_i8_sdot(w: &[u8], xq: &[i8]) -> i32 {
935 unsafe {
937 use core::arch::aarch64::*;
938 use core::arch::asm;
939 let wp = w.as_ptr() as *const i8;
940 let n = w.len();
941 let (mut a0, mut a1, mut a2, mut a3) =
942 (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
943 let mut i = 0;
944 while i + 64 <= n {
945 let (w0, x0) = (vld1q_s8(wp.add(i)), vld1q_s8(xq.as_ptr().add(i)));
946 let (w1, x1) = (vld1q_s8(wp.add(i + 16)), vld1q_s8(xq.as_ptr().add(i + 16)));
947 let (w2, x2) = (vld1q_s8(wp.add(i + 32)), vld1q_s8(xq.as_ptr().add(i + 32)));
948 let (w3, x3) = (vld1q_s8(wp.add(i + 48)), vld1q_s8(xq.as_ptr().add(i + 48)));
949 asm!(
950 "sdot {a0:v}.4s, {w0:v}.16b, {x0:v}.16b",
951 "sdot {a1:v}.4s, {w1:v}.16b, {x1:v}.16b",
952 "sdot {a2:v}.4s, {w2:v}.16b, {x2:v}.16b",
953 "sdot {a3:v}.4s, {w3:v}.16b, {x3:v}.16b",
954 a0 = inout(vreg) a0, a1 = inout(vreg) a1, a2 = inout(vreg) a2, a3 = inout(vreg) a3,
955 w0 = in(vreg) w0, x0 = in(vreg) x0, w1 = in(vreg) w1, x1 = in(vreg) x1,
956 w2 = in(vreg) w2, x2 = in(vreg) x2, w3 = in(vreg) w3, x3 = in(vreg) x3,
957 options(pure, nomem, nostack),
958 );
959 i += 64;
960 }
961 while i + 16 <= n {
962 let (wv, xv) = (vld1q_s8(wp.add(i)), vld1q_s8(xq.as_ptr().add(i)));
963 asm!("sdot {a:v}.4s, {w:v}.16b, {x:v}.16b",
964 a = inout(vreg) a0, w = in(vreg) wv, x = in(vreg) xv, options(pure, nomem, nostack));
965 i += 16;
966 }
967 let mut s = vaddvq_s32(vaddq_s32(vaddq_s32(a0, a1), vaddq_s32(a2, a3)));
968 while i < n {
969 s += (*wp.add(i)) as i32 * xq[i] as i32;
970 i += 1;
971 }
972 s
973}
974}
975
976#[cfg(target_arch = "aarch64")]
980#[target_feature(enable = "neon,dotprod")]
981unsafe fn dot_i8_sdot_4rows(w0: &[u8], w1: &[u8], w2: &[u8], w3: &[u8], xq: &[i8]) -> [i32; 4] {
982 unsafe {
984 use core::arch::aarch64::*;
985 use core::arch::asm;
986 let n = xq.len();
987 let px = xq.as_ptr();
988 let (p0, p1, p2, p3) = (
989 w0.as_ptr() as *const i8,
990 w1.as_ptr() as *const i8,
991 w2.as_ptr() as *const i8,
992 w3.as_ptr() as *const i8,
993 );
994 let (mut a0, mut a1, mut a2, mut a3) =
995 (vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0), vdupq_n_s32(0));
996 let mut i = 0;
997 while i + 16 <= n {
998 let x = vld1q_s8(px.add(i));
999 let v0 = vld1q_s8(p0.add(i));
1000 let v1 = vld1q_s8(p1.add(i));
1001 let v2 = vld1q_s8(p2.add(i));
1002 let v3 = vld1q_s8(p3.add(i));
1003 asm!(
1004 "sdot {a0:v}.4s, {v0:v}.16b, {x:v}.16b",
1005 "sdot {a1:v}.4s, {v1:v}.16b, {x:v}.16b",
1006 "sdot {a2:v}.4s, {v2:v}.16b, {x:v}.16b",
1007 "sdot {a3:v}.4s, {v3:v}.16b, {x:v}.16b",
1008 a0 = inout(vreg) a0, a1 = inout(vreg) a1, a2 = inout(vreg) a2, a3 = inout(vreg) a3,
1009 v0 = in(vreg) v0, v1 = in(vreg) v1, v2 = in(vreg) v2, v3 = in(vreg) v3, x = in(vreg) x,
1010 options(pure, nomem, nostack),
1011 );
1012 i += 16;
1013 }
1014 let mut r = [vaddvq_s32(a0), vaddvq_s32(a1), vaddvq_s32(a2), vaddvq_s32(a3)];
1015 while i < n {
1016 let xi = *px.add(i) as i32;
1017 r[0] += (*p0.add(i)) as i32 * xi;
1018 r[1] += (*p1.add(i)) as i32 * xi;
1019 r[2] += (*p2.add(i)) as i32 * xi;
1020 r[3] += (*p3.add(i)) as i32 * xi;
1021 i += 1;
1022 }
1023 r
1024}
1025}
1026
1027#[cfg(target_arch = "aarch64")]
1030#[inline]
1031fn row_dot_sdot(row: &[u8], act: &SplitAct) -> f32 {
1032 let mut acc = unsafe { dot_i8_sdot(row, &act.xq) } as f32 * act.sx;
1033 for &(j, xv) in &act.outliers {
1034 acc += (row[j] as i8) as f32 * xv;
1035 }
1036 acc
1037}
1038
1039#[cfg(target_arch = "aarch64")]
1047#[target_feature(enable = "neon,dotprod")]
1048unsafe fn dot_q4_row_sdot(packed: &[u8], scales: &[u8], g0: usize, gpr: usize, xq: &[i8]) -> f32 {
1049 unsafe {
1052 use core::arch::aarch64::*;
1053 use core::arch::asm;
1054 let lomask = vdupq_n_u8(0x0F);
1055 let eight = vdupq_n_s8(8);
1056 let mut acc = 0f32;
1057 for gi in 0..gpr {
1058 let g = g0 + gi;
1059 let s = f16_to_f32(u16::from_le_bytes([scales[g * 2], scales[g * 2 + 1]]));
1060 let b = vld1q_u8(packed.as_ptr().add(g * 16));
1061 let lo = vandq_u8(b, lomask);
1062 let hi = vshrq_n_u8::<4>(b);
1063 let e0 = vsubq_s8(vreinterpretq_s8_u8(vzip1q_u8(lo, hi)), eight);
1064 let e1 = vsubq_s8(vreinterpretq_s8_u8(vzip2q_u8(lo, hi)), eight);
1065 let x0 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE));
1066 let x1 = vld1q_s8(xq.as_ptr().add(gi * GROUP_SIZE + 16));
1067 let (mut a0, mut a1) = (vdupq_n_s32(0), vdupq_n_s32(0));
1068 asm!(
1069 "sdot {a0:v}.4s, {e0:v}.16b, {x0:v}.16b",
1070 "sdot {a1:v}.4s, {e1:v}.16b, {x1:v}.16b",
1071 a0 = inout(vreg) a0, a1 = inout(vreg) a1,
1072 e0 = in(vreg) e0, x0 = in(vreg) x0, e1 = in(vreg) e1, x1 = in(vreg) x1,
1073 options(pure, nomem, nostack),
1074 );
1075 acc += vaddvq_s32(vaddq_s32(a0, a1)) as f32 * s;
1076 }
1077 acc
1078 }
1079}
1080
1081#[inline]
1086pub(crate) fn axpy_i8_f32(acc: &mut [f32], row: &[i8], w: f32) {
1087 #[cfg(target_arch = "aarch64")]
1088 unsafe {
1089 return axpy_i8_f32_neon(acc, row, w);
1090 }
1091 #[allow(unreachable_code)]
1092 {
1093 for (a, &b) in acc.iter_mut().zip(row) {
1094 *a += w * b as f32;
1095 }
1096 }
1097}
1098
1099#[cfg(target_arch = "aarch64")]
1100#[target_feature(enable = "neon")]
1101unsafe fn axpy_i8_f32_neon(acc: &mut [f32], row: &[i8], w: f32) {
1102 unsafe {
1104 use core::arch::aarch64::*;
1105 let n = acc.len().min(row.len());
1106 let ap = acc.as_mut_ptr();
1107 let rp = row.as_ptr();
1108 let wv = vdupq_n_f32(w);
1109 let mut j = 0usize;
1110 while j + 16 <= n {
1111 let rb = vld1q_s8(rp.add(j));
1112 let lo = vmovl_s8(vget_low_s8(rb));
1113 let hi = vmovl_s8(vget_high_s8(rb));
1114 for (off, half) in [(0, lo), (8, hi)] {
1115 let f0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(half)));
1116 let f1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(half)));
1117 let o = j + off;
1118 vst1q_f32(ap.add(o), vfmaq_f32(vld1q_f32(ap.add(o)), wv, f0));
1119 vst1q_f32(ap.add(o + 4), vfmaq_f32(vld1q_f32(ap.add(o + 4)), wv, f1));
1120 }
1121 j += 16;
1122 }
1123 while j < n {
1124 *ap.add(j) += w * (*rp.add(j)) as f32;
1125 j += 1;
1126 }
1127}
1128}
1129
1130#[inline]
1133pub(crate) fn dot_i8_f32(w: &[u8], x: &[f32]) -> f32 {
1134 #[cfg(target_arch = "aarch64")]
1135 unsafe {
1136 return dot_i8_f32_neon(w, x);
1137 }
1138 #[allow(unreachable_code)]
1139 {
1140 let mut sum = 0.0f32;
1141 for (j, &b) in w.iter().enumerate() {
1142 sum += (b as i8) as f32 * x[j];
1143 }
1144 sum
1145 }
1146}
1147
1148#[inline]
1152fn dot_i8_col_f32(w: &[u8], x: &[f32], col: &[f32]) -> f32 {
1153 #[cfg(target_arch = "aarch64")]
1154 unsafe {
1155 return dot_i8_col_f32_neon(w, x, col);
1156 }
1157 #[allow(unreachable_code)]
1158 {
1159 let mut sum = 0.0f32;
1160 for (j, &b) in w.iter().enumerate() {
1161 sum += (b as i8) as f32 * x[j] * col[j];
1162 }
1163 sum
1164 }
1165}
1166
1167#[cfg(target_arch = "aarch64")]
1168#[target_feature(enable = "neon")]
1169unsafe fn dot_i8_col_f32_neon(w: &[u8], x: &[f32], col: &[f32]) -> f32 {
1170 unsafe {
1172 use core::arch::aarch64::*;
1173 let n = x.len();
1174 let wp = w.as_ptr() as *const i8;
1175 let xp = x.as_ptr();
1176 let cp = col.as_ptr();
1177 let (mut a0, mut a1, mut a2, mut a3) =
1178 (vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0));
1179 let mut j = 0usize;
1180 while j + 16 <= n {
1181 let wb = vld1q_s8(wp.add(j));
1182 let lo = vmovl_s8(vget_low_s8(wb));
1183 let hi = vmovl_s8(vget_high_s8(wb));
1184 let w0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo)));
1185 let w1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo)));
1186 let w2 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi)));
1187 let w3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi)));
1188 a0 = vfmaq_f32(a0, w0, vmulq_f32(vld1q_f32(xp.add(j)), vld1q_f32(cp.add(j))));
1189 a1 = vfmaq_f32(a1, w1, vmulq_f32(vld1q_f32(xp.add(j + 4)), vld1q_f32(cp.add(j + 4))));
1190 a2 = vfmaq_f32(a2, w2, vmulq_f32(vld1q_f32(xp.add(j + 8)), vld1q_f32(cp.add(j + 8))));
1191 a3 = vfmaq_f32(a3, w3, vmulq_f32(vld1q_f32(xp.add(j + 12)), vld1q_f32(cp.add(j + 12))));
1192 j += 16;
1193 }
1194 let mut sum = vaddvq_f32(vaddq_f32(vaddq_f32(a0, a1), vaddq_f32(a2, a3)));
1195 while j < n {
1196 sum += (*wp.add(j)) as f32 * *xp.add(j) * *cp.add(j);
1197 j += 1;
1198 }
1199 sum
1200}
1201}
1202
1203#[cfg(target_arch = "aarch64")]
1204#[target_feature(enable = "neon")]
1205unsafe fn dot_i8_f32_neon(w: &[u8], x: &[f32]) -> f32 {
1206 unsafe {
1208 use core::arch::aarch64::*;
1209 let n = x.len();
1210 let wp = w.as_ptr() as *const i8;
1211 let xp = x.as_ptr();
1212 let (mut a0, mut a1, mut a2, mut a3) =
1213 (vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0), vdupq_n_f32(0.0));
1214 let mut j = 0usize;
1215 while j + 16 <= n {
1216 let wb = vld1q_s8(wp.add(j));
1217 let lo = vmovl_s8(vget_low_s8(wb));
1218 let hi = vmovl_s8(vget_high_s8(wb));
1219 let w0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo)));
1220 let w1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo)));
1221 let w2 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi)));
1222 let w3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi)));
1223 a0 = vfmaq_f32(a0, w0, vld1q_f32(xp.add(j)));
1224 a1 = vfmaq_f32(a1, w1, vld1q_f32(xp.add(j + 4)));
1225 a2 = vfmaq_f32(a2, w2, vld1q_f32(xp.add(j + 8)));
1226 a3 = vfmaq_f32(a3, w3, vld1q_f32(xp.add(j + 12)));
1227 j += 16;
1228 }
1229 let mut sum = vaddvq_f32(vaddq_f32(vaddq_f32(a0, a1), vaddq_f32(a2, a3)));
1230 while j < n {
1231 sum += (*wp.add(j)) as f32 * *xp.add(j);
1232 j += 1;
1233 }
1234 sum
1235}
1236}
1237
1238#[allow(clippy::too_many_arguments)]
1239fn qmatvec(
1240 q: &[u8],
1241 row_scale: &[f32],
1242 xs: &[f32],
1243 rows: usize,
1244 cols: usize,
1245 out: &mut [f32],
1246 pool: Option<&Pool>,
1247) {
1248 debug_assert_eq!(out.len(), rows);
1249
1250 #[cfg(target_arch = "aarch64")]
1251 if sdot_enabled() {
1252 let act = split_act(xs);
1253 let out_addr = SendMut(out.as_mut_ptr());
1254 let run_range = |start: usize, end: usize| {
1255 let mut o = start;
1256 while o + 4 <= end {
1257 let r = unsafe {
1258 dot_i8_sdot_4rows(
1259 &q[o * cols..(o + 1) * cols],
1260 &q[(o + 1) * cols..(o + 2) * cols],
1261 &q[(o + 2) * cols..(o + 3) * cols],
1262 &q[(o + 3) * cols..(o + 4) * cols],
1263 &act.xq,
1264 )
1265 };
1266 for k in 0..4 {
1267 let mut acc = r[k] as f32 * act.sx;
1268 for &(j, xv) in &act.outliers {
1269 acc += (q[(o + k) * cols + j] as i8) as f32 * xv;
1270 }
1271 unsafe { *out_addr.at(o + k) = acc * row_scale[o + k] };
1273 }
1274 o += 4;
1275 }
1276 while o < end {
1277 let v = row_dot_sdot(&q[o * cols..(o + 1) * cols], &act) * row_scale[o];
1278 unsafe { *out_addr.at(o) = v };
1279 o += 1;
1280 }
1281 };
1282 match pool {
1283 Some(pool) if rows >= 256 => {
1284 pool.run(&|widx, n| {
1285 let chunk = rows.div_ceil(n);
1286 let start = widx * chunk;
1287 run_range(start, (start + chunk).min(rows));
1288 });
1289 }
1290 _ => run_range(0, rows),
1291 }
1292 return;
1293 }
1294
1295 let row_dot = |o: usize| -> f32 { dot_i8_f32(&q[o * cols..(o + 1) * cols], xs) * row_scale[o] };
1296 match pool {
1297 Some(pool) if rows >= 256 => {
1298 let out_addr = SendMut(out.as_mut_ptr());
1299 pool.run(&move |widx, n| {
1300 let chunk = rows.div_ceil(n);
1301 let start = widx * chunk;
1302 let end = (start + chunk).min(rows);
1303 for o in start..end {
1304 unsafe { *out_addr.at(o) = row_dot(o) };
1306 }
1307 });
1308 }
1309 _ => {
1310 for (o, dst) in out.iter_mut().enumerate() {
1311 *dst = row_dot(o);
1312 }
1313 }
1314 }
1315}
1316
1317#[allow(clippy::too_many_arguments)]
1318fn qmatvec2(
1319 q: &[u8],
1320 row_scale: &[f32],
1321 x1: &[f32],
1322 x2: &[f32],
1323 rows: usize,
1324 cols: usize,
1325 o1: &mut [f32],
1326 o2: &mut [f32],
1327 pool: Option<&Pool>,
1328) {
1329 #[cfg(target_arch = "aarch64")]
1330 if sdot_enabled() {
1331 let a1s = split_act(x1);
1332 let a2s = split_act(x2);
1333 let p1 = SendMut(o1.as_mut_ptr());
1334 let p2 = SendMut(o2.as_mut_ptr());
1335 let run_range = |start: usize, end: usize| {
1336 for o in start..end {
1337 let row = &q[o * cols..(o + 1) * cols];
1338 unsafe {
1340 *p1.at(o) = row_dot_sdot(row, &a1s) * row_scale[o];
1341 *p2.at(o) = row_dot_sdot(row, &a2s) * row_scale[o];
1342 }
1343 }
1344 };
1345 match pool {
1346 Some(pool) if rows >= 256 => {
1347 pool.run(&|widx, n| {
1348 let chunk = rows.div_ceil(n);
1349 let start = widx * chunk;
1350 run_range(start, (start + chunk).min(rows));
1351 });
1352 }
1353 _ => run_range(0, rows),
1354 }
1355 return;
1356 }
1357
1358 let row_dots = |o: usize| -> (f32, f32) {
1359 let row = &q[o * cols..(o + 1) * cols];
1360 (dot_i8_f32(row, x1) * row_scale[o], dot_i8_f32(row, x2) * row_scale[o])
1361 };
1362 match pool {
1363 Some(pool) if rows >= 256 => {
1364 let p1 = SendMut(o1.as_mut_ptr());
1365 let p2 = SendMut(o2.as_mut_ptr());
1366 pool.run(&move |widx, n| {
1367 let chunk = rows.div_ceil(n);
1368 let start = widx * chunk;
1369 let end = (start + chunk).min(rows);
1370 for o in start..end {
1371 let (s1, s2) = row_dots(o);
1372 unsafe {
1374 *p1.at(o) = s1;
1375 *p2.at(o) = s2;
1376 }
1377 }
1378 });
1379 }
1380 _ => {
1381 for o in 0..rows {
1382 let (s1, s2) = row_dots(o);
1383 o1[o] = s1;
1384 o2[o] = s2;
1385 }
1386 }
1387 }
1388}
1389
1390#[derive(Clone, Copy)]
1391struct SendMut(*mut f32);
1392unsafe impl Send for SendMut {}
1393unsafe impl Sync for SendMut {}
1394
1395impl SendMut {
1396 #[inline]
1397 fn at(self, i: usize) -> *mut f32 {
1398 unsafe { self.0.add(i) }
1399 }
1400}
1401
1402#[cfg(test)]
1403mod tests {
1404 use super::*;
1405
1406 #[test]
1407 fn f32_matvec_matches_matvec_rows_bitexact() {
1408 let (rows, cols) = (300, 40);
1409 let w: Vec<f32> = (0..rows * cols).map(|i| (i as f32 * 0.017).sin()).collect();
1410 let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.05).cos()).collect();
1411 let qt = QTensor::from_f32(w.clone(), rows, cols);
1412
1413 let mut a = vec![0.0f32; rows];
1414 matvec_rows(None, &w, &x, &mut a);
1415 let mut b = vec![0.0f32; rows];
1416 qt.matvec(&x, &mut b, None);
1417 assert_eq!(a, b);
1418 }
1419
1420 #[test]
1421 fn sdot_kernel_exact_on_grid() {
1422 eprintln!("sdot_enabled = {}", sdot_enabled());
1427 let (rows, cols) = (9, 80); let w: Vec<u8> = (0..rows * cols)
1429 .map(|i| (((i * 37) % 251) as i32 - 125) as i8 as u8)
1430 .collect();
1431 let scales: Vec<f32> = (0..rows).map(|o| 0.005 + o as f32 * 0.001).collect();
1432 let x: Vec<f32> = (0..cols)
1433 .map(|i| match i % 3 {
1434 0 => 1.0,
1435 1 => -1.0,
1436 _ => 0.0,
1437 })
1438 .collect();
1439 let mut a = vec![0.0f32; rows];
1440 qmatvec(&w, &scales, &x, rows, cols, &mut a, None);
1441 for o in 0..rows {
1442 let mut acc = 0.0f32;
1443 for j in 0..cols {
1444 acc += (w[o * cols + j] as i8) as f32 * x[j];
1445 }
1446 let expect = acc * scales[o];
1447 assert!(
1448 (a[o] - expect).abs() < 1e-3 * expect.abs().max(1e-3),
1449 "row {o}: {} vs {expect}",
1450 a[o]
1451 );
1452 }
1453 }
1454
1455 #[test]
1456 fn sdot_a8w8_noise_is_bounded() {
1457 let (rows, cols) = (16, 512);
1461 let w: Vec<u8> = (0..rows * cols)
1462 .map(|i| (((i * 37) % 251) as i32 - 125) as i8 as u8)
1463 .collect();
1464 let scales = vec![0.01f32; rows];
1465 let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.21).sin()).collect();
1466 let mut a = vec![0.0f32; rows];
1467 qmatvec(&w, &scales, &x, rows, cols, &mut a, None);
1468 let (mut num, mut den) = (0f64, 0f64);
1469 for o in 0..rows {
1470 let mut acc = 0.0f32;
1471 for j in 0..cols {
1472 acc += (w[o * cols + j] as i8) as f32 * x[j];
1473 }
1474 let expect = acc * scales[o];
1475 num += ((a[o] - expect) as f64).powi(2);
1476 den += (expect as f64).powi(2);
1477 }
1478 let rel = (num / den.max(1e-12)).sqrt();
1479 assert!(rel < 0.05, "A8W8 relative L2 error too high: {rel}");
1480 }
1481
1482 #[test]
1483 fn i8_dot_neon_matches_scalar() {
1484 let n = 100;
1485 let w: Vec<u8> = (0..n).map(|i| ((i * 37 + 11) % 251) as u8).collect();
1486 let x: Vec<f32> = (0..n).map(|i| (i as f32 * 0.13).sin()).collect();
1487 let mut scalar = 0.0f32;
1488 for j in 0..n {
1489 scalar += (w[j] as i8) as f32 * x[j];
1490 }
1491 let fast = dot_i8_f32(&w, &x);
1492 assert!((scalar - fast).abs() < 1e-3 * scalar.abs().max(1.0));
1493 }
1494
1495 #[test]
1497 fn vbitmatvec_matches_full_dequant() {
1498 let (rows, cols) = (6, 64);
1499 let ng = cols / GROUP_SIZE;
1500 let bits: Vec<u8> = vec![3, 4, 5, 6, 8, 4];
1502 let mut bytes = bits.clone();
1503 for g in 0..rows * ng {
1504 let s = 0.02 + 0.001 * g as f32;
1505 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
1506 }
1507 for r in 0..rows {
1508 let b = bits[r] as usize;
1509 let (mut acc, mut nb) = (0u64, 0usize);
1510 let mut rowbytes = Vec::new();
1511 for i in 0..cols {
1512 let v = ((i * 7 + r * 13) % (1 << b)) as u64;
1513 acc = (acc << b) | v;
1514 nb += b;
1515 while nb >= 8 {
1516 nb -= 8;
1517 rowbytes.push(((acc >> nb) & 0xFF) as u8);
1518 }
1519 }
1520 if nb > 0 {
1521 rowbytes.push(((acc << (8 - nb)) & 0xFF) as u8);
1522 }
1523 bytes.extend_from_slice(&rowbytes);
1524 }
1525 let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.19).sin()).collect();
1526
1527 let mut reference = vec![0f32; rows * cols];
1528 cortiq_core::quant::dequant_vbit(&bytes, rows, cols, &mut reference).unwrap();
1529 let mut expect = vec![0f32; rows];
1530 for r in 0..rows {
1531 expect[r] = reference[r * cols..(r + 1) * cols]
1532 .iter()
1533 .zip(&x)
1534 .map(|(w, xv)| w * xv)
1535 .sum();
1536 }
1537 let mut got = vec![0f32; rows];
1538 vbitmatvec(&bytes, &x, rows, cols, &mut got, None);
1539 let tol = if sdot_enabled() { 6e-2 } else { 1e-4 };
1543 let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1e-6);
1544 for r in 0..rows {
1545 assert!(
1546 (got[r] - expect[r]).abs() < tol * scale,
1547 "row {r}: {} vs {}",
1548 got[r],
1549 expect[r]
1550 );
1551 }
1552 }
1553
1554 #[test]
1557 fn q4matvec_matches_full_dequant() {
1558 let (rows, cols) = (8, 64);
1559 let groups = rows * cols / GROUP_SIZE;
1560 let mut bytes = Vec::with_capacity(groups * 16 + groups * 2);
1562 for i in 0..groups * 16 {
1563 bytes.push((((i * 7 + 3) % 256) & 0xFF) as u8);
1564 }
1565 for g in 0..groups {
1566 let s = 0.01 + 0.003 * g as f32;
1567 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
1568 }
1569 let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.17).sin()).collect();
1570
1571 let mut reference = vec![0.0f32; rows * cols];
1572 cortiq_core::quant::dequant_q4_block(&bytes, &mut reference);
1573 let mut expect = vec![0.0f32; rows];
1574 for r in 0..rows {
1575 expect[r] = reference[r * cols..(r + 1) * cols]
1576 .iter()
1577 .zip(&x)
1578 .map(|(w, xv)| w * xv)
1579 .sum();
1580 }
1581
1582 let mut got = vec![0.0f32; rows];
1583 q4matvec(&bytes, &x, rows, cols, &mut got, None);
1584 let tol = if sdot_enabled() { 6e-2 } else { 1e-4 };
1588 let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1.0);
1589 for r in 0..rows {
1590 assert!(
1591 (got[r] - expect[r]).abs() < tol * scale,
1592 "row {r}: {} vs {}",
1593 got[r],
1594 expect[r]
1595 );
1596 }
1597 }
1598
1599 #[test]
1606 fn q4matvec_sdot_outlier_exact() {
1607 let (rows, cols) = (4, 128);
1608 let groups = rows * cols / GROUP_SIZE;
1609 let mut bytes = Vec::with_capacity(groups * 16 + groups * 2);
1610 for i in 0..groups * 16 {
1611 bytes.push(((i * 11 + 5) % 256) as u8);
1612 }
1613 for g in 0..groups {
1614 let s = 0.02 + 0.002 * g as f32;
1615 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(s).to_le_bytes());
1616 }
1617 let mut x: Vec<f32> = (0..cols)
1618 .map(|i| match i % 3 {
1619 0 => 1.0,
1620 1 => -1.0,
1621 _ => 0.0,
1622 })
1623 .collect();
1624 x[17] = 300.0; let mut reference = vec![0.0f32; rows * cols];
1627 cortiq_core::quant::dequant_q4_block(&bytes, &mut reference);
1628 let mut expect = vec![0.0f32; rows];
1629 for r in 0..rows {
1630 expect[r] = reference[r * cols..(r + 1) * cols]
1631 .iter()
1632 .zip(&x)
1633 .map(|(w, xv)| w * xv)
1634 .sum();
1635 }
1636 let mut got = vec![0.0f32; rows];
1637 q4matvec(&bytes, &x, rows, cols, &mut got, None);
1638 let scale = expect.iter().fold(0f32, |m, v| m.max(v.abs())).max(1.0);
1639 for r in 0..rows {
1640 assert!(
1641 (got[r] - expect[r]).abs() < 2e-3 * scale,
1642 "row {r}: {} vs {} (outlier term must be exact)",
1643 got[r],
1644 expect[r]
1645 );
1646 }
1647 }
1648}