summaryrefslogtreecommitdiff
path: root/src/dynamic.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-24 09:07:03 -0700
committerScott Gasch <[email protected]>2026-10-24 09:07:03 -0700
commit5f901d737b17c5f99b6e456d6c0c7d5e3c093dc4 (patch)
tree6601dadbd36c8f45991c5dd821a3dbb2059baafe /src/dynamic.c
parent69525eb2f42ea9cca8255f6cc3b85e6b1312fd05 (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.c172
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)
/**