Releases: IchNukeDichWeg/Tetrarch
Release list
Tetrarch v7
v7 -- net v5, and the engine got 2.4x faster. Two independent gains in one
release, and the speed is by far the larger of them.
Elo | +63.37 +- 4.68 (95%) lazy accumulator perspectives
| +7.78 +- 4.61 (95%) net v5, vs net v4
SPRT | none -- sprt.py not built yet; fixed-N campaign
Conf | FIXED TIME movetime 200, classic/Teams, 105 workers
Games | 19,999 and 20,000 respectively, 4-game seat rotation
Base | vs v6 for the net; vs the v7 tree for the accumulator
Bench | 704,240 search nodes, 1,819,341 nps (M2 Pro, depth 7)
Note | the bench now measures the SEARCH. v6's 93,846,865 was perft, which
| cannot see the evaluation at all and never moved when a net loaded.
The speed is worth eight times the net
Net v5 is a whole generation of self-play: 125,000 games at 7,500 nodes with
net v4 teaching. It is worth +7.78 +- 4.61. One afternoon of profiling is
worth +63.37 +- 4.68.
The exchange rate that falls out is roughly +115 Elo per doubling of search
speed, well above the +50 to +70 a two-player engine expects. A 10% speedup is
about +15 Elo here, which is twice what generation 5 bought for hours of
many-core time.
Three speed changes, none of which can alter a single move:
- Lazy accumulator perspectives, +59.8% NPS. All four perspective
accumulators were maintained on every make and unmake; an evaluation reads
one. Toggles are now queued per perspective and cancel on unwind, so
descending into a subtree and coming back out costs no arithmetic at all for
a perspective nobody evaluated. - int8 SIMD for the propagation, +23.5% NPS. crelu already bounded every
activation to [0,127], so the whole propagation fit in int8 and was being
carried in int32. - Pins computed once per node, +3.9% NPS, so a move that cannot expose its
own king skips the attack scan.
Found by profiling the real thing, not by timing components
bench.py --profile said the evaluation was 26.7% of a node and the
accumulator about 8%. Sampling the running search said 3.1% and 23.3%. Timing
a component by running it on one position hundreds of thousands of times keeps
the same few rows of a 2 MB table resident, and a real search does not. The
isolation method did not merely have wide error bars, it inverted the ranking,
and the first fix attempted on its advice was 4% slower.
Also in this release
- Repetition detection. A position the search is walking into that has
already occurred scores 0 rather than being evaluated as new. Neutral at both
instruments and kept anyway: scoring a draw as a draw is a rules gap, not a
tuning choice, and the record says so rather than dressing it up. [StartFen4 "4PC"]is read as the modern start.
On the numbers being small
A partial run of the net v5 A/B read -2.87 +- 14.42 over 2,017 games and was
written up here as a null result. It was not one: the full 20,000 games put it
at +10.50, inside that same interval. An interval running from -18 to +13 means
the instrument cannot see the effect, which is not the same as the effect being
absent.
Full record, including that retraction and every rejection:
docs/AB.md
git clone https://github.com/IchNukeDichWeg/Tetrarch && cd Tetrarch && ./setup.shTetrarch v6
v6 -- NNUE evaluation. The engine plays with net v4 instead of the
hand-written evaluation it was bootstrapped on. First release to change the
evaluation rather than the search, and the first since v0 where a fresh clone
plays differently.
Elo | +76.79 +- 6.87 (95%)
SPRT | none -- sprt.py not built yet; fixed-N campaign
Conf | FIXED TIME movetime 200, Hash=256MB, classic/Teams, 10 workers
Games | N: 10000 (2500 openings x 4 rotations) score 6087.5 (60.88%)
Dist | 39, 2, 380, 13, 846, 25, 839, 17, 339
Null | -10.77 +- 13.96 on the same instrument, 2000 games
Base | vs v5
Bench | 93,846,865 perft nodes (unchanged; the bench is perft, not search)
Pins | unchanged -- with no net loaded not a line of this runs
Also confirmed at FIXED NODES 20000: +135.19 +- 7.36 over 10000 games.
Fixed time is the smaller number for the obvious reason: the net costs 1.37x
per node, so about 58 Elo buys back the depth it loses. It keeps well over
half.
Net v4 was trained on 12.31 M positions from 149,986 games that net v1 played.
Generation N+1 beating generation N is the mechanism the whole NNUE phase
rested on, and it had never once been demonstrated before this: v0 lost at
-40.13 and v1 at -2.26. v4 beats v1 by +90.22 +- 15.13 on a clock.
The fixed-time instrument was certified with its own null first. Every earlier
fixed-time result rested on an instrument that had never been checked.
Settled in the same campaign: lambda closed at 0.7 (the 0.30 and 0.15 arms lose
by 94.51 and 124.60); mobility in the hand eval rejected at both instruments.
Net=none still selects the hand eval. Full record, including every rejection
and one published analysis that a later measurement overturned entirely:
docs/AB.md
git clone https://github.com/IchNukeDichWeg/Tetrarch.git && cd Tetrarch && ./setup.shTetrarch v5
v5 -- quiescence check evasions. When the side to move is in check,
quiescence searches every legal move instead of standing pat, and reports mate
when there are none. Before this it could not see a mate at all.
Elo | +106.78 +- 6.88 (95%)
SPRT | none -- sprt.py not built yet; fixed-N campaign
Conf | FIXED NODES 20000, Hash=256MB, classic/Teams, 100 workers
Games | N: 10000 (2500 openings x 4 rotations) score 6490.0 (64.90%)
Dist | 28, 6, 224, 30, 832, 40, 895, 22, 423
Null | -2.64 +- 6.24 on the same harness, 10000 games
Base | vs v4
Bench | 93,846,865 perft nodes (unchanged; the bench is perft, not search)
Pins | classic d5: 7,449 -> 7,669 search nodes (103.0%)
Screened +104.68 over 2000 games and the confirm came in above the screen.
The largest single gain in the engine, on the one feature predicted to lose:
it costs nodes, wins nothing on node count, and nodes are depth. That
prediction was borrowed from two-player chess. A seat here can be checked by
three opponents at once, so checks are far more frequent and a quiescence that
stands pat through them is proportionally more broken.
Tetrarch v4
v4 -- late move pruning. LMR searches the quiet tail shallower; this stops
searching it at all. Once 4 + depth² quiet moves have been tried without a
cutoff, at shallow depth and not in check, the rest are dropped. At a branching
factor near 60 that tail is most of the tree.
Elo | +36.09 +- 6.69 (95%)
SPRT | none -- sprt.py not built yet; fixed-N campaign
Conf | FIXED NODES 20000, Hash=256MB, classic/Teams, 100 workers
Games | N: 10000 (2500 openings x 4 rotations) score 5517.5 (55.18%)
Dist | 96, 6, 437, 36, 968, 33, 683, 16, 225
Null | -2.64 +- 6.24 on the same harness, 10000 games
Base | vs v3
Bench | 93,846,865 perft nodes (unchanged; the bench is perft, not search)
Pins | classic d5: 17,772 -> 7,449 search nodes (41.9%)
Screened +36.62 +- 15.43 over 2000 games and the confirm landed almost exactly
on the screen.
Search
- LMP --
LMPMaxDepth3,LMPBase4. Mean depth at the instrument goes
4.25 -> 5.25 over 40 positions, a full extra ply, and the depth 2-3 tail
disappears entirely. - The guard that matters is
legal >= 1. Without it a node could prune every
move, fall through with no legal moves, and report a checkmate that is not
there. Over 84 positions LMP missed a mate the full search finds 3 times and
invented one 0 times;selftestpins the never-invents property.
Cumulative
Four confirmed gains in, the search tree at classic depth 5 has gone from
228,628 nodes to 7,449 -- 3.3% of where v0 started -- and mean depth at 20,000
nodes from 3.73 to 5.25.
Tooling
match.pyno longer refuses to start when a--logor--pgn4name is
reused. It takes the next free name and prints it. A campaign log is the only
record of a run, so overwriting one silently stays unacceptable; refusing to
start over a name collision was friction with no safety value.
Tetrarch v3
v3 -- lazy evaluation. The throwaway eval's king-danger term ran 32
is_attacked calls per node and measured 54% of all search time. This computes
material first and skips it when material alone already settles the bound by
more than the danger term could possibly move it.
Elo | +42.88 +- 6.50 (95%)
SPRT | none -- sprt.py not built yet; fixed-N campaign
Conf | FIXED TIME movetime 200, Hash=256MB, classic/Teams, 100 workers
Games | N: 10000 (2500 openings x 4 rotations) score 5614.0 (56.14%)
Dist | 61, 13, 428, 44, 953, 46, 719, 17, 219
Base | vs v2
Bench | 93,846,865 perft nodes (unchanged)
Pins | unchanged -- the tree does not move, only the clock
NPS | 1,418,577 -> 1,620,220 at depth 7 on an M2 Pro (+14.2%)
Measured on fixed time, and that mattered. It changes speed and not the
tree, so a fixed-nodes campaign would have reported exactly zero and
under-credited the entire gain. Screened +64.66 +- 15.12 over 2000 games; the
confirm came in lower because it ran against the new v2 baseline, and LMR had
already bought some of the depth lazy eval was buying. Confirming against the
current default rather than the one a feature was screened on is the point --
the two gains are not additive.
Evaluation
- Lazy eval -- bail margin
4 * 8 * king_danger = 384, the most the danger
term can move the score. Not exact: every cutoff DECISION is identical, but a
bail returns the material term rather than the true eval and fail-soft
propagates that value. It measured node-identical over 80 positions all the
same, whichselftestwatches rather than assumes.
A correction
This feature was described in-session as "exact, needs no games". That was
wrong, for the reason above. The claim was retracted before the A/B was run,
not after.
Tetrarch v2
v2 -- late move reductions. Quiet moves tried late are searched shallower
with a null window, at full depth only if one raises alpha. It is the first
feature to attack the branching factor rather than assume the ordering is
already good, and that is why it paid where two others did not.
Elo | +35.07 +- 6.51 (95%)
SPRT | none -- sprt.py not built yet; fixed-N campaign
Conf | FIXED NODES 20000, Hash=256MB, classic/Teams, 111 workers
Games | N: 10000 (2500 openings x 4 rotations) score 5503.0 (55.03%)
Dist | 88, 12, 415, 39, 1002, 45, 681, 16, 202
Null | -2.64 +- 6.24 on the same harness, 10000 games
Base | vs v1
Bench | 93,846,865 perft nodes (unchanged; the bench is perft, not search)
Pins | classic d5: 83,805 -> 17,772 search nodes (21.2%)
Screened +24.53 +- 14.19 over 2000 first. Reduction table
0.75 + log(depth)*log(move)/2.25, with LMRMinDepth and LMRMinMove exposed
so the recipe is tunable without a rebuild.
Not exact -- it can miss a line the full search would find -- which is why it
needed games rather than a node count. selftest's minimax oracle turns it off
for its comparison, since the oracle would otherwise be checking plain
alpha-beta against a search deliberately allowed to differ.
Search
- LMR -- depth >= 3, after 3 moves, quiet moves only, never in check.
Mean depth at 20,000 nodes: 3.73 -> 4.25 over 40 positions.
Rejected in the same window, kept dormant with their verdicts
- History heuristic -- 16% fewer nodes at depth 7-8 and nothing at 5-6, but
the engine reaches median depth 4 at the campaign instrument. Screening it
there would have measured noise and returned a confident null about a feature
that never engaged. Not screened; gate closed before spending games. - Principal variation search -- 118.9% of baseline nodes at depth 4, 115.3%
at 5. It makes the tree BIGGER where this engine searches, because it pays a
re-search whenever a later move beats alpha. Not screened; it would lose, and
the loss would be a statement about the ordering rather than about PVS.
Tetrarch v1
v1 -- killer moves. The search had no quiet-move ordering whatsoever:
transposition move, MVV-LVA captures, then generation order. Two killers per
ply closes the largest single ordering gap, and it is the first measured Elo
gain in the engine.
Elo | +50.42 +- 6.41 (95%)
SPRT | none -- sprt.py not built yet; fixed-N campaign
Conf | FIXED NODES 20000, Hash=256MB, classic/Teams, 111 workers
Games | N: 10000 (2500 openings x 4 rotations) score 5720.5 (57.20%)
Dist | 57, 8, 388, 31, 969, 47, 763, 21, 216
Null | -2.64 +- 6.24 on the same harness, 10000 games
Base | vs the Phase 3 baseline: alpha-beta + TT + quiescence, no quiet ordering
Bench | 93,846,865 perft nodes (unchanged; the bench is perft, not search)
Pins | classic d5: 228,628 -> 83,805 search nodes (36.7%)
Screened +40.66 +- 14.50 over 2000 games first, and the confirm came in higher
rather than regressing toward zero.
Search
- Killers -- two quiet moves per ply that last caused a beta cutoff there.
Scores sit strictly between quiet and capture, so captures are never
displaced. Cleared per search, not per game: a table left over from another
position orders by moves that meant something somewhere else.
Harness
This is the first result measured on a harness whose null self-test passes.
Two bugs had to be fixed first, both found by that test and neither visible any
other way:
- The seat rotation cancelled nothing. Rotating the board and the team
assignment together handed engine A the original R+Y armies in all four
rotations. Measured +36.26 +- 18.37 with the same engine on both sides. - A null self-test collapsed both sides into one subprocess, so all four seats
shared one transposition table -- the one run that certifies the harness was
the one run not exercising it.