← Back
Home › Systems › Games

neural-chess-engine

Picks chess moves by running a tree search in which a neural network judges every position, giving move policy and position value.

Leela Chess Zero

Open the neural-chess-engine pageneural-chess-engine Batched MCTS inferencegame-ai: Feed an accelerator from tree search driven by a learned evaluator: virtual loss gathers leaves within a tree, parallel searches fill batches, an async queue overlaps CPU and GPU.›Legal move generationgame-ai: Lists every legal move from bitboard attack tables, drops those that leave the own king in check, and is proven correct by perft node counts.›Monte Carlo tree searchgame-ai: Grows a search tree one simulation at a time, picking visits by a bandit rule (UCT or PUCT) and backing up averages; the search behind AlphaZero.›AlphaZero self-playmachine-learning: A policy-value net guides PUCT search; search visit counts and game results become its training targets, so it learns from its own improved play.›Transformer policy-valuenetworkmachine-learning: One encoder-only transformer reads the board as 64 square tokens and outputs move probabilities and a win/draw/loss estimate for a tree search.›Neural network trainingml-training: The end-to-end training loop, from batch, forward, loss, backward, clip, AdamW with warmup-cosine schedule, mixed precision, checkpoints, evaluation and data-parallel scaling.›MPMC bounded queueconcurrency: Vyukov's array queue for many producers and consumers: each slot carries a sequence number, so one CAS per operation claims a slot without locks.›Dynamic batchingml-training: Queue independent requests and dispatch them as one batch when it is full or the oldest has waited max_delay; bounded extra latency buys throughput.›Bitboardsgame-ai: One 64-bit word per set of up to 64 items, so set algebra becomes shifts, ANDs and table lookups; magic bitboards give ray queries in one multiply.›Zobrist hashinggame-ai: A state's hash is the XOR of one random 64-bit key per (item, slot) fact that holds in it, updated in a few XORs per change.›Transposition tablegame-ai: A fixed-size, lossy hash table of search results keyed by a state hash; gives ordering and cutoffs for repeated states, shared lock-free between threads.›Multi-armed banditsmachine-learning: Choose among K options repeatedly while learning their payoffs: epsilon-greedy, UCB1 and Thompson sampling keep total regret low where a fixed split or a fixed exploration rate pays forever.›PRNGs (PCG, xoshiro)numerics: Small, fast, statistically strong non-cryptographic generators: PCG32 and xoshiro256**, with seeding, parallel streams and unbiased ranges.›Experience replaymachine-learning: A fixed-size ring buffer of past experience, sampled uniformly or by priority, so an online learner trains on decorrelated mini-batches and reuses each item.›Symmetry-group augmentationmachine-learning: Transform each input and its target together with a symmetry group to multiply training data, or average predictions over the group at test time.›Attention variantsmachine-learning: Multi-head, multi-query, grouped-query, sliding-window and latent attention; same softmax(QKᵀ/√d)V core, different trade-offs between quality and KV-cache size.›Embeddingsmachine-learning: A learned table mapping discrete ids to dense vectors: gather forward, sparse scatter-add backward, row-wise optimizers to keep huge tables cheap.›Rotary position embedding(RoPE)machine-learning: Encode position by rotating pairs of query and key dimensions by position-dependent angles, so attention scores depend only on relative distance; the LLM default.›+ 9 moreFalse sharingconcurrency: Independent variables written by different cores on one cache line force the line to bounce between cores; pad or regroup hot data to fix it.›Backpressuresystems: Bounded queues and credit-based flow so slow consumers throttle producersHierarchical timing wheelsystems: Timers bucketed by expiry into wheels of slots, coarser wheels for far deadlines: O(1) insert and cancel for millions of timeouts.›Memory orderingconcurrency: What Relaxed, Acquire, Release and SeqCst atomics actually guarantee, what they cost on x86 and ARM, and which one each pattern needs.›Bayesian rate estimationstatistics: Estimate a conversion or event rate from few observations with a Beta-Binomial or Gamma-Poisson update: two counters, an honest interval, and P(B beats A) directly.›Reverse-mode automaticdifferentiationml-training: Record the forward computation on a tape, then sweep it backwards once to get the gradient of a scalar loss for all inputs.›+ 9 moreCPU caches and the memoryhierarchyhardware-os: Sizes, line widths and latencies of L1/L2/L3/DRAM on modern x86 and Apple M-series, and the data-layout rules that follow from them.›Slab allocatorsystems: Pre-carve pages into equal-size slots per size class with an in-place free list: O(1) alloc and free, no external fragmentation, cache-warm reuse.›+ 4 more

