use std::ffi::CString;
use crate::error::{Exception, Result};
use crate::utils::{guard::Guarded, SUCCESS};
use crate::{Array, Dtype, Stream};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TemplateArg {
Dtype(Dtype),
Int(i32),
Bool(bool),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MetalKernelOptions {
pub header: String,
pub ensure_row_contiguous: bool,
pub atomic_outputs: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetalKernelOutput {
pub shape: Vec<i32>,
pub dtype: Dtype,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MetalKernelApplyOptions {
pub template_args: Vec<(String, TemplateArg)>,
pub init_value: Option<f32>,
pub verbose: bool,
}
pub struct MetalKernel {
raw: mlx_sys::mlx_fast_metal_kernel,
}
impl MetalKernel {
pub fn new(
name: &str,
input_names: &[&str],
output_names: &[&str],
source: &str,
options: MetalKernelOptions,
) -> Result<Self> {
crate::error::INIT_ERR_HANDLER.call_once(crate::error::setup_mlx_error_handler);
let name = c_string(name)?;
let source = c_string(source)?;
let header = c_string(&options.header)?;
let input_names = StringVector::new(input_names)?;
let output_names = StringVector::new(output_names)?;
let raw = unsafe {
mlx_sys::mlx_fast_metal_kernel_new(
name.as_ptr(),
input_names.raw,
output_names.raw,
source.as_ptr(),
header.as_ptr(),
options.ensure_row_contiguous,
options.atomic_outputs,
)
};
if raw.ctx.is_null() {
return Err(crate::error::exception_from_status(
SUCCESS + 1,
"creating a Metal kernel",
));
}
Ok(Self { raw })
}
pub unsafe fn apply(
&self,
inputs: &[impl AsRef<Array>],
outputs: &[MetalKernelOutput],
grid: (i32, i32, i32),
thread_group: (i32, i32, i32),
options: MetalKernelApplyOptions,
) -> Result<Vec<Array>> {
crate::error::INIT_ERR_HANDLER.call_once(crate::error::setup_mlx_error_handler);
let config = KernelConfig::new()?;
for output in outputs {
config.check(
unsafe {
mlx_sys::mlx_fast_metal_kernel_config_add_output_arg(
config.raw,
output.shape.as_ptr(),
output.shape.len(),
output.dtype.into(),
)
},
"adding a Metal kernel output",
)?;
}
config.check(
unsafe {
mlx_sys::mlx_fast_metal_kernel_config_set_grid(config.raw, grid.0, grid.1, grid.2)
},
"setting the Metal kernel grid",
)?;
config.check(
unsafe {
mlx_sys::mlx_fast_metal_kernel_config_set_thread_group(
config.raw,
thread_group.0,
thread_group.1,
thread_group.2,
)
},
"setting the Metal kernel thread group",
)?;
for (name, arg) in &options.template_args {
let name = c_string(name)?;
let status = unsafe {
match *arg {
TemplateArg::Dtype(dtype) => {
mlx_sys::mlx_fast_metal_kernel_config_add_template_arg_dtype(
config.raw,
name.as_ptr(),
dtype.into(),
)
}
TemplateArg::Int(value) => {
mlx_sys::mlx_fast_metal_kernel_config_add_template_arg_int(
config.raw,
name.as_ptr(),
value,
)
}
TemplateArg::Bool(value) => {
mlx_sys::mlx_fast_metal_kernel_config_add_template_arg_bool(
config.raw,
name.as_ptr(),
value,
)
}
}
};
config.check(status, "adding a Metal kernel template argument")?;
}
if let Some(value) = options.init_value {
config.check(
unsafe { mlx_sys::mlx_fast_metal_kernel_config_set_init_value(config.raw, value) },
"setting the Metal kernel init value",
)?;
}
config.check(
unsafe {
mlx_sys::mlx_fast_metal_kernel_config_set_verbose(config.raw, options.verbose)
},
"setting Metal kernel verbosity",
)?;
let inputs = crate::utils::VectorArray::try_from_iter(inputs.iter())?;
let stream = Stream::thread_local_or_gpu();
Vec::<Array>::try_from_op(|res| unsafe {
mlx_sys::mlx_fast_metal_kernel_apply(
res,
self.raw,
inputs.as_ptr(),
config.raw,
stream.as_ref().as_ptr(),
)
})
}
}
impl Drop for MetalKernel {
fn drop(&mut self) {
unsafe { mlx_sys::mlx_fast_metal_kernel_free(self.raw) };
}
}
impl std::fmt::Debug for MetalKernel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MetalKernel").finish_non_exhaustive()
}
}
struct KernelConfig {
raw: mlx_sys::mlx_fast_metal_kernel_config,
}
impl KernelConfig {
fn new() -> Result<Self> {
let raw = unsafe { mlx_sys::mlx_fast_metal_kernel_config_new() };
if raw.ctx.is_null() {
return Err(crate::error::exception_from_status(
SUCCESS + 1,
"creating a Metal kernel config",
));
}
Ok(Self { raw })
}
fn check(&self, status: i32, operation: &str) -> Result<()> {
if status == SUCCESS {
Ok(())
} else {
Err(crate::error::exception_from_status(status, operation))
}
}
}
impl Drop for KernelConfig {
fn drop(&mut self) {
unsafe { mlx_sys::mlx_fast_metal_kernel_config_free(self.raw) };
}
}
struct StringVector {
raw: mlx_sys::mlx_vector_string,
}
impl StringVector {
fn new(values: &[&str]) -> Result<Self> {
let owned = values
.iter()
.map(|value| c_string(value))
.collect::<Result<Vec<_>>>()?;
let mut pointers: Vec<_> = owned.iter().map(|value| value.as_ptr()).collect();
let raw =
unsafe { mlx_sys::mlx_vector_string_new_data(pointers.as_mut_ptr(), pointers.len()) };
if raw.ctx.is_null() {
return Err(crate::error::exception_from_status(
SUCCESS + 1,
"creating an MLX string vector",
));
}
Ok(Self { raw })
}
}
impl Drop for StringVector {
fn drop(&mut self) {
let _ = unsafe { mlx_sys::mlx_vector_string_free(self.raw) };
}
}
fn c_string(value: &str) -> Result<CString> {
CString::new(value).map_err(|error| Exception::custom(error.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_metal_kernel_template_args_and_shapes() {
let kernel = MetalKernel::new(
"scale_shift",
&["a", "b"],
&["out1", "out2"],
"uint elem = thread_position_in_grid.x;
T v = a[elem] * T(SCALE);
out1[elem] = SHIFT ? v + b[0] : v;
out2[elem] = a_shape[0];",
MetalKernelOptions::default(),
)
.unwrap();
let a = &crate::ops::arange::<_, f32>(None, 8, None).unwrap() / 4.0;
let b = Array::from_slice(&[0.5_f32], &[1]);
let outputs = crate::with_device(crate::Device::gpu(), || unsafe {
kernel.apply(
&[&a, &b],
&[
MetalKernelOutput {
shape: vec![8],
dtype: Dtype::Float32,
},
MetalKernelOutput {
shape: vec![8],
dtype: Dtype::Int32,
},
],
(8, 1, 1),
(4, 1, 1),
MetalKernelApplyOptions {
template_args: vec![
("T".into(), TemplateArg::Dtype(Dtype::Float32)),
("SCALE".into(), TemplateArg::Int(3)),
("SHIFT".into(), TemplateArg::Bool(true)),
],
..Default::default()
},
)
})
.unwrap();
assert_eq!(
outputs[0].to_vec_exact::<f32>().unwrap(),
[0.5, 1.25, 2.0, 2.75, 3.5, 4.25, 5.0, 5.75]
);
assert_eq!(outputs[1].to_vec_exact::<i32>().unwrap(), [8; 8]);
}
#[test]
fn test_metal_kernel_init_value_fills_unwritten_outputs() {
let kernel = MetalKernel::new(
"touch_half",
&["a"],
&["out"],
"uint e = thread_position_in_grid.x; if (e < 2) out[e] = a[e];",
MetalKernelOptions::default(),
)
.unwrap();
let a = Array::from_slice(&[0.0_f32, 0.25, 0.5, 0.75], &[4]);
let out = crate::with_device(crate::Device::gpu(), || unsafe {
kernel.apply(
&[&a],
&[MetalKernelOutput {
shape: vec![4],
dtype: Dtype::Float32,
}],
(4, 1, 1),
(4, 1, 1),
MetalKernelApplyOptions {
init_value: Some(-1.0),
..Default::default()
},
)
})
.unwrap();
assert_eq!(
out[0].to_vec_exact::<f32>().unwrap(),
[0.0, 0.25, -1.0, -1.0]
);
}
#[test]
fn test_metal_kernel_rejects_invalid_arguments() {
let error = MetalKernel::new("none", &["a"], &[], "", MetalKernelOptions::default())
.expect_err("a kernel needs at least one output");
assert!(error.what().contains("at least one output"));
assert!(MetalKernel::new("nul", &["a\0"], &["out"], "", Default::default()).is_err());
let kernel = MetalKernel::new(
"copy",
&["a"],
&["out"],
"out[0] = a[0];",
Default::default(),
)
.unwrap();
let output = [MetalKernelOutput {
shape: vec![1],
dtype: Dtype::Float32,
}];
let a = Array::from_slice(&[1.0_f32], &[1]);
let no_inputs: [&Array; 0] = [];
let error = unsafe {
kernel.apply(
&no_inputs,
&output,
(1, 1, 1),
(1, 1, 1),
Default::default(),
)
}
.expect_err("input count must match construction");
assert!(error.what().contains("`inputs`"));
let error = crate::with_device(crate::Device::cpu(), || {
unsafe { kernel.apply(&[&a], &output, (1, 1, 1), (1, 1, 1), Default::default()) }
})
.expect_err("custom Metal kernels only run on the GPU");
assert!(error.what().contains("[metal_kernel]"));
}
#[test]
fn test_metal_kernel_gpu_fallback_and_scopes() {
let kernel = MetalKernel::new(
"stream_copy",
&["a"],
&["out"],
"out[0] = a[0];",
Default::default(),
)
.unwrap();
let a = Array::from_slice(&[3.0_f32], &[1]);
let output = [MetalKernelOutput {
shape: vec![1],
dtype: Dtype::Float32,
}];
let apply = || {
unsafe { kernel.apply(&[&a], &output, (1, 1, 1), (1, 1, 1), Default::default()) }
};
assert_eq!(apply().unwrap()[0].to_vec_exact::<f32>().unwrap(), [3.0]);
let previous = crate::Device::default();
crate::Device::set_default(&crate::Device::cpu());
let fallback = apply();
let gpu_scope = crate::with_device(crate::Device::gpu(), apply);
let cpu_scope = crate::with_device(crate::Device::cpu(), apply);
let gpu_stream = crate::with_stream(&Stream::gpu(), apply);
let cpu_stream = crate::with_stream(&Stream::cpu(), apply);
crate::Device::set_default(&previous);
for outputs in [fallback, gpu_scope, gpu_stream] {
assert_eq!(outputs.unwrap()[0].to_vec_exact::<f32>().unwrap(), [3.0]);
}
assert!(cpu_scope.is_err());
assert!(cpu_stream.is_err());
}
#[test]
fn test_metal_kernel_layout_copies_are_opt_in() {
let a = Array::from_slice(&[0.0_f32, 1.0, 2.0, 3.0, 4.0, 5.0], &[2, 3]);
let transposed = a.transpose().unwrap();
for (options, expected) in [
(
MetalKernelOptions::default(),
[0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
),
(
MetalKernelOptions {
ensure_row_contiguous: true,
..Default::default()
},
[0.0, 3.0, 1.0, 4.0, 2.0, 5.0],
),
] {
let kernel = MetalKernel::new(
"physical_copy",
&["a"],
&["out"],
"uint i = thread_position_in_grid.x; out[i] = a[i];",
options,
)
.unwrap();
let outputs = unsafe {
kernel.apply(
&[&transposed],
&[MetalKernelOutput {
shape: vec![6],
dtype: Dtype::Float32,
}],
(6, 1, 1),
(6, 1, 1),
Default::default(),
)
}
.unwrap();
assert_eq!(outputs[0].to_vec_exact::<f32>().unwrap(), expected);
}
}
#[test]
fn test_metal_kernel_errors_are_deferred() {
for (name, source, size, group) in [
("bad_source", "out[0] = a[0] +;", 1, 1),
(
"bad_group",
"uint i = thread_position_in_grid.x; out[i] = a[i];",
4096,
4096,
),
] {
let kernel =
MetalKernel::new(name, &["a"], &["out"], source, Default::default()).unwrap();
let input = Array::from_slice(&vec![1.0_f32; size as usize], &[size]);
let outputs = unsafe {
kernel.apply(
&[&input],
&[MetalKernelOutput {
shape: vec![size],
dtype: Dtype::Float32,
}],
(size, 1, 1),
(group, 1, 1),
Default::default(),
)
}
.expect("source and threadgroup errors are deferred");
assert!(outputs[0].eval().is_err());
}
let kernel = MetalKernel::new(
"after_error",
&["a"],
&["out"],
"out[0] = a;",
Default::default(),
)
.unwrap();
let input = Array::from_f32(2.0);
let outputs = unsafe {
kernel.apply(
&[&input],
&[MetalKernelOutput {
shape: vec![1],
dtype: Dtype::Float32,
}],
(1, 1, 1),
(1, 1, 1),
Default::default(),
)
}
.unwrap();
assert_eq!(outputs[0].to_vec_exact::<f32>().unwrap(), [2.0]);
}
}