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 if acc.is_nan() || x.is_nan() => f64::NAN,
83 ReduceOp::Min if acc.is_nan() || x.is_nan() => f64::NAN,
84 ReduceOp::Max => acc.max(x),
85 ReduceOp::Min => acc.min(x),
86 }
87 }
88}
89
90fn fill_par(out: &mut [f64], f: impl Fn(usize, &mut [f64]) + Sync) {
91 if out.len() >= PAR_THRESHOLD {
92 let chunk = PAR_THRESHOLD / 4;
93 out.par_chunks_mut(chunk)
94 .enumerate()
95 .for_each(|(i, c)| f(i * chunk, c));
96 } else {
97 f(0, out);
98 }
99}
100
101pub(crate) fn unary(op: UnaryOp, a: &Tensor) -> Result<Tensor> {
102 let src = f64_input(a, "unary")?;
103 let mut out = vec![0.0f64; a.numel()];
104 fill_par(&mut out, |start, chunk| {
105 let mut w = OffsetWalker::at(a.layout(), start);
106 for o in chunk.iter_mut() {
107 *o = op.eval_f64(src[w.next_offset()]);
108 }
109 });
110 Tensor::from_vec_f64(out, a.shape().clone())
111}
112
113pub(crate) fn binary(op: BinaryOp, a: &Tensor, b: &Tensor) -> Result<Tensor> {
114 let out_shape = broadcast_shapes(a.shape(), b.shape())?;
115 let av = a.broadcast_view(&out_shape)?;
116 let bv = b.broadcast_view(&out_shape)?;
117 let asrc = f64_input(&av, "binary")?;
118 let bsrc = f64_input(&bv, "binary")?;
119 let mut out = vec![0.0f64; out_shape.numel()];
120 fill_par(&mut out, |start, chunk| {
121 let mut wa = OffsetWalker::at(av.layout(), start);
122 let mut wb = OffsetWalker::at(bv.layout(), start);
123 for o in chunk.iter_mut() {
124 *o = op.eval_f64(asrc[wa.next_offset()], bsrc[wb.next_offset()]);
125 }
126 });
127 Tensor::from_vec_f64(out, out_shape)
128}
129
130pub(crate) fn matmul(a: &Tensor, b: &Tensor) -> Result<Tensor> {
131 let plan = crate::backend::plan_matmul(a.shape(), b.shape())?;
132 let av = a.to_vec_f64()?;
133 let bv = b.to_vec_f64()?;
134 let (m, k, n) = (plan.m, plan.k, plan.n);
135 let mut out = vec![0.0f64; plan.batch * m * n];
136 out.par_chunks_mut(m * n).enumerate().for_each(|(bi, c)| {
137 let ao = bi * plan.a_batch_stride;
138 let bo = bi * plan.b_batch_stride;
139 for i in 0..m {
140 let row = &mut c[i * n..(i + 1) * n];
141 for kk in 0..k {
142 let x = av[ao + i * k + kk];
143 if x == 0.0 {
144 continue;
145 }
146 let brow = &bv[bo + kk * n..bo + kk * n + n];
147 for (o, &y) in row.iter_mut().zip(brow) {
148 *o += x * y;
149 }
150 }
151 }
152 });
153 Tensor::from_vec_f64(out, plan.out_shape)
154}
155
156fn split_axes(dims: &[usize], axes: &[usize], keepdim: bool) -> (Vec<usize>, Vec<usize>) {
157 let mut out_dims = Vec::new();
158 let mut kept = Vec::new();
159 for (i, &d) in dims.iter().enumerate() {
160 if axes.contains(&i) {
161 if keepdim {
162 out_dims.push(1);
163 }
164 } else {
165 out_dims.push(d);
166 kept.push(i);
167 }
168 }
169 (out_dims, kept)
170}
171
172pub(crate) fn reduce(op: ReduceOp, a: &Tensor, axes: &[usize], keepdim: bool) -> Result<Tensor> {
173 let src = f64_input(a, "reduce")?;
174 let dims = a.dims().to_vec();
175 let strides = a.layout().strides.values().to_vec();
176 let base = a.layout().offset as isize;
177 let (out_dims, kept) = split_axes(&dims, axes, keepdim);
178 let reduced_dims: Vec<usize> = axes.iter().map(|&ax| dims[ax]).collect();
179 let reduced_strides: Vec<isize> = axes.iter().map(|&ax| strides[ax]).collect();
180 let kept_dims: Vec<usize> = kept.iter().map(|&ax| dims[ax]).collect();
181 let kept_strides: Vec<isize> = kept.iter().map(|&ax| strides[ax]).collect();
182 let out_numel: usize = out_dims.iter().product();
183 let empty = reduced_dims.contains(&0);
184 let reduce_one = |out_i: usize| -> f64 {
185 if empty {
186 return op.identity_f64();
187 }
188 let mut rem = out_i;
189 let mut offset = base;
190 for (i, &d) in kept_dims.iter().enumerate().rev() {
191 offset += (rem % d) as isize * kept_strides[i];
192 rem /= d;
193 }
194 let mut acc = op.identity_f64();
197 let mut comp = 0.0f64;
198 let mut coords = vec![0usize; reduced_dims.len()];
199 let mut off = offset;
200 loop {
201 let x = src[off as usize];
202 if op == ReduceOp::Sum {
203 let t = acc + x;
204 comp += if acc.abs() >= x.abs() {
205 (acc - t) + x
206 } else {
207 (x - t) + acc
208 };
209 acc = t;
210 } else {
211 acc = op.combine_f64(acc, x);
212 }
213 let mut d = reduced_dims.len();
214 loop {
215 if d == 0 {
216 return if op == ReduceOp::Sum { acc + comp } else { acc };
217 }
218 d -= 1;
219 coords[d] += 1;
220 off += reduced_strides[d];
221 if coords[d] < reduced_dims[d] {
222 break;
223 }
224 off -= reduced_dims[d] as isize * reduced_strides[d];
225 coords[d] = 0;
226 }
227 }
228 };
229 let out: Vec<f64> = if out_numel >= 1024 {
230 (0..out_numel).into_par_iter().map(reduce_one).collect()
231 } else {
232 (0..out_numel).map(reduce_one).collect()
233 };
234 Tensor::from_vec_f64(out, Shape::new(out_dims))
235}
236
237pub(crate) fn argmax(a: &Tensor, dim: usize, keepdim: bool) -> Result<Tensor> {
238 let src = f64_input(a, "argmax")?;
239 let dims = a.dims().to_vec();
240 let strides = a.layout().strides.values().to_vec();
241 let base = a.layout().offset as isize;
242 let (out_dims, kept) = split_axes(&dims, &[dim], keepdim);
243 let kept_strides: Vec<isize> = kept.iter().map(|&ax| strides[ax]).collect();
244 let kept_dims: Vec<usize> = kept.iter().map(|&ax| dims[ax]).collect();
245 let (n, s) = (dims[dim], strides[dim]);
246 if n == 0 {
247 return Err(Error::InvalidArgument {
248 op: "argmax",
249 detail: format!("dimension {dim} has extent 0; argmax of nothing is undefined"),
250 });
251 }
252 let out_numel: usize = out_dims.iter().product();
253 let out: Vec<i64> = (0..out_numel)
254 .map(|out_i| {
255 let mut rem = out_i;
256 let mut offset = base;
257 for (i, &d) in kept_dims.iter().enumerate().rev() {
258 offset += (rem % d) as isize * kept_strides[i];
259 rem /= d;
260 }
261 let mut best = f64::NEG_INFINITY;
262 let mut best_i = 0i64;
263 for j in 0..n {
264 let v = src[(offset + j as isize * s) as usize];
265 if v.is_nan() {
266 return Err(Error::InvalidArgument {
267 op: "argmax",
268 detail: "input contains NaN; the maximum is undefined".into(),
269 });
270 }
271 if v > best {
272 best = v;
273 best_i = j as i64;
274 }
275 }
276 Ok(best_i)
277 })
278 .collect::<Result<Vec<i64>>>()?;
279 Tensor::from_vec_i64(out, Shape::new(out_dims))
280}
281
282pub(crate) fn index_select(a: &Tensor, dim: usize, indices: &Tensor) -> Result<Tensor> {
283 let idx = indices.to_vec_i64()?;
284 let dims = a.dims().to_vec();
285 if dim >= dims.len() {
286 return Err(Error::InvalidArgument {
287 op: "index_select",
288 detail: format!("dim {dim} out of range for rank {}", dims.len()),
289 });
290 }
291 for (pos, &i) in idx.iter().enumerate() {
292 if i < 0 {
294 return Err(Error::InvalidArgument {
295 op: "index_select",
296 detail: format!("index {i} at position {pos} is negative"),
297 });
298 }
299 if i as usize >= dims[dim] {
300 return Err(Error::IndexOutOfBounds {
301 index: vec![i as usize],
302 shape: a.shape().clone(),
303 });
304 }
305 }
306 let src = a.to_vec_f64()?;
308 let inner: usize = dims[dim + 1..].iter().product();
309 let outer: usize = dims[..dim].iter().product();
310 let mut out_dims = dims.clone();
311 out_dims[dim] = idx.len();
312 let mut out = Vec::with_capacity(outer * idx.len() * inner);
313 for o in 0..outer {
314 for &sel in &idx {
315 let start = (o * dims[dim] + sel as usize) * inner;
316 out.extend_from_slice(&src[start..start + inner]);
317 }
318 }
319 Tensor::from_vec_f64(out, Shape::new(out_dims))
320}
321
322pub(crate) fn index_add(a: &Tensor, dim: usize, indices: &Tensor, srct: &Tensor) -> Result<Tensor> {
323 let idx = indices.to_vec_i64()?;
324 let dims = a.dims().to_vec();
325 if dim >= dims.len() {
326 return Err(Error::InvalidArgument {
327 op: "index_add",
328 detail: format!("dim {dim} out of range for rank {}", dims.len()),
329 });
330 }
331 let mut expected = dims.clone();
332 expected[dim] = idx.len();
333 if srct.dims() != expected.as_slice() {
334 return Err(Error::ShapeMismatch {
335 expected: Shape::new(expected),
336 got: srct.shape().clone(),
337 op: "index_add",
338 });
339 }
340 let mut out = a.to_vec_f64()?;
341 let s = srct.to_vec_f64()?;
342 let inner: usize = dims[dim + 1..].iter().product();
343 let outer: usize = dims[..dim].iter().product();
344 for o in 0..outer {
345 for (k, &sel) in idx.iter().enumerate() {
346 if sel < 0 || sel as usize >= dims[dim] {
347 return Err(Error::IndexOutOfBounds {
348 index: vec![sel.max(0) as usize],
349 shape: a.shape().clone(),
350 });
351 }
352 let dst = (o * dims[dim] + sel as usize) * inner;
353 let src = (o * idx.len() + k) * inner;
354 for j in 0..inner {
355 out[dst + j] += s[src + j];
356 }
357 }
358 }
359 Tensor::from_vec_f64(out, a.shape().clone())
360}
361
362pub(crate) fn cholesky(a: &Tensor) -> Result<Tensor> {
365 let d = a.dims();
366 let n = d[d.len() - 1];
367 let batch: usize = d[..d.len() - 2].iter().product();
368 let data = a.to_vec_f64()?;
369 let as_f32: Vec<f32> = data.iter().map(|&x| x as f32).collect();
370 crate::cpu_linalg::cholesky(&as_f32, batch, n)?;
373 let mut out = vec![0.0f64; batch * n * n];
374 for b in 0..batch {
375 let m = &data[b * n * n..(b + 1) * n * n];
376 let l = &mut out[b * n * n..(b + 1) * n * n];
377 for j in 0..n {
378 let mut dd = m[j * n + j];
379 for k in 0..j {
380 dd -= l[j * n + k] * l[j * n + k];
381 }
382 if dd.is_nan() || dd <= 0.0 || dd.is_infinite() {
383 return Err(Error::InvalidArgument {
384 op: "cholesky",
385 detail: format!(
386 "matrix {b} is not positive definite (pivot {j} is {dd:e}); cholesky/logdet need an SPD input"
387 ),
388 });
389 }
390 let ljj = dd.sqrt();
391 l[j * n + j] = ljj;
392 for i in j + 1..n {
393 let mut s = m[i * n + j];
394 for k in 0..j {
395 s -= l[i * n + k] * l[j * n + k];
396 }
397 l[i * n + j] = s / ljj;
398 }
399 }
400 }
401 Tensor::from_vec_f64(out, a.shape().clone())
402}