1use std::collections::HashMap;
9use std::sync::{Arc, OnceLock, RwLock};
10
11use oxmera_core::{Device, Error, Result, Shape};
12
13use crate::tensor::Tensor;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17#[non_exhaustive]
18pub enum UnaryOp {
19 Neg,
21 Exp,
23 Ln,
25 Abs,
27 Sqrt,
29 Sin,
31 Cos,
33 Tanh,
35 Relu,
37 Gelu,
39 Sigmoid,
41}
42
43impl UnaryOp {
44 pub fn name(self) -> &'static str {
46 match self {
47 UnaryOp::Neg => "neg",
48 UnaryOp::Exp => "exp",
49 UnaryOp::Ln => "ln",
50 UnaryOp::Abs => "abs",
51 UnaryOp::Sqrt => "sqrt",
52 UnaryOp::Sin => "sin",
53 UnaryOp::Cos => "cos",
54 UnaryOp::Tanh => "tanh",
55 UnaryOp::Relu => "relu",
56 UnaryOp::Gelu => "gelu",
57 UnaryOp::Sigmoid => "sigmoid",
58 }
59 }
60
61 pub fn all() -> &'static [UnaryOp] {
63 &[
64 UnaryOp::Neg,
65 UnaryOp::Exp,
66 UnaryOp::Ln,
67 UnaryOp::Abs,
68 UnaryOp::Sqrt,
69 UnaryOp::Sin,
70 UnaryOp::Cos,
71 UnaryOp::Tanh,
72 UnaryOp::Relu,
73 UnaryOp::Gelu,
74 UnaryOp::Sigmoid,
75 ]
76 }
77
78 pub fn eval(self, x: f32) -> f32 {
81 match self {
82 UnaryOp::Neg => -x,
83 UnaryOp::Exp => x.exp(),
84 UnaryOp::Ln => x.ln(),
85 UnaryOp::Abs => x.abs(),
86 UnaryOp::Sqrt => x.sqrt(),
87 UnaryOp::Sin => x.sin(),
88 UnaryOp::Cos => x.cos(),
89 UnaryOp::Tanh => x.tanh(),
90 UnaryOp::Relu => x.max(0.0),
91 UnaryOp::Gelu => {
92 const SQRT_2_OVER_PI: f32 = 0.797_884_6;
93 0.5 * x * (1.0 + (SQRT_2_OVER_PI * (x + 0.044_715 * x * x * x)).tanh())
94 }
95 UnaryOp::Sigmoid => 1.0 / (1.0 + (-x).exp()),
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102#[non_exhaustive]
103pub enum BinaryOp {
104 Add,
106 Sub,
108 Mul,
110 Div,
112 Pow,
114 Maximum,
116 Minimum,
118 Gt,
120 Eq,
122}
123
124impl BinaryOp {
125 pub fn name(self) -> &'static str {
127 match self {
128 BinaryOp::Add => "add",
129 BinaryOp::Sub => "sub",
130 BinaryOp::Mul => "mul",
131 BinaryOp::Div => "div",
132 BinaryOp::Pow => "pow",
133 BinaryOp::Maximum => "maximum",
134 BinaryOp::Minimum => "minimum",
135 BinaryOp::Gt => "gt",
136 BinaryOp::Eq => "eq",
137 }
138 }
139
140 pub fn all() -> &'static [BinaryOp] {
142 &[
143 BinaryOp::Add,
144 BinaryOp::Sub,
145 BinaryOp::Mul,
146 BinaryOp::Div,
147 BinaryOp::Pow,
148 BinaryOp::Maximum,
149 BinaryOp::Minimum,
150 BinaryOp::Gt,
151 BinaryOp::Eq,
152 ]
153 }
154
155 pub fn eval(self, a: f32, b: f32) -> f32 {
157 match self {
158 BinaryOp::Add => a + b,
159 BinaryOp::Sub => a - b,
160 BinaryOp::Mul => a * b,
161 BinaryOp::Div => a / b,
162 BinaryOp::Pow => a.powf(b),
163 BinaryOp::Maximum => a.max(b),
164 BinaryOp::Minimum => a.min(b),
165 BinaryOp::Gt => f32::from(a > b),
166 BinaryOp::Eq => f32::from(a == b),
167 }
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173#[non_exhaustive]
174pub enum ReduceOp {
175 Sum,
177 Max,
179 Min,
181}
182
183impl ReduceOp {
184 pub fn name(self) -> &'static str {
186 match self {
187 ReduceOp::Sum => "sum",
188 ReduceOp::Max => "max",
189 ReduceOp::Min => "min",
190 }
191 }
192
193 pub fn identity(self) -> f32 {
195 match self {
196 ReduceOp::Sum => 0.0,
197 ReduceOp::Max => f32::NEG_INFINITY,
198 ReduceOp::Min => f32::INFINITY,
199 }
200 }
201
202 pub fn combine(self, acc: f32, x: f32) -> f32 {
204 match self {
205 ReduceOp::Sum => acc + x,
206 ReduceOp::Max => acc.max(x),
207 ReduceOp::Min => acc.min(x),
208 }
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct MatmulPlan {
224 pub batch: usize,
226 pub m: usize,
228 pub k: usize,
230 pub n: usize,
232 pub a_batch_stride: usize,
234 pub b_batch_stride: usize,
236 pub out_shape: Shape,
238}
239
240pub fn plan_matmul(a: &Shape, b: &Shape) -> Result<MatmulPlan> {
243 let (ad, bd) = (a.dims(), b.dims());
244 let (ba, m, k) = match ad {
245 [m, k] => (1usize, *m, *k),
246 [b, m, k] => (*b, *m, *k),
247 _ => {
248 return Err(Error::InvalidArgument {
249 op: "matmul",
250 detail: format!(
251 "supported ranks are 2 and 3 (batched); got {}x{}",
252 ad.len(),
253 bd.len()
254 ),
255 });
256 }
257 };
258 let (bb, kb, n) = match bd {
259 [k, n] => (1usize, *k, *n),
260 [b, k, n] => (*b, *k, *n),
261 _ => {
262 return Err(Error::InvalidArgument {
263 op: "matmul",
264 detail: format!(
265 "supported ranks are 2 and 3 (batched); got {}x{}",
266 ad.len(),
267 bd.len()
268 ),
269 });
270 }
271 };
272 if k != kb {
273 return Err(Error::ShapeMismatch {
274 expected: if bd.len() == 3 {
275 Shape::from([bb, k, n])
276 } else {
277 Shape::from([k, n])
278 },
279 got: b.clone(),
280 op: "matmul",
281 });
282 }
283 let batch = match (ba, bb) {
284 (x, y) if x == y => x,
285 (1, y) => y,
286 (x, 1) => x,
287 _ => {
289 return Err(Error::BroadcastIncompatible {
290 lhs: a.clone(),
291 rhs: b.clone(),
292 });
293 }
294 };
295 let out_shape = if ad.len() == 2 && bd.len() == 2 {
296 Shape::from([m, n])
297 } else {
298 Shape::from([batch, m, n])
299 };
300 Ok(MatmulPlan {
301 batch,
302 m,
303 k,
304 n,
305 a_batch_stride: if ba == 1 { 0 } else { m * k },
306 b_batch_stride: if bb == 1 { 0 } else { k * n },
307 out_shape,
308 })
309}
310
311pub trait Backend: Send + Sync {
317 fn device(&self) -> Device;
319
320 fn name(&self) -> &'static str;
322
323 fn unary(&self, op: UnaryOp, a: &Tensor) -> Result<Tensor>;
326
327 fn binary(&self, op: BinaryOp, a: &Tensor, b: &Tensor) -> Result<Tensor>;
330
331 fn matmul(&self, a: &Tensor, b: &Tensor) -> Result<Tensor>;
334
335 fn reduce(&self, op: ReduceOp, a: &Tensor, axes: &[usize], keepdim: bool) -> Result<Tensor>;
338
339 fn argmax(&self, a: &Tensor, dim: usize, keepdim: bool) -> Result<Tensor>;
341
342 fn contiguous(&self, a: &Tensor) -> Result<Tensor>;
344
345 fn download(&self, a: &Tensor) -> Result<Tensor>;
347
348 fn upload(&self, a: &Tensor) -> Result<Tensor>;
350
351 fn index_select(&self, a: &Tensor, dim: usize, indices: &Tensor) -> Result<Tensor> {
356 let _ = (dim, indices);
357 Err(Error::NotImplemented {
358 op: "index_select",
359 detail: format!("backend {}", a.device().kind_name()),
360 })
361 }
362
363 fn index_add(&self, a: &Tensor, dim: usize, indices: &Tensor, src: &Tensor) -> Result<Tensor> {
367 let _ = (dim, indices, src);
368 Err(Error::NotImplemented {
369 op: "index_add",
370 detail: format!("backend {}", a.device().kind_name()),
371 })
372 }
373}
374
375type Registry = RwLock<HashMap<Device, Arc<dyn Backend>>>;
376
377fn registry() -> &'static Registry {
378 static REGISTRY: OnceLock<Registry> = OnceLock::new();
379 REGISTRY.get_or_init(|| RwLock::new(HashMap::new()))
380}
381
382pub fn register_backend(backend: Arc<dyn Backend>) {
387 registry()
388 .write()
389 .expect("backend registry poisoned")
390 .insert(backend.device(), backend);
391}
392
393pub fn backend_for(device: Device) -> Result<Arc<dyn Backend>> {
399 let found = registry()
400 .read()
401 .expect("backend registry poisoned")
402 .get(&device)
403 .cloned();
404 match found {
405 Some(b) => Ok(b),
406 None if device == Device::Cpu => {
407 crate::cpu::register();
408 registry()
409 .read()
410 .expect("backend registry poisoned")
411 .get(&device)
412 .cloned()
413 .ok_or(Error::BackendUnavailable { device })
414 }
415 None => Err(Error::BackendUnavailable { device }),
416 }
417}
418
419pub fn registered_devices() -> Vec<Device> {
421 let mut devices: Vec<Device> = registry()
422 .read()
423 .expect("backend registry poisoned")
424 .keys()
425 .copied()
426 .collect();
427 devices.sort_by_key(|d| (d.kind_name(), device_index(*d)));
428 devices
429}
430
431fn device_index(d: Device) -> usize {
432 match d {
433 Device::Cpu => 0,
434 Device::Metal { index } | Device::Cuda { index } => index,
435 _ => 0,
436 }
437}