How it is built

Architecture overview

A neural chess engine picks a move by running a tree search whose every leaf is judged by one neural network. The network reads a position and returns two things: a policy (how promising each legal move looks) and a value (who is winning). The search spends its visits where the policy points and backs the values up the tree; the most visited root move is played. The same engine, run against itself with exploration noise, produces the training data for the next network, so playing strength comes from a closed loop of search and learning, not from hand-written chess knowledge. Leela Chess Zero (Lc0) is the open reference; it follows the AlphaZero recipe and has moved from convolutional to transformer networks.

Parts:

            ┌──────────────────────── play (UCI) ────────────────────────┐
 position ─►│ move gen ─► PUCT search ──leaves──► batch queue ─► network │─► best move
            │      ▲          ▲   ◄──policy, value──┘   (GPU)           │
            │      └─ TT ─────┘                                          │
            └────────────────────────────────────────────────────────────┘
                                   ▲ new weights
 self-play workers ──games + visit counts──► object store ──► data loader ──► trainer
 (same search + net, noise on)                                (shuffle, augment)  (GPU cluster)

There are two products of one design: the play engine (one process, latency per move matters) and the training loop (many processes, throughput of games and gradient steps matters). Both share the move generator, the search and the network code.

How the parts fit together

Move generator

Legal move generation gives the search the legal moves of a position, plays and takes back moves, and scores game ends exactly (mate, stalemate, draws by rule). The board is a set of 64-bit masks (Bitboards), so moves come from shifts and precomputed attack tables, and the position key is updated with a few XORs per move (Zobrist hashing). Unlike an alpha-beta engine, move generation is not the bottleneck: a neural engine visits far fewer nodes, because each node costs a network evaluation. What matters is that the generator is exact, checked by perft node counts against a known engine, and that the network's encoder can read the board cheaply.

The search is Monte Carlo tree search with the PUCT selection rule from AlphaZero: at each node pick the child that maximises its mean value Q plus an exploration bonus proportional to the network's prior P for that move and to √(parent visits)/(1 + child visits). This is the bandit idea of Multi-armed bandits (UCB) with the network's policy as a prior, so good moves get searched early and bad moves are almost never expanded. There are no random rollouts: the value head replaces them.

Contracts with its neighbours:

  • To the network: a leaf is expanded once; its policy is stored as priors on its children and its value is backed up along the path with the sign flipped at each ply.
  • Unvisited children: need a first-play urgency (FPU) value; Lc0 exposes it as FpuValue (source: lc0 src/search/classic/params.cc). Too optimistic and the search wastes visits on bad moves, too pessimistic and it never tries a good move with a low prior.
  • Terminal positions (mate, stalemate, draw by rule) are scored exactly by the move generator, not by the net.
  • Transpositions: chess reaches the same position by different move orders. A cache keyed by the Zobrist hash (Transposition table) avoids evaluating the same position twice; Lc0 keeps an NN cache of network outputs for this.

Policy-value network

The Transformer policy-value network judges every leaf. Its encoder is the contract with the move generator. Lc0 feeds 112 input planes of 8×8: 13 planes per position (pieces of each type and colour, plus repetition) for the current and the last 7 positions, plus 8 auxiliary planes (castling rights, side to move, the fifty-move counter and constants) (source: lc0 src/neural/encoder.h, network.h). History is part of the input because repetitions and the fifty-move rule change the result of a position. The board is flipped so the side to move is always "us"; the network never needs to learn both colours separately.

