use crate::core::op::Op;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum Atom {
Mul,
Add,
Cmp(CmpOp),
Exp,
Read,
Write,
Reduce(ReduceOp),
Slide(SlidePattern),
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum CmpOp {
Max,
Min,
LessThan,
GreaterThan,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum ReduceOp {
Sum,
Max,
Mean,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum SlidePattern {
Window1D { kernel: usize, stride: usize },
Window2D { kernel: (usize, usize), stride: (usize, usize) },
Window3D { kernel: (usize, usize, usize), stride: (usize, usize, usize) },
}
pub fn decompose(op: &Op) -> Vec<Atom> {
match op {
Op::Matmul => vec![
Atom::Slide(SlidePattern::Window1D { kernel: 1, stride: 1 }),
Atom::Mul,
Atom::Reduce(ReduceOp::Sum),
],
Op::Add => vec![Atom::Add],
Op::Mul => vec![Atom::Mul],
Op::Sub => vec![Atom::Add, Atom::Mul], Op::Div => vec![Atom::Mul, Atom::Exp],
Op::Transpose { .. } => vec![Atom::Read],
Op::Reshape { .. } => vec![],
Op::Permute { .. } => vec![Atom::Read],
Op::Concat { .. } => vec![Atom::Write],
Op::Split { .. } => vec![Atom::Read],
Op::Chunk { .. } => vec![Atom::Read],
Op::Clamp { .. } => vec![Atom::Cmp(CmpOp::Max), Atom::Cmp(CmpOp::Min)],
Op::NanToNum { .. } => vec![Atom::Cmp(CmpOp::LessThan), Atom::Mul, Atom::Add],
Op::Argmax { .. } => vec![Atom::Cmp(CmpOp::GreaterThan), Atom::Reduce(ReduceOp::Max)],
Op::Sdpa { .. } | Op::SdpaCross { .. } | Op::FlashAttention { .. } => vec![
Atom::Mul, Atom::Reduce(ReduceOp::Sum), Atom::Exp, Atom::Reduce(ReduceOp::Sum), Atom::Mul,
Atom::Mul, Atom::Reduce(ReduceOp::Sum), ],
Op::SdpaWindow { .. } => vec![
Atom::Slide(SlidePattern::Window1D { kernel: 1, stride: 1 }),
Atom::Mul, Atom::Reduce(ReduceOp::Sum),
Atom::Exp, Atom::Reduce(ReduceOp::Sum),
Atom::Mul,
Atom::Mul, Atom::Reduce(ReduceOp::Sum),
],
Op::KvCache => vec![Atom::Write, Atom::Read],
Op::KvCompress { .. } | Op::KvDecompress { .. } => vec![Atom::Read, Atom::Write],
Op::Rope { .. } => vec![Atom::Mul, Atom::Add],
Op::SinusoidalEmbed { .. } => vec![Atom::Mul, Atom::Exp],
Op::RelativePosEmbedding { .. } => vec![Atom::Read],
Op::TokenEmbed | Op::PosEmbed => vec![Atom::Read],
Op::RmsNorm { .. } => vec![
Atom::Mul, Atom::Reduce(ReduceOp::Sum),
Atom::Exp, Atom::Mul, ],
Op::LayerNorm { .. } => vec![
Atom::Reduce(ReduceOp::Mean),
Atom::Add,
Atom::Mul,
Atom::Reduce(ReduceOp::Mean),
Atom::Mul,
Atom::Add,
],
Op::BatchNorm { .. } => vec![Atom::Add, Atom::Mul, Atom::Mul, Atom::Add],
Op::GroupNorm { .. } => vec![
Atom::Reduce(ReduceOp::Mean),
Atom::Add,
Atom::Mul,
Atom::Reduce(ReduceOp::Mean),
Atom::Mul,
Atom::Add,
],
Op::InstanceNorm { .. } => vec![
Atom::Reduce(ReduceOp::Mean),
Atom::Add,
Atom::Mul,
Atom::Reduce(ReduceOp::Mean),
Atom::Mul,
],
Op::AdaLN => vec![Atom::Mul, Atom::Add],
Op::Silu => vec![Atom::Mul, Atom::Exp, Atom::Add, Atom::Mul],
Op::Gelu { .. } => vec![Atom::Mul, Atom::Exp, Atom::Add, Atom::Mul],
Op::GeGlu => vec![Atom::Mul, Atom::Exp, Atom::Add, Atom::Mul, Atom::Mul],
Op::SwiGlu => vec![Atom::Mul, Atom::Exp, Atom::Add, Atom::Mul, Atom::Mul],
Op::Glu => vec![Atom::Exp, Atom::Add, Atom::Mul],
Op::Relu => vec![Atom::Cmp(CmpOp::Max)],
Op::LeakyRelu { .. } => vec![Atom::Cmp(CmpOp::Max), Atom::Mul, Atom::Add],
Op::PRelu => vec![Atom::Cmp(CmpOp::Max), Atom::Mul, Atom::Add],
Op::Sigmoid => vec![Atom::Exp, Atom::Add, Atom::Mul],
Op::Tanh => vec![Atom::Exp, Atom::Add, Atom::Mul],
Op::Softmax { .. } => vec![Atom::Exp, Atom::Reduce(ReduceOp::Sum), Atom::Mul],
Op::Conv1d { kernel, stride, .. } => vec![
Atom::Slide(SlidePattern::Window1D {
kernel: *kernel as usize,
stride: *stride as usize,
}),
Atom::Mul,
Atom::Reduce(ReduceOp::Sum),
],
Op::Conv2d { kernel, stride, .. } => vec![
Atom::Slide(SlidePattern::Window2D {
kernel: (kernel.0 as usize, kernel.1 as usize),
stride: (stride.0 as usize, stride.1 as usize),
}),
Atom::Mul,
Atom::Reduce(ReduceOp::Sum),
],
Op::Conv3d { kernel, stride, .. } => vec![
Atom::Slide(SlidePattern::Window3D {
kernel: (kernel.0 as usize, kernel.1 as usize, kernel.2 as usize),
stride: (stride.0 as usize, stride.1 as usize, stride.2 as usize),
}),
Atom::Mul,
Atom::Reduce(ReduceOp::Sum),
],
Op::ConvTranspose2d { kernel, stride, .. } => vec![
Atom::Slide(SlidePattern::Window2D {
kernel: (kernel.0 as usize, kernel.1 as usize),
stride: (stride.0 as usize, stride.1 as usize),
}),
Atom::Mul,
Atom::Reduce(ReduceOp::Sum),
],
Op::CausalConv1d { kernel } => vec![
Atom::Slide(SlidePattern::Window1D {
kernel: *kernel as usize,
stride: 1,
}),
Atom::Mul,
Atom::Reduce(ReduceOp::Sum),
],
Op::DepthwiseConv { kernel, stride } => vec![
Atom::Slide(SlidePattern::Window1D {
kernel: *kernel as usize,
stride: *stride as usize,
}),
Atom::Mul,
Atom::Reduce(ReduceOp::Sum),
],
Op::Pool { kernel, stride, .. } => vec![
Atom::Slide(SlidePattern::Window2D {
kernel: (kernel.0 as usize, kernel.1 as usize),
stride: (stride.0 as usize, stride.1 as usize),
}),
Atom::Reduce(ReduceOp::Max),
],
Op::Interpolate { .. } => vec![Atom::Read, Atom::Mul, Atom::Add],
Op::PixelShuffle { .. } => vec![],
Op::PixelUnshuffle { .. } => vec![],
Op::PatchEmbed { .. } => vec![
Atom::Slide(SlidePattern::Window2D {
kernel: (16, 16),
stride: (16, 16),
}),
Atom::Mul,
Atom::Reduce(ReduceOp::Sum),
],
Op::Unpatchify => vec![Atom::Write],
Op::NoiseSchedule => vec![Atom::Mul, Atom::Exp],
Op::FlowStep => vec![Atom::Mul, Atom::Add, Atom::Exp],
Op::Quantize { .. } => vec![Atom::Mul, Atom::Cmp(CmpOp::Max), Atom::Cmp(CmpOp::Min)],
Op::Dequantize => vec![Atom::Mul, Atom::Add],
Op::Sample { .. } => vec![
Atom::Exp,
Atom::Reduce(ReduceOp::Sum),
Atom::Mul,
Atom::Cmp(CmpOp::Max),
],
Op::LoraApply { .. } => vec![
Atom::Slide(SlidePattern::Window1D { kernel: 1, stride: 1 }),
Atom::Mul, Atom::Reduce(ReduceOp::Sum), Atom::Mul, Atom::Reduce(ReduceOp::Sum), Atom::Mul, Atom::Add, ],
Op::Kron => vec![Atom::Mul],
Op::MatrixInverse => vec![Atom::Mul, Atom::Add, Atom::Reduce(ReduceOp::Sum)],
Op::FusedNormMatmul { .. } => vec![
Atom::Mul, Atom::Reduce(ReduceOp::Sum), Atom::Exp, Atom::Mul,
Atom::Slide(SlidePattern::Window1D { kernel: 1, stride: 1 }),
Atom::Mul, Atom::Reduce(ReduceOp::Sum),
],
Op::FusedSkipNorm { .. } => vec![
Atom::Add,
Atom::Mul, Atom::Reduce(ReduceOp::Sum), Atom::Exp, Atom::Mul,
],
Op::FusedSwiGlu => vec![
Atom::Mul, Atom::Exp, Atom::Add, Atom::Mul,
Atom::Mul,
],
}
}
pub struct AtomInterpreter;
impl AtomInterpreter {
pub fn execute(atoms: &[Atom], inputs: &[&[f32]], output: &mut [f32]) {
for atom in atoms {
match atom {
Atom::Mul => {
for i in 0..output.len() {
output[i] = inputs[0].get(i).copied().unwrap_or(0.0)
* inputs[1].get(i).copied().unwrap_or(1.0);
}
}
Atom::Add => {
for i in 0..output.len() {
output[i] = inputs[0].get(i).copied().unwrap_or(0.0)
+ inputs[1].get(i).copied().unwrap_or(0.0);
}
}
Atom::Exp => {
for i in 0..output.len() {
output[i] = inputs[0].get(i).copied().unwrap_or(0.0).exp();
}
}
Atom::Cmp(CmpOp::Max) => {
for i in 0..output.len() {
output[i] = inputs[0].get(i).copied().unwrap_or(0.0).max(0.0);
}
}
Atom::Cmp(CmpOp::Min) => {
for i in 0..output.len() {
output[i] = inputs[0].get(i).copied().unwrap_or(0.0).min(0.0);
}
}
Atom::Cmp(CmpOp::LessThan) => {
for i in 0..output.len() {
let a = inputs[0].get(i).copied().unwrap_or(0.0);
let b = inputs[1].get(i).copied().unwrap_or(0.0);
output[i] = if a < b { 1.0 } else { 0.0 };
}
}
Atom::Cmp(CmpOp::GreaterThan) => {
for i in 0..output.len() {
let a = inputs[0].get(i).copied().unwrap_or(0.0);
let b = inputs[1].get(i).copied().unwrap_or(0.0);
output[i] = if a > b { 1.0 } else { 0.0 };
}
}
Atom::Reduce(ReduceOp::Sum) => {
output[0] = inputs[0].iter().sum();
}
Atom::Reduce(ReduceOp::Max) => {
output[0] = inputs[0]
.iter()
.copied()
.fold(f32::NEG_INFINITY, f32::max);
}
Atom::Reduce(ReduceOp::Mean) => {
let n = inputs[0].len() as f32;
output[0] = if n > 0.0 {
inputs[0].iter().sum::<f32>() / n
} else {
0.0
};
}
Atom::Read | Atom::Write => {
let len = output.len().min(inputs[0].len());
output[..len].copy_from_slice(&inputs[0][..len]);
}
Atom::Slide(_) => {
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matmul_decomposes_to_slide_mul_reduce() {
let atoms = decompose(&Op::Matmul);
assert_eq!(atoms.len(), 3);
assert!(matches!(atoms[0], Atom::Slide(_)));
assert_eq!(atoms[1], Atom::Mul);
assert!(matches!(atoms[2], Atom::Reduce(ReduceOp::Sum)));
}
#[test]
fn layout_ops_have_no_atoms() {
assert!(decompose(&Op::Reshape { shape: vec![1, -1] }).is_empty());
assert!(decompose(&Op::PixelShuffle { upscale_factor: 2 }).is_empty());
}
#[test]
fn interpreter_add_mul_reduce_relu_exp() {
let a = [1.0f32, 2.0, 3.0];
let b = [4.0f32, 5.0, 6.0];
let mut out = [0.0f32; 3];
AtomInterpreter::execute(&[Atom::Add], &[&a, &b], &mut out);
assert_eq!(out, [5.0, 7.0, 9.0]);
AtomInterpreter::execute(&[Atom::Mul], &[&a, &b], &mut out);
assert_eq!(out, [4.0, 10.0, 18.0]);
let mut one = [0.0f32; 1];
AtomInterpreter::execute(&[Atom::Reduce(ReduceOp::Sum)], &[&a], &mut one);
assert_eq!(one[0], 6.0);
let neg = [-1.0f32, 0.0, 2.0, -3.0];
let mut four = [0.0f32; 4];
AtomInterpreter::execute(&[Atom::Cmp(CmpOp::Max)], &[&neg], &mut four);
assert_eq!(four, [0.0, 0.0, 2.0, 0.0]);
let z = [0.0f32, 1.0];
let mut two = [0.0f32; 2];
AtomInterpreter::execute(&[Atom::Exp], &[&z], &mut two);
assert!((two[0] - 1.0).abs() < 1e-6);
assert!((two[1] - std::f32::consts::E).abs() < 1e-5);
}
#[test]
fn every_op_variant_decomposes_without_panic() {
use crate::core::op::{InterpolateMode, PoolMode, SampleMethod};
let ops = [
Op::Matmul, Op::Add, Op::Mul, Op::Sub, Op::Div,
Op::Transpose { perm: vec![0, 1] },
Op::Reshape { shape: vec![1, -1] },
Op::Permute { dims: vec![0, 1] },
Op::Concat { axis: 0 },
Op::Split { axis: 0, sizes: vec![1] },
Op::Chunk { axis: 0, chunks: 2 },
Op::Clamp { min: Some(-1.0), max: Some(1.0) },
Op::NanToNum { nan: 0.0, posinf: 1e6, neginf: -1e6 },
Op::Argmax { dim: -1 },
Op::Sdpa { num_heads: 8, kv_heads: 8, head_dim: 64, causal: true },
Op::SdpaCross { num_heads: 8, head_dim: 64 },
Op::SdpaWindow { num_heads: 8, head_dim: 64, window_size: 7 },
Op::KvCache,
Op::KvCompress { head_dim: 64, bits: 4 },
Op::KvDecompress { head_dim: 64, bits: 4 },
Op::Rope { head_dim: 64, rope_dim: 64, base: 10_000.0 },
Op::SinusoidalEmbed { dim: 256 },
Op::RelativePosEmbedding { num_buckets: 32 },
Op::TokenEmbed, Op::PosEmbed,
Op::RmsNorm { eps: 1e-5 }, Op::LayerNorm { eps: 1e-5 },
Op::BatchNorm { eps: 1e-5, momentum: 0.1 },
Op::GroupNorm { num_groups: 32, eps: 1e-5 },
Op::InstanceNorm { eps: 1e-5 }, Op::AdaLN,
Op::Silu, Op::Gelu { approximate: true },
Op::GeGlu, Op::SwiGlu, Op::Glu,
Op::Relu, Op::LeakyRelu { slope: 0.2 }, Op::PRelu,
Op::Sigmoid, Op::Tanh, Op::Softmax { dim: -1 },
Op::Conv1d { kernel: 3, stride: 1, padding: 1, dilation: 1, groups: 1 },
Op::Conv2d {
kernel: (3, 3), stride: (1, 1), padding: (1, 1),
dilation: (1, 1), groups: 1,
},
Op::Conv3d {
kernel: (3, 3, 3), stride: (1, 1, 1), padding: (1, 1, 1),
dilation: (1, 1, 1), groups: 1,
},
Op::ConvTranspose2d { kernel: (3, 3), stride: (2, 2), padding: (1, 1) },
Op::CausalConv1d { kernel: 3 },
Op::DepthwiseConv { kernel: 3, stride: 1 },
Op::Pool {
mode: PoolMode::Max,
kernel: (2, 2),
stride: (2, 2),
padding: (0, 0),
},
Op::Interpolate { mode: InterpolateMode::Nearest, scale: 2.0 },
Op::PixelShuffle { upscale_factor: 2 },
Op::PixelUnshuffle { downscale_factor: 2 },
Op::PatchEmbed { patch_size: 16 },
Op::Unpatchify,
Op::NoiseSchedule, Op::FlowStep,
Op::Quantize { dtype: crate::core::dtype::DType::Q4_0 },
Op::Dequantize,
Op::Sample { method: SampleMethod::Greedy },
Op::LoraApply { rank: 16, alpha: 1.0 },
Op::Kron, Op::MatrixInverse,
Op::FusedNormMatmul { eps: 1e-5 },
Op::FusedSkipNorm { eps: 1e-5 },
Op::FusedSwiGlu,
Op::FlashAttention { num_heads: 8, kv_heads: 8, head_dim: 64 },
];
for op in &ops {
let _ = decompose(op);
}
}
}