use super::fused::{
IndexTable, PAD_BYTES, PreTile, RunCursor, Window, fill_whole, n_scans, n_shifted,
run_fused_window, run_pre_tiled, seeds_itself,
};
use super::pool;
use super::resolve::{Rv, rm_strides};
use super::*;
const MIN_WORK_PER_WORKER: usize = 1 << 14;
const MIN_ELEMS_PER_WORKER: usize = 1024;
const REDUCE_CELLS_PER_WORKER: usize = 64;
fn reduce_cells_floor() -> usize {
#[cfg(test)]
if FORCE.with(std::cell::Cell::get).is_some() {
return 1;
}
REDUCE_CELLS_PER_WORKER
}
const ALIGN: usize = 8;
pub(super) fn call_ways(call_work: usize) -> usize {
#[cfg(test)]
if let Some(w) = FORCE.with(std::cell::Cell::get) {
return w;
}
let threads = rayon_threads();
if threads <= 1 {
return 1;
}
threads.min(call_work / MIN_WORK_PER_WORKER).max(1)
}
pub(super) fn thread_budget() -> usize {
#[cfg(test)]
if let Some(w) = FORCE.with(std::cell::Cell::get) {
return w;
}
rayon_threads()
}
fn rayon_threads() -> usize {
if rayon::current_thread_index().is_some() {
rayon::current_num_threads()
} else {
default_threads()
}
}
fn default_threads() -> usize {
static DEFAULT: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*DEFAULT.get_or_init(|| {
std::env::var("RAYON_NUM_THREADS")
.ok()
.and_then(|v| v.trim().parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or_else(|| std::thread::available_parallelism().map_or(1, |n| n.get()))
})
}
pub(super) fn forced() -> bool {
#[cfg(test)]
if FORCE.with(std::cell::Cell::get).is_some() {
return true;
}
false
}
pub(super) fn ways_for(n: usize, ways: usize) -> usize {
#[cfg(test)]
if FORCE.with(std::cell::Cell::get).is_some() {
return ways.min(n).max(1);
}
if ways > 1 && n >= ways * MIN_ELEMS_PER_WORKER {
ways
} else {
1
}
}
pub(super) fn lane_split_ways(lanes: usize, elems: usize, ways: usize) -> usize {
if ways > 1 && lanes >= ways * reduce_cells_floor() && ways_for(elems, ways) > 1 {
ways
} else {
1
}
}
const MIN_LANES_PER_WORKER: usize = 32;
pub(super) fn lane_ways(lanes: usize, cost: usize, ways: usize) -> usize {
#[cfg(test)]
if FORCE.with(std::cell::Cell::get).is_some() {
return ways.min(lanes).max(1);
}
if ways > 1
&& lanes >= ways * MIN_LANES_PER_WORKER
&& lanes.saturating_mul(cost) >= ways * MIN_WORK_PER_WORKER
{
ways
} else {
1
}
}
pub(super) fn lane_cost(ls: &LaneSpec) -> usize {
ls.micro.len() + ls.inputs.len() + ls.writes.len()
}
pub(super) fn program_work(prog: &TapeProgram) -> usize {
let start = (prog.n_const + prog.n_segment) as usize;
let mut work: usize = prog
.state_vars
.iter()
.map(|sv| sv.shape.iter().product::<usize>())
.sum();
for ins in &prog.instrs[start..] {
work = work.saturating_add(instr_work(prog, ins));
}
work
}
pub(super) fn instr_work(prog: &TapeProgram, ins: &Instr) -> usize {
match ins {
Instr::Fused { spec } => {
let fs = &prog.fused[*spec as usize];
let pre = fs.pre.as_ref().map_or(0, |p| p.rows * p.n_inner);
fs.shape
.iter()
.product::<usize>()
.saturating_mul(fused_cost(fs))
.saturating_add(pre)
}
Instr::DyWrite { write } => {
prog.slots[prog.dy_writes[*write as usize].slot as usize].elems()
}
Instr::Bin { out, .. }
| Instr::Un { out, .. }
| Instr::Neg { out, .. }
| Instr::Select { out, .. }
| Instr::Gather { out, .. }
| Instr::Copy { out, .. }
| Instr::IndexGather { out, .. }
| Instr::TableGather { out, .. }
| Instr::DataGather { out, .. }
| Instr::Scan { out, .. } => prog.slots[*out as usize].elems(),
Instr::Reduce { src_shape, .. } => src_shape.iter().product(),
Instr::Lanes { spec } => {
let ls = &prog.lanes[*spec as usize];
(ls.lanes as usize).saturating_mul(lane_cost(ls) + 1)
}
_ => 0,
}
}
fn fused_cost(fs: &FusedSpec) -> usize {
let loads = fs
.inputs
.iter()
.filter(|i| i.load_reg != GroupIx::MAX)
.count();
fs.micro.len() + fs.outputs.len() + loads + 1
}
pub(super) fn split_ways(fs: &FusedSpec, ways: usize) -> usize {
if ways <= 1 {
return 1;
}
let n: usize = fs.shape.iter().product();
let by_work = |n: usize| fused_ways(n, || fused_cost(fs), ways);
if let Some(p) = &fs.pre {
return if p.n_inner >= ways * super::fused::pre_cols(p.rows) || forced() {
by_work(n + p.rows * p.n_inner)
} else {
1
};
}
if let Some(cols) = leading_scan_cols(fs) {
return if cols >= ways * reduce_cells_floor() {
by_work(n)
} else {
1
};
}
match (&fs.reduce, scan_row(fs)) {
(Some(_), Some(_)) => 1,
(Some(r), None) if r.n_inner >= ways * reduce_cells_floor() => by_work(n),
(Some(_), None) => 1,
(None, Some(row)) if n / row < ways => 1,
(None, _) => by_work(n),
}
}
const MIN_FUSED_ELEMS_PER_WORKER: usize = 64;
fn fused_ways(n: usize, cost: impl Fn() -> usize, ways: usize) -> usize {
#[cfg(test)]
if FORCE.with(std::cell::Cell::get).is_some() {
return ways.min(n).max(1);
}
if ways_for(n, ways) > 1
|| ways > 1
&& n >= ways * MIN_FUSED_ELEMS_PER_WORKER
&& n.saturating_mul(cost()) >= ways * MIN_WORK_PER_WORKER
{
ways
} else {
1
}
}
pub(super) fn leading_scan_cols(fs: &FusedSpec) -> Option<usize> {
let n: usize = fs.shape.iter().product();
let mut cols = None;
for m in &fs.micro {
if let MicroOp::Scan { row, post, .. } = m {
let (row, post) = (*row as usize, *post as usize);
if row * post != n || cols.is_some_and(|c| c != post) {
return None;
}
cols = Some(post);
}
}
let cols = cols?;
if fs.pre.is_some() || fs.reduce.as_ref().is_some_and(|r| r.n_inner != cols) {
return None;
}
Some(cols)
}
fn scan_row(fs: &FusedSpec) -> Option<usize> {
fn gcd(a: usize, b: usize) -> usize {
if b == 0 { a } else { gcd(b, a % b) }
}
fs.micro
.iter()
.filter_map(|m| match m {
MicroOp::Scan { row, post, .. } => Some((*row as usize * *post as usize).max(1)),
_ => None,
})
.reduce(|a, b| a / gcd(a, b) * b)
}
pub(super) unsafe fn copy_strided(
call: usize,
dst: *mut f64,
dstr: &[i64],
src: *const f64,
sstr: &[i64],
shape: &[usize],
) {
let ways = copy_ways(call, dstr, shape);
if ways <= 1 {
unsafe { super::kernels::copy_strided(dst, dstr, src, sstr, shape) };
return;
}
let (dp, sp) = (SendPtr(dst), SendPtr(src as *mut f64));
pool::run(ways, &move |w| {
let (dp, sp) = (dp, sp);
unsafe { copy_strided_share(ways, w, dp.0, dstr, sp.0, sstr, shape) };
});
}
fn copy_axis(dstr: &[i64], shape: &[usize]) -> Option<usize> {
(0..shape.len())
.filter(|&d| shape[d] > 1)
.max_by_key(|&d| dstr[d].unsigned_abs())
}
pub(super) fn copy_ways(call: usize, dstr: &[i64], shape: &[usize]) -> usize {
let tot: usize = shape.iter().product();
copy_axis(dstr, shape).map_or(1, |d| ways_for(tot, call).min(shape[d]))
}
pub(super) unsafe fn copy_strided_share(
ways: usize,
w: usize,
dst: *mut f64,
dstr: &[i64],
src: *const f64,
sstr: &[i64],
shape: &[usize],
) {
let d = copy_axis(dstr, shape).unwrap_or(0);
if ways <= 1 {
if w == 0 {
unsafe { super::kernels::copy_strided(dst, dstr, src, sstr, shape) };
}
return;
}
if w >= ways {
return;
}
let tot: usize = shape.iter().product();
let rm = rm_strides(shape);
if dstr[..] == rm[..] && sstr[..] == rm[..] {
let (a, b) = share_at(tot, w, ways, dst);
if a < b {
unsafe { std::ptr::copy_nonoverlapping(src.add(a), dst.add(a), b - a) };
}
return;
}
let rows = shape[d];
let (a, b) = (rows * w / ways, rows * (w + 1) / ways);
if a >= b {
return;
}
let mut part: DimU = shape.iter().copied().collect();
part[d] = b - a;
unsafe {
super::kernels::copy_strided(
dst.offset(a as isize * dstr[d] as isize),
dstr,
src.offset(a as isize * sstr[d] as isize),
sstr,
&part,
)
};
}
pub(in crate::simulate_array::tape) struct AsmPart {
region: u32,
dbase: i64,
src: Rv,
}
unsafe impl Sync for AsmPart {}
unsafe impl Send for AsmPart {}
impl AsmPart {
pub(super) fn new(region: u32, dbase: i64, src: Rv) -> Self {
AsmPart { region, dbase, src }
}
}
pub(super) unsafe fn assemble(
ways: usize,
dst: *mut f64,
shape: &[usize],
regions: &[RegionSpec],
parts: &[AsmPart],
covered: bool,
) {
let dp = SendPtr(dst);
pool::run(ways, &move |w| {
let dp = dp;
unsafe { assemble_share(w, ways, dp.0, shape, regions, parts, covered) };
});
}
pub(super) unsafe fn assemble_share(
w: usize,
ways: usize,
dst: *mut f64,
shape: &[usize],
regions: &[RegionSpec],
parts: &[AsmPart],
covered: bool,
) {
let n: usize = shape.iter().product();
let out_rm = rm_strides(shape);
let sh = AsmShared {
dst: SendPtr(dst),
regions,
parts,
out_rm: &out_rm,
covered,
};
let (lo, hi) = share_at(n, w, ways, dst);
if lo < hi {
unsafe { assemble_window(&sh, lo, hi) };
}
}
struct AsmShared<'a> {
dst: SendPtr,
regions: &'a [RegionSpec],
parts: &'a [AsmPart],
out_rm: &'a [i64],
covered: bool,
}
unsafe impl Sync for AsmShared<'_> {}
unsafe fn assemble_window(sh: &AsmShared, lo: usize, hi: usize) {
let dst = sh.dst.0;
if !sh.covered {
unsafe { std::slice::from_raw_parts_mut(dst.add(lo), hi - lo).fill(0.0) };
}
for part in sh.parts {
let rsh = &sh.regions[part.region as usize].shape;
if rsh.contains(&0) {
continue;
}
let sstr: &[i64] = match &part.src {
Rv::V { strides, .. } => strides,
Rv::S(_) => &[],
};
let axes: DimU = (0..rsh.len()).filter(|&d| rsh[d] > 1).collect();
let db = part.dbase as usize;
let (row_ax, lead) = match axes.split_last() {
Some((&r, lead)) => (Some(r), lead),
None => (None, &[][..]),
};
let (len, ds, ss) = match row_ax {
Some(r) => (
rsh[r],
sh.out_rm[r] as usize,
sstr.get(r).copied().unwrap_or(0) as isize,
),
None => (1, 1, 0),
};
let (i0, i1) = match lead.first() {
None => (0, 1),
Some(&a0) => {
let s0 = sh.out_rm[a0] as usize;
let i1 = if hi > db { (hi - db).div_ceil(s0) } else { 0 };
(lo.saturating_sub(db) / s0, i1.min(rsh[a0]))
}
};
if i0 >= i1 {
continue;
}
let mut idx: DimU = SmallVec::from_elem(0, lead.len());
if let Some(i) = idx.first_mut() {
*i = i0;
}
loop {
let mut f = db;
let mut soff = 0isize;
for (k, &a) in lead.iter().enumerate() {
f += idx[k] * sh.out_rm[a] as usize;
soff += sstr.get(a).copied().unwrap_or(0) as isize * idx[k] as isize;
}
let k0 = if f >= lo { 0 } else { (lo - f).div_ceil(ds) };
let k1 = if hi > f {
(hi - f).div_ceil(ds).min(len)
} else {
0
};
if k0 < k1 {
let out = unsafe { dst.add(f + k0 * ds) };
let n = k1 - k0;
match &part.src {
Rv::S(v) => {
for k in 0..n {
unsafe { *out.add(k * ds) = *v };
}
}
Rv::V { ptr, .. } => {
let p = unsafe { ptr.offset(soff + k0 as isize * ss) };
if ds == 1 && ss == 1 {
unsafe { std::ptr::copy_nonoverlapping(p, out, n) };
} else {
for k in 0..n {
unsafe { *out.add(k * ds) = *p.offset(k as isize * ss) };
}
}
}
}
}
let mut k = lead.len();
let more = loop {
if k == 0 {
break false;
}
k -= 1;
idx[k] += 1;
let end = if k == 0 { i1 } else { rsh[lead[k]] };
if idx[k] < end {
break true;
}
if k == 0 {
break false;
}
idx[k] = 0;
};
if !more {
break;
}
}
}
}
#[derive(Clone, Copy)]
struct SendPtr(*mut f64);
unsafe impl Send for SendPtr {}
unsafe impl Sync for SendPtr {}
pub(super) fn share_at(n: usize, w: usize, ways: usize, at: *const f64) -> (usize, usize) {
let phase = (at as usize / std::mem::size_of::<f64>()) % ALIGN;
let cut = |k: usize| {
if k == ways {
n
} else {
let c = (n as u128 * k as u128 / ways as u128) as usize;
((c + phase) / ALIGN * ALIGN).saturating_sub(phase).min(n)
}
};
(cut(w), cut(w + 1))
}
fn share_aligned(n: usize, w: usize, ways: usize, align: usize) -> (usize, usize) {
let cut = |k: usize| {
if k == ways {
n
} else {
((n as u128 * k as u128 / ways as u128) as usize / align * align).min(n)
}
};
(cut(w), cut(w + 1))
}
#[repr(align(128))]
struct FusedWorker {
fregs: Vec<f64>,
cursor: RunCursor,
acc: Vec<f64>,
}
pub(in crate::simulate_array::tape) struct FusedWorkers {
pub(super) call_ways: usize,
pub(super) threads: usize,
workers: Vec<FusedWorker>,
fregs_len: usize,
depth: usize,
shifted: usize,
scans: usize,
}
#[derive(Clone, Copy)]
struct Shared<'a> {
fs: &'a FusedSpec,
svals: &'a [f64],
bases: &'a [*const f64],
whole_src: &'a [*const f64],
outs: &'a [(GroupIx, *mut f64)],
red: *mut f64,
idx: &'a [Option<IndexTable>],
node_elems: &'a [usize],
simd: SimdLevel,
precision: crate::precision::Precision,
pre: Option<PreTile>,
}
unsafe impl Send for Shared<'_> {}
unsafe impl Sync for Shared<'_> {}
impl FusedWorkers {
pub(super) fn for_program(prog: &TapeProgram) -> Self {
let most = |f: fn(&FusedSpec) -> usize| prog.fused.iter().map(f).max().unwrap_or(0);
FusedWorkers {
call_ways: 1,
threads: 1,
workers: Vec::new(),
fregs_len: most(|f| {
f.n_regs as usize + f.n_load_regs as usize + f.n_splat_regs as usize
}) * super::fused::FCHUNK,
depth: most(|f| f.schedule.depth),
shifted: most(n_shifted),
scans: most(n_scans),
}
}
#[allow(clippy::too_many_arguments)]
pub(super) unsafe fn run_split(
&mut self,
fs: &FusedSpec,
svals: &[f64],
bases: &[*const f64],
whole_src: &[*const f64],
outs: &[(GroupIx, *mut f64)],
red: *mut f64,
idx: &[Option<IndexTable>],
node_elems: &[usize],
simd: SimdLevel,
ways: usize,
pre: Option<PreTile>,
) {
self.grow(ways);
let g = SplitGeom::of(fs, outs, red, ways);
if g.init.is_some() {
for (w, wk) in self.workers[..ways].iter_mut().enumerate() {
let (lo, hi) = g.cut(w);
if wk.acc.len() < hi.saturating_sub(lo) {
wk.acc = padded(hi - lo);
}
}
}
let sh = Shared {
fs,
svals,
bases,
whole_src,
outs,
red,
idx,
node_elems,
simd,
precision: crate::precision::active(),
pre,
};
let ws = WorkersPtr(self.workers.as_mut_ptr());
pool::run(ways, &move |w| {
let (sh, ws) = (sh, ws);
let wk = unsafe { &mut *ws.0.add(w) };
unsafe { fused_share(&sh, wk, &g, w) };
});
}
#[allow(clippy::too_many_arguments)]
pub(super) unsafe fn run_own_share(
&mut self,
fs: &FusedSpec,
svals: &[f64],
bases: &[*const f64],
whole_src: &[*const f64],
outs: &[(GroupIx, *mut f64)],
red: *mut f64,
idx: &[Option<IndexTable>],
node_elems: &[usize],
simd: SimdLevel,
w: usize,
ways: usize,
pre: Option<PreTile>,
) {
self.grow(1);
let g = SplitGeom::of(fs, outs, red, ways);
let (lo, hi) = g.cut(w);
let wk = &mut self.workers[0];
if g.init.is_some() && wk.acc.len() < hi.saturating_sub(lo) {
wk.acc = padded(hi - lo);
}
let sh = Shared {
fs,
svals,
bases,
whole_src,
outs,
red,
idx,
node_elems,
simd,
precision: crate::precision::active(),
pre,
};
unsafe { fused_share(&sh, wk, &g, w) };
}
}
struct SplitGeom {
n: usize,
period: usize,
row: Option<usize>,
anchor: usize,
ways: usize,
init: Option<f64>,
seeded: bool,
}
impl SplitGeom {
fn of(fs: &FusedSpec, outs: &[(GroupIx, *mut f64)], red: *mut f64, ways: usize) -> Self {
let lead = leading_scan_cols(fs);
let (n, period) = match (&fs.reduce, &fs.pre, lead) {
(_, _, Some(cols)) => (cols, cols),
(Some(r), _, _) => (r.n_inner, r.n_inner),
(None, Some(p), _) => (p.n_inner, p.n_inner),
(None, None, None) => (fs.shape.iter().product(), 0),
};
let anchor = match outs.first() {
_ if !red.is_null() => red as usize,
Some(&(_, p)) => p as usize,
None => 0,
};
SplitGeom {
n,
period,
row: if lead.is_some() { None } else { scan_row(fs) },
anchor,
ways,
init: fs.reduce.as_ref().map(|r| r.init),
seeded: seeds_itself(fs),
}
}
fn cut(&self, w: usize) -> (usize, usize) {
match self.row {
Some(row) => share_aligned(self.n, w, self.ways, row),
None => share_at(self.n, w, self.ways, self.anchor as *const f64),
}
}
}
unsafe fn fused_share(sh: &Shared, wk: &mut FusedWorker, g: &SplitGeom, w: usize) {
let (lo, hi) = g.cut(w);
if lo >= hi {
return;
}
let period = g.period;
let _p = crate::precision::enter(sh.precision);
let red = match g.init {
Some(init) => {
let acc = &mut wk.acc[..hi - lo];
if !g.seeded {
acc.fill(init);
}
acc.as_mut_ptr().wrapping_sub(lo)
}
None => sh.red,
};
if let Some(pt) = sh.pre {
unsafe {
run_pre_tiled(
sh.simd,
sh.fs,
pt,
sh.svals,
sh.bases,
sh.whole_src,
sh.outs,
red,
sh.idx,
&mut wk.fregs,
&mut wk.cursor,
sh.node_elems,
lo,
hi,
)
};
return;
}
unsafe {
fill_whole(sh.fs, sh.bases, sh.whole_src, lo, hi, period);
if let Some((rows, len)) = sh.fs.rows_reversed() {
debug_assert_eq!(period, len, "a reverse group splits by columns");
for r in (0..rows).rev() {
let win = Window {
lo: r * len + lo,
hi: r * len + hi,
period: 0,
node_elems: sh.node_elems,
};
run_fused_window(
sh.simd,
sh.fs,
sh.svals,
sh.bases,
sh.outs,
red,
sh.idx,
&mut wk.fregs,
&mut wk.cursor,
win,
);
}
return;
}
run_fused_window(
sh.simd,
sh.fs,
sh.svals,
sh.bases,
sh.outs,
red,
sh.idx,
&mut wk.fregs,
&mut wk.cursor,
Window {
lo,
hi,
period,
node_elems: sh.node_elems,
},
);
if g.init.is_some() {
std::ptr::copy_nonoverlapping(wk.acc.as_ptr(), sh.red.add(lo), hi - lo);
}
}
}
impl FusedWorkers {
fn grow(&mut self, ways: usize) {
while self.workers.len() < ways {
self.workers.push(FusedWorker {
fregs: padded(self.fregs_len),
cursor: RunCursor::for_worker(self.depth, self.shifted, self.scans),
acc: Vec::new(),
});
}
}
pub(super) fn run_with_workers(
&mut self,
ways: usize,
job: &(dyn Fn(usize, &mut [f64], &mut RunCursor) + Sync),
) {
self.grow(ways);
let ws = WorkersPtr(self.workers.as_mut_ptr());
pool::run(ways, &move |w| {
let ws = ws;
let wk = unsafe { &mut *ws.0.add(w) };
job(w, &mut wk.fregs, &mut wk.cursor);
});
}
}
pub(super) fn padded(len: usize) -> Vec<f64> {
let mut v = Vec::with_capacity(len + PAD_BYTES / std::mem::size_of::<f64>());
v.resize(len, 0.0);
v
}
#[derive(Clone, Copy)]
struct WorkersPtr(*mut FusedWorker);
unsafe impl Send for WorkersPtr {}
unsafe impl Sync for WorkersPtr {}
#[cfg(test)]
thread_local! {
static FORCE: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
}
#[cfg(test)]
pub(crate) fn force_split(ways: Option<usize>) {
FORCE.with(|c| c.set(ways));
}