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