Skip to main content

mlas_sys/
lib.rs

1//! Thin FFI wrapper around a vendored subset of ONNX Runtime's MLAS
2//! single-precision GEMM (`MlasGemmBatch`).
3//!
4//! The vendored MLAS is compiled in its standalone `BUILD_MLAS_NO_ONNXRUNTIME`
5//! mode, whose threading primitives normally serialize. This crate installs a
6//! Rayon-backed parallel-for backend (see [`ensure_threading`] and
7//! `vendor/shim.cpp`) so MLAS keeps its own cache-aware GEMM tile partitioning
8//! while executing the tiles across the current Rayon pool — the same pool the
9//! rest of `onnx-runtime-ep-cpu` uses, so there is no oversubscription. See
10//! `docs/MLAS_SYS_SPIKE.md` for the original single-thread feasibility spike.
11
12use std::os::raw::c_int;
13use std::os::raw::c_void;
14use std::sync::Once;
15
16use rayon::prelude::*;
17
18unsafe extern "C" {
19    /// Vendored-MLAS SGEMM shim (single-threaded). Computes
20    /// `C := alpha * op(A) * op(B) + beta * C` with row-major matrices.
21    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    // ---- Blocked n-bit quantized GEMM (SQNBitGemm) ----
65    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    /// Register the Rust-backed threading backend with the vendored MLAS
117    /// standalone build (see `vendor/shim.cpp`). Passing the callbacks below
118    /// lets MLAS's own GEMM tile partitioning run across a real thread pool.
119    fn mlas_set_threading(
120        parallel_for: MlasParallelForFn,
121        max_threads: MlasMaxThreadsFn,
122        rust_ctx: *mut c_void,
123    );
124}
125
126/// One MLAS work unit: run partition `tid`. `task_ctx` is opaque C++ state.
127type MlasTaskFn = unsafe extern "C" fn(task_ctx: *mut c_void, tid: isize);
128/// Backend that runs `task(task_ctx, tid)` for every `tid` in `[0, iterations)`.
129type MlasParallelForFn = unsafe extern "C" fn(
130    rust_ctx: *mut c_void,
131    iterations: isize,
132    task: MlasTaskFn,
133    task_ctx: *mut c_void,
134);
135/// Backend that reports the degree of parallelism MLAS may use.
136type MlasMaxThreadsFn = unsafe extern "C" fn(rust_ctx: *mut c_void) -> c_int;
137
138/// Rayon-backed parallel-for. Runs on whatever pool is current at call time
139/// (i.e. the ep-cpu global pool, or a `ThreadPool::install` scope), so MLAS
140/// never spawns a second pool that would oversubscribe the machine.
141unsafe 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    // Carry the opaque C++ closure pointer across Rayon worker threads as an
151    // address (usize is Send + Sync). MLAS only *reads* the closure
152    // (`std::function::operator() const`) and each `tid` writes a disjoint
153    // output partition, so concurrent invocation is race-free.
154    let task_ctx = task_ctx as usize;
155    (0..iterations).into_par_iter().for_each(|tid| {
156        // SAFETY: `task_ctx` is valid for the whole `MlasGemmBatch` call that
157        // drives this parallel-for; each `tid` touches a disjoint output range.
158        unsafe { task(task_ctx as *mut c_void, tid) };
159    });
160}
161
162/// Report Rayon's current degree of parallelism to MLAS's partitioner, so the
163/// GEMM is split into as many tiles as there are worker threads available.
164unsafe 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
170/// Install the Rayon-backed threading backend into the vendored MLAS build.
171/// Idempotent; called before every GEMM entry point. Until this runs (e.g. in
172/// the mlas-sys unit tests that call the FFI directly) MLAS stays single
173/// threaded, matching the original spike behaviour.
174fn 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
180/// Runtime-selected f32 GEMM microkernel: 512 = AVX-512F, 3 = FMA3/AVX2,
181/// 1 = AVX, -1 = other/unknown, 0 = non-x86.
182pub fn selected_float_kernel() -> i32 {
183    unsafe { mlas_float_kernel_id() as i32 }
184}
185
186/// Pre-packed B weight buffer, mirroring how ORT pre-packs constant MatMul
187/// weights once and reuses the packed panel across calls.
188///
189/// MLAS's packed layout is accessed with aligned AVX-512 loads/stores, so the
190/// backing allocation is 64-byte aligned (a plain `Vec<u8>` is not).
191pub struct PackedB {
192    ptr: *mut u8,
193    layout: std::alloc::Layout,
194    n: usize,
195    k: usize,
196}
197
198// SAFETY: construction fully initializes the allocation, which is immutable
199// afterward. Packed GEMM calls only read it, so shared concurrent use is safe.
200unsafe impl Send for PackedB {}
201unsafe impl Sync for PackedB {}
202
203impl PackedB {
204    /// Pack a row-major `k x n` B matrix (no transpose, `ldb = n`).
205    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    /// Return the logical `(k, n)` dimensions of the packed B matrix.
216    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
227/// `C = A * packed(B)` for row-major A (`m x k`), reusing a pre-packed B.
228pub 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
251/// Safe wrapper computing `C = A * B` for row-major matrices with no transpose.
252///
253/// `a` is `m x k`, `b` is `k x n`, `c` is `m x n`. Uses `alpha = 1`,
254/// `beta = 0` (C is overwritten).
255pub 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/// General entry point mirroring the C shim, exposing transpose flags and
280/// alpha/beta. Leading dimensions default to the natural row-major strides.
281#[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/// Blocked n-bit quantized GEMM compute type, mirroring MLAS's
318/// `MLAS_QNBIT_GEMM_COMPUTE_TYPE`. Only the two x86 float-input variants used
319/// by the CPU `MatMulNBits` decode path are exposed.
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
321pub enum SQNBitComputeType {
322    /// fp32 activation, fp32 accumulate (`SQNBIT_CompFp32`).
323    Fp32,
324    /// int8 activation, int32 accumulate (`SQNBIT_CompInt8`); ONNX
325    /// `accuracy_level=4`.
326    Int8,
327}
328
329impl SQNBitComputeType {
330    #[inline]
331    fn raw(self) -> c_int {
332        // Values must match the MLAS_QNBIT_GEMM_COMPUTE_TYPE enum in
333        // vendor/mlas/.../inc/mlas_qnbit.h.
334        match self {
335            SQNBitComputeType::Fp32 => 0, // SQNBIT_CompFp32
336            SQNBitComputeType::Int8 => 3, // SQNBIT_CompInt8
337        }
338    }
339}
340
341/// Returns whether MLAS has a blocked n-bit GEMM kernel for the current host
342/// and the given `(bits, block_len, compute_type)`. Callers must gate every
343/// [`SQNBitPackedB`] / [`sqnbit_gemm`] use on this being `true`.
344pub 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
348/// MLAS-packed blockwise-quantized B weight for [`sqnbit_gemm`], mirroring how
349/// ORT pre-packs the constant `MatMulNBits` initializer once and reuses it.
350///
351/// The `B` bytes, scales, and optional zero points use the standard ONNX
352/// `MatMulNBits` layout (`[N, ceil(K/blk_len), blk_len*bits/8]`, LSB-first
353/// nibbles; scales `[N, ceil(K/blk_len)]`; packed uint8 zero points). For
354/// `Fp32` compute MLAS repacks only the nibbles and consumes scales/zero points
355/// at GEMM time (kept here so the packed weight is self-contained). For `Int8`
356/// compute MLAS bakes scale and zero point into per-block sums inside the packed
357/// buffer, so `scale`/`zp` are unused at GEMM time. A default (absent) zero
358/// point is the ONNX/MLAS midpoint (8 for int4), so symmetric weights need no
359/// zero point.
360pub 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
373// SAFETY: identical rationale to `PackedB`: construction fully initializes the
374// packed allocation and the owned scale/zp vectors, all of which are immutable
375// afterward. `sqnbit_gemm` only reads them, so sharing across threads (e.g.
376// MLAS's own tile parallelism) is race-free.
377unsafe impl Send for SQNBitPackedB {}
378unsafe impl Sync for SQNBitPackedB {}
379
380impl SQNBitPackedB {
381    /// Pack a blockwise-quantized B weight, returning `None` when MLAS reports
382    /// no packing/kernel is available for this shape on the current host (the
383    /// caller must then fall back to another path).
384    #[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    /// Logical `(k, n)` dimensions of the packed weight.
438    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
449/// Compute `C = A * dequant(packed) + bias` for row-major `A` (`m x k`) and
450/// `C` (`m x n`), reusing a pre-packed blockwise-quantized weight.
451///
452/// When `multithread` is true MLAS partitions the GEMM across the current Rayon
453/// pool (see [`PackedB`] threading notes); otherwise it runs serially. `bias`,
454/// when present, is added by MLAS itself (length `n`).
455pub 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    // MLAS rounds the workspace pointer up to an internal alignment, so
483    // over-allocate to keep the aligned [start, start+ws_size) region in bounds.
484    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    /// Naive row-major triple-loop reference: C = alpha*op(A)*op(B) + beta*C.
533    #[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        // Deterministic pseudo-values in a small range to keep f32 error low.
573        (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        // Sizes deliberately not multiples of typical 8/16 tile widths.
608        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        // B stored transposed: logical B is k x n, stored as n x k with ldb=k.
647        let (m, n, k) = (12, 20, 28);
648        let a = seq(m * k, 0.3);
649        let b_t = seq(n * k, 0.9); // n rows of length k
650        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        // A stored transposed: logical A is m x k, stored as k x m with lda=m.
676        let (m, n, k) = (14, 22, 18);
677        let a_t = seq(k * m, 0.4); // k rows of length m
678        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        // Proves parity-by-construction: on this AVX-512 host MLAS's runtime
719        // dispatch must pick the AVX-512F SGEMM microkernel.
720        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    /// Single-thread performance probe for the medium f32 MatMul shape
726    /// (32x512x512) recorded in docs/KERNEL_PERF.md. Ignored by default; run
727    /// with:
728    ///   cargo test -p mlas-sys --release -- --ignored --nocapture perf_sgemm_medium
729    #[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        // Warm up (caches + first-call platform init/dispatch).
740        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        // Prevent the loop from being optimized away.
751        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        // Pre-packed B (parity with ORT's constant-weight pre-packing).
762        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    /// Multi-thread scaling probe: measures the same 32x512x512 shape at 1 and
784    /// 8 Rayon threads to confirm MLAS's own tile partitioning now runs across
785    /// the pool. Ignored by default; run with:
786    ///   cargo test -p mlas-sys --release -- --ignored --nocapture perf_sgemm_multithread
787    #[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    // ---- SQNBitGemm (blocked int4) correctness ----
827
828    /// Quantize a row-major `N x K` f32 weight to ONNX `MatMulNBits` int4
829    /// blocks, returning `(packed_b, scales, zero_points, dequantized_nk)`.
830    /// `packed_b` is `[N, k_blocks, block_size/2]` LSB-first nibbles; `scales`
831    /// is `[N, k_blocks]`; `zero_points` (when `asymmetric`) is packed uint8
832    /// `[N, ceil(k_blocks/2)]`. `dequantized_nk` is the exact `(q-zp)*scale`
833    /// oracle in the same `N x K` layout.
834    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        // int8-activation compute quantizes A, so tolerances are looser.
966        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    /// Perf probe for int4 blockwise GEMM (decode M=1 + prefill M=32) at 1 and 8
977    /// threads. Ignored by default; run with:
978    ///   cargo test -p mlas-sys --release -- --ignored --nocapture perf_sqnbit
979    #[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}