Skip to content

Add Late Move Reductions and Principal Variation Search#56

Open
luccabb wants to merge 1 commit intomasterfrom
improve/lmr-pvs
Open

Add Late Move Reductions and Principal Variation Search#56
luccabb wants to merge 1 commit intomasterfrom
improve/lmr-pvs

Conversation

@luccabb
Copy link
Owner

@luccabb luccabb commented Feb 16, 2026

Summary

Two complementary search optimizations that together reduce nodes searched by ~60%:

  • Late Move Reductions (LMR): Quiet moves ordered late in the move list (index >= 3) at sufficient depth (>= 3) are searched with reduced depth using a precomputed log-based table: reduction = floor(log(depth) * log(move_index) / 2). If the reduced search finds a score above alpha, re-search at full depth to verify.
  • Principal Variation Search (PVS): The first move at each node (assumed best from move ordering) is searched with the full alpha-beta window. All subsequent moves use a null window (-alpha-1, -alpha). If a null-window search finds a score that improves alpha, re-search with the full window.

Tactical moves (captures, promotions, checks) and moves made while in check are never reduced by LMR.

Benchmark (depth 4, 48 positions)

Metric Master This PR Change
Nodes 4,760,507 1,936,234 −59.3%
NPS 22,634 22,894 +1.1%
Time 210.32s 84.57s −59.8%

Local Stockfish Benchmark

Settings: 20 games, Stockfish skill 3, 10s/move, no opening book.

W L D Win Rate
Master (baseline) 19 1 0 95%
This PR 18 1 1 93%

Use /run-stockfish-benchmark for CI validation with opening book and longer time control.

Test plan

  • Existing tests pass
  • /run-nps-benchmark for CI validation
  • /run-stockfish-benchmark for strength validation

- LMR: Late quiet moves (index >= 3, depth >= 3) are searched with
  reduced depth using a precomputed log-based reduction table. If the
  reduced search fails high, re-search at full depth.
- PVS: First move searched with full alpha-beta window (PV move).
  All subsequent moves use null window (-alpha-1, -alpha). If the
  null window search fails high, re-search with full window.
- Together these dramatically reduce the search tree by skipping
  full-depth searches on moves unlikely to improve the position.

Benchmark (depth 4, 48 positions):
- Nodes: 4,760,507 → 1,936,234 (−59.3%)
- Time: 210.32s → 84.57s (−59.8%)
@luccabb
Copy link
Owner Author

luccabb commented Feb 16, 2026

/run-nps-benchmark

@github-actions
Copy link

Benchmarks

The following benchmarks are available for this PR:

Command Description
/run-nps-benchmark NPS speed benchmark (depth 5, 48 positions)
/run-stockfish-benchmark Stockfish strength benchmark (300 games)

Post a comment with the command to trigger a benchmark run.

@greptile-apps
Copy link

greptile-apps bot commented Feb 16, 2026

Greptile Summary

This PR adds two standard search optimizations to the alpha-beta engine:

  • Late Move Reductions (LMR): Quiet moves ordered late (index >= 3) at sufficient depth (>= 3) are searched with reduced depth using a precomputed log(depth) * log(move_index) / 2 table. Tactical moves (captures, promotions, checks) and check evasions are correctly excluded from reductions.
  • Principal Variation Search (PVS): The first move uses a full alpha-beta window; all subsequent moves use a null window (-alpha-1, -alpha) and re-search with the full window only on fail-high.

Both techniques are well-established in chess engine development and are implemented correctly following standard patterns. The _LMR_TABLE is precomputed at module load, and the reduction is properly clamped to never drop search depth below 1. The in_check variable is cached to avoid a redundant is_check() call in null move pruning.

  • The implementation correctly excludes tactical moves and check evasions from LMR
  • PVS+LMR re-search ordering follows the standard 3-step pattern (reduced null window → full depth null window → full window)
  • The pre-existing cache key structure (which includes alpha/beta bounds) means null-window and full-window searches produce separate cache entries — functionally correct but limits transposition table reuse
  • Move ordering (organize_moves) shuffles within capture/non-capture categories, which may limit PVS effectiveness compared to a history-heuristic or killer-move ordering

Confidence Score: 4/5

  • This PR is safe to merge — it implements well-known search optimizations correctly with proper safeguards against over-reduction.
  • Score of 4 reflects a clean implementation of standard chess search techniques (LMR + PVS) with correct edge-case handling: tactical moves excluded from reduction, depth clamping prevents degenerate searches, and the 3-step re-search pattern is correctly ordered. Not a 5 because the Stockfish-benchmark validation is still pending and because the random move ordering within categories may limit PVS effectiveness.
  • No files require special attention — the single changed file is well-structured and follows established patterns.

Important Files Changed

Filename Overview
moonfish/engines/alpha_beta.py Adds LMR (precomputed log-based reduction table, quiet-move filtering) and PVS (null-window + re-search pattern) to negamax. Implementation follows standard chess programming patterns. No logical bugs found; caches in_check to avoid redundant call.

