use core::sync::atomic::{AtomicUsize, Ordering};
#[cfg(feature = "parallel")]
use crate::tuning;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Parallelism {
Serial,
Rayon(usize),
}
impl Default for Parallelism {
fn default() -> Self {
Parallelism::Rayon(0)
}
}
#[cfg(feature = "parallel")]
fn auto_threads() -> usize {
use std::sync::OnceLock;
static AUTO_THREADS: OnceLock<usize> = OnceLock::new();
*AUTO_THREADS.get_or_init(|| {
match std::thread::available_parallelism() {
Ok(n) => n.get(),
#[cfg(all(target_arch = "wasm32", feature = "wasm_threads"))]
Err(_) => crate::tuning::wasm_threads(),
#[cfg(not(all(target_arch = "wasm32", feature = "wasm_threads")))]
Err(_) => 1,
}
})
}
#[cfg(all(target_arch = "wasm32", feature = "wasm_threads"))]
fn wasm_pool() -> &'static rayon::ThreadPool {
use std::sync::OnceLock;
static POOL: OnceLock<rayon::ThreadPool> = OnceLock::new();
POOL.get_or_init(|| {
rayon::ThreadPoolBuilder::new()
.num_threads(crate::tuning::wasm_threads())
.build()
.expect("gemmkit: failed to build the wasm rayon thread pool")
})
}
#[cfg(all(
feature = "parallel",
not(target_arch = "wasm32"),
not(target_arch = "aarch64")
))]
const FULL_WIDTH_MNK_AUTO: usize = 110_000_000;
#[cfg(all(feature = "parallel", target_arch = "aarch64"))]
const FULL_WIDTH_MNK_AUTO: usize = 14_000_000;
#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
const GEMV_TIER_STEP_AUTO: usize = 8;
#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
fn class_sizes() -> ([usize; 3], usize) {
let n = tuning::pool_classes().min(3);
let cores = auto_threads();
let mut out = [0usize; 3];
let mut count = 0;
if n == 0 {
return (out, 0);
}
let mut div = 1usize << n;
while div >= 2 {
let size = cores / div;
if size >= 2 && size < cores {
out[count] = size;
count += 1;
}
div /= 2;
}
(out, count)
}
#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
fn class_pool(size: usize) -> Option<&'static rayon::ThreadPool> {
use std::sync::OnceLock;
static POOLS: [OnceLock<Option<rayon::ThreadPool>>; 3] =
[OnceLock::new(), OnceLock::new(), OnceLock::new()];
let cores = auto_threads();
let slot = if size >= cores / 2 {
0
} else if size >= cores / 4 {
1
} else {
2
};
POOLS[slot]
.get_or_init(|| {
rayon::ThreadPoolBuilder::new()
.num_threads(size)
.thread_name(|i| format!("gemmkit-pool-{i}"))
.build()
.ok()
})
.as_ref()
}
#[cfg(feature = "parallel")]
const RAYON_USABLE: bool = cfg!(any(
not(target_arch = "wasm32"),
feature = "wasm_threads",
target_feature = "atomics",
));
impl Parallelism {
#[cfg_attr(not(feature = "parallel"), allow(unused_variables))]
pub(crate) fn resolve(self, mnk: usize, n_jobs: usize) -> usize {
let n_jobs = n_jobs.max(1);
match self {
Parallelism::Serial => 1,
#[cfg(not(feature = "parallel"))]
Parallelism::Rayon(_) => 1,
#[cfg(feature = "parallel")]
Parallelism::Rayon(req) => {
if !RAYON_USABLE {
return 1;
}
let gate = tuning::parallel_threshold();
if mnk < gate {
return 1;
}
if req != 0 {
return req.min(auto_threads()).min(n_jobs).max(1);
}
let cores = auto_threads();
let want = mnk / tuning::par_mnk_per_worker().max(1);
#[cfg(not(target_arch = "wasm32"))]
let w = {
let (tiers, n_tiers) = class_sizes();
if n_tiers == 0 {
want
} else {
let full = match tuning::full_width_mnk() {
0 => FULL_WIDTH_MNK_AUTO,
v => v,
};
if mnk >= full {
cores
} else {
let tiers = &tiers[..n_tiers];
tiers
.iter()
.copied()
.find(|&t| want <= (3 * t) / 2)
.unwrap_or(tiers[n_tiers - 1])
}
}
};
#[cfg(target_arch = "wasm32")]
let w = want;
w.min(cores).min(n_jobs).max(1)
}
}
}
#[cfg_attr(not(feature = "parallel"), allow(unused_variables))]
pub(crate) fn resolve_bandwidth(self, bytes_touched: usize, rows: usize) -> usize {
let rows = rows.max(1);
match self {
Parallelism::Serial => 1,
#[cfg(not(feature = "parallel"))]
Parallelism::Rayon(_) => 1,
#[cfg(feature = "parallel")]
Parallelism::Rayon(req) => {
if !RAYON_USABLE {
return 1;
}
if bytes_touched < crate::cache::gemv_parallel_floor_bytes() {
return 1;
}
if req != 0 {
return req.min(auto_threads()).min(rows).max(1);
}
let cores = auto_threads();
bandwidth_cap(cores, bytes_touched)
.min(cores)
.min(rows)
.max(1)
}
}
}
}
#[cfg_attr(not(feature = "parallel"), allow(dead_code))]
pub(crate) enum BatchPlan {
Serial,
BatchParallel(usize),
SequentialInternal,
}
impl Parallelism {
#[cfg_attr(not(feature = "parallel"), allow(unused_variables))]
pub(crate) fn resolve_batch(
self,
m: usize,
k: usize,
n: usize,
sizeof: usize,
batch: usize,
) -> BatchPlan {
let batch = batch.max(1);
let elem_mnk = m.saturating_mul(k).saturating_mul(n);
match self {
Parallelism::Serial => BatchPlan::Serial,
#[cfg(not(feature = "parallel"))]
Parallelism::Rayon(_) => BatchPlan::Serial,
#[cfg(feature = "parallel")]
Parallelism::Rayon(req) => {
if !RAYON_USABLE {
return BatchPlan::Serial;
}
if elem_mnk.saturating_mul(batch) < tuning::parallel_threshold() {
return BatchPlan::Serial;
}
let budget = if req != 0 {
req.min(auto_threads())
} else {
auto_threads()
};
if budget <= 1 {
return BatchPlan::Serial;
}
if batch >= budget {
return BatchPlan::BatchParallel(budget);
}
let elem_bytes = m
.saturating_mul(k)
.saturating_add(k.saturating_mul(n))
.saturating_add(m.saturating_mul(n))
.saturating_mul(sizeof);
#[cfg(not(target_arch = "aarch64"))]
let split_wins = elem_bytes > crate::cache::topology().l2.effective_bytes().max(1);
#[cfg(target_arch = "aarch64")]
let split_wins =
elem_bytes > batch.saturating_mul(tuning::seq_internal_bytes_per_worker());
if m > 1 && n > 1 && split_wins {
BatchPlan::SequentialInternal
} else {
BatchPlan::BatchParallel(batch)
}
}
}
}
#[cfg_attr(not(feature = "parallel"), allow(unused_variables))]
pub(crate) fn resolve_batch_flat(self, total_mnk: usize, count: usize) -> usize {
let count = count.max(1);
match self {
Parallelism::Serial => 1,
#[cfg(not(feature = "parallel"))]
Parallelism::Rayon(_) => 1,
#[cfg(feature = "parallel")]
Parallelism::Rayon(req) => {
if !RAYON_USABLE || total_mnk < tuning::parallel_threshold() {
return 1;
}
let budget = if req != 0 {
req.min(auto_threads())
} else {
auto_threads()
};
budget.min(count).max(1)
}
}
}
}
#[cfg(feature = "parallel")]
fn bandwidth_cap(cores: usize, bytes: usize) -> usize {
let knob = tuning::gemv_thread_cap();
if knob != 0 {
return knob.max(1);
}
let flat = (cores / 2).max(2);
#[cfg(target_arch = "wasm32")]
{
let _ = bytes;
flat
}
#[cfg(not(target_arch = "wasm32"))]
{
let (tiers, n_tiers) = class_sizes();
if n_tiers == 0 {
return flat;
}
let step = match tuning::gemv_tier_step() {
0 => GEMV_TIER_STEP_AUTO,
v => v.max(1),
};
let mut rung = 0;
let mut bound = crate::cache::gemv_parallel_floor_bytes();
while rung + 1 < n_tiers {
bound = bound.saturating_mul(step);
if bytes < bound {
break;
}
rung += 1;
}
tiers[rung]
}
}
#[derive(Copy, Clone)]
pub(crate) struct Ptr<T>(pub(crate) *mut T);
unsafe impl<T> Send for Ptr<T> {}
unsafe impl<T> Sync for Ptr<T> {}
pub(crate) struct JobCursor {
next: AtomicUsize,
n_jobs: usize,
grain: usize,
}
impl JobCursor {
#[inline]
pub(crate) fn new(n_jobs: usize, grain: usize) -> Self {
Self {
next: AtomicUsize::new(0),
n_jobs,
grain: grain.max(1),
}
}
#[inline]
pub(crate) fn next_chunk(&self) -> Option<(usize, usize)> {
let start = self.next.fetch_add(self.grain, Ordering::Relaxed);
if start >= self.n_jobs {
None
} else {
Some((start, (start + self.grain).min(self.n_jobs)))
}
}
}
#[inline]
pub(crate) fn job_grain(n_jobs: usize, n_threads: usize) -> usize {
if n_threads <= 1 {
return n_jobs.max(1);
}
let oversample = crate::tuning::parallel_oversample();
(n_jobs / n_threads.saturating_mul(oversample)).max(1)
}
#[inline]
pub(crate) fn packed_block_grain(n_nt: usize, n_mc: usize, n_threads: usize) -> usize {
let target = crate::tuning::packed_oversample().saturating_mul(n_threads);
let mut splits = 1usize;
while n_mc * splits < target && n_nt / (splits * 2) >= 1 {
splits *= 2;
}
while splits > 1 && !n_nt.is_multiple_of(splits) {
splits /= 2;
}
(n_nt / splits).max(1)
}
#[cfg(feature = "parallel")]
pub(crate) fn for_each_worker<F>(n_threads: usize, f: F)
where
F: Fn(usize) + Sync + Send,
{
if n_threads <= 1 {
f(0);
return;
}
if !RAYON_USABLE {
for tid in 0..n_threads {
f(tid);
}
return;
}
use rayon::prelude::*;
#[cfg(all(target_arch = "wasm32", feature = "wasm_threads"))]
{
wasm_pool().install(|| (0..n_threads).into_par_iter().for_each(f));
}
#[cfg(all(target_arch = "wasm32", not(feature = "wasm_threads")))]
{
(0..n_threads).into_par_iter().for_each(f);
}
#[cfg(not(target_arch = "wasm32"))]
{
if rayon::current_thread_index().is_some() {
(0..n_threads).into_par_iter().for_each(f);
return;
}
let (tiers, n_tiers) = class_sizes();
for &size in &tiers[..n_tiers] {
if size >= n_threads {
if let Some(pool) = class_pool(size) {
pool.install(|| (0..n_threads).into_par_iter().for_each(f));
return;
}
break;
}
}
(0..n_threads).into_par_iter().for_each(f);
}
}
#[cfg(not(feature = "parallel"))]
pub(crate) fn for_each_worker<F>(n_threads: usize, f: F)
where
F: Fn(usize),
{
for tid in 0..n_threads {
f(tid);
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn cursor_tiles_range_exactly() {
for &n_jobs in &[0usize, 1, 2, 7, 100, 1000] {
for &grain in &[1usize, 3, 8, 64, 1000, 100_000] {
let cur = JobCursor::new(n_jobs, grain);
let mut seen = Vec::new();
while let Some((s, e)) = cur.next_chunk() {
assert!(
s < e && e <= n_jobs,
"chunk [{s}, {e}) escapes [0, {n_jobs})"
);
seen.extend(s..e);
}
assert_eq!(
seen,
(0..n_jobs).collect::<Vec<_>>(),
"n_jobs={n_jobs} grain={grain}"
);
}
}
}
#[test]
fn zero_grain_clamped_and_terminates() {
let cur = JobCursor::new(5, 0);
let mut n = 0;
while let Some((s, e)) = cur.next_chunk() {
assert_eq!(e - s, 1);
n += 1;
}
assert_eq!(n, 5);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn cursor_partition_is_bijective_under_threads() {
use std::sync::Mutex;
let n_jobs = 10_000usize;
let cur = JobCursor::new(n_jobs, 7);
let collected = Mutex::new(Vec::new());
std::thread::scope(|scope| {
for _ in 0..8 {
scope.spawn(|| {
let mut local = Vec::new();
while let Some((s, e)) = cur.next_chunk() {
local.extend(s..e);
}
collected.lock().unwrap().extend(local);
});
}
});
let mut all = collected.into_inner().unwrap();
all.sort_unstable();
assert_eq!(
all,
(0..n_jobs).collect::<Vec<_>>(),
"indices must partition [0, n_jobs)"
);
}
#[test]
fn job_grain_is_robust() {
assert_eq!(job_grain(100, 1), 100); assert_eq!(job_grain(0, 8), 1); let g = job_grain(10_000, 8);
assert!((1..=10_000).contains(&g));
}
#[test]
fn packed_block_grain_divides_and_balances() {
for &n_nt in &[1usize, 2, 3, 4, 96, 127, 128, 192, 500, 512] {
for &n_mc in &[1usize, 7, 14, 16, 32, 100] {
for &n_threads in &[2usize, 8, 14, 32] {
let g = packed_block_grain(n_nt, n_mc, n_threads);
assert!(g >= 1 && g <= n_nt, "grain {g} out of (0, {n_nt}]");
assert_eq!(n_nt % g, 0, "grain {g} does not divide n_nt {n_nt}");
if n_nt.is_power_of_two() && n_nt >= 2 {
let chunks = n_mc * (n_nt / g);
assert!(
chunks >= 2 * n_threads || g == 1,
"n_nt={n_nt} n_mc={n_mc} thr={n_threads}: {chunks} chunks underfills"
);
}
}
}
}
}
#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
static POOL_KNOB_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
fn resolve_matches_legacy_formula_with_tiers_off() {
let _lock = POOL_KNOB_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let prev = crate::tuning::pool_classes();
crate::tuning::set_pool_classes(0);
let cores = auto_threads();
let per = crate::tuning::par_mnk_per_worker().max(1);
let gate = crate::tuning::parallel_threshold();
let n_jobs = 1_000_000usize;
for &mnk in &[
gate,
gate * 2,
gate * 37,
per * (cores * 3 + 1),
usize::MAX / 4,
] {
let want = mnk / per;
let expect = if mnk < gate {
1
} else {
want.min(cores).min(n_jobs).max(1)
};
assert_eq!(
Parallelism::Rayon(0).resolve(mnk, n_jobs),
expect,
"tiers-off mnk={mnk}"
);
}
crate::tuning::set_pool_classes(prev);
}
#[test]
#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
fn resolve_auto_width_lands_on_a_tier_or_cores() {
let _lock = POOL_KNOB_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let cores = auto_threads();
if cores < 4 {
return; }
let prev_pc = crate::tuning::pool_classes();
let per = crate::tuning::par_mnk_per_worker().max(1);
let n_jobs = 1_000_000usize;
for &pc in &[1usize, 2, 3] {
crate::tuning::set_pool_classes(pc);
let (tiers, n_tiers) = class_sizes();
let mut allowed: Vec<usize> = vec![1usize, cores];
allowed.extend_from_slice(&tiers[..n_tiers]);
for want in 0..=(cores * 2 + 2) {
let mnk = (want * per).max(1);
let w = Parallelism::Rayon(0).resolve(mnk, n_jobs);
assert!(
allowed.contains(&w),
"pc={pc} want={want} mnk={mnk} -> width {w} not in {allowed:?}"
);
}
}
crate::tuning::set_pool_classes(prev_pc);
}
#[test]
#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
fn resolve_huge_mnk_routes_to_full_width() {
let _lock = POOL_KNOB_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let prev = crate::tuning::pool_classes();
crate::tuning::set_pool_classes(2); let cores = auto_threads();
let n_jobs = 1_000_000usize;
let w = Parallelism::Rayon(0).resolve(usize::MAX / 4, n_jobs);
assert_eq!(w, cores.min(n_jobs).max(1), "huge mnk must take full width");
crate::tuning::set_pool_classes(prev);
}
#[test]
#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
fn resolve_bandwidth_climbs_the_tier_ladder() {
let _lock = POOL_KNOB_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let prev_pc = crate::tuning::pool_classes();
crate::tuning::set_pool_classes(2);
let (tiers, n_tiers) = class_sizes();
if n_tiers < 2 {
crate::tuning::set_pool_classes(prev_pc);
return; }
let prev_step = crate::tuning::gemv_tier_step();
let step = 4;
crate::tuning::set_gemv_tier_step(step);
let floor = crate::cache::gemv_parallel_floor_bytes();
let at = |bytes: usize| Parallelism::Rayon(0).resolve_bandwidth(bytes, usize::MAX);
assert_eq!(
at(floor.saturating_sub(1)),
1,
"below the floor stays serial"
);
assert_eq!(at(floor), tiers[0], "the floor takes the smallest tier");
let first_step = floor.saturating_mul(step);
assert_eq!(at(first_step - 1), tiers[0], "just under the first step");
assert_eq!(at(first_step), tiers[1], "one step up takes the next tier");
assert_eq!(
at(usize::MAX / 2),
tiers[n_tiers - 1],
"past the ladder takes the top tier"
);
let mut prev_w = 0;
let mut bytes = floor;
for _ in 0..8 {
let w = at(bytes);
assert!(
w >= prev_w,
"width fell from {prev_w} to {w} at {bytes} bytes"
);
prev_w = w;
bytes = bytes.saturating_mul(2);
}
crate::tuning::set_gemv_tier_step(prev_step);
crate::tuning::set_pool_classes(prev_pc);
}
#[test]
#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
fn gemv_thread_cap_knob_pins_the_width_flat() {
let _lock = POOL_KNOB_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let pinned = 3;
if auto_threads() < pinned {
return; }
let prev = crate::tuning::gemv_thread_cap();
crate::tuning::set_gemv_thread_cap(pinned);
let floor = crate::cache::gemv_parallel_floor_bytes();
for bytes in [floor, floor.saturating_mul(64), usize::MAX / 2] {
assert_eq!(
Parallelism::Rayon(0).resolve_bandwidth(bytes, usize::MAX),
pinned,
"the pinned width must hold at {bytes} bytes"
);
}
crate::tuning::set_gemv_thread_cap(prev);
}
}