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