Skip to content

ChessWaveIO/maia2-uci

Repository files navigation

maia2-uci

maia2-uci — a human-like chess engine

License: MIT Rust Inference: ONNX Runtime Device: CPU | CUDA Maia-2 NeurIPS 2024 Version 1.0.1

A standalone UCI chess engine that plays human-like moves using the Maia-2 neural network, written in Rust with ONNX Runtime for inference and optional Syzygy endgame tablebase support.

Maia-2 is a move-prediction model, not a search engine: given a position and the Elo ratings of the two players, it predicts the move a human of that strength would most likely play, plus a win probability. This project wraps the exported model behind a UCI interface so you can load it into any chess GUI (Cutechess, Arena, BanksiaGUI, en-croissant, …) or play against it from the command line.

Maia-2 was created by the CSSLab at the University of Toronto and presented at NeurIPS 2024. Original project: https://github.com/CSSLab/maia2 · Paper: https://arxiv.org/abs/2409.20553. This repository is an independent UCI front-end; it is not affiliated with the Maia authors.

Features

  • Faithful re-implementation of Maia-2's board encoding, move vocabulary, Elo bucketing and black-to-move mirroring in Rust, verified byte-for-byte and numerically against the reference Python package (see Testing).
  • CPU and GPU inference. CPU works out of the box; build with --features cuda for NVIDIA GPUs.
  • Both pretrained models bundled: rapid and blitz.
  • Syzygy tablebases via Fathom (MIT) for perfect endgame play, with an optional "human blend" mode.
  • Analysis output — MultiPV with multi-ply principal variations and centipawn scores (rolled out from the model), like Stockfish/Leela.
  • Permissively licensed throughout — no GPL dependencies (no Stockfish, Lc0 or the original Maia engine code).

Why no search / threads?

Maia-2 models a single human decision; it does not search a game tree, so there is no alpha-beta, no node count and no search-thread parallelism. The one place threads help is inside a single forward pass (the conv/matmul kernels), which is exposed through the standard UCI Threads option and maps to ONNX Runtime's intra-op thread pool.

Building

Requires a Rust toolchain (this repo pins stable via rust-toolchain.toml, because ort needs rustc ≥ 1.88). ONNX Runtime binaries are downloaded automatically by the ort crate at build time.

# CPU (default)
cargo build --release

# NVIDIA CUDA
cargo build --release --features cuda

# NVIDIA TensorRT (implies CUDA)
cargo build --release --features tensorrt

The binary is target/release/maia2-uci.

Packaging a self-contained install bundle

To drop the engine into another app, build it and run the bundler:

cargo build --release --features cuda,embed-weights
./scripts/make-bundle.sh        # -> dist/maia2-uci-gpu/

With embed-weights the model weights are compiled into the binary, and the ONNX Runtime core is statically linked, so the bare binary is fully self-contained — copied on its own it runs the real engine on CPU. The bundle adds the ONNX Runtime CUDA provider libs and GPU math libraries (cuDNN 9 / cuBLAS / nvrtc / cudart / cufft) with $ORIGIN RPATHs baked in, so the GPU works from the bare binary with no wrapper and no environment variables. Point your app's engine path directly at maia2-uci; Device defaults to auto (GPU if available, else CPU).

GPU runtime requirements

The cuda build uses ONNX Runtime 1.24, which needs CUDA 12 and cuDNN 9 on the library path at runtime (plus an NVIDIA driver). If you don't have cuDNN 9 system-wide, the simplest no-root way to get it is the NVIDIA pip wheels, then source the helper that puts them on LD_LIBRARY_PATH:

pip install nvidia-cudnn-cu12 nvidia-cublas-cu12 nvidia-cuda-nvrtc-cu12
. scripts/cuda-env.sh                 # or: PYTHON=/path/to/python . scripts/cuda-env.sh
./target/release/maia2-uci

Set Device to cuda (see options below). The engine reports the device it actually used (info string loaded … on Cuda(0)). If the GPU stack can't be initialised it automatically falls back to CPU and says so — it never silently runs on the wrong device:

info string CUDA unavailable (...); falling back to CPU. The GPU build needs CUDA 12 + cuDNN 9 on the library path.
info string loaded models/maia2-rapid.onnx on Cpu

Usage

./target/release/maia2-uci

Then speak UCI:

uci
setoption name Model value rapid
setoption name SelfElo value 1100
setoption name OppElo value 1900
position startpos moves e2e4 e7e5
go

go returns immediately with a bestmove. Time-control tokens (wtime, movetime, …) are accepted and ignored — there is nothing to search.

UCI options

Option Type Default Meaning
Model combo rapid Which model to use: rapid or blitz.
ModelDir string models Directory containing maia2-<model>.onnx.
Device combo auto auto (GPU if available, else CPU), cpu, or cuda/cuda:N.
Threads spin 1 ONNX Runtime intra-op threads (per forward pass).
SelfElo spin 1500 Elo of the side to move (the player Maia imitates).
OppElo spin 1500 Elo of the opponent.
UCI_Elo spin 1500 Alias for SelfElo (GUI strength control).
MultiPV spin 1 How many candidate lines to report (1–20).
PVDepth spin 8 Plies to roll out for each principal variation (1–20).
SyzygyPath string empty Path(s) to Syzygy .rtbw/.rtbz files (;-separated).
UseSyzygyAtRoot check true Use tablebases at the root when in range.
SyzygyHumanBlend check false See below.

