Skip to content

Releases: IchNukeDichWeg/Pygin

Pygin v65

Choose a tag to compare

@IchNukeDichWeg IchNukeDichWeg released this 14 Oct 18:39

v65 -- a new evaluation network, +61.91 Elo on a fixed budget. The net is replaced; the search and the interface are v64's. It was screened at 10+0.1 on arm64 and confirmed at the shipping time control on x86 over a full budget with no early stop, so this is the number the ledger advances on.

Elo   | +61.91 +/- 4.3 vs v64 (FIXED budget, no early stop)
SPRT  | none on the confirm; the 10+0.1 screen was [0,4] normalized,
      | LLR +7.486 -> ACCEPT H1 at the 1,500-pair minimum (+60.46 +/- 8.5)
Games | N: 9976   two tranches on one box, same instrument, pooled
Penta | [65, 722, 1975, 1841, 385]   4988 pairs, pair ratio 2.83, norm Elo +100.02
Conf  | TIMED 50+0.50, x86 (2x EPYC 7443), workers 48, Threads=1, both sides
Base  | vs v64 on the same engine core; ledger ~+360 -> ~+422
Bench | 672,025 nodes (was 785,901)

What changed

  • A new evaluation network. Same architecture and size as the one it replaces; only the weights differ, so nothing about the search or the UCI interface changes.
  • Correctness fixes carried since v64, none moving the bench signature: the history bonus is clamped so deep iterations cannot overflow; no shift past the h8 square in the cuckoo table; an absurd time budget no longer wraps the deadline into the past; a Contempt change clears the TT so stalemates rescore, applied between searches rather than mid-search; MultiPV bestmove comes from the last completed depth; changing SyzygyPath re-detects the table piece count.
  • Speed on arm64: a real 4-row NEON dot kernel for the net, 4.5% fewer search cycles, node-identical. The binary is again a profile-guided build.

How it was measured

  • The confirm ran the full 10,000-game budget at 50+0.5. The first tranche was cut at 394 pairs when the box's session manager ended it; the second resumed at the next opening and pooled through the same state file, for 9,976 games in all.
  • The screen at 10+0.1 read +60.46 and the confirm at 50+0.5 read +61.91, so the gain held at the longer control.
  • Old Engine/65 is node-exact against the live engine in a fresh process at d10/d12/d14.

Known limits

  • The external placement (~3164 on the CCRL Blitz scale) was measured with v64. v65 has not been placed, and a self-play gain does not carry one to one onto another engine's scale.
  • The speed and mate-finding charts in the README still end at v64; they are re-swept as a whole on an idle machine, never appended to.
  • The confirm is x86 and the screen arm64; each architecture has one of the two measurements, not both.

Full Changelog: v64...v65

Pygin v64

Choose a tag to compare

@IchNukeDichWeg IchNukeDichWeg released this 13 Sep 05:56

v64 -- a new evaluation network, and four settings that only pay together. The net is replaced, and four search parameters that each measured as nothing on their own cross the accept bound when armed as one change. Both were measured at the shipping time control, and the net was measured on top of the bundle rather than beside it, so the two are known to work together.

Elo   | +78.82 +/- 8.3 for the net, on top of the bundle (SPRT-stopped at the minimum)
      | +6.19 +/- 4.1 for the bundle vs v63 (tranche 1 ran a FULL budget: +5.94 +/- 4.4)
SPRT  | [0,4] normalized, LLR +9.264 -> ACCEPT H1 (net, stopped early)
      | and LLR +2.969 -> ACCEPT H1 (bundle, extension stopped early)
Games | N: 3004   W: 1195  L: 525   D: 1284      (net, on the bundle tree)
      | N: 11896  W: n/a   L: n/a   D: n/a       (bundle, two tranches; penta below)
Penta | [17, 201, 547, 566, 169]      1500 pairs, pair ratio 3.37, norm Elo +121.86
      | [247, 1383, 2506, 1535, 277]  5948 pairs, pair ratio 1.12, norm Elo +9.54
Conf  | TIMED 50+0.50, x86, workers 48, Threads=1, Hash=192MB BOTH SIDES
Base  | net vs the bundle tree; bundle vs Old Engine/63. SUBSET_SEED 62
      | ledger ~+354 -> ~+360 on the bundle's UNBIASED tranche (+5.94); the net's
      | number stopped at a bound and is not banked
