mlx_rs/lib.rs
1//! Unofficial rust bindings for the [MLX
2//! framework](https://github.com/ml-explore/mlx).
3//!
4//! # Table of Contents
5//!
6//! - [Quick Start](#quick-start)
7//! - [Lazy Evaluation](#lazy-evaluation)
8//! - [Unified Memory](#unified-memory)
9//! - [Indexing Arrays](#indexing-arrays)
10//! - [Saving and Loading](#saving-and-loading)
11//!
12//! # Threading
13//!
14//! [`Array`] values may move between threads. MLX 0.32.2 keeps default streams per thread, so each
15//! worker must perform operations through its own default or thread-local scoped override.
16//! [`Stream`] and compiled closures are thread-affine and cannot move or be shared across threads.
17//! The scoped Rust override is thread-local, not asynchronous task-local, and does not propagate
18//! across `.await`.
19//!
20//! # Quick Start
21//!
22//! See also [MLX python
23//! documentation](https://ml-explore.github.io/mlx/build/html/usage/quick_start.html)
24//!
25//! ## Basics
26//!
27//! ```rust
28//! use mlx_rs::{array, Dtype};
29//!
30//! let a = array!([1, 2, 3, 4]);
31//! assert_eq!(a.shape(), &[4]);
32//! assert_eq!(a.dtype(), Dtype::Int32);
33//!
34//! let b = array!([1.0, 2.0, 3.0, 4.0]);
35//! assert_eq!(b.dtype(), Dtype::Float32);
36//! ```
37//!
38//! Operations in MLX are lazy. Use [`Array::eval`] to evaluate the the output
39//! of an operation. Operations are also automatically evaluated when inspecting
40//! an array with [`Array::item_exact`] or [`Array::item_cast`], printing an array, or attempting to obtain
41//! the underlying data with [`Array::as_slice`].
42//!
43//! ```rust
44//! use mlx_rs::{array, transforms::eval};
45//!
46//! let a = array!([1, 2, 3, 4]);
47//! let b = array!([1.0, 2.0, 3.0, 4.0]);
48//!
49//! let c = &a + &b; // c is not evaluated
50//! c.eval().unwrap(); // evaluates c
51//!
52//! let d = &a + &b;
53//! println!("{:?}", d); // evaluates d
54//!
55//! let e = &a + &b;
56//! let e_slice: &[f32] = e.as_slice(); // evaluates e
57//! ```
58//!
59//! See [Lazy Evaluation](#lazy-evaluation) for more details.
60//!
61//! ## Function and Graph Transformations
62//!
63//! TODO: https://github.com/oxiglade/mlx-rs/issues/214
64//!
65//! TODO: also document that all `Array` in the args for function
66//! transformations
67//!
68//! # Lazy Evaluation
69//!
70//! See also [MLX python
71//! documentation](https://ml-explore.github.io/mlx/build/html/usage/lazy_evaluation.html)
72//!
73//! ## Why Lazy Evaluation
74//!
75//! When you perform operations in MLX, no computation actually happens. Instead
76//! a compute graph is recorded. The actual computation only happens if an
77//! [`Array::eval`] is performed.
78//!
79//! MLX uses lazy evaluation because it has some nice features, some of which we
80//! describe below.
81//!
82//! ## Transforming Compute Graphs
83//!
84//! Lazy evaluation lets us record a compute graph without actually doing any
85//! computations. This is useful for function transformations like
86//! [`transforms::grad`] and graph optimizations.
87//!
88//! Currently, MLX does not compile and rerun compute graphs. They are all
89//! generated dynamically. However, lazy evaluation makes it much easier to
90//! integrate compilation for future performance enhancements.
91//!
92//! ## Only Compute What You Use
93//!
94//! In MLX you do not need to worry as much about computing outputs that are
95//! never used. For example:
96//!
97//! ```rust,ignore
98//! fn fun(x: &Array) -> (Array, Array) {
99//! let a = cheap_fun(x);
100//! let b = expensive_fun(x);
101//! (a, b)
102//! }
103//!
104//! let (y, _) = fun(&x);
105//! ```
106//!
107//! Here, we never actually compute the output of `expensive_fun`. Use this
108//! pattern with care though, as the graph of `expensive_fun` is still built,
109//! and that has some cost associated to it.
110//!
111//! Similarly, lazy evaluation can be beneficial for saving memory while keeping
112//! code simple. Say you have a very large model `Model` implementing
113//! [`module::Module`]. You can instantiate this model with `let model =
114//! Model::new()`. Typically, this will initialize all of the weights as
115//! `float32`, but the initialization does not actually compute anything until
116//! you perform an `eval()`. If you update the model with `float16` weights,
117//! your maximum consumed memory will be half that required if eager computation
118//! was used instead.
119//!
120//! This pattern is simple to do in MLX thanks to lazy computation:
121//!
122//! ```rust,ignore
123//! let mut model = Model::new();
124//! model.load_safetensors("model.safetensors").unwrap();
125//! ```
126//!
127//! ## When to Evaluate
128//!
129//! A common question is when to use `eval()`. The trade-off is between letting
130//! graphs get too large and not batching enough useful work.
131//!
132//! For example
133//!
134//! ```rust,ignore
135//! let mut a = array!([1, 2, 3, 4]);
136//! let mut b = array!([1.0, 2.0, 3.0, 4.0]);
137//!
138//! for _ in 0..100 {
139//! a = a + b;
140//! a.eval()?;
141//! b = b * 2.0;
142//! b.eval()?;
143//! }
144//! ```
145//!
146//! This is a bad idea because there is some fixed overhead with each graph
147//! evaluation. On the other hand, there is some slight overhead which grows
148//! with the compute graph size, so extremely large graphs (while
149//! computationally correct) can be costly.
150//!
151//! Luckily, a wide range of compute graph sizes work pretty well with MLX:
152//! anything from a few tens of operations to many thousands of operations per
153//! evaluation should be okay.
154//!
155//! Most numerical computations have an iterative outer loop (e.g. the iteration
156//! in stochastic gradient descent). A natural and usually efficient place to
157//! use `eval()` is at each iteration of this outer loop.
158//!
159//! Here is a concrete example:
160//!
161//! ```rust,ignore
162//! for batch in dataset {
163//! // Nothing has been evaluated yet
164//! let (loss, grad) = value_and_grad_fn(&mut model, batch)?;
165//!
166//! // Still nothing has been evaluated
167//! optimizer.update(&mut model, grad)?;
168//!
169//! // Evaluate the loss and the new parameters which will
170//! // run the full gradient computation and optimizer update
171//! eval_params(model.parameters())?;
172//! }
173//! ```
174//!
175//! An important behavior to be aware of is when the graph will be implicitly
176//! evaluated. Anytime you `print` an array, or otherwise access its memory via
177//! [`Array::as_slice`], the graph will be evaluated. Saving arrays via
178//! [`Array::save_numpy`] or [`Array::save_safetensors`] (or any other MLX
179//! saving functions) will also evaluate the array.
180//!
181//! Calling [`Array::item_exact`] or [`Array::item_cast`] on a scalar array will also evaluate it. In the
182//! example above, printing the loss (`println!("{:?}", loss)`) or pushing the
183//! loss scalar to a [`Vec`] (`losses.push(loss.item_exact::<f32>())`) would cause a
184//! graph evaluation. If these lines are before evaluating the loss and module
185//! parameters, then this will be a partial evaluation, computing only the
186//! forward pass.
187//!
188//! Also, calling `eval()` on an array or set of arrays multiple times is
189//! perfectly fine. This is effectively a no-op.
190//!
191//! **Warning**: Using scalar arrays for control-flow will cause an evaluation.
192//!
193//! ```rust,ignore
194//! fn fun(x: &Array) -> Array {
195//! let (h, y) = first_layer(x);
196//!
197//! if y.gt(array!(0.5)).unwrap().item_exact() {
198//! second_layer_a(h)
199//! } else {
200//! second_layer_b(h)
201//! }
202//! }
203//! ```
204//!
205//! Using arrays for control flow should be done with care. The above example
206//! works and can even be used with gradient transformations. However, this can
207//! be very inefficient if evaluations are done too frequently.
208//!
209//! # Unified Memory
210//!
211//! See also [MLX python
212//! documentation](https://ml-explore.github.io/mlx/build/html/usage/unified_memory.html)
213//!
214//! Apple silicon has a unified memory architecture. The CPU and GPU have direct
215//! access to the same memory pool. MLX is designed to take advantage of that.
216//!
217//! Concretely, when you make an array in MLX you don’t have to specify its
218//! location:
219//!
220//! ```rust
221//! let a = mlx_rs::random::normal::<f32>(&[100], None, None, None).unwrap();
222//! let b = mlx_rs::random::normal::<f32>(&[100], None, None, None).unwrap();
223//! ```
224//!
225//! Both `a` and `b` live in unified memory.
226//!
227//! In MLX, rather than moving arrays to devices, you specify the device when
228//! you run the operation. Any device can perform any operation on `a` and `b`
229//! without needing to move them from one memory location to another. For
230//! example:
231//!
232//! ```rust,ignore
233//! mlx_rs::with_device(mlx_rs::Device::cpu(), || mlx_rs::ops::add(&a, &b)).unwrap();
234//! mlx_rs::with_device(mlx_rs::Device::gpu(), || mlx_rs::ops::add(&a, &b)).unwrap();
235//! ```
236//!
237//! In the above, both the CPU and the GPU will perform the same add operation.
238//!
239//! TODO: The remaining python documentations states that the stream can be used
240//! to parallelize operations without worrying about racing conditions. We
241//! should check if this is true given that we've already observed data racing
242//! when executing unit tests in parallel.
243//!
244//! # Indexing Arrays
245//!
246//! See also [MLX python
247//! documentation](https://ml-explore.github.io/mlx/build/html/usage/indexing.html)
248//!
249//! Please refer to the indexing modules ([`ops::indexing`]) for more details.
250//!
251//! # Saving and Loading
252//!
253//! See also [MLX python
254//! documentation](https://ml-explore.github.io/mlx/build/html/usage/saving_and_loading.html)
255//!
256//! `mlx-rs` supports loading from `.npy` and `.safetensors` files and saving to
257//! `.safetensors` files. Module parameters and optimizer states can also be saved
258//! and loaded from `.safetensors` files.
259//!
260//! | type | load function | save function |
261//! |------|---------------|----------------|
262//! | [`Array`] | [`Array::load_numpy`] | [`Array::save_numpy`] |
263//! | `HashMap<String, Array>` | [`Array::load_safetensors`] | [`Array::save_safetensors`] |
264//! | [`module::Module`] | [`module::ModuleParametersExt::load_safetensors`] | [`module::ModuleParametersExt::save_safetensors`] |
265//! | [`optimizers::Optimizer`] | [`optimizers::OptimizerState::load_safetensors`] | [`optimizers::OptimizerState::save_safetensors`] |
266//!
267//! # Function Transforms
268//!
269//! See also [MLX python
270//! documentation](https://ml-explore.github.io/mlx/build/html/usage/function_transforms.html)
271//!
272//! Please refer to the transforms module ([`transforms`]) for more details.
273//!
274//! # Compilation
275//!
276//! See also [MLX python
277//! documentation](https://ml-explore.github.io/mlx/build/html/usage/compile.html)
278//!
279//! Please refer to the compilation module ([`transforms::compile`]) for more
280//! details.
281
282#![deny(unused_unsafe, missing_debug_implementations, missing_docs)]
283#![cfg_attr(test, allow(clippy::approx_constant))]
284
285#[macro_use]
286pub mod macros; // Must be first to ensure the other modules can use the macros
287
288mod array;
289pub mod builder;
290mod device;
291mod dtype;
292pub mod error;
293pub mod fast;
294pub mod fft;
295pub mod io;
296pub mod linalg;
297pub mod losses;
298pub mod memory;
299#[cfg(feature = "metal")]
300pub mod metal;
301pub mod module;
302pub mod nested;
303pub mod nn;
304pub mod ops;
305pub mod optimizers;
306mod options;
307pub mod quantization;
308pub mod random;
309mod stream;
310pub mod transforms;
311pub mod utils;
312
313/// Test-only assertion support shared with the workspace integration tests.
314#[doc(hidden)]
315pub mod test_utils;
316
317pub use array::*;
318pub use device::*;
319pub use dtype::*;
320pub use options::*;
321pub use stream::*;
322
323pub(crate) mod constants {
324 /// The default length of the stack-allocated vector in `SmallVec<[T; DEFAULT_STACK_VEC_LEN]>`
325 pub(crate) const DEFAULT_STACK_VEC_LEN: usize = 4;
326}
327
328pub(crate) mod sealed {
329 /// A marker trait to prevent external implementations of the `Sealed` trait.
330 pub trait Sealed {}
331
332 impl Sealed for () {}
333
334 impl<A> Sealed for (A,) where A: Sealed {}
335 impl<A, B> Sealed for (A, B)
336 where
337 A: Sealed,
338 B: Sealed,
339 {
340 }
341 impl<A, B, C> Sealed for (A, B, C)
342 where
343 A: Sealed,
344 B: Sealed,
345 C: Sealed,
346 {
347 }
348}