Skip to main content

candle_nn/
ops.rs

1//! Tensor ops.
2//!
3
4use candle::{CpuStorage, DType, Layout, Module, Result, Shape, Tensor, D};
5use rayon::prelude::*;
6
7/// Applies the softmax function to the input tensor, rescaling the element so that elements on
8/// a slice of fixed index on dimension `dim` are between 0 and 1 and sum to 1.
9///
10/// ```rust
11/// use candle::{Tensor, Device, test_utils::to_vec2_round};
12/// let a = Tensor::new(&[[0f32, 1., 0., 1.], [-2., 2., 3., -3.]], &Device::Cpu)?;
13/// let a = candle_nn::ops::softmax(&a, 1)?;
14/// assert_eq!(
15///     to_vec2_round(&a, 4)?,
16///     &[
17///         [0.1345, 0.3655, 0.1345, 0.3655],
18///         [0.0049, 0.2671, 0.7262, 0.0018]
19///     ]);
20/// # Ok::<(), candle::Error>(())
21/// ```
22pub fn softmax<D: candle::shape::Dim>(xs: &Tensor, dim: D) -> Result<Tensor> {
23    let dim = dim.to_index(xs.shape(), "softmax")?;
24    let max = xs.max_keepdim(dim)?;
25    let diff = xs.broadcast_sub(&max)?;
26    let num = diff.exp()?;
27    let den = num.sum_keepdim(dim)?;
28    num.broadcast_div(&den)
29}
30
31pub fn log_softmax<D: candle::shape::Dim>(xs: &Tensor, d: D) -> Result<Tensor> {
32    let d = d.to_index(xs.shape(), "log-softmax")?;
33    let max = xs.max_keepdim(d)?;
34    let diff = xs.broadcast_sub(&max)?;
35    let sum_exp = diff.exp()?.sum_keepdim(d)?;
36    let log_sm = diff.broadcast_sub(&sum_exp.log()?)?;
37    Ok(log_sm)
38}
39
40pub fn silu(xs: &Tensor) -> Result<Tensor> {
41    xs.silu()
42}
43
44pub fn swiglu(xs: &Tensor) -> Result<Tensor> {
45    let xs = xs.chunk(2, D::Minus1)?;
46    &xs[0].silu()? * &xs[1]
47}
48
49struct Sigmoid;
50
51impl candle::CustomOp1 for Sigmoid {
52    fn name(&self) -> &'static str {
53        "sigmoid"
54    }
55
56    fn cpu_fwd(&self, storage: &CpuStorage, layout: &Layout) -> Result<(CpuStorage, Shape)> {
57        use candle::backend::BackendStorage;
58
59        fn fwd<T: num_traits::Float>(v: T) -> T {
60            (v.neg().exp() + T::one()).recip()
61        }
62
63        // FIXME: using `candle::map_dtype` causes compilation errors.
64        let storage = match storage {
65            CpuStorage::BF16(slice) => {
66                CpuStorage::BF16(candle::cpu_backend::unary_map(slice, layout, fwd))
67            }
68            CpuStorage::F16(slice) => {
69                CpuStorage::F16(candle::cpu_backend::unary_map(slice, layout, fwd))
70            }
71            CpuStorage::F32(slice) => {
72                CpuStorage::F32(candle::cpu_backend::unary_map(slice, layout, fwd))
73            }
74            CpuStorage::F64(slice) => {
75                CpuStorage::F64(candle::cpu_backend::unary_map(slice, layout, fwd))
76            }
77            _ => Err(candle::Error::UnsupportedDTypeForOp(
78                storage.dtype(),
79                self.name(),
80            ))?,
81        };
82        Ok((storage, layout.shape().clone()))
83    }
84
85    #[cfg(feature = "cuda")]
86    fn cuda_fwd(
87        &self,
88        storage: &candle::CudaStorage,
89        layout: &Layout,
90    ) -> Result<(candle::CudaStorage, Shape)> {
91        use candle::backend::BackendStorage;
92        use candle::cuda_backend::cudarc::driver::{
93            CudaSlice, DeviceRepr, LaunchConfig, PushKernelArg, ValidAsZeroBits,
94        };
95        use candle::cuda_backend::SlicePtrOrNull;
96        use candle::cuda_backend::{kernel_name, kernels, Map1, WrapErr};
97        use candle::{CudaDevice, WithDType};
98
99        struct S;
100        impl Map1 for S {
101            fn f<T: DeviceRepr + WithDType + ValidAsZeroBits>(
102                &self,
103                src: &CudaSlice<T>,
104                dev: &CudaDevice,
105                layout: &Layout,
106            ) -> Result<CudaSlice<T>> {
107                let shape = layout.shape();
108                let dims = shape.dims();
109                let el_count = shape.elem_count();
110                let cfg = LaunchConfig::for_num_elems(el_count as u32);
111                let ds = SlicePtrOrNull::params_from_layout(dev, layout)?;
112                let src = &src.slice(layout.start_offset()..);
113                let func = dev.get_or_load_func(&kernel_name::<T>("usigmoid"), &kernels::UNARY)?;
114                // SAFETY: Set later by running the kernel.
115                let out = unsafe { dev.alloc::<T>(el_count)? };
116
117                let mut builder = func.builder();
118                candle::builder_arg!(builder, el_count, dims.len());
119                ds.builder_arg(&mut builder);
120                builder.arg(src);
121                builder.arg(&out);
122                // SAFETY: ffi.
123                unsafe { builder.launch(cfg) }.w()?;
124                Ok(out)
125            }
126        }
127
128        let dev = storage.device();
129        let slice = S.map(&storage.slice, dev, layout)?;
130        let dst = candle::CudaStorage {
131            slice,
132            device: dev.clone(),
133        };
134        Ok((dst, layout.shape().clone()))
135    }
136
137    #[cfg(feature = "metal")]
138    fn metal_fwd(
139        &self,
140        storage: &candle::MetalStorage,
141        layout: &Layout,
142    ) -> Result<(candle::MetalStorage, Shape)> {
143        use candle::backend::BackendStorage;
144        use candle::MetalError;
145        let device = storage.device();
146        let dtype = storage.dtype();
147        let shape = layout.shape();
148        let el_count = shape.elem_count();
149        let buffer = device
150            .new_buffer_builder()
151            .with_size_for(el_count, dtype)
152            .with_label("sigmoid")
153            .build()?;
154        let encoder = device.command_encoder()?;
155        encoder.set_label("sigmoid");
156        let src = candle_metal_kernels::BufferOffset {
157            buffer: storage.buffer(),
158            offset_in_bytes: layout.start_offset() * storage.dtype().size_in_bytes(),
159        };
160
161        if layout.is_contiguous() {
162            use candle_metal_kernels::unary::contiguous;
163            let kernel_name = match dtype {
164                DType::F16 => contiguous::sigmoid::HALF,
165                DType::F32 => contiguous::sigmoid::FLOAT,
166                DType::BF16 => contiguous::sigmoid::BFLOAT,
167                dtype => {
168                    candle::bail!("Metal contiguous unary sigmoid {dtype:?} not implemented")
169                }
170            };
171            candle_metal_kernels::call_unary_contiguous(
172                device.metal_device(),
173                &encoder,
174                device.kernels(),
175                kernel_name,
176                dtype.size_in_bytes(),
177                el_count,
178                src,
179                &buffer,
180            )
181            .map_err(MetalError::from)?;
182        } else {
183            use candle_metal_kernels::unary::strided;
184            let kernel_name = match dtype {
185                DType::F16 => strided::sigmoid::HALF,
186                DType::F32 => strided::sigmoid::FLOAT,
187                DType::BF16 => strided::sigmoid::BFLOAT,
188                dtype => {
189                    candle::bail!("Metal strided unary sigmoid {dtype:?} not implemented")
190                }
191            };
192            let dst = candle_metal_kernels::BufferOffset::zero_offset(&buffer);
193            candle_metal_kernels::call_unary_strided(
194                device.metal_device(),
195                &encoder,
196                device.kernels(),
197                kernel_name,
198                layout.dims(),
199                src,
200                layout.stride(),
201                dst,
202            )
203            .map_err(MetalError::from)?;
204        }
205
206        let new_storage = candle::MetalStorage::new(buffer, device.clone(), el_count, dtype);
207        Ok((new_storage, layout.shape().clone()))
208    }
209
210    fn bwd(&self, _arg: &Tensor, res: &Tensor, grad_res: &Tensor) -> Result<Option<Tensor>> {
211        // d/dx sigmoid(x) = (1 - sigmoid(x)) * sigmoid(x)
212        let d_dx_sigmoid = res.ones_like()?.sub(res)?.mul(res)?;
213        Ok(Some(grad_res.mul(&d_dx_sigmoid)?))
214    }
215}
216
217pub fn sigmoid(xs: &Tensor) -> Result<Tensor> {
218    xs.apply_op1(Sigmoid)
219}
220
221pub fn hard_sigmoid(xs: &Tensor) -> Result<Tensor> {
222    // TODO: Should we have a specialized op for this?
223    ((xs + 3.0)? / 6.0)?.clamp(0f32, 1f32)
224}
225
226pub fn mish(xs: &Tensor) -> Result<Tensor> {
227    xs * (1.0 + xs.exp()?)?.log()?.tanh()
228}
229
230pub fn leaky_relu(xs: &Tensor, negative_slope: f64) -> Result<Tensor> {
231    let zeros = xs.zeros_like()?;
232    xs.maximum(&zeros)? + xs.minimum(&zeros)? * negative_slope
233}
234
235pub fn selu(xs: &Tensor, alpha: f32, gamma: f32) -> Result<Tensor> {
236    let is_pos = xs.gt(0f32)?;
237    let alpha_t = Tensor::full(alpha, xs.dims(), xs.device())?;
238    let neg = xs.exp()?.mul(&alpha_t)?.sub(&alpha_t)?;
239    let selu = is_pos.where_cond(xs, &neg)?;
240    let gamma_t = Tensor::full(gamma, xs.dims(), xs.device())?;
241    selu.broadcast_mul(&gamma_t)
242}
243
244pub fn dropout(xs: &Tensor, drop_p: f32) -> Result<Tensor> {
245    // This implementation is inefficient as it stores the full mask for the backward pass.
246    // Instead we could just store the seed and have a specialized kernel that would both
247    // generate the random mask and apply it.
248    // Another easier optimization would be to be able to generate boolean mask using just a bit of
249    // entropy per element rather than generating a full float per element.
250    if !(0. ..1.).contains(&drop_p) {
251        candle::bail!("dropout probability has to be in [0, 1), got {drop_p}")
252    }
253    let rand = Tensor::rand(0f32, 1f32, xs.shape(), xs.device())?;
254    let scale = 1.0 / (1.0 - drop_p as f64);
255    let drop_p = Tensor::new(drop_p, xs.device())?.broadcast_as(xs.shape())?;
256    let mask = (rand.ge(&drop_p)?.to_dtype(xs.dtype())? * scale)?;
257    xs * mask
258}
259
260#[derive(Clone, Debug)]
261pub struct Dropout {
262    drop_p: f32,
263}
264
265impl Dropout {
266    pub fn new(drop_p: f32) -> Dropout {
267        Self { drop_p }
268    }
269
270    pub fn forward(&self, xs: &Tensor, train: bool) -> Result<Tensor> {
271        if train {
272            dropout(xs, self.drop_p)
273        } else {
274            Ok(xs.clone())
275        }
276    }
277}
278
279impl candle::ModuleT for Dropout {
280    fn forward_t(&self, xs: &Tensor, train: bool) -> Result<Tensor> {
281        self.forward(xs, train)
282    }
283}
284
285struct SoftmaxLastDim;
286
287impl candle::CustomOp1 for SoftmaxLastDim {
288    fn name(&self) -> &'static str {
289        "softmax-last-dim"
290    }
291
292    fn cpu_fwd(&self, storage: &CpuStorage, layout: &Layout) -> Result<(CpuStorage, Shape)> {
293        fn softmax<T: candle::WithDType + num_traits::Float>(
294            src: &[T],
295            layout: &Layout,
296        ) -> Result<(CpuStorage, Shape)> {
297            let src = match layout.contiguous_offsets() {
298                None => candle::bail!("input has to be contiguous"),
299                Some((o1, o2)) => &src[o1..o2],
300            };
301            let el_count = layout.shape().elem_count();
302            let dims = layout.shape().dims();
303            let dim_m1 = dims[dims.len() - 1];
304            let mut dst = vec![T::zero(); el_count];
305            src.par_chunks(dim_m1)
306                .zip(dst.par_chunks_mut(dim_m1))
307                .for_each(|(src, dst)| {
308                    let mut max = T::neg_infinity();
309                    unsafe { T::vec_reduce_max(src.as_ptr(), &mut max, dim_m1) };
310                    for (s, d) in src.iter().zip(dst.iter_mut()) {
311                        *d = (*s - max).exp();
312                    }
313                    let mut sum_exp = T::zero();
314                    unsafe { T::vec_reduce_sum(dst.as_ptr(), &mut sum_exp, dim_m1) };
315                    for d in dst.iter_mut() {
316                        *d /= sum_exp
317                    }
318                });
319            let storage = candle::WithDType::to_cpu_storage_owned(dst);
320            Ok((storage, Shape::from_dims(dims)))
321        }
322
323        match storage {
324            CpuStorage::BF16(slice) => softmax::<half::bf16>(slice, layout),
325            CpuStorage::F16(slice) => softmax::<half::f16>(slice, layout),
326            CpuStorage::F32(slice) => softmax::<f32>(slice, layout),
327            CpuStorage::F64(slice) => softmax::<f64>(slice, layout),
328            _ => candle::bail!("unsupported dtype for softmax {:?}", storage),
329        }
330    }
331
332    #[cfg(feature = "cuda")]
333    fn cuda_fwd(
334        &self,
335        storage: &candle::CudaStorage,
336        layout: &Layout,
337    ) -> Result<(candle::CudaStorage, Shape)> {
338        use candle::cuda_backend::cudarc::driver::{
339            CudaSlice, DeviceRepr, LaunchConfig, PushKernelArg,
340        };
341        use candle::cuda_backend::{kernel_name, kernels, Map1, WrapErr};
342        use candle::{CudaDevice, WithDType};
343
344        struct S;
345        impl Map1 for S {
346            fn f<T: DeviceRepr + WithDType>(
347                &self,
348                src: &CudaSlice<T>,
349                dev: &CudaDevice,
350                layout: &Layout,
351            ) -> Result<CudaSlice<T>> {
352                let src = match layout.contiguous_offsets() {
353                    None => candle::bail!("input has to be contiguous"),
354                    Some((o1, o2)) => src.slice(o1..o2),
355                };
356                let el = layout.shape().elem_count();
357                let dims = layout.shape().dims();
358                let dim_m1 = dims[dims.len() - 1];
359                let (n_rows, n_cols) = (el / dim_m1, dim_m1);
360
361                let cfg = LaunchConfig {
362                    grid_dim: (n_rows as u32, 1, 1),
363                    block_dim: (1, 32, 1),
364                    shared_mem_bytes: 0,
365                };
366                let func = dev.get_or_load_func(&kernel_name::<T>("softmax"), &kernels::REDUCE)?;
367                // SAFETY: Set later by running the kernel.
368                let dst = unsafe { dev.alloc::<T>(el)? };
369                let mut builder = func.builder();
370                builder.arg(&src);
371                builder.arg(&dst);
372                candle::builder_arg!(builder, n_cols as i32);
373                // SAFETY: ffi.
374                unsafe { builder.launch(cfg) }.w()?;
375                Ok(dst)
376            }
377        }
378
379        use candle::backend::BackendStorage;
380        let dev = storage.device();
381        let slice = S.map(&storage.slice, dev, layout)?;
382        let dst = candle::cuda_backend::CudaStorage {
383            slice,
384            device: dev.clone(),
385        };
386        Ok((dst, layout.shape().clone()))
387    }
388
389    #[cfg(feature = "metal")]
390    fn metal_fwd(
391        &self,
392        storage: &candle::MetalStorage,
393        layout: &Layout,
394    ) -> Result<(candle::MetalStorage, Shape)> {
395        use candle::backend::BackendStorage;
396        let device = storage.device();
397        let encoder = device.command_encoder()?;
398        encoder.set_label("softmax");
399        let kernels = device.kernels();
400        let name = match storage.dtype() {
401            DType::F32 => "softmax_f32",
402            DType::F16 => "softmax_f16",
403            DType::BF16 => "softmax_bf16",
404            dtype => candle::bail!("softmax-last-dim is not implemented for {dtype:?}"),
405        };
406
407        let n = layout.stride().len();
408        if !(layout.is_contiguous() && layout.stride()[n - 1] == 1) {
409            candle::bail!("Non contiguous softmax-last-dim is not implemented");
410        }
411
412        let last_dim = layout.dims()[layout.shape().rank() - 1];
413        let elem_count = layout.shape().elem_count();
414        let output = device
415            .new_buffer_builder()
416            .with_size_for(elem_count, storage.dtype())
417            .with_label("softmax")
418            .build()?;
419        candle_metal_kernels::call_last_softmax(
420            device.metal_device(),
421            &encoder,
422            kernels,
423            name,
424            elem_count,
425            last_dim,
426            storage.buffer(),
427            layout.start_offset() * storage.dtype().size_in_bytes(),
428            &output,
429        )
430        .map_err(candle::Error::wrap)?;
431        let newstorage =
432            candle::MetalStorage::new(output, device.clone(), elem_count, storage.dtype());
433        Ok((newstorage, layout.shape().clone()))
434    }
435}
436
437pub fn softmax_last_dim(xs: &Tensor) -> Result<Tensor> {
438    xs.apply_op1_no_bwd(&SoftmaxLastDim)
439}
440
441#[derive(Debug, Clone)]
442struct RmsNorm {
443    eps: f32,
444}
445
446impl candle::CustomOp2 for RmsNorm {
447    fn name(&self) -> &'static str {
448        "rms-norm"
449    }
450
451    fn cpu_fwd(
452        &self,
453        s1: &CpuStorage,
454        l1: &Layout,
455        s2: &CpuStorage,
456        l2: &Layout,
457    ) -> Result<(CpuStorage, Shape)> {
458        use candle::backend::BackendStorage;
459
460        let eps = self.eps;
461        fn inner<
462            T: candle::WithDType
463                + num_traits::Float
464                + num_traits::AsPrimitive<f32>
465                + num_traits::FromPrimitive,
466        >(
467            src: &[T],
468            layout: &Layout,
469            alpha: &[T],
470            alpha_layout: &Layout,
471            eps: f32,
472        ) -> Result<(CpuStorage, Shape)> {
473            let src = match layout.contiguous_offsets() {
474                None => candle::bail!("input has to be contiguous"),
475                Some((o1, o2)) => &src[o1..o2],
476            };
477            let alpha = match alpha_layout.contiguous_offsets() {
478                None => candle::bail!("alpha has to be contiguous"),
479                Some((o1, o2)) => &alpha[o1..o2],
480            };
481            let el_count = layout.shape().elem_count();
482            let dims = layout.shape().dims();
483            let dim_m1 = dims[dims.len() - 1];
484            let n_rows = el_count / dim_m1;
485            let mut dst = vec![T::zero(); el_count];
486
487            fn rms_row<
488                T: candle::WithDType
489                    + num_traits::Float
490                    + num_traits::AsPrimitive<f32>
491                    + num_traits::FromPrimitive,
492            >(
493                src: &[T],
494                alpha: &[T],
495                n: usize,
496                eps: f32,
497                dst: &mut [T],
498            ) {
499                let sum2 = src
500                    .iter()
501                    .map(|&v| {
502                        let v = v.as_();
503                        v * v
504                    })
505                    .sum::<f32>();
506                let m = (sum2 / n as f32 + eps).sqrt();
507                let m = T::from_f32(m).unwrap_or_else(T::nan);
508                for ((d, s), alpha) in dst.iter_mut().zip(src.iter()).zip(alpha) {
509                    *d = *s / m * *alpha
510                }
511            }
512
513            if n_rows <= 32 {
514                let n = dim_m1;
515                for row in 0..n_rows {
516                    let src = &src[row * n..(row + 1) * n];
517                    let dst = &mut dst[row * n..(row + 1) * n];
518                    rms_row(src, alpha, n, eps, dst);
519                }
520            } else {
521                src.par_chunks(dim_m1)
522                    .zip(dst.par_chunks_mut(dim_m1))
523                    .for_each(|(src, dst)| {
524                        let n = src.len();
525                        rms_row(src, alpha, n, eps, dst);
526                    });
527            }
528            let storage = candle::WithDType::to_cpu_storage_owned(dst);
529            Ok((storage, Shape::from_dims(dims)))
530        }
531
532        use CpuStorage as C;
533        match (s1, s2) {
534            (C::BF16(s1), C::BF16(s2)) => inner::<half::bf16>(s1, l1, s2, l2, eps),
535            (C::F16(s1), C::F16(s2)) => inner::<half::f16>(s1, l1, s2, l2, eps),
536            (C::F32(s1), C::F32(s2)) => inner::<f32>(s1, l1, s2, l2, eps),
537            _ => candle::bail!("unsupported dtype for rmsnorm {:?}", s1.dtype()),
538        }
539    }
540
541    #[cfg(feature = "cuda")]
542    fn cuda_fwd(
543        &self,
544        s1: &candle::CudaStorage,
545        l1: &Layout,
546        s2: &candle::CudaStorage,
547        l2: &Layout,
548    ) -> Result<(candle::CudaStorage, Shape)> {
549        use candle::cuda_backend::cudarc::driver::{
550            CudaSlice, DeviceRepr, LaunchConfig, PushKernelArg,
551        };
552        use candle::cuda_backend::{kernel_name, kernels, Map2, WrapErr};
553        use candle::{CudaDevice, WithDType};
554
555        struct S {
556            eps: f32,
557        }
558        impl Map2 for S {
559            fn f<T: DeviceRepr + WithDType>(
560                &self,
561                src: &CudaSlice<T>,
562                layout: &Layout,
563                alpha: &CudaSlice<T>,
564                alpha_layout: &Layout,
565                dev: &CudaDevice,
566            ) -> Result<CudaSlice<T>> {
567                let src = match layout.contiguous_offsets() {
568                    None => candle::bail!("input has to be contiguous"),
569                    Some((o1, o2)) => src.slice(o1..o2),
570                };
571                let alpha = match alpha_layout.contiguous_offsets() {
572                    None => candle::bail!("alpha has to be contiguous"),
573                    Some((o1, o2)) => alpha.slice(o1..o2),
574                };
575                let el = layout.shape().elem_count();
576                let dims = layout.shape().dims();
577                let dim_m1 = dims[dims.len() - 1];
578                let (n_rows, n_cols) = (el / dim_m1, dim_m1);
579
580                let block_size = if n_cols < 1024 { 32 } else { 1024 };
581                let cfg = LaunchConfig {
582                    grid_dim: (n_rows as u32, 1, 1),
583                    block_dim: (block_size, 1, 1),
584                    shared_mem_bytes: 0,
585                };
586                let func = dev.get_or_load_func(&kernel_name::<T>("rmsnorm"), &kernels::REDUCE)?;
587                // SAFETY: Set later by running the kernel.
588                let dst = unsafe { dev.alloc::<T>(el)? };
589                let mut builder = func.builder();
590                builder.arg(&src);
591                builder.arg(&dst);
592                builder.arg(&alpha);
593                candle::builder_arg!(builder, n_cols as i32, block_size as i32, self.eps);
594                // SAFETY: ffi.
595                unsafe { builder.launch(cfg) }.w()?;
596                Ok(dst)
597            }
598        }
599
600        use candle::backend::BackendStorage;
601        let dev = s1.device();
602        let slice = S { eps: self.eps }.map(&s1.slice, l1, &s2.slice, l2, dev)?;
603        let dst = candle::cuda_backend::CudaStorage {
604            slice,
605            device: dev.clone(),
606        };
607        Ok((dst, l1.shape().clone()))
608    }
609
610    #[cfg(feature = "metal")]
611    fn metal_fwd(
612        &self,
613        s1: &candle::MetalStorage,
614        l1: &Layout,
615        s2: &candle::MetalStorage,
616        l2: &Layout,
617    ) -> Result<(candle::MetalStorage, Shape)> {
618        use candle::backend::BackendStorage;
619        let device = s1.device();
620        let encoder = device.command_encoder()?;
621        encoder.set_label("rmsnorm");
622        let kernels = device.kernels();
623        let name = match (s1.dtype(), s2.dtype()) {
624            (DType::F32, DType::F32) => "rmsnorm_f32",
625            (DType::F16, DType::F16) => "rmsnorm_f16",
626            (DType::BF16, DType::BF16) => "rmsnorm_bf16",
627            (dt1, dt2) => candle::bail!("rmsnorm is not implemented for {dt1:?} {dt2:?}"),
628        };
629
630        if !(l1.is_contiguous() && l2.is_contiguous()) {
631            candle::bail!("Non contiguous rmsnorm is not implemented");
632        }
633
634        let last_dim = l1.dims()[l1.shape().rank() - 1];
635        let elem_count = l1.shape().elem_count();
636        let output = device
637            .new_buffer_builder()
638            .with_size_for(elem_count, s1.dtype())
639            .with_label("rmsnorm")
640            .build()?;
641        candle_metal_kernels::call_rms_norm(
642            device.metal_device(),
643            &encoder,
644            kernels,
645            name,
646            elem_count,
647            last_dim,
648            self.eps,
649            s1.buffer(),
650            l1.start_offset() * s1.dtype().size_in_bytes(),
651            s2.buffer(),
652            l2.start_offset() * s2.dtype().size_in_bytes(),
653            &output,
654        )
655        .map_err(candle::Error::wrap)?;
656        let newstorage = candle::MetalStorage::new(output, device.clone(), elem_count, s1.dtype());
657        Ok((newstorage, l1.shape().clone()))
658    }
659}
660
661pub fn rms_norm_slow(x: &Tensor, alpha: &Tensor, eps: f32) -> Result<Tensor> {
662    let x_dtype = x.dtype();
663    let internal_dtype = match x_dtype {
664        DType::F16 | DType::BF16 => DType::F32,
665        d => d,
666    };
667    let hidden_size = x.dim(D::Minus1)?;
668    let x = x.to_dtype(internal_dtype)?;
669    let norm_x = (x.sqr()?.sum_keepdim(D::Minus1)? / hidden_size as f64)?;
670    let x_normed = x.broadcast_div(&(norm_x + eps as f64)?.sqrt()?)?;
671    x_normed.to_dtype(x_dtype)?.broadcast_mul(alpha)
672}
673
674pub fn rms_norm(xs: &Tensor, alpha: &Tensor, eps: f32) -> Result<Tensor> {
675    let hidden_size_xs = xs.dim(D::Minus1)?;
676    let hidden_size_alpha = alpha.dims1()?;
677    if hidden_size_xs != hidden_size_alpha {
678        candle::bail!(
679            "shape mismatch in rms-norm {:?} {:?}",
680            xs.shape(),
681            alpha.shape()
682        )
683    }
684    xs.apply_op2_no_bwd(alpha, &RmsNorm { eps })
685}
686
687#[derive(Debug, Clone)]
688struct LayerNorm {
689    eps: f32,
690}
691
692impl candle::CustomOp3 for LayerNorm {
693    fn name(&self) -> &'static str {
694        "layer-norm"
695    }
696
697    fn cpu_fwd(
698        &self,
699        s1: &CpuStorage,
700        l1: &Layout,
701        s2: &CpuStorage,
702        l2: &Layout,
703        s3: &CpuStorage,
704        l3: &Layout,
705    ) -> Result<(CpuStorage, Shape)> {
706        use candle::backend::BackendStorage;
707
708        let eps = self.eps;
709        fn inner<
710            T: candle::WithDType
711                + num_traits::Float
712                + num_traits::AsPrimitive<f32>
713                + num_traits::FromPrimitive,
714        >(
715            src: &[T],
716            layout: &Layout,
717            alpha: &[T],
718            alpha_layout: &Layout,
719            beta: &[T],
720            beta_layout: &Layout,
721            eps: f32,
722        ) -> Result<(CpuStorage, Shape)> {
723            let src = match layout.contiguous_offsets() {
724                None => candle::bail!("input has to be contiguous"),
725                Some((o1, o2)) => &src[o1..o2],
726            };
727            let alpha = match alpha_layout.contiguous_offsets() {
728                None => candle::bail!("alpha has to be contiguous"),
729                Some((o1, o2)) => &alpha[o1..o2],
730            };
731            let beta = match beta_layout.contiguous_offsets() {
732                None => candle::bail!("beta has to be contiguous"),
733                Some((o1, o2)) => &beta[o1..o2],
734            };
735            let el_count = layout.shape().elem_count();
736            let dims = layout.shape().dims();
737            let dim_m1 = dims[dims.len() - 1];
738            let mut dst = vec![T::zero(); el_count];
739            src.par_chunks(dim_m1)
740                .zip(dst.par_chunks_mut(dim_m1))
741                .for_each(|(src, dst)| {
742                    let mut sum = 0f32;
743                    let mut sum2 = 0f32;
744                    for v in src {
745                        let v = v.as_();
746                        sum += v;
747                        sum2 += v * v;
748                    }
749                    let mean = sum / dim_m1 as f32;
750                    let var = sum2 / dim_m1 as f32 - mean * mean;
751                    let inv_std = (var + eps).sqrt().recip();
752                    for ((d, s), (alpha, beta)) in
753                        dst.iter_mut().zip(src.iter()).zip(alpha.iter().zip(beta))
754                    {
755                        let alpha = alpha.as_();
756                        let beta = beta.as_();
757                        let d_ = (s.as_() - mean) * inv_std * alpha + beta;
758                        *d = T::from_f32(d_).unwrap_or_else(T::nan);
759                    }
760                });
761            let storage = candle::WithDType::to_cpu_storage_owned(dst);
762            Ok((storage, Shape::from_dims(dims)))
763        }
764
765        use CpuStorage as C;
766        match (s1, s2, s3) {
767            (C::BF16(s1), C::BF16(s2), C::BF16(s3)) => {
768                inner::<half::bf16>(s1, l1, s2, l2, s3, l3, eps)
769            }
770            (C::F16(s1), C::F16(s2), C::F16(s3)) => inner::<half::f16>(s1, l1, s2, l2, s3, l3, eps),
771            (C::F32(s1), C::F32(s2), C::F32(s3)) => inner::<f32>(s1, l1, s2, l2, s3, l3, eps),
772            _ => candle::bail!("unsupported dtype for rmsnorm {:?}", s1.dtype()),
773        }
774    }
775
776    #[cfg(feature = "cuda")]
777    fn cuda_fwd(
778        &self,
779        s1: &candle::CudaStorage,
780        l1: &Layout,
781        s2: &candle::CudaStorage,
782        l2: &Layout,
783        s3: &candle::CudaStorage,
784        l3: &Layout,
785    ) -> Result<(candle::CudaStorage, Shape)> {
786        use candle::cuda_backend::cudarc::driver::{
787            CudaSlice, DeviceRepr, LaunchConfig, PushKernelArg,
788        };
789        use candle::cuda_backend::{kernel_name, kernels, Map3, WrapErr};
790        use candle::{CudaDevice, WithDType};
791
792        struct S {
793            eps: f32,
794        }
795        impl Map3 for S {
796            fn f<T: DeviceRepr + WithDType>(
797                &self,
798                src: &CudaSlice<T>,
799                layout: &Layout,
800                alpha: &CudaSlice<T>,
801                alpha_layout: &Layout,
802                beta: &CudaSlice<T>,
803                beta_layout: &Layout,
804                dev: &CudaDevice,
805            ) -> Result<CudaSlice<T>> {
806                let src = match layout.contiguous_offsets() {
807                    None => candle::bail!("input has to be contiguous"),
808                    Some((o1, o2)) => src.slice(o1..o2),
809                };
810                let alpha = match alpha_layout.contiguous_offsets() {
811                    None => candle::bail!("alpha has to be contiguous"),
812                    Some((o1, o2)) => alpha.slice(o1..o2),
813                };
814                let beta = match beta_layout.contiguous_offsets() {
815                    None => candle::bail!("beta has to be contiguous"),
816                    Some((o1, o2)) => beta.slice(o1..o2),
817                };
818                let el = layout.shape().elem_count();
819                let dims = layout.shape().dims();
820                let dim_m1 = dims[dims.len() - 1];
821                let (n_rows, n_cols) = (el / dim_m1, dim_m1);
822
823                let block_size = if n_cols < 1024 { 32 } else { 1024 };
824                let cfg = LaunchConfig {
825                    grid_dim: (n_rows as u32, 1, 1),
826                    block_dim: (block_size, 1, 1),
827                    shared_mem_bytes: 0,
828                };
829                let func =
830                    dev.get_or_load_func(&kernel_name::<T>("layernorm"), &kernels::REDUCE)?;
831                // SAFETY: Set later by running the kernel.
832                let dst = unsafe { dev.alloc::<T>(el)? };
833                let mut builder = func.builder();
834                builder.arg(&src);
835                builder.arg(&dst);
836                builder.arg(&alpha);
837                builder.arg(&beta);
838                candle::builder_arg!(builder, n_cols as i32, block_size as i32, self.eps);
839                // SAFETY: ffi.
840                unsafe { builder.launch(cfg) }.w()?;
841                Ok(dst)
842            }
843        }
844
845        use candle::backend::BackendStorage;
846        let dev = s1.device();
847        let slice = S { eps: self.eps }.map(&s1.slice, l1, &s2.slice, l2, &s3.slice, l3, dev)?;
848        let dst = candle::cuda_backend::CudaStorage {
849            slice,
850            device: dev.clone(),
851        };
852        Ok((dst, l1.shape().clone()))
853    }
854
855    #[cfg(feature = "metal")]
856    fn metal_fwd(
857        &self,
858        s1: &candle::MetalStorage,
859        l1: &Layout,
860        s2: &candle::MetalStorage,
861        l2: &Layout,
862        s3: &candle::MetalStorage,
863        l3: &Layout,
864    ) -> Result<(candle::MetalStorage, Shape)> {
865        use candle::backend::BackendStorage;
866        let device = s1.device();
867        let encoder = device.command_encoder()?;
868        encoder.set_label("layernorm");
869        let kernels = device.kernels();
870        let name = match (s1.dtype(), s2.dtype(), s3.dtype()) {
871            (DType::F32, DType::F32, DType::F32) => "layernorm_f32",
872            (DType::F16, DType::F16, DType::F16) => "layernorm_f16",
873            (DType::BF16, DType::BF16, DType::BF16) => "layernorm_bf16",
874            (dt1, dt2, dt3) => {
875                candle::bail!("layernorm is not implemented for {dt1:?} {dt2:?} {dt3:?}")
876            }
877        };
878
879        if !(l1.is_contiguous() && l2.is_contiguous() && l3.is_contiguous()) {
880            candle::bail!("Non contiguous layernorm is not implemented");
881        }
882
883        let last_dim = l1.dims()[l1.shape().rank() - 1];
884        let elem_count = l1.shape().elem_count();
885        let output = device
886            .new_buffer_builder()
887            .with_size_for(elem_count, s1.dtype())
888            .with_label("layernorm")
889            .build()?;
890        candle_metal_kernels::call_layer_norm(
891            device.metal_device(),
892            &encoder,
893            kernels,
894            name,
895            elem_count,
896            last_dim,
897            self.eps,
898            s1.buffer(),
899            l1.start_offset() * s1.dtype().size_in_bytes(),
900            s2.buffer(),
901            l2.start_offset() * s2.dtype().size_in_bytes(),
902            s3.buffer(),
903            l3.start_offset() * s3.dtype().size_in_bytes(),
904            &output,
905        )
906        .map_err(candle::Error::wrap)?;
907        let newstorage = candle::MetalStorage::new(output, device.clone(), elem_count, s1.dtype());
908        Ok((newstorage, l1.shape().clone()))
909    }
910}
911
912pub fn layer_norm_slow(x: &Tensor, alpha: &Tensor, beta: &Tensor, eps: f32) -> Result<Tensor> {
913    let x_dtype = x.dtype();
914    let internal_dtype = match x_dtype {
915        DType::F16 | DType::BF16 => DType::F32,
916        d => d,
917    };
918    let hidden_size = x.dim(D::Minus1)?;
919    let x = x.to_dtype(internal_dtype)?;
920    let x = {
921        let mean_x = (x.sum_keepdim(D::Minus1)? / hidden_size as f64)?;
922        x.broadcast_sub(&mean_x)?
923    };
924    let norm_x = (x.sqr()?.sum_keepdim(D::Minus1)? / hidden_size as f64)?;
925    let x_normed = x.broadcast_div(&(norm_x + eps as f64)?.sqrt()?)?;
926    x_normed
927        .to_dtype(x_dtype)?
928        .broadcast_mul(alpha)?
929        .broadcast_add(beta)
930}
931
932pub fn layer_norm(xs: &Tensor, alpha: &Tensor, beta: &Tensor, eps: f32) -> Result<Tensor> {
933    let hidden_size_xs = xs.dim(D::Minus1)?;
934    let hidden_size_alpha = alpha.dims1()?;
935    let hidden_size_beta = beta.dims1()?;
936    if hidden_size_xs != hidden_size_alpha || hidden_size_xs != hidden_size_beta {
937        candle::bail!(
938            "shape mismatch in layer-norm src: {:?} alpha: {:?} beta: {:?}",
939            xs.shape(),
940            alpha.shape(),
941            beta.shape()
942        )
943    }
944    xs.apply_op3_no_bwd(alpha, beta, &LayerNorm { eps })
945}
946
947// https://pytorch.org/docs/stable/generated/torch.nn.PixelShuffle.html
948pub fn pixel_shuffle(xs: &Tensor, upscale_factor: usize) -> Result<Tensor> {
949    let (b_size, c, h, w) = xs.dims4()?;
950    let out_c = c / upscale_factor / upscale_factor;
951    xs.reshape((b_size, out_c, upscale_factor, upscale_factor, h, w))?
952        .permute((0, 1, 4, 2, 5, 3))?
953        .reshape((b_size, out_c, h * upscale_factor, w * upscale_factor))
954}
955
956pub fn pixel_unshuffle(xs: &Tensor, downscale_factor: usize) -> Result<Tensor> {
957    let (b_size, c, h, w) = xs.dims4()?;
958    let out_c = c * downscale_factor * downscale_factor;
959    xs.reshape((
960        b_size,
961        c,
962        h / downscale_factor,
963        downscale_factor,
964        w / downscale_factor,
965        downscale_factor,
966    ))?
967    .permute((0, 1, 3, 5, 2, 4))?
968    .reshape((b_size, out_c, h / downscale_factor, w / downscale_factor))
969}
970
971// https://pytorch.org/docs/stable/generated/torch.nn.ReplicationPad2d.html
972pub fn replication_pad2d(xs: &Tensor, pad: usize) -> Result<Tensor> {
973    match pad {
974        0 => Ok(xs.clone()),
975        1 => {
976            let (_b_size, _c, h, w) = xs.dims4()?;
977            let (first, last) = (xs.narrow(3, 0, 1)?, xs.narrow(3, w - 1, 1)?);
978            let xs = Tensor::cat(&[&first, xs, &last], 3)?;
979            let (first, last) = (xs.narrow(2, 0, 1)?, xs.narrow(2, h - 1, 1)?);
980            Tensor::cat(&[&first, &xs, &last], 2)
981        }
982        n => candle::bail!("replication-pad with a size of {n} is not supported"),
983    }
984}
985
986#[derive(Clone, Debug)]
987pub struct Identity;
988
989impl Identity {
990    pub fn new() -> Identity {
991        Self
992    }
993}
994
995impl Default for Identity {
996    fn default() -> Self {
997        Self
998    }
999}
1000
1001impl Module for Identity {
1002    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
1003        Ok(xs.clone())
1004    }
1005}
1006
1007#[allow(dead_code)]
1008struct Sdpa {
1009    scale: f32,
1010    softcapping: f32,
1011    mask: Option<Tensor>,
1012    do_causal: bool,
1013}
1014
1015impl candle::CustomOp3 for Sdpa {
1016    fn name(&self) -> &'static str {
1017        "metal-sdpa"
1018    }
1019
1020    fn cpu_fwd(
1021        &self,
1022        _s1: &CpuStorage,
1023        _l1: &Layout,
1024        _s2: &CpuStorage,
1025        _l2: &Layout,
1026        _s3: &CpuStorage,
1027        _l3: &Layout,
1028    ) -> Result<(CpuStorage, Shape)> {
1029        candle::bail!("SDPA has no cpu impl")
1030    }
1031
1032    #[cfg(feature = "metal")]
1033    fn metal_fwd(
1034        &self,
1035        q: &candle::MetalStorage,
1036        q_l: &Layout,
1037        k: &candle::MetalStorage,
1038        k_l: &Layout,
1039        v: &candle::MetalStorage,
1040        v_l: &Layout,
1041    ) -> Result<(candle::MetalStorage, Shape)> {
1042        use candle::backend::BackendStorage;
1043        use candle_metal_kernels::SdpaDType;
1044
1045        let device = q.device();
1046
1047        let out_dims = vec![q_l.dim(0)?, q_l.dim(1)?, q_l.dim(2)?, v_l.dim(3)?];
1048        let elem_count: usize = out_dims.iter().product();
1049        let out_shape = Shape::from_dims(&out_dims);
1050        let out_layout = Layout::contiguous(out_shape.clone());
1051
1052        let output = device
1053            .new_buffer_builder()
1054            .with_size_for(elem_count, q.dtype())
1055            .with_label("sdpa_o")
1056            .build()?;
1057
1058        // q,k must have matching emb dim
1059        if q_l.dim(D::Minus1)? != k_l.dim(D::Minus1)? {
1060            candle::bail!("`q` and `k` last dims must match");
1061        }
1062
1063        // k,v must have matching n kv heads
1064        if v_l.dim(D::Minus(3))? != k_l.dim(D::Minus(3))? {
1065            candle::bail!("`k` and `v` head dims must match");
1066        }
1067
1068        // n_heads % n_kv_heads == 0; n_heads >= 1, n_kv_heads >= 1.
1069        if q_l.dim(D::Minus(3))? % k_l.dim(D::Minus(3))? != 0 {
1070            candle::bail!("query `n_heads` must be a multiple of `n_kv_heads`");
1071        }
1072
1073        let k_head = k_l.dim(D::Minus1)?;
1074        let q_head = q_l.dim(D::Minus1)?;
1075        let q_seq = q_l.dim(2)?;
1076        let k_seq = k_l.dim(2)?;
1077
1078        let mut implementation_supports_use_case = q_head == k_head;
1079        let supported_head_dim = q_head == 32
1080            || q_head == 64
1081            || q_head == 72
1082            || q_head == 80
1083            || q_head == 96
1084            || q_head == 128
1085            || q_head == 256
1086            || q_head == 512;
1087
1088        let supports_sdpa_full_mask = self.mask.is_none() || q_seq <= k_seq;
1089        // F32 full attention at head_dim=512 exceeds 32KB Metal threadgroup memory
1090        let supports_sdpa_full_dtype = !(q_head == 512 && q.dtype() == DType::F32);
1091        let supports_sdpa_full =
1092            q_seq > 1 && supported_head_dim && supports_sdpa_full_mask && supports_sdpa_full_dtype;
1093        let supports_sdpa_vector = q_seq == 1 && supported_head_dim && q_seq <= k_seq;
1094
1095        implementation_supports_use_case &= supports_sdpa_full || supports_sdpa_vector;
1096
1097        if !supported_head_dim {
1098            candle::bail!(
1099                "Meta SDPA does not support q head dim {q_head}: q dims {:?}, k dims {:?}, v dims {:?}.",
1100                q_l.dims(),
1101                k_l.dims(),
1102                v_l.dims()
1103            );
1104        }
1105        if !implementation_supports_use_case {
1106            candle::bail!(
1107                "Meta SDPA does not support q dims {:?}, k dims {:?}, v dims {:?}.",
1108                q_l.dims(),
1109                k_l.dims(),
1110                v_l.dims()
1111            );
1112        }
1113
1114        for t in [k.dtype(), v.dtype()] {
1115            if q.dtype() != t {
1116                candle::bail!("all q, k, v dtypes must match.");
1117            }
1118        }
1119
1120        let itype = match q.dtype() {
1121            DType::BF16 => SdpaDType::BF16,
1122            DType::F16 => SdpaDType::F16,
1123            DType::F32 => SdpaDType::F32,
1124            other => candle::bail!("unsupported sdpa type {other:?}"),
1125        };
1126
1127        let encoder = q.device().command_encoder()?;
1128        if supports_sdpa_vector {
1129            // Route to the 2 pass fused attention if the k seqlen is large.
1130            // https://github.com/ml-explore/mlx/pull/1597
1131            const TWO_PASS_K_THRESHOLD: usize = 1024;
1132            if k_seq >= TWO_PASS_K_THRESHOLD {
1133                let mut intermediate_shape = [
1134                    &out_dims[0..out_dims.len() - 2],
1135                    &[candle_metal_kernels::SDPA_2PASS_BLOCKS],
1136                    &[out_dims[out_dims.len() - 1]],
1137                ]
1138                .concat();
1139                let intermediate = device
1140                    .new_buffer_builder()
1141                    .with_size_for(intermediate_shape.iter().product::<usize>(), DType::F32)
1142                    .with_label("sdpa_2pass_intermediate")
1143                    .build()?;
1144                let _ = intermediate_shape.pop().unwrap();
1145                let sums = device
1146                    .new_buffer_builder()
1147                    .with_size_for(intermediate_shape.iter().product::<usize>(), DType::F32)
1148                    .with_label("sdpa_2pass_sums")
1149                    .build()?;
1150                let maxs = device
1151                    .new_buffer_builder()
1152                    .with_size_for(intermediate_shape.iter().product::<usize>(), DType::F32)
1153                    .with_label("sdpa_2pass_maxs")
1154                    .build()?;
1155
1156                encoder.set_label("vector_attention");
1157                candle_metal_kernels::call_sdpa_vector_2pass(
1158                    q.device().device(),
1159                    &encoder,
1160                    q.device().kernels(),
1161                    q_l.start_offset() * q.dtype().size_in_bytes(),
1162                    q_l.dims(),
1163                    q.buffer(),
1164                    k_l.start_offset() * k.dtype().size_in_bytes(),
1165                    k_l.dims(),
1166                    k_l.stride(),
1167                    k.buffer(),
1168                    v_l.start_offset() * v.dtype().size_in_bytes(),
1169                    v_l.stride(),
1170                    v.buffer(),
1171                    &output,
1172                    &intermediate,
1173                    &sums,
1174                    &maxs,
1175                    self.scale,
1176                    self.softcapping,
1177                    itype,
1178                )
1179                .map_err(candle::Error::wrap)?;
1180            } else {
1181                encoder.set_label("vector_attention");
1182                candle_metal_kernels::call_sdpa_vector(
1183                    q.device().device(),
1184                    &encoder,
1185                    q.device().kernels(),
1186                    q_l.start_offset() * q.dtype().size_in_bytes(),
1187                    q_l.dims(),
1188                    q.buffer(),
1189                    k_l.start_offset() * k.dtype().size_in_bytes(),
1190                    k_l.dims(),
1191                    k_l.stride(),
1192                    k.buffer(),
1193                    v_l.start_offset() * v.dtype().size_in_bytes(),
1194                    v_l.stride(),
1195                    v.buffer(),
1196                    &output,
1197                    self.scale,
1198                    self.softcapping,
1199                    itype,
1200                )
1201                .map_err(candle::Error::wrap)?;
1202            }
1203        } else if supports_sdpa_full {
1204            encoder.set_label("full_attention");
1205            if self.softcapping != 1. {
1206                candle::bail!("SDPA full requires softcapping to be disabled (1.0)");
1207            }
1208
1209            let mask_s_l = self.mask.as_ref().map(|m| m.storage_and_layout());
1210
1211            let (mask_type, mask_buffer, mask_strides) = if let Some(mask) = &self.mask {
1212                let (mask_s, mask_l) = mask_s_l.as_ref().unwrap();
1213
1214                let mask_buffer = match &**mask_s {
1215                    candle::Storage::Metal(m) => m.buffer(),
1216                    _ => candle::bail!("Expected metal device for mask"),
1217                };
1218
1219                let mask_type = match mask.dtype() {
1220                    DType::BF16 => SdpaDType::BF16,
1221                    DType::F16 => SdpaDType::F16,
1222                    DType::F32 => SdpaDType::F32,
1223                    other => candle::bail!("unsupported sdpa type {other:?}"),
1224                };
1225                if mask_type != itype {
1226                    candle::bail!("Mask type {mask_type:?} must match q type {itype:?}");
1227                }
1228
1229                if mask_l.dims() != [q_l.dim(0)?, q_l.dim(1)?, q_l.dim(2)?, k_seq] {
1230                    candle::bail!(
1231                        "Mask shape must be {:?} (bs, qheads, qseq, kseq), got {:?}",
1232                        [q_l.dim(0)?, q_head, q_l.dim(2)?, k_seq],
1233                        mask_l.dims()
1234                    );
1235                }
1236
1237                (
1238                    Some(mask_type),
1239                    Some(mask_buffer),
1240                    Some(mask_l.stride().to_vec()),
1241                )
1242            } else {
1243                (None, None, None)
1244            };
1245
1246            candle_metal_kernels::call_sdpa_full(
1247                q.device().device(),
1248                &encoder,
1249                q.device().kernels(),
1250                q_l.start_offset() * q.dtype().size_in_bytes(),
1251                q_l.dims(),
1252                q_l.stride(),
1253                q.buffer(),
1254                k_l.start_offset() * k.dtype().size_in_bytes(),
1255                k_l.dims(),
1256                k_l.stride(),
1257                k.buffer(),
1258                v_l.start_offset() * v.dtype().size_in_bytes(),
1259                v.buffer(),
1260                v_l.stride(),
1261                mask_type,
1262                mask_buffer,
1263                mask_strides.as_deref(),
1264                &output,
1265                out_layout.stride(),
1266                self.scale,
1267                self.do_causal,
1268                itype,
1269            )
1270            .map_err(candle::Error::wrap)?;
1271        } else {
1272            candle::bail!("must be vector or full sdpa kernel");
1273        }
1274
1275        let newstorage = candle::MetalStorage::new(output, device.clone(), elem_count, q.dtype());
1276        Ok((newstorage, out_shape))
1277    }
1278}
1279
1280/// Scaled dot product attention with a fused kernel.
1281///
1282/// Computes softmax(qk^T*scale)v.
1283///
1284/// **Inputs shapes:**
1285/// - `q`: (bs, qhead, seq, hidden)
1286/// - `k`: (bs, kv_head, kv_seq, hidden)
1287/// - `k`: (bs, kv_head, kv_seq, v_hidden)
1288/// - `mask`: (bs, qhead, seq, kv_seq)
1289/// - `do_causal`: Apply causal masking. If this is true, the mask does not need to be provided.
1290/// - `scale` is applied before softmax.
1291/// - If `softcapping` != 1.0:
1292///      - Computation is: softmax(tanh(qk^T*scale/cap)*cap)v
1293///
1294/// **Output shape:** (bs, qhead, seq, v_hidden)
1295///
1296/// Note: For Grouped Query Attention and Multi-Query Attention, the k and v inputs should not be pre-tiled to match q.
1297///
1298/// ## On Metal:
1299/// - If `seq` == 1:
1300///     - Use a vectorized kernel
1301///     - Supports `seq` != `kv_seq` (cross attn. support)
1302///     - Supports GQA when `qhead` is a multiple of `kv_head`
1303/// - Otherwise:
1304///     - Masking is supported
1305///     - Supports `seq` != `kv_seq` (cross attn. support)
1306///     - Supports GQA when `qhead` is a multiple of `kv_head`
1307///     - Softcapping is not supported.
1308pub fn sdpa(
1309    q: &Tensor,
1310    k: &Tensor,
1311    v: &Tensor,
1312    mask: Option<&Tensor>,
1313    do_causal: bool,
1314    scale: f32,
1315    softcapping: f32,
1316) -> Result<Tensor> {
1317    q.apply_op3_no_bwd(
1318        k,
1319        v,
1320        &Sdpa {
1321            scale,
1322            softcapping,
1323            mask: mask.cloned(),
1324            do_causal,
1325        },
1326    )
1327}