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