The body is a stack of Transformer blocks over 64 tokens, one per square. Each square's input planes are projected into a vector (Embeddings); attention lets every square look at every other square, which matches chess well: a bishop on a1 and a king on h8 interact in one layer, where a 3×3 convolution needs many layers to connect them. Older Lc0 and AlphaZero networks use residual Convolutional networks; they are still a valid, cheaper choice for small nets.

Heads on top of the shared body:

  • Policy head: one logit per possible move. Lc0's transformer nets compute it with attention between the from-square and to-square tokens, which suits the from-to structure of chess moves. Illegal moves are masked before the softmax.
  • Value head: win/draw/loss probabilities (WDL) rather than one scalar, so the engine knows a 0.0 that is "certain draw" from a 0.0 that is "sharp, either side can win".
  • Moves-left head (Lc0): predicts how long the game will last, used to prefer faster wins and slower losses.

Square positions enter through learned position embeddings or relative attention biases; the 64 squares are a fixed grid, so the rotary trick of Rotary position embedding is optional here. See Attention variants for the head layouts. For inference the attention runs as Flash attention, and fp16 or int8 weights (Weight quantization) cut memory traffic.

Inference batching

A single search asks for one leaf at a time, but a GPU is efficient only on batches of hundreds. This is the main engineering problem of the play engine, solved by Batched MCTS inference:

  • Virtual loss: while a leaf waits for its evaluation, its path is temporarily scored as a loss, so the next selection descends a different path; one search collects a minibatch of distinct leaves (Lc0: MinibatchSize).
  • Many searches per GPU in self-play: hundreds of games run at once and their leaves share one batch queue (Dynamic batching): dispatch when the batch is full or the oldest request has waited too long.
  • Backends: Lc0 ships CUDA/cuDNN, ROCm, Metal, ONNX runtime and BLAS backends behind one interface (source: lc0 README), so the search code never knows the hardware.

The trade-off is explicit: bigger batches mean more throughput but more virtual-loss distortion inside one tree, so the play engine uses small batches per search and the self-play workers use many parallel games instead.

Self-play

AlphaZero self-play closes the loop. A worker plays the current best network against itself with the same search, but with exploration switched on:

  • Dirichlet noise added to the root priors (AlphaZero used α = 0.3 for chess, mixed in at 25 %; Silver et al. 2018), so the engine keeps trying moves the net currently dislikes.
  • Moves sampled in proportion to visit counts early in the game, greedy later, so the games are varied but the endings are played well.
  • A fixed, small number of visits per move (AlphaZero: 800 simulations per move), much less than in a real game: the goal is many decent games, not a few perfect ones.

Each position is written with its search policy (the root visit distribution) and, once the game ends, the result. These are the training targets: the policy head learns to predict what search found, the value head learns the outcome. Because search is stronger than the raw network, the network improves toward search, and the better network makes the next search stronger.

The positions go into a replay buffer (Experience replay): a sliding window of the most recent games, because old games were played by weaker networks. The board's symmetries can multiply the data (Symmetry-group augmentation), but chess has only the left-right mirror, and only when castling rights are gone; AlphaZero did not augment chess data (Silver et al. 2018).

Training

Neural network training turns the window into the next network. Data loading pipelines feed it: sharded game files in object storage, shuffled across many games so consecutive samples are not from the same game, decoded on CPU workers, prefetched to the GPU.

The trainer minimises policy cross-entropy plus value loss plus weight decay, with AdamW or SGD with momentum and a warmup + cosine schedule, in mixed precision. Large nets train data-parallel across GPUs with gradients summed by Ring all-reduce and, when the model plus optimizer state no longer fits one GPU, ZeRO / FSDP sharding. New weights are published to the self-play workers on a fixed cadence; Lc0 runs this as a distributed project where volunteers' clients download the latest network and upload games.

Data and control flow

One move in a game (play engine):

  1. The GUI sends the position and the clock over UCI; the engine sets the move history.
  2. Time management decides the budget for this move from the clock and the increment.
  3. The search loop runs: select a path by PUCT with virtual loss → reach a leaf → check the NN cache → queue the leaf. When a minibatch is full, the backend runs the network once for all queued leaves.
  4. Each result expands its leaf (priors on children) and backs up its value; virtual losses are removed.
  5. Repeat until the budget is spent or the best move can no longer be overtaken by the remaining visits (Lc0 calls this smart pruning). Most of the time goes to network evaluation on the GPU; the CPU side must keep the next batch ready.
  6. Play the most visited root move; keep the subtree under it for the next move (tree reuse).

