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//! ```text
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//! ```text
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
135//! # use mlx_rs::array;
136//! let mut a = array!([1, 2, 3, 4]);
137//! let mut b = array!([1.0, 2.0, 3.0, 4.0]);
138//!
139//! for _ in 0..3 {
140//! a = a + &b;
141//! a.eval()?;
142//! b = b * 2.0;
143//! b.eval()?;
144//! }
145//! assert_eq!(a.as_slice::<f32>(), &[8.0, 16.0, 24.0, 32.0]);
146//! assert_eq!(b.as_slice::<f32>(), &[8.0, 16.0, 24.0, 32.0]);
147//! # Ok::<(), mlx_rs::error::Exception>(())
148//! ```
149//!
150//! This is a bad idea because there is some fixed overhead with each graph
151//! evaluation. On the other hand, there is some slight overhead which grows
152//! with the compute graph size, so extremely large graphs (while
153//! computationally correct) can be costly.
154//!
155//! Luckily, a wide range of compute graph sizes work pretty well with MLX:
156//! anything from a few tens of operations to many thousands of operations per
157//! evaluation should be okay.
158//!
159//! Most numerical computations have an iterative outer loop (e.g. the iteration
160//! in stochastic gradient descent). A natural and usually efficient place to
161//! use `eval()` is at each iteration of this outer loop.
162//!
163//! Here is a concrete example:
164//!
165//! ```text
166//! for batch in dataset {
167//! // Nothing has been evaluated yet
168//! let (loss, grad) = value_and_grad_fn(&mut model, batch)?;
169//!
170//! // Still nothing has been evaluated
171//! optimizer.update(&mut model, grad)?;
172//!
173//! // Evaluate the loss and the new parameters which will
174//! // run the full gradient computation and optimizer update
175//! eval_params(model.parameters())?;
176//! }
177//! ```
178//!
179//! An important behavior to be aware of is when the graph will be implicitly
180//! evaluated. Anytime you `print` an array, or otherwise access its memory via
181//! [`Array::as_slice`], the graph will be evaluated. Saving arrays via
182//! [`Array::save_numpy`] or [`Array::save_safetensors`] (or any other MLX
183//! saving functions) will also evaluate the array.
184//!
185//! Calling [`Array::item_exact`] or [`Array::item_cast`] on a scalar array will also evaluate it. In the
186//! example above, printing the loss (`println!("{:?}", loss)`) or pushing the
187//! loss scalar to a [`Vec`] (`losses.push(loss.item_exact::<f32>())`) would cause a
188//! graph evaluation. If these lines are before evaluating the loss and module
189//! parameters, then this will be a partial evaluation, computing only the
190//! forward pass.
191//!
192//! Also, calling `eval()` on an array or set of arrays multiple times is
193//! perfectly fine. This is effectively a no-op.
194//!
195//! **Warning**: Using scalar arrays for control-flow will cause an evaluation.
196//!
197//! ```text
198//! fn fun(x: &Array) -> Array {
199//! let (h, y) = first_layer(x);
200//!
201//! if y.gt(array!(0.5)).unwrap().item_exact() {
202//! second_layer_a(h)
203//! } else {
204//! second_layer_b(h)
205//! }
206//! }
207//! ```
208//!
209//! Using arrays for control flow should be done with care. The above example
210//! works and can even be used with gradient transformations. However, this can
211//! be very inefficient if evaluations are done too frequently.
212//!
213//! # Unified Memory
214//!
215//! See also [MLX python
216//! documentation](https://ml-explore.github.io/mlx/build/html/usage/unified_memory.html)
217//!
218//! Apple silicon has a unified memory architecture. The CPU and GPU have direct
219//! access to the same memory pool. MLX is designed to take advantage of that.
220//!
221//! Concretely, when you make an array in MLX you don’t have to specify its
222//! location:
223//!
224//! ```rust
225//! let a = mlx_rs::random::normal::<f32>(&[100], None, None, None).unwrap();
226//! let b = mlx_rs::random::normal::<f32>(&[100], None, None, None).unwrap();
227//! ```
228//!
229//! Both `a` and `b` live in unified memory.
230//!
231//! In MLX, rather than moving arrays to devices, you specify the device when
232//! you run the operation. Any device can perform any operation on `a` and `b`
233//! without needing to move them from one memory location to another. For
234//! example:
235//!
236//! ```rust
237//! use mlx_rs::{array, ops, with_device, Device};
238//!
239//! let a = array!([1.0, 2.0]);
240//! let b = array!([3.0, 4.0]);
241//! let on_cpu = with_device(Device::cpu(), || ops::add(&a, &b)).unwrap();
242//! # assert_eq!(on_cpu.as_slice::<f32>(), &[4.0, 6.0]);
243//! # #[cfg(feature = "metal")]
244//! let on_gpu = with_device(Device::gpu(), || ops::add(&a, &b)).unwrap();
245//! # #[cfg(feature = "metal")]
246//! assert_eq!(on_cpu.as_slice::<f32>(), on_gpu.as_slice::<f32>());
247//! ```
248//!
249//! In the above, both the CPU and the GPU will perform the same add operation.
250//!
251//! TODO: The remaining python documentations states that the stream can be used
252//! to parallelize operations without worrying about racing conditions. We
253//! should check if this is true given that we've already observed data racing
254//! when executing unit tests in parallel.
255//!
256//! # Indexing Arrays
257//!
258//! See also [MLX python
259//! documentation](https://ml-explore.github.io/mlx/build/html/usage/indexing.html)
260//!
261//! Please refer to the indexing modules ([`ops::indexing`]) for more details.
262//!
263//! # Saving and Loading
264//!
265//! See also [MLX python
266//! documentation](https://ml-explore.github.io/mlx/build/html/usage/saving_and_loading.html)
267//!
268//! `mlx-rs` supports loading from `.npy` and `.safetensors` files and saving to
269//! `.safetensors` files. Module parameters and optimizer states can also be saved
270//! and loaded from `.safetensors` files.
271//!
272//! | type | load function | save function |
273//! |------|---------------|----------------|
274//! | [`Array`] | [`Array::load_numpy`] | [`Array::save_numpy`] |
275//! | `HashMap<String, Array>` | [`Array::load_safetensors`] | [`Array::save_safetensors`] |
276//! | [`module::Module`] | [`module::ModuleParametersExt::load_safetensors`] | [`module::ModuleParametersExt::save_safetensors`] |
277//! | [`optimizers::Optimizer`] | [`optimizers::OptimizerState::load_safetensors`] | [`optimizers::OptimizerState::save_safetensors`] |
278//!
279//! # Function Transforms
280//!
281//! See also [MLX python
282//! documentation](https://ml-explore.github.io/mlx/build/html/usage/function_transforms.html)
283//!
284//! Please refer to the transforms module ([`transforms`]) for more details.
285//!
286//! # Compilation
287//!
288//! See also [MLX python
289//! documentation](https://ml-explore.github.io/mlx/build/html/usage/compile.html)
290//!
291//! Please refer to the compilation module ([`transforms::compile`]) for more
292//! details.
293
294#![deny(unused_unsafe, missing_debug_implementations, missing_docs)]
295#![cfg_attr(test, allow(clippy::approx_constant))]
296
297#[macro_use]
298pub mod macros; // Must be first to ensure the other modules can use the macros
299
300mod array;
301pub mod builder;
302mod device;
303mod dtype;
304pub mod error;
305pub mod fast;
306pub mod fft;
307pub mod io;
308pub mod linalg;
309pub mod losses;
310pub mod memory;
311#[cfg(feature = "metal")]
312pub mod metal;
313pub mod module;
314pub mod nested;
315pub mod nn;
316pub mod ops;
317pub mod optimizers;
318mod options;
319pub mod quantization;
320pub mod random;
321mod stream;
322pub mod transforms;
323pub mod utils;
324
325/// Test-only assertion support shared with the workspace integration tests.
326#[doc(hidden)]
327pub mod test_utils;
328
329pub use array::*;
330pub use device::*;
331pub use dtype::*;
332pub use options::*;
333pub use stream::*;
334
335pub(crate) mod constants {
336 /// The default length of the stack-allocated vector in `SmallVec<[T; DEFAULT_STACK_VEC_LEN]>`
337 pub(crate) const DEFAULT_STACK_VEC_LEN: usize = 4;
338}
339
340pub(crate) mod sealed {
341 /// A marker trait to prevent external implementations of the `Sealed` trait.
342 pub trait Sealed {}
343
344 impl Sealed for () {}
345
346 impl<A> Sealed for (A,) where A: Sealed {}
347 impl<A, B> Sealed for (A, B)
348 where
349 A: Sealed,
350 B: Sealed,
351 {
352 }
353 impl<A, B, C> Sealed for (A, B, C)
354 where
355 A: Sealed,
356 B: Sealed,
357 C: Sealed,
358 {
359 }
360}