use rlx_ir::op::{Activation, BinaryOp};
use rlx_ir::{DType, Graph, Op, Philox4x32, Shape};
use rlx_opscope::{Recorder, StatConfig, inject_matmul_stats};
use rlx_runtime::{Device, Session};
const PIXELS: usize = 784;
fn load_idx_images(path: &str, n: usize) -> std::io::Result<Vec<f32>> {
let bytes = std::fs::read(path)?;
let count = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize;
let take = n.min(count);
Ok(bytes[16..16 + take * PIXELS]
.iter()
.map(|&b| b as f32 / 255.0)
.collect())
}
fn mnist_mlp(batch: usize, hidden: usize) -> Graph {
let mut g = Graph::new("mnist_mlp");
let x = g.input("x", Shape::new(&[batch, PIXELS], DType::F32));
let w1 = g.param("fc1_w", Shape::new(&[PIXELS, hidden], DType::F32));
let b1 = g.param("fc1_b", Shape::new(&[hidden], DType::F32));
let w2 = g.param("fc2_w", Shape::new(&[hidden, 10], DType::F32));
let b2 = g.param("fc2_b", Shape::new(&[10], DType::F32));
let hs = Shape::new(&[batch, hidden], DType::F32);
let os = Shape::new(&[batch, 10], DType::F32);
let mm1 = g.matmul(x, w1, hs.clone());
g.node_mut(mm1).name = Some("fc1".into());
let h = g.add_node(Op::Binary(BinaryOp::Add), vec![mm1, b1], hs.clone());
let h = g.activation(Activation::Relu, h, hs);
let mm2 = g.matmul(h, w2, os.clone());
g.node_mut(mm2).name = Some("fc2".into());
let logits = g.add_node(Op::Binary(BinaryOp::Add), vec![mm2, b2], os);
g.set_outputs(vec![logits]);
g
}
fn he_weight(rng: &mut Philox4x32, rows: usize, cols: usize) -> Vec<f32> {
let mut w = vec![0f32; rows * cols];
rng.fill_normal(&mut w);
let scale = (2.0 / rows as f32).sqrt();
for v in &mut w {
*v *= scale;
}
w
}
fn home_mnist() -> String {
let home = std::env::var("HOME").unwrap_or_else(|_| "/Users/macmini".into());
format!("{home}/.cache/torchvision-mnist/MNIST/raw/train-images-idx3-ubyte")
}
fn main() -> std::io::Result<()> {
let out = std::env::args()
.nth(1)
.unwrap_or_else(|| "opscope_mnist.csv".into());
let steps: u64 = std::env::args()
.nth(2)
.and_then(|s| s.parse().ok())
.unwrap_or(12);
let (batch, hidden) = (64usize, 128usize);
let g = mnist_mlp(batch, hidden);
let (ginj, specs) = inject_matmul_stats(&g, &StatConfig::default());
let mut compiled = Session::new(Device::Cpu).compile(ginj);
let mut rng = Philox4x32::new(0xC0FFEE);
compiled.set_param("fc1_w", &he_weight(&mut rng, PIXELS, hidden));
compiled.set_param("fc2_w", &he_weight(&mut rng, hidden, 10));
compiled.set_param("fc1_b", &vec![0f32; hidden]);
compiled.set_param("fc2_b", &[0f32; 10]);
let path = home_mnist();
let pixels = load_idx_images(&path, batch * steps as usize)?;
let avail = pixels.len() / PIXELS;
if avail < batch {
eprintln!("[opscope] only {avail} images at {path}; need ≥{batch}");
return Ok(());
}
let steps = (avail / batch).min(steps as usize) as u64;
let mut rec = Recorder::create(&out)?;
for step in 0..steps {
let lo = step as usize * batch * PIXELS;
let x = &pixels[lo..lo + batch * PIXELS];
let outs = compiled.run(&[("x", x)]);
rec.record(0, step, "cpu", "mnist", batch, PIXELS, 0, &specs, &outs)?;
}
rec.flush()?;
eprintln!(
"[opscope] real MNIST MLP (batch={batch}, hidden={hidden}, {} sketches/step) × {steps} steps → {out}",
specs.len()
);
Ok(())
}