Flowchart

flowchart TD
    A[Start Move Loop] --> B{move_index == 0?}
    B -->|Yes| C[Full Window Search\n-beta, -alpha]
    B -->|No| D{Is Quiet & Not In Check\n& depth >= 3 & index >= 3?}
    D -->|Yes| E[Compute LMR Reduction\nfrom precomputed table]
    D -->|No| F[reduction = 0]
    E --> G[PVS Null Window Search\ndepth - 1 - reduction\n-alpha-1, -alpha]
    F --> G
    G --> H{reduction > 0\n& score > alpha?}
    H -->|Yes| I[LMR Re-search\nFull Depth, Null Window\n-alpha-1, -alpha]
    H -->|No| J{score > alpha\n& score < beta?}
    I --> J
    J -->|Yes| K[PVS Re-search\nFull Depth, Full Window\n-beta, -alpha]
    J -->|No| L[Use Current Score]
    K --> L
    C --> L
    L --> M[Update Alpha/Beta\nCheck Cutoffs]
Loading

Last reviewed commit: 2b69491

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, no comments

Edit Code Review Agent Settings | Greptile

@github-actions
Copy link

⚡ NPS Benchmark Results

Metric Value
Depth 5
Positions 48
Total nodes 6466536
Total time 694.00s
Nodes/second 9317

Node count is the primary signal — it's deterministic and catches search behavior changes. If the node count changes, the PR changed search behavior. NPS is informational only (CI runner performance varies).

Per-position breakdown
Position  1/48: nodes=56273      time=5.17s  nps=10881
Position  2/48: nodes=117974     time=13.82s  nps=8537
Position  3/48: nodes=7292       time=0.49s  nps=14885
Position  4/48: nodes=302586     time=30.96s  nps=9772
Position  5/48: nodes=47921      time=5.29s  nps=9066
Position  6/48: nodes=93138      time=10.33s  nps=9012
Position  7/48: nodes=158367     time=19.55s  nps=8099
Position  8/48: nodes=157501     time=15.94s  nps=9881
Position  9/48: nodes=226815     time=26.33s  nps=8613
Position 10/48: nodes=186975     time=23.02s  nps=8122
Position 11/48: nodes=232702     time=25.71s  nps=9052
Position 12/48: nodes=224552     time=27.24s  nps=8244
Position 13/48: nodes=207123     time=24.47s  nps=8464
Position 14/48: nodes=144856     time=16.28s  nps=8899
Position 15/48: nodes=217735     time=21.50s  nps=10125
Position 16/48: nodes=117246     time=11.64s  nps=10073
Position 17/48: nodes=15692      time=1.11s  nps=14151
Position 18/48: nodes=6133       time=0.28s  nps=22172
Position 19/48: nodes=17998      time=1.18s  nps=15220
Position 20/48: nodes=55911      time=3.27s  nps=17119
Position 21/48: nodes=5503       time=0.34s  nps=16414
Position 22/48: nodes=611        time=0.03s  nps=23458
Position 23/48: nodes=3678       time=0.18s  nps=20557
Position 24/48: nodes=28737      time=1.94s  nps=14836
Position 25/48: nodes=12378      time=0.68s  nps=18098
Position 26/48: nodes=21206      time=1.41s  nps=15068
Position 27/48: nodes=38763      time=2.78s  nps=13922
Position 28/48: nodes=111289     time=9.38s  nps=11870
Position 29/48: nodes=93744      time=8.04s  nps=11653
Position 30/48: nodes=1120       time=0.05s  nps=20418
Position 31/48: nodes=427746     time=39.81s  nps=10745
Position 32/48: nodes=337243     time=36.79s  nps=9167
Position 33/48: nodes=482598     time=69.82s  nps=6911
Position 34/48: nodes=253896     time=31.53s  nps=8051
Position 35/48: nodes=223932     time=20.27s  nps=11049
Position 36/48: nodes=916561     time=102.19s  nps=8968
Position 37/48: nodes=604888     time=62.22s  nps=9722
Position 38/48: nodes=5992       time=0.22s  nps=26882
Position 39/48: nodes=7253       time=0.26s  nps=28141
Position 40/48: nodes=3264       time=0.06s  nps=54888
Position 41/48: nodes=46926      time=2.67s  nps=17547
Position 42/48: nodes=27548      time=1.80s  nps=15336
Position 43/48: nodes=14768      time=1.30s  nps=11357
Position 44/48: nodes=55960      time=3.94s  nps=14216
Position 45/48: nodes=31609      time=2.13s  nps=14815
Position 46/48: nodes=114533     time=10.60s  nps=10807
Position 47/48: nodes=0          time=0.00s  nps=0  (terminal)
Position 48/48: nodes=0          time=0.00s  nps=0  (terminal)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant