Skip to main content

Optimizer

Trait Optimizer 

Source
pub trait Optimizer {
    // Required methods
    fn apply(
        &mut self,
        params: &[(&str, &Array)],
        grads: &[&Array],
    ) -> Result<Vec<Array>>;
    fn init_params(&mut self, params: &[(&str, &Array)]);
    fn visit_state(&self, v: &mut Visit<'_>);
    fn visit_state_mut(&mut self, v: &mut VisitMut<'_>);
    fn learning_rate(&self) -> Array;
    fn set_learning_rate(&mut self, lr: f32);
    fn clone_box(&self) -> Box<dyn Optimizer>;

    // Provided methods
    fn init<M: Tree>(&mut self, model: &M)
       where Self: Sized { ... }
    fn update<M: Tree>(&mut self, model: &mut M, grads: &M) -> Result<()>
       where Self: Sized { ... }
}
Expand description

Updates a model’s trainable leaves from their gradients, as MLX’s Optimizer.

The trait is object-safe (Box<dyn Optimizer> is an optimizer and a tree too). Its state is reached only through Optimizer::visit_state and Optimizer::visit_state_mut (each optimizer has its own slot type).

Required Methods§

Source

fn apply( &mut self, params: &[(&str, &Array)], grads: &[&Array], ) -> Result<Vec<Array>>

One step over named parameters (the path keys each parameter’s state) and their gradients: the updated parameters, in order. Increments the step once.

Source

fn init_params(&mut self, params: &[(&str, &Array)])

Creates zero state for params that have none (paths with state are kept).

Source

fn visit_state(&self, v: &mut Visit<'_>)

Visits the state (step, learning_rate, slots) and the hyperparameters.

Source

fn visit_state_mut(&mut self, v: &mut VisitMut<'_>)

Visits the state mutably.

Source

fn learning_rate(&self) -> Array

The learning rate: the 0-D float32 array in the state.

Source

fn set_learning_rate(&mut self, lr: f32)

Sets the learning rate array (in a compiled step it is an input: no retrace).

Source

fn clone_box(&self) -> Box<dyn Optimizer>

A boxed copy (object-safe Clone).

Provided Methods§

Source

fn init<M: Tree>(&mut self, model: &M)
where Self: Sized,

Creates the state of every trainable leaf of model now (instead of at the first update), so a compiled step’s state structure is fixed from the start; also needed before loading a checkpoint into a fresh optimizer.

Examples found in repository?
examples/mnist.rs (line 111)
83fn main() -> Result<()> {
84    let args: Vec<String> = std::env::args().skip(1).collect();
85    let dir = args
86        .iter()
87        .find(|a| !a.starts_with("--"))
88        .cloned()
89        .unwrap_or_else(|| "data/mnist".into());
90    let epochs: usize = args
91        .iter()
92        .position(|a| a == "--epochs")
93        .and_then(|i| args.get(i + 1))
94        .map_or(10, |v| v.parse().expect("--epochs takes a number"));
95    let asynchronous = args.iter().any(|a| a == "--async");
96    let compiled = args.iter().any(|a| a == "--compile");
97    if args.iter().any(|a| a == "--cpu") {
98        set_default_stream(Stream::default_for(Device::cpu()));
99    }
100    let (train_x, train_y, n) = load(&dir, "train").inspect_err(|_| {
101        eprintln!("cannot read MNIST from {dir}: run `just mnist` (it downloads the data)");
102    })?;
103    let (test_x, test_y, n_test) = load(&dir, "t10k")?;
104    let pixels = train_x.len() / n;
105    let test_x = Array::from_slice(&test_x, [n_test, pixels]);
106    let test_y = Array::from_slice(&test_y, [n_test]);
107
108    let (batch_size, lr) = (256, 0.1);
109    random::seed(0);
110    let mut state = (Mlp::new(&[pixels as i32, 32, 32, 10]), Sgd::new(lr));
111    state.1.init(&state.0); // the optimizer's state exists before the first step
112    nuvec::eval(&state)?;
113    // The training step, compiled with the model and the optimizer as its state.
114    let step = nuvec::compile_mut(|(model, opt): &mut (Mlp, Sgd), (x, y): &(Array, Array)| {
115        let loss_fn = |m: &Mlp| cross_entropy(&m.forward(x), y, Reduction::Mean);
116        let (loss, grads) = value_and_grad(loss_fn)(model)?;
117        opt.update(model, &grads)?;
118        Ok(loss)
119    });
120    let mut shuffle = Shuffle(0);
121    let device = nuvec::default_stream().device;
122    let mut times = Vec::new();
123    let mut accuracy = 0.0;
124    for epoch in 0..epochs {
125        let tic = Instant::now();
126        let perm = shuffle.permutation(n);
127        for ids in perm.chunks(batch_size) {
128            let mut xb = Vec::with_capacity(ids.len() * pixels);
129            for &i in ids {
130                xb.extend_from_slice(&train_x[i * pixels..(i + 1) * pixels]);
131            }
132            let yb: Vec<u32> = ids.iter().map(|&i| train_y[i]).collect();
133            let x = Array::from_slice(&xb, [ids.len(), pixels]);
134            let y = Array::from_slice(&yb, [ids.len()]);
135            let batch = (x, y);
136            if compiled {
137                let _loss = step(&mut state, &batch)?;
138            } else {
139                let (model, opt) = &mut state;
140                let loss_fn =
141                    |m: &Mlp| cross_entropy(&m.forward(&batch.0), &batch.1, Reduction::Mean);
142                let (_, grads) = value_and_grad(loss_fn)(&*model)?;
143                opt.update(model, &grads)?;
144            }
145            if asynchronous {
146                nuvec::async_eval(&state.0)?;
147            } else {
148                nuvec::eval(&state.0)?;
149            }
150        }
151        let predictions = state.0.forward(&test_x).argmax(1);
152        let hits = predictions.equal(&test_y).astype(DType::F32);
153        accuracy = hits.mean(..).item::<f32>()?;
154        let seconds = tic.elapsed().as_secs_f64();
155        times.push(seconds);
156        println!("Epoch {epoch}: Test accuracy {accuracy:.3}, Time {seconds:.3} (s)");
157    }
158    let mut steady: Vec<f64> = times.iter().skip(1).copied().collect();
159    steady.sort_by(f64::total_cmp);
160    let median = steady.get(steady.len() / 2).copied().unwrap_or(times[0]);
161    println!(
162        "RESULT device={device} async={asynchronous} compiled={compiled} accuracy={accuracy:.4} epoch_seconds={median:.4}"
163    );
164    Ok(())
165}
Source

fn update<M: Tree>(&mut self, model: &mut M, grads: &M) -> Result<()>
where Self: Sized,

Applies one step to the trainable leaves of model from grads, a gradient tree of the model’s type with the model’s paths (else an error). Frozen parameters and buffers are left alone; their state is kept.

Examples found in repository?
examples/mnist.rs (line 117)
83fn main() -> Result<()> {
84    let args: Vec<String> = std::env::args().skip(1).collect();
85    let dir = args
86        .iter()
87        .find(|a| !a.starts_with("--"))
88        .cloned()
89        .unwrap_or_else(|| "data/mnist".into());
90    let epochs: usize = args
91        .iter()
92        .position(|a| a == "--epochs")
93        .and_then(|i| args.get(i + 1))
94        .map_or(10, |v| v.parse().expect("--epochs takes a number"));
95    let asynchronous = args.iter().any(|a| a == "--async");
96    let compiled = args.iter().any(|a| a == "--compile");
97    if args.iter().any(|a| a == "--cpu") {
98        set_default_stream(Stream::default_for(Device::cpu()));
99    }
100    let (train_x, train_y, n) = load(&dir, "train").inspect_err(|_| {
101        eprintln!("cannot read MNIST from {dir}: run `just mnist` (it downloads the data)");
102    })?;
103    let (test_x, test_y, n_test) = load(&dir, "t10k")?;
104    let pixels = train_x.len() / n;
105    let test_x = Array::from_slice(&test_x, [n_test, pixels]);
106    let test_y = Array::from_slice(&test_y, [n_test]);
107
108    let (batch_size, lr) = (256, 0.1);
109    random::seed(0);
110    let mut state = (Mlp::new(&[pixels as i32, 32, 32, 10]), Sgd::new(lr));
111    state.1.init(&state.0); // the optimizer's state exists before the first step
112    nuvec::eval(&state)?;
113    // The training step, compiled with the model and the optimizer as its state.
114    let step = nuvec::compile_mut(|(model, opt): &mut (Mlp, Sgd), (x, y): &(Array, Array)| {
115        let loss_fn = |m: &Mlp| cross_entropy(&m.forward(x), y, Reduction::Mean);
116        let (loss, grads) = value_and_grad(loss_fn)(model)?;
117        opt.update(model, &grads)?;
118        Ok(loss)
119    });
120    let mut shuffle = Shuffle(0);
121    let device = nuvec::default_stream().device;
122    let mut times = Vec::new();
123    let mut accuracy = 0.0;
124    for epoch in 0..epochs {
125        let tic = Instant::now();
126        let perm = shuffle.permutation(n);
127        for ids in perm.chunks(batch_size) {
128            let mut xb = Vec::with_capacity(ids.len() * pixels);
129            for &i in ids {
130                xb.extend_from_slice(&train_x[i * pixels..(i + 1) * pixels]);
131            }
132            let yb: Vec<u32> = ids.iter().map(|&i| train_y[i]).collect();
133            let x = Array::from_slice(&xb, [ids.len(), pixels]);
134            let y = Array::from_slice(&yb, [ids.len()]);
135            let batch = (x, y);
136            if compiled {
137                let _loss = step(&mut state, &batch)?;
138            } else {
139                let (model, opt) = &mut state;
140                let loss_fn =
141                    |m: &Mlp| cross_entropy(&m.forward(&batch.0), &batch.1, Reduction::Mean);
142                let (_, grads) = value_and_grad(loss_fn)(&*model)?;
143                opt.update(model, &grads)?;
144            }
145            if asynchronous {
146                nuvec::async_eval(&state.0)?;
147            } else {
148                nuvec::eval(&state.0)?;
149            }
150        }
151        let predictions = state.0.forward(&test_x).argmax(1);
152        let hits = predictions.equal(&test_y).astype(DType::F32);
153        accuracy = hits.mean(..).item::<f32>()?;
154        let seconds = tic.elapsed().as_secs_f64();
155        times.push(seconds);
156        println!("Epoch {epoch}: Test accuracy {accuracy:.3}, Time {seconds:.3} (s)");
157    }
158    let mut steady: Vec<f64> = times.iter().skip(1).copied().collect();
159    steady.sort_by(f64::total_cmp);
160    let median = steady.get(steady.len() / 2).copied().unwrap_or(times[0]);
161    println!(
162        "RESULT device={device} async={asynchronous} compiled={compiled} accuracy={accuracy:.4} epoch_seconds={median:.4}"
163    );
164    Ok(())
165}

Trait Implementations§

Source§

impl Clone for Box<dyn Optimizer>

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for dyn Optimizer

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Leaves for Box<dyn Optimizer>

A boxed optimizer is a tree through its state: the one hand-written tree impl of nuvec-nn (a trait object cannot be derived).

Source§

fn visit(&self, v: &mut Visit<'_>)

Reports every leaf (and child tree, enum variant, mode flag and static field) to v, in order.
Source§

impl Optimizer for Box<dyn Optimizer>

Source§

fn apply( &mut self, params: &[(&str, &Array)], grads: &[&Array], ) -> Result<Vec<Array>>

One step over named parameters (the path keys each parameter’s state) and their gradients: the updated parameters, in order. Increments the step once.
Source§

fn init_params(&mut self, params: &[(&str, &Array)])

Creates zero state for params that have none (paths with state are kept).
Source§

fn visit_state(&self, v: &mut Visit<'_>)

Visits the state (step, learning_rate, slots) and the hyperparameters.
Source§

fn visit_state_mut(&mut self, v: &mut VisitMut<'_>)

Visits the state mutably.
Source§

fn learning_rate(&self) -> Array

The learning rate: the 0-D float32 array in the state.
Source§

fn set_learning_rate(&mut self, lr: f32)

Sets the learning rate array (in a compiled step it is an input: no retrace).
Source§

fn clone_box(&self) -> Box<dyn Optimizer>

A boxed copy (object-safe Clone).
Source§

fn init<M: Tree>(&mut self, model: &M)
where Self: Sized,

Creates the state of every trainable leaf of model now (instead of at the first update), so a compiled step’s state structure is fixed from the start; also needed before loading a checkpoint into a fresh optimizer.
Source§

fn update<M: Tree>(&mut self, model: &mut M, grads: &M) -> Result<()>
where Self: Sized,

Applies one step to the trainable leaves of model from grads, a gradient tree of the model’s type with the model’s paths (else an error). Frozen parameters and buffers are left alone; their state is kept.
Source§

impl Tree for Box<dyn Optimizer>

Source§

fn visit_mut(&mut self, v: &mut VisitMut<'_>)

Reports every leaf (and child tree and mode flag) to v mutably, in the order of Leaves::visit.
Source§

fn for_each(&self, f: impl FnMut(&str, &Array))

Calls f(path, leaf) for every leaf, in order.
Source§

fn map(&self, f: impl FnMut(&str, &Array) -> Array) -> Self

The tree with every leaf replaced by f(path, leaf) (same structure; parameters keep their frozen flag).
Source§

fn leaves(&self) -> Vec<Array>

The leaves, in order.
Source§

fn paths(&self) -> Vec<String>

The leaves’ paths, in order.
Source§

fn flatten(&self) -> Vec<(String, Array)>

The leaves with their paths, in order (MLX’s tree_flatten).
Source§

fn unflatten( &self, leaves: impl IntoIterator<Item = Array>, ) -> Result<Self, Error>

A tree of this structure (self is the template) with leaves in order: exactly Tree::len of them, else an error naming both counts.
Source§

fn load_flat( &mut self, named: impl IntoIterator<Item = (String, Array)>, mode: LoadMode, ) -> Result<(), Error>

Replaces leaves by path. LoadMode::Strict requires exactly the tree’s paths with matching shapes; LoadMode::Partial assigns the matching names and ignores unknown ones (shapes must still match). Loaded dtypes are kept. On any problem the error lists every one (missing, unknown and mismatched names, sorted) and the tree is unchanged. Duplicate paths (in the tree or in named) are an error.
Source§

fn len(&self) -> usize

The number of leaves.
Source§

fn is_empty(&self) -> bool

Whether the tree has no leaves.
Source§

fn def(&self) -> TreeDef

The tree’s structure.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementations on Foreign Types§

Source§

impl Optimizer for Box<dyn Optimizer>

Source§

fn apply( &mut self, params: &[(&str, &Array)], grads: &[&Array], ) -> Result<Vec<Array>>

Source§

fn init_params(&mut self, params: &[(&str, &Array)])

Source§

fn visit_state(&self, v: &mut Visit<'_>)

Source§

fn visit_state_mut(&mut self, v: &mut VisitMut<'_>)

Source§

fn learning_rate(&self) -> Array

Source§

fn set_learning_rate(&mut self, lr: f32)

Source§

fn clone_box(&self) -> Box<dyn Optimizer>

Implementors§