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