use super::*;
const SIGN: u64 = 1 << 63;
pub(super) const STRIP: usize = 32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub(super) enum FoldOp {
Add = 0,
Sub = 1,
Mul = 2,
Div = 3,
}
impl FoldOp {
fn of(op: BinCode) -> Option<Self> {
match op {
BinCode::Add => Some(FoldOp::Add),
BinCode::Sub => Some(FoldOp::Sub),
BinCode::Mul => Some(FoldOp::Mul),
BinCode::Div => Some(FoldOp::Div),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub(super) struct Term {
pub src: MRef,
pub neg: bool,
}
#[derive(Clone, Copy, Debug)]
pub(super) enum LOut {
Reg(GroupIx),
Dy(u32),
}
#[derive(Clone, Debug)]
pub(super) enum LOp {
Fold {
op: FoldOp,
terms: (u32, u32),
out: LOut,
},
Other(MicroOp),
}
#[derive(Clone, Debug)]
pub(super) struct LaneCode {
pub ops: Vec<LOp>,
pub terms: Vec<Term>,
pub n_regs: usize,
pub writes: Vec<Option<MRef>>,
pub strips: Strips,
}
pub(super) const SSTRIDE: usize = STRIP + 8;
pub(super) const B_STATE: usize = 0;
pub(super) const B_PARAM: usize = 1;
pub(super) const B_REG: usize = 2;
pub(super) const B_SPLAT: usize = 3;
pub(super) const B_DY: usize = 4;
pub(super) const N_BASES: usize = 5;
#[derive(Clone, Debug)]
pub(super) struct Strips {
pub sops: Vec<SOp>,
pub tptr: Vec<TPtr>,
pub inputs: Vec<TPtr>,
pub gathered: Vec<u32>,
pub ends: [usize; N_BASES],
pub reg_elems: usize,
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub dy_phase: Option<usize>,
}
fn operands_mut(op: &mut MicroOp, mut f: impl FnMut(&mut MRef)) {
match op {
MicroOp::Bin { a, b, .. } => {
f(a);
f(b);
}
MicroOp::Un { a, .. } | MicroOp::Neg { a, .. } | MicroOp::Mov { a, .. } => f(a),
MicroOp::Select { cond, a, b, .. } => {
f(cond);
f(a);
f(b);
}
MicroOp::Bin2 { .. }
| MicroOp::Bin3 { .. }
| MicroOp::Scan { .. }
| MicroOp::Load { .. } => {
unreachable!("a lane program holds no superops, scans or loads")
}
}
}
fn out_mut(op: &mut MicroOp) -> &mut GroupIx {
match op {
MicroOp::Bin { out, .. }
| MicroOp::Un { out, .. }
| MicroOp::Neg { out, .. }
| MicroOp::Mov { out, .. }
| MicroOp::Select { out, .. }
| MicroOp::Bin2 { out, .. }
| MicroOp::Bin3 { out, .. }
| MicroOp::Load { out, .. }
| MicroOp::Scan { out, .. } => out,
}
}
enum Node {
Fold {
op: Option<FoldOp>,
terms: Vec<Term>,
},
Other(MicroOp),
Dead,
}
pub(super) fn decode(ls: &LaneSpec) -> Option<LaneCode> {
let n = ls.micro.len();
if n >= GroupIx::MAX as usize {
return None;
}
let mut cur = vec![GroupIx::MAX; ls.n_regs as usize];
let ssa_ref = |m: &mut MRef, cur: &[GroupIx]| {
if let MRef::Reg(r) = m {
*r = cur[*r as usize];
}
};
let mut ssa: Vec<MicroOp> = Vec::with_capacity(n);
for (k, op) in ls.micro.iter().enumerate() {
let mut op = op.clone();
operands_mut(&mut op, |m| ssa_ref(m, &cur));
let o = out_mut(&mut op);
cur[*o as usize] = k as GroupIx;
*o = k as GroupIx;
ssa.push(op);
}
let wsrc: Vec<MRef> = ls
.writes
.iter()
.map(|w| {
let mut m = w.src;
ssa_ref(&mut m, &cur);
m
})
.collect();
let mut uses = vec![0u32; n];
for op in &mut ssa {
operands_mut(op, |m| {
if let MRef::Reg(r) = m {
uses[*r as usize] += 1;
}
});
}
for m in &wsrc {
if let MRef::Reg(r) = m {
uses[*r as usize] += 1;
}
}
let mut nodes: Vec<Node> = Vec::with_capacity(n);
for op in &ssa {
let node = match op {
MicroOp::Neg { a, .. } => Node::Fold {
op: None,
terms: vec![term(*a, true, &mut nodes, &mut uses)],
},
MicroOp::Mov { a, .. } => Node::Fold {
op: None,
terms: vec![term(*a, false, &mut nodes, &mut uses)],
},
MicroOp::Bin { op, a, b, .. } if FoldOp::of(*op).is_some() => {
let f = FoldOp::of(*op);
let mut terms = match *a {
MRef::Reg(t)
if uses[t as usize] == 1
&& matches!(&nodes[t as usize],
Node::Fold { op: o, .. } if o.is_none() || *o == f) =>
{
let Node::Fold { terms, .. } =
std::mem::replace(&mut nodes[t as usize], Node::Dead)
else {
unreachable!()
};
uses[t as usize] = 0;
terms
}
a => vec![term(a, false, &mut nodes, &mut uses)],
};
terms.push(term(*b, false, &mut nodes, &mut uses));
Node::Fold { op: f, terms }
}
other => Node::Other(other.clone()),
};
nodes.push(node);
}
let mut direct: Vec<Option<u32>> = vec![None; n];
let mut writes: Vec<Option<MRef>> = Vec::with_capacity(wsrc.len());
for (k, (w, src)) in ls.writes.iter().zip(&wsrc).enumerate() {
if let MRef::Reg(v) = *src
&& uses[v as usize] == 1
&& matches!(nodes[v as usize], Node::Fold { .. })
&& matches!(w.dst, LaneDst::Dy(LaneIx::Affine { step: 1, .. }))
{
direct[v as usize] = Some(k as u32);
writes.push(None);
} else {
writes.push(Some(*src));
}
}
let order: Vec<usize> = (0..n)
.filter(|&k| uses[k] > 0 && !matches!(nodes[k], Node::Dead))
.collect();
let mut last = vec![0usize; n];
for (p, &k) in order.iter().enumerate() {
reads(&mut nodes[k], &mut |m| {
if let MRef::Reg(v) = m {
last[*v as usize] = p;
}
});
}
for m in writes.iter().flatten() {
if let MRef::Reg(v) = m {
last[*v as usize] = usize::MAX;
}
}
let mut phys = vec![GroupIx::MAX; n];
let mut free: Vec<GroupIx> = Vec::new();
let mut n_regs: usize = 0;
let mut code = LaneCode {
ops: Vec::with_capacity(order.len()),
terms: Vec::new(),
n_regs: 0,
writes: Vec::new(),
strips: Strips {
sops: Vec::new(),
tptr: Vec::new(),
inputs: Vec::new(),
gathered: Vec::new(),
ends: [0; N_BASES],
reg_elems: 0,
dy_phase: None,
},
};
for (p, &k) in order.iter().enumerate() {
let out = match direct[k] {
Some(w) => LOut::Dy(w),
None => {
let r = free.pop().unwrap_or_else(|| {
n_regs += 1;
(n_regs - 1) as GroupIx
});
phys[k] = r;
LOut::Reg(r)
}
};
let mut dying: smallvec::SmallVec<[GroupIx; 4]> = smallvec::SmallVec::new();
reads(&mut nodes[k], &mut |m| {
if let MRef::Reg(v) = m {
let v0 = *v as usize;
*v = phys[v0];
if last[v0] == p && !dying.contains(v) {
dying.push(*v);
}
}
});
free.extend(dying);
match std::mem::replace(&mut nodes[k], Node::Dead) {
Node::Fold { op, terms } => {
let lo = code.terms.len() as u32;
code.terms.extend(terms);
code.ops.push(LOp::Fold {
op: op.unwrap_or(FoldOp::Add),
terms: (lo, code.terms.len() as u32),
out,
});
}
Node::Other(mut op) => {
let LOut::Reg(r) = out else {
unreachable!("only a fold stores into dy")
};
*out_mut(&mut op) = r;
code.ops.push(LOp::Other(op));
}
Node::Dead => unreachable!("dead values are not ordered"),
}
}
code.writes = writes
.into_iter()
.map(|m| {
m.map(|m| match m {
MRef::Reg(v) => MRef::Reg(phys[v as usize]),
other => other,
})
})
.collect();
code.n_regs = n_regs;
code.strips = strips_of(ls, &code);
Some(code)
}
fn strips_of(ls: &LaneSpec, code: &LaneCode) -> Strips {
let n_regs = code.n_regs;
let lanes = ls.lanes as usize;
let mut ends = [0usize; N_BASES];
let mut gathered = Vec::new();
let inputs: Vec<TPtr> = ls
.inputs
.iter()
.enumerate()
.map(|(i, inp)| match (inp.kind, &inp.ix) {
(LaneKind::State | LaneKind::Param, LaneIx::Affine { base, step: 1 }) => {
let b = if inp.kind == LaneKind::State {
B_STATE
} else {
B_PARAM
};
ends[b] = ends[b].max(*base as usize + lanes);
TPtr::at(b, *base as usize, false, true)
}
_ => {
gathered.push(i as u32);
TPtr::at(B_REG, (n_regs + i) * SSTRIDE, false, false)
}
})
.collect();
let tptr = code
.terms
.iter()
.map(|t| match t.src {
MRef::Reg(r) => TPtr::at(B_REG, r as usize * SSTRIDE, t.neg, false),
MRef::In(i) => inputs[i as usize].negated(t.neg),
MRef::Scal(s) => TPtr::at(B_SPLAT, s as usize * STRIP, t.neg, false),
})
.collect();
let sops = code
.ops
.iter()
.enumerate()
.map(|(k, op)| match op {
LOp::Fold { op, terms, out } => match out {
LOut::Reg(r) => SOp::fold(*op, *terms, B_REG, *r as usize * SSTRIDE, false),
LOut::Dy(w) => {
let LaneDst::Dy(LaneIx::Affine { base, .. }) = &ls.writes[*w as usize].dst
else {
unreachable!("a fold stores into a unit-step dy run")
};
ends[B_DY] = ends[B_DY].max(*base as usize + lanes);
SOp::fold(*op, *terms, B_DY, *base as usize, true)
}
},
LOp::Other(m) => SOp {
n: k as u32,
off: super::super::fuse::micro_out(m) as usize * SSTRIDE,
..SOp::NULL
},
})
.collect();
let mut phases = [0usize; 8];
for w in &ls.writes {
if let LaneDst::Dy(LaneIx::Affine { base, step: 1 }) = w.dst {
phases[base as usize % 8] += 1;
}
}
let dy_phase = (0..8)
.max_by_key(|&p| (phases[p], std::cmp::Reverse(p)))
.filter(|&p| phases[p] > 0);
Strips {
dy_phase,
sops,
tptr,
inputs,
gathered,
ends,
reg_elems: (n_regs + ls.inputs.len()) * SSTRIDE,
}
}
fn reads(node: &mut Node, f: &mut dyn FnMut(&mut MRef)) {
match node {
Node::Fold { terms, .. } => terms.iter_mut().for_each(|t| f(&mut t.src)),
Node::Other(op) => operands_mut(op, f),
Node::Dead => {}
}
}
fn term(m: MRef, neg: bool, nodes: &mut [Node], uses: &mut [u32]) -> Term {
if let MRef::Reg(t) = m
&& let Node::Fold { op: None, terms } = &nodes[t as usize]
&& let [inner] = terms[..]
{
let t = t as usize;
uses[t] -= 1;
if uses[t] == 0 {
nodes[t] = Node::Dead;
} else if let MRef::Reg(s) = inner.src {
uses[s as usize] += 1;
}
return Term {
src: inner.src,
neg: inner.neg ^ neg,
};
}
Term { src: m, neg }
}
#[derive(Clone, Copy, Debug)]
pub(super) struct TPtr {
pub off: usize,
pub neg: u64,
pub adv: usize,
pub base: usize,
}
impl TPtr {
fn at(base: usize, off: usize, neg: bool, moves: bool) -> Self {
TPtr {
off,
neg: if neg { SIGN } else { 0 },
adv: if moves { usize::MAX } else { 0 },
base,
}
}
fn negated(self, neg: bool) -> Self {
TPtr {
neg: if neg { SIGN } else { 0 },
..self
}
}
#[inline(always)]
pub(super) fn ptr(&self, bases: &[*mut f64; N_BASES], off: usize) -> *const f64 {
bases[self.base].wrapping_add(self.off + (off & self.adv))
}
}
pub(super) const OTHER: u8 = 4;
#[derive(Clone, Copy, Debug)]
pub(super) struct SOp {
pub kind: u8,
pub n: u32,
pub t: u32,
pub base: usize,
pub off: usize,
pub dadv: usize,
}
impl SOp {
const NULL: SOp = SOp {
kind: OTHER,
n: 0,
t: 0,
base: B_REG,
off: 0,
dadv: 0,
};
fn fold(op: FoldOp, terms: (u32, u32), base: usize, off: usize, moves: bool) -> Self {
SOp {
kind: op as u8,
n: terms.1 - terms.0,
t: terms.0,
base,
off,
dadv: if moves { usize::MAX } else { 0 },
}
}
}
#[derive(Clone, Copy, Debug)]
pub(super) struct RTerm {
pub p: *const f64,
pub adv: usize,
pub neg: u64,
}
impl RTerm {
#[inline(always)]
pub(super) fn of(t: &TPtr, bases: &[*mut f64; N_BASES]) -> Self {
RTerm {
p: bases[t.base].wrapping_add(t.off),
adv: t.adv,
neg: t.neg,
}
}
}
pub(super) const R_OTHER: u8 = 128;
#[derive(Clone, Copy, Debug)]
pub(super) struct ROp {
pub code: u8,
pub n: u32,
pub t: *const RTerm,
pub dst: *mut f64,
pub adv: usize,
}
fn shape(t: &[RTerm]) -> u8 {
if t.len() > 4 {
return 15;
}
let s = t
.iter()
.skip(1)
.enumerate()
.fold(0u8, |s, (j, tj)| s | (u8::from(tj.neg != 0) << j));
match t.len() {
1 => 0,
2 => 1 + s,
3 => 3 + s,
4 => 7 + s,
_ => 15,
}
}
impl ROp {
#[inline(always)]
pub(super) fn of(
o: &SOp,
k: usize,
terms: &[RTerm],
tptr: &[TPtr],
bases: &[*mut f64; N_BASES],
) -> Self {
let dst = bases[o.base].wrapping_add(o.off);
if o.kind == OTHER {
return ROp {
code: R_OTHER,
n: k as u32,
t: std::ptr::null(),
dst,
adv: o.dadv,
};
}
let t = &terms[o.t as usize..][..o.n as usize];
ROp {
code: o.kind * 32 + 16 * u8::from(tptr[o.t as usize].base == B_SPLAT) + shape(t),
n: o.n,
t: t.as_ptr(),
dst,
adv: o.dadv,
}
}
#[inline(always)]
pub(super) fn at(&self, off: usize) -> *mut f64 {
self.dst.wrapping_add(off & self.adv)
}
}
#[inline(always)]
unsafe fn first<const SCAL: bool>(t: &RTerm, off: usize) -> [f64; STRIP] {
if SCAL {
return [f64::from_bits(unsafe { *t.p }.to_bits() ^ t.neg); STRIP];
}
let mut acc = [0.0f64; STRIP];
let p = t.p.wrapping_add(off & t.adv);
for (k, a) in acc.iter_mut().enumerate() {
*a = f64::from_bits(unsafe { *p.add(k) }.to_bits() ^ t.neg);
}
acc
}
#[inline(always)]
unsafe fn apply<const NEG: bool>(
acc: &mut [f64; STRIP],
t: &RTerm,
off: usize,
f: impl Fn(f64, f64) -> f64 + Copy,
) {
let p = t.p.wrapping_add(off & t.adv);
if NEG {
for (k, a) in acc.iter_mut().enumerate() {
*a = f(*a, f64::from_bits(unsafe { *p.add(k) }.to_bits() ^ t.neg));
}
} else {
for (k, a) in acc.iter_mut().enumerate() {
*a = f(*a, unsafe { *p.add(k) });
}
}
}
#[inline(always)]
unsafe fn strip_any(t: &[RTerm], off: usize, dst: *mut f64, f: impl Fn(f64, f64) -> f64 + Copy) {
let mut acc = unsafe { first::<false>(&t[0], off) };
for tj in &t[1..] {
unsafe { apply::<true>(&mut acc, tj, off, f) };
}
unsafe { std::ptr::copy_nonoverlapping(acc.as_ptr(), dst, STRIP) };
}
macro_rules! fixed {
($t:expr, $off:expr, $dst:expr, $f:expr, $s:literal; $($j:literal $neg:literal),*) => {{
let t: *const RTerm = $t;
#[allow(unused_mut)]
let mut acc = unsafe { first::<$s>(&*t, $off) };
$( unsafe { apply::<$neg>(&mut acc, &*t.add($j), $off, $f) }; )*
unsafe { std::ptr::copy_nonoverlapping(acc.as_ptr(), $dst, STRIP) };
}};
}
#[inline(always)]
pub(super) unsafe fn strip_fold(o: &ROp, off: usize) {
let dst = o.at(off);
let t = o.t;
macro_rules! by_shape {
($f:expr, $s:literal) => {
match o.code % 16 {
0 => fixed!(t, off, dst, $f, $s;),
1 => fixed!(t, off, dst, $f, $s; 1 false),
2 => fixed!(t, off, dst, $f, $s; 1 true),
3 => fixed!(t, off, dst, $f, $s; 1 false, 2 false),
4 => fixed!(t, off, dst, $f, $s; 1 true, 2 false),
5 => fixed!(t, off, dst, $f, $s; 1 false, 2 true),
6 => fixed!(t, off, dst, $f, $s; 1 true, 2 true),
7 => fixed!(t, off, dst, $f, $s; 1 false, 2 false, 3 false),
8 => fixed!(t, off, dst, $f, $s; 1 true, 2 false, 3 false),
9 => fixed!(t, off, dst, $f, $s; 1 false, 2 true, 3 false),
10 => fixed!(t, off, dst, $f, $s; 1 true, 2 true, 3 false),
11 => fixed!(t, off, dst, $f, $s; 1 false, 2 false, 3 true),
12 => fixed!(t, off, dst, $f, $s; 1 true, 2 false, 3 true),
13 => fixed!(t, off, dst, $f, $s; 1 false, 2 true, 3 true),
14 => fixed!(t, off, dst, $f, $s; 1 true, 2 true, 3 true),
_ => unsafe {
strip_any(std::slice::from_raw_parts(t, o.n as usize), off, dst, $f)
},
}
};
}
macro_rules! by_lead {
($f:expr) => {
if o.code & 16 != 0 {
by_shape!($f, true)
} else {
by_shape!($f, false)
}
};
}
match o.code / 32 {
0 => by_lead!(|x: f64, y: f64| x + y),
1 => by_lead!(|x: f64, y: f64| x - y),
2 => by_lead!(|x: f64, y: f64| x * y),
_ => by_lead!(|x: f64, y: f64| x / y),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn input(base: u32) -> LaneTable {
LaneTable {
kind: LaneKind::State,
ix: LaneIx::Affine { base, step: 1 },
}
}
#[test]
fn decode_folds_chains_and_negations() {
let bin = |op, a, b, out| MicroOp::Bin { op, a, b, out };
let ls = LaneSpec {
lanes: 64,
inputs: vec![input(0), input(64), input(128)],
scalars: vec![Operand::Param(0)],
micro: vec![
bin(BinCode::Mul, MRef::Scal(0), MRef::In(0), 0),
bin(BinCode::Mul, MRef::Reg(0), MRef::In(1), 1),
MicroOp::Neg {
a: MRef::Reg(1),
out: 2,
},
bin(BinCode::Add, MRef::Reg(2), MRef::In(2), 3),
bin(BinCode::Add, MRef::Reg(3), MRef::Reg(1), 4),
],
n_regs: 5,
writes: vec![
LaneWrite {
src: MRef::Reg(4),
dst: LaneDst::Dy(LaneIx::Affine { base: 0, step: 1 }),
},
LaneWrite {
src: MRef::Reg(1),
dst: LaneDst::Dy(LaneIx::Affine { base: 64, step: 1 }),
},
],
direct: Vec::new(),
};
let code = decode(&ls).expect("decodes");
assert_eq!(code.ops.len(), 2, "{:?}", code.ops);
let LOp::Fold {
op: FoldOp::Mul,
terms: (0, 3),
out: LOut::Reg(r),
} = code.ops[0]
else {
panic!("{:?}", code.ops[0])
};
assert!(matches!(
code.ops[1],
LOp::Fold {
op: FoldOp::Add,
terms: (3, 6),
out: LOut::Dy(0)
}
));
let negs: Vec<bool> = code.terms.iter().map(|t| t.neg).collect();
assert_eq!(negs, [false, false, false, true, false, false]);
assert_eq!(code.terms[3].src, MRef::Reg(r));
assert_eq!(code.terms[5].src, MRef::Reg(r));
assert!(code.writes[0].is_none());
assert_eq!(code.writes[1], Some(MRef::Reg(r)));
assert_eq!(code.n_regs, 1);
assert_eq!(code.strips.sops.len(), 2);
assert_eq!(code.strips.ends[B_STATE], 128 + 64);
assert_eq!(code.strips.ends[B_DY], 64);
}
#[test]
fn decode_keeps_operand_order() {
let ls = LaneSpec {
lanes: 64,
inputs: vec![input(0), input(64)],
scalars: Vec::new(),
micro: vec![
MicroOp::Bin {
op: BinCode::Add,
a: MRef::In(0),
b: MRef::In(1),
out: 0,
},
MicroOp::Bin {
op: BinCode::Add,
a: MRef::In(0),
b: MRef::Reg(0),
out: 1,
},
],
n_regs: 2,
writes: vec![LaneWrite {
src: MRef::Reg(1),
dst: LaneDst::Dy(LaneIx::Affine { base: 0, step: 1 }),
}],
direct: Vec::new(),
};
let code = decode(&ls).expect("decodes");
assert_eq!(code.ops.len(), 2, "{:?}", code.ops);
assert_eq!(code.terms.len(), 4);
}
}