From dddcaa09ad12f1972a3128748b5d90d22f9a9326 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Thu, 3 Sep 2026 17:40:17 -0700 Subject: Cherry-pick non-LMR fixes and tooling from the "LMR" stash Pulled the parts of the stashed LMR work that are genuinely independent of the reduction logic itself, leaving the actual LMR redesign for separate review: - Fix extension-taper table overflow: remove the flat MAX_EXTEND_PER_LINE cap and instead clamp the depth used to build g_uExtensionReduction[] so a deep `sd` request can't leave the whole taper table stuck at "0 penalty" (every index unreachable). - Remove a spuriously-firing ASSERT(fMovesRescoredByIID) in Search(): RescoreMovesViaSearch's own fail-high branch deliberately leaves that flag FALSE by contract, so the assert could fire on any DEBUG build given an unlucky rescore, making the DEBUG/TEST harness unreliable. - Misc correctness/portability fixes: unix.c pointer-truncation casts, chess.h's CONTAINING_STRUCT/IS_ENPASSANT/ABS_DIFF macro hardening (plus gating the branchless bit-tricks on _X64_ too, not just _X86_), removal of dead Slide*WithoutSigs prototypes, main.c's hash default bumped to 256m and its CPP self-test's arch gate widened to _X64_. - eval_tune/match_play.py: cosmetic SPRT progress-bar/output rework. - Delete eval_tune/run_ecm.sh (superseded, unreferenced elsewhere). - run_tests.sh: parameterize suites/SD/SN via args/env vars instead of hardcoding the three curated suites and sd10/sn5M (defaults kept pointing at the existing curated suites, since the stash's own lmr_sensitive_30/lmr_control_30 default suites aren't present in the repo). Deliberately left out of this commit: the stash's actual LMR reduction logic, the M-SIGNAL-SHADOW diagnostic subsystem, the large PERF_COUNTERS instrumentation buildout, the history-table gravity rework, and the FindEnprisePiece pre-move staleness fix (skipped per request pending a decision on whether to also change EFP's pruning behavior). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01MjdDfHry3i2jfJzyDXaG8A --- src/chess.h | 23 +++++++++--------- src/eval_tune/match_play.py | 29 +++++++++++++++------- src/eval_tune/run_ecm.sh | 41 ------------------------------- src/eval_tune/test_vs_head.sh | 2 +- src/main.c | 4 ++-- src/root.c | 56 ++++++++++++++++++++++++++++--------------- src/run_tests.sh | 53 +++++++++++++++++++++++++++------------- src/search.c | 32 ++++++++++++++++--------- src/split.c | 13 ++++------ src/unix.c | 4 ++-- 10 files changed, 135 insertions(+), 122 deletions(-) delete mode 100755 src/eval_tune/run_ecm.sh diff --git a/src/chess.h b/src/chess.h index 062e3dc..5e1d4a5 100755 --- a/src/chess.h +++ b/src/chess.h @@ -162,7 +162,7 @@ typedef struct _DLIST_ENTRY #endif #ifndef CONTAINING_STRUCT #define CONTAINING_STRUCT(address, type, field) \ - ((type *)((BYTE *)(address) - (OFFSET_OF(field, type)))) + ((type *)((BYTE *)(address) - (BYTE *)(OFFSET_OF(field, type)))) #endif #define WHITE (1) @@ -189,7 +189,6 @@ typedef struct _DLIST_ENTRY #define THREE_PLY 192 #define FOUR_PLY 256 #define MAX_DEPTH_PER_SEARCH (MAX_PLY_PER_SEARCH * ONE_PLY) -#define MAX_EXTEND_PER_LINE (MAX_PLY_PER_SEARCH * ONE_PLY / 2) #define IS_VALID_DEPTH(x) (((x) >= 0) && \ ((x) <= MAX_DEPTH_PER_SEARCH) && \ @@ -521,7 +520,7 @@ typedef union _MOVE ((mv).uMove & 0x0FF00000) #define IS_ENPASSANT(mv) \ - (IS_SPECIAL_MOVE(mv) && (mv.pCaptured) && !IS_PROMOTION(mv)) + (IS_SPECIAL_MOVE(mv) && ((mv).pCaptured) && !IS_PROMOTION(mv)) #define IS_DOUBLE_JUMP(mv) \ (IS_SPECIAL_MOVE(mv) && !IS_CAPTURE_OR_PROMOTION(mv)) @@ -1206,7 +1205,13 @@ _assert(CHAR *szFile, ULONG uLine); #define MIN(x, y) (((x) < (y)) ? (x) : (y)) #define MAX(x, y) (((x) > (y)) ? (x) : (y)) -#ifdef _X86_ +#if defined(_X86_) || defined(_X64_) +// +// Note: these are 32-bit int bit tricks, not actually x86-specific -- +// they only require sizeof(int) == 4, which still holds under the LP64 +// data model used by 64-bit (_X64_) builds. Gated on both so a SIXTYFOUR +// build (which defines _X64_, not _X86_) still gets the branchless +// versions instead of falling through to the plain-C fallbacks below. // // Note: MAXU, MINU and ABS_DIFF require arguments with the high order // bit CLEAR to work right. @@ -1236,7 +1241,7 @@ _assert(CHAR *szFile, ULONG uLine); #define ABS_DIFF(a, b) \ (((b)-(a)) - ((((b) - (a)) & (((int)((b) - (a))) >> 31) ) << 1)) -#endif // _X86_ +#endif // _X86_ || _X64_ #ifndef MINU #define MINU(x, y) (MIN((x), (y))) @@ -1255,7 +1260,7 @@ _assert(CHAR *szFile, ULONG uLine); #endif #ifndef ABS_DIFF -#define ABS_DIFF(a, b) (abs((int)(a) - (int)(b))) +#define ABS_DIFF(a, b) (abs((a) - (b))) #endif #define FILE_DISTANCE(a, b) (ABS_DIFF(FILE((a)), FILE((b)))) @@ -1745,15 +1750,9 @@ SetRootToInitialPosition(void); void SlidePiece(POSITION *pos, COOR cFrom, COOR cTo); -void -SlidePieceWithoutSigs(POSITION *pos, COOR cFrom, COOR cTo); - void SlidePawn(POSITION *pos, COOR cFrom, COOR cTo); -void -SlidePawnWithoutSigs(POSITION *pos, COOR cFrom, COOR cTo); - PIECE LiftPiece(POSITION *pos, COOR cSquare); diff --git a/src/eval_tune/match_play.py b/src/eval_tune/match_play.py index 6891964..9591330 100755 --- a/src/eval_tune/match_play.py +++ b/src/eval_tune/match_play.py @@ -367,6 +367,18 @@ def elo_to_score(elo): return 1.0 / (1.0 + 10.0 ** (-elo / 400.0)) +def sprt_bar(llr, la, lb, width=9): + """Render an ASCII gauge of where `llr` sits between the H0 (`la`) and + H1 (`lb`) SPRT bounds, e.g. '|---------|V---------|' with V marking + the rounded llr position.""" + frac = 0.5 if lb == la else (llr - la) / (lb - la) + frac = min(max(frac, 0.0), 1.0) + slot = round(frac * (2 * width)) + bar = "|" + "-" * width + "|" + "-" * width + "|" + idx = slot + 1 + return bar[:idx] + "V" + bar[idx:] + + class Sprt: """Sequential Probability Ratio Test for engine-vs-engine gating, same formulation fishtest/cutechess-cli use for exactly this problem: two @@ -602,17 +614,16 @@ def main(): avg_game_sec = sum(game_durations) / len(game_durations) eta_sec = (avg_game_sec * max(len(jobs) - done, 0)) / args.workers + score = candidate_points / done sprt_note = "" if sprt is not None: - sprt_note = (f" llr={sprt.llr():+.2f} " - f"(H0<={sprt.la:.2f} " - f"H1>={sprt.lb:.2f})") - print(f" {done} games played " - f"(score, in submission order through game " - f"{next_report_idx}: " - f"{reported_points/max(reported_count,1):.3f}) " - f"avg={avg_game_sec:.1f}s/game " - f"ETA={eta_sec/60:.1f}min{sprt_note}", + sprt_note = (f": llr={sprt.llr():+.2f}, " + f"H0={sprt.la:+.2f}" + f"{sprt_bar(sprt.llr(), sprt.la, sprt.lb)}" + f"H1={sprt.lb:+.2f}") + print(f" {done} games: -{losses} ={draws} +{wins} " + f"({avg_game_sec:.1f}s avg, eta={eta_sec/60:.0f}min, " + f"score={score:.3f}){sprt_note}", file=sys.stderr) if sprt is not None and sprt_decision is None: diff --git a/src/eval_tune/run_ecm.sh b/src/eval_tune/run_ecm.sh deleted file mode 100755 index a6c1284..0000000 --- a/src/eval_tune/run_ecm.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/sh -# Run the ECM tactical suite at a fixed search depth (not fixed time -- -# see CLAUDE.md: st introduces machine-load noise that sd avoids) and -# print just the "correct solutions" tally so callers can parse it. -# -# Usage: run_ecm.sh