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