diff options
| author | Scott Gasch <[email protected]> | 2026-10-20 19:46:51 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-20 19:46:51 -0700 |
| commit | 6b883e83935f5e8110788565cfcb77cd6fbf8950 (patch) | |
| tree | 2ed8ca325c45edcf65bac76a20ba47e8fe8a348e /src/chess.h | |
| parent | 4e6e78fc767985b9a06ce0fc5dda392027bcc25a (diff) | |
Restore 5D-alpha + 4D-beta FH% collection (improve axis + whole-node bucket), fix two bugs found via validation
Restores the full dual-table FH% collection instrumentation under
FH_TABLE_COLLECTION, which had regressed to a 4D-only alpha joint with
no beta side at all (see the prior commit's note): adds back the
`improve` axis to the alpha joint and a new 4D beta joint (depth,
gap-to-beta, own-improve, enemy-improve), both with real INC() call
sites and ALPHARAW/BETARAW/ESTRAW dump lines matching dual_table_run/'s
existing (unmodified) aggregate.py/dashboard.py/marginal_crosscheck.py
parsers exactly.
Also adds a genuine NODE-level sample into the alpha table's previously
unused late==0 bucket: "will something at this node fail high" when
iEval <= iAlpha, ground truth = the real move loop's outcome. This is
the alpha-side analog of the beta table (which only covers iEval >
iBeta) -- together they cover every null-window child's self-estimate
with no gap, motivating a follow-up (not part of this change) on
combining a parent's per-move FH% prediction with a child's own
node-level self-estimate for a sharper NODE_CUT/NODE_ALL signal than
CommonSearchInit's current purely-structural PVS alternation.
Collecting real ground truth for both new node-level samples requires
not taking the beta-side static-null-move shortcut's early `goto end`
during collection builds, same reasoning as the existing LMP disable.
Two bugs found and fixed via validation before trusting this for a
multi-day run:
1. goto-skips-initializer: Search()'s early QSearch-depth `goto end`
jumped over the new collection variables' declarations, leaving
them uninitialized (indeterminate stack garbage) on that path. Only
manifested in the release (-O3) build -- a DEBUG (-O0) build worked
by accident (the skipped stack slots happened to read as zero).
Caught via lldb on a real segfault: garbage "eligible" flags feeding
wildly out-of-range array indices. Fixed by moving the declarations
before the early goto.
2. MAX_SPLITS amplification: the new arrays were nested inside
COUNTERS.tree, which is embedded both in SEARCHER_THREAD_CONTEXT and
in split.c's g_SplitInfo[MAX_SPLITS] array -- so the ~19MB growth
got replicated 64x, inflating the binary's BSS from ~220MB to
~1.47GB and breaking valgrind's fixed-address mmap outright (harmless
at runtime for a --cpus 1 collection shard, since g_SplitInfo's
copies are never touched, but it also meant any real MP split would
have silently lost a helper thread's collected data, since no
merge-back code exists for these fields). Fixed by moving them into
a new standalone FH_COLLECTION_COUNTERS struct referenced only from
SEARCHER_THREAD_CONTEXT, not from SPLIT_INFO. BSS dropped to ~281MB;
valgrind (--track-origins=yes) now runs clean: 0 errors, 0 bytes
definitely/indirectly/possibly lost.
Validated end-to-end against real NNUE weights (--nnueweights
eval.nnue), release build, full tests/ecm_ringers.ep_ suite at sd 10:
ALPHARAW/BETARAW/ESTRAW all populated (including real late==0 "whole
node" rows), dual_table_run/'s existing aggregate.py and
marginal_crosscheck.py parse it unmodified -- depth/gap/improve axes
match exactly; the only two "mismatches" (dumb and late bucket 0) are
exactly and fully explained by the new whole-node samples, which
deliberately don't feed those two per-move marginals (hang=0/late=0
are placeholders there, not real measurements) -- not a real bug, and
reproduced byte-for-byte identical after the struct relocation above.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/chess.h')
| -rwxr-xr-x | src/chess.h | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/src/chess.h b/src/chess.h index 34099b2..8c5d86c 100755 --- a/src/chess.h +++ b/src/chess.h @@ -1942,6 +1942,76 @@ KING_DEFECT_DUMP_INFO; // #define COUNTER_MOVE_TABLE_SIZE (0x20000) +#ifdef FH_TABLE_COLLECTION +// 2026-10-20: full dual-table (5D alpha + 4D beta) collection, +// restored for the NNUE-eval re-collection run -- see docs/NNUE.md. +// Deliberately NOT a member of COUNTERS (unlike every other +// PERF_COUNTERS field) -- COUNTERS is embedded both in +// SEARCHER_THREAD_CONTEXT *and* in SPLIT_INFO's g_SplitInfo[MAX_SPLITS] +// array, so anything added there gets silently multiplied by +// MAX_SPLITS. This struct alone is already ~19MB (the 5D alpha array +// dominates); embedded in COUNTERS it inflated the binary's BSS to +// ~1.47GB (confirmed via `size typhoon` + `nm --size-sort`) purely +// from g_SplitInfo's replication -- harmless at runtime since a +// --cpus 1 collection shard never touches g_SplitInfo's copies (pure +// never-faulted zero pages), but it broke valgrind's fixed-address +// mmap of the BSS segment outright, and would have silently dropped +// any data a helper thread collected during a real MP split (no +// merge-back code exists for these fields, unlike COUNTERS' other +// per-split fields) if collection were ever run with --cpus > 1. +// Giving it its own struct, referenced only from +// SEARCHER_THREAD_CONTEXT below, fixes both: SPLIT_INFO never carries +// a copy at all now. +typedef struct _FH_COLLECTION_COUNTERS +{ + // Alpha axes: [depth][dumb][gap][late][improve], same + // fPruneReduceEligible population as u64JointTries/FH (COUNTERS.tree) + // for late >= 2 (real per-move samples). late == 1 is the existing + // "early move" extrapolation proxy used live at the null-move-skip + // site (search.c) -- NOT collected here, it's a lookup-time + // fiction, never a real sample. late == 0 is reserved and, before + // this change, permanently unproduced (uMoveNum is 1-based and the + // per-move eligibility gate requires uLegalMoves > 1) -- repurposed + // here for a genuine NODE-level sample (hang pinned to 0, no + // specific move) answering "will SOMETHING at this node fail + // high", taken once per eligible node when iEval <= iAlpha, ground + // truth = the real move loop's iBestScore >= iBeta at Search()'s + // end: label. This is the alpha-side analog of the beta table's + // own whole-node sample (which only covers iEval > iBeta) -- + // together the two cover every null-window child's self-estimate + // with no gap, since alpha_child/beta_child are adjacent integers + // for a null window. See docs/NNUE.md's node-type discussion. + UINT64 u64Alpha5Tries[SEARCH_DEPTH_BUCKETS][HANG_CP_BUCKETS][GAP_CP_BUCKETS][MOVE_NUM_BUCKETS][IMPROVE_BUCKETS]; + UINT64 u64Alpha5FH[SEARCH_DEPTH_BUCKETS][HANG_CP_BUCKETS][GAP_CP_BUCKETS][MOVE_NUM_BUCKETS][IMPROVE_BUCKETS]; + // Beta axes: [depth][gapBeta][ownImprove][enemyImprove], one + // sample per eligible node (iEval > iBeta), same real-move-loop + // ground truth. Collecting this requires NOT taking the + // static-null-move shortcut's early `goto end` during collection + // builds (same reasoning as the LMP disable in searchsup.c) -- + // otherwise the current, soon-to-be-replaced data.c table would + // censor exactly the population this collection is trying to + // measure. + UINT64 u64BetaTries[SEARCH_DEPTH_BUCKETS][GAP_BETA_BUCKETS][IMPROVE_BUCKETS][IMPROVE_BUCKETS]; + UINT64 u64BetaFH[SEARCH_DEPTH_BUCKETS][GAP_BETA_BUCKETS][IMPROVE_BUCKETS][IMPROVE_BUCKETS]; + // 1D marginals, independent INC() call sites (not derived from the + // joints above) -- see marginal_crosscheck.py. The alpha table's + // depth/dumb/gap/late marginals already exist in COUNTERS.tree + // (u64DepthEstimateTries etc.) and are reused as-is for the 5D + // cross-check; only the improve marginal is new, here. + UINT64 u64ImproveEstimateTries[IMPROVE_BUCKETS]; + UINT64 u64ImproveEstimateFH[IMPROVE_BUCKETS]; + UINT64 u64BetaDepthEstimateTries[SEARCH_DEPTH_BUCKETS]; + UINT64 u64BetaDepthEstimateFH[SEARCH_DEPTH_BUCKETS]; + UINT64 u64BetaGapEstimateTries[GAP_BETA_BUCKETS]; + UINT64 u64BetaGapEstimateFH[GAP_BETA_BUCKETS]; + UINT64 u64BetaOwnImpEstimateTries[IMPROVE_BUCKETS]; + UINT64 u64BetaOwnImpEstimateFH[IMPROVE_BUCKETS]; + UINT64 u64BetaEnemyImpEstimateTries[IMPROVE_BUCKETS]; + UINT64 u64BetaEnemyImpEstimateFH[IMPROVE_BUCKETS]; +} +FH_COLLECTION_COUNTERS; +#endif // FH_TABLE_COLLECTION + typedef struct _SEARCHER_THREAD_CONTEXT { ULONG uPly; // its distance from root @@ -1985,6 +2055,13 @@ typedef struct _SEARCHER_THREAD_CONTEXT COOR cTrapped[MAX_PLY_PER_SEARCH]; // trapped piece hint PIECE pTrapped[MAX_PLY_PER_SEARCH]; COUNTERS sCounters; +#ifdef FH_TABLE_COLLECTION + // Deliberately NOT inside sCounters -- see FH_COLLECTION_COUNTERS' + // own comment (just above this struct) for why: sCounters is also + // embedded in SPLIT_INFO's g_SplitInfo[MAX_SPLITS] array, and + // anything added there gets silently multiplied by MAX_SPLITS. + FH_COLLECTION_COUNTERS sFhCollection; +#endif ULONG uThreadNumber; SPLIT_INFO *pSplitInfo[NUM_SPLIT_PTRS_IN_CONTEXT]; MOVE mvRootMove; @@ -2124,6 +2201,24 @@ typedef struct _GAME_OPTIONS UINT64 u64LMPPruned; UINT64 u64NullMoveTriesByR[NULL_R_BUCKETS]; UINT64 u64NullMoveSuccessByR[NULL_R_BUCKETS]; +#ifdef FH_TABLE_COLLECTION + // Mirrors SEARCHER_THREAD_CONTEXT's sCounters.tree fields of the + // same name -- see that struct's comment for the full design. + UINT64 u64Alpha5Tries[SEARCH_DEPTH_BUCKETS][HANG_CP_BUCKETS][GAP_CP_BUCKETS][MOVE_NUM_BUCKETS][IMPROVE_BUCKETS]; + UINT64 u64Alpha5FH[SEARCH_DEPTH_BUCKETS][HANG_CP_BUCKETS][GAP_CP_BUCKETS][MOVE_NUM_BUCKETS][IMPROVE_BUCKETS]; + UINT64 u64BetaTries[SEARCH_DEPTH_BUCKETS][GAP_BETA_BUCKETS][IMPROVE_BUCKETS][IMPROVE_BUCKETS]; + UINT64 u64BetaFH[SEARCH_DEPTH_BUCKETS][GAP_BETA_BUCKETS][IMPROVE_BUCKETS][IMPROVE_BUCKETS]; + UINT64 u64ImproveEstimateTries[IMPROVE_BUCKETS]; + UINT64 u64ImproveEstimateFH[IMPROVE_BUCKETS]; + UINT64 u64BetaDepthEstimateTries[SEARCH_DEPTH_BUCKETS]; + UINT64 u64BetaDepthEstimateFH[SEARCH_DEPTH_BUCKETS]; + UINT64 u64BetaGapEstimateTries[GAP_BETA_BUCKETS]; + UINT64 u64BetaGapEstimateFH[GAP_BETA_BUCKETS]; + UINT64 u64BetaOwnImpEstimateTries[IMPROVE_BUCKETS]; + UINT64 u64BetaOwnImpEstimateFH[IMPROVE_BUCKETS]; + UINT64 u64BetaEnemyImpEstimateTries[IMPROVE_BUCKETS]; + UINT64 u64BetaEnemyImpEstimateFH[IMPROVE_BUCKETS]; +#endif // FH_TABLE_COLLECTION #ifdef JOINT_EXTRA_VERIFY UINT64 u64JointExtraVerifyTries[JOINT_EXTRA_TIER_COUNT][POST_REDUCE_DEPTH_BUCKETS]; UINT64 u64JointExtraVerifySilentMiss[JOINT_EXTRA_TIER_COUNT][POST_REDUCE_DEPTH_BUCKETS]; |
