CoolFace
Datasetpublic

cetusian/chess-sft-mix-200k

Chess SFT Mix — GM games + puzzles + Stockfish A ~400,000-row supervised fine-tuning dataset assembled for competitive chess LLM training. Unified chat format, single system prompt, three complementary sources of signal. This is the follow-up to cetusian/chess-sft-lichess-2200, which was pure GM behaviour cloning. This dataset adds tactical puzzles and engine-ground-truth labels on top of it, because BC alone caps at the teacher's (fallible) move quality. Summary… See the full description on the dataset page: https://huggingface.co/datasets/cetusian/chess-sft-mix-200k.

sourceHugging Facecc0-1.0updated 5mo agoView on Hugging Face
0likes38downloads
Dataset Card

Chess SFT Mix — GM games + puzzles + Stockfish

A ~400,000-row supervised fine-tuning dataset assembled for competitive chess LLM training. Unified chat format, single system prompt, three complementary sources of signal.

This is the follow-up to `cetusian/chess-sft-lichess-2200`, which was pure GM behaviour cloning. This dataset adds tactical puzzles and engine-ground-truth labels on top of it, because BC alone caps at the teacher's (fallible) move quality.

Summary

ComponentRowsWhat it teaches
A. GM games (≥2200 Elo)199,999Opening theory, middlegame plans, sequence fluency, style
B. Lichess puzzles (rating-stratified)150,000"Only move" correctness, forcing sequences, tactical sharpness
C. Stockfish-enriched positions (depth 15)50,000Per-move engine-ground-truth in natural game context
Total399,999(391,999 train / 8,000 validation)

All three components share a single chat format and a single system prompt, so the model learns one task: given a chess position, play the best move in SAN. The input shape tells the model whether the position came from a game (PGN) or a puzzle (FEN).

Format

Every row is an OpenAI-style messages list with exactly three messages:

json
{
  "messages": [
    {"role": "system",    "content": "You are a chess grandmaster. Given a chess position (as a PGN move sequence or a FEN), play the best move in standard algebraic notation (SAN)."},
    {"role": "user",      "content": "1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4"},
    {"role": "assistant", "content": "Nf6"}
  ]
}

User message shapes:

  • —PGN form (components A and C) — the move history as a standard PGN prefix. Example: "1. e4 e5 2. Nf3 Nc6 3. Bb5".
  • —FEN form (component B) — "FEN: <fen>". Example: "FEN: r1bqk2r/pp1nbNp1/2p1p2p/...".

Assistant message: always a single move in SAN (e.g. "Nf6", "O-O", "e8=Q", "Qe6+", "Qxh7#").

This shape plugs directly into any modern SFT runner that accepts messages / conversation format — including TRL's SFTTrainer (with assistant_only_loss=True) and Surogate's type: conversation.

Component details

A. GM games (199,999 rows)

Imported verbatim from `cetusian/chess-sft-lichess-2200` — Lichess games with both players ≥ 2200 Elo, Termination == "Normal", ≥ 20 ply, FEN-deduplicated, opening-capped, colour-balanced. One random-ply position per game. The output is the move the human GM actually played.

