1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum UnaryOp {
21 Negative,
23 Abs,
25 Exp,
27 Log,
29 Sqrt,
31 Sin,
33 Cos,
35 Tanh,
37 Sigmoid,
39 Conjugate,
41 ErfInv,
43}
44
45impl UnaryOp {
46 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70#[non_exhaustive]
71pub enum BinaryOp {
72 Add,
74 Subtract,
76 Multiply,
78 Divide,
80 Maximum,
82 Minimum,
84 LogAddExp,
86 FloorDivide,
90 Power,
93 Equal,
95 NotEqual,
97 Less,
99 LessEqual,
101 Greater,
103 GreaterEqual,
105}
106
107impl BinaryOp {
108 pub(crate) fn is_comparison(self) -> bool {
110 matches!(
111 self,
112 Self::Equal
113 | Self::NotEqual
114 | Self::Less
115 | Self::LessEqual
116 | Self::Greater
117 | Self::GreaterEqual
118 )
119 }
120
121 pub(crate) fn name(self) -> &'static str {
122 match self {
123 Self::Add => "add",
124 Self::Subtract => "subtract",
125 Self::Multiply => "multiply",
126 Self::Divide => "divide",
127 Self::Maximum => "maximum",
128 Self::Minimum => "minimum",
129 Self::LogAddExp => "logaddexp",
130 Self::FloorDivide => "floor_divide",
131 Self::Power => "power",
132 Self::Equal => "equal",
133 Self::NotEqual => "not_equal",
134 Self::Less => "less",
135 Self::LessEqual => "less_equal",
136 Self::Greater => "greater",
137 Self::GreaterEqual => "greater_equal",
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144#[non_exhaustive]
145pub enum ReduceOp {
146 Sum,
148 Prod,
150 Max,
152 Min,
154 And,
156 Or,
158}
159
160impl ReduceOp {
161 pub(crate) fn name(self) -> &'static str {
162 match self {
163 Self::Sum => "sum",
164 Self::Prod => "prod",
165 Self::Max => "max",
166 Self::Min => "min",
167 Self::And => "all",
168 Self::Or => "any",
169 }
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
175#[non_exhaustive]
176pub enum ArgReduceOp {
177 Max,
179 Min,
181}
182
183impl ArgReduceOp {
184 pub(crate) fn name(self) -> &'static str {
185 match self {
186 Self::Max => "argmax",
187 Self::Min => "argmin",
188 }
189 }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
194#[non_exhaustive]
195pub enum ScanOp {
196 Sum,
198 Prod,
200 Max,
202 Min,
204 LogAddExp,
206}
207
208impl ScanOp {
209 pub(crate) fn name(self) -> &'static str {
210 match self {
211 Self::Sum => "cumsum",
212 Self::Prod => "cumprod",
213 Self::Max => "cummax",
214 Self::Min => "cummin",
215 Self::LogAddExp => "logcumsumexp",
216 }
217 }
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
222#[non_exhaustive]
223pub enum ScatterReduce {
224 None,
226 Sum,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Hash)]
232#[non_exhaustive]
233pub enum Primitive {
234 Unary(UnaryOp),
236 Binary(BinaryOp),
238 Select,
240 AsType(DType),
242 Reshape(Vec<usize>),
244 Squeeze(Vec<usize>),
248 ExpandDims(Vec<usize>),
251 Transpose(Vec<usize>),
253 Slice {
255 start: Vec<usize>,
257 step: Vec<isize>,
259 len: Vec<usize>,
261 },
262 Broadcast(Vec<usize>),
264 StopGradient,
266 View(DType),
270 ToFp8,
273 FromFp8(DType),
275 SortedOffsets {
279 groups: usize,
281 },
282 SliceUpdate {
285 start: Vec<usize>,
287 step: Vec<isize>,
289 len: Vec<usize>,
291 },
292 Reduce {
294 op: ReduceOp,
296 axes: Vec<usize>,
298 },
299 ArgReduce {
301 op: ArgReduceOp,
303 axis: usize,
305 },
306 GatherAxis {
311 axis: usize,
313 },
314 ScatterAxis {
318 axis: usize,
320 reduce: ScatterReduce,
322 },
323 Sort {
325 axis: usize,
327 },
328 ArgSort {
330 axis: usize,
332 },
333 Scan {
335 op: ScanOp,
337 axis: usize,
339 reverse: bool,
341 inclusive: bool,
344 },
345 Concatenate {
347 axis: usize,
349 },
350 Matmul,
353 AddMM {
356 alpha: u32,
358 beta: u32,
360 },
361 Quantize {
365 quant: crate::Quant,
367 },
368 Dequantize {
372 quant: crate::Quant,
374 dtype: DType,
376 },
377 QuantizedMatmul {
380 quant: crate::Quant,
382 transpose: bool,
384 },
385 Fast(crate::fast::imp::FastOp),
387 Compiled(crate::fused::Fused),
390 RandomBits {
393 shape: Vec<usize>,
395 width: u8,
397 },
398 Custom(CustomCall),
402 MetalKernel(crate::kernel::KernelCall),
404 Copy,
407 Depends {
412 n_deps: usize,
414 },
415 Collective {
417 op: crate::distributed::CollectiveOp,
419 group: usize,
421 },
422 #[cfg(test)]
424 AddMultiply,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq)]
429pub(crate) struct OutputSpec {
430 pub(crate) dtype: DType,
431 pub(crate) shape: Shape,
432 pub(crate) strides: Option<Strides>,
434}
435
436impl Primitive {
437 pub(crate) fn addmm_factors(&self) -> (f32, f32) {
439 match self {
440 Self::AddMM { alpha, beta } => (f32::from_bits(*alpha), f32::from_bits(*beta)),
441 _ => (1.0, 0.0),
442 }
443 }
444
445 pub fn name(&self) -> &'static str {
447 match self {
448 Self::Unary(op) => op.name(),
449 Self::Binary(op) => op.name(),
450 Self::Select => "where",
451 Self::AsType(_) => "astype",
452 Self::Reshape(_) => "reshape",
453 Self::Squeeze(_) => "squeeze",
454 Self::ExpandDims(_) => "expand_dims",
455 Self::Transpose(_) => "transpose",
456 Self::Slice { .. } => "slice",
457 Self::Broadcast(_) => "broadcast_to",
458 Self::StopGradient => "stop_gradient",
459 Self::View(_) => "view",
460 Self::ToFp8 => "to_fp8",
461 Self::SortedOffsets { .. } => "sorted_offsets",
462 Self::FromFp8(_) => "from_fp8",
463 Self::SliceUpdate { .. } => "slice_update",
464 Self::Reduce { op, .. } => op.name(),
465 Self::ArgReduce { op, .. } => op.name(),
466 Self::GatherAxis { .. } => "take_along_axis",
467 Self::ScatterAxis {
468 reduce: ScatterReduce::None,
469 ..
470 } => "put_along_axis",
471 Self::ScatterAxis { .. } => "scatter_add_along_axis",
472 Self::Concatenate { .. } => "concatenate",
473 Self::Sort { .. } => "sort",
474 Self::ArgSort { .. } => "argsort",
475 Self::Scan { op, .. } => op.name(),
476 Self::Matmul => "matmul",
477 Self::AddMM { .. } => "addmm",
478 Self::RandomBits { .. } => "random_bits",
479 Self::Fast(op) => op.name(),
480 Self::Compiled(_) => "compiled",
481 Self::Quantize { .. } => "quantize",
482 Self::Dequantize { .. } => "dequantize",
483 Self::QuantizedMatmul { .. } => "quantized_matmul",
484 Self::Custom(_) => "custom_function",
485 Self::MetalKernel(_) => "metal_kernel",
486 Self::Copy => "copy",
487 Self::Depends { .. } => "depends",
488 Self::Collective { op, .. } => op.name(),
489 #[cfg(test)]
490 Self::AddMultiply => "add_multiply",
491 }
492 }
493
494 pub fn fixes_shape(&self) -> bool {
500 use crate::fast::imp::FastOp;
501 match self {
502 Self::Reshape(_)
503 | Self::Slice { .. }
504 | Self::SliceUpdate { .. }
505 | Self::Broadcast(_)
506 | Self::RandomBits { .. }
507 | Self::MetalKernel(_)
508 | Self::Fast(FastOp::Attention { .. } | FastOp::CausalSoftmax { .. })
509 | Self::Collective {
510 op: crate::distributed::CollectiveOp::Recv { .. },
511 ..
512 } => true,
513 Self::Unary(_)
514 | Self::Binary(_)
515 | Self::Select
516 | Self::AsType(_)
517 | Self::Transpose(_)
518 | Self::Squeeze(_)
519 | Self::ExpandDims(_)
520 | Self::StopGradient
521 | Self::View(_)
522 | Self::ToFp8
523 | Self::FromFp8(_)
524 | Self::SortedOffsets { .. }
525 | Self::Reduce { .. }
526 | Self::ArgReduce { .. }
527 | Self::GatherAxis { .. }
528 | Self::ScatterAxis { .. }
529 | Self::Sort { .. }
530 | Self::ArgSort { .. }
531 | Self::Scan { .. }
532 | Self::Concatenate { .. }
533 | Self::Matmul
534 | Self::AddMM { .. }
535 | Self::Quantize { .. }
536 | Self::Dequantize { .. }
537 | Self::QuantizedMatmul { .. }
538 | Self::Fast(_)
539 | Self::Compiled(_)
540 | Self::Custom(_)
541 | Self::Copy
542 | Self::Collective { .. }
543 | Self::Depends { .. } => false,
544 #[cfg(test)]
545 Self::AddMultiply => false,
546 }
547 }
548
549 pub(crate) fn unit_axes_shape(&self, dims: &[usize]) -> Result<Vec<usize>> {
551 let sorted = |axes: &[usize], n: usize| {
552 axes.windows(2).all(|w| w[0] < w[1]) && axes.last().is_none_or(|&a| a < n)
553 };
554 match self {
555 Self::Squeeze(axes) => {
556 if !sorted(axes, dims.len()) || axes.iter().any(|&a| dims[a] != 1) {
557 return Err(Error::InvalidArgument(format!(
558 "squeeze: cannot remove axes {axes:?} of shape {dims:?}"
559 )));
560 }
561 Ok(dims
562 .iter()
563 .enumerate()
564 .filter(|(i, _)| !axes.contains(i))
565 .map(|(_, &d)| d)
566 .collect())
567 }
568 Self::ExpandDims(axes) => {
569 let n = dims.len() + axes.len();
570 if !sorted(axes, n) {
571 return Err(Error::InvalidArgument(format!(
572 "expand_dims: invalid axes {axes:?} for {n} dimensions"
573 )));
574 }
575 let mut rest = dims.iter();
576 Ok((0..n)
577 .map(|i| {
578 if axes.contains(&i) {
579 1
580 } else {
581 *rest.next().expect("n")
582 }
583 })
584 .collect())
585 }
586 _ => unreachable!("not a unit-axes view"),
587 }
588 }
589
590 pub(crate) fn output_specs(&self, inputs: &[Array]) -> Result<SmallVec<[OutputSpec; 1]>> {
592 let computed = |dtype: DType, shape: &[usize]| OutputSpec {
593 dtype,
594 shape: shape.into(),
595 strides: None,
596 };
597 let spec = match self {
598 Self::Unary(op) => {
599 let [a] = self.arity::<1>(inputs)?;
600 let supported = match op {
602 UnaryOp::Negative => a.dtype() != DType::Bool,
603 UnaryOp::Abs | UnaryOp::Conjugate => true,
604 _ => a.dtype().category() == Category::Float,
605 };
606 if !supported {
607 return Err(Error::unsupported_dtype(op.name(), a.dtype()));
608 }
609 let dtype = match (op, a.dtype()) {
610 (UnaryOp::Abs, DType::Complex64) => DType::F32,
611 (_, d) => d,
612 };
613 computed(dtype, a.dims())
614 }
615 Self::Binary(op) => {
616 let [a, b] = self.arity::<2>(inputs)?;
617 self.same_shape_and_dtype(a, b)?;
618 let category = a.dtype().category();
619 let unsupported = match op {
620 BinaryOp::LogAddExp => category != Category::Float,
621 BinaryOp::FloorDivide | BinaryOp::Power => {
622 matches!(category, Category::Bool | Category::Complex)
623 }
624 BinaryOp::Divide => !matches!(category, Category::Float | Category::Complex),
625 BinaryOp::Maximum
626 | BinaryOp::Minimum
627 | BinaryOp::Less
628 | BinaryOp::LessEqual
629 | BinaryOp::Greater
630 | BinaryOp::GreaterEqual => category == Category::Complex,
631 _ => false,
632 };
633 if unsupported {
634 return Err(Error::unsupported_dtype(op.name(), a.dtype()));
635 }
636 computed(
637 if op.is_comparison() {
638 DType::Bool
639 } else {
640 a.dtype()
641 },
642 a.dims(),
643 )
644 }
645 Self::Select => {
646 let [c, a, b] = self.arity::<3>(inputs)?;
647 self.same_shape_and_dtype(a, b)?;
648 if c.dtype() != DType::Bool || c.dims() != a.dims() {
649 return Err(Error::InvalidArgument(
650 "where: condition must be a bool array of the same shape".into(),
651 ));
652 }
653 computed(a.dtype(), a.dims())
654 }
655 Self::AsType(dtype) => {
656 let [a] = self.arity::<1>(inputs)?;
657 computed(*dtype, a.dims())
658 }
659 Self::Collective { op, group } => {
660 use crate::distributed::CollectiveOp;
661 match op {
662 CollectiveOp::Recv { shape, dtype, .. } => {
663 self.arity::<0>(inputs)?;
664 computed(*dtype, shape)
665 }
666 CollectiveOp::Gather => {
667 let [a] = self.arity::<1>(inputs)?;
668 let n = crate::distributed::group_size(*group)?;
669 let mut shape = a.dims().to_vec();
670 match shape.first_mut() {
671 Some(d) => *d *= n,
672 None => shape.push(n),
673 }
674 computed(a.dtype(), &shape)
675 }
676 CollectiveOp::SumScatter => {
677 let [a] = self.arity::<1>(inputs)?;
678 let n = crate::distributed::group_size(*group)?;
679 let mut shape = a.dims().to_vec();
680 shape[0] /= n;
681 computed(a.dtype(), &shape)
682 }
683 CollectiveOp::Reduce(_) | CollectiveOp::Send { .. } => {
684 let [a] = self.arity::<1>(inputs)?;
685 computed(a.dtype(), a.dims())
686 }
687 }
688 }
689 Self::Squeeze(_) | Self::ExpandDims(_) => {
690 let [a] = self.arity::<1>(inputs)?;
691 let shape = self.unit_axes_shape(a.dims())?;
692 OutputSpec {
693 dtype: a.dtype(),
694 strides: view::nocopy_reshape(a.dims(), a.strides(), &shape),
695 shape: shape.as_slice().into(),
696 }
697 }
698 Self::Reshape(shape) => {
699 let [a] = self.arity::<1>(inputs)?;
700 if crate::shape::checked_size(shape)? != a.size() {
701 return Err(Error::InvalidArgument(format!(
702 "reshape: cannot reshape {} elements into shape {shape:?}",
703 a.size()
704 )));
705 }
706 OutputSpec {
707 dtype: a.dtype(),
708 shape: shape.as_slice().into(),
709 strides: view::nocopy_reshape(a.dims(), a.strides(), shape),
710 }
711 }
712 Self::Transpose(axes) => {
713 let [a] = self.arity::<1>(inputs)?;
714 let (shape, strides) = view::permute(a.dims(), a.strides(), axes)?;
715 OutputSpec {
716 dtype: a.dtype(),
717 shape,
718 strides: Some(strides),
719 }
720 }
721 Self::Slice { len, .. } => {
722 let [a] = self.arity::<1>(inputs)?;
723 let dims = self.dim_slices(a.ndim())?;
724 if !region_fits(&dims, a.dims()) {
725 return Err(Error::InvalidArgument(format!(
726 "slice: region {dims:?} does not fit shape {:?}",
727 a.dims()
728 )));
729 }
730 let (strides, _) = view::slice_geometry(a.strides(), &dims);
731 OutputSpec {
732 dtype: a.dtype(),
733 shape: len.as_slice().into(),
734 strides: Some(strides),
735 }
736 }
737 Self::Broadcast(shape) => {
738 let [a] = self.arity::<1>(inputs)?;
739 let strides = view::broadcast_strides(a.dims(), a.strides(), shape)?;
740 OutputSpec {
741 dtype: a.dtype(),
742 shape: shape.as_slice().into(),
743 strides: Some(strides),
744 }
745 }
746 Self::Copy => {
747 let [a] = self.arity::<1>(inputs)?;
748 computed(a.dtype(), a.dims())
749 }
750 Self::Depends { n_deps } => {
751 if inputs.len() != n_deps + 1 {
752 return Err(Error::InvalidArgument(format!(
753 "depends: {} dependencies expected, got {}",
754 n_deps,
755 inputs.len().saturating_sub(1)
756 )));
757 }
758 let a = &inputs[0];
759 OutputSpec {
760 dtype: a.dtype(),
761 shape: a.dims().into(),
762 strides: Some(a.strides().into()),
763 }
764 }
765 Self::StopGradient => {
766 let [a] = self.arity::<1>(inputs)?;
767 OutputSpec {
768 dtype: a.dtype(),
769 shape: a.dims().into(),
770 strides: Some(a.strides().into()),
771 }
772 }
773 Self::SortedOffsets { groups } => {
774 let [a] = self.arity::<1>(inputs)?;
775 if a.dtype() != DType::U32 || a.ndim() == 0 {
776 return Err(Error::InvalidArgument(format!(
777 "sorted_offsets: indices must be uint32 with at least one axis, got {} {:?}",
778 a.dtype(),
779 a.dims()
780 )));
781 }
782 let mut shape = a.dims()[..a.ndim() - 1].to_vec();
783 shape.push(*groups);
784 computed(DType::I32, &shape)
785 }
786 Self::ToFp8 => {
787 let [a] = self.arity::<1>(inputs)?;
788 if a.dtype().category() != Category::Float {
789 return Err(Error::unsupported_dtype("to_fp8", a.dtype()));
790 }
791 computed(DType::U8, a.dims())
792 }
793 Self::FromFp8(dtype) => {
794 let [a] = self.arity::<1>(inputs)?;
795 if a.dtype() != DType::U8 {
796 return Err(Error::InvalidArgument(format!(
797 "from_fp8: E4M3 bytes must be uint8, got {}",
798 a.dtype()
799 )));
800 }
801 if dtype.category() != Category::Float {
802 return Err(Error::unsupported_dtype("from_fp8", *dtype));
803 }
804 computed(*dtype, a.dims())
805 }
806 Self::View(dtype) => {
807 let [a] = self.arity::<1>(inputs)?;
808 let (from, to) = (a.dtype().size(), dtype.size());
809 if from == to {
810 OutputSpec {
811 dtype: *dtype,
812 shape: a.dims().into(),
813 strides: Some(a.strides().into()),
814 }
815 } else {
816 let n = a.ndim();
817 let last = a.dims().last().copied();
818 let scaled = match last {
819 Some(d) if (d * from) % to == 0 => d * from / to,
820 _ => {
821 return Err(Error::InvalidArgument(match last {
822 None => format!(
823 "view: a scalar of {} cannot be viewed as {dtype} (another size)",
824 a.dtype()
825 ),
826 Some(d) => format!(
827 "view: the last dimension ({d}) times {from} bytes does not divide into {dtype} values of {to} bytes"
828 ),
829 }));
830 }
831 };
832 let mut shape: Shape = a.dims().into();
833 shape[n - 1] = scaled;
834 let row = crate::shape::flags_for(a.dims(), a.strides()).row_contiguous;
837 OutputSpec {
838 dtype: *dtype,
839 strides: row.then(|| crate::shape::row_contiguous_strides(&shape)),
840 shape,
841 }
842 }
843 }
844 Self::SliceUpdate { len, .. } => {
845 let [base, update] = self.arity::<2>(inputs)?;
846 let dims = self.dim_slices(base.ndim())?;
847 if !region_fits(&dims, base.dims()) || update.dims() != len.as_slice() {
848 return Err(Error::InvalidArgument(format!(
849 "slice_update: region {dims:?} does not fit shape {:?} with update {:?}",
850 base.dims(),
851 update.dims()
852 )));
853 }
854 if base.dtype() != update.dtype() {
855 return Err(Error::dtype_mismatch(
856 "slice_update",
857 base.dtype(),
858 update.dtype(),
859 ));
860 }
861 computed(base.dtype(), base.dims())
862 }
863 Self::Reduce { op, axes } => {
864 let [a] = self.arity::<1>(inputs)?;
865 let shape = self.reduced_shape(a.dims(), axes)?;
866 if !axes.windows(2).all(|w| w[0] < w[1]) {
867 return Err(Error::InvalidArgument(format!(
868 "{}: axes {axes:?} must be sorted and unique",
869 self.name()
870 )));
871 }
872 let supported = match op {
873 ReduceOp::And | ReduceOp::Or => a.dtype() == DType::Bool,
874 ReduceOp::Max | ReduceOp::Min => a.dtype() != DType::Complex64,
875 ReduceOp::Sum | ReduceOp::Prod => true,
876 };
877 if !supported {
878 return Err(Error::unsupported_dtype(self.name(), a.dtype()));
879 }
880 let ordered = matches!(op, ReduceOp::Max | ReduceOp::Min);
881 if ordered && axes.iter().any(|&d| a.dims()[d] == 0) {
882 return Err(Error::InvalidArgument(format!(
883 "{}: cannot reduce a zero-length axis (no identity)",
884 self.name()
885 )));
886 }
887 computed(a.dtype(), &shape)
888 }
889 Self::ArgReduce { axis, .. } => {
890 let [a] = self.arity::<1>(inputs)?;
891 if a.dtype() == DType::Complex64 {
892 return Err(Error::unsupported_dtype(self.name(), a.dtype()));
893 }
894 computed(DType::U32, &self.reduced_shape(a.dims(), &[*axis])?)
895 }
896 Self::GatherAxis { axis } => {
897 let [src, indices] = self.arity::<2>(inputs)?;
898 self.indexed(src, indices, *axis)?;
899 computed(src.dtype(), indices.dims())
900 }
901 Self::ScatterAxis { axis, .. } => {
902 let [dst, indices, updates] = self.arity::<3>(inputs)?;
903 self.indexed(dst, indices, *axis)?;
904 if updates.dims() != indices.dims() {
905 return Err(Error::shape_mismatch(
906 self.name(),
907 indices.dims(),
908 updates.dims(),
909 ));
910 }
911 if updates.dtype() != dst.dtype() {
912 return Err(Error::dtype_mismatch(
913 self.name(),
914 dst.dtype(),
915 updates.dtype(),
916 ));
917 }
918 computed(dst.dtype(), dst.dims())
919 }
920 Self::Sort { axis } | Self::ArgSort { axis } | Self::Scan { axis, .. } => {
921 let [a] = self.arity::<1>(inputs)?;
922 if *axis >= a.ndim() {
923 return Err(Error::InvalidArgument(format!(
924 "{}: axis {axis} out of range for {} dimensions",
925 self.name(),
926 a.ndim()
927 )));
928 }
929 let ordered = matches!(
930 self,
931 Self::Sort { .. }
932 | Self::ArgSort { .. }
933 | Self::Scan {
934 op: ScanOp::Max | ScanOp::Min,
935 ..
936 }
937 );
938 let float_only = matches!(
939 self,
940 Self::Scan {
941 op: ScanOp::LogAddExp,
942 ..
943 }
944 );
945 let category = a.dtype().category();
946 if (ordered && category == crate::Category::Complex)
947 || (float_only && category != crate::Category::Float)
948 {
949 return Err(Error::unsupported_dtype(self.name(), a.dtype()));
950 }
951 let dtype = match self {
952 Self::ArgSort { .. } => DType::U32,
953 _ => a.dtype(),
954 };
955 computed(dtype, a.dims())
956 }
957 Self::Concatenate { axis } => {
958 let first = inputs.first().ok_or_else(|| {
959 Error::InvalidArgument("concatenate: no arrays to join".into())
960 })?;
961 if *axis >= first.ndim() {
962 return Err(Error::InvalidArgument(format!(
963 "concatenate: axis {axis} out of range for {} dimensions",
964 first.ndim()
965 )));
966 }
967 let mut shape: Shape = first.dims().into();
968 for a in &inputs[1..] {
969 let fits = a.ndim() == first.ndim()
970 && a.dims()
971 .iter()
972 .zip(first.dims())
973 .enumerate()
974 .all(|(d, (x, y))| d == *axis || x == y);
975 if !fits {
976 return Err(Error::shape_mismatch("concatenate", first.dims(), a.dims()));
977 }
978 if a.dtype() != first.dtype() {
979 return Err(Error::dtype_mismatch(
980 "concatenate",
981 first.dtype(),
982 a.dtype(),
983 ));
984 }
985 shape[*axis] = shape[*axis].checked_add(a.dims()[*axis]).ok_or_else(|| {
986 Error::InvalidArgument("concatenate: the joined size overflows".into())
987 })?;
988 }
989 computed(first.dtype(), &shape)
990 }
991 Self::Matmul | Self::AddMM { .. } => {
992 let (a, b) = match self {
993 Self::Matmul => {
994 let [a, b] = self.arity::<2>(inputs)?;
995 (a, b)
996 }
997 _ => {
998 let [a, b, _] = self.arity::<3>(inputs)?;
999 (a, b)
1000 }
1001 };
1002 let (sa, sb) = (a.dims(), b.dims());
1003 let n = sa.len();
1004 let valid =
1005 n >= 2 && sb.len() == n && sa[..n - 2] == sb[..n - 2] && sa[n - 1] == sb[n - 2];
1006 if !valid {
1007 return Err(Error::shape_mismatch(self.name(), sa, sb));
1008 }
1009 if a.dtype() != b.dtype() {
1010 return Err(Error::dtype_mismatch(self.name(), a.dtype(), b.dtype()));
1011 }
1012 if !matches!(a.dtype().category(), Category::Float | Category::Complex) {
1013 return Err(Error::unsupported_dtype(self.name(), a.dtype()));
1014 }
1015 let mut shape: Shape = sa.into();
1016 shape[n - 1] = sb[n - 1];
1017 if let Self::AddMM { .. } = self {
1018 let c = &inputs[2];
1019 if c.dims() != shape.as_slice() || c.dtype() != a.dtype() {
1020 return Err(Error::InvalidArgument(format!(
1021 "addmm: c must be {} of shape {shape:?}, got {} {:?}",
1022 a.dtype(),
1023 c.dtype(),
1024 c.dims()
1025 )));
1026 }
1027 }
1028 computed(a.dtype(), &shape)
1029 }
1030 Self::RandomBits { shape, width } => {
1031 let [keys] = self.arity::<1>(inputs)?;
1032 if keys.dtype() != DType::U32 || keys.dims().last() != Some(&2) {
1033 return Err(Error::InvalidArgument(format!(
1034 "random_bits: keys must be uint32 of shape [..., 2], got {} {:?}",
1035 keys.dtype(),
1036 keys.dims()
1037 )));
1038 }
1039 let dtype = match width {
1040 1 => DType::U8,
1041 2 => DType::U16,
1042 4 => DType::U32,
1043 _ => {
1044 return Err(Error::InvalidArgument(format!(
1045 "random_bits: width must be 1, 2 or 4, got {width}"
1046 )));
1047 }
1048 };
1049 let out: Vec<usize> = keys.dims()[..keys.ndim() - 1]
1050 .iter()
1051 .chain(shape)
1052 .copied()
1053 .collect();
1054 computed(dtype, &out)
1055 }
1056 Self::Fast(op) => {
1057 let strides = op.output_strides(inputs);
1058 return Ok(op
1059 .outputs(inputs)?
1060 .into_iter()
1061 .map(|(dtype, shape)| OutputSpec {
1062 dtype,
1063 shape: shape.into(),
1064 strides: strides.clone(),
1065 })
1066 .collect());
1067 }
1068 Self::Compiled(fused) => {
1069 let dtypes: Vec<DType> = inputs.iter().map(Array::dtype).collect();
1070 if dtypes != fused.inputs() {
1071 return Err(Error::InvalidArgument(format!(
1072 "compiled: inputs {dtypes:?}, expected {:?}",
1073 fused.inputs()
1074 )));
1075 }
1076 let shape = inputs[0].dims();
1077 if let Some(other) = inputs.iter().find(|a| a.dims() != shape) {
1078 return Err(Error::shape_mismatch("compiled", shape, other.dims()));
1079 }
1080 return Ok(fused
1081 .dtypes()
1082 .into_iter()
1083 .map(|d| computed(d, shape))
1084 .collect());
1085 }
1086 Self::Quantize { quant } => {
1087 let [w] = self.arity::<1>(inputs)?;
1088 let (group_size, bits) = quant.checked(self.name())?;
1089 let n = w.ndim();
1090 if !matches!(w.dtype(), DType::F16 | DType::BF16 | DType::F32) {
1091 return Err(Error::unsupported_dtype("quantize", w.dtype()));
1092 }
1093 if n < 2 || w.dims()[n - 1] % group_size != 0 {
1094 return Err(Error::InvalidArgument(format!(
1095 "quantize: needs at least 2 dimensions with the last divisible by \
1096 the group size {group_size}, got shape {:?}",
1097 w.dims()
1098 )));
1099 }
1100 let last = w.dims()[n - 1];
1101 let with_last = |d: usize| {
1102 let mut s: Shape = w.dims().into();
1103 s[n - 1] = d;
1104 s
1105 };
1106 let packed = computed(DType::U32, &with_last(last * bits / 32));
1107 if quant.mode != crate::QuantMode::Affine {
1108 return Ok(smallvec![
1109 packed,
1110 computed(DType::U8, &with_last(last / group_size))
1111 ]);
1112 }
1113 let groups = computed(w.dtype(), &with_last(last / group_size));
1114 return Ok(smallvec![packed, groups.clone(), groups]);
1115 }
1116 Self::Dequantize { quant, dtype } => {
1117 let affine = quant.mode == crate::QuantMode::Affine;
1118 let w = self.quantized_arity(inputs, 2 + usize::from(affine))?;
1119 let mut shape: Shape = w.dims().into();
1120 let n = shape.len();
1121 shape[n - 1] = self.quantized_columns(w, &inputs[1..], *quant)?;
1122 if (affine && *dtype != inputs[1].dtype())
1123 || !matches!(dtype, DType::F16 | DType::BF16 | DType::F32)
1124 {
1125 return Err(Error::unsupported_dtype("dequantize", *dtype));
1126 }
1127 computed(*dtype, &shape)
1128 }
1129 Self::QuantizedMatmul { quant, transpose } => {
1130 let affine = quant.mode == crate::QuantMode::Affine;
1131 let x = self.quantized_arity(inputs, 3 + usize::from(affine))?;
1132 let (w, scales) = (&inputs[1], &inputs[2]);
1133 let columns = self.quantized_columns(w, &inputs[2..], *quant)?;
1134 let rows = w.dims()[w.ndim() - 2];
1135 let (inner, outer) = if *transpose {
1136 (columns, rows)
1137 } else {
1138 (rows, columns)
1139 };
1140 let (xn, wn) = (x.ndim(), w.ndim());
1141 let batch_ok = wn == 2 || (xn == wn && x.dims()[..xn - 2] == w.dims()[..wn - 2]);
1142 if xn == 0 || x.dims()[xn - 1] != inner || !batch_ok {
1143 return Err(Error::shape_mismatch(
1144 "quantized_matmul",
1145 x.dims(),
1146 w.dims(),
1147 ));
1148 }
1149 if affine && x.dtype() != scales.dtype() {
1150 return Err(Error::dtype_mismatch(
1151 "quantized_matmul",
1152 x.dtype(),
1153 scales.dtype(),
1154 ));
1155 }
1156 if !matches!(x.dtype(), DType::F16 | DType::BF16 | DType::F32) {
1157 return Err(Error::unsupported_dtype("quantized_matmul", x.dtype()));
1158 }
1159 let mut shape: Shape = x.dims().into();
1160 shape[xn - 1] = outer;
1161 computed(x.dtype(), &shape)
1162 }
1163 Self::MetalKernel(call) => {
1164 return Ok(call
1165 .outputs
1166 .iter()
1167 .map(|(dtype, shape)| computed(*dtype, shape))
1168 .collect());
1169 }
1170 Self::Custom(call) => {
1171 let outputs = inputs.get(call.primals..).unwrap_or_default();
1172 if outputs.is_empty() {
1173 return Err(Error::InvalidArgument(
1174 "custom_function: the function must return at least one array".into(),
1175 ));
1176 }
1177 return Ok(outputs
1178 .iter()
1179 .map(|a| OutputSpec {
1180 dtype: a.dtype(),
1181 shape: a.dims().into(),
1182 strides: Some(a.strides().into()),
1183 })
1184 .collect());
1185 }
1186 #[cfg(test)]
1187 Self::AddMultiply => {
1188 let [a, b] = self.arity::<2>(inputs)?;
1189 self.same_shape_and_dtype(a, b)?;
1190 let spec = computed(a.dtype(), a.dims());
1191 return Ok(smallvec![spec.clone(), spec]);
1192 }
1193 };
1194 Ok(smallvec![spec])
1195 }
1196
1197 pub(crate) fn dim_slices(&self, ndim: usize) -> Result<Vec<DimSlice>> {
1200 match self {
1201 Self::Slice { start, step, len } | Self::SliceUpdate { start, step, len }
1202 if start.len() == ndim && step.len() == ndim && len.len() == ndim =>
1203 {
1204 Ok((0..ndim)
1205 .map(|i| DimSlice {
1206 start: start[i],
1207 step: step[i],
1208 len: len[i],
1209 })
1210 .collect())
1211 }
1212 _ => Err(Error::InvalidArgument(
1213 "slice: one start, step and length per dimension".into(),
1214 )),
1215 }
1216 }
1217
1218 fn quantized_arity<'a>(&self, inputs: &'a [Array], arity: usize) -> Result<&'a Array> {
1221 if inputs.len() != arity {
1222 return Err(Error::InvalidArgument(format!(
1223 "{}: {arity} inputs expected, got {}",
1224 self.name(),
1225 inputs.len()
1226 )));
1227 }
1228 Ok(&inputs[0])
1229 }
1230
1231 pub(crate) fn quantized_columns(
1236 &self,
1237 w: &Array,
1238 groups: &[Array],
1239 quant: crate::Quant,
1240 ) -> Result<usize> {
1241 let (group_size, bits) = quant.checked(self.name())?;
1242 let scales = &groups[0];
1243 let n = w.ndim();
1244 let invalid =
1245 |what: String| Err(Error::InvalidArgument(format!("{}: {what}", self.name())));
1246 if w.dtype() != DType::U32 {
1247 return invalid(format!("packed weights must be uint32, got {}", w.dtype()));
1248 }
1249 if quant.mode == crate::QuantMode::Affine {
1250 let biases = &groups[1];
1251 if !matches!(scales.dtype(), DType::F16 | DType::BF16 | DType::F32)
1252 || biases.dtype() != scales.dtype()
1253 || biases.dims() != scales.dims()
1254 {
1255 return invalid(format!(
1256 "scales and biases must share a float dtype and shape, got {} {:?} and {} {:?}",
1257 scales.dtype(),
1258 scales.dims(),
1259 biases.dtype(),
1260 biases.dims()
1261 ));
1262 }
1263 } else if scales.dtype() != DType::U8 {
1264 return invalid(format!(
1265 "scale type must be uint8 but received type {}",
1266 scales.dtype()
1267 ));
1268 }
1269 let packed = w.dims().last().copied().unwrap_or(0) * 32;
1270 let groups = scales.dims().last().copied().unwrap_or(0) * group_size;
1271 if n < 2
1272 || scales.ndim() != n
1273 || w.dims()[..n - 1] != scales.dims()[..n - 1]
1274 || packed != groups * bits
1275 {
1276 return invalid(format!(
1277 "packed weights {:?} and scales {:?} do not match group size {group_size} \
1278 and {bits} bits",
1279 w.dims(),
1280 scales.dims()
1281 ));
1282 }
1283 Ok(groups)
1284 }
1285
1286 fn indexed(&self, array: &Array, indices: &Array, axis: usize) -> Result<()> {
1289 if !matches!(
1290 indices.dtype().category(),
1291 crate::Category::Signed | crate::Category::Unsigned
1292 ) {
1293 return Err(Error::InvalidArgument(format!(
1294 "{}: indices must be integers, got {}",
1295 self.name(),
1296 indices.dtype()
1297 )));
1298 }
1299 let fits = indices.ndim() == array.ndim()
1300 && axis < array.ndim()
1301 && indices
1302 .dims()
1303 .iter()
1304 .zip(array.dims())
1305 .enumerate()
1306 .all(|(d, (i, a))| d == axis || i == a);
1307 if !fits {
1308 return Err(Error::InvalidArgument(format!(
1309 "{}: indices of shape {:?} do not fit an array of shape {:?} along axis {axis}",
1310 self.name(),
1311 indices.dims(),
1312 array.dims()
1313 )));
1314 }
1315 if array.dims()[axis] == 0 && indices.size() > 0 {
1316 return Err(Error::InvalidArgument(format!(
1317 "{}: cannot index an empty axis",
1318 self.name()
1319 )));
1320 }
1321 Ok(())
1322 }
1323
1324 fn reduced_shape(&self, shape: &[usize], axes: &[usize]) -> Result<Shape> {
1326 let mut out: Shape = shape.into();
1327 for &axis in axes {
1328 *out.get_mut(axis).ok_or_else(|| {
1329 Error::InvalidArgument(format!(
1330 "{}: axis {axis} out of range for shape {shape:?}",
1331 self.name()
1332 ))
1333 })? = 1;
1334 }
1335 Ok(out)
1336 }
1337
1338 fn arity<'a, const N: usize>(&self, inputs: &'a [Array]) -> Result<&'a [Array; N]> {
1339 inputs.try_into().map_err(|_| {
1340 Error::InvalidArgument(format!(
1341 "{} takes {N} inputs, got {}",
1342 self.name(),
1343 inputs.len()
1344 ))
1345 })
1346 }
1347
1348 fn same_shape_and_dtype(&self, a: &Array, b: &Array) -> Result<()> {
1349 if a.dims() != b.dims() {
1350 return Err(Error::shape_mismatch(self.name(), a.dims(), b.dims()));
1351 }
1352 if a.dtype() != b.dtype() {
1353 return Err(Error::dtype_mismatch(self.name(), a.dtype(), b.dtype()));
1354 }
1355 Ok(())
1356 }
1357}
1358
1359pub(crate) fn copy(x: &Array) -> Result<Array> {
1361 Ok(Array::from_op(Primitive::Copy, vec![x.clone()])?.swap_remove(0))
1362}
1363
1364pub(crate) fn depends(x: &Array, deps: &[Array]) -> Result<Array> {
1366 let mut inputs = Vec::with_capacity(deps.len() + 1);
1367 inputs.push(x.clone());
1368 inputs.extend_from_slice(deps);
1369 let primitive = Primitive::Depends { n_deps: deps.len() };
1370 Ok(Array::from_op(primitive, inputs)?.swap_remove(0))
1371}
1372
1373pub(crate) fn region_fits(dims: &[DimSlice], shape: &[usize]) -> bool {
1377 dims.len() == shape.len()
1378 && dims.iter().zip(shape).all(|(d, &n)| {
1379 if d.step == 0 {
1380 return false;
1381 }
1382 if d.len == 0 {
1383 return d.start <= n;
1384 }
1385 let last = isize::try_from(d.len - 1)
1386 .ok()
1387 .and_then(|steps| steps.checked_mul(d.step))
1388 .and_then(|span| isize::try_from(d.start).ok()?.checked_add(span));
1389 d.start < n && last.is_some_and(|last| (0..n as isize).contains(&last))
1390 })
1391}
1392
1393#[cfg(test)]
1394mod tests {
1395 use std::collections::hash_map::DefaultHasher;
1396 use std::hash::{Hash, Hasher};
1397
1398 use super::*;
1399
1400 fn hash(p: &Primitive) -> u64 {
1401 let mut h = DefaultHasher::new();
1402 p.hash(&mut h);
1403 h.finish()
1404 }
1405
1406 #[test]
1407 fn equal_primitives_compare_and_hash_equal() {
1408 let add = Primitive::Binary(BinaryOp::Add);
1409 assert_eq!(add, Primitive::Binary(BinaryOp::Add));
1410 assert_eq!(hash(&add), hash(&Primitive::Binary(BinaryOp::Add)));
1411 assert_ne!(add, Primitive::Binary(BinaryOp::Multiply));
1412 let s = |start: Vec<usize>| Primitive::Slice {
1413 start,
1414 step: vec![1],
1415 len: vec![2],
1416 };
1417 assert_eq!(s(vec![1]), s(vec![1]));
1418 assert_ne!(s(vec![1]), s(vec![2]));
1419 }
1420}