Skip to main content

mlx_c_rust/metal/
kernel.rs

1use std::ffi::{CString, NulError};
2
3use crate::MlxBindingsContext;
4use crate::{MlxArray, MlxArrayVector, MlxCError, MlxDtype, MlxStream, error::check_status, raw};
5
6/// Output shape and dtype requested from one custom Metal kernel launch.
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub struct MlxMetalKernelOutput {
9    shape: Vec<i32>,
10    dtype: MlxDtype,
11}
12
13impl MlxMetalKernelOutput {
14    #[must_use]
15    pub const fn new(shape: Vec<i32>, dtype: MlxDtype) -> Self {
16        Self { shape, dtype }
17    }
18}
19
20/// Compile-time template argument passed to an MLX custom Metal kernel launch.
21#[derive(Clone, Copy, Debug, PartialEq)]
22pub enum MlxMetalKernelTemplateArgument {
23    Dtype {
24        name: &'static str,
25        dtype: MlxDtype,
26    },
27    Integer {
28        name: &'static str,
29        integer_template_argument: i32,
30    },
31    Boolean {
32        name: &'static str,
33        boolean_template_argument: bool,
34    },
35}
36
37/// Owned MLX custom Metal kernel function compiled from a source string.
38#[derive(Debug)]
39pub struct MlxMetalKernel {
40    raw_kernel: raw::mlx_fast_metal_kernel,
41}
42
43impl MlxMetalKernel {
44    pub fn new(
45        kernel_name: &str,
46        input_names: &[&str],
47        output_names: &[&str],
48        kernel_source: &str,
49    ) -> Result<Self, MlxCError> {
50        Self::new_with_header(kernel_name, input_names, output_names, "", kernel_source)
51    }
52
53    pub fn new_with_header(
54        kernel_name: &str,
55        input_names: &[&str],
56        output_names: &[&str],
57        kernel_header: &str,
58        kernel_source: &str,
59    ) -> Result<Self, MlxCError> {
60        Self::new_with_options(
61            kernel_name,
62            input_names,
63            output_names,
64            kernel_header,
65            kernel_source,
66            false,
67        )
68    }
69
70    pub fn new_with_options(
71        kernel_name: &str,
72        input_names: &[&str],
73        output_names: &[&str],
74        kernel_header: &str,
75        kernel_source: &str,
76        has_atomic_outputs: bool,
77    ) -> Result<Self, MlxCError> {
78        const OPERATION: &str = "create an MLX custom Metal kernel";
79        if input_names.is_empty() || output_names.is_empty() {
80            return Err(MlxCError {
81                operation: OPERATION,
82                description: "custom Metal kernels must declare at least one input and output"
83                    .to_owned(),
84            });
85        }
86        let kernel_name = c_string(kernel_name, OPERATION, "kernel name")?;
87        let kernel_source = c_string(kernel_source, OPERATION, "kernel source")?;
88        let kernel_header = c_string(kernel_header, OPERATION, "kernel header")?;
89        let input_name_vector = MlxStringVector::new(input_names, OPERATION)?;
90        let output_name_vector = MlxStringVector::new(output_names, OPERATION)?;
91        // SAFETY: C strings and vectors stay live through this copying constructor;
92        // Row-contiguous inputs and the explicit atomic-output contract match this kernel.
93        let raw_kernel = unsafe {
94            raw::mlx_fast_metal_kernel_new(
95                kernel_name.as_ptr(),
96                input_name_vector.raw(),
97                output_name_vector.raw(),
98                kernel_source.as_ptr(),
99                kernel_header.as_ptr(),
100                true,
101                has_atomic_outputs,
102            )
103        };
104        if raw_kernel.ctx.is_null() {
105            return Err(MlxCError {
106                operation: OPERATION,
107                description: "MLX returned an empty custom Metal kernel handle".to_owned(),
108            });
109        }
110        Ok(Self { raw_kernel })
111    }
112
113    const fn raw(&self) -> raw::mlx_fast_metal_kernel {
114        self.raw_kernel
115    }
116}
117
118impl Drop for MlxMetalKernel {
119    fn drop(&mut self) {
120        // SAFETY: This owner releases its live kernel handle exactly once.
121        unsafe {
122            raw::mlx_fast_metal_kernel_free(self.raw_kernel);
123        }
124    }
125}
126
127impl MlxBindingsContext {
128    /// Applies one owned custom Metal kernel on the runtime's GPU stream.
129    pub fn apply_metal_kernel(
130        &self,
131        kernel: &MlxMetalKernel,
132        input_arrays: &[&MlxArray],
133        output_specs: &[MlxMetalKernelOutput],
134        grid: [i32; 3],
135        thread_group: [i32; 3],
136        template_arguments: &[MlxMetalKernelTemplateArgument],
137    ) -> Result<Vec<MlxArray>, MlxCError> {
138        self.apply_metal_kernel_with_output_initialization(
139            kernel,
140            input_arrays,
141            output_specs,
142            grid,
143            thread_group,
144            template_arguments,
145            None,
146        )
147    }
148
149    #[allow(clippy::too_many_arguments)]
150    pub fn apply_metal_kernel_with_output_initialization(
151        &self,
152        kernel: &MlxMetalKernel,
153        input_arrays: &[&MlxArray],
154        output_specs: &[MlxMetalKernelOutput],
155        grid: [i32; 3],
156        thread_group: [i32; 3],
157        template_arguments: &[MlxMetalKernelTemplateArgument],
158        output_initial_value: Option<f32>,
159    ) -> Result<Vec<MlxArray>, MlxCError> {
160        const OPERATION: &str = "apply an MLX custom Metal kernel";
161        validate_kernel_launch(input_arrays, output_specs, grid, thread_group)?;
162        let input_vector = MlxArrayVector::new(input_arrays)?;
163        let kernel_config =
164            MlxMetalKernelConfig::new(output_specs, grid, thread_group, output_initial_value)?;
165        kernel_config.add_template_arguments(template_arguments)?;
166        let mut output_vector = MlxArrayVector::empty(OPERATION)?;
167        // SAFETY: Kernel, input vector, config, stream, and output vector are live;
168        // MLX populates the output vector synchronously as a lazy graph result.
169        let status = unsafe {
170            raw::mlx_fast_metal_kernel_apply(
171                output_vector.raw_mut(),
172                kernel.raw(),
173                input_vector.raw(),
174                kernel_config.raw(),
175                self.gpu_stream().raw(),
176            )
177        };
178        check_status(status, OPERATION)?;
179        let output_count = output_vector.len();
180        if output_count != output_specs.len() {
181            return Err(MlxCError {
182                operation: OPERATION,
183                description: format!(
184                    "custom Metal kernel returned {output_count} outputs but {} were requested",
185                    output_specs.len()
186                ),
187            });
188        }
189        (0..output_count)
190            .map(|output_index| output_vector.array_at(output_index, OPERATION))
191            .collect::<Result<Vec<_>, MlxCError>>()
192    }
193}
194
195/// Applies one custom Metal kernel inside a compiled-graph trace.
196///
197/// Identical to [`MlxBindingsContext::apply_metal_kernel`] minus the runtime handle:
198/// compiled-graph builders run without a runtime, on the stream the trace is
199/// being built for. Errors surface as plain status codes; the input contract
200/// (row-contiguous arrays, validated launch dimensions) is the builder's
201/// responsibility.
202#[allow(clippy::too_many_arguments)]
203pub fn apply_metal_kernel_in_graph(
204    kernel: &MlxMetalKernel,
205    input_arrays: &[&MlxArray],
206    output_specs: &[MlxMetalKernelOutput],
207    grid: [i32; 3],
208    thread_group: [i32; 3],
209    template_arguments: &[MlxMetalKernelTemplateArgument],
210    gpu_stream: &MlxStream,
211) -> Result<Vec<MlxArray>, i32> {
212    let input_vector = MlxArrayVector::new(input_arrays).map_err(|_| 1)?;
213    let kernel_config =
214        MlxMetalKernelConfig::new(output_specs, grid, thread_group, None).map_err(|_| 1)?;
215    kernel_config
216        .add_template_arguments(template_arguments)
217        .map_err(|_| 1)?;
218    let mut output_vector =
219        MlxArrayVector::empty("apply a Metal kernel in a graph trace").map_err(|_| 1)?;
220    // SAFETY: Kernel, input vector, config, stream, and output vector are live;
221    // MLX populates the output vector synchronously as a lazy graph result.
222    let status = unsafe {
223        raw::mlx_fast_metal_kernel_apply(
224            output_vector.raw_mut(),
225            kernel.raw(),
226            input_vector.raw(),
227            kernel_config.raw(),
228            gpu_stream.raw(),
229        )
230    };
231    if status != 0 {
232        return Err(status);
233    }
234    let output_count = output_vector.len();
235    if output_count != output_specs.len() {
236        return Err(1);
237    }
238    let mut outputs = Vec::with_capacity(output_count);
239    for output_index in 0..output_count {
240        outputs.push(
241            output_vector
242                .array_at(output_index, "apply a Metal kernel in a graph trace")
243                .map_err(|_| 1)?,
244        );
245    }
246    Ok(outputs)
247}
248
249#[derive(Debug)]
250pub struct MlxMetalKernelConfig {
251    raw_config: raw::mlx_fast_metal_kernel_config,
252}
253
254impl MlxMetalKernelConfig {
255    pub fn new(
256        output_specs: &[MlxMetalKernelOutput],
257        grid: [i32; 3],
258        thread_group: [i32; 3],
259        output_initial_value: Option<f32>,
260    ) -> Result<Self, MlxCError> {
261        const OPERATION: &str = "configure an MLX custom Metal kernel";
262        // SAFETY: The runtime error handler is installed before model loading,
263        // and MLX returns one owned config handle.
264        let raw_config = unsafe { raw::mlx_fast_metal_kernel_config_new() };
265        if raw_config.ctx.is_null() {
266            return Err(MlxCError {
267                operation: OPERATION,
268                description: "MLX returned an empty custom Metal kernel config".to_owned(),
269            });
270        }
271        let kernel_config = Self { raw_config };
272        if let Some(output_initial_value) = output_initial_value {
273            // SAFETY: The scalar initialization value is copied by MLX.
274            let status = unsafe {
275                raw::mlx_fast_metal_kernel_config_set_init_value(
276                    kernel_config.raw(),
277                    output_initial_value,
278                )
279            };
280            check_status(status, OPERATION)?;
281        }
282        for output_spec in output_specs {
283            // SAFETY: The shape slice remains live for this copying call.
284            let status = unsafe {
285                raw::mlx_fast_metal_kernel_config_add_output_arg(
286                    kernel_config.raw(),
287                    output_spec.shape.as_ptr(),
288                    output_spec.shape.len(),
289                    output_spec.dtype.to_raw(),
290                )
291            };
292            check_status(status, OPERATION)?;
293        }
294        // SAFETY: Scalar launch dimensions are validated before construction.
295        let status = unsafe {
296            raw::mlx_fast_metal_kernel_config_set_grid(
297                kernel_config.raw(),
298                grid[0],
299                grid[1],
300                grid[2],
301            )
302        };
303        check_status(status, OPERATION)?;
304        // SAFETY: Scalar launch dimensions are validated before construction.
305        let status = unsafe {
306            raw::mlx_fast_metal_kernel_config_set_thread_group(
307                kernel_config.raw(),
308                thread_group[0],
309                thread_group[1],
310                thread_group[2],
311            )
312        };
313        check_status(status, OPERATION)?;
314        Ok(kernel_config)
315    }
316
317    const fn raw(&self) -> raw::mlx_fast_metal_kernel_config {
318        self.raw_config
319    }
320
321    fn add_template_arguments(
322        &self,
323        template_arguments: &[MlxMetalKernelTemplateArgument],
324    ) -> Result<(), MlxCError> {
325        const OPERATION: &str = "configure an MLX custom Metal kernel";
326        for template_argument in template_arguments {
327            match *template_argument {
328                MlxMetalKernelTemplateArgument::Dtype { name, dtype } => {
329                    let argument_name = c_string(name, OPERATION, "template dtype name")?;
330                    // SAFETY: Name is live for this copying call; dtype is validated by MLX.
331                    let status = unsafe {
332                        raw::mlx_fast_metal_kernel_config_add_template_arg_dtype(
333                            self.raw(),
334                            argument_name.as_ptr(),
335                            dtype.to_raw(),
336                        )
337                    };
338                    check_status(status, OPERATION)?;
339                }
340                MlxMetalKernelTemplateArgument::Integer {
341                    name,
342                    integer_template_argument,
343                } => {
344                    let argument_name = c_string(name, OPERATION, "template integer name")?;
345                    // SAFETY: Name is live for this copying call; the scalar argument is copied.
346                    let status = unsafe {
347                        raw::mlx_fast_metal_kernel_config_add_template_arg_int(
348                            self.raw(),
349                            argument_name.as_ptr(),
350                            integer_template_argument,
351                        )
352                    };
353                    check_status(status, OPERATION)?;
354                }
355                MlxMetalKernelTemplateArgument::Boolean {
356                    name,
357                    boolean_template_argument,
358                } => {
359                    let argument_name = c_string(name, OPERATION, "template boolean name")?;
360                    // SAFETY: Name is live for this copying call; the scalar argument is copied.
361                    let status = unsafe {
362                        raw::mlx_fast_metal_kernel_config_add_template_arg_bool(
363                            self.raw(),
364                            argument_name.as_ptr(),
365                            boolean_template_argument,
366                        )
367                    };
368                    check_status(status, OPERATION)?;
369                }
370            }
371        }
372        Ok(())
373    }
374
375    /// Toggles MLX's verbose logging for this kernel launch.
376    pub fn set_verbose(&mut self, verbose: bool) -> Result<(), MlxCError> {
377        const OPERATION: &str = "configure MLX custom Metal kernel logging";
378        // SAFETY: The config handle is live and the flag is a plain value.
379        let status = unsafe { raw::mlx_fast_metal_kernel_config_set_verbose(self.raw(), verbose) };
380        check_status(status, OPERATION)
381    }
382}
383
384impl Drop for MlxMetalKernelConfig {
385    fn drop(&mut self) {
386        // SAFETY: This owner releases its live config handle exactly once.
387        unsafe {
388            raw::mlx_fast_metal_kernel_config_free(self.raw_config);
389        }
390    }
391}
392
393#[derive(Debug)]
394struct MlxStringVector {
395    raw_vector: raw::mlx_vector_string,
396}
397
398impl MlxStringVector {
399    fn new(strings: &[&str], operation: &'static str) -> Result<Self, MlxCError> {
400        let c_strings = strings
401            .iter()
402            .map(|source_string| c_string(source_string, operation, "string vector entry"))
403            .collect::<Result<Vec<_>, _>>()?;
404        let mut string_pointers = c_strings
405            .iter()
406            .map(|c_string| c_string.as_ptr())
407            .collect::<Vec<_>>();
408        // SAFETY: Pointers remain live for this copying constructor.
409        let raw_vector = unsafe {
410            raw::mlx_vector_string_new_data(string_pointers.as_mut_ptr(), string_pointers.len())
411        };
412        if raw_vector.ctx.is_null() {
413            return Err(MlxCError {
414                operation,
415                description: "MLX returned an empty string vector handle".to_owned(),
416            });
417        }
418        Ok(Self { raw_vector })
419    }
420
421    const fn raw(&self) -> raw::mlx_vector_string {
422        self.raw_vector
423    }
424}
425
426impl Drop for MlxStringVector {
427    fn drop(&mut self) {
428        // SAFETY: This owner releases its live string vector handle exactly once.
429        unsafe {
430            raw::mlx_vector_string_free(self.raw_vector);
431        }
432    }
433}
434
435fn validate_kernel_launch(
436    input_arrays: &[&MlxArray],
437    output_specs: &[MlxMetalKernelOutput],
438    grid: [i32; 3],
439    thread_group: [i32; 3],
440) -> Result<(), MlxCError> {
441    const OPERATION: &str = "apply an MLX custom Metal kernel";
442    if input_arrays.is_empty() || output_specs.is_empty() {
443        return Err(MlxCError {
444            operation: OPERATION,
445            description: "custom Metal kernel launches require at least one input and output"
446                .to_owned(),
447        });
448    }
449    if grid.iter().any(|dimension| *dimension <= 0)
450        || thread_group.iter().any(|dimension| *dimension <= 0)
451    {
452        return Err(MlxCError {
453            operation: OPERATION,
454            description: "custom Metal kernel grid and threadgroup dimensions must be positive"
455                .to_owned(),
456        });
457    }
458    Ok(())
459}
460
461fn c_string(
462    source_text: &str,
463    operation: &'static str,
464    value_description: &'static str,
465) -> Result<CString, MlxCError> {
466    CString::new(source_text).map_err(|source| nul_error(operation, value_description, source))
467}
468
469fn nul_error(
470    operation: &'static str,
471    value_description: &'static str,
472    source: NulError,
473) -> MlxCError {
474    MlxCError {
475        operation,
476        description: format!("{value_description} contains an interior NUL byte: {source}"),
477    }
478}