summaryrefslogtreecommitdiff
path: root/src/searchsup.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-25 10:16:16 -0700
committerScott Gasch <[email protected]>2026-10-25 10:16:16 -0700
commit3927a4cac6ec254a8e34a5e864300f75c3efc9f3 (patch)
treed34d06481bd57276b3ef50bc5513f5bd65950a74 /src/searchsup.c
parent874ef0478078ef4eb436af7d2b68a353f61f4d70 (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.c338
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;