<feed xmlns='http://www.w3.org/2005/Atom'>
<title>typhoon.git/src/eval_tune, branch master</title>
<subtitle>The typhoon chess playing engine.
</subtitle>
<id>https://git.acknak.org/cgit/typhoon.git/atom?h=master</id>
<link rel='self' href='https://git.acknak.org/cgit/typhoon.git/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/'/>
<updated>2026-10-23T20:19:17Z</updated>
<entry>
<title>NNUE search integration: FH% tables, NODE_TYPE refinement, TREEDUMP rewrite</title>
<updated>2026-10-23T20:19:17Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-23T20:19:17Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=69525eb2f42ea9cca8255f6cc3b85e6b1312fd05'/>
<id>urn:sha1:69525eb2f42ea9cca8255f6cc3b85e6b1312fd05</id>
<content type='text'>
- data.c/chess.h: widen the joint FH% table's late axis 16-&gt;17, dedicating
  the new bucket 0 to a whole-node ("will something here fail high at
  all") sample rather than sharing it with the first-leftover-move curve.
  Add an NNUE-eval counterpart table (g_uJointFHThousandths_NNUE) and an
  NNUE counterpart of the beta-side table, selected at runtime based on
  whether NNUE eval is active.
- search.c/searchsup.c: add _RefineNodeTypeGuess, which uses the FH%
  tables (via g_NodeTypeGuessTable) to refine a node's NODE_TYPE guess
  instead of the old heuristic; thread uParentMoveFHEstimate through
  PLY_INFO so a node can see its parent's prediction for the move that
  led to it.
- generate.c: initialize sKillers[2]/[3].uBonus unconditionally (an
  uninitialized-stack read caught by valgrind under LTO; harmless today
  but a real bug).
- dumptree.c/chess.h/GNUmakefile: replace the long-dead DUMP_TREE XML
  tree dump (written for a GUI this project no longer has) with TREEDUMP,
  a flat, path-keyed move/result trace meant to diff against an
  identically-instrumented Stockfish build -- see docs/TREEDUMP_FORMAT.md.
- docs/DEEPER_PRUNING.md: notes on closing the search-efficiency gap vs
  Stockfish.
- crafty_egtb/stockfish_xboard/typhoon_nnue: wrapper scripts so
  match_play.py's Engine class can drive Crafty, Stockfish (via
  PolyGlot), and an NNUE-enabled typhoon the same way it drives HCE
  typhoon.
- tests/lop: rename lop.txt -&gt; lop.ep_; update compare_lop.cpp.
- annotate_pgns: add aggregate_marginsafety.py, dedup_pgn.py,
  fit_lazy_probe.py; update annotate.py/run_nnue_pipeline.py for the NNUE
  training pipeline.
- .gitignore: ignore compile_commands.json (clangd build DB) and
  eval.nnue (a symlink into the untracked head_reference/ comparison
  baseline), neither of which belongs in the repo.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Wire NNUE eval into search, build the training pipeline, and add a fast</title>
<updated>2026-10-20T16:26:44Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-20T16:26:44Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=c8c6bbc2c1a103e5b07b079a0f8bdf66c7724d80'/>
<id>urn:sha1:c8c6bbc2c1a103e5b07b079a0f8bdf66c7724d80</id>
<content type='text'>
move/eval-agreement benchmark against Stockfish (tests/lop)

Implements the architecture decisions in src/docs/NNUE.md: HalfKAv2_hm
feature set, int16/int8 quantization, per-ply accumulator backup for
king moves, and a lazy-eval-ready integration into search.c/eval.c.
Adds the full annotate_pgns/ training pipeline (extraction, K-fit/
labeling, quiet filtering, tensor building, training, export) plus an
end-to-end orchestrator (run_nnue_pipeline.py) and validation tooling
(sanity_check_net.py, compare_vs_stockfish_net.py). Adds tests/lop:
a 10k-position list-of-positions benchmark comparing engine eval rank
and move choice against pre-computed Stockfish/lc0 references, much
cheaper than a full self-play match for day-to-day progress checks.

