Skip to main content

tract_linalg/frame/mmm/
mod.rs

1#[macro_use]
2mod macros;
3
4pub mod cost_model;
5#[macro_use]
6pub(crate) mod fuse;
7pub(crate) mod input_store;
8pub(crate) mod kernel;
9#[macro_use]
10pub(crate) mod panel_extract;
11mod scratch;
12mod storage;
13
14#[cfg(test)]
15#[macro_use]
16pub mod tests;
17
18use crate::multithread::Executor;
19use std::borrow::Cow;
20use std::cmp::Ordering;
21use std::fmt::Debug;
22use tract_data::internal::*;
23
24pub use cost_model::*;
25pub use fuse::*;
26pub use input_store::*;
27pub use kernel::*;
28pub use panel_extract::*;
29pub use scratch::*;
30pub use storage::*;
31
32pub fn no_prefetch(_ptr: *const u8, _len: usize) {}
33
34#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
35pub enum ImplementationQuality {
36    /// Individual operations are emulated by individual conversion (f16->f32->f16)
37    Dreadful,
38    /// Rust scalar operation (with whatever optimisation the compiler manages)
39    Generic,
40    /// Implicit vectorization (e.g. Rust code, some unrolled loops, explicit template instantiations for small constant)
41    RustOptimized,
42    /// Explicit vectorization (e.g. intrinsics vector code)
43    TargetOptimized,
44    /// Hand optimized (assembly)
45    ManuallyOptimized,
46}
47
48impl ImplementationQuality {
49    pub fn best_to_worst() -> &'static [ImplementationQuality] {
50        use ImplementationQuality::*;
51        &[ManuallyOptimized, TargetOptimized, RustOptimized, Generic, Dreadful]
52    }
53
54    pub fn cost(&self) -> usize {
55        ImplementationQuality::best_to_worst().iter().position(|x| x == self).unwrap()
56    }
57}
58
59impl PartialOrd for ImplementationQuality {
60    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
61        Some(usize::from(*self).cmp(&usize::from(*other)))
62    }
63}
64
65impl From<ImplementationQuality> for usize {
66    fn from(value: ImplementationQuality) -> Self {
67        value.cost()
68    }
69}
70
71pub trait MatMatMul: Debug + dyn_clone::DynClone + Send + Sync + std::any::Any {
72    fn name(&self) -> &str;
73    fn mr(&self) -> usize;
74    fn nr(&self) -> usize;
75
76    fn quality(&self) -> ImplementationQuality;
77    fn dynamic_boost(&self) -> isize;
78
79    /// Whether this kernel is runnable on the current CPU (platform feature
80    /// gate, e.g. FEAT_DotProd for the SDOT i8 kernel).
81    fn is_supported_here(&self) -> bool;
82
83    #[allow(clippy::type_complexity)]
84    fn packings(&self) -> &[(Box<dyn MMMInputFormat>, Box<dyn MMMInputFormat>)];
85
86    fn internal_type(&self) -> DatumType;
87
88    unsafe fn c_view(&self, m_axis: Option<usize>, n_axis: Option<usize>) -> OutputStoreSpec;
89    unsafe fn c_from_data_and_strides(
90        &self,
91        item_size: usize,
92        row_stride: isize,
93        col_stride: isize,
94    ) -> OutputStoreSpec;
95
96    fn can_fuse(&self, spec: &FusedSpec) -> bool;
97
98    fn stores(&self) -> Cow<'_, [DatumType]>;
99
100    unsafe fn run(&self, m: usize, n: usize, non_linear: &[FusedSpec]) -> TractResult<()> {
101        unsafe {
102            let mut scratch = self.allocate_scratch_space();
103            self.run_with_scratch_space(m, n, &mut *scratch, non_linear)
104        }
105    }
106
107    unsafe fn allocate_scratch_space(&self) -> Box<dyn ScratchSpace>;
108    unsafe fn can_use_scratch_space(&self, scratch: &dyn ScratchSpace) -> bool;
109    unsafe fn run_with_scratch_space(
110        &self,
111        m: usize,
112        n: usize,
113        scratch: &mut dyn ScratchSpace,
114        non_linear: &[FusedSpec],
115    ) -> TractResult<()>;
116}
117
118dyn_clone::clone_trait_object!(MatMatMul);
119
120impl PartialEq for Box<dyn MatMatMul> {
121    fn eq(&self, other: &Box<dyn MatMatMul>) -> bool {
122        self.name() == other.name()
123    }
124}
125impl Eq for Box<dyn MatMatMul> {}
126
127impl std::hash::Hash for Box<dyn MatMatMul> {
128    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
129        self.name().hash(state)
130    }
131}
132
133impl<K: MatMatMulKer> MatMatMul for K {
134    fn name(&self) -> &str {
135        self.name()
136    }
137    fn mr(&self) -> usize {
138        self.mr()
139    }
140    fn nr(&self) -> usize {
141        self.nr()
142    }
143
144    fn quality(&self) -> ImplementationQuality {
145        MatMatMulKer::quality(self)
146    }
147
148    fn dynamic_boost(&self) -> isize {
149        MatMatMulKer::dynamic_boost(self)
150    }
151
152    fn is_supported_here(&self) -> bool {
153        MatMatMulKer::is_supported_here(self)
154    }
155
156    fn packings(&self) -> &[(Box<dyn MMMInputFormat>, Box<dyn MMMInputFormat>)] {
157        self.packings()
158    }
159
160    fn internal_type(&self) -> DatumType {
161        K::Acc::datum_type()
162    }
163
164    fn can_fuse(&self, spec: &FusedSpec) -> bool {
165        self.can_fuse(spec)
166    }
167
168    unsafe fn c_view(&self, m_axis: Option<usize>, n_axis: Option<usize>) -> OutputStoreSpec {
169        OutputStoreSpec::View { m_axis, n_axis, mr: self.mr(), nr: self.nr() }
170    }
171
172    unsafe fn c_from_data_and_strides(
173        &self,
174        item_size: usize,
175        row_stride: isize,
176        col_stride: isize,
177    ) -> OutputStoreSpec {
178        OutputStoreSpec::Strides {
179            row_byte_stride: row_stride * item_size as isize,
180            col_byte_stride: col_stride * item_size as isize,
181            mr: self.mr(),
182            nr: self.nr(),
183        }
184    }
185
186    fn stores(&self) -> Cow<'_, [DatumType]> {
187        self.stores()
188    }
189
190    unsafe fn allocate_scratch_space(&self) -> Box<dyn ScratchSpace> {
191        Box::<ScratchSpaceImpl<K::Acc>>::default()
192    }
193
194    unsafe fn can_use_scratch_space(&self, scratch: &dyn ScratchSpace) -> bool {
195        scratch.downcast_ref::<ScratchSpaceImpl<K::Acc>>().is_some()
196    }
197
198    unsafe fn run_with_scratch_space(
199        &self,
200        m: usize,
201        n: usize,
202        scratch: &mut dyn ScratchSpace,
203        non_linear: &[FusedSpec],
204    ) -> TractResult<()> {
205        unsafe {
206            let scratch = scratch
207                .downcast_mut::<ScratchSpaceImpl<K::Acc>>()
208                .context("Wrong scratch space type")?;
209            scratch.prepare(self, m, n, non_linear)?;
210            if n == 1 && self.nr() == 1 {
211                run_with_scratch_space_vec(self, m, scratch, non_linear)
212            } else {
213                let (mut prefer_col, mut prefer_row) = (0, 0);
214                for uop in non_linear.iter() {
215                    if let Some(col) = uop.prefer_col_outer() {
216                        prefer_col = col as usize;
217                        prefer_row = (!col) as usize;
218                    }
219                }
220                // k drives the single-thread cache-block size; read it from the
221                // first AddMatMul's packed input (0 if none → max block).
222                let k = non_linear
223                    .iter()
224                    .find_map(|f| match f {
225                        FusedSpec::AddMatMul { a, .. } => Some(a.k()),
226                        _ => None,
227                    })
228                    .unwrap_or(0);
229                if prefer_col > prefer_row {
230                    run_with_scratch_space_col_outer(self, m, n, k, scratch, non_linear)
231                } else {
232                    run_with_scratch_space_row_outer(self, m, n, k, scratch, non_linear)
233                }
234            }
235        }
236    }
237}
238
239unsafe fn run_with_scratch_space_vec<K: MatMatMulKer>(
240    ker: &K,
241    m: usize,
242    scratch: &mut ScratchSpaceImpl<K::Acc>,
243    non_linear: &[FusedSpec],
244) -> TractResult<()> {
245    unsafe {
246        match crate::multithread::current_tract_executor() {
247            Executor::SingleThread => scratch.run_in_tls_scope(|scratch, tls| {
248                for ia in 0..m.divceil(ker.mr()) {
249                    scratch.run_one_tile(ker, non_linear, tls, ia, 0)?;
250                }
251                TractResult::Ok(())
252            }),
253            #[cfg(feature = "multithread-mm")]
254            Executor::MultiThread(pool) => chunked_dispatch_rayon(
255                Some(&pool),
256                m.divceil(ker.mr()),
257                1,
258                |ia_start, ia_end, _, _| {
259                    scratch.run_in_tls_scope(|scratch, tls| {
260                        for ia in ia_start..ia_end {
261                            scratch.run_one_tile(ker, non_linear, tls, ia, 0)?;
262                        }
263                        TractResult::Ok(())
264                    })
265                },
266            ),
267            #[cfg(feature = "multithread-mm")]
268            Executor::RayonGlobal => {
269                chunked_dispatch_rayon(None, m.divceil(ker.mr()), 1, |ia_start, ia_end, _, _| {
270                    scratch.run_in_tls_scope(|scratch, tls| {
271                        for ia in ia_start..ia_end {
272                            scratch.run_one_tile(ker, non_linear, tls, ia, 0)?;
273                        }
274                        TractResult::Ok(())
275                    })
276                })
277            }
278        }
279    }
280}
281
282/// Upper bound on the inner (L2-resident) panel-block edge (matches the
283/// multithread `chunk_grid` default).
284const ST_BLK_MAX: usize = 16;
285
286/// Upper bound on the outer (L3-resident) super-block edge. 4× the inner cap so
287/// an L3 several times larger than L2 can hold a meaningfully bigger super-block.
288const ST_BLK_L3_MAX: usize = 64;
289
290/// Panel-block working-set budget (bytes) from a detected cache size: a fraction
291/// `num/den` of the cache (leaving room for the C accumulator tile + packing
292/// metadata), clamped to a sane range. `0` (cache unknown) ⇒ `fallback`, which
293/// is kept small so the block ≈ the naive loop and can never over-block a cache
294/// it can't see. Sizes come from the shared [`crate::cache`] probe.
295fn tier_budget_bytes(cache_bytes: usize, num: usize, den: usize, fallback: usize) -> usize {
296    if cache_bytes == 0 {
297        fallback
298    } else {
299        (cache_bytes * num / den).clamp(64 * 1024, 64 * 1024 * 1024)
300    }
301}
302
303/// Inner tier: ~a third of L2 (private per perf-core), 256 KiB fallback.
304fn l2_block_budget_bytes() -> usize {
305    tier_budget_bytes(crate::cache::cache_info().l2, 1, 3, 256 * 1024)
306}
307
308/// Outer tier: `(llc_bytes, budget_bytes)` — the raw last-level-cache size and the
309/// fraction of it a single thread may budget for the outer super-block — but only
310/// when an L3/LLC larger than L2 is detected (otherwise an outer tier just
311/// duplicates the inner one). `None` ⇒ no outer tier; the walk stays single-level
312/// (identical to before). The raw size is returned alongside the budget so the
313/// caller can check whether the working set even spills the cache before blocking.
314fn l3_block_budget_bytes() -> Option<(usize, usize)> {
315    use crate::cache::LlcKind;
316    let (bytes, kind) = crate::cache::last_level_cache()?;
317    // Dedicated cluster L3: ~half. A shared System-Level Cache is contended by the
318    // GPU/NPU/display, so we can't assume residency of lines they keep evicting —
319    // budget it to ~a quarter.
320    let (num, den) = match kind {
321        LlcKind::Dedicated => (1, 2),
322        LlcKind::SystemLevel => (1, 4),
323    };
324    Some((bytes, tier_budget_bytes(bytes, num, den, 0)))
325}
326
327/// Cache-adaptive panel-block edge for a given byte budget: large enough to
328/// amortise streaming, small enough that the block's A+B sub-panels
329/// (`~blk·(mr+nr)·k·elem_bytes`) stay cache-resident at the given `k`. Capped at
330/// `cap`; the floor of 1 degrades exactly to the naive loop, so an unknown/small
331/// cache can never over-block (regression-safe).
332#[inline]
333fn block_edge_for(
334    budget: usize,
335    mr: usize,
336    nr: usize,
337    k: usize,
338    elem_bytes: usize,
339    cap: usize,
340) -> usize {
341    if k == 0 {
342        return cap;
343    }
344    let per_blk = ((mr + nr) * k * elem_bytes.max(1)).max(1);
345    (budget / per_blk).clamp(1, cap)
346}
347
348/// Inner (L2) panel-block edge. Budget is **cache-size derived** (not a
349/// hard-coded constant), so it is correct across hardware.
350#[inline]
351fn st_block_edge(mr: usize, nr: usize, k: usize, elem_bytes: usize) -> usize {
352    block_edge_for(l2_block_budget_bytes(), mr, nr, k, elem_bytes, ST_BLK_MAX)
353}
354
355/// Whether an L3 outer super-block can capture reuse the inner (L2) tier cannot.
356/// It only can when the packed working set (`A + B ≈ (m·mr + n·nr)·k·elem`)
357/// actually spills the last-level cache: if both operands already fit, they stay
358/// resident across the sweep regardless of traversal order, so the reorder buys
359/// no reuse and only disturbs the hardware prefetchers — a measured net loss on
360/// small models that never leave L3 (voicecom_float on jetson-orin-nx, +15.6%).
361/// This is exactly the precondition the outer tier was introduced for ("a grid
362/// that exceeds L2 still re-fetches A/B from DRAM"); without the check the tier
363/// also engages on grids that never leave the LLC.
364fn outer_tier_pays(
365    m_panels: usize,
366    n_panels: usize,
367    mr: usize,
368    nr: usize,
369    k: usize,
370    elem_bytes: usize,
371    llc_bytes: usize,
372) -> bool {
373    let working_set = m_panels
374        .saturating_mul(mr)
375        .saturating_add(n_panels.saturating_mul(nr))
376        .saturating_mul(k)
377        .saturating_mul(elem_bytes);
378    llc_bytes > 0 && working_set > llc_bytes
379}
380
381/// Outer (L3) super-block edge, or `usize::MAX` (one block over the whole grid,
382/// i.e. no outer tier) when no usable L3 is detected or the working set already
383/// fits it (see [`outer_tier_pays`]). Never smaller than the inner edge `inner`.
384#[inline]
385fn st_outer_block_edge(
386    mr: usize,
387    nr: usize,
388    k: usize,
389    elem_bytes: usize,
390    inner: usize,
391    m_panels: usize,
392    n_panels: usize,
393) -> usize {
394    let Some((llc, budget)) = l3_block_budget_bytes() else { return usize::MAX };
395    if !outer_tier_pays(m_panels, n_panels, mr, nr, k, elem_bytes, llc) {
396        return usize::MAX;
397    }
398    block_edge_for(budget, mr, nr, k, elem_bytes, ST_BLK_L3_MAX).max(inner)
399}
400
401/// Visit every `(ia, ib)` tile of the `m_panels × n_panels` grid exactly once,
402/// blocked two levels deep: an outer `blk_outer` super-block (L3-resident) holds
403/// inner `blk` blocks (L2-resident). `col_outer` selects the within-block inner
404/// order (B-reuse vs A-reuse). When `blk_outer >= max(m,n)` the outer loop runs
405/// once and this is exactly the single-level inner walk. Pure tile reordering ⇒
406/// no result changes; extracted so the nesting can be unit-tested independently
407/// of the kernel.
408#[inline]
409fn for_each_blocked_tile(
410    m_panels: usize,
411    n_panels: usize,
412    blk: usize,
413    blk_outer: usize,
414    col_outer: bool,
415    mut f: impl FnMut(usize, usize) -> TractResult<()>,
416) -> TractResult<()> {
417    let blk = blk.max(1);
418    let blk_outer = blk_outer.max(blk);
419    let mut jb3 = 0;
420    while jb3 < n_panels {
421        let jb3_end = jb3.saturating_add(blk_outer).min(n_panels);
422        let mut ja3 = 0;
423        while ja3 < m_panels {
424            let ja3_end = ja3.saturating_add(blk_outer).min(m_panels);
425            let mut jb = jb3;
426            while jb < jb3_end {
427                let jb_end = (jb + blk).min(jb3_end);
428                let mut ja = ja3;
429                while ja < ja3_end {
430                    let ja_end = (ja + blk).min(ja3_end);
431                    if col_outer {
432                        for ib in jb..jb_end {
433                            for ia in ja..ja_end {
434                                f(ia, ib)?;
435                            }
436                        }
437                    } else {
438                        for ia in ja..ja_end {
439                            for ib in jb..jb_end {
440                                f(ia, ib)?;
441                            }
442                        }
443                    }
444                    ja = ja_end;
445                }
446                jb = jb_end;
447            }
448            ja3 = ja3_end;
449        }
450        jb3 = jb3_end;
451    }
452    Ok(())
453}
454
455/// Single-thread tile walk over the `m_panels × n_panels` grid, blocked into
456/// cache-sized panel blocks for locality (the naive nested loop re-streams the
457/// whole inner operand per outer panel at large k; the multithread path already
458/// blocks this way via `chunk_grid`). Two tiers: an inner L2-resident block and,
459/// where an L3 is detected, an outer L3-resident super-block. Reordering
460/// independent tiles changes no result — bit-exact with the naive loop.
461#[inline]
462unsafe fn run_single_thread_blocked<K: MatMatMulKer>(
463    ker: &K,
464    m_panels: usize,
465    n_panels: usize,
466    k: usize,
467    col_outer: bool,
468    scratch: &mut ScratchSpaceImpl<K::Acc>,
469    non_linear: &[FusedSpec],
470) -> TractResult<()> {
471    unsafe {
472        let elem = K::Acc::datum_type().size_of();
473        let blk = st_block_edge(ker.mr(), ker.nr(), k, elem);
474        let blk_outer = st_outer_block_edge(ker.mr(), ker.nr(), k, elem, blk, m_panels, n_panels);
475        scratch.run_in_tls_scope(|scratch, tls| {
476            for_each_blocked_tile(m_panels, n_panels, blk, blk_outer, col_outer, |ia, ib| {
477                scratch.run_one_tile(ker, non_linear, tls, ia, ib)
478            })
479        })
480    }
481}
482
483unsafe fn run_with_scratch_space_col_outer<K: MatMatMulKer>(
484    ker: &K,
485    m: usize,
486    n: usize,
487    k: usize,
488    scratch: &mut ScratchSpaceImpl<K::Acc>,
489    non_linear: &[FusedSpec],
490) -> TractResult<()> {
491    unsafe {
492        match crate::multithread::current_tract_executor() {
493            Executor::SingleThread => run_single_thread_blocked(
494                ker,
495                m.divceil(ker.mr()),
496                n.divceil(ker.nr()),
497                k,
498                true,
499                scratch,
500                non_linear,
501            ),
502            #[cfg(feature = "multithread-mm")]
503            Executor::MultiThread(pool) => chunked_dispatch_rayon(
504                Some(&pool),
505                m.divceil(ker.mr()),
506                n.divceil(ker.nr()),
507                |ia_start, ia_end, ib_start, ib_end| {
508                    scratch.run_in_tls_scope(|scratch, tls| {
509                        for ib in ib_start..ib_end {
510                            for ia in ia_start..ia_end {
511                                scratch.run_one_tile(ker, non_linear, tls, ia, ib)?;
512                            }
513                        }
514                        TractResult::Ok(())
515                    })
516                },
517            ),
518            #[cfg(feature = "multithread-mm")]
519            Executor::RayonGlobal => chunked_dispatch_rayon(
520                None,
521                m.divceil(ker.mr()),
522                n.divceil(ker.nr()),
523                |ia_start, ia_end, ib_start, ib_end| {
524                    scratch.run_in_tls_scope(|scratch, tls| {
525                        for ib in ib_start..ib_end {
526                            for ia in ia_start..ia_end {
527                                scratch.run_one_tile(ker, non_linear, tls, ia, ib)?;
528                            }
529                        }
530                        TractResult::Ok(())
531                    })
532                },
533            ),
534        }
535    }
536}
537
538unsafe fn run_with_scratch_space_row_outer<K: MatMatMulKer>(
539    ker: &K,
540    m: usize,
541    n: usize,
542    k: usize,
543    scratch: &mut ScratchSpaceImpl<K::Acc>,
544    non_linear: &[FusedSpec],
545) -> TractResult<()> {
546    unsafe {
547        match crate::multithread::current_tract_executor() {
548            Executor::SingleThread => run_single_thread_blocked(
549                ker,
550                m.divceil(ker.mr()),
551                n.divceil(ker.nr()),
552                k,
553                false,
554                scratch,
555                non_linear,
556            ),
557            #[cfg(feature = "multithread-mm")]
558            Executor::MultiThread(pool) => chunked_dispatch_rayon(
559                Some(&pool),
560                m.divceil(ker.mr()),
561                n.divceil(ker.nr()),
562                |ia_start, ia_end, ib_start, ib_end| {
563                    scratch.run_in_tls_scope(|scratch, tls| {
564                        for ia in ia_start..ia_end {
565                            for ib in ib_start..ib_end {
566                                scratch.run_one_tile(ker, non_linear, tls, ia, ib)?;
567                            }
568                        }
569                        TractResult::Ok(())
570                    })
571                },
572            ),
573            #[cfg(feature = "multithread-mm")]
574            Executor::RayonGlobal => chunked_dispatch_rayon(
575                None,
576                m.divceil(ker.mr()),
577                n.divceil(ker.nr()),
578                |ia_start, ia_end, ib_start, ib_end| {
579                    scratch.run_in_tls_scope(|scratch, tls| {
580                        for ia in ia_start..ia_end {
581                            for ib in ib_start..ib_end {
582                                scratch.run_one_tile(ker, non_linear, tls, ia, ib)?;
583                            }
584                        }
585                        TractResult::Ok(())
586                    })
587                },
588            ),
589        }
590    }
591}
592
593/// Chunk grid for the 2D dispatch.
594///
595/// Mirrors ggml's `mul_mat` heuristic (`ggml/src/ggml-cpu/ggml-cpu.c:1378-1398`):
596///  * 16-tile panel chunks by default;
597///  * 64-tile chunks when one dimension is 1 (vec / vec-mat);
598///  * fallback to "block-per-thread along the longer axis" when the natural
599///    grid would have fewer than `4·nth` chunks.
600///
601/// Returns `(nchunks_m, nchunks_n, dr_m, dr_n)`.
602#[cfg(feature = "multithread-mm")]
603fn chunk_grid(n_panels_m: usize, n_panels_n: usize, nth: usize) -> (usize, usize, usize, usize) {
604    let chunk_size = if n_panels_m == 1 || n_panels_n == 1 { 64 } else { 16 };
605    let mut nchunks_m = n_panels_m.div_ceil(chunk_size);
606    let mut nchunks_n = n_panels_n.div_ceil(chunk_size);
607    if nchunks_m * nchunks_n < 4 * nth {
608        if n_panels_m > n_panels_n {
609            nchunks_m = nth;
610            nchunks_n = 1;
611        } else {
612            nchunks_m = 1;
613            nchunks_n = nth;
614        }
615    }
616    let dr_m = n_panels_m.div_ceil(nchunks_m).max(1);
617    let dr_n = n_panels_n.div_ceil(nchunks_n).max(1);
618    (nchunks_m, nchunks_n, dr_m, dr_n)
619}
620
621/// 2D chunked dispatcher across the (m_panels × n_panels) grid for the
622/// rayon path. Replaces a 1D `into_par_iter` over a single panel axis.
623/// Better-utilises threads on small/skewed shapes where one dimension has
624/// fewer panels than there are workers.
625///
626/// The closure receives **chunk bounds** (`ia_start, ia_end, ib_start, ib_end`),
627/// not per-tile indices. This lets the caller amortise per-worker setup
628/// (e.g. `ScratchSpaceImpl::run_in_tls_scope`) across all tiles in the
629/// chunk, mirroring #2206 for the multi-threaded path. The closure is
630/// invoked exactly once per rayon work item (and once total when the
631/// small-graph fallback path is taken).
632///
633/// `pool`:
634///   * `Some(p)` with `p.current_num_threads() > 1` → scoped via `p.install`
635///     (native, custom pool path).
636///   * `Some(p)` with single-thread pool, or `None` → dispatched via
637///     `into_par_iter` directly, which uses rayon's GLOBAL pool. This is
638///     the only working path on `wasm32-unknown-unknown` via
639///     `wasm_bindgen_rayon::init_thread_pool`.
640#[cfg(feature = "multithread-mm")]
641unsafe fn chunked_dispatch_rayon<F>(
642    pool: Option<&rayon::ThreadPool>,
643    n_panels_m: usize,
644    n_panels_n: usize,
645    run_chunk: F,
646) -> TractResult<()>
647where
648    F: Fn(usize, usize, usize, usize) -> TractResult<()> + Sync,
649{
650    use rayon::prelude::*;
651    if n_panels_m == 0 || n_panels_n == 0 {
652        return Ok(());
653    }
654    if n_panels_m * n_panels_n < crate::multithread::current_threading_panel_threshold() {
655        // Below the threading threshold: run the whole grid as a single chunk
656        // on the calling thread. Closure handles its own TLS scope.
657        return run_chunk(0, n_panels_m, 0, n_panels_n);
658    }
659    let use_global = pool.is_none_or(|p| p.current_num_threads() <= 1);
660    let body = || {
661        let nth = rayon::current_num_threads();
662        let (nchunks_m, nchunks_n, dr_m, dr_n) = chunk_grid(n_panels_m, n_panels_n, nth);
663        let total = nchunks_m * nchunks_n;
664        (0..total).into_par_iter().try_for_each(|idx| {
665            let im = idx % nchunks_m;
666            let in_ = idx / nchunks_m;
667            let ia_start = im * dr_m;
668            let ia_end = (ia_start + dr_m).min(n_panels_m);
669            let ib_start = in_ * dr_n;
670            let ib_end = (ib_start + dr_n).min(n_panels_n);
671            run_chunk(ia_start, ia_end, ib_start, ib_end)
672        })
673    };
674    if use_global { body() } else { pool.unwrap().install(body) }
675}
676
677#[cfg(test)]
678mod blocked_walk_tests {
679    use super::*;
680    use std::collections::HashSet;
681
682    fn collect(
683        m: usize,
684        n: usize,
685        blk: usize,
686        blk_outer: usize,
687        col_outer: bool,
688    ) -> Vec<(usize, usize)> {
689        let mut v = Vec::new();
690        for_each_blocked_tile(m, n, blk, blk_outer, col_outer, |ia, ib| {
691            v.push((ia, ib));
692            Ok(())
693        })
694        .unwrap();
695        v
696    }
697
698    /// Every grid tile is visited exactly once, for both inner orders and a
699    /// range of (blk, blk_outer) — single-tier (outer = MAX), two-tier, and
700    /// degenerate edges. Coverage being a permutation is what makes the walk
701    /// bit-exact with the naive loop.
702    #[test]
703    fn covers_every_tile_once() {
704        for &(m, n) in &[(1, 1), (3, 5), (16, 16), (40, 7), (7, 40), (80, 80)] {
705            for &blk in &[1, 3, 16] {
706                for &blk_outer in &[blk, blk + 1, 64, usize::MAX] {
707                    for &col_outer in &[false, true] {
708                        let tiles = collect(m, n, blk, blk_outer, col_outer);
709                        assert_eq!(tiles.len(), m * n, "m={m} n={n} blk={blk} outer={blk_outer}");
710                        let set: HashSet<_> = tiles.iter().copied().collect();
711                        assert_eq!(
712                            set.len(),
713                            m * n,
714                            "duplicate tiles m={m} n={n} blk={blk} outer={blk_outer}"
715                        );
716                        for ia in 0..m {
717                            for ib in 0..n {
718                                assert!(set.contains(&(ia, ib)), "missing ({ia},{ib})");
719                            }
720                        }
721                    }
722                }
723            }
724        }
725    }
726
727    /// With no outer tier (blk_outer = MAX) the two-tier walk must emit the exact
728    /// same order as the original single-level blocked loop — guarantees the L3
729    /// path is a pure no-op on hardware without a detectable L3.
730    #[test]
731    fn outer_max_matches_single_level() {
732        for &(m, n) in &[(40, 7), (80, 80), (13, 29)] {
733            for &blk in &[1, 4, 16] {
734                for &col_outer in &[false, true] {
735                    let two_tier = collect(m, n, blk, usize::MAX, col_outer);
736                    let mut single = Vec::new();
737                    let mut jb = 0;
738                    while jb < n {
739                        let jb_end = (jb + blk).min(n);
740                        let mut ja = 0;
741                        while ja < m {
742                            let ja_end = (ja + blk).min(m);
743                            if col_outer {
744                                for ib in jb..jb_end {
745                                    for ia in ja..ja_end {
746                                        single.push((ia, ib));
747                                    }
748                                }
749                            } else {
750                                for ia in ja..ja_end {
751                                    for ib in jb..jb_end {
752                                        single.push((ia, ib));
753                                    }
754                                }
755                            }
756                            ja = ja_end;
757                        }
758                        jb = jb_end;
759                    }
760                    assert_eq!(two_tier, single, "m={m} n={n} blk={blk} col_outer={col_outer}");
761                }
762            }
763        }
764    }
765
766    /// The outer tier engages only when the packed working set spills the LLC.
767    /// A grid that already fits stays single-level (the reorder buys no reuse and
768    /// only hurts prefetch — the voicecom_float/Orin regression).
769    #[test]
770    fn outer_tier_gated_on_working_set_spilling_llc() {
771        let llc = 2 * 1024 * 1024; // 2 MiB, f32 (elem = 4)
772        // Small grid: (64·8 + 8·8)·64·4 ≈ 144 KiB ⇒ fits ⇒ no outer tier.
773        assert!(!outer_tier_pays(64, 8, 8, 8, 64, 4, llc));
774        // Large grid: (256·8 + 256·8)·256·4 ≈ 4 MiB ⇒ spills ⇒ engage.
775        assert!(outer_tier_pays(256, 256, 8, 8, 256, 4, llc));
776        // A boundary working set equal to the LLC does not spill it.
777        assert!(!outer_tier_pays(1, 0, llc, 0, 1, 1, llc));
778        // Unknown LLC (0) never engages, whatever the grid.
779        assert!(!outer_tier_pays(4096, 4096, 8, 8, 4096, 4, 0));
780        // k = 0 (empty reduction) has no working set ⇒ never engages.
781        assert!(!outer_tier_pays(4096, 4096, 8, 8, 0, 4, llc));
782    }
783}