Skip to content

Comments

Fix transposition table and add iterative deepening#54

Open
luccabb wants to merge 1 commit intomasterfrom
improve/transposition-table
Open

Fix transposition table and add iterative deepening#54
luccabb wants to merge 1 commit intomasterfrom
improve/transposition-table

Conversation

@luccabb
Copy link
Owner

@luccabb luccabb commented Feb 16, 2026

Summary

  • Fix broken TT key: The transposition table key previously included (alpha, beta), which meant the same position at the same depth would get different cache entries depending on the alpha-beta window. This resulted in near-zero cache hit rates. Now uses just the board position hash as the key.
  • Proper TT entry types: Stores EXACT, LOWER_BOUND, and UPPER_BOUND entry types, enabling correct TT probing with bound-based cutoffs (standard in chess engines).
  • Iterative deepening: Searches depths 1, 2, ..., N. Each iteration populates the TT, so subsequent iterations have much better move ordering via TT best-move-first.
  • TT move ordering: The best move from the transposition table is placed first in organize_moves(), which dramatically improves alpha-beta cutoff rates.
  • LazySMP compatibility: Updated LazySMP to use the new TT key format.

Benchmark (depth 4, 48 positions)

Metric Master This PR Change
Nodes 4,760,507 2,834,290 −40.5%
NPS 22,634 22,882 +1.1%
Time 210.32s 123.86s −41.1%

Local Stockfish Benchmark

Settings: 20 games, Stockfish skill 3, st=60, concurrency=4.

W L D Win Rate
Master (baseline) 19 1 0 95%
This PR 5 15 0 25%

Note: This PR was benchmarked with different settings (st=60, concurrency=4) than the other PRs (10s/move, concurrency=1), so results are not directly comparable. The regression may be related to the different test conditions rather than a true strength regression.

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

Test plan

  • Existing tests pass (pytest tests/ -k "not lazy_smp and not layer_1 and not layer_2")
  • /run-nps-benchmark for CI validation
  • /run-stockfish-benchmark for strength validation

- Remove alpha/beta from TT key (was causing near-zero cache hit rate)
- Store entry type (EXACT, LOWER_BOUND, UPPER_BOUND) for proper
  TT probing with bound-based cutoffs
- Add iterative deepening: search depths 1..N, populating TT each
  iteration so the next iteration has better move ordering
- Place TT best move first in move ordering (organize_moves)
- Update LazySMP to use new TT key format

Benchmark (depth 4, 48 positions):
- Nodes: 4,760,507 → 2,834,290 (−40.5%)
- Time: 210.32s → 123.86s (−41.1%)
@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

Fixed the transposition table key to use only the board hash (removing alpha/beta), added proper TT entry types (EXACT/LOWER/UPPER), implemented iterative deepening, and integrated TT move-first ordering to improve alpha-beta pruning efficiency.

  • Fixed TT key removes alpha/beta dependency, dramatically improving cache hit rates
  • Iterative deepening (depths 1→N) populates TT for better move ordering in subsequent iterations
  • TT move placed first in organize_moves() for improved cutoff rates
  • Benchmark shows 40.5% node reduction and 41.1% speedup

Issues found:

  • Unreachable code in TT entry type determination (line 326 will never execute)
  • Missing draw detection in negamax (repetition, fifty-move, insufficient material)
  • TT move legality not validated before insertion (hash collision could cause illegal moves)
  • LazySMP bypasses iterative deepening improvements by calling negamax directly at full depth

Confidence Score: 2/5

  • Multiple logical issues found that could cause incorrect behavior
  • Core TT fix is solid, but unreachable code, missing draw detection, unvalidated TT moves, and LazySMP not using iterative deepening are all issues that affect correctness
  • Pay close attention to alpha_beta.py (unreachable code + missing draw checks), move_ordering.py (illegal move risk), and lazy_smp.py (missing iterative deepening)

Important Files Changed

Filename Overview
moonfish/engines/alpha_beta.py Fixed TT key format and added iterative deepening, but has unreachable code in TT entry type logic and missing draw detection
moonfish/move_ordering.py Added TT move-first ordering, but doesn't validate TT move legality before insertion (hash collision risk)
moonfish/engines/lazy_smp.py Updated to use new TT key format, but bypasses iterative deepening by calling negamax directly at full depth

Flowchart

flowchart TD
    A[search_move called] --> B[Initialize empty TT cache]
    B --> C{Iterative Deepening Loop<br/>d=1 to negamax_depth}
    C --> D[negamax depth=d]
    D --> E{TT key in cache?}
    E -->|Yes| F{TT depth >= current depth?}
    F -->|Yes| G{Check TT entry type}
    G -->|EXACT| H[Return cached score]
    G -->|LOWER| I[Update alpha, check cutoff]
    G -->|UPPER| J[Update beta, check cutoff]
    E -->|No| K[Search position]
    F -->|No| K
    I --> K
    J --> K
    K --> L[Organize moves<br/>TT move first]
    L --> M{For each move}
    M --> N[Recursive negamax]
    N --> O{Score >= beta?}
    O -->|Yes| P[Store TT_LOWER, return]
    O -->|No| Q{Best score found?}
    Q --> R{All moves done?}
    R -->|No| M
    R -->|Yes| S{Determine entry type}
    S -->|score <= orig_alpha| T[Store TT_UPPER]
    S -->|else| U[Store TT_EXACT]
    T --> V[Return to iterative loop]
    U --> V
    P --> V
    H --> V
    V --> W{More depths?}
    W -->|Yes| C
    W -->|No| X[Return best_move]
