1use std::time::Instant;
17
18use nuvec::{Array, DType, Device, Result, Stream, io, random, set_default_stream, value_and_grad};
19use nuvec_nn::losses::{Reduction, cross_entropy};
20use nuvec_nn::optim::{Optimizer, Sgd};
21use nuvec_nn::{Linear, Module, relu};
22
23#[derive(Clone, Module)]
24struct Mlp {
25 layers: Vec<Linear>,
26}
27
28impl Mlp {
29 fn new(sizes: &[i32]) -> Self {
30 let layers = sizes.windows(2).map(|w| Linear::new(w[0], w[1])).collect();
31 Self { layers }
32 }
33
34 fn forward(&self, x: &Array) -> Array {
35 let (last, hidden) = self.layers.split_last().expect("at least one layer");
36 let mut h = x.clone();
37 for layer in hidden {
38 h = relu(&layer.forward(&h));
39 }
40 last.forward(&h)
41 }
42}
43
44fn load(dir: &str, prefix: &str) -> Result<(Vec<f32>, Vec<u32>, usize)> {
46 let images = io::read_idx(format!("{dir}/{prefix}-images-idx3-ubyte"))?;
47 let labels = io::read_idx(format!("{dir}/{prefix}-labels-idx1-ubyte"))?;
48 let n = labels.size();
49 let x = images
50 .to_vec::<u8>()?
51 .iter()
52 .map(|&p| f32::from(p) / 255.0)
53 .collect();
54 let y = labels
55 .to_vec::<u8>()?
56 .iter()
57 .map(|&l| u32::from(l))
58 .collect();
59 Ok((x, y, n))
60}
61
62struct Shuffle(u64);
64
65impl Shuffle {
66 fn next(&mut self) -> u64 {
67 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
68 let mut z = self.0;
69 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
70 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
71 z ^ (z >> 31)
72 }
73
74 fn permutation(&mut self, n: usize) -> Vec<usize> {
75 let mut p: Vec<usize> = (0..n).collect();
76 for i in (1..n).rev() {
77 p.swap(i, (self.next() % (i as u64 + 1)) as usize);
78 }
79 p
80 }
81}
82
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); nuvec::eval(&state)?;
113 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}