1use std::os::raw::c_int;
13use std::os::raw::c_void;
14use std::sync::Once;
15
16use rayon::prelude::*;
17
18unsafe extern "C" {
19 fn mlas_sgemm(
22 trans_a: c_int,
23 trans_b: c_int,
24 m: usize,
25 n: usize,
26 k: usize,
27 alpha: f32,
28 a: *const f32,
29 lda: usize,
30 b: *const f32,
31 ldb: usize,
32 beta: f32,
33 c: *mut f32,
34 ldc: usize,
35 );
36
37 fn mlas_sgemm_pack_b_size(trans_a: c_int, trans_b: c_int, n: usize, k: usize) -> usize;
38 fn mlas_sgemm_pack_b(
39 trans_a: c_int,
40 trans_b: c_int,
41 n: usize,
42 k: usize,
43 b: *const f32,
44 ldb: usize,
45 packed_b: *mut u8,
46 );
47 fn mlas_sgemm_packed(
48 trans_a: c_int,
49 trans_b: c_int,
50 m: usize,
51 n: usize,
52 k: usize,
53 alpha: f32,
54 a: *const f32,
55 lda: usize,
56 packed_b: *const u8,
57 beta: f32,
58 c: *mut f32,
59 ldc: usize,
60 );
61
62 fn mlas_float_kernel_id() -> c_int;
63
64 fn mlas_qnbit_gemm_available(bits: usize, blk_len: usize, comp_type: c_int) -> c_int;
66 fn mlas_qnbit_gemm_pack_b_size(
67 n: usize,
68 k: usize,
69 bits: usize,
70 blk_len: usize,
71 has_zp: c_int,
72 comp_type: c_int,
73 ) -> usize;
74 fn mlas_qnbit_gemm_pack_b(
75 n: usize,
76 k: usize,
77 bits: usize,
78 blk_len: usize,
79 comp_type: c_int,
80 quant_b_data: *const c_void,
81 packed_b: *mut u8,
82 quant_b_scale: *const f32,
83 has_zp: c_int,
84 quant_b_zero_point: *const c_void,
85 );
86 fn mlas_qnbit_gemm_workspace_size(
87 m: usize,
88 n: usize,
89 k: usize,
90 bits: usize,
91 blk_len: usize,
92 has_zp: c_int,
93 comp_type: c_int,
94 ) -> usize;
95 #[allow(clippy::too_many_arguments)]
96 fn mlas_qnbit_gemm(
97 m: usize,
98 n: usize,
99 k: usize,
100 bits: usize,
101 blk_len: usize,
102 comp_type: c_int,
103 a: *const f32,
104 lda: usize,
105 packed_b: *const u8,
106 quant_b_scale: *const f32,
107 has_zp: c_int,
108 quant_b_zero_point: *const c_void,
109 bias: *const f32,
110 c: *mut f32,
111 ldc: usize,
112 workspace: *mut u8,
113 multithread: c_int,
114 );
115
116 fn mlas_set_threading(
120 parallel_for: MlasParallelForFn,
121 max_threads: MlasMaxThreadsFn,
122 rust_ctx: *mut c_void,
123 );
124}
125
126type MlasTaskFn = unsafe extern "C" fn(task_ctx: *mut c_void, tid: isize);
128type MlasParallelForFn = unsafe extern "C" fn(
130 rust_ctx: *mut c_void,
131 iterations: isize,
132 task: MlasTaskFn,
133 task_ctx: *mut c_void,
134);
135type MlasMaxThreadsFn = unsafe extern "C" fn(rust_ctx: *mut c_void) -> c_int;
137
138unsafe extern "C" fn rayon_parallel_for(
142 _rust_ctx: *mut c_void,
143 iterations: isize,
144 task: MlasTaskFn,
145 task_ctx: *mut c_void,
146) {
147 if iterations <= 0 {
148 return;
149 }
150 let task_ctx = task_ctx as usize;
155 (0..iterations).into_par_iter().for_each(|tid| {
156 unsafe { task(task_ctx as *mut c_void, tid) };
159 });
160}
161
162unsafe extern "C" fn rayon_max_threads(_rust_ctx: *mut c_void) -> c_int {
165 rayon::current_num_threads().max(1) as c_int
166}
167
168static THREADING_INIT: Once = Once::new();
169
170fn ensure_threading() {
175 THREADING_INIT.call_once(|| unsafe {
176 mlas_set_threading(rayon_parallel_for, rayon_max_threads, std::ptr::null_mut());
177 });
178}
179
180pub fn selected_float_kernel() -> i32 {
183 unsafe { mlas_float_kernel_id() as i32 }
184}
185
186pub struct PackedB {
192 ptr: *mut u8,
193 layout: std::alloc::Layout,
194 n: usize,
195 k: usize,
196}
197
198unsafe impl Send for PackedB {}
201unsafe impl Sync for PackedB {}
202
203impl PackedB {
204 pub fn new(n: usize, k: usize, b: &[f32]) -> Self {
206 assert_eq!(b.len(), k * n);
207 let size = unsafe { mlas_sgemm_pack_b_size(0, 0, n, k) }.max(1);
208 let layout = std::alloc::Layout::from_size_align(size, 64).unwrap();
209 let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
210 assert!(!ptr.is_null(), "packed-B allocation failed");
211 unsafe { mlas_sgemm_pack_b(0, 0, n, k, b.as_ptr(), n, ptr) };
212 Self { ptr, layout, n, k }
213 }
214
215 pub fn dimensions(&self) -> (usize, usize) {
217 (self.k, self.n)
218 }
219}
220
221impl Drop for PackedB {
222 fn drop(&mut self) {
223 unsafe { std::alloc::dealloc(self.ptr, self.layout) };
224 }
225}
226
227pub fn sgemm_nn_packed(m: usize, a: &[f32], packed: &PackedB, c: &mut [f32]) {
229 let (n, k) = (packed.n, packed.k);
230 assert_eq!(a.len(), m * k);
231 assert_eq!(c.len(), m * n);
232 ensure_threading();
233 unsafe {
234 mlas_sgemm_packed(
235 0,
236 0,
237 m,
238 n,
239 k,
240 1.0,
241 a.as_ptr(),
242 k,
243 packed.ptr,
244 0.0,
245 c.as_mut_ptr(),
246 n,
247 );
248 }
249}
250
251pub fn sgemm_nn(m: usize, n: usize, k: usize, a: &[f32], b: &[f32], c: &mut [f32]) {
256 assert_eq!(a.len(), m * k, "A must be m*k");
257 assert_eq!(b.len(), k * n, "B must be k*n");
258 assert_eq!(c.len(), m * n, "C must be m*n");
259 ensure_threading();
260 unsafe {
261 mlas_sgemm(
262 0,
263 0,
264 m,
265 n,
266 k,
267 1.0,
268 a.as_ptr(),
269 k,
270 b.as_ptr(),
271 n,
272 0.0,
273 c.as_mut_ptr(),
274 n,
275 );
276 }
277}
278
279#[allow(clippy::too_many_arguments)]
282pub fn sgemm(
283 trans_a: bool,
284 trans_b: bool,
285 m: usize,
286 n: usize,
287 k: usize,
288 alpha: f32,
289 a: &[f32],
290 lda: usize,
291 b: &[f32],
292 ldb: usize,
293 beta: f32,
294 c: &mut [f32],
295 ldc: usize,
296) {
297 ensure_threading();
298 unsafe {
299 mlas_sgemm(
300 trans_a as c_int,
301 trans_b as c_int,
302 m,
303 n,
304 k,
305 alpha,
306 a.as_ptr(),
307 lda,
308 b.as_ptr(),
309 ldb,
310 beta,
311 c.as_mut_ptr(),
312 ldc,
313 );
314 }
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
321pub enum SQNBitComputeType {
322 Fp32,
324 Int8,
327}
328
329impl SQNBitComputeType {
330 #[inline]
331 fn raw(self) -> c_int {
332 match self {
335 SQNBitComputeType::Fp32 => 0, SQNBitComputeType::Int8 => 3, }
338 }
339}
340
341pub fn sqnbit_gemm_available(bits: usize, blk_len: usize, comp: SQNBitComputeType) -> bool {
345 unsafe { mlas_qnbit_gemm_available(bits, blk_len, comp.raw()) != 0 }
346}
347
348pub struct SQNBitPackedB {
361 ptr: *mut u8,
362 layout: std::alloc::Layout,
363 n: usize,
364 k: usize,
365 bits: usize,
366 blk_len: usize,
367 comp: SQNBitComputeType,
368 has_zp: bool,
369 scale: Vec<f32>,
370 zp: Option<Vec<u8>>,
371}
372
373unsafe impl Send for SQNBitPackedB {}
378unsafe impl Sync for SQNBitPackedB {}
379
380impl SQNBitPackedB {
381 #[allow(clippy::too_many_arguments)]
385 pub fn new(
386 n: usize,
387 k: usize,
388 bits: usize,
389 blk_len: usize,
390 comp: SQNBitComputeType,
391 quant_b_data: &[u8],
392 scale: &[f32],
393 zp: Option<&[u8]>,
394 ) -> Option<Self> {
395 if !sqnbit_gemm_available(bits, blk_len, comp) {
396 return None;
397 }
398 let has_zp = zp.is_some();
399 let size = unsafe {
400 mlas_qnbit_gemm_pack_b_size(n, k, bits, blk_len, has_zp as c_int, comp.raw())
401 };
402 if size == 0 {
403 return None;
404 }
405 let layout = std::alloc::Layout::from_size_align(size, 64).unwrap();
406 let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
407 assert!(!ptr.is_null(), "SQNBit packed-B allocation failed");
408 let zp_ptr = zp.map_or(std::ptr::null(), |z| z.as_ptr()) as *const c_void;
409 unsafe {
410 mlas_qnbit_gemm_pack_b(
411 n,
412 k,
413 bits,
414 blk_len,
415 comp.raw(),
416 quant_b_data.as_ptr() as *const c_void,
417 ptr,
418 scale.as_ptr(),
419 has_zp as c_int,
420 zp_ptr,
421 );
422 }
423 Some(Self {
424 ptr,
425 layout,
426 n,
427 k,
428 bits,
429 blk_len,
430 comp,
431 has_zp,
432 scale: scale.to_vec(),
433 zp: zp.map(<[u8]>::to_vec),
434 })
435 }
436
437 pub fn dimensions(&self) -> (usize, usize) {
439 (self.k, self.n)
440 }
441}
442
443impl Drop for SQNBitPackedB {
444 fn drop(&mut self) {
445 unsafe { std::alloc::dealloc(self.ptr, self.layout) };
446 }
447}
448
449pub fn sqnbit_gemm(
456 packed: &SQNBitPackedB,
457 m: usize,
458 a: &[f32],
459 bias: Option<&[f32]>,
460 c: &mut [f32],
461 multithread: bool,
462) {
463 let (k, n) = (packed.k, packed.n);
464 assert_eq!(a.len(), m * k, "A must be m*k");
465 assert_eq!(c.len(), m * n, "C must be m*n");
466 if let Some(bias) = bias {
467 assert_eq!(bias.len(), n, "bias must be length n");
468 }
469 ensure_threading();
470
471 let ws_size = unsafe {
472 mlas_qnbit_gemm_workspace_size(
473 m,
474 n,
475 k,
476 packed.bits,
477 packed.blk_len,
478 packed.has_zp as c_int,
479 packed.comp.raw(),
480 )
481 };
482 let mut workspace: Vec<u8> = if ws_size == 0 {
485 Vec::new()
486 } else {
487 vec![0u8; ws_size + 64]
488 };
489 let ws_ptr = if ws_size == 0 {
490 std::ptr::null_mut()
491 } else {
492 workspace.as_mut_ptr()
493 };
494
495 let zp_ptr = packed.zp.as_ref().map_or(std::ptr::null(), |z| z.as_ptr()) as *const c_void;
496 let bias_ptr = bias.map_or(std::ptr::null(), <[f32]>::as_ptr);
497
498 unsafe {
499 mlas_qnbit_gemm(
500 m,
501 n,
502 k,
503 packed.bits,
504 packed.blk_len,
505 packed.comp.raw(),
506 a.as_ptr(),
507 k,
508 packed.ptr,
509 packed.scale.as_ptr(),
510 packed.has_zp as c_int,
511 zp_ptr,
512 bias_ptr,
513 c.as_mut_ptr(),
514 n,
515 ws_ptr,
516 multithread as c_int,
517 );
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 fn assert_send_sync<T: Send + Sync>() {}
526
527 #[test]
528 fn packed_b_is_send_sync() {
529 assert_send_sync::<PackedB>();
530 }
531
532 #[allow(clippy::too_many_arguments)]
534 fn ref_sgemm(
535 trans_a: bool,
536 trans_b: bool,
537 m: usize,
538 n: usize,
539 k: usize,
540 alpha: f32,
541 a: &[f32],
542 lda: usize,
543 b: &[f32],
544 ldb: usize,
545 beta: f32,
546 c: &mut [f32],
547 ldc: usize,
548 ) {
549 for i in 0..m {
550 for j in 0..n {
551 let mut acc = 0.0f32;
552 for p in 0..k {
553 let av = if trans_a {
554 a[p * lda + i]
555 } else {
556 a[i * lda + p]
557 };
558 let bv = if trans_b {
559 b[j * ldb + p]
560 } else {
561 b[p * ldb + j]
562 };
563 acc += av * bv;
564 }
565 let cell = &mut c[i * ldc + j];
566 *cell = alpha * acc + beta * *cell;
567 }
568 }
569 }
570
571 fn seq(n: usize, seed: f32) -> Vec<f32> {
572 (0..n)
574 .map(|i| ((i as f32 * 0.013 + seed).sin()) * 2.0)
575 .collect()
576 }
577
578 fn assert_close(a: &[f32], b: &[f32], tol: f32, ctx: &str) {
579 assert_eq!(a.len(), b.len());
580 for (idx, (x, y)) in a.iter().zip(b.iter()).enumerate() {
581 let diff = (x - y).abs();
582 let rel = diff / (y.abs().max(1.0));
583 assert!(
584 diff <= tol || rel <= tol,
585 "{ctx}: mismatch at {idx}: mlas={x} ref={y} diff={diff}"
586 );
587 }
588 }
589
590 fn check_nn(m: usize, n: usize, k: usize) {
591 let a = seq(m * k, 0.5);
592 let b = seq(k * n, 1.5);
593 let mut c_mlas = vec![0.0f32; m * n];
594 let mut c_ref = vec![0.0f32; m * n];
595 sgemm_nn(m, n, k, &a, &b, &mut c_mlas);
596 ref_sgemm(false, false, m, n, k, 1.0, &a, k, &b, n, 0.0, &mut c_ref, n);
597 assert_close(&c_mlas, &c_ref, 1e-3, &format!("nn {m}x{n}x{k}"));
598 }
599
600 #[test]
601 fn correctness_square() {
602 check_nn(64, 64, 64);
603 }
604
605 #[test]
606 fn correctness_non_square_and_non_tile_multiples() {
607 check_nn(1, 1, 1);
609 check_nn(3, 5, 7);
610 check_nn(17, 31, 13);
611 check_nn(32, 512, 512);
612 check_nn(33, 65, 129);
613 check_nn(100, 1, 100);
614 check_nn(1, 100, 100);
615 }
616
617 #[test]
618 fn correctness_alpha_beta() {
619 let (m, n, k) = (23, 19, 41);
620 let a = seq(m * k, 0.2);
621 let b = seq(k * n, 0.7);
622 let base = seq(m * n, 2.0);
623 let mut c_mlas = base.clone();
624 let mut c_ref = base.clone();
625 sgemm(
626 false,
627 false,
628 m,
629 n,
630 k,
631 0.5,
632 &a,
633 k,
634 &b,
635 n,
636 2.0,
637 &mut c_mlas,
638 n,
639 );
640 ref_sgemm(false, false, m, n, k, 0.5, &a, k, &b, n, 2.0, &mut c_ref, n);
641 assert_close(&c_mlas, &c_ref, 1e-3, "alpha_beta");
642 }
643
644 #[test]
645 fn correctness_transpose_b() {
646 let (m, n, k) = (12, 20, 28);
648 let a = seq(m * k, 0.3);
649 let b_t = seq(n * k, 0.9); let mut c_mlas = vec![0.0f32; m * n];
651 let mut c_ref = vec![0.0f32; m * n];
652 sgemm(
653 false,
654 true,
655 m,
656 n,
657 k,
658 1.0,
659 &a,
660 k,
661 &b_t,
662 k,
663 0.0,
664 &mut c_mlas,
665 n,
666 );
667 ref_sgemm(
668 false, true, m, n, k, 1.0, &a, k, &b_t, k, 0.0, &mut c_ref, n,
669 );
670 assert_close(&c_mlas, &c_ref, 1e-3, "transpose_b");
671 }
672
673 #[test]
674 fn correctness_transpose_a() {
675 let (m, n, k) = (14, 22, 18);
677 let a_t = seq(k * m, 0.4); let b = seq(k * n, 0.6);
679 let mut c_mlas = vec![0.0f32; m * n];
680 let mut c_ref = vec![0.0f32; m * n];
681 sgemm(
682 true,
683 false,
684 m,
685 n,
686 k,
687 1.0,
688 &a_t,
689 m,
690 &b,
691 n,
692 0.0,
693 &mut c_mlas,
694 n,
695 );
696 ref_sgemm(
697 true, false, m, n, k, 1.0, &a_t, m, &b, n, 0.0, &mut c_ref, n,
698 );
699 assert_close(&c_mlas, &c_ref, 1e-3, "transpose_a");
700 }
701
702 #[test]
703 fn correctness_packed_b() {
704 for (m, n, k) in [(32usize, 512usize, 512usize), (7, 13, 19), (1, 64, 64)] {
705 let a = seq(m * k, 0.5);
706 let b = seq(k * n, 1.5);
707 let mut c_mlas = vec![0.0f32; m * n];
708 let mut c_ref = vec![0.0f32; m * n];
709 let packed = PackedB::new(n, k, &b);
710 sgemm_nn_packed(m, &a, &packed, &mut c_mlas);
711 ref_sgemm(false, false, m, n, k, 1.0, &a, k, &b, n, 0.0, &mut c_ref, n);
712 assert_close(&c_mlas, &c_ref, 1e-3, &format!("packed {m}x{n}x{k}"));
713 }
714 }
715
716 #[test]
717 fn avx512_kernel_is_selected() {
718 let id = selected_float_kernel();
721 eprintln!("selected f32 GEMM kernel id = {id} (512 = AVX-512F)");
722 assert_eq!(id, 512, "expected AVX-512F SGEMM kernel to be selected");
723 }
724
725 #[test]
730 #[ignore = "perf probe; run explicitly with --ignored --nocapture"]
731 fn perf_sgemm_medium() {
732 use std::time::Instant;
733
734 let (m, n, k) = (32usize, 512usize, 512usize);
735 let a = seq(m * k, 0.5);
736 let b = seq(k * n, 1.5);
737 let mut c = vec![0.0f32; m * n];
738
739 for _ in 0..50 {
741 sgemm_nn(m, n, k, &a, &b, &mut c);
742 }
743
744 let iters = 5000u32;
745 let start = Instant::now();
746 for _ in 0..iters {
747 sgemm_nn(m, n, k, &a, &b, &mut c);
748 }
749 let elapsed = start.elapsed();
750 let checksum: f32 = c.iter().copied().sum();
752
753 let per_us = elapsed.as_secs_f64() * 1e6 / iters as f64;
754 let flops = 2.0 * m as f64 * n as f64 * k as f64;
755 let gflops = flops / (per_us * 1e3);
756 eprintln!(
757 "vendored-MLAS SGEMM 32x512x512 single-thread (repack B/call): {per_us:.1} us/iter \
758 ({gflops:.1} GFLOP/s), checksum={checksum:.3}"
759 );
760
761 let packed = PackedB::new(n, k, &b);
763 for _ in 0..50 {
764 sgemm_nn_packed(m, &a, &packed, &mut c);
765 }
766 let start = Instant::now();
767 for _ in 0..iters {
768 sgemm_nn_packed(m, &a, &packed, &mut c);
769 }
770 let elapsed_p = start.elapsed();
771 let checksum_p: f32 = c.iter().copied().sum();
772 let per_us_p = elapsed_p.as_secs_f64() * 1e6 / iters as f64;
773 let gflops_p = flops / (per_us_p * 1e3);
774 eprintln!(
775 "vendored-MLAS SGEMM 32x512x512 single-thread (pre-packed B): {per_us_p:.1} us/iter \
776 ({gflops_p:.1} GFLOP/s), checksum={checksum_p:.3}"
777 );
778 eprintln!(
779 "recorded baselines (docs/KERNEL_PERF.md): ORT 1-thread ~131 us, SimdX86 ~285 us"
780 );
781 }
782
783 #[test]
788 #[ignore = "perf probe; run explicitly with --ignored --nocapture"]
789 fn perf_sgemm_multithread() {
790 use std::time::Instant;
791
792 let (m, n, k) = (32usize, 512usize, 512usize);
793 let a = seq(m * k, 0.5);
794 let b = seq(k * n, 1.5);
795 let flops = 2.0 * m as f64 * n as f64 * k as f64;
796
797 for threads in [1usize, 8] {
798 let pool = rayon::ThreadPoolBuilder::new()
799 .num_threads(threads)
800 .build()
801 .unwrap();
802 let (per_us, checksum) = pool.install(|| {
803 let mut c = vec![0.0f32; m * n];
804 for _ in 0..100 {
805 sgemm_nn(m, n, k, &a, &b, &mut c);
806 }
807 let iters = 5000u32;
808 let start = Instant::now();
809 for _ in 0..iters {
810 sgemm_nn(m, n, k, &a, &b, &mut c);
811 }
812 let per_us = start.elapsed().as_secs_f64() * 1e6 / iters as f64;
813 (per_us, c.iter().copied().sum::<f32>())
814 });
815 let gflops = flops / (per_us * 1e3);
816 eprintln!(
817 "vendored-MLAS SGEMM 32x512x512 repack-B, {threads} thread(s): {per_us:.1} us/iter \
818 ({gflops:.1} GFLOP/s), checksum={checksum:.3}"
819 );
820 }
821 eprintln!(
822 "recorded ORT baselines (docs/KERNEL_PERF.md): 1-thread ~131 us, 8-thread ~28-30 us"
823 );
824 }
825
826 fn quantize_int4(
835 weights_nk: &[f32],
836 n: usize,
837 k: usize,
838 block_size: usize,
839 asymmetric: bool,
840 ) -> (Vec<u8>, Vec<f32>, Option<Vec<u8>>, Vec<f32>) {
841 let blocks = k.div_ceil(block_size);
842 let blob = block_size / 2;
843 let zp_row = blocks.div_ceil(2);
844 let mut packed = vec![0u8; n * blocks * blob];
845 let mut scales = vec![0.0f32; n * blocks];
846 let mut zps = vec![0u8; n * zp_row];
847 let mut dequant = vec![0.0f32; n * k];
848 for row in 0..n {
849 for block in 0..blocks {
850 let start = block * block_size;
851 let end = (start + block_size).min(k);
852 let values = &weights_nk[row * k + start..row * k + end];
853 let (scale, zp) = if asymmetric {
854 let min = values.iter().copied().fold(f32::INFINITY, f32::min);
855 let max = values.iter().copied().fold(f32::NEG_INFINITY, f32::max);
856 let scale = ((max - min) / 15.0).max(1e-6);
857 (scale, (-min / scale).round().clamp(0.0, 15.0) as u8)
858 } else {
859 let max_abs = values.iter().map(|v| v.abs()).fold(0.0, f32::max);
860 ((max_abs / 7.0).max(1e-6), 8u8)
861 };
862 scales[row * blocks + block] = scale;
863 if asymmetric {
864 zps[row * zp_row + block / 2] |= zp << (4 * (block % 2));
865 }
866 for (offset, &value) in values.iter().enumerate() {
867 let q = (value / scale + zp as f32).round().clamp(0.0, 15.0) as u8;
868 packed[(row * blocks + block) * blob + offset / 2] |= q << (4 * (offset % 2));
869 dequant[row * k + start + offset] = (q as f32 - zp as f32) * scale;
870 }
871 }
872 }
873 (packed, scales, asymmetric.then_some(zps), dequant)
874 }
875
876 fn ref_gemm_nk(
877 a: &[f32],
878 w_nk: &[f32],
879 m: usize,
880 k: usize,
881 n: usize,
882 bias: Option<&[f32]>,
883 ) -> Vec<f32> {
884 let mut c = vec![0.0f32; m * n];
885 for row in 0..m {
886 for col in 0..n {
887 let mut acc = bias.map_or(0.0, |b| b[col]);
888 for depth in 0..k {
889 acc += a[row * k + depth] * w_nk[col * k + depth];
890 }
891 c[row * n + col] = acc;
892 }
893 }
894 c
895 }
896
897 fn check_sqnbit(
898 comp: SQNBitComputeType,
899 m: usize,
900 n: usize,
901 k: usize,
902 block_size: usize,
903 asymmetric: bool,
904 with_bias: bool,
905 ) {
906 let weights: Vec<f32> = (0..n * k).map(|i| (i as f32 * 0.017 + 0.3).sin()).collect();
907 let (packed_b, scales, zps, dequant) =
908 quantize_int4(&weights, n, k, block_size, asymmetric);
909 let a: Vec<f32> = (0..m * k)
910 .map(|i| ((i as f32 * 0.011 + 0.7).cos()) * 0.5)
911 .collect();
912 let bias: Option<Vec<f32>> =
913 with_bias.then(|| (0..n).map(|i| (i as f32 * 0.03).sin()).collect());
914
915 let packed = match SQNBitPackedB::new(
916 n,
917 k,
918 4,
919 block_size,
920 comp,
921 &packed_b,
922 &scales,
923 zps.as_deref(),
924 ) {
925 Some(p) => p,
926 None => {
927 eprintln!(
928 "SQNBit int4 blk={block_size} comp={comp:?} unavailable on host; skipping"
929 );
930 return;
931 }
932 };
933 let mut c = vec![0.0f32; m * n];
934 sqnbit_gemm(&packed, m, &a, bias.as_deref(), &mut c, true);
935 let expected = ref_gemm_nk(&a, &dequant, m, k, n, bias.as_deref());
936 assert_close(
937 &c,
938 &expected,
939 2e-2,
940 &format!(
941 "sqnbit {comp:?} m{m} n{n} k{k} blk{block_size} asym{asymmetric} bias{with_bias}"
942 ),
943 );
944 }
945
946 #[test]
947 fn sqnbit_packed_b_is_send_sync() {
948 assert_send_sync::<SQNBitPackedB>();
949 }
950
951 #[test]
952 fn sqnbit_int4_compfp32_matches_reference() {
953 for &blk in &[32usize, 64, 128] {
954 for &m in &[1usize, 5] {
955 for &asym in &[false, true] {
956 check_sqnbit(SQNBitComputeType::Fp32, m, 96, 256, blk, asym, false);
957 }
958 }
959 }
960 check_sqnbit(SQNBitComputeType::Fp32, 4, 128, 512, 32, false, true);
961 }
962
963 #[test]
964 fn sqnbit_int4_compint8_matches_reference() {
965 for &blk in &[32usize, 64, 128] {
967 for &m in &[1usize, 8] {
968 for &asym in &[false, true] {
969 check_sqnbit(SQNBitComputeType::Int8, m, 96, 256, blk, asym, false);
970 }
971 }
972 }
973 check_sqnbit(SQNBitComputeType::Int8, 4, 128, 512, 32, false, true);
974 }
975
976 #[test]
980 #[ignore = "perf probe; run explicitly with --ignored --nocapture"]
981 fn perf_sqnbit() {
982 use std::time::Instant;
983 for &(k, n) in &[(2048usize, 2048usize), (4096, 11008)] {
984 let weights: Vec<f32> = (0..n * k).map(|i| (i as f32 * 0.017).sin()).collect();
985 let (packed_b, scales, _zps, _d) = quantize_int4(&weights, n, k, 32, false);
986 for comp in [SQNBitComputeType::Fp32, SQNBitComputeType::Int8] {
987 let packed = match SQNBitPackedB::new(n, k, 4, 32, comp, &packed_b, &scales, None) {
988 Some(p) => p,
989 None => continue,
990 };
991 for &m in &[1usize, 32] {
992 let a: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.011).cos()).collect();
993 for threads in [1usize, 8] {
994 let pool = rayon::ThreadPoolBuilder::new()
995 .num_threads(threads)
996 .build()
997 .unwrap();
998 let per_us = pool.install(|| {
999 let mut c = vec![0.0f32; m * n];
1000 for _ in 0..20 {
1001 sqnbit_gemm(&packed, m, &a, None, &mut c, true);
1002 }
1003 let iters = 200u32;
1004 let start = Instant::now();
1005 for _ in 0..iters {
1006 sqnbit_gemm(&packed, m, &a, None, &mut c, true);
1007 }
1008 start.elapsed().as_secs_f64() * 1e6 / iters as f64
1009 });
1010 eprintln!("SQNBit int4 {comp:?} K={k} N={n} M={m} {threads}t: {per_us:.1} us/iter");
1011 }
1012 }
1013 }
1014 }
1015 }
1016}