Bench | 785,901 nodes (was 1,123,567)
Speed | +4.7% from a profile-guided build, node-identical (3 x 15 paired d13 rounds)

What changed

  • A new evaluation network. Same architecture and size as the one it replaces; only the weights differ, so nothing about the search or the interface changes.
  • The bundle, armed as one change. LMR_DIV 200 -> 170 (less late-move reduction), SEE_SCALED_K 50 -> 75 (a sharper capture-pruning threshold), SOFT_STOP_STABLE_FRAC 0.40 -> 0.45 and SOFT_STOP_STABLE_ITERS 2 -> 3 (a stable best move gets a little more clock). Each of the four measured as a NULL alone; the four together cross.
  • Profile-guided build. The shipped binary is compiled against its own execution profile: +4.7% faster with an identical node count, so it is speed and nothing else.

How it was measured

  • The bundle's first tranche ran the full 10,000 games without stopping early, which is the number the ledger advances on (+5.94 +/- 4.4). Its extension crossed a bound and is quoted separately.
  • The net was screened at 10+0.1 (+65.66 +/- 8.9) and then confirmed at 50+0.5, where it measured larger, not smaller. A screen gain that grows at the longer control is the opposite of the usual failure mode.
  • Old Engine/64 is node-exact against the live engine in a fresh process at d10/d12/d14.

Known limits

  • A bundle verdict prices the bundle, not its members. Nothing here says which of the four settings pays, and separating them would cost four more campaigns.
  • The net's number stopped at a bound, so its magnitude carries the usual upward bias. The sign and the scale are not in doubt; the exact figure is.
  • Measured on x86 (2x EPYC 7443). The speed figure is from the arm64 reference machine. Neither is confirmed on the other.
  • A fixed-budget v63-vs-v61 measurement was never run, so that link of the ledger chain remains unpriced.

Full Changelog: v63...v64

Pygin v63

Choose a tag to compare

@IchNukeDichWeg IchNukeDichWeg released this 11 Sep 15:54

v63 -- losing captures near the leaves are skipped, and v62's rejected TT rule is gone. v62 shipped a transposition-table replacement rule that had already been rejected; v63 reverts it, so the core is v61's again. On that clean core it adds FI-38: a capture that loses material by static exchange is not searched when it loses more than 50cp per remaining ply near the leaves.

Elo   | +9.50 +/- 6.0 for FI-38 K=50 on the clean core (SPRT-stopped, biased up)
      | +20.95 +/- 7.9 for the clean core + v62's window vs v61 (SPRT-stopped)
SPRT  | [0,4] normalized, LLR +2.961 -> ACCEPT H1 (stopped early)
      | and LLR +3.049 -> ACCEPT H1 for the clean-core run (stopped early)
Games | N: 6802  W: 1893  L: 1707  D: 3202      (FI-38, two tranches)
      | N: 3006  W: 880   L: 699   D: 1427      (clean core vs v61)
Penta | [121, 759, 1491, 860, 165]   3396 pairs, pair ratio 1.16, norm Elo +15.19
      | [49, 309, 620, 453, 69]      1500 pairs, pair ratio 1.46, norm Elo +33.53
Conf  | TIMED 50+0.50, x86, workers 48, Threads=1, Hash=192MB BOTH SIDES
Base  | vs Old Engine/61 (same nnue_v12 net both arms), SUBSET_SEED 62
      | ledger HELD at ~+354 -- every verdict here stopped at a bound
Bench | 1,123,567 nodes (was 1,203,792)

What changed

  • The b05 TT rule is reverted. v62 carried b4339ae + a1a31cd, the FI-115 completion, which had measured ACCEPT H0 (LLR -5.98) against v61 before the release. The C core is v61's again, plus the FI-38 setter.
  • FI-38 depth-scaled SEE pruning of captures. At depth 6 or less, off the PV, not in check and not giving check, a capture whose SEE is below -50 x depth is skipped. SEE_SCALED_K = 0 restores v61's tree node-for-node.
  • Carries v62's aspiration-window repair (FI-21) unchanged.

