Skip to main content

nuvec_core/
fast.rs

1//! Fused operations with dedicated GPU kernels, as MLX's `mx.fast`: [`rms_norm`],
2//! [`layer_norm`], [`rope`], [`cross_entropy`], [`scaled_dot_product_attention`] and
3//! [`gated_delta`] (mlx-lm's Gated DeltaNet kernels); nuvec's [`add_rms_norm`] (a residual
4//! connection and its norm), [`norm_rope`] (norm, RoPE and a cache write) and
5//! [`paged_attention`] with [`paged_write`] (a block-table KV cache); and, through
6//! [`crate::softmax`] and [`crate::logsumexp`] over the last axis, MLX's softmax and
7//! log-sum-exp kernels.
8//!
9//! Each operation is one GPU kernel (MLX's, or nuvec's own) with an equivalent graph of ordinary
10//! operations, its *fallback*. As in MLX, the fallback runs on the CPU stream, and it
11//! defines the forward-mode derivative and batching; reverse mode uses dedicated kernels
12//! where MLX has them.
13//!
14//! ```
15//! use nuvec_core::{Array, Result, fast};
16//!
17//! # fn main() -> Result<()> {
18//! let x = Array::from_slice(&[1.0f32, 2.0, 3.0, 4.0], &[2, 2])?;
19//! let y = fast::rms_norm(&x, None, 1e-5)?; // x / sqrt(mean(x²) + eps), per row
20//! let v = y.to_vec::<f32>()?;
21//! assert!((v[0] - 1.0 / 2.5f32.sqrt()).abs() < 1e-5);
22//! # Ok(())
23//! # }
24//! ```
25
26use crate::cast::Canon;
27use crate::kernels::Operand;
28use crate::ops::{
29    self, add, arange, astype, broadcast_to, cos, divide, equal, exp, expand_dims, filled, less,
30    log, max, mean, multiply, reshape, sin, slice, sqrt, squeeze, stop_gradient, subtract, sum,
31    where_,
32};
33use crate::primitive::Primitive;
34use crate::{
35    Array, Category, DType, Device, DeviceKind, Error, Result, SliceArg, Stream, default_stream,
36    set_default_stream,
37};
38use crate::{stack, zeros};
39
40pub use crate::kernel::{KernelCall, KernelConfig, MetalKernel, TemplateArg};
41
42/// A fused operation (see the [module documentation](self)).
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44#[non_exhaustive]
45pub enum FastOp {
46    /// `(x, w)`: `x / sqrt(mean(x²) + eps) · w` over the last axis; `w` is 1-D or a scalar 1.
47    RmsNorm {
48        /// `f32::to_bits` of epsilon.
49        eps: u32,
50    },
51    /// `(x, r, w)`: `h = x + r` and [`FastOp::RmsNorm`] of `h` (see [`add_rms_norm`]).
52    AddRmsNorm {
53        /// `f32::to_bits` of epsilon.
54        eps: u32,
55    },
56    /// `(x, w, g)`: the gradients of [`FastOp::RmsNorm`] for `x` and `w`.
57    RmsNormVjp {
58        /// `f32::to_bits` of epsilon.
59        eps: u32,
60    },
61    /// `(x, w, b)`: `(x - mean) / sqrt(var + eps) · w + b` over the last axis; `w` and `b`
62    /// are 1-D or the scalars 1 and 0.
63    LayerNorm {
64        /// `f32::to_bits` of epsilon.
65        eps: u32,
66    },
67    /// `(x, w, b, g)`: the gradients of [`FastOp::LayerNorm`] for `x`, `w` and `b`.
68    LayerNormVjp {
69        /// `f32::to_bits` of epsilon.
70        eps: u32,
71    },
72    /// `(x, offset[, freqs])`: rotary position embeddings of the first `dims` features.
73    Rope {
74        /// Rotated features (even).
75        dims: usize,
76        /// Rotate interleaved pairs instead of the two halves.
77        traditional: bool,
78        /// `f32::to_bits` of the frequency base (unused with `freqs`).
79        base: u32,
80        /// `f32::to_bits` of the position scale.
81        scale: u32,
82        /// Rotate forwards (`false`: the inverse rotation, the gradient).
83        forward: bool,
84    },
85    /// `([cache,] x, w[, freqs])`: [`norm_rope`]: each `[b, h, l]` row of `x` RMS-normalised
86    /// (with `eps`) and rotated as [`FastOp::Rope`] at position `offset + l`; written into
87    /// `cache` at the same positions, or returned as a new `[B, H, L, D]` array.
88    NormRope {
89        /// `f32::to_bits` of epsilon, or no normalisation.
90        eps: Option<u32>,
91        /// Rotated features (even).
92        dims: usize,
93        /// Rotate interleaved pairs instead of the two halves.
94        traditional: bool,
95        /// `f32::to_bits` of the frequency base (unused with `freqs`).
96        base: u32,
97        /// `f32::to_bits` of the position scale.
98        scale: u32,
99        /// The position of the first row.
100        offset: usize,
101        /// Whether the first input is a cache written at the rows' positions.
102        cache: bool,
103    },
104    /// `(x, w)`: [`crate::conv1d`] with `groups` equal to the channels (depthwise).
105    DepthwiseConv1d {
106        /// Positions between outputs.
107        stride: usize,
108        /// Zeros before and after the input.
109        padding: usize,
110        /// Positions between taps.
111        dilation: usize,
112    },
113    /// `(q, k_cache, v_cache, block_tables, lengths)`: [`paged_attention`].
114    PagedAttention {
115        /// `f32::to_bits` of the score scale.
116        scale: u32,
117    },
118    /// `(cache, new, slots)`: [`paged_write`].
119    PagedWrite,
120    /// `(x)`: softmax over the last axis, accumulated in float32.
121    Softmax,
122    /// `(x)`: `log(sum(exp(x)))` over the last axis, which becomes size 1.
123    LogSumExp,
124    /// `(logits, targets)`: `logsumexp(logits) - logits[target]` per row, in float32.
125    CrossEntropy,
126    /// `(logits, targets, loss, g)`: the gradient of [`FastOp::CrossEntropy`] for the logits.
127    CrossEntropyVjp,
128    /// `(q, k, v[, mask][, sinks])`: scaled dot-product attention (see
129    /// [`scaled_dot_product_attention`]).
130    Attention {
131        /// `f32::to_bits` of the score scale.
132        scale: u32,
133        /// Causal masking (keys after the query's position, aligned at the end, are hidden).
134        causal: bool,
135        /// Whether an array mask (bool or additive, `[B, Hq, Lq, Lk]`) is the fourth input.
136        mask: bool,
137        /// Whether per-head attention sinks (`[Hq]`) are the last input.
138        sinks: bool,
139    },
140    /// `(q, k, v, g, beta, state)`: the gated delta rule (see [`gated_delta`]), with the
141    /// outputs and the final state (or every step's, [`gated_delta_steps`]).
142    GatedDelta {
143        /// The state after every step (`[B, T, Hv, Dv, Dk]`) instead of the final one.
144        steps: bool,
145    },
146    /// `(q, k, v, g, beta, pool, slots)`: [`gated_delta_slots`], with the outputs and the
147    /// updated pool.
148    GatedDeltaSlots,
149    /// `(a, b, lhs_indices, rhs_indices[, offsets])`: matrix products of gathered batches
150    /// (see [`crate::gather_mm`]), as [`FastOp::GatherQmm`] in full precision.
151    GatherMm,
152    /// `(x, w, scales, biases, lhs_indices, rhs_indices[, offsets])`: quantized matrix
153    /// products of gathered batches (see [`crate::gather_qmm`]); the indices are uint32
154    /// and broadcast to the output's batch shape. `offsets` (int32, one per batch of `w`)
155    /// marks where each weight batch's rows start when the rows are sorted by it.
156    GatherQmm {
157        /// Values per scale and bias.
158        group_size: usize,
159        /// Bits per value.
160        bits: u8,
161        /// `x · wᵀ` (else `x · w`).
162        transpose: bool,
163    },
164}
165
166/// The mask of [`scaled_dot_product_attention`].
167#[derive(Debug, Clone, Copy)]
168#[non_exhaustive]
169pub enum AttentionMask<'a> {
170    /// Every query attends to every key.
171    None,
172    /// Query `i` attends to keys `0..=i + Lk - Lq` (the queries are the last `Lq` positions).
173    Causal,
174    /// A boolean mask (true attends) or an additive mask, broadcastable to
175    /// `[B, Hq, Lq, Lk]`.
176    Array(&'a Array),
177}
178
179fn float(bits: u32) -> f32 {
180    f32::from_bits(bits)
181}
182
183fn check_inputs(inputs: &[&Array]) -> Result<()> {
184    match inputs.iter().find_map(|a| a.error()) {
185        Some(err) => Err(err),
186        None => Ok(()),
187    }
188}
189
190/// A 0-D array holding `value` in `dtype`.
191fn scalar(value: f64, dtype: DType) -> Result<Array> {
192    filled(&[], dtype, |_| Canon::Float(value))
193}
194
195/// Whether the kernels cover `dtype` on the current stream.
196pub(crate) fn on_gpu(dtype: DType, size: usize) -> bool {
197    default_stream().device.kind == DeviceKind::Gpu
198        && matches!(dtype, DType::F16 | DType::BF16 | DType::F32)
199        && size > 0
200        && size < u32::MAX as usize
201}
202
203/// Records `op` as one GPU kernel, or builds its fallback graph (CPU stream, other dtypes,
204/// empty arrays).
205pub(crate) fn record_or_fallback(op: FastOp, inputs: Vec<Array>) -> Result<Vec<Array>> {
206    if on_gpu(inputs[0].dtype(), inputs[0].size()) {
207        Array::from_op(Primitive::Fast(op), inputs)
208    } else {
209        op.fallback(&inputs)
210    }
211}
212
213fn one(op: FastOp, inputs: Vec<Array>) -> Result<Array> {
214    Ok(record_or_fallback(op, inputs)?.swap_remove(0))
215}
216
217fn floating(op: &'static str, dtype: DType) -> Result<()> {
218    match dtype.category() {
219        Category::Float => Ok(()),
220        _ => Err(Error::UnsupportedDType { op, dtype }),
221    }
222}
223
224/// A 1-D parameter of the last axis's size.
225fn check_param(op: &'static str, what: &str, p: &Array, x: &Array) -> Result<()> {
226    if p.ndim() != 1 || p.size() != x.shape()[x.ndim() - 1] {
227        return Err(Error::InvalidArgument(format!(
228            "{op}: {what} must be 1-D with {} elements, got shape {:?}",
229            x.shape()[x.ndim() - 1],
230            p.shape()
231        )));
232    }
233    Ok(())
234}
235
236/// Root-mean-square normalisation over the last axis, `x / sqrt(mean(x²) + eps) · weight`,
237/// as MLX's `fast.rms_norm`: statistics in float32, the result in the promoted type of `x`
238/// and `weight` (1-D, the last axis's size).
239pub fn rms_norm(x: &Array, weight: Option<&Array>, eps: f32) -> Result<Array> {
240    check_inputs(&[x])?;
241    if x.ndim() == 0 {
242        return Err(Error::InvalidArgument(
243            "rms_norm: the input needs at least one dimension".into(),
244        ));
245    }
246    let dtype = match weight {
247        Some(w) => {
248            check_inputs(&[w])?;
249            check_param("rms_norm", "weight", w, x)?;
250            crate::promote_types(x.dtype(), w.dtype())
251        }
252        None => x.dtype(),
253    };
254    floating("rms_norm", dtype)?;
255    let w = match weight {
256        Some(w) => astype(w, dtype)?,
257        None => scalar(1.0, dtype)?,
258    };
259    let op = FastOp::RmsNorm { eps: eps.to_bits() };
260    one(op, vec![astype(x, dtype)?, w])
261}
262
263/// A residual connection followed by RMS normalisation: `h = x + residual` and
264/// [`rms_norm`]`(h, weight, eps)`, returned as `(h, normed)`, from one GPU kernel. The
265/// results equal [`crate::add`] then [`rms_norm`] exactly; a weight that would widen the
266/// sum's dtype runs the two operations.
267pub fn add_rms_norm(
268    x: &Array,
269    residual: &Array,
270    weight: Option<&Array>,
271    eps: f32,
272) -> Result<(Array, Array)> {
273    check_inputs(&[x, residual])?;
274    let dtype = crate::promote_types(x.dtype(), residual.dtype());
275    let fused = x.shape() == residual.shape()
276        && x.ndim() > 0
277        && dtype.category() == Category::Float
278        && weight.is_none_or(|w| {
279            w.error().is_none()
280                && w.ndim() == 1
281                && w.size() == x.shape()[x.ndim() - 1]
282                && crate::promote_types(dtype, w.dtype()) == dtype
283        });
284    if !fused {
285        let h = add(x, residual)?;
286        let y = rms_norm(&h, weight, eps)?;
287        return Ok((h, y));
288    }
289    let w = match weight {
290        Some(w) => astype(w, dtype)?,
291        None => scalar(1.0, dtype)?,
292    };
293    let op = FastOp::AddRmsNorm { eps: eps.to_bits() };
294    let mut out = record_or_fallback(op, vec![astype(x, dtype)?, astype(residual, dtype)?, w])?;
295    let y = out.pop().expect("two outputs");
296    Ok((out.pop().expect("two outputs"), y))
297}
298
299/// Layer normalisation over the last axis, `(x - mean) / sqrt(var + eps) · weight + bias`,
300/// as MLX's `fast.layer_norm`: statistics in float32, the result in the promoted type.
301pub fn layer_norm(
302    x: &Array,
303    weight: Option<&Array>,
304    bias: Option<&Array>,
305    eps: f32,
306) -> Result<Array> {
307    check_inputs(&[x])?;
308    if x.ndim() == 0 {
309        return Err(Error::InvalidArgument(
310            "layer_norm: the input needs at least one dimension".into(),
311        ));
312    }
313    let mut dtype = x.dtype();
314    for (p, what) in [(weight, "weight"), (bias, "bias")] {
315        if let Some(p) = p {
316            check_inputs(&[p])?;
317            check_param("layer_norm", what, p, x)?;
318            dtype = crate::promote_types(dtype, p.dtype());
319        }
320    }
321    floating("layer_norm", dtype)?;
322    let w = match weight {
323        Some(w) => astype(w, dtype)?,
324        None => scalar(1.0, dtype)?,
325    };
326    let b = match bias {
327        Some(b) => astype(b, dtype)?,
328        None => scalar(0.0, dtype)?,
329    };
330    let op = FastOp::LayerNorm { eps: eps.to_bits() };
331    one(op, vec![astype(x, dtype)?, w, b])
332}
333
334/// Rotary position embeddings, as MLX's `fast.rope`, of `x` (at least 3-D: `[B, ...,
335/// T, D]`, positions along `T`): the first `dims` features (even) are rotated in pairs,
336/// the two halves (or interleaved pairs, `traditional`) by angle `(offset + t) · scale ·
337/// θᵢ`, with `θᵢ = base^(-2i/dims)` or `1 / freqs[i]`. Exactly one of `base` and `freqs`
338/// is given. `offset` is an integer scalar or one per batch entry.
339pub fn rope(
340    x: &Array,
341    dims: usize,
342    traditional: bool,
343    base: Option<f32>,
344    scale: f32,
345    offset: &Array,
346    freqs: Option<&Array>,
347) -> Result<Array> {
348    check_inputs(&[x, offset])?;
349    let invalid = |msg: String| Err(Error::InvalidArgument(format!("rope: {msg}")));
350    if x.ndim() < 3 {
351        return invalid(format!(
352            "the input needs at least 3 dimensions, got {:?}",
353            x.shape()
354        ));
355    }
356    floating("rope", x.dtype())?;
357    if offset.ndim() > 1 || (offset.size() != 1 && offset.size() != x.shape()[0]) {
358        return invalid(format!(
359            "offset must be a scalar or have {} elements, got shape {:?}",
360            x.shape()[0],
361            offset.shape()
362        ));
363    }
364    if !matches!(
365        offset.dtype().category(),
366        Category::Signed | Category::Unsigned
367    ) {
368        return invalid(format!("offset must be an integer, got {}", offset.dtype()));
369    }
370    if dims == 0 || !dims.is_multiple_of(2) || dims > x.shape()[x.ndim() - 1] {
371        return invalid(format!(
372            "dims must be even, positive and at most {}, got {dims}",
373            x.shape()[x.ndim() - 1]
374        ));
375    }
376    let offset = match offset.dtype().size() {
377        4 => offset.clone(),
378        _ => astype(offset, DType::I32)?,
379    };
380    let mut inputs = vec![x.clone(), offset];
381    let base = match (base, freqs) {
382        (Some(b), None) => b,
383        (None, Some(f)) => {
384            check_inputs(&[f])?;
385            if f.ndim() != 1 || f.size() != dims / 2 {
386                return invalid(format!(
387                    "freqs must be 1-D with {} elements, got shape {:?}",
388                    dims / 2,
389                    f.shape()
390                ));
391            }
392            inputs.push(astype(f, DType::F32)?);
393            1.0
394        }
395        _ => return invalid("exactly one of base and freqs must be given".into()),
396    };
397    let op = FastOp::Rope {
398        dims,
399        traditional,
400        base: base.to_bits(),
401        scale: scale.to_bits(),
402        forward: true,
403    };
404    one(op, inputs)
405}
406
407/// Cross entropy over the last axis of `logits` against integer class indices `targets`
408/// (the logits' shape without the last axis; negative indices count from the end), as
409/// MLX's `fast.cross_entropy`: `logsumexp(logits) - logits[target]` per row, in float32.
410pub fn cross_entropy(logits: &Array, targets: &Array) -> Result<Array> {
411    check_inputs(&[logits, targets])?;
412    if logits.ndim() == 0 || targets.shape() != &logits.shape()[..logits.ndim() - 1] {
413        return Err(Error::InvalidArgument(format!(
414            "cross_entropy: targets of shape {:?} do not match logits of shape {:?}",
415            targets.shape(),
416            logits.shape()
417        )));
418    }
419    floating("cross_entropy", logits.dtype())?;
420    if !matches!(
421        targets.dtype().category(),
422        Category::Signed | Category::Unsigned
423    ) {
424        return Err(Error::InvalidArgument(format!(
425            "cross_entropy: targets must be integer class indices, got {}",
426            targets.dtype()
427        )));
428    }
429    one(
430        FastOp::CrossEntropy,
431        vec![logits.clone(), astype(targets, DType::I32)?],
432    )
433}
434
435/// Records a `SliceUpdate` of `base`'s last axis at `start, start + step, …` (`len` values)
436/// with `update`.
437fn update_last(base: &Array, update: &Array, start: usize, step: isize) -> Result<Array> {
438    let n = base.ndim();
439    let mut starts = vec![0; n];
440    starts[n - 1] = start;
441    let mut steps = vec![1; n];
442    steps[n - 1] = step;
443    let primitive = Primitive::SliceUpdate {
444        start: starts,
445        step: steps,
446        len: update.shape().to_vec(),
447    };
448    Ok(Array::from_op(primitive, vec![base.clone(), update.clone()])?.swap_remove(0))
449}
450
451/// The last axis of `x` from `start` in steps of `step`, `len` values.
452fn last(x: &Array, start: usize, step: isize, len: usize) -> Result<Array> {
453    let mut args = vec![SliceArg::full(); x.ndim()];
454    let stop = start as isize + step * len as isize;
455    args[x.ndim() - 1] = SliceArg::new(Some(start as isize), Some(stop), step);
456    slice(x, &args)
457}
458
459/// `1 / sqrt(x)`.
460fn rsqrt(x: &Array) -> Result<Array> {
461    divide(1.0, sqrt(x)?)
462}
463
464/// `softmax` over the last axis from ordinary operations, in float32 for half types.
465fn softmax_graph(x: &Array) -> Result<Array> {
466    let xf = if x.dtype() == DType::F32 {
467        x.clone()
468    } else {
469        astype(x, DType::F32)?
470    };
471    let m = stop_gradient(&max(&xf, &[-1], true)?)?;
472    let e = exp(&subtract(&xf, &m)?)?;
473    astype(&divide(&e, sum(&e, &[-1], true)?)?, x.dtype())
474}
475
476/// `log(sum(exp(x)))` over the last axis (kept), from ordinary operations.
477fn logsumexp_graph(x: &Array) -> Result<Array> {
478    let m = stop_gradient(&max(x, &[-1], true)?)?;
479    let out = add(log(&sum(&exp(&subtract(x, &m)?)?, &[-1], true)?)?, &m)?;
480    where_(equal(&ops::abs(&m)?, f64::INFINITY)?, &m, &out)
481}
482
483/// `[..., C]` one-hot rows of the (non-negative) class indices `y`, in `dtype`.
484fn one_hot(y: &Array, classes: usize, dtype: DType) -> Result<Array> {
485    let positions = arange(0.0, classes as f64, 1.0, y.dtype())?;
486    equal(&expand_dims(y, -1)?, &positions)?.astype(dtype)
487}
488
489/// Class indices with negative values counted from the end.
490fn wrap(y: &Array, classes: usize) -> Result<Array> {
491    where_(less(y, 0)?, add(y, classes as f64)?, y)
492}
493
494impl FastOp {
495    pub(crate) fn name(&self) -> &'static str {
496        match self {
497            Self::RmsNorm { .. } => "rms_norm",
498            Self::AddRmsNorm { .. } => "add_rms_norm",
499            Self::RmsNormVjp { .. } => "rms_norm_vjp",
500            Self::LayerNorm { .. } => "layer_norm",
501            Self::LayerNormVjp { .. } => "layer_norm_vjp",
502            Self::Rope { .. } => "rope",
503            Self::NormRope { .. } => "norm_rope",
504            Self::DepthwiseConv1d { .. } => "conv1d",
505            Self::PagedAttention { .. } => "paged_attention",
506            Self::PagedWrite => "paged_write",
507            Self::Softmax => "softmax",
508            Self::LogSumExp => "logsumexp",
509            Self::CrossEntropy => "cross_entropy",
510            Self::CrossEntropyVjp => "cross_entropy_vjp",
511            Self::Attention { .. } => "scaled_dot_product_attention",
512            Self::GatedDelta { .. } | Self::GatedDeltaSlots => "gated_delta",
513            Self::GatherQmm { .. } => "gather_qmm",
514            Self::GatherMm => "gather_mm",
515        }
516    }
517
518    /// `(dtype, shape)` per output, after checking the input count.
519    pub(crate) fn outputs(&self, inputs: &[Array]) -> Result<Vec<(DType, Vec<usize>)>> {
520        let arity = match self {
521            Self::Softmax | Self::LogSumExp => 1,
522            Self::RmsNorm { .. } | Self::CrossEntropy | Self::DepthwiseConv1d { .. } => 2,
523            Self::PagedAttention { .. } => 5,
524            Self::PagedWrite => 3,
525            Self::AddRmsNorm { .. } | Self::RmsNormVjp { .. } | Self::LayerNorm { .. } => 3,
526            Self::LayerNormVjp { .. } | Self::CrossEntropyVjp => 4,
527            Self::Rope { .. } => inputs.len().clamp(2, 3),
528            Self::NormRope { cache, .. } => inputs
529                .len()
530                .clamp(2 + usize::from(*cache), 3 + usize::from(*cache)),
531            Self::Attention { mask, sinks, .. } => 3 + usize::from(*mask) + usize::from(*sinks),
532            Self::GatedDelta { .. } => 6,
533            Self::GatedDeltaSlots => 7,
534            Self::GatherQmm { .. } => inputs.len().clamp(6, 7),
535            Self::GatherMm => inputs.len().clamp(4, 5),
536        };
537        if inputs.len() != arity || inputs[0].ndim() == 0 {
538            return Err(Error::InvalidArgument(format!(
539                "{}: {arity} inputs expected, got {}",
540                self.name(),
541                inputs.len()
542            )));
543        }
544        let x = &inputs[0];
545        let like = |a: &Array| (a.dtype(), a.shape().to_vec());
546        Ok(match self {
547            Self::RmsNorm { .. }
548            | Self::LayerNorm { .. }
549            | Self::Rope { .. }
550            | Self::NormRope { .. }
551            | Self::Softmax => {
552                vec![like(x)]
553            }
554            Self::LogSumExp => {
555                let mut shape = x.shape().to_vec();
556                *shape.last_mut().expect("1-D") = 1;
557                vec![(x.dtype(), shape)]
558            }
559            Self::AddRmsNorm { .. } => vec![like(x), like(x)],
560            Self::PagedAttention { .. } | Self::PagedWrite => vec![like(x)],
561            Self::DepthwiseConv1d {
562                stride,
563                padding,
564                dilation,
565            } => {
566                let (l, k) = (x.shape()[1], inputs[1].shape()[1]);
567                let l_out = (l + 2 * padding - dilation * (k - 1) - 1) / stride + 1;
568                vec![(x.dtype(), vec![x.shape()[0], l_out, x.shape()[2]])]
569            }
570            Self::RmsNormVjp { .. } => vec![like(x), like(&inputs[1])],
571            Self::LayerNormVjp { .. } => vec![like(x), like(&inputs[1]), like(&inputs[2])],
572            Self::CrossEntropy => vec![(DType::F32, inputs[1].shape().to_vec())],
573            Self::CrossEntropyVjp => vec![like(x)],
574            Self::Attention { .. } => {
575                let mut shape = x.shape().to_vec();
576                shape[3] = inputs[2].shape()[3];
577                vec![(x.dtype(), shape)]
578            }
579            Self::GatedDeltaSlots => {
580                vec![(x.dtype(), inputs[2].shape().to_vec()), like(&inputs[5])]
581            }
582            Self::GatedDelta { steps } => {
583                let state = match steps {
584                    false => inputs[5].shape().to_vec(),
585                    true => {
586                        let mut shape = inputs[5].shape().to_vec();
587                        shape.insert(1, x.shape()[1]);
588                        shape
589                    }
590                };
591                vec![
592                    (x.dtype(), inputs[2].shape().to_vec()),
593                    (inputs[5].dtype(), state),
594                ]
595            }
596            Self::GatherMm => {
597                let b = inputs[1].shape();
598                let mut shape = inputs[2].shape().to_vec();
599                shape.extend([x.shape()[x.ndim() - 2], b[b.len() - 1]]);
600                vec![(x.dtype(), shape)]
601            }
602            Self::GatherQmm {
603                bits, transpose, ..
604            } => {
605                let w = inputs[1].shape();
606                let n = match transpose {
607                    true => w[w.len() - 2],
608                    false => w[w.len() - 1] * 32 / usize::from(*bits),
609                };
610                let mut shape = inputs[4].shape().to_vec();
611                shape.extend([x.shape()[x.ndim() - 2], n]);
612                vec![(x.dtype(), shape)]
613            }
614        })
615    }
616
617    /// The same computation from ordinary operations (MLX's fallbacks).
618    pub(crate) fn fallback(&self, inputs: &[Array]) -> Result<Vec<Array>> {
619        let x = &inputs[0];
620        let f32x = || astype(x, DType::F32);
621        let out = match self {
622            Self::RmsNorm { eps } => {
623                let xf = f32x()?;
624                let n = rsqrt(&add(mean(&multiply(&xf, &xf)?, &[-1], true)?, float(*eps))?)?;
625                let y = multiply(&xf, &n)?.astype(x.dtype())?;
626                let w = &inputs[1];
627                vec![if w.ndim() == 0 { y } else { multiply(&y, w)? }]
628            }
629            Self::AddRmsNorm { eps } => {
630                let h = add(x, &inputs[1])?;
631                let op = Self::RmsNorm { eps: *eps };
632                let y = record_or_fallback(op, vec![h.clone(), inputs[2].clone()])?;
633                vec![h, y.into_iter().next().expect("one output")]
634            }
635            Self::LayerNorm { eps } => {
636                let xf = f32x()?;
637                let xc = subtract(&xf, mean(&xf, &[-1], true)?)?;
638                let v = mean(&multiply(&xc, &xc)?, &[-1], true)?;
639                let mut y = multiply(&xc, rsqrt(&add(&v, float(*eps))?)?)?.astype(x.dtype())?;
640                if inputs[1].ndim() != 0 {
641                    y = multiply(&y, &inputs[1])?;
642                }
643                if inputs[2].ndim() != 0 {
644                    y = add(&y, &inputs[2])?;
645                }
646                vec![y]
647            }
648            Self::RmsNormVjp { eps } => {
649                let (w, g) = (&inputs[1], &inputs[2]);
650                let n = rsqrt(&add(mean(&multiply(x, x)?, &[-1], true)?, float(*eps))?)?;
651                let n3 = multiply(&multiply(&n, &n)?, &n)?;
652                let gw = multiply(g, w)?;
653                let t = mean(&multiply(&gw, x)?, &[-1], true)?;
654                let dx = subtract(multiply(&gw, &n)?, multiply(multiply(x, &t)?, &n3)?)?;
655                let dw = match w.ndim() {
656                    0 => scalar(0.0, w.dtype())?,
657                    _ => sum_rows(&multiply(g, multiply(x, &n)?)?)?,
658                };
659                vec![dx, dw]
660            }
661            Self::LayerNormVjp { eps } => {
662                let (w, b, g) = (&inputs[1], &inputs[2], &inputs[3]);
663                let mu = mean(x, &[-1], true)?;
664                let xc = subtract(x, &mu)?;
665                let var = mean(&multiply(&xc, &xc)?, &[-1], true)?;
666                let n = rsqrt(&add(&var, float(*eps))?)?;
667                let n3 = multiply(&multiply(&n, &n)?, &n)?;
668                let wg = multiply(w, g)?;
669                let mwg = mean(&wg, &[-1], true)?;
670                let mwgxc = mean(&multiply(&wg, &xc)?, &[-1], true)?;
671                let dx = subtract(
672                    multiply(subtract(&wg, &mwg)?, &n)?,
673                    multiply(multiply(&xc, &mwgxc)?, &n3)?,
674                )?;
675                let dw = match w.ndim() {
676                    0 => scalar(0.0, w.dtype())?,
677                    _ => sum_rows(&multiply(g, multiply(&xc, &n)?)?)?,
678                };
679                let db = match b.ndim() {
680                    0 => scalar(0.0, b.dtype())?,
681                    _ => sum_rows(g)?,
682                };
683                vec![dx, dw, db]
684            }
685            Self::Attention {
686                scale,
687                causal,
688                mask,
689                sinks,
690            } => vec![attention_graph(
691                inputs,
692                float(*scale),
693                *causal,
694                *mask,
695                *sinks,
696            )?],
697            Self::Rope {
698                dims,
699                traditional,
700                base,
701                scale,
702                forward,
703            } => vec![rope_graph(
704                inputs,
705                *dims,
706                *traditional,
707                float(*base),
708                float(*scale),
709                *forward,
710            )?],
711            Self::NormRope {
712                eps,
713                dims,
714                traditional,
715                base,
716                scale,
717                offset,
718                cache,
719            } => {
720                let at = usize::from(*cache);
721                let (x, w) = (&inputs[at], &inputs[at + 1]);
722                let mut h = x.clone();
723                if let Some(eps) = eps {
724                    let op = Self::RmsNorm { eps: *eps };
725                    h = record_or_fallback(op, vec![h, w.clone()])?.swap_remove(0);
726                }
727                let mut rope_inputs = vec![h, Array::scalar(*offset as i32)];
728                rope_inputs.extend(inputs.get(at + 2).cloned());
729                let op = Self::Rope {
730                    dims: *dims,
731                    traditional: *traditional,
732                    base: *base,
733                    scale: *scale,
734                    forward: true,
735                };
736                let r = record_or_fallback(op, rope_inputs)?.swap_remove(0);
737                vec![match cache {
738                    true => {
739                        let mut region = vec![SliceArg::full(); 4];
740                        let end = (offset + r.shape()[2]) as isize;
741                        region[2] = SliceArg::new(Some(*offset as isize), Some(end), 1);
742                        crate::slice_update(&inputs[0], &r, &region)?
743                    }
744                    false => r,
745                }]
746            }
747            Self::DepthwiseConv1d {
748                stride,
749                padding,
750                dilation,
751            } => {
752                let c = x.shape()[2];
753                vec![crate::conv::general(
754                    x,
755                    &inputs[1],
756                    (*stride, *padding, *dilation, c),
757                )?]
758            }
759            Self::PagedAttention { scale } => vec![paged_attention_graph(inputs, float(*scale))?],
760            Self::PagedWrite => vec![paged_write_graph(inputs)?],
761            Self::Softmax => vec![softmax_graph(x)?],
762            Self::GatedDelta { steps } => gated_delta_graph(inputs, *steps)?,
763            Self::GatedDeltaSlots => {
764                // The states at the slots, the rule, then the new states back at the slots.
765                let (pool, slots) = (&inputs[5], &inputs[6]);
766                let states = crate::take(pool, slots, Some(0))?;
767                let mut args = inputs[..5].to_vec();
768                args.push(states);
769                let mut out = gated_delta_graph(&args, false)?;
770                let new = out.pop().expect("two outputs");
771                let at = broadcast_to(&reshape(slots, &[-1, 1, 1, 1])?, new.shape())?;
772                let pool = crate::put_along_axis(pool, &at, &new, 0)?;
773                vec![out.pop().expect("two outputs"), pool]
774            }
775            Self::GatherMm => {
776                let rows = |a: &Array, indices: &Array| -> Result<Array> {
777                    let nd = a.ndim();
778                    let shape = [-1, a.shape()[nd - 2] as isize, a.shape()[nd - 1] as isize];
779                    crate::take(&reshape(a, &shape)?, indices, Some(0))
780                };
781                vec![crate::matmul(
782                    &rows(x, &inputs[2])?,
783                    &rows(&inputs[1], &inputs[3])?,
784                )?]
785            }
786            Self::GatherQmm {
787                group_size,
788                bits,
789                transpose,
790            } => vec![gather_qmm_graph(
791                inputs,
792                *group_size,
793                u32::from(*bits),
794                *transpose,
795            )?],
796            Self::LogSumExp => vec![logsumexp_graph(x)?],
797            Self::CrossEntropy => {
798                let classes = x.shape()[x.ndim() - 1];
799                let y = wrap(&inputs[1], classes)?;
800                let hit = one_hot(&y, classes, DType::Bool)?;
801                let score = sum(&where_(&hit, x, 0.0)?, &[-1], false)?;
802                let lse = squeeze(&logsumexp_graph(x)?, &[-1])?;
803                vec![subtract(&lse, &score)?.astype(DType::F32)?]
804            }
805            Self::CrossEntropyVjp => {
806                let classes = x.shape()[x.ndim() - 1];
807                let (y, loss, g) = (wrap(&inputs[1], classes)?, &inputs[2], &inputs[3]);
808                let hit = one_hot(&y, classes, DType::Bool)?;
809                let score = sum(&where_(&hit, x, 0.0)?, &[-1], false)?;
810                let lse = add(loss, score.astype(DType::F32)?)?;
811                let p = exp(&subtract(&f32x()?, expand_dims(&lse, -1)?)?)?;
812                let gx = multiply(
813                    expand_dims(g, -1)?,
814                    subtract(&p, one_hot(&y, classes, DType::F32)?)?,
815                )?;
816                vec![gx.astype(x.dtype())?]
817            }
818        };
819        Ok(out)
820    }
821
822    /// Reverse-mode rule: one cotangent per input (`None` where not wanted).
823    pub(crate) fn vjp(
824        &self,
825        inputs: &[Array],
826        outputs: &[Array],
827        cotangents: &[Array],
828        wanted: &[bool],
829    ) -> Result<Vec<Option<Array>>> {
830        let g = &cotangents[0];
831        let pick = |all: Vec<Array>| -> Vec<Option<Array>> {
832            all.into_iter()
833                .zip(wanted)
834                .map(|(a, &w)| w.then_some(a))
835                .collect()
836        };
837        match self {
838            Self::RmsNorm { eps } => {
839                let op = Self::RmsNormVjp { eps: *eps };
840                let grads =
841                    record_or_fallback(op, vec![inputs[0].clone(), inputs[1].clone(), g.clone()])?;
842                Ok(pick(grads))
843            }
844            Self::LayerNorm { eps } => {
845                let op = Self::LayerNormVjp { eps: *eps };
846                let mut all = inputs.to_vec();
847                all.push(g.clone());
848                Ok(pick(record_or_fallback(op, all)?))
849            }
850            Self::Rope { forward, .. } => {
851                if wanted[1..].iter().any(|&w| w) {
852                    return Err(Error::InvalidArgument(
853                        "rope: no gradient with respect to the offset or frequencies".into(),
854                    ));
855                }
856                let Self::Rope {
857                    dims,
858                    traditional,
859                    base,
860                    scale,
861                    ..
862                } = *self
863                else {
864                    unreachable!()
865                };
866                let inverse = Self::Rope {
867                    dims,
868                    traditional,
869                    base,
870                    scale,
871                    forward: !forward,
872                };
873                let mut all = inputs.to_vec();
874                all[0] = g.clone();
875                let mut out = vec![Some(one(inverse, all)?)];
876                out.extend(std::iter::repeat_n(None, inputs.len() - 1));
877                Ok(out)
878            }
879            Self::Softmax => {
880                let s = &outputs[0];
881                let sg = multiply(s, g)?;
882                let dx = subtract(&sg, multiply(s, sum(&sg, &[-1], true)?)?)?;
883                Ok(vec![Some(dx)])
884            }
885            Self::LogSumExp => {
886                let s = one(Self::Softmax, vec![inputs[0].clone()])?;
887                Ok(vec![Some(multiply(g, &s)?)])
888            }
889            Self::CrossEntropy => {
890                if wanted[1] {
891                    return Err(Error::InvalidArgument(
892                        "cross_entropy: no gradient with respect to the targets".into(),
893                    ));
894                }
895                let all = vec![
896                    inputs[0].clone(),
897                    inputs[1].clone(),
898                    outputs[0].clone(),
899                    g.clone(),
900                ];
901                Ok(vec![Some(one(Self::CrossEntropyVjp, all)?), None])
902            }
903            Self::AddRmsNorm { .. }
904            | Self::NormRope { .. }
905            | Self::PagedAttention { .. }
906            | Self::PagedWrite
907            | Self::DepthwiseConv1d { .. }
908            | Self::RmsNormVjp { .. }
909            | Self::LayerNormVjp { .. }
910            | Self::CrossEntropyVjp
911            | Self::Attention { .. }
912            | Self::GatedDelta { .. }
913            | Self::GatedDeltaSlots
914            | Self::GatherQmm { .. }
915            | Self::GatherMm => self.fallback_vjp(inputs, cotangents, wanted),
916        }
917    }
918
919    /// Reverse mode through the fallback graph, for the wanted floating-point inputs.
920    fn fallback_vjp(
921        &self,
922        inputs: &[Array],
923        cotangents: &[Array],
924        wanted: &[bool],
925    ) -> Result<Vec<Option<Array>>> {
926        let active: Vec<usize> = (0..inputs.len())
927            .filter(|&i| wanted[i] && inputs[i].dtype().category() == Category::Float)
928            .collect();
929        let primals: Vec<Array> = active.iter().map(|&i| inputs[i].clone()).collect();
930        let f = |ps: &[Array]| {
931            let mut all = inputs.to_vec();
932            for (p, &i) in ps.iter().zip(&active) {
933                all[i] = p.clone();
934            }
935            self.fallback(&all)
936        };
937        let (_, grads) = crate::vjp(f, &primals, cotangents)?;
938        let mut out = vec![None; inputs.len()];
939        for (g, &i) in grads.into_iter().zip(&active) {
940            out[i] = Some(g);
941        }
942        Ok(out)
943    }
944
945    /// Forward-mode rule through the fallback graph (integer inputs are constants).
946    pub(crate) fn jvp(&self, inputs: &[Array], tangents: &[Option<Array>]) -> Result<Vec<Array>> {
947        let active: Vec<usize> = (0..inputs.len())
948            .filter(|&i| tangents[i].is_some() && inputs[i].dtype().category() == Category::Float)
949            .collect();
950        let primals: Vec<Array> = active.iter().map(|&i| inputs[i].clone()).collect();
951        let ts: Vec<Array> = active
952            .iter()
953            .map(|&i| tangents[i].clone().expect("active"))
954            .collect();
955        let f = |ps: &[Array]| {
956            let mut all = inputs.to_vec();
957            for (p, &i) in ps.iter().zip(&active) {
958                all[i] = p.clone();
959            }
960            self.fallback(&all)
961        };
962        Ok(crate::jvp(f, &primals, &ts)?.1)
963    }
964
965    /// Batching rule: row-wise operations with only the first input batched (and cross
966    /// entropy) keep their kernel with the batch axis first; otherwise the fallback graph
967    /// is batched.
968    pub(crate) fn vmap(
969        &self,
970        inputs: &[Array],
971        axes: &[Option<usize>],
972    ) -> Result<(Vec<Array>, usize)> {
973        let rest_unbatched = axes[1..].iter().all(Option::is_none);
974        let front = |i: usize| -> Result<Array> {
975            let a = &inputs[i];
976            match axes[i] {
977                Some(ax) => crate::batching::move_axis(a, ax, 0),
978                None => {
979                    let b = inputs
980                        .iter()
981                        .zip(axes)
982                        .find_map(|(a, ax)| ax.map(|ax| a.shape()[ax]))
983                        .expect("one batched input");
984                    let mut shape = vec![b];
985                    shape.extend_from_slice(a.shape());
986                    broadcast_to(&expand_dims(a, 0)?, &shape)
987                }
988            }
989        };
990        let simple = match self {
991            Self::RmsNorm { .. } | Self::LayerNorm { .. } | Self::Softmax | Self::LogSumExp => {
992                rest_unbatched
993            }
994            Self::Rope { .. } => rest_unbatched && inputs[1].size() == 1,
995            Self::CrossEntropy => true,
996            _ => false,
997        };
998        if simple {
999            let mut all = inputs.to_vec();
1000            all[0] = front(0)?;
1001            if *self == Self::CrossEntropy {
1002                all[1] = front(1)?;
1003            }
1004            return Ok((record_or_fallback(self.clone(), all)?, 0));
1005        }
1006        let f = |xs: &[Array]| self.fallback(xs);
1007        Ok((crate::transforms::batch(&f, inputs, axes)?, 0))
1008    }
1009
1010    /// Runs the fallback graph on the CPU over evaluated operands, for work no kernel takes.
1011    pub(crate) fn eval_cpu(&self, operands: &[Operand<'_>]) -> Result<Vec<crate::array::Data>> {
1012        let leaves = operands
1013            .iter()
1014            .map(|(meta, data)| {
1015                let buffer = crate::kernels::contiguous(meta, data)?;
1016                Array::from_buffer(buffer, meta.dtype, &meta.shape)
1017            })
1018            .collect::<Result<Vec<_>>>()?;
1019        let previous = default_stream();
1020        set_default_stream(Stream::default_for(Device::cpu()));
1021        let outputs = self.fallback(&leaves);
1022        set_default_stream(previous);
1023        let outputs = outputs?;
1024        crate::eval(&outputs)?;
1025        outputs
1026            .iter()
1027            .map(|a| {
1028                let data = a.data().expect("evaluated");
1029                Ok(crate::array::Data::new(crate::kernels::contiguous(
1030                    a.out(),
1031                    data,
1032                )?))
1033            })
1034            .collect()
1035    }
1036}
1037
1038/// `g` summed over every axis but the last.
1039fn sum_rows(g: &Array) -> Result<Array> {
1040    let axes: Vec<isize> = (0..g.ndim() as isize - 1).collect();
1041    sum(g, &axes, false)
1042}
1043
1044/// MLX's RoPE fallback: the rotated features replace the input's in place.
1045fn rope_graph(
1046    inputs: &[Array],
1047    dims: usize,
1048    traditional: bool,
1049    base: f32,
1050    scale: f32,
1051    forward: bool,
1052) -> Result<Array> {
1053    let x = &inputs[0];
1054    let shape: Vec<isize> = x.shape().iter().map(|&d| d as isize).collect();
1055    let nd = x.ndim();
1056    let (b, t, d) = (x.shape()[0], x.shape()[nd - 2], x.shape()[nd - 1]);
1057    let x4 = reshape(x, &[b as isize, -1, t as isize, d as isize])?;
1058    let dtype = x.dtype();
1059    let half = dims / 2;
1060    let offset = &inputs[1];
1061    let offset = if offset.size() > 1 {
1062        reshape(offset, &[-1, 1, 1])?
1063    } else {
1064        reshape(offset, &[])?
1065    };
1066    let positions = multiply(
1067        add(arange(0.0, t as f64, 1.0, DType::F32)?, &offset)?,
1068        f64::from(scale),
1069    )?;
1070    let inv_freqs = match inputs.get(2) {
1071        Some(f) => divide(1.0, f)?,
1072        None => exp(&multiply(
1073            arange(0.0, -(half as f64), -1.0, DType::F32)?,
1074            f64::from(base.ln() / half as f32),
1075        )?)?,
1076    };
1077    let theta = multiply(expand_dims(&positions, -1)?, &inv_freqs)?;
1078    let (c, s) = (cos(&theta)?.astype(dtype)?, sin(&theta)?.astype(dtype)?);
1079    let (start2, step) = if traditional { (1, 2) } else { (half, 1) };
1080    let x1 = last(&x4, 0, step, half)?;
1081    let x2 = last(&x4, start2, step, half)?;
1082    let (r1, r2) = if forward {
1083        (
1084            subtract(multiply(&x1, &c)?, multiply(&x2, &s)?)?,
1085            add(multiply(&x1, &s)?, multiply(&x2, &c)?)?,
1086        )
1087    } else {
1088        (
1089            add(multiply(&x2, &s)?, multiply(&x1, &c)?)?,
1090            subtract(multiply(&x2, &c)?, multiply(&x1, &s)?)?,
1091        )
1092    };
1093    let full = |r: Array| broadcast_to(&r, x1.shape());
1094    let out = update_last(&x4, &full(r1)?, 0, step)?;
1095    let out = update_last(&out, &full(r2)?, start2, step)?;
1096    reshape(&out, &shape)
1097}
1098
1099/// The rotation of [`norm_rope`]: [`rope`]'s parameters.
1100#[derive(Debug, Clone, Copy)]
1101pub struct Rotation<'a> {
1102    /// Rotated features (even, at most the head size).
1103    pub dims: usize,
1104    /// Rotate interleaved pairs instead of the two halves.
1105    pub traditional: bool,
1106    /// The frequency base (exactly one of `base` and `freqs`).
1107    pub base: Option<f32>,
1108    /// The position scale.
1109    pub scale: f32,
1110    /// Per-pair frequencies (`[dims / 2]`).
1111    pub freqs: Option<&'a Array>,
1112}
1113
1114/// The largest head size [`norm_rope`]'s kernel takes.
1115const NORM_ROPE_MAX: usize = 1024;
1116
1117/// An attention layer's query or key preparation in one kernel: each head's features of
1118/// `x` (`[B, H, L, D]`, any strides, as the transposed projection) optionally normalised
1119/// as [`rms_norm`] with `norm = (weight, eps)` (Qwen 3's `q_norm` and `k_norm`), then
1120/// rotated as [`rope`] at positions `offset..offset + L`.
1121///
1122/// Without `cache`, the result is a new `[B, H, L, D]` array. With `cache` (`[B, H, C, D]`,
1123/// `C ≥ offset + L`), the result is `cache` with positions `offset..offset + L` replaced
1124/// (as [`crate::slice_update`], and written in place when the evaluation owns the cache),
1125/// so keys go into a key cache without a separate copy.
1126///
1127/// The results equal [`rms_norm`], [`rope`] and [`crate::slice_update`] composed (on the GPU,
1128/// bit for bit); differing dtypes, other ranks and head sizes over 1024 run that composition.
1129pub fn norm_rope(
1130    x: &Array,
1131    norm: Option<(&Array, f32)>,
1132    rotation: Rotation<'_>,
1133    offset: usize,
1134    cache: Option<&Array>,
1135) -> Result<Array> {
1136    check_inputs(&[x])?;
1137    let mut inputs = Vec::with_capacity(4);
1138    let mut fused = x.ndim() == 4
1139        && x.dtype().category() == Category::Float
1140        && x.shape()[3] <= NORM_ROPE_MAX
1141        && offset + x.shape()[2] <= i32::MAX as usize;
1142    if let Some(c) = cache {
1143        check_inputs(&[c])?;
1144        let s = x.shape();
1145        let fits = c.ndim() == 4
1146            && c.shape()[..2] == s[..2]
1147            && c.shape()[3] == s[3]
1148            && c.shape()[2] >= offset + s[2];
1149        if !fits {
1150            return Err(Error::InvalidArgument(format!(
1151                "norm_rope: a cache of shape {:?} cannot take {:?} at position {offset}",
1152                c.shape(),
1153                s
1154            )));
1155        }
1156        fused &= c.dtype() == x.dtype();
1157        inputs.push(c.clone());
1158    }
1159    if let Some((w, _)) = norm {
1160        check_inputs(&[w])?;
1161        fused &= w.dtype() == x.dtype() && w.ndim() == 1 && w.size() == x.shape()[x.ndim() - 1];
1162    }
1163    let freqs_valid = rotation
1164        .freqs
1165        .is_none_or(|f| f.error().is_none() && f.ndim() == 1 && f.size() == rotation.dims / 2);
1166    let dims_valid = rotation.dims > 0
1167        && rotation.dims.is_multiple_of(2)
1168        && x.ndim() > 0
1169        && rotation.dims <= x.shape()[x.ndim() - 1];
1170    if !fused || !freqs_valid || !dims_valid || rotation.base.is_some() == rotation.freqs.is_some()
1171    {
1172        // The composition (which also reports invalid arguments).
1173        return norm_rope_graph(x, norm, rotation, offset, cache);
1174    }
1175    inputs.push(x.clone());
1176    inputs.push(match norm {
1177        Some((w, _)) => w.clone(),
1178        None => scalar(1.0, x.dtype())?,
1179    });
1180    if let Some(f) = rotation.freqs {
1181        inputs.push(astype(f, DType::F32)?);
1182    }
1183    let op = FastOp::NormRope {
1184        eps: norm.map(|(_, eps)| eps.to_bits()),
1185        dims: rotation.dims,
1186        traditional: rotation.traditional,
1187        base: rotation.base.unwrap_or(1.0).to_bits(),
1188        scale: rotation.scale.to_bits(),
1189        offset,
1190        cache: cache.is_some(),
1191    };
1192    one(op, inputs)
1193}
1194
1195/// [`norm_rope`] composed of [`rms_norm`], [`rope`] and [`crate::slice_update`].
1196fn norm_rope_graph(
1197    x: &Array,
1198    norm: Option<(&Array, f32)>,
1199    rotation: Rotation<'_>,
1200    offset: usize,
1201    cache: Option<&Array>,
1202) -> Result<Array> {
1203    let x = match norm {
1204        Some((w, eps)) => rms_norm(x, Some(w), eps)?,
1205        None => x.clone(),
1206    };
1207    let r = rope(
1208        &x,
1209        rotation.dims,
1210        rotation.traditional,
1211        rotation.base,
1212        rotation.scale,
1213        &Array::scalar(offset as i32),
1214        rotation.freqs,
1215    )?;
1216    match cache {
1217        Some(c) => {
1218            let mut region = vec![SliceArg::full(); 4];
1219            region[2] = SliceArg::new(
1220                Some(offset as isize),
1221                Some((offset + r.shape()[2]) as isize),
1222                1,
1223            );
1224            crate::slice_update(c, &r, &region)
1225        }
1226        None => Ok(r),
1227    }
1228}
1229
1230/// Attention over a paged KV cache, for a batch of sequences of different lengths.
1231///
1232/// Keys and values live in blocks of positions, `k_cache` and `v_cache` being
1233/// `[blocks, kv_heads, block_size, D]`; sequence `b` keeps its positions in the blocks
1234/// `block_tables[b]` lists (`[B, max_blocks]`, integers; entries past the sequence's blocks
1235/// are ignored but must be valid block indices). `q` is `[B, Hq, L, D]` (`Hq` a multiple of
1236/// `kv_heads`): the last `L` positions of each sequence, whose keys are already in the
1237/// cache. Query `l` of sequence `b` attends positions `0..=lengths[b] − L + l` (causal at
1238/// the end; `lengths[b] ≥ L`). Returns `[B, Hq, L, D]`.
1239///
1240/// On the GPU this is MLX's vector attention (one pass, or two for long caches) reading
1241/// keys through the block table, for head sizes 64, 128 and 256 and power-of-two block
1242/// sizes (others run the composition). Each sequence's result equals
1243/// [`scaled_dot_product_attention`]'s vector kernel over the same keys stored contiguously
1244/// when both take the same number of passes.
1245pub fn paged_attention(
1246    q: &Array,
1247    k_cache: &Array,
1248    v_cache: &Array,
1249    block_tables: &Array,
1250    lengths: &Array,
1251    scale: f32,
1252) -> Result<Array> {
1253    check_inputs(&[q, k_cache, v_cache, block_tables, lengths])?;
1254    let invalid = |msg: String| Err(Error::InvalidArgument(format!("paged_attention: {msg}")));
1255    floating("paged_attention", q.dtype())?;
1256    if q.ndim() != 4 || k_cache.ndim() != 4 || k_cache.shape() != v_cache.shape() {
1257        return invalid(format!(
1258            "q must be [B, Hq, L, D] and the caches [blocks, Hkv, block_size, D], got {:?}, \
1259             {:?}, {:?}",
1260            q.shape(),
1261            k_cache.shape(),
1262            v_cache.shape()
1263        ));
1264    }
1265    let (b, hq, d) = (q.shape()[0], q.shape()[1], q.shape()[3]);
1266    let hkv = k_cache.shape()[1];
1267    if k_cache.shape()[3] != d || hkv == 0 || hq % hkv != 0 {
1268        return invalid(format!(
1269            "q's heads ({hq}) must be a multiple of the caches' ({hkv}), with head size {d}"
1270        ));
1271    }
1272    let integer = |a: &Array| matches!(a.dtype().category(), Category::Signed | Category::Unsigned);
1273    if block_tables.ndim() != 2 || block_tables.shape()[0] != b || !integer(block_tables) {
1274        return invalid(format!(
1275            "block_tables must be [{b}, max_blocks] integers, got {} {:?}",
1276            block_tables.dtype(),
1277            block_tables.shape()
1278        ));
1279    }
1280    if lengths.shape() != [b] || !integer(lengths) {
1281        return invalid(format!(
1282            "lengths must be [{b}] integers, got {} {:?}",
1283            lengths.dtype(),
1284            lengths.shape()
1285        ));
1286    }
1287    let dtype = q.dtype();
1288    let inputs = vec![
1289        q.clone(),
1290        astype(k_cache, dtype)?,
1291        astype(v_cache, dtype)?,
1292        astype(block_tables, DType::I32)?,
1293        astype(lengths, DType::I32)?,
1294    ];
1295    let op = FastOp::PagedAttention {
1296        scale: scale.to_bits(),
1297    };
1298    // The kernels use 32-bit offsets into the caches.
1299    let small = k_cache.size() < u32::MAX as usize;
1300    if matches!(d, 64 | 128 | 256) && k_cache.shape()[2].is_power_of_two() && small {
1301        one(op, inputs)
1302    } else {
1303        Ok(op.fallback(&inputs)?.swap_remove(0))
1304    }
1305}
1306
1307/// `cache` (`[blocks, kv_heads, block_size, D]`) with the positions `new` holds
1308/// (`[T, kv_heads, D]`) written into their slots: `slots[t]` (integers, `[T]`) is block
1309/// `slots[t] / block_size`, position `slots[t] % block_size`. Slots must be distinct. In
1310/// place when the evaluation owns the cache, as [`crate::slice_update`].
1311pub fn paged_write(cache: &Array, new: &Array, slots: &Array) -> Result<Array> {
1312    check_inputs(&[cache, new, slots])?;
1313    let invalid = |msg: String| Err(Error::InvalidArgument(format!("paged_write: {msg}")));
1314    let fits = cache.ndim() == 4
1315        && new.ndim() == 3
1316        && new.shape()[1] == cache.shape()[1]
1317        && new.shape()[2] == cache.shape()[3];
1318    if !fits || slots.shape() != [new.shape().first().copied().unwrap_or(0)] {
1319        return invalid(format!(
1320            "cache must be [blocks, Hkv, block_size, D], new [T, Hkv, D] and slots [T], got \
1321             {:?}, {:?}, {:?}",
1322            cache.shape(),
1323            new.shape(),
1324            slots.shape()
1325        ));
1326    }
1327    if !matches!(
1328        slots.dtype().category(),
1329        Category::Signed | Category::Unsigned
1330    ) {
1331        return invalid(format!("slots must be integers, got {}", slots.dtype()));
1332    }
1333    let inputs = vec![
1334        cache.clone(),
1335        astype(new, cache.dtype())?,
1336        astype(slots, DType::I32)?,
1337    ];
1338    one(FastOp::PagedWrite, inputs)
1339}
1340
1341/// The keys of every listed block per sequence, `[B, Hkv, max_blocks · block_size, D]`.
1342fn paged_keys(cache: &Array, tables: &Array) -> Result<Array> {
1343    let (b, max_blocks) = (tables.shape()[0], tables.shape()[1]);
1344    let (hkv, bs, d) = (cache.shape()[1], cache.shape()[2], cache.shape()[3]);
1345    let gathered = crate::take(cache, &reshape(tables, &[-1])?, Some(0))?; // [B·M, Hkv, bs, D]
1346    let gathered = reshape(
1347        &gathered,
1348        &[
1349            b as isize,
1350            max_blocks as isize,
1351            hkv as isize,
1352            bs as isize,
1353            d as isize,
1354        ],
1355    )?;
1356    let gathered = crate::transpose(&gathered, &[0, 2, 1, 3, 4])?;
1357    reshape(
1358        &gathered,
1359        &[
1360            b as isize,
1361            hkv as isize,
1362            (max_blocks * bs) as isize,
1363            d as isize,
1364        ],
1365    )
1366}
1367
1368/// [`paged_attention`] from ordinary operations: every listed block gathered, then
1369/// attention with a mask of each sequence's positions.
1370fn paged_attention_graph(inputs: &[Array], scale: f32) -> Result<Array> {
1371    let [q, k_cache, v_cache, tables, lengths] = inputs else {
1372        unreachable!("five inputs")
1373    };
1374    let (b, lq) = (q.shape()[0], q.shape()[2]);
1375    let (k, v) = (paged_keys(k_cache, tables)?, paged_keys(v_cache, tables)?);
1376    let s = k.shape()[2];
1377    // Query `l` sees positions up to `lengths[b] − L + l`.
1378    let positions = reshape(
1379        &arange(0.0, s as f64, 1.0, DType::I32)?,
1380        &[1, 1, 1, s as isize],
1381    )?;
1382    let queries = reshape(
1383        &arange(0.0, lq as f64, 1.0, DType::I32)?,
1384        &[1, 1, lq as isize, 1],
1385    )?;
1386    let end = add(
1387        reshape(lengths, &[b as isize, 1, 1, 1])?,
1388        subtract(&queries, (lq - 1) as f64)?,
1389    )?;
1390    let mask = less(&positions, &end)?;
1391    scaled_dot_product_attention(q, &k, &v, scale, AttentionMask::Array(&mask), None)
1392}
1393
1394/// [`paged_write`] from ordinary operations: the cache as one row per slot, the rows of
1395/// `slots` replaced.
1396fn paged_write_graph(inputs: &[Array]) -> Result<Array> {
1397    let [cache, new, slots] = inputs else {
1398        unreachable!("three inputs")
1399    };
1400    let (blocks, hkv, bs, d) = (
1401        cache.shape()[0],
1402        cache.shape()[1],
1403        cache.shape()[2],
1404        cache.shape()[3],
1405    );
1406    let rows = reshape(
1407        &crate::transpose(cache, &[0, 2, 1, 3])?,
1408        &[(blocks * bs) as isize, hkv as isize, d as isize],
1409    )?;
1410    let at = broadcast_to(&reshape(slots, &[-1, 1, 1])?, new.shape())?;
1411    let rows = crate::put_along_axis(&rows, &at, new, 0)?;
1412    let rows = reshape(
1413        &rows,
1414        &[blocks as isize, bs as isize, hkv as isize, d as isize],
1415    )?;
1416    crate::transpose(&rows, &[0, 2, 1, 3])
1417}
1418
1419/// Scaled dot-product attention, as MLX's `fast.scaled_dot_product_attention`:
1420/// `softmax(scale · q kᵀ + mask) v` over `[B, H, L, D]` arrays.
1421///
1422/// `k` and `v` may have fewer heads than `q` (grouped-query attention; `Hq` a multiple of
1423/// `Hkv`), and `v` its own head size. Optional `sinks` (`[Hq]`) add one logit per head that
1424/// takes part in the softmax but attends to nothing. On the GPU, short queries (at most 8)
1425/// use MLX's vector kernels and longer ones its tiled kernel, for the head sizes MLX
1426/// supports; other cases run the equivalent composition (as MLX does).
1427pub fn scaled_dot_product_attention(
1428    q: &Array,
1429    k: &Array,
1430    v: &Array,
1431    scale: f32,
1432    mask: AttentionMask<'_>,
1433    sinks: Option<&Array>,
1434) -> Result<Array> {
1435    check_inputs(&[q, k, v])?;
1436    let invalid = |msg: String| {
1437        Err(Error::InvalidArgument(format!(
1438            "scaled_dot_product_attention: {msg}"
1439        )))
1440    };
1441    if [q, k, v].iter().any(|a| a.ndim() != 4) {
1442        return invalid(format!(
1443            "q, k and v must be 4-D, got {:?}, {:?} and {:?}",
1444            q.shape(),
1445            k.shape(),
1446            v.shape()
1447        ));
1448    }
1449    let (qs, ks, vs) = (q.shape(), k.shape(), v.shape());
1450    if ks[0] != qs[0] || vs[0] != qs[0] || qs[3] != ks[3] || ks[1] != vs[1] || ks[2] != vs[2] {
1451        return invalid(format!("incompatible shapes {qs:?}, {ks:?} and {vs:?}"));
1452    }
1453    if ks[1] == 0 || qs[1] % ks[1] != 0 {
1454        return invalid(format!(
1455            "the query heads ({}) must be a multiple of the key heads ({})",
1456            qs[1], ks[1]
1457        ));
1458    }
1459    let dtype = crate::promote_types(crate::promote_types(q.dtype(), k.dtype()), v.dtype());
1460    floating("scaled_dot_product_attention", dtype)?;
1461    let mut inputs = vec![astype(q, dtype)?, astype(k, dtype)?, astype(v, dtype)?];
1462    let (causal, has_mask) = match mask {
1463        AttentionMask::None => (false, false),
1464        AttentionMask::Causal => (true, false),
1465        AttentionMask::Array(m) => {
1466            check_inputs(&[m])?;
1467            if m.ndim() > 4 {
1468                return invalid(format!(
1469                    "the mask must have at most 4 dimensions, got {:?}",
1470                    m.shape()
1471                ));
1472            }
1473            let m = if m.dtype() == DType::Bool {
1474                m.clone()
1475            } else if crate::promote_types(m.dtype(), dtype) == dtype {
1476                astype(m, dtype)?
1477            } else {
1478                return invalid(format!("a {} mask does not promote to {dtype}", m.dtype()));
1479            };
1480            inputs.push(broadcast_to(&m, &[qs[0], qs[1], qs[2], ks[2]])?);
1481            (false, true)
1482        }
1483    };
1484    if let Some(s) = sinks {
1485        check_inputs(&[s])?;
1486        if s.ndim() != 1 || s.size() != qs[1] || crate::promote_types(s.dtype(), dtype) != dtype {
1487            return invalid(format!(
1488                "sinks must be [{}] of a type promoting to {dtype}",
1489                qs[1]
1490            ));
1491        }
1492        inputs.push(astype(s, dtype)?);
1493    }
1494    let op = FastOp::Attention {
1495        scale: scale.to_bits(),
1496        causal,
1497        mask: has_mask,
1498        sinks: sinks.is_some(),
1499    };
1500    if on_gpu(dtype, q.size()) && attention_kernel(&inputs, causal, has_mask) {
1501        Ok(Array::from_op(Primitive::Fast(op), inputs)?.swap_remove(0))
1502    } else {
1503        Ok(op.fallback(&inputs)?.swap_remove(0))
1504    }
1505}
1506
1507/// Whether MLX routes these shapes to a fused kernel (`has_fused_kernel` and
1508/// `use_fallback`, without the NAX and blocked head-size-512 kernels).
1509fn attention_kernel(inputs: &[Array], causal: bool, has_mask: bool) -> bool {
1510    let (q, k, v) = (&inputs[0], &inputs[1], &inputs[2]);
1511    let (lq, lk, d, dv) = (q.shape()[2], k.shape()[2], q.shape()[3], v.shape()[3]);
1512    let gqa = q.shape()[1] / k.shape()[1];
1513    if lk == 0 || lq == 0 {
1514        return false;
1515    }
1516    if lq > 8 {
1517        let head =
1518            (d == dv && matches!(d, 64 | 72 | 80 | 96 | 128 | 192 | 256)) || (d, dv) == (96, 64);
1519        let causal_ok = !causal || lq <= lk;
1520        // MLX: the composition is faster for these head sizes, except long causal
1521        // self-attention in half precision. Measured on an M4 Pro, the tiled kernel is faster
1522        // for causal chunks of 1,024 queries or more over up to 8,192 keys (prefill chunks;
1523        // up to 21%, in every dtype), the composition beyond and for shorter chunks. The
1524        // composition materialises the scores, so above 4 GiB of them (a 2,048-token chunk
1525        // over ~64K keys and more) the tiled kernel runs anyway.
1526        let scores = q.shape()[0] * q.shape()[1] * lq * lk * q.dtype().size();
1527        let huge = scores > 4 << 30;
1528        let slow = match d {
1529            192 => !huge,
1530            256 => !(huge || (causal && lq >= 1024 && lk <= 8192)),
1531            _ => false,
1532        };
1533        head && causal_ok && !slow
1534    } else {
1535        let head = (d == dv && matches!(d, 64 | 96 | 128 | 256 | 512))
1536            || (d, dv) == (192, 128)
1537            || (d, dv) == (96, 64);
1538        let d512 = d != 512 || (lq == 1 && gqa == 8 && !has_mask && lk >= 1024);
1539        head && lq <= lk && lq * gqa <= 32 && d512
1540    }
1541}
1542
1543/// MLX's attention fallback, with sinks folded into the softmax.
1544fn attention_graph(
1545    inputs: &[Array],
1546    scale: f32,
1547    causal: bool,
1548    has_mask: bool,
1549    has_sinks: bool,
1550) -> Result<Array> {
1551    let (q, k, v) = (&inputs[0], &inputs[1], &inputs[2]);
1552    let dtype = q.dtype();
1553    let (b, hq, lq) = (q.shape()[0], q.shape()[1], q.shape()[2]);
1554    let (hkv, lk) = (k.shape()[1], k.shape()[2]);
1555    let rep = hq / hkv;
1556    let i = |n: usize| n as isize;
1557    let mut q = multiply(q, f64::from(scale))?;
1558    let (mut k, mut v) = (k.clone(), v.clone());
1559    if rep > 1 {
1560        q = reshape(&q, &[i(b), i(hkv), i(rep), i(lq), i(q.shape()[3])])?;
1561        k = expand_dims(&k, 2)?;
1562        v = expand_dims(&v, 2)?;
1563    }
1564    let n = k.ndim();
1565    let mut axes: Vec<usize> = (0..n).collect();
1566    axes.swap(n - 2, n - 1);
1567    let mut scores = crate::matmul(&q, &crate::transpose(&k, &axes)?)?;
1568    let mask = if causal {
1569        let offset = lk as f64 - lq as f64;
1570        let qi = expand_dims(&arange(offset, offset + lq as f64, 1.0, DType::I32)?, 1)?;
1571        let ki = expand_dims(&arange(0.0, lk as f64, 1.0, DType::I32)?, 0)?;
1572        Some(crate::greater_equal(&qi, &ki)?)
1573    } else if has_mask {
1574        let m = &inputs[3];
1575        Some(if rep > 1 {
1576            reshape(m, &[i(b), i(hkv), i(rep), i(lq), i(lk)])?
1577        } else {
1578            m.clone()
1579        })
1580    } else {
1581        None
1582    };
1583    if let Some(m) = mask {
1584        scores = if m.dtype() == DType::Bool {
1585            let lowest = match dtype {
1586                DType::F16 => f64::from(crate::f16::MIN),
1587                DType::BF16 => f64::from(crate::bf16::MIN),
1588                DType::F64 => f64::MIN,
1589                _ => f64::from(f32::MIN),
1590            };
1591            where_(&m, &scores, lowest)?
1592        } else {
1593            add(&scores, &m)?
1594        };
1595    }
1596    let probs = if has_sinks {
1597        // softmax over [sink, scores...], without the sink's column.
1598        let s = astype(&scores, DType::F32)?;
1599        let sink = astype(inputs.last().expect("sinks"), DType::F32)?;
1600        let shape: Vec<isize> = if rep > 1 {
1601            vec![1, i(hkv), i(rep), 1, 1]
1602        } else {
1603            vec![1, i(hq), 1, 1]
1604        };
1605        let sink = reshape(&sink, &shape)?;
1606        let m = stop_gradient(&crate::maximum(&max(&s, &[-1], true)?, &sink)?)?;
1607        let e = exp(&subtract(&s, &m)?)?;
1608        let denom = add(sum(&e, &[-1], true)?, exp(&subtract(&sink, &m)?)?)?;
1609        astype(&divide(&e, &denom)?, dtype)?
1610    } else if on_gpu(dtype, scores.size()) {
1611        crate::softmax(&scores, &[-1])?
1612    } else {
1613        softmax_graph(&scores)?
1614    };
1615    let out = crate::matmul(&probs, &v)?;
1616    if rep > 1 {
1617        reshape(&out, &[i(b), i(hq), i(lq), i(v.shape()[n - 1])])
1618    } else {
1619        Ok(out)
1620    }
1621}
1622
1623/// The gated delta rule, Gated DeltaNet's recurrence (as mlx-lm's `gated_delta_update`
1624/// once its gates are computed). For each step `t` and value head, with `S` its state
1625/// (`[Dv, Dk]`):
1626///
1627/// `S ← g_t · S`, then `S ← S + β_t (v_t − S k_t) k_tᵀ`, and `y_t = S q_t`.
1628///
1629/// Shapes: `q` and `k` are `[B, T, Hk, Dk]`, `v` is `[B, T, Hv, Dv]` (value head `h` reads
1630/// key head `h / (Hv / Hk)`), `g` and `beta` are `[B, T, Hv]` and `state` is
1631/// `[B, Hv, Dv, Dk]`. Returns `y` (`[B, T, Hv, Dv]`, in `q`'s dtype) and the final state
1632/// (in `state`'s dtype); the recurrence runs in float32. On the GPU it is one kernel
1633/// (mlx-lm's, for `Dk` a multiple of 32), else the steps as ordinary operations, which also
1634/// define the derivatives.
1635///
1636/// ```
1637/// use nuvec_core::{Array, DType, Result, fast, ones, zeros};
1638///
1639/// # fn main() -> Result<()> {
1640/// let q = ones(&[1, 2, 1, 32], DType::F32)?;
1641/// let v = ones(&[1, 2, 1, 4], DType::F32)?;
1642/// let (g, beta) = (ones(&[1, 2, 1], DType::F32)?, ones(&[1, 2, 1], DType::F32)?);
1643/// let state = zeros(&[1, 1, 4, 32], DType::F32)?;
1644/// let (y, s) = fast::gated_delta(&(&q * 0.25), &(&q * 0.125), &v, &g, &(&beta * 0.5), &state)?;
1645/// // k · k = 0.5 and k · q = 1. Step 1: S = 0.5 · v kᵀ, so y = 0.5. Step 2: S k = 0.25,
1646/// // so S gains (1 − 0.25) · 0.5 · v kᵀ, and y = 0.875.
1647/// assert_eq!(y.to_vec::<f32>()?, [0.5, 0.5, 0.5, 0.5, 0.875, 0.875, 0.875, 0.875]);
1648/// assert_eq!(s.shape(), [1, 1, 4, 32]);
1649/// # Ok(())
1650/// # }
1651/// ```
1652pub fn gated_delta(
1653    q: &Array,
1654    k: &Array,
1655    v: &Array,
1656    g: &Array,
1657    beta: &Array,
1658    state: &Array,
1659) -> Result<(Array, Array)> {
1660    gated_delta_op(q, k, v, g, beta, state, false)
1661}
1662
1663/// [`gated_delta`] returning the state after every step, `[B, T, Hv, Dv, Dk]` (step `t`'s
1664/// state at index `t`; the last is [`gated_delta`]'s final state), so that a caller can
1665/// roll the recurrence back to any step, as speculative decoding does after a rejected
1666/// draft. The outputs equal [`gated_delta`]'s bit for bit; writing every step's state
1667/// costs `T` times the state's bytes.
1668pub fn gated_delta_steps(
1669    q: &Array,
1670    k: &Array,
1671    v: &Array,
1672    g: &Array,
1673    beta: &Array,
1674    state: &Array,
1675) -> Result<(Array, Array)> {
1676    gated_delta_op(q, k, v, g, beta, state, true)
1677}
1678
1679/// [`gated_delta`] for a batch of sequences whose states live in a pool: sequence `b`
1680/// starts from `pool[slots[b]]` (`pool` is `[slots, Hv, Dv, Dk]`, `slots` `[B]` integers,
1681/// distinct) and its final state replaces it. Returns the outputs and the updated pool,
1682/// which is written in place when the evaluation owns it, so decoding many sequences
1683/// moves no states. Each sequence's results equal [`gated_delta`]'s on its state alone.
1684pub fn gated_delta_slots(
1685    q: &Array,
1686    k: &Array,
1687    v: &Array,
1688    g: &Array,
1689    beta: &Array,
1690    pool: &Array,
1691    slots: &Array,
1692) -> Result<(Array, Array)> {
1693    check_inputs(&[pool, slots])?;
1694    let b = q.shape().first().copied().unwrap_or(0);
1695    let fits = pool.ndim() == 4
1696        && slots.shape() == [b]
1697        && matches!(
1698            slots.dtype().category(),
1699            Category::Signed | Category::Unsigned
1700        );
1701    if !fits {
1702        return Err(Error::InvalidArgument(format!(
1703            "gated_delta_slots: pool must be [slots, Hv, Dv, Dk] and slots [{b}] integers, got \
1704             {:?} and {} {:?}",
1705            pool.shape(),
1706            slots.dtype(),
1707            slots.shape()
1708        )));
1709    }
1710    // The checks of `gated_delta`, on one slot's state.
1711    let one = crate::take(pool, &reshape(&astype(slots, DType::I32)?, &[-1])?, Some(0))?;
1712    gated_delta_op(q, k, v, g, beta, &one, false)?;
1713    let inputs = vec![
1714        q.clone(),
1715        k.clone(),
1716        v.clone(),
1717        g.clone(),
1718        beta.clone(),
1719        pool.clone(),
1720        astype(slots, DType::I32)?,
1721    ];
1722    let kernel = on_gpu(q.dtype(), q.size())
1723        && q.dtype() == v.dtype()
1724        && q.shape()[3].is_multiple_of(32)
1725        && [g, beta, pool]
1726            .iter()
1727            .all(|a| matches!(a.dtype(), DType::F16 | DType::BF16 | DType::F32));
1728    let op = FastOp::GatedDeltaSlots;
1729    let mut out = if kernel {
1730        Array::from_op(Primitive::Fast(op), inputs)?
1731    } else {
1732        op.fallback(&inputs)?
1733    };
1734    let pool = out.pop().expect("two outputs");
1735    Ok((out.pop().expect("two outputs"), pool))
1736}
1737
1738fn gated_delta_op(
1739    q: &Array,
1740    k: &Array,
1741    v: &Array,
1742    g: &Array,
1743    beta: &Array,
1744    state: &Array,
1745    steps: bool,
1746) -> Result<(Array, Array)> {
1747    check_inputs(&[q, k, v, g, beta, state])?;
1748    let op = "gated_delta";
1749    for a in [q, k, v, g, beta, state] {
1750        floating(op, a.dtype())?;
1751    }
1752    let invalid = |msg: String| Err(Error::InvalidArgument(format!("{op}: {msg}")));
1753    if q.ndim() != 4 || q.shape() != k.shape() || v.ndim() != 4 {
1754        return invalid(format!(
1755            "q and k must be [B, T, Hk, Dk] and v [B, T, Hv, Dv], got {:?}, {:?}, {:?}",
1756            q.shape(),
1757            k.shape(),
1758            v.shape()
1759        ));
1760    }
1761    let (b, t, hk, dk) = (q.shape()[0], q.shape()[1], q.shape()[2], q.shape()[3]);
1762    let (hv, dv) = (v.shape()[2], v.shape()[3]);
1763    if v.shape()[..2] != [b, t] || hk == 0 || hv % hk != 0 {
1764        return invalid(format!(
1765            "v must be [{b}, {t}, Hv, Dv] with Hv a multiple of {hk}, got {:?}",
1766            v.shape()
1767        ));
1768    }
1769    if g.shape() != [b, t, hv] || beta.shape() != [b, t, hv] {
1770        return invalid(format!(
1771            "g and beta must be [{b}, {t}, {hv}], got {:?} and {:?}",
1772            g.shape(),
1773            beta.shape()
1774        ));
1775    }
1776    if state.shape() != [b, hv, dv, dk] {
1777        return invalid(format!(
1778            "state must be [{b}, {hv}, {dv}, {dk}], got {:?}",
1779            state.shape()
1780        ));
1781    }
1782    let inputs = vec![
1783        q.clone(),
1784        k.clone(),
1785        v.clone(),
1786        g.clone(),
1787        beta.clone(),
1788        state.clone(),
1789    ];
1790    let kernel = on_gpu(q.dtype(), q.size())
1791        && q.dtype() == v.dtype()
1792        && dk % 32 == 0
1793        && [g, beta, state]
1794            .iter()
1795            .all(|a| matches!(a.dtype(), DType::F16 | DType::BF16 | DType::F32));
1796    let op = FastOp::GatedDelta { steps };
1797    let mut out = if kernel {
1798        Array::from_op(Primitive::Fast(op), inputs)?
1799    } else {
1800        op.fallback(&inputs)?
1801    };
1802    let s = out.pop().expect("two outputs");
1803    Ok((out.pop().expect("two outputs"), s))
1804}
1805
1806/// The gated delta rule step by step from ordinary operations (mlx-lm's
1807/// `gated_delta_ops`).
1808fn gated_delta_graph(inputs: &[Array], steps: bool) -> Result<Vec<Array>> {
1809    let [q, k, v, g, beta, state] = inputs else {
1810        unreachable!("six inputs")
1811    };
1812    let (b, t, hk, dk) = (q.shape()[0], q.shape()[1], q.shape()[2], q.shape()[3]);
1813    let (hv, dv) = (v.shape()[2], v.shape()[3]);
1814    // Each key head serves `Hv / Hk` consecutive value heads.
1815    let heads = |x: &Array| -> Result<Array> {
1816        if hv == hk {
1817            return Ok(x.clone());
1818        }
1819        let wide = broadcast_to(&expand_dims(x, 3)?, &[b, t, hk, hv / hk, dk])?;
1820        reshape(&wide, &[b as isize, t as isize, hv as isize, dk as isize])
1821    };
1822    // In float32 at least, as the kernels.
1823    let wide = |x: &Array| match x.dtype() {
1824        DType::F64 => Ok(x.clone()),
1825        _ => astype(x, DType::F32),
1826    };
1827    let (qh, kh) = (heads(&wide(q)?)?, heads(&wide(k)?)?);
1828    let (v, g, beta) = (&wide(v)?, &wide(g)?, &wide(beta)?);
1829    let at = |x: &Array, i: usize| -> Result<Array> {
1830        let mut args = vec![SliceArg::full(); x.ndim()];
1831        args[1] = SliceArg::new(Some(i as isize), Some(i as isize + 1), 1);
1832        squeeze(&slice(x, &args)?, &[1])
1833    };
1834    let mut s = wide(state)?;
1835    let mut ys = Vec::with_capacity(t);
1836    let mut states = Vec::with_capacity(if steps { t } else { 0 });
1837    for i in 0..t {
1838        let (qt, kt, vt) = (at(&qh, i)?, expand_dims(&at(&kh, i)?, 2)?, at(v, i)?);
1839        let decay = reshape(&at(g, i)?, &[b as isize, hv as isize, 1, 1])?;
1840        s = multiply(&s, &decay)?;
1841        let kv_mem = sum(&multiply(&s, &kt)?, &[-1], false)?;
1842        let delta = multiply(subtract(&vt, &kv_mem)?, expand_dims(&at(beta, i)?, -1)?)?;
1843        s = add(&s, multiply(&kt, expand_dims(&delta, -1)?)?)?;
1844        let y = sum(&multiply(&s, expand_dims(&qt, 2)?)?, &[-1], false)?;
1845        ys.push(astype(&y, q.dtype())?);
1846        if steps {
1847            states.push(astype(&s, state.dtype())?);
1848        }
1849    }
1850    let y = match ys.is_empty() {
1851        true => zeros(&[b, 0, hv, dv], q.dtype())?,
1852        false => stack(&ys, 1)?,
1853    };
1854    let s = match (steps, states.is_empty()) {
1855        (false, _) => astype(&s, state.dtype())?,
1856        (true, false) => stack(&states, 1)?,
1857        (true, true) => zeros(&[b, 0, hv, dv, dk], state.dtype())?,
1858    };
1859    Ok(vec![y, s])
1860}
1861
1862/// The gathered quantized products from ordinary operations: the indexed batches of `x` and
1863/// of the weights, then one batched quantized product.
1864fn gather_qmm_graph(
1865    inputs: &[Array],
1866    group_size: usize,
1867    bits: u32,
1868    transpose: bool,
1869) -> Result<Array> {
1870    let (lhs, rhs) = (&inputs[4], &inputs[5]);
1871    let rows = |a: &Array, indices: &Array| -> Result<Array> {
1872        let nd = a.ndim();
1873        let flat = reshape(
1874            a,
1875            &[-1, a.shape()[nd - 2] as isize, a.shape()[nd - 1] as isize],
1876        )?;
1877        crate::take(&flat, indices, Some(0))
1878    };
1879    crate::quantized_matmul(
1880        &rows(&inputs[0], lhs)?,
1881        &rows(&inputs[1], rhs)?,
1882        &rows(&inputs[2], rhs)?,
1883        &rows(&inputs[3], rhs)?,
1884        transpose,
1885        group_size,
1886        bits,
1887    )
1888}