Skip to main content

mlx_c_rust/compile/
transforms.rs

1//! The MLX compile and transform domain: graph compilation, the compile
2//! cache, and the checkpoint/custom/JVP/VJP/value-and-gradient transforms.
3
4use crate::error::check_status;
5use crate::raw;
6use crate::types::closure_custom::{MlxClosureCustom, MlxClosureCustomJvp, MlxClosureCustomVmap};
7use crate::types::closures::{MlxClosure, MlxClosureValueAndGrad};
8use crate::types::vectors::MlxVectorInt;
9use crate::{MlxArray, MlxArrayVector, MlxCError};
10
11impl MlxClosure {
12    /// Wraps the closure so every call is compiled and cached by MLX.
13    pub fn compiled(&self, shapeless: bool) -> Result<Self, MlxCError> {
14        let operation: &'static str = "compile an MLX closure";
15        let mut output = Self::empty();
16        // SAFETY: Both handles are live and the output pointer is valid
17        // writable storage.
18        let status = unsafe { raw::mlx_compile(output.raw_mut(), self.0, shapeless) };
19        check_status(status, operation)?;
20        output.require_populated(operation)?;
21        Ok(output)
22    }
23
24    /// Wraps the closure so calls stop at the traced boundary, matching
25    /// MLX's checkpoint transform.
26    pub fn checkpointed(&self) -> Result<Self, MlxCError> {
27        let operation: &'static str = "checkpoint an MLX closure";
28        let mut output = Self::empty();
29        // SAFETY: Both handles are live and the output pointer is valid
30        // writable storage.
31        let status = unsafe { raw::mlx_checkpoint(output.raw_mut(), self.0) };
32        check_status(status, operation)?;
33        output.require_populated(operation)?;
34        Ok(output)
35    }
36
37    /// Wraps the closure with a custom vector-Jacobian product.
38    pub fn with_custom_vjp(&self, backward: &MlxClosureCustom) -> Result<Self, MlxCError> {
39        let operation: &'static str = "attach a custom VJP to an MLX closure";
40        let mut output = Self::empty();
41        // SAFETY: All handles are live and the output pointer is valid
42        // writable storage.
43        let status = unsafe { raw::mlx_custom_vjp(output.raw_mut(), self.0, backward.raw()) };
44        check_status(status, operation)?;
45        output.require_populated(operation)?;
46        Ok(output)
47    }
48
49    /// Wraps the closure with custom VJP, JVP, and VMAP rules.
50    #[allow(clippy::too_many_arguments)]
51    pub fn with_custom_rules(
52        &self,
53        backward: &MlxClosureCustom,
54        forward: &MlxClosureCustomJvp,
55        linearize: &MlxClosureCustomVmap,
56    ) -> Result<Self, MlxCError> {
57        let operation: &'static str = "attach custom rules to an MLX closure";
58        let mut output = Self::empty();
59        // SAFETY: All handles are live and the output pointer is valid
60        // writable storage.
61        let status = unsafe {
62            raw::mlx_custom_function(
63                output.raw_mut(),
64                self.0,
65                backward.raw(),
66                forward.raw(),
67                linearize.raw(),
68            )
69        };
70        check_status(status, operation)?;
71        output.require_populated(operation)?;
72        Ok(output)
73    }
74}
75
76/// Computes the Jacobian-vector product of a closure.
77///
78/// # Errors
79/// Returns the captured MLX-C description when the transform fails.
80pub fn jvp_of_closure(
81    function: &MlxClosure,
82    primals: &MlxArrayVector,
83    tangents: &MlxArrayVector,
84) -> Result<(MlxArrayVector, MlxArrayVector), MlxCError> {
85    let operation: &'static str = "apply the MLX JVP transform";
86    let mut outputs = MlxArrayVector::empty(operation)?;
87    let mut tangent_outputs = MlxArrayVector::empty(operation)?;
88    // SAFETY: All handles are live and both output pointers are valid
89    // writable storage.
90    let status = unsafe {
91        raw::mlx_jvp(
92            outputs.raw_mut(),
93            tangent_outputs.raw_mut(),
94            function.raw(),
95            primals.raw(),
96            tangents.raw(),
97        )
98    };
99    check_status(status, operation)?;
100    Ok((outputs, tangent_outputs))
101}
102
103/// Computes the vector-Jacobian product of a closure.
104///
105/// # Errors
106/// Returns the captured MLX-C description when the transform fails.
107pub fn vjp_of_closure(
108    function: &MlxClosure,
109    primals: &MlxArrayVector,
110    cotangents: &MlxArrayVector,
111) -> Result<(MlxArrayVector, MlxArrayVector), MlxCError> {
112    let operation: &'static str = "apply the MLX VJP transform";
113    let mut outputs = MlxArrayVector::empty(operation)?;
114    let mut cotangent_outputs = MlxArrayVector::empty(operation)?;
115    // SAFETY: All handles are live and both output pointers are valid
116    // writable storage.
117    let status = unsafe {
118        raw::mlx_vjp(
119            outputs.raw_mut(),
120            cotangent_outputs.raw_mut(),
121            function.raw(),
122            primals.raw(),
123            cotangents.raw(),
124        )
125    };
126    check_status(status, operation)?;
127    Ok((outputs, cotangent_outputs))
128}
129
130/// Builds the value-and-gradient closure for the given closure.
131///
132/// # Errors
133/// Returns the captured MLX-C description when the transform fails.
134pub fn value_and_grad_of_closure(
135    function: &MlxClosure,
136    argument_indices: &[i32],
137) -> Result<MlxClosureValueAndGrad, MlxCError> {
138    let operation: &'static str = "build the MLX value-and-gradient transform";
139    let mut output = MlxClosureValueAndGrad::empty();
140    // SAFETY: The closure handle is live, the slice remains valid for the
141    // call, and the output pointer is valid writable storage.
142    let status = unsafe {
143        raw::mlx_value_and_grad(
144            output.raw_mut(),
145            function.raw(),
146            argument_indices.as_ptr(),
147            argument_indices.len(),
148        )
149    };
150    check_status(status, operation)?;
151    Ok(output)
152}
153
154/// Owned MLX compile cache handle released exactly once.
155#[derive(Debug)]
156pub struct MlxCompileCache(raw::mlx_compile_cache);
157
158impl MlxCompileCache {
159    /// Creates an empty compile cache handle.
160    pub fn empty() -> Self {
161        let mut raw_cache = raw::mlx_compile_cache {
162            ctx: std::ptr::null_mut(),
163        };
164        // SAFETY: The output pointer is valid writable storage.
165        let status = unsafe { raw::mlx_detail_compile_cache(&mut raw_cache) };
166        debug_assert_eq!(status, 0);
167        Self(raw_cache)
168    }
169
170    /// Erases every compiled entry in this cache.
171    pub fn clear(&mut self) -> Result<(), MlxCError> {
172        // SAFETY: The cache handle is live.
173        let status = unsafe { raw::mlx_detail_compile_clear_cache(self.0) };
174        check_status(status, "clear an MLX compile cache")
175    }
176
177    /// Erases the compiled entry registered under `function_identity`.
178    pub fn erase(&mut self, function_identity: usize) -> Result<(), MlxCError> {
179        // SAFETY: The cache handle is live and the identity is a plain value.
180        let status = unsafe { raw::mlx_detail_compile_erase(self.0, function_identity) };
181        check_status(status, "erase an MLX compile cache entry")
182    }
183}
184
185impl Drop for MlxCompileCache {
186    fn drop(&mut self) {
187        // SAFETY: This owner releases its live cache exactly once and never
188        // accesses the handle afterward.
189        unsafe {
190            raw::mlx_compile_cache_free(self.0);
191        }
192    }
193}
194
195impl MlxClosure {
196    /// Compiles the closure with explicit tracing details into the process
197    /// compile cache.
198    ///
199    /// The `constants` array is the identity MLX uses for compile-time
200    /// constant folding.
201    ///
202    /// # Errors
203    /// Returns the captured MLX-C description when compilation fails.
204    pub fn compile_detailed(
205        &self,
206        function_identity: usize,
207        shapeless: bool,
208        constants: &[u64],
209    ) -> Result<Self, MlxCError> {
210        let operation: &'static str = "compile an MLX closure with tracing details";
211        let mut output = Self::empty();
212        // SAFETY: The closure handle is live, the slice remains valid for the
213        // call, and the output pointer is valid writable storage.
214        let status = unsafe {
215            raw::mlx_detail_compile(
216                output.raw_mut(),
217                self.0,
218                function_identity,
219                shapeless,
220                constants.as_ptr(),
221                constants.len(),
222            )
223        };
224        check_status(status, operation)?;
225        output.require_populated(operation)?;
226        Ok(output)
227    }
228}
229
230/// Disables MLX's compilation transform globally.
231pub fn disable_compile() -> Result<(), MlxCError> {
232    // SAFETY: Disabling takes no inputs and reports status through the
233    // captured-error machinery.
234    let status = unsafe { raw::mlx_disable_compile() };
235    check_status(status, "disable MLX compilation")
236}
237
238/// Re-enables MLX's compilation transform globally.
239pub fn enable_compile() -> Result<(), MlxCError> {
240    // SAFETY: Enabling takes no inputs and reports status through the
241    // captured-error machinery.
242    let status = unsafe { raw::mlx_enable_compile() };
243    check_status(status, "enable MLX compilation")
244}
245
246/// Selects the global compilation mode.
247pub fn set_compile_mode(mode: raw::mlx_compile_mode) -> Result<(), MlxCError> {
248    // SAFETY: The mode is a plain value and reports status through the
249    // captured-error machinery.
250    let status = unsafe { raw::mlx_set_compile_mode(mode) };
251    check_status(status, "set the MLX compile mode")
252}
253
254/// Replaces one traced array in a vector-of-arrays graph, the building block
255/// of the VMAP transform.
256///
257/// # Errors
258/// Returns the captured MLX-C description when the replacement fails.
259pub fn vmap_replace(
260    inputs: &MlxArrayVector,
261    traced_inputs: &MlxArrayVector,
262    traced_outputs: &MlxArrayVector,
263    input_axes: &[i32],
264    output_axes: &[i32],
265) -> Result<MlxArrayVector, MlxCError> {
266    let operation: &'static str = "apply the MLX VMAP replacement";
267    let mut output = MlxArrayVector::empty(operation)?;
268    // SAFETY: All handles are live, the slices remain valid for the call,
269    // and the output pointer is valid writable storage.
270    let status = unsafe {
271        raw::mlx_detail_vmap_replace(
272            output.raw_mut(),
273            inputs.raw(),
274            traced_inputs.raw(),
275            traced_outputs.raw(),
276            input_axes.as_ptr(),
277            input_axes.len(),
278            output_axes.as_ptr(),
279            output_axes.len(),
280        )
281    };
282    check_status(status, operation)?;
283    Ok(output)
284}
285
286/// Traces a closure for the VMAP transform, producing traced inputs and
287/// outputs plus the resolved axes.
288///
289/// # Errors
290/// Returns the captured MLX-C description when tracing fails.
291pub fn vmap_trace(
292    function: &MlxClosure,
293    inputs: &MlxArrayVector,
294    input_axes: &[i32],
295) -> Result<(MlxArrayVector, MlxArrayVector, MlxVectorInt), MlxCError> {
296    let operation: &'static str = "apply the MLX VMAP trace";
297    let mut traced_inputs = MlxArrayVector::empty(operation)?;
298    let mut traced_outputs = MlxArrayVector::empty(operation)?;
299    let output_axes = MlxVectorInt::empty();
300    // SAFETY: All handles are live, the slice remains valid for the call,
301    // and all output pointers are valid writable storage.
302    let status = unsafe {
303        raw::mlx_detail_vmap_trace(
304            traced_inputs.raw_mut(),
305            traced_outputs.raw_mut(),
306            function.raw(),
307            inputs.raw(),
308            input_axes.as_ptr(),
309            input_axes.len(),
310        )
311    };
312    check_status(status, operation)?;
313    Ok((traced_inputs, traced_outputs, output_axes))
314}
315
316impl MlxArray {
317    /// Schedules this array's evaluation after the dependencies and returns
318    /// the evaluated handle, matching MLX-C `mlx_depends`.
319    ///
320    /// # Errors
321    /// Returns the captured MLX-C description when scheduling fails.
322    pub fn evaluate_after(
323        &self,
324        dependencies: &MlxArrayVector,
325    ) -> Result<MlxArrayVector, MlxCError> {
326        let operation: &'static str = "evaluate an MLX array after its dependencies";
327        let outputs = MlxArrayVector::from_array(self)?;
328        let mut result = MlxArrayVector::empty(operation)?;
329        // SAFETY: All handles are live and the output pointer is valid
330        // writable storage.
331        let status =
332            unsafe { raw::mlx_depends(result.raw_mut(), outputs.raw(), dependencies.raw()) };
333        check_status(status, operation)?;
334        Ok(result)
335    }
336}