diff options
| author | Scott Gasch <[email protected]> | 2026-10-24 09:07:03 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-24 09:07:03 -0700 |
| commit | 5f901d737b17c5f99b6e456d6c0c7d5e3c093dc4 (patch) | |
| tree | 6601dadbd36c8f45991c5dd821a3dbb2059baafe /src/dynamic.c | |
| parent | 69525eb2f42ea9cca8255f6cc3b85e6b1312fd05 (diff) | |
More aggressive move reductions; Wilson bound overrides static signal when available
SPRT playtest (2000 games, sd/depth matched): -535 =875 +590, score=0.514,
llr=+1.04 (floated around +1.0 the whole run, never went negative) toward
H1=+2.94. Not a confirmed win, but searches meaningfully deeper (avg depth
14.35 vs 13.50, EBF 3.131 vs 3.305) at no measurable strength cost -- worth
banking and continuing to accumulate games against.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/dynamic.c')
| -rw-r--r-- | src/dynamic.c | 172 |
1 files changed, 168 insertions, 4 deletions
diff --git a/src/dynamic.c b/src/dynamic.c index 07fc02e..50bc36a 100644 --- a/src/dynamic.c +++ b/src/dynamic.c @@ -37,10 +37,11 @@ Revision History: #include "chess.h" -// Declared directly instead of #include <math.h> -- that header -// #defines its own INFINITY_SCORE, which collides with ours (chess.h:198, -// MAX_SHORT). Only needed once, at startup, to build g_iLMRQuietReduction. -extern double log(double); +// Only needed once, at startup, to build g_iLMRQuietReduction and +// g_uWilsonUpperBoundThousandths below -- math.h's own INFINITY macro +// used to collide with ours before chess.h's was renamed to +// INFINITY_SCORE (2026-10-23). +#include <math.h> ULONG g_HistoryCounters[14][128]; ULONG g_ContinuationHistory[CONTINUATION_TABLE_SIZE]; @@ -69,6 +70,53 @@ typedef struct _FH_STATS FH_STATS g_FailHighs[FH_STATS_TABLE_SIZE]; +// Precomputed Wilson-score upper-confidence-bound table for the dynamic +// per-move-identity fail-high rate, in the same "thousandths of a +// percent" unit g_uJointFHThousandths uses -- so a caller can feed this +// straight into the same reduction-magnitude formula without any unit +// conversion. See GetLMRReduction's 2026-10-23 dynamic-blend comment for +// why an upper CONFIDENCE BOUND (not the raw FailHighs/Attempts ratio) +// is the right thing to feed that formula: a bound widens automatically +// for a thin sample (0 fail-highs in 2 tries proves almost nothing) and +// narrows toward the true rate as real evidence accumulates (0 fail- +// highs in 65,000 tries is real evidence of safety), which is exactly +// the asymmetric trust a raw point estimate can't express on its own. +// +// Computing the real Wilson formula (one divide for p-hat, several more +// for the z^2/n terms, one sqrt) per move at Search's call frequency is +// a non-starter -- this is the hottest function in the engine. Instead: +// bucket both inputs onto small, branchless-indexable ranges and pay +// the real formula's cost exactly once per cell, at startup, in +// InitializeWilsonBoundTable below. At the one call site that matters +// (GetLMRReduction's hot path), the whole lookup is two bit-length/min +// ops and one array index -- no FP divide, no sqrt, nothing branchy +// keyed on "essentially random move metadata" the way a comparison +// chain would be. +// +// Bucket shape, chosen to be cheap to compute rather than retrofitted +// onto one (contrast with _ShipDepthBucket/_ShipLateBucket's comparison +// chains elsewhere in this file's sibling searchsup.c, which can't be +// changed this way without also re-baking the static table they index -- +// not a problem here, this table has no offline-baked counterpart to +// stay in lockstep with): +// - Attempts bucket = FastLastBit(Attempts | 1) (chess.h) -- a single +// bsr/clz instruction, 1-based bit-length, so bucket b covers the +// power-of-two range [2^(b-1), 2^b - 1]. u16Attempts is at most +// 65535, so b runs 1..16; index 0 is unused (kept so the raw +// FastLastBit result can be used directly with no decrement). +// - FailHigh bucket = MIN(FailHighs, 3) -- a single branchless min +// (MINU already compiles to cmov elsewhere in this codebase), 0/1/2 +// exact, 3 meaning "3 or more". Finer resolution buys little here: +// by the time a move identity has a handful of real fail-highs on +// record, the formula this feeds already treats it as maximally +// dangerous regardless of the exact count (see the 10%/50%/90% +// finding from the same day's design discussion -- all three +// already clamp to zero extra reduction against the 0.4% baseline, +// so distinguishing "3 fail-highs" from "30" buys nothing further). +#define WILSON_ATTEMPTS_BUCKETS (17) +#define WILSON_FH_BUCKETS (4) +USHORT g_uWilsonUpperBoundThousandths[WILSON_ATTEMPTS_BUCKETS][WILSON_FH_BUCKETS]; + #ifdef MP volatile static ULONG g_uDynamicLock; #define DYN_IS_LOCKED (g_uDynamicLock != 0) @@ -86,6 +134,122 @@ volatile static ULONG g_uDynamicLock; void +InitializeWilsonBoundTable(void) +/** + +Routine description: + + Build g_uWilsonUpperBoundThousandths once at startup -- see that + table's own comment for why (cost: real Wilson-score math, paid + once per bucket cell here, never at Search's call frequency). + + z=2.0 (~95.4% one-sided confidence) chosen as a simple round + constant, same spirit as InitializeLMRTable's own Ethereal-derived + starting constants a few lines down: a reasonable starting shape, + not load-bearing, expect to retune once this is actually wired into + a reduction decision and measured. + + Representative (n, k) per cell: n = the bucket's own lower power- + of-two bound (2^(bucket-1) for bucket >= 1) -- deliberately the + SMALLER end of the bucket's range, so the bound computed is the + more cautious (wider) one a real Attempts value anywhere in that + bucket could justify, never an optimistic extrapolation past what + the smallest member of the bucket actually earns. k = the FH + bucket's own index (0, 1, 2, or 3), capped to n (k can't exceed + attempts) -- using 3 as the representative for the "3+" bucket + rather than something larger only makes that cell's bound MORE + cautious than reality, which is the safe direction to be wrong in. + +Parameters: + + void + +Return value: + + void + +**/ +{ + ULONG uAttemptsBucket, uFhBucket; + double z = 2.0; + double z2 = z * z; + + for (uAttemptsBucket = 0; uAttemptsBucket < WILSON_ATTEMPTS_BUCKETS; uAttemptsBucket++) + { + double n = (uAttemptsBucket == 0) ? + 1.0 : (double)(1UL << (uAttemptsBucket - 1)); + + for (uFhBucket = 0; uFhBucket < WILSON_FH_BUCKETS; uFhBucket++) + { + double k = (double)uFhBucket; + double phat, center, spread, denom, upper; + ULONG uThousandths; + + if (k > n) + { + k = n; + } + phat = k / n; + denom = 1.0 + z2 / n; + center = phat + z2 / (2.0 * n); + spread = z * sqrt((phat * (1.0 - phat) / n) + (z2 / (4.0 * n * n))); + upper = (center + spread) / denom; + if (upper > 1.0) + { + upper = 1.0; + } + uThousandths = (ULONG)((upper * 100000.0) + 0.5); + if (uThousandths > 100000) + { + uThousandths = 100000; + } + g_uWilsonUpperBoundThousandths[uAttemptsBucket][uFhBucket] = + (USHORT)uThousandths; + } + } +} + + +// Branchless lookup into g_uWilsonUpperBoundThousandths -- see that +// table's own comment for the design rationale. FastLastBit (chess.h) +// is a single bsr/clz instruction; MINU is this codebase's existing +// branchless-min macro (compiles to cmov). No divide, no sqrt, no +// comparison chain, at this call site -- all of that cost was paid +// once, at startup, in InitializeWilsonBoundTable above. +ULONG +GetMoveFailHighWilsonUpperBoundThousandths(IN MOVE mv) +/** + +Routine description: + + Look up a move identity's dynamic fail-high rate as a Wilson-score + upper confidence bound (thousandths of a percent, same unit + g_uJointFHThousandths uses) rather than the raw FailHighs/Attempts + ratio GetMoveFailHighPctThousandths returns -- see + g_uWilsonUpperBoundThousandths's comment for why that distinction + matters for feeding a reduction-magnitude formula specifically. + +Parameters: + + MOVE mv + +Return value: + + ULONG : Wilson upper bound, thousandths of a percent (0-100000) + +**/ +{ + ULONG u = MOVE_TO_FH_INDEX(mv); + ULONG uAttempts = g_FailHighs[u].u16Attempts; + ULONG uFailHighs = g_FailHighs[u].u16FailHighs; + ULONG uAttemptsBucket = FastLastBit((BITBOARD)(uAttempts | 1)); + ULONG uFhBucket = MINU(uFailHighs, (ULONG)(WILSON_FH_BUCKETS - 1)); + + return(g_uWilsonUpperBoundThousandths[uAttemptsBucket][uFhBucket]); +} + + +void InitializeLMRTable(void) /** |