Testing

  • HEAD with v62's settings vs v61 at 50+0.5, same net both sides: ACCEPT H1 at 3,006 games, ptnml 49/309/620/453/69, nElo +33.53.
  • FI-38 K=50 vs that: 10+0.1 screen ACCEPT H1 (LLR +2.946, 7,813 games, +9.34 +/- 5.3), then 50+0.5 ACCEPT H1 (LLR +2.961, 6,802 games over two tranches, ptnml 121/759/1491/860/165, nElo +15.19).
  • The K sweep around 50: K=25 +6.15 at the 10k cap, K=75 +2.74 and K=100 +0.63, both NULL at the full budget. 50 is the setting.
  • selftest.py now pins a retained-TT probe (28 plies at d10 with the table kept), the one check that sees a TT replacement rule: 2,479,366 on this core against v62's 3,294,864.
  • Old Engine/63 is node-exact against the live engine in a fresh process at d10/d12/d14.

Known limits

  • Every verdict in this release stopped at an SPRT bound, so both magnitudes are biased upward and the ledger is NOT advanced on them. A fixed-budget 50+0.5 run of v63 vs v61 is owed for the size.
  • Measured on x86 (2x EPYC 7443). The Mac is unmeasured.
  • Rejected on the way: the FI-38 quiets half (K2=24 and K2=50, both ACCEPT H0) and E-04 material-scaled net output (-10.64 +/- 6.3, fixed-node).
  • PGO (+3.9% NPS measured on x86) is not in this build; it ships separately with its own measurement.

Full Changelog: v62...v63

Pygin v62

Choose a tag to compare

@IchNukeDichWeg IchNukeDichWeg released this 31 Aug 16:24

CORRECTION (2026-09-05). The sentence below claiming the C core is
byte-identical to v61 is wrong. v62 as released also carries b4339ae +
a1a31cd, the b05 FI-115 completion, which had already measured ACCEPT H0
(LLR -5.98 at 1,500 pairs) against v61 in this same campaign and should have
been reverted before the release. The verdict was recorded; the code was not
reverted. It has been reverted since, and the current tree's csearch.so is
byte-identical to v61's.

The +18.40 below still stands for what it measured. Both arms of that A/B
were Python shims over the same binary, so the b05 core sat on both sides and
cancelled out. It is an honest reading of the aspiration-window change
relative to that core. What was never measured, and is claimed nowhere in
these notes, is v62-as-released against v61-as-released.

Every routine check was blind to this by construction: bench (1,203,792) and
kiwipete d12 (670,778) are identical on both cores, because they reset the
transposition table, and a replacement rule only runs when the table is full
and carried across moves. A 28-ply retained-TT probe separates them cleanly
(3,294,864 against 2,479,366) and is now pinned in selftest.py.

v62 -- the aspiration window stopped fighting itself. The root window opened flat at 30cp whatever the position and widened 2x on every failure, and a fail-low left beta where it was -- so a position could oscillate low/high and pay the whole ladder out to the 1920 full-window fallback. Three riders replace that as one policy. Driver-only: the C core is byte-identical to v61. (see the correction above)

Elo   | +18.40 +/- 7.6 (52.65%; SPRT-stopped, so the magnitude is biased up)
SPRT  | [0,4] normalized, LLR +2.959 -> ACCEPT H1 (stopped early)
Games | N: 3439  W: 1037  L: 855  D: 1547
Penta | [62, 361, 735, 450, 109]   1717 pairs, pair ratio 1.32, norm Elo +28.25
Conf  | TIMED 50+0.50, x86, workers 48, Threads=1, Hash=192MB BOTH SIDES
Base  | vs v61 engine_v61b_deadtag (same nnue_v12 net both arms), SUBSET_SEED 59
Bench | 1,203,792 nodes (was 1,140,099 -- see Known limits)

What changed

  • Score-scaled opening window. delta = 14 + prev^2/16384 instead of a flat 30cp, so a decided position opens wide and a balanced one opens tight.
  • Fail-low pulls beta to the midpoint before alpha drops -- Stockfish's order. This is the piece that kills the oscillation; previously a fail-low widened downward while beta stayed put, so the next fail-high widened back up and the pair could walk the whole ladder.
  • 1.5x growth instead of 2x, so the ladder has more rungs and overshoots less.
  • The three are one mechanism and were measured as one change, not three toggles. ASPIRATION_FI21 = False restores the v61 window for A/B.

