Downloads · 30 days
0
amanm10000/chessllm
chessllm is a text generation model from amanm10000. Use it when you need the model to write or continue text. It is set up for pytorch. The card lists the license as mit.
A decoder-only transformer trained from scratch on Lichess games written in SAN notation. It has never seen a board: no FEN, no piece list, no legal-move generator. It reads the text of a game and predicts the charact…
Downloads · 30 days
0
Access
Public
Updated Aug 9, 2026
Repo size
378 MB
Likes
0
Public
Click a slice to open those files.
.pt378 MB · 100%
From the Hugging Face model README
A decoder-only transformer trained from scratch on Lichess games written in SAN notation. It has never seen a board: no FEN, no piece list, no legal-move generator. It reads the text of a game and predicts the characters that come next. Everything it does with chess it inferred from move notation alone.
Trained on a single RTX 4060 Laptop (8GB).
Its sibling is miniLLM — the same architecture and training code, trained on ordinary text instead of move notation.
| Parameters | 94.4M |
| Layers | 15 |
| Attention | 12 query heads / 4 KV heads (GQA), head_dim 64 |
| d_model | 768 |
| FFN | SwiGLU, hidden 2048 |
| Position | RoPE, θ=10000 |
| Context | 1024 tokens |
| Vocab | 40 characters (frozen id map) |
| Norm | RMSNorm, QK-norm |
| Embeddings | Tied |
| Optimizer | Muon (matrices) + AdamW (rest) |
| Final val loss | 0.4150 at step 7505/7505 |
Llama-3-shaped, scaled to fit 8GB.
Games are plain characters, one game per line:
;<20> e4 e5 Nf3 Nc6 Bb5 a6 ...
; starts a game, <20> is a rating bucket (mean player rating // 100, so <15>
through <24> = 1500–2499), moves are space-separated SAN, and a newline ends the
game. The 40-id vocabulary covers exactly the characters SAN needs.
Honest numbers, including the ones that miss their targets.
| Metric | Measured | Target | |
|---|---|---|---|
| Legal moves (self-play) | 99.22% over 11,216 moves | >99.5% | close |
| Mean survival before first illegal | 55.6 plies | — | |
| Elo vs Stockfish | ~1131 (±103, 95%) | 1300–1600 | misses |
| Board probe (linear, layer 10) | 85.6% vs 64.5% baseline | >90% | misses |
Stockfish match: 8 wins / 5 draws / 47 losses over 60 games, 17.5% score, plus 24 illegal-move forfeits and 12 adjudicated resignations.
The two misses are the interesting part. 99.22% legality in self-play against 40% of games forfeiting on an illegal move versus a real engine is not a contradiction — it's the finding. Stockfish steers positions out of the training distribution fast, and the model's internal board degrades as it goes. The linear probe agrees: 85.6% per-square accuracy means a real but lossy board representation, well above the 64.5% majority-class baseline yet short of a clean one. Probe train/test are split on whole-game boundaries; splitting on random positions inflates this number badly, since positions one ply apart are nearly identical.
Syntax is solved; state is not. Of the 87 illegal moves in the legality run, 80 (92%) are semantic — well-formed notation describing a move that is illegal in that position, dominated by "piece cannot reach that square from where it is." Only 7 of 11,216 are malformed SAN. So notation is right 99.94% of the time while board-tracking is right 99.29% of the time: the residual error sits almost entirely in the half of the task that requires a board.
The model also stops playing when it is losing, which it was never told to do. Adjudicating all 88 self-terminated games with Stockfish: 55 justified (clearly lost or facing forced mate), 25 genuinely terminal, 15 indefensible. Against a control of random earlier plies from the same games, stop positions are a median −616cp versus −18cp — the end token is conditioned on the position, not on elapsed length.
Greedy decoding (temperature 0) is strictly worse: 54 samples collapsed to ~2 distinct games, surviving 39 plies against 55.6 at temperature 0.7. Every one of those failures was "piece cannot reach that square", with zero notation errors.
import torch
from model import MiniLLM, ModelConfig # from github.com/amanmprojects/llm
import chess_format as cf # from that repo's chess/ directory
ck = torch.load("ckpt.pt", map_location="cpu", weights_only=False)
cfg = ModelConfig(**{k: v for k, v in ck["cfg"].items()
if k in ModelConfig.__dataclass_fields__})
model = MiniLLM(cfg)
model.load_state_dict(ck["model"])
model.eval()
ids = [cf.GAME_START_ID] + cf.tokenize("<20> e4 e5") + [cf.SPACE_ID]
out = model.generate(torch.tensor([ids]), 200, temperature=0.7,
eos_id=cf.GAME_END_ID)
print(cf.decode(out[0].tolist()))
Full code, training pipeline, and evaluation harness: https://github.com/amanmprojects/llm
This checkpoint is inference-only — optimizer state stripped (756 MB → 378 MB).
<15> and <20> identical records (5W-3D-32L, ~1115
Elo) and <24> slightly worse (3W-3D-34L, ~1041), all inside ±126. The tag shifts
style at most. Do not use it as a difficulty setting.Filtered Lichess games (SAN, rating buckets 1500–2499), from the public Lichess database. Weights are MIT-licensed; the underlying games are Lichess users'.