chess is one of the games i'm fascinated by, so i constrained myself to a laptop rtx 4060 and asked how far i could push a small chess model. this repo is that experiment: a 15.2m-param attention-first policy/value model (a chess-relative attention encoder feeding a hybrid attention + selective-SSM trunk) trained on lichess elite games, evaluated with search, and iterated through dozens of failed and useful ideas. the honest headline is 2581 elo on a proper stockfish-anchored tournament, everything trained on one consumer gpu.
(the codebase supports both attention-only and a transformer + selective-SSM hybrid, up to 32.5m params. the best trained checkpoint right now is 15.2m, attention-first, 142m positions -- that's what all the evals below use.)
a kibitzer (yiddish, from the german kiebitzen, "to look on at a card or chess game") is the onlooker who leans over the players' shoulders and offers unsolicited commentary on the best move. that is exactly what this model is. it never owns a game of its own; it watches a single position, forms an opinion, and tells you the move it would play. a spectator with a very strong opinion about your board.
official tournament elo: Ordo-rated over 171 clean games against a stockfish UCI_Elo ladder (2200-3100), both colors, model wrapped in 512-sim PUCT, anchored sf-2500 = 2500.
| rating | games | overall score | |
|---|---|---|---|
| kibitzer @ 512 sims | 2581.6 ± 102.3 | 171 | 42% |
it crushes sf-2200 and sf-2500, loses to sf-2700 and above, with the 50% crossover around sf-2600. read it as the strength of the model with search: deep PUCT is doing real work that does not distill back into the raw weights. full method, the whole ladder, and the honest caveats are in LOGBOOK.md (D67). a complementary searchless-ladder estimate (2483 ± 32 at 64 sims) is in current state below.
how it works, end to end. a position is not fed as pixels or a move list but as structured chess state: each of the 64 squares becomes one of 13 piece tokens (six white pieces, six black, or empty), alongside a 7-dim auxiliary vector holding side-to-move, the four castling rights, en passant, and the halfmove clock. the model can also consume a history of up to 128 plies, not just the current board.
the board first passes through a 3-layer position encoder built on Shaw-style relative attention: instead of absolute square embeddings, every pair of squares attends through its file and rank offset, bucketed into 15x15 = 225 learned relative vectors that are folded into the attention bias (the Chessformer / Shaw form). this makes the geometry translation-invariant, so a knight-move or a diagonal relationship means the same thing wherever it sits on the board. the 64 encoded squares are then mean-pooled into a single dense vector per position, with RMSNorm and SwiGLU feed-forwards throughout.
those per-position vectors flow into a 10-layer trunk that runs over the ply history, and this is exactly why "attention-only" undersells it: the trunk interleaves two block types. every third block is causal self-attention for global reasoning over the game so far; the rest are selective state-space blocks (SSM, Mamba-style), each a causal scan with input-dependent gates, a depthwise local convolution, and a cheap recurrent state that mixes the sequence in linear time. the bet is simple: attention is powerful but quadratic, SSM is cheap and linear, so interleave them and pay the full-attention cost only where it earns its keep. the best 15.2m checkpoint leans on the attention path; the default 32.5m config runs the full four-attention / six-SSM hybrid.
finally an RMSNorm feeds two heads: an AlphaZero-style policy over the fixed 4,672-move action space (64 from-squares x 73 move planes) and a tanh-bounded value head that outputs a side-to-move score in [-1, 1]. at play time those policy priors and leaf values drive a PUCT search. so it is not a single trick: a chess-relative attention encoder, a hybrid attention plus selective-SSM temporal trunk, and dual policy / value heads, trained together.
current best checkpoint (all evals use this):
| thing | value |
|---|---|
| d_model | 256 |
| trunk layers | 10 (all causal attention) |
| attention heads | 8 |
| position encoding | 3-layer cross-attention (shaw) |
| max sequence | 128 (plies of history) |
| params | 15.2m |
| training data | 142m lichess elite positions |
default config (code design, not yet trained at this size):
| thing | value |
|---|---|
| d_model | 320 |
| trunk layers | 10 (4 attention, 6 selective SSM) |
| ssm state dim | 8 |
| params | 32.5m |
the diagram above is the current code path, with exact parameter counts written into the figure.
strongest published checkpoint: Pradheep1647/kibitzer-tactical-repair (full model card: docs/hf_model_card.md).
base supervised checkpoint: Pradheep1647/kibitzer-s2-shaw-142m-comp.
| artifact | value |
|---|---|
| checkpoint | tactical_repair.pt |
| local path | runs/tactical/tactical_repair.pt |
| hf repo | Pradheep1647/kibitzer-tactical-repair |
| training objective | tactical_supervised_repair_r1_policy_only |
| eval setup | 128-sim PUCT vs Leela/Maia proxy unless stated otherwise |
given a board, run PUCT search (like alphazero) - the model provides priors for the search tree and evaluates leaf positions. the search returns the best move and visit counts.
assessed against stockfish ladder, 64 sims PUCT:
protocol: Stockfish was run with UCI_LimitStrength at fixed Elo settings; Kibitzer used the S2 Shaw 142m checkpoint plus 64 PUCT simulations per move. Score is (wins + 0.5 * draws) / games from Kibitzer's side. The Elo estimate is a fit over the ladder, not a raw-policy rating.
| opponent (sf elo) | score | result |
|---|---|---|
| 1900 | 0.938 | 37W / 1D / 2L |
| 2100 | 0.850 | 31W / 6D / 3L |
| 2300 | 0.700 | 24W / 8D / 8L |
| 2500 | 0.525 | 13W / 16D / 11L |
estimated elo: 2483 ± 32 (search-based, 64 sims)
read this as: with search enabled, the model is roughly even with the 2500 Stockfish setting under this specific ladder, and clearly above the 1900-2300 settings. it is not a standalone no-search rating, and it is not a claim about tournament engine strength outside this protocol. PGNs and JSON summaries live under reports/scaling_law/elo_local/.
the attention-first backbone scales as a clean power law (S0 → S3, all on 5m positions):
| tag | params | policy ce | top-1 move match |
|---|---|---|---|
| S0 | 3.0m | 2.357 | 30.2% |
| S1 | 7.4m | 2.334 | 30.8% |
| S2 | 14.9m | 2.329 | 30.9% |
| S3 | 22.9m | 2.310 | 31.6% |
the raw policy ce keeps dropping with scale, but top-1 move match is flattening fast - we're hitting the human-move ceiling (~31-32%). the model correctly predicts the human move roughly a third of the time from 5m training positions. more data helps more than more params.
the production model (S2 shaw comp, 14.9m params) was trained on 142m positions and is the one used in all gameplay evaluations below.
the strongest local checkpoint right now is runs/tactical/tactical_repair.pt. it is not a new published base yet; it is a tactical supervised repair on top of policy_regret_repair.pt.
paired 80-game gates vs the Leela/Maia-2700 proxy at 128 sims:
| checkpoint | W/D/L | score | implied elo |
|---|---|---|---|
tactical_repair.pt seed 17 |
7W / 25D / 48L | 0.244 | 2503 |
tactical_repair.pt seed 23 |
12W / 23D / 45L | 0.294 | 2548 |
tactical_repair_r2.pt seed 23 |
9W / 18D / 53L | 0.225 | 2485 |
policy_regret_repair.pt seed 23 |
8W / 16D / 56L | 0.200 | 2459 |
takeaway: tactical R1 is the current best local branch. tactical R2 passed the held-out top-1 gate but failed the external gate, so it should not be promoted.
the branch tried next was teacher-preference repair: Stockfish/LC0-style rankings as
pairwise feedback, DPO/AWAC-style policy improvement from tactical_repair.pt, external gate as
the only promotion signal. it was rejected, the offline pair metrics did not transfer to play
(LOGBOOK.md D59). reproduce with:
bash scripts/run_preference_repair.shthen gate it against tactical R1:
CANDIDATE_NAME=preference_repair \
CANDIDATE_CHECKPOINT=runs/preference/preference_repair.pt \
CANDIDATE_REPORT_DIR=reports/preference_repair \
SEED=31 \
bash scripts/run_repair_eval_gate.shfolder-level plots:
- repair eval rollup
- tactical repair plots
- preference repair plots
- regret repair plots
- regret-start plots
- az eval plots
D63 is the first clearly positive non-training signal after the repair/RL failures: the same
tactical_repair.pt checkpoint gets much stronger when PUCT is allowed to search deeper.
Only the simulation count changes; the opponent stays the Leela/Maia-2700 proxy at nodes=1.
| sims | W/D/L | score | implied proxy elo |
|---|---|---|---|
| 64 | 3 / 1 / 36 | 0.087 | 2293 |
| 128 | 6 / 11 / 23 | 0.287 | 2542 |
| 256 | 5 / 16 / 19 | 0.325 | 2573 |
| 512 | 29 / 8 / 3 | 0.825 | 2969 |
takeaway: the checkpoint was compute-starved at the 128-sim gate. 512 sims does not mean the model itself is 2969 Elo; it means deep PUCT extracts a lot more strength against a searchless external yardstick. the next confirmation is a rented-GPU 1024/2048 sweep against 2700 plus stronger Leela checkpoints. full report.
the adaptive gate reuses one tree across 128/256/512/1024 checkpoints, records the real simulation and time budget per move, and compares against uniform 512 search on paired openings:
bash scripts/run_adaptive_search_gate.shfor the clean official tournament rating, run bash scripts/run_official_elo.sh (a cutechess
gauntlet vs the stockfish ladder, then Ordo). every PGN is checked for time forfeits, illegal
moves, malformed games, and incomplete runs; bash scripts/rate_pgn.sh <pgn> salvages a rating
from a finished run by dropping only the contaminated games. this is how the 2581 headline was
produced.
alphazero-style self-play: the model plays against itself using PUCT search with dirichlet root noise, trains on the visit distribution + game outcome, then we match the new model vs the old one.
| iter | vs base score | vs maia 2700 | notes |
|---|---|---|---|
| 1 | 0.625 | 0.100 | beats itself, regresses vs maia |
| 2 | killed | - | 400 sims too slow, see LOGBOOK.md |
the pattern: az improves the model against its own play style (0.625 h2h) but makes it worse against strong opponents (maia 2700: 0.100 vs base ref 0.225). classic self-play overfitting when the data is narrow - 80 games isn't enough diversity. the new config (200 games @ 200 sims) aims to fix this.
student plays its own games, lc0 labels each position. trains reverse-KL toward the teacher.
promising trajectory - the distilled model holds up decently against maia 2700. the teacher knowledge transfers well because the positions are from the student's own distribution.
self-play vs stockfish with online value-head training. curriculum ladder: 1320 → 1500 → 1700 → 1900.
crushed 1320-1700 easily, hit a wall at 1900 (rolling score ~0.35-0.45, never made it to 2100). the value head learns fast early on but plateaus when stockfish stops making tactical blunders. 200 games, 50 updates, ~15s/game.
two separate experiments, same conclusion: the value head is not the bottleneck.
joint-scratch (D30-D35): trained policy + value together from a random init instead of the two-phase policy-then-value approach. the decisive-sign metric improved +6.67pp (65.95% → 72.62%). in actual play? tied-to-worse vs stockfish-1320 within the ±0.09 noise of a 20-game match.
scaled value head (D52): the value head is suspiciously thin: 33,025 params bolted onto a 15m trunk. enlarged it 4× to 131,841 params, froze the trunk, retrained against stockfish depth-14 labels.
| metric | legacy (33k) | enlarged (132k) | direction |
|---|---|---|---|
| offline mse (100m base) | 0.0403 | 0.0196 | -51%, improved |
| offline mse (142m comp) | 0.0569 | 0.0178 | -69%, improved |
| PUCT vs sf-1900 (100m) | 0.775 | 0.625 | -15pp, regressed |
| PUCT vs sf-1900 (142m comp) | 0.783 | 0.650 | -13pp, regressed |
| PUCT vs leela ~2700 | 0.225 | 0.150 | -7.5pp, regressed |
both experiments converged: offline metrics don't predict play. the value head is now a closed lever.
D60 - genuine RL as the last non-scale lever. critic-free GRPO on an external verifiable reward (game outcome vs an adaptive stockfish elo ladder), with 128-sim searched rollouts, an exact total-variation DPPO trust region over the legal moves, and a KL anchor to the base. no self-play targets, no value critic - built specifically to dodge the "beats its own sibling, regresses vs real opponents" trap.
the adaptive ladder climbed 1900 → 2500 and the model held ~54% there. but that climb is exactly what a static 2500 model produces (the ladder only steps ±100/iter until it hits the model's level), so it confirms the base's strength, not a gain.
on the leela-2700 gate at the identical config as the base:
| checkpoint | W/D/L | score | implied elo |
|---|---|---|---|
| grpo_v5 (best) | 12/20/48 | 0.275 | ~2532 |
| tactical_repair (base) | 12/23/45 | 0.294 | ~2548 |
flat within noise - identical wins (12=12), three base draws turned into losses. the fixed-opponent probe@2000 was flat too (0.9125 → 0.900). GRPO held the model's strength but added nothing externally: the 9th non-scale lever to hold-or-lose against the external yardstick. full plots + report.
not a strength experiment. a mechanistic-interp pass on the base: replay real leela-2700 games (win/draw/loss), hook every position, and watch how it reasons. findings: head specialization only shows up in the last encoder layer, there's a fixed d8 "attention-sink" head, the mean-pool crushes per-square activation norm from ~79 to ~2 (a real bottleneck, and an argument for attention pooling), and the value head lands on the right sign but noisily and late. includes side-by-side board + attention videos per game.
full interp study, figures, and videos
this repo is closer to a lab notebook than a clean model release. the short version:
| line | result |
|---|---|
| scaling/data | worked best; data mattered more than more params past ~15m |
| search depth | strongest live positive signal; 512 sims jumped from 0.287 to 0.825 vs the Leela-2700 proxy |
| az self-play | beat its own base, regressed vs maia/leela-style opponents |
| td-leaf | fixed easy curriculum rungs, stalled around 1900 |
| value-head repair | improved offline value metrics, regressed real play |
| tactical repair | small external gain; R1 kept, R2 rejected |
| teacher-preference repair | first DPO-style attempt rejected; offline pair metrics did not transfer |
| grpo + dppo rl (D60) | neutral; searched rollouts + trust region held ~2500 strength, no external gain (0.275 vs base 0.294) |
| joint scratch / point tweaks | mostly negative or inconclusive |
the longer failure log is in LOGBOOK.md; the scaling summary is in docs/scaling_study/README.md.
what actually moved external strength:
- scaling the backbone (clean power law, more data > more params past ~15m)
- more search sims (stronger play, but it is an inference crutch that does not distill into the weights)
what got tested and closed (full autopsy in LOGBOOK.md, D48-D67): teacher-preference repair, az self-play (including 512-sim expert iteration), on-policy lc0 distillation, td-leaf, value-head enlargement, grpo/dppo rl, and oracle process-reward repair. every one of them held or lost strength against a fixed external opponent. the only lever with a positive slope still open is parameter and data scale, and that is a deliberate not-now: the whole point was the ceiling of a small model on a laptop, and 2581 elo is that ceiling, honestly measured.
uv sync
uv run pytest # 80 tests
bash scripts/az_run.sh # start az self-play loop
bash scripts/run_preference_repair.sh # current repair branch
uv run python scripts/train_bc.py -h # supervised training| file | what | when |
|---|---|---|
runs/scaling_shaw_comp/S2_shaw_142M_comp.pt |
best supervised model (14.9m, 142m pos) | scaling sweep |
runs/az/az_iter_1.pt |
az iter 1 (one pass of self-play) | 2026-07-08 |
runs/tactical/tactical_repair.pt |
best local repair branch; tactical R1 | 2026-07-09 |
runs/tactical/tactical_repair_r2.pt |
rejected tactical R2; worse external gate | 2026-07-09 |
generated checkpoints are gitignored. the strongest published checkpoint is on Hugging Face; hf push support lives in kibitzer/hf_utils.py.
MIT, see LICENSE. the model weights are trained on public lichess elite games and released under the same terms.
this is the paper trail behind the architectures and experiments in this repo. it includes ideas that worked, ideas that failed their gate, and papers that directly shaped those tests.
- alphazero - self-play policy/value learning with puct search
- searchless chess - 2895 elo with no search, 270m params, 15b positions
- knightcap - early learned chess evaluation combined with tree search
- maia - human move prediction at rating-specific skill levels
- maia-2 - one human-aligned model spanning skill levels
- maia-3 (paper) - chessformer-based human move prediction across skill levels
- allie - human-aligned chess with time-adaptive search
- maia4all - efficient individual human-behavior adaptation
- unimaia - language-steered human-like chess policy control
- chessmimic - per-rating move, clock, and outcome transformers
- elo-disentangled style embeddings - player-style modeling using maia-3 policy logits
- attention is all you need - transformer attention backbone
- self-attention with relative position representations - the Shaw relative-attention form used by the board encoder
- mastering chess with a transformer model - the earlier chessformer position-representation study behind the shaw ablation
- mamba - selective state spaces behind the lightweight ssm blocks
- rmsnorm - normalization used throughout the model
- glu variants improve transformer - the swiglu feed-forward form
- kaplan scaling laws - the original loss-versus-scale methodology
- chinchilla scaling laws - compute-optimal parameter/data allocation
- tdleaf(lambda) - temporal-difference learning through game-tree leaves
- proximal policy optimization - clipped policy optimization baseline
- advantage-weighted regression - supervised-looking off-policy policy improvement
- awac - advantage-weighted actor-critic with offline data
- generalized knowledge distillation - on-policy distillation from student-generated states
- direct preference optimization - pairwise preference learning without a separate reward model
- conservative q-learning - offline-rl protection against optimistic out-of-distribution values
- reinforced self-training - generate, score, filter, and reuse model samples
- deepseekmath / grpo - group-relative policy optimization without a learned critic
- dppo - divergence-constrained trust regions used in the grpo experiment
- maximum likelihood reinforcement learning - maxrl for sparse verifiable rewards
- post-training insights from learning chess - grpo reward design and chess-specific post-training evidence
- policy gradient search - online planning and expert iteration without a search tree
- gumbel muzero - gumbel action sampling and sequential-halving search
- search-contempt - compute-aware self-play search for alphazero-like engines
- pgx - hardware-accelerated parallel game simulation for rl
- lc0 - teacher for on-policy distillation and the fixed external opponent















