Skip to main content

nuvec_core/fast/
imp.rs

1//! The implementation of the fused operations (see [`crate::fast`]): `Result`-returning
2//! bodies, kernels' records and fallback graphs.
3
4use crate::cast::Canon;
5use crate::indexing::stack;
6use crate::kernels::Operand;
7use crate::ops::imp::zeros;
8use crate::ops::imp::{
9    self as ops, add, arange, astype, broadcast_to, cos, divide, equal, exp, expand_dims, filled,
10    less, log, max, mean, multiply, reshape, sin, slice, sqrt, squeeze, stop_gradient, subtract,
11    sum, where_,
12};
13use crate::primitive::Primitive;
14
15use super::plan::{self, GroupKind, MaskKind, Route, RouteKey, Target};
16use crate::{
17    Array, Category, DType, Device, DeviceKind, Error, Result, SliceArg, Stream, default_stream,
18};
19
20/// A fused operation (see [`crate::fast`]).
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22#[non_exhaustive]
23pub enum FastOp {
24    /// `(x, w)`: `x / sqrt(mean(x²) + eps) · w` over the last axis; `w` is 1-D or a scalar 1.
25    RmsNorm {
26        /// `f32::to_bits` of epsilon.
27        eps: u32,
28    },
29    /// `(x, r, w)`: `h = x + r` and [`FastOp::RmsNorm`] of `h` (see [`add_rms_norm`](crate::fast::add_rms_norm)).
30    AddRmsNorm {
31        /// `f32::to_bits` of epsilon.
32        eps: u32,
33    },
34    /// `(x, w, g)`: the gradients of [`FastOp::RmsNorm`] for `x` and `w`.
35    RmsNormVjp {
36        /// `f32::to_bits` of epsilon.
37        eps: u32,
38    },
39    /// `(x, w, b)`: `(x - mean) / sqrt(var + eps) · w + b` over the last axis; `w` and `b`
40    /// are 1-D or the scalars 1 and 0.
41    LayerNorm {
42        /// `f32::to_bits` of epsilon.
43        eps: u32,
44    },
45    /// `(x, w, b, g)`: the gradients of [`FastOp::LayerNorm`] for `x`, `w` and `b`.
46    LayerNormVjp {
47        /// `f32::to_bits` of epsilon.
48        eps: u32,
49    },
50    /// `(x, offset[, freqs])`: rotary position embeddings of the first `dims` features.
51    Rope {
52        /// Rotated features (even).
53        dims: usize,
54        /// Rotate interleaved pairs instead of the two halves.
55        traditional: bool,
56        /// `f32::to_bits` of the frequency base (unused with `freqs`).
57        base: u32,
58        /// `f32::to_bits` of the position scale.
59        scale: u32,
60        /// Rotate forwards (`false`: the inverse rotation, the gradient).
61        forward: bool,
62    },
63    /// `([cache,] x, w[, freqs])`: [`norm_rope`](crate::fast::norm_rope): each `[b, h, l]` row of `x` RMS-normalised
64    /// (with `eps`) and rotated as [`FastOp::Rope`] at position `offset + l`; written into
65    /// `cache` at the same positions, or returned as a new `[B, H, L, D]` array.
66    NormRope {
67        /// `f32::to_bits` of epsilon, or no normalisation.
68        eps: Option<u32>,
69        /// Rotated features (even).
70        dims: usize,
71        /// Rotate interleaved pairs instead of the two halves.
72        traditional: bool,
73        /// `f32::to_bits` of the frequency base (unused with `freqs`).
74        base: u32,
75        /// `f32::to_bits` of the position scale.
76        scale: u32,
77        /// The position of the first row.
78        offset: usize,
79        /// Whether the first input is a cache written at the rows' positions.
80        cache: bool,
81    },
82    /// `(x, w)`: [`conv1d`](crate::Array::conv1d) with `groups` equal to the channels (depthwise).
83    DepthwiseConv1d {
84        /// Positions between outputs.
85        stride: usize,
86        /// Zeros before and after the input.
87        padding: usize,
88        /// Positions between taps.
89        dilation: usize,
90    },
91    /// `(q, k_cache, v_cache, block_tables, lengths)`: [`paged_attention`](crate::fast::paged_attention).
92    PagedAttention {
93        /// `f32::to_bits` of the score scale.
94        scale: u32,
95    },
96    /// `(cache, new, slots)`: [`paged_write`](crate::fast::paged_write).
97    PagedWrite,
98    /// `(x)`: softmax over the last axis, accumulated in float32.
99    Softmax,
100    /// `(x)`: `log(sum(exp(x)))` over the last axis, which becomes size 1.
101    LogSumExp,
102    /// `(logits, targets)`: `logsumexp(logits) - logits[target]` per row, in float32.
103    CrossEntropy,
104    /// `(logits, targets, loss, g)`: the gradient of [`FastOp::CrossEntropy`] for the logits.
105    CrossEntropyVjp,
106    /// `(q, k, v[, mask][, sinks])`: scaled dot-product attention (see
107    /// [`scaled_dot_product_attention`](crate::fast::scaled_dot_product_attention)).
108    Attention {
109        /// `f32::to_bits` of the score scale.
110        scale: u32,
111        /// Causal masking (keys after the query's position, aligned at the end, are hidden).
112        causal: bool,
113        /// Whether an array mask (bool or additive, `[B, Hq, Lq, Lk]`) is the fourth input.
114        mask: bool,
115        /// Whether per-head attention sinks (`[Hq]`) are the last input.
116        sinks: bool,
117        /// The vector kernels' passes over the keys (1 or 2; at most 8 queries), or 0 for
118        /// the tiled kernel. Set by the attention plan.
119        passes: u8,
120        /// Key partitions of the first of two vector passes.
121        blocks: u32,
122        /// The decode-only grouped-query two-pass kernel (one query, no mask or sinks).
123        gqa_kernel: bool,
124        /// The one-key-per-step variant of the two-pass kernel.
125        tk1: bool,
126    },
127    /// `(q, k, v, g, beta, state)`: the gated delta rule (see [`gated_delta`](crate::fast::gated_delta)), with the
128    /// outputs and the final state (or every step's, [`gated_delta_steps`](crate::fast::gated_delta_steps)).
129    GatedDelta {
130        /// The state after every step (`[B, T, Hv, Dv, Dk]`) instead of the final one.
131        steps: bool,
132    },
133    /// `(q, k, v, g, beta, pool, slots)`: [`gated_delta_slots`](crate::fast::gated_delta_slots), with the outputs and the
134    /// updated pool.
135    GatedDeltaSlots,
136    /// `(a, b, lhs_indices, rhs_indices[, offsets])`: matrix products of gathered batches
137    /// (see [`gather_mm`](crate::ops::gather_mm)), as [`FastOp::GatherQmm`] in full precision.
138    GatherMm,
139    /// `(x, w, scales, biases, lhs_indices, rhs_indices[, offsets])`: quantized matrix
140    /// products of gathered batches (see [`gather_qmm`](crate::ops::gather_qmm)); the indices are uint32
141    /// and broadcast to the output's batch shape. `offsets` (int32, one per batch of `w`)
142    /// marks where each weight batch's rows start when the rows are sorted by it.
143    GatherQmm {
144        /// Values per scale and bias.
145        group_size: usize,
146        /// Bits per value.
147        bits: u8,
148        /// `x · wᵀ` (else `x · w`).
149        transpose: bool,
150    },
151}
152
153/// The mask of [`scaled_dot_product_attention`](crate::fast::scaled_dot_product_attention).
154#[derive(Debug, Clone, Copy, Default)]
155#[non_exhaustive]
156pub enum AttentionMask<'a> {
157    /// Every query attends to every key.
158    #[default]
159    None,
160    /// Query `i` attends to keys `0..=i + Lk - Lq` (the queries are the last `Lq` positions).
161    Causal,
162    /// A boolean mask (true attends) or an additive mask, broadcastable to
163    /// `[B, Hq, Lq, Lk]`.
164    Array(&'a Array),
165}
166
167fn float(bits: u32) -> f32 {
168    f32::from_bits(bits)
169}
170
171/// A 0-D array holding `value` in `dtype`.
172fn scalar(value: f64, dtype: DType) -> Result<Array> {
173    filled(&[], dtype, |_| Canon::Float(value))
174}
175
176/// Whether the kernels cover `dtype` on the current stream.
177pub(crate) fn on_gpu(dtype: DType, size: usize) -> bool {
178    default_stream().device.kind == DeviceKind::Gpu
179        && matches!(dtype, DType::F16 | DType::BF16 | DType::F32)
180        && size > 0
181        && size < u32::MAX as usize
182}
183
184/// Records `op` as one GPU kernel, or builds its fallback graph (CPU stream, other dtypes,
185/// empty arrays).
186pub(crate) fn record_or_fallback(op: FastOp, inputs: Vec<Array>) -> Result<Vec<Array>> {
187    if on_gpu(inputs[0].dtype(), inputs[0].size()) {
188        Array::from_op(Primitive::Fast(op), inputs)
189    } else {
190        op.fallback(&inputs)
191    }
192}
193
194fn one(op: FastOp, inputs: Vec<Array>) -> Result<Array> {
195    Ok(record_or_fallback(op, inputs)?.swap_remove(0))
196}
197
198fn floating(op: &'static str, dtype: DType) -> Result<()> {
199    match dtype.category() {
200        Category::Float => Ok(()),
201        _ => Err(Error::unsupported_dtype(op, dtype)),
202    }
203}
204
205/// A 1-D parameter of the last axis's size.
206fn check_param(op: &'static str, what: &str, p: &Array, x: &Array) -> Result<()> {
207    if p.ndim() != 1 || p.size() != x.dims()[x.ndim() - 1] {
208        return Err(Error::InvalidArgument(format!(
209            "{op}: {what} must be 1-D with {} elements, got shape {:?}",
210            x.dims()[x.ndim() - 1],
211            p.dims()
212        )));
213    }
214    Ok(())
215}
216
217/// The body of [`crate::fast::rms_norm`].
218pub(crate) fn rms_norm(x: &Array, weight: Option<&Array>, eps: f32) -> Result<Array> {
219    if x.ndim() == 0 {
220        return Err(Error::InvalidArgument(
221            "rms_norm: the input needs at least one dimension".into(),
222        ));
223    }
224    let dtype = match weight {
225        Some(w) => {
226            check_param("rms_norm", "weight", w, x)?;
227            crate::promote_types(x.dtype(), w.dtype())
228        }
229        None => x.dtype(),
230    };
231    floating("rms_norm", dtype)?;
232    let w = match weight {
233        Some(w) => astype(w, dtype)?,
234        None => scalar(1.0, dtype)?,
235    };
236    let op = FastOp::RmsNorm { eps: eps.to_bits() };
237    one(op, vec![astype(x, dtype)?, w])
238}
239
240/// The body of [`crate::fast::add_rms_norm`].
241pub(crate) fn add_rms_norm(
242    x: &Array,
243    residual: &Array,
244    weight: Option<&Array>,
245    eps: f32,
246) -> Result<(Array, Array)> {
247    let dtype = crate::promote_types(x.dtype(), residual.dtype());
248    let fused = x.dims() == residual.dims()
249        && x.ndim() > 0
250        && dtype.category() == Category::Float
251        && weight.is_none_or(|w| {
252            w.slot_error().is_none()
253                && w.ndim() == 1
254                && w.size() == x.dims()[x.ndim() - 1]
255                && crate::promote_types(dtype, w.dtype()) == dtype
256        });
257    if !fused {
258        let h = add(x, residual)?;
259        let y = rms_norm(&h, weight, eps)?;
260        return Ok((h, y));
261    }
262    let w = match weight {
263        Some(w) => astype(w, dtype)?,
264        None => scalar(1.0, dtype)?,
265    };
266    let op = FastOp::AddRmsNorm { eps: eps.to_bits() };
267    let mut out = record_or_fallback(op, vec![astype(x, dtype)?, astype(residual, dtype)?, w])?;
268    let y = out.pop().expect("two outputs");
269    Ok((out.pop().expect("two outputs"), y))
270}
271
272/// The body of [`crate::fast::layer_norm`].
273pub(crate) fn layer_norm(
274    x: &Array,
275    weight: Option<&Array>,
276    bias: Option<&Array>,
277    eps: f32,
278) -> Result<Array> {
279    if x.ndim() == 0 {
280        return Err(Error::InvalidArgument(
281            "layer_norm: the input needs at least one dimension".into(),
282        ));
283    }
284    let mut dtype = x.dtype();
285    for (p, what) in [(weight, "weight"), (bias, "bias")] {
286        if let Some(p) = p {
287            check_param("layer_norm", what, p, x)?;
288            dtype = crate::promote_types(dtype, p.dtype());
289        }
290    }
291    floating("layer_norm", dtype)?;
292    let w = match weight {
293        Some(w) => astype(w, dtype)?,
294        None => scalar(1.0, dtype)?,
295    };
296    let b = match bias {
297        Some(b) => astype(b, dtype)?,
298        None => scalar(0.0, dtype)?,
299    };
300    let op = FastOp::LayerNorm { eps: eps.to_bits() };
301    one(op, vec![astype(x, dtype)?, w, b])
302}
303
304/// The body of [`crate::fast::rope`].
305pub(crate) fn rope(
306    x: &Array,
307    dims: usize,
308    traditional: bool,
309    base: Option<f32>,
310    scale: f32,
311    offset: &Array,
312    freqs: Option<&Array>,
313) -> Result<Array> {
314    let invalid = |msg: String| Err(Error::InvalidArgument(format!("rope: {msg}")));
315    if x.ndim() < 3 {
316        return invalid(format!(
317            "the input needs at least 3 dimensions, got {:?}",
318            x.dims()
319        ));
320    }
321    floating("rope", x.dtype())?;
322    if offset.ndim() > 1 || (offset.size() != 1 && offset.size() != x.dims()[0]) {
323        return invalid(format!(
324            "offset must be a scalar or have {} elements, got shape {:?}",
325            x.dims()[0],
326            offset.dims()
327        ));
328    }
329    if !matches!(
330        offset.dtype().category(),
331        Category::Signed | Category::Unsigned
332    ) {
333        return invalid(format!("offset must be an integer, got {}", offset.dtype()));
334    }
335    if dims == 0 || !dims.is_multiple_of(2) || dims > x.dims()[x.ndim() - 1] {
336        return invalid(format!(
337            "dims must be even, positive and at most {}, got {dims}",
338            x.dims()[x.ndim() - 1]
339        ));
340    }
341    let offset = match offset.dtype().size() {
342        4 => offset.clone(),
343        _ => astype(offset, DType::I32)?,
344    };
345    let mut inputs = vec![x.clone(), offset];
346    let base = match (base, freqs) {
347        (Some(b), None) => b,
348        (None, Some(f)) => {
349            if f.ndim() != 1 || f.size() != dims / 2 {
350                return invalid(format!(
351                    "freqs must be 1-D with {} elements, got shape {:?}",
352                    dims / 2,
353                    f.dims()
354                ));
355            }
356            inputs.push(astype(f, DType::F32)?);
357            1.0
358        }
359        _ => return invalid("exactly one of base and freqs must be given".into()),
360    };
361    let op = FastOp::Rope {
362        dims,
363        traditional,
364        base: base.to_bits(),
365        scale: scale.to_bits(),
366        forward: true,
367    };
368    one(op, inputs)
369}
370
371/// The body of [`crate::fast::cross_entropy`].
372pub(crate) fn cross_entropy(logits: &Array, targets: &Array) -> Result<Array> {
373    if logits.ndim() == 0 || targets.dims() != &logits.dims()[..logits.ndim() - 1] {
374        return Err(Error::InvalidArgument(format!(
375            "cross_entropy: targets of shape {:?} do not match logits of shape {:?}",
376            targets.dims(),
377            logits.dims()
378        )));
379    }
380    floating("cross_entropy", logits.dtype())?;
381    if !matches!(
382        targets.dtype().category(),
383        Category::Signed | Category::Unsigned
384    ) {
385        return Err(Error::InvalidArgument(format!(
386            "cross_entropy: targets must be integer class indices, got {}",
387            targets.dtype()
388        )));
389    }
390    one(
391        FastOp::CrossEntropy,
392        vec![logits.clone(), astype(targets, DType::I32)?],
393    )
394}
395
396/// Records a `SliceUpdate` of `base`'s last axis at `start, start + step, …` (`len` values)
397/// with `update`.
398fn update_last(base: &Array, update: &Array, start: usize, step: isize) -> Result<Array> {
399    let n = base.ndim();
400    let mut starts = vec![0; n];
401    starts[n - 1] = start;
402    let mut steps = vec![1; n];
403    steps[n - 1] = step;
404    let primitive = Primitive::SliceUpdate {
405        start: starts,
406        step: steps,
407        len: update.dims().to_vec(),
408    };
409    Ok(Array::from_op(primitive, vec![base.clone(), update.clone()])?.swap_remove(0))
410}
411
412/// The last axis of `x` from `start` in steps of `step`, `len` values.
413fn last(x: &Array, start: usize, step: isize, len: usize) -> Result<Array> {
414    let mut args = vec![SliceArg::full(); x.ndim()];
415    let stop = start as isize + step * len as isize;
416    args[x.ndim() - 1] = SliceArg::new(Some(start as isize), Some(stop), step);
417    slice(x, &args)
418}
419
420/// `1 / sqrt(x)`.
421fn rsqrt(x: &Array) -> Result<Array> {
422    divide(1.0, sqrt(x)?)
423}
424
425/// `softmax` over the last axis from ordinary operations, in float32 for half types.
426fn softmax_graph(x: &Array) -> Result<Array> {
427    let xf = if x.dtype() == DType::F32 {
428        x.clone()
429    } else {
430        astype(x, DType::F32)?
431    };
432    let m = stop_gradient(&max(&xf, &[-1], true)?)?;
433    let e = exp(&subtract(&xf, &m)?)?;
434    astype(&divide(&e, sum(&e, &[-1], true)?)?, x.dtype())
435}
436
437/// `log(sum(exp(x)))` over the last axis (kept), from ordinary operations.
438fn logsumexp_graph(x: &Array) -> Result<Array> {
439    let m = stop_gradient(&max(x, &[-1], true)?)?;
440    let out = add(log(&sum(&exp(&subtract(x, &m)?)?, &[-1], true)?)?, &m)?;
441    where_(equal(&ops::abs(&m)?, f64::INFINITY)?, &m, &out)
442}
443
444/// `[..., C]` one-hot rows of the (non-negative) class indices `y`, in `dtype`.
445fn one_hot(y: &Array, classes: usize, dtype: DType) -> Result<Array> {
446    let positions = arange(0.0, classes as f64, 1.0, y.dtype())?;
447    equal(&expand_dims(y, -1)?, &positions)?.try_astype(dtype)
448}
449
450/// Class indices with negative values counted from the end.
451fn wrap(y: &Array, classes: usize) -> Result<Array> {
452    where_(less(y, 0)?, add(y, classes as f64)?, y)
453}
454
455impl FastOp {
456    pub(crate) fn name(&self) -> &'static str {
457        match self {
458            Self::RmsNorm { .. } => "rms_norm",
459            Self::AddRmsNorm { .. } => "add_rms_norm",
460            Self::RmsNormVjp { .. } => "rms_norm_vjp",
461            Self::LayerNorm { .. } => "layer_norm",
462            Self::LayerNormVjp { .. } => "layer_norm_vjp",
463            Self::Rope { .. } => "rope",
464            Self::NormRope { .. } => "norm_rope",
465            Self::DepthwiseConv1d { .. } => "conv1d",
466            Self::PagedAttention { .. } => "paged_attention",
467            Self::PagedWrite => "paged_write",
468            Self::Softmax => "softmax",
469            Self::LogSumExp => "logsumexp",
470            Self::CrossEntropy => "cross_entropy",
471            Self::CrossEntropyVjp => "cross_entropy_vjp",
472            Self::Attention { .. } => "scaled_dot_product_attention",
473            Self::GatedDelta { .. } | Self::GatedDeltaSlots => "gated_delta",
474            Self::GatherQmm { .. } => "gather_qmm",
475            Self::GatherMm => "gather_mm",
476        }
477    }
478
479    /// `(dtype, shape)` per output, after checking the input count.
480    pub(crate) fn outputs(&self, inputs: &[Array]) -> Result<Vec<(DType, Vec<usize>)>> {
481        let arity = match self {
482            Self::Softmax | Self::LogSumExp => 1,
483            Self::RmsNorm { .. } | Self::CrossEntropy | Self::DepthwiseConv1d { .. } => 2,
484            Self::PagedAttention { .. } => 5,
485            Self::PagedWrite => 3,
486            Self::AddRmsNorm { .. } | Self::RmsNormVjp { .. } | Self::LayerNorm { .. } => 3,
487            Self::LayerNormVjp { .. } | Self::CrossEntropyVjp => 4,
488            Self::Rope { .. } => inputs.len().clamp(2, 3),
489            Self::NormRope { cache, .. } => inputs
490                .len()
491                .clamp(2 + usize::from(*cache), 3 + usize::from(*cache)),
492            Self::Attention { mask, sinks, .. } => 3 + usize::from(*mask) + usize::from(*sinks),
493            Self::GatedDelta { .. } => 6,
494            Self::GatedDeltaSlots => 7,
495            Self::GatherQmm { .. } => inputs.len().clamp(6, 7),
496            Self::GatherMm => inputs.len().clamp(4, 5),
497        };
498        if inputs.len() != arity || inputs[0].ndim() == 0 {
499            return Err(Error::InvalidArgument(format!(
500                "{}: {arity} inputs expected, got {}",
501                self.name(),
502                inputs.len()
503            )));
504        }
505        self.check(inputs)?;
506        let x = &inputs[0];
507        let like = |a: &Array| (a.dtype(), a.dims().to_vec());
508        Ok(match self {
509            Self::RmsNorm { .. }
510            | Self::LayerNorm { .. }
511            | Self::Rope { .. }
512            | Self::NormRope { .. }
513            | Self::Softmax => {
514                vec![like(x)]
515            }
516            Self::LogSumExp => {
517                let mut shape = x.dims().to_vec();
518                *shape.last_mut().expect("1-D") = 1;
519                vec![(x.dtype(), shape)]
520            }
521            Self::AddRmsNorm { .. } => vec![like(x), like(x)],
522            Self::PagedAttention { .. } | Self::PagedWrite => vec![like(x)],
523            Self::DepthwiseConv1d {
524                stride,
525                padding,
526                dilation,
527            } => {
528                let (l, k) = (x.dims()[1], inputs[1].dims()[1]);
529                let l_out = (l + 2 * padding - dilation * (k - 1) - 1) / stride + 1;
530                vec![(x.dtype(), vec![x.dims()[0], l_out, x.dims()[2]])]
531            }
532            Self::RmsNormVjp { .. } => vec![like(x), like(&inputs[1])],
533            Self::LayerNormVjp { .. } => vec![like(x), like(&inputs[1]), like(&inputs[2])],
534            Self::CrossEntropy => vec![(DType::F32, inputs[1].dims().to_vec())],
535            Self::CrossEntropyVjp => vec![like(x)],
536            Self::Attention { .. } => {
537                let mut shape = x.dims().to_vec();
538                shape[3] = inputs[2].dims()[3];
539                vec![(x.dtype(), shape)]
540            }
541            Self::GatedDeltaSlots => {
542                vec![(x.dtype(), inputs[2].dims().to_vec()), like(&inputs[5])]
543            }
544            Self::GatedDelta { steps } => {
545                let state = match steps {
546                    false => inputs[5].dims().to_vec(),
547                    true => {
548                        let mut shape = inputs[5].dims().to_vec();
549                        shape.insert(1, x.dims()[1]);
550                        shape
551                    }
552                };
553                vec![
554                    (x.dtype(), inputs[2].dims().to_vec()),
555                    (inputs[5].dtype(), state),
556                ]
557            }
558            Self::GatherMm => {
559                let b = inputs[1].dims();
560                let mut shape = inputs[2].dims().to_vec();
561                shape.extend([x.dims()[x.ndim() - 2], b[b.len() - 1]]);
562                vec![(x.dtype(), shape)]
563            }
564            Self::GatherQmm {
565                bits, transpose, ..
566            } => {
567                let w = inputs[1].dims();
568                let n = match transpose {
569                    true => w[w.len() - 2],
570                    false => w[w.len() - 1] * 32 / usize::from(*bits),
571                };
572                let mut shape = inputs[4].dims().to_vec();
573                shape.extend([x.dims()[x.ndim() - 2], n]);
574                vec![(x.dtype(), shape)]
575            }
576        })
577    }
578
579    /// Validates the inputs of a recorded fast operation (the input count is checked
580    /// already): what the public functions check before they record one, including the
581    /// conditions under which they choose the kernel, so that a primitive built directly
582    /// (`Array::from_primitive`) is either rejected or one the kernels and fallbacks take.
583    fn check(&self, inputs: &[Array]) -> Result<()> {
584        let name = self.name();
585        let invalid = |msg: String| Err(Error::InvalidArgument(format!("{name}: {msg}")));
586        let x = &inputs[0];
587        let kernel_float = |a: &Array| matches!(a.dtype(), DType::F16 | DType::BF16 | DType::F32);
588        let integer = |a: &Array| matches!(a.dtype(), DType::I32 | DType::U32);
589        let last = |a: &Array| a.dims().last().copied().unwrap_or(0);
590        // A scalar or a 1-D parameter of the last axis's size, in `x`'s dtype.
591        let param = |p: &Array| {
592            p.dtype() == x.dtype() && (p.ndim() == 0 || (p.ndim() == 1 && p.size() == last(x)))
593        };
594        let like = |a: &Array| a.dtype() == x.dtype() && a.dims() == x.dims();
595        if !kernel_float(x) {
596            return Err(Error::unsupported_dtype(name, x.dtype()));
597        }
598        let rotation = |dims: usize, x: &Array, freqs: Option<&Array>| {
599            let d = last(x);
600            dims > 0
601                && dims.is_multiple_of(2)
602                && dims <= d
603                && freqs.is_none_or(|f| {
604                    f.dtype() == DType::F32 && f.ndim() == 1 && f.size() == dims / 2
605                })
606        };
607        let ok = match self {
608            Self::RmsNorm { .. } | Self::LayerNorm { .. } => inputs[1..].iter().all(param),
609            Self::AddRmsNorm { .. } => like(&inputs[1]) && param(&inputs[2]),
610            Self::RmsNormVjp { .. } => param(&inputs[1]) && like(&inputs[2]),
611            Self::LayerNormVjp { .. } => param(&inputs[1]) && param(&inputs[2]) && like(&inputs[3]),
612            Self::Softmax | Self::LogSumExp => true,
613            Self::Rope { dims, .. } => {
614                let offset = &inputs[1];
615                x.ndim() >= 3
616                    && integer(offset)
617                    && offset.ndim() <= 1
618                    && (offset.size() == 1 || offset.size() == x.dims()[0])
619                    && rotation(*dims, x, inputs.get(2))
620            }
621            Self::NormRope {
622                eps,
623                dims,
624                offset,
625                cache,
626                ..
627            } => {
628                let at = usize::from(*cache);
629                let (y, w) = (&inputs[at], &inputs[at + 1]);
630                let s = y.dims();
631                let Some(end) = offset.checked_add(s.get(2).copied().unwrap_or(0)) else {
632                    return invalid(format!("position {offset} overflows"));
633                };
634                let fits = y.ndim() == 4
635                    && y.dtype() == x.dtype()
636                    && s[3] <= NORM_ROPE_MAX
637                    && end <= i32::MAX as usize;
638                let cache_fits = !cache
639                    || (fits
640                        && x.ndim() == 4
641                        && x.dims()[..2] == s[..2]
642                        && x.dims()[3] == s[3]
643                        && x.dims()[2] >= end);
644                let weight = || {
645                    w.dtype() == x.dtype()
646                        && match eps {
647                            Some(_) => w.ndim() == 1 && w.size() == s[3],
648                            None => w.ndim() == 0,
649                        }
650                };
651                fits && cache_fits && weight() && rotation(*dims, y, inputs.get(at + 2))
652            }
653            Self::DepthwiseConv1d {
654                stride,
655                padding,
656                dilation,
657            } => {
658                let w = &inputs[1];
659                let span = w
660                    .dims()
661                    .get(1)
662                    .and_then(|&k| k.checked_sub(1))
663                    .and_then(|k| dilation.checked_mul(k)?.checked_add(1));
664                let padded = padding
665                    .checked_mul(2)
666                    .and_then(|p| p.checked_add(x.dims().get(1).copied()?));
667                x.ndim() == 3
668                    && w.ndim() == 3
669                    && w.dtype() == x.dtype()
670                    && w.dims()[0] == x.dims()[2]
671                    && w.dims()[2] == 1
672                    && *stride > 0
673                    && *dilation > 0
674                    && matches!((span, padded), (Some(s), Some(p)) if p >= s)
675            }
676            Self::PagedAttention { .. } => {
677                let [q, k, v, tables, lengths] = inputs else {
678                    unreachable!("five inputs")
679                };
680                let (b, hq, d) = (q.dims().first(), q.dims().get(1), q.dims().get(3));
681                q.ndim() == 4
682                    && k.ndim() == 4
683                    && k.dims() == v.dims()
684                    && k.dtype() == q.dtype()
685                    && v.dtype() == q.dtype()
686                    && Some(&k.dims()[3]) == d
687                    && k.dims()[1] > 0
688                    && hq.is_some_and(|h| h % k.dims()[1] == 0)
689                    && matches!(d, Some(64 | 128 | 256))
690                    && k.dims()[2].is_power_of_two()
691                    && k.size() < u32::MAX as usize
692                    && tables.ndim() == 2
693                    && tables.dims().first() == b
694                    && tables.dtype() == DType::I32
695                    && lengths.ndim() == 1
696                    && lengths.dims().first() == b
697                    && lengths.dtype() == DType::I32
698            }
699            Self::PagedWrite => {
700                let [cache, new, slots] = inputs else {
701                    unreachable!("three inputs")
702                };
703                cache.ndim() == 4
704                    && new.ndim() == 3
705                    && new.dtype() == cache.dtype()
706                    && new.dims()[1] == cache.dims()[1]
707                    && new.dims()[2] == cache.dims()[3]
708                    && slots.dims() == [new.dims()[0]]
709                    && slots.dtype() == DType::I32
710            }
711            Self::CrossEntropy | Self::CrossEntropyVjp => {
712                let rows = &x.dims()[..x.ndim() - 1];
713                let targets = &inputs[1];
714                let rest = inputs[2..]
715                    .iter()
716                    .all(|a| a.dtype() == DType::F32 && a.dims() == rows);
717                targets.dtype() == DType::I32 && targets.dims() == rows && rest
718            }
719            Self::Attention { mask, sinks, .. } => {
720                let (q, k, v) = (&inputs[0], &inputs[1], &inputs[2]);
721                if [q, k, v]
722                    .iter()
723                    .any(|a| a.ndim() != 4 || a.dtype() != x.dtype())
724                {
725                    return invalid("q, k and v must be 4-D arrays of one dtype".into());
726                }
727                let (qs, ks, vs) = (q.dims(), k.dims(), v.dims());
728                let shapes = ks[0] == qs[0]
729                    && vs[0] == qs[0]
730                    && qs[3] == ks[3]
731                    && ks[1] == vs[1]
732                    && ks[2] == vs[2]
733                    && ks[1] > 0
734                    && qs[1] % ks[1] == 0;
735                let mask_ok = !mask
736                    || (inputs[3].dims() == [qs[0], qs[1], qs[2], ks[2]]
737                        && matches!(inputs[3].dtype(), d if d == DType::Bool || d == x.dtype()));
738                let sinks_ok = !sinks || {
739                    let s = inputs.last().expect("sinks");
740                    s.ndim() == 1 && s.size() == qs[1] && s.dtype() == x.dtype()
741                };
742                shapes && mask_ok && sinks_ok && attention_config(self, inputs)
743            }
744            Self::GatedDelta { .. } | Self::GatedDeltaSlots => {
745                let (q, k, v, g, beta) =
746                    (&inputs[0], &inputs[1], &inputs[2], &inputs[3], &inputs[4]);
747                if q.ndim() != 4 || v.ndim() != 4 || q.dims() != k.dims() {
748                    return invalid("q and k must be [B, T, Hk, Dk] and v [B, T, Hv, Dv]".into());
749                }
750                let (b, t, hk, dk) = (q.dims()[0], q.dims()[1], q.dims()[2], q.dims()[3]);
751                let (hv, dv) = (v.dims()[2], v.dims()[3]);
752                let state = match self {
753                    Self::GatedDeltaSlots => {
754                        let (pool, slots) = (&inputs[5], &inputs[6]);
755                        pool.ndim() == 4
756                            && pool.dims()[1..] == [hv, dv, dk]
757                            && slots.dims() == [b]
758                            && slots.dtype() == DType::I32
759                            && kernel_float(pool)
760                    }
761                    _ => inputs[5].dims() == [b, hv, dv, dk] && kernel_float(&inputs[5]),
762                };
763                k.dtype() == q.dtype()
764                    && v.dtype() == q.dtype()
765                    && v.dims()[..2] == [b, t]
766                    && hk > 0
767                    && hv % hk == 0
768                    && dk.is_multiple_of(32)
769                    && g.dims() == [b, t, hv]
770                    && beta.dims() == [b, t, hv]
771                    && kernel_float(g)
772                    && kernel_float(beta)
773                    && state
774            }
775            Self::GatherMm | Self::GatherQmm { .. } => {
776                let (w, lhs, rhs) = match self {
777                    Self::GatherMm => (&inputs[1], &inputs[2], &inputs[3]),
778                    _ => (&inputs[1], &inputs[4], &inputs[5]),
779                };
780                if x.ndim() < 2 || w.ndim() < 2 {
781                    return invalid("operands must be [..., M, K] and [..., K, N]".into());
782                }
783                let (m, k) = (x.dims()[x.ndim() - 2], last(x));
784                let inner = match self {
785                    Self::GatherQmm {
786                        group_size,
787                        bits,
788                        transpose,
789                    } => {
790                        crate::quantized::check_params(name, *group_size, u32::from(*bits))?;
791                        let (scales, biases) = (&inputs[2], &inputs[3]);
792                        let n = w.ndim();
793                        let packing = scales.dtype() == x.dtype()
794                            && biases.dtype() == x.dtype()
795                            && biases.dims() == scales.dims()
796                            && w.dtype() == DType::U32
797                            && scales.ndim() == n
798                            && w.dims()[..n - 1] == scales.dims()[..n - 1]
799                            && last(w).checked_mul(32)
800                                == last(scales)
801                                    .checked_mul(*group_size)
802                                    .and_then(|c| c.checked_mul(usize::from(*bits)));
803                        // The kernel takes transposed weights (`x · wᵀ`) only.
804                        let columns = last(scales) * group_size;
805                        (packing && *transpose).then_some(columns)
806                    }
807                    _ => (w.dtype() == x.dtype()).then_some(w.dims()[w.ndim() - 2]),
808                };
809                let offsets = match inputs.get(if *self == Self::GatherMm { 4 } else { 6 }) {
810                    None => true,
811                    Some(o) => {
812                        let experts: usize = w.dims()[..w.ndim() - 2].iter().product();
813                        x.ndim() == 3
814                            && m == 1
815                            && rhs.ndim() == 1
816                            && o.dtype() == DType::I32
817                            && o.dims() == [experts]
818                    }
819                };
820                inner == Some(k)
821                    && lhs.dtype() == DType::U32
822                    && rhs.dtype() == DType::U32
823                    && lhs.dims() == rhs.dims()
824                    && offsets
825            }
826        };
827        if ok {
828            Ok(())
829        } else {
830            invalid(format!(
831                "invalid inputs {:?}",
832                inputs
833                    .iter()
834                    .map(|a| (a.dtype(), a.dims()))
835                    .collect::<Vec<_>>()
836            ))
837        }
838    }
839
840    /// The same computation from ordinary operations (MLX's fallbacks).
841    pub(crate) fn fallback(&self, inputs: &[Array]) -> Result<Vec<Array>> {
842        let x = &inputs[0];
843        let f32x = || astype(x, DType::F32);
844        let out = match self {
845            Self::RmsNorm { eps } => {
846                let xf = f32x()?;
847                let n = rsqrt(&add(mean(&multiply(&xf, &xf)?, &[-1], true)?, float(*eps))?)?;
848                let y = multiply(&xf, &n)?.try_astype(x.dtype())?;
849                let w = &inputs[1];
850                vec![if w.ndim() == 0 { y } else { multiply(&y, w)? }]
851            }
852            Self::AddRmsNorm { eps } => {
853                let h = add(x, &inputs[1])?;
854                let op = Self::RmsNorm { eps: *eps };
855                let y = record_or_fallback(op, vec![h.clone(), inputs[2].clone()])?;
856                vec![h, y.into_iter().next().expect("one output")]
857            }
858            Self::LayerNorm { eps } => {
859                let xf = f32x()?;
860                let xc = subtract(&xf, mean(&xf, &[-1], true)?)?;
861                let v = mean(&multiply(&xc, &xc)?, &[-1], true)?;
862                let mut y = multiply(&xc, rsqrt(&add(&v, float(*eps))?)?)?.try_astype(x.dtype())?;
863                if inputs[1].ndim() != 0 {
864                    y = multiply(&y, &inputs[1])?;
865                }
866                if inputs[2].ndim() != 0 {
867                    y = add(&y, &inputs[2])?;
868                }
869                vec![y]
870            }
871            Self::RmsNormVjp { eps } => {
872                let (w, g) = (&inputs[1], &inputs[2]);
873                let n = rsqrt(&add(mean(&multiply(x, x)?, &[-1], true)?, float(*eps))?)?;
874                let n3 = multiply(&multiply(&n, &n)?, &n)?;
875                let gw = multiply(g, w)?;
876                let t = mean(&multiply(&gw, x)?, &[-1], true)?;
877                let dx = subtract(multiply(&gw, &n)?, multiply(multiply(x, &t)?, &n3)?)?;
878                let dw = match w.ndim() {
879                    0 => scalar(0.0, w.dtype())?,
880                    _ => sum_rows(&multiply(g, multiply(x, &n)?)?)?,
881                };
882                vec![dx, dw]
883            }
884            Self::LayerNormVjp { eps } => {
885                let (w, b, g) = (&inputs[1], &inputs[2], &inputs[3]);
886                let mu = mean(x, &[-1], true)?;
887                let xc = subtract(x, &mu)?;
888                let var = mean(&multiply(&xc, &xc)?, &[-1], true)?;
889                let n = rsqrt(&add(&var, float(*eps))?)?;
890                let n3 = multiply(&multiply(&n, &n)?, &n)?;
891                let wg = multiply(w, g)?;
892                let mwg = mean(&wg, &[-1], true)?;
893                let mwgxc = mean(&multiply(&wg, &xc)?, &[-1], true)?;
894                let dx = subtract(
895                    multiply(subtract(&wg, &mwg)?, &n)?,
896                    multiply(multiply(&xc, &mwgxc)?, &n3)?,
897                )?;
898                let dw = match w.ndim() {
899                    0 => scalar(0.0, w.dtype())?,
900                    _ => sum_rows(&multiply(g, multiply(&xc, &n)?)?)?,
901                };
902                let db = match b.ndim() {
903                    0 => scalar(0.0, b.dtype())?,
904                    _ => sum_rows(g)?,
905                };
906                vec![dx, dw, db]
907            }
908            Self::Attention {
909                scale,
910                causal,
911                mask,
912                sinks,
913                ..
914            } => vec![attention_graph(
915                inputs,
916                float(*scale),
917                *causal,
918                *mask,
919                *sinks,
920                None,
921                None,
922            )?],
923            Self::Rope {
924                dims,
925                traditional,
926                base,
927                scale,
928                forward,
929            } => vec![rope_graph(
930                inputs,
931                *dims,
932                *traditional,
933                float(*base),
934                float(*scale),
935                *forward,
936            )?],
937            Self::NormRope {
938                eps,
939                dims,
940                traditional,
941                base,
942                scale,
943                offset,
944                cache,
945            } => {
946                let at = usize::from(*cache);
947                let (x, w) = (&inputs[at], &inputs[at + 1]);
948                let mut h = x.clone();
949                if let Some(eps) = eps {
950                    let op = Self::RmsNorm { eps: *eps };
951                    h = record_or_fallback(op, vec![h, w.clone()])?.swap_remove(0);
952                }
953                let mut rope_inputs = vec![h, Array::from(*offset as i32)];
954                rope_inputs.extend(inputs.get(at + 2).cloned());
955                let op = Self::Rope {
956                    dims: *dims,
957                    traditional: *traditional,
958                    base: *base,
959                    scale: *scale,
960                    forward: true,
961                };
962                let r = record_or_fallback(op, rope_inputs)?.swap_remove(0);
963                vec![match cache {
964                    true => {
965                        let mut region = vec![SliceArg::full(); 4];
966                        let end = (offset + r.dims()[2]) as isize;
967                        region[2] = SliceArg::new(Some(*offset as isize), Some(end), 1);
968                        crate::ops::imp::slice_update(&inputs[0], &r, &region)?
969                    }
970                    false => r,
971                }]
972            }
973            Self::DepthwiseConv1d {
974                stride,
975                padding,
976                dilation,
977            } => {
978                let c = x.dims()[2];
979                vec![crate::conv::general(
980                    x,
981                    &inputs[1],
982                    (*stride, *padding, *dilation, c),
983                )?]
984            }
985            Self::PagedAttention { scale } => vec![paged_attention_graph(inputs, float(*scale))?],
986            Self::PagedWrite => vec![paged_write_graph(inputs)?],
987            Self::Softmax => vec![softmax_graph(x)?],
988            Self::GatedDelta { steps } => gated_delta_graph(inputs, *steps)?,
989            Self::GatedDeltaSlots => {
990                // The states at the slots, the rule, then the new states back at the slots.
991                let (pool, slots) = (&inputs[5], &inputs[6]);
992                let states = crate::indexing::take(pool, slots, Some(0))?;
993                let mut args = inputs[..5].to_vec();
994                args.push(states);
995                let mut out = gated_delta_graph(&args, false)?;
996                let new = out.pop().expect("two outputs");
997                let at = broadcast_to(&reshape(slots, &[-1, 1, 1, 1])?, new.dims())?;
998                let pool = crate::indexing::put_along_axis(pool, &at, &new, 0)?;
999                vec![out.pop().expect("two outputs"), pool]
1000            }
1001            Self::GatherMm => {
1002                let rows = |a: &Array, indices: &Array| -> Result<Array> {
1003                    let nd = a.ndim();
1004                    let shape = [-1, a.dims()[nd - 2] as isize, a.dims()[nd - 1] as isize];
1005                    crate::indexing::take(&reshape(a, &shape)?, indices, Some(0))
1006                };
1007                vec![crate::ops::imp::matmul(
1008                    &rows(x, &inputs[2])?,
1009                    &rows(&inputs[1], &inputs[3])?,
1010                )?]
1011            }
1012            Self::GatherQmm {
1013                group_size,
1014                bits,
1015                transpose,
1016            } => vec![gather_qmm_graph(
1017                inputs,
1018                *group_size,
1019                u32::from(*bits),
1020                *transpose,
1021            )?],
1022            Self::LogSumExp => vec![logsumexp_graph(x)?],
1023            Self::CrossEntropy => {
1024                let classes = x.dims()[x.ndim() - 1];
1025                let y = wrap(&inputs[1], classes)?;
1026                let hit = one_hot(&y, classes, DType::Bool)?;
1027                let score = sum(&where_(&hit, x, 0.0)?, &[-1], false)?;
1028                let lse = squeeze(&logsumexp_graph(x)?, &[-1])?;
1029                vec![subtract(&lse, &score)?.try_astype(DType::F32)?]
1030            }
1031            Self::CrossEntropyVjp => {
1032                let classes = x.dims()[x.ndim() - 1];
1033                let (y, loss, g) = (wrap(&inputs[1], classes)?, &inputs[2], &inputs[3]);
1034                let hit = one_hot(&y, classes, DType::Bool)?;
1035                let score = sum(&where_(&hit, x, 0.0)?, &[-1], false)?;
1036                let lse = add(loss, score.try_astype(DType::F32)?)?;
1037                let p = exp(&subtract(&f32x()?, expand_dims(&lse, -1)?)?)?;
1038                let gx = multiply(
1039                    expand_dims(g, -1)?,
1040                    subtract(&p, one_hot(&y, classes, DType::F32)?)?,
1041                )?;
1042                vec![gx.try_astype(x.dtype())?]
1043            }
1044        };
1045        Ok(out)
1046    }
1047
1048    /// Reverse-mode rule: one cotangent per input (`None` where not wanted).
1049    pub(crate) fn vjp(
1050        &self,
1051        inputs: &[Array],
1052        outputs: &[Array],
1053        cotangents: &[Array],
1054        wanted: &[bool],
1055    ) -> Result<Vec<Option<Array>>> {
1056        let g = &cotangents[0];
1057        let pick = |all: Vec<Array>| -> Vec<Option<Array>> {
1058            all.into_iter()
1059                .zip(wanted)
1060                .map(|(a, &w)| w.then_some(a))
1061                .collect()
1062        };
1063        match self {
1064            Self::RmsNorm { eps } => {
1065                let op = Self::RmsNormVjp { eps: *eps };
1066                let grads =
1067                    record_or_fallback(op, vec![inputs[0].clone(), inputs[1].clone(), g.clone()])?;
1068                Ok(pick(grads))
1069            }
1070            Self::LayerNorm { eps } => {
1071                let op = Self::LayerNormVjp { eps: *eps };
1072                let mut all = inputs.to_vec();
1073                all.push(g.clone());
1074                Ok(pick(record_or_fallback(op, all)?))
1075            }
1076            Self::Rope { forward, .. } => {
1077                if wanted[1..].iter().any(|&w| w) {
1078                    return Err(Error::InvalidArgument(
1079                        "rope: no gradient with respect to the offset or frequencies".into(),
1080                    ));
1081                }
1082                let Self::Rope {
1083                    dims,
1084                    traditional,
1085                    base,
1086                    scale,
1087                    ..
1088                } = *self
1089                else {
1090                    unreachable!()
1091                };
1092                let inverse = Self::Rope {
1093                    dims,
1094                    traditional,
1095                    base,
1096                    scale,
1097                    forward: !forward,
1098                };
1099                let mut all = inputs.to_vec();
1100                all[0] = g.clone();
1101                let mut out = vec![Some(one(inverse, all)?)];
1102                out.extend(std::iter::repeat_n(None, inputs.len() - 1));
1103                Ok(out)
1104            }
1105            Self::Softmax => {
1106                let s = &outputs[0];
1107                let sg = multiply(s, g)?;
1108                let dx = subtract(&sg, multiply(s, sum(&sg, &[-1], true)?)?)?;
1109                Ok(vec![Some(dx)])
1110            }
1111            Self::LogSumExp => {
1112                let s = one(Self::Softmax, vec![inputs[0].clone()])?;
1113                Ok(vec![Some(multiply(g, &s)?)])
1114            }
1115            Self::CrossEntropy => {
1116                if wanted[1] {
1117                    return Err(Error::InvalidArgument(
1118                        "cross_entropy: no gradient with respect to the targets".into(),
1119                    ));
1120                }
1121                let all = vec![
1122                    inputs[0].clone(),
1123                    inputs[1].clone(),
1124                    outputs[0].clone(),
1125                    g.clone(),
1126                ];
1127                Ok(vec![Some(one(Self::CrossEntropyVjp, all)?), None])
1128            }
1129            Self::AddRmsNorm { .. }
1130            | Self::NormRope { .. }
1131            | Self::PagedAttention { .. }
1132            | Self::PagedWrite
1133            | Self::DepthwiseConv1d { .. }
1134            | Self::RmsNormVjp { .. }
1135            | Self::LayerNormVjp { .. }
1136            | Self::CrossEntropyVjp
1137            | Self::Attention { .. }
1138            | Self::GatedDelta { .. }
1139            | Self::GatedDeltaSlots
1140            | Self::GatherQmm { .. }
1141            | Self::GatherMm => self.fallback_vjp(inputs, cotangents, wanted),
1142        }
1143    }
1144
1145    /// Reverse mode through the fallback graph, for the wanted floating-point inputs.
1146    fn fallback_vjp(
1147        &self,
1148        inputs: &[Array],
1149        cotangents: &[Array],
1150        wanted: &[bool],
1151    ) -> Result<Vec<Option<Array>>> {
1152        let active: Vec<usize> = (0..inputs.len())
1153            .filter(|&i| wanted[i] && inputs[i].dtype().category() == Category::Float)
1154            .collect();
1155        let primals: Vec<Array> = active.iter().map(|&i| inputs[i].clone()).collect();
1156        let f = |ps: &[Array]| {
1157            let mut all = inputs.to_vec();
1158            for (p, &i) in ps.iter().zip(&active) {
1159                all[i] = p.clone();
1160            }
1161            self.fallback(&all)
1162        };
1163        let (_, grads) = crate::vjp_flat(f, &primals, cotangents)?;
1164        let mut out = vec![None; inputs.len()];
1165        for (g, &i) in grads.into_iter().zip(&active) {
1166            out[i] = Some(g);
1167        }
1168        Ok(out)
1169    }
1170
1171    /// Forward-mode rule through the fallback graph (integer inputs are constants).
1172    pub(crate) fn jvp(&self, inputs: &[Array], tangents: &[Option<Array>]) -> Result<Vec<Array>> {
1173        let active: Vec<usize> = (0..inputs.len())
1174            .filter(|&i| tangents[i].is_some() && inputs[i].dtype().category() == Category::Float)
1175            .collect();
1176        let primals: Vec<Array> = active.iter().map(|&i| inputs[i].clone()).collect();
1177        let ts: Vec<Array> = active
1178            .iter()
1179            .map(|&i| tangents[i].clone().expect("active"))
1180            .collect();
1181        let f = |ps: &[Array]| {
1182            let mut all = inputs.to_vec();
1183            for (p, &i) in ps.iter().zip(&active) {
1184                all[i] = p.clone();
1185            }
1186            self.fallback(&all)
1187        };
1188        Ok(crate::jvp_flat(f, &primals, &ts)?.1)
1189    }
1190
1191    /// Batching rule: row-wise operations with only the first input batched (and cross
1192    /// entropy) keep their kernel with the batch axis first; otherwise the fallback graph
1193    /// is batched.
1194    pub(crate) fn vmap(
1195        &self,
1196        inputs: &[Array],
1197        axes: &[Option<usize>],
1198    ) -> Result<(Vec<Array>, usize)> {
1199        let rest_unbatched = axes[1..].iter().all(Option::is_none);
1200        let front = |i: usize| -> Result<Array> {
1201            let a = &inputs[i];
1202            match axes[i] {
1203                Some(ax) => crate::batching::move_axis(a, ax, 0),
1204                None => {
1205                    let b = inputs
1206                        .iter()
1207                        .zip(axes)
1208                        .find_map(|(a, ax)| ax.map(|ax| a.dims()[ax]))
1209                        .expect("one batched input");
1210                    let mut shape = vec![b];
1211                    shape.extend_from_slice(a.dims());
1212                    broadcast_to(&expand_dims(a, 0)?, &shape)
1213                }
1214            }
1215        };
1216        let simple = match self {
1217            Self::RmsNorm { .. } | Self::LayerNorm { .. } | Self::Softmax | Self::LogSumExp => {
1218                rest_unbatched
1219            }
1220            Self::Rope { .. } => rest_unbatched && inputs[1].size() == 1,
1221            Self::CrossEntropy => true,
1222            _ => false,
1223        };
1224        if simple {
1225            let mut all = inputs.to_vec();
1226            all[0] = front(0)?;
1227            if *self == Self::CrossEntropy {
1228                all[1] = front(1)?;
1229            }
1230            return Ok((record_or_fallback(self.clone(), all)?, 0));
1231        }
1232        let f = |xs: &[Array]| self.fallback(xs);
1233        Ok((crate::transforms::batch(&f, inputs, axes)?, 0))
1234    }
1235
1236    /// Runs the fallback graph on the CPU over evaluated operands, for work no kernel takes.
1237    pub(crate) fn eval_cpu(&self, operands: &[Operand<'_>]) -> Result<Vec<crate::array::Data>> {
1238        run_on_cpu(operands, |leaves| self.fallback(leaves))
1239    }
1240}
1241
1242/// Builds a graph with `f` over evaluated `operands` (as contiguous leaves) on the CPU
1243/// stream, evaluates it and returns its outputs' data (contiguous): the CPU fallback of
1244/// fast operations and fused nodes. The thread's default stream is restored afterwards,
1245/// even if `f` or the evaluation panics. The caller holds no node lock (`eval` drops it
1246/// while computing), so the nested evaluation cannot deadlock with the outer one.
1247pub(crate) fn run_on_cpu(
1248    operands: &[Operand<'_>],
1249    f: impl FnOnce(&[Array]) -> Result<Vec<Array>>,
1250) -> Result<Vec<crate::array::Data>> {
1251    let leaves = operands
1252        .iter()
1253        .map(|(meta, data)| {
1254            let buffer = crate::kernels::contiguous(meta, data)?;
1255            Array::from_buffer(buffer, meta.dtype, &meta.shape)
1256        })
1257        .collect::<Result<Vec<_>>>()?;
1258    let outputs = {
1259        let _cpu = crate::device::StreamGuard::set(Stream::default_for(Device::cpu()));
1260        f(&leaves)?
1261    };
1262    crate::eval(&outputs)?;
1263    outputs
1264        .iter()
1265        .map(|a| {
1266            let data = a.data().expect("evaluated");
1267            Ok(crate::array::Data::new(crate::kernels::contiguous(
1268                a.out(),
1269                data,
1270            )?))
1271        })
1272        .collect()
1273}
1274
1275#[cfg(test)]
1276mod tests {
1277    use super::*;
1278
1279    #[test]
1280    fn a_panicking_cpu_fallback_restores_the_default_stream() {
1281        let gpu = Stream::default_for(Device::gpu(0));
1282        crate::set_default_stream(gpu);
1283        let x = Array::from(1.0f32);
1284        let data = x.data().expect("host data").clone();
1285        let operands = [(x.out(), &data)];
1286        let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1287            run_on_cpu(&operands, |_| -> Result<Vec<Array>> {
1288                assert_eq!(default_stream().device, Device::cpu(), "runs on the CPU");
1289                panic!("fallback hook panics");
1290            })
1291        }));
1292        assert!(caught.is_err());
1293        assert_eq!(default_stream(), gpu, "the default stream is restored");
1294        crate::set_default_stream(Stream::default_for(Device::cpu()));
1295    }
1296}
1297
1298/// `g` summed over every axis but the last.
1299fn sum_rows(g: &Array) -> Result<Array> {
1300    if g.ndim() <= 1 {
1301        // No axis to sum over (`sum` with no axes would sum over all of them).
1302        return Ok(g.clone());
1303    }
1304    let axes: Vec<isize> = (0..g.ndim() as isize - 1).collect();
1305    sum(g, &axes, false)
1306}
1307
1308/// MLX's RoPE fallback: the rotated features replace the input's in place.
1309fn rope_graph(
1310    inputs: &[Array],
1311    dims: usize,
1312    traditional: bool,
1313    base: f32,
1314    scale: f32,
1315    forward: bool,
1316) -> Result<Array> {
1317    let x = &inputs[0];
1318    let shape: Vec<isize> = x.dims().iter().map(|&d| d as isize).collect();
1319    let nd = x.ndim();
1320    let (b, t, d) = (x.dims()[0], x.dims()[nd - 2], x.dims()[nd - 1]);
1321    let x4 = reshape(x, &[b as isize, -1, t as isize, d as isize])?;
1322    let dtype = x.dtype();
1323    let half = dims / 2;
1324    let offset = &inputs[1];
1325    let offset = if offset.size() > 1 {
1326        reshape(offset, &[-1, 1, 1])?
1327    } else {
1328        reshape(offset, &[])?
1329    };
1330    let positions = multiply(
1331        add(arange(0.0, t as f64, 1.0, DType::F32)?, &offset)?,
1332        f64::from(scale),
1333    )?;
1334    let inv_freqs = match inputs.get(2) {
1335        Some(f) => divide(1.0, f)?,
1336        None => exp(&multiply(
1337            arange(0.0, -(half as f64), -1.0, DType::F32)?,
1338            f64::from(base.ln() / half as f32),
1339        )?)?,
1340    };
1341    let theta = multiply(expand_dims(&positions, -1)?, &inv_freqs)?;
1342    let (c, s) = (
1343        cos(&theta)?.try_astype(dtype)?,
1344        sin(&theta)?.try_astype(dtype)?,
1345    );
1346    let (start2, step) = if traditional { (1, 2) } else { (half, 1) };
1347    let x1 = last(&x4, 0, step, half)?;
1348    let x2 = last(&x4, start2, step, half)?;
1349    let (r1, r2) = if forward {
1350        (
1351            subtract(multiply(&x1, &c)?, multiply(&x2, &s)?)?,
1352            add(multiply(&x1, &s)?, multiply(&x2, &c)?)?,
1353        )
1354    } else {
1355        (
1356            add(multiply(&x2, &s)?, multiply(&x1, &c)?)?,
1357            subtract(multiply(&x2, &c)?, multiply(&x1, &s)?)?,
1358        )
1359    };
1360    let full = |r: Array| broadcast_to(&r, x1.dims());
1361    let out = update_last(&x4, &full(r1)?, 0, step)?;
1362    let out = update_last(&out, &full(r2)?, start2, step)?;
1363    reshape(&out, &shape)
1364}
1365
1366/// The rotation of [`norm_rope`](crate::fast::norm_rope): [`rope`](crate::fast::rope)'s parameters.
1367#[derive(Debug, Clone, Copy)]
1368pub struct Rotation<'a> {
1369    /// Rotated features (even, at most the head size).
1370    pub dims: i32,
1371    /// Rotate interleaved pairs instead of the two halves.
1372    pub traditional: bool,
1373    /// The frequency base (exactly one of `base` and `freqs`).
1374    pub base: Option<f32>,
1375    /// The position scale.
1376    pub scale: f32,
1377    /// Per-pair frequencies (`[dims / 2]`).
1378    pub freqs: Option<&'a Array>,
1379}
1380
1381/// The largest head size [`norm_rope`]'s kernel takes.
1382const NORM_ROPE_MAX: usize = 1024;
1383
1384/// The body of [`crate::fast::norm_rope`].
1385pub(crate) fn norm_rope(
1386    x: &Array,
1387    norm: Option<(&Array, f32)>,
1388    rotation: Rotation<'_>,
1389    offset: usize,
1390    cache: Option<&Array>,
1391) -> Result<Array> {
1392    let rdims = usize::try_from(rotation.dims).map_err(|_| {
1393        Error::InvalidArgument(format!(
1394            "norm_rope: dims must not be negative, got {}",
1395            rotation.dims
1396        ))
1397    })?;
1398    let mut inputs = Vec::with_capacity(4);
1399    let mut fused = x.ndim() == 4
1400        && x.dtype().category() == Category::Float
1401        && x.dims()[3] <= NORM_ROPE_MAX
1402        && offset + x.dims()[2] <= i32::MAX as usize;
1403    if let Some(c) = cache {
1404        let s = x.dims();
1405        let fits = c.ndim() == 4
1406            && c.dims()[..2] == s[..2]
1407            && c.dims()[3] == s[3]
1408            && c.dims()[2] >= offset + s[2];
1409        if !fits {
1410            return Err(Error::InvalidArgument(format!(
1411                "norm_rope: a cache of shape {:?} cannot take {:?} at position {offset}",
1412                c.dims(),
1413                s
1414            )));
1415        }
1416        fused &= c.dtype() == x.dtype();
1417        inputs.push(c.clone());
1418    }
1419    if let Some((w, _)) = norm {
1420        fused &= w.dtype() == x.dtype() && w.ndim() == 1 && w.size() == x.dims()[x.ndim() - 1];
1421    }
1422    let freqs_valid = rotation
1423        .freqs
1424        .is_none_or(|f| f.slot_error().is_none() && f.ndim() == 1 && f.size() == rdims / 2);
1425    let dims_valid =
1426        rdims > 0 && rdims.is_multiple_of(2) && x.ndim() > 0 && rdims <= x.dims()[x.ndim() - 1];
1427    if !fused || !freqs_valid || !dims_valid || rotation.base.is_some() == rotation.freqs.is_some()
1428    {
1429        // The composition (which also reports invalid arguments).
1430        return norm_rope_graph(x, norm, rotation, offset, cache);
1431    }
1432    inputs.push(x.clone());
1433    inputs.push(match norm {
1434        Some((w, _)) => w.clone(),
1435        None => scalar(1.0, x.dtype())?,
1436    });
1437    if let Some(f) = rotation.freqs {
1438        inputs.push(astype(f, DType::F32)?);
1439    }
1440    let op = FastOp::NormRope {
1441        eps: norm.map(|(_, eps)| eps.to_bits()),
1442        dims: rdims,
1443        traditional: rotation.traditional,
1444        base: rotation.base.unwrap_or(1.0).to_bits(),
1445        scale: rotation.scale.to_bits(),
1446        offset,
1447        cache: cache.is_some(),
1448    };
1449    one(op, inputs)
1450}
1451
1452/// [`norm_rope`] composed of [`rms_norm`], [`rope`] and [`crate::slice_update`].
1453fn norm_rope_graph(
1454    x: &Array,
1455    norm: Option<(&Array, f32)>,
1456    rotation: Rotation<'_>,
1457    offset: usize,
1458    cache: Option<&Array>,
1459) -> Result<Array> {
1460    let rdims = usize::try_from(rotation.dims).map_err(|_| {
1461        Error::InvalidArgument(format!(
1462            "norm_rope: dims must not be negative, got {}",
1463            rotation.dims
1464        ))
1465    })?;
1466    let x = match norm {
1467        Some((w, eps)) => rms_norm(x, Some(w), eps)?,
1468        None => x.clone(),
1469    };
1470    let r = rope(
1471        &x,
1472        rdims,
1473        rotation.traditional,
1474        rotation.base,
1475        rotation.scale,
1476        &Array::from(offset as i32),
1477        rotation.freqs,
1478    )?;
1479    match cache {
1480        Some(c) => {
1481            let mut region = vec![SliceArg::full(); 4];
1482            region[2] = SliceArg::new(
1483                Some(offset as isize),
1484                Some((offset + r.dims()[2]) as isize),
1485                1,
1486            );
1487            crate::ops::imp::slice_update(c, &r, &region)
1488        }
1489        None => Ok(r),
1490    }
1491}
1492
1493/// The body of [`crate::fast::paged_attention`].
1494pub(crate) fn paged_attention(
1495    q: &Array,
1496    k_cache: &Array,
1497    v_cache: &Array,
1498    block_tables: &Array,
1499    lengths: &Array,
1500    scale: f32,
1501) -> Result<Array> {
1502    let invalid = |msg: String| Err(Error::InvalidArgument(format!("paged_attention: {msg}")));
1503    floating("paged_attention", q.dtype())?;
1504    if q.ndim() != 4 || k_cache.ndim() != 4 || k_cache.dims() != v_cache.dims() {
1505        return invalid(format!(
1506            "q must be [B, Hq, L, D] and the caches [blocks, Hkv, block_size, D], got {:?}, \
1507             {:?}, {:?}",
1508            q.dims(),
1509            k_cache.dims(),
1510            v_cache.dims()
1511        ));
1512    }
1513    let (b, hq, d) = (q.dims()[0], q.dims()[1], q.dims()[3]);
1514    let hkv = k_cache.dims()[1];
1515    if k_cache.dims()[3] != d || hkv == 0 || hq % hkv != 0 {
1516        return invalid(format!(
1517            "q's heads ({hq}) must be a multiple of the caches' ({hkv}), with head size {d}"
1518        ));
1519    }
1520    let integer = |a: &Array| matches!(a.dtype().category(), Category::Signed | Category::Unsigned);
1521    if block_tables.ndim() != 2 || block_tables.dims()[0] != b || !integer(block_tables) {
1522        return invalid(format!(
1523            "block_tables must be [{b}, max_blocks] integers, got {} {:?}",
1524            block_tables.dtype(),
1525            block_tables.dims()
1526        ));
1527    }
1528    if lengths.dims() != [b] || !integer(lengths) {
1529        return invalid(format!(
1530            "lengths must be [{b}] integers, got {} {:?}",
1531            lengths.dtype(),
1532            lengths.dims()
1533        ));
1534    }
1535    if k_cache.dims()[0] == 0 {
1536        return invalid("the caches hold no blocks".into());
1537    }
1538    let dtype = q.dtype();
1539    let inputs = vec![
1540        q.clone(),
1541        astype(k_cache, dtype)?,
1542        astype(v_cache, dtype)?,
1543        astype(block_tables, DType::I32)?,
1544        astype(lengths, DType::I32)?,
1545    ];
1546    let op = FastOp::PagedAttention {
1547        scale: scale.to_bits(),
1548    };
1549    // The kernels use 32-bit offsets into the caches.
1550    let small = k_cache.size() < u32::MAX as usize;
1551    if matches!(d, 64 | 128 | 256) && k_cache.dims()[2].is_power_of_two() && small {
1552        one(op, inputs)
1553    } else {
1554        Ok(op.fallback(&inputs)?.swap_remove(0))
1555    }
1556}
1557
1558/// The body of [`crate::fast::paged_write`].
1559pub(crate) fn paged_write(cache: &Array, new: &Array, slots: &Array) -> Result<Array> {
1560    let invalid = |msg: String| Err(Error::InvalidArgument(format!("paged_write: {msg}")));
1561    let fits = cache.ndim() == 4
1562        && new.ndim() == 3
1563        && new.dims()[1] == cache.dims()[1]
1564        && new.dims()[2] == cache.dims()[3];
1565    if !fits || slots.dims() != [new.dims().first().copied().unwrap_or(0)] {
1566        return invalid(format!(
1567            "cache must be [blocks, Hkv, block_size, D], new [T, Hkv, D] and slots [T], got \
1568             {:?}, {:?}, {:?}",
1569            cache.dims(),
1570            new.dims(),
1571            slots.dims()
1572        ));
1573    }
1574    if !matches!(
1575        slots.dtype().category(),
1576        Category::Signed | Category::Unsigned
1577    ) {
1578        return invalid(format!("slots must be integers, got {}", slots.dtype()));
1579    }
1580    let inputs = vec![
1581        cache.clone(),
1582        astype(new, cache.dtype())?,
1583        astype(slots, DType::I32)?,
1584    ];
1585    one(FastOp::PagedWrite, inputs)
1586}
1587
1588/// The keys of every listed block per sequence, `[B, Hkv, max_blocks · block_size, D]`.
1589/// Block ids are clamped to `0..blocks`, as the kernels do.
1590fn paged_keys(cache: &Array, tables: &Array) -> Result<Array> {
1591    let (b, max_blocks) = (tables.dims()[0], tables.dims()[1]);
1592    let (blocks, hkv, bs, d) = (
1593        cache.dims()[0],
1594        cache.dims()[1],
1595        cache.dims()[2],
1596        cache.dims()[3],
1597    );
1598    let last = i32::try_from(blocks.saturating_sub(1)).unwrap_or(i32::MAX);
1599    let ids = ops::minimum(ops::maximum(reshape(tables, &[-1])?, 0i32)?, last)?;
1600    let gathered = crate::indexing::take(cache, &ids, Some(0))?; // [B·M, Hkv, bs, D]
1601    let gathered = reshape(
1602        &gathered,
1603        &[
1604            b as isize,
1605            max_blocks as isize,
1606            hkv as isize,
1607            bs as isize,
1608            d as isize,
1609        ],
1610    )?;
1611    let gathered = crate::ops::imp::transpose(&gathered, &[0, 2, 1, 3, 4])?;
1612    reshape(
1613        &gathered,
1614        &[
1615            b as isize,
1616            hkv as isize,
1617            (max_blocks * bs) as isize,
1618            d as isize,
1619        ],
1620    )
1621}
1622
1623/// [`paged_attention`] from ordinary operations: every listed block gathered, then
1624/// attention with a mask of each sequence's positions.
1625fn paged_attention_graph(inputs: &[Array], scale: f32) -> Result<Array> {
1626    let [q, k_cache, v_cache, tables, lengths] = inputs else {
1627        unreachable!("five inputs")
1628    };
1629    let (b, lq) = (q.dims()[0], q.dims()[2]);
1630    let (k, v) = (paged_keys(k_cache, tables)?, paged_keys(v_cache, tables)?);
1631    let s = k.dims()[2];
1632    // Lengths past the capacity see every listed position (clamped first, so that the
1633    // arithmetic below cannot wrap).
1634    let capacity = i32::try_from(s).unwrap_or(i32::MAX);
1635    let lengths = &ops::minimum(ops::maximum(lengths, 0i32)?, capacity)?;
1636    // Query `l` sees positions up to `lengths[b] − L + l`.
1637    let positions = reshape(
1638        &arange(0.0, s as f64, 1.0, DType::I32)?,
1639        &[1, 1, 1, s as isize],
1640    )?;
1641    let queries = reshape(
1642        &arange(0.0, lq as f64, 1.0, DType::I32)?,
1643        &[1, 1, lq as isize, 1],
1644    )?;
1645    let end = add(
1646        reshape(lengths, &[b as isize, 1, 1, 1])?,
1647        subtract(&queries, (lq - 1) as f64)?,
1648    )?;
1649    let mask = less(&positions, &end)?;
1650    let options = crate::fast::AttentionOptions::default().mask(&mask);
1651    scaled_dot_product_attention(q, &k, &v, scale, &options)
1652}
1653
1654/// [`paged_write`] from ordinary operations: the cache as one row per slot, the rows of
1655/// `slots` replaced. Out-of-range slots write a scratch row past the end, dropped after.
1656fn paged_write_graph(inputs: &[Array]) -> Result<Array> {
1657    let [cache, new, slots] = inputs else {
1658        unreachable!("three inputs")
1659    };
1660    let (blocks, hkv, bs, d) = (
1661        cache.dims()[0],
1662        cache.dims()[1],
1663        cache.dims()[2],
1664        cache.dims()[3],
1665    );
1666    let rows = reshape(
1667        &crate::ops::imp::transpose(cache, &[0, 2, 1, 3])?,
1668        &[(blocks * bs) as isize, hkv as isize, d as isize],
1669    )?;
1670    let end = i32::try_from(blocks * bs)
1671        .map_err(|_| Error::InvalidArgument("paged_write: more than 2^31 slots".into()))?;
1672    let slots = where_(
1673        less(slots, 0i32)?,
1674        end,
1675        where_(less(slots, end)?, slots, end)?,
1676    )?;
1677    let scratch = zeros(&[1, hkv, d], rows.dtype())?;
1678    let rows = crate::indexing::concatenate(&[rows, scratch], 0)?;
1679    let at = broadcast_to(&reshape(&slots, &[-1, 1, 1])?, new.dims())?;
1680    let rows = crate::indexing::put_along_axis(&rows, &at, new, 0)?;
1681    let rows = slice(
1682        &rows,
1683        &[
1684            SliceArg::new(Some(0), Some(end as isize), 1),
1685            SliceArg::full(),
1686            SliceArg::full(),
1687        ],
1688    )?;
1689    let rows = reshape(
1690        &rows,
1691        &[blocks as isize, bs as isize, hkv as isize, d as isize],
1692    )?;
1693    crate::ops::imp::transpose(&rows, &[0, 2, 1, 3])
1694}
1695
1696/// The routing key of an attention call from its shapes, dtype and options, validated as
1697/// the op validates them (the mask's and the sinks' shapes and dtypes).
1698pub(crate) fn attention_key(
1699    qs: &[usize],
1700    ks: &[usize],
1701    vs: &[usize],
1702    dtype: DType,
1703    options: &crate::fast::AttentionOptions<'_>,
1704    target: Target,
1705) -> Result<RouteKey> {
1706    let invalid = |msg: String| {
1707        Err(Error::InvalidArgument(format!(
1708            "scaled_dot_product_attention: {msg}"
1709        )))
1710    };
1711    if [qs, ks, vs].iter().any(|s| s.len() != 4) {
1712        return invalid(format!(
1713            "q, k and v must be 4-D, got {qs:?}, {ks:?} and {vs:?}"
1714        ));
1715    }
1716    if ks[0] != qs[0] || vs[0] != qs[0] || qs[3] != ks[3] || ks[1] != vs[1] || ks[2] != vs[2] {
1717        return invalid(format!("incompatible shapes {qs:?}, {ks:?} and {vs:?}"));
1718    }
1719    if ks[1] == 0 || !qs[1].is_multiple_of(ks[1]) {
1720        return invalid(format!(
1721            "the query heads ({}) must be a multiple of the key heads ({})",
1722            qs[1], ks[1]
1723        ));
1724    }
1725    floating("scaled_dot_product_attention", dtype)?;
1726    let (mask, bool_mask) = match options.mask {
1727        AttentionMask::None => (MaskKind::None, false),
1728        AttentionMask::Causal => (MaskKind::Causal, false),
1729        AttentionMask::Array(m) => {
1730            if m.ndim() > 4 {
1731                return invalid(format!(
1732                    "the mask must have at most 4 dimensions, got {:?}",
1733                    m.dims()
1734                ));
1735            }
1736            if m.dtype() != DType::Bool && crate::promote_types(m.dtype(), dtype) != dtype {
1737                return invalid(format!("a {} mask does not promote to {dtype}", m.dtype()));
1738            }
1739            let full = [qs[0], qs[1], qs[2], ks[2]];
1740            let fits = m
1741                .dims()
1742                .iter()
1743                .rev()
1744                .zip(full.iter().rev())
1745                .all(|(&a, &b)| a == b || a == 1);
1746            if !fits {
1747                return invalid(format!(
1748                    "the mask {:?} does not broadcast to {full:?}",
1749                    m.dims()
1750                ));
1751            }
1752            (MaskKind::Array, m.dtype() == DType::Bool)
1753        }
1754    };
1755    if let Some(s) = options.sinks
1756        && (s.ndim() != 1 || s.size() != qs[1] || crate::promote_types(s.dtype(), dtype) != dtype)
1757    {
1758        return invalid(format!(
1759            "sinks must be [{}] of a type promoting to {dtype}",
1760            qs[1]
1761        ));
1762    }
1763    let window = match options.window {
1764        Some(w) if w < 1 => return invalid(format!("the window must be at least 1, got {w}")),
1765        Some(w) => Some(w as usize),
1766        None => None,
1767    };
1768    Ok(RouteKey {
1769        b: qs[0],
1770        hq: qs[1],
1771        hkv: ks[1],
1772        lq: qs[2],
1773        lk: ks[2],
1774        d: qs[3],
1775        dv: vs[3],
1776        dtype,
1777        mask,
1778        bool_mask,
1779        sinks: options.sinks.is_some(),
1780        window,
1781        cap: options.max_score_bytes,
1782        target,
1783    })
1784}
1785
1786/// Where attention over `dtype` runs on the current stream.
1787fn attention_target(dtype: DType, size: usize) -> Target {
1788    if on_gpu(dtype, size.max(1)) {
1789        Target::Gpu {
1790            class: crate::gpu::class(),
1791        }
1792    } else {
1793        Target::Cpu
1794    }
1795}
1796
1797/// The body of [`crate::fast::plan_attention`].
1798pub(crate) fn plan_attention(
1799    q: &[i32],
1800    k: &[i32],
1801    v: &[i32],
1802    dtype: DType,
1803    options: &crate::fast::AttentionOptions<'_>,
1804) -> Result<crate::fast::AttentionPlan> {
1805    let dims = |s: &[i32]| -> Result<Vec<usize>> {
1806        s.iter()
1807            .map(|&d| {
1808                usize::try_from(d).map_err(|_| {
1809                    Error::InvalidArgument(format!(
1810                        "plan_attention: dimensions must not be negative, got {s:?}"
1811                    ))
1812                })
1813            })
1814            .collect()
1815    };
1816    let (qs, ks, vs) = (dims(q)?, dims(k)?, dims(v)?);
1817    let size = qs.iter().product();
1818    let key = attention_key(&qs, &ks, &vs, dtype, options, attention_target(dtype, size))?;
1819    let plan = plan::plan(&key)?;
1820    let as_i32 = |n: usize| i32::try_from(n).unwrap_or(i32::MAX);
1821    let kernel = match &plan.route {
1822        Route::Vector(groups) => {
1823            let (mut passes, mut blocks) = (1u8, 1usize);
1824            for g in groups {
1825                if let GroupKind::Vector {
1826                    passes: p,
1827                    blocks: b,
1828                    ..
1829                } = g.kind
1830                {
1831                    passes = passes.max(p);
1832                    blocks = blocks.max(b);
1833                }
1834            }
1835            crate::fast::AttentionKernel::Vector {
1836                passes,
1837                blocks: as_i32(blocks),
1838                dispatches: as_i32(groups.len()),
1839            }
1840        }
1841        Route::Tiled => crate::fast::AttentionKernel::Tiled,
1842        Route::Composed { rows } if *rows < key.lq => {
1843            crate::fast::AttentionKernel::ComposedBlocks {
1844                rows: as_i32(*rows),
1845            }
1846        }
1847        Route::Composed { .. } => crate::fast::AttentionKernel::Composed,
1848    };
1849    Ok(crate::fast::AttentionPlan {
1850        kernel,
1851        scratch_bytes: plan.scratch,
1852        max_intermediate_bytes: plan.max_intermediate,
1853        capped: plan.capped,
1854    })
1855}
1856
1857/// The body of [`crate::fast::scaled_dot_product_attention`]: validates, plans, and runs
1858/// the plan.
1859pub(crate) fn scaled_dot_product_attention(
1860    q: &Array,
1861    k: &Array,
1862    v: &Array,
1863    scale: f32,
1864    options: &crate::fast::AttentionOptions<'_>,
1865) -> Result<Array> {
1866    if [q, k, v].iter().any(|a| a.ndim() != 4) {
1867        return Err(Error::InvalidArgument(format!(
1868            "scaled_dot_product_attention: q, k and v must be 4-D, got {:?}, {:?} and {:?}",
1869            q.dims(),
1870            k.dims(),
1871            v.dims()
1872        )));
1873    }
1874    let dtype = crate::promote_types(crate::promote_types(q.dtype(), k.dtype()), v.dtype());
1875    let key = attention_key(
1876        q.dims(),
1877        k.dims(),
1878        v.dims(),
1879        dtype,
1880        options,
1881        attention_target(dtype, q.size()),
1882    )?;
1883    let plan = plan::plan(&key)?;
1884    let (qs, ks) = (q.dims(), k.dims());
1885    let (q, k, v) = (astype(q, dtype)?, astype(k, dtype)?, astype(v, dtype)?);
1886    let mask = match options.mask {
1887        AttentionMask::Array(m) => {
1888            let m = if m.dtype() == DType::Bool {
1889                m.clone()
1890            } else {
1891                astype(m, dtype)?
1892            };
1893            Some(broadcast_to(&m, &[qs[0], qs[1], qs[2], ks[2]])?)
1894        }
1895        _ => None,
1896    };
1897    let sinks = match options.sinks {
1898        Some(s) => Some(astype(s, dtype)?),
1899        None => None,
1900    };
1901    let call = Call {
1902        key: &key,
1903        scale,
1904        q: &q,
1905        k: &k,
1906        v: &v,
1907        mask: mask.as_ref(),
1908        sinks: sinks.as_ref(),
1909    };
1910    match &plan.route {
1911        Route::Tiled => call.fused(0, key.lq, key.lk, Kernel::Tiled),
1912        Route::Vector(groups) => {
1913            if let [g] = groups.as_slice()
1914                && g.start == 0
1915                && g.end == key.lq
1916                && g.keys == key.lk
1917            {
1918                return call.group(g);
1919            }
1920            let outs = groups
1921                .iter()
1922                .map(|g| call.group(g))
1923                .collect::<Result<Vec<_>>>()?;
1924            crate::indexing::concatenate(&outs, 2)
1925        }
1926        Route::Composed { rows } => {
1927            let rows = (*rows).max(1);
1928            let mut outs: Vec<Array> = Vec::with_capacity(key.lq.div_ceil(rows));
1929            let mut start = 0;
1930            while start < key.lq {
1931                let end = (start + rows).min(key.lq);
1932                // Each block's queries wait for the previous block's output, so one
1933                // block's scores are live at a time.
1934                let after = outs.last().cloned();
1935                outs.push(call.composed(start, end, after)?);
1936                start = end;
1937            }
1938            if outs.len() == 1 {
1939                Ok(outs.pop().expect("one block"))
1940            } else {
1941                crate::indexing::concatenate(&outs, 2)
1942            }
1943        }
1944    }
1945}
1946
1947/// A kernel choice for [`Call::fused`].
1948#[derive(Clone, Copy)]
1949enum Kernel {
1950    Tiled,
1951    Vector {
1952        passes: u8,
1953        blocks: usize,
1954        gqa_kernel: bool,
1955        tk1: bool,
1956    },
1957}
1958
1959/// One validated attention call, ready to run parts of.
1960struct Call<'a> {
1961    key: &'a RouteKey,
1962    scale: f32,
1963    q: &'a Array,
1964    k: &'a Array,
1965    v: &'a Array,
1966    /// Broadcast to `[B, Hq, Lq, Lk]`.
1967    mask: Option<&'a Array>,
1968    sinks: Option<&'a Array>,
1969}
1970
1971impl Call<'_> {
1972    /// Queries `rows`, keys `keys` and the mask's matching part.
1973    fn slices(
1974        &self,
1975        rows: (usize, usize),
1976        keys: (usize, usize),
1977    ) -> Result<(Array, Array, Array, Option<Array>)> {
1978        let range = |(a, b): (usize, usize)| SliceArg::new(Some(a as isize), Some(b as isize), 1);
1979        let whole_q = rows == (0, self.key.lq);
1980        let whole_k = keys == (0, self.key.lk);
1981        let full = SliceArg::full;
1982        let q = match whole_q {
1983            true => self.q.clone(),
1984            false => slice(self.q, &[full(), full(), range(rows), full()])?,
1985        };
1986        let (k, v) = match whole_k {
1987            true => (self.k.clone(), self.v.clone()),
1988            false => (
1989                slice(self.k, &[full(), full(), range(keys), full()])?,
1990                slice(self.v, &[full(), full(), range(keys), full()])?,
1991            ),
1992        };
1993        let mask = match self.mask {
1994            Some(m) if !(whole_q && whole_k) => {
1995                Some(slice(m, &[full(), full(), range(rows), range(keys)])?)
1996            }
1997            other => other.cloned(),
1998        };
1999        Ok((q, k, v, mask))
2000    }
2001
2002    /// The fused kernel over query rows `start..start + rows` and the first `keys` keys
2003    /// (`rows`, `keys` the whole call when `start == 0` and they are `Lq`, `Lk`).
2004    fn fused(&self, start: usize, end: usize, keys: usize, kernel: Kernel) -> Result<Array> {
2005        let (q, k, v, mask) = self.slices((start, end), (0, keys))?;
2006        let (passes, blocks, gqa_kernel, tk1) = match kernel {
2007            Kernel::Tiled => (0, 1, false, false),
2008            Kernel::Vector {
2009                passes,
2010                blocks,
2011                gqa_kernel,
2012                tk1,
2013            } => (passes, blocks, gqa_kernel, tk1),
2014        };
2015        let op = FastOp::Attention {
2016            scale: self.scale.to_bits(),
2017            causal: self.key.mask == MaskKind::Causal,
2018            mask: mask.is_some(),
2019            sinks: self.sinks.is_some(),
2020            passes,
2021            blocks: blocks as u32,
2022            gqa_kernel,
2023            tk1,
2024        };
2025        let mut inputs = vec![q, k, v];
2026        inputs.extend(mask);
2027        inputs.extend(self.sinks.cloned());
2028        Ok(Array::from_op(Primitive::Fast(op), inputs)?.swap_remove(0))
2029    }
2030
2031    /// One dispatch group of the vector schedule.
2032    fn group(&self, g: &plan::Group) -> Result<Array> {
2033        match g.kind {
2034            GroupKind::Vector {
2035                passes,
2036                blocks,
2037                gqa_kernel,
2038                tk1,
2039            } => self.fused(
2040                g.start,
2041                g.end,
2042                g.keys,
2043                Kernel::Vector {
2044                    passes,
2045                    blocks,
2046                    gqa_kernel,
2047                    tk1,
2048                },
2049            ),
2050            GroupKind::Composed => {
2051                let (q, k, v, mask) = self.slices((g.start, g.end), (0, g.keys))?;
2052                let mut inputs = vec![q, k, v];
2053                let has_mask = mask.is_some();
2054                inputs.extend(mask);
2055                inputs.extend(self.sinks.cloned());
2056                attention_graph(
2057                    &inputs,
2058                    self.scale,
2059                    self.key.mask == MaskKind::Causal,
2060                    has_mask,
2061                    self.sinks.is_some(),
2062                    None,
2063                    None,
2064                )
2065            }
2066        }
2067    }
2068
2069    /// The composition over query rows `start..end`, with the keys those rows see (the
2070    /// causal prefix, cut to the window), evaluated after `after` when given.
2071    fn composed(&self, start: usize, end: usize, after: Option<Array>) -> Result<Array> {
2072        let key = self.key;
2073        let causal = key.mask == MaskKind::Causal;
2074        // Under a causal mask, row `i` sees keys up to `i + Lk − Lq`.
2075        let hi = match causal {
2076            true => (key.lk + end).saturating_sub(key.lq).clamp(1, key.lk),
2077            false => key.lk,
2078        };
2079        // The window of row `start` begins at key `start + Lk − Lq − window + 1`.
2080        let lo = match key.window {
2081            Some(w) => (start + key.lk).saturating_sub(key.lq + w - 1).min(hi - 1),
2082            None => 0,
2083        };
2084        let offset = (start + key.lk) as isize - key.lq as isize - lo as isize;
2085        let (mut q, k, v, mask) = self.slices((start, end), (lo, hi))?;
2086        if let Some(prev) = after {
2087            q = crate::primitive::depends(&q, &[prev])?;
2088        }
2089        let mut inputs = vec![q, k, v];
2090        let has_mask = mask.is_some();
2091        inputs.extend(mask);
2092        inputs.extend(self.sinks.cloned());
2093        attention_graph(
2094            &inputs,
2095            self.scale,
2096            causal,
2097            has_mask,
2098            self.sinks.is_some(),
2099            Some(offset),
2100            key.window,
2101        )
2102    }
2103}
2104
2105/// Whether a recorded attention node's kernel choice fits its inputs (the planner's
2106/// choices always do; this guards nodes built through `graph::from_primitive`).
2107fn attention_config(op: &FastOp, inputs: &[Array]) -> bool {
2108    let FastOp::Attention {
2109        causal,
2110        mask,
2111        sinks,
2112        passes,
2113        blocks,
2114        gqa_kernel,
2115        tk1,
2116        ..
2117    } = *op
2118    else {
2119        return false;
2120    };
2121    let (q, k, v) = (&inputs[0], &inputs[1], &inputs[2]);
2122    let (lq, lk, d, dv) = (q.dims()[2], k.dims()[2], q.dims()[3], v.dims()[3]);
2123    let gqa = q.dims()[1] / k.dims()[1];
2124    if lq == 0 || lk == 0 {
2125        return false;
2126    }
2127    match passes {
2128        0 => plan::tiled_head(d, dv) && (!causal || lq <= lk) && !gqa_kernel && !tk1,
2129        1 | 2 => {
2130            let vector = lq <= 8 && lq <= lk && plan::vector_head(d, dv) && (d != 512 || lq == 1);
2131            let two = passes == 1
2132                || ((1..=1024).contains(&blocks)
2133                    && 32 * gqa * lq <= 1024
2134                    && (!gqa_kernel
2135                        || (lq == 1
2136                            && !mask
2137                            && !sinks
2138                            && d == dv
2139                            && ((gqa == 8 && (d == 64 || d == 128))
2140                                || ((gqa == 12 || gqa == 16) && d == 128))))
2141                    && (!tk1 || (!gqa_kernel && plan::tk1_variant(q.dtype(), d, dv))));
2142            vector && two && (passes == 2 || (!gqa_kernel && !tk1))
2143        }
2144        _ => false,
2145    }
2146}
2147
2148/// MLX's attention fallback, with sinks folded into the softmax.
2149///
2150/// Under `causal`, query `i` sees keys up to `i + offset` (`offset` defaults to `Lk − Lq`,
2151/// the queries being the last positions), and with a `window` only the `window` keys up
2152/// to that one.
2153fn attention_graph(
2154    inputs: &[Array],
2155    scale: f32,
2156    causal: bool,
2157    has_mask: bool,
2158    has_sinks: bool,
2159    offset: Option<isize>,
2160    window: Option<usize>,
2161) -> Result<Array> {
2162    let (q, k, v) = (&inputs[0], &inputs[1], &inputs[2]);
2163    let dtype = q.dtype();
2164    let (b, hq, lq) = (q.dims()[0], q.dims()[1], q.dims()[2]);
2165    let (hkv, lk) = (k.dims()[1], k.dims()[2]);
2166    let rep = hq / hkv;
2167    let i = |n: usize| n as isize;
2168    let mut q = multiply(q, f64::from(scale))?;
2169    let (mut k, mut v) = (k.clone(), v.clone());
2170    if rep > 1 {
2171        q = reshape(&q, &[i(b), i(hkv), i(rep), i(lq), i(q.dims()[3])])?;
2172        k = expand_dims(&k, 2)?;
2173        v = expand_dims(&v, 2)?;
2174    }
2175    let n = k.ndim();
2176    let mut axes: Vec<usize> = (0..n).collect();
2177    axes.swap(n - 2, n - 1);
2178    let mut scores = crate::ops::imp::matmul(&q, &crate::ops::imp::transpose(&k, &axes)?)?;
2179    let mask = if causal {
2180        let offset = offset.unwrap_or(lk as isize - lq as isize) as f64;
2181        let qi = expand_dims(&arange(offset, offset + lq as f64, 1.0, DType::I32)?, 1)?;
2182        let ki = expand_dims(&arange(0.0, lk as f64, 1.0, DType::I32)?, 0)?;
2183        let seen = crate::ops::imp::greater_equal(&qi, &ki)?;
2184        Some(match window {
2185            // Key `j` is in the window of query position `p` when `j > p − window`.
2186            Some(w) => multiply(
2187                &seen,
2188                &crate::ops::imp::greater(&ki, &subtract(&qi, w as f64)?)?,
2189            )?,
2190            None => seen,
2191        })
2192    } else if has_mask {
2193        let m = &inputs[3];
2194        Some(if rep > 1 {
2195            reshape(m, &[i(b), i(hkv), i(rep), i(lq), i(lk)])?
2196        } else {
2197            m.clone()
2198        })
2199    } else {
2200        None
2201    };
2202    if let Some(m) = mask {
2203        scores = if m.dtype() == DType::Bool {
2204            let lowest = match dtype {
2205                DType::F16 => f64::from(crate::f16::MIN),
2206                DType::BF16 => f64::from(crate::bf16::MIN),
2207                DType::F64 => f64::MIN,
2208                _ => f64::from(f32::MIN),
2209            };
2210            where_(&m, &scores, lowest)?
2211        } else {
2212            add(&scores, &m)?
2213        };
2214    }
2215    let probs = if has_sinks {
2216        // softmax over [sink, scores...], without the sink's column.
2217        let s = astype(&scores, DType::F32)?;
2218        let sink = astype(inputs.last().expect("sinks"), DType::F32)?;
2219        let shape: Vec<isize> = if rep > 1 {
2220            vec![1, i(hkv), i(rep), 1, 1]
2221        } else {
2222            vec![1, i(hq), 1, 1]
2223        };
2224        let sink = reshape(&sink, &shape)?;
2225        let m = stop_gradient(&crate::ops::imp::maximum(&max(&s, &[-1], true)?, &sink)?)?;
2226        let e = exp(&subtract(&s, &m)?)?;
2227        let denom = add(sum(&e, &[-1], true)?, exp(&subtract(&sink, &m)?)?)?;
2228        astype(&divide(&e, &denom)?, dtype)?
2229    } else if on_gpu(dtype, scores.size()) {
2230        crate::ops::imp::softmax(&scores, &[-1])?
2231    } else {
2232        softmax_graph(&scores)?
2233    };
2234    let out = crate::ops::imp::matmul(&probs, &v)?;
2235    if rep > 1 {
2236        reshape(&out, &[i(b), i(hq), i(lq), i(v.dims()[n - 1])])
2237    } else {
2238        Ok(out)
2239    }
2240}
2241
2242/// The body of [`crate::fast::gated_delta`].
2243pub(crate) fn gated_delta(
2244    q: &Array,
2245    k: &Array,
2246    v: &Array,
2247    g: &Array,
2248    beta: &Array,
2249    state: &Array,
2250) -> Result<(Array, Array)> {
2251    gated_delta_op(q, k, v, g, beta, state, false)
2252}
2253
2254/// The body of [`crate::fast::gated_delta_steps`].
2255pub(crate) fn gated_delta_steps(
2256    q: &Array,
2257    k: &Array,
2258    v: &Array,
2259    g: &Array,
2260    beta: &Array,
2261    state: &Array,
2262) -> Result<(Array, Array)> {
2263    gated_delta_op(q, k, v, g, beta, state, true)
2264}
2265
2266/// The body of [`crate::fast::gated_delta_slots`].
2267pub(crate) fn gated_delta_slots(
2268    q: &Array,
2269    k: &Array,
2270    v: &Array,
2271    g: &Array,
2272    beta: &Array,
2273    pool: &Array,
2274    slots: &Array,
2275) -> Result<(Array, Array)> {
2276    let b = q.dims().first().copied().unwrap_or(0);
2277    let fits = pool.ndim() == 4
2278        && slots.dims() == [b]
2279        && matches!(
2280            slots.dtype().category(),
2281            Category::Signed | Category::Unsigned
2282        );
2283    if !fits {
2284        return Err(Error::InvalidArgument(format!(
2285            "gated_delta_slots: pool must be [slots, Hv, Dv, Dk] and slots [{b}] integers, got \
2286             {:?} and {} {:?}",
2287            pool.dims(),
2288            slots.dtype(),
2289            slots.dims()
2290        )));
2291    }
2292    // The checks of `gated_delta`, on one slot's state.
2293    let one = crate::indexing::take(pool, &reshape(&astype(slots, DType::I32)?, &[-1])?, Some(0))?;
2294    let _validated = gated_delta_op(q, k, v, g, beta, &one, false)?;
2295    let inputs = vec![
2296        q.clone(),
2297        k.clone(),
2298        v.clone(),
2299        g.clone(),
2300        beta.clone(),
2301        pool.clone(),
2302        astype(slots, DType::I32)?,
2303    ];
2304    let kernel = on_gpu(q.dtype(), q.size())
2305        && q.dtype() == v.dtype()
2306        && q.dims()[3].is_multiple_of(32)
2307        && [g, beta, pool]
2308            .iter()
2309            .all(|a| matches!(a.dtype(), DType::F16 | DType::BF16 | DType::F32));
2310    let op = FastOp::GatedDeltaSlots;
2311    let mut out = if kernel {
2312        Array::from_op(Primitive::Fast(op), inputs)?
2313    } else {
2314        op.fallback(&inputs)?
2315    };
2316    let pool = out.pop().expect("two outputs");
2317    Ok((out.pop().expect("two outputs"), pool))
2318}
2319
2320fn gated_delta_op(
2321    q: &Array,
2322    k: &Array,
2323    v: &Array,
2324    g: &Array,
2325    beta: &Array,
2326    state: &Array,
2327    steps: bool,
2328) -> Result<(Array, Array)> {
2329    let op = "gated_delta";
2330    for a in [q, k, v, g, beta, state] {
2331        floating(op, a.dtype())?;
2332    }
2333    let invalid = |msg: String| Err(Error::InvalidArgument(format!("{op}: {msg}")));
2334    if q.ndim() != 4 || q.dims() != k.dims() || v.ndim() != 4 {
2335        return invalid(format!(
2336            "q and k must be [B, T, Hk, Dk] and v [B, T, Hv, Dv], got {:?}, {:?}, {:?}",
2337            q.dims(),
2338            k.dims(),
2339            v.dims()
2340        ));
2341    }
2342    let (b, t, hk, dk) = (q.dims()[0], q.dims()[1], q.dims()[2], q.dims()[3]);
2343    let (hv, dv) = (v.dims()[2], v.dims()[3]);
2344    if v.dims()[..2] != [b, t] || hk == 0 || hv % hk != 0 {
2345        return invalid(format!(
2346            "v must be [{b}, {t}, Hv, Dv] with Hv a multiple of {hk}, got {:?}",
2347            v.dims()
2348        ));
2349    }
2350    if g.dims() != [b, t, hv] || beta.dims() != [b, t, hv] {
2351        return invalid(format!(
2352            "g and beta must be [{b}, {t}, {hv}], got {:?} and {:?}",
2353            g.dims(),
2354            beta.dims()
2355        ));
2356    }
2357    if state.dims() != [b, hv, dv, dk] {
2358        return invalid(format!(
2359            "state must be [{b}, {hv}, {dv}, {dk}], got {:?}",
2360            state.dims()
2361        ));
2362    }
2363    let inputs = vec![
2364        q.clone(),
2365        k.clone(),
2366        v.clone(),
2367        g.clone(),
2368        beta.clone(),
2369        state.clone(),
2370    ];
2371    let kernel = on_gpu(q.dtype(), q.size())
2372        && q.dtype() == v.dtype()
2373        && dk % 32 == 0
2374        && [g, beta, state]
2375            .iter()
2376            .all(|a| matches!(a.dtype(), DType::F16 | DType::BF16 | DType::F32));
2377    let op = FastOp::GatedDelta { steps };
2378    let mut out = if kernel {
2379        Array::from_op(Primitive::Fast(op), inputs)?
2380    } else {
2381        op.fallback(&inputs)?
2382    };
2383    let s = out.pop().expect("two outputs");
2384    Ok((out.pop().expect("two outputs"), s))
2385}
2386
2387/// The gated delta rule step by step from ordinary operations (mlx-lm's
2388/// `gated_delta_ops`).
2389fn gated_delta_graph(inputs: &[Array], steps: bool) -> Result<Vec<Array>> {
2390    let [q, k, v, g, beta, state] = inputs else {
2391        unreachable!("six inputs")
2392    };
2393    let (b, t, hk, dk) = (q.dims()[0], q.dims()[1], q.dims()[2], q.dims()[3]);
2394    let (hv, dv) = (v.dims()[2], v.dims()[3]);
2395    // Each key head serves `Hv / Hk` consecutive value heads.
2396    let heads = |x: &Array| -> Result<Array> {
2397        if hv == hk {
2398            return Ok(x.clone());
2399        }
2400        let wide = broadcast_to(&expand_dims(x, 3)?, &[b, t, hk, hv / hk, dk])?;
2401        reshape(&wide, &[b as isize, t as isize, hv as isize, dk as isize])
2402    };
2403    // In float32 at least, as the kernels.
2404    let wide = |x: &Array| match x.dtype() {
2405        DType::F64 => Ok(x.clone()),
2406        _ => astype(x, DType::F32),
2407    };
2408    let (qh, kh) = (heads(&wide(q)?)?, heads(&wide(k)?)?);
2409    let (v, g, beta) = (&wide(v)?, &wide(g)?, &wide(beta)?);
2410    let at = |x: &Array, i: usize| -> Result<Array> {
2411        let mut args = vec![SliceArg::full(); x.ndim()];
2412        args[1] = SliceArg::new(Some(i as isize), Some(i as isize + 1), 1);
2413        squeeze(&slice(x, &args)?, &[1])
2414    };
2415    let mut s = wide(state)?;
2416    let mut ys = Vec::with_capacity(t);
2417    let mut states = Vec::with_capacity(if steps { t } else { 0 });
2418    for i in 0..t {
2419        let (qt, kt, vt) = (at(&qh, i)?, expand_dims(&at(&kh, i)?, 2)?, at(v, i)?);
2420        let decay = reshape(&at(g, i)?, &[b as isize, hv as isize, 1, 1])?;
2421        s = multiply(&s, &decay)?;
2422        let kv_mem = sum(&multiply(&s, &kt)?, &[-1], false)?;
2423        let delta = multiply(subtract(&vt, &kv_mem)?, expand_dims(&at(beta, i)?, -1)?)?;
2424        s = add(&s, multiply(&kt, expand_dims(&delta, -1)?)?)?;
2425        let y = sum(&multiply(&s, expand_dims(&qt, 2)?)?, &[-1], false)?;
2426        ys.push(astype(&y, q.dtype())?);
2427        if steps {
2428            states.push(astype(&s, state.dtype())?);
2429        }
2430    }
2431    let y = match ys.is_empty() {
2432        true => zeros(&[b, 0, hv, dv], q.dtype())?,
2433        false => stack(&ys, 1)?,
2434    };
2435    let s = match (steps, states.is_empty()) {
2436        (false, _) => astype(&s, state.dtype())?,
2437        (true, false) => stack(&states, 1)?,
2438        (true, true) => zeros(&[b, 0, hv, dv, dk], state.dtype())?,
2439    };
2440    Ok(vec![y, s])
2441}
2442
2443/// The gathered quantized products from ordinary operations: the indexed batches of `x` and
2444/// of the weights, then one batched quantized product.
2445fn gather_qmm_graph(
2446    inputs: &[Array],
2447    group_size: usize,
2448    bits: u32,
2449    transpose: bool,
2450) -> Result<Array> {
2451    let (lhs, rhs) = (&inputs[4], &inputs[5]);
2452    let rows = |a: &Array, indices: &Array| -> Result<Array> {
2453        let nd = a.ndim();
2454        let flat = reshape(
2455            a,
2456            &[-1, a.dims()[nd - 2] as isize, a.dims()[nd - 1] as isize],
2457        )?;
2458        crate::indexing::take(&flat, indices, Some(0))
2459    };
2460    crate::quantized::quantized_matmul(
2461        &rows(&inputs[0], lhs)?,
2462        &rows(&inputs[1], rhs)?,
2463        &rows(&inputs[2], rhs)?,
2464        &rows(&inputs[3], rhs)?,
2465        transpose,
2466        group_size,
2467        bits,
2468    )
2469}