diff options
| author | Scott Gasch <[email protected]> | 2026-08-26 11:50:25 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-08-26 11:50:25 -0700 |
| commit | 7857096f39e16619a42ee85b4aa593abd846b74a (patch) | |
| tree | ce697561b7e6c0978f53323256c0ab9038662e3e /src/eval.c | |
| parent | 01f5b71fd484801f7b06df3bfb0156f5bb69fef7 (diff) | |
Replace ctx->uPositional with a data-calibrated Eval() return value.
uPositional was a per-thread EWMA of abs(material - true score) used to
size lazy-eval and futility margins. It was history-derived (reflecting
whatever recent, unrelated positions looked like) rather than derived
from the position actually being margined, and its update/consumption
was tangled with EVAL_HASH (now disabled).
Eval() now takes an optional SCORE *piPositional out-param and fills it
in on every return path: exact (abs(material-delta)) on a full eval,
or an estimate from a new EstimatePositionalScore() on a lazy exit.
EstimatePositionalScore()'s two terms (king-safety-defect-bucketed, and
a flat residual for mobility/passers/everything else) are calibrated
from ~1.6M measured full-eval samples (p90 of the actual swing), not
guessed -- an initial guessed version measurably regressed ECM solve
rate (630 vs a 650 baseline at sn=4M); the recalibrated version is back
at parity (649/879).
search.c's qsearch futility now reads the value Eval() just computed
instead of the stale/shared ctx field. Also removes QSearchInDangerNoStandPat
and SideCanStandPat, dead since the danger-hash check that fed them was
already commented out (e08387a) -- they depended on the same enprise/
trapped-piece data this conversation is about to move off of
g_PositionHash entirely.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/eval.c')
| -rwxr-xr-x | src/eval.c | 169 |
1 files changed, 67 insertions, 102 deletions
@@ -2227,20 +2227,47 @@ Return value: static void -_QuicklyEstimateKingSafetyTerm(IN POSITION *pos, - IN OUT SCORE *piAlphaMargin, - IN OUT SCORE *piBetaMargin) +EstimatePositionalScore(IN POSITION *pos, + IN UNUSED PAWN_HASH_ENTRY *pHash, + IN OUT SCORE *piAlphaMargin, + IN OUT SCORE *piBetaMargin) /** Routine description: - Before doing an early lazy eval, look at the position and try to - quickly see if there is a large king safety positional component - to the score. + Before doing an early lazy eval, look at the position and widen + the alpha/beta margins to account for the positional terms that + have not been computed yet at this point in Eval() -- king safety + plus a flat residual covering mobility, passers, and everything + else that genuinely requires attack-generation to know exactly. + + Both terms below are calibrated from measured data (CALIBRATE_ + POSITIONAL instrumentation, ~1.6M full-eval samples over an ECM + slice), not guessed: for each value, p90 of the *actual* score + swing between this point in Eval() and full-eval completion, so + the resulting margin is wrong (too narrow) on at most ~10% of + calls, in either bucket. See conversation history for the + percentile tables -- if re-tuning, regenerate them, don't hand- + edit these numbers. + + Note: king safety and "everything else" turned out to be close to + independent of piece count and of each other, so summing their + two p90s (rather than deriving one joint p90) is a deliberately + conservative (wider than strictly necessary) combination. + + O(1) -- no attack bitboard generation -- so it's safe to call + whenever the cheap material-only lazy check (the caller's + first-pass margin) wasn't enough to resolve the cutoff on its own. Parameters: POSITION *pos + PAWN_HASH_ENTRY *pHash : unused now (kept for call-site symmetry); + pHash->iScore is already folded into pos->iScore by this point, + and the passer-specific estimate this used to compute turned + out to be negligible next to the residual term below. + SCORE *piAlphaMargin, *piBetaMargin : widened in place, identically + (no measured basis for an asymmetric alpha/beta split) Return value: @@ -2248,98 +2275,21 @@ Return value: **/ { - // - // IDEA: make this stuff dynamic like ctx->uPositional - // - static const SCORE iScoreByDefectCount[15] = { - 33, 50, 88, 146, 215, 245, 280, 430, 550, 630, 646, 646, 646, 646, 646 + // p90 of |true king-safety swing|, indexed by combined defect + // count (CountKingSafetyDefects(stm) + CountKingSafetyDefects(xsm)), + // clamped above index 10 (sparse data beyond that). + static const SCORE iKingSwingP90[11] = { + 47, 62, 87, 119, 169, 157, 181, 282, 342, 342, 385 }; - SCORE iPenaltyEst[2]; - SCORE iRet; - - ASSERT(CountKingSafetyDefects(pos, pos->uToMove) < 15); - iPenaltyEst[WHITE] = - iScoreByDefectCount[CountKingSafetyDefects(pos, WHITE)]; - ASSERT(iPenaltyEst[WHITE] > 0); - - ASSERT(CountKingSafetyDefects(pos, BLACK) < 15); - iPenaltyEst[BLACK] = - iScoreByDefectCount[CountKingSafetyDefects(pos, BLACK)]; - ASSERT(iPenaltyEst[BLACK] > 0); - - // - // For the beta margin, assume our king safety penalty will stand - // and our opponents will be less severe than we guess... net bonus - // to them / our bonus not as high. - // - iRet = iPenaltyEst[pos->uToMove] - iPenaltyEst[FLIP(pos->uToMove)] / 2; - iRet = MAX0(iRet); - ASSERT(iRet >= 0); - *piBetaMargin += iRet; + // p90 of |mobility + passers + everything else combined|, measured + // directly (no useful correlation found with piece count). + static const SCORE iResidualP90 = 154; + ULONG uDefects = (CountKingSafetyDefects(pos, WHITE) + + CountKingSafetyDefects(pos, BLACK)); + SCORE iKingTerm = iKingSwingP90[MINU(10, uDefects)]; - // - // For the alpha margin, assume our king safety penalty will not - // be too bad and our opponents will be as bad as we guess... net - // bonus to us / their offsetting bonus not as high. - // - iRet = iPenaltyEst[FLIP(pos->uToMove)] - iPenaltyEst[pos->uToMove] / 2; - iRet = MAX0(iRet); - ASSERT(iRet >= 0); - *piAlphaMargin += iRet; -} - - -static void -_QuicklyEstimatePasserBonuses(POSITION *pos, - PAWN_HASH_ENTRY *pHash, - SCORE *piAlphaMargin, - SCORE *piBetaMargin) -{ - SCORE iBonusEst[2] = {0, 0}; - SCORE iRet; - ULONG u; - - // - // Guess about the passer bonuses - // - if (pHash->bbPasserLocations[WHITE]) - { - u = pos->uNonPawnMaterial[BLACK] - VALUE_KING; - u /= VALUE_PAWN; - u = MINU(31, u); - ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0); - iBonusEst[WHITE] = - (CountBits(pHash->bbPasserLocations[WHITE]) * - PASSER_BONUS_AS_MATERIAL_COMES_OFF[u]); - } - if (pHash->bbPasserLocations[BLACK]) - { - u = pos->uNonPawnMaterial[WHITE] - VALUE_KING; - u /= VALUE_PAWN; - u = MINU(31, u); - ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0); - iBonusEst[BLACK] = - (CountBits(pHash->bbPasserLocations[BLACK]) * - PASSER_BONUS_AS_MATERIAL_COMES_OFF[u]); - } - - // - // For the beta margin, assume our passer bonus gets reduced and - // our opponent's is enhanced... net bonus to them / our bonus - // not as high. - // - iRet = iBonusEst[pos->uToMove] / 2 - iBonusEst[FLIP(pos->uToMove)]; - iRet = MAX0(iRet); - *piBetaMargin += iRet; - - // - // For the alpha bonus, assume our passer bonus gets doubled and - // our opponent's is reduced. Net bonus to us / their bonus not - // as high. - // - iRet = iBonusEst[pos->uToMove] - iBonusEst[FLIP(pos->uToMove)] / 2; - iRet = MAX0(iRet); - *piAlphaMargin += iRet; + *piAlphaMargin += iKingTerm + iResidualP90; + *piBetaMargin += iKingTerm + iResidualP90; } @@ -5533,7 +5483,8 @@ Return value: SCORE Eval(IN SEARCHER_THREAD_CONTEXT *ctx, IN SCORE iAlpha, - IN SCORE iBeta) + IN SCORE iBeta, + OUT SCORE *piPositional) /** Routine description: @@ -5543,6 +5494,11 @@ Parameters: SEARCHER_THREAD_CONTEXT *ctx, SCORE iAlpha, SCORE iBeta, + SCORE *piPositional : if non-NULL, filled in with a magnitude + (always >= 0) estimating the non-material component of the + score -- exact if a full eval ran, a cheap estimate otherwise. + Callers must treat it as an estimate either way; it's only + ever used to size a pruning margin, never as a hard fact. Return value: @@ -5693,7 +5649,7 @@ Return value: // We build two "margins" to account for these components of the // score. // - iAlphaMargin = iBetaMargin = (50 + (SCORE)ctx->uPositional); + iAlphaMargin = iBetaMargin = LAZY_EVAL_BASE_MARGIN; // // If (score + alpha_margin) is already > alpha -OR- @@ -5712,13 +5668,15 @@ Return value: // widen the margin further for king safety issues and passed // pawns. // - _QuicklyEstimateKingSafetyTerm(pos, &iAlphaMargin, &iBetaMargin); - _QuicklyEstimatePasserBonuses(pos, pHash, &iAlphaMargin, &iBetaMargin); + EstimatePositionalScore(pos, pHash, &iAlphaMargin, &iBetaMargin); if (iScoreForSideToMove + iAlphaMargin <= iAlpha) { - ctx->uPositional = MINU(200, ctx->uPositional); INC(ctx->sCounters.tree.u64LazyEvals); + if (NULL != piPositional) + { + *piPositional = iAlphaMargin; + } #ifdef EVAL_HASH // // The true eval can't reach iAlpha; that's a fact about @@ -5733,8 +5691,11 @@ Return value: } else if (iScoreForSideToMove - iBetaMargin >= iBeta) { - ctx->uPositional = MINU(200, ctx->uPositional); INC(ctx->sCounters.tree.u64LazyEvals); + if (NULL != piPositional) + { + *piPositional = iBetaMargin; + } #ifdef EVAL_HASH StoreEvalHash(ctx, iScoreForSideToMove, (SCORE)(iScoreForSideToMove - iBetaMargin), @@ -6101,6 +6062,10 @@ Return value: // Adjust dynamic positional component. // iAlphaMargin = abs(pos->iMaterialBalance[pos->uToMove] - iScoreForSideToMove); + if (NULL != piPositional) + { + *piPositional = iAlphaMargin; + } if ((ULONG)iAlphaMargin > ctx->uPositional) { ctx->uPositional = (ULONG)iAlphaMargin; |