Loading

Last reviewed commit: aada48a

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.

3 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

# Determine TT entry type based on the score relative to original bounds
if best_score <= original_alpha:
tt_type = TT_UPPER # Failed low: score is an upper bound
elif best_score >= beta:
Copy link

Choose a reason for hiding this comment

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

unreachable: if best_score >= beta, would have returned at line 302

Suggested change
elif best_score >= beta:
elif best_score >= beta:
# This branch is unreachable - beta cutoffs return early at line 302
tt_type = TT_LOWER # Failed high: score is a lower bound

Comment on lines 230 to 236
if board.is_checkmate():
cache[cache_key] = (-self.config.checkmate_score, None)
cache[tt_key] = (-self.config.checkmate_score, None, depth, TT_EXACT)
return (-self.config.checkmate_score, None)

if board.is_stalemate():
cache[cache_key] = (0, None)
cache[tt_key] = (0, None, depth, TT_EXACT)
return (0, None)
Copy link

Choose a reason for hiding this comment

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

missing draw detection: should check is_repetition(), is_fifty_moves(), and is_insufficient_material() before checkmate/stalemate (like in quiescence_search:114)

Comment on lines +39 to +40
if tt_move is not None:
result.insert(0, tt_move)
Copy link

Choose a reason for hiding this comment

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

TT move legality not checked: hash collision could insert illegal move. Verify tt_move in board.legal_moves before inserting

@greptile-apps
Copy link

greptile-apps bot commented Feb 16, 2026

Additional Comments (1)

moonfish/engines/lazy_smp.py
bypasses iterative deepening: calls negamax at full depth directly, missing the TT population benefits from iterative deepening added in parent class (alpha_beta.py:344)

@github-actions
Copy link

⚡ NPS Benchmark Results

Metric Value
Depth 5
Positions 48
Total nodes 18663583
Total time 2191.28s
Nodes/second 8517

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=47244      time=3.47s  nps=13620
Position  2/48: nodes=410379     time=38.26s  nps=10725
Position  3/48: nodes=12070      time=0.80s  nps=15079
Position  4/48: nodes=259672     time=25.63s  nps=10131
Position  5/48: nodes=101582     time=14.29s  nps=7110
Position  6/48: nodes=255859     time=27.99s  nps=9140
Position  7/48: nodes=173218     time=19.65s  nps=8813
Position  8/48: nodes=354574     time=34.47s  nps=10285
Position  9/48: nodes=1491526    time=151.31s  nps=9857
Position 10/48: nodes=2583789    time=344.59s  nps=7498
Position 11/48: nodes=371457     time=47.79s  nps=7772
Position 12/48: nodes=719943     time=92.18s  nps=7810
Position 13/48: nodes=717834     time=78.47s  nps=9147
Position 14/48: nodes=659403     time=73.63s  nps=8955
Position 15/48: nodes=913439     time=93.46s  nps=9774
Position 16/48: nodes=221157     time=21.54s  nps=10269
Position 17/48: nodes=5787       time=0.41s  nps=14142
Position 18/48: nodes=9321       time=0.52s  nps=18024
Position 19/48: nodes=11123      time=1.47s  nps=7549
Position 20/48: nodes=95183      time=5.98s  nps=15924
Position 21/48: nodes=16365      time=1.21s  nps=13544
Position 22/48: nodes=944        time=0.06s  nps=17110
Position 23/48: nodes=2408       time=0.13s  nps=18750
Position 24/48: nodes=30402      time=2.85s  nps=10650
Position 25/48: nodes=4617       time=0.31s  nps=14887
Position 26/48: nodes=30464      time=2.18s  nps=13968
Position 27/48: nodes=52553      time=4.98s  nps=10557
Position 28/48: nodes=526803     time=53.67s  nps=9816
Position 29/48: nodes=28564      time=3.00s  nps=9512
Position 30/48: nodes=3057       time=0.24s  nps=12899
Position 31/48: nodes=1495921    time=156.36s  nps=9566
Position 32/48: nodes=907577     time=96.22s  nps=9432
Position 33/48: nodes=2417025    time=418.18s  nps=5779
Position 34/48: nodes=627252     time=91.02s  nps=6891
Position 35/48: nodes=238376     time=19.79s  nps=12042
Position 36/48: nodes=1559272    time=149.80s  nps=10409
Position 37/48: nodes=994614     time=90.22s  nps=11023
Position 38/48: nodes=3251       time=0.15s  nps=21198
Position 39/48: nodes=4642       time=0.23s  nps=20082
Position 40/48: nodes=25037      time=0.44s  nps=56750
Position 41/48: nodes=35946      time=3.60s  nps=9989
Position 42/48: nodes=38681      time=2.66s  nps=14546
Position 43/48: nodes=9837       time=0.53s  nps=18479
Position 44/48: nodes=78504      time=7.43s  nps=10571
Position 45/48: nodes=23496      time=2.27s  nps=10368
Position 46/48: nodes=93415      time=7.84s  nps=11916
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