| Age | Commit message (Collapse) | Author |
|
trace it at startup alongside the build timestamp.
A --logfile trace could previously only be tied back to a build
timestamp, not the exact source state -- distinguishing same-day
rebuilds during A/B testing required diffing binaries. GIT_COMMIT is
injected via GNUmakefile (git rev-parse --short HEAD, kept out of the
PROFILE variable itself since PROFILE gets separately stringified
whole for the "Make profile used" trace line, and this value's
embedded quotes broke that outer string literal when first tried
folded in there).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EortUUkDVpsfrbqshBJYJg
|
|
type-mixing bug and add a quiet-refutation killer backfill.
Killer tiers now try both of this ply's own killers before either
ply-2-back one, matching Crafty's ordering. Two earlier attempts at
this same swap were reverted for regressing; this pass lands on top of
NumLeftoverMovesToSelect (more SelectBestWithHistory budget to reach
these lower-tier slots) and a real bug fix below, and beats interleaved
order head-to-head on solves, node count, and first-move beta cutoff
across the three curated suites.
The bug: mvNullmoveRefutations's empty-killer-slot backfill could only
ever contain a capturing move (TryNullmovePruning only wrote it inside
the capture-refutation branch), but IS_SAME_MOVE's mask includes the
pCaptured bits, so that backfilled value could never match a real
quiet candidate -- the backfill was silently dead code. Fixed by
recording genuinely quiet null-move refutations into a new, separate
mvNullmoveQuietRefutations array (kept separate so it can't clobber the
capture history mvNullmoveRefutations still needs for the
Botvinnik-Markoff same-piece-two-squares extension check) and
backfilling the regular killer table from that instead. The
check-evasion killer table intentionally does *not* get this backfill:
a null-move refutation can never legitimately be an escaping-check
move (null moves can't deliver check), so backfilling there risks
IS_SAME_MOVE cross-context false positives instead of the old
guaranteed-inert no-op.
Measured at sd10 across ecm_ringers/ecm_confident_quick/ecm_hard_quick
against head_reference (commit d11e973): 115/191 solves (vs. 116
baseline), 924.36M total nodes (vs. 933.23M), first-move beta cutoff
within 0.1-0.9 points of baseline on all three suites -- and clearly
better than the same fix under interleaved order (113/191 solves,
963.10M nodes), which loses to head_reference on every metric.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EortUUkDVpsfrbqshBJYJg
|
|
NumLeftoverMovesToSelect, indexed by remaining depth; make EFP's
leftover-only scope explicit.
SEARCH_SORT_LIMIT[ply] was a poor proxy for what actually matters here
-- how large the remaining subtree below this node is. Distance from
root only correlates with that when total search depth is roughly
fixed; it says nothing once extensions/reductions/iterative-deepening
are in play. NumLeftoverMovesToSelect(ctx, uDepth) uses remaining depth
instead, only ever consulted once every high-performer move (winning/
even capture, killer, killer-mate -- anything >= GOOD_MOVE) has already
been exhausted; this never limits how many of *those* get selected,
only how much further care to spend on the ordinary/leftover tail.
Table values carried over verbatim from the old one as an untuned
starting point, just reindexed.
Also adds an explicit (TRUE == fInLeftovers) gate to EFP's per-move
checklist (landed last commit) -- every high-performer move was already
excluded as a side effect of the capture/check/killer exemptions, but
this makes "EFP only ever touches leftovers" a real, direct condition
rather than an emergent property of unrelated checks.
Verified against HEAD (commit bb07fbd) at sd10:
ecm_ringers: 9/11 -> 10/11 (+1 solve), ~flat nodes (-0.04%)
ecm_confident_quick: 88/90 -> 88/90 (even), +0.43% nodes
ecm_hard_quick: 15/90 -> 18/90 (+3 solves), +4.4% nodes
Net +4 solves across 269 positions for a negligible node-count cost.
|
|
of (cFrom, cTo, color), and expose sample size.
MOVE_TO_INDEX (used elsewhere for the counter-move table) folds any
two moves with the same from/to/color into one fail-high bucket --
e.g. a king shuffle and a queen sac to the same square/color shared
a slot. New MOVE_TO_FH_INDEX uses the low 20 bits of mv.uMove
(cFrom+cTo+pMoved), which already encodes color in pMoved's low bit,
so this is strictly more granular for free. Table grown 0x20000 ->
0x100000 entries to match. Also adds an optional ULONG *puAttempts
out-param so future callers can weight by sample confidence instead
of trusting a percentage computed from as few as one observation.
GetLMRReduction (proto-LMR, live at HEAD) is the only real consumer
right now; verified against head_reference at sd10 across
ecm_ringers/ecm_confident_quick/ecm_hard_quick: net +2 solves (88 vs
87 on confident_quick, 18 vs 17 on hard_quick), node counts flat
within noise (-1.5%/+1.4%/+0.5%).
|
|
of iValue; harden against a latent ComputeMoveExtension bug.
DO_IID's "is the top move crappy" gate compared raw iValue against
SORT_THESE_FIRST only, missing that ordinary killer moves (FIRST_KILLER
through FOURTH_KILLER) sit below that threshold too -- a killer that
already proved itself elsewhere in the tree was being treated as
"crappy" and triggering an unnecessary shallow rescore. Fixed by also
excluding killer-flagged moves from the gate.
RescoreMovesViaSearch corrupted the winning move's real search score by
OR-ing in SORT_THESE_FIRST to force it to sort first (`mvf[uBest].iValue
|= SORT_THESE_FIRST`) -- unnecessary (SelectBest{With,No}History already
find the true max by plain magnitude comparison, no flag needed) and
actively dangerous: a later ComputeMoveScore() call on that same move,
if it's a capture, would see the corrupted value, mistake it for
generate.c's biased-capture-ordering format, and subtract the wrong
bias entirely. Removed the OR; added an explicit
PLY_INFO.fMovesRescoredByIID flag so ComputeMoveScore and the main
search-loop's move-selection call can both recognize "this ply's
iValue holds a real eval-axis score" without relying on bit-pattern
inference.
Consequently, ComputeMoveScore now trusts an IID-rescored move's score
outright instead of running it through the capture-bias-subtraction or
quiet-move-collapse-to-0 logic (both of which assume generate.c's
ordering encoding, which a rescored ply no longer holds). Separately
hardened it against quiet killer-mate moves, which can reach
SORT_THESE_FIRST via a different, capture-unrelated path and were
incorrectly getting the capture bias subtracted from them; they now
correctly collapse to 0 like other quiet moves.
Two follow-on ideas -- blending history into the real IID score (scaled
or capped) and a exact-tie-only history tiebreak -- were implemented,
measured, and rejected: blending invents a new, leak-prone move-scoring
axis for no measured benefit, and the tiebreak-only compromise still
cost solves relative to just trusting the real score outright. Main
search's move-selection call now branches once per selection (not once
per candidate move) between SelectBestNoHistory (IID-rescored plies)
and SelectBestWithHistory (everyone else), keeping the overwhelmingly
common non-rescored path at zero added cost.
Net measured effect (ecm_ringers.ep_/ecm_confident_quick.ep_/
ecm_hard_quick.ep_, sn=5M): 10/90/9, down from a pre-existing 11/88/10
on ringers and hard specifically -- see lmr_testing/RESULTS.md for the
full sweep of rejected alternatives and why the regression was accepted
as the cost of removing a latent, leak-prone bug class rather than
chasing the exact prior numbers.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YGSMkwjqiCk4XhbfN7ugD2
|
|
killer-mate edge case; fix PV-display cycle hang.
_ShouldWeConsiderThisMove (QSearch's move-consider gate) read the raw,
move-ordering-biased mvf[].iValue directly instead of going through
ComputeMoveScore, so it inherited the same +120-ish flat bias (plus
small MVV-LVA nudges) on winning/even captures that ComputeMoveScore
was already fixed to strip out. Fixed via the same MOVE_SCORE_ORDERING_BIAS
subtraction, now factored into a shared chess.h macro. Restoring the old
effective leniency required an explicit QSEARCH_CONSIDER_MARGIN (120,
A/B'd against 0/60/120 on ecm_ringers/confident_quick/hard_quick) rather
than assuming the bug's magnitude was itself a meaningful margin -- net
effect vs the pre-fix baseline is -2 solves on hard_quick, accepted as
the cost of correctness (see lmr_testing/RESULTS.md for the full sweep).
ComputeMoveScore separately mishandled quiet killer-mate moves: they can
reach SORT_THESE_FIRST via generate.c's killer-mate bonus (unrelated to
the capture-bias path), so the bias-subtraction was wrongly applied to a
move that never had that bias. Gated the subtraction on
IS_CAPTURE_OR_PROMOTION(mv); quiet moves (including killer-mate ones) now
correctly collapse to 0, per the function's contract of estimating a
move's value on the 100=1-pawn axis. Measured as a no-op on all three
suites -- rare in practice, but a real correctness fix. Left a comment
documenting two candidate refinements for scoring quiet moves as
non-uniform future work, deliberately not implemented (each needs its
own isolated test).
FinishPVTailFromHash (cosmetic PV-display hash-walk, used only for
printing) had no cycle detection, so a drawish/repeating position could
spin until the output buffer filled instead of terminating naturally.
Added visited-position-signature tracking and a <REP> marker.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YGSMkwjqiCk4XhbfN7ugD2
|
|
Finishes work left half-done in 7857096 ("Replace ctx->uPositional with
a data-calibrated Eval() return value"): that commit added Eval()'s new
piPositional out-param but never migrated GetRoughEvalScore onto it, so
GetRoughEvalScore's mid/deep-tree fallback kept reading the old
ctx->uPositional field -- a per-thread EWMA written only on full-eval
calls and never touched by the (far more common) lazy-eval path, so it
carried a stale value from whatever unrelated position last triggered a
full eval, potentially many nodes/plies away. Combined with EVAL_HASH
being long since disabled (its probe branch already dead), every
GetRoughEvalScore call past ply 4 was effectively "material + garbage."
Fixed by having GetRoughEvalScore just call Eval() directly -- its own
lazy-exit machinery already is the cheap, calibrated estimate this
function exists to provide, so there's no separate estimator to
maintain. Removed ctx->uPositional entirely (struct field, its EWMA
update in eval.c, both root.c init sites, split.c's cross-split
propagation, testeval.c's reset) along with the entire EVAL_HASH
subsystem (struct, table, Probe/StoreEvalHash, main.c's now-dead
reporting branch, the GNUmakefile flag) -- confirmed unused elsewhere
and explicitly being cut for good, not coming back in this form.
Also fixed GetRoughEvalScore's prototype being wrongly declared inside
#ifdef EVAL_HASH in chess.h even though the function itself is defined
and called unconditionally -- this was the source of the recurring
"call to undeclared function 'GetRoughEvalScore'" implicit-declaration
warning seen throughout this session's builds.
Separately, fixed QSearch to match its own documented intent: the
en-prise/trapped-piece "don't let this side stand pat" check now only
fires if the side hasn't already been allowed to stand pat earlier in
this qsearch line (matching the comment above it, which already said
this but the code never implemented it).
Verified against baseline/typhoon_baseline (pristine, pre-session) on
ecm_ringers.ep_ (4), ecm_hard_quick.ep_ (50-sample), and
ecm_confident_quick.ep_ (40) at sn=5M, --cpus 1, book disabled:
pristine baseline solves 3/50 on the hard sample; this commit solves
6/50, with the stand-pat fix and GetRoughEvalScore fix each
contributing +1 independently confirmed. No regressions on the other
two suites (4/4 and 40/40 unchanged throughout).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YGSMkwjqiCk4XhbfN7ugD2
|
|
EBF/beta-cutoff/counter-move stats, script.c FPE fix.
No LMR, no counter-move-driven move ordering (both explored separately,
kept out for now -- counter-move measured worse, ~655->647 solved on
ecm879 @ sn=4M with a leaner tree beforehand). Futility pruning restored.
Verified: 647/879 solved, EBF 4.609 @ sn=4M; 684/879 solved, EBF 3.995
@ 20s/move, 1cpu, 256m hash (typhoon_baseline.log).
The counter-move table is still written and its stats still tracked
(dynamic.c) for diagnostic purposes, but generate.c no longer reads it
for move ordering, so it has no effect on search behavior in this
commit.
lmr_testing/ holds the in-flight graded-LMR + counter-move code (not
applied here) with notes on what was already tried and measured, so a
future session can resume without re-deriving it.
|
|
uPositional was a per-thread EWMA of abs(material - true score) used to
size lazy-eval and futility margins. It was history-derived (reflecting
whatever recent, unrelated positions looked like) rather than derived
from the position actually being margined, and its update/consumption
was tangled with EVAL_HASH (now disabled).
Eval() now takes an optional SCORE *piPositional out-param and fills it
in on every return path: exact (abs(material-delta)) on a full eval,
or an estimate from a new EstimatePositionalScore() on a lazy exit.
EstimatePositionalScore()'s two terms (king-safety-defect-bucketed, and
a flat residual for mobility/passers/everything else) are calibrated
from ~1.6M measured full-eval samples (p90 of the actual swing), not
guessed -- an initial guessed version measurably regressed ECM solve
rate (630 vs a 650 baseline at sn=4M); the recalibrated version is back
at parity (649/879).
search.c's qsearch futility now reads the value Eval() just computed
instead of the stale/shared ctx field. Also removes QSearchInDangerNoStandPat
and SideCanStandPat, dead since the danger-hash check that fed them was
already commented out (e08387a) -- they depended on the same enprise/
trapped-piece data this conversation is about to move off of
g_PositionHash entirely.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
|
|
|
|
|
|
Fixes a real pathology: checks along a long unbroken forcing line could
extend for free (net zero cost against the qsearch boundary), letting
tree size blow up multiple orders of magnitude on positions like a
near-all-check forced mate (ECM.089: 4.5B nodes / 31min at depth 12
before this change).
- Main-search check extension: gate on SEE soundness (a losing
sacrifice check gets a small consolation QUARTER_PLY instead of the
full bonus a sound check gets), and flatten the sound-check bonus to
a flat THREE_QUARTERS_PLY instead of a near-free ONE_PLY.
- Lower the qsearch entry threshold to match (THREE_QUARTERS_PLY
instead of ONE_PLY) so a lone check still buys one extra full-width
ply as before; root.c trims QUARTER_PLY off the per-iteration depth
budget so this doesn't add a blanket 1/4 ply to every search.
- Qsearch's own check-widening (QSearchFromCheckNoStandPat) now relies
on fCouldStandPat history plus a g_uIterateDepth/4 ceiling instead of
an unconditional per-check grant, and QPLIES_OF_NON_CAPTURE_CHECKS
moved from 1 to 2 to cover both "enter qsearch already in check" and
"opponent's reply is the first real check" cases with one baseline
window instead of ad hoc attacker-color tracking.
Net effect on ECM.089 (sn 4M canary): ~7.5x fewer nodes and ~4x less
time at depth 12 versus the original, unbounded behavior. Costs solve
count on the full ECM suite (879 pos, sn 4M): 650 baseline -> 636 here
-- expected and accepted, since ECM is unusually check-extension-heavy
tactics and not representative of real games. Self-play vs baseline
(1000 games, st 1) came back at B_SCORE=0.4955, ELO=-3.1+/-21.5 --
statistically neutral, confirming the fix costs nothing in real play.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
|
|
Late Move Reductions using a depth x movecount table (Ethereal-style
formula), gated off PV nodes and the ply directly below a PV node
(PLY_INFO.fIsPVNode), with magnitude-aware re-search on fail-high.
Verified node-for-node identical to the previously tested-good v7
binary on a canary position (sd 10) after reconstructing from a ZFS
snapshot of search.c/root.c/split.c taken just before that binary was
built.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
|
|
|
|
|
|
|
|
it found)
|
|
|