use super::super::taped;
use super::par::force_split;
use super::pool::DISPATCHES;
use crate::{Compiler, ProblemOptions, Rhs, esm_problem};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
fn fixtures() -> Vec<PathBuf> {
let root =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/conformance/scaling/fixtures");
let mut out = Vec::new();
for fam in std::fs::read_dir(&root).expect("the scaling fixtures") {
let fam = fam.unwrap().path();
if fam.is_dir() {
for doc in std::fs::read_dir(&fam).unwrap() {
let doc = doc.unwrap().path();
if doc.extension().is_some_and(|e| e == "esm") {
out.push(doc);
}
}
}
}
out.sort();
assert!(
out.len() >= 20,
"expected the scaling fixtures, found {out:?}"
);
out
}
fn rhs(path: &Path, ways: Option<usize>) -> Vec<f64> {
let opts = ProblemOptions {
compiler: Some(Compiler::Native),
rhs: Rhs::Always,
..Default::default()
};
let prob =
esm_problem(path, (0.0, 1.0), opts).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let c = prob.debug_array_compiled().expect("a right-hand side");
let n = c.state_variable_names().len();
let state: Vec<f64> = (0..n)
.map(|k| 1.0 + 0.1 * ((k as f64) * 0.37).sin())
.collect();
let params = c.debug_resolve_params(&HashMap::new());
let mut scratch = c
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
let mut stats = crate::simulate_array::RhsStats::default();
let mut dy = vec![0.0; n];
force_split(ways);
c.debug_eval_rhs_into(&state, 0.0, ¶ms, &mut dy, &mut scratch, &mut stats);
let first = dy.clone();
c.debug_eval_rhs_into(&state, 0.0, ¶ms, &mut dy, &mut scratch, &mut stats);
force_split(None);
assert!(
first
.iter()
.zip(&dy)
.all(|(a, b)| a.to_bits() == b.to_bits()),
"{}: steady call differs from the first",
path.display()
);
dy
}
#[test]
fn split_calls_are_bit_identical_to_serial() {
for path in fixtures() {
let serial = rhs(&path, Some(1));
for ways in [2, 3, 4, 7] {
let before = DISPATCHES.load(Ordering::Relaxed);
let split = rhs(&path, Some(ways));
assert!(
DISPATCHES.load(Ordering::Relaxed) > before,
"{}: nothing split {ways} wide",
path.display()
);
for (k, (a, b)) in serial.iter().zip(&split).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"{}: dy[{k}] split {ways} wide is {b:?}, serial {a:?}",
path.display()
);
}
}
}
}
#[test]
fn the_regrid_apply_is_one_split_chain() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/conformance/scaling/fixtures/regrid/regrid_N100.esm");
let text = std::fs::read_to_string(&path).expect("scaling fixture reads");
let doc: serde_json::Value = serde_json::from_str(&text).expect("scaling fixture parses");
let compiled = crate::simulate_array::tape::tests::compile(doc);
let (prog, _) = taped(compiled.build_tape(&std::collections::HashSet::new()));
let exec = super::TapeExec::new(&prog);
assert_eq!(exec.seg_chains.len(), 1, "one chain");
let ch = &exec.seg_chains[0];
assert!(ch.fused.is_some(), "the target cells' fused group joins it");
assert!(ch.start < ch.seg, "the pair gather and product join it");
let opts = ProblemOptions {
compiler: Some(Compiler::Native),
rhs: Rhs::Always,
..Default::default()
};
let prob = esm_problem(path.as_path(), (0.0, 1.0), opts).expect("regrid builds");
let c = prob.debug_array_compiled().expect("a right-hand side");
let n = c.state_variable_names().len();
let state: Vec<f64> = (0..n).map(|k| 1.0 + 0.1 * (k as f64).sin()).collect();
let params = c.debug_resolve_params(&HashMap::new());
let mut scratch = c
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
let mut stats = crate::simulate_array::RhsStats::default();
let mut dy = vec![0.0; n];
force_split(Some(4));
c.debug_eval_rhs_into(&state, 0.0, ¶ms, &mut dy, &mut scratch, &mut stats);
let steady = (0..10)
.map(|_| {
let before = DISPATCHES.load(Ordering::Relaxed);
c.debug_eval_rhs_into(&state, 0.0, ¶ms, &mut dy, &mut scratch, &mut stats);
DISPATCHES.load(Ordering::Relaxed) - before
})
.min();
force_split(None);
assert_eq!(steady, Some(1), "one dispatch per steady call");
}
#[test]
fn the_regrid_chain_folds_only_admitted_pairs() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/conformance/scaling/fixtures/regrid/regrid_N100.esm");
let text = std::fs::read_to_string(&path).expect("scaling fixture reads");
let doc: serde_json::Value = serde_json::from_str(&text).expect("scaling fixture parses");
let compiled = crate::simulate_array::tape::tests::compile(doc);
let (prog, _) = taped(compiled.build_tape(&std::collections::HashSet::new()));
let exec = super::TapeExec::new(&prog);
assert!(
exec.seg_chains[0].compact.is_some_and(|p| p.w.is_some()),
"the regrid chain compacts its pairs and weights"
);
let opts = ProblemOptions {
compiler: Some(Compiler::Native),
rhs: Rhs::Always,
..Default::default()
};
let prob = esm_problem(path.as_path(), (0.0, 1.0), opts).expect("regrid builds");
let c = prob.debug_array_compiled().expect("a right-hand side");
let n = c.state_variable_names().len();
let state: Vec<f64> = (0..n).map(|k| 1.0 + 0.1 * (k as f64).sin()).collect();
let atol = c
.parameter_names()
.iter()
.find(|p| p.ends_with("atol"))
.expect("an atol parameter")
.clone();
for ways in [1, 3] {
let mut scratch = c
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
let mut stats = crate::simulate_array::RhsStats::default();
let mut dy = vec![0.0; n];
for a in [1e-12, 0.6, 1e-12] {
let p: HashMap<String, f64> = [(atol.clone(), a)].into_iter().collect();
let (oracle, _) = c.debug_eval_rhs(&state, 0.0, &p, true);
let params = c.debug_resolve_params(&p);
force_split(Some(ways));
for _ in 0..2 {
c.debug_eval_rhs_into(&state, 0.0, ¶ms, &mut dy, &mut scratch, &mut stats);
for (k, (x, y)) in oracle.iter().zip(&dy).enumerate() {
assert_eq!(
x.to_bits(),
y.to_bits(),
"atol {a}, {ways} wide: dy[{k}] is {y:?}, the oracle {x:?}"
);
}
}
force_split(None);
}
}
}
#[test]
fn the_ragged_chain_runs_its_tuples_through_block_buffers() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../tests/conformance/scaling/fixtures/ragged_contraction/ragged_contraction_N1000.esm",
);
let text = std::fs::read_to_string(&path).expect("scaling fixture reads");
let doc: serde_json::Value = serde_json::from_str(&text).expect("scaling fixture parses");
let compiled = crate::simulate_array::tape::tests::compile(doc);
let (prog, _) = taped(compiled.build_tape(&std::collections::HashSet::new()));
let exec = super::TapeExec::new(&prog);
assert_eq!(exec.seg_chains.len(), 1, "one chain");
let ch = &exec.seg_chains[0];
assert!(ch.private, "nothing else reads the chain's tuples");
assert!(
ch.seg - ch.start >= 3,
"the gathers and the arithmetic join the chain"
);
let opts = ProblemOptions {
compiler: Some(Compiler::Native),
rhs: Rhs::Always,
..Default::default()
};
let prob = esm_problem(path.as_path(), (0.0, 1.0), opts).expect("the fixture builds");
let c = prob.debug_array_compiled().expect("a right-hand side");
let n = c.state_variable_names().len();
let state: Vec<f64> = (0..n).map(|k| 1.0 + 0.1 * (k as f64).sin()).collect();
let (oracle, _) = c.debug_eval_rhs(&state, 0.0, &HashMap::new(), true);
let params = c.debug_resolve_params(&HashMap::new());
for ways in [1, 2, 3, 4, 7] {
let mut scratch = c
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
let mut stats = crate::simulate_array::RhsStats::default();
let mut dy = vec![0.0; n];
force_split(Some(ways));
for _ in 0..2 {
c.debug_eval_rhs_into(&state, 0.0, ¶ms, &mut dy, &mut scratch, &mut stats);
for (k, (x, y)) in oracle.iter().zip(&dy).enumerate() {
assert_eq!(
x.to_bits(),
y.to_bits(),
"{ways} wide: dy[{k}] is {y:?}, the oracle {x:?}"
);
}
}
force_split(None);
}
}
fn steady_dispatches(path: &Path, ways: usize) -> usize {
let opts = ProblemOptions {
compiler: Some(Compiler::Native),
rhs: Rhs::Always,
..Default::default()
};
let prob = esm_problem(path, (0.0, 1.0), opts).expect("the fixture builds");
let c = prob.debug_array_compiled().expect("a right-hand side");
let n = c.state_variable_names().len();
let state: Vec<f64> = (0..n).map(|k| 1.0 + 0.1 * (k as f64).sin()).collect();
let params = c.debug_resolve_params(&HashMap::new());
let mut scratch = c
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
let mut stats = crate::simulate_array::RhsStats::default();
let mut dy = vec![0.0; n];
force_split(Some(ways));
c.debug_eval_rhs_into(&state, 0.0, ¶ms, &mut dy, &mut scratch, &mut stats);
let steady = (0..10)
.map(|_| {
let before = DISPATCHES.load(Ordering::Relaxed);
c.debug_eval_rhs_into(&state, 0.0, ¶ms, &mut dy, &mut scratch, &mut stats);
DISPATCHES.load(Ordering::Relaxed) - before
})
.min()
.unwrap_or(usize::MAX);
force_split(None);
steady
}
#[test]
fn a_chemistry_call_forks_once_per_phase() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/conformance/scaling/fixtures/chemistry_grid/chemistry_grid_N1000.esm");
let forks = steady_dispatches(&path, 4);
assert!(
forks <= 6,
"a steady chemistry call made {forks} fork-joins"
);
}
fn try_rhs(path: &Path, ways: usize) -> Option<Vec<f64>> {
let opts = ProblemOptions {
compiler: Some(Compiler::Native),
rhs: Rhs::Always,
..Default::default()
};
let run = || {
let prob = esm_problem(path, (0.0, 1.0), opts).ok()?;
let c = prob.debug_array_compiled()?;
let n = c.state_variable_names().len();
let state: Vec<f64> = (0..n)
.map(|k| 1.0 + 0.1 * ((k as f64) * 0.37).sin())
.collect();
let params = c.debug_resolve_params(&HashMap::new());
let mut scratch = c
.debug_new_scratch_taped()
.expect("the tape lowers every rule");
let mut stats = crate::simulate_array::RhsStats::default();
let mut dy = vec![0.0; n];
force_split(Some(ways));
c.debug_eval_rhs_into(&state, 0.0, ¶ms, &mut dy, &mut scratch, &mut stats);
c.debug_eval_rhs_into(&state, 0.0, ¶ms, &mut dy, &mut scratch, &mut stats);
force_split(None);
Some(dy)
};
let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run))
.ok()
.flatten();
force_split(None);
out
}
#[test]
fn split_calls_match_serial_across_the_corpus() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests");
let mut docs = Vec::new();
let mut stack = vec![root.join("conformance"), root.join("valid")];
while let Some(dir) = stack.pop() {
for e in std::fs::read_dir(&dir).expect("the test corpus") {
let p = e.unwrap().path();
if p.is_dir() {
stack.push(p);
} else if p.extension().is_some_and(|x| x == "esm") {
docs.push(p);
}
}
}
docs.sort();
let mut checked = 0usize;
for path in &docs {
let Some(serial) = try_rhs(path, 1) else {
continue;
};
let split =
try_rhs(path, 3).unwrap_or_else(|| panic!("{}: split call failed", path.display()));
for (k, (a, b)) in serial.iter().zip(&split).enumerate() {
assert!(
a.to_bits() == b.to_bits() || (a.is_nan() && b.is_nan()),
"{}: dy[{k}] split 3 wide is {b:?}, serial {a:?}",
path.display()
);
}
checked += 1;
}
assert!(checked >= 50, "only {checked} documents built");
}