Overview of mlx-sys
mainmlx-sys provides low-level Rust bindings to the mlx-c C API. These bindings are automatically generated using bindgen. This crate is intended for low-level access to the MLX framework via its C interface.repository·main·Indexed 18 days ago
https://github.com/oxiglade/mlx-rsSafe, idiomatic Rust bindings for Apple's MLX machine learning library, optimized for Apple Silicon. It features lazy evaluation, dynamic graphs, unified memory, and multi-device support (CPU/GPU). The project includes mlx-sys for low-level C API access and mlx-lm for LLM inference, providing tools for Llama model loading, KV cache management via the KeyValueCache trait, and text generation using a builder-pattern API.
mlx-sys provides low-level Rust bindings to the mlx-c C API. These bindings are automatically generated using bindgen. This crate is intended for low-level access to the MLX framework via its C interface.The mlx-rs library provides safe and idiomatic Rust bindings for Apple's MLX framework, featuring:
Unlike Python's MLX, mlx-rs requires you to be explicit about which arrays are traced in the computation graph. To avoid segfaults, do not capture arrays from the outer scope inside your loss function closure. Instead, pass all required arrays as inputs to the closure.
|inputs: &[Array]| -> Result<Array, Exception>.inputs slice inside the closure.transforms::grad to create the gradient function..eval() on the updated array.// 1. Define loss function with explicit inputs
let loss_fn = |inputs: &[Array]| -> Result<Array, Exception> {
let w = &inputs[0];
let x = &inputs[1];
let y = &inputs[2];
let y_pred = x.matmul(w)?;
let loss = Array::from_f32(0.5) * ops::mean(&ops::square(y_pred - y)?, None, None)?;
Ok(loss)
};
let argnums = &[0]; // Differentiate with respect to the first argument (w)
let mut inputs = vec![w, x, y];
// 2. Compute gradients
let grad = transforms::grad(loss_fn, argnums)(&inputs)?;
// 3. Update weights in the training loop
inputs[0] = &inputs[0] - Array::from_f32(learning_rate) * grad;
inputs[0].eval()?; // Ensure the update is materializedTo use mlx-rs in your Rust project, add it to your Cargo.toml dependencies. Note that the project follows MLX's versioning for the main crate.
[dependencies]
mlx-rs = "0.21.0"In MLX, performing operations on arrays does not trigger immediate computation; instead, it records a compute graph. To execute the operations and compute the actual values, you must explicitly call Array::eval or trigger an implicit evaluation.
Use Array::eval() to evaluate the output of an operation.
An array is automatically evaluated when you:
Array::item to get a scalar value.println!.Array::as_slice.Array::save_numpy or Array::save_safetensors.eval() too frequently (e.g., inside every single arithmetic operation) as there is fixed overhead per evaluation. A good pattern is to evaluate at the end of each training iteration (e.g., after computing loss and gradients).if array.item() > 0.5 { ... }) triggers an evaluation, which can be inefficient if done frequently.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 eMLX leverages Apple Silicon's unified memory architecture, where the CPU and GPU share the same memory pool. When you create an array, you do not need to specify a device location (like 'cpu' or 'gpu').
Instead of moving data between devices, you specify the device at the time of the operation. This allows any device to perform operations on the same array without expensive memory transfers.
// Arrays are created in unified memory by default
let a = mlx_rs::normal!(shape=&[100]).unwrap();
let b = mlx_rs::normal!(shape=&[100]).unwrap();
// Specify the device during the operation
mlx_rs::add!(&a, &b, stream=StreamOrDevice::cpu()).unwrap();
mlx_rs::add!(&a, &b, stream=StreamOrDevice::gpu()).unwrap();In mlx-rs, a Module represents a functional unit of a neural network (like a layer or a full model), while ModuleParameters represents the state (the weights) of that unit.
Module is composed of other Modules. When you call training_mode or freeze_parameters on a parent module, it is expected that these calls propagate down to all child modules.NestedHashMap (preserving the structural hierarchy of the model) or as a FlattenedModuleParam (a simple HashMap<Rc<str>, Array> where keys are full paths to the parameters).training_mode method is crucial for layers like Dropout or Batch Normalization, which behave differently during training and inference. The ModuleParametersExt::eval() method can be used to evaluate the module parameters.RandomState is used for reproducible random number generation. It holds the PRNG state and is compatible with compile_with_state, allowing random state to be tracked across JIT compilation boundaries (similar to Python's @partial(mx.compile, inputs=mx.random.state, ...)).
RandomState::new(): Creates a new state using a time-based seed.RandomState::with_seed(seed: u64): Creates a state from a specific seed for reproducibility.RandomState::from_key(key: Array): Creates a state from an existing PRNG key array.RandomState::next_key(): Advances the state and returns a new key for use in random operations.RandomState::seed(seed: u64): Reseeds the existing state.RandomState::as_array(): Returns a reference to the underlying state array.To use RandomState within a compiled function, pass it as a mutable argument to compile_with_state so the state can be updated during execution.
use mlx_rs::random::RandomState;
use mlx_rs::transforms::compile::compile_with_state;
use mlx_rs::random::categorical;
use mlx_rs::Array;
let mut state = RandomState::with_seed(42).unwrap();
let logits = Array::zeros::<f32>(&[1, 10]).unwrap();
let mut compiled = compile_with_state(
|state: &mut RandomState, x: &Array| {
let key = state.next_key()?;
categorical(x, None, None, Some(&key))
},
None
);
let result = compiled(&mut state, &logits).unwrap();In Fourier analysis, the zero-frequency component is often at the start of the array. fftshift moves this component to the center of the spectrum by swapping half-spaces. To revert this transformation, use ifftshift.
Note on Odd Lengths: For arrays with an odd number of elements, fftshift and ifftshift are not identical. You must use ifftshift to correctly undo a fftshift operation on odd-length data.
The scaling_config passed to RoPE initialization uses a HashMap<String, FloatOrString>. The FloatOrString enum allows configuration values to be either a literal f32 or a String (which can be parsed into an f32).
This is useful for handling configuration files where numbers might be represented as strings (e.g., "4096") or where special keywords like "default" or "linear" are used to select the RoPE type.
Most errors in mlx-rs originate from the underlying C++ MLX API and are wrapped in the Exception struct. An Exception contains a descriptive error message (what()) and the Location in the code where the error was first encountered.
Many high-level functions return a Result<T>, which is a type alias for std::result::Result<T, Exception>. When an operation fails, you can inspect the Exception to understand the cause (e.g., shape mismatches during tensor operations).
// Example of handling a shape mismatch error
let a = array!([1.0, 2.0, 3.0]);
let b = array!([4.0, 5.0]);
let result = a.add(&b);
if let Err(e) = result {
println!("Error: {}", e.what());
// Output might be: "Shapes (3) and (2) cannot be broadcast." at [location]
}You can enable specific hardware acceleration features using feature flags in your Cargo.toml:
metal: Enables Metal (GPU) usage.accelerate: Enables the use of Apple's Accelerate framework.[dependencies]
mlx-rs = {
version = "0.21.0",
features = ["metal", "accelerate"]
}