1use std::os::raw::c_int;
13use std::os::raw::c_void;
14use std::ptr::NonNull;
15use std::sync::Once;
16
17use rayon::prelude::*;
18
19unsafe extern "C" {
20 fn mlas_sgemm(
23 trans_a: c_int,
24 trans_b: c_int,
25 m: usize,
26 n: usize,
27 k: usize,
28 alpha: f32,
29 a: *const f32,
30 lda: usize,
31 b: *const f32,
32 ldb: usize,
33 beta: f32,
34 c: *mut f32,
35 ldc: usize,
36 );
37
38 fn mlas_sgemm_pack_b_size(trans_a: c_int, trans_b: c_int, n: usize, k: usize) -> usize;
39 fn mlas_sgemm_pack_b(
40 trans_a: c_int,
41 trans_b: c_int,
42 n: usize,
43 k: usize,
44 b: *const f32,
45 ldb: usize,
46 packed_b: *mut u8,
47 );
48 fn mlas_sgemm_packed(
49 trans_a: c_int,
50 trans_b: c_int,
51 m: usize,
52 n: usize,
53 k: usize,
54 alpha: f32,
55 a: *const f32,
56 lda: usize,
57 packed_b: *const u8,
58 beta: f32,
59 c: *mut f32,
60 ldc: usize,
61 );
62
63 fn mlas_float_kernel_id() -> c_int;
64
65 fn mlas_compute_logistic(input: *const f32, output: *mut f32, n: usize);
68 fn mlas_compute_silu(input: *const f32, output: *mut f32, n: usize);
71 fn mlas_eltwise_add(left: *const f32, right: *const f32, output: *mut f32, n: usize);
72 fn mlas_compute_activation(
73 kind: c_int,
74 minimum: f32,
75 maximum: f32,
76 input: *const f32,
77 output: *mut f32,
78 n: usize,
79 );
80
81 fn mlas_conv_prepare(
82 dimensions: usize,
83 batch_count: usize,
84 group_count: usize,
85 input_channels_per_group: usize,
86 input_shape: *const i64,
87 kernel_shape: *const i64,
88 dilation_shape: *const i64,
89 padding: *const i64,
90 stride_shape: *const i64,
91 output_shape: *const i64,
92 filter_count_per_group: usize,
93 working_buffer_elements: *mut usize,
94 ) -> *mut c_void;
95 fn mlas_conv_run(
96 plan: *const c_void,
97 input: *const f32,
98 filter: *const f32,
99 bias: *const f32,
100 working_buffer: *mut f32,
101 output: *mut f32,
102 );
103 fn mlas_conv_plan_destroy(plan: *mut c_void);
104
105 fn mlas_nchwc_block_size() -> usize;
107 fn mlas_nchwc_reorder_input_nchw(
108 source: *const f32,
109 dest: *mut f32,
110 channels: usize,
111 input_size: usize,
112 );
113 fn mlas_nchwc_reorder_output_nchw(output_shape: *const i64, source: *const f32, dest: *mut f32);
114 fn mlas_nchwc_reorder_filter_bibo(filter_shape: *const i64, source: *const f32, dest: *mut f32);
115 fn mlas_nchwc_reorder_filter_bo(filter_shape: *const i64, source: *const f32, dest: *mut f32);
116 #[allow(clippy::too_many_arguments)]
117 fn mlas_nchwc_conv(
118 input_shape: *const i64,
119 kernel_shape: *const i64,
120 dilation_shape: *const i64,
121 padding: *const i64,
122 stride_shape: *const i64,
123 output_shape: *const i64,
124 group_count: usize,
125 input: *const f32,
126 filter: *const f32,
127 bias: *const f32,
128 output: *mut f32,
129 activation_kind: c_int,
130 activation_value0: f32,
131 activation_value1: f32,
132 zero_mode: c_int,
133 );
134 fn mlas_pool(
135 kind: c_int,
136 dimensions: usize,
137 input_shape: *const i64,
138 kernel_shape: *const i64,
139 padding: *const i64,
140 stride_shape: *const i64,
141 output_shape: *const i64,
142 input: *const f32,
143 output: *mut f32,
144 );
145 #[allow(clippy::too_many_arguments)]
146 fn mlas_nchwc_pool(
147 kind: c_int,
148 input_shape: *const i64,
149 kernel_shape: *const i64,
150 dilation_shape: *const i64,
151 padding: *const i64,
152 stride_shape: *const i64,
153 output_shape: *const i64,
154 input: *const f32,
155 output: *mut f32,
156 );
157
158 fn mlas_qnbit_gemm_available(bits: usize, blk_len: usize, comp_type: c_int) -> c_int;
160 fn mlas_qnbit_gemm_pack_b_size(
161 n: usize,
162 k: usize,
163 bits: usize,
164 blk_len: usize,
165 has_zp: c_int,
166 comp_type: c_int,
167 ) -> usize;
168 fn mlas_qnbit_gemm_pack_b(
169 n: usize,
170 k: usize,
171 bits: usize,
172 blk_len: usize,
173 comp_type: c_int,
174 quant_b_data: *const c_void,
175 packed_b: *mut u8,
176 quant_b_scale: *const f32,
177 has_zp: c_int,
178 quant_b_zero_point: *const c_void,
179 );
180 fn mlas_qnbit_gemm_workspace_size(
181 m: usize,
182 n: usize,
183 k: usize,
184 bits: usize,
185 blk_len: usize,
186 has_zp: c_int,
187 comp_type: c_int,
188 ) -> usize;
189 #[allow(clippy::too_many_arguments)]
190 fn mlas_qnbit_gemm(
191 m: usize,
192 n: usize,
193 k: usize,
194 bits: usize,
195 blk_len: usize,
196 comp_type: c_int,
197 a: *const f32,
198 lda: usize,
199 packed_b: *const u8,
200 quant_b_scale: *const f32,
201 has_zp: c_int,
202 quant_b_zero_point: *const c_void,
203 bias: *const f32,
204 c: *mut f32,
205 ldc: usize,
206 workspace: *mut u8,
207 multithread: c_int,
208 );
209
210 fn mlas_set_threading(
214 parallel_for: MlasParallelForFn,
215 max_threads: MlasMaxThreadsFn,
216 rust_ctx: *mut c_void,
217 );
218}
219
220type MlasTaskFn = unsafe extern "C" fn(task_ctx: *mut c_void, tid: isize);
222type MlasParallelForFn = unsafe extern "C" fn(
224 rust_ctx: *mut c_void,
225 iterations: isize,
226 task: MlasTaskFn,
227 task_ctx: *mut c_void,
228);
229type MlasMaxThreadsFn = unsafe extern "C" fn(rust_ctx: *mut c_void) -> c_int;
231
232unsafe extern "C" fn rayon_parallel_for(
236 _rust_ctx: *mut c_void,
237 iterations: isize,
238 task: MlasTaskFn,
239 task_ctx: *mut c_void,
240) {
241 if iterations <= 0 {
242 return;
243 }
244 let task_ctx = task_ctx as usize;
249 (0..iterations).into_par_iter().for_each(|tid| {
250 unsafe { task(task_ctx as *mut c_void, tid) };
253 });
254}
255
256unsafe extern "C" fn rayon_max_threads(_rust_ctx: *mut c_void) -> c_int {
259 rayon::current_num_threads().max(1) as c_int
260}
261
262static THREADING_INIT: Once = Once::new();
263
264fn ensure_threading() {
269 THREADING_INIT.call_once(|| unsafe {
270 mlas_set_threading(rayon_parallel_for, rayon_max_threads, std::ptr::null_mut());
271 });
272}
273
274pub fn selected_float_kernel() -> i32 {
277 unsafe { mlas_float_kernel_id() as i32 }
278}
279
280pub fn compute_logistic(input: &[f32], output: &mut [f32]) {
287 assert_eq!(
288 input.len(),
289 output.len(),
290 "compute_logistic input and output must have equal length"
291 );
292 if input.is_empty() {
293 return;
294 }
295 unsafe { mlas_compute_logistic(input.as_ptr(), output.as_mut_ptr(), input.len()) };
298}
299
300pub fn compute_silu(input: &[f32], output: &mut [f32]) {
305 assert_eq!(
306 input.len(),
307 output.len(),
308 "compute_silu input and output must have equal length"
309 );
310 if input.is_empty() {
311 return;
312 }
313 unsafe { mlas_compute_silu(input.as_ptr(), output.as_mut_ptr(), input.len()) };
316}
317
318pub fn eltwise_add(left: &[f32], right: &[f32], output: &mut [f32]) {
320 assert_eq!(left.len(), right.len());
321 assert_eq!(left.len(), output.len());
322 unsafe {
323 mlas_eltwise_add(
324 left.as_ptr(),
325 right.as_ptr(),
326 output.as_mut_ptr(),
327 output.len(),
328 );
329 }
330}
331
332pub fn compute_relu(input: &[f32], output: &mut [f32]) {
334 assert_eq!(input.len(), output.len());
335 unsafe {
336 mlas_compute_activation(
337 1,
338 0.0,
339 0.0,
340 input.as_ptr(),
341 output.as_mut_ptr(),
342 output.len(),
343 );
344 }
345}
346
347pub fn compute_clip(input: &[f32], output: &mut [f32], minimum: f32, maximum: f32) {
349 assert_eq!(input.len(), output.len());
350 unsafe {
351 mlas_compute_activation(
352 5,
353 minimum,
354 maximum,
355 input.as_ptr(),
356 output.as_mut_ptr(),
357 output.len(),
358 );
359 }
360}
361
362pub struct ConvPlan {
364 ptr: NonNull<c_void>,
365 working_buffer_elements: usize,
366}
367
368unsafe impl Send for ConvPlan {}
371unsafe impl Sync for ConvPlan {}
372
373impl ConvPlan {
374 #[allow(clippy::too_many_arguments)]
376 pub fn new(
377 batch_count: usize,
378 group_count: usize,
379 input_channels_per_group: usize,
380 input_shape: &[i64],
381 kernel_shape: &[i64],
382 dilation_shape: &[i64],
383 padding: &[i64],
384 stride_shape: &[i64],
385 output_shape: &[i64],
386 filter_count_per_group: usize,
387 ) -> Option<Self> {
388 let dimensions = input_shape.len();
389 assert!((1..=3).contains(&dimensions));
390 assert_eq!(kernel_shape.len(), dimensions);
391 assert_eq!(dilation_shape.len(), dimensions);
392 assert_eq!(padding.len(), dimensions * 2);
393 assert_eq!(stride_shape.len(), dimensions);
394 assert_eq!(output_shape.len(), dimensions);
395 ensure_threading();
396 let mut working_buffer_elements = 0;
397 let ptr = unsafe {
398 mlas_conv_prepare(
399 dimensions,
400 batch_count,
401 group_count,
402 input_channels_per_group,
403 input_shape.as_ptr(),
404 kernel_shape.as_ptr(),
405 dilation_shape.as_ptr(),
406 padding.as_ptr(),
407 stride_shape.as_ptr(),
408 output_shape.as_ptr(),
409 filter_count_per_group,
410 &mut working_buffer_elements,
411 )
412 };
413 Some(Self {
414 ptr: NonNull::new(ptr)?,
415 working_buffer_elements,
416 })
417 }
418
419 pub fn working_buffer_elements(&self) -> usize {
421 self.working_buffer_elements
422 }
423
424 pub fn run(
426 &self,
427 input: &[f32],
428 filter: &[f32],
429 bias: Option<&[f32]>,
430 working_buffer: &mut [f32],
431 output: &mut [f32],
432 ) {
433 assert!(working_buffer.len() >= self.working_buffer_elements);
434 ensure_threading();
435 unsafe {
436 mlas_conv_run(
437 self.ptr.as_ptr(),
438 input.as_ptr(),
439 filter.as_ptr(),
440 bias.map_or(std::ptr::null(), <[f32]>::as_ptr),
441 if self.working_buffer_elements == 0 {
442 std::ptr::null_mut()
443 } else {
444 working_buffer.as_mut_ptr()
445 },
446 output.as_mut_ptr(),
447 );
448 }
449 }
450}
451
452impl Drop for ConvPlan {
453 fn drop(&mut self) {
454 unsafe { mlas_conv_plan_destroy(self.ptr.as_ptr()) };
455 }
456}
457
458#[derive(Clone, Copy, Debug)]
463pub struct NchwcActivation {
464 pub kind: i32,
466 pub values: [f32; 2],
468}
469
470impl NchwcActivation {
471 pub const IDENTITY: Self = Self {
473 kind: 0,
474 values: [0.0, 0.0],
475 };
476 pub const RELU: Self = Self {
478 kind: 1,
479 values: [0.0, 0.0],
480 };
481
482 pub fn clip(minimum: f32, maximum: f32) -> Self {
484 Self {
485 kind: 5,
486 values: [minimum, maximum],
487 }
488 }
489}
490
491pub fn nchwc_block_size() -> usize {
495 unsafe { mlas_nchwc_block_size() }
496}
497
498pub fn nchwc_reorder_filter_bibo(filter_shape: &[i64; 4], source: &[f32], dest: &mut [f32]) {
502 unsafe {
503 mlas_nchwc_reorder_filter_bibo(filter_shape.as_ptr(), source.as_ptr(), dest.as_mut_ptr())
504 };
505}
506
507pub fn nchwc_reorder_filter_bo(filter_shape: &[i64; 4], source: &[f32], dest: &mut [f32]) {
512 unsafe {
513 mlas_nchwc_reorder_filter_bo(filter_shape.as_ptr(), source.as_ptr(), dest.as_mut_ptr())
514 };
515}
516
517pub fn nchwc_reorder_input_nchw(
521 source: &[f32],
522 dest: &mut [f32],
523 channels: usize,
524 input_size: usize,
525) {
526 ensure_threading();
527 unsafe {
528 mlas_nchwc_reorder_input_nchw(source.as_ptr(), dest.as_mut_ptr(), channels, input_size)
529 };
530}
531
532pub fn nchwc_reorder_output_nchw(output_shape: &[i64; 4], source: &[f32], dest: &mut [f32]) {
535 ensure_threading();
536 unsafe {
537 mlas_nchwc_reorder_output_nchw(output_shape.as_ptr(), source.as_ptr(), dest.as_mut_ptr())
538 };
539}
540
541#[allow(clippy::too_many_arguments)]
550pub fn nchwc_conv(
551 input_shape: &[i64; 4],
552 kernel_shape: &[i64; 2],
553 dilation_shape: &[i64; 2],
554 padding: &[i64; 4],
555 stride_shape: &[i64; 2],
556 output_shape: &[i64; 4],
557 group_count: usize,
558 input: &[f32],
559 filter: &[f32],
560 bias: Option<&[f32]>,
561 output: &mut [f32],
562 activation: NchwcActivation,
563 zero_mode: bool,
564) {
565 ensure_threading();
566 unsafe {
567 mlas_nchwc_conv(
568 input_shape.as_ptr(),
569 kernel_shape.as_ptr(),
570 dilation_shape.as_ptr(),
571 padding.as_ptr(),
572 stride_shape.as_ptr(),
573 output_shape.as_ptr(),
574 group_count,
575 input.as_ptr(),
576 filter.as_ptr(),
577 bias.map_or(std::ptr::null(), <[f32]>::as_ptr),
578 output.as_mut_ptr(),
579 activation.kind,
580 activation.values[0],
581 activation.values[1],
582 i32::from(zero_mode),
583 );
584 }
585}
586
587#[derive(Clone, Copy, Debug)]
589#[repr(i32)]
590pub enum PoolKind {
591 Maximum = 0,
592 AverageExcludePad = 1,
593 AverageIncludePad = 2,
594}
595
596#[allow(clippy::too_many_arguments)]
598pub fn pool(
599 kind: PoolKind,
600 input_shape: &[i64],
601 kernel_shape: &[i64],
602 padding: &[i64],
603 stride_shape: &[i64],
604 output_shape: &[i64],
605 input: &[f32],
606 output: &mut [f32],
607) {
608 let dimensions = input_shape.len().saturating_sub(2);
609 assert!((1..=3).contains(&dimensions));
610 assert_eq!(kernel_shape.len(), dimensions);
611 assert_eq!(padding.len(), dimensions * 2);
612 assert_eq!(stride_shape.len(), dimensions);
613 assert_eq!(output_shape.len(), dimensions + 2);
614 ensure_threading();
615 unsafe {
616 mlas_pool(
617 kind as c_int,
618 dimensions,
619 input_shape.as_ptr(),
620 kernel_shape.as_ptr(),
621 padding.as_ptr(),
622 stride_shape.as_ptr(),
623 output_shape.as_ptr(),
624 input.as_ptr(),
625 output.as_mut_ptr(),
626 );
627 }
628}
629
630#[allow(clippy::too_many_arguments)]
638pub fn nchwc_pool(
639 kind: PoolKind,
640 input_shape: &[i64; 4],
641 kernel_shape: &[i64; 2],
642 dilation_shape: &[i64; 2],
643 padding: &[i64; 4],
644 stride_shape: &[i64; 2],
645 output_shape: &[i64; 4],
646 input: &[f32],
647 output: &mut [f32],
648) {
649 ensure_threading();
650 unsafe {
651 mlas_nchwc_pool(
652 kind as c_int,
653 input_shape.as_ptr(),
654 kernel_shape.as_ptr(),
655 dilation_shape.as_ptr(),
656 padding.as_ptr(),
657 stride_shape.as_ptr(),
658 output_shape.as_ptr(),
659 input.as_ptr(),
660 output.as_mut_ptr(),
661 );
662 }
663}
664pub struct PackedB {
668 ptr: *mut u8,
669 layout: std::alloc::Layout,
670 n: usize,
671 k: usize,
672}
673
674unsafe impl Send for PackedB {}
677unsafe impl Sync for PackedB {}
678
679impl PackedB {
680 pub fn new(n: usize, k: usize, b: &[f32]) -> Self {
682 assert_eq!(b.len(), k * n);
683 let size = unsafe { mlas_sgemm_pack_b_size(0, 0, n, k) }.max(1);
684 let layout = std::alloc::Layout::from_size_align(size, 64).unwrap();
685 let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
686 assert!(!ptr.is_null(), "packed-B allocation failed");
687 unsafe { mlas_sgemm_pack_b(0, 0, n, k, b.as_ptr(), n, ptr) };
688 Self { ptr, layout, n, k }
689 }
690
691 pub fn dimensions(&self) -> (usize, usize) {
693 (self.k, self.n)
694 }
695}
696
697impl Drop for PackedB {
698 fn drop(&mut self) {
699 unsafe { std::alloc::dealloc(self.ptr, self.layout) };
700 }
701}
702
703pub fn sgemm_nn_packed(m: usize, a: &[f32], packed: &PackedB, c: &mut [f32]) {
705 let (n, k) = (packed.n, packed.k);
706 assert_eq!(a.len(), m * k);
707 assert_eq!(c.len(), m * n);
708 ensure_threading();
709 unsafe {
710 mlas_sgemm_packed(
711 0,
712 0,
713 m,
714 n,
715 k,
716 1.0,
717 a.as_ptr(),
718 k,
719 packed.ptr,
720 0.0,
721 c.as_mut_ptr(),
722 n,
723 );
724 }
725}
726
727pub fn sgemm_nn(m: usize, n: usize, k: usize, a: &[f32], b: &[f32], c: &mut [f32]) {
732 assert_eq!(a.len(), m * k, "A must be m*k");
733 assert_eq!(b.len(), k * n, "B must be k*n");
734 assert_eq!(c.len(), m * n, "C must be m*n");
735 ensure_threading();
736 unsafe {
737 mlas_sgemm(
738 0,
739 0,
740 m,
741 n,
742 k,
743 1.0,
744 a.as_ptr(),
745 k,
746 b.as_ptr(),
747 n,
748 0.0,
749 c.as_mut_ptr(),
750 n,
751 );
752 }
753}
754
755#[allow(clippy::too_many_arguments)]
758pub fn sgemm(
759 trans_a: bool,
760 trans_b: bool,
761 m: usize,
762 n: usize,
763 k: usize,
764 alpha: f32,
765 a: &[f32],
766 lda: usize,
767 b: &[f32],
768 ldb: usize,
769 beta: f32,
770 c: &mut [f32],
771 ldc: usize,
772) {
773 ensure_threading();
774 unsafe {
775 mlas_sgemm(
776 trans_a as c_int,
777 trans_b as c_int,
778 m,
779 n,
780 k,
781 alpha,
782 a.as_ptr(),
783 lda,
784 b.as_ptr(),
785 ldb,
786 beta,
787 c.as_mut_ptr(),
788 ldc,
789 );
790 }
791}
792
793#[derive(Debug, Clone, Copy, PartialEq, Eq)]
797pub enum SQNBitComputeType {
798 Fp32,
800 Int8,
803}
804
805impl SQNBitComputeType {
806 #[inline]
807 fn raw(self) -> c_int {
808 match self {
811 SQNBitComputeType::Fp32 => 0, SQNBitComputeType::Int8 => 3, }
814 }
815}
816
817pub fn sqnbit_gemm_available(bits: usize, blk_len: usize, comp: SQNBitComputeType) -> bool {
821 unsafe { mlas_qnbit_gemm_available(bits, blk_len, comp.raw()) != 0 }
822}
823
824pub struct SQNBitPackedB {
837 ptr: *mut u8,
838 layout: std::alloc::Layout,
839 n: usize,
840 k: usize,
841 bits: usize,
842 blk_len: usize,
843 comp: SQNBitComputeType,
844 has_zp: bool,
845 scale: Vec<f32>,
846 zp: Option<Vec<u8>>,
847}
848
849unsafe impl Send for SQNBitPackedB {}
854unsafe impl Sync for SQNBitPackedB {}
855
856impl SQNBitPackedB {
857 #[allow(clippy::too_many_arguments)]
861 pub fn new(
862 n: usize,
863 k: usize,
864 bits: usize,
865 blk_len: usize,
866 comp: SQNBitComputeType,
867 quant_b_data: &[u8],
868 scale: &[f32],
869 zp: Option<&[u8]>,
870 ) -> Option<Self> {
871 if !sqnbit_gemm_available(bits, blk_len, comp) {
872 return None;
873 }
874 let has_zp = zp.is_some();
875 let size = unsafe {
876 mlas_qnbit_gemm_pack_b_size(n, k, bits, blk_len, has_zp as c_int, comp.raw())
877 };
878 if size == 0 {
879 return None;
880 }
881 let layout = std::alloc::Layout::from_size_align(size, 64).unwrap();
882 let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
883 assert!(!ptr.is_null(), "SQNBit packed-B allocation failed");
884 let zp_ptr = zp.map_or(std::ptr::null(), |z| z.as_ptr()) as *const c_void;
885 unsafe {
886 mlas_qnbit_gemm_pack_b(
887 n,
888 k,
889 bits,
890 blk_len,
891 comp.raw(),
892 quant_b_data.as_ptr() as *const c_void,
893 ptr,
894 scale.as_ptr(),
895 has_zp as c_int,
896 zp_ptr,
897 );
898 }
899 Some(Self {
900 ptr,
901 layout,
902 n,
903 k,
904 bits,
905 blk_len,
906 comp,
907 has_zp,
908 scale: scale.to_vec(),
909 zp: zp.map(<[u8]>::to_vec),
910 })
911 }
912
913 pub fn dimensions(&self) -> (usize, usize) {
915 (self.k, self.n)
916 }
917}
918
919impl Drop for SQNBitPackedB {
920 fn drop(&mut self) {
921 unsafe { std::alloc::dealloc(self.ptr, self.layout) };
922 }
923}
924
925pub fn sqnbit_gemm(
932 packed: &SQNBitPackedB,
933 m: usize,
934 a: &[f32],
935 bias: Option<&[f32]>,
936 c: &mut [f32],
937 multithread: bool,
938) {
939 let n = packed.n;
940 assert_eq!(c.len(), m * n, "C must be m*n");
941 unsafe { sqnbit_gemm_into(packed, m, a, bias, c.as_mut_ptr(), n, multithread) };
945}
946
947pub unsafe fn sqnbit_gemm_into(
959 packed: &SQNBitPackedB,
960 m: usize,
961 a: &[f32],
962 bias: Option<&[f32]>,
963 c: *mut f32,
964 ldc: usize,
965 multithread: bool,
966) {
967 let (k, n) = (packed.k, packed.n);
968 assert_eq!(a.len(), m * k, "A must be m*k");
969 assert!(ldc >= n, "ldc must be >= packed N");
970 if let Some(bias) = bias {
971 assert_eq!(bias.len(), n, "bias must be length n");
972 }
973 ensure_threading();
974
975 let ws_size = unsafe {
976 mlas_qnbit_gemm_workspace_size(
977 m,
978 n,
979 k,
980 packed.bits,
981 packed.blk_len,
982 packed.has_zp as c_int,
983 packed.comp.raw(),
984 )
985 };
986 let mut workspace: Vec<u8> = if ws_size == 0 {
989 Vec::new()
990 } else {
991 vec![0u8; ws_size + 64]
992 };
993 let ws_ptr = if ws_size == 0 {
994 std::ptr::null_mut()
995 } else {
996 workspace.as_mut_ptr()
997 };
998
999 let zp_ptr = packed.zp.as_ref().map_or(std::ptr::null(), |z| z.as_ptr()) as *const c_void;
1000 let bias_ptr = bias.map_or(std::ptr::null(), <[f32]>::as_ptr);
1001
1002 unsafe {
1003 mlas_qnbit_gemm(
1004 m,
1005 n,
1006 k,
1007 packed.bits,
1008 packed.blk_len,
1009 packed.comp.raw(),
1010 a.as_ptr(),
1011 k,
1012 packed.ptr,
1013 packed.scale.as_ptr(),
1014 packed.has_zp as c_int,
1015 zp_ptr,
1016 bias_ptr,
1017 c,
1018 ldc,
1019 ws_ptr,
1020 multithread as c_int,
1021 );
1022 }
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027 use super::*;
1028
1029 fn assert_send_sync<T: Send + Sync>() {}
1030
1031 #[test]
1032 fn packed_b_is_send_sync() {
1033 assert_send_sync::<PackedB>();
1034 }
1035
1036 #[allow(clippy::too_many_arguments)]
1038 fn ref_sgemm(
1039 trans_a: bool,
1040 trans_b: bool,
1041 m: usize,
1042 n: usize,
1043 k: usize,
1044 alpha: f32,
1045 a: &[f32],
1046 lda: usize,
1047 b: &[f32],
1048 ldb: usize,
1049 beta: f32,
1050 c: &mut [f32],
1051 ldc: usize,
1052 ) {
1053 for i in 0..m {
1054 for j in 0..n {
1055 let mut acc = 0.0f32;
1056 for p in 0..k {
1057 let av = if trans_a {
1058 a[p * lda + i]
1059 } else {
1060 a[i * lda + p]
1061 };
1062 let bv = if trans_b {
1063 b[j * ldb + p]
1064 } else {
1065 b[p * ldb + j]
1066 };
1067 acc += av * bv;
1068 }
1069 let cell = &mut c[i * ldc + j];
1070 *cell = alpha * acc + beta * *cell;
1071 }
1072 }
1073 }
1074
1075 fn seq(n: usize, seed: f32) -> Vec<f32> {
1076 (0..n)
1078 .map(|i| ((i as f32 * 0.013 + seed).sin()) * 2.0)
1079 .collect()
1080 }
1081
1082 fn assert_close(a: &[f32], b: &[f32], tol: f32, ctx: &str) {
1083 assert_eq!(a.len(), b.len());
1084 for (idx, (x, y)) in a.iter().zip(b.iter()).enumerate() {
1085 let diff = (x - y).abs();
1086 let rel = diff / (y.abs().max(1.0));
1087 assert!(
1088 diff <= tol || rel <= tol,
1089 "{ctx}: mismatch at {idx}: mlas={x} ref={y} diff={diff}"
1090 );
1091 }
1092 }
1093
1094 fn check_nn(m: usize, n: usize, k: usize) {
1095 let a = seq(m * k, 0.5);
1096 let b = seq(k * n, 1.5);
1097 let mut c_mlas = vec![0.0f32; m * n];
1098 let mut c_ref = vec![0.0f32; m * n];
1099 sgemm_nn(m, n, k, &a, &b, &mut c_mlas);
1100 ref_sgemm(false, false, m, n, k, 1.0, &a, k, &b, n, 0.0, &mut c_ref, n);
1101 assert_close(&c_mlas, &c_ref, 1e-3, &format!("nn {m}x{n}x{k}"));
1102 }
1103
1104 #[test]
1105 fn correctness_square() {
1106 check_nn(64, 64, 64);
1107 }
1108
1109 #[test]
1110 fn correctness_non_square_and_non_tile_multiples() {
1111 check_nn(1, 1, 1);
1113 check_nn(3, 5, 7);
1114 check_nn(17, 31, 13);
1115 check_nn(32, 512, 512);
1116 check_nn(33, 65, 129);
1117 check_nn(100, 1, 100);
1118 check_nn(1, 100, 100);
1119 }
1120
1121 #[test]
1122 fn correctness_alpha_beta() {
1123 let (m, n, k) = (23, 19, 41);
1124 let a = seq(m * k, 0.2);
1125 let b = seq(k * n, 0.7);
1126 let base = seq(m * n, 2.0);
1127 let mut c_mlas = base.clone();
1128 let mut c_ref = base.clone();
1129 sgemm(
1130 false,
1131 false,
1132 m,
1133 n,
1134 k,
1135 0.5,
1136 &a,
1137 k,
1138 &b,
1139 n,
1140 2.0,
1141 &mut c_mlas,
1142 n,
1143 );
1144 ref_sgemm(false, false, m, n, k, 0.5, &a, k, &b, n, 2.0, &mut c_ref, n);
1145 assert_close(&c_mlas, &c_ref, 1e-3, "alpha_beta");
1146 }
1147
1148 #[test]
1149 fn correctness_transpose_b() {
1150 let (m, n, k) = (12, 20, 28);
1152 let a = seq(m * k, 0.3);
1153 let b_t = seq(n * k, 0.9); let mut c_mlas = vec![0.0f32; m * n];
1155 let mut c_ref = vec![0.0f32; m * n];
1156 sgemm(
1157 false,
1158 true,
1159 m,
1160 n,
1161 k,
1162 1.0,
1163 &a,
1164 k,
1165 &b_t,
1166 k,
1167 0.0,
1168 &mut c_mlas,
1169 n,
1170 );
1171 ref_sgemm(
1172 false, true, m, n, k, 1.0, &a, k, &b_t, k, 0.0, &mut c_ref, n,
1173 );
1174 assert_close(&c_mlas, &c_ref, 1e-3, "transpose_b");
1175 }
1176
1177 #[test]
1178 fn correctness_transpose_a() {
1179 let (m, n, k) = (14, 22, 18);
1181 let a_t = seq(k * m, 0.4); let b = seq(k * n, 0.6);
1183 let mut c_mlas = vec![0.0f32; m * n];
1184 let mut c_ref = vec![0.0f32; m * n];
1185 sgemm(
1186 true,
1187 false,
1188 m,
1189 n,
1190 k,
1191 1.0,
1192 &a_t,
1193 m,
1194 &b,
1195 n,
1196 0.0,
1197 &mut c_mlas,
1198 n,
1199 );
1200 ref_sgemm(
1201 true, false, m, n, k, 1.0, &a_t, m, &b, n, 0.0, &mut c_ref, n,
1202 );
1203 assert_close(&c_mlas, &c_ref, 1e-3, "transpose_a");
1204 }
1205
1206 #[test]
1207 fn correctness_packed_b() {
1208 for (m, n, k) in [(32usize, 512usize, 512usize), (7, 13, 19), (1, 64, 64)] {
1209 let a = seq(m * k, 0.5);
1210 let b = seq(k * n, 1.5);
1211 let mut c_mlas = vec![0.0f32; m * n];
1212 let mut c_ref = vec![0.0f32; m * n];
1213 let packed = PackedB::new(n, k, &b);
1214 sgemm_nn_packed(m, &a, &packed, &mut c_mlas);
1215 ref_sgemm(false, false, m, n, k, 1.0, &a, k, &b, n, 0.0, &mut c_ref, n);
1216 assert_close(&c_mlas, &c_ref, 1e-3, &format!("packed {m}x{n}x{k}"));
1217 }
1218 }
1219
1220 #[test]
1221 fn float_kernel_matches_detected_isa() {
1222 let id = selected_float_kernel();
1223 let expected = if std::arch::is_x86_feature_detected!("avx512f") {
1224 512
1225 } else if std::arch::is_x86_feature_detected!("avx2")
1226 && std::arch::is_x86_feature_detected!("fma")
1227 {
1228 3
1229 } else if std::arch::is_x86_feature_detected!("avx") {
1230 1
1231 } else {
1232 -1
1233 };
1234 eprintln!("selected f32 GEMM kernel id = {id}; expected {expected} for host ISA");
1235 assert_eq!(
1236 id, expected,
1237 "MLAS f32 GEMM dispatch did not match host ISA"
1238 );
1239 }
1240
1241 #[test]
1246 #[ignore = "perf probe; run explicitly with --ignored --nocapture"]
1247 fn perf_sgemm_medium() {
1248 use std::time::Instant;
1249
1250 let (m, n, k) = (32usize, 512usize, 512usize);
1251 let a = seq(m * k, 0.5);
1252 let b = seq(k * n, 1.5);
1253 let mut c = vec![0.0f32; m * n];
1254
1255 for _ in 0..50 {
1257 sgemm_nn(m, n, k, &a, &b, &mut c);
1258 }
1259
1260 let iters = 5000u32;
1261 let start = Instant::now();
1262 for _ in 0..iters {
1263 sgemm_nn(m, n, k, &a, &b, &mut c);
1264 }
1265 let elapsed = start.elapsed();
1266 let checksum: f32 = c.iter().copied().sum();
1268
1269 let per_us = elapsed.as_secs_f64() * 1e6 / iters as f64;
1270 let flops = 2.0 * m as f64 * n as f64 * k as f64;
1271 let gflops = flops / (per_us * 1e3);
1272 eprintln!(
1273 "vendored-MLAS SGEMM 32x512x512 single-thread (repack B/call): {per_us:.1} us/iter \
1274 ({gflops:.1} GFLOP/s), checksum={checksum:.3}"
1275 );
1276
1277 let packed = PackedB::new(n, k, &b);
1279 for _ in 0..50 {
1280 sgemm_nn_packed(m, &a, &packed, &mut c);
1281 }
1282 let start = Instant::now();
1283 for _ in 0..iters {
1284 sgemm_nn_packed(m, &a, &packed, &mut c);
1285 }
1286 let elapsed_p = start.elapsed();
1287 let checksum_p: f32 = c.iter().copied().sum();
1288 let per_us_p = elapsed_p.as_secs_f64() * 1e6 / iters as f64;
1289 let gflops_p = flops / (per_us_p * 1e3);
1290 eprintln!(
1291 "vendored-MLAS SGEMM 32x512x512 single-thread (pre-packed B): {per_us_p:.1} us/iter \
1292 ({gflops_p:.1} GFLOP/s), checksum={checksum_p:.3}"
1293 );
1294 eprintln!(
1295 "recorded baselines (docs/KERNEL_PERF.md): ORT 1-thread ~131 us, SimdX86 ~285 us"
1296 );
1297 }
1298
1299 #[test]
1304 #[ignore = "perf probe; run explicitly with --ignored --nocapture"]
1305 fn perf_sgemm_multithread() {
1306 use std::time::Instant;
1307
1308 let (m, n, k) = (32usize, 512usize, 512usize);
1309 let a = seq(m * k, 0.5);
1310 let b = seq(k * n, 1.5);
1311 let flops = 2.0 * m as f64 * n as f64 * k as f64;
1312
1313 for threads in [1usize, 8] {
1314 let pool = rayon::ThreadPoolBuilder::new()
1315 .num_threads(threads)
1316 .build()
1317 .unwrap();
1318 let (per_us, checksum) = pool.install(|| {
1319 let mut c = vec![0.0f32; m * n];
1320 for _ in 0..100 {
1321 sgemm_nn(m, n, k, &a, &b, &mut c);
1322 }
1323 let iters = 5000u32;
1324 let start = Instant::now();
1325 for _ in 0..iters {
1326 sgemm_nn(m, n, k, &a, &b, &mut c);
1327 }
1328 let per_us = start.elapsed().as_secs_f64() * 1e6 / iters as f64;
1329 (per_us, c.iter().copied().sum::<f32>())
1330 });
1331 let gflops = flops / (per_us * 1e3);
1332 eprintln!(
1333 "vendored-MLAS SGEMM 32x512x512 repack-B, {threads} thread(s): {per_us:.1} us/iter \
1334 ({gflops:.1} GFLOP/s), checksum={checksum:.3}"
1335 );
1336 }
1337 eprintln!(
1338 "recorded ORT baselines (docs/KERNEL_PERF.md): 1-thread ~131 us, 8-thread ~28-30 us"
1339 );
1340 }
1341
1342 fn quantize_int4(
1351 weights_nk: &[f32],
1352 n: usize,
1353 k: usize,
1354 block_size: usize,
1355 asymmetric: bool,
1356 ) -> (Vec<u8>, Vec<f32>, Option<Vec<u8>>, Vec<f32>) {
1357 let blocks = k.div_ceil(block_size);
1358 let blob = block_size / 2;
1359 let zp_row = blocks.div_ceil(2);
1360 let mut packed = vec![0u8; n * blocks * blob];
1361 let mut scales = vec![0.0f32; n * blocks];
1362 let mut zps = vec![0u8; n * zp_row];
1363 let mut dequant = vec![0.0f32; n * k];
1364 for row in 0..n {
1365 for block in 0..blocks {
1366 let start = block * block_size;
1367 let end = (start + block_size).min(k);
1368 let values = &weights_nk[row * k + start..row * k + end];
1369 let (scale, zp) = if asymmetric {
1370 let min = values.iter().copied().fold(f32::INFINITY, f32::min);
1371 let max = values.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1372 let scale = ((max - min) / 15.0).max(1e-6);
1373 (scale, (-min / scale).round().clamp(0.0, 15.0) as u8)
1374 } else {
1375 let max_abs = values.iter().map(|v| v.abs()).fold(0.0, f32::max);
1376 ((max_abs / 7.0).max(1e-6), 8u8)
1377 };
1378 scales[row * blocks + block] = scale;
1379 if asymmetric {
1380 zps[row * zp_row + block / 2] |= zp << (4 * (block % 2));
1381 }
1382 for (offset, &value) in values.iter().enumerate() {
1383 let q = (value / scale + zp as f32).round().clamp(0.0, 15.0) as u8;
1384 packed[(row * blocks + block) * blob + offset / 2] |= q << (4 * (offset % 2));
1385 dequant[row * k + start + offset] = (q as f32 - zp as f32) * scale;
1386 }
1387 }
1388 }
1389 (packed, scales, asymmetric.then_some(zps), dequant)
1390 }
1391
1392 fn ref_gemm_nk(
1393 a: &[f32],
1394 w_nk: &[f32],
1395 m: usize,
1396 k: usize,
1397 n: usize,
1398 bias: Option<&[f32]>,
1399 ) -> Vec<f32> {
1400 let mut c = vec![0.0f32; m * n];
1401 for row in 0..m {
1402 for col in 0..n {
1403 let mut acc = bias.map_or(0.0, |b| b[col]);
1404 for depth in 0..k {
1405 acc += a[row * k + depth] * w_nk[col * k + depth];
1406 }
1407 c[row * n + col] = acc;
1408 }
1409 }
1410 c
1411 }
1412
1413 fn check_sqnbit(
1414 comp: SQNBitComputeType,
1415 m: usize,
1416 n: usize,
1417 k: usize,
1418 block_size: usize,
1419 asymmetric: bool,
1420 with_bias: bool,
1421 ) {
1422 let weights: Vec<f32> = (0..n * k).map(|i| (i as f32 * 0.017 + 0.3).sin()).collect();
1423 let (packed_b, scales, zps, dequant) =
1424 quantize_int4(&weights, n, k, block_size, asymmetric);
1425 let a: Vec<f32> = (0..m * k)
1426 .map(|i| ((i as f32 * 0.011 + 0.7).cos()) * 0.5)
1427 .collect();
1428 let bias: Option<Vec<f32>> =
1429 with_bias.then(|| (0..n).map(|i| (i as f32 * 0.03).sin()).collect());
1430
1431 let packed = match SQNBitPackedB::new(
1432 n,
1433 k,
1434 4,
1435 block_size,
1436 comp,
1437 &packed_b,
1438 &scales,
1439 zps.as_deref(),
1440 ) {
1441 Some(p) => p,
1442 None => {
1443 eprintln!(
1444 "SQNBit int4 blk={block_size} comp={comp:?} unavailable on host; skipping"
1445 );
1446 return;
1447 }
1448 };
1449 let mut c = vec![0.0f32; m * n];
1450 sqnbit_gemm(&packed, m, &a, bias.as_deref(), &mut c, true);
1451 let expected = ref_gemm_nk(&a, &dequant, m, k, n, bias.as_deref());
1452 assert_close(
1453 &c,
1454 &expected,
1455 2e-2,
1456 &format!(
1457 "sqnbit {comp:?} m{m} n{n} k{k} blk{block_size} asym{asymmetric} bias{with_bias}"
1458 ),
1459 );
1460 }
1461
1462 #[test]
1463 fn sqnbit_packed_b_is_send_sync() {
1464 assert_send_sync::<SQNBitPackedB>();
1465 }
1466
1467 #[test]
1468 fn sqnbit_int4_compfp32_matches_reference() {
1469 for &blk in &[32usize, 64, 128] {
1470 for &m in &[1usize, 5] {
1471 for &asym in &[false, true] {
1472 check_sqnbit(SQNBitComputeType::Fp32, m, 96, 256, blk, asym, false);
1473 }
1474 }
1475 }
1476 check_sqnbit(SQNBitComputeType::Fp32, 4, 128, 512, 32, false, true);
1477 }
1478
1479 #[test]
1492 fn sqnbit_int4_n_shards_match_full() {
1493 let n = 96usize;
1494 for &(k, block_size) in &[(256usize, 32usize), (256, 64), (256, 128), (384, 64)] {
1497 for &m in &[1usize, 5] {
1498 for &asym in &[false, true] {
1499 for &with_bias in &[false, true] {
1500 let weights: Vec<f32> =
1501 (0..n * k).map(|i| (i as f32 * 0.017 + 0.3).sin()).collect();
1502 let (packed_b, scales, zps, _) =
1503 quantize_int4(&weights, n, k, block_size, asym);
1504 let a: Vec<f32> = (0..m * k)
1505 .map(|i| ((i as f32 * 0.011 + 0.7).cos()) * 0.5)
1506 .collect();
1507 let bias: Option<Vec<f32>> =
1508 with_bias.then(|| (0..n).map(|i| (i as f32 * 0.03).sin()).collect());
1509
1510 let full = match SQNBitPackedB::new(
1511 n,
1512 k,
1513 4,
1514 block_size,
1515 SQNBitComputeType::Fp32,
1516 &packed_b,
1517 &scales,
1518 zps.as_deref(),
1519 ) {
1520 Some(p) => p,
1521 None => {
1522 eprintln!("SQNBit blk={block_size} unavailable; skipping");
1523 return;
1524 }
1525 };
1526 let mut c_full = vec![0.0f32; m * n];
1527 sqnbit_gemm(&full, m, &a, bias.as_deref(), &mut c_full, true);
1528
1529 let blocks = k.div_ceil(block_size);
1530 let blob = block_size / 2;
1531 let zp_row = blocks.div_ceil(2);
1532 let shards: &[(usize, usize)] = &[(0, 17), (17, 30), (47, 1), (48, 48)];
1535 for &mt in &[false, true] {
1538 let mut c_shard = vec![0.0f32; m * n];
1539 for &(start, len) in shards {
1540 let pb =
1541 &packed_b[start * blocks * blob..(start + len) * blocks * blob];
1542 let sc = &scales[start * blocks..(start + len) * blocks];
1543 let zp = zps
1544 .as_deref()
1545 .map(|z| &z[start * zp_row..(start + len) * zp_row]);
1546 let packed = SQNBitPackedB::new(
1547 len,
1548 k,
1549 4,
1550 block_size,
1551 SQNBitComputeType::Fp32,
1552 pb,
1553 sc,
1554 zp,
1555 )
1556 .expect("shard packs when the full weight packs");
1557 let bias_shard = bias.as_deref().map(|b| &b[start..start + len]);
1558 unsafe {
1561 sqnbit_gemm_into(
1562 &packed,
1563 m,
1564 &a,
1565 bias_shard,
1566 c_shard.as_mut_ptr().add(start),
1567 n,
1568 mt,
1569 );
1570 }
1571 }
1572 assert_close(
1576 &c_shard,
1577 &c_full,
1578 1e-3,
1579 &format!(
1580 "N-sharded (multithread={mt}) vs full: \
1581 k{k} blk{block_size} m{m} asym{asym} bias{with_bias}"
1582 ),
1583 );
1584 }
1585 }
1586 }
1587 }
1588 }
1589 }
1590
1591 #[test]
1604 fn sqnbit_int4_tile_aligned_shards_are_bit_exact() {
1605 let n = 176usize;
1607 let mut any_mid_tile_drift = false;
1608 for &(k, block_size) in &[(256usize, 128usize), (512, 32), (256, 64)] {
1609 for &asym in &[false, true] {
1610 let weights: Vec<f32> =
1611 (0..n * k).map(|i| (i as f32 * 0.017 + 0.3).sin()).collect();
1612 let (packed_b, scales, zps, _) = quantize_int4(&weights, n, k, block_size, asym);
1613 let a: Vec<f32> = (0..k)
1614 .map(|i| ((i as f32 * 0.011 + 0.7).cos()) * 0.5)
1615 .collect();
1616
1617 let full = match SQNBitPackedB::new(
1618 n,
1619 k,
1620 4,
1621 block_size,
1622 SQNBitComputeType::Fp32,
1623 &packed_b,
1624 &scales,
1625 zps.as_deref(),
1626 ) {
1627 Some(p) => p,
1628 None => {
1629 eprintln!("SQNBit blk={block_size} unavailable; skipping");
1630 return;
1631 }
1632 };
1633 let mut c_full = vec![0.0f32; n];
1634 sqnbit_gemm(&full, 1, &a, None, &mut c_full, false);
1635
1636 let blocks = k.div_ceil(block_size);
1637 let blob = block_size / 2;
1638 let zp_row = blocks.div_ceil(2);
1639 let run_shards = |shards: &[(usize, usize)]| -> Vec<f32> {
1640 let mut c = vec![0.0f32; n];
1641 for &(start, len) in shards {
1642 let pb = &packed_b[start * blocks * blob..(start + len) * blocks * blob];
1643 let sc = &scales[start * blocks..(start + len) * blocks];
1644 let zp = zps
1645 .as_deref()
1646 .map(|z| &z[start * zp_row..(start + len) * zp_row]);
1647 let packed = SQNBitPackedB::new(
1648 len,
1649 k,
1650 4,
1651 block_size,
1652 SQNBitComputeType::Fp32,
1653 pb,
1654 sc,
1655 zp,
1656 )
1657 .expect("shard packs when the full weight packs");
1658 unsafe {
1660 sqnbit_gemm_into(
1661 &packed,
1662 1,
1663 &a,
1664 None,
1665 c.as_mut_ptr().add(start),
1666 n,
1667 false,
1668 );
1669 }
1670 }
1671 c
1672 };
1673
1674 let aligned: &[(usize, usize)] = &[(0, 16), (16, 48), (64, 64), (128, 48)];
1676 assert_eq!(aligned.iter().map(|&(_, l)| l).sum::<usize>(), n);
1677 let c_aligned = run_shards(aligned);
1678 let aligned_bits_match = c_aligned
1679 .iter()
1680 .zip(&c_full)
1681 .all(|(a, b)| a.to_bits() == b.to_bits());
1682 assert!(
1683 aligned_bits_match,
1684 "k{k} blk{block_size} asym{asym}: 16-aligned N shards must be \
1685 bit-identical to full-width, but differ"
1686 );
1687
1688 let mid_tile: &[(usize, usize)] = &[(0, 17), (17, 30), (47, 1), (48, 128)];
1694 assert_eq!(mid_tile.iter().map(|&(_, l)| l).sum::<usize>(), n);
1695 let c_mid = run_shards(mid_tile);
1696 any_mid_tile_drift |= c_mid
1697 .iter()
1698 .zip(&c_full)
1699 .any(|(a, b)| a.to_bits() != b.to_bits());
1700 }
1701 }
1702 assert!(
1703 any_mid_tile_drift,
1704 "expected at least one mid-tile-split N shard layout to drift from full-width \
1705 (non-vacuous guard); none did, so the 16-alignment fix is untested on this host"
1706 );
1707 }
1708
1709 #[test]
1710 fn sqnbit_int4_compint8_matches_reference() {
1711 if !std::arch::is_x86_feature_detected!("avx512f") {
1715 eprintln!(
1716 "skipping SQNBit int4 CompInt8 reference check: AVX-512F is unavailable; \
1717 AVX2 CompInt8 SQNBit M=1 asymmetric-weight bug: microsoft/onnxruntime#29853"
1718 );
1719 return;
1720 }
1721 #[cfg(target_arch = "x86_64")]
1744 let host_has_avx512 = std::arch::is_x86_feature_detected!("avx512f")
1745 && std::arch::is_x86_feature_detected!("avx512bw")
1746 && std::arch::is_x86_feature_detected!("avx512dq")
1747 && std::arch::is_x86_feature_detected!("avx512vl");
1748 #[cfg(not(target_arch = "x86_64"))]
1749 let host_has_avx512 = false;
1750 for &blk in &[32usize, 64, 128] {
1751 for &m in &[1usize, 8] {
1752 for &asym in &[false, true] {
1753 if m == 1 && asym && !host_has_avx512 {
1754 eprintln!(
1755 "skipping MLAS-broken AVX2 M=1 asymmetric CompInt8 blk{blk} \
1756 (production uses the hand int8 kernel here)"
1757 );
1758 continue;
1759 }
1760 check_sqnbit(SQNBitComputeType::Int8, m, 96, 256, blk, asym, false);
1761 }
1762 }
1763 }
1764 check_sqnbit(SQNBitComputeType::Int8, 4, 128, 512, 32, false, true);
1765 }
1766
1767 #[test]
1771 #[ignore = "perf probe; run explicitly with --ignored --nocapture"]
1772 fn perf_sqnbit() {
1773 use std::time::Instant;
1774 for &(k, n) in &[(2048usize, 2048usize), (4096, 11008)] {
1775 let weights: Vec<f32> = (0..n * k).map(|i| (i as f32 * 0.017).sin()).collect();
1776 let (packed_b, scales, _zps, _d) = quantize_int4(&weights, n, k, 32, false);
1777 for comp in [SQNBitComputeType::Fp32, SQNBitComputeType::Int8] {
1778 let packed = match SQNBitPackedB::new(n, k, 4, 32, comp, &packed_b, &scales, None) {
1779 Some(p) => p,
1780 None => continue,
1781 };
1782 for &m in &[1usize, 32] {
1783 let a: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.011).cos()).collect();
1784 for threads in [1usize, 8] {
1785 let pool = rayon::ThreadPoolBuilder::new()
1786 .num_threads(threads)
1787 .build()
1788 .unwrap();
1789 let per_us = pool.install(|| {
1790 let mut c = vec![0.0f32; m * n];
1791 for _ in 0..20 {
1792 sqnbit_gemm(&packed, m, &a, None, &mut c, true);
1793 }
1794 let iters = 200u32;
1795 let start = Instant::now();
1796 for _ in 0..iters {
1797 sqnbit_gemm(&packed, m, &a, None, &mut c, true);
1798 }
1799 start.elapsed().as_secs_f64() * 1e6 / iters as f64
1800 });
1801 eprintln!(
1802 "SQNBit int4 {comp:?} K={k} N={n} M={m} {threads}t: {per_us:.1} us/iter"
1803 );
1804 }
1805 }
1806 }
1807 }
1808 }
1809
1810 fn round_up(value: usize, multiple: usize) -> usize {
1811 value.div_ceil(multiple) * multiple
1812 }
1813
1814 #[allow(clippy::too_many_arguments)]
1816 fn ref_conv_nchw(
1817 input: &[f32],
1818 filter: &[f32],
1819 bias: Option<&[f32]>,
1820 n: usize,
1821 cin: usize,
1822 hin: usize,
1823 win: usize,
1824 cout: usize,
1825 kh: usize,
1826 kw: usize,
1827 pad: [usize; 4],
1828 stride: [usize; 2],
1829 group: usize,
1830 ) -> (Vec<f32>, usize, usize) {
1831 let hout = (hin + pad[0] + pad[2] - kh) / stride[0] + 1;
1832 let wout = (win + pad[1] + pad[3] - kw) / stride[1] + 1;
1833 let cin_g = cin / group;
1834 let cout_g = cout / group;
1835 let mut out = vec![0.0f32; n * cout * hout * wout];
1836 for ni in 0..n {
1837 for oc in 0..cout {
1838 let g = oc / cout_g;
1839 for oy in 0..hout {
1840 for ox in 0..wout {
1841 let mut acc = bias.map_or(0.0, |b| b[oc]);
1842 for icg in 0..cin_g {
1843 let ic = g * cin_g + icg;
1844 for ky in 0..kh {
1845 let iy = oy * stride[0] + ky;
1846 if iy < pad[0] || iy - pad[0] >= hin {
1847 continue;
1848 }
1849 let iy = iy - pad[0];
1850 for kx in 0..kw {
1851 let ix = ox * stride[1] + kx;
1852 if ix < pad[1] || ix - pad[1] >= win {
1853 continue;
1854 }
1855 let ix = ix - pad[1];
1856 let iv = input[((ni * cin + ic) * hin + iy) * win + ix];
1857 let fv = filter[(((oc * cin_g) + icg) * kh + ky) * kw + kx];
1858 acc += iv * fv;
1859 }
1860 }
1861 }
1862 out[((ni * cout + oc) * hout + oy) * wout + ox] = acc;
1863 }
1864 }
1865 }
1866 }
1867 (out, hout, wout)
1868 }
1869
1870 #[allow(clippy::too_many_arguments)]
1871 fn run_nchwc_group1(
1872 input: &[f32],
1873 filter: &[f32],
1874 bias: Option<&[f32]>,
1875 n: usize,
1876 cin: usize,
1877 hin: usize,
1878 win: usize,
1879 cout: usize,
1880 kh: usize,
1881 kw: usize,
1882 pad: [usize; 4],
1883 stride: [usize; 2],
1884 ) -> Vec<f32> {
1885 let block = nchwc_block_size();
1886 let hout = (hin + pad[0] + pad[2] - kh) / stride[0] + 1;
1887 let wout = (win + pad[1] + pad[3] - kw) / stride[1] + 1;
1888 let nchwc_cout = round_up(cout, block);
1889 let filter_shape = [cout as i64, cin as i64, kh as i64, kw as i64];
1890
1891 let reorder_input = cin >= block;
1892 let (packed_filter, conv_input, in_channels_for_shape) = if reorder_input {
1893 let nchwc_cin = round_up(cin, block);
1894 let mut pf = vec![0.0f32; nchwc_cout * nchwc_cin * kh * kw];
1895 nchwc_reorder_filter_bibo(&filter_shape, filter, &mut pf);
1896 let mut blocked = vec![0.0f32; n * nchwc_cin * hin * win];
1897 for ni in 0..n {
1898 nchwc_reorder_input_nchw(
1899 &input[ni * cin * hin * win..(ni + 1) * cin * hin * win],
1900 &mut blocked[ni * nchwc_cin * hin * win..(ni + 1) * nchwc_cin * hin * win],
1901 cin,
1902 hin * win,
1903 );
1904 }
1905 (pf, blocked, nchwc_cin)
1906 } else {
1907 let mut pf = vec![0.0f32; nchwc_cout * cin * kh * kw];
1908 nchwc_reorder_filter_bo(&filter_shape, filter, &mut pf);
1909 (pf, input.to_vec(), cin)
1910 };
1911
1912 let padded_bias = bias.map(|b| {
1913 let mut pb = vec![0.0f32; nchwc_cout];
1914 pb[..cout].copy_from_slice(b);
1915 pb
1916 });
1917
1918 let mut blocked_out = vec![0.0f32; n * nchwc_cout * hout * wout];
1919 nchwc_conv(
1920 &[
1921 n as i64,
1922 in_channels_for_shape as i64,
1923 hin as i64,
1924 win as i64,
1925 ],
1926 &[kh as i64, kw as i64],
1927 &[1, 1],
1928 &[pad[0] as i64, pad[1] as i64, pad[2] as i64, pad[3] as i64],
1929 &[stride[0] as i64, stride[1] as i64],
1930 &[n as i64, nchwc_cout as i64, hout as i64, wout as i64],
1931 1,
1932 &conv_input,
1933 &packed_filter,
1934 padded_bias.as_deref(),
1935 &mut blocked_out,
1936 NchwcActivation::IDENTITY,
1937 true,
1938 );
1939
1940 let mut out = vec![0.0f32; n * cout * hout * wout];
1941 nchwc_reorder_output_nchw(
1942 &[n as i64, cout as i64, hout as i64, wout as i64],
1943 &blocked_out,
1944 &mut out,
1945 );
1946 out
1947 }
1948
1949 fn max_abs_diff(a: &[f32], b: &[f32]) -> f32 {
1950 a.iter()
1951 .zip(b)
1952 .fold(0.0f32, |m, (x, y)| m.max((x - y).abs()))
1953 }
1954
1955 #[test]
1956 fn nchwc_block_size_is_supported() {
1957 assert!(nchwc_block_size() >= 8, "block size {}", nchwc_block_size());
1959 }
1960
1961 #[test]
1962 fn nchwc_conv_pointwise_matches_reference() {
1963 let block = nchwc_block_size();
1964 let (n, cin, hin, win, cout) = (1, 2 * block, 7, 7, 3 * block);
1965 let input: Vec<f32> = (0..n * cin * hin * win)
1966 .map(|i| ((i % 13) as f32 - 6.0) * 0.1)
1967 .collect();
1968 let filter: Vec<f32> = (0..cout * cin)
1969 .map(|i| ((i % 7) as f32 - 3.0) * 0.05)
1970 .collect();
1971 let bias: Vec<f32> = (0..cout).map(|i| (i as f32) * 0.01).collect();
1972 let (want, _, _) = ref_conv_nchw(
1973 &input,
1974 &filter,
1975 Some(&bias),
1976 n,
1977 cin,
1978 hin,
1979 win,
1980 cout,
1981 1,
1982 1,
1983 [0; 4],
1984 [1, 1],
1985 1,
1986 );
1987 let got = run_nchwc_group1(
1988 &input,
1989 &filter,
1990 Some(&bias),
1991 n,
1992 cin,
1993 hin,
1994 win,
1995 cout,
1996 1,
1997 1,
1998 [0; 4],
1999 [1, 1],
2000 );
2001 assert!(
2002 max_abs_diff(&want, &got) < 1e-4,
2003 "diff {}",
2004 max_abs_diff(&want, &got)
2005 );
2006 }
2007
2008 #[test]
2009 fn nchwc_conv_3x3_blocked_matches_reference() {
2010 let block = nchwc_block_size();
2011 let (n, cin, hin, win, cout) = (1, block, 9, 9, block);
2012 let input: Vec<f32> = (0..n * cin * hin * win)
2013 .map(|i| ((i % 17) as f32 - 8.0) * 0.05)
2014 .collect();
2015 let filter: Vec<f32> = (0..cout * cin * 9)
2016 .map(|i| ((i % 11) as f32 - 5.0) * 0.03)
2017 .collect();
2018 let (want, _, _) = ref_conv_nchw(
2019 &input,
2020 &filter,
2021 None,
2022 n,
2023 cin,
2024 hin,
2025 win,
2026 cout,
2027 3,
2028 3,
2029 [1, 1, 1, 1],
2030 [1, 1],
2031 1,
2032 );
2033 let got = run_nchwc_group1(
2034 &input,
2035 &filter,
2036 None,
2037 n,
2038 cin,
2039 hin,
2040 win,
2041 cout,
2042 3,
2043 3,
2044 [1, 1, 1, 1],
2045 [1, 1],
2046 );
2047 assert!(
2048 max_abs_diff(&want, &got) < 1e-4,
2049 "diff {}",
2050 max_abs_diff(&want, &got)
2051 );
2052 }
2053
2054 #[test]
2055 fn nchwc_conv_first_layer_nchw_input_matches_reference() {
2056 let block = nchwc_block_size();
2058 let (n, cin, hin, win, cout) = (1, 3, 16, 16, block + block / 2);
2059 let input: Vec<f32> = (0..n * cin * hin * win)
2060 .map(|i| ((i % 19) as f32 - 9.0) * 0.04)
2061 .collect();
2062 let filter: Vec<f32> = (0..cout * cin * 9)
2063 .map(|i| ((i % 13) as f32 - 6.0) * 0.02)
2064 .collect();
2065 let bias: Vec<f32> = (0..cout).map(|i| (i as f32) * 0.02 - 0.3).collect();
2066 let (want, _, _) = ref_conv_nchw(
2067 &input,
2068 &filter,
2069 Some(&bias),
2070 n,
2071 cin,
2072 hin,
2073 win,
2074 cout,
2075 3,
2076 3,
2077 [1, 1, 1, 1],
2078 [2, 2],
2079 1,
2080 );
2081 let got = run_nchwc_group1(
2082 &input,
2083 &filter,
2084 Some(&bias),
2085 n,
2086 cin,
2087 hin,
2088 win,
2089 cout,
2090 3,
2091 3,
2092 [1, 1, 1, 1],
2093 [2, 2],
2094 );
2095 assert!(
2096 max_abs_diff(&want, &got) < 1e-4,
2097 "diff {}",
2098 max_abs_diff(&want, &got)
2099 );
2100 }
2101
2102 #[test]
2103 fn nchwc_conv_depthwise_matches_reference() {
2104 let block = nchwc_block_size();
2106 let channels = 2 * block; let (n, hin, win) = (1, 8, 8);
2108 let input: Vec<f32> = (0..n * channels * hin * win)
2109 .map(|i| ((i % 15) as f32 - 7.0) * 0.06)
2110 .collect();
2111 let filter: Vec<f32> = (0..channels * 9)
2113 .map(|i| ((i % 7) as f32 - 3.0) * 0.05)
2114 .collect();
2115 let bias: Vec<f32> = (0..channels).map(|i| (i as f32) * 0.01).collect();
2116 let (want, hout, wout) = ref_conv_nchw(
2117 &input,
2118 &filter,
2119 Some(&bias),
2120 n,
2121 channels,
2122 hin,
2123 win,
2124 channels,
2125 3,
2126 3,
2127 [1, 1, 1, 1],
2128 [1, 1],
2129 channels,
2130 );
2131
2132 let nchwc_ch = round_up(channels, block);
2133 let mut pf = vec![0.0f32; nchwc_ch * 9];
2134 nchwc_reorder_filter_bo(&[channels as i64, 1, 3, 3], &filter, &mut pf);
2135 let mut blocked_in = vec![0.0f32; n * nchwc_ch * hin * win];
2136 nchwc_reorder_input_nchw(&input, &mut blocked_in, channels, hin * win);
2137 let mut padded_bias = vec![0.0f32; nchwc_ch];
2138 padded_bias[..channels].copy_from_slice(&bias);
2139 let mut blocked_out = vec![0.0f32; n * nchwc_ch * hout * wout];
2140 nchwc_conv(
2141 &[n as i64, nchwc_ch as i64, hin as i64, win as i64],
2142 &[3, 3],
2143 &[1, 1],
2144 &[1, 1, 1, 1],
2145 &[1, 1],
2146 &[n as i64, nchwc_ch as i64, hout as i64, wout as i64],
2147 nchwc_ch, &blocked_in,
2149 &pf,
2150 Some(&padded_bias),
2151 &mut blocked_out,
2152 NchwcActivation::IDENTITY,
2153 true,
2154 );
2155 let mut got = vec![0.0f32; n * channels * hout * wout];
2156 nchwc_reorder_output_nchw(
2157 &[n as i64, channels as i64, hout as i64, wout as i64],
2158 &blocked_out,
2159 &mut got,
2160 );
2161 assert!(
2162 max_abs_diff(&want, &got) < 1e-4,
2163 "diff {}",
2164 max_abs_diff(&want, &got)
2165 );
2166 }
2167
2168 #[test]
2169 fn nchwc_conv_relu_activation_matches_reference() {
2170 let block = nchwc_block_size();
2171 let (n, cin, hin, win, cout) = (1, block, 5, 5, block);
2172 let input: Vec<f32> = (0..n * cin * hin * win)
2173 .map(|i| ((i % 9) as f32 - 4.0) * 0.2)
2174 .collect();
2175 let filter: Vec<f32> = (0..cout * cin)
2176 .map(|i| ((i % 5) as f32 - 2.0) * 0.1)
2177 .collect();
2178 let (mut want, _, _) = ref_conv_nchw(
2179 &input,
2180 &filter,
2181 None,
2182 n,
2183 cin,
2184 hin,
2185 win,
2186 cout,
2187 1,
2188 1,
2189 [0; 4],
2190 [1, 1],
2191 1,
2192 );
2193 for v in &mut want {
2194 *v = v.max(0.0);
2195 }
2196 let nchwc_cout = round_up(cout, block);
2198 let nchwc_cin = round_up(cin, block);
2199 let mut pf = vec![0.0f32; nchwc_cout * nchwc_cin];
2200 nchwc_reorder_filter_bibo(&[cout as i64, cin as i64, 1, 1], &filter, &mut pf);
2201 let mut blocked_in = vec![0.0f32; n * nchwc_cin * hin * win];
2202 nchwc_reorder_input_nchw(&input, &mut blocked_in, cin, hin * win);
2203 let mut blocked_out = vec![0.0f32; n * nchwc_cout * hin * win];
2204 nchwc_conv(
2205 &[n as i64, nchwc_cin as i64, hin as i64, win as i64],
2206 &[1, 1],
2207 &[1, 1],
2208 &[0; 4],
2209 &[1, 1],
2210 &[n as i64, nchwc_cout as i64, hin as i64, win as i64],
2211 1,
2212 &blocked_in,
2213 &pf,
2214 None,
2215 &mut blocked_out,
2216 NchwcActivation::RELU,
2217 true,
2218 );
2219 let mut got = vec![0.0f32; n * cout * hin * win];
2220 nchwc_reorder_output_nchw(
2221 &[n as i64, cout as i64, hin as i64, win as i64],
2222 &blocked_out,
2223 &mut got,
2224 );
2225 assert!(
2226 max_abs_diff(&want, &got) < 1e-4,
2227 "diff {}",
2228 max_abs_diff(&want, &got)
2229 );
2230 }
2231
2232 #[test]
2233 fn nchwc_pool_max_and_average_match_reference() {
2234 let block = nchwc_block_size();
2235 let channels = block + block / 2; let (n, hin, win) = (1, 8, 8);
2237 let (kh, kw) = (2usize, 2usize);
2238 let (sh, sw) = (2usize, 2usize);
2239 let hout = (hin - kh) / sh + 1;
2240 let wout = (win - kw) / sw + 1;
2241 let input: Vec<f32> = (0..n * channels * hin * win)
2242 .map(|i| ((i % 23) as f32 - 11.0) * 0.13)
2243 .collect();
2244
2245 let nchwc_ch = round_up(channels, block);
2246 let mut blocked_in = vec![0.0f32; n * nchwc_ch * hin * win];
2247 nchwc_reorder_input_nchw(&input, &mut blocked_in, channels, hin * win);
2248
2249 for kind in [PoolKind::Maximum, PoolKind::AverageIncludePad] {
2250 let mut blocked_out = vec![0.0f32; n * nchwc_ch * hout * wout];
2251 nchwc_pool(
2252 kind,
2253 &[n as i64, nchwc_ch as i64, hin as i64, win as i64],
2254 &[kh as i64, kw as i64],
2255 &[1, 1],
2256 &[0, 0, 0, 0],
2257 &[sh as i64, sw as i64],
2258 &[n as i64, nchwc_ch as i64, hout as i64, wout as i64],
2259 &blocked_in,
2260 &mut blocked_out,
2261 );
2262 let mut got = vec![0.0f32; n * channels * hout * wout];
2263 nchwc_reorder_output_nchw(
2264 &[n as i64, channels as i64, hout as i64, wout as i64],
2265 &blocked_out,
2266 &mut got,
2267 );
2268
2269 let mut want = vec![0.0f32; n * channels * hout * wout];
2270 for c in 0..channels {
2271 for oh in 0..hout {
2272 for ow in 0..wout {
2273 let mut acc = if matches!(kind, PoolKind::Maximum) {
2274 f32::NEG_INFINITY
2275 } else {
2276 0.0
2277 };
2278 for ky in 0..kh {
2279 for kx in 0..kw {
2280 let ih = oh * sh + ky;
2281 let iw = ow * sw + kx;
2282 let v = input[((c * hin) + ih) * win + iw];
2283 if matches!(kind, PoolKind::Maximum) {
2284 acc = acc.max(v);
2285 } else {
2286 acc += v;
2287 }
2288 }
2289 }
2290 if !matches!(kind, PoolKind::Maximum) {
2291 acc /= (kh * kw) as f32;
2292 }
2293 want[((c * hout) + oh) * wout + ow] = acc;
2294 }
2295 }
2296 }
2297 assert!(
2298 max_abs_diff(&want, &got) < 1e-4,
2299 "kind {kind:?} diff {}",
2300 max_abs_diff(&want, &got)
2301 );
2302 }
2303 }
2304
2305 #[test]
2311 fn nchwc_reorder_round_trip_is_identity() {
2312 let block = nchwc_block_size();
2313 for &channels in &[block, block + 4] {
2316 let (n, h, w) = (1usize, 5usize, 7usize);
2317 let plane = h * w;
2318 let input: Vec<f32> = (0..n * channels * plane)
2319 .map(|i| ((i % 17) as f32 - 8.0) * 0.07)
2320 .collect();
2321
2322 let nchwc_ch = round_up(channels, block);
2323 let mut blocked = vec![7.0f32; n * nchwc_ch * plane]; nchwc_reorder_input_nchw(&input, &mut blocked, channels, plane);
2325
2326 let mut back = vec![0.0f32; n * channels * plane];
2327 nchwc_reorder_output_nchw(
2328 &[n as i64, channels as i64, h as i64, w as i64],
2329 &blocked,
2330 &mut back,
2331 );
2332
2333 assert_eq!(back, input, "round-trip mismatch for channels={channels}");
2334 }
2335 }
2336}