First sd10-matched self-play result against classic HCE: LLR +2.28
(H1=+2.94), -72=119+169 over 360 games, confirming the net is
"smarter per node" at matched depth. A real-time (--st 4) match is
currently weaker (candidate loses ~3 plies of depth to a ~3.3-3.7x
nps gap against an unrecalibrated search), consistent with
NNUE.md's adoption-criterion section -- pruning/reduction constants
still need retuning for the new eval's score scale before a fair
real-time comparison is possible.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>test_vs_head.sh: add NNUE_WEIGHTS to run an NNUE candidate vs HCE head</title>
<updated>2026-10-15T23:28:55Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-15T23:28:55Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=3b5d3677bb14ae14728a1c4b221176bb83667d92'/>
<id>urn:sha1:3b5d3677bb14ae14728a1c4b221176bb83667d92</id>
<content type='text'>
match_play.py's Engine passes identical engine_args to both sides, so
there was no way to get an NNUE candidate playing against the classic-
eval head_reference without patching match_play.py itself. Instead,
when NNUE_WEIGHTS is set this wraps the candidate binary in a thin
exec shim that always injects "--nnueweights $NNUE_WEIGHTS" -- works
unmodified because Engine spawns the given path directly via Popen
(no shell), so a shebang script in its place is indistinguishable
from a real binary to it. HEAD is untouched, so it keeps running HCE.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Add 5D FH% joint table (improve axis) + beta-side static-null-move</title>
<updated>2026-10-13T18:12:35Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-13T18:12:35Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=7400001fc2d43a507b54281f9860c60177860fe5'/>
<id>urn:sha1:7400001fc2d43a507b54281f9860c60177860fe5</id>
<content type='text'>
table, wire up static null-move pruning; misc searchsup.c tuning;
--cpus=4 fix; multi-pool match_play.py openings.

data.c: g_uJointFHThousandths grows a 5th "improve" axis
([12][11][11][15] -&gt; [12][11][11][16][11]); new
g_uBetaFHThousandths[12][21][11][11] table added. chess.h/searchsup.c/
search.c/split.c: minimal consumer wiring for the improve axis
(ImproveBucket(), extended _JointTablePredictedFHThousandths
signature/callers).

search.c: new static (score-only, no verification search) null-move
cutoff ahead of the existing real null-move check -- when iEval &gt; iBeta
by enough that the new beta-side table predicts a real null-move
search would fail high with &gt;95% confidence, take the cutoff on the
table's say-so instead of paying for the verification search.

searchsup.c: EstimateHangingMaterialCp now gives half credit when the
mover has an unrelated piece already flagged trapped at node entry;
bumped the SEE&gt;=0 check extension from 3/4 ply to a full ply when the
opponent has significant material left; removed the "entering the
endgame extension" bonus (recapture extension and its window check
untouched).

main.c: accept "--opt=value" in addition to "--opt value" on the
command line (a bare "--cpus=4" used to silently fail to match and
leave uNumProcessors at its default of 1); also fixes a latent
off-by-one that could read one past the end of argv for a value-taking
flag given as the last command-line argument.

unix.c: brace-scoping cleanup around signal handler setup, no behavior
change.

eval_tune/match_play.py, test_vs_head.sh: sample opening positions from
multiple PGN pools (twic_filtered.pgn + lichess*.pgn) instead of one,
for more opening diversity in match-play testing.

