#[macro_use]
mod macros;
pub mod cost_model;
#[macro_use]
pub(crate) mod fuse;
pub(crate) mod input_store;
pub(crate) mod kernel;
#[macro_use]
pub(crate) mod panel_extract;
mod scratch;
mod storage;
#[cfg(test)]
#[macro_use]
pub mod tests;
use crate::multithread::Executor;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::fmt::Debug;
use tract_data::internal::*;
pub use cost_model::*;
pub use fuse::*;
pub use input_store::*;
pub use kernel::*;
pub use panel_extract::*;
pub use scratch::*;
pub use storage::*;
pub fn no_prefetch(_ptr: *const u8, _len: usize) {}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ImplementationQuality {
Dreadful,
Generic,
RustOptimized,
TargetOptimized,
ManuallyOptimized,
}
impl ImplementationQuality {
pub fn best_to_worst() -> &'static [ImplementationQuality] {
use ImplementationQuality::*;
&[ManuallyOptimized, TargetOptimized, RustOptimized, Generic, Dreadful]
}
pub fn cost(&self) -> usize {
ImplementationQuality::best_to_worst().iter().position(|x| x == self).unwrap()
}
}
impl PartialOrd for ImplementationQuality {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(usize::from(*self).cmp(&usize::from(*other)))
}
}
impl From<ImplementationQuality> for usize {
fn from(value: ImplementationQuality) -> Self {
value.cost()
}
}
pub trait MatMatMul: Debug + dyn_clone::DynClone + Send + Sync + std::any::Any {
fn name(&self) -> &str;
fn mr(&self) -> usize;
fn nr(&self) -> usize;
fn quality(&self) -> ImplementationQuality;
fn dynamic_boost(&self) -> isize;
fn is_supported_here(&self) -> bool;
#[allow(clippy::type_complexity)]
fn packings(&self) -> &[(Box<dyn MMMInputFormat>, Box<dyn MMMInputFormat>)];
fn internal_type(&self) -> DatumType;
unsafe fn c_view(&self, m_axis: Option<usize>, n_axis: Option<usize>) -> OutputStoreSpec;
unsafe fn c_from_data_and_strides(
&self,
item_size: usize,
row_stride: isize,
col_stride: isize,
) -> OutputStoreSpec;
fn can_fuse(&self, spec: &FusedSpec) -> bool;
fn stores(&self) -> Cow<'_, [DatumType]>;
unsafe fn run(&self, m: usize, n: usize, non_linear: &[FusedSpec]) -> TractResult<()> {
unsafe {
let mut scratch = self.allocate_scratch_space();
self.run_with_scratch_space(m, n, &mut *scratch, non_linear)
}
}
unsafe fn allocate_scratch_space(&self) -> Box<dyn ScratchSpace>;
unsafe fn can_use_scratch_space(&self, scratch: &dyn ScratchSpace) -> bool;
unsafe fn run_with_scratch_space(
&self,
m: usize,
n: usize,
scratch: &mut dyn ScratchSpace,
non_linear: &[FusedSpec],
) -> TractResult<()>;
}
dyn_clone::clone_trait_object!(MatMatMul);
impl PartialEq for Box<dyn MatMatMul> {
fn eq(&self, other: &Box<dyn MatMatMul>) -> bool {
self.name() == other.name()
}
}
impl Eq for Box<dyn MatMatMul> {}
impl std::hash::Hash for Box<dyn MatMatMul> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name().hash(state)
}
}
impl<K: MatMatMulKer> MatMatMul for K {
fn name(&self) -> &str {
self.name()
}
fn mr(&self) -> usize {
self.mr()
}
fn nr(&self) -> usize {
self.nr()
}
fn quality(&self) -> ImplementationQuality {
MatMatMulKer::quality(self)
}
fn dynamic_boost(&self) -> isize {
MatMatMulKer::dynamic_boost(self)
}
fn is_supported_here(&self) -> bool {
MatMatMulKer::is_supported_here(self)
}
fn packings(&self) -> &[(Box<dyn MMMInputFormat>, Box<dyn MMMInputFormat>)] {
self.packings()
}
fn internal_type(&self) -> DatumType {
K::Acc::datum_type()
}
fn can_fuse(&self, spec: &FusedSpec) -> bool {
self.can_fuse(spec)
}
unsafe fn c_view(&self, m_axis: Option<usize>, n_axis: Option<usize>) -> OutputStoreSpec {
OutputStoreSpec::View { m_axis, n_axis, mr: self.mr(), nr: self.nr() }
}
unsafe fn c_from_data_and_strides(
&self,
item_size: usize,
row_stride: isize,
col_stride: isize,
) -> OutputStoreSpec {
OutputStoreSpec::Strides {
row_byte_stride: row_stride * item_size as isize,
col_byte_stride: col_stride * item_size as isize,
mr: self.mr(),
nr: self.nr(),
}
}
fn stores(&self) -> Cow<'_, [DatumType]> {
self.stores()
}
unsafe fn allocate_scratch_space(&self) -> Box<dyn ScratchSpace> {
Box::<ScratchSpaceImpl<K::Acc>>::default()
}
unsafe fn can_use_scratch_space(&self, scratch: &dyn ScratchSpace) -> bool {
scratch.downcast_ref::<ScratchSpaceImpl<K::Acc>>().is_some()
}
unsafe fn run_with_scratch_space(
&self,
m: usize,
n: usize,
scratch: &mut dyn ScratchSpace,
non_linear: &[FusedSpec],
) -> TractResult<()> {
unsafe {
let scratch = scratch
.downcast_mut::<ScratchSpaceImpl<K::Acc>>()
.context("Wrong scratch space type")?;
scratch.prepare(self, m, n, non_linear)?;
if n == 1 && self.nr() == 1 {
run_with_scratch_space_vec(self, m, scratch, non_linear)
} else {
let (mut prefer_col, mut prefer_row) = (0, 0);
for uop in non_linear.iter() {
if let Some(col) = uop.prefer_col_outer() {
prefer_col = col as usize;
prefer_row = (!col) as usize;
}
}
let k = non_linear
.iter()
.find_map(|f| match f {
FusedSpec::AddMatMul { a, .. } => Some(a.k()),
_ => None,
})
.unwrap_or(0);
if prefer_col > prefer_row {
run_with_scratch_space_col_outer(self, m, n, k, scratch, non_linear)
} else {
run_with_scratch_space_row_outer(self, m, n, k, scratch, non_linear)
}
}
}
}
}
unsafe fn run_with_scratch_space_vec<K: MatMatMulKer>(
ker: &K,
m: usize,
scratch: &mut ScratchSpaceImpl<K::Acc>,
non_linear: &[FusedSpec],
) -> TractResult<()> {
unsafe {
match crate::multithread::current_tract_executor() {
Executor::SingleThread => scratch.run_in_tls_scope(|scratch, tls| {
for ia in 0..m.divceil(ker.mr()) {
scratch.run_one_tile(ker, non_linear, tls, ia, 0)?;
}
TractResult::Ok(())
}),
#[cfg(feature = "multithread-mm")]
Executor::MultiThread(pool) => chunked_dispatch_rayon(
Some(&pool),
m.divceil(ker.mr()),
1,
|ia_start, ia_end, _, _| {
scratch.run_in_tls_scope(|scratch, tls| {
for ia in ia_start..ia_end {
scratch.run_one_tile(ker, non_linear, tls, ia, 0)?;
}
TractResult::Ok(())
})
},
),
#[cfg(feature = "multithread-mm")]
Executor::RayonGlobal => {
chunked_dispatch_rayon(None, m.divceil(ker.mr()), 1, |ia_start, ia_end, _, _| {
scratch.run_in_tls_scope(|scratch, tls| {
for ia in ia_start..ia_end {
scratch.run_one_tile(ker, non_linear, tls, ia, 0)?;
}
TractResult::Ok(())
})
})
}
}
}
}
const ST_BLK_MAX: usize = 16;
const ST_BLK_L3_MAX: usize = 64;
fn tier_budget_bytes(cache_bytes: usize, num: usize, den: usize, fallback: usize) -> usize {
if cache_bytes == 0 {
fallback
} else {
(cache_bytes * num / den).clamp(64 * 1024, 64 * 1024 * 1024)
}
}
fn l2_block_budget_bytes() -> usize {
tier_budget_bytes(crate::cache::cache_info().l2, 1, 3, 256 * 1024)
}
fn l3_block_budget_bytes() -> Option<(usize, usize)> {
use crate::cache::LlcKind;
let (bytes, kind) = crate::cache::last_level_cache()?;
let (num, den) = match kind {
LlcKind::Dedicated => (1, 2),
LlcKind::SystemLevel => (1, 4),
};
Some((bytes, tier_budget_bytes(bytes, num, den, 0)))
}
#[inline]
fn block_edge_for(
budget: usize,
mr: usize,
nr: usize,
k: usize,
elem_bytes: usize,
cap: usize,
) -> usize {
if k == 0 {
return cap;
}
let per_blk = ((mr + nr) * k * elem_bytes.max(1)).max(1);
(budget / per_blk).clamp(1, cap)
}
#[inline]
fn st_block_edge(mr: usize, nr: usize, k: usize, elem_bytes: usize) -> usize {
block_edge_for(l2_block_budget_bytes(), mr, nr, k, elem_bytes, ST_BLK_MAX)
}
fn outer_tier_pays(
m_panels: usize,
n_panels: usize,
mr: usize,
nr: usize,
k: usize,
elem_bytes: usize,
llc_bytes: usize,
) -> bool {
let working_set = m_panels
.saturating_mul(mr)
.saturating_add(n_panels.saturating_mul(nr))
.saturating_mul(k)
.saturating_mul(elem_bytes);
llc_bytes > 0 && working_set > llc_bytes
}
#[inline]
fn st_outer_block_edge(
mr: usize,
nr: usize,
k: usize,
elem_bytes: usize,
inner: usize,
m_panels: usize,
n_panels: usize,
) -> usize {
let Some((llc, budget)) = l3_block_budget_bytes() else { return usize::MAX };
if !outer_tier_pays(m_panels, n_panels, mr, nr, k, elem_bytes, llc) {
return usize::MAX;
}
block_edge_for(budget, mr, nr, k, elem_bytes, ST_BLK_L3_MAX).max(inner)
}
#[inline]
fn for_each_blocked_tile(
m_panels: usize,
n_panels: usize,
blk: usize,
blk_outer: usize,
col_outer: bool,
mut f: impl FnMut(usize, usize) -> TractResult<()>,
) -> TractResult<()> {
let blk = blk.max(1);
let blk_outer = blk_outer.max(blk);
let mut jb3 = 0;
while jb3 < n_panels {
let jb3_end = jb3.saturating_add(blk_outer).min(n_panels);
let mut ja3 = 0;
while ja3 < m_panels {
let ja3_end = ja3.saturating_add(blk_outer).min(m_panels);
let mut jb = jb3;
while jb < jb3_end {
let jb_end = (jb + blk).min(jb3_end);
let mut ja = ja3;
while ja < ja3_end {
let ja_end = (ja + blk).min(ja3_end);
if col_outer {
for ib in jb..jb_end {
for ia in ja..ja_end {
f(ia, ib)?;
}
}
} else {
for ia in ja..ja_end {
for ib in jb..jb_end {
f(ia, ib)?;
}
}
}
ja = ja_end;
}
jb = jb_end;
}
ja3 = ja3_end;
}
jb3 = jb3_end;
}
Ok(())
}
#[inline]
unsafe fn run_single_thread_blocked<K: MatMatMulKer>(
ker: &K,
m_panels: usize,
n_panels: usize,
k: usize,
col_outer: bool,
scratch: &mut ScratchSpaceImpl<K::Acc>,
non_linear: &[FusedSpec],
) -> TractResult<()> {
unsafe {
let elem = K::Acc::datum_type().size_of();
let blk = st_block_edge(ker.mr(), ker.nr(), k, elem);
let blk_outer = st_outer_block_edge(ker.mr(), ker.nr(), k, elem, blk, m_panels, n_panels);
scratch.run_in_tls_scope(|scratch, tls| {
for_each_blocked_tile(m_panels, n_panels, blk, blk_outer, col_outer, |ia, ib| {
scratch.run_one_tile(ker, non_linear, tls, ia, ib)
})
})
}
}
unsafe fn run_with_scratch_space_col_outer<K: MatMatMulKer>(
ker: &K,
m: usize,
n: usize,
k: usize,
scratch: &mut ScratchSpaceImpl<K::Acc>,
non_linear: &[FusedSpec],
) -> TractResult<()> {
unsafe {
match crate::multithread::current_tract_executor() {
Executor::SingleThread => run_single_thread_blocked(
ker,
m.divceil(ker.mr()),
n.divceil(ker.nr()),
k,
true,
scratch,
non_linear,
),
#[cfg(feature = "multithread-mm")]
Executor::MultiThread(pool) => chunked_dispatch_rayon(
Some(&pool),
m.divceil(ker.mr()),
n.divceil(ker.nr()),
|ia_start, ia_end, ib_start, ib_end| {
scratch.run_in_tls_scope(|scratch, tls| {
for ib in ib_start..ib_end {
for ia in ia_start..ia_end {
scratch.run_one_tile(ker, non_linear, tls, ia, ib)?;
}
}
TractResult::Ok(())
})
},
),
#[cfg(feature = "multithread-mm")]
Executor::RayonGlobal => chunked_dispatch_rayon(
None,
m.divceil(ker.mr()),
n.divceil(ker.nr()),
|ia_start, ia_end, ib_start, ib_end| {
scratch.run_in_tls_scope(|scratch, tls| {
for ib in ib_start..ib_end {
for ia in ia_start..ia_end {
scratch.run_one_tile(ker, non_linear, tls, ia, ib)?;
}
}
TractResult::Ok(())
})
},
),
}
}
}
unsafe fn run_with_scratch_space_row_outer<K: MatMatMulKer>(
ker: &K,
m: usize,
n: usize,
k: usize,
scratch: &mut ScratchSpaceImpl<K::Acc>,
non_linear: &[FusedSpec],
) -> TractResult<()> {
unsafe {
match crate::multithread::current_tract_executor() {
Executor::SingleThread => run_single_thread_blocked(
ker,
m.divceil(ker.mr()),
n.divceil(ker.nr()),
k,
false,
scratch,
non_linear,
),
#[cfg(feature = "multithread-mm")]
Executor::MultiThread(pool) => chunked_dispatch_rayon(
Some(&pool),
m.divceil(ker.mr()),
n.divceil(ker.nr()),
|ia_start, ia_end, ib_start, ib_end| {
scratch.run_in_tls_scope(|scratch, tls| {
for ia in ia_start..ia_end {
for ib in ib_start..ib_end {
scratch.run_one_tile(ker, non_linear, tls, ia, ib)?;
}
}
TractResult::Ok(())
})
},
),
#[cfg(feature = "multithread-mm")]
Executor::RayonGlobal => chunked_dispatch_rayon(
None,
m.divceil(ker.mr()),
n.divceil(ker.nr()),
|ia_start, ia_end, ib_start, ib_end| {
scratch.run_in_tls_scope(|scratch, tls| {
for ia in ia_start..ia_end {
for ib in ib_start..ib_end {
scratch.run_one_tile(ker, non_linear, tls, ia, ib)?;
}
}
TractResult::Ok(())
})
},
),
}
}
}
#[cfg(feature = "multithread-mm")]
fn chunk_grid(n_panels_m: usize, n_panels_n: usize, nth: usize) -> (usize, usize, usize, usize) {
let chunk_size = if n_panels_m == 1 || n_panels_n == 1 { 64 } else { 16 };
let mut nchunks_m = n_panels_m.div_ceil(chunk_size);
let mut nchunks_n = n_panels_n.div_ceil(chunk_size);
if nchunks_m * nchunks_n < 4 * nth {
if n_panels_m > n_panels_n {
nchunks_m = nth;
nchunks_n = 1;
} else {
nchunks_m = 1;
nchunks_n = nth;
}
}
let dr_m = n_panels_m.div_ceil(nchunks_m).max(1);
let dr_n = n_panels_n.div_ceil(nchunks_n).max(1);
(nchunks_m, nchunks_n, dr_m, dr_n)
}
#[cfg(feature = "multithread-mm")]
unsafe fn chunked_dispatch_rayon<F>(
pool: Option<&rayon::ThreadPool>,
n_panels_m: usize,
n_panels_n: usize,
run_chunk: F,
) -> TractResult<()>
where
F: Fn(usize, usize, usize, usize) -> TractResult<()> + Sync,
{
use rayon::prelude::*;
if n_panels_m == 0 || n_panels_n == 0 {
return Ok(());
}
if n_panels_m * n_panels_n < crate::multithread::current_threading_panel_threshold() {
return run_chunk(0, n_panels_m, 0, n_panels_n);
}
let use_global = pool.is_none_or(|p| p.current_num_threads() <= 1);
let body = || {
let nth = rayon::current_num_threads();
let (nchunks_m, nchunks_n, dr_m, dr_n) = chunk_grid(n_panels_m, n_panels_n, nth);
let total = nchunks_m * nchunks_n;
(0..total).into_par_iter().try_for_each(|idx| {
let im = idx % nchunks_m;
let in_ = idx / nchunks_m;
let ia_start = im * dr_m;
let ia_end = (ia_start + dr_m).min(n_panels_m);
let ib_start = in_ * dr_n;
let ib_end = (ib_start + dr_n).min(n_panels_n);
run_chunk(ia_start, ia_end, ib_start, ib_end)
})
};
if use_global { body() } else { pool.unwrap().install(body) }
}
#[cfg(test)]
mod blocked_walk_tests {
use super::*;
use std::collections::HashSet;
fn collect(
m: usize,
n: usize,
blk: usize,
blk_outer: usize,
col_outer: bool,
) -> Vec<(usize, usize)> {
let mut v = Vec::new();
for_each_blocked_tile(m, n, blk, blk_outer, col_outer, |ia, ib| {
v.push((ia, ib));
Ok(())
})
.unwrap();
v
}
#[test]
fn covers_every_tile_once() {
for &(m, n) in &[(1, 1), (3, 5), (16, 16), (40, 7), (7, 40), (80, 80)] {
for &blk in &[1, 3, 16] {
for &blk_outer in &[blk, blk + 1, 64, usize::MAX] {
for &col_outer in &[false, true] {
let tiles = collect(m, n, blk, blk_outer, col_outer);
assert_eq!(tiles.len(), m * n, "m={m} n={n} blk={blk} outer={blk_outer}");
let set: HashSet<_> = tiles.iter().copied().collect();
assert_eq!(
set.len(),
m * n,
"duplicate tiles m={m} n={n} blk={blk} outer={blk_outer}"
);
for ia in 0..m {
for ib in 0..n {
assert!(set.contains(&(ia, ib)), "missing ({ia},{ib})");
}
}
}
}
}
}
}
#[test]
fn outer_max_matches_single_level() {
for &(m, n) in &[(40, 7), (80, 80), (13, 29)] {
for &blk in &[1, 4, 16] {
for &col_outer in &[false, true] {
let two_tier = collect(m, n, blk, usize::MAX, col_outer);
let mut single = Vec::new();
let mut jb = 0;
while jb < n {
let jb_end = (jb + blk).min(n);
let mut ja = 0;
while ja < m {
let ja_end = (ja + blk).min(m);
if col_outer {
for ib in jb..jb_end {
for ia in ja..ja_end {
single.push((ia, ib));
}
}
} else {
for ia in ja..ja_end {
for ib in jb..jb_end {
single.push((ia, ib));
}
}
}
ja = ja_end;
}
jb = jb_end;
}
assert_eq!(two_tier, single, "m={m} n={n} blk={blk} col_outer={col_outer}");
}
}
}
}
#[test]
fn outer_tier_gated_on_working_set_spilling_llc() {
let llc = 2 * 1024 * 1024; assert!(!outer_tier_pays(64, 8, 8, 8, 64, 4, llc));
assert!(outer_tier_pays(256, 256, 8, 8, 256, 4, llc));
assert!(!outer_tier_pays(1, 0, llc, 0, 1, 1, llc));
assert!(!outer_tier_pays(4096, 4096, 8, 8, 4096, 4, 0));
assert!(!outer_tier_pays(4096, 4096, 8, 8, 0, 4, llc));
}
}