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::FastOp),
356    /// Element-wise operations fused into one generated kernel ([`crate::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    /// Test-only two-output primitive: `(a + b, a * b)`.
374    #[cfg(test)]
375    AddMultiply,
376}
377
378/// The dtype, shape and (for views) strides of one output of a primitive.
379#[derive(Debug, Clone, PartialEq, Eq)]
380pub(crate) struct OutputSpec {
381    pub(crate) dtype: DType,
382    pub(crate) shape: Shape,
383    /// `None` means row-contiguous.
384    pub(crate) strides: Option<Strides>,
385}
386
387impl Primitive {
388    /// `(alpha, beta)` of an `AddMM`.
389    pub(crate) fn addmm_factors(&self) -> (f32, f32) {
390        match self {
391            Self::AddMM { alpha, beta } => (f32::from_bits(*alpha), f32::from_bits(*beta)),
392            _ => (1.0, 0.0),
393        }
394    }
395
396    /// The operation's name, as used in error messages.
397    pub fn name(&self) -> &'static str {
398        match self {
399            Self::Unary(op) => op.name(),
400            Self::Binary(op) => op.name(),
401            Self::Select => "where",
402            Self::AsType(_) => "astype",
403            Self::Reshape(_) => "reshape",
404            Self::Transpose(_) => "transpose",
405            Self::Slice { .. } => "slice",
406            Self::Broadcast(_) => "broadcast_to",
407            Self::StopGradient => "stop_gradient",
408            Self::SliceUpdate { .. } => "slice_update",
409            Self::Reduce { op, .. } => op.name(),
410            Self::ArgReduce { op, .. } => op.name(),
411            Self::GatherAxis { .. } => "take_along_axis",
412            Self::ScatterAxis {
413                reduce: ScatterReduce::None,
414                ..
415            } => "put_along_axis",
416            Self::ScatterAxis { .. } => "scatter_add_along_axis",
417            Self::Concatenate { .. } => "concatenate",
418            Self::Sort { .. } => "sort",
419            Self::ArgSort { .. } => "argsort",
420            Self::Scan { op, .. } => op.name(),
421            Self::Matmul => "matmul",
422            Self::AddMM { .. } => "addmm",
423            Self::RandomBits { .. } => "random_bits",
424            Self::Fast(op) => op.name(),
425            Self::Compiled(_) => "compiled",
426            Self::Quantize { .. } => "quantize",
427            Self::Dequantize { .. } => "dequantize",
428            Self::QuantizedMatmul { .. } => "quantized_matmul",
429            Self::Custom(_) => "custom_function",
430            Self::MetalKernel(_) => "metal_kernel",
431            #[cfg(test)]
432            Self::AddMultiply => "add_multiply",
433        }
434    }
435
436    /// Validates the inputs and infers the outputs, without computing anything.
437    pub(crate) fn output_specs(&self, inputs: &[Array]) -> Result<SmallVec<[OutputSpec; 1]>> {
438        let computed = |dtype: DType, shape: &[usize]| OutputSpec {
439            dtype,
440            shape: shape.into(),
441            strides: None,
442        };
443        let spec = match self {
444            Self::Unary(op) => {
445                let [a] = self.arity::<1>(inputs)?;
446                let dtype = match (op, a.dtype()) {
447                    (UnaryOp::Abs, DType::Complex64) => DType::F32,
448                    (_, d) => d,
449                };
450                computed(dtype, a.shape())
451            }
452            Self::Binary(op) => {
453                let [a, b] = self.arity::<2>(inputs)?;
454                self.same_shape_and_dtype(a, b)?;
455                if *op == BinaryOp::LogAddExp && a.dtype().category() != Category::Float {
456                    return Err(Error::UnsupportedDType {
457                        op: op.name(),
458                        dtype: a.dtype(),
459                    });
460                }
461                computed(
462                    if op.is_comparison() {
463                        DType::Bool
464                    } else {
465                        a.dtype()
466                    },
467                    a.shape(),
468                )
469            }
470            Self::Select => {
471                let [c, a, b] = self.arity::<3>(inputs)?;
472                self.same_shape_and_dtype(a, b)?;
473                if c.dtype() != DType::Bool || c.shape() != a.shape() {
474                    return Err(Error::InvalidArgument(
475                        "where: condition must be a bool array of the same shape".into(),
476                    ));
477                }
478                computed(a.dtype(), a.shape())
479            }
480            Self::AsType(dtype) => {
481                let [a] = self.arity::<1>(inputs)?;
482                computed(*dtype, a.shape())
483            }
484            Self::Reshape(shape) => {
485                let [a] = self.arity::<1>(inputs)?;
486                OutputSpec {
487                    dtype: a.dtype(),
488                    shape: shape.as_slice().into(),
489                    strides: view::nocopy_reshape(a.shape(), a.strides(), shape),
490                }
491            }
492            Self::Transpose(axes) => {
493                let [a] = self.arity::<1>(inputs)?;
494                let (shape, strides) = view::permute(a.shape(), a.strides(), axes)?;
495                OutputSpec {
496                    dtype: a.dtype(),
497                    shape,
498                    strides: Some(strides),
499                }
500            }
501            Self::Slice { len, .. } => {
502                let [a] = self.arity::<1>(inputs)?;
503                let (strides, _) = view::slice_geometry(a.strides(), &self.dim_slices(a.ndim())?);
504                OutputSpec {
505                    dtype: a.dtype(),
506                    shape: len.as_slice().into(),
507                    strides: Some(strides),
508                }
509            }
510            Self::Broadcast(shape) => {
511                let [a] = self.arity::<1>(inputs)?;
512                let strides = view::broadcast_strides(a.shape(), a.strides(), shape)?;
513                OutputSpec {
514                    dtype: a.dtype(),
515                    shape: shape.as_slice().into(),
516                    strides: Some(strides),
517                }
518            }
519            Self::StopGradient => {
520                let [a] = self.arity::<1>(inputs)?;
521                OutputSpec {
522                    dtype: a.dtype(),
523                    shape: a.shape().into(),
524                    strides: Some(a.strides().into()),
525                }
526            }
527            Self::SliceUpdate { len, .. } => {
528                let [base, update] = self.arity::<2>(inputs)?;
529                let dims = self.dim_slices(base.ndim())?;
530                let in_bounds = dims.iter().zip(base.shape()).all(|(d, &n)| {
531                    let last = d.start as isize + (d.len as isize - 1) * d.step;
532                    d.len == 0 || (d.start < n && (0..n as isize).contains(&last))
533                });
534                if !in_bounds || update.shape() != len.as_slice() {
535                    return Err(Error::InvalidArgument(format!(
536                        "slice_update: region {dims:?} does not fit shape {:?} with update {:?}",
537                        base.shape(),
538                        update.shape()
539                    )));
540                }
541                if base.dtype() != update.dtype() {
542                    return Err(Error::DTypeMismatch {
543                        op: "slice_update",
544                        lhs: base.dtype(),
545                        rhs: update.dtype(),
546                    });
547                }
548                computed(base.dtype(), base.shape())
549            }
550            Self::Reduce { axes, .. } => {
551                let [a] = self.arity::<1>(inputs)?;
552                computed(a.dtype(), &self.reduced_shape(a.shape(), axes)?)
553            }
554            Self::ArgReduce { axis, .. } => {
555                let [a] = self.arity::<1>(inputs)?;
556                computed(DType::U32, &self.reduced_shape(a.shape(), &[*axis])?)
557            }
558            Self::GatherAxis { axis } => {
559                let [src, indices] = self.arity::<2>(inputs)?;
560                self.indexed(src, indices, *axis)?;
561                computed(src.dtype(), indices.shape())
562            }
563            Self::ScatterAxis { axis, .. } => {
564                let [dst, indices, updates] = self.arity::<3>(inputs)?;
565                self.indexed(dst, indices, *axis)?;
566                if updates.shape() != indices.shape() {
567                    return Err(Error::ShapeMismatch {
568                        op: self.name(),
569                        lhs: indices.shape().to_vec(),
570                        rhs: updates.shape().to_vec(),
571                    });
572                }
573                if updates.dtype() != dst.dtype() {
574                    return Err(Error::DTypeMismatch {
575                        op: self.name(),
576                        lhs: dst.dtype(),
577                        rhs: updates.dtype(),
578                    });
579                }
580                computed(dst.dtype(), dst.shape())
581            }
582            Self::Sort { axis } | Self::ArgSort { axis } | Self::Scan { axis, .. } => {
583                let [a] = self.arity::<1>(inputs)?;
584                if *axis >= a.ndim() {
585                    return Err(Error::InvalidArgument(format!(
586                        "{}: axis {axis} out of range for {} dimensions",
587                        self.name(),
588                        a.ndim()
589                    )));
590                }
591                let ordered = matches!(
592                    self,
593                    Self::Sort { .. }
594                        | Self::ArgSort { .. }
595                        | Self::Scan {
596                            op: ScanOp::Max | ScanOp::Min,
597                            ..
598                        }
599                );
600                let float_only = matches!(
601                    self,
602                    Self::Scan {
603                        op: ScanOp::LogAddExp,
604                        ..
605                    }
606                );
607                let category = a.dtype().category();
608                if (ordered && category == crate::Category::Complex)
609                    || (float_only && category != crate::Category::Float)
610                {
611                    return Err(Error::UnsupportedDType {
612                        op: self.name(),
613                        dtype: a.dtype(),
614                    });
615                }
616                let dtype = match self {
617                    Self::ArgSort { .. } => DType::U32,
618                    _ => a.dtype(),
619                };
620                computed(dtype, a.shape())
621            }
622            Self::Concatenate { axis } => {
623                let first = inputs.first().ok_or_else(|| {
624                    Error::InvalidArgument("concatenate: no arrays to join".into())
625                })?;
626                if *axis >= first.ndim() {
627                    return Err(Error::InvalidArgument(format!(
628                        "concatenate: axis {axis} out of range for {} dimensions",
629                        first.ndim()
630                    )));
631                }
632                let mut shape: Shape = first.shape().into();
633                for a in &inputs[1..] {
634                    let fits = a.ndim() == first.ndim()
635                        && a.shape()
636                            .iter()
637                            .zip(first.shape())
638                            .enumerate()
639                            .all(|(d, (x, y))| d == *axis || x == y);
640                    if !fits {
641                        return Err(Error::ShapeMismatch {
642                            op: "concatenate",
643                            lhs: first.shape().to_vec(),
644                            rhs: a.shape().to_vec(),
645                        });
646                    }
647                    if a.dtype() != first.dtype() {
648                        return Err(Error::DTypeMismatch {
649                            op: "concatenate",
650                            lhs: first.dtype(),
651                            rhs: a.dtype(),
652                        });
653                    }
654                    shape[*axis] += a.shape()[*axis];
655                }
656                computed(first.dtype(), &shape)
657            }
658            Self::Matmul | Self::AddMM { .. } => {
659                let (a, b) = match self {
660                    Self::Matmul => {
661                        let [a, b] = self.arity::<2>(inputs)?;
662                        (a, b)
663                    }
664                    _ => {
665                        let [a, b, _] = self.arity::<3>(inputs)?;
666                        (a, b)
667                    }
668                };
669                let (sa, sb) = (a.shape(), b.shape());
670                let n = sa.len();
671                let valid =
672                    n >= 2 && sb.len() == n && sa[..n - 2] == sb[..n - 2] && sa[n - 1] == sb[n - 2];
673                if !valid {
674                    return Err(Error::ShapeMismatch {
675                        op: self.name(),
676                        lhs: sa.to_vec(),
677                        rhs: sb.to_vec(),
678                    });
679                }
680                if a.dtype() != b.dtype() {
681                    return Err(Error::DTypeMismatch {
682                        op: self.name(),
683                        lhs: a.dtype(),
684                        rhs: b.dtype(),
685                    });
686                }
687                let mut shape: Shape = sa.into();
688                shape[n - 1] = sb[n - 1];
689                if let Self::AddMM { .. } = self {
690                    let c = &inputs[2];
691                    if c.shape() != shape.as_slice() || c.dtype() != a.dtype() {
692                        return Err(Error::InvalidArgument(format!(
693                            "addmm: c must be {} of shape {shape:?}, got {} {:?}",
694                            a.dtype(),
695                            c.dtype(),
696                            c.shape()
697                        )));
698                    }
699                }
700                computed(a.dtype(), &shape)
701            }
702            Self::RandomBits { shape, width } => {
703                let [keys] = self.arity::<1>(inputs)?;
704                if keys.dtype() != DType::U32 || keys.shape().last() != Some(&2) {
705                    return Err(Error::InvalidArgument(format!(
706                        "random_bits: keys must be uint32 of shape [..., 2], got {} {:?}",
707                        keys.dtype(),
708                        keys.shape()
709                    )));
710                }
711                let dtype = match width {
712                    1 => DType::U8,
713                    2 => DType::U16,
714                    4 => DType::U32,
715                    _ => {
716                        return Err(Error::InvalidArgument(format!(
717                            "random_bits: width must be 1, 2 or 4, got {width}"
718                        )));
719                    }
720                };
721                let out: Vec<usize> = keys.shape()[..keys.ndim() - 1]
722                    .iter()
723                    .chain(shape)
724                    .copied()
725                    .collect();
726                computed(dtype, &out)
727            }
728            Self::Fast(op) => {
729                return Ok(op
730                    .outputs(inputs)?
731                    .into_iter()
732                    .map(|(dtype, shape)| computed(dtype, &shape))
733                    .collect());
734            }
735            Self::Compiled(fused) => {
736                let dtypes: Vec<DType> = inputs.iter().map(Array::dtype).collect();
737                if dtypes != fused.inputs() {
738                    return Err(Error::InvalidArgument(format!(
739                        "compiled: inputs {dtypes:?}, expected {:?}",
740                        fused.inputs()
741                    )));
742                }
743                let shape = inputs[0].shape();
744                if let Some(other) = inputs.iter().find(|a| a.shape() != shape) {
745                    return Err(Error::ShapeMismatch {
746                        op: "compiled",
747                        lhs: shape.to_vec(),
748                        rhs: other.shape().to_vec(),
749                    });
750                }
751                return Ok(fused
752                    .dtypes()
753                    .into_iter()
754                    .map(|d| computed(d, shape))
755                    .collect());
756            }
757            Self::Quantize { group_size, bits } => {
758                let [w] = self.arity::<1>(inputs)?;
759                let n = w.ndim();
760                if !matches!(w.dtype(), DType::F16 | DType::BF16 | DType::F32) {
761                    return Err(Error::UnsupportedDType {
762                        op: "quantize",
763                        dtype: w.dtype(),
764                    });
765                }
766                if n < 2 || w.shape()[n - 1] % group_size != 0 {
767                    return Err(Error::InvalidArgument(format!(
768                        "quantize: needs at least 2 dimensions with the last divisible by \
769                         the group size {group_size}, got shape {:?}",
770                        w.shape()
771                    )));
772                }
773                let last = w.shape()[n - 1];
774                let with_last = |d: usize| {
775                    let mut s: Shape = w.shape().into();
776                    s[n - 1] = d;
777                    s
778                };
779                let packed = computed(DType::U32, &with_last(last * usize::from(*bits) / 32));
780                let groups = computed(w.dtype(), &with_last(last / group_size));
781                return Ok(smallvec![packed, groups.clone(), groups]);
782            }
783            Self::Dequantize { group_size, bits } => {
784                let [w, scales, _] = self.arity::<3>(inputs)?;
785                let mut shape: Shape = w.shape().into();
786                let n = shape.len();
787                shape[n - 1] = self.quantized_columns(w, &inputs[1..], *group_size, *bits)?;
788                computed(scales.dtype(), &shape)
789            }
790            Self::QuantizedMatmul {
791                group_size,
792                bits,
793                transpose,
794            } => {
795                let [x, w, scales, _] = self.arity::<4>(inputs)?;
796                let columns = self.quantized_columns(w, &inputs[2..], *group_size, *bits)?;
797                let rows = w.shape()[w.ndim() - 2];
798                let (inner, outer) = if *transpose {
799                    (columns, rows)
800                } else {
801                    (rows, columns)
802                };
803                let (xn, wn) = (x.ndim(), w.ndim());
804                let batch_ok = wn == 2 || (xn == wn && x.shape()[..xn - 2] == w.shape()[..wn - 2]);
805                if xn == 0 || x.shape()[xn - 1] != inner || !batch_ok {
806                    return Err(Error::ShapeMismatch {
807                        op: "quantized_matmul",
808                        lhs: x.shape().to_vec(),
809                        rhs: w.shape().to_vec(),
810                    });
811                }
812                if x.dtype() != scales.dtype() {
813                    return Err(Error::DTypeMismatch {
814                        op: "quantized_matmul",
815                        lhs: x.dtype(),
816                        rhs: scales.dtype(),
817                    });
818                }
819                let mut shape: Shape = x.shape().into();
820                shape[xn - 1] = outer;
821                computed(x.dtype(), &shape)
822            }
823            Self::MetalKernel(call) => {
824                return Ok(call
825                    .outputs
826                    .iter()
827                    .map(|(dtype, shape)| computed(*dtype, shape))
828                    .collect());
829            }
830            Self::Custom(call) => {
831                let outputs = inputs.get(call.primals..).unwrap_or_default();
832                if outputs.is_empty() {
833                    return Err(Error::InvalidArgument(
834                        "custom_function: the function must return at least one array".into(),
835                    ));
836                }
837                return Ok(outputs
838                    .iter()
839                    .map(|a| OutputSpec {
840                        dtype: a.dtype(),
841                        shape: a.shape().into(),
842                        strides: Some(a.strides().into()),
843                    })
844                    .collect());
845            }
846            #[cfg(test)]
847            Self::AddMultiply => {
848                let [a, b] = self.arity::<2>(inputs)?;
849                self.same_shape_and_dtype(a, b)?;
850                let spec = computed(a.dtype(), a.shape());
851                return Ok(smallvec![spec.clone(), spec]);
852            }
853        };
854        Ok(smallvec![spec])
855    }
856
857    /// The per-dimension slices of a `Slice` or `SliceUpdate` primitive over `ndim`
858    /// dimensions.
859    pub(crate) fn dim_slices(&self, ndim: usize) -> Result<Vec<DimSlice>> {
860        match self {
861            Self::Slice { start, step, len } | Self::SliceUpdate { start, step, len }
862                if start.len() == ndim && step.len() == ndim && len.len() == ndim =>
863            {
864                Ok((0..ndim)
865                    .map(|i| DimSlice {
866                        start: start[i],
867                        step: step[i],
868                        len: len[i],
869                    })
870                    .collect())
871            }
872            _ => Err(Error::InvalidArgument(
873                "slice: one start, step and length per dimension".into(),
874            )),
875        }
876    }
877
878    /// The number of values per row of packed codes `w` (uint32, at least 2-D), after
879    /// validating it against `[scales, biases]` (one float dtype, equal shapes, one value per
880    /// group), as MLX's `validate_quantized_input`.
881    fn quantized_columns(
882        &self,
883        w: &Array,
884        groups: &[Array],
885        group_size: usize,
886        bits: u8,
887    ) -> Result<usize> {
888        let (scales, biases) = (&groups[0], &groups[1]);
889        let n = w.ndim();
890        let invalid =
891            |what: String| Err(Error::InvalidArgument(format!("{}: {what}", self.name())));
892        if w.dtype() != DType::U32 {
893            return invalid(format!("packed weights must be uint32, got {}", w.dtype()));
894        }
895        if !matches!(scales.dtype(), DType::F16 | DType::BF16 | DType::F32)
896            || biases.dtype() != scales.dtype()
897            || biases.shape() != scales.shape()
898        {
899            return invalid(format!(
900                "scales and biases must share a float dtype and shape, got {} {:?} and {} {:?}",
901                scales.dtype(),
902                scales.shape(),
903                biases.dtype(),
904                biases.shape()
905            ));
906        }
907        let packed = w.shape().last().copied().unwrap_or(0) * 32;
908        let groups = scales.shape().last().copied().unwrap_or(0) * group_size;
909        if n < 2
910            || scales.ndim() != n
911            || w.shape()[..n - 1] != scales.shape()[..n - 1]
912            || packed != groups * usize::from(bits)
913        {
914            return invalid(format!(
915                "packed weights {:?} and scales {:?} do not match group size {group_size} \
916                 and {bits} bits",
917                w.shape(),
918                scales.shape()
919            ));
920        }
921        Ok(groups)
922    }
923
924    /// Validates an indexing operation's array and indices: integer indices with the
925    /// array's shape except along `axis`.
926    fn indexed(&self, array: &Array, indices: &Array, axis: usize) -> Result<()> {
927        if !matches!(
928            indices.dtype().category(),
929            crate::Category::Signed | crate::Category::Unsigned
930        ) {
931            return Err(Error::InvalidArgument(format!(
932                "{}: indices must be integers, got {}",
933                self.name(),
934                indices.dtype()
935            )));
936        }
937        let fits = indices.ndim() == array.ndim()
938            && axis < array.ndim()
939            && indices
940                .shape()
941                .iter()
942                .zip(array.shape())
943                .enumerate()
944                .all(|(d, (i, a))| d == axis || i == a);
945        if !fits {
946            return Err(Error::InvalidArgument(format!(
947                "{}: indices of shape {:?} do not fit an array of shape {:?} along axis {axis}",
948                self.name(),
949                indices.shape(),
950                array.shape()
951            )));
952        }
953        if array.shape()[axis] == 0 && indices.size() > 0 {
954            return Err(Error::InvalidArgument(format!(
955                "{}: cannot index an empty axis",
956                self.name()
957            )));
958        }
959        Ok(())
960    }
961
962    /// `shape` with `axes` set to 1, after validating the axes.
963    fn reduced_shape(&self, shape: &[usize], axes: &[usize]) -> Result<Shape> {
964        let mut out: Shape = shape.into();
965        for &axis in axes {
966            *out.get_mut(axis).ok_or_else(|| {
967                Error::InvalidArgument(format!(
968                    "{}: axis {axis} out of range for shape {shape:?}",
969                    self.name()
970                ))
971            })? = 1;
972        }
973        Ok(out)
974    }
975
976    fn arity<'a, const N: usize>(&self, inputs: &'a [Array]) -> Result<&'a [Array; N]> {
977        inputs.try_into().map_err(|_| {
978            Error::InvalidArgument(format!(
979                "{} takes {N} inputs, got {}",
980                self.name(),
981                inputs.len()
982            ))
983        })
984    }
985
986    fn same_shape_and_dtype(&self, a: &Array, b: &Array) -> Result<()> {
987        if a.shape() != b.shape() {
988            return Err(Error::ShapeMismatch {
989                op: self.name(),
990                lhs: a.shape().to_vec(),
991                rhs: b.shape().to_vec(),
992            });
993        }
994        if a.dtype() != b.dtype() {
995            return Err(Error::DTypeMismatch {
996                op: self.name(),
997                lhs: a.dtype(),
998                rhs: b.dtype(),
999            });
1000        }
1001        Ok(())
1002    }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007    use std::collections::hash_map::DefaultHasher;
1008    use std::hash::{Hash, Hasher};
1009
1010    use super::*;
1011
1012    fn hash(p: &Primitive) -> u64 {
1013        let mut h = DefaultHasher::new();
1014        p.hash(&mut h);
1015        h.finish()
1016    }
1017
1018    #[test]
1019    fn equal_primitives_compare_and_hash_equal() {
1020        let add = Primitive::Binary(BinaryOp::Add);
1021        assert_eq!(add, Primitive::Binary(BinaryOp::Add));
1022        assert_eq!(hash(&add), hash(&Primitive::Binary(BinaryOp::Add)));
1023        assert_ne!(add, Primitive::Binary(BinaryOp::Multiply));
1024        let s = |start: Vec<usize>| Primitive::Slice {
1025            start,
1026            step: vec![1],
1027            len: vec![2],
1028        };
1029        assert_eq!(s(vec![1]), s(vec![1]));
1030        assert_ne!(s(vec![1]), s(vec![2]));
1031    }
1032}