Brought out of a larger stashed changeset one piece at a time, each
verified independently against a --cpus 1 / --cpus 4 ratio benchmark
to rule out the MP scaling regression that changeset originally
triggered (see SCOTT_IS_A_MORON.md) -- no regression reproduced in any
of the pieces here. Playing-strength impact measured as neutral via
self-play (170 games, llr=-0.04, score=0.500) despite ~1 ply deeper
average search. The stashed changeset's EFP margin rewrite, LMP
threshold rewrite, and a disabled null-move-skip experiment are
deliberately excluded -- those need their own careful, separate
commits and testing. The stash's eval.c retuning is abandoned (measured
~even/worse over a ~700-game overnight self-play run).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Revert "Minor changes; eval code was play tested as ~even."</title>
<updated>2026-10-13T16:07:30Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-13T16:07:23Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=78881b8e6422b7980e5f71780ae4bed862b93e72'/>
<id>urn:sha1:78881b8e6422b7980e5f71780ae4bed862b93e72</id>
<content type='text'>
This reverts commit f03bcac9ce3c6542310010bfbef2b126bb6ae861.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Minor changes; eval code was play tested as ~even.</title>
<updated>2026-10-13T15:59:29Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-13T15:59:29Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=f03bcac9ce3c6542310010bfbef2b126bb6ae861'/>
<id>urn:sha1:f03bcac9ce3c6542310010bfbef2b126bb6ae861</id>
<content type='text'>
</content>
</entry>
<entry>
<title>Continue dynamic move-ordering/LMR session: SPRT-validated at 1770+ games</title>
<updated>2026-10-05T16:01:25Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-05T16:01:25Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=64b5d3307a7a95975f7f8c0248f3f1febedd8617'/>
<id>urn:sha1:64b5d3307a7a95975f7f8c0248f3f1febedd8617</id>
<content type='text'>
Gate match vs head (st4.0) shows LLR=+2.89 approaching the H1=+2.94
bound, candidate score 0.5324 (LOWER95=0.5092, ELO=+22.6+/-16.2),
candidate depth/ebf both improved (13.98 vs 13.61 avg depth, ebf 3.324
vs 3.464) with comparable nps -- consistent with real search-efficiency
gains rather than noise. match_play.py gets additional instrumentation
to report these stats; search.c/searchsup.c/generate.c/chess.h/eval.c
carry the underlying move-ordering/LMR changes from this session's
line of work.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Dynamic move-ordering/LMR session: honest FH tracking, decoupled admission/magnitude, bugfixes, tooling</title>
<updated>2026-10-04T15:17:52Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-04T15:17:52Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=bbdf1bda8f410df6aa9be0a38e71b7db04982198'/>
<id>urn:sha1:bbdf1bda8f410df6aa9be0a38e71b7db04982198</id>
<content type='text'>
Lands the 2026-09-28..10-03 move-ordering investigation (full history in
lmr_testing/*.md). Summary of what ships:

LMR/move-ordering redesign (searchsup.c, dynamic.c, data.c, chess.h):
- GetLMRReduction's admission decision (do we reduce at all) is now
  decoupled from magnitude (how much): LMR_STATIC_ADMIT_THOUSANDTHS=500
  gates admission via the 4D joint fail-high table (data.c) plus a
  countermove-match veto; magnitude is a flat Crafty/Ethereal-shaped
  log(depth)*log(moves) table (g_iLMRQuietReduction), which measured as
  good or better than the old per-tier joint-table payout while being
  structurally immune to the per-bucket miscalibration that sank the
  tiered approach. g_FailHighs honesty fix (real 0/0 init, not a fake
  prior) underlies the dynamic-danger signal.
- Retired NumLeftoverMovesToSelect's depth-indexed selection budget --
  evidence showed the leftover pool has real findable signal a bailout
  was discarding.
- ShouldLatePrune (overt late-move pruning) implemented but disabled at
  its call site -- measured net-negative at the tested threshold; kept
  for reference and as a base for a future hang-bucket-9-targeted retry.
- PRUNE_VERIFY, PRUNE_LIVE, DISABLE_LMR: opt-in diagnostic build flags
  (GNUmakefile), off in the standard profile.

Bug fixes found and fixed while exercising this work:
- JOINT_EXTRA_TIER_COUNT 4-&gt;6 (chess.h): fixed an out-of-bounds counter
  write reachable at remaining depth &gt;= 14 (unreachable at sd10, routine
  in real search/self-play).
- Node-type instrumentation (search.c, root.c, searchsup.c): fixed four
  compounding defects that produced impossible percentages (e.g. 106.2%
  of PV nodes) -- wrong denominator, qsearch misclassified as main
  search, untyped early-exit nodes, and fail-low tests using the
  in-place-raised alpha instead of the initial one.
- FH-rate unit unification (search.c, searchsup.c, dynamic.c, chess.h):
  GetMoveFailHigh* returned per-mille while g_uJointFHThousandths is
  thousandths-of-a-percent, two different units sharing a "_THOUSANDTHS"
  suffix. Unified to one scale, renamed to "_PCT_THOUSANDTHS".
  RateBucket's band edges fixed to match.
- QSearch option B (search.c): waives the opponent-stand-pat requirement
  at NODE_PV only (sound there since the exact score propagates; not
  sound at null-window nodes). iExtensionAmount unit fix (was 0/2 into a
  ONE_PLY-scaled field). pi-&gt;iEval now published when Eval() runs.
- eval_tune/match_play.py local_ebf: was reporting 0.000 in every match
  since 2026-09-18 (regex only matched the trailing-stats parser, not
  the live move-scan loop; engines were also spawned with "nopost" so
  root.c never emitted the lines at all). True per-ply EBF is ~2.0, not
  the ~3.9 every prior match report implied.
- eval_tune/test_vs_head.sh: positional-arg shift ordering bug caused
  every no-extra-args run to silently rebuild ./typhoon with a
  non-standard profile and, separately, could feed a flag's value in as
  the candidate binary path.
- PERF_COUNTERS cleanup (eval.c, search.c): moved 3 leaked rdtsc sites to
  the existing EVAL_TIME guard, deleted two concluded one-off diagnostics
  (u64HeadStyle*, u64FHJointHist/RateBucket/AttemptsBucket) and their
  report plumbing, and re-gated a per-move hang-material estimate behind
  FH_TABLE_COLLECTION instead of running it unconditionally.

Infrastructure:
- match_play.py time-odds support (--odds/--st-head/--st-candidate),
  validated against the engine seat rather than color.
- Wilson-upper-bound re-bake of the joint table (data.c): cells store a
  confidence-aware bound instead of a raw point estimate, so a consumer
  can threshold on the value alone; monotone, so every consumer became
  strictly more conservative. Verified behavior-neutral for LMR.

Known-open items (not resolved here, see lmr_testing/GET_LMR_REDUCTION.md
section 10 and 2026-10-03_UNCOMMITTED_TRIAGE.md): the self-play gate on
this candidate is underpowered (+5 Elo, 95% CI roughly [-6, +17]); the
curated-suite solve-count deltas used earlier in the investigation turned
out to be noise-dominated cancellation when diffed per position, not the
clean signal they first appeared to be.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Two ideas:</title>
<updated>2026-09-25T15:44:25Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-09-25T15:44:25Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=9b28080f8b3dca4588cb2aaaf748451f45e3c626'/>
<id>urn:sha1:9b28080f8b3dca4588cb2aaaf748451f45e3c626</id>
<content type='text'>
1. Nullmove R=3 + depth/6 along with some ShouldWeDoNullmoveSearch
tweaking and calibrating.

2. Probe EGTB everywhere in the search as long as the table/position
is resident in memory.
</content>
</entry>
<entry>
<title>Redesign lazy/super-lazy eval margins with position-specific percentile</title>
<updated>2026-09-18T16:43:17Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-09-18T16:43:17Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=f698c02de213d0ed8968fb7cca5f70df51fa0763'/>
<id>urn:sha1:f698c02de213d0ed8968fb7cca5f70df51fa0763</id>
<content type='text'>
tables, and fix four real search/recognizer bugs found while validating it

Main theme: replace the flat, heavily-padded LAZY_EVAL_BASE_MARGIN /
SUPER_LAZY_MARGIN_BY_ARMY constants with per-material-bucket percentile
tables (king safety by attacker army, residual by own army, separate
tables for the lazy vs super-lazy baselines) derived from a 30k-position
calibration pool, targeting a measured ~1-1.5% exceeded-margin rate
instead of an unmeasured "add lots of headroom" floor. QSearch's
FUTILITY_BASE_MARGIN_LAZY/SUPERLAZY were re-derived to match the new,
now-meaningful (p50-based) iPositional this produces. Self-play confirms
the working tree beats both HEAD, a more liberal (~5% miss target)
variant, and a more conservative (near-zero miss) variant of the same
margin mechanism.

Along the way, an initial self-play regression (this margin work bundled
with several changes in one pass, one of which broke things) led to a
day of ZFS-snapshot bisection and then targeted DEBUG-build fuzzing
against a stratified 200k-position corpus (tests/cross_sectional_200k.ep_,
new), which is what actually found and fixed four distinct, real,
pre-existing engine bugs -- none of them related to the margin redesign
itself:

- searchsup.c (CommonSearchInit): three early-return paths (timer
  abort, draw, too-deep bailout) left a node's own PV[] row stale
  instead of clearing it, letting an ancestor's UpdatePV splice a
  leftover move from a completely unrelated node into the displayed
  PV -- root cause of a WalkPV/MakeMove assert ("moving a piece to
  capture, but nothing was on the target square").

- dynamic.c (RecordEnprisePieceAtPly): the two-slot en-prise dedup
  compared (square, piece) instead of square alone, so a same-square
  re-record with a different piece (legitimate -- a fail-high
  capture's victim is restored by UnmakeMove and can differ across
  calls at the same ply) shifted a duplicate square into both slots,
  tripping ValueOfMaterialInTroubleDespiteMove's two-distinct-squares
  assert.

- recogn.c (_RecognizeKBKP): the wrong-colored-bishop corner-fortress
  draw shortcut fired even when the weak side has its own pawn(s),
  which can hand the strong side real zugzwang resources the fortress
  logic doesn't account for -- produced a false hard draw bound where
  EGTB shows a near-forced loss.

- eval.c (_EvalPasserRacesAgainstLoneKings): a rook pawn's race was
  credited as winning even when the strong side's own king sits
  exactly on its own pawn's queening square (distance 0 specifically --
  adjacent is fine, the square is empty) -- a classic stalemate
  resource the race heuristic can't see. Only reachable through
  _RecognizeKPK's pawn-hash-cache-hit path, which is why
  testrecogn.c's TestRecognExhaustiveKPK reported "0 wrong" the whole
  time; extended it to cover both color assignments and the full board
  (no mirror-shortcut) and documented the remaining cache-hit coverage
  gap for a future session to close.

Also: match_play.py gains a local_ebf metric (geometric-mean of
depth-normalized node-growth ratios between consecutive completed
iterations) alongside the existing cumulative nodes**(1/depth) ebf,
which turned out to be misleading when comparing engines with
different lazy-exit aggressiveness -- and probe.c/hash.c pick up two
small, independently-found correctness fixes (TB root-probe promotion
piece missing its color bit; ASSERT(g_pHashTable) crashing on the
legitimate "--hash none" config) noticed along the way.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
