diff options
| author | Scott Gasch <[email protected]> | 2026-10-25 09:55:30 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-25 09:55:30 -0700 |
| commit | 874ef0478078ef4eb436af7d2b68a353f61f4d70 (patch) | |
| tree | e9954c289393c64d88402beff04637b52732ca73 /src/chess.h | |
| parent | 5f901d737b17c5f99b6e456d6c0c7d5e3c093dc4 (diff) | |
Learned FH% logistic-regression model (alpha full, beta whole-node-only)
Replaces the data.c histogram tables (g_uJointFHThousandths/
g_uBetaFHThousandths and their _NNUE counterparts) with two quantized
logistic regressions as the default FH%-prediction mechanism under
FH_USE_REGRESSION_MODEL=1, trained against real collected search data
(fhcollect.c) per docs/REGRESSION.md.
Alpha side is a full per-move regression, validated against the old
table (0.119pp weighted mean deviation across 10,684 matched cells).
Beta side is whole-node-only (matching the old table's own scope --
no functionality lost), refit on the NO_MOVE population after an
earlier mixed-tier fit saturated and over-pruned; its skip threshold
(FH_BETA_SKIP_THRESHOLD=99000) was confirmed by direct self-play
sweeps, not just ECM solve-count intuition (see docs/REGRESSION.md's
"Category A vs B" discussion).
Gated by an overnight SPRT match (regression vs HEAD tables, same
NNUE eval, sd8): at 4120 games, llr=+2.53 (never dipped negative,
peaked +2.8), score=0.517, candidate reaching comparable depth at
higher nps/ebf than head. Did not cross the H1=+2.94 SPRT bound, but
the trend across the full run is unambiguous and the marginal value of
waiting out the remaining games was judged not worth it.
The data.c tables are left in place as the inactive #else branch of
FH_USE_REGRESSION_MODEL for now; a follow-up commit will flip the
build default and remove them once this commit's head_reference
baseline and ECM numbers are recorded.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/chess.h')
| -rwxr-xr-x | src/chess.h | 475 |
1 files changed, 384 insertions, 91 deletions
diff --git a/src/chess.h b/src/chess.h index 05052c6..0d5a4a1 100755 --- a/src/chess.h +++ b/src/chess.h @@ -22,6 +22,9 @@ Revision History: #define CHESS_ #include "compiler.h" +#include <stdio.h> // FILE -- needed early for FH_COLLECT_WRITER below; + // the rest of this file's own <stdio.h> include + // further down is now redundant but left alone // // Datatype wrappers @@ -2011,75 +2014,292 @@ 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 +// Forward declaration -- SEARCHER_THREAD_CONTEXT itself isn't defined +// until later in this header, but the FhCollect* prototypes below need +// to name a pointer to it at file scope (not just function-prototype +// scope, which is what a bare "struct _SEARCHER_THREAD_CONTEXT *" +// inside a parameter list alone would give it). +struct _SEARCHER_THREAD_CONTEXT; + +// 2026-10-24: per-event binary record collection for the learned FH% +// regression (replaces the discrete 5D-alpha/4D-beta histogram +// collection, FH_TABLE_COLLECTION -- see docs/REGRESSION.md). Each +// logged event is written directly to a per-thread binary file as one +// small fixed-width record; no shared/replicated array, no merge-back +// code needed for a real MP split. +// +// Records store RAW measured quantities only (gap in centipawns, +// remaining depth in quarter-plies, raw move ordinal, etc.) -- NOT +// derived training features (log transforms, normalized ratios, cross +// terms). Feature engineering happens in the offline training +// pipeline, where it's far cheaper to iterate on than in committed, +// compiled C. +// +// Call-site design: Search()/the move loop call the three FhCollect* +// hook functions below UNCONDITIONALLY, with no #ifdef of their own -- +// the enums, FH_COLLECT_PENDING, and the three hook prototypes are +// always visible (cheap: a handful of scalar fields, no large arrays) +// so callers never need to know or care whether this is a collection +// build. When FH_REGRESSION_COLLECTION isn't defined, the inline stubs +// in the #else branch below compile to nothing. This keeps the +// instrumentation entirely out of Search()'s own control flow -- it +// reads like any other helper-function call -- and keeps the machinery +// permanently in the tree (not a one-off diagnostic to rip out later) +// in case a future model needs the same kind of per-node/per-move +// hook. See fhcollect.c for the real implementation. +typedef enum _FH_COLLECT_TABLE +{ + FHC_TABLE_ALPHA = 0, // iEval <= iAlpha + FHC_TABLE_BETA = 1 // iEval >= iBeta +} +FH_COLLECT_TABLE; + +typedef enum _FH_COLLECT_TIER +{ + FHC_TIER_NO_MOVE = 0, // is_whole_node sentinel row -- no + // specific move, logged once per + // eligible node before the move loop + // starts + FHC_TIER_NULL, // the null-move probe + FHC_TIER_HASH, // the hash move + FHC_TIER_GOOD_CAPTURE, // winning/even capture (SEE >= 0) + FHC_TIER_KILLER, + FHC_TIER_COUNTERMOVE, + FHC_TIER_LEFTOVER_QUIET, + FHC_TIER_LOSING_CAPTURE // SEE < 0 +} +FH_COLLECT_TIER; + +// Per-node collection state, spanning from FhCollectNodeEntry (top of +// Search(), once iEval is known) through one FhCollectMoveTried call +// per move/null-move-probe tried, to FhCollectNodeExit (the end: label, +// once iBestScore's real fate is known). Declared as an ordinary LOCAL +// variable in Search() -- deliberately NOT stored in +// SEARCHER_THREAD_CONTEXT or PLY_INFO, since Search() recurses and a +// ctx-wide (or even uPly-indexed) copy would get clobbered by a child +// node's own FhCollectNodeEntry before the parent reaches +// FhCollectNodeExit. An ordinary C local, one per stack frame, is the +// simplest thing that's automatically correct across recursion. +typedef struct _FH_COLLECT_PENDING +{ + FLAG fEligible; // this node is eligible at all + // (iEval <= iAlpha or >= iBeta, + // MWS only) -- FALSE means every + // FhCollect* call below is a no-op + // for this node regardless of build + UCHAR bTable; // FH_COLLECT_TABLE, valid iff + // fEligible + UCHAR bNodeTypeGuess; // NODE_TYPE, as passed to + // FhCollectNodeEntry -- the caller's + // responsibility to pass the raw, + // PARENT-derived guess, never the + // refined one (see docs/REGRESSION.md) + SCORE iGap; + SCORE iImprove; + SCORE iEnemyOrOwnImprove; + ULONG uRemainingDepthTicks; + UINT64 u64PositionSig; + ULONG uGoodCapturesTriedNoCutoff; // running loop-state counter, + // incremented by + // FhCollectMoveTried itself as + // good/even captures are tried + ULONG uTotalNonPawnMaterial; // pos->uNonPawnMaterial[WHITE] + + // [BLACK] -- a coarse, + // essentially-free game-phase + // proxy, read straight off the + // position, no extra + // computation +} +FH_COLLECT_PENDING; + +#ifdef FH_REGRESSION_COLLECTION +void FhCollectNodeEntry(struct _SEARCHER_THREAD_CONTEXT *ctx, + FH_COLLECT_PENDING *pp, + SCORE iEval, SCORE iAlpha, SCORE iBeta, + ULONG uDepthTicks, + SCORE iImprove, SCORE iEnemyOrOwnImprove, + NODE_TYPE eNodeTypeGuess, + UINT64 u64PositionSig, + ULONG uTotalNonPawnMaterial); +void FhCollectMoveTried(struct _SEARCHER_THREAD_CONTEXT *ctx, + FH_COLLECT_PENDING *pp, + FH_COLLECT_TIER eTier, + SCORE iScore, SCORE iBeta, + ULONG uMoveOrdinal, SCORE iMoveValue, + ULONG uMoveFhPctThousandths, ULONG uMoveFhAttempts, + FLAG fMovesKnown, ULONG uTotalLegalMoves); +void FhCollectNodeExit(struct _SEARCHER_THREAD_CONTEXT *ctx, + FH_COLLECT_PENDING *pp, + SCORE iBestScore, SCORE iBeta, FLAG fAborted); +#else +// Non-collection build: every FhCollect* call in Search()/the move +// loop compiles down to nothing. Real bodies live in fhcollect.c, +// built only when FH_REGRESSION_COLLECTION is defined (GNUmakefile) -- +// these inline stubs are what let Search() call them unconditionally, +// with no #ifdef of its own. See this section's opening comment above. +static INLINE void +FhCollectNodeEntry(struct _SEARCHER_THREAD_CONTEXT *ctx, + FH_COLLECT_PENDING *pp, + SCORE iEval, SCORE iAlpha, SCORE iBeta, + ULONG uDepthTicks, + SCORE iImprove, SCORE iEnemyOrOwnImprove, + NODE_TYPE eNodeTypeGuess, + UINT64 u64PositionSig, + ULONG uTotalNonPawnMaterial) +{ + (void)(ctx); + (void)(pp); + (void)(iEval); + (void)(iAlpha); + (void)(iBeta); + (void)(uTotalNonPawnMaterial); + (void)(uDepthTicks); + (void)(iImprove); + (void)(iEnemyOrOwnImprove); + (void)(eNodeTypeGuess); + (void)(u64PositionSig); +} + +static INLINE void +FhCollectMoveTried(struct _SEARCHER_THREAD_CONTEXT *ctx, + FH_COLLECT_PENDING *pp, + FH_COLLECT_TIER eTier, + SCORE iScore, SCORE iBeta, + ULONG uMoveOrdinal, SCORE iMoveValue, + ULONG uMoveFhPctThousandths, ULONG uMoveFhAttempts, + FLAG fMovesKnown, ULONG uTotalLegalMoves) +{ + (void)(ctx); + (void)(pp); + (void)(eTier); + (void)(iScore); + (void)(iBeta); + (void)(uMoveOrdinal); + (void)(iMoveValue); + (void)(uMoveFhPctThousandths); + (void)(uMoveFhAttempts); + (void)(fMovesKnown); + (void)(uTotalLegalMoves); +} + +static INLINE void +FhCollectNodeExit(struct _SEARCHER_THREAD_CONTEXT *ctx, + FH_COLLECT_PENDING *pp, + SCORE iBestScore, SCORE iBeta, FLAG fAborted) +{ + (void)(ctx); + (void)(pp); + (void)(iBestScore); + (void)(iBeta); + (void)(fAborted); +} +#endif // FH_REGRESSION_COLLECTION + +// Decodes which tier a scored move belongs to, straight from +// generate.c's _ScoreAllMoves packed iValue sort key -- no separate +// tag is stored anywhere, so this is the one place that needs to know +// FIRST_KILLER/SECOND_KILLER/.../COUNTERMOVE_TIER_BONUS/SORT_THESE_FIRST's +// bit layout (see generate.c's _ScoreAllMoves and chess.h's own +// comments on those constants). Checked in the same priority order +// _ScoreAllMoves documents (killer-mate/winning-capture before killer +// before countermove) since a move can in principle satisfy more than +// one -- e.g. a stored killer that also clears the countermove- +// evidence threshold -- and the bits alone don't otherwise say which +// one search.c's own classification would treat as authoritative. +// Always a real declaration now (searchsup.c, 2026-10-24) -- needed +// unconditionally by fhmodel.c's production FH-regression inference, +// not just FH_REGRESSION_COLLECTION builds. +FH_COLLECT_TIER FhClassifyMoveTier(MOVE mv, MOVE mvHash, + FLAG fIsCaptureOrPromotion, + SCORE iValue); + +#ifdef FH_REGRESSION_COLLECTION +// One record per logged event (a whole-node row, or one row per +// move/null-move-probe tried at an eligible node). Fixed width, +// largest-alignment fields first to avoid padding surprises. +typedef struct _FH_COLLECT_RECORD +{ + UINT64 u64PositionSig; // pos->u64Sig at this node -- + // for dedup/debugging only, not + // a training feature + SCORE iGap; // signed: (iEval-iAlpha) on the + // alpha side, (iEval-iBeta) on + // the beta side -- always >= 0 + // by construction (this + // collection is MWS-only) + SCORE iMoveValue; // signed SEE/IID/hang-estimate + // centipawns; 0 for the + // FHC_TIER_NO_MOVE row + SCORE iImprove; // signed improving estimate + // (this side) + SCORE iEnemyOrOwnImprove; // alpha side: enemy-improving; + // beta side: own-improving -- + // semantics depend on bTable, + // see docs/REGRESSION.md + ULONG uRemainingDepthTicks; + ULONG uMoveOrdinal; // 1-based; 0 for NO_MOVE/NULL + ULONG uTotalLegalMoves; // only meaningful if + // bMovesKnown + ULONG uMoveFhPctThousandths; // this move's own Wilson-bound + // FH estimate, 0..100000; 0 for + // NO_MOVE + ULONG uMoveFhAttempts; // raw evidence count backing + // uMoveFhPctThousandths -- lets + // training distinguish a cold + // move identity (0 attempts, + // reads as confidently-safe by + // construction, see + // GetMoveFailHighAttempts' + // comment) from a genuinely + // well-evidenced one, which the + // Wilson bound alone cannot + ULONG uGoodCapturesTriedNoCutoff; // running loop-state counter -- + // generalizes the old + // IS_CAPTURE(mvHash) flag, see + // docs/REGRESSION.md + ULONG uTotalNonPawnMaterial; // pos->uNonPawnMaterial[WHITE] + + // [BLACK] at this node -- coarse + // game-phase proxy + UCHAR bTable; // FH_COLLECT_TABLE + UCHAR bTier; // FH_COLLECT_TIER + UCHAR bNodeTypeGuess; // NODE_TYPE, PARENT-derived + // only -- never the refined + // guess this model itself + // produces (would be leakage), + // see docs/REGRESSION.md + UCHAR bMovesKnown; // 0/1 -- paired with + // uTotalLegalMoves per + // docs/REGRESSION.md's + // moves_known/imputation design + UCHAR bLabel; // 0/1: did this resolve + // >= iBeta, committed at the + // same end: site the old + // histogram commit used + UCHAR bPad[3]; +} +FH_COLLECT_RECORD; + +// One per-thread buffered writer -- flushed in whole-batch writes to +// avoid a syscall per record. No cross-thread state: each thread owns +// its own file, opened once at collection-build startup +// (FhCollectOpen) and closed at shutdown (FhCollectClose), named by +// thread number so a --cpus > 1 collection run never collides on one +// fd. +#define FH_COLLECT_BUFFER_RECORDS (4096) +typedef struct _FH_COLLECT_WRITER { - // 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]; + FILE *pFile; + ULONG uBufferedCount; + FH_COLLECT_RECORD rgBuffer[FH_COLLECT_BUFFER_RECORDS]; } -FH_COLLECTION_COUNTERS; -#endif // FH_TABLE_COLLECTION +FH_COLLECT_WRITER; + +void FhCollectOpen(struct _SEARCHER_THREAD_CONTEXT *ctx, ULONG uThreadNumber); +void FhCollectFlush(struct _SEARCHER_THREAD_CONTEXT *ctx); +void FhCollectClose(struct _SEARCHER_THREAD_CONTEXT *ctx); +void FhCollectEmit(struct _SEARCHER_THREAD_CONTEXT *ctx, const FH_COLLECT_RECORD *prec); +void FhCollectCloseAll(void); +#endif // FH_REGRESSION_COLLECTION typedef struct _SEARCHER_THREAD_CONTEXT { @@ -2124,12 +2344,12 @@ 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; +#ifdef FH_REGRESSION_COLLECTION + // Deliberately NOT inside sCounters -- see FH_COLLECT_WRITER's own + // comment above for why that's no longer even a concern the way it + // was for the old histogram design (no shared/replicated array + // here to multiply across SPLIT_INFO in the first place). + FH_COLLECT_WRITER sFhCollectWriter; #endif ULONG uThreadNumber; SPLIT_INFO *pSplitInfo[NUM_SPLIT_PTRS_IN_CONTEXT]; @@ -2251,6 +2471,8 @@ typedef struct _GAME_OPTIONS UINT64 u64LeftoverTries; UINT64 u64LeftoverAlpha; UINT64 u64LeftoverFH; + UINT64 u64QuickNullMoves; + UINT64 u64QuickNullMoveSuccess; #ifdef HASHCAP_COLLECTION UINT64 u64HashCapTries[2][2]; UINT64 u64HashCapFH[2][2]; @@ -2274,24 +2496,6 @@ 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]; @@ -4370,6 +4574,9 @@ InitializeWilsonBoundTable(void); ULONG GetMoveFailHighWilsonUpperBoundThousandths(MOVE mv); +ULONG +GetMoveFailHighAttempts(MOVE mv); + FLAG CanWeReduceOrPruneMove(IN SEARCHER_THREAD_CONTEXT *ctx, IN MOVE mv, @@ -4443,9 +4650,76 @@ _RefineNodeTypeGuess(IN SEARCHER_THREAD_CONTEXT *ctx, IN SCORE iAlpha, IN SCORE iBeta, IN ULONG uDepth, + IN SCORE iImprovement, IN SCORE iEnemyImprovement, IN ULONG uRawImproveB); +// fhmodel.c -- production inference for the FH% logistic-regression +// models (FH_USE_REGRESSION_MODEL), see docs/REGRESSION.md. No +// separate header -- matches fhcollect.c's own convention of +// declaring everything here rather than a per-module header. +// +// Fully quantized fixed-point integer arithmetic -- no floats, no +// runtime log()/exp() calls. +// +// HOISTED node-level/move-level split (alpha side): `gap` (and every +// other node-level term -- depth, improve, enemy_improve, material, +// node_type) is fixed for this node's ENTIRE move loop, so it's +// computed exactly ONCE per node (FhModelAlphaNodeBaseQ16, called at +// the same point FhCollectNodeEntry already is, in search.c) rather +// than recomputed inside SearchOneMove on every move; SearchOneMove +// then only adds the genuinely move-varying terms. FHM_MATE_GATED +// mirrors JOINT_TABLE_SENTINEL/MOVE_FH_ESTIMATE_NO_DATA's existing +// "no data" convention (same numeric value, 0xFFFF) -- callers +// already handle that gracefully everywhere. +// +// Beta is whole-node only (the old beta table was never per-move, and +// its sole production call site -- _RefineNodeTypeGuess -- is itself +// a whole-node question), so it has no separate per-move function. +#define FHM_MATE_GATED (MOVE_FH_ESTIMATE_NO_DATA) +typedef INT FHM_NODE_BASE; + +FHM_NODE_BASE +FhModelAlphaNodeBaseQ16(IN SCORE iEval, + IN SCORE iAlpha, + IN ULONG uDepth, + IN SCORE iImprovement, + IN SCORE iEnemyImprovement, + IN ULONG uTotalNonPawnMaterial, + IN NODE_TYPE eNodeTypeGuess); + +static INLINE FLAG +FhModelNodeBaseIsMateGated(FHM_NODE_BASE base) +{ + return(FHM_MATE_GATED == (ULONG)base); +} + +ULONG +FhModelAlphaMoveThousandths(IN FHM_NODE_BASE nodeBase, + IN SCORE iMoveValue, + IN ULONG uMoveOrdinal, + IN ULONG uMoveFhPctThousandths, + IN ULONG uMoveFhAttempts, + IN FH_COLLECT_TIER eTier); + +ULONG +FhModelBetaPredictedFHThousandths(IN SCORE iEval, + IN SCORE iBeta, + IN ULONG uDepth, + IN SCORE iImprovement, + IN SCORE iEnemyImprovement, + IN ULONG uTotalNonPawnMaterial, + IN NODE_TYPE eNodeTypeGuess); + +// Classifies a move's tier using this engine's own x==0-means-hash- +// move convention (no separately stored mvHash exists anywhere to +// compare against -- confirmed by grep) rather than +// FhClassifyMoveTier's mv-vs-mvHash comparison. iMoveSortValue is the +// move's packed _ScoreAllMoves key (ctx->sMoveStack.mvf[x-1].iValue +// at the real call site). +FH_COLLECT_TIER +FhModelTierForMove(IN ULONG x, IN MOVE mv, IN SCORE iMoveSortValue); + FLAG WeShouldTryNullmovePruning(IN SEARCHER_THREAD_CONTEXT *ctx, IN SCORE iAlpha, @@ -4555,6 +4829,12 @@ SearchOneMove(IN SEARCHER_THREAD_CONTEXT *ctx, IN ULONG uRawImproveB, // caller computes once, not per-move IN BITBOARD bbMinorAttacksSnapshot[2], IN BITBOARD bbRookAttacksSnapshot[2], +#ifdef FH_USE_REGRESSION_MODEL + IN FHM_NODE_BASE sFhAlphaNodeBase, // hoisted node-level + // precompute, same loop-invariant + // convention as uRawDepthB/ + // uRawImproveB above +#endif #ifdef MOVE_ORDERING_EVIDENCE_COLLECTION IN FLAG fThisMoveIsCountermoveMatch, IN ULONG uCMEvidenceBucket, @@ -4573,6 +4853,19 @@ SearchOneMove(IN SEARCHER_THREAD_CONTEXT *ctx, , OUT ULONG *puThisMoveNumBucket , OUT ULONG *puThisMoveDepthBucket #endif +#ifdef FH_REGRESSION_COLLECTION + // EstimateHangingMaterialCp (searchsup.c) asserts the + // move is already applied on the board -- it can only + // be called from inside this function's own Make/ + // Unmake window, never by a caller after this function + // has already returned (and un-made the move). Computed + // once here, for every move (not just the leftover- + // eligible population PERF_COUNTERS' own hang bucket + // above is restricted to), and handed back for the + // caller's FhCollectMoveTried call. See + // docs/REGRESSION.md's session note on this bug. + , OUT SCORE *piMoveValueForFhCollect +#endif ); SCORE |
