diff options
| author | Scott Gasch <[email protected]> | 2026-10-25 10:16:16 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-25 10:16:16 -0700 |
| commit | 3927a4cac6ec254a8e34a5e864300f75c3efc9f3 (patch) | |
| tree | d34d06481bd57276b3ef50bc5513f5bd65950a74 /src/searchsup.c | |
| parent | 874ef0478078ef4eb436af7d2b68a353f61f4d70 (diff) | |
Make the FH% regression model the default; remove data.c's retired tables
Flips FH_USE_REGRESSION_MODEL from an opt-in safety toggle to the only
behavior: fhmodel.c's quantized logistic regressions are now always
used, with no build-time choice left between them and the old
histogram tables. Removes g_uJointFHThousandths/g_uBetaFHThousandths
and their _NNUE counterparts from data.c (4085 lines), their extern
declarations from chess.h, and the now-dead
_JointTablePredictedFHThousandths/_BetaTablePredictedFHThousandths
lookup functions and their supporting bucket helpers
(HangCpBucket/GapCpBucket/MoveNumBucket/DepthBucket/ImproveBucket/
GapBetaBucket/_ShipDepthBucket/_ShipLateBucket) from searchsup.c, along
with the uRawDepthB/uRawImproveB plumbing that only existed to feed
them (search.c, split.c, SearchOneMove's parameter list).
Also drops the beta-side static-nullmove skip threshold's provisional-
96000 #else branch -- 99000 is now simply the default, confirmed (not
provisional) by docs/REGRESSION.md's trust-audit sweep (+291 Elo over
96000 at 200 games, LOWER95=0.79).
Verified: clean release build, clean DEBUG=1 TEST=1 self-test run (no
asserts), and a fresh head_reference/ checkpoint
(head-reference-20261025 -> 874ef04's regression-model binary) with
sd10/sn5000000 solve counts on the three curated suites essentially
matching the previous table-based checkpoint (70/191 vs 71/191 sd10,
93/191 vs 96/191 sn5M).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/searchsup.c')
| -rw-r--r-- | src/searchsup.c | 338 |
1 files changed, 26 insertions, 312 deletions
diff --git a/src/searchsup.c b/src/searchsup.c index 0bddc31..dc3529a 100644 --- a/src/searchsup.c +++ b/src/searchsup.c @@ -396,160 +396,6 @@ Return value: return 0; } -// See chess.h's HANG_CP_BUCKETS comment for the bucket shape. -// -// 2026-10-04: was ASSERT(iHangCp >= 0), on the belief that -// EstimateHangingMaterialCp (its only real producer) could never -// return a negative value. That ASSERT is wrong, not just stale -- see -// EstimateHangingMaterialCp's own return-value comment: IID rescoring -// can legitimately make it negative (a capture SEE-sign flagged as -// losing, but which a real shallow search found is actually the best -// move in the position). A negative input just means "even more -// definitely not hung" than 0 already does, so it belongs in the same -// bucket 0 -- clamp rather than assert. -ULONG -HangCpBucket(SCORE iHangCp) -{ - ULONG u; - - if (iHangCp <= 0) - { - return 0; - } - u = 1 + (((ULONG)iHangCp - 1) / HANG_CP_BUCKET_WIDTH); - return MINU(u, HANG_CP_BUCKETS - 1); -} - -// See chess.h's GAP_CP_BUCKETS comment for the bucket shape. Unlike -// HangCpBucket, the input can legitimately be negative or zero (this -// node's own eval already meets or beats alpha) -- that whole -// non-positive range collapses into bucket 0, same as HANG's "not -// dumb". -ULONG -GapCpBucket(SCORE iGapCp) -{ - ULONG u; - - if (iGapCp <= 0) - { - return 0; - } - u = 1 + (((ULONG)iGapCp - 1) / GAP_CP_BUCKET_WIDTH); - return MINU(u, GAP_CP_BUCKETS - 1); -} - -// See chess.h's IMPROVE_BUCKETS comment for the bucket shape. Unlike -// HangCpBucket/GapCpBucket, there is no single "safe/below-threshold" -// catchall here -- iImprovement is genuinely signed and two-sided (a -// node legitimately being worse than two plies ago is an expected, -// common outcome, not a degenerate corner case), so this buckets both -// tails symmetrically around a center "didn't really move" bucket -// instead of collapsing one whole sign onto bucket 0. -ULONG -ImproveBucket(SCORE iImprovement) -{ - // Center bucket (index 5 of 11): "didn't really move" either way. - if ((iImprovement > -IMPROVE_CP_CENTER_WIDTH) && - (iImprovement < IMPROVE_CP_CENTER_WIDTH)) - { - return 5; - } - if (iImprovement > 0) - { - // 6..10: getting better, 100cp-wide bins, 10 is the 500cp+ - // overflow. - ULONG u = 6 + (((ULONG)iImprovement - IMPROVE_CP_CENTER_WIDTH) / - IMPROVE_BUCKET_WIDTH); - return MINU(u, IMPROVE_BUCKETS - 1); - } - // 0..4: getting worse, 100cp-wide bins, 0 is the 500cp+ overflow - // (most negative == smallest index, mirroring 6..10's direction). - { - ULONG uMag = (ULONG)(-iImprovement) - IMPROVE_CP_CENTER_WIDTH; - ULONG u = 4 - (uMag / IMPROVE_BUCKET_WIDTH); - - if (uMag / IMPROVE_BUCKET_WIDTH >= 5) - { - return 0; - } - return u; - } -} - -// See chess.h's GAP_BETA_BUCKETS comment. Unlike ImproveBucket, this -// is called only where the caller has already gated iEval > iBeta -// (the static-NM/beta-table's own eligibility), so iGapBeta is always -// >= 0 here -- no symmetric/center bucket needed, just plain -// 100cp-wide bins from 0, wide enough (2000cp) to find where FH% -// actually saturates. -ULONG -GapBetaBucket(SCORE iGapBeta) -{ - ULONG u = (ULONG)iGapBeta / GAP_BETA_BUCKET_WIDTH; - return MINU(u, GAP_BETA_BUCKETS - 1); -} - - -// See chess.h's MOVE_NUM_BUCKETS comment. uMoveNum is 1-based (the -// caller passes uLegalMoves, right after MakeMove succeeded) -- index -// 0 is never produced. -ULONG -MoveNumBucket(ULONG uMoveNum) -{ - ASSERT(uMoveNum >= 1); - return MINU(uMoveNum, MOVE_NUM_BUCKETS - 1); -} - -// See chess.h's SEARCH_DEPTH_BUCKETS comment. uCandDepth is uDepth / -// ONE_PLY, whole plies, legitimately 0 or above. -ULONG -DepthBucket(ULONG uCandDepth) -{ - return MINU(uCandDepth, SEARCH_DEPTH_BUCKETS - 1); -} - -// Maps this project's raw, marginal-resolution bucket indices (exactly -// what DepthBucket/MoveNumBucket produce, and what the 2026-09-28 -// collection run counted against) down to the coarser, cache-shaped -// indices g_uJointFHThousandths was actually baked at. dumb and gap -// pass through unchanged (shipped shape kept them at full marginal -// resolution -- see the plan doc's table). Must stay in exact lockstep -// with bake_joint.py's ship_depth()/ship_late() -- these are the same -// mapping, just re-derived in C for the live lookup path. File-private -// -- only the prediction lookup just below needs the shipped shape; -// search.c's own collection instrumentation counts at full raw -// resolution and never calls these. -static ULONG -_ShipDepthBucket(ULONG uRawDepthBucket) -{ - if (uRawDepthBucket <= 9) return uRawDepthBucket; - if (uRawDepthBucket <= 13) return 10; - return 11; -} - -static ULONG -_ShipLateBucket(ULONG uRawLateBucket) -{ - // 2026-10-21: rewritten to match dual_table_run{,_nnue}/dashboard.py's - // real, data-derived ship_late_17() exactly -- this function had - // drifted out of sync with the actual shipped shape baked into - // data.c (it still had the old 2026-09-30, pre-derive_late_buckets.py - // 15-bucket boundaries; the live table has been 16/17-bucket-shaped - // for a while). MUST stay in exact lockstep with ship_late_17() -- - // see that function's own comment for the full raw-bucket story - // (why raw==0 gets its own dedicated slot now, why raw==1 is - // permanently unreachable and harmlessly lumped with raw==2). - if (0 == uRawLateBucket) return 0; - if (uRawLateBucket <= 1) return 1; - if (uRawLateBucket <= 11) return uRawLateBucket - 1; - if (uRawLateBucket <= 13) return 11; - if (uRawLateBucket <= 15) return 12; - if (uRawLateBucket <= 18) return 13; - if (uRawLateBucket <= 21) return 14; - if (uRawLateBucket <= 31) return 15; - return 16; -} - #define JOINT_TABLE_SENTINEL (0xFFFF) // Decodes generate.c's _ScoreAllMoves packed iValue sort key back @@ -602,96 +448,6 @@ FhClassifyMoveTier(MOVE mv, MOVE mvHash, FLAG fIsCaptureOrPromotion, return(FHC_TIER_LEFTOVER_QUIET); } -// Looks up this move's predicted FH% (thousandths of a percent) from -// the baked joint table, given the four RAW bucket indices (as -// HangCpBucket/GapCpBucket/MoveNumBucket/DepthBucket produce them). -// Returns JOINT_TABLE_SENTINEL both for a genuine below-sample-floor -// cell AND for dumb bucket 1 (1-100cp "hung") -- traced to ~97% -// SEE-scored losing captures/underpromotions misfiled into the -// quiet-move axis (2026-09-28 plan doc), not a real "this move looks -// safe" signal. Treating it as "no data" rather than trusting its -// literal (low) value means it can never justify extra reduction, nor -// withhold it -- it just falls back to whatever the other checks say. -USHORT -_JointTablePredictedFHThousandths(IN ULONG uRawDepthBucket, - IN ULONG uRawDumbBucket, - IN ULONG uRawGapBucket, - IN ULONG uRawLateBucket, - IN ULONG uRawImproveBucket) -{ - if (1 == uRawDumbBucket) - { - return JOINT_TABLE_SENTINEL; - } - // 2026-10-21: two tables compiled in (HCE-collected and NNUE- - // collected, see chess.h's g_uJointFHThousandths_NNUE comment) -- - // runtime-selected, not a build-time #ifdef, since NNUE eval - // itself is already a runtime switch in this binary. Both flags - // checked (not just g_fUseNnueEval) for the same reason eval.c's - // own NNUE dispatch does: g_fUseNnueEval alone doesn't guarantee - // real weights are behind it. - if (g_fUseNnueEval && g_fNnueWeightsLoaded) - { - return g_uJointFHThousandths_NNUE[_ShipDepthBucket(uRawDepthBucket)] - [uRawDumbBucket] - [uRawGapBucket] - [_ShipLateBucket(uRawLateBucket)] - [uRawImproveBucket]; - } - return g_uJointFHThousandths[_ShipDepthBucket(uRawDepthBucket)] - [uRawDumbBucket] - [uRawGapBucket] - [_ShipLateBucket(uRawLateBucket)] - [uRawImproveBucket]; -} - -// Looks up this node's predicted FH% (thousandths of a percent, i.e. -// 100000 == 100.0%) from g_uBetaFHThousandths, given the raw depth -// bucket (shipped via _ShipDepthBucket same as the alpha lookup -// above) and the gap/own-improve/enemy-improve buckets. Caller is -// responsible for eligibility (iEval > iBeta, etc.) -- this function -// does no gating of its own and returns JOINT_TABLE_SENTINEL (0xFFFF) -// for any below-floor cell regardless. -// -// g_uBetaFHThousandths itself stores the GAP TO 100% (thousandths of -// a percentage point), not FH% directly -- a USHORT can't hold -// FH%*1000 up to 100000, so the direct encoding would saturate every -// cell above FH%=65.534% into one bucket, right where this table is -// actually read. Storing the complement instead keeps full -// resolution across the near-100% range that matters. This function -// re-inverts back to a true FH%*1000 before returning, so callers get -// ordinary "bigger == more confident" semantics. -ULONG -_BetaTablePredictedFHThousandths(IN ULONG uRawDepthBucket, - IN ULONG uGapBetaBucket, - IN ULONG uOwnImproveBucket, - IN ULONG uEnemyImproveBucket) -{ - USHORT uRawGap; - - // Same runtime HCE/NNUE table selection as - // _JointTablePredictedFHThousandths above -- see its comment. - if (g_fUseNnueEval && g_fNnueWeightsLoaded) - { - uRawGap = g_uBetaFHThousandths_NNUE[_ShipDepthBucket(uRawDepthBucket)] - [uGapBetaBucket] - [uOwnImproveBucket] - [uEnemyImproveBucket]; - } - else - { - uRawGap = g_uBetaFHThousandths[_ShipDepthBucket(uRawDepthBucket)] - [uGapBetaBucket] - [uOwnImproveBucket] - [uEnemyImproveBucket]; - } - if (JOINT_TABLE_SENTINEL == uRawGap) - { - return JOINT_TABLE_SENTINEL; - } - return (ULONG)(100000 - uRawGap); -} - // Buckets a predicted-FH%-thousandths value (from either table above, // or one of the two PLY_INFO.uParentMoveFHEstimate sentinels) into one // of NODE_TYPE_FH_BANDS coarse bands for g_NodeTypeGuessTable's (data.c) @@ -712,10 +468,10 @@ FHThousandthsToBand(IN ULONG uFHThousandths) // Refines CommonSearchInit's ancestor-only NODE_TYPE guess (above, // this file) using two more signals that aren't available yet at // CommonSearchInit time (before iEval is computed): this node's own -// predicted FH% (from whichever of the alpha/beta tables applies -- -// the same two call sites Search()'s nullmove block already consults -// them from) and the PARENT's predicted FH% for the move that led -// here (PLY_INFO.uParentMoveFHEstimate, chess.h -- written by this +// predicted FH% (from whichever of fhmodel.c's alpha/beta regressions +// applies -- the same two call sites Search()'s nullmove block already +// consults them from) and the PARENT's predicted FH% for the move that +// led here (PLY_INFO.uParentMoveFHEstimate, chess.h -- written by this // file's SearchOneMove before it recursed into this node). See // data.c's g_NodeTypeGuessTable for how the three signals combine and // the measurement behind it (90.7% vs today's 72.3%, on the @@ -727,12 +483,12 @@ FHThousandthsToBand(IN ULONG uFHThousandths) // Returns the local (this-node, non-PV-only) predicted FH thousandths // it just computed -- MOVE_FH_ESTIMATE_NO_DATA if neither the beta nor // alpha branch applied (including the PV early-return). Search()'s own -// static-nullmove/LMR-skip blocks need the exact same table lookup, -// keyed off the exact same (iEval>iBeta) / (iEval<=iAlpha) branch and -// the exact same bucket inputs -- returning it here lets the caller -// reuse it instead of redoing the same data.c table consult a second -// time per node. This is the hottest function in the engine by -// profiler; don't reintroduce the duplicate lookup. +// static-nullmove/LMR-skip blocks need the exact same model inference, +// keyed off the exact same (iEval>iBeta) / (iEval<=iAlpha) branch -- +// returning it here lets the caller reuse it instead of redoing the +// same fhmodel.c inference a second time per node. This is the hottest +// function in the engine by profiler; don't reintroduce the duplicate +// lookup. ULONG _RefineNodeTypeGuess(SEARCHER_THREAD_CONTEXT *ctx, SCORE iEval, @@ -740,14 +496,11 @@ _RefineNodeTypeGuess(SEARCHER_THREAD_CONTEXT *ctx, SCORE iBeta, ULONG uDepth, SCORE iImprovement, - SCORE iEnemyImprovement, - ULONG uRawImproveB) + SCORE iEnemyImprovement) { PLY_INFO *pi = &ctx->sPlyInfo[ctx->uPly]; ULONG uAncestorGuess; ULONG uLocalFH = MOVE_FH_ESTIMATE_NO_DATA; - ULONG uDepthInPlies; -#ifdef FH_USE_REGRESSION_MODEL // Parent-derived guess, read BEFORE this function's own // assignment below overwrites pi->eNodeType -- same leakage // concern as search.c's eNodeTypeGuessParent capture (feeding the @@ -759,27 +512,17 @@ _RefineNodeTypeGuess(SEARCHER_THREAD_CONTEXT *ctx, ULONG uTotalNonPawnMaterial = (ctx->sPosition.uNonPawnMaterial[WHITE] - VALUE_KING) + (ctx->sPosition.uNonPawnMaterial[BLACK] - VALUE_KING); -#endif if (NODE_PV == pi->eNodeType) { return(uLocalFH); } uAncestorGuess = (NODE_CUT == pi->eNodeType) ? 0 : 1; - uDepthInPlies = uDepth / ONE_PLY; if (iEval > iBeta) { -#ifdef FH_USE_REGRESSION_MODEL uLocalFH = FhModelBetaPredictedFHThousandths( iEval, iBeta, uDepth, iImprovement, iEnemyImprovement, uTotalNonPawnMaterial, eNodeTypeGuessParent); -#else - uLocalFH = _BetaTablePredictedFHThousandths( - DepthBucket(uDepthInPlies), - GapBetaBucket((SCORE)(iEval - iBeta)), - uRawImproveB, - ImproveBucket(iEnemyImprovement)); -#endif } else if (iEval <= iAlpha) { @@ -789,23 +532,12 @@ _RefineNodeTypeGuess(SEARCHER_THREAD_CONTEXT *ctx, // forced to NO_MOVE, not a real per-move tier guess). The // actual per-move alpha prediction (SearchOneMove, below) is // a separate, later call with real move-level values. -#ifdef FH_USE_REGRESSION_MODEL - { - FHM_NODE_BASE sBase = FhModelAlphaNodeBaseQ16( - iEval, iAlpha, uDepth, iImprovement, iEnemyImprovement, - uTotalNonPawnMaterial, eNodeTypeGuessParent); - uLocalFH = FhModelNodeBaseIsMateGated(sBase) ? - MOVE_FH_ESTIMATE_NO_DATA : - FhModelAlphaMoveThousandths(sBase, 0, 0, 0, 0, FHC_TIER_NO_MOVE); - } -#else - uLocalFH = _JointTablePredictedFHThousandths( - DepthBucket(uDepthInPlies), - 0, // hang bucket: assume nothing hung - GapCpBucket((SCORE)(iAlpha - iEval)), - 0, // late bucket: real whole-node sample - uRawImproveB); -#endif + FHM_NODE_BASE sBase = FhModelAlphaNodeBaseQ16( + iEval, iAlpha, uDepth, iImprovement, iEnemyImprovement, + uTotalNonPawnMaterial, eNodeTypeGuessParent); + uLocalFH = FhModelNodeBaseIsMateGated(sBase) ? + MOVE_FH_ESTIMATE_NO_DATA : + FhModelAlphaMoveThousandths(sBase, 0, 0, 0, 0, FHC_TIER_NO_MOVE); } pi->eNodeType = g_NodeTypeGuessTable @@ -2002,13 +1734,9 @@ SearchOneMove(IN SEARCHER_THREAD_CONTEXT *ctx, IN FLAG fIsLeftoverMove, IN FLAG fThreat, IN FLAG fFullWindow, - IN ULONG uRawDepthB, - IN ULONG uRawImproveB, IN BITBOARD bbMinorAttacksSnapshot[2], IN BITBOARD bbRookAttacksSnapshot[2], -#ifdef FH_USE_REGRESSION_MODEL IN FHM_NODE_BASE sFhAlphaNodeBase, -#endif #ifdef MOVE_ORDERING_EVIDENCE_COLLECTION IN FLAG fThisMoveIsCountermoveMatch, IN ULONG uCMEvidenceBucket, @@ -2177,37 +1905,30 @@ Return value: ULONG uFHAttempts = 0; ULONG uFHPctThousandths = GetMoveFailHighPctThousandths(mv, &uFHAttempts); - // Our 5D prediction table, given: - // - the gap from iEval to iAlpha here, - // - move lateness (number), - // - board material apparently hung by mv, - // - remaining depth to search, and - // - improvement... - // - // ...it will give us a predicted FH% for the move. + // fhmodel.c's alpha regression, given the move's own hang + // estimate plus the node-level base (sFhAlphaNodeBase: gap, + // depth, improve, material, node type -- all hoisted above, + // see this function's own parameter comment) and this move's + // tier/ordinal, gives a predicted FH% for the move. - ULONG uRawGapB = GapCpBucket((SCORE)(iAlpha - iEval)); - ULONG uRawLateB = MoveNumBucket(*puLegalMoves); SCORE iHangCp = EstimateHangingMaterialCp(ctx, mv, (x - 1), bbMinorAttacksSnapshot, bbRookAttacksSnapshot); - ULONG uRawDumbB = HangCpBucket(iHangCp); // Note: A normal fIsLeftoverMove move has a FH% rate // of about 0.4% but this is misleading -- the // population is actually bimodal, split between moves // near 0% and moves legitimately much higher. ULONG uPredictedFH; -#ifdef FH_USE_REGRESSION_MODEL // sFhAlphaNodeBase was computed ONCE for this whole node // (search.c, same point FhCollectNodeEntry is called) -- // every move in this loop just adds its own move-level terms // to that same precomputed base, rather than recomputing the // node-level terms (gap, depth, improve, material, node type) - // on every move. FhModelNodeBaseIsMateGated mirrors the old - // table's JOINT_TABLE_SENTINEL/MOVE_FH_ESTIMATE_NO_DATA "no - // data" convention -- callers below already handle that - // value gracefully, no new branching needed there. + // on every move. FhModelNodeBaseIsMateGated mirrors + // MOVE_FH_ESTIMATE_NO_DATA's "no data" convention -- callers + // below already handle that value gracefully, no new + // branching needed there. if (TRUE == FhModelNodeBaseIsMateGated(sFhAlphaNodeBase)) { uPredictedFH = MOVE_FH_ESTIMATE_NO_DATA; @@ -2220,13 +1941,6 @@ Return value: sFhAlphaNodeBase, iHangCp, *puLegalMoves, uFHPctThousandths, uFHAttempts, eTier); } -#else - uPredictedFH = _JointTablePredictedFHThousandths(uRawDepthB, - uRawDumbB, - uRawGapB, - uRawLateB, - uRawImproveB); -#endif // JOINT_TABLE_SENTINEL and MOVE_FH_ESTIMATE_NO_DATA are the // same numeric value on purpose -- see both macros' comments. uThisMoveFHEstimate = uPredictedFH; |
