diff options
| -rwxr-xr-x | src/chess.h | 23 | ||||
| -rwxr-xr-x | src/eval_tune/match_play.py | 29 | ||||
| -rwxr-xr-x | src/eval_tune/run_ecm.sh | 41 | ||||
| -rwxr-xr-x | src/eval_tune/test_vs_head.sh | 2 | ||||
| -rwxr-xr-x | src/main.c | 4 | ||||
| -rwxr-xr-x | src/root.c | 56 | ||||
| -rwxr-xr-x | src/run_tests.sh | 53 | ||||
| -rwxr-xr-x | src/search.c | 32 | ||||
| -rwxr-xr-x | src/split.c | 13 | ||||
| -rw-r--r-- | src/unix.c | 4 |
10 files changed, 135 insertions, 122 deletions
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)))) @@ -1746,14 +1751,8 @@ 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 <label> [depth] -# Writes eval_tune/ecm_logs/<label>.log (full Trace() output) and -# appends one line to eval_tune/ecm_history.log. - -set -e -cd "$(dirname "$0")/.." # repo src/ root - -label="$1" -depth="${2:-11}" -if [ -z "$label" ]; then - echo "Usage: $0 <label> [depth]" >&2 - exit 1 -fi - -mkdir -p eval_tune/ecm_logs -logfile="eval_tune/ecm_logs/${label}.log" - -opts='--cpus 1 --hash 256m --egtbpath /zscratch/egtb' -# --batch exits non-zero on normal EOF ("Exhausted input in batch -# mode") -- that's expected, not a failure, so don't let set -e treat -# it as one; the real success/failure check is the grep below. -./typhoon ${opts} --logfile "$logfile" --batch \ - --command "sd ${depth}; script ../tests/ecm.ep_" || true - -correct=$(grep -o 'correct solutions : [0-9]*' "$logfile" | tail -1 | grep -o '[0-9]*$') -total=$(grep -o 'total problems : [0-9]*' "$logfile" | tail -1 | grep -o '[0-9]*$') - -if [ -z "$correct" ] || [ -z "$total" ]; then - echo "ERROR: couldn't parse solved count from $logfile" >&2 - exit 1 -fi - -echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) label=$label depth=$depth solved=${correct}/${total}" \ - >> eval_tune/ecm_history.log - -echo "${correct} ${total}" diff --git a/src/eval_tune/test_vs_head.sh b/src/eval_tune/test_vs_head.sh index c7c578e..d5d8006 100755 --- a/src/eval_tune/test_vs_head.sh +++ b/src/eval_tune/test_vs_head.sh @@ -3,7 +3,7 @@ python3 ./match_play.py ../../head_reference/typhoon ../typhoon \ --pgn ../../pgn/twic_filtered.pgn \ --games 20000 \ - --workers 20 \ + --workers 12 \ --st 1 \ --sprt --elo0 0 --elo1 5 \ --scratch /usr/local/tmp/typhoon_match_overnight \ @@ -296,7 +296,7 @@ Return value: strcpy(g_Options.szEGTBPath, "/zscratch/egtb"); strcpy(g_Options.szLogfile, "/usr/local/tmp/typhoon.log"); strcpy(g_Options.szBookName, "book.bin"); - g_Options.uNumHashTableEntries = 0x10000; + g_Options.uNumHashTableEntries = _ParseHashOption("256m", sizeof(HASH_ENTRY)); g_Options.uNumProcessors = 1; g_Options.fStatusLine = TRUE; g_Options.iResignThreshold = 0; @@ -527,7 +527,7 @@ Return value: **/ { -#ifdef _X86_ +#if defined(_X86_) || defined(_X64_) ULONG u, x, y; Trace("Testing CPP macros...\n"); @@ -950,27 +950,45 @@ _IterateSetSearchGlobals(ULONG uDepth) // g_uSoftExtendLimit = g_uIterateDepth * 2; g_uHardExtendLimit = g_uIterateDepth * 4; - for (u = 0; u < MAX_PLY_PER_SEARCH; u++) + + // g_uExtensionReduction[]'s own bands, below, must stay reachable + // within [0, MAX_PLY_PER_SEARCH) regardless of how deep g_uIterateDepth + // is -- naive g_uSoftExtendLimit/g_uHardExtendLimit (2x/4x iterate + // depth) can hugely exceed MAX_PLY_PER_SEARCH for a deep requested + // search (e.g. sd=30 -> hard limit 120 >> 64), which would leave every + // reachable u in the loop below permanently in the "0 penalty" band -- + // no taper at all for the entire representable ply range. Deliberately + // a LOCAL, separately-capped depth for this table only -- + // g_uSoftExtendLimit/g_uHardExtendLimit themselves stay uncapped (raw + // g_uIterateDepth * 2/4) since g_uHardExtendLimit is also read directly + // elsewhere (searchsup.c's QSearch runaway-depth cutoff) where capping + // it would be an unrelated, unintended behavior change. { - if (u < g_uSoftExtendLimit) - { - g_uExtensionReduction[u] = 0; - } - else if (u < (g_uSoftExtendLimit + g_uIterateDepth / 2)) - { - g_uExtensionReduction[u] = QUARTER_PLY; - } - else if (u < (g_uSoftExtendLimit + g_uIterateDepth)) - { - g_uExtensionReduction[u] = HALF_PLY; - } - else if (u < g_uHardExtendLimit) - { - g_uExtensionReduction[u] = THREE_QUARTERS_PLY; - } - else + ULONG uEffIterateDepth = MIN(g_uIterateDepth, (MAX_PLY_PER_SEARCH - 4) / 4); + ULONG uEffSoftLimit = uEffIterateDepth * 2; + ULONG uEffHardLimit = uEffIterateDepth * 4; + for (u = 0; u < MAX_PLY_PER_SEARCH; u++) { - g_uExtensionReduction[u] = 5 * ONE_PLY; + if (u < uEffSoftLimit) + { + g_uExtensionReduction[u] = 0; + } + else if (u < (uEffSoftLimit + uEffIterateDepth / 2)) + { + g_uExtensionReduction[u] = QUARTER_PLY; + } + else if (u < (uEffSoftLimit + uEffIterateDepth)) + { + g_uExtensionReduction[u] = HALF_PLY; + } + else if (u < uEffHardLimit) + { + g_uExtensionReduction[u] = THREE_QUARTERS_PLY; + } + else + { + g_uExtensionReduction[u] = 5 * ONE_PLY; + } } } diff --git a/src/run_tests.sh b/src/run_tests.sh index d5905e4..7e3a053 100755 --- a/src/run_tests.sh +++ b/src/run_tests.sh @@ -6,26 +6,41 @@ LOGDIR="/tmp/typhoon/manual" HEAD_LOGDIR="../head_reference/logs" TESTS="../tests" -/bin/rm -rf "${LOGDIR}/*" +# Suite names default to the three curated suites (see CLAUDE.md's +# head_reference protocol). Override on the command line to run other +# suites, e.g. ./run_tests.sh ecm_ringers ecm_lmr_delta +SUITES=("$@") +if [ ${#SUITES[@]} -eq 0 ]; then + SUITES=(ecm_ringers ecm_confident_quick ecm_hard_quick) +fi -clear -echo -n "Launching all test suites @ sn=5M and sd=10 in parallel (expect ~5-6 min)... " +# sd (fixed depth) is the right choice when judging whether a pruning/ +# ordering change makes the tree smaller or larger for the same search +# effort; sn (fixed node budget) is the right choice when judging how many +# positions solve within a fixed cost. Override via SD/SN env vars. +SD="${SD:-10}" +SN="${SN:-5000000}" + +/bin/rm -rf "${LOGDIR}" +mkdir -p "${LOGDIR}" + +echo -n "Launching test suites (${SUITES[*]}) @ sn=${SN} and sd=${SD} in parallel... " _start_time=$(date +%s) -for suite in ecm_ringers ecm_confident_quick ecm_hard_quick; do +for suite in "${SUITES[@]}"; do ./typhoon \ --cpus 1 \ --hash 256m \ - --logfile "$LOGDIR/sd10_${suite}.log" \ + --logfile "$LOGDIR/sd${SD}_${suite}.log" \ --batch \ - --command "force; book name /nonexistent.book.bin; sd 10; script $TESTS/${suite}.ep_" \ - > "$LOGDIR/sd10_${suite}.out" 2>&1 & + --command "force; book name /nonexistent.book.bin; sd ${SD}; script $TESTS/${suite}.ep_" \ + > "$LOGDIR/sd${SD}_${suite}.out" 2>&1 & ./typhoon \ --cpus 1 \ --hash 256m \ - --logfile "$LOGDIR/sn5m_${suite}.log" \ + --logfile "$LOGDIR/sn${SN}_${suite}.log" \ --batch \ - --command "force; book name /nonexistent.book.bin; sn 5000000; script $TESTS/${suite}.ep_" \ - > "$LOGDIR/sn5m_${suite}.out" 2>&1 & + --command "force; book name /nonexistent.book.bin; sn ${SN}; script $TESTS/${suite}.ep_" \ + > "$LOGDIR/sn${SN}_${suite}.out" 2>&1 & done wait _elapsed=$(( $(date +%s) - _start_time )) @@ -69,15 +84,19 @@ print_stats_table() { ' "$headfile" "$candfile" } -for suite in ecm_ringers ecm_confident_quick ecm_hard_quick; do - echo "----[ $suite sd=10 ]----" +for suite in "${SUITES[@]}"; do + echo "----[ $suite sd=${SD} ]------------------------------------------------------------------" print_stats_table \ - <(sed -n '/correct solutions/,/script time/p' "$HEAD_LOGDIR/sd10_${suite}.out") \ - <(sed -n '/correct solutions/,/script time/p' "$LOGDIR/sd10_${suite}.out") + <(sed -n '/correct solutions/,/script time/p' "$HEAD_LOGDIR/sd${SD}_${suite}.out") \ + <(sed -n '/correct solutions/,/script time/p' "$LOGDIR/sd${SD}_${suite}.out") echo - echo "----[ $suite sn=5M ]----" + echo "----[ $suite sn=${SN} ]------------------------------------------------------------------" print_stats_table \ - <(sed -n '/correct solutions/,/script time/p' "$HEAD_LOGDIR/sn5m_${suite}.out") \ - <(sed -n '/correct solutions/,/script time/p' "$LOGDIR/sn5m_${suite}.out") + <(sed -n '/correct solutions/,/script time/p' "$HEAD_LOGDIR/sn${SN}_${suite}.out") \ + <(sed -n '/correct solutions/,/script time/p' "$LOGDIR/sn${SN}_${suite}.out") echo done +echo +echo "NOTE: The logfiles for these runs are in /tmp/typhoon/manual and will be" +echo " overwritten by the next invocation of this script; save now them if" +echo " you want them!" diff --git a/src/search.c b/src/search.c index 31220e1..807f69d 100755 --- a/src/search.c +++ b/src/search.c @@ -486,7 +486,21 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, ctx->sSearchFlags.fAvoidNullmove = TRUE; RescoreMovesViaSearch(ctx, uDepth, iAlpha, iBeta); ctx->sSearchFlags.fAvoidNullmove = FALSE; - ASSERT(TRUE == pi->fMovesRescoredByIID); + // NOT always TRUE here -- pre-existing bug, found + // via debug_smoke_test.sh (a deeper/larger-than- + // usual sample finally hit the rare path). + // RescoreMovesViaSearch's own fail-high branch + // (searchsup.c) deliberately leaves this FALSE by + // design -- a fail-high only proves uBest is good + // enough, not honest eval-axis scores for every + // move, so claiming fMovesRescoredByIID would be a + // lie. This assert demanded the opposite of that + // documented contract; DO_IID is unconditionally + // compiled in (chess.h) so this could fire on any + // DEBUG build given an unlucky enough rescore -- + // ASSERT is a no-op in release, so this never + // crashed in production, but it made the DEBUG/ + // TEST harness itself unreliable at random. } } #endif @@ -727,15 +741,12 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, iCheckSee, &iExtend); - // Cap how many extension plies this line may spend in total - // (root to here) so that a chain of checks/threats/etc. can't - // stall uDepth's descent indefinitely and burn the entire - // MAX_PLY_PER_SEARCH ply budget on one forcing sequence. - if (iExtend > 0) - { - iExtend = MIN(iExtend, - MAX(MAX_EXTEND_PER_LINE - pf->iCumulativeExtend, 0)); - } + // Note: MAX_EXTEND_PER_LINE (a flat, non-depth-relative cap on + // total extension spent per line) used to be applied here. + // Removed -- g_uExtensionReduction[] (consumed inside + // ComputeMoveExtension, scaled off g_uIterateDepth) is the + // sole extension-runaway guard now; see root.c's construction + // of that table. // Decide how much (if any) to reduce this move's depth -- // graded LMR. @@ -799,7 +810,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, ULONG uFHAttempts = 0; ULONG uFHPct = GetMoveFailHighPercentage(mv, &uFHAttempts); fThisMoveEFPPruned = - (mv.cFrom != FindEnprisePiece(ctx, pos->uToMove)) && ((uFHAttempts < EFP_FH_MIN_SAMPLES) || (uFHPct <= EFP_FH_PRUNE_THRESHOLD)) && (!IS_SAME_MOVE(mv, ctx->mvKiller[ctx->uPly-1][0])) && diff --git a/src/split.c b/src/split.c index 28d5ce0..a1f9d32 100755 --- a/src/split.c +++ b/src/split.c @@ -1128,15 +1128,12 @@ Return value: &iExtend); // - // Cap total extension plies spent on this line, same as the - // non-split move loop in search.c does. + // Note: MAX_EXTEND_PER_LINE (a flat, non-depth-relative cap on + // total extension spent per line) used to be applied here. + // Removed -- g_uExtensionReduction[] is the sole extension- + // runaway guard now, same as the non-split move loop in + // search.c. // - if (iExtend > 0) - { - iExtend = MIN(iExtend, - MAX(MAX_EXTEND_PER_LINE - - ctx->sSearchFlags.iCumulativeExtend, 0)); - } // // Decide how much (if any) to reduce this move's depth. @@ -51,7 +51,7 @@ typedef struct _ALLOC_RECORD } ALLOC_RECORD; ALLOC_RECORD g_AllocHash[ALLOC_HASH_SIZE]; -#define PTR_TO_ALLOC_HASH(x) ((((ULONG)(size_t)(x)) >> 3) & (ALLOC_HASH_SIZE - 1)) +#define PTR_TO_ALLOC_HASH(x) ((((size_t)(x)) >> 3) & (ALLOC_HASH_SIZE - 1)) ULONG GetHeapMemoryUsage(void) @@ -838,7 +838,7 @@ Return value: NULL, "mprotect", (void *)(size_t)errno, - (void *)(PROT_READ | PROT_WRITE), + (void *)(size_t)(PROT_READ | PROT_WRITE), __FILE__, __LINE__); } return(TRUE); |
