use super::super::*;
use super::ir::*;
use ndarray::ArrayD;
use smallvec::SmallVec;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
mod chain;
mod fused;
mod interp;
mod kernels;
mod lane_fold;
mod lanes;
#[cfg(not(target_arch = "wasm32"))]
mod par;
#[cfg(all(test, not(target_arch = "wasm32")))]
mod par_tests;
#[cfg(all(test, not(target_arch = "wasm32")))]
pub(super) use par::force_split;
#[cfg(not(target_arch = "wasm32"))]
mod plan;
#[cfg(not(target_arch = "wasm32"))]
mod pool;
mod resolve;
#[cfg(test)]
mod simd_tests;
#[cfg(test)]
pub(super) use fused::eval_micro_op;
#[cfg(test)]
pub(super) use fused::fold_fused_op;
use fused::FCHUNK;
use interp::run_range;
#[cfg(not(target_arch = "wasm32"))]
use par::copy_strided as copy_strided_maybe_split;
#[cfg(target_arch = "wasm32")]
unsafe fn copy_strided_maybe_split(
_call: usize,
dst: *mut f64,
dstr: &[i64],
src: *const f64,
sstr: &[i64],
shape: &[usize],
) {
unsafe { kernels::copy_strided(dst, dstr, src, sstr, shape) }
}
fn call_ways(exec: &TapeExec) -> usize {
#[cfg(not(target_arch = "wasm32"))]
{
exec.scratch.fscratch.workers.call_ways
}
#[cfg(target_arch = "wasm32")]
{
let _ = exec;
1
}
}
use resolve::{cm_strides, rm_strides};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum SimdLevel {
Generic,
#[cfg(target_arch = "x86_64")]
Avx2,
#[cfg(target_arch = "x86_64")]
Avx512,
}
pub(crate) fn simd_level() -> SimdLevel {
use std::sync::OnceLock;
static LEVEL: OnceLock<SimdLevel> = OnceLock::new();
*LEVEL.get_or_init(|| {
let off = std::env::var("ESS_TAPE_SIMD_DISABLE")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if off {
return SimdLevel::Generic;
}
let cap = std::env::var("ESS_TAPE_SIMD_LEVEL").unwrap_or_default();
if cap.eq_ignore_ascii_case("generic") {
return SimdLevel::Generic;
}
#[cfg(target_arch = "x86_64")]
{
let allow512 = cap.eq_ignore_ascii_case("avx512");
if allow512
&& std::arch::is_x86_feature_detected!("avx512f")
&& std::arch::is_x86_feature_detected!("avx512vl")
&& std::arch::is_x86_feature_detected!("avx512dq")
&& std::arch::is_x86_feature_detected!("avx512bw")
{
return SimdLevel::Avx512;
}
if std::arch::is_x86_feature_detected!("avx2") {
return SimdLevel::Avx2;
}
}
SimdLevel::Generic
})
}
pub(crate) struct TapeExec {
slab: Vec<f64>,
slot_off: Vec<usize>,
pub(crate) obs: ArrMap,
parked: Vec<Option<(String, ArrayD<f64>)>>,
pending: Vec<(u32, u32)>,
state_rm: Vec<f64>,
mirror: Vec<bool>,
mirror_vars: Vec<u32>,
dy_zero: Vec<(usize, usize)>,
dy_zero_len: usize,
dy_home: Vec<usize>,
dy_home_quiet: Vec<usize>,
plan_full: Vec<bool>,
asm_covered: Vec<bool>,
primed_param_epoch: u64,
primed_forcing_epoch: u64,
scratch: Scratch,
idx_tables: Vec<Vec<Option<fused::IndexTable>>>,
lscratch: lanes::LaneScratch,
exports_active: bool,
pub(crate) n_taped: usize,
serial: bool,
simd: SimdLevel,
#[cfg(not(target_arch = "wasm32"))]
call_work: usize,
#[cfg(not(target_arch = "wasm32"))]
seg_chains: Vec<chain::SegChain>,
#[cfg(not(target_arch = "wasm32"))]
chain_scratch: chain::ChainScratch,
#[cfg(not(target_arch = "wasm32"))]
plan: Option<plan::CallPlan>,
#[cfg(not(target_arch = "wasm32"))]
peers: Vec<Scratch>,
#[cfg(not(target_arch = "wasm32"))]
base_slot_off: Vec<usize>,
}
pub(super) struct Scratch {
fregs: Vec<f64>,
fscratch: fused::FusedScratch,
#[cfg(not(target_arch = "wasm32"))]
asm_parts: Vec<par::AsmPart>,
}
impl TapeExec {
pub(crate) fn new(prog: &TapeProgram) -> Self {
let slab = vec![0.0f64; prog.slab.total_elems];
let slot_off: Vec<usize> = prog
.slots
.iter()
.map(|s| {
if s.storage == u32::MAX {
usize::MAX
} else {
prog.slab.storages[s.storage as usize].offset
}
})
.collect();
let obs: ArrMap = ArrMap::with_capacity_and_hasher(prog.exports.len(), Default::default());
let parked = prog
.exports
.iter()
.map(|(name, slot)| {
let desc = &prog.slots[*slot as usize];
let shape: Vec<usize> = if desc.scalar {
Vec::new()
} else if prog.col_major {
desc.shape.iter().rev().copied().collect()
} else {
desc.shape.to_vec()
};
Some((name.clone(), ArrayD::<f64>::zeros(IxDyn(&shape))))
})
.collect();
let mirror: Vec<bool> = prog
.state_vars
.iter()
.map(|sv| !prog.col_major && !super::layout::order_free(&sv.shape))
.collect();
let n_state: usize = prog
.state_vars
.iter()
.zip(&mirror)
.filter(|(_, m)| **m)
.map(|(sv, _)| sv.flat_offset + sv.shape.iter().product::<usize>().max(1))
.max()
.unwrap_or(0);
let plan_full = prog
.plans
.iter()
.map(|plan| {
plan.shape.iter().enumerate().all(|(d, &extent)| {
let mut segs: Vec<(usize, usize)> =
plan.segs[d].iter().map(|&(o, l, _)| (o, l)).collect();
segs.sort_unstable();
let mut next = 0usize;
for (o, l) in segs {
if o != next {
return false;
}
next = o + l;
}
next == extent
})
})
.collect();
let max_fregs = prog
.fused
.iter()
.map(|f| f.n_regs as usize + f.n_load_regs as usize + f.n_splat_regs as usize)
.max()
.unwrap_or(0);
#[cfg(not(target_arch = "wasm32"))]
let seg_chains = chain::seg_chains(prog, &slot_off);
#[cfg(not(target_arch = "wasm32"))]
let slot_off_base = slot_off.clone();
TapeExec {
slab,
slot_off,
obs,
parked,
pending: Vec::with_capacity(16),
state_rm: vec![0.0f64; n_state],
mirror_vars: (0..mirror.len() as u32)
.filter(|&i| mirror[i as usize])
.collect(),
mirror,
dy_zero: Vec::new(),
dy_zero_len: usize::MAX,
dy_home: Vec::new(),
dy_home_quiet: Vec::new(),
plan_full,
asm_covered: assemblies_covered(prog),
primed_param_epoch: 0,
primed_forcing_epoch: 0,
scratch: Scratch {
fregs: vec![0.0f64; max_fregs * FCHUNK],
fscratch: fused::FusedScratch::for_program(prog),
#[cfg(not(target_arch = "wasm32"))]
asm_parts: Vec::new(),
},
idx_tables: fused::index_tables_for(prog),
lscratch: lanes::LaneScratch::for_program(prog),
exports_active: false,
n_taped: prog.rules.len(),
serial: false,
simd: simd_level(),
#[cfg(not(target_arch = "wasm32"))]
call_work: par::program_work(prog),
#[cfg(not(target_arch = "wasm32"))]
seg_chains,
#[cfg(not(target_arch = "wasm32"))]
chain_scratch: chain::ChainScratch::default(),
#[cfg(not(target_arch = "wasm32"))]
plan: None,
#[cfg(not(target_arch = "wasm32"))]
peers: Vec::new(),
#[cfg(not(target_arch = "wasm32"))]
base_slot_off: slot_off_base,
}
}
}
fn assemblies_covered(prog: &TapeProgram) -> Vec<bool> {
let mut covered = vec![true; prog.assemblies.len()];
for ins in &prog.instrs {
let Instr::Assemble { table, out } = ins else {
continue;
};
let shape = &prog.slots[*out as usize].shape;
let rm = rm_strides(shape);
let mut hit = vec![false; shape.iter().product::<usize>().max(1)];
for (_, region) in &prog.assemblies[*table as usize].parts {
let spec = &prog.regions[*region as usize];
let n: usize = spec.shape.iter().product();
let mut idx: DimU = SmallVec::from_elem(0, spec.shape.len());
for _ in 0..n {
let flat: i64 = (0..idx.len())
.map(|d| rm[d] * (spec.dest_lo[d] + idx[d]) as i64)
.sum();
hit[flat as usize] = true;
for d in (0..idx.len()).rev() {
idx[d] += 1;
if idx[d] < spec.shape[d] {
break;
}
idx[d] = 0;
}
}
}
covered[*table as usize] &= hit.iter().all(|&h| h);
}
covered
}
pub(in crate::simulate_array) struct TapeCtx {
pub(crate) prog: Arc<TapeProgram>,
pub(crate) exec: TapeExec,
param_epoch: u64,
forcing_epoch: u64,
last_params: Option<Vec<f64>>,
}
impl TapeCtx {
pub(in crate::simulate_array) fn new(prog: Arc<TapeProgram>) -> Self {
let exec = TapeExec::new(&prog);
TapeCtx {
prog,
exec,
param_epoch: 0,
forcing_epoch: 1,
last_params: None,
}
}
pub(crate) fn bump_forcing_epoch(&mut self) {
self.forcing_epoch += 1;
}
pub(crate) fn set_serial(&mut self) {
self.exec.serial = true;
}
pub(crate) fn invalidate_sections(&mut self) {
self.last_params = None;
self.exec.primed_param_epoch = 0;
self.exec.primed_forcing_epoch = 0;
}
pub(crate) fn set_exports_active(&mut self, on: bool) {
self.exec.exports_active = on;
}
pub(in crate::simulate_array) fn exported_observeds(&self) -> &ArrMap {
&self.exec.obs
}
}
struct Env<'a> {
prog: &'a TapeProgram,
forcing: ForcingView<'a>,
state: &'a [f64],
state_rm: &'a [f64],
mirror: &'a [bool],
params: &'a [f64],
t: f64,
declared: &'a HashSet<String>,
}
pub(in crate::simulate_array::tape) fn load_forcing(
fr: &ForcingRef,
buffer: &HashMap<String, ArrayD<f64>>,
declared: &HashSet<String>,
reversed: bool,
dst: &mut [f64],
) {
let Some(a) = buffer.get(&fr.name) else {
latch_unbound_read(&fr.name, declared);
dst.fill(f64::NAN);
return;
};
if a.shape() != &fr.shape[..] {
latch_forcing_shape_mismatch(&fr.name, &fr.shape, a.shape());
dst.fill(f64::NAN);
return;
}
if fr.shape.is_empty() {
dst[0] = crate::precision::active().round(a[IxDyn(&[])]);
} else if reversed && !super::layout::order_free(&fr.shape) {
for (d, v) in dst.iter_mut().zip(a.view().reversed_axes().iter()) {
*d = *v;
}
} else if let Some(src) = a.as_slice() {
dst.copy_from_slice(src);
} else {
for (d, v) in dst.iter_mut().zip(a.iter()) {
*d = *v;
}
}
}
#[derive(Clone, Copy)]
pub(in crate::simulate_array) enum ForcingView<'a> {
Map(&'a HashMap<String, ArrayD<f64>>),
Slots {
bufs: &'a [Arc<[f64]>],
slot_of: &'a [u32],
},
}
pub(in crate::simulate_array::tape) fn load_forcing_slot(
fr: &ForcingRef,
src: &[f64],
col_major: bool,
dst: &mut [f64],
) {
if fr.shape.is_empty() {
dst[0] = crate::precision::active().round(src[0]);
} else if col_major || super::layout::order_free(&fr.shape) {
dst.copy_from_slice(src);
} else {
let rm = rm_strides(&fr.shape);
let cm = cm_strides(&fr.shape);
unsafe { kernels::copy_strided(dst.as_mut_ptr(), &rm, src.as_ptr(), &cm, &fr.shape) }
}
}
pub(in crate::simulate_array::tape) fn forcing_len(fr: &ForcingRef) -> usize {
if fr.shape.is_empty() {
1
} else {
fr.shape.iter().product()
}
}
fn run_continuous(env: &Env, range: std::ops::Range<usize>, exec: &mut TapeExec, dy: &mut [f64]) {
#[cfg(not(target_arch = "wasm32"))]
{
let ways = call_ways(exec);
if ways > 1 && !exec.exports_active {
if !exec.plan.as_ref().is_some_and(|p| p.fits(&range, ways)) {
exec.plan = plan::CallPlan::build(
env.prog,
range.clone(),
ways,
&exec.seg_chains,
&exec.dy_home_quiet,
&exec.base_slot_off,
);
if let Some(p) = &exec.plan {
if exec.slab.len() < p.slab_len {
exec.slab.resize(p.slab_len, 0.0);
}
exec.slot_off.clone_from(&p.slot_off);
}
}
if let Some(p) = exec.plan.take() {
plan::run_plan(&p, env, exec, dy);
exec.plan = Some(p);
return;
}
}
}
run_range(env, range, exec, dy);
}
#[cfg(test)]
thread_local! {
static SECTION_PRIMES: std::cell::Cell<(u64, u64)> = const { std::cell::Cell::new((0, 0)) };
}
#[cfg(test)]
pub(crate) fn section_primes() -> (u64, u64) {
SECTION_PRIMES.with(std::cell::Cell::get)
}
fn same_params(last: &[f64], params: &[f64]) -> bool {
last.len() == params.len()
&& last
.iter()
.zip(params)
.all(|(a, b)| a.to_bits() == b.to_bits())
}
fn dy_zero_ranges(prog: &TapeProgram, n: usize) -> Vec<(usize, usize)> {
let cont = prog.section_range(Cadence::Continuous);
let every_call = cont.start..cont.end.min(cont.start + prog.n_rhs as usize);
let conditional = prog.guarded_positions();
let mut written = vec![false; n];
for pc in every_call {
if conditional[pc] {
continue;
}
let write = match &prog.instrs[pc] {
Instr::DyWrite { write } => write,
Instr::Lanes { spec } => {
let ls = &prog.lanes[*spec as usize];
for w in &ls.writes {
if let LaneDst::Dy(pos) = &w.dst {
for l in 0..ls.lanes as usize {
written[pos.at(l) as usize] = true;
}
}
}
continue;
}
_ => continue,
};
let w = &prog.dy_writes[*write as usize];
if let Some(pos) = &w.scatter {
for &p in pos {
written[p] = true;
}
} else if let Some(flat) = w.scalar_flat {
written[flat] = true;
} else {
let sv = &prog.state_vars[w.var as usize];
let shape = &prog.slots[w.slot as usize].shape;
let st = dy_strides(prog, &sv.shape);
let total: usize = shape.iter().product();
let nd = shape.len();
let mut idx: SmallVec<[usize; 4]> = SmallVec::from_elem(0, nd);
for _ in 0..total {
let mut off = sv.flat_offset;
for d in 0..nd {
off += (w.dest_lo[d] + idx[d]) * st[d] as usize;
}
written[off] = true;
let mut d = nd;
while d > 0 {
d -= 1;
idx[d] += 1;
if idx[d] < shape[d] {
break;
}
idx[d] = 0;
}
}
}
}
let mut ranges = Vec::new();
let mut k = 0;
while k < n {
if written[k] {
k += 1;
continue;
}
let start = k;
while k < n && !written[k] {
k += 1;
}
ranges.push((start, k));
}
ranges
}
fn dy_homes(prog: &TapeProgram, n: usize) -> Vec<usize> {
let mut home = vec![usize::MAX; prog.slots.len()];
let tables = prog.tables();
let mut readers = vec![0u32; prog.slots.len()];
let mut defs = vec![0u32; prog.slots.len()];
let mut def_pc = vec![usize::MAX; prog.slots.len()];
for (pc, i) in prog.instrs.iter().enumerate() {
i.for_each_read(&tables, |s| readers[s as usize] += 1);
i.for_each_def(&tables, |s| {
defs[s as usize] += 1;
def_pc[s as usize] = pc;
});
}
let cont = prog.section_range(Cadence::Continuous);
let conditional = prog.guarded_positions();
for pc in cont.clone() {
let Instr::DyWrite { write } = &prog.instrs[pc] else {
continue;
};
let w = &prog.dy_writes[*write as usize];
let s = w.slot as usize;
if conditional[pc] || w.scatter.is_some() || w.scalar_flat.is_some() {
continue;
}
if readers[s] != 1 || defs[s] != 1 || !cont.contains(&def_pc[s]) || conditional[def_pc[s]] {
continue;
}
let Instr::Fused { spec } = &prog.instrs[def_pc[s]] else {
continue;
};
let fs = &prog.fused[*spec as usize];
let desc = &prog.slots[s];
let fits = match &fs.reduce {
Some(r) if r.out as usize == s => desc.elems() == r.n_inner,
_ => fs.outputs.iter().any(|&(_, o)| o as usize == s) && desc.shape[..] == fs.shape[..],
};
if desc.scalar || !fits {
continue;
}
let sv = &prog.state_vars[w.var as usize];
let st = dy_strides(prog, &sv.shape);
let rm = rm_strides(&desc.shape);
let contiguous = (0..desc.shape.len()).all(|d| desc.shape[d] <= 1 || st[d] == rm[d]);
let mut off = sv.flat_offset;
for d in 0..desc.shape.len() {
off += w.dest_lo[d] * st[d] as usize;
}
if contiguous && off + desc.elems() <= n {
home[s] = off;
}
}
home
}
pub(super) const UNSTORED: usize = usize::MAX - 1;
fn quiet_homes(prog: &TapeProgram, home: &[usize]) -> Vec<usize> {
let mut quiet = home.to_vec();
let rhs_end = (prog.n_const + prog.n_segment + prog.n_rhs) as usize;
let cont = prog.section_range(Cadence::Continuous);
let tables = prog.tables();
let mut read_early = vec![false; prog.slots.len()];
for i in prog.instrs.iter().take(rhs_end) {
i.for_each_read(&tables, |s| read_early[s as usize] = true);
}
for pc in cont.start..rhs_end.min(cont.end) {
let Instr::Fused { spec } = &prog.instrs[pc] else {
continue;
};
for &(_, slot) in &prog.fused[*spec as usize].outputs {
let s = slot as usize;
if !read_early[s] && home[s] == usize::MAX {
quiet[s] = UNSTORED;
}
}
}
quiet
}
pub(super) fn dy_strides(prog: &TapeProgram, shape: &[usize]) -> DimI {
if prog.col_major {
rm_strides(shape)
} else {
cm_strides(shape)
}
}
pub(in crate::simulate_array) fn run_tape_call(
ctx: &mut TapeCtx,
call: &RhsCall,
dy: &mut [f64],
stats: &mut RhsStats,
) {
let &RhsCall {
state,
params,
forcing,
t,
declared,
..
} = call;
let forcing = forcing.borrow();
let inputs = CallInputs {
state,
params,
forcing: ForcingView::Map(&forcing),
forcing_epoch: None,
t,
declared,
};
run_tape_inputs(ctx, &inputs, dy, true);
stats.taped_rules += ctx.exec.n_taped;
}
pub(in crate::simulate_array) struct CallInputs<'a> {
pub(in crate::simulate_array) state: &'a [f64],
pub(in crate::simulate_array) params: &'a [f64],
pub(in crate::simulate_array) forcing: ForcingView<'a>,
pub(in crate::simulate_array) forcing_epoch: Option<u64>,
pub(in crate::simulate_array) t: f64,
pub(in crate::simulate_array) declared: &'a HashSet<String>,
}
pub(in crate::simulate_array) fn run_tape_inputs(
ctx: &mut TapeCtx,
inputs: &CallInputs,
dy: &mut [f64],
zero_dy: bool,
) {
let &CallInputs {
state,
params,
forcing,
forcing_epoch,
t,
declared,
} = inputs;
if let Some(epoch) = forcing_epoch {
ctx.forcing_epoch = epoch;
}
match &mut ctx.last_params {
Some(last) if same_params(last, params) => {}
slot => {
let last = slot.get_or_insert_with(Vec::new);
last.clear();
last.extend_from_slice(params);
ctx.param_epoch += 1;
}
}
let (param_epoch, forcing_epoch) = (ctx.param_epoch, ctx.forcing_epoch);
let prog = &*ctx.prog;
let exec = &mut ctx.exec;
#[cfg(not(target_arch = "wasm32"))]
{
let (ways, threads) = if exec.serial {
(1, 1)
} else {
(par::call_ways(exec.call_work), par::thread_budget())
};
exec.scratch.fscratch.workers.call_ways = ways;
exec.scratch.fscratch.workers.threads = threads;
}
if exec.dy_zero_len != dy.len() {
exec.dy_zero = dy_zero_ranges(prog, dy.len());
exec.dy_home = dy_homes(prog, dy.len());
exec.dy_home_quiet = quiet_homes(prog, &exec.dy_home);
exec.dy_zero_len = dy.len();
#[cfg(not(target_arch = "wasm32"))]
{
exec.plan = None;
}
}
if zero_dy {
for &(a, b) in &exec.dy_zero {
dy[a..b].fill(0.0);
}
}
let ways = call_ways(exec);
for &i in &exec.mirror_vars {
let sv = &prog.state_vars[i as usize];
let rm = rm_strides(&sv.shape);
let cm = cm_strides(&sv.shape);
unsafe {
copy_strided_maybe_split(
ways,
exec.state_rm.as_mut_ptr().add(sv.flat_offset),
&rm,
state.as_ptr().add(sv.flat_offset),
&cm,
&sv.shape,
);
}
}
let state_rm = std::mem::take(&mut exec.state_rm);
let mirror = std::mem::take(&mut exec.mirror);
let env = Env {
prog,
forcing,
state,
state_rm: &state_rm,
mirror: &mirror,
params,
t,
declared,
};
let const_end = prog.n_const as usize;
let prime_end = (prog.n_const + prog.n_segment) as usize;
if exec.primed_param_epoch != param_epoch {
exec.primed_param_epoch = param_epoch;
exec.primed_forcing_epoch = forcing_epoch;
#[cfg(test)]
SECTION_PRIMES.with(|c| c.set((c.get().0 + 1, c.get().1)));
run_range(&env, 0..prime_end, exec, dy);
unsafe {
fused::refill_index_tables(&mut exec.idx_tables, exec.slab.as_ptr(), &exec.slot_off)
};
#[cfg(not(target_arch = "wasm32"))]
unsafe {
chain::refill_compact(
&exec.seg_chains,
&mut exec.chain_scratch,
prog,
exec.slab.as_ptr(),
&exec.slot_off,
)
};
} else if exec.primed_forcing_epoch != forcing_epoch {
exec.primed_forcing_epoch = forcing_epoch;
#[cfg(test)]
SECTION_PRIMES.with(|c| c.set((c.get().0, c.get().1 + 1)));
run_range(&env, const_end..prime_end, exec, dy);
unsafe {
fused::refill_index_tables(&mut exec.idx_tables, exec.slab.as_ptr(), &exec.slot_off)
};
#[cfg(not(target_arch = "wasm32"))]
unsafe {
chain::refill_compact(
&exec.seg_chains,
&mut exec.chain_scratch,
prog,
exec.slab.as_ptr(),
&exec.slot_off,
)
};
}
let end = if exec.exports_active {
prog.instrs.len()
} else {
prime_end + prog.n_rhs as usize
};
run_continuous(&env, prime_end..end, exec, dy);
exec.state_rm = state_rm;
exec.mirror = mirror;
}