One training cycle (training loop):

  1. Workers download network N and play games with noise and a fixed visit count.
  2. Each game is uploaded as positions + search policy + result.
  3. The data loader samples from the window of the latest games, shuffles and batches.
  4. The trainer runs a number of gradient steps and exports network N+1.
  5. Optionally a gating match: N+1 must beat N to be published (AlphaGo Zero did this; AlphaZero dropped it and always used the latest network; Silver et al. 2018).

Scaling

Limits, in the order they bite, and the lever for each:

  1. Play strength per move is bound by network evaluations per second. Levers: a smaller or quantized net, fp16 kernels, bigger GPU, better batching; then a better net (more search does not fix a weak evaluator).
  2. Self-play throughput is bound by GPU time per game. Levers: many games per GPU to fill batches, fewer visits per move, a smaller net for self-play, more workers (it is embarrassingly parallel).
  3. Training throughput is bound by data loading as often as by compute: keep the GPU fed (prefetch, many decode workers), then add GPUs data-parallel, then shard optimizer state.
  4. Learning quality is bound by data freshness and diversity: window size, noise level, and how fast new nets reach the workers. The original AlphaZero run used about 5,000 first-generation TPUs for self-play and 64 second-generation TPUs for training (Silver et al. 2018): self-play, not gradient steps, dominates the compute.

Failure modes

FailureSymptomCauseFix
Starved GPUlow GPU utilisation, few nodes per secondbatches too small, CPU search or encoder too slowlarger minibatch, more parallel games, profile the CPU side
Virtual-loss distortionweaker play at high batch sizesmany leaves per tree push the search off the best linesmaller batch per search, more searches per GPU
Value sign bugengine plays the worst moves, or strong only for one colourvalue not flipped per ply, or the board not flipped for the side to movetest with mirrored positions; sign flip in one place
Repetition and fifty-move blindnesswalks into draws when winning, misses perpetual checkshistory planes or the fifty-move plane not filled, or the NN cache ignoring historyencode history; key the cache on what the net sees
Overfitting to recent gamesstrength oscillates between netswindow too small, too little noise, games too similarlarger window, root noise, opening book for self-play
Policy collapsethe net stops considering moves search would findpriors near zero, FPU too pessimisticroot noise, check FPU, temperature in self-play
Train/play mismatchnet is strong in training metrics, weak in gamesdifferent encoding or input format between trainer and engineone encoder spec, a golden-file test on both sides

Real implementations

namewhat it isrepolicencenotes
Leela Chess Zero (lc0)the engine: PUCT search, multiple GPU/CPU backendsLeelaChessZero/lc0GPL-3.0open reference for this whole design
lczero-trainingthe training code for Lc0 networksLeelaChessZero/lczero-trainingsee repotrainer and data pipeline
Stockfishthe alternative design: alpha-beta search with a small CPU network (NNUE)official-stockfish/StockfishGPL-3.0see Alpha-beta search, NNUE

The alternative: an alpha-beta engine with an efficiently updatable CPU network (NNUE, Alpha-beta search, Move ordering and pruning) searches millions of nodes per second with a much weaker evaluator per node, and needs no GPU. Pick the neural MCTS design when a GPU is available and positional judgement matters most; pick alpha-beta + NNUE for CPU-only targets and very short time controls.

Sources

  • D. Silver et al., A general reinforcement learning algorithm that masters chess, shogi, and Go through self-play, Science 2018 (preprint: Mastering Chess and Shogi by Self-Play with a General Reinforcement Learning Algorithm). paper
  • A. Ruoss et al., Amortized Planning with Large-Scale Transformers: A Case Study on Chess, 2024: a transformer trained on engine-labelled positions, playing without search. paper
  • Leela Chess Zero: lczero.org, lc0 source (encoder src/neural/encoder.h, search parameters src/search/classic/params.cc, backends in README).

Similar systems

Share the most building blocks with this one.