#![allow(clippy::result_large_err)]
use numrs2::expr::{BinOp, ExprNode, UnaryOp};
use numrs2::prelude::*;
use proptest::prelude::*;
use proptest::arbitrary::any;
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Rng(seed ^ 0x9E37_79B9_7F4A_7C15)
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn below(&mut self, n: usize) -> usize {
(self.next_u64() % n as u64) as usize
}
fn boolean(&mut self) -> bool {
self.next_u64() & 1 == 1
}
}
const POOL: [f64; 16] = [
0.0,
-0.0,
1.0,
-1.0,
0.5,
-0.25,
3.0,
-7.5,
1e-300,
1e300,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NAN,
f64::MIN_POSITIVE,
2.0,
-2.0,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
Contiguous,
Transposed,
MixedLayout,
Broadcast,
}
impl Mode {
fn from_index(i: usize) -> Mode {
match i % 4 {
0 => Mode::Contiguous,
1 => Mode::Transposed,
2 => Mode::MixedLayout,
_ => Mode::Broadcast,
}
}
}
struct Ctx {
rng: Rng,
rows: usize,
cols: usize,
mode: Mode,
}
impl Ctx {
fn value(&mut self) -> f64 {
POOL[self.rng.below(POOL.len())]
}
fn leaf(&mut self) -> Array<f64> {
let (rows, cols) = (self.rows, self.cols);
match self.mode {
Mode::Contiguous => self.contiguous(rows, cols),
Mode::Transposed => self.transposed(rows, cols),
Mode::MixedLayout => {
if self.rng.boolean() {
self.transposed(rows, cols)
} else {
self.contiguous(rows, cols)
}
}
Mode::Broadcast => {
if self.rng.boolean() {
self.contiguous(1, cols)
} else {
self.contiguous(rows, 1)
}
}
}
}
fn contiguous(&mut self, rows: usize, cols: usize) -> Array<f64> {
let data: Vec<f64> = (0..rows * cols).map(|_| self.value()).collect();
Array::from_vec(data).reshape(&[rows, cols])
}
fn transposed(&mut self, rows: usize, cols: usize) -> Array<f64> {
let base = self.contiguous(cols, rows);
base.transpose_axis(0, 1)
}
fn bin_op(&mut self) -> BinOp {
match self.rng.below(4) {
0 => BinOp::Add,
1 => BinOp::Sub,
2 => BinOp::Mul,
_ => BinOp::Div,
}
}
fn unary_op(&mut self) -> UnaryOp {
match self.rng.below(5) {
0 => UnaryOp::Neg,
1 => UnaryOp::Abs,
2 => UnaryOp::Sqrt,
3 => UnaryOp::Exp,
_ => UnaryOp::Ln,
}
}
}
fn build(ctx: &mut Ctx, budget: usize) -> (ExprNode<f64>, Array<f64>) {
if budget == 0 {
let a = ctx.leaf();
let value = a.clone();
return (ExprNode::Leaf(a), value);
}
match ctx.rng.below(6) {
0 => {
let a = ctx.leaf();
let value = a.clone();
(ExprNode::Leaf(a), value)
}
1 => {
let op = ctx.bin_op();
let (ln, lv) = build(ctx, budget - 1);
let (rn, rv) = build(ctx, budget - 1);
let value = match op {
BinOp::Add => &lv + &rv,
BinOp::Sub => &lv - &rv,
BinOp::Mul => &lv * &rv,
BinOp::Div => &lv / &rv,
};
(ExprNode::Binary(op, Box::new(ln), Box::new(rn)), value)
}
2 => {
let op = ctx.bin_op();
let k = ctx.value();
let (n, v) = build(ctx, budget - 1);
let value = match op {
BinOp::Add => &v + k,
BinOp::Sub => &v - k,
BinOp::Mul => &v * k,
BinOp::Div => &v / k,
};
(ExprNode::ScalarRhs(op, Box::new(n), k), value)
}
3 => {
let op = ctx.bin_op();
let k = ctx.value();
let (n, v) = build(ctx, budget - 1);
let value = match op {
BinOp::Add => v.map(|x| k + x),
BinOp::Sub => v.map(|x| k - x),
BinOp::Mul => v.map(|x| k * x),
BinOp::Div => v.map(|x| k / x),
};
(ExprNode::ScalarLhs(op, k, Box::new(n)), value)
}
4 => {
let op = ctx.unary_op();
let (n, v) = build(ctx, budget - 1);
let value = match op {
UnaryOp::Neg => -&v,
UnaryOp::Abs => v.map(f64::abs),
UnaryOp::Sqrt => v.map(f64::sqrt),
UnaryOp::Exp => v.map(f64::exp),
UnaryOp::Ln => v.map(f64::ln),
};
(ExprNode::Unary(op, Box::new(n)), value)
}
_ => {
let (an, av) = build(ctx, budget - 1);
let (bn, bv) = build(ctx, budget - 1);
let (cn, cv) = build(ctx, budget - 1);
let value = &(&av * &bv) + &cv;
(
ExprNode::Fma(Box::new(an), Box::new(bn), Box::new(cn)),
value,
)
}
}
}
#[must_use]
fn assert_values_eq(got: &Array<f64>, want: &Array<f64>, what: &str) -> usize {
assert_eq!(got.shape(), want.shape(), "{what}: shape");
let (g, w) = (got.to_vec(), want.to_vec());
let mut payload_exempt = 0usize;
for (i, (x, y)) in g.iter().zip(w.iter()).enumerate() {
if x.to_bits() == y.to_bits() {
continue;
}
if x.is_nan() && y.is_nan() {
payload_exempt += 1;
continue;
}
assert_eq!(
x.to_bits(),
y.to_bits(),
"{what}: element {i}: fused {x} vs eager {y}"
);
}
payload_exempt
}
#[must_use]
fn assert_values_eq_f32(got: &Array<f32>, want: &Array<f32>, what: &str) -> usize {
assert_eq!(got.shape(), want.shape(), "{what}: shape");
let (g, w) = (got.to_vec(), want.to_vec());
let mut payload_exempt = 0usize;
for (i, (x, y)) in g.iter().zip(w.iter()).enumerate() {
if x.to_bits() == y.to_bits() {
continue;
}
if x.is_nan() && y.is_nan() {
payload_exempt += 1;
continue;
}
assert_eq!(
x.to_bits(),
y.to_bits(),
"{what}: element {i}: fused {x} vs eager {y}"
);
}
payload_exempt
}
fn leaf_arrays(node: &ExprNode<f64>) -> Vec<&Array<f64>> {
let mut out = Vec::new();
fn go<'a>(node: &'a ExprNode<f64>, out: &mut Vec<&'a Array<f64>>) {
match node {
ExprNode::Leaf(a) => out.push(a),
ExprNode::Binary(_, l, r) => {
go(l, out);
go(r, out);
}
ExprNode::ScalarRhs(_, e, _) | ExprNode::ScalarLhs(_, _, e) => go(e, out),
ExprNode::Unary(_, e) => go(e, out),
ExprNode::Fma(a, b, c) => {
go(a, out);
go(b, out);
go(c, out);
}
}
}
go(node, &mut out);
out
}
fn should_fuse(node: &ExprNode<f64>) -> bool {
fn operands_ok(node: &ExprNode<f64>) -> bool {
let leaves = leaf_arrays(node);
match leaves.first() {
None => false,
Some(first) => {
let shape = first.shape();
leaves
.iter()
.all(|l| l.shape() == shape && l.as_slice().is_some())
}
}
}
fn is_leaf(node: &ExprNode<f64>) -> bool {
matches!(node, ExprNode::Leaf(_))
}
fn shape_ok(node: &ExprNode<f64>) -> bool {
match node {
ExprNode::Leaf(_) => true,
ExprNode::Binary(_, l, r) => {
(is_leaf(l) && is_leaf(r))
|| (matches!(&**l, ExprNode::Binary(_, a, b) if is_leaf(a) && is_leaf(b))
&& is_leaf(r))
|| (is_leaf(l)
&& matches!(&**r, ExprNode::Binary(_, a, b) if is_leaf(a) && is_leaf(b)))
|| (matches!(&**l, ExprNode::ScalarRhs(_, e, _) if is_leaf(e)) && is_leaf(r))
|| (is_leaf(l) && matches!(&**r, ExprNode::ScalarRhs(_, e, _) if is_leaf(e)))
|| matches!(
(&**l, &**r),
(ExprNode::Binary(_, a, b), ExprNode::Binary(_, c, d))
if is_leaf(a) && is_leaf(b) && is_leaf(c) && is_leaf(d)
)
}
ExprNode::Fma(a, b, c) => is_leaf(a) && is_leaf(b) && is_leaf(c),
ExprNode::ScalarRhs(_, e, _) | ExprNode::ScalarLhs(_, _, e) => is_leaf(e),
ExprNode::Unary(_, e) => is_leaf(e),
}
}
operands_ok(node) && shape_ok(node)
}
fn check_one(
seed: u64,
rows: usize,
cols: usize,
mode: Mode,
depth: usize,
) -> Result<(bool, usize)> {
let mut ctx = Ctx {
rng: Rng::new(seed),
rows,
cols,
mode,
};
let (node, eager) = build(&mut ctx, depth);
let what = format!(
"seed={seed} mode={mode:?} shape=[{rows},{cols}] depth<={}",
depth + 1
);
let fused_path = node.will_fuse();
assert_eq!(
fused_path,
should_fuse(&node),
"{what}: will_fuse() disagrees with the documented precondition"
);
let got = node.eval()?;
let payload_exempt = assert_values_eq(&got, &eager, &what);
Ok((fused_path, payload_exempt))
}
fn to_f32(node: &ExprNode<f64>) -> ExprNode<f32> {
match node {
ExprNode::Leaf(a) => ExprNode::Leaf(a.map(|x| x as f32)),
ExprNode::Binary(op, l, r) => {
ExprNode::Binary(*op, Box::new(to_f32(l)), Box::new(to_f32(r)))
}
ExprNode::ScalarRhs(op, e, s) => ExprNode::ScalarRhs(*op, Box::new(to_f32(e)), *s as f32),
ExprNode::ScalarLhs(op, s, e) => ExprNode::ScalarLhs(*op, *s as f32, Box::new(to_f32(e))),
ExprNode::Unary(op, e) => ExprNode::Unary(*op, Box::new(to_f32(e))),
ExprNode::Fma(a, b, c) => ExprNode::Fma(
Box::new(to_f32(a)),
Box::new(to_f32(b)),
Box::new(to_f32(c)),
),
}
}
fn eager_ref_f32(node: &ExprNode<f32>) -> Array<f32> {
match node {
ExprNode::Leaf(a) => a.clone(),
ExprNode::Binary(op, l, r) => {
let (lv, rv) = (eager_ref_f32(l), eager_ref_f32(r));
match op {
BinOp::Add => &lv + &rv,
BinOp::Sub => &lv - &rv,
BinOp::Mul => &lv * &rv,
BinOp::Div => &lv / &rv,
}
}
ExprNode::ScalarRhs(op, e, k) => {
let v = eager_ref_f32(e);
match op {
BinOp::Add => &v + *k,
BinOp::Sub => &v - *k,
BinOp::Mul => &v * *k,
BinOp::Div => &v / *k,
}
}
ExprNode::ScalarLhs(op, k, e) => {
let (v, k) = (eager_ref_f32(e), *k);
match op {
BinOp::Add => v.map(|x| k + x),
BinOp::Sub => v.map(|x| k - x),
BinOp::Mul => v.map(|x| k * x),
BinOp::Div => v.map(|x| k / x),
}
}
ExprNode::Unary(op, e) => {
let v = eager_ref_f32(e);
match op {
UnaryOp::Neg => -&v,
UnaryOp::Abs => v.map(f32::abs),
UnaryOp::Sqrt => v.map(f32::sqrt),
UnaryOp::Exp => v.map(f32::exp),
UnaryOp::Ln => v.map(f32::ln),
}
}
ExprNode::Fma(a, b, c) => {
let (av, bv, cv) = (eager_ref_f32(a), eager_ref_f32(b), eager_ref_f32(c));
&(&av * &bv) + &cv
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(2048))]
#[test]
fn fused_eval_matches_eager_bitwise(
seed in any::<u64>(),
rows in 1usize..=5,
cols in 1usize..=7,
mode_idx in 0usize..4,
depth in 0usize..=3,
) {
let mode = Mode::from_index(mode_idx);
let (fused, _payload_exempt) = check_one(seed, rows, cols, mode, depth)
.expect("every generated tree has broadcast-compatible leaves");
if mode == Mode::Transposed && rows > 1 && cols > 1 {
prop_assert!(!fused, "a transposed leaf can never fuse");
}
}
#[test]
fn fused_eval_matches_eager_bitwise_f32(
seed in any::<u64>(),
rows in 1usize..=4,
cols in 1usize..=6,
mode_idx in 0usize..4,
depth in 0usize..=3,
) {
let mode = Mode::from_index(mode_idx);
let mut ctx = Ctx { rng: Rng::new(seed), rows, cols, mode };
let (node64, _) = build(&mut ctx, depth);
let node = to_f32(&node64);
let want = eager_ref_f32(&node);
let got = node.eval().expect("broadcast-compatible leaves");
let _payload_exempt = assert_values_eq_f32(
&got,
&want,
&format!("f32 seed={seed} mode={mode:?} shape=[{rows},{cols}]"),
);
}
}
#[test]
fn varied_size_sweep_matches_eager() -> Result<()> {
let mut fused_seen = 0usize;
let mut total = 0usize;
let mut payload_exempt = 0usize;
for &cols in &[1usize, 1023, 1024, 1025, 2049] {
for seed in 0u64..24 {
total += 1;
let (fused, exempt) = check_one(seed, 1, cols, Mode::Contiguous, 3)?;
payload_exempt += exempt;
if fused {
fused_seen += 1;
}
}
}
println!("varied-size sweep: NaN-payload exemptions taken: {payload_exempt}");
assert!(
fused_seen * 4 >= total,
"expected the contiguous sweep to reach the fused path often, got {fused_seen}/{total}"
);
Ok(())
}
#[test]
fn fallback_modes_match_eager_and_reach_the_fallback() -> Result<()> {
for mode in [Mode::Transposed, Mode::MixedLayout, Mode::Broadcast] {
let mut fell_back = 0usize;
let mut total = 0usize;
let mut payload_exempt = 0usize;
for seed in 0u64..300 {
let (fused, exempt) = check_one(seed, 3, 5, mode, 3)?;
payload_exempt += exempt;
total += 1;
if !fused {
fell_back += 1;
}
}
println!("{mode:?}: NaN-payload exemptions taken: {payload_exempt}");
match mode {
Mode::Transposed => assert_eq!(
fell_back, total,
"{mode:?}: every tree at [3,5] must take the eager fallback"
),
_ => assert!(
fell_back * 5 >= total,
"{mode:?}: expected the fallback to be reached in bulk, got {fell_back}/{total}"
),
}
}
Ok(())
}
#[test]
fn depth_four_trees_match_eager() -> Result<()> {
let mut payload_exempt = 0usize;
for mode_idx in 0..4 {
let mode = Mode::from_index(mode_idx);
for seed in 1000u64..1200 {
let (_fused, exempt) = check_one(seed, 4, 6, mode, 3)?;
payload_exempt += exempt;
}
}
println!("depth-4 sweep: NaN-payload exemptions taken: {payload_exempt}");
Ok(())
}
#[test]
fn fuse_fma_rewrite_preserves_values_bitwise() -> Result<()> {
let mut payload_exempt = 0usize;
for mode_idx in 0..4 {
let mode = Mode::from_index(mode_idx);
for seed in 5000u64..5200 {
let mut ctx = Ctx {
rng: Rng::new(seed),
rows: 3,
cols: 7,
mode,
};
let (node, eager) = build(&mut ctx, 3);
let plain = node.clone().eval()?;
let rewritten = node.fuse_fma().eval()?;
payload_exempt +=
assert_values_eq(&plain, &eager, &format!("plain seed={seed} mode={mode:?}"));
payload_exempt += assert_values_eq(
&rewritten,
&eager,
&format!("fuse_fma seed={seed} mode={mode:?}"),
);
}
}
println!("fuse_fma sweep: NaN-payload exemptions taken: {payload_exempt}");
Ok(())
}
#[test]
fn canonical_chain_matches_eager_at_scale() -> Result<()> {
for n in [1usize, 999, 1024, 4096, 100_000] {
let a = Array::from_vec((0..n).map(|i| i as f64 * 0.5 - 3.0).collect());
let b = Array::from_vec((0..n).map(|i| i as f64 * -0.125 + 1.0).collect());
let c = Array::from_vec((0..n).map(|i| 1.0 / (i as f64 + 0.5)).collect());
let e = a.expr() + b.expr() * c.expr();
assert!(e.will_fuse(), "n={n}");
let payload_exempt =
assert_values_eq(&e.eval()?, &(&a + &(&b * &c)), &format!("a+b*c n={n}"));
assert_eq!(
payload_exempt, 0,
"n={n}: finite data must compare bit for bit with no exemption"
);
}
Ok(())
}
#[test]
fn tree_construction_shares_storage_at_every_size() {
for n in [1usize, 1_000, 1_000_000] {
let a = Array::from_vec(vec![1.5_f64; n]);
let b = Array::from_vec(vec![2.5_f64; n]);
assert!(
a.is_unique() && b.is_unique(),
"n={n}: fresh arrays are unique"
);
let tree = a.expr() + b.expr() * a.expr();
assert_eq!(tree.leaf_count(), 3);
assert!(
!a.is_unique(),
"n={n}: leaf must share `a`'s buffer, not copy it"
);
assert!(
!b.is_unique(),
"n={n}: leaf must share `b`'s buffer, not copy it"
);
drop(tree);
assert!(a.is_unique() && b.is_unique(), "n={n}: buffers released");
}
}
#[test]
fn nan_payload_case_that_used_to_diverge() -> Result<()> {
let neg_nan = f64::from_bits(0xfff8_0000_0000_0000);
assert!(neg_nan.is_nan() && neg_nan.is_sign_negative());
let mut payload_exempt = 0usize;
for n in [1usize, 2, 4, 12, 1024, 5000] {
let a = Array::from_vec(vec![f64::NAN; n]);
let b = Array::from_vec(vec![f64::INFINITY; n]);
let c = Array::from_vec(vec![neg_nan; n]);
let node = ExprNode::Fma(
Box::new(ExprNode::ScalarRhs(BinOp::Add, Box::new(a.expr()), 0.0)),
Box::new(b.expr()),
Box::new(c.expr()),
);
let got = node.eval()?;
let want = &(&(&a + 0.0) * &b) + &c;
payload_exempt += assert_values_eq(&got, &want, &format!("historic NaN case, n={n}"));
let fused_shape = a.expr() * b.expr() + c.expr();
assert!(
fused_shape.will_fuse(),
"n={n}: (a*b)+c is a specialised shape"
);
payload_exempt += assert_values_eq(
&fused_shape.eval()?,
&(&(&a * &b) + &c),
&format!("fused NaN case, n={n}"),
);
}
println!("historic NaN case: NaN-payload exemptions taken: {payload_exempt}");
Ok(())
}
#[test]
fn two_distinct_nans_into_one_add_may_differ_in_payload() -> Result<()> {
for n in [1usize, 2, 5, 10, 11, 64, 1024, 5000] {
let a = Array::from_vec(vec![0.0_f64; n]);
let b = Array::from_vec(vec![f64::INFINITY; n]);
let c = Array::from_vec(vec![f64::NAN; n]);
let node = ExprNode::Fma(Box::new(a.expr()), Box::new(b.expr()), Box::new(c.expr()));
assert!(
node.will_fuse(),
"n={n}: Fma(Leaf, Leaf, Leaf) over contiguous same-shape leaves must fuse"
);
let fused = node.eval()?;
let eager = &(&a * &b) + &c;
assert_eq!(fused.shape(), eager.shape(), "n={n}: shape");
let (f, e) = (fused.to_vec(), eager.to_vec());
for (i, (g, w)) in f.iter().zip(e.iter()).enumerate() {
assert!(g.is_nan(), "n={n}: fused element {i} is {g}, expected NaN");
assert!(w.is_nan(), "n={n}: eager element {i} is {w}, expected NaN");
}
let payload_diffs = f
.iter()
.zip(e.iter())
.filter(|(g, w)| g.to_bits() != w.to_bits())
.count();
println!("(0*inf)+NaN, n={n}: all NaN; payload/sign differs in {payload_diffs}/{n}");
}
Ok(())
}
#[test]
fn strict_bitwise_sweep_covers_nan_results_too() -> Result<()> {
let mut compared = 0usize;
let mut nan_results = 0usize;
let mut payload_exempt = 0usize;
for mode_idx in 0..4 {
let mode = Mode::from_index(mode_idx);
for seed in 0u64..150 {
let mut ctx = Ctx {
rng: Rng::new(seed),
rows: 3,
cols: 5,
mode,
};
let (node, eager) = build(&mut ctx, 3);
let got = node.eval()?;
assert_eq!(got.shape(), eager.shape(), "seed={seed} mode={mode:?}");
for (i, (g, w)) in got.to_vec().iter().zip(eager.to_vec().iter()).enumerate() {
if g.to_bits() != w.to_bits() {
if g.is_nan() && w.is_nan() {
payload_exempt += 1;
} else {
assert_eq!(
g.to_bits(),
w.to_bits(),
"seed={seed} mode={mode:?} element {i}: {g} vs {w}"
);
}
}
compared += 1;
if g.is_nan() {
nan_results += 1;
}
}
}
}
println!(
"bitwise comparisons: {compared}, of which NaN results: {nan_results}, \
of which took the NaN-payload exemption: {payload_exempt}"
);
assert!(
compared > 2_000,
"expected a substantial strict sweep, got {compared}"
);
assert!(
nan_results > 100,
"the pool is meant to be adversarial; only {nan_results} NaN results"
);
Ok(())
}