Skip to main content

hanzo_nn/
ops.rs

1//! Tensor ops.
2//!
3
4use hanzo_ml::{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 hanzo_ml::{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 = hanzo_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::<(), hanzo_ml::Error>(())
21/// ```
22pub fn softmax<D: hanzo_ml::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: hanzo_ml::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 hanzo_ml::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 hanzo_ml::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 `hanzo_ml::map_dtype` causes compilation errors.
64        let storage = match storage {
65            CpuStorage::BF16(slice) => {
66                CpuStorage::BF16(hanzo_ml::cpu_backend::unary_map(slice, layout, fwd))
67            }
68            CpuStorage::F16(slice) => {
69                CpuStorage::F16(hanzo_ml::cpu_backend::unary_map(slice, layout, fwd))
70            }
71            CpuStorage::F32(slice) => {
72                CpuStorage::F32(hanzo_ml::cpu_backend::unary_map(slice, layout, fwd))
73            }
74            CpuStorage::F64(slice) => {
75                CpuStorage::F64(hanzo_ml::cpu_backend::unary_map(slice, layout, fwd))
76            }
77            _ => Err(hanzo_ml::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: &hanzo_ml::CudaStorage,
89        layout: &Layout,
90    ) -> Result<(hanzo_ml::CudaStorage, Shape)> {
91        use hanzo_ml::backend::BackendStorage;
92        use hanzo_ml::cuda_backend::cudarc::driver::{
93            CudaSlice, DeviceRepr, LaunchConfig, PushKernelArg, ValidAsZeroBits,
94        };
95        use hanzo_ml::cuda_backend::SlicePtrOrNull;
96        use hanzo_ml::cuda_backend::{kernel_name, kernels, Map1, WrapErr};
97        use hanzo_ml::{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                hanzo_ml::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 = hanzo_ml::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: &hanzo_ml::MetalStorage,
141        layout: &Layout,
142    ) -> Result<(hanzo_ml::MetalStorage, Shape)> {
143        use hanzo_ml::backend::BackendStorage;
144        use hanzo_ml::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.new_buffer(el_count, dtype, "sigmoid")?;
150        let encoder = device.command_encoder()?;
151        encoder.set_label("sigmoid");
152        let src = hanzo_metal_kernels::BufferOffset {
153            buffer: storage.buffer(),
154            offset_in_bytes: layout.start_offset() * storage.dtype().size_in_bytes(),
155        };
156
157        if layout.is_contiguous() {
158            use hanzo_metal_kernels::unary::contiguous;
159            let kernel_name = match dtype {
160                DType::F16 => contiguous::sigmoid::HALF,
161                DType::F32 => contiguous::sigmoid::FLOAT,
162                DType::BF16 => contiguous::sigmoid::BFLOAT,
163                dtype => {
164                    hanzo_ml::bail!("Metal contiguous unary sigmoid {dtype:?} not implemented")
165                }
166            };
167            hanzo_metal_kernels::call_unary_contiguous(
168                device.metal_device(),
169                &encoder,
170                device.kernels(),
171                kernel_name,
172                dtype.size_in_bytes(),
173                el_count,
174                src,
175                &buffer,
176            )
177            .map_err(MetalError::from)?;
178        } else {
179            use hanzo_metal_kernels::unary::strided;
180            let kernel_name = match dtype {
181                DType::F16 => strided::sigmoid::HALF,
182                DType::F32 => strided::sigmoid::FLOAT,
183                DType::BF16 => strided::sigmoid::BFLOAT,
184                dtype => {
185                    hanzo_ml::bail!("Metal strided unary sigmoid {dtype:?} not implemented")
186                }
187            };
188            let dst = hanzo_metal_kernels::BufferOffset::zero_offset(&buffer);
189            hanzo_metal_kernels::call_unary_strided(
190                device.metal_device(),
191                &encoder,
192                device.kernels(),
193                kernel_name,
194                layout.dims(),
195                src,
196                layout.stride(),
197                dst,
198            )
199            .map_err(MetalError::from)?;
200        }
201
202        let new_storage = hanzo_ml::MetalStorage::new(buffer, device.clone(), el_count, dtype);
203        Ok((new_storage, layout.shape().clone()))
204    }
205
206    fn bwd(&self, _arg: &Tensor, res: &Tensor, grad_res: &Tensor) -> Result<Option<Tensor>> {
207        // d/dx sigmoid(x) = (1 - sigmoid(x)) * sigmoid(x)
208        let d_dx_sigmoid = res.ones_like()?.sub(res)?.mul(res)?;
209        Ok(Some(grad_res.mul(&d_dx_sigmoid)?))
210    }
211}
212
213pub fn sigmoid(xs: &Tensor) -> Result<Tensor> {
214    xs.apply_op1(Sigmoid)
215}
216
217pub fn hard_sigmoid(xs: &Tensor) -> Result<Tensor> {
218    // TODO: Should we have a specialized op for this?
219    ((xs + 3.0)? / 6.0)?.clamp(0f32, 1f32)
220}
221
222pub fn mish(xs: &Tensor) -> Result<Tensor> {
223    xs * (1.0 + xs.exp()?)?.log()?.tanh()
224}
225
226pub fn leaky_relu(xs: &Tensor, negative_slope: f64) -> Result<Tensor> {
227    let zeros = xs.zeros_like()?;
228    xs.maximum(&zeros)? + xs.minimum(&zeros)? * negative_slope
229}
230
231pub fn selu(xs: &Tensor, alpha: f32, gamma: f32) -> Result<Tensor> {
232    let is_pos = xs.gt(0f32)?;
233    let alpha_t = Tensor::full(alpha, xs.dims(), xs.device())?;
234    let neg = xs.exp()?.mul(&alpha_t)?.sub(&alpha_t)?;
235    let selu = is_pos.where_cond(xs, &neg)?;
236    let gamma_t = Tensor::full(gamma, xs.dims(), xs.device())?;
237    selu.broadcast_mul(&gamma_t)
238}
239
240pub fn dropout(xs: &Tensor, drop_p: f32) -> Result<Tensor> {
241    // This implementation is inefficient as it stores the full mask for the backward pass.
242    // Instead we could just store the seed and have a specialized kernel that would both
243    // generate the random mask and apply it.
244    // Another easier optimization would be to be able to generate boolean mask using just a bit of
245    // entropy per element rather than generating a full float per element.
246    if !(0. ..1.).contains(&drop_p) {
247        hanzo_ml::bail!("dropout probability has to be in [0, 1), got {drop_p}")
248    }
249    let rand = Tensor::rand(0f32, 1f32, xs.shape(), xs.device())?;
250    let scale = 1.0 / (1.0 - drop_p as f64);
251    let drop_p = Tensor::new(drop_p, xs.device())?.broadcast_as(xs.shape())?;
252    let mask = (rand.ge(&drop_p)?.to_dtype(xs.dtype())? * scale)?;
253    xs * mask
254}
255
256#[derive(Clone, Debug)]
257pub struct Dropout {
258    drop_p: f32,
259}
260
261impl Dropout {
262    pub fn new(drop_p: f32) -> Dropout {
263        Self { drop_p }
264    }
265
266    pub fn forward(&self, xs: &Tensor, train: bool) -> Result<Tensor> {
267        if train {
268            dropout(xs, self.drop_p)
269        } else {
270            Ok(xs.clone())
271        }
272    }
273}
274
275impl hanzo_ml::ModuleT for Dropout {
276    fn forward_t(&self, xs: &Tensor, train: bool) -> Result<Tensor> {
277        self.forward(xs, train)
278    }
279}
280
281struct SoftmaxLastDim;
282
283impl hanzo_ml::CustomOp1 for SoftmaxLastDim {
284    fn name(&self) -> &'static str {
285        "softmax-last-dim"
286    }
287
288    fn cpu_fwd(&self, storage: &CpuStorage, layout: &Layout) -> Result<(CpuStorage, Shape)> {
289        fn softmax<T: hanzo_ml::WithDType + num_traits::Float>(
290            src: &[T],
291            layout: &Layout,
292        ) -> Result<(CpuStorage, Shape)> {
293            let src = match layout.contiguous_offsets() {
294                None => hanzo_ml::bail!("input has to be contiguous"),
295                Some((o1, o2)) => &src[o1..o2],
296            };
297            let el_count = layout.shape().elem_count();
298            let dims = layout.shape().dims();
299            let dim_m1 = dims[dims.len() - 1];
300            let mut dst = vec![T::zero(); el_count];
301            src.par_chunks(dim_m1)
302                .zip(dst.par_chunks_mut(dim_m1))
303                .for_each(|(src, dst)| {
304                    let mut max = T::neg_infinity();
305                    unsafe { T::vec_reduce_max(src.as_ptr(), &mut max, dim_m1) };
306                    for (s, d) in src.iter().zip(dst.iter_mut()) {
307                        *d = (*s - max).exp();
308                    }
309                    let mut sum_exp = T::zero();
310                    unsafe { T::vec_reduce_sum(dst.as_ptr(), &mut sum_exp, dim_m1) };
311                    for d in dst.iter_mut() {
312                        *d /= sum_exp
313                    }
314                });
315            let storage = hanzo_ml::WithDType::to_cpu_storage_owned(dst);
316            Ok((storage, Shape::from_dims(dims)))
317        }
318
319        match storage {
320            CpuStorage::BF16(slice) => softmax::<half::bf16>(slice, layout),
321            CpuStorage::F16(slice) => softmax::<half::f16>(slice, layout),
322            CpuStorage::F32(slice) => softmax::<f32>(slice, layout),
323            CpuStorage::F64(slice) => softmax::<f64>(slice, layout),
324            _ => hanzo_ml::bail!("unsupported dtype for softmax {:?}", storage),
325        }
326    }
327
328    #[cfg(feature = "vulkan")]
329    fn vulkan_fwd(
330        &self,
331        storage: &hanzo_ml::VulkanStorage,
332        layout: &Layout,
333    ) -> Result<(hanzo_ml::VulkanStorage, Shape)> {
334        let out = storage.softmax_last_dim(layout)?;
335        Ok((out, layout.shape().clone()))
336    }
337
338    #[cfg(feature = "rocm")]
339    fn rocm_fwd(
340        &self,
341        storage: &hanzo_ml::RocmStorage,
342        layout: &Layout,
343    ) -> Result<(hanzo_ml::RocmStorage, Shape)> {
344        let out = storage.softmax_last_dim(layout)?;
345        Ok((out, layout.shape().clone()))
346    }
347
348    #[cfg(feature = "cuda")]
349    fn cuda_fwd(
350        &self,
351        storage: &hanzo_ml::CudaStorage,
352        layout: &Layout,
353    ) -> Result<(hanzo_ml::CudaStorage, Shape)> {
354        use hanzo_ml::cuda_backend::cudarc::driver::{
355            CudaSlice, DeviceRepr, LaunchConfig, PushKernelArg,
356        };
357        use hanzo_ml::cuda_backend::{kernel_name, kernels, Map1, WrapErr};
358        use hanzo_ml::{CudaDevice, WithDType};
359
360        struct S;
361        impl Map1 for S {
362            fn f<T: DeviceRepr + WithDType>(
363                &self,
364                src: &CudaSlice<T>,
365                dev: &CudaDevice,
366                layout: &Layout,
367            ) -> Result<CudaSlice<T>> {
368                let src = match layout.contiguous_offsets() {
369                    None => hanzo_ml::bail!("input has to be contiguous"),
370                    Some((o1, o2)) => src.slice(o1..o2),
371                };
372                let el = layout.shape().elem_count();
373                let dims = layout.shape().dims();
374                let dim_m1 = dims[dims.len() - 1];
375                let (n_rows, n_cols) = (el / dim_m1, dim_m1);
376
377                let cfg = LaunchConfig {
378                    grid_dim: (n_rows as u32, 1, 1),
379                    block_dim: (1, 32, 1),
380                    shared_mem_bytes: 0,
381                };
382                let func = dev.get_or_load_func(&kernel_name::<T>("softmax"), &kernels::REDUCE)?;
383                // SAFETY: Set later by running the kernel.
384                let dst = unsafe { dev.alloc::<T>(el)? };
385                let mut builder = func.builder();
386                builder.arg(&src);
387                builder.arg(&dst);
388                hanzo_ml::builder_arg!(builder, n_cols as i32);
389                // SAFETY: ffi.
390                unsafe { builder.launch(cfg) }.w()?;
391                Ok(dst)
392            }
393        }
394
395        use hanzo_ml::backend::BackendStorage;
396        let dev = storage.device();
397        let slice = S.map(&storage.slice, dev, layout)?;
398        let dst = hanzo_ml::cuda_backend::CudaStorage {
399            slice,
400            device: dev.clone(),
401        };
402        Ok((dst, layout.shape().clone()))
403    }
404
405    #[cfg(feature = "metal")]
406    fn metal_fwd(
407        &self,
408        storage: &hanzo_ml::MetalStorage,
409        layout: &Layout,
410    ) -> Result<(hanzo_ml::MetalStorage, Shape)> {
411        use hanzo_ml::backend::BackendStorage;
412        let device = storage.device();
413        let encoder = device.command_encoder()?;
414        encoder.set_label("softmax");
415        let kernels = device.kernels();
416        let name = match storage.dtype() {
417            DType::F32 => "softmax_f32",
418            DType::F16 => "softmax_f16",
419            DType::BF16 => "softmax_bf16",
420            dtype => hanzo_ml::bail!("softmax-last-dim is not implemented for {dtype:?}"),
421        };
422
423        let n = layout.stride().len();
424        if !(layout.is_contiguous() && layout.stride()[n - 1] == 1) {
425            hanzo_ml::bail!("Non contiguous softmax-last-dim is not implemented");
426        }
427
428        let last_dim = layout.dims()[layout.shape().rank() - 1];
429        let elem_count = layout.shape().elem_count();
430        let output = device.new_buffer(elem_count, storage.dtype(), "softmax")?;
431        hanzo_metal_kernels::call_last_softmax(
432            device.metal_device(),
433            &encoder,
434            kernels,
435            name,
436            elem_count,
437            last_dim,
438            storage.buffer(),
439            layout.start_offset() * storage.dtype().size_in_bytes(),
440            &output,
441        )
442        .map_err(hanzo_ml::Error::wrap)?;
443        let newstorage =
444            hanzo_ml::MetalStorage::new(output, device.clone(), elem_count, storage.dtype());
445        Ok((newstorage, layout.shape().clone()))
446    }
447}
448
449pub fn softmax_last_dim(xs: &Tensor) -> Result<Tensor> {
450    // The fused rocm softmax kernel handles contiguous F16/F32/BF16; fall back to the unfused
451    // composite for anything else so we never feed the kernel a shape/dtype it can't handle.
452    if xs.device().is_rocm() {
453        let supported =
454            matches!(xs.dtype(), DType::F16 | DType::F32 | DType::BF16) && xs.is_contiguous();
455        if !supported {
456            return softmax(xs, D::Minus1);
457        }
458    }
459    xs.apply_op1_no_bwd(&SoftmaxLastDim)
460}
461
462#[derive(Debug, Clone)]
463struct RmsNorm {
464    eps: f32,
465}
466
467impl hanzo_ml::CustomOp2 for RmsNorm {
468    fn name(&self) -> &'static str {
469        "rms-norm"
470    }
471
472    fn cpu_fwd(
473        &self,
474        s1: &CpuStorage,
475        l1: &Layout,
476        s2: &CpuStorage,
477        l2: &Layout,
478    ) -> Result<(CpuStorage, Shape)> {
479        use hanzo_ml::backend::BackendStorage;
480
481        let eps = self.eps;
482        fn inner<
483            T: hanzo_ml::WithDType
484                + num_traits::Float
485                + num_traits::AsPrimitive<f32>
486                + num_traits::FromPrimitive,
487        >(
488            src: &[T],
489            layout: &Layout,
490            alpha: &[T],
491            alpha_layout: &Layout,
492            eps: f32,
493        ) -> Result<(CpuStorage, Shape)> {
494            let src = match layout.contiguous_offsets() {
495                None => hanzo_ml::bail!("input has to be contiguous"),
496                Some((o1, o2)) => &src[o1..o2],
497            };
498            let alpha = match alpha_layout.contiguous_offsets() {
499                None => hanzo_ml::bail!("alpha has to be contiguous"),
500                Some((o1, o2)) => &alpha[o1..o2],
501            };
502            let el_count = layout.shape().elem_count();
503            let dims = layout.shape().dims();
504            let dim_m1 = dims[dims.len() - 1];
505            let mut dst = vec![T::zero(); el_count];
506            src.par_chunks(dim_m1)
507                .zip(dst.par_chunks_mut(dim_m1))
508                .for_each(|(src, dst)| {
509                    let sum2 = src
510                        .iter()
511                        .map(|&v| {
512                            let v = v.as_();
513                            v * v
514                        })
515                        .sum::<f32>();
516                    let m = (sum2 / dim_m1 as f32 + eps).sqrt();
517                    let m = T::from_f32(m).unwrap_or_else(T::nan);
518                    for ((d, s), alpha) in dst.iter_mut().zip(src.iter()).zip(alpha) {
519                        *d = *s / m * *alpha
520                    }
521                });
522            let storage = hanzo_ml::WithDType::to_cpu_storage_owned(dst);
523            Ok((storage, Shape::from_dims(dims)))
524        }
525
526        use CpuStorage as C;
527        match (s1, s2) {
528            (C::BF16(s1), C::BF16(s2)) => inner::<half::bf16>(s1, l1, s2, l2, eps),
529            (C::F16(s1), C::F16(s2)) => inner::<half::f16>(s1, l1, s2, l2, eps),
530            (C::F32(s1), C::F32(s2)) => inner::<f32>(s1, l1, s2, l2, eps),
531            _ => hanzo_ml::bail!("unsupported dtype for rmsnorm {:?}", s1.dtype()),
532        }
533    }
534
535    #[cfg(feature = "vulkan")]
536    fn vulkan_fwd(
537        &self,
538        s1: &hanzo_ml::VulkanStorage,
539        l1: &Layout,
540        s2: &hanzo_ml::VulkanStorage,
541        l2: &Layout,
542    ) -> Result<(hanzo_ml::VulkanStorage, Shape)> {
543        let out = s1.rms_norm(l1, s2, l2, self.eps)?;
544        Ok((out, l1.shape().clone()))
545    }
546
547    #[cfg(feature = "cuda")]
548    fn cuda_fwd(
549        &self,
550        s1: &hanzo_ml::CudaStorage,
551        l1: &Layout,
552        s2: &hanzo_ml::CudaStorage,
553        l2: &Layout,
554    ) -> Result<(hanzo_ml::CudaStorage, Shape)> {
555        use hanzo_ml::cuda_backend::cudarc::driver::{
556            CudaSlice, DeviceRepr, LaunchConfig, PushKernelArg,
557        };
558        use hanzo_ml::cuda_backend::{kernel_name, kernels, Map2, WrapErr};
559        use hanzo_ml::{CudaDevice, WithDType};
560
561        struct S {
562            eps: f32,
563        }
564        impl Map2 for S {
565            fn f<T: DeviceRepr + WithDType>(
566                &self,
567                src: &CudaSlice<T>,
568                layout: &Layout,
569                alpha: &CudaSlice<T>,
570                alpha_layout: &Layout,
571                dev: &CudaDevice,
572            ) -> Result<CudaSlice<T>> {
573                let src = match layout.contiguous_offsets() {
574                    None => hanzo_ml::bail!("input has to be contiguous"),
575                    Some((o1, o2)) => src.slice(o1..o2),
576                };
577                let alpha = match alpha_layout.contiguous_offsets() {
578                    None => hanzo_ml::bail!("alpha has to be contiguous"),
579                    Some((o1, o2)) => alpha.slice(o1..o2),
580                };
581                let el = layout.shape().elem_count();
582                let dims = layout.shape().dims();
583                let dim_m1 = dims[dims.len() - 1];
584                let (n_rows, n_cols) = (el / dim_m1, dim_m1);
585
586                let block_size = if n_cols < 1024 { 32 } else { 1024 };
587                let cfg = LaunchConfig {
588                    grid_dim: (n_rows as u32, 1, 1),
589                    block_dim: (block_size, 1, 1),
590                    shared_mem_bytes: 0,
591                };
592                let func = dev.get_or_load_func(&kernel_name::<T>("rmsnorm"), &kernels::REDUCE)?;
593                // SAFETY: Set later by running the kernel.
594                let dst = unsafe { dev.alloc::<T>(el)? };
595                let mut builder = func.builder();
596                builder.arg(&src);
597                builder.arg(&dst);
598                builder.arg(&alpha);
599                hanzo_ml::builder_arg!(builder, n_cols as i32, block_size as i32, self.eps);
600                // SAFETY: ffi.
601                unsafe { builder.launch(cfg) }.w()?;
602                Ok(dst)
603            }
604        }
605
606        use hanzo_ml::backend::BackendStorage;
607        let dev = s1.device();
608        let slice = S { eps: self.eps }.map(&s1.slice, l1, &s2.slice, l2, dev)?;
609        let dst = hanzo_ml::cuda_backend::CudaStorage {
610            slice,
611            device: dev.clone(),
612        };
613        Ok((dst, l1.shape().clone()))
614    }
615
616    #[cfg(feature = "rocm")]
617    fn rocm_fwd(
618        &self,
619        s1: &hanzo_ml::RocmStorage,
620        l1: &Layout,
621        s2: &hanzo_ml::RocmStorage,
622        l2: &Layout,
623    ) -> Result<(hanzo_ml::RocmStorage, Shape)> {
624        let out = s1.rms_norm(l1, s2, l2, self.eps)?;
625        Ok((out, l1.shape().clone()))
626    }
627
628    #[cfg(feature = "metal")]
629    fn metal_fwd(
630        &self,
631        s1: &hanzo_ml::MetalStorage,
632        l1: &Layout,
633        s2: &hanzo_ml::MetalStorage,
634        l2: &Layout,
635    ) -> Result<(hanzo_ml::MetalStorage, Shape)> {
636        use hanzo_ml::backend::BackendStorage;
637        let device = s1.device();
638        let encoder = device.command_encoder()?;
639        encoder.set_label("rmsnorm");
640        let kernels = device.kernels();
641        let name = match (s1.dtype(), s2.dtype()) {
642            (DType::F32, DType::F32) => "rmsnorm_f32",
643            (DType::F16, DType::F16) => "rmsnorm_f16",
644            (DType::BF16, DType::BF16) => "rmsnorm_bf16",
645            (dt1, dt2) => hanzo_ml::bail!("rmsnorm is not implemented for {dt1:?} {dt2:?}"),
646        };
647
648        if !(l1.is_contiguous() && l2.is_contiguous()) {
649            hanzo_ml::bail!("Non contiguous rmsnorm is not implemented");
650        }
651
652        let last_dim = l1.dims()[l1.shape().rank() - 1];
653        let elem_count = l1.shape().elem_count();
654        let output = device.new_buffer(elem_count, s1.dtype(), "rmsnorm")?;
655        hanzo_metal_kernels::call_rms_norm(
656            device.metal_device(),
657            &encoder,
658            kernels,
659            name,
660            elem_count,
661            last_dim,
662            self.eps,
663            s1.buffer(),
664            l1.start_offset() * s1.dtype().size_in_bytes(),
665            s2.buffer(),
666            l2.start_offset() * s2.dtype().size_in_bytes(),
667            &output,
668        )
669        .map_err(hanzo_ml::Error::wrap)?;
670        let newstorage =
671            hanzo_ml::MetalStorage::new(output, device.clone(), elem_count, s1.dtype());
672        Ok((newstorage, l1.shape().clone()))
673    }
674}
675
676pub fn rms_norm_slow(x: &Tensor, alpha: &Tensor, eps: f32) -> Result<Tensor> {
677    let x_dtype = x.dtype();
678    let internal_dtype = match x_dtype {
679        DType::F16 | DType::BF16 => DType::F32,
680        d => d,
681    };
682    let hidden_size = x.dim(D::Minus1)?;
683    let x = x.to_dtype(internal_dtype)?;
684    let norm_x = (x.sqr()?.sum_keepdim(D::Minus1)? / hidden_size as f64)?;
685    let x_normed = x.broadcast_div(&(norm_x + eps as f64)?.sqrt()?)?;
686    x_normed.to_dtype(x_dtype)?.broadcast_mul(alpha)
687}
688
689pub fn rms_norm(xs: &Tensor, alpha: &Tensor, eps: f32) -> Result<Tensor> {
690    let hidden_size_xs = xs.dim(D::Minus1)?;
691    let hidden_size_alpha = alpha.dims1()?;
692    if hidden_size_xs != hidden_size_alpha {
693        hanzo_ml::bail!(
694            "shape mismatch in rms-norm {:?} {:?}",
695            xs.shape(),
696            alpha.shape()
697        )
698    }
699    // The fused CustomOp2 (cpu/cuda/metal/rocm) expects alpha to live on the same device
700    // (and, for the gpu kernels, the same dtype) as xs. RmsNorm weights are frequently held
701    // on CPU / in a different dtype, which used to surface as the intermittent
702    // "device mismatch in rms-norm Cpu vs Rocm" flake. Normalize alpha up front.
703    let alpha = if alpha.device().same_device(xs.device()) {
704        alpha.clone()
705    } else {
706        alpha.to_device(xs.device())?
707    };
708    let alpha = if alpha.dtype() == xs.dtype() {
709        alpha
710    } else {
711        alpha.to_dtype(xs.dtype())?
712    };
713    // The fused rocm kernel only handles contiguous F16/F32; fall back to the unfused
714    // composite (rms_norm_slow) for anything else so we never feed the kernel a shape/dtype
715    // it can't handle.
716    if xs.device().is_rocm() {
717        let supported = matches!(xs.dtype(), DType::F16 | DType::F32)
718            && xs.is_contiguous()
719            && alpha.is_contiguous();
720        if !supported {
721            return rms_norm_slow(xs, &alpha, eps);
722        }
723    }
724    xs.apply_op2_no_bwd(&alpha, &RmsNorm { eps })
725}
726
727// Fused SwiGLU: silu(a) * b, elementwise (both same shape). One op instead of silu + mul.
728struct SiluMul;
729
730impl hanzo_ml::CustomOp2 for SiluMul {
731    fn name(&self) -> &'static str {
732        "silu-mul"
733    }
734
735    fn cpu_fwd(
736        &self,
737        s1: &CpuStorage,
738        l1: &Layout,
739        s2: &CpuStorage,
740        l2: &Layout,
741    ) -> Result<(CpuStorage, Shape)> {
742        fn inner<
743            T: hanzo_ml::WithDType
744                + num_traits::Float
745                + num_traits::AsPrimitive<f32>
746                + num_traits::FromPrimitive,
747        >(
748            a: &[T],
749            la: &Layout,
750            b: &[T],
751            lb: &Layout,
752        ) -> Result<(CpuStorage, Shape)> {
753            let a = match la.contiguous_offsets() {
754                Some((o1, o2)) => &a[o1..o2],
755                None => hanzo_ml::bail!("silu-mul: a must be contiguous"),
756            };
757            let b = match lb.contiguous_offsets() {
758                Some((o1, o2)) => &b[o1..o2],
759                None => hanzo_ml::bail!("silu-mul: b must be contiguous"),
760            };
761            let dst: Vec<T> = a
762                .iter()
763                .zip(b.iter())
764                .map(|(&x, &y)| {
765                    let xf = x.as_();
766                    T::from_f32(xf / (1.0 + (-xf).exp()) * y.as_()).unwrap_or_else(T::nan)
767                })
768                .collect();
769            Ok((
770                hanzo_ml::WithDType::to_cpu_storage_owned(dst),
771                Shape::from_dims(la.shape().dims()),
772            ))
773        }
774        use hanzo_ml::backend::BackendStorage;
775        use CpuStorage as C;
776        match (s1, s2) {
777            (C::BF16(a), C::BF16(b)) => inner::<half::bf16>(a, l1, b, l2),
778            (C::F16(a), C::F16(b)) => inner::<half::f16>(a, l1, b, l2),
779            (C::F32(a), C::F32(b)) => inner::<f32>(a, l1, b, l2),
780            _ => hanzo_ml::bail!("silu-mul: unsupported dtype {:?}", s1.dtype()),
781        }
782    }
783
784    #[cfg(feature = "vulkan")]
785    fn vulkan_fwd(
786        &self,
787        s1: &hanzo_ml::VulkanStorage,
788        l1: &Layout,
789        s2: &hanzo_ml::VulkanStorage,
790        l2: &Layout,
791    ) -> Result<(hanzo_ml::VulkanStorage, Shape)> {
792        let out = s1.silu_mul(l1, s2, l2)?;
793        Ok((out, l1.shape().clone()))
794    }
795
796    #[cfg(feature = "rocm")]
797    fn rocm_fwd(
798        &self,
799        s1: &hanzo_ml::RocmStorage,
800        l1: &Layout,
801        s2: &hanzo_ml::RocmStorage,
802        l2: &Layout,
803    ) -> Result<(hanzo_ml::RocmStorage, Shape)> {
804        let out = s1.silu_mul(l1, s2, l2)?;
805        Ok((out, l1.shape().clone()))
806    }
807}
808
809/// Fused SwiGLU: `silu(gate) * up`. Falls back to the unfused tensor ops where there's no kernel.
810pub fn silu_mul(gate: &Tensor, up: &Tensor) -> Result<Tensor> {
811    if gate.device().is_cuda() || gate.device().is_metal() {
812        // No fused kernel on these yet; compose (silu then mul).
813        return silu(gate)?.mul(up);
814    }
815    // The fused rocm kernel only handles contiguous F16/F32/BF16 of identical shape; fall back to
816    // the unfused composite otherwise so we never feed it a shape/dtype it can't handle.
817    if gate.device().is_rocm() {
818        let supported = matches!(gate.dtype(), DType::F16 | DType::F32 | DType::BF16)
819            && gate.dtype() == up.dtype()
820            && gate.is_contiguous()
821            && up.is_contiguous()
822            && gate.shape() == up.shape();
823        if !supported {
824            return silu(gate)?.mul(up);
825        }
826    }
827    gate.apply_op2_no_bwd(up, &SiluMul)
828}
829
830#[derive(Debug, Clone)]
831struct LayerNorm {
832    eps: f32,
833}
834
835impl hanzo_ml::CustomOp3 for LayerNorm {
836    fn name(&self) -> &'static str {
837        "layer-norm"
838    }
839
840    fn cpu_fwd(
841        &self,
842        s1: &CpuStorage,
843        l1: &Layout,
844        s2: &CpuStorage,
845        l2: &Layout,
846        s3: &CpuStorage,
847        l3: &Layout,
848    ) -> Result<(CpuStorage, Shape)> {
849        use hanzo_ml::backend::BackendStorage;
850
851        let eps = self.eps;
852        fn inner<
853            T: hanzo_ml::WithDType
854                + num_traits::Float
855                + num_traits::AsPrimitive<f32>
856                + num_traits::FromPrimitive,
857        >(
858            src: &[T],
859            layout: &Layout,
860            alpha: &[T],
861            alpha_layout: &Layout,
862            beta: &[T],
863            beta_layout: &Layout,
864            eps: f32,
865        ) -> Result<(CpuStorage, Shape)> {
866            let src = match layout.contiguous_offsets() {
867                None => hanzo_ml::bail!("input has to be contiguous"),
868                Some((o1, o2)) => &src[o1..o2],
869            };
870            let alpha = match alpha_layout.contiguous_offsets() {
871                None => hanzo_ml::bail!("alpha has to be contiguous"),
872                Some((o1, o2)) => &alpha[o1..o2],
873            };
874            let beta = match beta_layout.contiguous_offsets() {
875                None => hanzo_ml::bail!("beta has to be contiguous"),
876                Some((o1, o2)) => &beta[o1..o2],
877            };
878            let el_count = layout.shape().elem_count();
879            let dims = layout.shape().dims();
880            let dim_m1 = dims[dims.len() - 1];
881            let mut dst = vec![T::zero(); el_count];
882            src.par_chunks(dim_m1)
883                .zip(dst.par_chunks_mut(dim_m1))
884                .for_each(|(src, dst)| {
885                    let mut sum = 0f32;
886                    let mut sum2 = 0f32;
887                    for v in src {
888                        let v = v.as_();
889                        sum += v;
890                        sum2 += v * v;
891                    }
892                    let mean = sum / dim_m1 as f32;
893                    let var = sum2 / dim_m1 as f32 - mean * mean;
894                    let inv_std = (var + eps).sqrt().recip();
895                    for ((d, s), (alpha, beta)) in
896                        dst.iter_mut().zip(src.iter()).zip(alpha.iter().zip(beta))
897                    {
898                        let alpha = alpha.as_();
899                        let beta = beta.as_();
900                        let d_ = (s.as_() - mean) * inv_std * alpha + beta;
901                        *d = T::from_f32(d_).unwrap_or_else(T::nan);
902                    }
903                });
904            let storage = hanzo_ml::WithDType::to_cpu_storage_owned(dst);
905            Ok((storage, Shape::from_dims(dims)))
906        }
907
908        use CpuStorage as C;
909        match (s1, s2, s3) {
910            (C::BF16(s1), C::BF16(s2), C::BF16(s3)) => {
911                inner::<half::bf16>(s1, l1, s2, l2, s3, l3, eps)
912            }
913            (C::F16(s1), C::F16(s2), C::F16(s3)) => inner::<half::f16>(s1, l1, s2, l2, s3, l3, eps),
914            (C::F32(s1), C::F32(s2), C::F32(s3)) => inner::<f32>(s1, l1, s2, l2, s3, l3, eps),
915            _ => hanzo_ml::bail!("unsupported dtype for rmsnorm {:?}", s1.dtype()),
916        }
917    }
918
919    #[cfg(feature = "cuda")]
920    fn cuda_fwd(
921        &self,
922        s1: &hanzo_ml::CudaStorage,
923        l1: &Layout,
924        s2: &hanzo_ml::CudaStorage,
925        l2: &Layout,
926        s3: &hanzo_ml::CudaStorage,
927        l3: &Layout,
928    ) -> Result<(hanzo_ml::CudaStorage, Shape)> {
929        use hanzo_ml::cuda_backend::cudarc::driver::{
930            CudaSlice, DeviceRepr, LaunchConfig, PushKernelArg,
931        };
932        use hanzo_ml::cuda_backend::{kernel_name, kernels, Map3, WrapErr};
933        use hanzo_ml::{CudaDevice, WithDType};
934
935        struct S {
936            eps: f32,
937        }
938        impl Map3 for S {
939            fn f<T: DeviceRepr + WithDType>(
940                &self,
941                src: &CudaSlice<T>,
942                layout: &Layout,
943                alpha: &CudaSlice<T>,
944                alpha_layout: &Layout,
945                beta: &CudaSlice<T>,
946                beta_layout: &Layout,
947                dev: &CudaDevice,
948            ) -> Result<CudaSlice<T>> {
949                let src = match layout.contiguous_offsets() {
950                    None => hanzo_ml::bail!("input has to be contiguous"),
951                    Some((o1, o2)) => src.slice(o1..o2),
952                };
953                let alpha = match alpha_layout.contiguous_offsets() {
954                    None => hanzo_ml::bail!("alpha has to be contiguous"),
955                    Some((o1, o2)) => alpha.slice(o1..o2),
956                };
957                let beta = match beta_layout.contiguous_offsets() {
958                    None => hanzo_ml::bail!("beta has to be contiguous"),
959                    Some((o1, o2)) => beta.slice(o1..o2),
960                };
961                let el = layout.shape().elem_count();
962                let dims = layout.shape().dims();
963                let dim_m1 = dims[dims.len() - 1];
964                let (n_rows, n_cols) = (el / dim_m1, dim_m1);
965
966                let block_size = if n_cols < 1024 { 32 } else { 1024 };
967                let cfg = LaunchConfig {
968                    grid_dim: (n_rows as u32, 1, 1),
969                    block_dim: (block_size, 1, 1),
970                    shared_mem_bytes: 0,
971                };
972                let func =
973                    dev.get_or_load_func(&kernel_name::<T>("layernorm"), &kernels::REDUCE)?;
974                // SAFETY: Set later by running the kernel.
975                let dst = unsafe { dev.alloc::<T>(el)? };
976                let mut builder = func.builder();
977                builder.arg(&src);
978                builder.arg(&dst);
979                builder.arg(&alpha);
980                builder.arg(&beta);
981                hanzo_ml::builder_arg!(builder, n_cols as i32, block_size as i32, self.eps);
982                // SAFETY: ffi.
983                unsafe { builder.launch(cfg) }.w()?;
984                Ok(dst)
985            }
986        }
987
988        use hanzo_ml::backend::BackendStorage;
989        let dev = s1.device();
990        let slice = S { eps: self.eps }.map(&s1.slice, l1, &s2.slice, l2, &s3.slice, l3, dev)?;
991        let dst = hanzo_ml::cuda_backend::CudaStorage {
992            slice,
993            device: dev.clone(),
994        };
995        Ok((dst, l1.shape().clone()))
996    }
997
998    #[cfg(feature = "metal")]
999    fn metal_fwd(
1000        &self,
1001        s1: &hanzo_ml::MetalStorage,
1002        l1: &Layout,
1003        s2: &hanzo_ml::MetalStorage,
1004        l2: &Layout,
1005        s3: &hanzo_ml::MetalStorage,
1006        l3: &Layout,
1007    ) -> Result<(hanzo_ml::MetalStorage, Shape)> {
1008        use hanzo_ml::backend::BackendStorage;
1009        let device = s1.device();
1010        let encoder = device.command_encoder()?;
1011        encoder.set_label("layernorm");
1012        let kernels = device.kernels();
1013        let name = match (s1.dtype(), s2.dtype(), s3.dtype()) {
1014            (DType::F32, DType::F32, DType::F32) => "layernorm_f32",
1015            (DType::F16, DType::F16, DType::F16) => "layernorm_f16",
1016            (DType::BF16, DType::BF16, DType::BF16) => "layernorm_bf16",
1017            (dt1, dt2, dt3) => {
1018                hanzo_ml::bail!("layernorm is not implemented for {dt1:?} {dt2:?} {dt3:?}")
1019            }
1020        };
1021
1022        if !(l1.is_contiguous() && l2.is_contiguous() && l3.is_contiguous()) {
1023            hanzo_ml::bail!("Non contiguous layernorm is not implemented");
1024        }
1025
1026        let last_dim = l1.dims()[l1.shape().rank() - 1];
1027        let elem_count = l1.shape().elem_count();
1028        let output = device.new_buffer(elem_count, s1.dtype(), "layernorm")?;
1029        hanzo_metal_kernels::call_layer_norm(
1030            device.metal_device(),
1031            &encoder,
1032            kernels,
1033            name,
1034            elem_count,
1035            last_dim,
1036            self.eps,
1037            s1.buffer(),
1038            l1.start_offset() * s1.dtype().size_in_bytes(),
1039            s2.buffer(),
1040            l2.start_offset() * s2.dtype().size_in_bytes(),
1041            s3.buffer(),
1042            l3.start_offset() * s3.dtype().size_in_bytes(),
1043            &output,
1044        )
1045        .map_err(hanzo_ml::Error::wrap)?;
1046        let newstorage =
1047            hanzo_ml::MetalStorage::new(output, device.clone(), elem_count, s1.dtype());
1048        Ok((newstorage, l1.shape().clone()))
1049    }
1050}
1051
1052pub fn layer_norm_slow(x: &Tensor, alpha: &Tensor, beta: &Tensor, eps: f32) -> Result<Tensor> {
1053    let x_dtype = x.dtype();
1054    let internal_dtype = match x_dtype {
1055        DType::F16 | DType::BF16 => DType::F32,
1056        d => d,
1057    };
1058    let hidden_size = x.dim(D::Minus1)?;
1059    let x = x.to_dtype(internal_dtype)?;
1060    let x = {
1061        let mean_x = (x.sum_keepdim(D::Minus1)? / hidden_size as f64)?;
1062        x.broadcast_sub(&mean_x)?
1063    };
1064    let norm_x = (x.sqr()?.sum_keepdim(D::Minus1)? / hidden_size as f64)?;
1065    let x_normed = x.broadcast_div(&(norm_x + eps as f64)?.sqrt()?)?;
1066    x_normed
1067        .to_dtype(x_dtype)?
1068        .broadcast_mul(alpha)?
1069        .broadcast_add(beta)
1070}
1071
1072pub fn layer_norm(xs: &Tensor, alpha: &Tensor, beta: &Tensor, eps: f32) -> Result<Tensor> {
1073    let hidden_size_xs = xs.dim(D::Minus1)?;
1074    let hidden_size_alpha = alpha.dims1()?;
1075    let hidden_size_beta = beta.dims1()?;
1076    if hidden_size_xs != hidden_size_alpha || hidden_size_xs != hidden_size_beta {
1077        hanzo_ml::bail!(
1078            "shape mismatch in layer-norm src: {:?} alpha: {:?} beta: {:?}",
1079            xs.shape(),
1080            alpha.shape(),
1081            beta.shape()
1082        )
1083    }
1084    if xs.device().is_rocm() || xs.device().is_vulkan() {
1085        return layer_norm_slow(xs, alpha, beta, eps);
1086    }
1087    xs.apply_op3_no_bwd(alpha, beta, &LayerNorm { eps })
1088}
1089
1090// https://pytorch.org/docs/stable/generated/torch.nn.PixelShuffle.html
1091pub fn pixel_shuffle(xs: &Tensor, upscale_factor: usize) -> Result<Tensor> {
1092    let (b_size, c, h, w) = xs.dims4()?;
1093    let out_c = c / upscale_factor / upscale_factor;
1094    xs.reshape((b_size, out_c, upscale_factor, upscale_factor, h, w))?
1095        .permute((0, 1, 4, 2, 5, 3))?
1096        .reshape((b_size, out_c, h * upscale_factor, w * upscale_factor))
1097}
1098
1099pub fn pixel_unshuffle(xs: &Tensor, downscale_factor: usize) -> Result<Tensor> {
1100    let (b_size, c, h, w) = xs.dims4()?;
1101    let out_c = c * downscale_factor * downscale_factor;
1102    xs.reshape((
1103        b_size,
1104        c,
1105        h / downscale_factor,
1106        downscale_factor,
1107        w / downscale_factor,
1108        downscale_factor,
1109    ))?
1110    .permute((0, 1, 3, 5, 2, 4))?
1111    .reshape((b_size, out_c, h / downscale_factor, w / downscale_factor))
1112}
1113
1114// https://pytorch.org/docs/stable/generated/torch.nn.ReplicationPad2d.html
1115pub fn replication_pad2d(xs: &Tensor, pad: usize) -> Result<Tensor> {
1116    match pad {
1117        0 => Ok(xs.clone()),
1118        1 => {
1119            let (_b_size, _c, h, w) = xs.dims4()?;
1120            let (first, last) = (xs.narrow(3, 0, 1)?, xs.narrow(3, w - 1, 1)?);
1121            let xs = Tensor::cat(&[&first, xs, &last], 3)?;
1122            let (first, last) = (xs.narrow(2, 0, 1)?, xs.narrow(2, h - 1, 1)?);
1123            Tensor::cat(&[&first, &xs, &last], 2)
1124        }
1125        n => hanzo_ml::bail!("replication-pad with a size of {n} is not supported"),
1126    }
1127}
1128
1129#[derive(Clone, Debug)]
1130pub struct Identity;
1131
1132impl Identity {
1133    pub fn new() -> Identity {
1134        Self
1135    }
1136}
1137
1138impl Default for Identity {
1139    fn default() -> Self {
1140        Self
1141    }
1142}
1143
1144impl Module for Identity {
1145    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
1146        Ok(xs.clone())
1147    }
1148}
1149
1150#[allow(dead_code)]
1151struct Sdpa {
1152    scale: f32,
1153    softcapping: f32,
1154    mask: Option<Tensor>,
1155    do_causal: bool,
1156}
1157
1158// Flash-decoding for the Vulkan decode attention (cached; the decode path re-enters vulkan_fwd once
1159// per layer per token). Default ON -- the split-K kernel fills the GPU vs the one-workgroup-per-head
1160// sdpa_blk (256 VGPR / min occupancy). VK_SDPA_SPLIT_OFF=1 reverts for A/B; matches vk_sdpa_graph.
1161#[cfg(feature = "vulkan")]
1162fn vk_sdpa_split() -> bool {
1163    static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1164    *S.get_or_init(|| std::env::var("VK_SDPA_SPLIT_OFF").map(|v| v == "0").unwrap_or(true))
1165}
1166#[cfg(feature = "vulkan")]
1167fn vk_sdpa_nsplit() -> usize {
1168    static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
1169    *N.get_or_init(|| {
1170        std::env::var("VK_SDPA_NSPLIT")
1171            .ok()
1172            .and_then(|v| v.parse::<usize>().ok())
1173            .filter(|&n| n >= 1)
1174            .unwrap_or(8)
1175    })
1176}
1177
1178impl hanzo_ml::CustomOp3 for Sdpa {
1179    fn name(&self) -> &'static str {
1180        "metal-sdpa"
1181    }
1182
1183    fn cpu_fwd(
1184        &self,
1185        _s1: &CpuStorage,
1186        _l1: &Layout,
1187        _s2: &CpuStorage,
1188        _l2: &Layout,
1189        _s3: &CpuStorage,
1190        _l3: &Layout,
1191    ) -> Result<(CpuStorage, Shape)> {
1192        hanzo_ml::bail!("SDPA has no cpu impl")
1193    }
1194
1195    // Fused GQA flash SDPA on Vulkan: the DSL `sdpa_blk` .spv, one workgroup per (batch,head,query),
1196    // GQA-native (reads the shared KV head, no repeat_kv). Preconditions (single-query decode shape,
1197    // head_dim 128, no softcap/mask) are gated by the caller (engine attention::vulkan_decode_attn);
1198    // this validates defensively and bails so an unsupported call surfaces an error rather than a
1199    // silent miscompute. Inputs are contiguous f32 (the caller makes them contiguous).
1200    #[cfg(feature = "vulkan")]
1201    fn vulkan_fwd(
1202        &self,
1203        q: &hanzo_ml::VulkanStorage,
1204        q_l: &Layout,
1205        k: &hanzo_ml::VulkanStorage,
1206        k_l: &Layout,
1207        v: &hanzo_ml::VulkanStorage,
1208        v_l: &Layout,
1209    ) -> Result<(hanzo_ml::VulkanStorage, Shape)> {
1210        use hanzo_ml::backend::BackendStorage;
1211        if q.dtype() != hanzo_ml::DType::F32
1212            || k.dtype() != hanzo_ml::DType::F32
1213            || v.dtype() != hanzo_ml::DType::F32
1214        {
1215            hanzo_ml::bail!("sdpa_blk vulkan: f32 only (got q{:?})", q.dtype());
1216        }
1217        let (b, h, sq, d) = q_l.shape().dims4()?;
1218        let (_, hkv, l, kd) = k_l.shape().dims4()?;
1219        let (_, _, _, vd) = v_l.shape().dims4()?;
1220        if d != 128 || kd != 128 || vd != 128 {
1221            hanzo_ml::bail!("sdpa_blk vulkan: only head_dim 128 (got q{d}/k{kd}/v{vd})");
1222        }
1223        if self.softcapping != 1.0 || self.mask.is_some() {
1224            hanzo_ml::bail!("sdpa_blk vulkan: no softcap/mask support");
1225        }
1226        if hkv == 0 || h % hkv != 0 {
1227            hanzo_ml::bail!("sdpa_blk vulkan: n_heads {h} not a multiple of n_kv {hkv}");
1228        }
1229        // q must be contiguous at offset 0 (it's a single decode token -- tiny to materialize). k/v may
1230        // be a STRIDED view of a max_seq-sized KV cache: the kernel reads them in place at their real
1231        // strides (no per-layer .contiguous() of the whole active cache), needing only the innermost
1232        // (head_dim) stride == 1, offset 0, and k/v sharing one layout (one kbase feeds both reads).
1233        if !q_l.is_contiguous() || q_l.start_offset() != 0 {
1234            hanzo_ml::bail!("sdpa_blk vulkan: q must be contiguous at offset 0");
1235        }
1236        let ks = k_l.stride();
1237        let vs = v_l.stride();
1238        if k_l.start_offset() != 0 || v_l.start_offset() != 0 || ks != vs || ks[3] != 1 {
1239            hanzo_ml::bail!("sdpa_blk vulkan: k/v need matching strides, head_dim contiguous, offset 0");
1240        }
1241        let dev = q.device().clone();
1242        // Flash-decoding A/B (VK_SDPA_SPLIT=1): the split-K occupancy fix vs the one-workgroup-per-head
1243        // sdpa_blk. n_split via VK_SDPA_NSPLIT (default 4). Eager path only; the graph path is separate.
1244        let out = if vk_sdpa_split() {
1245            dev.sdpa_decode_split_vk(
1246                q, k, v, b, h, hkv, sq, l, d, self.scale, vk_sdpa_nsplit(),
1247                ks[0], ks[1], ks[2],
1248            )?
1249        } else {
1250            dev.sdpa_blk_vk(
1251                q, k, v, b, h, hkv, sq, l, d, self.scale, self.do_causal,
1252                ks[0], ks[1], ks[2],
1253            )?
1254        };
1255        Ok((out, Shape::from_dims(&[b, h, sq, d])))
1256    }
1257
1258    #[cfg(feature = "metal")]
1259    fn metal_fwd(
1260        &self,
1261        q: &hanzo_ml::MetalStorage,
1262        q_l: &Layout,
1263        k: &hanzo_ml::MetalStorage,
1264        k_l: &Layout,
1265        v: &hanzo_ml::MetalStorage,
1266        v_l: &Layout,
1267    ) -> Result<(hanzo_ml::MetalStorage, Shape)> {
1268        use hanzo_metal_kernels::SdpaDType;
1269        use hanzo_ml::backend::BackendStorage;
1270
1271        let device = q.device();
1272
1273        let out_dims = vec![q_l.dim(0)?, q_l.dim(1)?, q_l.dim(2)?, v_l.dim(3)?];
1274        let elem_count: usize = out_dims.iter().product();
1275        let out_shape = Shape::from_dims(&out_dims);
1276        let out_layout = Layout::contiguous(out_shape.clone());
1277
1278        let output = device.new_buffer(elem_count, q.dtype(), "sdpa_o")?;
1279
1280        // q,k must have matching emb dim
1281        if q_l.dim(D::Minus1)? != k_l.dim(D::Minus1)? {
1282            hanzo_ml::bail!("`q` and `k` last dims must match");
1283        }
1284
1285        // k,v must have matching n kv heads
1286        if v_l.dim(D::Minus(3))? != k_l.dim(D::Minus(3))? {
1287            hanzo_ml::bail!("`k` and `v` head dims must match");
1288        }
1289
1290        // n_heads % n_kv_heads == 0; n_heads >= 1, n_kv_heads >= 1.
1291        if q_l.dim(D::Minus(3))? % k_l.dim(D::Minus(3))? != 0 {
1292            hanzo_ml::bail!("query `n_heads` must be a multiple of `n_kv_heads`");
1293        }
1294
1295        let k_head = k_l.dim(D::Minus1)?;
1296        let q_head = q_l.dim(D::Minus1)?;
1297        let q_seq = q_l.dim(2)?;
1298        let k_seq = k_l.dim(2)?;
1299
1300        let mut implementation_supports_use_case = q_head == k_head;
1301        let supported_head_dim = q_head == 32
1302            || q_head == 64
1303            || q_head == 72
1304            || q_head == 80
1305            || q_head == 96
1306            || q_head == 128
1307            || q_head == 256
1308            || q_head == 512;
1309
1310        let supports_sdpa_full_mask = self.mask.is_none() || q_seq <= k_seq;
1311        // F32 full attention at head_dim=512 exceeds 32KB Metal threadgroup memory
1312        let supports_sdpa_full_dtype = !(q_head == 512 && q.dtype() == DType::F32);
1313        // The vector kernel reduces over a single query row, so it is only correct at q_seq == 1;
1314        // everything longer goes through the full kernel. The split is exclusive by construction.
1315        let supports_sdpa_full =
1316            q_seq > 1 && supported_head_dim && supports_sdpa_full_mask && supports_sdpa_full_dtype;
1317        let supports_sdpa_vector = q_seq == 1 && supported_head_dim && q_seq <= k_seq;
1318
1319        implementation_supports_use_case &= supports_sdpa_full || supports_sdpa_vector;
1320
1321        if !supported_head_dim {
1322            hanzo_ml::bail!(
1323                "Meta SDPA does not support q head dim {q_head}: q dims {:?}, k dims {:?}, v dims {:?}.",
1324                q_l.dims(),
1325                k_l.dims(),
1326                v_l.dims()
1327            );
1328        }
1329        if !implementation_supports_use_case {
1330            hanzo_ml::bail!(
1331                "Meta SDPA does not support q dims {:?}, k dims {:?}, v dims {:?}.",
1332                q_l.dims(),
1333                k_l.dims(),
1334                v_l.dims()
1335            );
1336        }
1337
1338        for t in [k.dtype(), v.dtype()] {
1339            if q.dtype() != t {
1340                hanzo_ml::bail!("all q, k, v dtypes must match.");
1341            }
1342        }
1343
1344        let itype = match q.dtype() {
1345            DType::BF16 => SdpaDType::BF16,
1346            DType::F16 => SdpaDType::F16,
1347            DType::F32 => SdpaDType::F32,
1348            other => hanzo_ml::bail!("unsupported sdpa type {other:?}"),
1349        };
1350
1351        let encoder = q.device().command_encoder()?;
1352        if supports_sdpa_vector {
1353            // Route to the 2 pass fused attention if the k seqlen is large.
1354            // https://github.com/ml-explore/mlx/pull/1597
1355            const TWO_PASS_K_THRESHOLD: usize = 1024;
1356            if k_seq >= TWO_PASS_K_THRESHOLD {
1357                let mut intermediate_shape = [
1358                    &out_dims[0..out_dims.len() - 2],
1359                    &[hanzo_metal_kernels::SDPA_2PASS_BLOCKS],
1360                    &[out_dims[out_dims.len() - 1]],
1361                ]
1362                .concat();
1363                let intermediate = device.new_buffer(
1364                    intermediate_shape.iter().product::<usize>(),
1365                    DType::F32,
1366                    "sdpa_2pass_intermediate",
1367                )?;
1368                let _ = intermediate_shape.pop().unwrap();
1369                let sums = device.new_buffer(
1370                    intermediate_shape.iter().product::<usize>(),
1371                    DType::F32,
1372                    "sdpa_2pass_sums",
1373                )?;
1374                let maxs = device.new_buffer(
1375                    intermediate_shape.iter().product::<usize>(),
1376                    DType::F32,
1377                    "sdpa_2pass_maxs",
1378                )?;
1379
1380                encoder.set_label("vector_attention");
1381                hanzo_metal_kernels::call_sdpa_vector_2pass(
1382                    q.device().device(),
1383                    &encoder,
1384                    q.device().kernels(),
1385                    q_l.start_offset(),
1386                    q_l.dims(),
1387                    q.buffer(),
1388                    k_l.start_offset(),
1389                    k_l.dims(),
1390                    k_l.stride(),
1391                    k.buffer(),
1392                    v_l.start_offset(),
1393                    v_l.stride(),
1394                    v.buffer(),
1395                    &output,
1396                    &intermediate,
1397                    &sums,
1398                    &maxs,
1399                    self.scale,
1400                    self.softcapping,
1401                    itype,
1402                )
1403                .map_err(hanzo_ml::Error::wrap)?;
1404            } else {
1405                encoder.set_label("vector_attention");
1406                hanzo_metal_kernels::call_sdpa_vector(
1407                    q.device().device(),
1408                    &encoder,
1409                    q.device().kernels(),
1410                    q_l.start_offset(),
1411                    q_l.dims(),
1412                    q.buffer(),
1413                    k_l.start_offset(),
1414                    k_l.dims(),
1415                    k_l.stride(),
1416                    k.buffer(),
1417                    v_l.start_offset(),
1418                    v_l.stride(),
1419                    v.buffer(),
1420                    &output,
1421                    self.scale,
1422                    self.softcapping,
1423                    itype,
1424                )
1425                .map_err(hanzo_ml::Error::wrap)?;
1426            }
1427        } else if supports_sdpa_full {
1428            encoder.set_label("full_attention");
1429            if self.softcapping != 1. {
1430                hanzo_ml::bail!("SDPA full requires softcapping to be disabled (1.0)");
1431            }
1432
1433            let mask_s_l = self.mask.as_ref().map(|m| m.storage_and_layout());
1434
1435            let (mask_type, mask_buffer, mask_strides) = if let Some(mask) = &self.mask {
1436                let (mask_s, mask_l) = mask_s_l.as_ref().unwrap();
1437
1438                let mask_buffer = match &**mask_s {
1439                    hanzo_ml::Storage::Metal(m) => m.buffer(),
1440                    _ => hanzo_ml::bail!("Expected metal device for mask"),
1441                };
1442
1443                let mask_type = match mask.dtype() {
1444                    DType::BF16 => SdpaDType::BF16,
1445                    DType::F16 => SdpaDType::F16,
1446                    DType::F32 => SdpaDType::F32,
1447                    other => hanzo_ml::bail!("unsupported sdpa type {other:?}"),
1448                };
1449                if mask_type != itype {
1450                    hanzo_ml::bail!("Mask type {mask_type:?} must match q type {itype:?}");
1451                }
1452
1453                if mask_l.dims() != [q_l.dim(0)?, q_l.dim(1)?, q_l.dim(2)?, k_seq] {
1454                    hanzo_ml::bail!(
1455                        "Mask shape must be {:?} (bs, qheads, qseq, kseq), got {:?}",
1456                        [q_l.dim(0)?, q_head, q_l.dim(2)?, k_seq],
1457                        mask_l.dims()
1458                    );
1459                }
1460
1461                (
1462                    Some(mask_type),
1463                    Some(mask_buffer),
1464                    Some(mask_l.stride().to_vec()),
1465                )
1466            } else {
1467                (None, None, None)
1468            };
1469
1470            hanzo_metal_kernels::call_sdpa_full(
1471                q.device().device(),
1472                &encoder,
1473                q.device().kernels(),
1474                q_l.start_offset(),
1475                q_l.dims(),
1476                q_l.stride(),
1477                q.buffer(),
1478                k_l.start_offset(),
1479                k_l.dims(),
1480                k_l.stride(),
1481                k.buffer(),
1482                v_l.start_offset(),
1483                v.buffer(),
1484                v_l.stride(),
1485                mask_type,
1486                mask_buffer,
1487                mask_strides.as_deref(),
1488                &output,
1489                out_layout.stride(),
1490                self.scale,
1491                self.do_causal,
1492                itype,
1493            )
1494            .map_err(hanzo_ml::Error::wrap)?;
1495        } else {
1496            hanzo_ml::bail!("must be vector or full sdpa kernel");
1497        }
1498
1499        let newstorage = hanzo_ml::MetalStorage::new(output, device.clone(), elem_count, q.dtype());
1500        Ok((newstorage, out_shape))
1501    }
1502}
1503
1504/// Scaled dot product attention with a fused kernel.
1505///
1506/// Computes softmax(qk^T*scale)v.
1507///
1508/// **Inputs shapes:**
1509/// - `q`: (bs, qhead, seq, hidden)
1510/// - `k`: (bs, kv_head, kv_seq, hidden)
1511/// - `k`: (bs, kv_head, kv_seq, v_hidden)
1512/// - `mask`: (bs, qhead, seq, kv_seq)
1513/// - `do_causal`: Apply causal masking. If this is true, the mask does not need to be provided.
1514/// - `scale` is applied before softmax.
1515/// - If `softcapping` != 1.0:
1516///      - Computation is: softmax(tanh(qk^T*scale/cap)*cap)v
1517///
1518/// **Output shape:** (bs, qhead, seq, v_hidden)
1519///
1520/// Note: For Grouped Query Attention and Multi-Query Attention, the k and v inputs should not be pre-tiled to match q.
1521///
1522/// ## On Metal:
1523/// - If `seq` == 1:
1524///     - Use a vectorized kernel
1525///     - Supports `seq` != `kv_seq` (cross attn. support)
1526///     - Supports GQA when `qhead` is a multiple of `kv_head`
1527/// - Otherwise:
1528///     - Masking is supported
1529///     - Supports `seq` != `kv_seq` (cross attn. support)
1530///     - Supports GQA when `qhead` is a multiple of `kv_head`
1531///     - Softcapping is not supported.
1532pub fn sdpa(
1533    q: &Tensor,
1534    k: &Tensor,
1535    v: &Tensor,
1536    mask: Option<&Tensor>,
1537    do_causal: bool,
1538    scale: f32,
1539    softcapping: f32,
1540) -> Result<Tensor> {
1541    q.apply_op3_no_bwd(
1542        k,
1543        v,
1544        &Sdpa {
1545            scale,
1546            softcapping,
1547            mask: mask.cloned(),
1548            do_causal,
1549        },
1550    )
1551}