1use std::ffi::CStr;
4
5use crate::error::Result;
6use crate::utils::guard::Guarded;
7use crate::utils::IntoOption;
8use crate::{Array, Stream};
9use mlx_internal_macros::generate_macro;
10
11#[cfg(feature = "metal")]
12mod metal_kernel;
13#[cfg(feature = "metal")]
14pub use metal_kernel::{
15 MetalKernel, MetalKernelApplyOptions, MetalKernelOptions, MetalKernelOutput, TemplateArg,
16};
17
18#[allow(clippy::too_many_arguments)]
20pub fn rope<'a>(
21 array: impl AsRef<Array>,
22 dimensions: i32,
23 traditional: bool,
24 base: impl Into<Option<f32>>,
25 scale: f32,
26 offset: i32,
27 freqs: impl Into<Option<&'a Array>>,
28) -> Result<Array> {
29 let stream = Stream::thread_local_or_default();
30 let base = base.into();
31 let base = mlx_sys::mlx_optional_float {
32 value: base.unwrap_or(0.0),
33 has_value: base.is_some(),
34 };
35 let freqs = freqs.into();
36 Array::try_from_op(|res| unsafe {
37 mlx_sys::mlx_fast_rope(
38 res,
39 array.as_ref().as_ptr(),
40 dimensions,
41 traditional,
42 base,
43 scale,
44 offset,
45 freqs
46 .map(|a| a.as_ptr())
47 .unwrap_or(mlx_sys::mlx_array_new()),
48 stream.as_ref().as_ptr(),
49 )
50 })
51}
52
53#[allow(clippy::too_many_arguments)]
55#[generate_macro(customize(forwarding_shim = true, root = "$crate::fast"))]
56#[deprecated(
57 since = "0.26.0",
58 note = "use `with_stream` or `with_device` around `rope`"
59)]
60pub fn rope_device<'a>(
61 #[named] array: impl AsRef<Array>,
62 #[named] dimensions: i32,
63 #[named] traditional: bool,
64 #[optional] base: impl Into<Option<f32>>,
65 #[named] scale: f32,
66 #[named] offset: i32,
67 #[optional] freqs: impl Into<Option<&'a Array>>,
68 #[optional] stream: impl AsRef<Stream>,
69) -> Result<Array> {
70 crate::with_stream(stream.as_ref(), || {
71 rope(array, dimensions, traditional, base, scale, offset, freqs)
72 })
73}
74
75#[allow(clippy::too_many_arguments)]
91pub fn rope_dynamic<'a>(
92 array: impl AsRef<Array>,
93 dimensions: i32,
94 traditional: bool,
95 base: impl Into<Option<f32>>,
96 scale: f32,
97 offset: impl AsRef<Array>,
98 freqs: impl Into<Option<&'a Array>>,
99) -> Result<Array> {
100 let stream = Stream::thread_local_or_default();
101 let base = base.into();
102 let base = mlx_sys::mlx_optional_float {
103 value: base.unwrap_or(0.0),
104 has_value: base.is_some(),
105 };
106 let freqs = freqs.into();
107 Array::try_from_op(|res| unsafe {
108 mlx_sys::mlx_fast_rope_dynamic(
109 res,
110 array.as_ref().as_ptr(),
111 dimensions,
112 traditional,
113 base,
114 scale,
115 offset.as_ref().as_ptr(),
116 freqs
117 .map(|a| a.as_ptr())
118 .unwrap_or(mlx_sys::mlx_array_new()),
119 stream.as_ref().as_ptr(),
120 )
121 })
122}
123
124#[allow(clippy::too_many_arguments)]
126#[generate_macro(customize(forwarding_shim = true, root = "$crate::fast"))]
127#[deprecated(
128 since = "0.26.0",
129 note = "use `with_stream` or `with_device` around `rope_dynamic`"
130)]
131pub fn rope_dynamic_device<'a>(
132 #[named] array: impl AsRef<Array>,
133 #[named] dimensions: i32,
134 #[named] traditional: bool,
135 #[optional] base: impl Into<Option<f32>>,
136 #[named] scale: f32,
137 #[named] offset: impl AsRef<Array>,
138 #[optional] freqs: impl Into<Option<&'a Array>>,
139 #[optional] stream: impl AsRef<Stream>,
140) -> Result<Array> {
141 crate::with_stream(stream.as_ref(), || {
142 rope_dynamic(array, dimensions, traditional, base, scale, offset, freqs)
143 })
144}
145
146const DEFAULT_MASK_MODE: &CStr = c"";
147const CAUSAL_MASK_MODE: &CStr = c"causal";
148
149#[derive(Debug)]
151pub enum ScaledDotProductAttentionMask<'a> {
152 Array(&'a Array),
154
155 Causal,
157}
158
159impl<'a> From<&'a Array> for ScaledDotProductAttentionMask<'a> {
160 fn from(mask: &'a Array) -> Self {
161 ScaledDotProductAttentionMask::Array(mask)
162 }
163}
164
165impl<'a> IntoOption<ScaledDotProductAttentionMask<'a>> for &'a Array {
166 fn into_option(self) -> Option<ScaledDotProductAttentionMask<'a>> {
167 Some(ScaledDotProductAttentionMask::Array(self))
168 }
169}
170
171impl ScaledDotProductAttentionMask<'_> {
172 fn as_mode_and_mask(&self) -> (&'static CStr, mlx_sys::mlx_array) {
173 match self {
174 ScaledDotProductAttentionMask::Array(mask) => (DEFAULT_MASK_MODE, mask.as_ptr()),
175 ScaledDotProductAttentionMask::Causal => {
176 (CAUSAL_MASK_MODE, unsafe { mlx_sys::mlx_array_new() })
177 }
178 }
179 }
180}
181
182pub fn scaled_dot_product_attention<'a>(
192 queries: impl AsRef<Array>,
193 keys: impl AsRef<Array>,
194 values: impl AsRef<Array>,
195 scale: f32,
196 mask: impl IntoOption<ScaledDotProductAttentionMask<'a>>,
197 sinks: impl Into<Option<&'a Array>>,
198) -> Result<Array> {
199 let stream = Stream::thread_local_or_default();
200 let (mask_mode, mask_arr) = mask.into_option().map_or_else(
201 || (DEFAULT_MASK_MODE, unsafe { mlx_sys::mlx_array_new() }),
202 |m| m.as_mode_and_mask(),
203 );
204
205 Array::try_from_op(|res| unsafe {
206 mlx_sys::mlx_fast_scaled_dot_product_attention(
207 res,
208 queries.as_ref().as_ptr(),
209 keys.as_ref().as_ptr(),
210 values.as_ref().as_ptr(),
211 scale,
212 mask_mode.as_ptr(),
213 mask_arr,
214 sinks
215 .into()
216 .map(|a| a.as_ptr())
217 .unwrap_or(mlx_sys::mlx_array_new()),
218 false,
219 stream.as_ref().as_ptr(),
220 )
221 })
222}
223
224#[generate_macro(customize(forwarding_shim = true, root = "$crate::fast"))]
226#[deprecated(
227 since = "0.26.0",
228 note = "use `with_stream` or `with_device` around `scaled_dot_product_attention`"
229)]
230pub fn scaled_dot_product_attention_device<'a>(
231 queries: impl AsRef<Array>,
232 keys: impl AsRef<Array>,
233 values: impl AsRef<Array>,
234 scale: f32,
235 #[optional] mask: impl IntoOption<ScaledDotProductAttentionMask<'a>>,
236 #[optional] sinks: impl Into<Option<&'a Array>>,
237 #[optional] stream: impl AsRef<Stream>,
238) -> Result<Array> {
239 crate::with_stream(stream.as_ref(), || {
240 scaled_dot_product_attention(queries, keys, values, scale, mask, sinks)
241 })
242}
243
244pub fn rms_norm(x: impl AsRef<Array>, weight: Option<&Array>, eps: f32) -> Result<Array> {
255 let stream = Stream::thread_local_or_default();
256 let empty_weight = weight
257 .is_none()
258 .then(|| unsafe { Array::from_ptr(mlx_sys::mlx_array_new()) });
259 let weight = weight
260 .map(Array::as_ptr)
261 .unwrap_or_else(|| empty_weight.as_ref().unwrap().as_ptr());
262 Array::try_from_op(|res| unsafe {
263 mlx_sys::mlx_fast_rms_norm(
264 res,
265 x.as_ref().as_ptr(),
266 weight,
267 eps,
268 stream.as_ref().as_ptr(),
269 )
270 })
271}
272
273#[generate_macro(customize(forwarding_shim = true, root = "$crate::fast"))]
275#[deprecated(
276 since = "0.26.0",
277 note = "use `with_stream` or `with_device` around `rms_norm`"
278)]
279pub fn rms_norm_device(
280 x: impl AsRef<Array>,
281 weight: impl AsRef<Array>,
282 eps: f32,
283 #[optional] stream: impl AsRef<Stream>,
284) -> Result<Array> {
285 crate::with_stream(stream.as_ref(), || rms_norm(x, Some(weight.as_ref()), eps))
286}
287
288pub fn layer_norm<'a>(
302 x: impl AsRef<Array>,
303 weight: impl Into<Option<&'a Array>>,
304 bias: impl Into<Option<&'a Array>>,
305 eps: f32,
306) -> Result<Array> {
307 let stream = Stream::thread_local_or_default();
308 Array::try_from_op(|res| unsafe {
309 mlx_sys::mlx_fast_layer_norm(
310 res,
311 x.as_ref().as_ptr(),
312 weight
313 .into()
314 .map(|a| a.as_ptr())
315 .unwrap_or(mlx_sys::mlx_array_new()),
316 bias.into()
317 .map(|a| a.as_ptr())
318 .unwrap_or(mlx_sys::mlx_array_new()),
319 eps,
320 stream.as_ref().as_ptr(),
321 )
322 })
323}
324
325#[generate_macro(customize(forwarding_shim = true, root = "$crate::fast"))]
327#[deprecated(
328 since = "0.26.0",
329 note = "use `with_stream` or `with_device` around `layer_norm`"
330)]
331pub fn layer_norm_device<'a>(
332 #[named] x: impl AsRef<Array>,
333 #[optional] weight: impl Into<Option<&'a Array>>,
334 #[optional] bias: impl Into<Option<&'a Array>>,
335 #[named] eps: f32,
336 #[optional] stream: impl AsRef<Stream>,
337) -> Result<Array> {
338 crate::with_stream(stream.as_ref(), || layer_norm(x, weight, bias, eps))
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use crate::{
345 ops::indexing::{ArrayIndexOp, IndexOp},
346 random::normal,
347 };
348 use float_eq::assert_float_eq;
349 use pretty_assertions::assert_eq;
350
351 #[test]
352 fn test_rope() {
353 crate::random::seed(71).unwrap();
354 let a = crate::random::uniform::<_, f32>(0.0, 1.0, &[2, 8, 16], None).unwrap();
355 assert_eq!(a.shape(), [2, 8, 16]);
356 assert_eq!(a.dtype(), crate::Dtype::Float32);
357
358 let result = rope(a, 8, false, 10000., 1.0, 0, None).unwrap();
359 assert_eq!(result.shape(), [2, 8, 16]);
360 assert_eq!(result.dtype(), crate::Dtype::Float32);
361 assert_float_eq!(
362 result.mean(None).unwrap().item_exact::<f32>(),
363 0.456_253_77,
364 abs <= 0.009_125_075
365 );
366 assert_float_eq!(
367 result.sum(None).unwrap().item_exact::<f32>(),
368 116.800_964,
369 abs <= 2.336_019_3
370 );
371 }
372
373 #[test]
376 fn test_rope_dynamic() {
377 crate::random::seed(71).unwrap();
378 let a = crate::random::uniform::<_, f32>(0.0, 1.0, &[2, 8, 16], None).unwrap();
379 assert_eq!(a.shape(), [2, 8, 16]);
380 assert_eq!(a.dtype(), crate::Dtype::Float32);
381
382 let offset = crate::Array::from_int(3);
384 let result = rope_dynamic(&a, 8, false, 10000., 1.0, &offset, None).unwrap();
385 assert_eq!(result.shape(), [2, 8, 16]);
386 assert_eq!(result.dtype(), crate::Dtype::Float32);
387
388 let result_int_offset = rope(&a, 8, false, 10000., 1.0, 3, None).unwrap();
390 assert_eq!(result_int_offset.shape(), [2, 8, 16]);
391
392 let diff = &result - &result_int_offset;
394 let max_diff = diff.abs().unwrap().max(None).unwrap().item_exact::<f32>();
395 assert!(max_diff < 1e-5, "Max difference was {}", max_diff);
396 }
397
398 #[test]
399 fn test_rms_norm() {
400 crate::random::seed(103).unwrap();
401 let a = crate::random::uniform::<_, f32>(0.0, 1.0, &[2, 8, 16], None).unwrap();
402 assert_eq!(a.shape(), [2, 8, 16]);
403 assert_eq!(a.dtype(), crate::Dtype::Float32);
404
405 let weight = Array::ones::<f32>(&[16]).unwrap();
406 let result = rms_norm(a, Some(&weight), 1e-5).unwrap();
407 assert_eq!(result.shape(), [2, 8, 16]);
408 assert_eq!(result.dtype(), crate::Dtype::Float32);
409 assert_float_eq!(
410 result.mean(None).unwrap().item_exact::<f32>(),
411 0.872_938_75,
412 abs <= 0.017_458_774
413 );
414 assert_float_eq!(
415 result.sum(None).unwrap().item_exact::<f32>(),
416 223.472_32,
417 abs <= 4.469_446
418 );
419
420 let ones = Array::ones::<f32>(&[2, 4]).unwrap();
421 let normalized = rms_norm(&ones, None, 0.0).unwrap();
422 assert_eq!(normalized.as_slice::<f32>(), &[1.0; 8]);
423 }
424
425 #[test]
426 pub fn test_layer_norm_affine() {
427 crate::random::seed(635).unwrap();
428 let a = crate::random::uniform::<_, f32>(0.0, 1.0, &[2, 8, 16], None).unwrap();
429 assert_eq!(a.shape(), [2, 8, 16]);
430 assert_eq!(a.dtype(), crate::Dtype::Float32);
431
432 let weight = Array::ones::<f32>(&[16]).unwrap();
433 let bias = Array::zeros::<f32>(&[16]).unwrap();
434 let result = layer_norm(a, &weight, &bias, 1e-5).unwrap();
435 let result = result.index((ArrayIndexOp::Ellipsis, 0));
436 assert_eq!(result.shape(), [2, 8]);
437 assert_eq!(result.dtype(), crate::Dtype::Float32);
438 assert_float_eq!(
439 result.mean(None).unwrap().item_exact::<f32>(),
440 0.290_990_38,
441 abs <= 0.005_819_807_8
442 );
443 assert_float_eq!(
444 result.sum(None).unwrap().item_exact::<f32>(),
445 4.655_846,
446 abs <= 0.093_116_924
447 );
448 }
449
450 #[test]
451 #[allow(non_snake_case)]
452 fn test_fast_sdpa() {
453 let Dk = 64;
457 let scale = 1.0 / (Dk as f32).sqrt();
458 for seq_len in [63, 129, 400] {
459 for dtype in [crate::Dtype::Float32, crate::Dtype::Float16] {
460 let B = 2;
461 let H = 24;
462 let q = normal::<f32>(&[B, H, seq_len, Dk], None, None, None)
463 .unwrap()
464 .as_dtype(dtype)
465 .unwrap();
466 let k = normal::<f32>(&[B, H, seq_len, Dk], None, None, None)
467 .unwrap()
468 .as_dtype(dtype)
469 .unwrap();
470 let v = normal::<f32>(&[B, H, seq_len, Dk], None, None, None)
471 .unwrap()
472 .as_dtype(dtype)
473 .unwrap();
474
475 let result = scaled_dot_product_attention(q, k, v, scale, None, None).unwrap();
476 assert_eq!(result.shape(), [B, H, seq_len, Dk]);
477 assert_eq!(result.dtype(), dtype);
478 result.eval().unwrap();
479 }
480 }
481 }
482
483 #[test]
485 fn test_fast_sdpa_with_sinks() {
486 let b = 2;
487 let n_q = 8;
488 let t_q = 128;
489 let t_kv = 128;
490 let d = 64;
491
492 let q = normal::<f32>(&[b, n_q, t_q, d], None, None, None).unwrap();
493 let k = normal::<f32>(&[b, n_q, t_kv, d], None, None, None).unwrap();
494 let v = normal::<f32>(&[b, n_q, t_kv, d], None, None, None).unwrap();
495 let scale = (d as f32).powf(-0.5);
496
497 let sinks = normal::<f32>(&[n_q], None, None, None).unwrap() * 10.0;
499
500 let result = scaled_dot_product_attention(&q, &k, &v, scale, None, &sinks).unwrap();
501 assert_eq!(result.shape(), &[b, n_q, t_q, d]);
502 }
503}