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