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§
Sourcefn apply(
&mut self,
params: &[(&str, &Array)],
grads: &[&Array],
) -> Result<Vec<Array>>
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.
Sourcefn init_params(&mut self, params: &[(&str, &Array)])
fn init_params(&mut self, params: &[(&str, &Array)])
Creates zero state for params that have none (paths with state are kept).
Sourcefn visit_state(&self, v: &mut Visit<'_>)
fn visit_state(&self, v: &mut Visit<'_>)
Visits the state (step, learning_rate, slots) and the hyperparameters.
Sourcefn visit_state_mut(&mut self, v: &mut VisitMut<'_>)
fn visit_state_mut(&mut self, v: &mut VisitMut<'_>)
Visits the state mutably.
Sourcefn learning_rate(&self) -> Array
fn learning_rate(&self) -> Array
The learning rate: the 0-D float32 array in the state.
Sourcefn set_learning_rate(&mut self, lr: f32)
fn set_learning_rate(&mut self, lr: f32)
Sets the learning rate array (in a compiled step it is an input: no retrace).
Provided Methods§
Sourcefn init<M: Tree>(&mut self, model: &M)where
Self: Sized,
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?
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}Sourcefn update<M: Tree>(&mut self, model: &mut M, grads: &M) -> Result<()>where
Self: Sized,
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?
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 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).
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§impl Optimizer for Box<dyn Optimizer>
impl Optimizer for Box<dyn Optimizer>
Source§fn apply(
&mut self,
params: &[(&str, &Array)],
grads: &[&Array],
) -> Result<Vec<Array>>
fn apply( &mut self, params: &[(&str, &Array)], grads: &[&Array], ) -> Result<Vec<Array>>
Source§fn init_params(&mut self, params: &[(&str, &Array)])
fn init_params(&mut self, params: &[(&str, &Array)])
params that have none (paths with state are kept).Source§fn visit_state(&self, v: &mut Visit<'_>)
fn visit_state(&self, v: &mut Visit<'_>)
step, learning_rate, slots) and the hyperparameters.Source§fn visit_state_mut(&mut self, v: &mut VisitMut<'_>)
fn visit_state_mut(&mut self, v: &mut VisitMut<'_>)
Source§fn learning_rate(&self) -> Array
fn learning_rate(&self) -> Array
Source§fn set_learning_rate(&mut self, lr: f32)
fn set_learning_rate(&mut self, lr: f32)
Source§impl Tree for Box<dyn Optimizer>
impl Tree for Box<dyn Optimizer>
Source§fn visit_mut(&mut self, v: &mut VisitMut<'_>)
fn visit_mut(&mut self, v: &mut VisitMut<'_>)
v mutably, in the order of
Leaves::visit.Source§fn map(&self, f: impl FnMut(&str, &Array) -> Array) -> Self
fn map(&self, f: impl FnMut(&str, &Array) -> Array) -> Self
f(path, leaf) (same structure; parameters keep
their frozen flag).Source§fn flatten(&self) -> Vec<(String, Array)>
fn flatten(&self) -> Vec<(String, Array)>
tree_flatten).Source§fn unflatten(
&self,
leaves: impl IntoIterator<Item = Array>,
) -> Result<Self, Error>
fn unflatten( &self, leaves: impl IntoIterator<Item = Array>, ) -> Result<Self, Error>
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>
fn load_flat( &mut self, named: impl IntoIterator<Item = (String, Array)>, mode: LoadMode, ) -> Result<(), Error>
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.Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".