use crate::fuse::{apply_bin, apply_un, BinOp, Class, Red, UnOp};
use crate::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Node {
In(usize),
Const(u32),
Un(UnOp, usize),
Bin(BinOp, usize, usize),
Reduce(Red, usize),
}
impl Node {
pub fn class(&self) -> Class {
match self {
Node::Reduce(..) => Class::Reduce,
_ => Class::Map,
}
}
fn inputs(&self) -> heapless_inputs::Inputs {
match *self {
Node::In(_) | Node::Const(_) => heapless_inputs::Inputs::none(),
Node::Un(_, a) | Node::Reduce(_, a) => heapless_inputs::Inputs::one(a),
Node::Bin(_, a, b) => heapless_inputs::Inputs::two(a, b),
}
}
}
mod heapless_inputs {
#[derive(Clone, Copy)]
pub struct Inputs {
buf: [usize; 2],
len: u8,
}
impl Inputs {
pub fn none() -> Self {
Inputs {
buf: [0, 0],
len: 0,
}
}
pub fn one(a: usize) -> Self {
Inputs {
buf: [a, 0],
len: 1,
}
}
pub fn two(a: usize, b: usize) -> Self {
Inputs {
buf: [a, b],
len: 2,
}
}
}
impl<'a> IntoIterator for &'a Inputs {
type Item = usize;
type IntoIter = core::iter::Copied<core::slice::Iter<'a, usize>>;
fn into_iter(self) -> Self::IntoIter {
self.buf[..self.len as usize].iter().copied()
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Dag {
pub nodes: Vec<Node>,
pub n_inputs: usize,
}
#[derive(Default)]
pub struct Tape {
nodes: core::cell::RefCell<Vec<Node>>,
n_inputs: core::cell::Cell<usize>,
}
impl Tape {
pub fn new() -> Tape {
Tape::default()
}
pub fn input(&self) -> Var<'_> {
let slot = self.n_inputs.get();
self.n_inputs.set(slot + 1);
self.push(Node::In(slot))
}
pub fn konst(&self, c: f32) -> Var<'_> {
self.push(Node::Const(c.to_bits()))
}
fn push(&self, n: Node) -> Var<'_> {
let mut v = self.nodes.borrow_mut();
let id = v.len();
v.push(n);
Var { tape: self, id }
}
pub fn finish(&self, root: Var<'_>) -> Dag {
Dag {
nodes: self.nodes.borrow().clone(),
n_inputs: self.n_inputs.get(),
}
.pruned_to(root.id)
}
}
#[derive(Clone, Copy)]
pub struct Var<'t> {
tape: &'t Tape,
id: usize,
}
impl<'t> Var<'t> {
pub fn id(self) -> usize {
self.id
}
fn un(self, op: UnOp) -> Var<'t> {
self.tape.push(Node::Un(op, self.id))
}
pub fn silu(self) -> Var<'t> {
self.un(UnOp::Silu)
}
pub fn gelu(self) -> Var<'t> {
self.un(UnOp::Gelu)
}
pub fn sigmoid(self) -> Var<'t> {
self.un(UnOp::Sigmoid)
}
pub fn exp(self) -> Var<'t> {
self.un(UnOp::Exp)
}
pub fn tanh(self) -> Var<'t> {
self.un(UnOp::Tanh)
}
pub fn rsqrt(self) -> Var<'t> {
self.un(UnOp::Rsqrt)
}
pub fn recip(self) -> Var<'t> {
self.un(UnOp::Recip)
}
pub fn abs(self) -> Var<'t> {
self.un(UnOp::Abs)
}
pub fn reduce(self, r: Red) -> Var<'t> {
self.tape.push(Node::Reduce(r, self.id))
}
}
macro_rules! bin_op {
($Trait:ident, $method:ident, $variant:ident) => {
impl<'t> core::ops::$Trait for Var<'t> {
type Output = Var<'t>;
fn $method(self, rhs: Var<'t>) -> Var<'t> {
self.tape.push(Node::Bin(BinOp::$variant, self.id, rhs.id))
}
}
};
}
bin_op!(Add, add, Add);
bin_op!(Sub, sub, Sub);
bin_op!(Mul, mul, Mul);
bin_op!(Div, div, Div);
impl Dag {
fn pruned_to(mut self, root: usize) -> Dag {
let n = self.nodes.len();
let mut live = vec![false; n];
let mut stack = vec![root];
while let Some(id) = stack.pop() {
if live[id] {
continue;
}
live[id] = true;
for i in self.nodes[id].inputs().into_iter() {
stack.push(i);
}
}
let mut remap = vec![usize::MAX; n];
let mut kept = Vec::new();
for (old, &alive) in live.iter().enumerate() {
if alive {
remap[old] = kept.len();
let mut node = self.nodes[old];
node = match node {
Node::Un(op, a) => Node::Un(op, remap[a]),
Node::Bin(op, a, b) => Node::Bin(op, remap[a], remap[b]),
Node::Reduce(r, a) => Node::Reduce(r, remap[a]),
leaf => leaf,
};
kept.push(node);
}
}
self.nodes = kept;
self
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn eval(&self, inputs: &[&[f32]], n: usize) -> Vec<f32> {
let len = inputs[0].len();
let mut vals: Vec<Vec<f32>> = Vec::with_capacity(self.nodes.len());
for node in &self.nodes {
let v = match *node {
Node::In(slot) => inputs[slot].to_vec(),
Node::Const(bits) => vec![f32::from_bits(bits); len],
Node::Un(op, a) => vals[a].iter().map(|&x| apply_un(op, x)).collect(),
Node::Bin(op, a, b) => (0..len)
.map(|i| apply_bin(op, vals[a][i], vals[b][i]))
.collect(),
Node::Reduce(r, a) => reduce_rows(&vals[a], r, n),
};
vals.push(v);
}
vals.pop().unwrap()
}
}
fn reduce_rows(cur: &[f32], r: Red, n: usize) -> Vec<f32> {
let len = cur.len();
let rows = len / n;
let mut out = vec![0.0f32; len];
for row in 0..rows {
let base = row * n;
let acc = match r {
Red::Sum => cur[base..base + n].iter().sum(),
Red::Max => cur[base..base + n].iter().copied().fold(f32::MIN, f32::max),
};
for i in 0..n {
out[base + i] = acc;
}
}
out
}
#[derive(Clone, Debug, PartialEq)]
pub enum Region {
Map {
local: Vec<Node>,
live_in: Vec<ValueSrc>,
output: usize,
},
Reduce { r: Red, src: ValueSrc },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ValueSrc {
Input(usize),
Region(usize),
}
impl Dag {
pub fn fuse(&self) -> Vec<Region> {
let n = self.nodes.len();
let mut level = vec![0usize; n];
for id in 0..n {
let in_max = self.nodes[id]
.inputs()
.into_iter()
.map(|i| level[i])
.max()
.unwrap_or(0);
level[id] = in_max + usize::from(self.nodes[id].class() == Class::Reduce);
}
let mut uf = UnionFind::new(n);
for (id, node) in self.nodes.iter().enumerate() {
if node.class() == Class::Reduce || matches!(node, Node::Const(_)) {
continue;
}
for input in node.inputs().into_iter() {
let inp = self.nodes[input];
if inp.class() == Class::Map
&& !matches!(inp, Node::Const(_))
&& level[input] == level[id]
{
uf.union(id, input);
}
}
}
let mut region_of_node = vec![usize::MAX; n];
let mut region_of_unit: std::collections::HashMap<usize, usize> = Default::default();
let mut kinds: Vec<RegionKind> = Vec::new();
for id in 0..n {
if matches!(self.nodes[id], Node::Const(_)) {
continue; }
let is_reduce = self.nodes[id].class() == Class::Reduce;
let unit = if is_reduce { id } else { uf.find(id) };
let region_id = *region_of_unit.entry(unit).or_insert_with(|| {
let rid = kinds.len();
kinds.push(if is_reduce {
RegionKind::Reduce
} else {
RegionKind::Map
});
rid
});
region_of_node[id] = region_id;
}
(0..kinds.len())
.map(|region_id| match kinds[region_id] {
RegionKind::Reduce => {
let rid_node = (0..n)
.find(|&id| {
self.nodes[id].class() == Class::Reduce
&& region_of_node[id] == region_id
})
.unwrap();
let Node::Reduce(r, a) = self.nodes[rid_node] else {
unreachable!()
};
Region::Reduce {
r,
src: self.value_src(a, ®ion_of_node),
}
}
RegionKind::Map => self.build_map_region(region_id, ®ion_of_node),
})
.collect()
}
fn value_src(&self, gid: usize, region_of_node: &[usize]) -> ValueSrc {
match self.nodes[gid] {
Node::In(slot) => ValueSrc::Input(slot),
_ => ValueSrc::Region(region_of_node[gid]),
}
}
fn build_map_region(&self, region_id: usize, region_of_node: &[usize]) -> Region {
let n = self.nodes.len();
let members = (0..n)
.filter(|&id| self.nodes[id].class() == Class::Map && region_of_node[id] == region_id);
let mut local: Vec<Node> = Vec::new();
let mut live_in: Vec<ValueSrc> = Vec::new();
let mut local_of_global: std::collections::HashMap<usize, usize> = Default::default();
let mut leaf_of_src: std::collections::HashMap<ValueSrc, usize> = Default::default();
let mut last_local = 0usize;
for gid in members {
let operand = |g: usize,
local: &mut Vec<Node>,
live_in: &mut Vec<ValueSrc>,
leaf_of_src: &mut std::collections::HashMap<ValueSrc, usize>|
-> usize {
if let Some(&lid) = local_of_global.get(&g) {
return lid;
}
if let Node::Const(bits) = self.nodes[g] {
let lid = local.len();
local.push(Node::Const(bits));
return lid;
}
let src = self.value_src(g, region_of_node);
*leaf_of_src.entry(src).or_insert_with(|| {
let li = live_in.len();
live_in.push(src);
let lid = local.len();
local.push(Node::In(li));
lid
})
};
let ln = match self.nodes[gid] {
Node::In(_) => {
let src = self.value_src(gid, region_of_node);
if let Some(&lid) = leaf_of_src.get(&src) {
local_of_global.insert(gid, lid);
last_local = lid;
continue;
}
let li = live_in.len();
live_in.push(src);
leaf_of_src.insert(src, local.len());
Node::In(li)
}
Node::Const(bits) => Node::Const(bits),
Node::Un(op, a) => {
Node::Un(op, operand(a, &mut local, &mut live_in, &mut leaf_of_src))
}
Node::Bin(op, a, b) => {
let la = operand(a, &mut local, &mut live_in, &mut leaf_of_src);
let lb = operand(b, &mut local, &mut live_in, &mut leaf_of_src);
Node::Bin(op, la, lb)
}
Node::Reduce(..) => unreachable!("reduce not in a Map region"),
};
let lid = local.len();
local.push(ln);
local_of_global.insert(gid, lid);
last_local = lid;
}
Region::Map {
local,
live_in,
output: last_local,
}
}
pub fn kernel_count(&self) -> usize {
self.fuse().len()
}
}
enum RegionKind {
Map,
Reduce,
}
struct UnionFind {
parent: Vec<usize>,
}
impl UnionFind {
fn new(n: usize) -> Self {
UnionFind {
parent: (0..n).collect(),
}
}
fn find(&self, mut x: usize) -> usize {
while self.parent[x] != x {
x = self.parent[x];
}
x
}
fn union(&mut self, a: usize, b: usize) {
let (ra, rb) = (self.find(a), self.find(b));
if ra != rb {
let (lo, hi) = if ra < rb { (ra, rb) } else { (rb, ra) };
self.parent[hi] = lo;
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DagInstr {
LoadIn(usize),
LoadConst(u32),
Un(UnOp, usize),
Bin(BinOp, usize, usize),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
pub struct DagProgram {
pub instrs: Vec<DagInstr>,
pub n_sides: usize,
pub output: usize,
}
impl Region {
pub fn to_program(&self) -> Option<(DagProgram, Vec<ValueSrc>)> {
let Region::Map {
local,
live_in,
output,
} = self
else {
return None;
};
let instrs = local
.iter()
.map(|node| match *node {
Node::In(li) => DagInstr::LoadIn(li),
Node::Const(bits) => DagInstr::LoadConst(bits),
Node::Un(op, a) => DagInstr::Un(op, a),
Node::Bin(op, a, b) => DagInstr::Bin(op, a, b),
Node::Reduce(..) => unreachable!("reduce in a Map region"),
})
.collect();
let prog = DagProgram {
instrs,
n_sides: live_in.len(),
output: *output,
};
Some((prog, live_in.clone()))
}
}
#[device]
fn dsig<F: Float>(x: F) -> F {
F::new(1.0) / (F::new(1.0) + (-x).exp())
}
#[device]
fn un_dev<F: Float>(#[comptime] op: UnOp, x: F) -> F {
match op {
UnOp::Neg => -x,
UnOp::Recip => F::new(1.0) / x,
UnOp::Abs => x.abs(),
UnOp::Exp => x.exp(),
UnOp::Tanh => x.tanh(),
UnOp::Rsqrt => F::new(1.0) / x.sqrt(),
UnOp::Sigmoid => dsig::<F>(x),
UnOp::Silu => x * dsig::<F>(x),
UnOp::Gelu => {
let c = F::new(0.797_884_56);
let inner = c * (x + F::new(0.044715) * x * x * x);
F::new(0.5) * x * (F::new(1.0) + inner.tanh())
}
}
}
#[device]
fn bin_dev<F: Float>(#[comptime] op: BinOp, a: F, b: F) -> F {
match op {
BinOp::Add => a + b,
BinOp::Sub => a - b,
BinOp::Mul => a * b,
BinOp::Div => a / b,
}
}
#[kernel(targets(cuda, metal, vulkan, webgpu, cpu), unchecked)]
pub fn dag_interp<F: Float>(
sides: &Sequence<Array<F>>,
out: &mut Array<F>,
#[comptime] prog: DagProgram,
) {
let i = ABSOLUTE_POS;
if i < out.len() {
let n_slots = comptime!(prog.instrs.len());
let mut slot = Array::<F>::new(n_slots);
#[unroll]
for k in 0..prog.instrs.len() {
match comptime!(prog.instrs[k]) {
DagInstr::LoadIn(in_slot) => slot[k] = sides.index(in_slot)[i],
DagInstr::LoadConst(bits) => {
let c = comptime!(f32::from_bits(bits));
slot[k] = F::new(c);
}
DagInstr::Un(op, a) => slot[k] = un_dev::<F>(op, slot[a]),
DagInstr::Bin(op, a, b) => slot[k] = bin_dev::<F>(op, slot[a], slot[b]),
}
}
out[i] = slot[comptime!(prog.output)];
}
}
const BLOCK: u32 = 256;
#[kernel(targets(cuda, metal, vulkan, webgpu, cpu), unchecked)]
pub fn reduce_broadcast<F: Float>(
x: &Array<F>,
out: &mut Array<F>,
#[comptime] red: Red,
#[comptime] n: usize,
) {
let i = ABSOLUTE_POS;
if i < out.len() {
let row = i / n;
let base = row * n;
let mut acc = match red {
Red::Sum => F::new(0.0),
Red::Max => F::new(-3.4e38),
};
for j in 0..n {
let v = x[base + j];
acc = match red {
Red::Sum => acc + v,
Red::Max => {
if v > acc {
v
} else {
acc
}
}
};
}
out[i] = acc;
}
}
pub struct FusedRun {
pub out: Vec<f32>,
pub launches: usize,
}
impl Dag {
pub fn fuse_and_run<R: Runtime>(
&self,
client: &ComputeClient<R>,
inputs: &[&[f32]],
n: usize,
) -> FusedRun {
let len = inputs[0].len();
let regions = self.fuse();
let input_handles: Vec<_> = inputs
.iter()
.map(|s| client.create_from_slice(f32::as_bytes(s)))
.collect();
let mut region_out: Vec<Option<cubecl::server::Handle>> = vec![None; regions.len()];
let resolve = |src: ValueSrc,
input_handles: &[cubecl::server::Handle],
region_out: &[Option<cubecl::server::Handle>]|
-> cubecl::server::Handle {
match src {
ValueSrc::Input(slot) => input_handles[slot].clone(),
ValueSrc::Region(rid) => region_out[rid]
.clone()
.expect("region scheduled before use"),
}
};
let grid = Grid::Static((len as u32).div_ceil(BLOCK), 1, 1);
let mut launches = 0usize;
for (rid, region) in regions.iter().enumerate() {
match region {
Region::Map { .. } => {
let (prog, srcs) = region.to_program().unwrap();
let oh = client.create_from_slice(f32::as_bytes(&vec![0.0f32; len]));
let mut sides = SequenceArg::new();
let side_handles: Vec<_> = srcs
.iter()
.map(|&s| resolve(s, &input_handles, ®ion_out))
.collect();
for h in &side_handles {
sides.push(unsafe { ArrayArg::from_raw_parts(h.clone(), len) });
}
unsafe {
dag_interp::launch_unchecked::<f32, R>(
client,
grid.clone(),
Block::new_1d(BLOCK),
sides,
ArrayArg::from_raw_parts(oh.clone(), len),
prog,
);
}
region_out[rid] = Some(oh);
launches += 1;
}
Region::Reduce { r, src } => {
let ih = resolve(*src, &input_handles, ®ion_out);
let oh = client.create_from_slice(f32::as_bytes(&vec![0.0f32; len]));
unsafe {
reduce_broadcast::launch_unchecked::<f32, R>(
client,
grid.clone(),
Block::new_1d(BLOCK),
ArrayArg::from_raw_parts(ih.clone(), len),
ArrayArg::from_raw_parts(oh.clone(), len),
*r,
n,
);
}
region_out[rid] = Some(oh);
launches += 1;
}
}
}
let final_h = region_out.last().unwrap().clone().unwrap();
let out = f32::from_bytes(&client.read_one_unchecked(final_h)).to_vec();
FusedRun { out, launches }
}
}
#[device]
fn n_un<F: Float>(#[comptime] op: UnOp, x: F) -> F {
un_dev::<F>(op, x)
}
#[kernel(targets(cuda, metal, vulkan, webgpu, cpu), unchecked)]
fn node_un<F: Float>(x: &Array<F>, out: &mut Array<F>, #[comptime] op: UnOp) {
let i = ABSOLUTE_POS;
if i < out.len() {
out[i] = n_un::<F>(op, x[i]);
}
}
#[kernel(targets(cuda, metal, vulkan, webgpu, cpu), unchecked)]
fn node_bin<F: Float>(a: &Array<F>, b: &Array<F>, out: &mut Array<F>, #[comptime] op: BinOp) {
let i = ABSOLUTE_POS;
if i < out.len() {
out[i] = bin_dev::<F>(op, a[i], b[i]);
}
}
#[kernel(targets(cuda, metal, vulkan, webgpu, cpu), unchecked)]
fn node_const<F: Float>(out: &mut Array<F>, #[comptime] bits: u32) {
let i = ABSOLUTE_POS;
if i < out.len() {
let c = comptime!(f32::from_bits(bits));
out[i] = F::new(c);
}
}
impl Dag {
pub fn naive_run<R: Runtime>(
&self,
client: &ComputeClient<R>,
inputs: &[&[f32]],
n: usize,
) -> FusedRun {
let len = inputs[0].len();
let grid = Grid::Static((len as u32).div_ceil(BLOCK), 1, 1);
let mut handles: Vec<cubecl::server::Handle> = Vec::with_capacity(self.nodes.len());
let mut launches = 0usize;
for node in &self.nodes {
let oh = client.create_from_slice(f32::as_bytes(&vec![0.0f32; len]));
match *node {
Node::In(slot) => {
let h = client.create_from_slice(f32::as_bytes(inputs[slot]));
handles.push(h);
continue;
}
Node::Const(bits) => unsafe {
node_const::launch_unchecked::<f32, R>(
client,
grid.clone(),
Block::new_1d(BLOCK),
ArrayArg::from_raw_parts(oh.clone(), len),
bits,
);
},
Node::Un(op, a) => unsafe {
node_un::launch_unchecked::<f32, R>(
client,
grid.clone(),
Block::new_1d(BLOCK),
ArrayArg::from_raw_parts(handles[a].clone(), len),
ArrayArg::from_raw_parts(oh.clone(), len),
op,
);
},
Node::Bin(op, a, b) => unsafe {
node_bin::launch_unchecked::<f32, R>(
client,
grid.clone(),
Block::new_1d(BLOCK),
ArrayArg::from_raw_parts(handles[a].clone(), len),
ArrayArg::from_raw_parts(handles[b].clone(), len),
ArrayArg::from_raw_parts(oh.clone(), len),
op,
);
},
Node::Reduce(r, a) => unsafe {
reduce_broadcast::launch_unchecked::<f32, R>(
client,
grid.clone(),
Block::new_1d(BLOCK),
ArrayArg::from_raw_parts(handles[a].clone(), len),
ArrayArg::from_raw_parts(oh.clone(), len),
r,
n,
);
},
}
handles.push(oh);
launches += 1;
}
let out =
f32::from_bytes(&client.read_one_unchecked(handles.last().unwrap().clone())).to_vec();
FusedRun { out, launches }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn xorshift_vec(n: usize, seed: u64) -> Vec<f32> {
let mut s = seed;
(0..n)
.map(|_| {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
(s % 4000) as f32 / 1000.0 - 2.0
})
.collect()
}
fn bits(v: &[f32]) -> Vec<u32> {
v.iter().map(|x| x.to_bits()).collect()
}
#[test]
fn fan_out_traces_to_a_shared_node() {
let tape = Tape::new();
let a = tape.input();
let b = tape.input();
let y = a.silu() * b + a;
let dag = tape.finish(y);
assert_eq!(
dag.len(),
5,
"fan-out shares the `a` node, not duplicates it"
);
let regions = dag.fuse();
assert_eq!(
regions.len(),
1,
"all-Map DAG with fan-out -> ONE fused kernel"
);
assert!(matches!(regions[0], Region::Map { .. }));
}
#[test]
fn reduce_partitions_the_dag() {
let tape = Tape::new();
let a = tape.input();
let sq = a * a; let s = sq.reduce(Red::Sum); let eps = tape.konst(1e-6);
let y = (s + eps).rsqrt() * a; let dag = tape.finish(y);
let regions = dag.fuse();
assert_eq!(regions.len(), 3, "Map | Reduce | Map");
assert!(matches!(regions[0], Region::Map { .. }));
assert!(matches!(regions[1], Region::Reduce { r: Red::Sum, .. }));
assert!(matches!(regions[2], Region::Map { .. }));
}
#[test]
fn eval_oracle_matches_fused_partition_semantics() {
let tape = Tape::new();
let a = tape.input();
let b = tape.input();
let y = a.silu() * b + a;
let dag = tape.finish(y);
let n = 16usize;
let av = xorshift_vec(n, 1);
let bv = xorshift_vec(n, 2);
let whole = dag.eval(&[&av, &bv], n);
let refv: Vec<f32> = (0..n)
.map(|i| apply_un(UnOp::Silu, av[i]) * bv[i] + av[i])
.collect();
assert_eq!(
bits(&whole),
bits(&refv),
"oracle disagrees with plain Rust"
);
}
#[cfg(feature = "cpu")]
fn cpu_client() -> ComputeClient<cubecl::cpu::CpuRuntime> {
use cubecl::cpu::{CpuDevice, CpuRuntime};
CpuRuntime::client(&CpuDevice::default())
}
#[cfg(feature = "cpu")]
#[test]
fn fanout_dag_fused_equals_naive_bit_exact() {
let client = cpu_client();
let n = 1024usize;
let a = xorshift_vec(n, 0x1234_5678_9abc_def0);
let b = xorshift_vec(n, 0x0fed_cba9_8765_4321);
let tape = Tape::new();
let va = tape.input();
let vb = tape.input();
let y = va.silu() * vb + va; let dag = tape.finish(y);
let fused = dag.fuse_and_run::<cubecl::cpu::CpuRuntime>(&client, &[&a, &b], n);
let naive = dag.naive_run::<cubecl::cpu::CpuRuntime>(&client, &[&a, &b], n);
let refv: Vec<f32> = (0..n)
.map(|i| apply_un(UnOp::Silu, a[i]) * b[i] + a[i])
.collect();
assert_eq!(
bits(&fused.out),
bits(&naive.out),
"fused != naive (bit level)"
);
let maxerr = fused
.out
.iter()
.zip(&refv)
.map(|(g, r)| (g - r).abs())
.fold(0.0f32, f32::max);
eprintln!(
"[dag CPU] fan-out silu(a)*b+a: fused {} launch vs naive {} launches; bit-exact; max|fused-ref|={maxerr:.2e}",
fused.launches, naive.launches
);
assert_eq!(fused.launches, 1, "one Map region -> one launch");
assert!(fused.launches < naive.launches, "fusion must cut launches");
assert!(maxerr < 1e-5, "reference disagreement {maxerr}");
}
#[cfg(feature = "cpu")]
#[test]
fn rmsnorm_shape_fanout_across_fence_bit_exact() {
let client = cpu_client();
let rows = 32usize;
let n = 64usize; let len = rows * n;
let a = xorshift_vec(len, 0xdead_beef_cafe_babe);
let eps = 1e-6f32;
let tape = Tape::new();
let va = tape.input();
let sq = va * va;
let s = sq.reduce(Red::Sum);
let ke = tape.konst(eps);
let y = (s + ke).rsqrt() * va; let dag = tape.finish(y);
let fused = dag.fuse_and_run::<cubecl::cpu::CpuRuntime>(&client, &[&a], n);
let naive = dag.naive_run::<cubecl::cpu::CpuRuntime>(&client, &[&a], n);
let mut refv = vec![0.0f32; len];
for row in 0..rows {
let base = row * n;
let ss: f32 = a[base..base + n].iter().map(|&x| x * x).sum();
let scale = 1.0f32 / (ss + eps).sqrt();
for i in 0..n {
refv[base + i] = scale * a[base + i];
}
}
assert_eq!(
bits(&fused.out),
bits(&naive.out),
"fused != naive (bit level)"
);
let maxerr = fused
.out
.iter()
.zip(&refv)
.map(|(g, r)| (g - r).abs())
.fold(0.0f32, f32::max);
eprintln!(
"[dag CPU] RMS-shape rsqrt(sum(a*a)+eps)*a (fan-out ACROSS fence): fused {} launches vs naive {} launches; bit-exact; max|fused-ref|={maxerr:.2e}",
fused.launches, naive.launches
);
assert_eq!(fused.launches, 3, "Map | Reduce | Map -> 3 launches");
assert!(fused.launches < naive.launches, "fusion must cut launches");
assert!(maxerr < 1e-5, "reference disagreement {maxerr}");
}
#[cfg(feature = "cpu")]
#[test]
fn multi_region_dag_fused_equals_naive_bit_exact() {
let client = cpu_client();
let rows = 16usize;
let n = 128usize;
let len = rows * n;
let a = xorshift_vec(len, 0x0123_4567_89ab_cdef);
let b = xorshift_vec(len, 0xfedc_ba98_7654_3210);
let tape = Tape::new();
let va = tape.input();
let vb = tape.input();
let t = va.silu() * vb;
let s = t.reduce(Red::Sum);
let y = s.gelu() + t;
let dag = tape.finish(y);
let regions = dag.fuse();
assert_eq!(regions.len(), 3);
let Region::Map { live_in, .. } = ®ions[2] else {
panic!("epilogue is a Map region")
};
assert!(
live_in.contains(&ValueSrc::Region(0)) && live_in.contains(&ValueSrc::Region(1)),
"epilogue must read region 0 (t, across the fence) AND region 1 (s): {live_in:?}"
);
let fused = dag.fuse_and_run::<cubecl::cpu::CpuRuntime>(&client, &[&a, &b], n);
let naive = dag.naive_run::<cubecl::cpu::CpuRuntime>(&client, &[&a, &b], n);
let mut refv = vec![0.0f32; len];
let mut tv = vec![0.0f32; len];
for i in 0..len {
tv[i] = apply_un(UnOp::Silu, a[i]) * b[i];
}
for row in 0..rows {
let base = row * n;
let ss: f32 = tv[base..base + n].iter().sum();
let g = apply_un(UnOp::Gelu, ss);
for i in 0..n {
refv[base + i] = g + tv[base + i];
}
}
assert_eq!(
bits(&fused.out),
bits(&naive.out),
"fused != naive (bit level)"
);
let maxerr = fused
.out
.iter()
.zip(&refv)
.map(|(g, r)| (g - r).abs())
.fold(0.0f32, f32::max);
eprintln!(
"[dag CPU] multi-region silu(a)*b -> sum -> gelu(.)+t (fan-in across fence): fused {} launches vs naive {} launches; bit-exact; max|fused-ref|={maxerr:.2e}",
fused.launches, naive.launches
);
assert_eq!(fused.launches, 3, "Map | Reduce | Map -> 3 launches");
assert!(fused.launches < naive.launches, "fusion must cut launches");
assert!(maxerr < 1e-5, "reference disagreement {maxerr}");
}
#[test]
fn rms_norm_gated_engine_pattern_fuses() {
let client = cpu_client();
let rows = 16usize;
let n = 128usize;
let len = rows * n;
let x = xorshift_vec(len, 0xa1b2_c3d4_e5f6_0718);
let w = xorshift_vec(len, 0x1122_3344_5566_7788);
let g = xorshift_vec(len, 0x9900_aabb_ccdd_eeff);
let tape = Tape::new();
let vx = tape.input();
let vw = tape.input();
let vg = tape.input();
let eps = tape.konst(1e-6);
let ss = (vx * vx).reduce(Red::Sum); let inv = (ss + eps).rsqrt(); let y = vx * inv * vw * vg.silu(); let dag = tape.finish(y);
let fused = dag.fuse_and_run::<cubecl::cpu::CpuRuntime>(&client, &[&x, &w, &g], n);
let naive = dag.naive_run::<cubecl::cpu::CpuRuntime>(&client, &[&x, &w, &g], n);
let mut refv = vec![0.0f32; len];
for row in 0..rows {
let base = row * n;
let ss: f32 = x[base..base + n].iter().map(|v| v * v).sum();
let inv = 1.0f32 / (ss + 1e-6).sqrt();
for i in 0..n {
let j = base + i;
refv[j] = x[j] * inv * w[j] * apply_un(UnOp::Silu, g[j]);
}
}
assert_eq!(bits(&fused.out), bits(&naive.out), "fused != naive (bit level)");
let maxerr = fused
.out
.iter()
.zip(&refv)
.map(|(a, b)| (a - b).abs())
.fold(0.0f32, f32::max);
eprintln!(
"[dag CPU] RmsNormGated rms_norm(x)*w*silu(g) (real engine gdn.rs chain): fused {} vs naive {} launches; bit-exact; max|fused-ref|={maxerr:.2e}",
fused.launches, naive.launches
);
assert!(fused.launches < naive.launches, "fusion must cut the per-layer launch count");
assert!(maxerr < 1e-5, "reference disagreement {maxerr}");
}
}