use crate::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum UnOp {
Neg,
Recip,
Abs,
Exp,
Tanh,
Rsqrt,
Sigmoid,
Silu,
Gelu,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BinOp {
Add,
Sub,
Mul,
Div,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Red {
Sum,
Max,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Class {
Map,
Reduce,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Expr {
Cur,
In(usize),
Const(f32),
Un(UnOp, Box<Expr>),
Bin(BinOp, Box<Expr>, Box<Expr>),
}
impl Expr {
pub fn input(id: usize) -> Expr {
Expr::In(id)
}
pub fn cur() -> Expr {
Expr::Cur
}
pub fn konst(c: f32) -> Expr {
Expr::Const(c)
}
pub fn un(self, op: UnOp) -> Expr {
Expr::Un(op, Box::new(self))
}
pub fn silu(self) -> Expr {
self.un(UnOp::Silu)
}
pub fn gelu(self) -> Expr {
self.un(UnOp::Gelu)
}
pub fn sigmoid(self) -> Expr {
self.un(UnOp::Sigmoid)
}
pub fn exp(self) -> Expr {
self.un(UnOp::Exp)
}
pub fn tanh(self) -> Expr {
self.un(UnOp::Tanh)
}
pub fn rsqrt(self) -> Expr {
self.un(UnOp::Rsqrt)
}
pub fn recip(self) -> Expr {
self.un(UnOp::Recip)
}
pub fn abs(self) -> Expr {
self.un(UnOp::Abs)
}
pub fn compose_cur(&self, inner: &Expr) -> Expr {
match self {
Expr::Cur => inner.clone(),
Expr::In(id) => Expr::In(*id),
Expr::Const(c) => Expr::Const(*c),
Expr::Un(op, a) => Expr::Un(*op, Box::new(a.compose_cur(inner))),
Expr::Bin(op, a, b) => {
Expr::Bin(*op, Box::new(a.compose_cur(inner)), Box::new(b.compose_cur(inner)))
}
}
}
pub fn size(&self) -> usize {
match self {
Expr::Cur | Expr::In(_) | Expr::Const(_) => 1,
Expr::Un(_, a) => 1 + a.size(),
Expr::Bin(_, a, b) => 1 + a.size() + b.size(),
}
}
pub fn body(&self) -> String {
match self {
Expr::Cur => "cur".into(),
Expr::In(id) => format!("in{id}[i]"),
Expr::Const(c) => format!("{c}"),
Expr::Un(op, a) => {
let name = match op {
UnOp::Neg => return format!("(-{})", a.body()),
UnOp::Recip => "recip",
UnOp::Abs => "abs",
UnOp::Exp => "exp",
UnOp::Tanh => "tanh",
UnOp::Rsqrt => "rsqrt",
UnOp::Sigmoid => "sigmoid",
UnOp::Silu => "silu",
UnOp::Gelu => "gelu",
};
format!("{name}({})", a.body())
}
Expr::Bin(op, a, b) => {
let s = match op {
BinOp::Add => "+",
BinOp::Sub => "-",
BinOp::Mul => "*",
BinOp::Div => "/",
};
format!("({} {} {})", a.body(), s, b.body())
}
}
}
}
impl core::ops::Add for Expr {
type Output = Expr;
fn add(self, rhs: Expr) -> Expr {
Expr::Bin(BinOp::Add, Box::new(self), Box::new(rhs))
}
}
impl core::ops::Sub for Expr {
type Output = Expr;
fn sub(self, rhs: Expr) -> Expr {
Expr::Bin(BinOp::Sub, Box::new(self), Box::new(rhs))
}
}
impl core::ops::Mul for Expr {
type Output = Expr;
fn mul(self, rhs: Expr) -> Expr {
Expr::Bin(BinOp::Mul, Box::new(self), Box::new(rhs))
}
}
impl core::ops::Div for Expr {
type Output = Expr;
fn div(self, rhs: Expr) -> Expr {
Expr::Bin(BinOp::Div, Box::new(self), Box::new(rhs))
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Step {
Map(Expr),
Reduce(Red),
}
impl Step {
pub fn class(&self) -> Class {
match self {
Step::Map(_) => Class::Map,
Step::Reduce(_) => Class::Reduce,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Region {
Map(Expr),
Reduce(Red),
}
#[derive(Clone, Debug, Default)]
pub struct Chain {
pub steps: Vec<Step>,
}
impl Chain {
pub fn new() -> Chain {
Chain { steps: Vec::new() }
}
pub fn map_unary(mut self, op: UnOp) -> Chain {
self.steps.push(Step::Map(Expr::Un(op, Box::new(Expr::Cur))));
self
}
pub fn map_binary(mut self, op: BinOp, id: usize) -> Chain {
self.steps
.push(Step::Map(Expr::Bin(op, Box::new(Expr::Cur), Box::new(Expr::In(id)))));
self
}
pub fn map_const(mut self, op: BinOp, c: f32) -> Chain {
self.steps
.push(Step::Map(Expr::Bin(op, Box::new(Expr::Cur), Box::new(Expr::Const(c)))));
self
}
pub fn map(mut self, e: Expr) -> Chain {
self.steps.push(Step::Map(e));
self
}
pub fn reduce(mut self, r: Red) -> Chain {
self.steps.push(Step::Reduce(r));
self
}
pub fn fuse(&self) -> Vec<Region> {
let mut regions = Vec::new();
let mut acc: Option<Expr> = None;
for step in &self.steps {
match step {
Step::Map(e) => {
acc = Some(match acc.take() {
None => e.clone(),
Some(prev) => e.compose_cur(&prev),
});
}
Step::Reduce(r) => {
if let Some(e) = acc.take() {
regions.push(Region::Map(e));
}
regions.push(Region::Reduce(*r));
}
}
}
if let Some(e) = acc.take() {
regions.push(Region::Map(e));
}
regions
}
pub fn kernel_count(&self) -> usize {
self.fuse().len()
}
}
pub fn apply_un(op: UnOp, x: f32) -> f32 {
match op {
UnOp::Neg => -x,
UnOp::Recip => 1.0 / x,
UnOp::Abs => x.abs(),
UnOp::Exp => x.exp(),
UnOp::Tanh => x.tanh(),
UnOp::Rsqrt => 1.0 / x.sqrt(),
UnOp::Sigmoid => 1.0 / (1.0 + (-x).exp()),
UnOp::Silu => x / (1.0 + (-x).exp()),
UnOp::Gelu => {
const C: f32 = 0.797_884_56; 0.5 * x * (1.0 + (C * (x + 0.044715 * x * x * x)).tanh())
}
}
}
pub fn apply_bin(op: BinOp, a: f32, b: f32) -> f32 {
match op {
BinOp::Add => a + b,
BinOp::Sub => a - b,
BinOp::Mul => a * b,
BinOp::Div => a / b,
}
}
fn eval_expr(e: &Expr, cur: f32, inputs: &[&[f32]], i: usize) -> f32 {
match e {
Expr::Cur => cur,
Expr::In(id) => inputs[*id][i],
Expr::Const(c) => *c,
Expr::Un(op, a) => apply_un(*op, eval_expr(a, cur, inputs, i)),
Expr::Bin(op, a, b) => {
apply_bin(*op, eval_expr(a, cur, inputs, i), eval_expr(b, cur, inputs, i))
}
}
}
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
}
pub fn eval(chain: &Chain, inputs: &[&[f32]], n: usize) -> Vec<f32> {
let len = inputs[0].len();
let mut cur: Vec<f32> = inputs[0].to_vec();
for step in &chain.steps {
match step {
Step::Map(e) => cur = (0..len).map(|i| eval_expr(e, cur[i], inputs, i)).collect(),
Step::Reduce(r) => cur = reduce_rows(&cur, *r, n),
}
}
cur
}
pub fn eval_fused(regions: &[Region], inputs: &[&[f32]], n: usize) -> Vec<f32> {
let len = inputs[0].len();
let mut cur: Vec<f32> = inputs[0].to_vec();
for r in regions {
match r {
Region::Map(e) => cur = (0..len).map(|i| eval_expr(e, cur[i], inputs, i)).collect(),
Region::Reduce(red) => cur = reduce_rows(&cur, *red, n),
}
}
cur
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Instr {
Un(UnOp),
BinIn(BinOp, usize),
BinConst(BinOp, u32),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
pub struct Program {
pub instrs: Vec<Instr>,
pub n_sides: usize,
}
#[device]
fn dsigmoid<F: Float>(x: F) -> F {
F::new(1.0) / (F::new(1.0) + (-x).exp())
}
#[device]
fn apply_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 => dsigmoid::<F>(x),
UnOp::Silu => x * dsigmoid::<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 apply_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 fused_interp<F: Float>(
x: &Array<F>,
sides: &Sequence<Array<F>>,
out: &mut Array<F>,
#[comptime] prog: Program,
) {
let i = ABSOLUTE_POS;
if i < out.len() {
let mut acc = x[i];
#[unroll]
for step in 0..prog.instrs.len() {
match comptime!(prog.instrs[step]) {
Instr::Un(op) => acc = apply_un_dev::<F>(op, acc),
Instr::BinIn(op, slot) => acc = apply_bin_dev::<F>(op, acc, sides.index(slot)[i]),
Instr::BinConst(op, bits) => {
let c = comptime!(f32::from_bits(bits));
acc = apply_bin_dev::<F>(op, acc, F::new(c))
}
}
}
out[i] = acc;
}
}
const BLOCK: u32 = 256;
pub struct Fuse<'a> {
primary: &'a [f32],
sides: Vec<&'a [f32]>,
instrs: Vec<Instr>,
}
impl<'a> Fuse<'a> {
pub fn new(primary: &'a [f32]) -> Self {
Fuse { primary, sides: Vec::new(), instrs: Vec::new() }
}
fn push_bin_in(mut self, op: BinOp, rhs: &'a [f32]) -> Self {
let slot = self.sides.len();
self.sides.push(rhs);
self.instrs.push(Instr::BinIn(op, slot));
self
}
fn push_bin_const(mut self, op: BinOp, c: f32) -> Self {
self.instrs.push(Instr::BinConst(op, c.to_bits()));
self
}
fn push_un(mut self, op: UnOp) -> Self {
self.instrs.push(Instr::Un(op));
self
}
pub fn add(self, rhs: &'a [f32]) -> Self {
self.push_bin_in(BinOp::Add, rhs)
}
pub fn sub(self, rhs: &'a [f32]) -> Self {
self.push_bin_in(BinOp::Sub, rhs)
}
pub fn mul(self, rhs: &'a [f32]) -> Self {
self.push_bin_in(BinOp::Mul, rhs)
}
pub fn div(self, rhs: &'a [f32]) -> Self {
self.push_bin_in(BinOp::Div, rhs)
}
pub fn add_scalar(self, c: f32) -> Self {
self.push_bin_const(BinOp::Add, c)
}
pub fn sub_scalar(self, c: f32) -> Self {
self.push_bin_const(BinOp::Sub, c)
}
pub fn mul_scalar(self, c: f32) -> Self {
self.push_bin_const(BinOp::Mul, c)
}
pub fn div_scalar(self, c: f32) -> Self {
self.push_bin_const(BinOp::Div, c)
}
pub fn neg(self) -> Self {
self.push_un(UnOp::Neg)
}
pub fn recip(self) -> Self {
self.push_un(UnOp::Recip)
}
pub fn abs(self) -> Self {
self.push_un(UnOp::Abs)
}
pub fn exp(self) -> Self {
self.push_un(UnOp::Exp)
}
pub fn tanh(self) -> Self {
self.push_un(UnOp::Tanh)
}
pub fn rsqrt(self) -> Self {
self.push_un(UnOp::Rsqrt)
}
pub fn sigmoid(self) -> Self {
self.push_un(UnOp::Sigmoid)
}
pub fn silu(self) -> Self {
self.push_un(UnOp::Silu)
}
pub fn gelu(self) -> Self {
self.push_un(UnOp::Gelu)
}
pub fn compile(&self) -> Program {
Program { instrs: self.instrs.clone(), n_sides: self.sides.len() }
}
pub fn eval_ref(&self) -> Vec<f32> {
let mut acc = self.primary.to_vec();
for ins in &self.instrs {
for (i, a) in acc.iter_mut().enumerate() {
*a = match *ins {
Instr::Un(op) => apply_un(op, *a),
Instr::BinIn(op, slot) => apply_bin(op, *a, self.sides[slot][i]),
Instr::BinConst(op, bits) => apply_bin(op, *a, f32::from_bits(bits)),
};
}
}
acc
}
pub fn run<R: Runtime>(&self, client: &ComputeClient<R>) -> Vec<f32> {
let n = self.primary.len();
let grid = Grid::Static((n as u32).div_ceil(BLOCK), 1, 1);
let xh = client.create_from_slice(f32::as_bytes(self.primary));
let oh = client.create_from_slice(f32::as_bytes(&vec![0.0f32; n]));
let handles: Vec<_> =
self.sides.iter().map(|s| client.create_from_slice(f32::as_bytes(s))).collect();
let mut sides = SequenceArg::new();
for h in &handles {
sides.push(unsafe { ArrayArg::from_raw_parts(h.clone(), n) });
}
unsafe {
fused_interp::launch_unchecked::<f32, R>(
client,
grid,
Block::new_1d(BLOCK),
ArrayArg::from_raw_parts(xh.clone(), n),
sides,
ArrayArg::from_raw_parts(oh.clone(), n),
self.compile(),
);
}
f32::from_bytes(&client.read_one_unchecked(oh)).to_vec()
}
}
#[device]
pub fn dmul<F: Float>(a: F, b: F) -> F {
a * b
}
#[device]
pub fn dadd<F: Float>(a: F, b: F) -> F {
a + b
}
#[device]
pub fn dsilu<F: Float>(x: F) -> F {
x * dsigmoid::<F>(x)
}
#[kernel(targets(cuda, metal, vulkan, webgpu, cpu), unchecked)]
pub fn mul_k<F: Float>(a: &Array<F>, b: &Array<F>, out: &mut Array<F>) {
let i = ABSOLUTE_POS;
if i < out.len() {
out[i] = dmul::<F>(a[i], b[i]);
}
}
#[kernel(targets(cuda, metal, vulkan, webgpu, cpu), unchecked)]
pub fn add_k<F: Float>(a: &Array<F>, b: &Array<F>, out: &mut Array<F>) {
let i = ABSOLUTE_POS;
if i < out.len() {
out[i] = dadd::<F>(a[i], b[i]);
}
}
#[kernel(targets(cuda, metal, vulkan, webgpu, cpu), unchecked)]
pub fn silu_k<F: Float>(x: &Array<F>, out: &mut Array<F>) {
let i = ABSOLUTE_POS;
if i < out.len() {
out[i] = dsilu::<F>(x[i]);
}
}
pub fn swiglu_naive2<R: Runtime>(client: &ComputeClient<R>, a: &[f32], b: &[f32]) -> Vec<f32> {
let n = a.len();
let grid = Grid::Static((n as u32).div_ceil(BLOCK), 1, 1);
let ah = client.create_from_slice(f32::as_bytes(a));
let bh = client.create_from_slice(f32::as_bytes(b));
let t = client.create_from_slice(f32::as_bytes(&vec![0.0f32; n]));
let oh = client.create_from_slice(f32::as_bytes(&vec![0.0f32; n]));
unsafe {
silu_k::launch_unchecked::<f32, R>(
client,
grid.clone(),
Block::new_1d(BLOCK),
ArrayArg::from_raw_parts(ah.clone(), n),
ArrayArg::from_raw_parts(t.clone(), n),
);
mul_k::launch_unchecked::<f32, R>(
client,
grid,
Block::new_1d(BLOCK),
ArrayArg::from_raw_parts(t.clone(), n),
ArrayArg::from_raw_parts(bh.clone(), n),
ArrayArg::from_raw_parts(oh.clone(), n),
);
}
f32::from_bytes(&client.read_one_unchecked(oh)).to_vec()
}
pub fn naive3_run<R: Runtime>(client: &ComputeClient<R>, a: &[f32], b: &[f32], c: &[f32]) -> Vec<f32> {
let n = a.len();
let grid = Grid::Static((n as u32).div_ceil(BLOCK), 1, 1);
let ah = client.create_from_slice(f32::as_bytes(a));
let bh = client.create_from_slice(f32::as_bytes(b));
let ch = client.create_from_slice(f32::as_bytes(c));
let t1 = client.create_from_slice(f32::as_bytes(&vec![0.0f32; n]));
let t2 = client.create_from_slice(f32::as_bytes(&vec![0.0f32; n]));
let oh = client.create_from_slice(f32::as_bytes(&vec![0.0f32; n]));
unsafe {
mul_k::launch_unchecked::<f32, R>(
client,
grid.clone(),
Block::new_1d(BLOCK),
ArrayArg::from_raw_parts(ah.clone(), n),
ArrayArg::from_raw_parts(bh.clone(), n),
ArrayArg::from_raw_parts(t1.clone(), n),
);
add_k::launch_unchecked::<f32, R>(
client,
grid.clone(),
Block::new_1d(BLOCK),
ArrayArg::from_raw_parts(t1.clone(), n),
ArrayArg::from_raw_parts(ch.clone(), n),
ArrayArg::from_raw_parts(t2.clone(), n),
);
silu_k::launch_unchecked::<f32, R>(
client,
grid,
Block::new_1d(BLOCK),
ArrayArg::from_raw_parts(t2.clone(), n),
ArrayArg::from_raw_parts(oh.clone(), n),
);
}
f32::from_bytes(&client.read_one_unchecked(oh)).to_vec()
}
pub const fn path_to_full_auto_fusion() {}
#[cfg(test)]
mod tests {
use super::*;
fn silu_mul_add_chain() -> Chain {
Chain::new()
.map_binary(BinOp::Mul, 1) .map_binary(BinOp::Add, 2) .map_unary(UnOp::Silu) }
#[test]
fn three_maps_fuse_to_one_kernel() {
let chain = silu_mul_add_chain();
assert_eq!(chain.steps.len(), 3, "reified as 3 pointwise steps");
let regions = chain.fuse();
assert_eq!(regions.len(), 1, "all Map -> one fused kernel");
match ®ions[0] {
Region::Map(e) => assert_eq!(e.body(), "silu(((cur * in1[i]) + in2[i]))"),
_ => panic!("expected a Map region"),
}
}
#[test]
fn reduce_is_a_fence() {
let chain = Chain::new()
.map(Expr::cur() * Expr::cur()) .map(Expr::cur()) .reduce(Red::Sum) .map_const(BinOp::Mul, 0.5) .map_unary(UnOp::Rsqrt); assert_eq!(chain.steps.len(), 5);
let regions = chain.fuse();
assert_eq!(regions.len(), 3);
assert!(matches!(regions[0], Region::Map(_)));
assert!(matches!(regions[1], Region::Reduce(Red::Sum)));
assert!(matches!(regions[2], Region::Map(_)));
}
#[test]
fn functor_law_holds_in_the_evaluator() {
let chain = silu_mul_add_chain();
let n = 8usize;
let a: Vec<f32> = (0..n).map(|i| i as f32 * 0.1 - 0.3).collect();
let b: Vec<f32> = (0..n).map(|i| 0.5 - i as f32 * 0.07).collect();
let c: Vec<f32> = (0..n).map(|i| i as f32 * 0.02).collect();
let inputs: [&[f32]; 3] = [&a, &b, &c];
let step_by_step = eval(&chain, &inputs, n);
let fused = eval_fused(&chain.fuse(), &inputs, n);
assert_eq!(step_by_step, fused, "fusion changed the numerics");
}
#[test]
fn every_unop_matches_its_reference_shape() {
for &op in &[UnOp::Silu, UnOp::Gelu, UnOp::Sigmoid, UnOp::Rsqrt, UnOp::Exp, UnOp::Tanh] {
for x in [-2.0f32, -0.3, 0.7, 3.0] {
let y = apply_un(op, if op == UnOp::Rsqrt { x.abs() + 0.1 } else { x });
assert!(y.is_finite(), "{op:?}({x}) not finite: {y}");
}
}
assert!((apply_un(UnOp::Sigmoid, 0.0) - 0.5).abs() < 1e-7);
assert!(apply_un(UnOp::Silu, 0.0).abs() < 1e-7);
assert!(apply_un(UnOp::Gelu, 0.0).abs() < 1e-7);
}
#[cfg(feature = "cpu")]
fn cpu_client() -> ComputeClient<cubecl::cpu::CpuRuntime> {
use cubecl::cpu::{CpuDevice, CpuRuntime};
CpuRuntime::client(&CpuDevice::default())
}
#[cfg(feature = "cpu")]
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()
}
#[cfg(feature = "cpu")]
fn bits(v: &[f32]) -> Vec<u32> {
v.iter().map(|x| x.to_bits()).collect()
}
#[cfg(feature = "cpu")]
#[test]
fn swiglu_tail_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 fused = Fuse::new(&a).silu().mul(&b).run::<cubecl::cpu::CpuRuntime>(&client);
let naive = swiglu_naive2::<cubecl::cpu::CpuRuntime>(&client, &a, &b);
let refv: Vec<f32> =
a.iter().zip(&b).map(|(&x, &y)| apply_un(UnOp::Silu, x) * y).collect();
assert_eq!(bits(&fused), bits(&naive), "fused (1 launch) != naive (2 launches)");
assert_eq!(Fuse::new(&a).silu().mul(&b).compile().instrs.len(), 2);
let maxerr =
fused.iter().zip(&refv).map(|(g, w)| (g - w).abs()).fold(0.0f32, f32::max);
eprintln!("[fuse CPU] SwiGLU tail silu(a)*b: 2 kernels -> 1; bit-exact; max|fused-ref|={maxerr:.2e}");
assert!(maxerr < 1e-5, "reference disagreement {maxerr}");
}
#[cfg(feature = "cpu")]
#[test]
fn three_op_chain_fused_equals_naive_bit_exact() {
let client = cpu_client();
let n = 1024usize;
let a = xorshift_vec(n, 0x2545_f491_4f6c_dd1d);
let w = xorshift_vec(n, 0xdead_beef_cafe_babe);
let b = xorshift_vec(n, 0x0123_4567_89ab_cdef);
let fused = Fuse::new(&a).mul(&w).add(&b).silu().run::<cubecl::cpu::CpuRuntime>(&client);
let naive = naive3_run::<cubecl::cpu::CpuRuntime>(&client, &a, &w, &b);
let refv: Vec<f32> = (0..n)
.map(|i| apply_un(UnOp::Silu, apply_bin(BinOp::Add, a[i] * w[i], b[i])))
.collect();
assert_eq!(bits(&fused), bits(&naive), "fused (1) != naive (3)");
assert_eq!(Fuse::new(&a).mul(&w).add(&b).silu().compile().instrs.len(), 3);
let maxerr =
fused.iter().zip(&refv).map(|(g, w)| (g - w).abs()).fold(0.0f32, f32::max);
eprintln!("[fuse CPU] 3-op silu(a*w+b): 3 kernels -> 1; bit-exact; max|fused-ref|={maxerr:.2e}");
assert!(maxerr < 1e-5, "reference disagreement {maxerr}");
}
#[cfg(feature = "cpu")]
#[test]
fn long_mixed_chain_fused_matches_reference() {
let client = cpu_client();
let n = 2048usize;
let a = xorshift_vec(n, 0xa5a5_5a5a_c3c3_3c3c);
let w = xorshift_vec(n, 0x1111_2222_3333_4444);
let b = xorshift_vec(n, 0x9999_8888_7777_6666);
let sig_w: Vec<f32> = w.iter().map(|&x| apply_un(UnOp::Sigmoid, x)).collect();
let chain = Fuse::new(&a)
.mul_scalar(2.0)
.sub_scalar(1.0)
.abs()
.add(&b)
.div(&sig_w)
.gelu()
.tanh();
let prog = chain.compile();
assert_eq!(prog.instrs.len(), 7, "seven fused instructions, one launch");
let fused = chain.run::<cubecl::cpu::CpuRuntime>(&client);
let refv = chain.eval_ref(); let maxerr =
fused.iter().zip(&refv).map(|(g, r)| (g - r).abs()).fold(0.0f32, f32::max);
eprintln!("[fuse CPU] 7-op mixed chain: one launch; max|fused-ref|={maxerr:.2e}");
assert!(maxerr < 1e-5, "long-chain disagreement {maxerr}");
}
}