use std::ffi::CStr;
use crate::error::Result;
use crate::utils::guard::Guarded;
use crate::utils::IntoOption;
use crate::{Array, Stream};
use mlx_internal_macros::generate_macro;
#[cfg(feature = "metal")]
mod metal_kernel;
#[cfg(feature = "metal")]
pub use metal_kernel::{
MetalKernel, MetalKernelApplyOptions, MetalKernelOptions, MetalKernelOutput, TemplateArg,
};
#[allow(clippy::too_many_arguments)]
pub fn rope<'a>(
array: impl AsRef<Array>,
dimensions: i32,
traditional: bool,
base: impl Into<Option<f32>>,
scale: f32,
offset: i32,
freqs: impl Into<Option<&'a Array>>,
) -> Result<Array> {
let stream = Stream::thread_local_or_default();
let base = base.into();
let base = mlx_sys::mlx_optional_float {
value: base.unwrap_or(0.0),
has_value: base.is_some(),
};
let freqs = freqs.into();
Array::try_from_op(|res| unsafe {
mlx_sys::mlx_fast_rope(
res,
array.as_ref().as_ptr(),
dimensions,
traditional,
base,
scale,
offset,
freqs
.map(|a| a.as_ptr())
.unwrap_or(mlx_sys::mlx_array_new()),
stream.as_ref().as_ptr(),
)
})
}
#[allow(clippy::too_many_arguments)]
#[generate_macro(customize(forwarding_shim = true, root = "$crate::fast"))]
#[deprecated(
since = "0.26.0",
note = "use `with_stream` or `with_device` around `rope`"
)]
pub fn rope_device<'a>(
#[named] array: impl AsRef<Array>,
#[named] dimensions: i32,
#[named] traditional: bool,
#[optional] base: impl Into<Option<f32>>,
#[named] scale: f32,
#[named] offset: i32,
#[optional] freqs: impl Into<Option<&'a Array>>,
#[optional] stream: impl AsRef<Stream>,
) -> Result<Array> {
crate::with_stream(stream.as_ref(), || {
rope(array, dimensions, traditional, base, scale, offset, freqs)
})
}
#[allow(clippy::too_many_arguments)]
pub fn rope_dynamic<'a>(
array: impl AsRef<Array>,
dimensions: i32,
traditional: bool,
base: impl Into<Option<f32>>,
scale: f32,
offset: impl AsRef<Array>,
freqs: impl Into<Option<&'a Array>>,
) -> Result<Array> {
let stream = Stream::thread_local_or_default();
let base = base.into();
let base = mlx_sys::mlx_optional_float {
value: base.unwrap_or(0.0),
has_value: base.is_some(),
};
let freqs = freqs.into();
Array::try_from_op(|res| unsafe {
mlx_sys::mlx_fast_rope_dynamic(
res,
array.as_ref().as_ptr(),
dimensions,
traditional,
base,
scale,
offset.as_ref().as_ptr(),
freqs
.map(|a| a.as_ptr())
.unwrap_or(mlx_sys::mlx_array_new()),
stream.as_ref().as_ptr(),
)
})
}
#[allow(clippy::too_many_arguments)]
#[generate_macro(customize(forwarding_shim = true, root = "$crate::fast"))]
#[deprecated(
since = "0.26.0",
note = "use `with_stream` or `with_device` around `rope_dynamic`"
)]
pub fn rope_dynamic_device<'a>(
#[named] array: impl AsRef<Array>,
#[named] dimensions: i32,
#[named] traditional: bool,
#[optional] base: impl Into<Option<f32>>,
#[named] scale: f32,
#[named] offset: impl AsRef<Array>,
#[optional] freqs: impl Into<Option<&'a Array>>,
#[optional] stream: impl AsRef<Stream>,
) -> Result<Array> {
crate::with_stream(stream.as_ref(), || {
rope_dynamic(array, dimensions, traditional, base, scale, offset, freqs)
})
}
const DEFAULT_MASK_MODE: &CStr = c"";
const CAUSAL_MASK_MODE: &CStr = c"causal";
#[derive(Debug)]
pub enum ScaledDotProductAttentionMask<'a> {
Array(&'a Array),
Causal,
}
impl<'a> From<&'a Array> for ScaledDotProductAttentionMask<'a> {
fn from(mask: &'a Array) -> Self {
ScaledDotProductAttentionMask::Array(mask)
}
}
impl<'a> IntoOption<ScaledDotProductAttentionMask<'a>> for &'a Array {
fn into_option(self) -> Option<ScaledDotProductAttentionMask<'a>> {
Some(ScaledDotProductAttentionMask::Array(self))
}
}
impl ScaledDotProductAttentionMask<'_> {
fn as_mode_and_mask(&self) -> (&'static CStr, mlx_sys::mlx_array) {
match self {
ScaledDotProductAttentionMask::Array(mask) => (DEFAULT_MASK_MODE, mask.as_ptr()),
ScaledDotProductAttentionMask::Causal => {
(CAUSAL_MASK_MODE, unsafe { mlx_sys::mlx_array_new() })
}
}
}
}
pub fn scaled_dot_product_attention<'a>(
queries: impl AsRef<Array>,
keys: impl AsRef<Array>,
values: impl AsRef<Array>,
scale: f32,
mask: impl IntoOption<ScaledDotProductAttentionMask<'a>>,
sinks: impl Into<Option<&'a Array>>,
) -> Result<Array> {
let stream = Stream::thread_local_or_default();
let (mask_mode, mask_arr) = mask.into_option().map_or_else(
|| (DEFAULT_MASK_MODE, unsafe { mlx_sys::mlx_array_new() }),
|m| m.as_mode_and_mask(),
);
Array::try_from_op(|res| unsafe {
mlx_sys::mlx_fast_scaled_dot_product_attention(
res,
queries.as_ref().as_ptr(),
keys.as_ref().as_ptr(),
values.as_ref().as_ptr(),
scale,
mask_mode.as_ptr(),
mask_arr,
sinks
.into()
.map(|a| a.as_ptr())
.unwrap_or(mlx_sys::mlx_array_new()),
false,
stream.as_ref().as_ptr(),
)
})
}
#[generate_macro(customize(forwarding_shim = true, root = "$crate::fast"))]
#[deprecated(
since = "0.26.0",
note = "use `with_stream` or `with_device` around `scaled_dot_product_attention`"
)]
pub fn scaled_dot_product_attention_device<'a>(
queries: impl AsRef<Array>,
keys: impl AsRef<Array>,
values: impl AsRef<Array>,
scale: f32,
#[optional] mask: impl IntoOption<ScaledDotProductAttentionMask<'a>>,
#[optional] sinks: impl Into<Option<&'a Array>>,
#[optional] stream: impl AsRef<Stream>,
) -> Result<Array> {
crate::with_stream(stream.as_ref(), || {
scaled_dot_product_attention(queries, keys, values, scale, mask, sinks)
})
}
pub fn rms_norm(x: impl AsRef<Array>, weight: Option<&Array>, eps: f32) -> Result<Array> {
let stream = Stream::thread_local_or_default();
let empty_weight = weight
.is_none()
.then(|| unsafe { Array::from_ptr(mlx_sys::mlx_array_new()) });
let weight = weight
.map(Array::as_ptr)
.unwrap_or_else(|| empty_weight.as_ref().unwrap().as_ptr());
Array::try_from_op(|res| unsafe {
mlx_sys::mlx_fast_rms_norm(
res,
x.as_ref().as_ptr(),
weight,
eps,
stream.as_ref().as_ptr(),
)
})
}
#[generate_macro(customize(forwarding_shim = true, root = "$crate::fast"))]
#[deprecated(
since = "0.26.0",
note = "use `with_stream` or `with_device` around `rms_norm`"
)]
pub fn rms_norm_device(
x: impl AsRef<Array>,
weight: impl AsRef<Array>,
eps: f32,
#[optional] stream: impl AsRef<Stream>,
) -> Result<Array> {
crate::with_stream(stream.as_ref(), || rms_norm(x, Some(weight.as_ref()), eps))
}
pub fn layer_norm<'a>(
x: impl AsRef<Array>,
weight: impl Into<Option<&'a Array>>,
bias: impl Into<Option<&'a Array>>,
eps: f32,
) -> Result<Array> {
let stream = Stream::thread_local_or_default();
Array::try_from_op(|res| unsafe {
mlx_sys::mlx_fast_layer_norm(
res,
x.as_ref().as_ptr(),
weight
.into()
.map(|a| a.as_ptr())
.unwrap_or(mlx_sys::mlx_array_new()),
bias.into()
.map(|a| a.as_ptr())
.unwrap_or(mlx_sys::mlx_array_new()),
eps,
stream.as_ref().as_ptr(),
)
})
}
#[generate_macro(customize(forwarding_shim = true, root = "$crate::fast"))]
#[deprecated(
since = "0.26.0",
note = "use `with_stream` or `with_device` around `layer_norm`"
)]
pub fn layer_norm_device<'a>(
#[named] x: impl AsRef<Array>,
#[optional] weight: impl Into<Option<&'a Array>>,
#[optional] bias: impl Into<Option<&'a Array>>,
#[named] eps: f32,
#[optional] stream: impl AsRef<Stream>,
) -> Result<Array> {
crate::with_stream(stream.as_ref(), || layer_norm(x, weight, bias, eps))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ops::indexing::{ArrayIndexOp, IndexOp},
random::normal,
};
use float_eq::assert_float_eq;
use pretty_assertions::assert_eq;
#[test]
fn test_rope() {
crate::random::seed(71).unwrap();
let a = crate::random::uniform::<_, f32>(0.0, 1.0, &[2, 8, 16], None).unwrap();
assert_eq!(a.shape(), [2, 8, 16]);
assert_eq!(a.dtype(), crate::Dtype::Float32);
let result = rope(a, 8, false, 10000., 1.0, 0, None).unwrap();
assert_eq!(result.shape(), [2, 8, 16]);
assert_eq!(result.dtype(), crate::Dtype::Float32);
assert_float_eq!(
result.mean(None).unwrap().item_exact::<f32>(),
0.456_253_77,
abs <= 0.009_125_075
);
assert_float_eq!(
result.sum(None).unwrap().item_exact::<f32>(),
116.800_964,
abs <= 2.336_019_3
);
}
#[test]
fn test_rope_dynamic() {
crate::random::seed(71).unwrap();
let a = crate::random::uniform::<_, f32>(0.0, 1.0, &[2, 8, 16], None).unwrap();
assert_eq!(a.shape(), [2, 8, 16]);
assert_eq!(a.dtype(), crate::Dtype::Float32);
let offset = crate::Array::from_int(3);
let result = rope_dynamic(&a, 8, false, 10000., 1.0, &offset, None).unwrap();
assert_eq!(result.shape(), [2, 8, 16]);
assert_eq!(result.dtype(), crate::Dtype::Float32);
let result_int_offset = rope(&a, 8, false, 10000., 1.0, 3, None).unwrap();
assert_eq!(result_int_offset.shape(), [2, 8, 16]);
let diff = &result - &result_int_offset;
let max_diff = diff.abs().unwrap().max(None).unwrap().item_exact::<f32>();
assert!(max_diff < 1e-5, "Max difference was {}", max_diff);
}
#[test]
fn test_rms_norm() {
crate::random::seed(103).unwrap();
let a = crate::random::uniform::<_, f32>(0.0, 1.0, &[2, 8, 16], None).unwrap();
assert_eq!(a.shape(), [2, 8, 16]);
assert_eq!(a.dtype(), crate::Dtype::Float32);
let weight = Array::ones::<f32>(&[16]).unwrap();
let result = rms_norm(a, Some(&weight), 1e-5).unwrap();
assert_eq!(result.shape(), [2, 8, 16]);
assert_eq!(result.dtype(), crate::Dtype::Float32);
assert_float_eq!(
result.mean(None).unwrap().item_exact::<f32>(),
0.872_938_75,
abs <= 0.017_458_774
);
assert_float_eq!(
result.sum(None).unwrap().item_exact::<f32>(),
223.472_32,
abs <= 4.469_446
);
let ones = Array::ones::<f32>(&[2, 4]).unwrap();
let normalized = rms_norm(&ones, None, 0.0).unwrap();
assert_eq!(normalized.as_slice::<f32>(), &[1.0; 8]);
}
#[test]
pub fn test_layer_norm_affine() {
crate::random::seed(635).unwrap();
let a = crate::random::uniform::<_, f32>(0.0, 1.0, &[2, 8, 16], None).unwrap();
assert_eq!(a.shape(), [2, 8, 16]);
assert_eq!(a.dtype(), crate::Dtype::Float32);
let weight = Array::ones::<f32>(&[16]).unwrap();
let bias = Array::zeros::<f32>(&[16]).unwrap();
let result = layer_norm(a, &weight, &bias, 1e-5).unwrap();
let result = result.index((ArrayIndexOp::Ellipsis, 0));
assert_eq!(result.shape(), [2, 8]);
assert_eq!(result.dtype(), crate::Dtype::Float32);
assert_float_eq!(
result.mean(None).unwrap().item_exact::<f32>(),
0.290_990_38,
abs <= 0.005_819_807_8
);
assert_float_eq!(
result.sum(None).unwrap().item_exact::<f32>(),
4.655_846,
abs <= 0.093_116_924
);
}
#[test]
#[allow(non_snake_case)]
fn test_fast_sdpa() {
let Dk = 64;
let scale = 1.0 / (Dk as f32).sqrt();
for seq_len in [63, 129, 400] {
for dtype in [crate::Dtype::Float32, crate::Dtype::Float16] {
let B = 2;
let H = 24;
let q = normal::<f32>(&[B, H, seq_len, Dk], None, None, None)
.unwrap()
.as_dtype(dtype)
.unwrap();
let k = normal::<f32>(&[B, H, seq_len, Dk], None, None, None)
.unwrap()
.as_dtype(dtype)
.unwrap();
let v = normal::<f32>(&[B, H, seq_len, Dk], None, None, None)
.unwrap()
.as_dtype(dtype)
.unwrap();
let result = scaled_dot_product_attention(q, k, v, scale, None, None).unwrap();
assert_eq!(result.shape(), [B, H, seq_len, Dk]);
assert_eq!(result.dtype(), dtype);
result.eval().unwrap();
}
}
}
#[test]
fn test_fast_sdpa_with_sinks() {
let b = 2;
let n_q = 8;
let t_q = 128;
let t_kv = 128;
let d = 64;
let q = normal::<f32>(&[b, n_q, t_q, d], None, None, None).unwrap();
let k = normal::<f32>(&[b, n_q, t_kv, d], None, None, None).unwrap();
let v = normal::<f32>(&[b, n_q, t_kv, d], None, None, None).unwrap();
let scale = (d as f32).powf(-0.5);
let sinks = normal::<f32>(&[n_q], None, None, None).unwrap() * 10.0;
let result = scaled_dot_product_attention(&q, &k, &v, scale, None, &sinks).unwrap();
assert_eq!(result.shape(), &[b, n_q, t_q, d]);
}
}