From 7857096f39e16619a42ee85b4aa593abd846b74a Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Wed, 26 Aug 2026 11:50:25 -0700 Subject: 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 --- src/eval.c | 171 ++++++++++++++++++++++++------------------------------------- 1 file changed, 68 insertions(+), 103 deletions(-) (limited to 'src/eval.c') diff --git a/src/eval.c b/src/eval.c index 92d1edc..876e882 100755 --- a/src/eval.c +++ b/src/eval.c @@ -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; - - // - // 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; + // 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)]; + + *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; -- cgit v1.3