makepad/libs/relax/backend/tests/pair256.rs

404 lines
18 KiB
Rust

//! pair256: V128 halves fused into 256-bit AVX instructions compute bit-for-bit what the halves
//! compute. Each function runs with the pass and without it (`MAKEPAD_RUN_OPT=-pair256`) on the
//! host; the pass must fire (a V256 vreg appears) where the halves pair up.
#![cfg(all(target_arch = "x86_64", unix))]
use relax_backend::{exec::Code, rir::*};
type Entry3 = extern "sysv64" fn(*const u8, *const u8, *mut u8);
type Entry4 = extern "sysv64" fn(*mut u8, *const u8, *mut u8, u64);
fn options(pair: bool) -> relax_session::Options {
let mut o = relax_session::Options::default();
o.cpu = relax_backend::x64::detect_cpu(&o);
if !pair {
o.opt.push("-pair256".into());
}
o
}
fn compile(options: &relax_session::Options, f: &Func) -> Code {
let env = relax_backend::x64::Env { slots: SlotsPtr::NONE, tls_key: 0, tls_slow: 0, resume: 0, opt_regs: true, syms: 0, sym_class: 0, near: false };
let c = relax_backend::x64::compile(options, f, &env).unwrap();
assert!(c.relocs.is_empty());
Code::new(&c.code).unwrap()
}
fn caps() -> Option<relax_backend::pair256::Caps> {
let o = options(true);
o.cpu.avx.then_some(relax_backend::pair256::Caps { avx2: o.cpu.avx2, fma: o.cpu.fma, ints: false })
}
/// the pass fuses `f` (on any host: a CPU with AVX2 and FMA assumed)
fn fires(f: &Func) -> bool {
let mut g = f.clone();
relax_backend::pair256::pair256(&mut g, relax_backend::pair256::Caps { avx2: true, fma: true, ints: false }) && g.vregs.contains(&Cls::V256)
}
#[test]
fn shapes_fuse() {
assert!(fires(&binop(VX::FAdd(VW::S, NanBits::Host))));
assert!(fires(&indexed()));
assert!(fires(&kernel(false)), "kernel without fma");
assert!(fires(&kernel(true)), "kernel with fma");
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
/// an f32 or f64 lane value: specials, small numbers or random bits
fn lane(&mut self, d: bool) -> u64 {
let r = self.next();
let sp32 = [0.0f32, -0.0, 1.0, -1.0, 0.5, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, 1.0e-40];
let sp64 = [0.0f64, -0.0, 1.0, -1.0, 0.5, f64::INFINITY, f64::NEG_INFINITY, f64::MAX, 1.0e-310];
match r % 5 {
0 => self.next() & if d { u64::MAX } else { 0xffff_ffff },
1 if d => [0x7ff8_0000_0000_0000u64, 0xfff4_0000_1234_5678][(r >> 8) as usize % 2],
1 => [0x7fc0_0000u64, 0xff81_2345][(r >> 8) as usize % 2],
2 if d => sp64[(r >> 8) as usize % sp64.len()].to_bits(),
2 => sp32[(r >> 8) as usize % sp32.len()].to_bits() as u64,
_ if d => (((r >> 11) % 2001) as f64 / 100.0 - 10.0).to_bits(),
_ => ((((r >> 11) % 2001) as f32) / 100.0 - 10.0).to_bits() as u64,
}
}
fn bytes(&mut self, d: bool, out: &mut [u8]) {
let w = if d { 8 } else { 4 };
for c in out.chunks_mut(w) {
let x = self.lane(d);
c.copy_from_slice(&x.to_le_bytes()[..w]);
}
}
}
fn push(f: &mut Func, b: usize, i: Inst) {
f.blocks[b].insts.push(i);
f.blocks[b].pos.push(0);
}
/// fn(a, b, d): d[0..32] = op(a[0..32], b[0..32]) as two 16-byte halves
fn binop(op: VX) -> Func {
let mut f = Func::new("pair_op".into(), 0);
let (pa, pb, pd) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I));
f.params = vec![pa, pb, pd];
f.block();
let v: Vec<VReg> = (0..6).map(|_| f.vreg(Cls::V128)).collect();
push(&mut f, 0, Inst::VLoad(v[0], pa, 0));
push(&mut f, 0, Inst::VLoad(v[1], pa, 16));
push(&mut f, 0, Inst::VLoad(v[2], pb, 0));
push(&mut f, 0, Inst::VLoad(v[3], pb, 16));
push(&mut f, 0, Inst::VX(op, v[4], v[0], v[2]));
push(&mut f, 0, Inst::VX(op, v[5], v[1], v[3]));
push(&mut f, 0, Inst::VStore(pd, 0, v[4]));
push(&mut f, 0, Inst::VStore(pd, 16, v[5]));
f.blocks[0].term = Term::Ret(vec![]);
f.verify().unwrap();
f
}
#[test]
fn ops_match_halves() {
let Some(_) = caps() else { return };
let mut ops = vec![VX::And, VX::Or, VX::Xor, VX::AndNot];
for w in [VW::S, VW::D] {
for n in [NanBits::Host, NanBits::Intel] {
ops.extend([VX::FAdd(w, n), VX::FSub(w, n), VX::FMul(w, n), VX::FDiv(w, n)]);
}
ops.extend([VX::FMinLt(w), VX::FMaxGt(w)]);
for c in [Cond::Lt, Cond::Le, Cond::Gt, Cond::Ge, Cond::Eq, Cond::Ne] {
ops.push(VX::FCmp(c, w));
}
}
let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
let (on, off) = (options(true), options(false));
for op in ops {
let f = binop(op);
assert!(fires(&f), "{op:?}: not fused");
let (c1, c2) = (compile(&on, &f), compile(&off, &f));
let (e1, e2): (Entry3, Entry3) = unsafe { (std::mem::transmute(c1.ptr()), std::mem::transmute(c2.ptr())) };
let d = matches!(op, VX::FAdd(VW::D, _) | VX::FSub(VW::D, _) | VX::FMul(VW::D, _) | VX::FDiv(VW::D, _) | VX::FMinLt(VW::D) | VX::FMaxGt(VW::D) | VX::FCmp(_, VW::D));
for _ in 0..300 {
let (mut a, mut b) = ([0u8; 32], [0u8; 32]);
rng.bytes(d, &mut a);
rng.bytes(d, &mut b);
let (mut r1, mut r2) = ([0u8; 32], [0u8; 32]);
e1(a.as_ptr(), b.as_ptr(), r1.as_mut_ptr());
e2(a.as_ptr(), b.as_ptr(), r2.as_mut_ptr());
assert_eq!(r1, r2, "{op:?} a={a:?} b={b:?}");
}
}
}
/// fn(a, b, d, i): d[i*4..][0..32] = a[i*4..][0..32] * b[i*4..][0..32], indexed accesses
fn indexed() -> Func {
let mut f = Func::new("pair_indexed".into(), 0);
let (pa, pb, pd, i) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I));
f.params = vec![pa, pb, pd, i];
f.block();
let v: Vec<VReg> = (0..6).map(|_| f.vreg(Cls::V128)).collect();
let mul = VX::FMul(VW::S, NanBits::Host);
push(&mut f, 0, Inst::VLoadX(v[0], pa, i, 2, 0));
push(&mut f, 0, Inst::VLoadX(v[1], pa, i, 2, 16));
push(&mut f, 0, Inst::VLoadX(v[2], pb, i, 2, 0));
push(&mut f, 0, Inst::VLoadX(v[3], pb, i, 2, 16));
push(&mut f, 0, Inst::VX(mul, v[4], v[0], v[2]));
push(&mut f, 0, Inst::VX(mul, v[5], v[1], v[3]));
push(&mut f, 0, Inst::VStoreX(pd, i, 2, 0, v[4]));
push(&mut f, 0, Inst::VStoreX(pd, i, 2, 16, v[5]));
f.blocks[0].term = Term::Ret(vec![]);
f.verify().unwrap();
f
}
#[test]
fn indexed_matches_halves() {
let Some(_) = caps() else { return };
let f = indexed();
assert!(fires(&f), "indexed: not fused");
let (on, off) = (options(true), options(false));
let (c1, c2) = (compile(&on, &f), compile(&off, &f));
type E = extern "sysv64" fn(*const u8, *const u8, *mut u8, u64);
let (e1, e2): (E, E) = unsafe { (std::mem::transmute(c1.ptr()), std::mem::transmute(c2.ptr())) };
let mut rng = Rng(77);
for i in 0..5u64 {
let (mut a, mut b) = ([0u8; 64], [0u8; 64]);
rng.bytes(false, &mut a);
rng.bytes(false, &mut b);
let (mut r1, mut r2) = ([0u8; 64], [0u8; 64]);
e1(a.as_ptr(), b.as_ptr(), r1.as_mut_ptr(), i);
e2(a.as_ptr(), b.as_ptr(), r2.as_mut_ptr(), i);
assert_eq!(r1, r2, "i={i}");
}
}
/// The piano modal kernel's shape (core::arch `__m256` code as halves): six state/coefficient
/// pairs live across a loop, a splat of a loaded scalar used for both halves, fused multiply-adds,
/// a loop-carried pair through a Mov, a per-iteration load/fma/store of an accumulator row, and an
/// unfused read of a half after the loop.
///
/// fn(st: *mut [f32; 48], inp: *const f32, acc: *mut [[f32; 8]; n], n)
fn kernel(fma: bool) -> Func {
let mut f = Func::new("pair_kernel".into(), 0);
let (st, inp, acc, n) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I));
f.params = vec![st, inp, acc, n];
for _ in 0..4 {
f.block();
}
// zr, zi, cr, ci, gin, gout: two halves each
let h: Vec<[VReg; 2]> = (0..6).map(|_| [f.vreg(Cls::V128), f.vreg(Cls::V128)]).collect();
for (k, p) in h.iter().enumerate() {
push(&mut f, 0, Inst::VLoad(p[0], st, 32 * k as i32));
push(&mut f, 0, Inst::VLoad(p[1], st, 32 * k as i32 + 16));
}
let i = f.vreg(Cls::I);
push(&mut f, 0, Inst::Iconst(i, 0));
f.blocks[0].term = Term::Jump(1);
let c = f.vreg(Cls::I);
push(&mut f, 1, Inst::ICmp(Cond::Lt, false, c, i, n));
f.blocks[1].term = Term::Branch(c, 2, 3);
// body
let (off, a, s, sp) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::F32), f.vreg(Cls::V128));
push(&mut f, 2, Inst::IBinI(IOp::Shl, IntTy { bits: 64, signed: false }, off, i, 2));
push(&mut f, 2, Inst::IBin(IOp::Add, IntTy { bits: 64, signed: false }, a, inp, off));
push(&mut f, 2, Inst::Load(Mem::F32, s, a, 0));
push(&mut f, 2, Inst::VSplat(sp, s));
let (ao, aa) = (f.vreg(Cls::I), f.vreg(Cls::I));
push(&mut f, 2, Inst::IBinI(IOp::Shl, IntTy { bits: 64, signed: false }, ao, i, 5));
push(&mut f, 2, Inst::IBin(IOp::Add, IntTy { bits: 64, signed: false }, aa, acc, ao));
let mul = VX::FMul(VW::S, NanBits::Host);
let mut t = [VReg(0); 2];
for (half, tt) in t.iter_mut().enumerate() {
let [zr, zi, cr, ci, gin, _] = [h[0][half], h[1][half], h[2][half], h[3][half], h[4][half], h[5][half]];
let (m1, m2, sb, t1, m3) = (f.vreg(Cls::V128), f.vreg(Cls::V128), f.vreg(Cls::V128), f.vreg(Cls::V128), f.vreg(Cls::V128));
push(&mut f, 2, Inst::VX(mul, m1, cr, zr));
push(&mut f, 2, Inst::VX(mul, m2, ci, zi));
push(&mut f, 2, Inst::VX(VX::FSub(VW::S, NanBits::Host), sb, m1, m2));
if fma {
push(&mut f, 2, Inst::VFma(false, false, t1, gin, sp, sb));
} else {
let p = f.vreg(Cls::V128);
push(&mut f, 2, Inst::VX(mul, p, gin, sp));
push(&mut f, 2, Inst::VX(VX::FAdd(VW::S, NanBits::Host), t1, p, sb));
}
push(&mut f, 2, Inst::VX(mul, m3, cr, zi));
if fma {
push(&mut f, 2, Inst::VFma(false, true, zi, ci, zr, m3));
} else {
let p = f.vreg(Cls::V128);
push(&mut f, 2, Inst::VX(mul, p, ci, zr));
push(&mut f, 2, Inst::VX(VX::FSub(VW::S, NanBits::Host), zi, p, m3));
}
*tt = t1;
}
for half in 0..2 {
push(&mut f, 2, Inst::Mov(h[0][half], t[half]));
}
for half in 0..2 {
let (av, r) = (f.vreg(Cls::V128), f.vreg(Cls::V128));
push(&mut f, 2, Inst::VLoad(av, aa, 16 * half as i32));
if fma {
push(&mut f, 2, Inst::VFma(false, false, r, h[5][half], h[1][half], av));
} else {
let p = f.vreg(Cls::V128);
push(&mut f, 2, Inst::VX(mul, p, h[5][half], h[1][half]));
push(&mut f, 2, Inst::VX(VX::FAdd(VW::S, NanBits::Host), r, p, av));
}
push(&mut f, 2, Inst::VStore(aa, 16 * half as i32, r));
}
push(&mut f, 2, Inst::IBinI(IOp::Add, IntTy { bits: 64, signed: false }, i, i, 1));
f.blocks[2].term = Term::Jump(1);
// exit: state back, and lane 1 of zr's high half on its own
for k in 0..2 {
push(&mut f, 3, Inst::VStore(st, 32 * k as i32, h[k][0]));
push(&mut f, 3, Inst::VStore(st, 32 * k as i32 + 16, h[k][1]));
}
let x = f.vreg(Cls::F32);
push(&mut f, 3, Inst::VExtract(x, h[0][1], 1));
push(&mut f, 3, Inst::Store(Mem::F32, st, 192, x));
f.blocks[3].term = Term::Ret(vec![]);
f.verify().unwrap();
f
}
#[test]
fn kernel_matches_halves() {
let Some(cp) = caps() else { return };
let mut rng = Rng(0x2545_f491_4f6c_dd1d);
let (on, off) = (options(true), options(false));
for fma in [false, true] {
if fma && !cp.fma {
continue;
}
let f = kernel(fma);
assert!(fires(&f), "kernel (fma {fma}): not fused");
let (c1, c2) = (compile(&on, &f), compile(&off, &f));
let (e1, e2): (Entry4, Entry4) = unsafe { (std::mem::transmute(c1.ptr()), std::mem::transmute(c2.ptr())) };
for n in [0usize, 1, 2, 7, 37] {
let mut st = vec![0u8; 196];
let mut inp = vec![0u8; 4 * n.max(1)];
let mut acc = vec![0u8; 32 * n.max(1)];
for (k, v) in [&mut st[..192], &mut inp[..], &mut acc[..]].into_iter().enumerate() {
for c in v.chunks_mut(4) {
let x = (((rng.next() >> 20) % 2001) as f32 / 1000.0 - 1.0) * if k == 2 { 3.0 } else { 0.99 };
c.copy_from_slice(&x.to_bits().to_le_bytes());
}
}
let (mut st2, inp2, mut acc2) = (st.clone(), inp.clone(), acc.clone());
e1(st.as_mut_ptr(), inp.as_ptr(), acc.as_mut_ptr(), n as u64);
e2(st2.as_mut_ptr(), inp2.as_ptr(), acc2.as_mut_ptr(), n as u64);
assert_eq!(st, st2, "state, n={n} fma={fma}");
assert_eq!(acc, acc2, "acc, n={n} fma={fma}");
}
}
}
/// fn(a, b, d) for the `Caps::ints` forms: the unfused halves (`-pair256`) against the halves
/// fused by `pair256` with `ints` (seeded by `Func::halves`, as compute's eight-lane kernels).
fn int_forms() -> Vec<(String, Func, bool)> {
let mut v = Vec::new();
let mut ops = vec![VX::Add(VW::S), VX::Sub(VW::S), VX::MulLo(VW::S), VX::MinU(VW::S), VX::MaxU(VW::S), VX::MinS(VW::S), VX::MaxS(VW::S), VX::Neg(VW::S)];
ops.extend([VX::ShlV(VW::S), VX::ShrSV(VW::S), VX::ShrUV(VW::S), VX::ZipLo(VW::S), VX::ZipHi(VW::S), VX::ZipLo(VW::D), VX::ZipHi(VW::D)]);
for c in [Cond::Lt, Cond::Le, Cond::Gt, Cond::Ge, Cond::Eq, Cond::Ne] {
ops.push(VX::ICmp(c, VW::S));
}
for k in [0u8, 1, 7, 22, 31] {
ops.extend([VX::ShlI(VW::S, k), VX::ShrSI(VW::S, k), VX::ShrUI(VW::S, k)]);
}
let fl = [VX::FNeg(VW::S), VX::FAbs(VW::S), VX::FSqrt(VW::S, NanBits::Host), VX::IToF(VW::S), VX::FToI(VW::S), VX::FRound(VW::S, RoundTo::Floor), VX::FRound(VW::S, RoundTo::Ceil), VX::FRound(VW::S, RoundTo::Trunc), VX::FRound(VW::S, RoundTo::Away)];
for op in ops {
v.push((format!("{op:?}"), binop(op), false));
}
for op in fl {
v.push((format!("{op:?}"), binop(op), true));
}
// selects by a compare's mask (a byte mask: vpblendvb) and by loaded bits (and / andn / or)
for by_cmp in [true, false] {
let mut f = Func::new("pair_sel".into(), 0);
let (pa, pb, pd) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I));
f.params = vec![pa, pb, pd];
f.block();
let r: Vec<VReg> = (0..8).map(|_| f.vreg(Cls::V128)).collect();
push(&mut f, 0, Inst::VLoad(r[0], pa, 0));
push(&mut f, 0, Inst::VLoad(r[1], pa, 16));
push(&mut f, 0, Inst::VLoad(r[2], pb, 0));
push(&mut f, 0, Inst::VLoad(r[3], pb, 16));
if by_cmp {
push(&mut f, 0, Inst::VX(VX::ICmp(Cond::Gt, VW::S), r[4], r[0], r[2]));
push(&mut f, 0, Inst::VX(VX::ICmp(Cond::Gt, VW::S), r[5], r[1], r[3]));
} else {
push(&mut f, 0, Inst::VX(VX::Xor, r[4], r[0], r[2]));
push(&mut f, 0, Inst::VX(VX::Xor, r[5], r[1], r[3]));
}
push(&mut f, 0, Inst::VSel(r[6], r[4], r[0], r[2]));
push(&mut f, 0, Inst::VSel(r[7], r[5], r[1], r[3]));
push(&mut f, 0, Inst::VStore(pd, 0, r[6]));
push(&mut f, 0, Inst::VStore(pd, 16, r[7]));
f.blocks[0].term = Term::Ret(vec![]);
v.push((format!("VSel by_cmp={by_cmp}"), f, false));
}
// a splat pair (the halves of one broadcast, seeded) added to loaded halves, and a VAny of
// the halves' or
{
let mut f = Func::new("pair_splat".into(), 0);
let (pa, pb, pd) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I));
f.params = vec![pa, pb, pd];
f.block();
let s = f.vreg(Cls::I);
let r: Vec<VReg> = (0..7).map(|_| f.vreg(Cls::V128)).collect();
let any = f.vreg(Cls::I);
push(&mut f, 0, Inst::Load(Mem::Int(4, true), s, pb, 0));
push(&mut f, 0, Inst::VSplat(r[0], s));
push(&mut f, 0, Inst::VSplat(r[1], s));
push(&mut f, 0, Inst::VLoad(r[2], pa, 0));
push(&mut f, 0, Inst::VLoad(r[3], pa, 16));
push(&mut f, 0, Inst::VX(VX::Add(VW::S), r[4], r[2], r[0]));
push(&mut f, 0, Inst::VX(VX::Add(VW::S), r[5], r[3], r[1]));
push(&mut f, 0, Inst::VStore(pd, 0, r[4]));
push(&mut f, 0, Inst::VStore(pd, 16, r[5]));
push(&mut f, 0, Inst::VX(VX::Or, r[6], r[4], r[5]));
push(&mut f, 0, Inst::VAny(any, r[6]));
push(&mut f, 0, Inst::Store(Mem::Int(4, true), pd, 32, any));
f.blocks[0].term = Term::Ret(vec![]);
f.halves = vec![(r[0], r[1]), (r[4], r[5])];
v.push(("VSplat + VAny".into(), f, false));
}
v
}
#[test]
fn int_forms_match_halves() {
let o = options(true);
if !(o.cpu.avx2 && o.cpu.fma) {
return;
}
let off = options(false);
let mut rng = Rng(0x2545_f491_4f6c_dd1d);
let forms = int_forms();
let n = forms.len();
for (name, f, float) in forms {
f.verify().unwrap();
let mut g = f.clone();
assert!(relax_backend::pair256::pair256(&mut g, relax_backend::pair256::Caps { avx2: true, fma: true, ints: true }) && g.vregs.contains(&Cls::V256), "{name}: not fused");
let (c1, c2) = (compile(&off, &g), compile(&off, &f));
let (e1, e2): (Entry3, Entry3) = unsafe { (std::mem::transmute(c1.ptr()), std::mem::transmute(c2.ptr())) };
for k in 0..400 {
let (mut a, mut b) = ([0u8; 32], [0u8; 32]);
if float || k % 3 == 0 {
rng.bytes(false, &mut a);
rng.bytes(false, &mut b);
} else {
for c in a.chunks_mut(8).chain(b.chunks_mut(8)) {
c.copy_from_slice(&rng.next().to_le_bytes());
}
}
let (mut r1, mut r2) = ([0u8; 36], [0u8; 36]);
e1(a.as_ptr(), b.as_ptr(), r1.as_mut_ptr());
e2(a.as_ptr(), b.as_ptr(), r2.as_mut_ptr());
assert_eq!(r1, r2, "{name} a={a:?} b={b:?}");
}
}
eprintln!("{n} int forms fused and equal to their halves");
}