This directory supplies reproducible grid energy and time for the current MNIST-small 60% and MNIST-medium 96% submissions. It lowers their complete ordered-FP32 training and prediction to instruction set v4 and scores a legal globally serialized schedule. It is a conservative baseline: just one access is outstanding across the machine, including tape staging and remote accesses. It makes no claim about the best attainable parallel grid performance.
| Configuration | Grid energy (mJ) | Grid time (ms) | Allocated scratch (bytes) | Time to score (s) |
|---|---|---|---|---|
| Small: 9→32→10, 300 epochs, batch 25, 1,000 train / 1,000 test | 0.24 | 3.1 × 10³ | 91,116 | 0.14 |
| Medium: 81→512→10, 200 epochs, batch 25, 10,000 train / 10,000 test | 4.4 × 10² | 3.7 × 10⁶ | 3,973,260 | 5.1 |
These are costs for one full training-and-prediction dataset, not the sum of 11 evaluation datasets. The trace and placement do not depend on the input values. Changing the declared learner seed changes initialization literal bits without changing the number or address of any access, so all 11 runs of a fixed configuration have these same grid costs. The linked submission reports contain the separate accuracy and measured A100 evidence. No A100 measurement is used to infer a grid score.
The machine is
spatial-computer pitch 128 / ISA v4 at revision 01a0bd5e0d2564825b0f53dd766f763c82dbc7c0.
This revision was verified from the repository’s main on September 12, 2026.
It has 250 × 125 processor tiles, each with 12,288 addressable 32-bit scratch
words; bottom-edge tapes are charged and are striped over 250 ports.
The program includes scratch initialization, seed-only initial parameter
literals, receiving every input, pixel normalization x * 4 - 0.5, one-hot
targets, every minibatch of training, every prediction, and all output labels.
The supplied inputs are already 3 × 3 or 9 × 9 FP32 images; dataset sampling and
downsampling define the workload inputs and are outside this training and
prediction scope. No trained values, test labels, or input-dependent immediates
enter the generated program.
Multiplication, addition and subtraction round separately to IEEE FP32 with round-to-nearest, ties-to-even. There is no fused multiply-add. Reductions run in ascending index order. ReLU and first-index argmax use strict comparisons. Labels and output class indices are raw uint32 words. Label equality and class constants agree for the complete allowed range 0–9. Conditional selection copies one raw word; both candidate source operands are read and charged. The model charges data movement rather than arithmetic or instruction fetch.
model_ir.py generalizes the historical affine MLP generator. affine.py
retains its bounded affine-loop validation, definite-initialization proof and
exact address-use histograms, while replacing the former single-core revision
and placement contract. score.py supplies the new physical placement, tape
lowering and serialized cycle/energy accounting. Historical free-tape,
single-core energy/time or area scores are not reused.
The same workload tape layout is fixed for both configurations before scoring any solution:
Global input word k is input tape word floor(k/250) at port k mod 250.
There is no packing of multiple pixels or labels into one word. Output word
q is the raw uint32 predicted label for test image q, on output port
q mod 250 at index floor(q/250). No test labels are on any input tape.
Small consumes 19,000 words and emits 1,000 words. Medium consumes 1,630,000
words and emits 10,000 words. The exact per-port counts are in each score JSON.
Training images, labels and one-hot targets remain in scratch across epochs;
tapes are never rewound or used as memory. Test queries stream through one
reused D-word scratch buffer after training, with one output emitted per query.
All arithmetic executes on P(125,0), whose core is at (64,64). Each bottom
processor P(i,0) also issues its own tape instructions. There are 250
instruction-issuing processors, with at most one executing at any instant.
Other owning tiles service remote scratch requests without running arithmetic.
Within a tile a cell (u,v) means coordinate
(-16000 + 128*i + u, 128*j + 1 + v). The core is at local (64,63).
Cells with both 32 ≤ u ≤ 95 and 32 ≤ v ≤ 95 are excluded. Sort all remaining
cells by (abs(u-64)+abs(v-63), u, v). Reserve the first cell, (64,31), as the
stage in every bottom-row tile; it is 32 node hops from its core.
Order tiles by (abs(i-125)+j, j, i). Fill the ordered legal cells of each tile
with consecutive program addresses, skipping each bottom stage. Region order
and address counts are declared in the program JSON. This is a fixed injective
mapping with no free migration or replicas. No scratch access uses the reserved
core/router square. placement() reconstructs the complete map; its coordinate
arrays are hashed in the score result.
Small has 22,529 program words plus 250 stages, 22,779 words total. Medium has
993,065 program words plus 250 stages, 993,315 words total. Every used tile stays
within 12,288 words. Small uses 250 memory tiles including mostly empty tape
stage tiles; medium uses 314. All program words are explicitly zeroed before
use, then initial constants are set and charged. A stage is written by recv
or copy before its first read. The reported peak allocated bytes include all
stages, even when a particular stage is temporarily unused.
Expand the affine loops in their declared iteration order. Execute one instruction at a time, completing each source access in listed order followed by its destination write before issuing the next instruction. A repeated source is read again. Lower the original tape operations as follows:
kth recv dst:
P(k mod 250,0): recv stage[k mod 250]
P(125,0): copy dst, stage[k mod 250]
qth send src:
P(125,0): copy stage[q mod 250], src
P(q mod 250,0): send stage[q mod 250]
This preserves each fixed tape’s order and delivers each input word exactly once. Every copy source and destination incurs an access charge. The schedule does not encode data in dependencies: all loop bounds, processor choices, addresses and starts depend only on configuration and earlier completion times.
For cell distance d from its owner and mesh distance L from P(125,0):
| Access | Energy (fJ) | Cycles |
|---|---|---|
| Local read or write | max(50,2d) |
1 |
| Remote read | 256L + max(50,2d) |
2L+1 |
| Remote write | 256L + max(50,2d) |
L+2 |
Bottom recv or send to a stage at distance 32 |
128 | 2 |
A read request routes horizontally and then vertically, takes one local scratch
cycle, and returns along the reverse path. A remote write sends address and data
as consecutive words along the same path, then performs its local write.
If a path has L links and begins at boundary t, a read request uses link r
at t+r, local service at t+L, and return link r at t+L+1+r. A write uses
path link r for its address at t+r and data at t+r+1, followed by owner
service at t+L+1. Next-link transfers begin at the arrival boundary.
Global serialization ensures that other accesses cannot contend for a scratch slot or directed link. The address/data write pipeline fits one word per incoming link plus the owner’s pending request slot; a forwarding FIFO may dequeue and accept its replacement on the same boundary. Nothing uses an unbounded queue. Memory effects and output append complete before any dependent operation begins. The fixed schedule therefore has no races or deadlocks.
scheduled_instructions() lazily emits the concrete per-processor v4 trace.
scheduled_accesses() emits its exact access start and finish cycles, addresses
and coordinates. A complete schedule can be reconstructed from these generators
without storing billions of events. The cost scorer instead sums exact affine
address histograms. Since all accesses serialize, summing their elapsed cycles
is exactly the run’s elapsed time; it is not a sum of concurrent processor times.
No uncontended-latency assumption is applied to competing traffic.
Energy is the sum of all accesses and tape movement. With this legal placement,
all local distances are at least 32, so the 50 fJ floor is inactive. Consequently
the integer fJ score also equals the total word-node hops at 1 fJ per hop. One
cycle is 1 ns. Divide integer energy by 10^12 for mJ and cycles by 10^6 for
ms. No old single-core area conversion is used.
Exact small score: 242,990,182,992 fJ; 3,073,508,456 cycles. Exact medium score: 437,180,228,679,904 fJ; 3,740,014,858,820 cycles.
The five checked-in tests pass. They exercise published access examples, legal placement/capacity, tape wrap across 250 ports and remote memory, and rejection of uninitialized sources. A complete tiny MLP is executed instruction by instruction and matches an independent ordered NumPy learner bitwise on every final parameter word and every prediction. A separate expanded wire-event reference checks scratch-slot and directed-link capacity and reproduces the scorer’s exact energy and cycles. Full-size CPU/A100 numerical checks are in the two submission reports; the grid scorer itself does not run the full numeric training computation.
From the repository root, with Python and requirements.txt installed:
python mnist/submissions/grid-mlp-scoring-20260912/test_score.py
python mnist/submissions/grid-mlp-scoring-20260912/score.py \
--features 9 --width 32 --epochs 300 --batch 25 \
--n-train 1000 --n-test 1000 --learning-rate .2 --seed 101 \
--output mnist/submissions/grid-mlp-scoring-20260912/small60
python mnist/submissions/grid-mlp-scoring-20260912/score.py \
--features 81 --width 512 --epochs 200 --batch 25 \
--n-train 10000 --n-test 10000 --learning-rate .1 --seed 101 \
--output mnist/submissions/grid-mlp-scoring-20260912/medium96
Alternatively prefix each script command with
uvx --with numpy==2.2.6 python. The saved score includes software versions,
source and program hashes, exact component totals, initialization proof counts,
memory capacity and host scoring duration. A score covers program validation,
placement, histograms, exact integer totals and hashes; it excludes JSON/file
I/O, generating the program, and numerical accuracy execution.
score.py accepts --optimizer adam --nr-iterations K (default sgd, K=12).
The Adam path uses the same affine/distance pricing with m/v state, per-step
bias correction and a Newton-Raphson square root built from existing ops
(div, add, mul). The SGD path is unchanged and reproduces the original
scores exactly. Example:
uv run --with numpy==2.2.6 python score.py --features 9 --width 32 \
--epochs 100 --batch 25 --n-train 1000 --n-test 1000 --learning-rate 0.2 \
--seed 101 --optimizer adam --nr-iterations 8 \
--output small-adam-nr-k8-20260912