Doubling Network Depth 20 → 40 Blocks Didn't Help
5M positions for a 24M-param network is already in the over-parameterized regime. More capacity finds no structure that isn't there.
chess & go · distilled from Stockfish/KataGo · one GPU per run
AlphaZero used 5,000 TPUs for self-play data generation and 64 TPU-v2s to train. We use the same network family (20- or 40-block × 256-channel ResNet, ~24–50M parameters) on one GPU, supervised distillation from Stockfish on 30–46M positions, evaluated against a calibrated Stockfish opponent. Total cost per training run: ~16–80 GPU-hours on a single L40S. Roughly four orders of magnitude less compute than the AlphaZero paper.
2,301 is the project's top point estimate and the tightest-CI measurement that crosses 2,300: the 95% lower bound (2,190) clears the previous peak (d10 30M ep 15 = 2,189). A wider-CI sibling, R2 v2 ep 4 at the same anchor, was 2,285 [2,177, 2,554]; both clear the d10 ceiling. R1 v2 ep 7 = 2,209 [2,115, 2,389] is the 40×256 cosine variant's best. Full numbers on the experiments page. The self-play follow-on hasn't survived a spot AZ on chess but is currently running successfully on the Go side — postmortem & live status.
→ How we got here → Method & Elo bisection → vs AZ → vs Leela → vs MuZero → Go (9×9) → What's next
Two phases. Training is a single supervised pass: Stockfish labels positions, the student fits those labels. Inference is Monte Carlo Tree Search using the trained network as its prior. The network alone never picks a move — MCTS does, and the network just guides where MCTS looks.
π over the 8 candidates (softmax at T = 1 pawn). The game outcome z ∈ {−1, 0, +1} is recorded from each side-to-move POV.π, MSE on the value head against z. No self-play, no MCTS at training time.20 residual blocks × 256 channels, ~24M parameters. Input: a 19-plane encoding of the current board (piece positions, side-to-move, castling rights, repetitions). Output: a 4,672-dim policy logits vector and a scalar value in [−1, +1]. Same architecture AlphaZero used — only the way we got the weights differs.
The trained network is a prior over moves and a value estimator for leaf positions; the actual move comes from Monte Carlo Tree Search built on top of those two signals. One move at the root costs N simulations (800 for the routine eval, 4,000 for the search-saturated eval). Each simulation is:
Q + U: average value seen below this move + an exploration bonus proportional to the network's prior P and inversely proportional to how many times we've already visited this child. The network's policy is the bias; the running statistics correct it as evidence accumulates.Q with the new estimate. Sign-flip at every ply because chess alternates side-to-move.Repeat for N sims. Then pick the move at the root with the highest visit count (not highest Q; visit counts are MCTS's improved policy estimate). At higher sims/move, MCTS spends more rollouts refining the prior into a sharper visit distribution — that's why the same checkpoint scores 1,807 Elo at 800 sims and 2,084 Elo at 4,000 sims (see the search ablation).
Implementation: the encode is encode_board, the network forward is AlphaZeroNet.forward, and the MCTS loop is run_mcts / run_mcts_batched. The selection formula is straight from the AlphaGo Zero paper: U(s,a) = c_puct · P(s,a) · √Σ_b N(s,b) / (1 + N(s,a)).
Single-variable ablations on top of the distilled baseline. Each card shows the hypothesis, the verdict, and the headline numbers. Full setup + interpretation on the experiments page.
5M positions for a 24M-param network is already in the over-parameterized regime. More capacity finds no structure that isn't there.
Distilled priors transfer well to deeper inference-time search, exactly Lc0's empirical finding. The "1,800 ceiling" was an eval-time artifact.
More data beat more parameters and a stronger teacher. The two confirmed levers (search, data) are both "more compute"; the rejected lever was "smarter network".
→ All experiments, with charts and CIs → Soft vs hard targets → Stacking search + data together
| AlphaZero (2017) | This Project | |
|---|---|---|
| approach | tabula-rasa self-play RL | supervised distillation from stockfish |
| network | 20 blocks × 256 ch | identical |
| training compute | ~45,000 TPU-v1-hr + 576 TPU-v2-hr | ~16 GPU-hours (single L40S) |
| training data | ~3.5 B positions | 5M–30M positions |
| peak elo | ~3,500 | 2,301 (R2 v2 ep 14, lower-CI bound 2,190 clears 2,300) · 2,285 (ep 4) · 2,209 (R1 v2 ep 7) |
Every checkpoint we trained, at the standard 800-sim eval. UCI=1,350 anchor — the only one every checkpoint has been measured against, so the comparison is apples-to-apples across runs. Bold marks the canonical run's best epoch.
| Run | ep 5 | ep 10 | ep 15 | ep 20 |
|---|---|---|---|---|
| d15 5M baseline (20×256) | 1,651 | 1,862 | 1,759 | 1,807 |
| d15 5M capacity (40×256) | 1,851 | 1,851 | 1,759 | 1,820 |
| d10 5M subset (20×256) | 1,749 | 1,770 | 1,688 | 1,730 |
| d10 30M full (20×256) | 2,084 | 1,961 | 1,888 | 1,961 |
| d15 46M R1 v1 (40×256, const LR) | 1,933 | 2,033 | — | — |
| d15 46M R2 v1 (20×256, const LR, lowlr) | 1,933 | 1,933 | — | 1,933 (ep 29 final) |
| d15 46M R1 v2 (40×256, cosine, killed at ep 15) | 2,033 | 2,033 | — | — |
| d15 46M R2 v2 (20×256, cosine, killed at ep 22) | 1,933 | 1,933 | 2,276 (ep 14) | — |
100-game evals at UCI=1,350. CI is ~±100 Elo per cell — the variation epoch-to-epoch within a run (e.g. baseline 10 = 1,862) is mostly noise, not real swing. The cross-run comparisons (5M baseline → 5M capacity → 30M data) are larger than the noise.
Same checkpoints, more MCTS sims at inference. Numbers in [ ] are 95% CIs.
| Checkpoint | sims | opponent | Elo |
|---|---|---|---|
| d15 baseline ep 20 | 800 | UCI=1,350 | 1,807 |
| d15 baseline ep 20 | 4,000 | UCI=1,350 | 2,084 |
| d10 30M ep 5 | 800 | UCI=1,800 | 2,004 |
| d10 30M ep 5 | 2,000 | UCI=1,800 | 2,034 |
| d10 30M ep 5 | 4,000 | UCI=1,800 | 2,084 [2005, 2197] |
| d10 30M ep 5 | 4,000 | UCI=2,000 | 2,110 [2044, 2187] |
| d10 30M ep 10 | 4,000 | UCI=1,800 | 2,171 [2082, 2324] |
| d10 30M ep 15 | 4,000 | UCI=1,800 | 2,189 [2098, 2354] |
| d10 30M ep 20 | 4,000 | UCI=1,800 | 2,154 [2067, 2297] |
| d15 46M R1 const-LR ep 7 | 4,000 | UCI=1,800 | 2,146 [2060, 2285] |
| d15 46M R1 v2 cosine ep 2 | 4,000 | UCI=1,800 | 2,138 [2054, 2274] |
| d15 46M R1 v2 cosine ep 7 | 4,000 | UCI=1,800 | 2,209 [2115, 2389] |
| d15 46M R2 v2 cosine ep 4 | 4,000 | UCI=1,800 | 2,285 [2177, 2554] |
| d15 46M R2 v2 cosine ep 14 | 4,000 | UCI=1,800 | 2,301 [2190, 2601] ← lower CI clears 2,300 |
Highest point estimate: 2,301 Elo at R2 v2 cosine ep 14 — and uniquely, its 95% lower CI bound (2,190) clears the previous peak (d10 ep 15 = 2,189). The wider-CI ep 4 measurement (2,285) is consistent. R1 v2 cosine ep 7 at 2,209 is the 40×256 variant's best. The 4,000-sim deep-search numbers stack the two confirmed ablations (search + data) on one checkpoint; the cosine LR schedule on top recovered another ~+100 Elo over the constant-LR R1 v1 / R2 v1 runs.
The largest remaining gap to AlphaZero is that AZ has no teacher — its targets come from its own MCTS visit counts, not from Stockfish. The Lc0 recipe — distill first, RL second — runs an AlphaZero-style self-play loop on top of the distilled prior.
Self-play has been attempted six times on spot since 2026-05-25; every run was evicted before iteration 1 finished (iters take ~90 min, AZ eviction windows are shorter). The algorithm is fixed (LR=1e-5, no catastrophic forgetting); the binding constraint is moving the trainer off spot. → Self-play postmortem
The d15-46M cosine trainers (R1 v2 and R2 v2) were killed at 2026-05-26 17:15 UTC after producing the top-2 ckpts in the deep-search table above. Late epochs at sims=800 were not trending up, and the autoeval daemon is offline — finishing them on OD would have been ~$320 with low chance of a new peak. → Why we stopped
Two cheap follow-up evals are in flight to confirm the ≥ 2,300 question: sims=8,000 vs UCI=2,000 on R2 v2 ep 4 and sims=4,000 vs UCI=1,800 on R2 v2 ep 14.
Four ablations · hypothesis prove/disprove · charts and CIs
The loss · MCTS at inference · Elo bisection
EKS · bare-EC2 · multi-region · self-terminating runners
Same network · different recipe · 10,000× less compute
Distill-then-RL · we ran 7 attempts · loop runs, ~10⁵× less compute kills it
Learned-model MCTS vs known-rules MCTS · we kept the rules engine
8×128 ResNet · distill from KataGo · parity at v200 in 15 epochs
Self-play loop · more d15 data · roadmap
Code · 253 tests · launchers · dockerfiles