Skip to main content

nuvec_core/
primitive.rs

1//! What a graph node computes.
2//!
3//! Primitives form a closed enum: equality and hashing — the basis for
4//! common-subexpression elimination — are derived, and every per-primitive rule (output
5//! inference here; gradient, batching and fusion rules later) is an exhaustive `match`.
6//!
7//! Primitives require operands of equal shape and dtype. Promotion and broadcasting happen
8//! in the ops layer, which inserts `AsType` and `Broadcast` nodes.
9
10use smallvec::{SmallVec, smallvec};
11
12use crate::custom::CustomCall;
13use crate::shape::{Shape, Strides};
14use crate::view::{self, DimSlice};
15use crate::{Array, Category, DType, Error, Result};
16
17/// An element-wise function of one operand.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum UnaryOp {
21    /// `-x`.
22    Negative,
23    /// `|x|`; the magnitude for complex numbers.
24    Abs,
25    /// `e^x`.
26    Exp,
27    /// Natural logarithm.
28    Log,
29    /// Square root.
30    Sqrt,
31    /// Sine.
32    Sin,
33    /// Cosine.
34    Cos,
35    /// Hyperbolic tangent.
36    Tanh,
37    /// `1 / (1 + e^-x)`.
38    Sigmoid,
39    /// The complex conjugate (identity for real numbers).
40    Conjugate,
41    /// The inverse error function.
42    ErfInv,
43}
44
45impl UnaryOp {
46    /// Whether the function computes in floating point (integer inputs are promoted).
47    pub(crate) fn is_transcendental(self) -> bool {
48        !matches!(self, Self::Negative | Self::Abs | Self::Conjugate)
49    }
50
51    pub(crate) fn name(self) -> &'static str {
52        match self {
53            Self::Negative => "negative",
54            Self::Abs => "abs",
55            Self::Exp => "exp",
56            Self::Log => "log",
57            Self::Sqrt => "sqrt",
58            Self::Sin => "sin",
59            Self::Cos => "cos",
60            Self::Tanh => "tanh",
61            Self::Sigmoid => "sigmoid",
62            Self::Conjugate => "conjugate",
63            Self::ErfInv => "erfinv",
64        }
65    }
66}
67
68/// An element-wise function of two operands.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70#[non_exhaustive]
71pub enum BinaryOp {
72    /// `a + b`.
73    Add,
74    /// `a - b`.
75    Subtract,
76    /// `a * b`.
77    Multiply,
78    /// `a / b` (true division).
79    Divide,
80    /// The larger operand; NaN propagates.
81    Maximum,
82    /// The smaller operand; NaN propagates.
83    Minimum,
84    /// `log(exp(a) + exp(b))`, computed stably (MLX's `logaddexp`); NaN propagates.
85    LogAddExp,
86    /// `a == b`.
87    Equal,
88    /// `a != b`.
89    NotEqual,
90    /// `a < b`.
91    Less,
92    /// `a <= b`.
93    LessEqual,
94    /// `a > b`.
95    Greater,
96    /// `a >= b`.
97    GreaterEqual,
98}
99
100impl BinaryOp {
101    /// Whether the result is boolean.
102    pub(crate) fn is_comparison(self) -> bool {
103        matches!(
104            self,
105            Self::Equal
106                | Self::NotEqual
107                | Self::Less
108                | Self::LessEqual
109                | Self::Greater
110                | Self::GreaterEqual
111        )
112    }
113
114    pub(crate) fn name(self) -> &'static str {
115        match self {
116            Self::Add => "add",
117            Self::Subtract => "subtract",
118            Self::Multiply => "multiply",
119            Self::Divide => "divide",
120            Self::Maximum => "maximum",
121            Self::Minimum => "minimum",
122            Self::LogAddExp => "logaddexp",
123            Self::Equal => "equal",
124            Self::NotEqual => "not_equal",
125            Self::Less => "less",
126            Self::LessEqual => "less_equal",
127            Self::Greater => "greater",
128            Self::GreaterEqual => "greater_equal",
129        }
130    }
131}
132
133/// A reduction over a set of axes.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
135#[non_exhaustive]
136pub enum ReduceOp {
137    /// Sum (identity 0).
138    Sum,
139    /// Product (identity 1).
140    Prod,
141    /// Maximum; NaN propagates. No identity.
142    Max,
143    /// Minimum; NaN propagates. No identity.
144    Min,
145    /// Logical AND of bools (identity true).
146    And,
147    /// Logical OR of bools (identity false).
148    Or,
149}
150
151impl ReduceOp {
152    pub(crate) fn name(self) -> &'static str {
153        match self {
154            Self::Sum => "sum",
155            Self::Prod => "prod",
156            Self::Max => "max",
157            Self::Min => "min",
158            Self::And => "all",
159            Self::Or => "any",
160        }
161    }
162}
163
164/// The position of an extreme value along one axis.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
166#[non_exhaustive]
167pub enum ArgReduceOp {
168    /// Index of the maximum (first occurrence; first NaN wins).
169    Max,
170    /// Index of the minimum (first occurrence; first NaN wins).
171    Min,
172}
173
174impl ArgReduceOp {
175    pub(crate) fn name(self) -> &'static str {
176        match self {
177            Self::Max => "argmax",
178            Self::Min => "argmin",
179        }
180    }
181}
182
183/// The combination a cumulative scan applies.
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
185#[non_exhaustive]
186pub enum ScanOp {
187    /// Running sum (`cumsum`).
188    Sum,
189    /// Running product (`cumprod`).
190    Prod,
191    /// Running maximum (`cummax`); NaN propagates.
192    Max,
193    /// Running minimum (`cummin`); NaN propagates.
194    Min,
195    /// Running `log(Σ exp)` (`logcumsumexp`).
196    LogAddExp,
197}
198
199impl ScanOp {
200    pub(crate) fn name(self) -> &'static str {
201        match self {
202            Self::Sum => "cumsum",
203            Self::Prod => "cumprod",
204            Self::Max => "cummax",
205            Self::Min => "cummin",
206            Self::LogAddExp => "logcumsumexp",
207        }
208    }
209}
210
211/// How a scatter combines an update with the value it lands on.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
213#[non_exhaustive]
214pub enum ScatterReduce {
215    /// The update replaces the value (with repeated indices, one of the updates wins).
216    None,
217    /// The update is added to the value (repeated indices accumulate).
218    Sum,
219}
220
221/// An operation recorded in the graph, with its parameters.
222#[derive(Debug, Clone, PartialEq, Eq, Hash)]
223#[non_exhaustive]
224pub enum Primitive {
225    /// Element-wise function of one operand.
226    Unary(UnaryOp),
227    /// Element-wise function of two operands of equal shape and dtype.
228    Binary(BinaryOp),
229    /// `where(condition, a, b)`: a boolean condition selects from `a` or `b`.
230    Select,
231    /// Conversion to another element type.
232    AsType(DType),
233    /// The same elements in another shape. A view when the layout permits, else a copy.
234    Reshape(Vec<usize>),
235    /// Axes reordered: output axis `i` is input axis `axes[i]`. A view.
236    Transpose(Vec<usize>),
237    /// A normalised slice per dimension. A view.
238    Slice {
239        /// First index per dimension.
240        start: Vec<usize>,
241        /// Step per dimension (non-zero, may be negative).
242        step: Vec<isize>,
243        /// Number of selected indices per dimension (the output shape).
244        len: Vec<usize>,
245    },
246    /// Presented as a larger shape with zero strides. A view.
247    Broadcast(Vec<usize>),
248    /// The input unchanged, but no gradient flows through it. A view.
249    StopGradient,
250    /// `(base, update)`: a copy of `base` whose region selected as by `Slice` is replaced by
251    /// `update` (of shape `len`).
252    SliceUpdate {
253        /// First index per dimension.
254        start: Vec<usize>,
255        /// Step per dimension (non-zero, may be negative).
256        step: Vec<isize>,
257        /// Number of selected indices per dimension (the update's shape).
258        len: Vec<usize>,
259    },
260    /// Reduction over sorted, unique `axes`; reduced dimensions become size 1.
261    Reduce {
262        /// The reduction.
263        op: ReduceOp,
264        /// Reduced axes, sorted and unique.
265        axes: Vec<usize>,
266    },
267    /// Index of an extreme value along `axis` (uint32); the axis becomes size 1.
268    ArgReduce {
269        /// Maximum or minimum.
270        op: ArgReduceOp,
271        /// The reduced axis.
272        axis: usize,
273    },
274    /// `(src, indices)`: the elements of `src` at `indices` along `axis` (MLX's
275    /// `GatherAxis`, `take_along_axis`). `indices` (integers) has `src`'s shape except
276    /// along `axis` and gives the output's shape. Negative indices count from the end;
277    /// out-of-range indices are clamped.
278    GatherAxis {
279        /// The indexed axis.
280        axis: usize,
281    },
282    /// `(dst, indices, updates)`: a copy of `dst` with `updates` placed at `indices` along
283    /// `axis` (MLX's `ScatterAxis`, `put_along_axis`). `indices` (integers) and `updates`
284    /// share a shape, which is `dst`'s except along `axis`. Indices as in `GatherAxis`.
285    ScatterAxis {
286        /// The indexed axis.
287        axis: usize,
288        /// How updates combine with `dst`.
289        reduce: ScatterReduce,
290    },
291    /// The input sorted along `axis` (ascending, stable, NaN last).
292    Sort {
293        /// The sorted axis.
294        axis: usize,
295    },
296    /// The indices (uint32) that sort the input along `axis`, as [`Primitive::Sort`].
297    ArgSort {
298        /// The sorted axis.
299        axis: usize,
300    },
301    /// A cumulative scan along `axis`.
302    Scan {
303        /// The combination.
304        op: ScanOp,
305        /// The scanned axis.
306        axis: usize,
307        /// Scan from the end of the axis.
308        reverse: bool,
309        /// Whether each output includes its own element (else it holds the elements
310        /// before it, starting from the combination's identity).
311        inclusive: bool,
312    },
313    /// The inputs (one dtype, equal shapes except along `axis`) joined along `axis`.
314    Concatenate {
315        /// The joined axis.
316        axis: usize,
317    },
318    /// Batched matrix product of `[B..., M, K]` and `[B..., K, N]` with equal batch
319    /// dimensions.
320    Matmul,
321    /// `alpha · (a @ b) + beta · c` for inputs `(a, b, c)`: a `Matmul` whose `c` has the
322    /// output's shape. The factors are stored as `f32` bits (so the enum stays `Eq + Hash`).
323    AddMM {
324        /// `f32::to_bits` of alpha.
325        alpha: u32,
326        /// `f32::to_bits` of beta.
327        beta: u32,
328    },
329    /// Affine quantization of the last axis in groups (MLX's `quantize`). Outputs the packed
330    /// codes (uint32, `bits` per value), the scales and the biases (the input's dtype).
331    Quantize {
332        /// Values per scale and bias: 32, 64 or 128.
333        group_size: usize,
334        /// Bits per value: 1–6 or 8.
335        bits: u8,
336    },
337    /// `(w, scales, biases)`: `code · scale + bias` per value, in the scales' dtype.
338    Dequantize {
339        /// Values per scale and bias.
340        group_size: usize,
341        /// Bits per value.
342        bits: u8,
343    },
344    /// `(x, w, scales, biases)`: `x · dequantize(w)ᵀ` (`transpose`) or `x · dequantize(w)`.
345    /// `w` is two-dimensional, or has `x`'s batch dimensions.
346    QuantizedMatmul {
347        /// Values per scale and bias.
348        group_size: usize,
349        /// Bits per value.
350        bits: u8,
351        /// Whether the dequantized matrix is used transposed (MLX's default).
352        transpose: bool,
353    },
354    /// A fused operation with a dedicated GPU kernel ([`crate::fast`]).
355    Fast(crate::fast::imp::FastOp),
356    /// Element-wise operations fused into one generated kernel ([`crate::graph::Fused`]), as
357    /// `compile` builds them. The inputs share the output's shape.
358    Compiled(crate::fused::Fused),
359    /// Random bits from threefry2x32 keys of shape `[..., 2]` (uint32): the output has shape
360    /// `keys.shape[..-1] + shape` and `width` bytes per element (MLX's layout).
361    RandomBits {
362        /// The shape drawn per key.
363        shape: Vec<usize>,
364        /// Bytes per element: 1, 2 or 4.
365        width: u8,
366    },
367    /// A call of a [`crate::custom_function`]: the inputs are the primals followed by the
368    /// function's outputs, which this node presents unchanged (views); transforms use the
369    /// function's custom rules.
370    Custom(CustomCall),
371    /// A custom Metal kernel ([`crate::fast::MetalKernel`]).
372    MetalKernel(crate::kernel::KernelCall),
373    /// A row-contiguous copy of the input into a new buffer. Never simplified away (as a
374    /// same-type conversion or identity view would be).
375    Copy,
376    /// `(x, deps...)`: `x` unchanged (sharing its data), evaluated only after the
377    /// `n_deps` other inputs. Gradients flow to `x` only. It orders work, as MLX's
378    /// `depends`: a recomputation that depends on cotangents cannot be merged with the
379    /// forward pass.
380    Depends {
381        /// The number of dependencies after `x`.
382        n_deps: usize,
383    },
384    /// Test-only two-output primitive: `(a + b, a * b)`.
385    #[cfg(test)]
386    AddMultiply,
387}
388
389/// The dtype, shape and (for views) strides of one output of a primitive.
390#[derive(Debug, Clone, PartialEq, Eq)]
391pub(crate) struct OutputSpec {
392    pub(crate) dtype: DType,
393    pub(crate) shape: Shape,
394    /// `None` means row-contiguous.
395    pub(crate) strides: Option<Strides>,
396}
397
398impl Primitive {
399    /// `(alpha, beta)` of an `AddMM`.
400    pub(crate) fn addmm_factors(&self) -> (f32, f32) {
401        match self {
402            Self::AddMM { alpha, beta } => (f32::from_bits(*alpha), f32::from_bits(*beta)),
403            _ => (1.0, 0.0),
404        }
405    }
406
407    /// The operation's name, as used in error messages.
408    pub fn name(&self) -> &'static str {
409        match self {
410            Self::Unary(op) => op.name(),
411            Self::Binary(op) => op.name(),
412            Self::Select => "where",
413            Self::AsType(_) => "astype",
414            Self::Reshape(_) => "reshape",
415            Self::Transpose(_) => "transpose",
416            Self::Slice { .. } => "slice",
417            Self::Broadcast(_) => "broadcast_to",
418            Self::StopGradient => "stop_gradient",
419            Self::SliceUpdate { .. } => "slice_update",
420            Self::Reduce { op, .. } => op.name(),
421            Self::ArgReduce { op, .. } => op.name(),
422            Self::GatherAxis { .. } => "take_along_axis",
423            Self::ScatterAxis {
424                reduce: ScatterReduce::None,
425                ..
426            } => "put_along_axis",
427            Self::ScatterAxis { .. } => "scatter_add_along_axis",
428            Self::Concatenate { .. } => "concatenate",
429            Self::Sort { .. } => "sort",
430            Self::ArgSort { .. } => "argsort",
431            Self::Scan { op, .. } => op.name(),
432            Self::Matmul => "matmul",
433            Self::AddMM { .. } => "addmm",
434            Self::RandomBits { .. } => "random_bits",
435            Self::Fast(op) => op.name(),
436            Self::Compiled(_) => "compiled",
437            Self::Quantize { .. } => "quantize",
438            Self::Dequantize { .. } => "dequantize",
439            Self::QuantizedMatmul { .. } => "quantized_matmul",
440            Self::Custom(_) => "custom_function",
441            Self::MetalKernel(_) => "metal_kernel",
442            Self::Copy => "copy",
443            Self::Depends { .. } => "depends",
444            #[cfg(test)]
445            Self::AddMultiply => "add_multiply",
446        }
447    }
448
449    /// Validates the inputs and infers the outputs, without computing anything.
450    pub(crate) fn output_specs(&self, inputs: &[Array]) -> Result<SmallVec<[OutputSpec; 1]>> {
451        let computed = |dtype: DType, shape: &[usize]| OutputSpec {
452            dtype,
453            shape: shape.into(),
454            strides: None,
455        };
456        let spec = match self {
457            Self::Unary(op) => {
458                let [a] = self.arity::<1>(inputs)?;
459                // As the ops layer promotes and rejects (the kernels rely on it).
460                let supported = match op {
461                    UnaryOp::Negative => a.dtype() != DType::Bool,
462                    UnaryOp::Abs | UnaryOp::Conjugate => true,
463                    _ => a.dtype().category() == Category::Float,
464                };
465                if !supported {
466                    return Err(Error::unsupported_dtype(op.name(), a.dtype()));
467                }
468                let dtype = match (op, a.dtype()) {
469                    (UnaryOp::Abs, DType::Complex64) => DType::F32,
470                    (_, d) => d,
471                };
472                computed(dtype, a.dims())
473            }
474            Self::Binary(op) => {
475                let [a, b] = self.arity::<2>(inputs)?;
476                self.same_shape_and_dtype(a, b)?;
477                let category = a.dtype().category();
478                let unsupported = match op {
479                    BinaryOp::LogAddExp => category != Category::Float,
480                    BinaryOp::Divide => !matches!(category, Category::Float | Category::Complex),
481                    BinaryOp::Maximum
482                    | BinaryOp::Minimum
483                    | BinaryOp::Less
484                    | BinaryOp::LessEqual
485                    | BinaryOp::Greater
486                    | BinaryOp::GreaterEqual => category == Category::Complex,
487                    _ => false,
488                };
489                if unsupported {
490                    return Err(Error::unsupported_dtype(op.name(), a.dtype()));
491                }
492                computed(
493                    if op.is_comparison() {
494                        DType::Bool
495                    } else {
496                        a.dtype()
497                    },
498                    a.dims(),
499                )
500            }
501            Self::Select => {
502                let [c, a, b] = self.arity::<3>(inputs)?;
503                self.same_shape_and_dtype(a, b)?;
504                if c.dtype() != DType::Bool || c.dims() != a.dims() {
505                    return Err(Error::InvalidArgument(
506                        "where: condition must be a bool array of the same shape".into(),
507                    ));
508                }
509                computed(a.dtype(), a.dims())
510            }
511            Self::AsType(dtype) => {
512                let [a] = self.arity::<1>(inputs)?;
513                computed(*dtype, a.dims())
514            }
515            Self::Reshape(shape) => {
516                let [a] = self.arity::<1>(inputs)?;
517                if crate::shape::checked_size(shape)? != a.size() {
518                    return Err(Error::InvalidArgument(format!(
519                        "reshape: cannot reshape {} elements into shape {shape:?}",
520                        a.size()
521                    )));
522                }
523                OutputSpec {
524                    dtype: a.dtype(),
525                    shape: shape.as_slice().into(),
526                    strides: view::nocopy_reshape(a.dims(), a.strides(), shape),
527                }
528            }
529            Self::Transpose(axes) => {
530                let [a] = self.arity::<1>(inputs)?;
531                let (shape, strides) = view::permute(a.dims(), a.strides(), axes)?;
532                OutputSpec {
533                    dtype: a.dtype(),
534                    shape,
535                    strides: Some(strides),
536                }
537            }
538            Self::Slice { len, .. } => {
539                let [a] = self.arity::<1>(inputs)?;
540                let dims = self.dim_slices(a.ndim())?;
541                if !region_fits(&dims, a.dims()) {
542                    return Err(Error::InvalidArgument(format!(
543                        "slice: region {dims:?} does not fit shape {:?}",
544                        a.dims()
545                    )));
546                }
547                let (strides, _) = view::slice_geometry(a.strides(), &dims);
548                OutputSpec {
549                    dtype: a.dtype(),
550                    shape: len.as_slice().into(),
551                    strides: Some(strides),
552                }
553            }
554            Self::Broadcast(shape) => {
555                let [a] = self.arity::<1>(inputs)?;
556                let strides = view::broadcast_strides(a.dims(), a.strides(), shape)?;
557                OutputSpec {
558                    dtype: a.dtype(),
559                    shape: shape.as_slice().into(),
560                    strides: Some(strides),
561                }
562            }
563            Self::Copy => {
564                let [a] = self.arity::<1>(inputs)?;
565                computed(a.dtype(), a.dims())
566            }
567            Self::Depends { n_deps } => {
568                if inputs.len() != n_deps + 1 {
569                    return Err(Error::InvalidArgument(format!(
570                        "depends: {} dependencies expected, got {}",
571                        n_deps,
572                        inputs.len().saturating_sub(1)
573                    )));
574                }
575                let a = &inputs[0];
576                OutputSpec {
577                    dtype: a.dtype(),
578                    shape: a.dims().into(),
579                    strides: Some(a.strides().into()),
580                }
581            }
582            Self::StopGradient => {
583                let [a] = self.arity::<1>(inputs)?;
584                OutputSpec {
585                    dtype: a.dtype(),
586                    shape: a.dims().into(),
587                    strides: Some(a.strides().into()),
588                }
589            }
590            Self::SliceUpdate { len, .. } => {
591                let [base, update] = self.arity::<2>(inputs)?;
592                let dims = self.dim_slices(base.ndim())?;
593                if !region_fits(&dims, base.dims()) || update.dims() != len.as_slice() {
594                    return Err(Error::InvalidArgument(format!(
595                        "slice_update: region {dims:?} does not fit shape {:?} with update {:?}",
596                        base.dims(),
597                        update.dims()
598                    )));
599                }
600                if base.dtype() != update.dtype() {
601                    return Err(Error::dtype_mismatch(
602                        "slice_update",
603                        base.dtype(),
604                        update.dtype(),
605                    ));
606                }
607                computed(base.dtype(), base.dims())
608            }
609            Self::Reduce { op, axes } => {
610                let [a] = self.arity::<1>(inputs)?;
611                let shape = self.reduced_shape(a.dims(), axes)?;
612                if !axes.windows(2).all(|w| w[0] < w[1]) {
613                    return Err(Error::InvalidArgument(format!(
614                        "{}: axes {axes:?} must be sorted and unique",
615                        self.name()
616                    )));
617                }
618                let supported = match op {
619                    ReduceOp::And | ReduceOp::Or => a.dtype() == DType::Bool,
620                    ReduceOp::Max | ReduceOp::Min => a.dtype() != DType::Complex64,
621                    ReduceOp::Sum | ReduceOp::Prod => true,
622                };
623                if !supported {
624                    return Err(Error::unsupported_dtype(self.name(), a.dtype()));
625                }
626                let ordered = matches!(op, ReduceOp::Max | ReduceOp::Min);
627                if ordered && axes.iter().any(|&d| a.dims()[d] == 0) {
628                    return Err(Error::InvalidArgument(format!(
629                        "{}: cannot reduce a zero-length axis (no identity)",
630                        self.name()
631                    )));
632                }
633                computed(a.dtype(), &shape)
634            }
635            Self::ArgReduce { axis, .. } => {
636                let [a] = self.arity::<1>(inputs)?;
637                if a.dtype() == DType::Complex64 {
638                    return Err(Error::unsupported_dtype(self.name(), a.dtype()));
639                }
640                computed(DType::U32, &self.reduced_shape(a.dims(), &[*axis])?)
641            }
642            Self::GatherAxis { axis } => {
643                let [src, indices] = self.arity::<2>(inputs)?;
644                self.indexed(src, indices, *axis)?;
645                computed(src.dtype(), indices.dims())
646            }
647            Self::ScatterAxis { axis, .. } => {
648                let [dst, indices, updates] = self.arity::<3>(inputs)?;
649                self.indexed(dst, indices, *axis)?;
650                if updates.dims() != indices.dims() {
651                    return Err(Error::shape_mismatch(
652                        self.name(),
653                        indices.dims(),
654                        updates.dims(),
655                    ));
656                }
657                if updates.dtype() != dst.dtype() {
658                    return Err(Error::dtype_mismatch(
659                        self.name(),
660                        dst.dtype(),
661                        updates.dtype(),
662                    ));
663                }
664                computed(dst.dtype(), dst.dims())
665            }
666            Self::Sort { axis } | Self::ArgSort { axis } | Self::Scan { axis, .. } => {
667                let [a] = self.arity::<1>(inputs)?;
668                if *axis >= a.ndim() {
669                    return Err(Error::InvalidArgument(format!(
670                        "{}: axis {axis} out of range for {} dimensions",
671                        self.name(),
672                        a.ndim()
673                    )));
674                }
675                let ordered = matches!(
676                    self,
677                    Self::Sort { .. }
678                        | Self::ArgSort { .. }
679                        | Self::Scan {
680                            op: ScanOp::Max | ScanOp::Min,
681                            ..
682                        }
683                );
684                let float_only = matches!(
685                    self,
686                    Self::Scan {
687                        op: ScanOp::LogAddExp,
688                        ..
689                    }
690                );
691                let category = a.dtype().category();
692                if (ordered && category == crate::Category::Complex)
693                    || (float_only && category != crate::Category::Float)
694                {
695                    return Err(Error::unsupported_dtype(self.name(), a.dtype()));
696                }
697                let dtype = match self {
698                    Self::ArgSort { .. } => DType::U32,
699                    _ => a.dtype(),
700                };
701                computed(dtype, a.dims())
702            }
703            Self::Concatenate { axis } => {
704                let first = inputs.first().ok_or_else(|| {
705                    Error::InvalidArgument("concatenate: no arrays to join".into())
706                })?;
707                if *axis >= first.ndim() {
708                    return Err(Error::InvalidArgument(format!(
709                        "concatenate: axis {axis} out of range for {} dimensions",
710                        first.ndim()
711                    )));
712                }
713                let mut shape: Shape = first.dims().into();
714                for a in &inputs[1..] {
715                    let fits = a.ndim() == first.ndim()
716                        && a.dims()
717                            .iter()
718                            .zip(first.dims())
719                            .enumerate()
720                            .all(|(d, (x, y))| d == *axis || x == y);
721                    if !fits {
722                        return Err(Error::shape_mismatch("concatenate", first.dims(), a.dims()));
723                    }
724                    if a.dtype() != first.dtype() {
725                        return Err(Error::dtype_mismatch(
726                            "concatenate",
727                            first.dtype(),
728                            a.dtype(),
729                        ));
730                    }
731                    shape[*axis] = shape[*axis].checked_add(a.dims()[*axis]).ok_or_else(|| {
732                        Error::InvalidArgument("concatenate: the joined size overflows".into())
733                    })?;
734                }
735                computed(first.dtype(), &shape)
736            }
737            Self::Matmul | Self::AddMM { .. } => {
738                let (a, b) = match self {
739                    Self::Matmul => {
740                        let [a, b] = self.arity::<2>(inputs)?;
741                        (a, b)
742                    }
743                    _ => {
744                        let [a, b, _] = self.arity::<3>(inputs)?;
745                        (a, b)
746                    }
747                };
748                let (sa, sb) = (a.dims(), b.dims());
749                let n = sa.len();
750                let valid =
751                    n >= 2 && sb.len() == n && sa[..n - 2] == sb[..n - 2] && sa[n - 1] == sb[n - 2];
752                if !valid {
753                    return Err(Error::shape_mismatch(self.name(), sa, sb));
754                }
755                if a.dtype() != b.dtype() {
756                    return Err(Error::dtype_mismatch(self.name(), a.dtype(), b.dtype()));
757                }
758                if !matches!(a.dtype().category(), Category::Float | Category::Complex) {
759                    return Err(Error::unsupported_dtype(self.name(), a.dtype()));
760                }
761                let mut shape: Shape = sa.into();
762                shape[n - 1] = sb[n - 1];
763                if let Self::AddMM { .. } = self {
764                    let c = &inputs[2];
765                    if c.dims() != shape.as_slice() || c.dtype() != a.dtype() {
766                        return Err(Error::InvalidArgument(format!(
767                            "addmm: c must be {} of shape {shape:?}, got {} {:?}",
768                            a.dtype(),
769                            c.dtype(),
770                            c.dims()
771                        )));
772                    }
773                }
774                computed(a.dtype(), &shape)
775            }
776            Self::RandomBits { shape, width } => {
777                let [keys] = self.arity::<1>(inputs)?;
778                if keys.dtype() != DType::U32 || keys.dims().last() != Some(&2) {
779                    return Err(Error::InvalidArgument(format!(
780                        "random_bits: keys must be uint32 of shape [..., 2], got {} {:?}",
781                        keys.dtype(),
782                        keys.dims()
783                    )));
784                }
785                let dtype = match width {
786                    1 => DType::U8,
787                    2 => DType::U16,
788                    4 => DType::U32,
789                    _ => {
790                        return Err(Error::InvalidArgument(format!(
791                            "random_bits: width must be 1, 2 or 4, got {width}"
792                        )));
793                    }
794                };
795                let out: Vec<usize> = keys.dims()[..keys.ndim() - 1]
796                    .iter()
797                    .chain(shape)
798                    .copied()
799                    .collect();
800                computed(dtype, &out)
801            }
802            Self::Fast(op) => {
803                return Ok(op
804                    .outputs(inputs)?
805                    .into_iter()
806                    .map(|(dtype, shape)| computed(dtype, &shape))
807                    .collect());
808            }
809            Self::Compiled(fused) => {
810                let dtypes: Vec<DType> = inputs.iter().map(Array::dtype).collect();
811                if dtypes != fused.inputs() {
812                    return Err(Error::InvalidArgument(format!(
813                        "compiled: inputs {dtypes:?}, expected {:?}",
814                        fused.inputs()
815                    )));
816                }
817                let shape = inputs[0].dims();
818                if let Some(other) = inputs.iter().find(|a| a.dims() != shape) {
819                    return Err(Error::shape_mismatch("compiled", shape, other.dims()));
820                }
821                return Ok(fused
822                    .dtypes()
823                    .into_iter()
824                    .map(|d| computed(d, shape))
825                    .collect());
826            }
827            Self::Quantize { group_size, bits } => {
828                let [w] = self.arity::<1>(inputs)?;
829                crate::quantized::check_params(self.name(), *group_size, u32::from(*bits))?;
830                let n = w.ndim();
831                if !matches!(w.dtype(), DType::F16 | DType::BF16 | DType::F32) {
832                    return Err(Error::unsupported_dtype("quantize", w.dtype()));
833                }
834                if n < 2 || w.dims()[n - 1] % group_size != 0 {
835                    return Err(Error::InvalidArgument(format!(
836                        "quantize: needs at least 2 dimensions with the last divisible by \
837                         the group size {group_size}, got shape {:?}",
838                        w.dims()
839                    )));
840                }
841                let last = w.dims()[n - 1];
842                let with_last = |d: usize| {
843                    let mut s: Shape = w.dims().into();
844                    s[n - 1] = d;
845                    s
846                };
847                let packed = computed(DType::U32, &with_last(last * usize::from(*bits) / 32));
848                let groups = computed(w.dtype(), &with_last(last / group_size));
849                return Ok(smallvec![packed, groups.clone(), groups]);
850            }
851            Self::Dequantize { group_size, bits } => {
852                let [w, scales, _] = self.arity::<3>(inputs)?;
853                crate::quantized::check_params(self.name(), *group_size, u32::from(*bits))?;
854                let mut shape: Shape = w.dims().into();
855                let n = shape.len();
856                shape[n - 1] = self.quantized_columns(w, &inputs[1..], *group_size, *bits)?;
857                computed(scales.dtype(), &shape)
858            }
859            Self::QuantizedMatmul {
860                group_size,
861                bits,
862                transpose,
863            } => {
864                let [x, w, scales, _] = self.arity::<4>(inputs)?;
865                crate::quantized::check_params(self.name(), *group_size, u32::from(*bits))?;
866                let columns = self.quantized_columns(w, &inputs[2..], *group_size, *bits)?;
867                let rows = w.dims()[w.ndim() - 2];
868                let (inner, outer) = if *transpose {
869                    (columns, rows)
870                } else {
871                    (rows, columns)
872                };
873                let (xn, wn) = (x.ndim(), w.ndim());
874                let batch_ok = wn == 2 || (xn == wn && x.dims()[..xn - 2] == w.dims()[..wn - 2]);
875                if xn == 0 || x.dims()[xn - 1] != inner || !batch_ok {
876                    return Err(Error::shape_mismatch(
877                        "quantized_matmul",
878                        x.dims(),
879                        w.dims(),
880                    ));
881                }
882                if x.dtype() != scales.dtype() {
883                    return Err(Error::dtype_mismatch(
884                        "quantized_matmul",
885                        x.dtype(),
886                        scales.dtype(),
887                    ));
888                }
889                let mut shape: Shape = x.dims().into();
890                shape[xn - 1] = outer;
891                computed(x.dtype(), &shape)
892            }
893            Self::MetalKernel(call) => {
894                return Ok(call
895                    .outputs
896                    .iter()
897                    .map(|(dtype, shape)| computed(*dtype, shape))
898                    .collect());
899            }
900            Self::Custom(call) => {
901                let outputs = inputs.get(call.primals..).unwrap_or_default();
902                if outputs.is_empty() {
903                    return Err(Error::InvalidArgument(
904                        "custom_function: the function must return at least one array".into(),
905                    ));
906                }
907                return Ok(outputs
908                    .iter()
909                    .map(|a| OutputSpec {
910                        dtype: a.dtype(),
911                        shape: a.dims().into(),
912                        strides: Some(a.strides().into()),
913                    })
914                    .collect());
915            }
916            #[cfg(test)]
917            Self::AddMultiply => {
918                let [a, b] = self.arity::<2>(inputs)?;
919                self.same_shape_and_dtype(a, b)?;
920                let spec = computed(a.dtype(), a.dims());
921                return Ok(smallvec![spec.clone(), spec]);
922            }
923        };
924        Ok(smallvec![spec])
925    }
926
927    /// The per-dimension slices of a `Slice` or `SliceUpdate` primitive over `ndim`
928    /// dimensions.
929    pub(crate) fn dim_slices(&self, ndim: usize) -> Result<Vec<DimSlice>> {
930        match self {
931            Self::Slice { start, step, len } | Self::SliceUpdate { start, step, len }
932                if start.len() == ndim && step.len() == ndim && len.len() == ndim =>
933            {
934                Ok((0..ndim)
935                    .map(|i| DimSlice {
936                        start: start[i],
937                        step: step[i],
938                        len: len[i],
939                    })
940                    .collect())
941            }
942            _ => Err(Error::InvalidArgument(
943                "slice: one start, step and length per dimension".into(),
944            )),
945        }
946    }
947
948    /// The number of values per row of packed codes `w` (uint32, at least 2-D), after
949    /// validating it against `[scales, biases]` (one float dtype, equal shapes, one value per
950    /// group), as MLX's `validate_quantized_input`.
951    fn quantized_columns(
952        &self,
953        w: &Array,
954        groups: &[Array],
955        group_size: usize,
956        bits: u8,
957    ) -> Result<usize> {
958        let (scales, biases) = (&groups[0], &groups[1]);
959        let n = w.ndim();
960        let invalid =
961            |what: String| Err(Error::InvalidArgument(format!("{}: {what}", self.name())));
962        if w.dtype() != DType::U32 {
963            return invalid(format!("packed weights must be uint32, got {}", w.dtype()));
964        }
965        if !matches!(scales.dtype(), DType::F16 | DType::BF16 | DType::F32)
966            || biases.dtype() != scales.dtype()
967            || biases.dims() != scales.dims()
968        {
969            return invalid(format!(
970                "scales and biases must share a float dtype and shape, got {} {:?} and {} {:?}",
971                scales.dtype(),
972                scales.dims(),
973                biases.dtype(),
974                biases.dims()
975            ));
976        }
977        let packed = w.dims().last().copied().unwrap_or(0) * 32;
978        let groups = scales.dims().last().copied().unwrap_or(0) * group_size;
979        if n < 2
980            || scales.ndim() != n
981            || w.dims()[..n - 1] != scales.dims()[..n - 1]
982            || packed != groups * usize::from(bits)
983        {
984            return invalid(format!(
985                "packed weights {:?} and scales {:?} do not match group size {group_size} \
986                 and {bits} bits",
987                w.dims(),
988                scales.dims()
989            ));
990        }
991        Ok(groups)
992    }
993
994    /// Validates an indexing operation's array and indices: integer indices with the
995    /// array's shape except along `axis`.
996    fn indexed(&self, array: &Array, indices: &Array, axis: usize) -> Result<()> {
997        if !matches!(
998            indices.dtype().category(),
999            crate::Category::Signed | crate::Category::Unsigned
1000        ) {
1001            return Err(Error::InvalidArgument(format!(
1002                "{}: indices must be integers, got {}",
1003                self.name(),
1004                indices.dtype()
1005            )));
1006        }
1007        let fits = indices.ndim() == array.ndim()
1008            && axis < array.ndim()
1009            && indices
1010                .dims()
1011                .iter()
1012                .zip(array.dims())
1013                .enumerate()
1014                .all(|(d, (i, a))| d == axis || i == a);
1015        if !fits {
1016            return Err(Error::InvalidArgument(format!(
1017                "{}: indices of shape {:?} do not fit an array of shape {:?} along axis {axis}",
1018                self.name(),
1019                indices.dims(),
1020                array.dims()
1021            )));
1022        }
1023        if array.dims()[axis] == 0 && indices.size() > 0 {
1024            return Err(Error::InvalidArgument(format!(
1025                "{}: cannot index an empty axis",
1026                self.name()
1027            )));
1028        }
1029        Ok(())
1030    }
1031
1032    /// `shape` with `axes` set to 1, after validating the axes.
1033    fn reduced_shape(&self, shape: &[usize], axes: &[usize]) -> Result<Shape> {
1034        let mut out: Shape = shape.into();
1035        for &axis in axes {
1036            *out.get_mut(axis).ok_or_else(|| {
1037                Error::InvalidArgument(format!(
1038                    "{}: axis {axis} out of range for shape {shape:?}",
1039                    self.name()
1040                ))
1041            })? = 1;
1042        }
1043        Ok(out)
1044    }
1045
1046    fn arity<'a, const N: usize>(&self, inputs: &'a [Array]) -> Result<&'a [Array; N]> {
1047        inputs.try_into().map_err(|_| {
1048            Error::InvalidArgument(format!(
1049                "{} takes {N} inputs, got {}",
1050                self.name(),
1051                inputs.len()
1052            ))
1053        })
1054    }
1055
1056    fn same_shape_and_dtype(&self, a: &Array, b: &Array) -> Result<()> {
1057        if a.dims() != b.dims() {
1058            return Err(Error::shape_mismatch(self.name(), a.dims(), b.dims()));
1059        }
1060        if a.dtype() != b.dtype() {
1061            return Err(Error::dtype_mismatch(self.name(), a.dtype(), b.dtype()));
1062        }
1063        Ok(())
1064    }
1065}
1066
1067/// A row-contiguous copy of `x` ([`Primitive::Copy`]).
1068pub(crate) fn copy(x: &Array) -> Result<Array> {
1069    Ok(Array::from_op(Primitive::Copy, vec![x.clone()])?.swap_remove(0))
1070}
1071
1072/// `x`, evaluated after `deps` ([`Primitive::Depends`]).
1073pub(crate) fn depends(x: &Array, deps: &[Array]) -> Result<Array> {
1074    let mut inputs = Vec::with_capacity(deps.len() + 1);
1075    inputs.push(x.clone());
1076    inputs.extend_from_slice(deps);
1077    let primitive = Primitive::Depends { n_deps: deps.len() };
1078    Ok(Array::from_op(primitive, inputs)?.swap_remove(0))
1079}
1080
1081/// Whether each dimension's slice selects indices within `shape` with a non-zero step (a
1082/// zero-length slice starts at most at the end), so that no offset computed from it
1083/// overflows. The step of a slice of at most one index is never applied (any value).
1084pub(crate) fn region_fits(dims: &[DimSlice], shape: &[usize]) -> bool {
1085    dims.len() == shape.len()
1086        && dims.iter().zip(shape).all(|(d, &n)| {
1087            if d.step == 0 {
1088                return false;
1089            }
1090            if d.len == 0 {
1091                return d.start <= n;
1092            }
1093            let last = isize::try_from(d.len - 1)
1094                .ok()
1095                .and_then(|steps| steps.checked_mul(d.step))
1096                .and_then(|span| isize::try_from(d.start).ok()?.checked_add(span));
1097            d.start < n && last.is_some_and(|last| (0..n as isize).contains(&last))
1098        })
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103    use std::collections::hash_map::DefaultHasher;
1104    use std::hash::{Hash, Hasher};
1105
1106    use super::*;
1107
1108    fn hash(p: &Primitive) -> u64 {
1109        let mut h = DefaultHasher::new();
1110        p.hash(&mut h);
1111        h.finish()
1112    }
1113
1114    #[test]
1115    fn equal_primitives_compare_and_hash_equal() {
1116        let add = Primitive::Binary(BinaryOp::Add);
1117        assert_eq!(add, Primitive::Binary(BinaryOp::Add));
1118        assert_eq!(hash(&add), hash(&Primitive::Binary(BinaryOp::Add)));
1119        assert_ne!(add, Primitive::Binary(BinaryOp::Multiply));
1120        let s = |start: Vec<usize>| Primitive::Slice {
1121            start,
1122            step: vec![1],
1123            len: vec![2],
1124        };
1125        assert_eq!(s(vec![1]), s(vec![1]));
1126        assert_ne!(s(vec![1]), s(vec![2]));
1127    }
1128}