Skip to main content

mlx_rs/transforms/compile/
mod.rs

1//! Compilation of functions.
2//!
3//! See also [MLX python
4//! documentation](https://ml-explore.github.io/mlx/build/html/usage/compile.html).
5//!
6//! MLX has a [`compile()`] function transformation which compiles computation
7//! graphs. Function compilation results in smaller graphs by merging common
8//! work and fusing certain operations. In many cases this can lead to big
9//! improvements in run-time and memory use.
10//!
11//! Getting started with compile() is simple, but there are some edge cases that
12//! are good to be aware of for more complex graphs and advanced usage.
13//!
14//! **WARN**: Because function transforms including compilation works on the
15//! computation graph, the user must ensure that all `Array`s are passed as
16//! inputs to the function/closure. Closures with captured `Array`s may not work
17//! as expected and may lead to undefined behavior.
18//!
19//! # Basic usage
20//!
21//! ```rust
22//! use mlx_rs::{Array, array, transforms::compile::compile, error::Exception};
23//!
24//! let fun = |(x, y): (&Array, &Array)| -> Result<Array, Exception> {
25//!    mlx_rs::ops::exp(x.negative()?)?.add(y)
26//! };
27//!
28//! let x = array!(1.0);
29//! let y = array!(2.0);
30//!
31//! // Regular call, no compilation
32//! let result = fun((&x, &y)).unwrap();
33//! // Prints: array(2.36788, dtype=float32)
34//! println!("{:?}", result);
35//!
36//! // Compile the function
37//! let mut compiled_fun = compile(fun, None);
38//! let result = compiled_fun((&x, &y)).unwrap();
39//! // Prints: array(2.36788, dtype=float32)
40//! println!("{:?}", result);
41//! ```
42//!
43//! The output of both the regular function and the compiled function is the
44//! same up to numerical precision.
45//!
46//! The first time you call a compiled function, MLX will build the compute
47//! graph, optimize it, and generate and compile code. This can be relatively
48//! slow. However, MLX will cache compiled functions, so calling a compiled
49//! function multiple times will not initiate a new compilation. This means you
50//! should typically compile functions that you plan to use more than once.
51//!
52//! ```rust
53//! use mlx_rs::{Array, array, transforms::compile::compile};
54//!
55//! let fun = |(x, y): (&Array, &Array)| {
56//!    mlx_rs::ops::exp(x.negative()?)?.add(y)
57//! };
58//!
59//! let x = array!(1.0);
60//! let y = array!(2.0);
61//!
62//! let mut compiled_fun = compile(fun, None);
63//!
64//! // Compiled here
65//! let result = compiled_fun((&x, &y)).unwrap();
66//!
67//! // Not compiled again
68//! let result = compiled_fun((&x, &y)).unwrap();
69//!
70//! // Not compiled again
71//! let compiled_fun2 = compile(fun, None);
72//! ```
73//!
74//! There are some important cases to be aware of that can cause a function to
75//! be recompiled:
76//!
77//! - Changing the shape or number of dimensions
78//! - Changing the type of any of the inputs
79//! - Changing the number of inputs to the function
80//!
81//! In certain cases only some of the compilation stack will be rerun (for
82//! example when changing the shapes) and in other cases the full compilation
83//! stack will be rerun (for example when changing the types). In general you
84//! should avoid compiling functions too frequently.
85//!
86//! Another idiom to watch out for is compiling functions which get created and
87//! destroyed frequently. This can happen, for example, when compiling an
88//! closure in a loop.
89//!
90//! # Pure Functions
91//!
92//! Compiled functions are intended to be pure; that is they should not have
93//! side effects. For example:
94//!
95//! ```rust,ignore
96//! use mlx_rs::{Array, array, transforms::compile::compile};
97//!
98//! let mut c = array!(0.5);
99//!
100//! let fun = |(x, y): (&Array, &Array)| {
101//!     let z = (x + y) * c;
102//!     mlx_rs::ops::exp(z)
103//! };
104//!
105//! let mut compiled = compile(fun, None);
106//!
107//! let x = array!(1.0);
108//! let y = array!(2.0);
109//!
110//! // This may lead to undefined behavior
111//! let result = compiled((&x, &y)).unwrap();
112//! println!("{:?}", result);
113//! ```
114//!
115//! Use [`compile_with_state()`] to compile functions that have side effects and
116//! pass the state as an mutable reference.
117//!
118//! ```rust
119//! use mlx_rs::{Array, array, transforms::compile::compile_with_state};
120//! let mut state = vec![];
121//!
122//! let fun = |state: &mut Vec<Array>, (x, y): (&Array, &Array)| {
123//!     let z = x + y;
124//!     let result = mlx_rs::ops::exp(&z);
125//!     state.push(z);
126//!     result
127//! };
128//!
129//! let x = array!(1.0);
130//! let y = array!(2.0);
131//!
132//! let mut compiled = compile_with_state(fun, None);
133//! let result = compiled(&mut state, (&x, &y)).unwrap();
134//! println!("{:?}", result);
135//! // println!("{:?}", state); // TODO: this currently doesn't work somehow
136//! ```
137//!
138//! This is particularly useful for compiling a function which includes an
139//! update to a container of arrays, as is commonly done when training the
140//! parameters of a [`crate::module::Module`].
141//!
142//! See mlx-rs/mlx-tests/tests/test_compile_with_state.rs for more examples.
143//!
144
145use std::{
146    marker::PhantomData,
147    rc::Rc,
148    sync::atomic::{AtomicUsize, Ordering},
149};
150
151use super::{Closure, Guarded, VectorArray};
152use crate::{
153    error::Exception,
154    utils::{StateLayoutEntry, SUCCESS},
155};
156
157#[allow(clippy::module_inception)]
158mod compile;
159mod compile_with_state;
160
161pub use compile::*;
162pub use compile_with_state::*;
163
164/// Globally enable the compilation of functions.
165///
166/// Default is enabled.
167pub fn enable_compile() {
168    unsafe {
169        mlx_sys::mlx_enable_compile();
170    }
171}
172
173/// Globally disable the compilation of functions.
174///
175/// Default is enabled.
176pub fn disable_compile() {
177    unsafe {
178        mlx_sys::mlx_disable_compile();
179    }
180}
181
182/// Clear the memory cache.
183pub fn clear_cache() {
184    if let Ok(cache) = CompileCache::current() {
185        cache.clear();
186    }
187}
188
189/// A compiled function that can be called.
190#[derive(Debug, Clone)]
191pub struct Compiled<F, G> {
192    f_marker: PhantomData<(F, Rc<()>)>,
193    state: CompiledState<G>,
194}
195
196struct CompileCache {
197    handle: mlx_sys::mlx_compile_cache,
198}
199
200type StateLayout = Vec<StateLayoutEntry>;
201
202impl std::fmt::Debug for CompileCache {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        f.debug_struct("CompileCache").finish_non_exhaustive()
205    }
206}
207
208impl CompileCache {
209    fn current() -> Result<Self, Exception> {
210        crate::error::INIT_ERR_HANDLER.call_once(crate::error::setup_mlx_error_handler);
211        let mut cache = Self {
212            handle: unsafe { mlx_sys::mlx_compile_cache_new() },
213        };
214        let status = unsafe { mlx_sys::mlx_detail_compile_cache(&mut cache.handle) };
215        crate::error::resume_closure_panic();
216        if status == SUCCESS {
217            return Ok(cache);
218        }
219
220        Err(crate::error::exception_from_status(
221            status,
222            "resolving the current MLX compile cache",
223        ))
224    }
225
226    fn clear(&self) {
227        consume_status(unsafe { mlx_sys::mlx_detail_compile_clear_cache(self.handle) });
228    }
229
230    fn erase(&self, fun_id: usize) {
231        consume_status(unsafe { mlx_sys::mlx_detail_compile_erase(self.handle, fun_id) });
232    }
233}
234
235impl Drop for CompileCache {
236    fn drop(&mut self) {
237        consume_status(unsafe { mlx_sys::mlx_compile_cache_free(self.handle) });
238    }
239}
240
241fn consume_status(status: i32) {
242    if status != SUCCESS {
243        let _ = crate::error::get_and_clear_last_mlx_error();
244    }
245}
246
247#[derive(Debug)]
248struct CompiledState<F> {
249    f: F,
250    shapeless: bool,
251    id: usize,
252    cache: CompileCache,
253    num_function_outputs: Option<usize>,
254    state_layout: Option<StateLayout>,
255}
256
257static NEXT_COMPILE_ID: AtomicUsize = AtomicUsize::new(1);
258
259impl<F> CompiledState<F> {
260    fn new(f: F, shapeless: bool) -> Self {
261        Self {
262            f,
263            shapeless,
264            id: NEXT_COMPILE_ID.fetch_add(1, Ordering::Relaxed),
265            cache: CompileCache::current().expect("failed to capture the MLX compile cache"),
266            num_function_outputs: None,
267            state_layout: None,
268        }
269    }
270}
271
272impl<F: Clone> Clone for CompiledState<F> {
273    fn clone(&self) -> Self {
274        Self::new(self.f.clone(), self.shapeless)
275    }
276}
277
278impl<F> Drop for CompiledState<F> {
279    fn drop(&mut self) {
280        self.cache.erase(self.id);
281    }
282}