1use std::ffi::{CString, NulError};
2
3use crate::MlxBindingsContext;
4use crate::{MlxArray, MlxArrayVector, MlxCError, MlxDtype, MlxStream, error::check_status, raw};
5
6#[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#[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#[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 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 unsafe {
122 raw::mlx_fast_metal_kernel_free(self.raw_kernel);
123 }
124 }
125}
126
127impl MlxBindingsContext {
128 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 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#[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 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 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 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 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 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 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 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 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 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 pub fn set_verbose(&mut self, verbose: bool) -> Result<(), MlxCError> {
377 const OPERATION: &str = "configure MLX custom Metal kernel logging";
378 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 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 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 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}