Label quality (from the upstream card's Stockfish probe, depth 18, 2k positions): 41.1% top-1 agreement, 68.3% top-3, median 13cp loss, 7.5% blunder rate.

B. Lichess puzzles (150,000 rows)

Sampled from `Lichess/chess-puzzles` (5.8M puzzles, CC0) with uniform stratification across five rating buckets:

Rating bucketRows
800 – 1,19930,000
1,200 – 1,59930,000
1,600 – 1,99930,000
2,000 – 2,39930,000
2,400 – 3,32030,000

For each puzzle we:

  1. 1.Start from the given FEN (position before the opponent's setup move).
  2. 2.Apply the first UCI move of Moves — the opponent's move that sets up the tactic.
  3. 3.Convert the second UCI move — the player's first correct answer — to SAN.
  4. 4.Emit {user: "FEN: <position-after-setup>", assistant: "<SAN-answer>"}.

FENs are deduplicated across puzzles. Puzzles are where most of the dataset's # (mate) labels come from — GM games almost never record the final mating move.

C. Stockfish-enriched GM positions (50,000 rows)

Fifty thousand positions resampled from component A's PGN prefixes, analysed with Stockfish 16 at depth 15 on 64 parallel workers. The assistant output for these rows is Stockfish's preferred move, not the GM's. When SF agrees with the GM these rows reinforce; when it disagrees (≈59% of the time per the probe in A) the model sees a contrasting label for a familiar-looking position and learns the engine's preference.

Generation pipeline: data/prepare_mixed_sft.py (and companion data/stockfish_probe.py) in the source repository.

Statistics

Size & composition

SplitRowsPGN inputs (A+C)FEN inputs (B)
train391,999244,894147,105
validation8,000~4,994~3,006

Output shape

PropertyCount
Castles (O-O / O-O-O)6,918
Checks (SAN ending +)77,552
Mates (SAN ending `#`)11,233
Promotions (SAN containing =)923
Unique SAN answers3,560

The tactical footprint is much heavier than in component A alone — 7× checks and ~11k mates (vs zero), courtesy of the puzzle mix.

User-message length (characters)

medianmeanp95max
all rows881674971,744

FEN inputs are short and fixed-ish (~70 chars); PGN prefixes are longer. The long tail still comes from deep endgame PGNs in the GM component.

Usage

Load

python
from datasets import load_dataset
ds = load_dataset("cetusian/chess-sft-mix-200k")
# DatasetDict({
#   train: Dataset(features=['messages'], num_rows=391999),
#   validation: Dataset(features=['messages'], num_rows=8000)
# })

Train (TRL SFTTrainer)

python
from transformers import AutoTokenizer, AutoModelForCausalLM
from trl import SFTTrainer, SFTConfig

MODEL = "Qwen/Qwen3-1.7B"    # or Qwen3-8B, Qwen3.5, SmolLM2, etc.
tok = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype="auto")

trainer = SFTTrainer(
    model=model, tokenizer=tok,
    train_dataset=ds["train"], eval_dataset=ds["validation"],
    args=SFTConfig(
        output_dir="runs/chess-sft-mix",
        max_seq_length=1024,            # covers >99% of rows
        per_device_train_batch_size=16,
        gradient_accumulation_steps=2,
        num_train_epochs=1,             # 392k rows is ample
        learning_rate=2e-5,
        assistant_only_loss=True,       # CRITICAL: supervise only the 2–8 move tokens
        packing=False,                  # keep position boundaries
        bf16=True, warmup_ratio=0.03,
    ),
)
trainer.train()

Train (Surogate)

yaml
datasets:
  - path: cetusian/chess-sft-mix-200k
    split: train
    type: conversation
    messages_field: messages

validation_datasets:
  - path: cetusian/chess-sft-mix-200k
    split: validation
    type: conversation
    messages_field: messages

Design notes

  • —Why one system prompt, not four per task? The user-message shape (PGN vs FEN-tagged) already tells the model which mode it's in. Multiple prompts would just over-specialise per task and force extra mode-routing for no benefit.
  • —Why 150k puzzles out of 5.8M available? Uniform 30k-per-bucket sampling gives the model tactics at every skill level without letting the huge beginner/intermediate bucket drown positional play signal.
  • —Why only 50k Stockfish rows? Depth-15 engine labels are the most expensive component. 50k is the sweet spot where correctness signal is visible in loss curves without blowing past a few minutes of 64-core compute.
  • —Why no tablebase? Syzygy 6-piece is ~150 GB on disk; marginal signal versus the Stockfish component on the same positions. Easy to add later.

Limitations

  • —Mixed-source label contradictions. Some positions appear in both A (with GM label) and C (with SF label). When the two disagree the model sees both moves as correct — acceptable for a behaviour-cloning base but something to be aware of.
  • —Puzzles drop opening context. Component B uses FEN: only, so the model does not see how the puzzle was reached. This is intentional — puzzles are about the immediate tactic — but means the FEN and PGN modes train somewhat separately.
  • —Single-move-per-row puzzles. Only the player's first move in each puzzle is emitted. Subsequent forced moves are dropped to keep row count bounded and to avoid upweighting long puzzles.
  • —Ceiling is still engine-level, not world-championship. Stockfish at depth 15 is very strong but not tablebase-perfect. The Stockfish component's labels inherit this cap.

Source & license

This derivative dataset is released under CC0 1.0 Universal.

Citation

bibtex
@misc{lichess-database,
  title  = {Lichess Open Database},
  author = {{Lichess}},
  year   = {2024},
  url    = {https://database.lichess.org/},
  note   = {CC0 1.0 Universal}
}
@misc{stockfish,
  title  = {Stockfish: A strong open source chess engine},
  author = {The Stockfish developers},
  url    = {https://stockfishchess.org/},
  note   = {GPLv3}
}