Skip to content

v0.7.0 — Go incremental liberty tracking

Latest

Choose a tag to compare

@phiat phiat released this 17 May 02:22

2.67× single-thread throughput lift on the 9×9 Go bench, from replacing per-call group flood-fill in is_legal_flat and do_move with incremental per-block liberty tracking.

Bench

9×9 Go, 1600 sims/move × 32 moves, uniform-policy evaluator, single-thread, -o:speed -no-bounds-check:

Implementation sims/s vs. v0.7.0
mcts-odin v0.7.0 ~289,000
mcts-odin v0.6.0 ~108,000 2.7× slower
autogodin (C++) 8,470 34× slower
autogodin (Odin) 2,859 101× slower

(The autogodin baselines marshal the evaluator through a Python callback; mcts-odin runs it inline. Not strictly comparable.)

What changed

games/go/blocks.odin + games/go/board.odin: union-find per cell (parent[] points directly at root — no path compression, keeps journaling trivial), a circular block_next[] linked list for block traversal, a compile-time-folded blk_libs[root] liberty bitset (2 u64 words for 9×9 with BOARD_SIZE_HINT=9, 6 words for 19×19), and blk_size[root] for stone count.

do_move journals every parent / block_next / blk_libs / blk_size mutation on four per-board stacks; undo_move pops them in reverse to restore the exact pre-move state — the property that lets the MCTS hot path mutate working_state in place without per-leaf cloning.

is_legal_flat does at most 4 bitset reads + a popcount per neighbor block: capture detection is (blk_libs[opp_root] & ~bit(index)) == 0, suicide detection is (union of friendly-block libs and empty neighbors of index, with bit(index) cleared) == 0.

play_flat_unchecked (forward-only) routes through do_move and drops the journal, keeping b.blocks in sync without duplicating the algorithm.

Test coverage

tests/games/go/go_game_test.odin adds four round-trip guards targeting the worst-case shapes for a journaled union-find — multi-group merge, multi-group capture via shared liberty, three-friendly-merge, and a deterministic-LCG 50-move random self-play with bit-for-bit board equality after full unwind. tests/games/go/blocks_test.odin adds 7 BlockIndex tests (empty / single / chain / separate-groups / post-capture / 30-move random / clone). Test count 137 → 148; all suites pass under Odin's memory tracker.

No API surface changes

The mcts.Game vtable, MoveDelta, and all public Go-board procs keep the same signatures. Existing consumers (including autogodin's FFI) need no changes.

Future work

Pseudo-liberties (Pawlewicz / libEGO: {lib_cnt, lib_sum, lib_sum2} triple) would simplify the implementation further and give atari detection for free via a Cauchy-Schwarz identity. Not pursued here because the bitset version already exceeds the original 1.5–1.8× target by ~50%.

Closes mcts-odin-81j.9.