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 Dreadful,
38 Generic,
40 RustOptimized,
42 TargetOptimized,
44 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 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 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
282const ST_BLK_MAX: usize = 16;
285
286const ST_BLK_L3_MAX: usize = 64;
289
290fn 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
303fn l2_block_budget_bytes() -> usize {
305 tier_budget_bytes(crate::cache::cache_info().l2, 1, 3, 256 * 1024)
306}
307
308fn l3_block_budget_bytes() -> Option<(usize, usize)> {
315 use crate::cache::LlcKind;
316 let (bytes, kind) = crate::cache::last_level_cache()?;
317 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#[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#[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
355fn 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#[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#[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#[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#[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#[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 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 #[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 #[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 #[test]
770 fn outer_tier_gated_on_working_set_spilling_llc() {
771 let llc = 2 * 1024 * 1024; assert!(!outer_tier_pays(64, 8, 8, 8, 64, 4, llc));
774 assert!(outer_tier_pays(256, 256, 8, 8, 256, 4, llc));
776 assert!(!outer_tier_pays(1, 0, llc, 0, 1, 1, llc));
778 assert!(!outer_tier_pays(4096, 4096, 8, 8, 4096, 4, 0));
780 assert!(!outer_tier_pays(4096, 4096, 8, 8, 0, 4, llc));
782 }
783}