Chess as autocomplete

Can autocomplete play chess blindfolded? A journey into applied AI research

Play against the final model directly in your browser.

TL;DR

A 91M parameter transformer sees only chess moves (plus three tokens naming the players' ratings and the time control), never the board, never the rules. It learns to play blindfolded at near-state-of-the-art human move accuracy, purely by getting good at predicting the next token.

In my previous article, we built a mental model of LLMs as autocomplete.

As a quick recap: LLMs are given the sentence

To be , or not to be ,
that
Figure 1: Predicting the next token.

and simply predict the next token.

So in theory, if we frame the game of chess as a language autocomplete problem, we should be able to train an LLM to play chess.

Can we train a transformer to play chess blindfolded by autocompleting the language of chess?

Blindfold chess is a variant of chess where the players do not see the board. Someone reads out the moves to them, and they reply by announcing their own next move out loud.

This sounds exactly like the framing we want: moves in, moves out.

The most commonly used language to represent chess games is called Standard Algebraic Notation (SAN). For example, the Italian Game starts with the moves

1. e4 e5 2. Nf3 Nc6 3. Bc4

The Italian Game opening position
Figure 2: The position after 1. e4 e5 2. Nf3 Nc6 3. Bc4, the Italian Game.

And can be understood as follows:

WhiteBlack
1. e4♙︎ pawn e2 → e4 e5♟︎ pawn e7 → e5
2. Nf3♘︎ knight g1 → f3 Nc6♞︎ knight b8 → c6
3. Bc4♗︎ bishop f1 → c4
Table 1: The Italian Game in Standard Algebraic Notation.

If you wish to explore SAN in detail you can go to this chess.com article.

Why and what?

…general methods that leverage computation are ultimately the most effective, and by a large margin.

Richard Sutton, The Bitter Lesson, 2019

This project takes that idea to an extreme. Karvonen (2024) showed that a small GPT trained on move text alone learns the board well enough to be probed for it, at 16 million games and 25M parameters. As far as I know nobody has run that idea at scale: a general model (a standard model with nothing chess-specific added: no board, no search, no extra heads) trained on a massive corpus of human games. One transformer, trained on nearly 1.8 billion human games, about a hundred times that corpus.

Predicting the next move in a game is the same problem an LLM solves predicting the next word, so I borrow the whole apparatus: a decoder-only transformer (the model family behind ChatGPT) trained with the recipe modern LLMs are built with. The interface is tokens in, tokens out, nothing else: the model only ever sees a stream of moves and learns to continue it.

The model must learn to calculate everything else: where every piece sits, whose turn it is, which moves are even legal. It reconstructs all of it from the move stream alone. That is blindfold chess in the most literal sense.

Deep diveHow others teach models to play chess like humans

Plenty of others have taught machines human-like chess. They differ in how much chess is built into the architecture, whether they search at move time, whether they learn from a strong engine like Stockfish, and how much data they train on. This project sits at the spare end of the first three (no board, no search, no engine teacher) and leans on the last: data.

System (year) Chess-specific architecture Search / engine teacher Training positions Params Time controls
Maia (2020) 8×8 board planes; AlphaZero-style CNN; last 12 plies fed in None ≈0.6B 1 10.3M (×9) Blitz/rapid/classical
Maia-2 (2024) Board planes; ResNet CNN + skill-aware attention None 9.1B 23.3M Rapid only
Maia-3 / Chessformer (2026) 64 squares as tokens; geometric attention bias; policy + value heads None ≈0.5B 2 79M Blitz only
Grandmaster without search (2024) FEN snapshot tokenized; action-value classification head Stockfish teacher ≈0.53B 3 270M n/a
Allie (2024) Decoder-only LM over UCI move tokens; + value & think-time heads MCTS search ≈262B 4 355M Blitz only
This post None: vanilla decoder-only LM over move tokens; no board, no value head None 122.9B 91M All four
Table 2: Related systems that learn human chess.

Position = one board state trained on; one move token per ply for the move-stream LMs (Allie, this post). Time controls = the Lichess game speeds in the training data; "all four" = bullet, blitz, rapid, classical.

  1. Maia: ≈0.6B per model, one per rating bin (×9); 12M games/model × ~50 kept moves/game; model sees 409.6M (400k steps × 1024 batch).
  2. Maia-3: corpus size unpublished; ≈512M positions processed in training (1M steps × 512 effective batch), shown as a scale proxy.
  3. Grandmaster: 15.3B Stockfish action-value labels over ≈0.53B distinct positions.
  4. Allie: 6.6B-token training set (91M games) trained ~40 epochs (2M steps × 131,072 tokens), so ≈262B tokens seen; the 6.6B is the dataset, the 262B the total seen during training.

Board-based human imitators: the Maia line. Maia (McIlroy-Young et al., 2020) reframed the goal of chess AI from playing the best move to predicting the move a human of a given rating would actually play. It uses an AlphaZero-style convolutional network that reads the board as a stack of 8×8 planes (so the board, its geometry, and the legal moves are built in rather than learned), and it plays with no search. The catch is that "a given rating" meant nine separate models, one per 100-Elo band. Maia-2 (Tang et al., 2024) folded those into one network with a "skill-aware attention" module that conditions on both players' ratings, and Maia-3 / Chessformer (Monroe et al., 2026) pushed the idea furthest: a transformer that still reads the board, now as 64 square-tokens with a learned geometric attention bias, and currently holds the human move match record among board-based models (57.1% at 79M parameters).

The engine-distillation outlier. DeepMind's Grandmaster-Level Chess Without Search (Ruoss et al., 2024) is the odd one out. It is also a searchless transformer, but it does not learn from humans at all: it reads a board (a tokenized FEN) and is trained to reproduce Stockfish 16's win probability for every legal move, on the order of 15 billion engine evaluations, with human games used only as a source of positions. The result plays at grandmaster strength (~2895 Lichess blitz Elo) without ever searching. It distills an engine rather than imitating people; its goal is strength, not human-likeness.

The world-model probes: Chess-GPT. Adam Karvonen (2024) trained small GPTs on nothing but PGN text, character by character, then went looking inside them. Linear probes recover the state of every square to 99.6% accuracy: a model given no board builds one anyway. A second probe separates players below 1550 Elo from those above 2050 with 90.5% accuracy, so the model estimates skill as a latent variable purely to predict the next character better, which is the unsupervised version of the metadata tokens this project feeds in explicitly. It is the chess version of the Othello-GPT result. What differs here is scale: 16 million games at 25M to 50M parameters, against 1.76 billion games. Karvonen establishes that the effect exists; this post asks what it is worth when the corpus is a hundred times larger and the model is trained to be played rather than probed.

The move-stream cousin: Allie. Allie (Zhang et al., 2024) is the closest relative to this project: a decoder-only transformer over a stream of moves (in UCI notation), a language model of chess in the same sense ours is. It trains "exclusively from humans" (no engine distillation) and adds heads that predict how long a human would think and how the game will end. Its search-free policy scores 55.7% move match, in the same range this project lands in. The headline difference is its "bit of search": a Monte-Carlo tree search whose budget scales with the model's own predicted human think-time. That search keeps Allie calibrated against very strong opponents; this project, with no search at all, gives that up.

Every time control, not one. The human imitators here each train on a single slice of Lichess: Maia-2 on rapid, Maia-3 and Allie on blitz, and even the original Maia throws out bullet. Fast games get dismissed as noise, classical as too scarce. We train on all time controls.

Where the others add a board, a search, an engine teacher, or extra heads, this one adds only data: a single pass across every time control rather than many passes over a narrow slice. That is what makes it a general model in the sense above, rather than a chess-specific one.

Hardware and compute

This being a personal project, I do not have access to large computational resources. I have a personal computer with an Intel i5-13500 CPU, an NVIDIA RTX 4070 Ti Super (16 GB) GPU, 16 GB of RAM, and under 1 TB of available storage. This will impact several engineering choices made throughout the project. Partway through the project I unexpectedly gained access to a couple of VMs with A100 and H100 GPUs, but by then those choices had already been shaped by the hardware above.

Pre-training

The goal of pre-training our chess autocomplete is to learn the language of chess, the rules of the game. We will measure the performance of a pre-trained model with two metrics:

For move validity, the important number is the valid move probability mass and not the top-1-legal rate. This is because we may sample from the output distribution to choose the next move, and this metric correctly shows the probability of sampling an illegal move.

For game end prediction however, the correct metric is how often the most likely output is the correct game end prediction. Whenever the model output is a game end prediction, we never sample; we always pick the most likely one. That is why accuracy is the right metric here, not probability mass.

These two metrics will give us a way to evaluate how well the model has learned the rules of chess. Our goal is to reach 100% VPM and 100% GPA.

We will also get a sense of the model's playing strength by playing it against the Stockfish 18 chess engine at a range of strength levels. This is not an explicit optimization target: the goal of this project is not to build the highest-rated chess model, only to understand how well a model trained as pure autocomplete ends up playing.

The dataset

The open-source online platform Lichess publishes the games played on their platform as an open database. This database contains (as of May 2026) a total of 7,863,012,346 rated chess games, spanning over 13 years, from complete beginners to grandmasters. This database has a total compressed size of 2.49 TB.

Figure 3: Rating distribution on Lichess by time control, January 2024–December 2025. Toggle a control on or off; hover to read off a rating.

Elo peaks at around 1500, which may be because players start with 1500 Elo on Lichess. The most popular time controls are blitz and bullet, followed by rapid, ultrabullet, and classical.

Deep diveFrom 7.8 billion games to a training set

The Lichess open database holds about 7.8 billion games, but the final model never sees most of them. It trains on the 21 monthly standard-rated files spanning January 2024 through September 2025, 1,950,403,943 games. The validation set is held out as the most recent 10% of games from the newest file (September 2025); everything before that is training data.

The files are read newest first, and the 600,000-step budget runs out before the pool does. The run ends 13% into the February 2024 file and never opens January 2024, so it sees 1.76 billion of those games, 90% of the pool, almost all of them exactly once.

Lichess implements a couple of rules differently than other platforms. A threefold repetition is not an automatic draw: the side to move has to claim it, and many players simply play on, so the data routinely holds positions sitting on a repetition nobody claimed (Lichess only forces the draw on the fifth repetition). The fifty move rule is the opposite: Lichess draws the game automatically the moment it applies, so no game ever continues past it.

Data processing

The games in the Lichess open database are stored as zstd compressed PGN (portable game notation) files. A PGN file is a commonly used text format for storing both metadata and moves from one or more games of chess. Below is an example of the PGN of a chess game.

[Event "Troll Masters"]
[Site "Gausdal NOR"]
[Date "2001.01.05"]
[Round "1"]
[White "Edvardsen,R"]
[Black "Carlsen,Magnus"]
[Result "1/2-1/2"]
[WhiteElo "2055"]
[BlackElo ""]
[ECO "D12"]

1.d4 Nf6 2.Nf3 d5 3.e3 Bf5 4.c4 c6 5.Nc3 e6 6.Bd3 Bxd3 7.Qxd3 Nbd7 8.b3 Bd6
9.O-O O-O 10.Bb2 Qe7 11.Rad1 Rad8 12.Rfe1 dxc4 13.bxc4 e5 14.dxe5 Nxe5 15.Nxe5 Bxe5
16.Qe2 Rxd1 17.Rxd1 Rd8 18.Rxd8+ Qxd8 19.Qd1 Qxd1+ 20.Nxd1 Bxb2 21.Nxb2 b5
22.f3 Kf8 23.Kf2 Ke7  1/2-1/2

The moves in a PGN file are stored in SAN. SAN is convenient for chess players, but it is not the most convenient representation for this project, for reasons we get into when choosing the model's input representation.

Binary game representation

I designed a compact 16-bit binary format: every move fits in exactly two bytes, can be decoded on its own without replaying the game, and any game can be read straight out of the middle of a file. The full bit layout (and the two companion files that make individual games randomly addressable) are in the deep-dive below.

Deep diveDataset representation

To give a sense of scale: our complete training set, the 21 monthly Lichess files from January 2024 through September 2025, is 635 GB compressed and about 4.2 TB uncompressed as PGN. In our binary representation it takes only 292 GB, itself uncompressed and supporting O(1) random access to any game.

Encoding one PGN file produces a triplet of files that share a base name. For the September 2025 database that is:

  • …2025-09.bin: the moves of every game, back to back.
  • …2025-09-map.bin: one byte offset per game, for random access.
  • …2025-09-metadata.parquet: one row of metadata per game.

The three files are aligned by row index: game N in the .bin is entry N in the map and row N in the parquet.

The moves: .bin

The move format was designed against these requirements:

  1. Contains all necessary information
    • Which piece was moved
    • From which square
    • To which square
    • If it was a promotion, to which piece
    • If it was a castling move, which one
    • If the game ended, how
  2. Can be quickly parsed without a board representation
  3. Byte aligned
  4. As space efficient as possible
  5. Allow random access to games
  6. No lookup tables

The sixteen bits are laid out as five fields: a move type, then the from-square and to-square as file/rank pairs:

0000move type 000from file 000from rank 000to file 000to rank
0000pawn 100e 0012 100e 0114
Figure 4: The 16-bit move layout, and the opening move e2→e4 encoded in it.

The first 4 bits encode the move type:

BitsMove typeBitsMove type
0000Pawn move1000Game end
0001Knight move1001Promotion to knight
0010Bishop move1010Promotion to bishop
0011Rook move1011Promotion to rook
0100Queen move1100Promotion to queen
0101King move1101Black king side castle
0110White king side castle1110Black queen side castle
0111White queen side castle1111Unused
Table 3: Move-type codes: the first four bits.

The remaining 12 bits encode the from-square and to-square as two file/rank pairs of 3 bits each:

BitsFileRank
000a1
001b2
010c3
011d4
100e5
101f6
110g7
111h8
Table 4: File and rank codes, three bits each.

En passant needs no special move type: it is stored as an ordinary pawn capture. The 16 bits alone do not reveal that the captured pawn sits on a different square than the destination, but that is recoverable from the position, and nothing in the pipeline needs it.

And there are 6 "special" numbers representing game end states:

TokenValueMeaning
<|unknown|>0x8000Recorded moves run out before any real ending
<|checkmate|>0x8001Checkmate
<|stalemate|>0x8002Stalemate
<|insufficient_material|>0x8003Insufficient material
<|fifty_move_rule|>0x8004Fifty-move rule
<|threefold_repetition|>0x8005Threefold repetition
Table 5: The six game end tokens.

Each game is its move tokens followed by exactly one of these end tokens, and games are written one after another with no separator between them: the end token doubles as the boundary. Every token is two bytes, little-endian, so the file is exactly two bytes per token from start to finish. Every special token (the six endings and <|game_start|>) uses move-type 1000, a code no real move occupies, so it can never collide with a move.

Random access: -map.bin

Scanning for end tokens walks the file front to back, but jumping straight to game five million needs an index. The map file is a flat array of uint64 little-endian byte offsets, one per game, each holding the end byte position of its game in the .bin. Game 0 starts at byte 0 and game i starts where game i-1 ended, so two adjacent entries bracket any game in constant time. The training pipeline memory-maps both files and reads a game as a single slice.

Who was playing: -metadata.parquet

The .bin deliberately holds only moves; everything about a game lives in a separate parquet file, one row per game in the same order. Keeping it apart leaves the moves a clean token stream, and lets anyone filter the dataset on metadata without touching the binary. The columns:

ColumnTypeMeaning
GameIndexu641-based position of the game in the source PGN, counting games later skipped
WhiteRating/16u8White Elo divided by 16
BlackRating/16u8Black Elo divided by 16
InitialTimeu16Initial clock, in seconds
Incrementu8Increment per move, in seconds
Table 6: The per-game metadata columns.

GameIndex counts the skipped games too, so any row traces back to its exact game in the original PGN.

The September 2025 Lichess open database file has a total of 87,049,890 games, a compressed size of 28.3 GB, and an uncompressed size of about 189 GB. Encoding it produces an 11 GB .bin, a 0.7 GB map, and a 0.5 GB metadata file, about 12.5 GB in total. This is about a 15x reduction from the uncompressed size, and 2x reduction from the compressed size.

A baseline, we have to start somewhere

The initial goal is to see whether this can work at all. I designed a simple transformer, a simple tokenization strategy and a simple training procedure.

Choosing an input representation for the transformer

The most obvious representation would be to use SAN directly: map every possible SAN move to its own token. I discarded this early, for two reasons.

First, SAN is not self-contained. A move like Nxf4 tells you a knight captured on f4, but not which knight or from where; to recover that you have to replay the game on a board. I wanted a representation that makes the piece, the from-square and the to-square explicit, keeping our options open for the rest of the project.

Second, the same physical move can be written several different ways depending on what else is on the board, and if it gives check, a + is appended, adding yet another spelling like Nxf4+. Mapping SAN straight to tokens would force the model to learn a separate token for each spelling, although they all mean the same move:

♞︎d5→f4 one physical move (a capture)
  1. Nxf4only one knight can reach f4
  2. N5xf4a second knight on d3: same file, disambiguate by rank
  3. Ndxf4a second knight on h5: same rank, disambiguate by file
Figure 5: The same physical move (a knight from d5 captures on f4) written three ways depending on what else is on the board. SAN encodes the destination, not the origin.

For simplicity's sake I opted to start by using our 16-bit binary game representation, where each of the 2^16 possible moves represents its own token. This also comes with the added benefit of not needing a tokenizer, since the binary move representations are our tokens. We only need to add a <|game_start|> token.

A tokenized game ready to be processed by our model looks like:

<|game_start|> ♙︎f2→f3 ♟︎e7→e5 ♙︎g2→g4 ♛︎d8→h4 <|checkmate|>
Figure 6: The fastest possible loss, the fool's mate, as our model sees it.

The model never receives <|unknown|> as a target. This is because <|unknown|> can mean that a player resigned, a draw was agreed, a player timed out, etc. We do not want the model to predict any of these. Who wants a chess engine that resigns?

Model definition and training (v0)

I opted to start with a simple decoder only transformer following the same architecture as GPT-2 with the same hyperparameters as the smallest GPT-2 model with some minor modifications. The model is trained on the first 64 half moves (plies) of each game, for a total of 360k steps: 138 million games, 8.8 billion tokens, 36.7 hours of training.

Deep diveModel and training configuration
Model configuration
dimension
768
num heads
12
num blocks
12
positional encoding
RoPE
non-linearity
GELU
vocabulary size
216
Training configuration
max sequence length
64
batch size
384
optimizer
AdamW (weight decay 0)
max learning rate
6e-4

The model is trained on a max sequence length of 64 (64 half moves) for a total of 360k steps (138.2 million games). The learning rate is kept at 6e-4 for the first 300k steps, and then decayed to zero over the next 60k steps following an inverse square root schedule.

36.7h duration
138.2M games
8.847B tokens
9.9×1018 FLOPs

At the end of the 360k steps the model reached 99.4% VPM and 95.3% GPA on the validation set:

Baseline training curves for valid move probability mass and game end prediction accuracy
Figure 7: The baseline learns most of the rules in the first 20k steps, then spends 340k more polishing the details.

Playing strength over 100 games per opponent, scored as an Elo performance rating (the Elo you would need to score that result against that opponent):

Opponent Win Draw Loss Performance rating
Stockfish, 1347 rated 61.0% 9.7% 29.3% 1464
Stockfish, 1566 rated 37.1% 11.8% 51.1% 1516
Table 7: Stockfish at depth 5 with 150–178ms per move.

A rating around 1490. The language of chess has been learned, mostly. Now to improve on it.

Improving the baseline model

The baseline gave us a working training pipeline and several places to improve it. I followed the process of creating hypotheses of what could improve the model, then testing these hypotheses with real training runs. Testing different configurations one by one and evaluating their performance is referred to as an ablation sweep. Below are the improvements made.

Deep diveHow the sweep was run

The sweep rests on one number: the noise floor. The baseline configuration was trained five times with different random seeds, and the seed-to-seed standard deviation of the validation loss came out at 0.0038 nats (a nat is just the natural-log unit the loss is measured in). That sets the bar for what counts as a real change rather than luck.

  • One change at a time, equal compute. Every run gets the same budget of 7.93x1017 FLOPs (floating-point operations, a hardware-independent measure of how much computation a run costs), about 1.5B tokens, roughly one hour on the Azure H100. This is the “small” budget.
  • A two-sigma gate. A change is accepted only if it beats the baseline by more than two standard deviations, 0.0077 nats. Anything inside that band is noise, no matter how promising it looks.
  • Survive scale. The best variants are re-run at 4x the compute with three seeds each, to check the win does not evaporate once the model trains longer. Several did not survive it.

In total the sweep took 47 training runs and about 123B training tokens, almost exactly what the single final run would later consume on its own.

A vocabulary of impossible moves

Our binary representation gives us 65,536 possible tokens, but the vast majority of them are moves that can never be played: a pawn moving from a1 to h8, a king jumping across the entire board, a rook moving diagonally. If we only keep the moves that can actually occur on a chess board we are left with 4,128 tokens. I call this the realizable vocabulary.

Switching the input to the realizable vocabulary reduced the number of parameters from 186M to 138M. Switching the output to this vocabulary further reduced the number of parameters down to 91M. Over half of the baseline model was spent embedding/decoding tokens that can never appear in a game.

The sweep confirmed it. The realizable input/output combination was the best of every encoder/decoder tested.

Deep diveHow 65,536 moves become 4,135

The realizable vocabulary is built by enumerating every move that is geometrically possible on an empty board, for every piece type and every square:

  • Sliding pieces (queen, rook, bishop) along every ray out to the edge of the board.
  • King and knight moves from every square.
  • Pawn single pushes, double pushes, diagonal captures, and promotions, for both colors.
  • The four castling moves.

That yields 4,128 distinct move tokens. Seven more describe the game rather than a move, one for the start and six for the ways a game can end, which makes the 4,135 classes the model predicts. The input side carries 36 metadata tokens on top of those: 30 rating buckets and a marker for a missing rating, then five time controls. So 4,171 tokens go in and 4,135 come out, because the model reads metadata but never has to write it. Each of the remaining ~61,000 of the 65,536 possible 16-bit words (a bishop moving like a rook, a pawn teleporting across the board) describes something that can never legally happen, so it never needs a token.

Training on full games

The initial model was trained on the first 64 half moves of each game. We consider full games to be up to 200 half moves as the vast majority of games in the dataset are finished long before that. Real games vary a lot in length, so the pipeline has to handle variable-length sequences without wasting compute on padding. The approach I chose packs games of different lengths into a single padding-free stream, so every position the GPU touches is a real move (the mechanics are in the deep dive below). This change never entered the sweep: seeing whole games is a prerequisite for learning full games, not a tuning knob to validate.

Deep diveHow games of different lengths share a batch

The simplest way to put games of different lengths in one batch is to pick a fixed width and pad. Every game becomes a row of the same length; any game shorter than that width is filled out with a padding token, and the loss is told to ignore those positions. It trains correctly, but the padding still flows through every attention and feed-forward layer. The GPU does the work and then throws it away. At a width of 64 half moves that overhead is tolerable. At 200 it is not: most games finish long before 200 half moves, so a fixed-width batch would be mostly padding, and most of the compute would go to waste.

game 1
game 2
game 3
Figure 8: Three games padded to a fixed width of eight. The dashed cells are padding: the GPU computes them, the loss ignores them. Here a third of the batch is wasted, and real game lengths vary far more than this.

So the batch dimension is dropped entirely. Instead of a rectangle of games, the games are concatenated into a single long stream, sized to a fixed token budget: every position in it is a real move, and there is nothing to pad. Because each step processes the same number of tokens, memory and step time stay flat from batch to batch. (The one game that would spill past the budget is cropped to fit, which costs about a hundred tokens per batch, a rounding error.)

This packing must be handled carefully: run attention naïvely over the stream and the transformer can attend across games, not good. Preventing it takes two pieces of bookkeeping. The first records where each game starts and ends, so attention can be confined to a block-diagonal pattern: every token attends only within its own game, never across a boundary. The second gives each token its position within its own game, resetting to zero at every boundary, so the rotary positional encoding numbers each game from its first move wherever it lands in the stream.

0 1 2 3 4 5 0 1 2 0 1 2 3 4 5 6
cu_seqlens = [0, 6, 9, 16]
Figure 9: The same three games packed end to end into one stream of sixteen real tokens, colored by game. The number under each token is its position within its own game. Note it resets to zero at every boundary. cu_seqlens marks those boundaries so attention never crosses them.

In the first example, padding spends 24 tokens of compute to process 16 tokens of information; packed, the same three games take exactly 16. A third of the padded batch was wasted work.

This packed attention is implemented in FlashAttention's variable-length kernel, flash_attn_varlen_func. The ordinary kernel expects a (batch, seq_len, heads, dim) rectangle; the varlen one expects the flat stream and is told the seams. Its parameters:

flash_attn_varlen_func(
    q, k, v,
    cu_seqlens_q,
    cu_seqlens_k,
    max_seqlen_q,
    max_seqlen_k,
    causal=True,
)
  • q, k, v: query, key and value for every token in the packed stream, shape (total_tokens, n_heads, head_dim), no batch dimension.
  • cu_seqlens_q, cu_seqlens_k: cumulative sequence lengths, the offsets [0, 6, 9, 16] from the diagram marking where each game begins and ends. Query side and key side are the same tensor, because a token's queries and the keys it may attend to live in the same game. These seams are what make attention block-diagonal.
  • max_seqlen_q, max_seqlen_k: length of the longest game in the batch. The kernel needs an upper bound up front to size the work it launches; it changes how the work is tiled, not the result.
  • causal=True: inside each game a move attends only to the moves before it. Together with cu_seqlens, every token sees exactly its own game's past and nothing else.

Adding game metadata

A game between two 800 rated players looks very different from a game between two grandmasters, and the same player plays very differently in bullet than in classical. Our initial model sees none of this. It sees only moves.

The PGN headers already contain the ratings of both players and the time control, so we can give them to the model. Three metadata tokens are prepended to every game: the time control, the white player's rating, and the black player's rating. These are ordinary special tokens, the same device every LLM already uses for markers like start-of-text; the model reads them exactly as it reads a move, and nothing about the architecture changes.

<|rapid|> <|white:2500|> <|black:2500|> <|game_start|> ♙︎e2→e4 ♟︎e7→e5 <|checkmate|>
Figure 10: A game with metadata tokens prepended.

But at inference time we may not know these values. To handle this, each metadata token is randomly replaced with a special <|missing|> token 30% of the time during training. The model learns to use the metadata when it is available, and to fall back to a sensible average when it is not.

Verdict: Metadata is a real win: given the ratings and time control, the model beats the no-metadata baseline. The dropout is what makes it safe: without it, the model falls apart whenever a value is missing.

Deep diveHow the metadata tokens are built

Ratings are not stored exactly. Each one is rounded into one of 16 buckets, 100 points wide, from 1000 to 2500; anything below 1000 lands in the bottom bucket and anything above 2500 in the top one. A 2480 and a 2530 player both become the same 2500 token.

The three tokens are always written in the order time control, white rating, black rating, immediately before <|game_start|>. The time control is one of four values: bullet, blitz, rapid, or classical.

During training, each of the three tokens is independently swapped for a <|missing|> token 30% of the time. That is why the model still plays when you tell it nothing: it has seen plenty of games where the rating or time control was absent. The metadata positions are never used as prediction targets: the model reads them, it never has to generate them.

A better optimizer

The baseline used AdamW for every parameter. The biggest single win of the whole sweep came from replacing it on the transformer-block weights with the Muon optimizer, while AdamW keeps the embeddings, the output layer and the norms. (The optimizer is the algorithm that turns each batch's gradients into an actual weight update.)

The first attempt was a disaster: reusing AdamW's learning rate of 6e-4 for Muon left the model about 0.18 nats worse than baseline. Muon wants its own learning rate, more than 10x higher. Unsurprising in hindsight, since Muon orthogonalizes its updates, so its learning rate lives on a different scale. After a proper sweep the best value was 9.5e-3. With Muon carrying the block weights, AdamW's learning rate for the remaining parameters was swept again and also preferred to be higher, landing at 1.9e-3.

Muon learning rate sweep, U-shaped curve with minimum at 9.5e-3
Figure 11: The Muon learning rate sweep. AdamW's 6e-4 sits far up the left slope; the sweet spot is more than 10x higher, and the whole valley from roughly 5e-3 to 2e-2 is good.

Other changes that were considered but not accepted

Most of the architecture knobs turned out not to matter. A deeper, thinner model was clearly worse. A version with more attention heads never separated from baseline either way, so it was dropped without a 4x rerun. A wider, shallower one looked like a tie at the small budget and then lost outright when re-run at 4x compute, as did a higher AdamW learning rate I would otherwise have shipped. RMSNorm landed within noise of LayerNorm, so LayerNorm stays. Two parts of the final recipe rode in without an ablation win: SwiGLU (a now-standard variant of the transformer's feed-forward layer, adopted by convention because it is what modern LLMs use) and the learning-rate schedule, a constant rate with an inverse-square-root warmdown, carried over unchanged from the paper it comes from rather than tuned here. And weight decay bought nothing at this scale, so the recipe inherited none, a choice that caused problems at full scale.

Deep diveThe sweep, phase by phase

Every phase below changes one thing against the same baseline and is judged against the same gate: beat the baseline by more than 0.0077 nats (two standard deviations) to count, then get re-run at 4x compute to prove the win survives. The phases ran in roughly this order, each locking its winner before the next began.

Tokenizer and representation. The first question was how to encode a move. Feeding the model realizable tokens on the input was the best encoder of everything tested (1.531 nats vs the 1.537 baseline) and, as a bonus, dropped the ~50M-parameter input embedding for free. The decisive result was on the output side: keeping the full 65,536-token output layer was catastrophic, around +0.06 nats worse at equal compute and down on every auxiliary metric.

Metadata tokens. Prepending the ratings and time control is a clear win when the model is given them (about -0.017 nats) but the interesting question was what happens when it is not. A model trained with no dropout on the metadata learns to lean on it so hard that, asked to play without it, it falls apart: +0.23 nats worse than its own with-metadata score, well below even a model that never saw metadata. Training with 30% dropout removes the dependence: without metadata, it sits right on the no-metadata baseline; with metadata, it keeps almost the whole win. That is the variant in the recipe.

Optimizer sanity check. Before tuning anything exotic, I checked the optimizer settings inherited from modded-nanogpt: the AdamW betas, and weight decay. Nothing beat the gate. Notably, adding the standard weight decay of 0.1 was a no-op at this scale (it bought nothing), so the recipe kept weight decay at zero. That looked safe here. It was not safe at 20x the compute.

Model shape. Holding the parameter count roughly fixed, I tried trading width for depth. A deeper, thinner model (576-wide, 21 blocks) was clearly worse, +0.019 nats. A wider, shallower one (1024-wide, 7 blocks) and a version with twice the attention heads were both statistical ties, and the wider one trained about 20% faster, which made it tempting. It was promoted to the 4x round, where the tie broke against it.

Learning rate, before Muon. Sweeping AdamW's learning rate a half-decade each way around the inherited 6e-4 showed 6e-4 was conservative: 1e-3 won by -0.014 nats at the small budget. Going the other way fell off a cliff: 3e-4 and below collapsed entirely. So 1e-3 was promoted to the 4x round. Like the wide-shallow shape, it then failed to hold up.

Normalization and the MLP. RMSNorm, the now-common alternative to LayerNorm, was run at 4x compute and landed +0.0015 nats from LayerNorm, inside the noise band, on the wrong side of zero. The MLP never got that treatment. A SwiGLU ablation was written and staged, then never run at any budget, and SwiGLU went into the final recipe anyway on the strength of what modern LLMs do. The other untested choice is the learning-rate schedule: the constant-then-inverse-sqrt warmdown was carried over unchanged from the paper it comes from and never ablated.

Muon optimizer. The largest result in the sweep was switching the transformer-block weights to Muon. The first run buried it: handed AdamW's 6e-4, Muon came out +0.18 nats worse than baseline, and a naïve reading would have thrown the optimizer out. Sweeping its learning rate across a log grid told the real story: Muon wants roughly 10–15x AdamW's rate, with a broad good valley from about 5e-3 to 2e-2 and a hard divergence cliff past it (5.3e-2 blew up entirely). The best point, 9.5e-3, beat the baseline by -0.036 nats, by far the biggest single win. Best-case Muon also beat best-case plain AdamW by another -0.022.

AdamW's rate, with Muon locked in. With Muon carrying the block weights, the remaining AdamW parameters (embeddings, output layer, norms) got their own learning-rate sweep. It improved monotonically up to 1.9e-3 with no cliff in the tested range, so that is the value used, though the gain over the previous setting is inside the noise gate: the best observed point rather than a proven optimum.

Surviving 4x compute. The whole point of the final round was to keep the small-budget winners honest. Two of them did not make it: the wider-shallower shape and the higher AdamW learning rate both shed their edge once the models trained four times as long, ending up clearly behind the plain choice. The realizable tokenizer, the original shape, and the metadata-with-dropout win all held (metadata stayed about -0.015 nats ahead at scale, and free in the worst case) and those are what went into the final recipe.

The final model

With the ablation sweep finished, every choice is settled. The final recipe:

The final recipe
dimension
768
num blocks
12
num heads
12
normalization
LayerNorm
non-linearity
SwiGLU
parameters
91M
input vocab
4,171
output vocab
4,135
metadata dropout
30%
Muon lr (blocks)
9.5e-3
AdamW lr (rest)
1.9e-3

The final training run takes this recipe to the full training set: 600,000 steps, about 123B tokens of chess games (600,000 steps × 1,024 sequences × 200 half moves), a single near-complete pass in which almost every game is seen exactly once. It did not go smoothly the first time (the first attempt exploded past step 160,000 and had to be restarted with a small amount of weight decay added) but the rerun trained cleanly all the way through, three and a half days on the H100. The result is chess-autocomplete-v1-91m, the model you can play against at the top of this page.

Training configuration
max sequence length
200
batch size
1,024 (512 microbatch × 2 accumulation)
steps
600,000
weight decay
0.01 Muon / 0.005 AdamW
schedule
constant, then inverse-sqrt warmdown over the last 120k steps
82.5h duration
1.76B games
122.9B tokens
6.7×1019 FLOPs
Deep diveThe final run

The recipe that came out of the sweep carried no weight decay, and the first attempt at the final run did not survive that. At step 160k everything was healthy: validation loss falling, VPM at 100%, gradient norms (the overall size of each update step) around 0.08. At step 165k the gradient norm spiked to 55. By step 172k it had reached 108. The validation loss climbed to 23.8, and VPM collapsed to 1.8%.

Validation loss and gradient norm of the exploded run vs the final run
Figure 12: The first attempt explodes at step 165k. The rerun with weight decay trains cleanly to 600k steps.

I first tried to rescue the run. Restarting from a healthy checkpoint with lower learning rates looked promising for a few thousand steps, but every variant either diverged again or showed the same upward bend in the loss. Whatever this was, lowering the learning rate only delayed it.

Digging through the checkpoints revealed something surprising: the failure was localized. In plain terms, one tiny part of the network started growing without limit and dragged everything else down with it. Specifically, a single channel in the LayerNorm before the attention of block 1 grew from 0.16 to 6.3 between steps 150k and 200k while its paired bias swung to -6.0, and the Muon updates to that block's attention projections concentrated on that same channel. One normalization channel and a handful of weight columns had entered a feedback loop, and they took the rest of the model down with them.

The recipe had inherited one more choice from modded-nanogpt that I never questioned: no weight decay. Nothing in the training was pushing back against a weight that only wants to grow. The fix was a small amount of weight decay (0.005 for AdamW, 0.01 for Muon, values inspired by the NanoGPT speedrun community): a gentle force that shrinks every weight toward zero on each step, so nothing can grow unchecked. Then restart from scratch.

So, can autocomplete play chess blindfolded?

In short, usually. From our original goal of 100% VPM, our model is able to achieve 99.93%, with a 99.99% top-1 valid move accuracy. Game end accuracy was worse, out of our stated goal of 100% GPA our model achieves 99.49%, with a total correct game end probability mass of 99.19%

These numbers are from evaluating the model in a large, balanced, held-out dataset. The dataset consists of 768,000 games it never saw during training: 768 buckets, 16 white ratings (1000-2500) x 16 black ratings (1000-2500) x 3 time controls (bullet, blitz, rapid), 1,000 games per bucket, drawn from 2022-2023. Classical is left out: there are not enough classical games in that window to fill all 768 buckets.

99.93%Valid probability mass
99.49%Game end accuracy
Held-out metric (with metadata)BulletBlitzRapidTotal
Cross-entropy over legal moves (nats)1.4461.3211.3321.366
Perplexity over legal moves4.253.753.793.92
Valid move probability mass99.93%99.94%99.92%99.93%
Top-1 move = human's55.12%58.07%57.83%57.01%
Game-end prediction accuracy99.78%99.46%99.29%99.49%
Table 8: Every metric on the held-out human set. Cross-entropy and perplexity renormalize the model's distribution over the legal moves in each position, and are lower-is-better; the other three, higher. Every value is micro-averaged over the 768 cells, pooling the raw counts so that each move target counts once, and is computed from per_cell_metrics.csv.

The two deep dives below explore where the remaining 0.07% VPM and 0.51% GPA are.

Deep diveWhere the model plays an illegal move

The top move is legal 99.99% of the time, yet the distribution still leaks 0.07% of its probability onto illegal ones, the gap under a 99.93% VPM.

92.6% of this 0.07% is the model choosing illegal moves. The remaining 7.4% is the model assigning probability to a game end token before the game has ended.

Illegal moves
92.6%
Spurious game-ends
7.4%
Figure 13: The 0.07% of invalid probability mass, split by kind. Illegal moves hold almost all of it; spurious game ends, where the model declares a still-live game over, take the small remainder.

The type of illegal move with the most probability mass is leaving the king in check, followed by the move not being possible due to the path being blocked, and then the model attempting to move a piece that is not there.

Leaves king in check
26.2%
Blocked path
16.1%
No piece on from-square
13.3%
Own piece on to-square
8.4%
Illegal pawn move
5.7%
Wrong-color piece
3.6%
Illegal en passant
2.9%
Illegal castle
0.9%
Other
14.9%
Unclassified
8.0%
Figure 14: Illegal-move mass by reason, as a share of the illegal-move mass. Leaves king in check and blocked path lead. Other and Unclassified pool the residual: odd cases, and mass too small per position to pin on one rule.

The spurious game ends spread their probability mass fairly evenly across the ending types, with stalemate and the fifty-move rule the least likely.

Checkmate
32.2%
Insufficient material
26.8%
Threefold repetition
25.3%
Fifty-move rule
10.5%
Stalemate
5.2%
Figure 15: False game-end mass by ending type.

A greedy player who always takes the model's top move meets a different slice: the 4,830 positions, out of 45.7M, where that top move is illegal. False game ends take 42.5% of this slice against 7.4% of the mass above. When the model calls a live game over it commits a large share of its probability, so the error weighs more under the top move than in the mass. The 1,534 spurious game ends here are the same false alarms the game end deep dive counts (Table 9).

Spurious game end
42.51% / 1,534
Leaves king in check
26.20% / 1,461
No piece on from-square
11.03% / 642
Wrong-color piece
7.20% / 366
Other
5.68% / 390
Own piece on to-square
4.05% / 219
Blocked path
1.52% / 95
Illegal pawn move
1.15% / 75
Illegal castle
0.52% / 37
Illegal en passant
0.15% / 11
Figure 16: The argmax view: of the 4,830 positions whose most likely token is illegal, the reason split weighted by that token's probability, with the position count. The model is often confident in its false endings, so spurious game ends and check violations lead here, a different picture from where the probability mass sits (Figure 13).
Deep diveWhere the game end predictions fail

The 99.49% GPA pools easy endings with hard ones. The model nails the sharp, on-the-board endings and stumbles on the two that are rare or inconsistently drawn in the training data:

Checkmate
99.90% / 266k
Insufficient material
99.72% / 3k
Stalemate
99.31% / 4k
Threefold repetition
85.54% / 7k
Fifty move rule
4.70% / 149
Figure 17: Accuracy at predicting each ending, with metadata. Checkmate reads off the board; the fifty move rule is so rare in the data the model barely sees one.

The misses split in two. A false alarm (calling a live game over) is the one to avoid, and it stays rare: across the 91,457,538 move predictions in the two scorings, with and without metadata, the model raised one 3,077 times, about 0.003%. Of those, 1,534 come from the with-metadata run below. The opposite error, missing a real ending and playing on, is what the 99.49% measures, and nearly all those misses land on threefold repetition and the fifty move rule.

Split across the 280,472 real terminal positions and the 45.7M ongoing ones, with metadata:

Failure modeCountRate
Real ending missed, played a move instead1,1340.404%
Real ending not even in the model's top 52030.072%
Right that it ended, wrong reason170.006%
False alarm (declared a live game over)1,5340.003%
Table 9: Almost every miss is a threefold-repetition or fifty move ending, the two the model sees least cleanly: fifty move endings are vanishingly rare in the data, and Lichess never forces a threefold draw, so players often play on past one toward a fivefold repetition.

The value of Elo and time control information

The metadata tokens were meant to let the model adapt to the players in front of it. Do they change how it plays? This is tested by scoring the model twice on the same games: once with all three metadata tokens present, once with every one replaced by <|missing|>. The gain is smallest when both players sit near the 1664 median, where the model already guesses the typical continuation, and largest when the players are far from average:

White rating ↓
Black rating →
1000–13001400–17001800–21002200–2500
1000–1300+0.81+0.86+1.32+2.03
1400–1700+0.78+0.39+0.62+1.29
1800–2100+1.12+0.53+0.43+0.85
2200–2500+1.76+1.19+0.85+0.99
smaller gainlarger gain
Figure 18: Extra human move match (percentage points) from telling the model the real ratings and time control, versus not, grouped by the two players' ratings. It helps least when both are average (+0.4) and most when a weak player faces a strong one (+2.0).

The pattern shows the model acts on the rating tokens rather than ignoring them. Between two average players there is little to add: the no-metadata guess is already the common move. Tell it instead that a 1000 rated player is facing a 2400, and it shifts toward moves it would otherwise never reach, a pairing nothing in its no-metadata prior accounts for.

The coarse 4×4 heatmap (Figure 18) bins a finer 16×16 grid. The full version is in the appendix: the metadata gain (Figure 23), raw move match (Figure 24), and loss (Figure 25), cell by cell.

Playing strength

Deep diveHow playing strength is measured

The model plays Stockfish 18 at a range of Elo levels, and each game is scored for the model as a win, draw, or loss (1, ½, or 0). Games that finish on the board are scored by their result; a game that hits a cap of 200 half moves without ending is adjudicated by a full-strength Stockfish (Elo limiter off, single-threaded, search depth 18), scored as a win or loss once its evaluation passes 200 centipawns and a draw otherwise. The win, draw, and loss counts over all the games give the relative rating.

Each game is seeded with an opening rather than started from the empty board. Without that, a greedy model against fixed Stockfish settings would replay nearly the same game every time. The openings are the 32 most common lines (those most often reached in the training corpus, each taken at a fixed depth of four moves) and the 32 rarest, each followed as deep as needed to reach a line the model almost never saw. We keep only equal openings, meaning Stockfish gives neither side more than a 25-centipawn advantage in the position after the opening. That keeps a game turning on how the two sides play rather than on a lopsided position handed to one of them before its first choice. No opening is a continuation of another, so the 64 openings cover genuinely distinct lines; each is played twice in every configuration, once with the model as white and once as black, and 64 openings × 2 colors is where the 128 games per cell reported below come from. Both groups are listed in full below.

ECOOpeningGamesEvalMoves
C57Italian Game: Two Knights Defense, Knight Attack, Normal Variation11,968,381+0.241. e4 e5 2. ♘︎f3 ♞︎c6 3. ♗︎c4 ♞︎f6 4. ♘︎g5 d5
C54Italian Game: Classical Variation8,320,274+0.241. e4 e5 2. ♘︎f3 ♞︎c6 3. ♗︎c4 ♝︎c5 4. c3 ♞︎f6
C50Italian Game: Giuoco Pianissimo, Normal7,706,006+0.221. e4 e5 2. ♘︎f3 ♞︎c6 3. ♗︎c4 ♞︎f6 4. d3 ♝︎c5
C45Scotch Game: Schmidt Variation4,627,923+0.141. e4 e5 2. ♘︎f3 ♞︎c6 3. d4 exd4 4. ♘︎xd4 ♞︎f6
C50Four Knights Game: Italian Variation3,689,342+0.181. e4 e5 2. ♘︎f3 ♞︎c6 3. ♗︎c4 ♝︎c5 4. ♘︎c3 ♞︎f6
C47Four Knights Game: Scotch Variation Accepted3,532,392+0.071. e4 e5 2. ♘︎f3 ♞︎c6 3. ♘︎c3 ♞︎f6 4. d4 exd4
C51Italian Game: Evans Gambit Accepted2,993,907-0.211. e4 e5 2. ♘︎f3 ♞︎c6 3. ♗︎c4 ♝︎c5 4. b4 ♝︎xb4
D53Queen's Gambit Declined2,542,137+0.131. d4 d5 2. c4 e6 3. ♘︎c3 ♞︎f6 4. ♗︎g5 ♝︎e7
C44Scotch Game: Haxo Gambit2,382,962+0.071. e4 e5 2. ♘︎f3 ♞︎c6 3. d4 exd4 4. ♗︎c4 ♝︎c5
B13Caro-Kann Defense: Exchange Variation2,314,558+0.241. e4 c6 2. d4 d5 3. exd5 cxd5 4. ♘︎f3 ♞︎c6
C44Scotch Game: Scotch Gambit, Dubois Réti Defense2,279,040+0.051. e4 e5 2. ♘︎f3 ♞︎c6 3. ♗︎c4 ♞︎f6 4. d4 exd4
C22Center Game: Berger Variation1,621,763-0.181. e4 e5 2. d4 exd4 3. ♕︎xd4 ♞︎c6 4. ♕︎e3 ♞︎f6
B22Sicilian Defense: Alapin Variation, Smith-Morra Declined1,022,691+0.201. e4 c5 2. c3 ♞︎f6 3. e5 ♞︎d5 4. d4 cxd4
B24Sicilian Defense: Closed798,689-0.021. e4 c5 2. ♘︎c3 ♞︎c6 3. g3 g6 4. ♗︎g2 ♝︎g7
C44Scotch Game: Scotch Gambit, London Defense767,016+0.031. e4 e5 2. ♘︎f3 ♞︎c6 3. d4 exd4 4. ♗︎c4 ♝︎b4+
C49Four Knights Game: Spanish Variation, Double Spanish725,017+0.171. e4 e5 2. ♘︎f3 ♞︎c6 3. ♘︎c3 ♞︎f6 4. ♗︎b5 ♝︎b4
A48London System481,292+0.181. d4 ♞︎f6 2. ♘︎f3 g6 3. ♗︎f4 ♝︎g7 4. e3 d6
E46Nimzo-Indian Defense: Normal Variation473,960+0.191. d4 ♞︎f6 2. c4 e6 3. ♘︎c3 ♝︎b4 4. e3 O-O
C48Four Knights Game: Spanish Variation, Rubinstein Variation319,315+0.101. e4 e5 2. ♘︎f3 ♞︎c6 3. ♘︎c3 ♞︎f6 4. ♗︎b5 ♞︎d4
D38Queen's Gambit Declined: Ragozin Defense318,441+0.241. d4 ♞︎f6 2. c4 e6 3. ♘︎f3 d5 4. ♘︎c3 ♝︎b4
E34Nimzo-Indian Defense: Classical Variation, Noa Variation296,038+0.161. d4 ♞︎f6 2. c4 e6 3. ♘︎c3 ♝︎b4 4. ♕︎c2 d5
B10Caro-Kann Defense: Accelerated Panov Attack, Modern Variation261,433+0.121. e4 c6 2. c4 d5 3. exd5 cxd5 4. cxd5 ♞︎f6
C36King's Gambit Accepted: Abbazia Defense216,096-0.221. e4 e5 2. f4 exf4 3. ♘︎f3 d5 4. exd5 ♞︎f6
C07French Defense: Tarrasch Variation, Chistyakov Defense211,808+0.191. e4 e6 2. d4 d5 3. ♘︎d2 c5 4. exd5 ♛︎xd5
C44Scotch Game: Scotch Gambit, Göring Gambit Declined197,073+0.031. e4 e5 2. ♘︎f3 ♞︎c6 3. d4 exd4 4. c3 d5
A48Indian Defense: Colle System, King's Indian Variation191,400+0.221. d4 ♞︎f6 2. ♘︎f3 g6 3. e3 ♝︎g7 4. ♗︎d3 d6
D04Queen's Pawn Game: Colle System, Grünfeld Formation138,514-0.151. d4 d5 2. ♘︎f3 ♞︎f6 3. e3 g6 4. ♗︎d3 ♝︎g7
A25English Opening: King's English Variation, Taimanov Variation93,747+0.221. c4 e5 2. ♘︎c3 ♞︎c6 3. g3 g6 4. ♗︎g2 ♝︎g7
A23English Opening: King's English Variation, Two Knights Variation, Keres Variation80,311+0.161. c4 e5 2. ♘︎c3 ♞︎f6 3. g3 ♝︎c5 4. ♗︎g2 c6
D00Blackmar-Diemer Gambit Declined: Lamb Defense77,192-0.171. d4 d5 2. e4 dxe4 3. ♘︎c3 ♞︎f6 4. f3 ♞︎c6
A36English Opening: Symmetrical Variation, Ultra-Symmetrical Variation72,407+0.251. c4 c5 2. g3 g6 3. ♗︎g2 ♝︎g7 4. ♘︎c3 ♞︎c6
A48Queen's Pawn Game: Barry Attack71,768+0.001. d4 ♞︎f6 2. ♘︎f3 g6 3. ♘︎c3 d5 4. ♗︎f4 ♝︎g7
Table 10: The 32 most common seeding openings, most-reached first, each exactly four moves deep. Games is how many games in the training corpus reach the line; Eval is Stockfish's depth-22 evaluation of the position after the listed moves, from White's point of view, in pawns (so every line sits within ±0.25).
ECOOpeningGamesEvalMoves
A33English Opening: Symmetrical Variation, Anti-Benoni Variation, Geller Variation0+0.081. c4 e6 2. ♘︎f3 ♞︎f6 3. ♘︎c3 c5 4. d4 ♞︎c6 5. g3 cxd4 6. ♘︎xd4 ♛︎b6
B74Sicilian Defense: Dragon Variation, Classical Variation, Stockholm Attack0-0.161. e4 c5 2. ♘︎c3 d6 3. f4 ♞︎c6 4. ♘︎f3 g6 5. d4 cxd4 6. ♘︎xd4 ♝︎g7 7. ♗︎e3 ♞︎f6 8. ♗︎e2 O-O 9. ♘︎b3 ♝︎e6 10. O-O ♞︎a5 11. f5 ♝︎c4 12. ♘︎xa5 ♝︎xe2 13. ♕︎xe2 ♛︎xa5 14. g4
D34Tarrasch Defense: Classical Variation, Bogoljubow Variation0+0.241. d4 ♞︎f6 2. c4 e6 3. ♘︎c3 c5 4. ♘︎f3 d5 5. cxd5 exd5 6. g3 ♞︎c6 7. ♗︎g2 ♝︎e7 8. O-O O-O 9. ♗︎g5 ♝︎e6 10. ♖︎c1 c4
D49Semi-Slav Defense: Meran Variation, Rellstab Attack0+0.011. d4 d5 2. c4 c6 3. ♘︎c3 ♞︎f6 4. ♘︎f3 e6 5. e3 ♞︎bd7 6. ♗︎d3 dxc4 7. ♗︎xc4 b5 8. ♗︎d3 a6 9. e4 c5 10. e5 cxd4 11. ♘︎xb5 ♞︎xe5 12. ♘︎xe5 axb5 13. O-O ♛︎d5 14. ♕︎e2 ♝︎a6 15. ♗︎g5
D64Queen's Gambit Declined: Orthodox Defense, Rubinstein Attack0+0.221. d4 ♞︎f6 2. c4 e6 3. ♘︎c3 d5 4. ♘︎f3 ♝︎e7 5. ♗︎g5 O-O 6. e3 ♞︎bd7 7. ♖︎c1 c6 8. a3 a6 9. ♕︎c2
D68Queen's Gambit Declined: Orthodox Defense, Classical Variation0+0.091. d4 d5 2. c4 e6 3. ♘︎f3 ♞︎f6 4. ♘︎c3 ♝︎e7 5. ♗︎g5 O-O 6. e3 ♞︎bd7 7. ♖︎c1 c6 8. ♗︎d3 dxc4 9. ♗︎xc4 ♞︎d5 10. ♗︎xe7 ♛︎xe7 11. O-O ♞︎xc3 12. ♖︎xc3 e5 13. ♕︎b1
E08Catalan Opening: Closed, Zagoryansky Variation0+0.181. d4 e6 2. c4 ♞︎f6 3. ♘︎f3 d5 4. g3 ♝︎e7 5. ♗︎g2 O-O 6. O-O ♞︎bd7 7. ♕︎c2 c6 8. ♖︎d1 b6 9. a4
A30English Opening: Symmetrical, Hedgehog, Flexible Formation1+0.221. c4 e6 2. ♘︎f3 c5 3. ♘︎c3 ♞︎f6 4. g3 b6 5. ♗︎g2 ♝︎b7 6. O-O ♝︎e7 7. d4 cxd4 8. ♕︎xd4 d6 9. ♖︎d1 a6 10. b3 ♞︎bd7
B51Sicilian Defense: Moscow Variation, Dorfman Gambit1-0.061. e4 c5 2. ♘︎f3 d6 3. ♗︎b5+ ♞︎c6 4. O-O ♝︎d7 5. ♕︎e2 g6 6. e5
D12Slav Defense: Quiet Variation, Landau Variation1-0.251. d4 d5 2. c4 c6 3. ♘︎f3 ♞︎f6 4. e3 ♝︎f5 5. cxd5 cxd5 6. ♕︎b3 ♛︎c8 7. ♗︎d2 e6 8. ♘︎a3
D26Queen's Gambit Accepted: Classical, Furman Variation1-0.051. d4 d5 2. c4 dxc4 3. ♘︎f3 ♞︎f6 4. e3 e6 5. ♗︎xc4 c5 6. ♕︎e2 a6 7. dxc5 ♝︎xc5 8. O-O ♞︎c6 9. e4 b5 10. e5
D28Queen's Gambit Accepted: Classical, Flohr Variation1+0.001. d4 d5 2. c4 dxc4 3. ♘︎f3 ♞︎f6 4. e3 e6 5. ♗︎xc4 c5 6. O-O a6 7. ♕︎e2 b5 8. ♗︎b3 ♞︎c6 9. ♖︎d1 c4 10. ♗︎c2 ♞︎b4 11. ♘︎c3 ♞︎xc2 12. ♕︎xc2 ♝︎b7 13. d5 ♛︎c7
D49Semi-Slav Defense: Meran Variation, Stahlberg Variation1+0.221. d4 d5 2. c4 c6 3. ♘︎c3 ♞︎f6 4. e3 e6 5. ♘︎f3 ♞︎bd7 6. ♗︎d3 dxc4 7. ♗︎xc4 b5 8. ♗︎d3 a6 9. e4 c5 10. e5 cxd4 11. ♘︎xb5 ♞︎xe5 12. ♘︎xe5 axb5 13. ♕︎f3
C41Philidor Defense: Lopez Countergambit, Jaenisch Variation2+0.001. e4 e5 2. ♘︎f3 d6 3. d4 f5 4. ♗︎c4 exd4 5. ♘︎g5 ♞︎h6 6. ♘︎xh7
C42Petrov's Defense: Classical Attack, Maróczy Variation2-0.121. e4 e5 2. ♘︎f3 ♞︎f6 3. ♘︎xe5 d6 4. ♘︎f3 ♞︎xe4 5. d4 d5 6. ♗︎d3 ♝︎e7 7. O-O ♞︎c6 8. ♖︎e1 ♝︎g4 9. c3 f5 10. c4 ♝︎h4
B06Pterodactyl Defense: Fianchetto, King Pterodactyl4+0.201. e4 g6 2. d4 ♝︎g7 3. g3 c5 4. ♘︎f3 ♛︎a5+
A45Trompowsky Attack: Edge Variation, Hergert Gambit5+0.241. d4 ♞︎f6 2. ♗︎g5 ♞︎e4 3. ♗︎h4 c6 4. ♘︎d2 ♛︎a5 5. c3 ♞︎xd2 6. ♕︎xd2 d5 7. e4
C33King's Gambit Accepted: Bishop's Gambit, Fraser Variation5-0.241. e4 e5 2. f4 exf4 3. ♗︎c4 ♛︎h4+ 4. ♔︎f1 g5 5. ♘︎c3 ♝︎g7 6. g3 fxg3 7. ♕︎f3
D35Queen's Gambit Declined: Exchange Variation, Chameleon Variation5-0.221. d4 ♞︎f6 2. c4 e6 3. ♘︎c3 d5 4. cxd5 exd5 5. ♗︎g5 ♝︎e7 6. e3 O-O 7. ♗︎d3 ♞︎bd7 8. ♕︎c2 ♜︎e8 9. ♘︎ge2 ♞︎f8 10. O-O-O
D98Grünfeld Defense: Russian Variation, Keres Variation6+0.201. d4 ♞︎f6 2. c4 g6 3. ♘︎c3 d5 4. ♘︎f3 ♝︎g7 5. ♕︎b3 dxc4 6. ♕︎xc4 O-O 7. e4 ♝︎g4 8. ♗︎e3 ♞︎fd7 9. ♗︎e2 ♞︎b6 10. ♕︎d3 ♞︎c6 11. O-O-O
E08Catalan Opening: Closed6+0.251. d4 ♞︎f6 2. c4 e6 3. g3 d5 4. ♗︎g2 ♝︎e7 5. ♘︎f3 O-O 6. O-O ♞︎bd7 7. ♕︎c2 c6 8. ♗︎f4 b6 9. ♘︎bd2 ♝︎b7 10. e4
E29Nimzo-Indian Defense: Sämisch Variation, Capablanca Variation6-0.051. d4 ♞︎f6 2. c4 e6 3. ♘︎c3 ♝︎b4 4. e3 c5 5. ♗︎d3 ♞︎c6 6. a3 ♝︎xc3+ 7. bxc3 O-O 8. ♘︎e2 b6 9. e4 ♞︎e8
C88Ruy Lopez: Closed, Alekhine Gambit7+0.001. e4 e5 2. ♘︎f3 ♞︎c6 3. ♗︎b5 a6 4. ♗︎a4 ♞︎f6 5. O-O ♝︎e7 6. ♖︎e1 b5 7. ♗︎b3 d6 8. c3 ♞︎a5 9. ♗︎c2 c5 10. d4 ♛︎c7 11. ♘︎bd2 O-O 12. ♘︎f1 ♝︎g4 13. ♘︎e3 ♝︎xf3 14. ♕︎xf3
D67Queen's Gambit Declined: Orthodox Defense, Janowski Variation7+0.251. d4 d5 2. ♘︎f3 ♞︎f6 3. c4 e6 4. ♘︎c3 ♝︎e7 5. ♗︎g5 O-O 6. e3 ♞︎bd7 7. ♖︎c1 c6 8. ♗︎d3 dxc4 9. ♗︎xc4 ♞︎d5 10. h4
E14Queen's Indian Defense: Averbakh Variation7+0.021. d4 ♞︎f6 2. c4 e6 3. ♘︎f3 b6 4. e3 ♝︎b7 5. ♗︎d3 c5 6. O-O ♝︎e7 7. b3 O-O 8. ♗︎b2 cxd4 9. ♘︎xd4
D40Queen's Gambit Declined: Semi-Tarrasch Defense, Levenfish Variation8-0.031. d4 d5 2. c4 e6 3. ♘︎c3 ♞︎f6 4. ♘︎f3 c5 5. e3 ♞︎c6 6. ♗︎d3 ♝︎d6 7. O-O O-O 8. ♕︎e2 ♛︎e7 9. dxc5 ♝︎xc5 10. e4
D49Semi-Slav Defense: Meran Variation, Sozin Variation8+0.001. d4 d5 2. ♘︎f3 ♞︎f6 3. c4 c6 4. ♘︎c3 e6 5. e3 ♞︎bd7 6. ♗︎d3 dxc4 7. ♗︎xc4 b5 8. ♗︎d3 a6 9. e4 c5 10. e5 cxd4 11. ♘︎xb5 ♞︎xe5 12. ♘︎xe5 axb5 13. O-O
D19Slav Defense: Czech Variation, Dutch Variation, Sämisch Variation9+0.201. d4 d5 2. c4 c6 3. ♘︎f3 ♞︎f6 4. ♘︎c3 dxc4 5. a4 ♝︎f5 6. e3 e6 7. ♗︎xc4 ♝︎b4 8. O-O O-O 9. ♕︎e2 ♞︎e4 10. g4
E63King's Indian Defense: Fianchetto Variation, Panno Variation, Korchnoi Line9+0.121. d4 ♞︎f6 2. c4 g6 3. ♘︎f3 ♝︎g7 4. g3 O-O 5. ♗︎g2 d6 6. O-O ♞︎c6 7. ♘︎c3 a6 8. h3 ♜︎b8 9. ♗︎e3 b5 10. ♘︎d2
E42Nimzo-Indian Defense: Rubinstein System, Rubinstein Variation, Sherbakov Attack10-0.051. d4 ♞︎f6 2. c4 e6 3. ♘︎c3 ♝︎b4 4. e3 c5 5. ♘︎e2 cxd4 6. exd4 O-O 7. c5
A40English Defense: Hartlaub Gambit Declined12+0.171. c4 e6 2. d4 b6 3. ♘︎c3 ♝︎b7 4. e4 f5 5. d5
C15French Defense: Winawer Variation, Fingerslip Variation, Schwarz's Line12-0.191. e4 e6 2. d4 d5 3. ♘︎c3 ♝︎b4 4. ♗︎d2 ♞︎e7 5. ♘︎b1
Table 11: The 32 rarest seeding openings, rarest first. Several never occur in the training corpus at all, and none is a continuation of another opening in either table. Columns as above.

The rating in each cell is a performance rating: the Elo you would need to score that result against that one fixed opponent, read off the standard (FIDE) rating-difference table, the same method Allie uses. Because it depends only on the win/draw/loss totals, not the order the games were played, it is order-independent and reproducible. The ± is one standard error of the score, mapped onto the Elo scale.

The five opponents in the final 46,080-game evaluation are not one Stockfish run at different depths. Each level is a distinct configuration, a search-depth cap plus a time budget per move, and the Elo shown is the nominal rating attached to that configuration:

LevelPinned EloDepth capTime / moveThreads
013475126 ms1
317295168 ms1
725185253 ms1
1128518337 ms1
16309912422 ms1
Table 12: The five Stockfish opponents and their settings.

The model itself plays greedily during evaluation (it always takes its single most likely legal move, no sampling). The depth caps and per-move times come from Lichess's fishnet client, and the Elo labels are Stockfish's own published skill-level ratings, interpolated to each level. Those Elo figures are labels for the configurations rather than a calibrated human scale, so a rating read off this ladder should be read as "relative to this ladder," not as a Lichess or FIDE number.

To measure strength the model played 46,080 games against Stockfish at five capped levels across openings and all four time controls, with long games adjudicated by a full-strength Stockfish. The model plays greedily: its single most likely legal move, no search. It is conditioned on a time control too, so each figure shows a single time control; this one is classical. Conditioned on a 2500 rating, the top of our grid, it scores:

SF 1347
95.3%±1.7
SF 1729
73.8%±3.4
SF 2518
44.1%±3.8
SF 2851
16.4%±2.3
SF 3099
9.4%±1.8
Figure 19: Score against the Stockfish ladder, conditioned on a 2500 rating, in classical games; the dashed line marks an even (50%) match and the whiskers are ±1 standard error over 128 games per opponent.

Each of those scores converts to a performance rating: the Elo you would need to score that much against that one opponent. For the 2500-conditioned model in classical:

SF 1347
1817±66
SF 1729
1913±30
SF 2518
2475±27
SF 2851
2567±29
SF 3099
2716±37
Figure 20: The 2500-conditioned model's performance rating against each Stockfish level in classical games. It never settles on a single number: ~1817 against the weakest level, climbing to ~2716 against the strongest.

The conditioned rating has only a minor effect on the performance rating of the model, and the effect disappears against stronger opponents. The factor that most affects the model's playing strength is its opponent. The stronger the opponent, the better the model plays. Greedy play also sets a floor on how weak the model can be. Its output is a poll of every 1000 rated player it has seen, and they blunder in different ways: the mistakes scatter across many moves while the reasonable move collects the most mass, so always playing the top move amounts to taking the crowd's vote. Even conditioned as a 1000, the model lands around 1600 against the weakest ladder level.

Figure 21: The full strength experiment: model score against the Stockfish ladder, one line per conditioned rating plus the dashed no-metadata line. Whiskers are ±1 standard error.

By default each line is one conditioned rating, drawn across the Stockfish ladder. The X axis control flips that: one line per Stockfish level, drawn across the conditioned ratings, with No metadata as a trailing column, so the chart reads two ways, across the ladder or across conditioning. Hover a column to read off every visible line with its wins, draws, and losses. Each point pools 128 games (the no-metadata line 1,152); the All time-control view pools all four controls into 512 (no metadata 4,608). Filtering to one opening family or one color halves the count, both together quarter it, and the whiskers widen to match. The dashed reference marks an even 50% score, or in the rating view the opponent's own Elo (shown only in the default Stockfish-x orientation). The same numbers, exact and cell by cell, are in the strength table (Figure 22).

Deep divePlaying strength, in detail

The table holds every conditioned rating against every Stockfish level, in classical games, 128 games per cell, with metadata; the bars above are its 2500 row. Its controls switch the metric (score, W/D/L, or performance rating), the time control, the opening family, and the colour the model played, and add a no-metadata row.

Figure 22: Every strength result: each conditioned rating (rows) against each Stockfish level (columns), 128 games per cell, opening on classical score.

Conditioning helps, but only within a band: in the weak and middle columns the cells get greener from the 1000 row up to 2500, and in the two strongest columns that gain is gone, the 2500-conditioned model no greener than the 1000-conditioned one, both red.

Turned into performance ratings (the Elo needed to score each result against that one opponent), the model traces a slope rather than settling on a number. The 2500-conditioned model rates about 1817 against the weakest Stockfish and about 2716 against the strongest, a ~900-point spread for one fixed model. Conditioning also matters most against weak opponents: on the rating metric the lines fan out about 240 points against the weakest engine and collapse to about 70 against the strongest. Switch Figure 21 to the performance-rating metric to see both. These are ratings on this Stockfish ladder, not a Lichess or FIDE scale.

How all 46,080 games ended:

Game endingCountShare
Checkmate36,68679.6%
Threefold repetition6,67214.5%
Stalemate6961.5%
Insufficient material3670.8%
Fifty move rule100.0%
Adjudicated at the 200-move cap1,6493.6%
Total games46,080
Table 13: How the 46,080 strength games ended.

Over three quarters end in checkmate on the board; threefold repetition is next at 14.5%, and only 3.6% needed adjudication at the 200-move cap.

An important caveat: these are results against specific configurations of Stockfish, not against humans. An engine chooses moves nothing like a person does, so a score against it says how the model performs in that match-up, not how it would do against a human of any given rating.

How does it compare to the state of the art?

There is a line of research focused on chess models that are able to play chess like a human would. Given that our model is trained on an enormous amount of human games, we should expect it to do quite well on this task. The standard benchmark is the test set released with Allie (884,049 Lichess blitz positions) scored by how often a system's top move equals the move the human actually played.

SystemMove match
chess-autocomplete-v1 (700M, 61.4B tokens)57.2%
Maia-3 (79M, 2026) †57.1%
chess-autocomplete-v1 (350M)57.0%
Maia-3 (23M, 2026) †56.6%
chess-autocomplete-v1 (91M)56.0%
Allie (adaptive search, 2024) † ⚙55.9%
Allie (policy, 2024) †55.7%
Maia-3 (5M, 2026) †55.4%
GPT-3.5 (2023) †53.7%
Maia-2 (2024) †52.0%
Maia⋆ (rating-matched, 2020) †51.6%
Table 14: Move-match on the Allie test set: 884,049 held-out Lichess blitz positions, with the opening (first five moves) and every sub-30-second move removed. numbers are taken from the Maia-3 paper rather than re-run here, so read the ranking as indicative. Allie (adaptive search) adds tree search at inference time. Every other row, ours included, is a single forward pass. Maia⋆ is not one network but a rating-matched ensemble: for each position it uses the Maia model whose Elo band is closest to the players'. All values carry a 95% confidence interval of about ±0.1. The chess-autocomplete-v1 350M and 700M models will be discussed in the next post.

The 700M takes the top spot on half the data: it is the T=300k anneal branch, 61.4B tokens against the 122.9B the other two each saw. The 350M lands third, a tenth of a point under Maia-3's 79M variant. The 91M sits fifth, with four rows above it, two of them our own larger models and two the largest Maia-3 variants (23M and 79M, released in May 2026); it still lands ahead of Allie, GPT-3.5, Maia-2, and the rating-matched original Maia.

That leaderboard is blitz. Maia-2 measures the same skill on rapid, its Maia-1 test set, where the moving player's rating splits the games into Skilled, Advanced, and Master. Both our models lead Maia-2 in every group:

ModelSkilledAdvancedMasterAvg
Maia-251.7254.1553.8753.25
Ours (91M)56.6959.3960.9058.99
Ours (350M)57.4060.3462.1759.97
Table 15: Top-1 move-prediction accuracy (%) on Maia-2's Maia-1 test set: 106,740 rapid positions from December 2019, grouped by the moving player's rapid rating into Skilled (under 1600), Advanced (1600–2000) and Master (over 2000), with the first ten plies and every sub-30-second move removed. Avg is the macro-average over the three groups. The Maia-2 row is quoted from its Table 1; our rows are computed. The chess-autocomplete-v1 350M model will be discussed in the next post.

The remaining reproductions, Maia-2's cross-skill grid, Allie's move-legality and move-type tables, and a breakdown of where the 56% comes from, are in the appendix.

Blindfolded, and near the top

The question was whether a model that reads nothing but a stream of moves, with no board in front of it and no rules to consult, could play chess blindfolded. It can.

On our held-out set of games it never saw, 99.93% of its probability lands on legal moves, and it correctly determines when a game has ended 99.49% of the time. It hasn't learned the rules perfectly, but almost.

On the standard human move benchmark it matches 56% of the moves real players chose, behind only the two largest Maia-3 networks and ahead of every Maia, Maia-2, Allie, and GPT-3.5 entry. That is 91M parameters, with none of the machinery the others lean on. These benchmarks each cover one thin slice of chess. The Allie set is blitz, its openings and time-scramble moves thrown out; the Maia-2 set is rapid, split into rating bands. The models behind them are narrow to begin with: Allie and Maia-3 trained on blitz alone, Maia-2 on rapid. Ours saw all of it, four time controls from bullet to classical and every rating, nothing held back, and it still scores near the top on their slice.

And all of this was achieved by simply:

predicting the next token

That board at the top? It's the full model

The board at the top of this page is the real thing: the full 91M parameter model running in your browser through ONNX Runtime with WebGPU. At each move the model produces a probability distribution over moves; illegal moves are masked out, and by default it plays its most likely legal move (you can switch on temperature and top-k sampling in the settings to make it less predictable). The interface handles the FIDE draw rules. By default the model is conditioned as a 2500 rated rapid player, the top rating bucket, and the same 2500 rating the strength numbers above used (those were in classical, this board is rapid). Good luck.

Get the model, dataset, and code

Everything in this post is open:

References

Appendix

The full rating×rating grids

The coarse 4×4 heatmap in The value of Elo and time control information bins a finer 16×16 grid. Here is the full version, all with metadata, white rating down the rows and black rating across the columns: the metadata gain (Figure 23), brightest at the corners; raw move match (Figure 24), which rises with rating because stronger players are more predictable; and the loss (Figure 25), which falls the same way. On a narrow screen the grids scroll sideways.

W ↓ / B →1000110012001300140015001600170018001900200021002200230024002500
1000+1.07+0.90+0.93+1.19+1.15+1.18+1.23+1.24+1.45+1.56+1.79+2.09+2.16+2.18+2.31+2.42
1100+0.71+0.70+0.69+0.74+0.82+0.87+0.85+1.11+1.33+1.10+1.36+1.90+1.92+2.02+2.10+2.19
1200+0.94+0.78+0.67+0.73+0.75+0.64+0.75+0.76+0.88+1.04+1.12+1.52+1.86+1.94+2.12+2.28
1300+0.91+0.80+0.66+0.60+0.54+0.62+0.61+0.59+0.76+0.90+1.06+1.32+1.49+1.87+1.82+1.86
1400+1.01+0.77+0.69+0.64+0.56+0.49+0.37+0.50+0.51+0.71+0.84+1.16+1.37+1.59+1.77+1.87
1500+0.99+0.74+0.66+0.58+0.40+0.48+0.39+0.31+0.44+0.61+0.63+0.93+0.91+1.21+1.24+1.62
1600+1.02+0.82+0.74+0.55+0.43+0.29+0.28+0.24+0.31+0.43+0.72+0.80+0.76+1.11+1.33+1.38
1700+1.12+0.86+0.68+0.66+0.47+0.47+0.24+0.25+0.25+0.27+0.65+0.71+0.72+1.10+1.23+1.38
1800+1.22+0.93+0.87+0.81+0.52+0.44+0.29+0.22+0.27+0.29+0.56+0.65+0.83+0.89+0.99+1.17
1900+1.35+0.86+1.01+0.90+0.72+0.49+0.34+0.31+0.25+0.24+0.42+0.65+0.55+0.85+0.92+1.12
2000+1.49+1.27+1.12+0.89+0.82+0.62+0.48+0.43+0.36+0.31+0.48+0.47+0.51+0.71+0.89+1.17
2100+1.36+1.49+1.28+1.08+0.88+0.83+0.60+0.54+0.47+0.43+0.53+0.59+0.52+0.67+0.81+1.08
2200+1.73+1.61+1.42+1.46+1.15+0.85+0.79+0.69+0.59+0.62+0.69+0.46+0.56+0.78+0.87+1.11
2300+1.83+1.91+1.57+1.44+1.39+1.08+0.96+0.92+0.75+0.73+0.78+0.70+0.69+0.75+0.92+0.99
2400+2.09+1.74+1.73+1.61+1.49+1.29+1.28+1.15+1.02+0.87+0.82+0.88+0.91+0.91+1.15+1.19
2500+2.11+2.11+1.80+1.96+1.73+1.52+1.52+1.31+1.26+1.09+1.12+1.20+1.14+1.23+1.32+1.40
Figure 23: Metadata gain in human move match (percentage points) over playing without metadata, 16×16 rating grid. Brightest at the corners.
W ↓ / B →1000110012001300140015001600170018001900200021002200230024002500
100050.952.551.951.852.351.952.653.153.553.953.854.154.954.955.555.4
110052.653.554.153.153.353.153.954.454.654.555.455.756.056.156.256.4
120051.654.254.654.754.453.754.254.254.855.955.656.156.856.857.257.4
130051.753.354.955.755.654.655.054.755.455.856.356.857.157.357.557.3
140052.553.654.655.656.156.055.355.855.956.256.456.857.457.657.857.9
150052.053.553.854.956.056.456.355.856.156.856.657.157.557.857.557.8
160052.754.154.355.255.756.556.956.956.757.257.257.657.858.258.658.2
170053.254.454.455.555.656.157.157.157.557.257.758.158.358.858.859.1
180053.855.054.955.956.056.856.657.658.057.958.157.958.758.858.958.9
190054.355.155.856.356.556.856.957.558.058.358.458.458.858.859.659.7
200054.555.656.456.557.056.957.357.557.458.259.058.959.259.059.259.6
210054.656.156.156.857.357.657.457.858.058.459.059.059.159.459.159.3
220055.056.356.857.357.557.458.258.358.658.859.359.359.459.759.859.5
230055.056.656.757.257.957.758.358.858.758.859.159.259.760.160.259.6
240055.656.457.458.058.058.058.859.159.259.559.459.260.160.260.160.2
250055.556.757.257.658.257.758.659.159.159.659.559.759.460.060.259.8
Figure 24: Top-1 human move match (%), 16×16 rating grid. Darker is higher.
W ↓ / B →1000110012001300140015001600170018001900200021002200230024002500
10001.6201.5531.5941.6051.5781.6071.5701.5581.5221.5071.5071.5051.4771.4761.4581.461
11001.5451.5081.4861.5381.5241.5341.4991.4911.4741.4771.4461.4371.4241.4221.4161.419
12001.6011.4801.4611.4551.4841.5121.4871.4841.4611.4201.4241.4211.3941.3931.3791.378
13001.5981.5321.4561.4191.4261.4661.4561.4611.4331.4151.3981.3821.3801.3721.3601.367
14001.5681.5161.4751.4281.4071.4061.4321.4121.4071.3991.3821.3751.3631.3521.3421.345
15001.5841.5221.5021.4591.3991.3881.3941.4161.3971.3801.3771.3601.3441.3361.3411.348
16001.5591.4951.4861.4401.4211.3821.3631.3601.3741.3551.3571.3351.3381.3151.3041.315
17001.5341.4811.4791.4331.4201.4071.3551.3551.3401.3461.3371.3241.3061.2921.2891.281
18001.5111.4541.4561.4261.4081.3841.3761.3361.3211.3161.3211.3211.2981.2851.2791.282
19001.4921.4591.4231.4101.3871.3761.3631.3421.3161.3031.3041.3091.2881.2921.2541.255
20001.4841.4351.4041.3911.3721.3681.3491.3411.3381.3041.2801.2821.2681.2731.2651.262
21001.4751.4141.4091.3771.3671.3481.3441.3261.3181.3021.2781.2751.2641.2551.2611.271
22001.4691.4101.3881.3631.3571.3531.3131.3041.2971.2931.2651.2651.2511.2421.2351.246
23001.4821.4021.3841.3671.3391.3381.3141.2901.2901.2811.2731.2611.2431.2291.2241.237
24001.4491.4101.3701.3381.3321.3331.2951.2821.2721.2621.2641.2701.2331.2211.2261.224
25001.4521.4051.3741.3551.3351.3561.3041.2821.2781.2601.2571.2501.2531.2361.2211.259
Figure 25: Mean cross-entropy loss in nats, 16×16 rating grid. Darker is lower loss.

Playing strength, in full

The playing-strength figures in the main text show one slice: classical games, with metadata, scored for the model (win = 1, draw = ½). The model is conditioned on the time control too, so the same experiment was run across all four (bullet, blitz, rapid, classical). The complete set — every conditioned rating against every Stockfish level, 128 games per cell, for each time control and as raw score, W/D/L, or a performance rating — is in the interactive table, Figure 22: change its Time control and Metric, or add the No-metadata row. The same numbers as a line chart are in Figure 21.

A warning on the rating metric. Against a single fixed opponent a performance rating is just the cell's score re-expressed on the Elo scale, so it is dominated by which opponent was played: scoring 10% against a 3099 rated Stockfish still rates around 2700, because that result is a 2700-level result by the Elo formula. It does not mean the model plays at 2700 strength while losing nine games in ten. Read each column as "how the model performs against this opponent," not as one number for the model; their disagreement across a row (about 1800 against the weak levels, about 2700 against the strong ones) is exactly why no single rating is more than a rough summary. And as everywhere in this section, these are estimates against Stockfish, whose move distribution is not human. See the caveats above. The ± is one standard error throughout.

Constrained moves are the easy ones

The 56% average is not uniform. The more constrained the move, the better the model guesses it. Forced and near-forced moves are far more predictable than a free choice in the middlegame:

All moves
56.0%
Castling
73.3%
En passant
73.0%
Pawn promotion
87.5%
Repetition draw
94.2%
Figure 26: Move-match by move type on the Allie set. Promotions and repetition draws are nearly forced; the model predicts them far more often than an average move.

Benchmark reproductions

The comparisons in this post are pulled from a larger evaluation suite that reproduces specific tables and figures from the Maia-2, Maia-3 and Allie papers as exactly as the released data allows. This appendix lists each reproduced artifact, the exact paper table or figure it maps to, and a compatibility status: paper-compatible (exact paper inputs, denominators and metric semantics), method-compatible (regenerated by the paper's own method where exact rows are not public), or blocked (a required paper input is unavailable, so the artifact is left unreproduced rather than approximated). Every figure below shows our model given the real metadata; values quoted from the source papers are labeled as such and were not recomputed.

B1. Maia-2 Table 1: move-prediction accuracy

Reproduces Maia-2 Table 1: top-1 move-prediction accuracy on the Maia-1 Testset, grouped Skilled / Advanced / Master with Avg the macro-averaged result (groups per Maia-2 §4.1). Status: paper-compatible, scored on the exact 106,740-row test set (Skilled 56,812, Advanced 41,747, Master 8,181). Our row is computed; the Maia-2 row is quoted verbatim from Maia-2 Table 1 and was not recomputed. The table itself, with our row in context, is in How does it compare to the state of the art?

B2. Maia-2 Figure 2: cross-skill grid

Reproduces Maia-2 Figure 2: move match as a function of the active player's rating (rows) and the opponent's rating (columns). Status: method-compatible: Maia-2 does not release the row-level cross-skill set, so it is regenerated from December 2023 Lichess rapid games (~4.93M scored positions), with per-cell counts targeted to Maia-2 Appendix Table 8. Move-match rises as either player gets stronger, with the active player's own rating the larger lever, the same surface Maia-2 reports.

active ↓
opponent →
110012001300140015001600170018002000+
110055.956.155.154.7
120056.856.756.456.056.1
130057.757.457.457.356.756.3
140059.058.358.257.957.557.756.8
150057.757.758.158.157.957.056.5
160059.759.759.158.858.458.357.4
170060.460.259.259.058.758.0
180060.960.759.359.358.9
2000+62.261.861.260.2
Figure 27: Our cross-skill move match (%), ~4.93M positions; blank cells are rating pairs more than three bins (≈300 points) apart, which the benchmark band does not cover.

The same grid as a delta against Maia-2's Figure 2: our move match minus the value Maia-2 prints in each Figure 2 cell, in percentage points. Maia-2 draws Figure 2 on a finer ten-bin rating axis (1900 and 2000+ split apart) than the nine-bin Appendix Table 8 scheme our eval used, so the delta is shown only where the bins coincide exactly: both players rated 1800 or below (44 cells). The 2000+ row and column are left blank rather than compared across mismatched bins. We lead Maia-2 in every comparable cell, by roughly four to five points.

active ↓
opponent →
110012001300140015001600170018002000+
1100+4.7+4.7+4.9+4.7
1200+4.7+4.7+4.2+5.4+5.0
1300+5.3+4.5+4.8+4.5+5.2+5.2
1400+5.1+5.0+4.8+4.5+4.1+5.6+5.2
1500+5.5+5.4+4.8+5.0+4.8+4.8+5.0
1600+4.9+5.2+5.1+5.1+5.0+5.3
1700+5.3+5.4+5.0+5.2+5.5
1800+5.3+5.5+4.9+5.0
2000+
Figure 28: Ours − Maia-2 Figure 2 move match (percentage points, all positive); darker is a larger lead. Maia-2's per-cell numbers are read from the labels printed in its Figure 2; blank cells are either outside the benchmark band or in the ≥1900 range where our nine-bin eval and Maia-2's ten-bin figure do not line up.

B3. Maia-3 Table 1: move match leaderboard

Reproduces our row of Maia-3 (Chessformer) Table 1: the top-1 human move match leaderboard on the 884,049-position Allie test set. Status: paper-compatible for our entry (move match 56.05, scored locally); every other system in the leaderboard is quoted from Maia-3 Table 1 and was not recomputed. The full ranking, with our row in context, is in How does it compare to the state of the art?

B4. Allie Table 2: move legality

Reproduces Allie Table 2 (§5.1): the share of positions where the top-1 move is legal, on Lichess games and on random out-of-distribution positions, each also restricted to positions where the side to move is in check. Status: the Lichess rows are paper-compatible; the random rows are method-compatible, because Allie does not release its exact random positions (regenerated with seed 42). Allie's column is quoted from Allie Table 2 (reported there with 95% confidence intervals); ours is computed on Lichess 884,049, Lichess in check 63,844, Random 377,500 and Random in check 20,662 positions.

Evaluation setAllieOurs
Lichess100.0±0.099.99±0.00
Lichess (under check)100.0±0.099.99±0.02
Random99.9±0.099.84±0.01
Random (under check)96.6±0.099.43±0.10
Table 16: Cells are the paper's Top-1 move is valid (%): the share of positions whose single most-likely move is legal (higher is better); bold marks the better model in each row. Allie's column is quoted from Allie Table 2; only our column is computed. ± values are 95% confidence intervals.

B5. Allie Table 3: move match by move type

Reproduces Allie Table 3 (§5.2): top-1 move match across all moves and the special-move categories, comparing Allie-Policy, Maia⋆ and GPT-3.5. Status: paper-compatible, on the same 884,049-position set. The Allie, Maia⋆ and GPT-3.5 columns are quoted from Allie Table 3 (reported there with 95% confidence intervals); our column is computed. The same move match figures appear in Constrained moves are the easy ones above.

Human plays…Allie-PolicyMaia⋆GPT-3.5Ours
All moves55.7±0.151.6±0.153.7±0.156.0±0.1
Castling74.3±0.573.3±0.672.4±0.673.3±0.6
En passant70.4±4.167.7±4.271.4±4.073.0±3.9
Pawn promotion86.9±1.785.1±1.886.0±1.787.5±1.6
Threefold repetition92.0±4.687.0±5.792.8±4.494.2±3.9
Table 17: Cells are top-1 move match accuracy (%); bold marks the best model in each row. Allie-Policy / Maia⋆ / GPT-3.5 are quoted from Allie Table 3; only our column is computed. ± values are 95% confidence intervals.