1use hanzo_ml::{CpuStorage, DType, Layout, Module, Result, Shape, Tensor, D};
5use rayon::prelude::*;
6
7pub 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 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 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 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 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 ((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 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 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 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 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 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 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 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 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
727struct 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
809pub fn silu_mul(gate: &Tensor, up: &Tensor) -> Result<Tensor> {
811 if gate.device().is_cuda() || gate.device().is_metal() {
812 return silu(gate)?.mul(up);
814 }
815 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 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 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
1090pub 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
1114pub 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#[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 #[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 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 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 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 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 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 let supports_sdpa_full_dtype = !(q_head == 512 && q.dtype() == DType::F32);
1313 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 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
1504pub 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}