Testing

  • Confirmed at both time controls against v61 with the same nnue_v12 net on both arms, so the window policy is the only variable: 10+0.1 screen ACCEPT H1 (LLR +2.969, 6,687 games), then the shipping 50+0.5 instrument ACCEPT H1 (LLR +2.959, 3,439 games, ptnml 62/361/735/450/109, nElo +28.25).
  • The ported cengine reproduces the measured shim node-for-node on three positions -- what ships is what was measured.
  • Old Engine/62 is node-exact against the live engine in a fresh process at d10/d12/d14.

Known limits

  • Both runs stopped at a bound, so +18.40 is biased upward and the ledger is deliberately NOT advanced on it. FI-115 read +33.83 the same way and came back +19.11 at a fixed budget. A fixed-budget 50+0.5 re-run is owed before this magnitude is treated as settled.
  • Expected value for this change was +1-4. The measured value is far above that, which is itself a reason to re-run rather than celebrate.
  • Measured on x86 (EPYC 7443). The M2 is unmeasured.
  • The 2026-08-31 campaign that produced it was otherwise barren: eleven candidates, six rejections, four nulls, one skipped. The NNUE corpus lane is now closed on all three axes (volume, schedule, label depth), and completing the FI-115 victim rule measured -43.36 and must not be retried.

Pygin v61

Choose a tag to compare

@IchNukeDichWeg IchNukeDichWeg released this 28 Aug 22:55

v61 -- dead entries in the transposition table are now evicted first.
Material is irreversible, so a TT entry whose stored piece count exceeds the
current root's describes a position that can never occur again. It is garbage
with certainty rather than a guess, so it is taken first, while a
still-reachable old entry is depth-protected instead of clobbered on age alone.
Nothing is swept: this is purely a better choice of victim, made at store time.

Elo   | +15.89 +/- 4.2 (52.29%; FIXED budget, so the magnitude is unbiased)
SPRT  | none -- no early stop; post-hoc GSPRT[0,4] LLR +7.771 vs +/-2.944
Games | N: 10000  W: 2826  L: 2369  D: 4805
Penta | [146, 1044, 2226, 1375, 209]   5000 pairs, pair ratio 1.33, norm Elo +25.77
Conf  | TIMED 50+0.50, x86, workers 48, Threads=1, Hash=192MB BOTH SIDES
Base  | vs Old Engine/60 (same nnue_v12 net both arms), SUBSET_SEED 59
Bench | 1,140,099 nodes (unchanged -- see Known limits)

What changed

  • FI-115 dead-entry TT replacement (TT_DEADTAG, armed). The piece count
    rides in 6 spare bits of the entry and is stamped whether or not the rule is
    on, so with it off the store path is byte-identical to v60.
  • FI-113 TT prefetch now covers both cache lines of the 24-byte entry.
    Node-identical, +1.09% NPS, no A/B slot spent.

Why the measurement took two attempts

The first screen accepted this at 10+0.1 with a 24 MiB table on both sides,
chosen to maximise replacement pressure. A verdict from the regime built to
favour a change does not transfer, so it was re-run at the shipped 192 MiB.
192 MiB at 10+0.1 would have measured nothing either: at 0.15s/move the table
only reaches about a 22-bit working set by ply 40, so almost nothing is evicted
and a victim rule has nothing to improve. It took the shipped size and a
clock long enough to saturate it.

Testing

  • testing/test_tt_deadtag.py -- randomised stress test written for this
    release: a 1.5 MB table kept warm across thousands of searches so every store
    is an eviction, driven by uniform-random legal positions rather than
    game-derived ones. Checks inertness with the tag off, determinism with it on,
    that ON actually differs from OFF, descending material against a warm table,
    and 3-to-5-man endings at the bottom of the 6-bit field.

Known limits

  • The bench signature and the depth-14 ladder are blind to this change by
    construction
    -- both search from a cold table, so it never fills and the
    replacement rule never fires. Do not use either to verify FI-115 is active.
  • Measured on x86. arm64 is unconfirmed.
  • The gain is table-size dependent. At a very large Hash the table may never
    saturate, in which case the rule rarely fires; that case is unmeasured.
  • Rejected alongside it: a growing TT that starts at 24 MiB and widens at
    75% occupancy measured -2.88 +/- 4.4 and was dropped. Without a rehash on
    each widen it discards half the live table twice mid-game. The copy-on-grow
    variant is untested.