Maia plays the side to move at SelfElo against an OppElo opponent. To make it play like a 1100 against a 1900, set SelfElo 1100 and OppElo 1900. UCI_Elo is an alias for SelfElo for GUIs with a strength slider.

Difficulty is SelfElo: lower plays more like a beginner, higher more like a strong club player. Maia-2 uses discrete Elo buckets, so only these levels are distinct: <1100, 1100–1199, 1200–1299, …, 1900–1999, ≥2000 — e.g. 1500 and 1550 behave identically.

Syzygy behaviour

When SyzygyPath is set and the position has few enough pieces:

  • Default (UseSyzygyAtRoot=true, SyzygyHumanBlend=false) — play the DTZ-optimal tablebase move. Guarantees correct conversion of won/drawn endgames.
  • Human blend (SyzygyHumanBlend=true) — keep Maia-2's most human move, but only among the moves that preserve the game-theoretic result (win/draw/loss). Style is retained; obvious endgame blunders are filtered out. Note this does not guarantee the fastest mate, so very long wins could in principle brush the 50-move rule; prefer the default for guaranteed conversion.
  • UseSyzygyAtRoot=false — ignore tablebases; pure Maia-2.

Analysis: PV lines and scores

Although Maia-2 doesn't search, the engine still emits Stockfish/Leela-style analysis output. Set MultiPV to see several candidate lines; each is a real multi-ply principal variation built by greedily rolling out Maia-2's most-human reply for both sides (PVDepth plies), and each carries a centipawn score from a 1-ply lookahead (play the move, evaluate the reply, take the side-to-move's win probability). Lines stay ordered most-human first, so multipv 1 is the move actually played.

> setoption name MultiPV value 3
> position startpos moves e2e4 e7e5
> go
info depth 8 multipv 1 score cp 60  nodes 8 pv g1f3 b8c6 f1b5 d7d6 d2d4 e5d4 f3d4 c8d7 string move_prob 0.1651 winprob 0.5850
info depth 8 multipv 2 score cp 41  nodes 8 pv d1e2 d8e7 e2d1 e7d8 g1f3 b8c6 f1b5 d7d6 string move_prob 0.1305 winprob 0.5590
info depth 8 multipv 3 score cp 106 nodes 8 pv f1c4 g8f6 d2d3 f8c5 h2h3 e8g8 g1f3 d7d6 string move_prob 0.1300 winprob 0.6476
bestmove g1f3

The score cp is from the side-to-move's perspective; the string fields give the model's probability of the first move and the line's win probability. The PV is Maia-2's human continuation, not a proven best line.

How it works

  1. The position is encoded into Maia-2's [18, 8, 8] tensor (12 piece planes, side to move, 4 castling planes, en-passant plane). When it is Black to move the board is mirrored so the model always sees White to move, exactly as in the reference implementation.
  2. Inputs (boards, elo_self, elo_oppo) are run through the ONNX model.
  3. Illegal-move logits are masked out, the rest are soft-maxed, and the most probable legal move is played (after un-mirroring for Black).
  4. The value head gives a win probability, reported as a UCI centipawn score.

Models / weights

The pretrained weights are pinned in this repository under models/ via Git LFS:

  • models/maia2-rapid.onnx, models/maia2-blitz.onnx — exported ONNX graphs (what the engine runs).
  • models/rapid_model.pt, models/blitz_model.pt — the original PyTorch checkpoints, kept for provenance.
  • models/all_moves.txt, models/elo_dict.json, models/config.yaml — the move vocabulary, Elo buckets and model config.

After cloning, fetch them with:

git lfs install
git lfs pull

The original weights are distributed by the Maia-2 authors: https://github.com/CSSLab/maia2 (rapid/blitz checkpoints on Google Drive, downloaded by model.from_pretrained).

Regenerating the ONNX from upstream

python/export_onnx.py downloads the official checkpoints and re-exports the ONNX graphs; python/gen_fixtures.py regenerates the test fixtures:

python -m venv .venv && . .venv/bin/activate
pip install "torch==2.4.0" maia2 onnx onnxruntime gdown "chess==1.10.0"
python python/export_onnx.py --type both
python python/gen_fixtures.py --type both

Testing

cargo test

The suite checks Rust against the Python reference:

  • Encoding parity — the board tensor, Elo buckets and legal-move vocabulary indices match the reference exactly (no model weights needed).
  • Inference parity — for a battery of positions (including castling, en passant, promotions, Black-to-move mirroring and endgames), the Rust ONNX pipeline reproduces the reference best move, win probability and per-move probability distribution. These are skipped automatically if the ONNX weights are not present.

Golden fixtures are generated from the original PyTorch model by python/gen_fixtures.py and stored in tests/fixtures/.

Licensing

This project is licensed under the MIT License.

Third-party components, all permissively licensed:

  • Maia-2 model and weights — MIT (CSSLab, University of Toronto).
  • Fathom Syzygy probing code (via fathom-syzygy) — MIT.
  • chess move generation — MIT.
  • ort / ONNX Runtime — MIT / Apache-2.0.

No GPL-licensed engine code (Stockfish, Lc0, the original Maia engine) is used.

Citation

If you use Maia-2, please cite the original work:

@inproceedings{tang2024maia2,
  title     = {Maia-2: A Unified Model for Human-AI Alignment in Chess},
  author    = {Tang, Zhenwei and others},
  booktitle = {Advances in Neural Information Processing Systems (NeurIPS)},
  year      = {2024}
}

About

Standalone UCI chess engine running the Maia-2 human-like model via ONNX Runtime (Rust, GPU/CPU, no GPL deps)

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors