Skip to main content

mlx_c_rust/compile/
graph.rs

1use std::os::raw::c_int;
2
3use crate::{MlxArray, MlxArrayVector, MlxCError, error::check_status, raw};
4
5/// The MLX C closure ABI every compiled-graph builder follows: it receives the
6/// output vector to populate and the input vector to read, and returns zero on
7/// success.
8pub type MlxGraphBuilder =
9    unsafe extern "C" fn(*mut raw::mlx_vector_array, raw::mlx_vector_array) -> c_int;
10
11/// Owns one compiled MLX graph with a single array output.
12///
13/// The closure ABI already returns an output vector, so one compiled graph
14/// serves both single-output [`MlxCompiledGraph::apply`] and multi-output
15/// [`MlxCompiledGraph::apply_multi`] replays.
16#[derive(Debug)]
17pub(crate) struct MlxCompiledGraph {
18    compiled_closure: MlxClosure,
19}
20
21impl MlxCompiledGraph {
22    /// Compiles one graph. `shapeless` selects MLX's shape-polymorphic
23    /// tracing: the model's shapeless elementwise graphs need it, but it
24    /// rejects static slices on this MLX version (their output shape cannot
25    /// be generalized), so static-shape graphs like the verification window
26    /// compile with `false`.
27    pub(crate) fn new(
28        graph_builder: MlxGraphBuilder,
29        compile_operation: &'static str,
30        shapeless: bool,
31    ) -> Result<Self, MlxCError> {
32        let source_closure = MlxClosure::from_function(graph_builder, compile_operation)?;
33        let mut compiled_closure = MlxClosure::empty();
34        // SAFETY: Both closure handles are live and uniquely owned. MLX copies
35        // the compiled function into `compiled_closure`.
36        let compile_status = unsafe {
37            raw::mlx_compile(compiled_closure.raw_mut(), source_closure.raw(), shapeless)
38        };
39        check_status(compile_status, compile_operation)?;
40        compiled_closure.require_populated(compile_operation)?;
41        Ok(Self { compiled_closure })
42    }
43
44    pub(crate) fn apply(
45        &self,
46        graph_inputs: &[&MlxArray],
47        apply_operation: &'static str,
48    ) -> Result<MlxArray, MlxCError> {
49        let input_vector = MlxArrayVector::new(graph_inputs)?;
50        let mut output_vector = MlxArrayVector::empty(apply_operation)?;
51        // SAFETY: The compiled closure and input vector remain live for the
52        // synchronous graph-building call, and the output vector is unique.
53        let apply_status = unsafe {
54            raw::mlx_closure_apply(
55                output_vector.raw_mut(),
56                self.compiled_closure.raw(),
57                input_vector.raw(),
58            )
59        };
60        check_status(apply_status, apply_operation)?;
61        if output_vector.len() != 1 {
62            return Err(MlxCError {
63                operation: apply_operation,
64                description: format!(
65                    "compiled MLX graph returned {} outputs instead of one",
66                    output_vector.len()
67                ),
68            });
69        }
70        output_vector.array_at(0, apply_operation)
71    }
72
73    /// Applies the compiled graph and returns every output it produced.
74    ///
75    /// Multi-output graphs order their outputs by position, exactly like the
76    /// graph builder wrote them; callers must consume them by that fixed ABI.
77    pub(crate) fn apply_multi(
78        &self,
79        graph_inputs: &[&MlxArray],
80        apply_operation: &'static str,
81    ) -> Result<Vec<MlxArray>, MlxCError> {
82        let input_vector = MlxArrayVector::new(graph_inputs)?;
83        let mut output_vector = MlxArrayVector::empty(apply_operation)?;
84        // SAFETY: The compiled closure and input vector remain live for the
85        // synchronous graph-building call, and the output vector is unique.
86        let apply_status = unsafe {
87            raw::mlx_closure_apply(
88                output_vector.raw_mut(),
89                self.compiled_closure.raw(),
90                input_vector.raw(),
91            )
92        };
93        check_status(apply_status, apply_operation)?;
94        let output_count = output_vector.len();
95        if output_count == 0 {
96            return Err(MlxCError {
97                operation: apply_operation,
98                description: "compiled MLX graph returned no outputs".to_owned(),
99            });
100        }
101        (0..output_count)
102            .map(|output_index| output_vector.array_at(output_index, apply_operation))
103            .collect::<Result<Vec<_>, MlxCError>>()
104    }
105}
106
107/// Reads one array out of a closure's input vector inside a graph builder.
108pub fn array_from_vector(
109    input_vector: raw::mlx_vector_array,
110    input_index: usize,
111) -> Result<MlxArray, c_int> {
112    let mut input_array = MlxArray::empty();
113    // SAFETY: The input vector is live for the callback and the destination
114    // array owner is uniquely writable. MLX copies the selected handle.
115    let get_status =
116        unsafe { raw::mlx_vector_array_get(input_array.raw_mut(), input_vector, input_index) };
117    if get_status != 0 || input_array.is_empty() {
118        return Err(if get_status == 0 { 1 } else { get_status });
119    }
120    Ok(input_array)
121}
122
123/// Runs one raw MLX operation inside a graph builder and owns its lazy output.
124pub fn graph_output_array(
125    build_graph: impl FnOnce(*mut raw::mlx_array) -> c_int,
126) -> Result<MlxArray, c_int> {
127    let mut output_array = MlxArray::empty();
128    let build_status = build_graph(output_array.raw_mut());
129    if build_status != 0 || output_array.is_empty() {
130        return Err(if build_status == 0 { 1 } else { build_status });
131    }
132    Ok(output_array)
133}
134
135/// Publishes the single output of a single-output graph builder.
136///
137/// # Safety
138/// The caller must guarantee that `output_vector` is unique, live, and a
139/// valid `mlx_vector_array` destination for the whole call.
140pub unsafe fn set_graph_output(
141    output_vector: *mut raw::mlx_vector_array,
142    graph_output: &MlxArray,
143) -> c_int {
144    // SAFETY: The caller guarantees that the destination vector is unique and
145    // live. MLX copies the lazy output handle before local owners are released.
146    unsafe { raw::mlx_vector_array_set_value(output_vector, graph_output.raw()) }
147}
148
149/// Appends one lazy output to a multi-output graph's result vector.
150///
151/// MLX appends through the vector's context pointer, so pass-by-value still
152/// mutates the caller's vector; the pointer is dereferenced once for the copy.
153/// Publishes a multi-output graph's complete ordered result vector at once.
154///
155/// Compiled-graph tracing hands builders an output vector whose context may
156/// still be null, and MLX's append operation throws on that null context while
157/// its whole-vector set allocates on demand; collecting the outputs locally and
158/// publishing them once at the end of the builder is therefore the only trace-
159/// safe multi-output pattern.
160///
161/// # Safety
162/// The caller must guarantee that `output_vector` is unique, live, and a
163/// valid `mlx_vector_array` destination for the whole call.
164pub unsafe fn set_graph_output_vector(
165    output_vector: *mut raw::mlx_vector_array,
166    graph_outputs: &[&MlxArray],
167) -> c_int {
168    let output_handles: Vec<raw::mlx_array> = graph_outputs.iter().map(|o| o.raw()).collect();
169    // SAFETY: The caller guarantees that the destination vector is unique and
170    // live. MLX copies the lazy output handles before local owners are released.
171    unsafe {
172        raw::mlx_vector_array_set_data(output_vector, output_handles.as_ptr(), output_handles.len())
173    }
174}
175
176#[derive(Debug)]
177struct MlxClosure {
178    raw_closure: raw::mlx_closure,
179}
180
181impl MlxClosure {
182    fn empty() -> Self {
183        // SAFETY: MLX returns its documented null-context output placeholder;
184        // `mlx_compile` must populate it before callers can apply the closure.
185        let raw_closure = unsafe { raw::mlx_closure_new() };
186        Self { raw_closure }
187    }
188
189    fn from_function(
190        graph_builder: MlxGraphBuilder,
191        compile_operation: &'static str,
192    ) -> Result<Self, MlxCError> {
193        // SAFETY: The callback has static lifetime and follows the MLX C closure ABI.
194        let raw_closure = unsafe { raw::mlx_closure_new_func(Some(graph_builder)) };
195        if raw_closure.ctx.is_null() {
196            return Err(MlxCError {
197                operation: compile_operation,
198                description: "MLX returned an empty closure handle".to_owned(),
199            });
200        }
201        Ok(Self { raw_closure })
202    }
203
204    const fn raw(&self) -> raw::mlx_closure {
205        self.raw_closure
206    }
207
208    fn raw_mut(&mut self) -> *mut raw::mlx_closure {
209        &mut self.raw_closure
210    }
211
212    fn require_populated(&self, compile_operation: &'static str) -> Result<(), MlxCError> {
213        if self.raw_closure.ctx.is_null() {
214            return Err(MlxCError {
215                operation: compile_operation,
216                description: "MLX left the compiled closure handle empty".to_owned(),
217            });
218        }
219        Ok(())
220    }
221}
222
223impl Drop for MlxClosure {
224    fn drop(&mut self) {
225        // SAFETY: This owner releases its live closure handle exactly once.
226        unsafe {
227            raw::mlx_closure_free(self.raw_closure);
228        }
229    }
230}