Self-contained binary: drop into any UCI GUI or lichess-bot, no Python or repo
needed. Options: Hash, Threads, OwnBook, UseTB, Move Overhead.

Full Changelog: v60...v61

Syzygy 3-4-5 tablebases (WDL, for training labels)

Choose a tag to compare

@IchNukeDichWeg IchNukeDichWeg released this 24 Aug 12:29

Syzygy 3-4-5 endgame tablebases, WDL only (145 .rtbw files, 379 MB).

Used for TRAINING LABELS: NNUE/gen_data.py --syzygy and NNUE/relabel_tb.py
replace the 5,000-node search label with tablebase truth for positions of 5
men or fewer, where the search is measurably wrong (15.7% of <=5-man result
labels and 41.6% of score signs, measured 2026-08-24).

Fetch onto a box with:
./scripts/fetch_syzygy.sh

DTZ (.rtbz) is deliberately NOT included -- training never needs it.
Source: the standard Syzygy distribution; see syzygy/fetch.sh for the URLs.

Pygin v60

Choose a tag to compare

@IchNukeDichWeg IchNukeDichWeg released this 23 Aug 20:51

v60 -- the first net trained on real games, not generated ones. Every net
before this learned from self-play positions manufactured for the purpose. v12
learned from 24.8M positions harvested out of Pygin's own A/B match logs,
labelled by the search those games actually ran at depth 12-16 rather than a
5,000-node budget. It is also the first net trained with the game-result term
switched off entirely, because that column could not be trusted.

Elo   | +42.50 +/- 17.3 (56.09%; stopped early -- magnitude bound-biased)
SPRT  | GSPRT[0,4] LLR +2.957 vs +/-2.944 -> ACCEPT H1  (stopped early)
Games | N: 1594  W: 553  L: 359  D: 682
Penta | [27, 137, 301, 272, 56]   793 pairs, pair ratio 2.00, norm Elo +63.91
Conf  | TIMED 50+0.50, x86 EPYC 7443, workers 48, Threads=1, Hash=192MB
Base  | vs Old Engine/59 (which ships nnue_v4), SUBSET_SEED 59
Bench | 1,140,099 nodes (was 1,214,534)

What changed

  • The net, and only the net. NNUE_FILE moves from nnue_v4 to
    nnue_v12. No search change, no toggle, no C change -- v59's engine with a
    different evaluation.
  • Trained on ab_logs: 24,825,823 positions from real A/B games
    (24,587,037 from pre-v49 campaigns plus 238,786 from two 2026-08-09 screens),
    every contributing side predating FI-29 so the cycle bound cannot
    draw-flatten a label through path history the features cannot see.
  • LAMBDA 1.0 -- search scores only, game result discarded. Not a tuning
    choice. Those logs predate the phantom-repetition fix, and replay showed
    1,479/1,479 and 2,215/2,215 of their repetition draws were phantom: the game
    ended a ply before the position actually repeated. It shows in the
    distribution -- W/D/L 39.0/41.9/19.1 against a clean corpus's
    41.7/18.6/39.7, both inflated and skewed. The cp column is sound, so it is
    kept and the result column is dropped.

Known limits

  • The magnitude is not trustworthy; the verdict is. The run stopped at a
    bound, so +42.50 is taken at a favourable fluctuation by construction. It
    says v12 beats v59. It does not say by how much, and the ledger is NOT
    advanced on it -- that needs a fixed-budget re-run.
  • v12 was not measured to be the best net available. Against v10 it drew:
    +3.75 +/- 6.8 over a full 10,000 games, LLR +1.235, no decision. v12 ships
    as the point-estimate leader of two nets that could not be separated, not as
    a winner.
  • The attribution behind this net is unsettled. v10 shares v4's corpus,
    recipe, dimensions and default seed and still beat the baseline carrying
    v4's net. Two runs of one recipe landing that far apart is wide enough to
    explain any single-net verdict on this ladder, this one included. An
    eight-net seed screen is running to size that spread.
  • It does not know basic endgame theory. tuning/eval_bench.py against
    Stockfish at depth 16 puts 39.6% of positions more than 10 percentage points
    apart in win probability, and the worst cases share one signature: rook+
    bishop vs rook and rook+knight vs rook -- textbook draws -- scored at +400
    to +475 while Stockfish reads 0.00. Six of the top ten offenders are that
    shape, all at phase 0-9.
  • Not confirmed on arm64. Every number here is x86.

