use std::collections::HashMap;
use std::sync::{Arc, OnceLock, RwLock};
use oxmera_core::{Device, Error, Result, Shape};
use crate::tensor::Tensor;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum UnaryOp {
Neg,
Exp,
Ln,
Abs,
Sqrt,
Sin,
Cos,
Tanh,
Relu,
Gelu,
Sigmoid,
}
impl UnaryOp {
pub fn name(self) -> &'static str {
match self {
UnaryOp::Neg => "neg",
UnaryOp::Exp => "exp",
UnaryOp::Ln => "ln",
UnaryOp::Abs => "abs",
UnaryOp::Sqrt => "sqrt",
UnaryOp::Sin => "sin",
UnaryOp::Cos => "cos",
UnaryOp::Tanh => "tanh",
UnaryOp::Relu => "relu",
UnaryOp::Gelu => "gelu",
UnaryOp::Sigmoid => "sigmoid",
}
}
pub fn all() -> &'static [UnaryOp] {
&[
UnaryOp::Neg,
UnaryOp::Exp,
UnaryOp::Ln,
UnaryOp::Abs,
UnaryOp::Sqrt,
UnaryOp::Sin,
UnaryOp::Cos,
UnaryOp::Tanh,
UnaryOp::Relu,
UnaryOp::Gelu,
UnaryOp::Sigmoid,
]
}
pub fn eval(self, x: f32) -> f32 {
match self {
UnaryOp::Neg => -x,
UnaryOp::Exp => x.exp(),
UnaryOp::Ln => x.ln(),
UnaryOp::Abs => x.abs(),
UnaryOp::Sqrt => x.sqrt(),
UnaryOp::Sin => x.sin(),
UnaryOp::Cos => x.cos(),
UnaryOp::Tanh => x.tanh(),
UnaryOp::Relu => x.max(0.0),
UnaryOp::Gelu => {
const SQRT_2_OVER_PI: f32 = 0.797_884_6;
0.5 * x * (1.0 + (SQRT_2_OVER_PI * (x + 0.044_715 * x * x * x)).tanh())
}
UnaryOp::Sigmoid => 1.0 / (1.0 + (-x).exp()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BinaryOp {
Add,
Sub,
Mul,
Div,
Pow,
Maximum,
Minimum,
Gt,
Eq,
}
impl BinaryOp {
pub fn name(self) -> &'static str {
match self {
BinaryOp::Add => "add",
BinaryOp::Sub => "sub",
BinaryOp::Mul => "mul",
BinaryOp::Div => "div",
BinaryOp::Pow => "pow",
BinaryOp::Maximum => "maximum",
BinaryOp::Minimum => "minimum",
BinaryOp::Gt => "gt",
BinaryOp::Eq => "eq",
}
}
pub fn all() -> &'static [BinaryOp] {
&[
BinaryOp::Add,
BinaryOp::Sub,
BinaryOp::Mul,
BinaryOp::Div,
BinaryOp::Pow,
BinaryOp::Maximum,
BinaryOp::Minimum,
BinaryOp::Gt,
BinaryOp::Eq,
]
}
pub fn eval(self, a: f32, b: f32) -> f32 {
match self {
BinaryOp::Add => a + b,
BinaryOp::Sub => a - b,
BinaryOp::Mul => a * b,
BinaryOp::Div => a / b,
BinaryOp::Pow => a.powf(b),
BinaryOp::Maximum => a.max(b),
BinaryOp::Minimum => a.min(b),
BinaryOp::Gt => f32::from(a > b),
BinaryOp::Eq => f32::from(a == b),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ReduceOp {
Sum,
Max,
Min,
}
impl ReduceOp {
pub fn name(self) -> &'static str {
match self {
ReduceOp::Sum => "sum",
ReduceOp::Max => "max",
ReduceOp::Min => "min",
}
}
pub fn identity(self) -> f32 {
match self {
ReduceOp::Sum => 0.0,
ReduceOp::Max => f32::NEG_INFINITY,
ReduceOp::Min => f32::INFINITY,
}
}
pub fn combine(self, acc: f32, x: f32) -> f32 {
match self {
ReduceOp::Sum => acc + x,
ReduceOp::Max => acc.max(x),
ReduceOp::Min => acc.min(x),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatmulPlan {
pub batch: usize,
pub m: usize,
pub k: usize,
pub n: usize,
pub a_batch_stride: usize,
pub b_batch_stride: usize,
pub out_shape: Shape,
}
pub fn plan_matmul(a: &Shape, b: &Shape) -> Result<MatmulPlan> {
let (ad, bd) = (a.dims(), b.dims());
let (ba, m, k) = match ad {
[m, k] => (1usize, *m, *k),
[b, m, k] => (*b, *m, *k),
_ => {
return Err(Error::InvalidArgument {
op: "matmul",
detail: format!(
"supported ranks are 2 and 3 (batched); got {}x{}",
ad.len(),
bd.len()
),
});
}
};
let (bb, kb, n) = match bd {
[k, n] => (1usize, *k, *n),
[b, k, n] => (*b, *k, *n),
_ => {
return Err(Error::InvalidArgument {
op: "matmul",
detail: format!(
"supported ranks are 2 and 3 (batched); got {}x{}",
ad.len(),
bd.len()
),
});
}
};
if k != kb {
return Err(Error::ShapeMismatch {
expected: if bd.len() == 3 {
Shape::from([bb, k, n])
} else {
Shape::from([k, n])
},
got: b.clone(),
op: "matmul",
});
}
let batch = match (ba, bb) {
(x, y) if x == y => x,
(1, y) => y,
(x, 1) => x,
_ => {
return Err(Error::BroadcastIncompatible {
lhs: a.clone(),
rhs: b.clone(),
});
}
};
let out_shape = if ad.len() == 2 && bd.len() == 2 {
Shape::from([m, n])
} else {
Shape::from([batch, m, n])
};
Ok(MatmulPlan {
batch,
m,
k,
n,
a_batch_stride: if ba == 1 { 0 } else { m * k },
b_batch_stride: if bb == 1 { 0 } else { k * n },
out_shape,
})
}
#[derive(Debug, Clone, Copy)]
pub struct AdamStep<'a> {
pub param: &'a Tensor,
pub grad: &'a Tensor,
pub m: Option<&'a Tensor>,
pub v: Option<&'a Tensor>,
pub lr: f32,
pub beta1: f32,
pub beta2: f32,
pub eps: f32,
pub weight_decay: f32,
pub decoupled: bool,
pub bias_correction1: f32,
pub bias_correction2: f32,
}
pub trait Backend: Send + Sync {
fn device(&self) -> Device;
fn name(&self) -> &'static str;
fn unary(&self, op: UnaryOp, a: &Tensor) -> Result<Tensor>;
fn binary(&self, op: BinaryOp, a: &Tensor, b: &Tensor) -> Result<Tensor>;
fn matmul(&self, a: &Tensor, b: &Tensor) -> Result<Tensor>;
fn reduce(&self, op: ReduceOp, a: &Tensor, axes: &[usize], keepdim: bool) -> Result<Tensor>;
fn argmax(&self, a: &Tensor, dim: usize, keepdim: bool) -> Result<Tensor>;
fn contiguous(&self, a: &Tensor) -> Result<Tensor>;
fn download(&self, a: &Tensor) -> Result<Tensor>;
fn upload(&self, a: &Tensor) -> Result<Tensor>;
fn index_select(&self, a: &Tensor, dim: usize, indices: &Tensor) -> Result<Tensor> {
let _ = (dim, indices);
Err(Error::NotImplemented {
op: "index_select",
detail: format!("backend {}", a.device().kind_name()),
})
}
fn index_add(&self, a: &Tensor, dim: usize, indices: &Tensor, src: &Tensor) -> Result<Tensor> {
let _ = (dim, indices, src);
Err(Error::NotImplemented {
op: "index_add",
detail: format!("backend {}", a.device().kind_name()),
})
}
fn cholesky(&self, a: &Tensor) -> Result<Tensor> {
Err(Error::NotImplemented {
op: "cholesky",
detail: format!("backend {}", a.device().kind_name()),
})
}
fn eigh(&self, a: &Tensor) -> Result<(Tensor, Tensor)> {
Err(Error::NotImplemented {
op: "eigh",
detail: format!("backend {}", a.device().kind_name()),
})
}
fn adam_step(&self, step: &AdamStep<'_>) -> Result<(Tensor, Tensor, Tensor)> {
Err(Error::NotImplemented {
op: "adam_step",
detail: format!("backend {}", step.param.device().kind_name()),
})
}
}
type Registry = RwLock<HashMap<Device, Arc<dyn Backend>>>;
fn registry() -> &'static Registry {
static REGISTRY: OnceLock<Registry> = OnceLock::new();
REGISTRY.get_or_init(|| RwLock::new(HashMap::new()))
}
pub fn register_backend(backend: Arc<dyn Backend>) {
registry()
.write()
.expect("backend registry poisoned")
.insert(backend.device(), backend);
}
pub fn backend_for(device: Device) -> Result<Arc<dyn Backend>> {
let found = registry()
.read()
.expect("backend registry poisoned")
.get(&device)
.cloned();
match found {
Some(b) => Ok(b),
None if device == Device::Cpu => {
crate::cpu::register();
registry()
.read()
.expect("backend registry poisoned")
.get(&device)
.cloned()
.ok_or(Error::BackendUnavailable { device })
}
None => Err(Error::BackendUnavailable { device }),
}
}
pub fn registered_devices() -> Vec<Device> {
let mut devices: Vec<Device> = registry()
.read()
.expect("backend registry poisoned")
.keys()
.copied()
.collect();
devices.sort_by_key(|d| (d.kind_name(), device_index(*d)));
devices
}
fn device_index(d: Device) -> usize {
match d {
Device::Cpu => 0,
Device::Metal { index } | Device::Cuda { index } => index,
_ => 0,
}
}