1use oxmera_core::shape::broadcast_shapes;
9use oxmera_core::{DType, Error, Result, Shape};
10use rayon::prelude::*;
11
12use crate::backend::{BinaryOp, ReduceOp, UnaryOp};
13use crate::cpu_iter::OffsetWalker;
14use crate::tensor::Tensor;
15
16const PAR_THRESHOLD: usize = 16 * 1024;
17
18pub(crate) fn f64_input<'t>(t: &'t Tensor, op: &'static str) -> Result<&'t [f64]> {
19 if t.dtype() != DType::F64 {
20 return Err(Error::UnsupportedDType {
21 dtype: t.dtype(),
22 op,
23 });
24 }
25 t.storage().cpu()?.f64s()
26}
27
28impl UnaryOp {
29 pub fn eval_f64(self, x: f64) -> f64 {
31 match self {
32 UnaryOp::Neg => -x,
33 UnaryOp::Exp => x.exp(),
34 UnaryOp::Ln => x.ln(),
35 UnaryOp::Abs => x.abs(),
36 UnaryOp::Sqrt => x.sqrt(),
37 UnaryOp::Sin => x.sin(),
38 UnaryOp::Cos => x.cos(),
39 UnaryOp::Tanh => x.tanh(),
40 UnaryOp::Relu => x.max(0.0),
41 UnaryOp::Gelu => {
42 const SQRT_2_OVER_PI: f64 = 0.797_884_560_802_865_4;
43 0.5 * x * (1.0 + (SQRT_2_OVER_PI * (x + 0.044_715 * x * x * x)).tanh())
44 }
45 UnaryOp::Sigmoid => 1.0 / (1.0 + (-x).exp()),
46 }
47 }
48}
49
50impl BinaryOp {
51 pub fn eval_f64(self, a: f64, b: f64) -> f64 {
53 match self {
54 BinaryOp::Add => a + b,
55 BinaryOp::Sub => a - b,
56 BinaryOp::Mul => a * b,
57 BinaryOp::Div => a / b,
58 BinaryOp::Pow => a.powf(b),
59 BinaryOp::Maximum => a.max(b),
60 BinaryOp::Minimum => a.min(b),
61 BinaryOp::Gt => f64::from(a > b),
62 BinaryOp::Eq => f64::from(a == b),
63 }
64 }
65}
66
67impl ReduceOp {
68 pub fn identity_f64(self) -> f64 {
70 match self {
71 ReduceOp::Sum => 0.0,
72 ReduceOp::Max => f64::NEG_INFINITY,
73 ReduceOp::Min => f64::INFINITY,
74 }
75 }
76
77 pub fn combine_f64(self, acc: f64, x: f64) -> f64 {
79 match self {
80 ReduceOp::Sum => acc + x,
81 ReduceOp::Max => acc.max(x),
82 ReduceOp::Min => acc.min(x),
83 }
84 }
85}
86
87fn fill_par(out: &mut [f64], f: impl Fn(usize, &mut [f64]) + Sync) {
88 if out.len() >= PAR_THRESHOLD {
89 let chunk = PAR_THRESHOLD / 4;
90 out.par_chunks_mut(chunk)
91 .enumerate()
92 .for_each(|(i, c)| f(i * chunk, c));
93 } else {
94 f(0, out);
95 }
96}
97
98pub(crate) fn unary(op: UnaryOp, a: &Tensor) -> Result<Tensor> {
99 let src = f64_input(a, "unary")?;
100 let mut out = vec![0.0f64; a.numel()];
101 fill_par(&mut out, |start, chunk| {
102 let mut w = OffsetWalker::at(a.layout(), start);
103 for o in chunk.iter_mut() {
104 *o = op.eval_f64(src[w.next_offset()]);
105 }
106 });
107 Tensor::from_vec_f64(out, a.shape().clone())
108}
109
110pub(crate) fn binary(op: BinaryOp, a: &Tensor, b: &Tensor) -> Result<Tensor> {
111 let out_shape = broadcast_shapes(a.shape(), b.shape())?;
112 let av = a.broadcast_view(&out_shape)?;
113 let bv = b.broadcast_view(&out_shape)?;
114 let asrc = f64_input(&av, "binary")?;
115 let bsrc = f64_input(&bv, "binary")?;
116 let mut out = vec![0.0f64; out_shape.numel()];
117 fill_par(&mut out, |start, chunk| {
118 let mut wa = OffsetWalker::at(av.layout(), start);
119 let mut wb = OffsetWalker::at(bv.layout(), start);
120 for o in chunk.iter_mut() {
121 *o = op.eval_f64(asrc[wa.next_offset()], bsrc[wb.next_offset()]);
122 }
123 });
124 Tensor::from_vec_f64(out, out_shape)
125}
126
127pub(crate) fn matmul(a: &Tensor, b: &Tensor) -> Result<Tensor> {
128 let plan = crate::backend::plan_matmul(a.shape(), b.shape())?;
129 let av = a.to_vec_f64()?;
130 let bv = b.to_vec_f64()?;
131 let (m, k, n) = (plan.m, plan.k, plan.n);
132 let mut out = vec![0.0f64; plan.batch * m * n];
133 out.par_chunks_mut(m * n).enumerate().for_each(|(bi, c)| {
134 let ao = bi * plan.a_batch_stride;
135 let bo = bi * plan.b_batch_stride;
136 for i in 0..m {
137 let row = &mut c[i * n..(i + 1) * n];
138 for kk in 0..k {
139 let x = av[ao + i * k + kk];
140 if x == 0.0 {
141 continue;
142 }
143 let brow = &bv[bo + kk * n..bo + kk * n + n];
144 for (o, &y) in row.iter_mut().zip(brow) {
145 *o += x * y;
146 }
147 }
148 }
149 });
150 Tensor::from_vec_f64(out, plan.out_shape)
151}
152
153fn split_axes(dims: &[usize], axes: &[usize], keepdim: bool) -> (Vec<usize>, Vec<usize>) {
154 let mut out_dims = Vec::new();
155 let mut kept = Vec::new();
156 for (i, &d) in dims.iter().enumerate() {
157 if axes.contains(&i) {
158 if keepdim {
159 out_dims.push(1);
160 }
161 } else {
162 out_dims.push(d);
163 kept.push(i);
164 }
165 }
166 (out_dims, kept)
167}
168
169pub(crate) fn reduce(op: ReduceOp, a: &Tensor, axes: &[usize], keepdim: bool) -> Result<Tensor> {
170 let src = f64_input(a, "reduce")?;
171 let dims = a.dims().to_vec();
172 let strides = a.layout().strides.values().to_vec();
173 let base = a.layout().offset as isize;
174 let (out_dims, kept) = split_axes(&dims, axes, keepdim);
175 let reduced_dims: Vec<usize> = axes.iter().map(|&ax| dims[ax]).collect();
176 let reduced_strides: Vec<isize> = axes.iter().map(|&ax| strides[ax]).collect();
177 let kept_dims: Vec<usize> = kept.iter().map(|&ax| dims[ax]).collect();
178 let kept_strides: Vec<isize> = kept.iter().map(|&ax| strides[ax]).collect();
179 let out_numel: usize = out_dims.iter().product();
180 let empty = reduced_dims.contains(&0);
181 let reduce_one = |out_i: usize| -> f64 {
182 if empty {
183 return op.identity_f64();
184 }
185 let mut rem = out_i;
186 let mut offset = base;
187 for (i, &d) in kept_dims.iter().enumerate().rev() {
188 offset += (rem % d) as isize * kept_strides[i];
189 rem /= d;
190 }
191 let mut acc = op.identity_f64();
194 let mut comp = 0.0f64;
195 let mut coords = vec![0usize; reduced_dims.len()];
196 let mut off = offset;
197 loop {
198 let x = src[off as usize];
199 if op == ReduceOp::Sum {
200 let t = acc + x;
201 comp += if acc.abs() >= x.abs() {
202 (acc - t) + x
203 } else {
204 (x - t) + acc
205 };
206 acc = t;
207 } else {
208 acc = op.combine_f64(acc, x);
209 }
210 let mut d = reduced_dims.len();
211 loop {
212 if d == 0 {
213 return if op == ReduceOp::Sum { acc + comp } else { acc };
214 }
215 d -= 1;
216 coords[d] += 1;
217 off += reduced_strides[d];
218 if coords[d] < reduced_dims[d] {
219 break;
220 }
221 off -= reduced_dims[d] as isize * reduced_strides[d];
222 coords[d] = 0;
223 }
224 }
225 };
226 let out: Vec<f64> = if out_numel >= 1024 {
227 (0..out_numel).into_par_iter().map(reduce_one).collect()
228 } else {
229 (0..out_numel).map(reduce_one).collect()
230 };
231 Tensor::from_vec_f64(out, Shape::new(out_dims))
232}
233
234pub(crate) fn argmax(a: &Tensor, dim: usize, keepdim: bool) -> Result<Tensor> {
235 let src = f64_input(a, "argmax")?;
236 let dims = a.dims().to_vec();
237 let strides = a.layout().strides.values().to_vec();
238 let base = a.layout().offset as isize;
239 let (out_dims, kept) = split_axes(&dims, &[dim], keepdim);
240 let kept_strides: Vec<isize> = kept.iter().map(|&ax| strides[ax]).collect();
241 let kept_dims: Vec<usize> = kept.iter().map(|&ax| dims[ax]).collect();
242 let (n, s) = (dims[dim], strides[dim]);
243 if n == 0 {
244 return Err(Error::InvalidArgument {
245 op: "argmax",
246 detail: format!("dimension {dim} has extent 0; argmax of nothing is undefined"),
247 });
248 }
249 let out_numel: usize = out_dims.iter().product();
250 let out: Vec<i64> = (0..out_numel)
251 .map(|out_i| {
252 let mut rem = out_i;
253 let mut offset = base;
254 for (i, &d) in kept_dims.iter().enumerate().rev() {
255 offset += (rem % d) as isize * kept_strides[i];
256 rem /= d;
257 }
258 let mut best = f64::NEG_INFINITY;
259 let mut best_i = 0i64;
260 for j in 0..n {
261 let v = src[(offset + j as isize * s) as usize];
262 if v > best {
263 best = v;
264 best_i = j as i64;
265 }
266 }
267 best_i
268 })
269 .collect();
270 Tensor::from_vec_i64(out, Shape::new(out_dims))
271}
272
273pub(crate) fn index_select(a: &Tensor, dim: usize, indices: &Tensor) -> Result<Tensor> {
274 let idx = indices.to_vec_i64()?;
275 let dims = a.dims().to_vec();
276 if dim >= dims.len() {
277 return Err(Error::InvalidArgument {
278 op: "index_select",
279 detail: format!("dim {dim} out of range for rank {}", dims.len()),
280 });
281 }
282 for &i in &idx {
283 if i < 0 || i as usize >= dims[dim] {
284 return Err(Error::IndexOutOfBounds {
285 index: vec![i.max(0) as usize],
286 shape: a.shape().clone(),
287 });
288 }
289 }
290 let src = a.to_vec_f64()?;
292 let inner: usize = dims[dim + 1..].iter().product();
293 let outer: usize = dims[..dim].iter().product();
294 let mut out_dims = dims.clone();
295 out_dims[dim] = idx.len();
296 let mut out = Vec::with_capacity(outer * idx.len() * inner);
297 for o in 0..outer {
298 for &sel in &idx {
299 let start = (o * dims[dim] + sel as usize) * inner;
300 out.extend_from_slice(&src[start..start + inner]);
301 }
302 }
303 Tensor::from_vec_f64(out, Shape::new(out_dims))
304}
305
306pub(crate) fn index_add(a: &Tensor, dim: usize, indices: &Tensor, srct: &Tensor) -> Result<Tensor> {
307 let idx = indices.to_vec_i64()?;
308 let dims = a.dims().to_vec();
309 if dim >= dims.len() {
310 return Err(Error::InvalidArgument {
311 op: "index_add",
312 detail: format!("dim {dim} out of range for rank {}", dims.len()),
313 });
314 }
315 let mut expected = dims.clone();
316 expected[dim] = idx.len();
317 if srct.dims() != expected.as_slice() {
318 return Err(Error::ShapeMismatch {
319 expected: Shape::new(expected),
320 got: srct.shape().clone(),
321 op: "index_add",
322 });
323 }
324 let mut out = a.to_vec_f64()?;
325 let s = srct.to_vec_f64()?;
326 let inner: usize = dims[dim + 1..].iter().product();
327 let outer: usize = dims[..dim].iter().product();
328 for o in 0..outer {
329 for (k, &sel) in idx.iter().enumerate() {
330 if sel < 0 || sel as usize >= dims[dim] {
331 return Err(Error::IndexOutOfBounds {
332 index: vec![sel.max(0) as usize],
333 shape: a.shape().clone(),
334 });
335 }
336 let dst = (o * dims[dim] + sel as usize) * inner;
337 let src = (o * idx.len() + k) * inner;
338 for j in 0..inner {
339 out[dst + j] += s[src + j];
340 }
341 }
342 }
343 Tensor::from_vec_f64(out, a.shape().clone())
344}
345
346pub(crate) fn cholesky(a: &Tensor) -> Result<Tensor> {
349 let d = a.dims();
350 let n = d[d.len() - 1];
351 let batch: usize = d[..d.len() - 2].iter().product();
352 let data = a.to_vec_f64()?;
353 let as_f32: Vec<f32> = data.iter().map(|&x| x as f32).collect();
354 crate::cpu_linalg::cholesky(&as_f32, batch, n)?;
357 let mut out = vec![0.0f64; batch * n * n];
358 for b in 0..batch {
359 let m = &data[b * n * n..(b + 1) * n * n];
360 let l = &mut out[b * n * n..(b + 1) * n * n];
361 for j in 0..n {
362 let mut dd = m[j * n + j];
363 for k in 0..j {
364 dd -= l[j * n + k] * l[j * n + k];
365 }
366 if dd.is_nan() || dd <= 0.0 || dd.is_infinite() {
367 return Err(Error::InvalidArgument {
368 op: "cholesky",
369 detail: format!(
370 "matrix {b} is not positive definite (pivot {j} is {dd:e}); cholesky/logdet need an SPD input"
371 ),
372 });
373 }
374 let ljj = dd.sqrt();
375 l[j * n + j] = ljj;
376 for i in j + 1..n {
377 let mut s = m[i * n + j];
378 for k in 0..j {
379 s -= l[i * n + k] * l[j * n + k];
380 }
381 l[i * n + j] = s / ljj;
382 }
383 }
384 }
385 Tensor::from_vec_f64(out, a.shape().clone())
386}