βοΈ RDTChess
A 11.05M-parameter residual network β recurrent depth Γ2, gated memory, Squeeze-and-Excitation β that plays chess in one forward pass β no search, no MCTS, no opening book. Trained from scratch on a single GTX 1080 Ti by supervised imitation of β10.99M human games from Lichess. This is RDTChess2.pt, currently the model live on the Lichess bot.
β οΈ One seed, one run. Every number in the Identity and Evaluation sections just below comes from a single training run on a single seed (123). Directions are consistent across three of the four instruments used there; magnitudes are not multi-seed validated. Read this as a lab notebook, not a benchmark.
This repository ships two checkpoints. RDTChess2.pt (this card's main subject, live on Lichess) is recurrent depth Γ2 + gated + SE on top of a 128Γ8 tower, trained on β11M games β compared against the plain architecture just below. RDTChess.pt is that plain architecture on its own: the original, single-seed, 5.5M-game run, described with its own numbers in π RDTChess (original) near the end of this card. Smaller and differently-shaped siblings were trained alongside both β narrower, shallower, other variants β but none of them are released here, and none of the numbers on this card come from them.
π§© Identity
| Architecture | Residual tower, 128 channels Γ 8 blocks, recurrence Γ2, gated, SE, two heads |
| Parameters | 11.05 M |
| Input | 19 planes of 8Γ8, canonical frame |
| Policy output | 4096 raw logits (from-square Γ 64 + to-square) |
| Value output | One scalar in [β1, 1], tanh |
| Search | None β one forward pass per move |
| Training data | β10.99 M Lichess games, both players β₯ 1800 Elo (8 sequential slices, one seed carried through with --resume) |
| Positions seen | 801,295,726, summed across the 8 slices |
| Training time | 68.6 h total on a GTX 1080 Ti, across the 8 sequential runs |
This is RDTChess2.pt. RDTChess.pt β the plain architecture on its own, no recurrence, no gate, no SE β has its own Identity/Evaluation/Data-scaling numbers in the π RDTChess (original) section near the end of this card.
The encoding, and why it is shaped this way
Everything lives in a canonical frame: the side to move is always "White, playing up the board". When Black is to move, every square is mirrored (square ^ 56) and the piece colours swap.
Two consequences, both deliberate:
- the value head always answers "how good is this for the player to move", which is a well-defined question β unlike an absolute encoding, which does not even tell the network whose turn it is;
- a position and its colour-reversed twin share one representation, so every game trains both colours at once.
The 19 planes: 12 for pieces (6 ours, 6 theirs), 4 castling rights, 1 en passant, 1 fifty-move counter, 1 repetition flag.
The policy head emits logits, not a softmax. That is what lets the caller mask illegal moves before normalising. Softmaxing over all 4096 actions and then zeroing ~99% of them spends most of the model's capacity learning that illegal moves are illegal β capacity this size cannot spare.
π¨ How it was made
Architecture. Same 128Γ8 residual tower as RDTChess.pt, plus three additions applied together:
- Recurrent depth Γ2 β the tower is applied twice per move, sharing the same convolution weights both times (effective depth Γ2 at no extra parameter cost for the convolutions themselves).
- Gated shortcut β a learned per-channel, per-square gate decides how much of the previous pass to keep versus overwrite (
z = sigmoid(W[candidate; previous]), initialised atsigmoid(-2) β 0.12so ~88 % of the previous state survives each pass by default). - Squeeze-and-Excitation in every residual block β a per-channel scale and bias computed from the whole board at once, buying long-range interaction (e.g. a1βh8) without extra convolutional depth.
Data. The Lichess July 2026 standard-game dump, split into 8 sequential slices of β1.37M games each (both players rated β₯ 1800). One seed (123) trained on the first slice, then carried through the other 7 with --resume, so the final checkpoint has seen all β10.99M games and 801,295,726 positions, one epoch each.
Objective. Plain behaviour cloning. The policy is cross-entropy against the move the human actually played, computed over the legal moves only. The value head regresses the final game result, expressed from the side to move.
Recipe. AdamW, learning rate 1e-3, weight decay 1e-4, betas (0.9, 0.999), batch size 512, gradient clipping 1.0, one epoch per slice, no learning-rate schedule.
That is the whole method. No self-play, no MCTS, no distillation from an engine, no opening book, no endgame tablebase.
π Evaluation
Compared against the plain (non-recurrent, non-gated, non-SE) 128Γ8 architecture, same data, same seed, one run each time β puzzles, Maia and minimax across all 8 slices; the round-robin across the first 7, run before the 8th finished training:
| Yardstick | Result |
|---|---|
|
Puzzles, first move (McNemar, 23,845 puzzles)
single forward pass, no search
|
ahead 8 / 8 β never inverted, always significant |
|
Maia-1100 / 1500 / 1900, no search either side
300 games per slice per level
|
ahead 23 / 24 β one isolated reversal, recovered next slice |
|
Full round-robin, 14 checkpoints, 91 pairs
27,300 games, no search either side, aggregate score vs the other 13
|
ahead 7 / 7 β never inverted, the most stable instrument of all four |
|
Minimax depth 2 (n=300)
an adversary that searches, unlike the three yardsticks above
|
ahead 4 / 8 β no stable direction, each swing confirmed on a second independent draw |
π« No absolute Elo is claimed. This model has never been played against a rated pool. Every figure here is a difference measured inside one family of models, on one training seed.
Actual scores against Maia (CSSLab's human-imitating engine, nodes=1, no search either side), on the final checkpoint β one training seed (123):
| Opponent | RDTChess2.pt | Plain architecture |
|---|---|---|
| Maia-1100 | 78.5 % | 77.3 % |
| Maia-1500 | 69.7 % | 68.8 % |
| Maia-1900 | 60.5 % | 55.8 % |
Read this precisely, not optimistically. Puzzles, Maia, and the round-robin all judge in one pass, no search β the same regime this model is actually deployed in β and all three are stable and one-sided across every slice tested. Minimax is a stress test against an opponent that searches for a refutation; the mechanism never collapses against it, but the signal is too noisy at n=300 to claim it wins there either. On the 5 hand-picked positions where the production model actually lost a real game, both architectures bounce between fixing and re-breaking the same handful of positions slice to slice β until, on the very last slice, both converged on choosing the exact identical move on all 6 tested, the first time that happened.
This was confirmed on one seed (123) only, extended sequentially across 8 slices of the same data with --resume. The 1.38M-game starting point was separately confirmed on three independent seeds (24, 123, 454) before this single-seed extension began; the extension itself has not been repeated on another seed. Full protocol, every intermediate number, and the honest instability of the minimax signal are in the project's own lab notebook, not reproduced here in full.
What it is good at, and where it breaks
Measured on the same 23,845 Lichess puzzles as the comparison above, on the final checkpoint (all 8 slices). One move per position, no search.
| First move of the solution | 61.5 % |
| Entire line | 42.5 % |
|
Random baseline
28.0 legal moves on average
|
3.6 % |
The entry move is the hard one
The network is asked at every move of the line β even after it has already erred, so that no step is measured on a pre-filtered sample.
| Length | Accuracy, first move β last |
|---|---|
| 2 moves | 54.5 β 78.3 |
| 3 moves | 56.9 β 71.1 β 87.0 |
| 4 moves | 64.1 β 72.7 β 77.7 β 89.6 |
| 5 moves | 68.7 β 75.5 β 77.6 β 83.0 β 91.7 |
The entry is the hardest move of every line here too, by 10 to 22 points β a smaller gap than RDTChess.pt's 20 to 35, but not closed. The last move of the line sits around 90 %, same as the original architecture.
And it is the quiet entry that breaks, same as the original
First move found, split by what kind of move it is.
| Entry move | < 1200 | 1200-1599 | 1600-1999 | 2000-2399 | 2400+ |
|---|---|---|---|---|---|
| Forcing β visible in the position | |||||
| Check | 79.7 | 65.2 | 52.6 | 42.0 | 55.3 |
| Capture | 92.2 | 77.4 | 63.8 | 60.7 | 59.9 |
| Non-forcing β justified only by what comes later | |||||
| Quiet | 95.1 | 80.3 | 59.1 | 42.0 | 33.3 |
The quiet move is still the hardest category at high rating β 33.3 % at 2400+, essentially unchanged from RDTChess.pt's 32.8 %. Every band improved a little (quiet at 1600-1999: 59.1 vs 54.7; at 2000-2399: 42.0 vs 39.3), but the shape β quiet collapsing from easiest to hardest as rating rises β is identical. Recurrent depth, gating and SE bought accuracy; they did not change what kind of move the network struggles to initiate.
π Usage
Play it right now, nothing to install
The model runs live as a Lichess bot: lichess.org/@/RDTChessBot
Challenge it in bullet, blitz, rapid or classical, casual or rated. Correspondence is declined on purpose β the bot answers in about 7 ms, so a game played over days would tie up a slot for weeks and tell you nothing.
It plays exactly the weights in this repository, through uci.py, with FinishPlies: 1 and two CPU threads. No search, no opening book, no tablebase: every move you see is one forward pass. The bot currently runs RDTChess2.pt β swapping uci.py's default is the only thing that changes when the deployed checkpoint changes, so check this line if you're reproducing a game and the move doesn't match what you'd expect from RDTChess.pt.
Install locally
pip install -r requirements.txt
Pick a move
python predict.py # initial position
python predict.py "6k1/5ppp/8/8/8/8/5PPP/R5K1 w - - 0 1" # any FEN
top 5 moves
Ra8# 64.3 %
Ra7 11.4 %
Re1 5.6 %
Play it in your browser
python play.py # you are White, opens http://localhost:8000
python play.py --black
python play.py --port 8080 --depth 3
Three modes in the page β play the model, play the minimax, or watch the two of them β plus a depth slider. Standard-library HTTP server, no build step, no CDN.
Use it in your own code
import chess, numpy as np, torch
from encoding import board_to_planes, legal_move_indices
from network import ChessNetwork
model = ChessNetwork.from_checkpoint("RDTChess2.pt", device="cpu") # or "RDTChess.pt"
board = chess.Board()
moves, indices = legal_move_indices(board) # legal moves + their indices
planes = torch.from_numpy(board_to_planes(board)).unsqueeze(0)
with torch.no_grad():
logits, value = model(planes)
legal = logits[0, torch.as_tensor(indices.astype(np.int64))]
probs = torch.softmax(legal.float(), dim=0).numpy() # over legal moves ONLY
print(board.san(moves[int(probs.argmax())]), float(value))
β οΈ The one mistake to avoid: reading argmax over the full 4096 logits. About 99 % of them are illegal in any given position. Always gather at legal_move_indices first.
ChessNetwork.from_checkpoint returns the model already in eval() mode. Keep it there β the tower is BatchNorm, and train-mode batch statistics will corrupt single-position inference.
What is in this repository
| file | role |
|---|---|
RDTChess2.pt |
128Γ8 + recurrence Γ2 + gated + SE, weights + architecture config (44 MB) |
RDTChess.pt |
plain 128Γ8, weights + architecture config (44 MB) |
network.py |
the architecture β loads either checkpoint, config comes from the file |
encoding.py |
board β tensor, move β index |
player.py |
the network as a player, get_move(engine) |
engine.py |
board wrapper, canonical encoding, hand-written evaluation |
algo_player.py |
alpha-beta minimax, the opponent used in evaluation |
gui.py, play.py |
the browser board |
predict.py |
one-position example |
Inference only. The training code is not part of this release; the recipe above is complete enough to reproduce it. Both released checkpoints carry weights and architecture config, with the optimiser state stripped β 44 MB each instead of ~131 MB.
β οΈ Limits β read these before quoting any number
- It imitates, it does not solve. The objective is agreement with 1800+ humans. Where those humans are systematically wrong, so is the model, by construction. It is not trying to find the best move; it is trying to find the likely move.
- No search, and it shows. One forward pass. The puzzle breakdown in the RDTChess (original) section locates the cost precisely: 90 % on forced continuations, 32.8 % on quiet entries in hard positions β the same weakness this architecture inherits, since neither addition changes what the model is asked to do.
- One seed, one run. No variance control on the extension past 1.38M games (the 1.38M starting point itself was 3-seed confirmed).
- Under-promotions cannot be represented. The action space collapses from/to pairs and always decodes promotions to a queen. This costs well under 0.1 % of moves in normal play, but it makes a handful of knight-promotion puzzles unsolvable by construction.
- No absolute rating. Never played against a rated pool. Do not read a win-rate against minimax as an Elo.
- The value head is weak supervision. It regresses the final result of the game, which is a very noisy label for a position at move 12.
- Distribution. Trained on rated blitz/rapid from one month of one server, at one rating floor. Behaviour outside that distribution β correspondence, odds games, composed positions β is untested.
- The released weights are the raw supervised checkpoint. No self-play fine-tuning was applied to it.
- The minimax comparison is noisy at n=300. Read the round-robin (91 pairs, 27,300 games) as the more direct instrument; minimax swings by double digits between two independent draws.
π RDTChess (original)
Everything in this section is about RDTChess.pt specifically: the plain 128Γ8 tower, no recurrence, no gate, no SE β the original single-seed, 5.5M-game run. None of the numbers below apply to RDTChess2.pt above; its own comparison against this architecture is in the Evaluation section near the top of this card.
| Architecture | Residual tower, 128 channels Γ 8 blocks, two heads |
| Parameters | 10.92 M |
| Input | 19 planes of 8Γ8, canonical frame |
| Policy output | 4096 raw logits (from-square Γ 64 + to-square) |
| Value output | One scalar in [β1, 1], tanh |
| Search | None β one forward pass per move |
| Training data | 5,497,103 Lichess games, both players β₯ 1800 Elo |
| Positions seen | 400,581,531, one epoch |
| Training time | 12 h 54 on a GTX 1080 Ti |
How it was made
Data. The Lichess July 2026 standard-game dump. A game is kept when both players are rated β₯ 1800 (the filter takes min(WhiteElo, BlackElo)), which leaves 5,497,103 games in the 5 GB archive β all of them used. Every position of every kept game becomes one training sample, from the first ply: 400,581,531 positions, seen once.
Objective. Plain behaviour cloning. The policy is cross-entropy against the move the human actually played, computed over the legal moves only. The value head regresses the final game result, expressed from the side to move.
Recipe. AdamW, learning rate 1e-3, weight decay 1e-4, betas (0.9, 0.999), batch size 512, gradient clipping 1.0, one epoch, no learning-rate schedule.
That is the whole method. No self-play, no MCTS, no distillation from an engine, no opening book, no endgame tablebase.
Evaluation
Three independent yardsticks, because they disagree β and the disagreement is itself a finding.
| Yardstick | Result |
|---|---|
|
Top-1 on held-out human moves
agreement with what the human actually played
|
51.7 % |
|
vs minimax depth 1
300 games Β· +249 =34 β17
|
88.7 % |
|
vs minimax depth 2
300 games Β· +144 =69 β87
|
59.5 % |
|
vs the same net trained on 2M games
600 games Β· +35 Elo Β· p = 0.0034
|
55.1 % |
A fixed opponent measures how well you exploit that opponent, not general strength: the depth-1 and depth-2 figures move by 29 points for the same model. The head-to-head duel is the most informative of the three.
π« No absolute Elo is claimed. This model has never been played against a rated pool. Every Elo figure here is a difference measured inside one family of models.
Data scaling
| Games | Net | Decade | Gain vs d1 | Rate |
|---|---|---|---|---|
| 150 k β 1 M | 96Γ4 | 0.82 | +21.2 pts | 25.7 / decade |
| 1 M β 2 M | 96Γ4 | 0.30 | +6.3 pts | 20.9 / decade |
| 2 M β 5.5 M | 128Γ8 | 0.44 | +8.0 pts | 18.2 / decade |
The rate bends steadily β 25.7, 20.9, 18.2 β without collapsing. The +8.0 was predicted at +9 before the run from the two earlier points.
The last row is a wider net than the first two, so the three rates are not a single controlled curve: read the bend as a trend, not as a measurement. The architecture comparison was run separately, at fixed data, and found depth to be the lever that pays and width almost nothing on its own β until the two are combined.
What it is good at, and where it breaks
Measured on 23,845 Lichess puzzles, sampled by cell (solution length Γ rating band) so that puzzle difficulty is not confounded with solution length. One move per position, no search.
| First move of the solution | 57.8 % |
| Entire line | 38.3 % |
|
Random baseline
28.0 legal moves on average
|
3.6 % |
The entry move is the hard one
The network is asked at every move of the line β even after it has already erred, so that no step is measured on a pre-filtered sample.
| Length | Accuracy, first move β last |
|---|---|
| 2 moves | 51.9 β 76.1 |
| 3 moves | 53.6 β 67.5 β 85.1 |
| 4 moves | 60.4 β 69.6 β 75.2 β 88.0 |
| 5 moves | 65.5 β 72.5 β 75.5 β 80.3 β 90.6 |
The entry is the hardest move of every line, by 20 to 35 points. The last is around 90 %, whatever the depth. Once inside a forcing sequence β recaptures, checks, the mate β the network barely errs. Starting one is the problem.
And it is the quiet entry that breaks
First move found, split by what kind of move it is.
| Entry move | < 1200 | 1200-1599 | 1600-1999 | 2000-2399 | 2400+ |
|---|---|---|---|---|---|
| Forcing β visible in the position | |||||
| Check | 74.4 | 59.5 | 47.5 | 39.5 | 51.4 |
| Capture | 91.6 | 73.8 | 60.7 | 57.7 | 56.1 |
| Non-forcing β justified only by what comes later | |||||
| Quiet | 94.6 | 75.6 | 54.7 | 39.3 | 32.8 |
The quiet move goes from the easiest category to the hardest β 23 points below captures in the same band β as difficulty rises. Captures and checks are visible in the position; a quiet preparatory move is justified only by what comes three plies later.
π‘ In one sentence: this network executes tactics, it does not initiate them.
π License
Apache-2.0.
π Citation
@misc{charlet2026rdtchess,
author = {ThΓ©o CHARLET},
title = {RDTChess: 128x8 chess policy and value networks trained on human Lichess games},
year = {2026},
publisher = {Hugging Face},
howpublished = {\url{https://huggingface.co/RDTvlokip/RDTChess}}
}
ThΓ©o CHARLET
TSSR Graduate (IT Systems & Networks Technician) β AI/ML Specialization
Creator of AG-BPE (Attention-Guided Byte-Pair Encoding)
π Seeking internship opportunities
Collection including RDTvlokip/RDTChess
Evaluation results
- First move of the solution on Lichess puzzle database, 23845 puzzles sampled by solution length x rating band, one seed (123)self-reported61.500
- Entire solution line on Lichess puzzle database, 23845 puzzles sampled by solution length x rating band, one seed (123)self-reported42.500
- Score vs minimax depth 2 on 300 games, seed 4242, 300-move cap, one training seed (123)self-reported60.800
- Stages ahead of the plain architecture (aggregate score) on 91 pairs, 27300 games, one training seed (123)self-reported7.000
- Top-1 move agreement on Lichess July 2026 standard games, both players 1800+ Elo, held-out splitself-reported51.700
- First move of the solution on Lichess puzzle database, 23845 puzzles sampled by solution length x rating bandself-reported57.800
- Entire solution line on Lichess puzzle database, 23845 puzzles sampled by solution length x rating bandself-reported38.300
- Quiet entry move, rating 2400+ on Lichess puzzle database, 23845 puzzles sampled by solution length x rating bandself-reported32.800