Skip to content

Commit 06fabbc

Browse files
alxmrsclaude
andcommitted
demo: train the MNIST MLP as one append-only model table
Rewrite mnist_mlp.py so the whole model and its entire training history live in a single append-only table model(step, layer, i, j, val): every parameter is a row tagged by generation, and a training step appends the next generation's rows rather than mutating anything. Each step is a single SQL statement (forward, grad(tanh(z),z) backprop, parameter update); evaluation is SQL too (a forward pass with ROW_NUMBER() for the argmax). Python no longer holds the weights or computes any gradients — it only sequences the steps. A 2-layer net can't be one recursive CTE (the recursive relation may be referenced only once, but W1/W2 are used several times per step) and unrolling the steps as non-recursive CTEs blows up exponentially (DataFusion inlines CTEs; no MATERIALIZED). Materialising between steps is therefore host-driven; the thin loop does exactly that. Reaches ~83% test accuracy over 60 steps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b8d3e83 commit 06fabbc

2 files changed

Lines changed: 265 additions & 199 deletions

File tree

benchmarks/README.md

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,37 @@ sidesteps that.)
6565
## `mnist_mlp.py` — train an MNIST MLP classifier in SQL
6666

6767
A one-hidden-layer neural network (196 -> 32 tanh -> 10 softmax, on 2x2-pooled
68-
14x14 MNIST) trained by gradient descent where **every gradient is computed in
69-
SQL**; the optimisation loop is plain Python. It is reverse-mode autodiff
70-
expressed as relational algebra:
68+
14x14 MNIST) where **every gradient is computed in SQL** and the whole model —
69+
with its entire training history — lives in a single table.
70+
71+
The model is one append-only table `model(step, layer, i, j, val)`: every
72+
parameter is a row, tagged by which generation (`step`) it belongs to. **A
73+
training step never mutates anything; it appends the next generation's rows.**
74+
`WHERE step = N` is the model at iteration N, and the full trajectory is the
75+
table. Each step is a *single* SQL statement that reads the current generation
76+
and writes the next — reverse-mode autodiff as relational algebra:
7177

7278
- **matmul = join + `GROUP BY SUM`** — a layer's pre-activation is
7379
`SUM(input * weight)` grouped by (sample, unit).
7480
- **local derivatives = `grad()`** — the hidden activation's Jacobian is
7581
`grad(tanh(z), z)`, the autograd feature doing the calculus per (sample, unit).
7682
- **cotangent propagation = join**, **parameter gradients = join + `GROUP BY
77-
AVG`**.
78-
79-
The MNIST images are registered as xarray (the library's core); the model
80-
weights and per-step intermediates are DataFusion in-memory tables (a matmul is
81-
a join over them). The only hand-written gradient is softmax + cross-entropy's
82-
`delta = softmax - onehot` (softmax couples classes through a per-sample
83-
normaliser, an aggregate `grad` does not cross). Reaches ~83% test accuracy in
84-
~20s. Downloads MNIST on first run.
85-
83+
AVG`**, and the update `w - lr*g` is emitted as the next generation's rows.
84+
85+
The images are registered as xarray (the library's core); evaluation is SQL too
86+
(a forward pass with `ROW_NUMBER()` for the argmax). The only hand-written
87+
gradient is softmax + cross-entropy's `delta = softmax - onehot` (softmax couples
88+
classes through a per-sample normaliser, which an aggregate `grad` does not
89+
cross). Reaches ~83% test accuracy over 60 steps (~140s on a laptop — the
90+
parameter updates run in SQL and every generation is kept as rows, so it trades
91+
speed for a fully relational, fully inspectable training history). Downloads
92+
MNIST on first run.
93+
94+
Why is the *outer* loop still Python rather than one recursive query (like
95+
`grad_descent.py`)? A recursive CTE may reference the recursive relation only
96+
once, but a 2-layer net uses the current weights several times per step (W1 and
97+
W2 forward, W2 again in backprop), so it can't be a single recursive statement.
98+
Training is also sequential and reuses each step's result, so steps must be
99+
*materialised* between iterations — which is exactly what the thin loop does
100+
(append a generation, then query it). All the maths stays in SQL; Python only
101+
sequences the steps.

0 commit comments

Comments
 (0)