Full Changelog: v59...v60

Pygin v59

Choose a tag to compare

@IchNukeDichWeg IchNukeDichWeg released this 10 Aug 00:02

v59 -- lazy NNUE evaluation, and the first release measured as exactly the config that ships. The engine now skips the net's forward pass wherever a cheap bound already decides the node, and spends the saved time searching. One toggle, isolated for the first time, same net on both sides of the A/B.

Elo   | +13.84 +/- 6.4 (pooled 51.99%; stopped early -- magnitude bound-biased)
SPRT  | GSPRT[0,4] LLR +2.950 vs +/-2.944 -> ACCEPT H1  (stopped early)
Games | N: 4537  W: 1292  L: 1107  D: 2138
Penta | [69, 509, 975, 595, 116]   2,264 pairs, pair ratio 1.23
Conf  | TIMED 50+0.50, x86 EPYC 7443, Threads=1, Hash=192MB
Base  | vs Old Engine/58, SUBSET_SEED 58, corrected (post-fc82cb7) harness
Bench | 1,214,534 nodes (was 1,074,820 -- lazy trades net calls for nodes)

Search

  • FI-106, lazy NNUE eval (LAZY_NNUE = True): at nodes where a cheap bound
    already brackets the decision, the net is not consulted at all. On the d11
    bench that is +13% nodes for +2.2% NPS; on the clock the trade pays.

Why this one is different

  • Every earlier number for lazy priced the whole NNUE-plus-lazy package against
    HCE baselines. This A/B put the same v4 net on both sides with the toggle
    as the only difference -- the first isolated measurement of the mechanism.
  • The candidate was byte-equivalent to the shipped engine plus this flip.
    v58's own +19.11 turned out to have been measured with lazy ON while the
    release ran it OFF (found 2026-08-09); v59 closes that gap by shipping
    exactly what was measured. That rule is policy now.

Testing

  • First release measured entirely on the corrected harness: fc82cb7 fixed a
    bug where all three match runners claimed threefold draws that had merely
    become available rather than actually occurred (419 of 2,000 games in one
    A/B, zero real repetitions among them). Ladder re-pinned for the lazy
    config; opening-sample rotation advanced to the v59 era.

Known limits

  • The Elo magnitude is a bound-stopped estimate; a fixed-budget restatement is
    owed if the number is ever quoted rather than the verdict.
  • Measured on x86 only. arm64 is still owed for v58's net arm as well.

Full Changelog: v58...v59

Pygin v58

Choose a tag to compare

@IchNukeDichWeg IchNukeDichWeg released this 03 Aug 22:51

v58 -- the first HCE/NNUE hybrid, and the first net that pays. The neural
net now replaces the hand-crafted evaluation inside the main search, while
qsearch stand-pat stays HCE. Three nets before this one measured at parity or
worse; what changed was not the network but how it was trained.

Elo   | +19.11 +/- 7.8 (95%)   normalized +30.96
SPRT  | GSPRT[0,4] LLR +2.950 vs +/-2.944 -> ACCEPT H1  (stopped early)
Conf  | TIMED 50+0.20, x86, Threads=1, Hash=192MB
Games | N: 3423  W: 1057  L: 868  D: 1498   (52.76%)
Penta | [71, 358, 691, 477, 105]   1,702 pairs, pair ratio 1.36
Split | screen 1,000 pairs (+17.56) -> pooled tranche 702 pairs
Base  | vs Old Engine/57, SUBSET_SEED 57, UHO_4060_v4, ledger +305 -> +324
Bench | 1,074,820 nodes (was 1,145,629 -- the net changes every node count)

