From 9e995e7c39a83ae9b5ba86f3346e0281744bf773 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 5 Sep 2026 21:52:40 -0700 Subject: King-safety recalibration, lazy-eval material floor, eval hot-path trimming Recalibrate iKingSwingP90 against the bitboard-rewritten CountKingSafetyDefects (~1.28B samples via new CALIBRATE_POSITIONAL/ CALIBRATE_BASE_MARGIN/CALIBRATE_MARGIN_SAFETY diagnostic build flags, board_representation/EVAL.md section 9). Add LAZY_EVAL_MIN_MATERIAL: measured the regular lazy exit's real swing exceeding its own assumed margin 20.6% of the time in near-bare-king endgames (vs <=0.36% elsewhere) -- skip lazy eval entirely below that material floor. Double the stale search.c/searchsup.c CountKingSafetyDefects extension thresholds as a stopgap pending their own recalibration. Eval hot-path trimming (measured via EVAL_TIME, ~1759 -> ~1386 avg cycles/eval on a representative middlegame position): - Pull _GetFileStormDefects out of EstimatePositionalScore's hot path (cost more than the "cheap cached lookup" it was assumed to be, running on ~90% of all Eval() calls). - Add pos->bbOccupiedSide[2], incrementally maintained alongside bbOccupied, so _BuildFriendlySideBB is a field read instead of a 6-term OR. - Switch CoorFromBitBoardRank8ToRank1/Rank1ToRank8 to the existing static-inline FastFirstBit/FastLastBit (same bsf/bsr instruction, no call/ret overhead). - Defer EvalPasserRaces' uRacerDist/fDontCountMeOut past its no-passer early return. - Remove the mailbox-era "max mobility in a row" term from _EvalBishop/_EvalRook (no bitboard-mobility equivalent need for it). - Simplify _EvalBishopPairs and rook file-openness/passer bonuses to flat DNA-tunable constants instead of distance/pawn-count-scaled tables, rook file-openness now a branchless bitboard-indexed lookup. - Remove pos->cPiece (write-only, no reader anywhere). - Collapse WHITE/BLACK mirror-branches (castle-rights block, rook-trapped-in-corner) to color-indexed constants. - Close the PAWN_BIT..KING_BIT gap (bits 7-3 -> bits 4-0), removing the bvPattern >>= 3 before its KING_COUNTER_BY_ATTACK_PATTERN lookup. This also fixes a real bug introduced earlier this session when _WhoControlsSquareFast was converted to read these constants directly: g_SwapTable is only [32][32], but the old bit values (up to 0xF8) indexed far out of bounds on any attacked square -- data.c's InitializeSwapTable was always built assuming the bits 0-4 range this change now actually produces. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01XxmVi2sTMwpPp4i6WYFjan --- src/chess.h | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 109 insertions(+), 9 deletions(-) (limited to 'src/chess.h') diff --git a/src/chess.h b/src/chess.h index acab153..8d16837 100755 --- a/src/chess.h +++ b/src/chess.h @@ -560,13 +560,17 @@ typedef union _MOVE // pattern from those same bbXAttacks reads, then index // KING_COUNTER_BY_ATTACK_PATTERN/g_SwapTable with it. Kept exactly as // before; only their old *storage* mechanism (ATTACK_BITV) is gone. -#define PAWN_BIT 0x00000080UL -#define MINOR_BIT 0x00000040UL -#define ROOK_BIT 0x00000020UL -#define QUEEN_BIT 0x00000010UL +// Bits 1-4 (2026-09-06: were 4-7, a leftover from a retired count field +// that used to live in bits 0-2 -- closing that gap removes the +// bvPattern >>= 3 that used to be needed before the +// KING_COUNTER_BY_ATTACK_PATTERN lookup, eval.c). +#define PAWN_BIT 0x00000010UL +#define MINOR_BIT 0x00000008UL +#define ROOK_BIT 0x00000004UL +#define QUEEN_BIT 0x00000002UL // King never x-rays (it can't move through a blocker), so there's no // KING_XRAY_BIT to go with this. -#define KING_BIT 0x00000008UL +#define KING_BIT 0x00000001UL #define INVALID_PIECE_INDEX (17) #define IS_VALID_PIECE_INDEX(x) ((x) < INVALID_PIECE_INDEX) @@ -650,6 +654,14 @@ typedef struct _POSITION // not as something callers should call directly anymore. BITBOARD bbOccupied; + // Per-side occupancy, same incremental-maintenance treatment as + // bbOccupied above -- _BuildFriendlySideBB (generate.c) used to + // rebuild this from scratch (a 6-term OR of bbPieces+bbPawns+king) + // on every call; read far more often per node than a position + // changes per move, so incremental beats on-the-fly OR-ing here + // for the same reason it already does for bbOccupied. + BITBOARD bbOccupiedSide[2]; + // Eval()-scoped attack-bitboard accumulators, one pair per piece // family, populated by Eval()'s piece-by-piece walk (pawns, then // knights/bishops, then rooks, then queens, then king) and read by @@ -718,11 +730,8 @@ typedef struct _POSITION SCORE iScore[2]; SCORE iReducedMaterialDownScaler[2]; SCORE iTempScore; - ULONG uMinMobility[2]; - COOR cPiece; ULONG uMinorsAtHome[2]; BITBOARD bb; - ULONG uPiecesPointingAtKing[2]; } POSITION; @@ -896,6 +905,19 @@ typedef struct _COUNTERS } pawnhash; + // EXPERIMENTAL, prototype-only -- see rgKingSafetyHash and + // _GetFileStormDefects (eval.c). Cycle counters let this be + // measured with a plain PERF_COUNTERS build, no separate + // EVAL_TIME rebuild needed. + struct + { + UINT64 u64Probes; + UINT64 u64Hits; + UINT64 u64CyclesHit; + UINT64 u64CyclesMiss; + } + kingsafetyhash; + struct { UINT64 u64TotalNodeCount; @@ -975,6 +997,21 @@ typedef struct _COUNTERS UINT64 u64CyclesEvalPreLazy; UINT64 u64CyclesEvalPostLazyMisc; UINT64 u64CyclesEvalAttackTablePop; + + // + // 2026-09-06 (board_representation/EVAL.md section 9): finer + // breakdown of what's inside u64CyclesEvalPreLazy, to find out + // exactly what got more expensive there this session. + // u64CyclesEvalLazyDecision times the whole material-gate + + // margin-check + EstimatePositionalScore block; the other two + // further split EstimatePositionalScore's own two calibrated + // sub-calls (each already summed over both colors). Everything + // else in "pre-lazy" (material/passers/bad-trades/bishop-pairs) + // is u64CyclesEvalPreLazy minus u64CyclesEvalLazyDecision. + // + UINT64 u64CyclesEvalLazyDecision; + UINT64 u64CyclesEvalCountKingSafetyDefects; + UINT64 u64CyclesEvalFileStormDefects; } tree; @@ -1098,7 +1135,6 @@ typedef struct _PLY_INFO MOVE PV[MAX_PLY_PER_SEARCH]; SCORE iKingScore[2]; - ULONG uMinMobility[2]; UINT64 u64NonPawnSig; UINT64 u64PawnSig; UINT64 u64Sig; @@ -1123,6 +1159,18 @@ typedef struct _PAWN_HASH_ENTRY } PAWN_HASH_ENTRY; +// EXPERIMENTAL, prototype-only (2026-09-05) -- see +// SEARCHER_THREAD_CONTEXT's rgKingSafetyHash comment. Same table size +// as the pawn hash for a fair, direct hit-rate comparison against the +// already-measured numbers. +#define KING_SAFETY_HASH_TABLE_SIZE PAWN_HASH_TABLE_SIZE +typedef struct _KING_SAFETY_HASH_ENTRY +{ + UINT64 u64Key; + ULONG uFileStormDefects[2]; +} +KING_SAFETY_HASH_ENTRY; + #define NUM_SPLIT_PTRS_IN_CONTEXT (8) // @@ -1159,6 +1207,31 @@ typedef struct _SEARCHER_THREAD_CONTEXT SCORE iRootScore; ULONG uRootDepth; PAWN_HASH_ENTRY rgPawnHash[PAWN_HASH_TABLE_SIZE]; + // EXPERIMENTAL, prototype-only (2026-09-05): real cache for + // _EvalKing's open-file/storming-pawn defect count only (the + // DO_KING_SAFETY_THRESHOLD-gated "safety branch" terms -- measured + // ~96% hit rate in general/middlegame positions). Deliberately + // does NOT cover the u<8-gated "endgame branch" terms (king- + // supporting-own-passer, in/out-of-action) -- those measured a + // much worse ~75-85% hit rate in bare K+P endgames, and are cheap + // enough in that low-piece-count regime that caching them isn't + // expected to pay off; they stay computed directly in _EvalKing, + // uncached, per board_representation/EVAL.md section 9's + // king-safety-hash discussion. Keyed on (pos->u64PawnSig, both + // kings' squares) -- both sides' defect counts are computed and + // stored together on a miss, so whichever king _EvalKing evaluates + // second within the same Eval() call is a guaranteed hit. + KING_SAFETY_HASH_ENTRY rgKingSafetyHash[KING_SAFETY_HASH_TABLE_SIZE]; +#ifdef CALIBRATE_POSITIONAL + // Set/cleared once per Eval() call, around the cheap base-margin + // ("super lazy") gate that decides whether EstimatePositionalScore + // would even run in a normal build -- _RecordPositionalCalibration + // only logs a sample when this is set, so the calibration histogram + // matches the population EstimatePositionalScore actually sees in + // real (uninstrumented) search, not every node Eval() is ever + // called on. + FLAG fCalibrateCandidate; +#endif CHAR szLastPV[SMALL_STRING_LEN_CHAR]; } SEARCHER_THREAD_CONTEXT; @@ -3058,6 +3131,18 @@ PawnHashLookup(SEARCHER_THREAD_CONTEXT *ctx); #define LAZY_EVAL_BASE_MARGIN (75) // cheap material-only lazy exit margin; // widened by EstimatePositionalScore // if this isn't enough on its own +// board_representation/EVAL.md section 9 (2026-09-06): below this +// combined pos->uArmyScaler[WHITE]+pos->uArmyScaler[BLACK] total, both +// stages of lazy eval are skipped entirely (always full eval). Measured +// directly via CALIBRATE_MARGIN_SAFETY: the regular (widened-margin) +// lazy exit's real swing exceeded its own assumed margin 20.6% of the +// time in the 0-7 combined-scaler bucket (n=1229) -- a near-bare-king +// endgame's positional swings (opposition, zugzwang, breakthrough +// timing) are too discontinuous for the margin's "small and bounded" +// assumption to hold. Set at 16 (start of the material bucket that +// measured a clean 0.05% rate) rather than exactly at the 0-7 boundary, +// since the 8-15 bucket's own 0% reading was too thin (n=50) to trust. +#define LAZY_EVAL_MIN_MATERIAL 16 extern const int g_iAhead[2]; extern const int g_iBehind[2]; @@ -3089,6 +3174,21 @@ ULONG CountKingSafetyDefects(POSITION *pos, ULONG uSide); +#ifdef CALIBRATE_POSITIONAL +void +DumpPositionalCalibration(void); +#endif + +#ifdef CALIBRATE_BASE_MARGIN +void +DumpBaseMarginCalibration(void); +#endif + +#ifdef CALIBRATE_MARGIN_SAFETY +void +DumpMarginSafetyCalibration(void); +#endif + // // testeval.c // -- cgit v1.3