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 std::ops::Range;
23use tract_data::internal::*;
24
25pub use cost_model::*;
26pub use fuse::*;
27pub use input_store::*;
28pub use kernel::*;
29pub use panel_extract::*;
30pub use scratch::*;
31pub use storage::*;
32
33pub fn no_prefetch(_ptr: *const u8, _len: usize) {}
34
35#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
36pub enum ImplementationQuality {
37 Dreadful,
39 Generic,
41 RustOptimized,
43 TargetOptimized,
45 ManuallyOptimized,
47}
48
49impl ImplementationQuality {
50 pub fn best_to_worst() -> &'static [ImplementationQuality] {
51 use ImplementationQuality::*;
52 &[ManuallyOptimized, TargetOptimized, RustOptimized, Generic, Dreadful]
53 }
54
55 pub fn cost(&self) -> usize {
56 ImplementationQuality::best_to_worst().iter().position(|x| x == self).unwrap()
57 }
58}
59
60impl PartialOrd for ImplementationQuality {
61 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
62 Some(usize::from(*self).cmp(&usize::from(*other)))
63 }
64}
65
66impl From<ImplementationQuality> for usize {
67 fn from(value: ImplementationQuality) -> Self {
68 value.cost()
69 }
70}
71
72pub trait MatMatMul: Debug + dyn_clone::DynClone + Send + Sync + std::any::Any {
73 fn name(&self) -> &str;
74 fn mr(&self) -> usize;
75 fn nr(&self) -> usize;
76
77 fn quality(&self) -> ImplementationQuality;
78 fn dynamic_boost(&self) -> isize;
79
80 fn is_supported_here(&self) -> bool;
83
84 #[allow(clippy::type_complexity)]
85 fn packings(&self) -> &[(Box<dyn MMMInputFormat>, Box<dyn MMMInputFormat>)];
86
87 fn internal_type(&self) -> DatumType;
88
89 unsafe fn c_view(&self, m_axis: Option<usize>, n_axis: Option<usize>) -> OutputStoreSpec;
90 unsafe fn c_from_data_and_strides(
91 &self,
92 item_size: usize,
93 row_stride: isize,
94 col_stride: isize,
95 ) -> OutputStoreSpec;
96
97 fn can_fuse(&self, spec: &FusedSpec) -> bool;
98
99 fn stores(&self) -> Cow<'_, [DatumType]>;
100
101 unsafe fn run(&self, m: usize, n: usize, non_linear: &[FusedSpec]) -> TractResult<()> {
102 unsafe {
103 let mut scratch = self.allocate_scratch_space();
104 self.run_with_scratch_space(m, n, &mut *scratch, non_linear)
105 }
106 }
107
108 unsafe fn allocate_scratch_space(&self) -> Box<dyn ScratchSpace>;
109 unsafe fn can_use_scratch_space(&self, scratch: &dyn ScratchSpace) -> bool;
110 unsafe fn run_with_scratch_space(
111 &self,
112 m: usize,
113 n: usize,
114 scratch: &mut dyn ScratchSpace,
115 non_linear: &[FusedSpec],
116 ) -> TractResult<()>;
117}
118
119dyn_clone::clone_trait_object!(MatMatMul);
120
121impl PartialEq for Box<dyn MatMatMul> {
122 fn eq(&self, other: &Box<dyn MatMatMul>) -> bool {
123 self.name() == other.name()
124 }
125}
126impl Eq for Box<dyn MatMatMul> {}
127
128impl std::hash::Hash for Box<dyn MatMatMul> {
129 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
130 self.name().hash(state)
131 }
132}
133
134impl<K: MatMatMulKer> MatMatMul for K {
135 fn name(&self) -> &str {
136 self.name()
137 }
138 fn mr(&self) -> usize {
139 self.mr()
140 }
141 fn nr(&self) -> usize {
142 self.nr()
143 }
144
145 fn quality(&self) -> ImplementationQuality {
146 MatMatMulKer::quality(self)
147 }
148
149 fn dynamic_boost(&self) -> isize {
150 MatMatMulKer::dynamic_boost(self)
151 }
152
153 fn is_supported_here(&self) -> bool {
154 MatMatMulKer::is_supported_here(self)
155 }
156
157 fn packings(&self) -> &[(Box<dyn MMMInputFormat>, Box<dyn MMMInputFormat>)] {
158 self.packings()
159 }
160
161 fn internal_type(&self) -> DatumType {
162 K::Acc::datum_type()
163 }
164
165 fn can_fuse(&self, spec: &FusedSpec) -> bool {
166 self.can_fuse(spec)
167 }
168
169 unsafe fn c_view(&self, m_axis: Option<usize>, n_axis: Option<usize>) -> OutputStoreSpec {
170 OutputStoreSpec::View { m_axis, n_axis, mr: self.mr(), nr: self.nr() }
171 }
172
173 unsafe fn c_from_data_and_strides(
174 &self,
175 item_size: usize,
176 row_stride: isize,
177 col_stride: isize,
178 ) -> OutputStoreSpec {
179 OutputStoreSpec::Strides {
180 row_byte_stride: row_stride * item_size as isize,
181 col_byte_stride: col_stride * item_size as isize,
182 mr: self.mr(),
183 nr: self.nr(),
184 }
185 }
186
187 fn stores(&self) -> Cow<'_, [DatumType]> {
188 self.stores()
189 }
190
191 unsafe fn allocate_scratch_space(&self) -> Box<dyn ScratchSpace> {
192 Box::<ScratchSpaceImpl<K::Acc>>::default()
193 }
194
195 unsafe fn can_use_scratch_space(&self, scratch: &dyn ScratchSpace) -> bool {
196 scratch.downcast_ref::<ScratchSpaceImpl<K::Acc>>().is_some()
197 }
198
199 unsafe fn run_with_scratch_space(
200 &self,
201 m: usize,
202 n: usize,
203 scratch: &mut dyn ScratchSpace,
204 non_linear: &[FusedSpec],
205 ) -> TractResult<()> {
206 #[cfg(debug_assertions)]
210 {
211 use crate::pack::PackedFormat;
212 fn compatible(expected: &dyn MMMInputFormat, got: &dyn MMMInputFormat) -> bool {
218 if expected.dyn_eq(got) {
219 return true;
220 }
221 match (expected.downcast_ref::<PackedFormat>(), got.downcast_ref::<PackedFormat>())
222 {
223 (Some(e), Some(g)) => e.dt == g.dt && e.r == g.r,
224 _ => true,
225 }
226 }
227 for spec in non_linear {
228 if let FusedSpec::AddMatMul { a, b, packing } = spec {
229 let (pa, pb) = &self.packings()[*packing];
230 debug_assert!(
231 compatible(&**pa, a.format()),
232 "A packed as {:?} but {} packing {packing} expects {pa:?}",
233 a.format(),
234 self.name(),
235 );
236 debug_assert!(
237 compatible(&**pb, b.format()),
238 "B packed as {:?} but {} packing {packing} expects {pb:?}",
239 b.format(),
240 self.name(),
241 );
242 }
243 }
244 }
245 unsafe {
246 let scratch = scratch
247 .downcast_mut::<ScratchSpaceImpl<K::Acc>>()
248 .context("Wrong scratch space type")?;
249 scratch.prepare(self, m, n, non_linear)?;
250 if n == 1 && self.nr() == 1 {
251 run_with_scratch_space_vec(self, m, scratch, non_linear)
252 } else {
253 let (mut prefer_col, mut prefer_row) = (0, 0);
254 for uop in non_linear.iter() {
255 if let Some(col) = uop.prefer_col_outer() {
256 prefer_col = col as usize;
257 prefer_row = (!col) as usize;
258 }
259 }
260 let k = non_linear
263 .iter()
264 .find_map(|f| match f {
265 FusedSpec::AddMatMul { a, .. } => Some(a.k()),
266 _ => None,
267 })
268 .unwrap_or(0);
269 run_with_scratch_space_2d(
270 self,
271 m,
272 n,
273 k,
274 prefer_col > prefer_row,
275 scratch,
276 non_linear,
277 )
278 }
279 }
280 }
281}
282
283unsafe fn run_with_scratch_space_vec<K: MatMatMulKer>(
284 ker: &K,
285 m: usize,
286 scratch: &mut ScratchSpaceImpl<K::Acc>,
287 non_linear: &[FusedSpec],
288) -> TractResult<()> {
289 unsafe {
290 match crate::multithread::current_tract_executor() {
291 Executor::SingleThread => scratch.run_in_tls_scope(|scratch, tls| {
292 for ia in 0..m.divceil(ker.mr()) {
293 scratch.run_one_tile(ker, non_linear, tls, ia, 0)?;
294 }
295 TractResult::Ok(())
296 }),
297 #[cfg(feature = "multithread-mm")]
298 Executor::MultiThread(pool) => chunked_dispatch_rayon(
299 Some(&pool),
300 m.divceil(ker.mr()),
301 1,
302 ker.mr(),
303 ker.nr(),
304 |ia_start, ia_end, _, _, _| {
305 scratch.run_in_tls_scope(|scratch, tls| {
306 for ia in ia_start..ia_end {
307 scratch.run_one_tile(ker, non_linear, tls, ia, 0)?;
308 }
309 TractResult::Ok(())
310 })
311 },
312 ),
313 #[cfg(feature = "multithread-mm")]
314 Executor::RayonGlobal => chunked_dispatch_rayon(
315 None,
316 m.divceil(ker.mr()),
317 1,
318 ker.mr(),
319 ker.nr(),
320 |ia_start, ia_end, _, _, _| {
321 scratch.run_in_tls_scope(|scratch, tls| {
322 for ia in ia_start..ia_end {
323 scratch.run_one_tile(ker, non_linear, tls, ia, 0)?;
324 }
325 TractResult::Ok(())
326 })
327 },
328 ),
329 }
330 }
331}
332
333const BLK_MAX: usize = 16;
335
336const BLK_L3_MAX: usize = 64;
339
340fn tier_budget_bytes(cache_bytes: usize, num: usize, den: usize, fallback: usize) -> usize {
346 if cache_bytes == 0 {
347 fallback
348 } else {
349 (cache_bytes * num / den).clamp(64 * 1024, 64 * 1024 * 1024)
350 }
351}
352
353fn l2_block_budget_bytes() -> usize {
355 tier_budget_bytes(crate::cache::cache_info().l2, 1, 3, 256 * 1024)
356}
357
358fn l3_block_budget_bytes() -> Option<(usize, usize)> {
366 use crate::cache::LlcKind;
367 let (bytes, kind) = crate::cache::last_level_cache()?;
368 let (num, den) = match kind {
372 LlcKind::Dedicated => (1, 2),
373 LlcKind::SystemLevel => (1, 4),
374 };
375 Some((bytes, tier_budget_bytes(bytes, num, den, 0)))
376}
377
378#[inline]
384fn block_edge_for(
385 budget: usize,
386 mr: usize,
387 nr: usize,
388 k: usize,
389 elem_bytes: usize,
390 cap: usize,
391) -> usize {
392 if k == 0 {
393 return cap;
394 }
395 let per_blk = ((mr + nr) * k * elem_bytes.max(1)).max(1);
396 (budget / per_blk).clamp(1, cap)
397}
398
399fn inner_tier_pays(panels: usize, r: usize, k: usize, elem_bytes: usize, l2_bytes: usize) -> bool {
407 let streamed = panels.saturating_mul(r).saturating_mul(k).saturating_mul(elem_bytes);
408 l2_bytes > 0 && streamed > l2_bytes
409}
410
411#[inline]
423#[allow(clippy::too_many_arguments)]
424fn inner_block_edge(
425 mr: usize,
426 nr: usize,
427 k: usize,
428 elem_bytes: usize,
429 m_panels: usize,
430 n_panels: usize,
431 col_outer: bool,
432 l2_share: usize,
433) -> usize {
434 let (panels, r) = if col_outer { (m_panels, mr) } else { (n_panels, nr) };
435 let share = l2_share.max(1);
436 if !inner_tier_pays(panels, r, k, elem_bytes, crate::cache::cache_info().l2 / share) {
437 return usize::MAX;
438 }
439 block_edge_for(l2_block_budget_bytes() / share, mr, nr, k, elem_bytes, BLK_MAX)
440}
441
442fn outer_tier_pays(
452 m_panels: usize,
453 n_panels: usize,
454 mr: usize,
455 nr: usize,
456 k: usize,
457 elem_bytes: usize,
458 llc_bytes: usize,
459) -> bool {
460 let working_set = m_panels
461 .saturating_mul(mr)
462 .saturating_add(n_panels.saturating_mul(nr))
463 .saturating_mul(k)
464 .saturating_mul(elem_bytes);
465 llc_bytes > 0 && working_set > llc_bytes
466}
467
468#[inline]
478#[allow(clippy::too_many_arguments)]
479fn outer_block_edge(
480 mr: usize,
481 nr: usize,
482 k: usize,
483 elem_bytes: usize,
484 inner: usize,
485 m_panels: usize,
486 n_panels: usize,
487 llc_share: usize,
488) -> usize {
489 let Some((llc, budget)) = l3_block_budget_bytes() else { return usize::MAX };
490 let share = llc_share.max(1);
491 if !outer_tier_pays(m_panels, n_panels, mr, nr, k, elem_bytes, llc / share) {
492 return usize::MAX;
493 }
494 block_edge_for(budget / share, mr, nr, k, elem_bytes, BLK_L3_MAX).max(inner)
495}
496
497#[inline]
505fn for_each_blocked_tile(
506 m: Range<usize>,
507 n: Range<usize>,
508 blk: usize,
509 blk_outer: usize,
510 col_outer: bool,
511 mut f: impl FnMut(usize, usize) -> TractResult<()>,
512) -> TractResult<()> {
513 let blk = blk.max(1);
514 let blk_outer = blk_outer.max(blk);
515 let mut jb3 = n.start;
516 while jb3 < n.end {
517 let jb3_end = jb3.saturating_add(blk_outer).min(n.end);
518 let mut ja3 = m.start;
519 while ja3 < m.end {
520 let ja3_end = ja3.saturating_add(blk_outer).min(m.end);
521 let mut jb = jb3;
522 while jb < jb3_end {
523 let jb_end = jb.saturating_add(blk).min(jb3_end);
524 let mut ja = ja3;
525 while ja < ja3_end {
526 let ja_end = ja.saturating_add(blk).min(ja3_end);
527 if col_outer {
528 for ib in jb..jb_end {
529 for ia in ja..ja_end {
530 f(ia, ib)?;
531 }
532 }
533 } else {
534 for ia in ja..ja_end {
535 for ib in jb..jb_end {
536 f(ia, ib)?;
537 }
538 }
539 }
540 ja = ja_end;
541 }
542 jb = jb_end;
543 }
544 ja3 = ja3_end;
545 }
546 jb3 = jb3_end;
547 }
548 Ok(())
549}
550
551#[inline]
561#[allow(clippy::too_many_arguments)]
562unsafe fn run_blocked<K: MatMatMulKer>(
563 ker: &K,
564 m: Range<usize>,
565 n: Range<usize>,
566 k: usize,
567 col_outer: bool,
568 llc_share: usize,
569 scratch: &ScratchSpaceImpl<K::Acc>,
570 non_linear: &[FusedSpec],
571) -> TractResult<()> {
572 unsafe {
573 let elem = K::Acc::datum_type().size_of();
574 let (mr, nr) = (ker.mr(), ker.nr());
575 let (m_panels, n_panels) = (m.len(), n.len());
576 let l2_share = llc_share.min(crate::cache::cache_info().l2_sharers_or_one());
577 let blk = inner_block_edge(mr, nr, k, elem, m_panels, n_panels, col_outer, l2_share);
578 let blk_outer = outer_block_edge(mr, nr, k, elem, blk, m_panels, n_panels, llc_share);
579 scratch.run_in_tls_scope(|scratch, tls| {
580 for_each_blocked_tile(m, n, blk, blk_outer, col_outer, |ia, ib| {
581 scratch.run_one_tile(ker, non_linear, tls, ia, ib)
582 })
583 })
584 }
585}
586
587unsafe fn run_with_scratch_space_2d<K: MatMatMulKer>(
593 ker: &K,
594 m: usize,
595 n: usize,
596 k: usize,
597 col_outer: bool,
598 scratch: &ScratchSpaceImpl<K::Acc>,
599 non_linear: &[FusedSpec],
600) -> TractResult<()> {
601 unsafe {
602 let (m_panels, n_panels) = (m.divceil(ker.mr()), n.divceil(ker.nr()));
603 #[cfg(feature = "multithread-mm")]
604 let chunk = |ia_start, ia_end, ib_start, ib_end, concurrency| {
605 run_blocked(
606 ker,
607 ia_start..ia_end,
608 ib_start..ib_end,
609 k,
610 col_outer,
611 concurrency,
612 scratch,
613 non_linear,
614 )
615 };
616 match crate::multithread::current_tract_executor() {
617 Executor::SingleThread => {
618 run_blocked(ker, 0..m_panels, 0..n_panels, k, col_outer, 1, scratch, non_linear)
619 }
620 #[cfg(feature = "multithread-mm")]
621 Executor::MultiThread(pool) => {
622 chunked_dispatch_rayon(Some(&pool), m_panels, n_panels, ker.mr(), ker.nr(), chunk)
623 }
624 #[cfg(feature = "multithread-mm")]
625 Executor::RayonGlobal => {
626 chunked_dispatch_rayon(None, m_panels, n_panels, ker.mr(), ker.nr(), chunk)
627 }
628 }
629 }
630}
631
632#[cfg(feature = "multithread-mm")]
639const CHUNKS_PER_THREAD: usize = 4;
640
641#[cfg(feature = "multithread-mm")]
657fn chunk_grid(
658 n_panels_m: usize,
659 n_panels_n: usize,
660 mr: usize,
661 nr: usize,
662 nth: usize,
663) -> (usize, usize, usize, usize) {
664 let chunks = (CHUNKS_PER_THREAD * nth).max(1);
665 let (m, n) = (n_panels_m * mr, (n_panels_n * nr).max(1));
666 let nchunks_m = (chunks.saturating_mul(m) / n).isqrt().clamp(1, n_panels_m);
667 let nchunks_n = (chunks / nchunks_m).clamp(1, n_panels_n);
668 let nchunks_m = (chunks / nchunks_n).clamp(1, n_panels_m);
669 let dr_m = n_panels_m.div_ceil(nchunks_m);
670 let dr_n = n_panels_n.div_ceil(nchunks_n);
671 (n_panels_m.div_ceil(dr_m), n_panels_n.div_ceil(dr_n), dr_m, dr_n)
674}
675
676#[cfg(feature = "multithread-mm")]
697unsafe fn chunked_dispatch_rayon<F>(
698 pool: Option<&rayon::ThreadPool>,
699 n_panels_m: usize,
700 n_panels_n: usize,
701 mr: usize,
702 nr: usize,
703 run_chunk: F,
704) -> TractResult<()>
705where
706 F: Fn(usize, usize, usize, usize, usize) -> TractResult<()> + Sync,
707{
708 use rayon::prelude::*;
709 if n_panels_m == 0 || n_panels_n == 0 {
710 return Ok(());
711 }
712 if n_panels_m * n_panels_n < crate::multithread::current_threading_panel_threshold() {
713 return run_chunk(0, n_panels_m, 0, n_panels_n, 1);
716 }
717 let use_global = pool.is_none_or(|p| p.current_num_threads() <= 1);
718 let body = || {
719 let nth = rayon::current_num_threads();
720 let (nchunks_m, nchunks_n, dr_m, dr_n) = chunk_grid(n_panels_m, n_panels_n, mr, nr, nth);
721 let total = nchunks_m * nchunks_n;
722 let concurrency = nth.min(total);
723 (0..total).into_par_iter().try_for_each(|idx| {
724 let im = idx % nchunks_m;
725 let in_ = idx / nchunks_m;
726 let ia_start = im * dr_m;
727 let ia_end = (ia_start + dr_m).min(n_panels_m);
728 let ib_start = in_ * dr_n;
729 let ib_end = (ib_start + dr_n).min(n_panels_n);
730 run_chunk(ia_start, ia_end, ib_start, ib_end, concurrency)
731 })
732 };
733 if use_global { body() } else { pool.unwrap().install(body) }
734}
735
736#[cfg(test)]
737mod blocked_walk_tests {
738 use super::*;
739 use std::collections::HashSet;
740
741 fn collect(
742 m: Range<usize>,
743 n: Range<usize>,
744 blk: usize,
745 blk_outer: usize,
746 col_outer: bool,
747 ) -> Vec<(usize, usize)> {
748 let mut v = Vec::new();
749 for_each_blocked_tile(m, n, blk, blk_outer, col_outer, |ia, ib| {
750 v.push((ia, ib));
751 Ok(())
752 })
753 .unwrap();
754 v
755 }
756
757 #[test]
762 fn covers_every_tile_once() {
763 for &(m, n) in &[(1, 1), (3, 5), (16, 16), (40, 7), (7, 40), (80, 80)] {
764 for &(m0, n0) in &[(0, 0), (3, 11)] {
765 for &blk in &[1, 3, 16, usize::MAX] {
768 for &blk_outer in &[blk, blk.saturating_add(1), 64, usize::MAX] {
769 for &col_outer in &[false, true] {
770 let tiles = collect(m0..m0 + m, n0..n0 + n, blk, blk_outer, col_outer);
771 assert_eq!(
772 tiles.len(),
773 m * n,
774 "m={m} n={n} blk={blk} outer={blk_outer}"
775 );
776 let set: HashSet<_> = tiles.iter().copied().collect();
777 assert_eq!(
778 set.len(),
779 m * n,
780 "duplicate tiles m={m} n={n} blk={blk} outer={blk_outer}"
781 );
782 for ia in m0..m0 + m {
783 for ib in n0..n0 + n {
784 assert!(set.contains(&(ia, ib)), "missing ({ia},{ib})");
785 }
786 }
787 }
788 }
789 }
790 }
791 }
792 }
793
794 #[test]
798 fn outer_max_matches_single_level() {
799 for &(m, n) in &[(40, 7), (80, 80), (13, 29)] {
800 for &blk in &[1, 4, 16] {
801 for &col_outer in &[false, true] {
802 let two_tier = collect(0..m, 0..n, blk, usize::MAX, col_outer);
803 let mut single = Vec::new();
804 let mut jb = 0;
805 while jb < n {
806 let jb_end = (jb + blk).min(n);
807 let mut ja = 0;
808 while ja < m {
809 let ja_end = (ja + blk).min(m);
810 if col_outer {
811 for ib in jb..jb_end {
812 for ia in ja..ja_end {
813 single.push((ia, ib));
814 }
815 }
816 } else {
817 for ia in ja..ja_end {
818 for ib in jb..jb_end {
819 single.push((ia, ib));
820 }
821 }
822 }
823 ja = ja_end;
824 }
825 jb = jb_end;
826 }
827 assert_eq!(two_tier, single, "m={m} n={n} blk={blk} col_outer={col_outer}");
828 }
829 }
830 }
831 }
832
833 #[test]
837 fn outer_tier_gated_on_working_set_spilling_llc() {
838 let llc = 2 * 1024 * 1024; assert!(!outer_tier_pays(64, 8, 8, 8, 64, 4, llc));
841 assert!(outer_tier_pays(256, 256, 8, 8, 256, 4, llc));
843 assert!(!outer_tier_pays(1, 0, llc, 0, 1, 1, llc));
845 assert!(!outer_tier_pays(4096, 4096, 8, 8, 4096, 4, 0));
847 assert!(!outer_tier_pays(4096, 4096, 8, 8, 0, 4, llc));
849 }
850
851 #[test]
855 fn inner_tier_gated_on_streamed_operand_spilling_l2() {
856 let l2 = 1024 * 1024; assert!(!inner_tier_pays(12, 16, 720, 4, l2));
860 assert!(inner_tier_pays(421, 12, 720, 4, l2));
862 assert!(inner_tier_pays(256, 16, 512, 4, l2));
864 assert!(!inner_tier_pays(4096, 16, 4096, 4, 0));
866 assert!(!inner_tier_pays(4096, 16, 0, 4, l2));
868 }
869
870 #[cfg(feature = "multithread-mm")]
874 const GRIDS: &[(usize, usize)] = &[
875 (1, 1),
876 (1, 5),
877 (5, 1),
878 (2, 3),
879 (3, 3),
880 (16, 96),
881 (96, 16),
882 (17, 17),
883 (64, 64),
884 (32, 384),
885 (128, 128),
886 (1, 4096),
887 (4096, 1),
888 (9, 1000),
889 ];
890
891 #[cfg(feature = "multithread-mm")]
892 const RATIOS: &[(usize, usize)] = &[(8, 8), (16, 4), (32, 32), (64, 1)];
893
894 #[cfg(feature = "multithread-mm")]
899 #[test]
900 fn chunk_grid_tiles_the_panel_grid() {
901 for &(m, n) in GRIDS {
902 for &(mr, nr) in RATIOS {
903 for nth in [1usize, 2, 3, 4, 6, 8, 16, 64] {
904 let (cm, cn, dr_m, dr_n) = chunk_grid(m, n, mr, nr, nth);
905 let ctx = format!("{m}x{n} panels, {mr}x{nr} kernel, {nth} threads");
906 let mut seen = vec![false; m * n];
907 for idx in 0..cm * cn {
908 let (im, in_) = (idx % cm, idx / cm);
909 let (a0, a1) = (im * dr_m, (im * dr_m + dr_m).min(m));
910 let (b0, b1) = (in_ * dr_n, (in_ * dr_n + dr_n).min(n));
911 assert!(a0 < a1 && b0 < b1, "empty chunk {idx} in {ctx}");
912 for ia in a0..a1 {
913 for ib in b0..b1 {
914 assert!(!seen[ia * n + ib], "tile ({ia},{ib}) twice in {ctx}");
915 seen[ia * n + ib] = true;
916 }
917 }
918 }
919 assert!(seen.iter().all(|s| *s), "tile left out in {ctx}");
920 }
921 }
922 }
923 }
924
925 #[cfg(feature = "multithread-mm")]
929 #[test]
930 fn chunk_grid_feeds_every_thread() {
931 for &(m, n) in GRIDS {
932 for &(mr, nr) in RATIOS {
933 for nth in [1usize, 2, 3, 4, 6, 8, 16, 64] {
934 let (cm, cn, ..) = chunk_grid(m, n, mr, nr, nth);
935 assert!(
936 cm * cn >= nth.min(m * n),
937 "{cm}x{cn} chunks for {nth} threads on {m}x{n} panels"
938 );
939 }
940 }
941 }
942 }
943
944 #[cfg(feature = "multithread-mm")]
949 #[test]
950 fn chunk_grid_shape_beats_a_band_on_operand_traffic() {
951 let traffic = |cm: usize, cn: usize, m: usize, n: usize| cn * m + cm * n;
952 for &(m, n) in GRIDS {
953 for &(mr, nr) in RATIOS {
954 for nth in [2usize, 4, 8, 16] {
955 let (cm, cn, ..) = chunk_grid(m, n, mr, nr, nth);
956 let chunks = cm * cn;
957 if chunks > m || chunks > n {
961 continue;
962 }
963 let (m_ext, n_ext) = (m * mr, n * nr);
964 let ours = traffic(cm, cn, m_ext, n_ext);
965 let band_m = traffic(chunks, 1, m_ext, n_ext);
966 let band_n = traffic(1, chunks, m_ext, n_ext);
967 assert!(
968 ours <= band_m.min(band_n),
969 "{cm}x{cn} costs {ours}, bands cost {band_m}/{band_n} \
970 on {m}x{n} panels, {mr}x{nr} kernel, {nth} threads"
971 );
972 }
973 }
974 }
975 }
976}