The net

  • USE_NNUE is armed on nnue_v4_6f910e35bb1e.nnue. The net evaluates
    inside negamax; qsearch stand-pat stays on the hand-crafted eval, and the
    eval cache is depth-gated so the two scales never cross.
  • The gain is the training schedule, not the architecture. v3 read
    +0.52 +/- 6.8 on this same instrument -- statistically nothing. v4
    changed neither the dataset nor a single dimension of the network: a cosine
    learning-rate schedule in place of a flat one took held-out validation loss
    0.074417 -> 0.066663, and that is the entire +19.
  • It is not bought with speed. Identical dimensions mean identical cost
    per evaluation, so v4 runs at exactly v3's NPS. The engine still hands the
    net roughly 30% of its single-thread NPS, and the +19.11 is measured net of
    that -- the judgement is worth more than the nodes it costs.

Safety

  • NNUE_REQUIRE_SIMD keeps the net off scalar builds. Without NEON or
    AVX2 the evaluation tail runs ~3x slower again and the trade stops paying,
    so on such a CPU the engine refuses to arm the net and plays the HCE.
  • The release binary bundles the net. There is no silent HCE fallback: a
    build that cannot find its net fails loudly at startup rather than quietly
    playing weaker.

Known limits

  • An arm64 confirmation is owed. v3 measured +5.70 +/- 4.6 on arm64
    against +0.52 on x86, so the spread between architectures is real, and only
    x86 has been measured for v4. The risk direction is favourable -- arm64 read
    higher for v3 -- but it is not yet measured.
  • Label depth is the next lever. Labels come from 5,000-node searches while
    the engine plays at 1.75M, so the net is being taught by a much weaker version
    of the thing it is trying to become.

(Corrected after publication: this section originally called the net
"data-limited, not epoch-limited". That diagnosis was wrong. A follow-up epoch
sweep closed the question in the other direction, finding 8 epochs to be the
minimum rather than the 40 this net was trained for: 6/.063989, 8/.063676,
12/.063858, 16/.064264, 40/.066663. The v5 net then took validation loss down
another 4.5% from there and measured +3.09 +/- 4.5, a null, with the LLR
flat across the whole run. Near the floor, validation loss is a weak predictor
of Elo, so a lower number is not by itself a reason to ship a net. The label
depth conclusion survives; the reasoning that reached it did not.)

Full Changelog: v56...v58

Pygin v57

Choose a tag to compare

@IchNukeDichWeg IchNukeDichWeg released this 31 Jul 17:25

v57 -- the last pure-HCE release. From here Pygin becomes an HCE/NNUE
hybrid, so this is the clean endpoint of the hand-crafted-eval line. Host
layer only: it plays exactly the same moves as v56 and reaches them the same
way, but it stops wasting time it has already earned.

Elo   | none claimed -- NODE-IDENTICAL to v56
SPRT  | no A/B slot spent (nothing to measure: same tree, same moves)
Conf  | ladder node-exact d1..d14, all search toggles at their v56 values
Games | 0
Penta | n/a
Split | n/a
Base  | vs Old Engine/56, ledger unchanged at +305
Bench | 1,145,629 nodes (unchanged -- the release is node-identical)

Time management

  • Ponderhit now honours the soft-stop. A prediction hit used to spend the
    FULL fresh budget re-confirming a move it had already settled on -- the
    documented v1 deviation from when pondering shipped. It now applies the same
    P-35/U-06 fractions the main search uses. Measured 1.666s -> 0.686s, a ratio
    of 0.412 against the designed 0.40, so ~59% of that budget goes back on the
    clock for later moves.
  • A latent bug fixed: the UCI layer restored a hardcoded 0.55 soft-stop
    fraction over whatever the engine itself set. Harmless today because the two
    agree -- but it meant any future soft-stop tuning would work in testing and
    be silently discarded in every real game.

UCI

  • SoftStop, SoftStopStable, SoftStopUnstable, SoftStopStableIters --
    the time-policy neighbourhood, sweepable with no rebuild. Defaults are the
    shipped values, so an engine nobody configures is unchanged. SoftStop 0
    disables the soft-stop entirely.

Testing

  • The frozen snapshots now carry their own engine.py. Old Engine/56 and
    57 were importing the LIVE one, which works only while it happens to
    match -- the next eval retune would have made every baseline silently adopt
    the new eval.

Self-contained binary -- no Python, no dependencies. Download, mark
executable, point your GUI at it.

Full Changelog: v56...v57