Skip to main content

nuvec_core/
ops.rs

1//! Array operations.
2//!
3//! Following MLX, operations promote operand types and broadcast shapes here, by inserting
4//! `AsType` and `Broadcast` nodes, so primitives always see operands of equal shape and
5//! dtype. Views (`reshape`, `transpose`, `slice`, `broadcast_to`, `expand_dims`,
6//! `squeeze`) share their input's data whenever the layout permits.
7
8use std::ops::{Add, Div, Mul, Neg, Sub};
9
10use crate::buffer::Buffer;
11use crate::cast::{Canon, Scalar, with_type};
12use crate::primitive::{ArgReduceOp, BinaryOp, Primitive, ReduceOp, UnaryOp};
13use crate::scalar::{ScalarOperand, ScalarOrArray, resolve};
14use crate::shape::{self, Shape};
15use crate::view::{self, SliceArg};
16use crate::{
17    Array, Category, Complex64, DType, Error, Result, at_least_float, bf16, f16, promote_types,
18};
19
20fn single(primitive: Primitive, inputs: Vec<Array>) -> Result<Array> {
21    let mut outputs = Array::from_op(primitive, inputs)?;
22    Ok(outputs.swap_remove(0))
23}
24
25/// Returns the error of the first input that has already failed, so that chained
26/// operations report the original cause.
27fn check_inputs(inputs: &[&Array]) -> Result<()> {
28    match inputs.iter().find_map(|a| a.error()) {
29        Some(err) => Err(err),
30        None => Ok(()),
31    }
32}
33
34// ── Element-wise binary ─────────────────────────────────────────────────────
35
36fn binary(op: BinaryOp, a: &Array, b: &Array) -> Result<Array> {
37    check_inputs(&[a, b])?;
38    let mut dtype = promote_types(a.dtype(), b.dtype());
39    if matches!(op, BinaryOp::Divide | BinaryOp::LogAddExp) {
40        dtype = at_least_float(dtype);
41    }
42    let ordered = matches!(
43        op,
44        BinaryOp::Maximum
45            | BinaryOp::Minimum
46            | BinaryOp::LogAddExp
47            | BinaryOp::Less
48            | BinaryOp::LessEqual
49            | BinaryOp::Greater
50            | BinaryOp::GreaterEqual
51    );
52    if ordered && dtype == DType::Complex64 {
53        return Err(Error::UnsupportedDType {
54            op: op.name(),
55            dtype,
56        });
57    }
58    let shape = view::broadcast_shapes(op.name(), a.shape(), b.shape())?;
59    let a = broadcast_to(&astype(a, dtype)?, &shape)?;
60    let b = broadcast_to(&astype(b, dtype)?, &shape)?;
61    single(Primitive::Binary(op), vec![a, b])
62}
63
64macro_rules! binary_fn {
65    ($($(#[$doc:meta])* $name:ident => $op:ident;)*) => {$(
66        $(#[$doc])*
67        pub fn $name(a: impl Into<ScalarOrArray>, b: impl Into<ScalarOrArray>) -> Result<Array> {
68            let (a, b) = resolve(a.into(), b.into())?;
69            binary(BinaryOp::$op, &a, &b)
70        }
71    )*};
72}
73
74// Every binary function accepts arrays or Rust numbers for either operand; numbers are weak
75// and adapt to the array operand (see [`ScalarOrArray`]).
76binary_fn! {
77    /// Element-wise `a + b`, with promotion and broadcasting. Integers wrap; for booleans
78    /// this is logical OR.
79    add => Add;
80    /// Element-wise `a - b`, with promotion and broadcasting. Integers wrap; for booleans
81    /// this is logical XOR.
82    subtract => Subtract;
83    /// Element-wise `a * b`, with promotion and broadcasting. Integers wrap; for booleans
84    /// this is logical AND.
85    multiply => Multiply;
86    /// Element-wise true division `a / b`. Integer operands are promoted to floating point.
87    divide => Divide;
88    /// Element-wise maximum; NaN propagates. Not defined for complex numbers.
89    maximum => Maximum;
90    /// Element-wise minimum; NaN propagates. Not defined for complex numbers.
91    minimum => Minimum;
92    /// Element-wise `log(exp(a) + exp(b))`, computed stably as MLX does: the larger operand
93    /// plus `log1p(exp(smaller − larger))`, or the larger one when the smaller is −∞ or the
94    /// larger +∞. Integers are promoted to floating point; NaN propagates. Not defined for
95    /// complex numbers.
96    logaddexp => LogAddExp;
97    /// Element-wise `a == b` as a bool array.
98    equal => Equal;
99    /// Element-wise `a != b` as a bool array.
100    not_equal => NotEqual;
101    /// Element-wise `a < b` as a bool array. Not defined for complex numbers.
102    less => Less;
103    /// Element-wise `a <= b` as a bool array. Not defined for complex numbers.
104    less_equal => LessEqual;
105    /// Element-wise `a > b` as a bool array. Not defined for complex numbers.
106    greater => Greater;
107    /// Element-wise `a >= b` as a bool array. Not defined for complex numbers.
108    greater_equal => GreaterEqual;
109}
110
111// ── Element-wise unary ──────────────────────────────────────────────────────
112
113fn unary(op: UnaryOp, a: &Array) -> Result<Array> {
114    check_inputs(&[a])?;
115    let dtype = if op.is_transcendental() {
116        at_least_float(a.dtype())
117    } else {
118        a.dtype()
119    };
120    let unsupported = match op {
121        UnaryOp::Negative => dtype == DType::Bool,
122        UnaryOp::Abs | UnaryOp::Conjugate => false,
123        _ => dtype == DType::Complex64,
124    };
125    if unsupported {
126        return Err(Error::UnsupportedDType {
127            op: op.name(),
128            dtype,
129        });
130    }
131    single(Primitive::Unary(op), vec![astype(a, dtype)?])
132}
133
134/// Element-wise complex conjugate. Real arrays are returned unchanged.
135pub fn conjugate(a: &Array) -> Result<Array> {
136    check_inputs(&[a])?;
137    if a.dtype() != DType::Complex64 {
138        return Ok(a.clone());
139    }
140    unary(UnaryOp::Conjugate, a)
141}
142
143/// Element-wise sign: `-1`, `0` or `1` in `a`'s dtype, as MLX's `sign` (`(a > 0) - (a < 0)`,
144/// so NaN gives 0). Unsigned integers and booleans give `a != 0`. Complex numbers are not
145/// supported.
146pub fn sign(a: &Array) -> Result<Array> {
147    check_inputs(&[a])?;
148    let dtype = a.dtype();
149    match dtype.category() {
150        Category::Complex => Err(Error::UnsupportedDType { op: "sign", dtype }),
151        Category::Bool | Category::Unsigned => astype(&greater(a, 0)?, dtype),
152        _ => subtract(
153            astype(&greater(a, 0)?, dtype)?,
154            astype(&less(a, 0)?, dtype)?,
155        ),
156    }
157}
158
159macro_rules! unary_fn {
160    ($($(#[$doc:meta])* $name:ident => $op:ident;)*) => {$(
161        $(#[$doc])*
162        pub fn $name(a: &Array) -> Result<Array> {
163            unary(UnaryOp::$op, a)
164        }
165    )*};
166}
167
168unary_fn! {
169    /// Element-wise `-a`. Integers wrap. Not defined for booleans.
170    negative => Negative;
171    /// Element-wise absolute value; the float32 magnitude for complex numbers.
172    abs => Abs;
173    /// Element-wise `e^a`, in at least float32.
174    exp => Exp;
175    /// Element-wise natural logarithm, in at least float32.
176    log => Log;
177    /// Element-wise square root, in at least float32.
178    sqrt => Sqrt;
179    /// Element-wise sine, in at least float32.
180    sin => Sin;
181    /// Element-wise cosine, in at least float32.
182    cos => Cos;
183    /// Element-wise hyperbolic tangent, in at least float32.
184    tanh => Tanh;
185    /// Element-wise logistic function `1 / (1 + e^-a)`, in at least float32.
186    sigmoid => Sigmoid;
187    /// Element-wise inverse error function, in at least float32.
188    erfinv => ErfInv;
189}
190
191/// `log(sum(exp(a)))` over `axes` (all axes if empty), computed stably as MLX does: the
192/// maximum is subtracted first (without gradient), and rows whose maximum is infinite
193/// return that maximum.
194pub fn logsumexp(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
195    check_inputs(&[a])?;
196    let a = astype(a, at_least_float(a.dtype()))?;
197    if last_axis_only(&a, axes) && crate::fast::on_gpu(a.dtype(), a.size()) {
198        let out = Array::from_op(Primitive::Fast(crate::fast::FastOp::LogSumExp), vec![a])?;
199        let out = out.into_iter().next().expect("one output");
200        return if keepdims {
201            Ok(out)
202        } else {
203            squeeze(&out, axes)
204        };
205    }
206    let maxval = stop_gradient(&max(&a, axes, true)?)?;
207    let out = log(&sum(&exp(&subtract(&a, &maxval)?)?, axes, true)?)?;
208    let out = add(&out, &maxval)?;
209    let out = where_(equal(&abs(&maxval)?, f64::INFINITY)?, &maxval, &out)?;
210    if keepdims {
211        Ok(out)
212    } else {
213        squeeze(&out, axes)
214    }
215}
216
217/// `exp(a) / sum(exp(a))` over `axes` (all axes if empty), with the maximum subtracted
218/// first (without gradient), as MLX does.
219pub fn softmax(a: &Array, axes: &[isize]) -> Result<Array> {
220    check_inputs(&[a])?;
221    let a = astype(a, at_least_float(a.dtype()))?;
222    if last_axis_only(&a, axes) && crate::fast::on_gpu(a.dtype(), a.size()) {
223        return single(Primitive::Fast(crate::fast::FastOp::Softmax), vec![a]);
224    }
225    let maxval = stop_gradient(&max(&a, axes, true)?)?;
226    let ex = exp(&subtract(&a, &maxval)?)?;
227    divide(&ex, sum(&ex, axes, true)?)
228}
229
230/// Whether `axes` (all if empty) reduce the last axis and otherwise only axes of size 1:
231/// MLX's condition for its last-axis softmax and log-sum-exp kernels.
232fn last_axis_only(a: &Array, axes: &[isize]) -> bool {
233    let n = a.ndim() as isize;
234    let all: Vec<isize> = if axes.is_empty() {
235        (0..n).collect()
236    } else {
237        axes.to_vec()
238    };
239    n > 0
240        && a.dtype() != DType::Complex64
241        && all.iter().all(|&ax| (-n..n).contains(&ax))
242        && all.iter().any(|&ax| ax.rem_euclid(n) == n - 1)
243        && all
244            .iter()
245            .all(|&ax| ax.rem_euclid(n) == n - 1 || a.shape()[ax.rem_euclid(n) as usize] == 1)
246}
247
248/// Element-wise selection: `a` where `condition` is true, otherwise `b`.
249///
250/// `condition` is converted to bool (non-zero is true); `a` and `b` are promoted to a
251/// common type; all three are broadcast together. Numbers are accepted for any operand
252/// (weak, as in [`ScalarOrArray`]).
253pub fn where_(
254    condition: impl Into<ScalarOrArray>,
255    a: impl Into<ScalarOrArray>,
256    b: impl Into<ScalarOrArray>,
257) -> Result<Array> {
258    let (a, b) = resolve(a.into(), b.into())?;
259    let condition = condition.into().into_array(None)?;
260    let (condition, a, b) = (&condition, &a, &b);
261    check_inputs(&[condition, a, b])?;
262    let dtype = promote_types(a.dtype(), b.dtype());
263    let shape = view::broadcast_shapes("where", condition.shape(), a.shape())?;
264    let shape = view::broadcast_shapes("where", &shape, b.shape())?;
265    let condition = broadcast_to(&astype(condition, DType::Bool)?, &shape)?;
266    let a = broadcast_to(&astype(a, dtype)?, &shape)?;
267    let b = broadcast_to(&astype(b, dtype)?, &shape)?;
268    single(Primitive::Select, vec![condition, a, b])
269}
270
271/// Converts the elements to `dtype`. Float-to-integer conversion saturates (NaN becomes
272/// 0); conversion to bool tests for non-zero; complex-to-real keeps the real part.
273pub fn astype(a: &Array, dtype: DType) -> Result<Array> {
274    if a.dtype() == dtype {
275        return Ok(a.clone());
276    }
277    single(Primitive::AsType(dtype), vec![a.clone()])
278}
279
280// ── Views ───────────────────────────────────────────────────────────────────
281
282/// The same elements in another shape; one dimension may be `-1` (inferred). Shares the
283/// data when the layout permits, otherwise copies.
284pub fn reshape(a: &Array, shape: &[isize]) -> Result<Array> {
285    let shape = view::infer_shape(a.size(), shape)?;
286    reshape_to(a, shape)
287}
288
289fn reshape_to(a: &Array, shape: Shape) -> Result<Array> {
290    if a.shape() == shape.as_slice() {
291        return Ok(a.clone());
292    }
293    single(Primitive::Reshape(shape.to_vec()), vec![a.clone()])
294}
295
296/// Reorders the axes: output axis `i` is input axis `axes[i]`. Shares the data.
297pub fn transpose(a: &Array, axes: &[usize]) -> Result<Array> {
298    view::permute(a.shape(), a.strides(), axes)?;
299    if axes.iter().enumerate().all(|(i, &axis)| i == axis) {
300        return Ok(a.clone());
301    }
302    single(Primitive::Transpose(axes.to_vec()), vec![a.clone()])
303}
304
305/// Selects `start:stop:step` in every dimension, with Python semantics (negative indices
306/// count from the end, bounds are clamped, steps may be negative). Shares the data.
307pub fn slice(a: &Array, args: &[SliceArg]) -> Result<Array> {
308    let dims = view::normalize_slice(a.shape(), args)?;
309    let full = dims
310        .iter()
311        .zip(a.shape())
312        .all(|(d, &n)| d.start == 0 && d.step == 1 && d.len == n);
313    if full {
314        return Ok(a.clone());
315    }
316    let primitive = Primitive::Slice {
317        start: dims.iter().map(|d| d.start).collect(),
318        step: dims.iter().map(|d| d.step).collect(),
319        len: dims.iter().map(|d| d.len).collect(),
320    };
321    single(primitive, vec![a.clone()])
322}
323
324/// `a` unchanged, but no gradient flows through it in [`crate::vjp`], [`crate::jvp`] and
325/// A copy of `a` with the region selected as by [`slice()`] replaced by `update` (MLX's
326/// `slice_update`, `a[...] = update`): `update` broadcasts to the region's shape and takes
327/// `a`'s dtype.
328///
329/// ```
330/// use nuvec_core::{Array, Result, SliceArg, slice_update};
331///
332/// # fn main() -> Result<()> {
333/// let a = Array::from_slice(&[0.0f32; 6], &[2, 3])?;
334/// let u = Array::from_slice(&[1.0f32, 2.0], &[2, 1])?;
335/// let b = slice_update(&a, &u, &[SliceArg::full(), SliceArg::from(1..2)])?;
336/// assert_eq!(b.to_vec::<f32>()?, [0.0, 1.0, 0.0, 0.0, 2.0, 0.0]);
337/// # Ok(())
338/// # }
339/// ```
340pub fn slice_update(a: &Array, update: &Array, args: &[SliceArg]) -> Result<Array> {
341    check_inputs(&[a, update])?;
342    let dims = view::normalize_slice(a.shape(), args)?;
343    let len: Vec<usize> = dims.iter().map(|d| d.len).collect();
344    let update = broadcast_to(&astype(update, a.dtype())?, &len)?;
345    single(
346        Primitive::SliceUpdate {
347            start: dims.iter().map(|d| d.start).collect(),
348            step: dims.iter().map(|d| d.step).collect(),
349            len,
350        },
351        vec![a.clone(), update],
352    )
353}
354
355/// [`crate::grad`]. Shares the data.
356pub fn stop_gradient(a: &Array) -> Result<Array> {
357    check_inputs(&[a])?;
358    single(Primitive::StopGradient, vec![a.clone()])
359}
360
361/// Presents `a` as the larger `shape` (NumPy broadcasting rules). Shares the data.
362pub fn broadcast_to(a: &Array, shape: &[usize]) -> Result<Array> {
363    if a.shape() == shape {
364        return Ok(a.clone());
365    }
366    single(Primitive::Broadcast(shape.to_vec()), vec![a.clone()])
367}
368
369/// Inserts a dimension of size 1 at `axis` (negative counts from the end of the result).
370/// Shares the data.
371pub fn expand_dims(a: &Array, axis: isize) -> Result<Array> {
372    let axis = view::normalize_axis(axis, a.ndim() + 1)?;
373    let mut shape: Shape = a.shape().into();
374    shape.insert(axis, 1);
375    reshape_to(a, shape)
376}
377
378/// Removes dimensions of size 1: the given `axes`, or all of them if `axes` is empty.
379/// Shares the data.
380pub fn squeeze(a: &Array, axes: &[isize]) -> Result<Array> {
381    let mut remove = vec![axes.is_empty(); a.ndim()];
382    for &axis in axes {
383        let i = view::normalize_axis(axis, a.ndim())?;
384        if a.shape()[i] != 1 {
385            return Err(Error::InvalidArgument(format!(
386                "cannot squeeze axis {axis} of size {} in shape {:?}",
387                a.shape()[i],
388                a.shape()
389            )));
390        }
391        remove[i] = true;
392    }
393    let shape = a
394        .shape()
395        .iter()
396        .zip(&remove)
397        .filter(|&(&d, &r)| !(r && d == 1))
398        .map(|(&d, _)| d)
399        .collect();
400    reshape_to(a, shape)
401}
402
403// ── Reductions ──────────────────────────────────────────────────────────────
404
405/// Sorted, unique axes; an empty list means every axis.
406fn normalize_axes(a: &Array, axes: &[isize]) -> Result<Vec<usize>> {
407    if axes.is_empty() {
408        return Ok((0..a.ndim()).collect());
409    }
410    let mut out = axes
411        .iter()
412        .map(|&axis| view::normalize_axis(axis, a.ndim()))
413        .collect::<Result<Vec<_>>>()?;
414    out.sort_unstable();
415    if out.windows(2).any(|w| w[0] == w[1]) {
416        return Err(Error::InvalidArgument(format!(
417            "duplicate axes in {axes:?}"
418        )));
419    }
420    Ok(out)
421}
422
423/// `out` with the (size-1) `axes` removed, unless `keepdims`.
424fn finish_reduction(out: Array, axes: &[usize], keepdims: bool) -> Result<Array> {
425    if keepdims {
426        return Ok(out);
427    }
428    let shape = out
429        .shape()
430        .iter()
431        .enumerate()
432        .filter(|(d, _)| !axes.contains(d))
433        .map(|(_, &n)| n)
434        .collect();
435    reshape_to(&out, shape)
436}
437
438fn reduction(
439    op: ReduceOp,
440    a: &Array,
441    axes: &[isize],
442    keepdims: bool,
443    dtype: DType,
444) -> Result<Array> {
445    check_inputs(&[a])?;
446    let axes = normalize_axes(a, axes)?;
447    if matches!(op, ReduceOp::Max | ReduceOp::Min) {
448        if a.dtype() == DType::Complex64 {
449            return Err(Error::UnsupportedDType {
450                op: op.name(),
451                dtype: a.dtype(),
452            });
453        }
454        if axes.iter().any(|&d| a.shape()[d] == 0) {
455            return Err(Error::InvalidArgument(format!(
456                "{}: cannot reduce a zero-length axis (no identity)",
457                op.name()
458            )));
459        }
460    }
461    let a = astype(a, dtype)?;
462    if axes.is_empty() {
463        return Ok(a); // a 0-D array: nothing to reduce
464    }
465    let out = single(
466        Primitive::Reduce {
467            op,
468            axes: axes.clone(),
469        },
470        vec![a],
471    )?;
472    finish_reduction(out, &axes, keepdims)
473}
474
475/// The type sums and products accumulate into, as in MLX: small integers widen.
476fn widened(dtype: DType) -> DType {
477    match (dtype.category(), dtype.size()) {
478        (Category::Bool, _) => DType::I32,
479        (Category::Signed, 1..=4) => DType::I32,
480        (Category::Signed, _) => DType::I64,
481        (Category::Unsigned, 1..=4) => DType::U32,
482        (Category::Unsigned, _) => DType::U64,
483        _ => dtype,
484    }
485}
486
487/// Sum over `axes` (empty = all). Integers and bools widen to 32 or 64 bits (as in MLX);
488/// half and single precision accumulate in double precision.
489pub fn sum(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
490    reduction(ReduceOp::Sum, a, axes, keepdims, widened(a.dtype()))
491}
492
493/// Product over `axes` (empty = all), with the same widening as [`sum`].
494pub fn prod(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
495    reduction(ReduceOp::Prod, a, axes, keepdims, widened(a.dtype()))
496}
497
498/// Maximum over `axes` (empty = all); NaN propagates. Zero-length axes are an error.
499pub fn max(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
500    reduction(ReduceOp::Max, a, axes, keepdims, a.dtype())
501}
502
503/// Minimum over `axes` (empty = all); NaN propagates. Zero-length axes are an error.
504pub fn min(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
505    reduction(ReduceOp::Min, a, axes, keepdims, a.dtype())
506}
507
508/// Whether every element over `axes` (empty = all) is non-zero.
509pub fn all(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
510    reduction(ReduceOp::And, a, axes, keepdims, DType::Bool)
511}
512
513/// Whether any element over `axes` (empty = all) is non-zero.
514pub fn any(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
515    reduction(ReduceOp::Or, a, axes, keepdims, DType::Bool)
516}
517
518/// Mean over `axes` (empty = all), in at least float32. Empty reductions are NaN.
519pub fn mean(a: &Array, axes: &[isize], keepdims: bool) -> Result<Array> {
520    let dtype = at_least_float(a.dtype());
521    let count: usize = normalize_axes(a, axes)?
522        .iter()
523        .map(|&d| a.shape()[d])
524        .product();
525    let scale = astype(&Array::scalar(1.0 / count as f64), dtype)?;
526    multiply(&sum(a, axes, keepdims)?, &scale)
527}
528
529fn arg_reduction(op: ArgReduceOp, a: &Array, axis: Option<isize>, keepdims: bool) -> Result<Array> {
530    check_inputs(&[a])?;
531    if a.dtype() == DType::Complex64 {
532        return Err(Error::UnsupportedDType {
533            op: op.name(),
534            dtype: a.dtype(),
535        });
536    }
537    let Some(axis) = axis else {
538        // Over the flattened array; with keepdims the result has one size-1 dimension per
539        // input dimension.
540        let flat = arg_reduction(op, &reshape(a, &[-1])?, Some(0), false)?;
541        return if keepdims {
542            reshape_to(&flat, smallvec::smallvec![1; a.ndim()])
543        } else {
544            Ok(flat)
545        };
546    };
547    let axis = view::normalize_axis(axis, a.ndim())?;
548    if a.shape()[axis] == 0 {
549        return Err(Error::InvalidArgument(format!(
550            "{}: cannot reduce a zero-length axis",
551            op.name()
552        )));
553    }
554    let out = single(Primitive::ArgReduce { op, axis }, vec![a.clone()])?;
555    finish_reduction(out, &[axis], keepdims)
556}
557
558/// Index (uint32) of the maximum along `axis`, or over the flattened array if `None`.
559/// The first occurrence wins; the first NaN wins over any number.
560pub fn argmax(a: &Array, axis: Option<isize>, keepdims: bool) -> Result<Array> {
561    arg_reduction(ArgReduceOp::Max, a, axis, keepdims)
562}
563
564/// Index (uint32) of the minimum along `axis`, or over the flattened array if `None`.
565/// The first occurrence wins; the first NaN wins over any number.
566pub fn argmin(a: &Array, axis: Option<isize>, keepdims: bool) -> Result<Array> {
567    arg_reduction(ArgReduceOp::Min, a, axis, keepdims)
568}
569
570// ── Matrix multiplication ───────────────────────────────────────────────────
571
572/// Matrix product of `[..., M, K]` and `[..., K, N]`, broadcasting batch dimensions.
573///
574/// A 1-D `a` is a row vector and a 1-D `b` a column vector; the inserted dimension is
575/// removed from the result. The promoted type must be floating point or complex.
576pub fn matmul(a: &Array, b: &Array) -> Result<Array> {
577    product(a, b, None)
578}
579
580/// `alpha · (a @ b) + beta · c`, with `c` broadcast to the product's shape, as MLX's
581/// `addmm`. On the GPU it is one kernel (the GEMM's epilogue), so `Linear` layers use it for
582/// their bias.
583pub fn addmm(c: &Array, a: &Array, b: &Array, alpha: f32, beta: f32) -> Result<Array> {
584    product(a, b, Some((c, alpha, beta)))
585}
586
587/// Matrix product with NumPy's rules for 1-D operands and batch broadcasting, optionally
588/// fused with `alpha·(·) + beta·c`.
589fn product(a: &Array, b: &Array, c: Option<(&Array, f32, f32)>) -> Result<Array> {
590    let op = if c.is_some() { "addmm" } else { "matmul" };
591    check_inputs(&[a, b])?;
592    if let Some((c, ..)) = c {
593        check_inputs(&[c])?;
594    }
595    if a.ndim() == 0 || b.ndim() == 0 {
596        return Err(Error::InvalidArgument(format!(
597            "{op}: operands need at least one dimension"
598        )));
599    }
600    let mut dtype = promote_types(a.dtype(), b.dtype());
601    if let Some((c, ..)) = c {
602        dtype = promote_types(dtype, c.dtype());
603    }
604    if !matches!(dtype.category(), Category::Float | Category::Complex) {
605        return Err(Error::UnsupportedDType { op, dtype });
606    }
607    let a2 = if a.ndim() == 1 {
608        expand_dims(a, 0)?
609    } else {
610        a.clone()
611    };
612    let b2 = if b.ndim() == 1 {
613        expand_dims(b, -1)?
614    } else {
615        b.clone()
616    };
617    let (sa, sb) = (a2.shape(), b2.shape());
618    if sa[sa.len() - 1] != sb[sb.len() - 2] {
619        return Err(Error::ShapeMismatch {
620            op,
621            lhs: a.shape().to_vec(),
622            rhs: b.shape().to_vec(),
623        });
624    }
625    let (a2, b2) = (astype(&a2, dtype)?, astype(&b2, dtype)?);
626    let (na, nb) = (a2.ndim(), b2.ndim());
627    let batch = view::broadcast_shapes(op, &a2.shape()[..na - 2], &b2.shape()[..nb - 2])?;
628    let (m, n) = (a2.shape()[na - 2], b2.shape()[nb - 1]);
629    let mut expanded: Shape = batch.clone();
630    expanded.extend_from_slice(&[m, n]);
631    // Dimensions inserted for 1-D operands, removed from the result.
632    let mut drop = Vec::new();
633    if a.ndim() == 1 {
634        drop.push(expanded.len() as isize - 2);
635    }
636    if b.ndim() == 1 {
637        drop.push(expanded.len() as isize - 1);
638    }
639    // `c`, broadcast to the result and then to the expanded product shape.
640    let c_full = match c {
641        Some((c, alpha, beta)) => {
642            let final_shape: Vec<usize> = expanded
643                .iter()
644                .enumerate()
645                .filter(|(i, _)| !drop.contains(&(*i as isize)))
646                .map(|(_, &d)| d)
647                .collect();
648            let mut cc = broadcast_to(&astype(c, dtype)?, &final_shape)?;
649            for &d in &drop {
650                cc = expand_dims(&cc, d)?;
651            }
652            Some((broadcast_to(&cc, &expanded)?, alpha, beta))
653        }
654        None => None,
655    };
656    let record = |x: Array, y: Array, z: Option<(Array, f32, f32)>| match z {
657        None => single(Primitive::Matmul, vec![x, y]),
658        Some((z, alpha, beta)) => single(
659            Primitive::AddMM {
660                alpha: alpha.to_bits(),
661                beta: beta.to_bits(),
662            },
663            vec![x, y, z],
664        ),
665    };
666    let out = if na > 2 && nb == 2 {
667        // One large product instead of a batch of small ones (as in MLX).
668        let k = a2.shape()[na - 1];
669        let rows = a2
670            .size()
671            .checked_div(k)
672            .unwrap_or_else(|| a2.shape()[..na - 1].iter().product());
673        let flat_c = match c_full {
674            Some((z, alpha, beta)) => {
675                Some((reshape_to(&z, smallvec::smallvec![rows, n])?, alpha, beta))
676            }
677            None => None,
678        };
679        let flat = record(
680            reshape_to(&a2, smallvec::smallvec![rows, k])?,
681            b2.clone(),
682            flat_c,
683        )?;
684        reshape_to(&flat, expanded)?
685    } else {
686        let full = |x: &Array, nd: usize| -> Result<Array> {
687            let mut shape = batch.clone();
688            shape.extend_from_slice(&x.shape()[nd - 2..]);
689            broadcast_to(x, &shape)
690        };
691        record(full(&a2, na)?, full(&b2, nb)?, c_full)?
692    };
693    if drop.is_empty() {
694        Ok(out)
695    } else {
696        squeeze(&out, &drop)
697    }
698}
699
700// ── Creation ────────────────────────────────────────────────────────────────
701
702/// An array of zeros.
703pub fn zeros(shape: &[usize], dtype: DType) -> Result<Array> {
704    let buffer = Buffer::try_zeroed(shape::checked_nbytes(shape, dtype.size())?)?;
705    Array::from_buffer(buffer, dtype, shape)
706}
707
708/// An array of ones.
709pub fn ones(shape: &[usize], dtype: DType) -> Result<Array> {
710    filled(shape, dtype, |_| Canon::Int(1))
711}
712
713/// Evenly spaced values `start, start + step, …` below `stop` (above it for negative
714/// steps), converted to `dtype`. The length is `ceil((stop - start) / step)`, as in MLX.
715pub fn arange(start: f64, stop: f64, step: f64, dtype: DType) -> Result<Array> {
716    let invalid = |msg: &str| Err(Error::InvalidArgument(format!("arange: {msg}")));
717    if dtype == DType::Bool {
718        return Err(Error::UnsupportedDType {
719            op: "arange",
720            dtype,
721        });
722    }
723    if start.is_nan() || stop.is_nan() || step.is_nan() || start.is_infinite() || stop.is_infinite()
724    {
725        return invalid("cannot compute the length from NaN or infinite bounds");
726    }
727    if step == 0.0 {
728        return invalid("step must be non-zero");
729    }
730    let len = if step.is_infinite() {
731        usize::from((step > 0.0 && start < stop) || (step < 0.0 && start > stop))
732    } else {
733        let real = ((stop - start) / step).ceil();
734        if real > i32::MAX as f64 {
735            return invalid("maximum size exceeded");
736        }
737        real.max(0.0) as usize
738    };
739    // `i * step` would be NaN for i = 0 and an infinite step.
740    filled(&[len], dtype, |i| {
741        Canon::Float(if i == 0 {
742            start
743        } else {
744            start + i as f64 * step
745        })
746    })
747}
748
749/// An array whose element `i` (row-major) is `value(i)` converted to `dtype`.
750pub(crate) fn filled(
751    shape: &[usize],
752    dtype: DType,
753    value: impl Fn(usize) -> Canon,
754) -> Result<Array> {
755    let mut buffer = Buffer::try_alloc(shape::checked_nbytes(shape, dtype.size())?)?;
756    with_type!(dtype, T => {
757        for (i, x) in buffer.typed_mut::<T>().iter_mut().enumerate() {
758            *x = T::from_canon(value(i));
759        }
760    });
761    Array::from_buffer(buffer, dtype, shape)
762}
763
764// ── Methods ─────────────────────────────────────────────────────────────────
765
766impl Array {
767    /// Converts the elements to `dtype`; see [`astype`].
768    pub fn astype(&self, dtype: DType) -> Result<Array> {
769        astype(self, dtype)
770    }
771
772    /// The same elements in another shape; see [`reshape`].
773    pub fn reshape(&self, shape: &[isize]) -> Result<Array> {
774        reshape(self, shape)
775    }
776
777    /// The array with all axes reversed (the matrix transpose for 2-D arrays).
778    pub fn t(&self) -> Result<Array> {
779        let axes: Vec<usize> = (0..self.ndim()).rev().collect();
780        transpose(self, &axes)
781    }
782}
783
784// ── Operators ───────────────────────────────────────────────────────────────
785
786/// Operators cannot return a `Result`. An invalid expression yields an array whose
787/// evaluation (or readback) returns the error, instead of panicking.
788fn or_failed(result: Result<Array>, lhs: &Array) -> Array {
789    result.unwrap_or_else(|err| Array::failed(err, lhs.dtype(), lhs.shape()))
790}
791
792macro_rules! operator {
793    ($trait:ident, $method:ident, $op:ident) => {
794        impl $trait<&Array> for &Array {
795            type Output = Array;
796            fn $method(self, rhs: &Array) -> Array {
797                or_failed($op(self, rhs), self)
798            }
799        }
800        impl $trait<Array> for &Array {
801            type Output = Array;
802            fn $method(self, rhs: Array) -> Array {
803                or_failed($op(self, &rhs), self)
804            }
805        }
806        impl $trait<&Array> for Array {
807            type Output = Array;
808            fn $method(self, rhs: &Array) -> Array {
809                or_failed($op(&self, rhs), &self)
810            }
811        }
812        impl $trait<Array> for Array {
813            type Output = Array;
814            fn $method(self, rhs: Array) -> Array {
815                or_failed($op(&self, &rhs), &self)
816            }
817        }
818    };
819}
820
821operator!(Add, add, add);
822operator!(Sub, sub, subtract);
823operator!(Mul, mul, multiply);
824operator!(Div, div, divide);
825
826/// Operators with a weak Rust number on the right: `&x * 2.0`.
827macro_rules! scalar_rhs_operator {
828    ($trait:ident, $method:ident, $op:ident) => {
829        impl<S: ScalarOperand> $trait<S> for &Array {
830            type Output = Array;
831            fn $method(self, rhs: S) -> Array {
832                or_failed($op(self, rhs.into()), self)
833            }
834        }
835        impl<S: ScalarOperand> $trait<S> for Array {
836            type Output = Array;
837            fn $method(self, rhs: S) -> Array {
838                or_failed($op(&self, rhs.into()), &self)
839            }
840        }
841    };
842}
843
844scalar_rhs_operator!(Add, add, add);
845scalar_rhs_operator!(Sub, sub, subtract);
846scalar_rhs_operator!(Mul, mul, multiply);
847scalar_rhs_operator!(Div, div, divide);
848
849/// Operators with a weak Rust number on the left: `1.0 - &x`.
850///
851/// These impls cannot be generic (orphan rule), so there is one per literal kind: integer
852/// literals resolve to `i32` and float literals to `f64` only while no other integer or
853/// float impl competes. Other number types go on the right or through the functions
854/// (`subtract(x_f32, &a)`); since numbers are weak, the result is the same.
855macro_rules! scalar_lhs_operator {
856    ($trait:ident, $method:ident, $op:ident; $($t:ty),*) => {$(
857        impl $trait<&Array> for $t {
858            type Output = Array;
859            fn $method(self, rhs: &Array) -> Array {
860                or_failed($op(self, rhs), rhs)
861            }
862        }
863        impl $trait<Array> for $t {
864            type Output = Array;
865            fn $method(self, rhs: Array) -> Array {
866                or_failed($op(self, &rhs), &rhs)
867            }
868        }
869    )*};
870}
871
872macro_rules! scalar_lhs_operators {
873    ($($t:ty),*) => {
874        scalar_lhs_operator!(Add, add, add; $($t),*);
875        scalar_lhs_operator!(Sub, sub, subtract; $($t),*);
876        scalar_lhs_operator!(Mul, mul, multiply; $($t),*);
877        scalar_lhs_operator!(Div, div, divide; $($t),*);
878    };
879}
880
881scalar_lhs_operators!(bool, i32, f64, f16, bf16, Complex64);
882
883impl Neg for &Array {
884    type Output = Array;
885    fn neg(self) -> Array {
886        or_failed(negative(self), self)
887    }
888}
889
890impl Neg for Array {
891    type Output = Array;
892    fn neg(self) -> Array {
893        or_failed(negative(&self), &self)
894    }
895}
896
897/// `a` with each element repeated `repeats` times along `axis` (of the flattened array
898/// without one), as MLX's `repeat`.
899///
900/// ```
901/// use nuvec_core::{Array, Result, repeat};
902///
903/// # fn main() -> Result<()> {
904/// let a = Array::from_slice(&[1i32, 2, 3, 4], &[2, 2])?;
905/// assert_eq!(repeat(&a, 2, Some(1))?.to_vec::<i32>()?, [1, 1, 2, 2, 3, 3, 4, 4]);
906/// assert_eq!(repeat(&a, 2, None)?.shape(), &[8]);
907/// # Ok(())
908/// # }
909/// ```
910pub fn repeat(a: &Array, repeats: usize, axis: Option<isize>) -> Result<Array> {
911    let (a, axis) = match axis {
912        Some(axis) => (a.clone(), view::normalize_axis(axis, a.ndim())?),
913        None => (reshape(a, &[-1])?, 0),
914    };
915    let mut wide: Vec<usize> = a.shape().to_vec();
916    wide.insert(axis + 1, repeats);
917    let mut out: Vec<isize> = a.shape().iter().map(|&d| d as isize).collect();
918    out[axis] *= repeats as isize;
919    reshape(
920        &broadcast_to(&expand_dims(&a, axis as isize + 1)?, &wide)?,
921        &out,
922    )
923}
924
925/// `a` in row-major memory: itself if it already is, else a copy (MLX's `contiguous`).
926pub fn contiguous(a: &Array) -> Result<Array> {
927    if a.flags().row_contiguous {
928        return Ok(a.clone());
929    }
930    let shape: Vec<isize> = a.shape().iter().map(|&d| d as isize).collect();
931    reshape(&reshape(a, &[-1])?, &shape)
932}
933
934#[cfg(test)]
935mod tests {
936    use super::*;
937    use crate::buffer::live;
938
939    /// Live buffers created while running `f` (after evaluating its result).
940    fn allocations(f: impl FnOnce() -> Array) -> isize {
941        let before = live::count();
942        let out = f();
943        out.eval().unwrap();
944        live::count() - before
945    }
946
947    #[test]
948    fn views_share_data() {
949        let base = arange(0.0, 24.0, 1.0, DType::F32)
950            .unwrap()
951            .reshape(&[2, 3, 4])
952            .unwrap();
953        base.eval().unwrap();
954        assert_eq!(allocations(|| base.reshape(&[6, 4]).unwrap()), 0);
955        assert_eq!(allocations(|| base.t().unwrap()), 0);
956        assert_eq!(
957            allocations(|| slice(
958                &base,
959                &[(..).into(), SliceArg::new(None, None, -2), (1..3).into()]
960            )
961            .unwrap()),
962            0
963        );
964        assert_eq!(
965            allocations(|| broadcast_to(&base, &[5, 2, 3, 4]).unwrap()),
966            0
967        );
968        assert_eq!(allocations(|| expand_dims(&base, 1).unwrap()), 0);
969        assert_eq!(
970            allocations(|| squeeze(&expand_dims(&base, -1).unwrap(), &[]).unwrap()),
971            0
972        );
973        // A transposed layout cannot be flattened without copying.
974        assert_eq!(allocations(|| base.t().unwrap().reshape(&[-1]).unwrap()), 1);
975    }
976
977    #[test]
978    fn noop_views_record_nothing() {
979        let a = Array::from_slice(&[1u8, 2, 3, 4], &[2, 2]).unwrap();
980        assert!(a.reshape(&[2, 2]).unwrap().primitive().is_none());
981        assert!(transpose(&a, &[0, 1]).unwrap().primitive().is_none());
982        assert!(
983            slice(&a, &[SliceArg::full(), SliceArg::full()])
984                .unwrap()
985                .primitive()
986                .is_none()
987        );
988        assert!(astype(&a, DType::U8).unwrap().primitive().is_none());
989        assert!(broadcast_to(&a, &[2, 2]).unwrap().primitive().is_none());
990    }
991
992    #[test]
993    fn view_flags() {
994        let a = zeros(&[2, 3], DType::F32).unwrap();
995        let t = a.t().unwrap();
996        assert!(t.flags().col_contiguous && !t.flags().row_contiguous);
997        assert_eq!(t.strides(), &[1, 3]);
998        let b = broadcast_to(&a, &[4, 2, 3]).unwrap();
999        assert_eq!(b.strides(), &[0, 3, 1]);
1000        assert!(!b.flags().contiguous);
1001    }
1002}