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::Reduce(_) | CollectiveOp::Send { .. } => {
677 let [a] = self.arity::<1>(inputs)?;
678 computed(a.dtype(), a.dims())
679 }
680 }
681 }
682 Self::Squeeze(_) | Self::ExpandDims(_) => {
683 let [a] = self.arity::<1>(inputs)?;
684 let shape = self.unit_axes_shape(a.dims())?;
685 OutputSpec {
686 dtype: a.dtype(),
687 strides: view::nocopy_reshape(a.dims(), a.strides(), &shape),
688 shape: shape.as_slice().into(),
689 }
690 }
691 Self::Reshape(shape) => {
692 let [a] = self.arity::<1>(inputs)?;
693 if crate::shape::checked_size(shape)? != a.size() {
694 return Err(Error::InvalidArgument(format!(
695 "reshape: cannot reshape {} elements into shape {shape:?}",
696 a.size()
697 )));
698 }
699 OutputSpec {
700 dtype: a.dtype(),
701 shape: shape.as_slice().into(),
702 strides: view::nocopy_reshape(a.dims(), a.strides(), shape),
703 }
704 }
705 Self::Transpose(axes) => {
706 let [a] = self.arity::<1>(inputs)?;
707 let (shape, strides) = view::permute(a.dims(), a.strides(), axes)?;
708 OutputSpec {
709 dtype: a.dtype(),
710 shape,
711 strides: Some(strides),
712 }
713 }
714 Self::Slice { len, .. } => {
715 let [a] = self.arity::<1>(inputs)?;
716 let dims = self.dim_slices(a.ndim())?;
717 if !region_fits(&dims, a.dims()) {
718 return Err(Error::InvalidArgument(format!(
719 "slice: region {dims:?} does not fit shape {:?}",
720 a.dims()
721 )));
722 }
723 let (strides, _) = view::slice_geometry(a.strides(), &dims);
724 OutputSpec {
725 dtype: a.dtype(),
726 shape: len.as_slice().into(),
727 strides: Some(strides),
728 }
729 }
730 Self::Broadcast(shape) => {
731 let [a] = self.arity::<1>(inputs)?;
732 let strides = view::broadcast_strides(a.dims(), a.strides(), shape)?;
733 OutputSpec {
734 dtype: a.dtype(),
735 shape: shape.as_slice().into(),
736 strides: Some(strides),
737 }
738 }
739 Self::Copy => {
740 let [a] = self.arity::<1>(inputs)?;
741 computed(a.dtype(), a.dims())
742 }
743 Self::Depends { n_deps } => {
744 if inputs.len() != n_deps + 1 {
745 return Err(Error::InvalidArgument(format!(
746 "depends: {} dependencies expected, got {}",
747 n_deps,
748 inputs.len().saturating_sub(1)
749 )));
750 }
751 let a = &inputs[0];
752 OutputSpec {
753 dtype: a.dtype(),
754 shape: a.dims().into(),
755 strides: Some(a.strides().into()),
756 }
757 }
758 Self::StopGradient => {
759 let [a] = self.arity::<1>(inputs)?;
760 OutputSpec {
761 dtype: a.dtype(),
762 shape: a.dims().into(),
763 strides: Some(a.strides().into()),
764 }
765 }
766 Self::SortedOffsets { groups } => {
767 let [a] = self.arity::<1>(inputs)?;
768 if a.dtype() != DType::U32 || a.ndim() == 0 {
769 return Err(Error::InvalidArgument(format!(
770 "sorted_offsets: indices must be uint32 with at least one axis, got {} {:?}",
771 a.dtype(),
772 a.dims()
773 )));
774 }
775 let mut shape = a.dims()[..a.ndim() - 1].to_vec();
776 shape.push(*groups);
777 computed(DType::I32, &shape)
778 }
779 Self::ToFp8 => {
780 let [a] = self.arity::<1>(inputs)?;
781 if a.dtype().category() != Category::Float {
782 return Err(Error::unsupported_dtype("to_fp8", a.dtype()));
783 }
784 computed(DType::U8, a.dims())
785 }
786 Self::FromFp8(dtype) => {
787 let [a] = self.arity::<1>(inputs)?;
788 if a.dtype() != DType::U8 {
789 return Err(Error::InvalidArgument(format!(
790 "from_fp8: E4M3 bytes must be uint8, got {}",
791 a.dtype()
792 )));
793 }
794 if dtype.category() != Category::Float {
795 return Err(Error::unsupported_dtype("from_fp8", *dtype));
796 }
797 computed(*dtype, a.dims())
798 }
799 Self::View(dtype) => {
800 let [a] = self.arity::<1>(inputs)?;
801 let (from, to) = (a.dtype().size(), dtype.size());
802 if from == to {
803 OutputSpec {
804 dtype: *dtype,
805 shape: a.dims().into(),
806 strides: Some(a.strides().into()),
807 }
808 } else {
809 let n = a.ndim();
810 let last = a.dims().last().copied();
811 let scaled = match last {
812 Some(d) if (d * from) % to == 0 => d * from / to,
813 _ => {
814 return Err(Error::InvalidArgument(match last {
815 None => format!(
816 "view: a scalar of {} cannot be viewed as {dtype} (another size)",
817 a.dtype()
818 ),
819 Some(d) => format!(
820 "view: the last dimension ({d}) times {from} bytes does not divide into {dtype} values of {to} bytes"
821 ),
822 }));
823 }
824 };
825 let mut shape: Shape = a.dims().into();
826 shape[n - 1] = scaled;
827 let row = crate::shape::flags_for(a.dims(), a.strides()).row_contiguous;
830 OutputSpec {
831 dtype: *dtype,
832 strides: row.then(|| crate::shape::row_contiguous_strides(&shape)),
833 shape,
834 }
835 }
836 }
837 Self::SliceUpdate { len, .. } => {
838 let [base, update] = self.arity::<2>(inputs)?;
839 let dims = self.dim_slices(base.ndim())?;
840 if !region_fits(&dims, base.dims()) || update.dims() != len.as_slice() {
841 return Err(Error::InvalidArgument(format!(
842 "slice_update: region {dims:?} does not fit shape {:?} with update {:?}",
843 base.dims(),
844 update.dims()
845 )));
846 }
847 if base.dtype() != update.dtype() {
848 return Err(Error::dtype_mismatch(
849 "slice_update",
850 base.dtype(),
851 update.dtype(),
852 ));
853 }
854 computed(base.dtype(), base.dims())
855 }
856 Self::Reduce { op, axes } => {
857 let [a] = self.arity::<1>(inputs)?;
858 let shape = self.reduced_shape(a.dims(), axes)?;
859 if !axes.windows(2).all(|w| w[0] < w[1]) {
860 return Err(Error::InvalidArgument(format!(
861 "{}: axes {axes:?} must be sorted and unique",
862 self.name()
863 )));
864 }
865 let supported = match op {
866 ReduceOp::And | ReduceOp::Or => a.dtype() == DType::Bool,
867 ReduceOp::Max | ReduceOp::Min => a.dtype() != DType::Complex64,
868 ReduceOp::Sum | ReduceOp::Prod => true,
869 };
870 if !supported {
871 return Err(Error::unsupported_dtype(self.name(), a.dtype()));
872 }
873 let ordered = matches!(op, ReduceOp::Max | ReduceOp::Min);
874 if ordered && axes.iter().any(|&d| a.dims()[d] == 0) {
875 return Err(Error::InvalidArgument(format!(
876 "{}: cannot reduce a zero-length axis (no identity)",
877 self.name()
878 )));
879 }
880 computed(a.dtype(), &shape)
881 }
882 Self::ArgReduce { axis, .. } => {
883 let [a] = self.arity::<1>(inputs)?;
884 if a.dtype() == DType::Complex64 {
885 return Err(Error::unsupported_dtype(self.name(), a.dtype()));
886 }
887 computed(DType::U32, &self.reduced_shape(a.dims(), &[*axis])?)
888 }
889 Self::GatherAxis { axis } => {
890 let [src, indices] = self.arity::<2>(inputs)?;
891 self.indexed(src, indices, *axis)?;
892 computed(src.dtype(), indices.dims())
893 }
894 Self::ScatterAxis { axis, .. } => {
895 let [dst, indices, updates] = self.arity::<3>(inputs)?;
896 self.indexed(dst, indices, *axis)?;
897 if updates.dims() != indices.dims() {
898 return Err(Error::shape_mismatch(
899 self.name(),
900 indices.dims(),
901 updates.dims(),
902 ));
903 }
904 if updates.dtype() != dst.dtype() {
905 return Err(Error::dtype_mismatch(
906 self.name(),
907 dst.dtype(),
908 updates.dtype(),
909 ));
910 }
911 computed(dst.dtype(), dst.dims())
912 }
913 Self::Sort { axis } | Self::ArgSort { axis } | Self::Scan { axis, .. } => {
914 let [a] = self.arity::<1>(inputs)?;
915 if *axis >= a.ndim() {
916 return Err(Error::InvalidArgument(format!(
917 "{}: axis {axis} out of range for {} dimensions",
918 self.name(),
919 a.ndim()
920 )));
921 }
922 let ordered = matches!(
923 self,
924 Self::Sort { .. }
925 | Self::ArgSort { .. }
926 | Self::Scan {
927 op: ScanOp::Max | ScanOp::Min,
928 ..
929 }
930 );
931 let float_only = matches!(
932 self,
933 Self::Scan {
934 op: ScanOp::LogAddExp,
935 ..
936 }
937 );
938 let category = a.dtype().category();
939 if (ordered && category == crate::Category::Complex)
940 || (float_only && category != crate::Category::Float)
941 {
942 return Err(Error::unsupported_dtype(self.name(), a.dtype()));
943 }
944 let dtype = match self {
945 Self::ArgSort { .. } => DType::U32,
946 _ => a.dtype(),
947 };
948 computed(dtype, a.dims())
949 }
950 Self::Concatenate { axis } => {
951 let first = inputs.first().ok_or_else(|| {
952 Error::InvalidArgument("concatenate: no arrays to join".into())
953 })?;
954 if *axis >= first.ndim() {
955 return Err(Error::InvalidArgument(format!(
956 "concatenate: axis {axis} out of range for {} dimensions",
957 first.ndim()
958 )));
959 }
960 let mut shape: Shape = first.dims().into();
961 for a in &inputs[1..] {
962 let fits = a.ndim() == first.ndim()
963 && a.dims()
964 .iter()
965 .zip(first.dims())
966 .enumerate()
967 .all(|(d, (x, y))| d == *axis || x == y);
968 if !fits {
969 return Err(Error::shape_mismatch("concatenate", first.dims(), a.dims()));
970 }
971 if a.dtype() != first.dtype() {
972 return Err(Error::dtype_mismatch(
973 "concatenate",
974 first.dtype(),
975 a.dtype(),
976 ));
977 }
978 shape[*axis] = shape[*axis].checked_add(a.dims()[*axis]).ok_or_else(|| {
979 Error::InvalidArgument("concatenate: the joined size overflows".into())
980 })?;
981 }
982 computed(first.dtype(), &shape)
983 }
984 Self::Matmul | Self::AddMM { .. } => {
985 let (a, b) = match self {
986 Self::Matmul => {
987 let [a, b] = self.arity::<2>(inputs)?;
988 (a, b)
989 }
990 _ => {
991 let [a, b, _] = self.arity::<3>(inputs)?;
992 (a, b)
993 }
994 };
995 let (sa, sb) = (a.dims(), b.dims());
996 let n = sa.len();
997 let valid =
998 n >= 2 && sb.len() == n && sa[..n - 2] == sb[..n - 2] && sa[n - 1] == sb[n - 2];
999 if !valid {
1000 return Err(Error::shape_mismatch(self.name(), sa, sb));
1001 }
1002 if a.dtype() != b.dtype() {
1003 return Err(Error::dtype_mismatch(self.name(), a.dtype(), b.dtype()));
1004 }
1005 if !matches!(a.dtype().category(), Category::Float | Category::Complex) {
1006 return Err(Error::unsupported_dtype(self.name(), a.dtype()));
1007 }
1008 let mut shape: Shape = sa.into();
1009 shape[n - 1] = sb[n - 1];
1010 if let Self::AddMM { .. } = self {
1011 let c = &inputs[2];
1012 if c.dims() != shape.as_slice() || c.dtype() != a.dtype() {
1013 return Err(Error::InvalidArgument(format!(
1014 "addmm: c must be {} of shape {shape:?}, got {} {:?}",
1015 a.dtype(),
1016 c.dtype(),
1017 c.dims()
1018 )));
1019 }
1020 }
1021 computed(a.dtype(), &shape)
1022 }
1023 Self::RandomBits { shape, width } => {
1024 let [keys] = self.arity::<1>(inputs)?;
1025 if keys.dtype() != DType::U32 || keys.dims().last() != Some(&2) {
1026 return Err(Error::InvalidArgument(format!(
1027 "random_bits: keys must be uint32 of shape [..., 2], got {} {:?}",
1028 keys.dtype(),
1029 keys.dims()
1030 )));
1031 }
1032 let dtype = match width {
1033 1 => DType::U8,
1034 2 => DType::U16,
1035 4 => DType::U32,
1036 _ => {
1037 return Err(Error::InvalidArgument(format!(
1038 "random_bits: width must be 1, 2 or 4, got {width}"
1039 )));
1040 }
1041 };
1042 let out: Vec<usize> = keys.dims()[..keys.ndim() - 1]
1043 .iter()
1044 .chain(shape)
1045 .copied()
1046 .collect();
1047 computed(dtype, &out)
1048 }
1049 Self::Fast(op) => {
1050 return Ok(op
1051 .outputs(inputs)?
1052 .into_iter()
1053 .map(|(dtype, shape)| computed(dtype, &shape))
1054 .collect());
1055 }
1056 Self::Compiled(fused) => {
1057 let dtypes: Vec<DType> = inputs.iter().map(Array::dtype).collect();
1058 if dtypes != fused.inputs() {
1059 return Err(Error::InvalidArgument(format!(
1060 "compiled: inputs {dtypes:?}, expected {:?}",
1061 fused.inputs()
1062 )));
1063 }
1064 let shape = inputs[0].dims();
1065 if let Some(other) = inputs.iter().find(|a| a.dims() != shape) {
1066 return Err(Error::shape_mismatch("compiled", shape, other.dims()));
1067 }
1068 return Ok(fused
1069 .dtypes()
1070 .into_iter()
1071 .map(|d| computed(d, shape))
1072 .collect());
1073 }
1074 Self::Quantize { quant } => {
1075 let [w] = self.arity::<1>(inputs)?;
1076 let (group_size, bits) = quant.checked(self.name())?;
1077 let n = w.ndim();
1078 if !matches!(w.dtype(), DType::F16 | DType::BF16 | DType::F32) {
1079 return Err(Error::unsupported_dtype("quantize", w.dtype()));
1080 }
1081 if n < 2 || w.dims()[n - 1] % group_size != 0 {
1082 return Err(Error::InvalidArgument(format!(
1083 "quantize: needs at least 2 dimensions with the last divisible by \
1084 the group size {group_size}, got shape {:?}",
1085 w.dims()
1086 )));
1087 }
1088 let last = w.dims()[n - 1];
1089 let with_last = |d: usize| {
1090 let mut s: Shape = w.dims().into();
1091 s[n - 1] = d;
1092 s
1093 };
1094 let packed = computed(DType::U32, &with_last(last * bits / 32));
1095 if quant.mode != crate::QuantMode::Affine {
1096 return Ok(smallvec![
1097 packed,
1098 computed(DType::U8, &with_last(last / group_size))
1099 ]);
1100 }
1101 let groups = computed(w.dtype(), &with_last(last / group_size));
1102 return Ok(smallvec![packed, groups.clone(), groups]);
1103 }
1104 Self::Dequantize { quant, dtype } => {
1105 let affine = quant.mode == crate::QuantMode::Affine;
1106 let w = self.quantized_arity(inputs, 2 + usize::from(affine))?;
1107 let mut shape: Shape = w.dims().into();
1108 let n = shape.len();
1109 shape[n - 1] = self.quantized_columns(w, &inputs[1..], *quant)?;
1110 if (affine && *dtype != inputs[1].dtype())
1111 || !matches!(dtype, DType::F16 | DType::BF16 | DType::F32)
1112 {
1113 return Err(Error::unsupported_dtype("dequantize", *dtype));
1114 }
1115 computed(*dtype, &shape)
1116 }
1117 Self::QuantizedMatmul { quant, transpose } => {
1118 let affine = quant.mode == crate::QuantMode::Affine;
1119 let x = self.quantized_arity(inputs, 3 + usize::from(affine))?;
1120 let (w, scales) = (&inputs[1], &inputs[2]);
1121 let columns = self.quantized_columns(w, &inputs[2..], *quant)?;
1122 let rows = w.dims()[w.ndim() - 2];
1123 let (inner, outer) = if *transpose {
1124 (columns, rows)
1125 } else {
1126 (rows, columns)
1127 };
1128 let (xn, wn) = (x.ndim(), w.ndim());
1129 let batch_ok = wn == 2 || (xn == wn && x.dims()[..xn - 2] == w.dims()[..wn - 2]);
1130 if xn == 0 || x.dims()[xn - 1] != inner || !batch_ok {
1131 return Err(Error::shape_mismatch(
1132 "quantized_matmul",
1133 x.dims(),
1134 w.dims(),
1135 ));
1136 }
1137 if affine && x.dtype() != scales.dtype() {
1138 return Err(Error::dtype_mismatch(
1139 "quantized_matmul",
1140 x.dtype(),
1141 scales.dtype(),
1142 ));
1143 }
1144 if !matches!(x.dtype(), DType::F16 | DType::BF16 | DType::F32) {
1145 return Err(Error::unsupported_dtype("quantized_matmul", x.dtype()));
1146 }
1147 let mut shape: Shape = x.dims().into();
1148 shape[xn - 1] = outer;
1149 computed(x.dtype(), &shape)
1150 }
1151 Self::MetalKernel(call) => {
1152 return Ok(call
1153 .outputs
1154 .iter()
1155 .map(|(dtype, shape)| computed(*dtype, shape))
1156 .collect());
1157 }
1158 Self::Custom(call) => {
1159 let outputs = inputs.get(call.primals..).unwrap_or_default();
1160 if outputs.is_empty() {
1161 return Err(Error::InvalidArgument(
1162 "custom_function: the function must return at least one array".into(),
1163 ));
1164 }
1165 return Ok(outputs
1166 .iter()
1167 .map(|a| OutputSpec {
1168 dtype: a.dtype(),
1169 shape: a.dims().into(),
1170 strides: Some(a.strides().into()),
1171 })
1172 .collect());
1173 }
1174 #[cfg(test)]
1175 Self::AddMultiply => {
1176 let [a, b] = self.arity::<2>(inputs)?;
1177 self.same_shape_and_dtype(a, b)?;
1178 let spec = computed(a.dtype(), a.dims());
1179 return Ok(smallvec![spec.clone(), spec]);
1180 }
1181 };
1182 Ok(smallvec![spec])
1183 }
1184
1185 pub(crate) fn dim_slices(&self, ndim: usize) -> Result<Vec<DimSlice>> {
1188 match self {
1189 Self::Slice { start, step, len } | Self::SliceUpdate { start, step, len }
1190 if start.len() == ndim && step.len() == ndim && len.len() == ndim =>
1191 {
1192 Ok((0..ndim)
1193 .map(|i| DimSlice {
1194 start: start[i],
1195 step: step[i],
1196 len: len[i],
1197 })
1198 .collect())
1199 }
1200 _ => Err(Error::InvalidArgument(
1201 "slice: one start, step and length per dimension".into(),
1202 )),
1203 }
1204 }
1205
1206 fn quantized_arity<'a>(&self, inputs: &'a [Array], arity: usize) -> Result<&'a Array> {
1209 if inputs.len() != arity {
1210 return Err(Error::InvalidArgument(format!(
1211 "{}: {arity} inputs expected, got {}",
1212 self.name(),
1213 inputs.len()
1214 )));
1215 }
1216 Ok(&inputs[0])
1217 }
1218
1219 pub(crate) fn quantized_columns(
1224 &self,
1225 w: &Array,
1226 groups: &[Array],
1227 quant: crate::Quant,
1228 ) -> Result<usize> {
1229 let (group_size, bits) = quant.checked(self.name())?;
1230 let scales = &groups[0];
1231 let n = w.ndim();
1232 let invalid =
1233 |what: String| Err(Error::InvalidArgument(format!("{}: {what}", self.name())));
1234 if w.dtype() != DType::U32 {
1235 return invalid(format!("packed weights must be uint32, got {}", w.dtype()));
1236 }
1237 if quant.mode == crate::QuantMode::Affine {
1238 let biases = &groups[1];
1239 if !matches!(scales.dtype(), DType::F16 | DType::BF16 | DType::F32)
1240 || biases.dtype() != scales.dtype()
1241 || biases.dims() != scales.dims()
1242 {
1243 return invalid(format!(
1244 "scales and biases must share a float dtype and shape, got {} {:?} and {} {:?}",
1245 scales.dtype(),
1246 scales.dims(),
1247 biases.dtype(),
1248 biases.dims()
1249 ));
1250 }
1251 } else if scales.dtype() != DType::U8 {
1252 return invalid(format!(
1253 "scale type must be uint8 but received type {}",
1254 scales.dtype()
1255 ));
1256 }
1257 let packed = w.dims().last().copied().unwrap_or(0) * 32;
1258 let groups = scales.dims().last().copied().unwrap_or(0) * group_size;
1259 if n < 2
1260 || scales.ndim() != n
1261 || w.dims()[..n - 1] != scales.dims()[..n - 1]
1262 || packed != groups * bits
1263 {
1264 return invalid(format!(
1265 "packed weights {:?} and scales {:?} do not match group size {group_size} \
1266 and {bits} bits",
1267 w.dims(),
1268 scales.dims()
1269 ));
1270 }
1271 Ok(groups)
1272 }
1273
1274 fn indexed(&self, array: &Array, indices: &Array, axis: usize) -> Result<()> {
1277 if !matches!(
1278 indices.dtype().category(),
1279 crate::Category::Signed | crate::Category::Unsigned
1280 ) {
1281 return Err(Error::InvalidArgument(format!(
1282 "{}: indices must be integers, got {}",
1283 self.name(),
1284 indices.dtype()
1285 )));
1286 }
1287 let fits = indices.ndim() == array.ndim()
1288 && axis < array.ndim()
1289 && indices
1290 .dims()
1291 .iter()
1292 .zip(array.dims())
1293 .enumerate()
1294 .all(|(d, (i, a))| d == axis || i == a);
1295 if !fits {
1296 return Err(Error::InvalidArgument(format!(
1297 "{}: indices of shape {:?} do not fit an array of shape {:?} along axis {axis}",
1298 self.name(),
1299 indices.dims(),
1300 array.dims()
1301 )));
1302 }
1303 if array.dims()[axis] == 0 && indices.size() > 0 {
1304 return Err(Error::InvalidArgument(format!(
1305 "{}: cannot index an empty axis",
1306 self.name()
1307 )));
1308 }
1309 Ok(())
1310 }
1311
1312 fn reduced_shape(&self, shape: &[usize], axes: &[usize]) -> Result<Shape> {
1314 let mut out: Shape = shape.into();
1315 for &axis in axes {
1316 *out.get_mut(axis).ok_or_else(|| {
1317 Error::InvalidArgument(format!(
1318 "{}: axis {axis} out of range for shape {shape:?}",
1319 self.name()
1320 ))
1321 })? = 1;
1322 }
1323 Ok(out)
1324 }
1325
1326 fn arity<'a, const N: usize>(&self, inputs: &'a [Array]) -> Result<&'a [Array; N]> {
1327 inputs.try_into().map_err(|_| {
1328 Error::InvalidArgument(format!(
1329 "{} takes {N} inputs, got {}",
1330 self.name(),
1331 inputs.len()
1332 ))
1333 })
1334 }
1335
1336 fn same_shape_and_dtype(&self, a: &Array, b: &Array) -> Result<()> {
1337 if a.dims() != b.dims() {
1338 return Err(Error::shape_mismatch(self.name(), a.dims(), b.dims()));
1339 }
1340 if a.dtype() != b.dtype() {
1341 return Err(Error::dtype_mismatch(self.name(), a.dtype(), b.dtype()));
1342 }
1343 Ok(())
1344 }
1345}
1346
1347pub(crate) fn copy(x: &Array) -> Result<Array> {
1349 Ok(Array::from_op(Primitive::Copy, vec![x.clone()])?.swap_remove(0))
1350}
1351
1352pub(crate) fn depends(x: &Array, deps: &[Array]) -> Result<Array> {
1354 let mut inputs = Vec::with_capacity(deps.len() + 1);
1355 inputs.push(x.clone());
1356 inputs.extend_from_slice(deps);
1357 let primitive = Primitive::Depends { n_deps: deps.len() };
1358 Ok(Array::from_op(primitive, inputs)?.swap_remove(0))
1359}
1360
1361pub(crate) fn region_fits(dims: &[DimSlice], shape: &[usize]) -> bool {
1365 dims.len() == shape.len()
1366 && dims.iter().zip(shape).all(|(d, &n)| {
1367 if d.step == 0 {
1368 return false;
1369 }
1370 if d.len == 0 {
1371 return d.start <= n;
1372 }
1373 let last = isize::try_from(d.len - 1)
1374 .ok()
1375 .and_then(|steps| steps.checked_mul(d.step))
1376 .and_then(|span| isize::try_from(d.start).ok()?.checked_add(span));
1377 d.start < n && last.is_some_and(|last| (0..n as isize).contains(&last))
1378 })
1379}
1380
1381#[cfg(test)]
1382mod tests {
1383 use std::collections::hash_map::DefaultHasher;
1384 use std::hash::{Hash, Hasher};
1385
1386 use super::*;
1387
1388 fn hash(p: &Primitive) -> u64 {
1389 let mut h = DefaultHasher::new();
1390 p.hash(&mut h);
1391 h.finish()
1392 }
1393
1394 #[test]
1395 fn equal_primitives_compare_and_hash_equal() {
1396 let add = Primitive::Binary(BinaryOp::Add);
1397 assert_eq!(add, Primitive::Binary(BinaryOp::Add));
1398 assert_eq!(hash(&add), hash(&Primitive::Binary(BinaryOp::Add)));
1399 assert_ne!(add, Primitive::Binary(BinaryOp::Multiply));
1400 let s = |start: Vec<usize>| Primitive::Slice {
1401 start,
1402 step: vec![1],
1403 len: vec![2],
1404 };
1405 assert_eq!(s(vec![1]), s(vec![1]));
1406 assert_ne!(s(vec![1]), s(vec![2]));
1407 }
1408}