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::ptr::NonNull;
15use std::sync::Once;
16
17use rayon::prelude::*;
18
19unsafe extern "C" {
20    /// Vendored-MLAS SGEMM shim (single-threaded). Computes
21    /// `C := alpha * op(A) * op(B) + beta * C` with row-major matrices.
22    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    /// Vectorized logistic (sigmoid) over `n` contiguous f32s: single-threaded
66    /// MLAS SIMD sigmoid, used to build SiLU without a scalar `expf` loop.
67    fn mlas_compute_logistic(input: *const f32, output: *mut f32, n: usize);
68    /// Vectorized fused SiLU over `n` contiguous f32s. MLAS runtime-dispatches
69    /// to its one-pass AVX-512F kernel when supported.
70    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    // ---- NCHWc blocked convolution ----
106    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    // ---- Blocked n-bit quantized GEMM (SQNBitGemm) ----
159    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    /// Register the Rust-backed threading backend with the vendored MLAS
211    /// standalone build (see `vendor/shim.cpp`). Passing the callbacks below
212    /// lets MLAS's own GEMM tile partitioning run across a real thread pool.
213    fn mlas_set_threading(
214        parallel_for: MlasParallelForFn,
215        max_threads: MlasMaxThreadsFn,
216        rust_ctx: *mut c_void,
217    );
218}
219
220/// One MLAS work unit: run partition `tid`. `task_ctx` is opaque C++ state.
221type MlasTaskFn = unsafe extern "C" fn(task_ctx: *mut c_void, tid: isize);
222/// Backend that runs `task(task_ctx, tid)` for every `tid` in `[0, iterations)`.
223type MlasParallelForFn = unsafe extern "C" fn(
224    rust_ctx: *mut c_void,
225    iterations: isize,
226    task: MlasTaskFn,
227    task_ctx: *mut c_void,
228);
229/// Backend that reports the degree of parallelism MLAS may use.
230type MlasMaxThreadsFn = unsafe extern "C" fn(rust_ctx: *mut c_void) -> c_int;
231
232/// Rayon-backed parallel-for. Runs on whatever pool is current at call time
233/// (i.e. the ep-cpu global pool, or a `ThreadPool::install` scope), so MLAS
234/// never spawns a second pool that would oversubscribe the machine.
235unsafe 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    // Carry the opaque C++ closure pointer across Rayon worker threads as an
245    // address (usize is Send + Sync). MLAS only *reads* the closure
246    // (`std::function::operator() const`) and each `tid` writes a disjoint
247    // output partition, so concurrent invocation is race-free.
248    let task_ctx = task_ctx as usize;
249    (0..iterations).into_par_iter().for_each(|tid| {
250        // SAFETY: `task_ctx` is valid for the whole `MlasGemmBatch` call that
251        // drives this parallel-for; each `tid` touches a disjoint output range.
252        unsafe { task(task_ctx as *mut c_void, tid) };
253    });
254}
255
256/// Report Rayon's current degree of parallelism to MLAS's partitioner, so the
257/// GEMM is split into as many tiles as there are worker threads available.
258unsafe 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
264/// Install the Rayon-backed threading backend into the vendored MLAS build.
265/// Idempotent; called before every GEMM entry point. Until this runs (e.g. in
266/// the mlas-sys unit tests that call the FFI directly) MLAS stays single
267/// threaded, matching the original spike behaviour.
268fn 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
274/// Runtime-selected f32 GEMM microkernel: 512 = AVX-512F, 3 = FMA3/AVX2,
275/// 1 = AVX, -1 = other/unknown, 0 = non-x86.
276pub fn selected_float_kernel() -> i32 {
277    unsafe { mlas_float_kernel_id() as i32 }
278}
279
280/// Compute the elementwise logistic (sigmoid) `output = 1 / (1 + exp(-input))`
281/// over equal-length contiguous f32 slices using MLAS's SIMD sigmoid. Single
282/// threaded; callers shard across threads themselves when needed.
283///
284/// This is the vectorized primitive behind SiLU (`x * sigmoid(x)`), replacing a
285/// scalar `expf` loop that LLVM cannot autovectorize.
286pub 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    // SAFETY: both slices are valid for `n` contiguous f32s; MLAS reads `input`
296    // and writes `output`, and Rust's borrow rules prove they do not alias.
297    unsafe { mlas_compute_logistic(input.as_ptr(), output.as_mut_ptr(), input.len()) };
298}
299
300/// Compute elementwise SiLU `output = input / (1 + exp(-input))` over
301/// equal-length contiguous f32 slices. MLAS runtime-dispatches to its fused
302/// one-pass AVX-512F kernel when available and uses a portable fallback
303/// elsewhere. Single threaded; callers shard across threads themselves.
304pub 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    // SAFETY: both slices are valid for `n` contiguous f32s; MLAS reads `input`
314    // and writes `output`, and Rust's borrow rules prove they do not alias.
315    unsafe { mlas_compute_silu(input.as_ptr(), output.as_mut_ptr(), input.len()) };
316}
317
318/// Compute contiguous Float32 elementwise addition with MLAS SIMD.
319pub 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
332/// Compute contiguous Float32 ReLU with MLAS SIMD.
333pub 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
347/// Compute contiguous Float32 clipping with MLAS SIMD.
348pub 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
362/// Prepared MLAS Float32 convolution parameters for one concrete NCHW shape.
363pub struct ConvPlan {
364    ptr: NonNull<c_void>,
365    working_buffer_elements: usize,
366}
367
368// SAFETY: MLAS treats prepared convolution parameters as immutable during
369// execution. Each call supplies disjoint input, scratch, and output buffers.
370unsafe impl Send for ConvPlan {}
371unsafe impl Sync for ConvPlan {}
372
373impl ConvPlan {
374    /// Prepare an N-dimensional NCHW convolution and return its scratch size.
375    #[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    /// Number of Float32 scratch elements required by [`Self::run`].
420    pub fn working_buffer_elements(&self) -> usize {
421        self.working_buffer_elements
422    }
423
424    /// Execute the prepared convolution.
425    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/// MLAS activation applied inside (or immediately after) a convolution.
459///
460/// Mirrors `MLAS_ACTIVATION_KIND`; the two `values` carry the kind-specific
461/// parameters (`Clip` min/max, `LeakyRelu`/`HardSigmoid` alpha/beta).
462#[derive(Clone, Copy, Debug)]
463pub struct NchwcActivation {
464    /// Raw `MLAS_ACTIVATION_KIND` discriminant (0 = identity, 1 = relu, 5 = clip).
465    pub kind: i32,
466    /// Kind-specific parameters, laid over the `Parameters.Values[2]` union.
467    pub values: [f32; 2],
468}
469
470impl NchwcActivation {
471    /// No activation (`MlasIdentityActivation`).
472    pub const IDENTITY: Self = Self {
473        kind: 0,
474        values: [0.0, 0.0],
475    };
476    /// ReLU (`MlasReluActivation`).
477    pub const RELU: Self = Self {
478        kind: 1,
479        values: [0.0, 0.0],
480    };
481
482    /// Clip activation (`MlasClipActivation`) with the given bounds.
483    pub fn clip(minimum: f32, maximum: f32) -> Self {
484        Self {
485            kind: 5,
486            values: [minimum, maximum],
487        }
488    }
489}
490
491/// SIMD channel-block width used by the MLAS NCHWc kernels (8 for AVX2, 16 for
492/// AVX-512). A value `<= 1` means the host has no blocked-convolution kernel and
493/// callers must use the plain [`ConvPlan`] path instead.
494pub fn nchwc_block_size() -> usize {
495    unsafe { mlas_nchwc_block_size() }
496}
497
498/// Reorder an `OIHW` filter into the `OIHWBiBo` layout (both input and output
499/// channels blocked), padding partial blocks with zeros. `dest` must hold
500/// `round_up(O, block) * round_up(I, block) * H * W` elements.
501pub 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
507/// Reorder an `OIHW` filter into the `OIHWBo` layout (only output channels
508/// blocked), padding partial output blocks with zeros. Used for the NCHW-input
509/// (first-layer) and depthwise algorithms. `dest` must hold
510/// `round_up(O, block) * I * H * W` elements.
511pub 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
517/// Reorder an NCHW activation plane set into NCHWc. `channels` must be a
518/// multiple of 4; `dest` must hold `round_up(channels, block) * input_size`
519/// elements (partial trailing block is zero padded).
520pub 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
532/// Reorder an NCHWc output buffer back to dense NCHW, keeping only
533/// `output_shape[1]` channels.
534pub 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/// Execute an NCHWc blocked 2-D convolution.
542///
543/// `input`/`output` are in NCHWc block layout except for the NCHW-input
544/// (first-layer) algorithm, where `input` stays plain NCHW. `filter` must be
545/// pre-reordered (`OIHWBiBo` or `OIHWBo`) to match the algorithm MLAS selects
546/// from the shape. `bias`, when present, must be padded to
547/// `round_up(output_channels, block)` elements. `zero_mode` false accumulates
548/// into `output` (Conv/Sum fusion); true overwrites it.
549#[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/// MLAS Float32 pooling mode.
588#[derive(Clone, Copy, Debug)]
589#[repr(i32)]
590pub enum PoolKind {
591    Maximum = 0,
592    AverageExcludePad = 1,
593    AverageIncludePad = 2,
594}
595
596/// Execute an N-dimensional NCHW Float32 pool using MLAS.
597#[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/// Execute an NCHWc blocked 2-D pool using MLAS.
631///
632/// `input_shape` / `output_shape` are the blocked NCHWc shapes
633/// `[N, round_up(C, block), H, W]`; MLAS pools each channel independently on the
634/// blocked buffer, so callers keep the activation in NCHWc across the pool with
635/// no reorder. `input` / `output` are blocked buffers. Mirrors ONNX Runtime's
636/// `NchwcTransformer` handling of pooling.
637#[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}
664///
665/// MLAS's packed layout is accessed with aligned AVX-512 loads/stores, so the
666/// backing allocation is 64-byte aligned (a plain `Vec<u8>` is not).
667pub struct PackedB {
668    ptr: *mut u8,
669    layout: std::alloc::Layout,
670    n: usize,
671    k: usize,
672}
673
674// SAFETY: construction fully initializes the allocation, which is immutable
675// afterward. Packed GEMM calls only read it, so shared concurrent use is safe.
676unsafe impl Send for PackedB {}
677unsafe impl Sync for PackedB {}
678
679impl PackedB {
680    /// Pack a row-major `k x n` B matrix (no transpose, `ldb = n`).
681    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    /// Return the logical `(k, n)` dimensions of the packed B matrix.
692    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
703/// `C = A * packed(B)` for row-major A (`m x k`), reusing a pre-packed B.
704pub 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
727/// Safe wrapper computing `C = A * B` for row-major matrices with no transpose.
728///
729/// `a` is `m x k`, `b` is `k x n`, `c` is `m x n`. Uses `alpha = 1`,
730/// `beta = 0` (C is overwritten).
731pub 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/// General entry point mirroring the C shim, exposing transpose flags and
756/// alpha/beta. Leading dimensions default to the natural row-major strides.
757#[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/// Blocked n-bit quantized GEMM compute type, mirroring MLAS's
794/// `MLAS_QNBIT_GEMM_COMPUTE_TYPE`. Only the two x86 float-input variants used
795/// by the CPU `MatMulNBits` decode path are exposed.
796#[derive(Debug, Clone, Copy, PartialEq, Eq)]
797pub enum SQNBitComputeType {
798    /// fp32 activation, fp32 accumulate (`SQNBIT_CompFp32`).
799    Fp32,
800    /// int8 activation, int32 accumulate (`SQNBIT_CompInt8`); ONNX
801    /// `accuracy_level=4`.
802    Int8,
803}
804
805impl SQNBitComputeType {
806    #[inline]
807    fn raw(self) -> c_int {
808        // Values must match the MLAS_QNBIT_GEMM_COMPUTE_TYPE enum in
809        // vendor/mlas/.../inc/mlas_qnbit.h.
810        match self {
811            SQNBitComputeType::Fp32 => 0, // SQNBIT_CompFp32
812            SQNBitComputeType::Int8 => 3, // SQNBIT_CompInt8
813        }
814    }
815}
816
817/// Returns whether MLAS has a blocked n-bit GEMM kernel for the current host
818/// and the given `(bits, block_len, compute_type)`. Callers must gate every
819/// [`SQNBitPackedB`] / [`sqnbit_gemm`] use on this being `true`.
820pub 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
824/// MLAS-packed blockwise-quantized B weight for [`sqnbit_gemm`], mirroring how
825/// ORT pre-packs the constant `MatMulNBits` initializer once and reuses it.
826///
827/// The `B` bytes, scales, and optional zero points use the standard ONNX
828/// `MatMulNBits` layout (`[N, ceil(K/blk_len), blk_len*bits/8]`, LSB-first
829/// nibbles; scales `[N, ceil(K/blk_len)]`; packed uint8 zero points). For
830/// `Fp32` compute MLAS repacks only the nibbles and consumes scales/zero points
831/// at GEMM time (kept here so the packed weight is self-contained). For `Int8`
832/// compute MLAS bakes scale and zero point into per-block sums inside the packed
833/// buffer, so `scale`/`zp` are unused at GEMM time. A default (absent) zero
834/// point is the ONNX/MLAS midpoint (8 for int4), so symmetric weights need no
835/// zero point.
836pub 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
849// SAFETY: identical rationale to `PackedB`: construction fully initializes the
850// packed allocation and the owned scale/zp vectors, all of which are immutable
851// afterward. `sqnbit_gemm` only reads them, so sharing across threads (e.g.
852// MLAS's own tile parallelism) is race-free.
853unsafe impl Send for SQNBitPackedB {}
854unsafe impl Sync for SQNBitPackedB {}
855
856impl SQNBitPackedB {
857    /// Pack a blockwise-quantized B weight, returning `None` when MLAS reports
858    /// no packing/kernel is available for this shape on the current host (the
859    /// caller must then fall back to another path).
860    #[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    /// Logical `(k, n)` dimensions of the packed weight.
914    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
925/// Compute `C = A * dequant(packed) + bias` for row-major `A` (`m x k`) and
926/// `C` (`m x n`), reusing a pre-packed blockwise-quantized weight.
927///
928/// When `multithread` is true MLAS partitions the GEMM across the current Rayon
929/// pool (see [`PackedB`] threading notes); otherwise it runs serially. `bias`,
930/// when present, is added by MLAS itself (length `n`).
931pub 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    // Contiguous output: leading dimension equals the packed weight's N.
942    // SAFETY: `c` is `m * n` contiguous f32s, so writing `m` rows of `n`
943    // columns at stride `n` stays in bounds.
944    unsafe { sqnbit_gemm_into(packed, m, a, bias, c.as_mut_ptr(), n, multithread) };
945}
946
947/// Compute one N-shard of `C = A * dequant(packed) + bias` into a caller-owned
948/// output whose leading dimension is `ldc` (columns per row), writing this
949/// shard's `packed.n` columns starting at `c` for each of the `m` rows. This
950/// lets a weight partitioned along N (e.g. one shard per decode worker) write
951/// its columns into a shared `[m, ldc]` output without a scatter copy; for a
952/// single full-width shard `ldc == packed.n` and it matches [`sqnbit_gemm`].
953///
954/// # Safety
955/// `c` must point at a valid f32 region covering `(m - 1) * ldc + packed.n`
956/// elements (the last row needs `packed.n` columns), `ldc >= packed.n`, and no
957/// other thread may write the same `[row, col]` cells concurrently.
958pub 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    // MLAS rounds the workspace pointer up to an internal alignment, so
987    // over-allocate to keep the aligned [start, start+ws_size) region in bounds.
988    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    /// Naive row-major triple-loop reference: C = alpha*op(A)*op(B) + beta*C.
1037    #[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        // Deterministic pseudo-values in a small range to keep f32 error low.
1077        (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        // Sizes deliberately not multiples of typical 8/16 tile widths.
1112        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        // B stored transposed: logical B is k x n, stored as n x k with ldb=k.
1151        let (m, n, k) = (12, 20, 28);
1152        let a = seq(m * k, 0.3);
1153        let b_t = seq(n * k, 0.9); // n rows of length k
1154        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        // A stored transposed: logical A is m x k, stored as k x m with lda=m.
1180        let (m, n, k) = (14, 22, 18);
1181        let a_t = seq(k * m, 0.4); // k rows of length m
1182        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    /// Single-thread performance probe for the medium f32 MatMul shape
1242    /// (32x512x512) recorded in docs/KERNEL_PERF.md. Ignored by default; run
1243    /// with:
1244    ///   cargo test -p mlas-sys --release -- --ignored --nocapture perf_sgemm_medium
1245    #[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        // Warm up (caches + first-call platform init/dispatch).
1256        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        // Prevent the loop from being optimized away.
1267        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        // Pre-packed B (parity with ORT's constant-weight pre-packing).
1278        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    /// Multi-thread scaling probe: measures the same 32x512x512 shape at 1 and
1300    /// 8 Rayon threads to confirm MLAS's own tile partitioning now runs across
1301    /// the pool. Ignored by default; run with:
1302    ///   cargo test -p mlas-sys --release -- --ignored --nocapture perf_sgemm_multithread
1303    #[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    // ---- SQNBitGemm (blocked int4) correctness ----
1343
1344    /// Quantize a row-major `N x K` f32 weight to ONNX `MatMulNBits` int4
1345    /// blocks, returning `(packed_b, scales, zero_points, dequantized_nk)`.
1346    /// `packed_b` is `[N, k_blocks, block_size/2]` LSB-first nibbles; `scales`
1347    /// is `[N, k_blocks]`; `zero_points` (when `asymmetric`) is packed uint8
1348    /// `[N, ceil(k_blocks/2)]`. `dequantized_nk` is the exact `(q-zp)*scale`
1349    /// oracle in the same `N x K` layout.
1350    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    /// N-sharding parity: splitting the weight into contiguous output-column
1480    /// shards and running each through [`sqnbit_gemm_into`] (writing its columns
1481    /// into a shared `[m, n]` output at stride `n`) reproduces the full-width
1482    /// [`sqnbit_gemm`] result. Each output column is a GEMV over K independent of
1483    /// the other columns, so partitioning N cannot change the arithmetic
1484    /// *modulo* MLAS's own SIMD column-tiling: the fp32 kernel processes columns
1485    /// in fixed-width tiles, so a shard boundary that falls mid-tile can reorder
1486    /// a block-sum reduction and shift a result by ~1 ULP. The tolerance is a few
1487    /// ULP (much tighter than the `2e-2` dequant-reference tolerance), which is
1488    /// the invariant the ep-cpu decode path relies on when it fans a projection's
1489    /// N-shards across the persistent decode workers (verified byte-identical
1490    /// end-to-end over 128 greedy tokens on Qwen2.5-0.5B).
1491    #[test]
1492    fn sqnbit_int4_n_shards_match_full() {
1493        let n = 96usize;
1494        // Include all export block sizes and a second K/block combination. The
1495        // deliberately uneven N shards below remain the decode-pool analogue.
1496        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                        // Deliberately uneven contiguous shards, like the decode
1533                        // pool's per-worker segments.
1534                        let shards: &[(usize, usize)] = &[(0, 17), (17, 30), (47, 1), (48, 48)];
1535                        // multithread=false mirrors the per-worker SPMD dispatch;
1536                        // multithread=true mirrors the prefill shard loop.
1537                        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                                // SAFETY: shards own disjoint contiguous column ranges
1559                                // of the [m, n] output; `start + len <= n`.
1560                                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                            // A few ULP at magnitude ~60 is ~2.5e-4; 1e-3 covers the
1573                            // worst-case tiling reorder with margin while still being
1574                            // ~20x tighter than the dequant-reference tolerance.
1575                            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    /// Regression lock for the persistent-pool decode fix: when contiguous N
1592    /// shards are cut on **N-tile-aligned** boundaries, the concatenated SQNBit
1593    /// CompFp32 GEMV is *bit-identical* (`to_bits`) to the full-width call --
1594    /// MLAS processes each whole N-tile the same way regardless of how many
1595    /// columns the shard holds. A boundary that splits an N-tile (odd column)
1596    /// instead forces MLAS's narrower remainder path and drifts by >= 1 ULP.
1597    ///
1598    /// The ep-cpu decode path snaps every interior shard boundary to a multiple
1599    /// of 16 (`MLAS_SQNBIT_DECODE_SHARD_ALIGN`) for exactly this reason; this
1600    /// test is the model-free proof that alignment is load-bearing (the
1601    /// mid-tile split below is asserted to actually differ, so the aligned
1602    /// assertion cannot pass vacuously).
1603    #[test]
1604    fn sqnbit_int4_tile_aligned_shards_are_bit_exact() {
1605        // qwen3-0.6b-flavoured widths: N not a multiple of the tile, block-128.
1606        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                        // SAFETY: disjoint contiguous column ranges; start+len <= n.
1659                        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                // Tile-aligned (multiple-of-16) interior boundaries: bit-exact.
1675                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                // Mid-tile boundaries (odd columns split an N-tile) drift from
1689                // the full-width call. The drift is data-dependent, so require it
1690                // to appear in at least one case overall (asserted after the loop)
1691                // rather than every case -- enough to prove the aligned assertion
1692                // above is non-vacuous.
1693                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        // Portability guard pending microsoft/onnxruntime#29853: only the AVX2
1712        // CompInt8 SQNBit path with M=1 and asymmetric weights is affected.
1713        // Keep validating AVX-512; SQNBit Int8 is not broken on all non-AVX-512 hosts.
1714        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        // int8-activation compute quantizes A, so tolerances are looser.
1722        //
1723        // Cross-CPU caveat: MLAS's *AVX2* M=1 CompInt8 SQNBit microkernel with a
1724        // zero point (`SQ4BitGemmM1Kernel_CompInt8_avx2`, all block sizes) is
1725        // numerically broken -- it disagrees with the dequantized reference by
1726        // ~46% (mlas=6.09 vs ref=11.29), far beyond int8 quantization tolerance.
1727        // The AVX-512 M=1 kernel and every AVX2 M>1 kernel (which apply the zero
1728        // point via the precomputed block-sum correction) are correct. Verified
1729        // under Intel SDE (`sde64 -hsw` fails, `-skx` passes); see
1730        // .squad/decisions/inbox/ripley-mlas-cross-cpu.md and the upstream issue
1731        // draft ripley-ort-issue-draft.md. Production never hits this path: int4
1732        // MatMulNBits with m=1 always routes to the hand int8 decode kernel (the
1733        // `sqnbit_decode_min() >= 2` crossover), and `try_mlas_sqnbit` additionally
1734        // refuses M=1 asymmetric CompInt8 on non-AVX-512 hosts. So the M=1
1735        // asymmetric case only exercises an MLAS capability we deliberately avoid;
1736        // gate it to AVX-512 hosts (where it is correct) rather than asserting a
1737        // value MLAS computes wrong on AVX2.
1738        // MLAS installs its correct AVX-512 SQNBit dispatch only when the host
1739        // has AVX512F *and* the core trio BW+DQ+VL (vendored platform.cpp:572,
1740        // under the AVX512F check at :547); AVX512F alone falls back to the
1741        // Avx2/Avx2vnni dispatch, i.e. the broken kernel. Mirror that exact gate
1742        // so the skip condition matches production's `host_has_mlas_sqnbit_avx512`.
1743        #[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    /// Perf probe for int4 blockwise GEMM (decode M=1 + prefill M=32) at 1 and 8
1768    /// threads. Ignored by default; run with:
1769    ///   cargo test -p mlas-sys --release -- --ignored --nocapture perf_sqnbit
1770    #[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    /// Naive NCHW convolution reference (group=1) with optional bias.
1815    #[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        // On x86_64 the vendored build always has an NCHWc kernel (8 or 16).
1958        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        // Input channels < block: the NCHW-input (first-layer) algorithm.
2057        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        // Depthwise: group == channels, one input & output channel per group.
2105        let block = nchwc_block_size();
2106        let channels = 2 * block; // must be a multiple of 4
2107        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        // Filter shape [channels, 1, 3, 3].
2112        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, // group count == blocked channel count for depthwise
2148            &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        // Reuse pointwise path but apply ReLU.
2197        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; // partial trailing block exercises padding
2236        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    /// NCHW -> NCHWc -> NCHW must reproduce the original activation exactly for
2306    /// the kept channels, including when the channel count leaves a partial
2307    /// trailing block (padding lanes are added on the way in and dropped on the
2308    /// way out). This is the layout round-trip the graph pass relies on at
2309    /// region entry/exit boundaries.
2310    #[test]
2311    fn nchwc_reorder_round_trip_is_identity() {
2312        let block = nchwc_block_size();
2313        // Exercise both an exact multiple of the block and a partial trailing
2314        // block (still a multiple of 4, the reorder's channel-group unit).
2315        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]; // non-zero fill
2324            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}