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