A checklist for reimplementing micrograd in Rust from scratch. Work top-to-bottom; each phase builds on the last. Check items off as you go.
Reference originals (in
../micrograd):engine.py,nn.py,test/test_engine.py,demo.ipynb,trace_graph.ipynb.
-
cargo new rustograd --libwithreplbinary - Module layout:
engine,nn,bin/repl,tests/engine.rs -
randdependency wired for weight init - Compiles green (
cargo build) and runs (cargo run --bin repl)
- Representation decided: arena / tape. Single
Nodestruct (data, grad, op),usizehandles,Graphowns theVec<Node>. NoRc/RefCell, no handle type, no operator overloading. (Skeleton already laid down.) - Flesh out the
Openum with every op you'll support (Leaf/Add/Mul/Pow/Relu — the full micrograd set;#[derive(Clone, Copy)]sobackwardcan match by value) -
Graph::value(data)leaf constructor +data/gradaccess viag.nodes[i] -
Displayfor a node matching Python'sValue(data=.., grad=..)
Each op computes its data from the inputs' data, then appends a node tagged
with the right Op variant and returns the new index:
-
add(a, b),mul(a, b)— core binary ops -
powf(a, exponent)— int/float powers only (as in micrograd) -
relu(a) - Derived ops (mirror
__neg__/__sub__/__truediv__):neg = mul(a, value(-1)),sub = add(a, neg(b)),div = mul(a, powf(b,-1)) - Scalar conveniences if you want them, e.g.
add_scalar(a, f64)— since there's no operator overloading, decide howValue + 2.0is spelled (skipped for now: tests/nn just create a scalar leaf withg.value(..))
- Topological sort — free: the append-only arena is already topo-ordered (every input index < its node's index), so no DFS/visited set is needed
- Seed output
grad = 1, walk nodes in reverse, apply each local gradient - Gradients accumulate (
+=), so a node used twice sums both paths - Correct local grads for every op: add, mul, pow, relu (and the derived ones)
- Port
test_sanity_check— bake in PyTorch's expecteddata/grad - Port
test_more_ops— exercise every op, assert within1e-6 - Remove the
#[ignore]attributes;cargo testpasses (3/3 green) - (Bonus) Numerical gradient check: finite-difference vs analytic grad
-
dropped — no polymorphism/Moduletraitdynuse and no shared default (we have nozero_grad), soparameters()is an inherent method on each type instead. Nozero_grad:backwardalready zeros every grad on entry. -
Neuron::new(g, nin, nonlin)withranduniform(-1, 1) weights, bias 0 -
Neuronforward:sum(wᵢ·xᵢ) + b, thenrelu()ifnonlin -
Layer=Vec<Neuron>; forward maps a layer over its neurons -
MLP=Vec<Layer>; last layer linear, earlier layers ReLU -
parameters()flattens the whole network's params -
Display/DebugmirroringMLP of [Layer of [...]]
- An
examples/demo.rsthat trains the MLP on a toy 2-D dataset (make-moons hand-rolled: two noisy arcs, Box-Muller noise, seeded RNG) - SVM "max-margin" hinge loss + L2 regularization, like the notebook
- Manual SGD loop: forward → loss →
backward()→ step params by-lr * grad(nozero_grad—backwardself-zeros; the watermarktruncateresets the tape) - Print loss + accuracy per step; confirm it actually learns (50% → 100% by step 40)
- (Bonus) ASCII decision-boundary plot at the end — a down payment on Phase 8
- Grow
src/bin/repl.rspast the echo skeleton - Add
rustylinefor line editing, history, and arrow keys - Bind variables (
a = Value(-4)) and evaluate expressions over them - Commands:
backward <var>,grad <var>,params,help,quit - Alternative/parallel track: use
evcxrfor a real Rust REPL —cargo install evcxr_repl, then:dep rustograd = { path = "." }. (evcxralso provides a Jupyter kernel, see Phase 8.)
Two kinds, matching the two micrograd notebooks:
- Computation graph (parity with
trace_graph.ipynb): walk the graph and emit Graphviz DOT text; render with thedotbinary (already installed) or thegraphviz-rustcrate. Show data + grad per node. - Data/plots (parity with
demo.ipynb): use theplotterscrate to draw the decision boundary and/or the loss curve to a PNG/SVG. - (Optional) Run the whole thing in a notebook via the
evcxrJupyter kernel for inline plots — the closest experience to the originals.
- Crate-level docs + a README with a quickstart
-
cargo clippyclean,cargo fmt - CI (GitHub Actions) running
build+test(optional)
| Need | Crate |
|---|---|
| Random weight init | rand (already added) |
| REPL line editing | rustyline |
| Plotting (matplotlib parity) | plotters |
| Graph rendering (graphviz parity) | graphviz-rust, or shell out to dot |
| Float-approx test asserts | approx (dev-dep) |
| Live REPL / Jupyter | evcxr (installed via cargo install, not a dep) |
The interesting Rust challenge was the representation: how to let many graph
parents share mutable child nodes under the borrow checker. Resolved by the
arena — share by usize index instead of by reference. With that decided, the
rest is mechanical: the real remaining substance is backward (Phase 3), which
becomes a reverse loop over the Vec with a match on each node's Op.