1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
Better understanding of stuff like KRKRB == drawish, Q's come off = !drawish
Pawnhash size tunable from cmdline again
Think about when to avoid nullmove pruning based on checks in the line
Think about futility pruning when a >= +NMATE
r2r4/p4pk1/2p3p1/1p1nPR2/5p1Q/2N5/PPPq4/1K4R1 w - - 0 0 -- check extension crazy
2026-09-17: search.c's extended futility pruning (VALUE_KNIGHT/VALUE_ROOK
gate, PREPARE_TO_TRY_MOVES) now folds in iImprovement + iPositional[side]
(the same per-tier p50 typical-value estimate QSearch's own futility gate
uses), but still uses one flat margin regardless of which Eval() exit tier
(PLY_INFO.uLastPositionalSource) produced iEval/iPositional at that node.
iPositional corrects for a tier's mean bias, not its spread -- a coarse
(super-lazy) read could carry more variance/tail risk than a fine one even
after mean-centering. Rather than a categorical "never prune from
super-lazy" rule (rejected as too blunt/magic-number-shaped -- iPositional
already replaces trust-by-category with trust-by-measured-bias), the
principled fix is a real EFP_BASE_MARGIN_BY_SOURCE[tier], calibrated the
same way as QSearch's FUTILITY_BASE_MARGIN_BY_SOURCE (CALIBRATE_QSEARCH_
FUTILITY-style harness: search the would-be-pruned move anyway, bucket
surprise rate by distance-short-of-bar AND uLastPositionalSource, check
whether the tiers actually need different margins here too). Not yet
built -- deferred behind self-play validation of today's iPositional/
futility-margin changes landing first.
2026-09-18: build two eval.c lazy-margin variants and compare all three
(current/baseline, "messy but fast", "super conservative") head to head.
"Messy but fast": tune iResidualP2/98ByOwnArmy + iSuperLazyResidualP2/98-
ByOwnArmy to accept ~5% exceeded rate against tests/cross_sectional_200k.ep_
(search under each root position, not just eval in isolation -- want
real search-tree exceeded rate, not a static-position read). "Super
conservative": tune the same tables so the 200k set hardly ever misses.
Then: (1) self-play match_play.py between baseline and each variant, and
between the two variants directly; (2) full tests/ecm.ep_ run (not a
curated subset) under each of the three binaries, same sd, compare
solve counts. Interesting because it's a direct speed-vs-soundness
tradeoff measurement on the exact margin mechanism this session spent a
day mistakenly blaming for the 2026-09-17 regression (see recent git
log/session notes) -- the real bug was searchsup.c's CommonSearchInit
PV staleness, not the margins, so this is now safe to actually explore
properly with a real experiment instead of blind percentile-guessing.
Cheaper derivation than 11 rounds of guess-rebuild-rerun (see eval.c's
guess #1-11 history comments): PersistMarginSafetyHistogramsToFile
(calibrate marginsafety, CALIBRATE_MARGIN_SAFETY build) persists the
raw per-bucket SWING distribution (real full-eval score minus the
lazy/super-lazy estimate), not just a boolean exceeded count -- so
one calibration run against the 200k pool lets a script sweep any
candidate margin M against the persisted histogram and read "fraction
of swings > M" directly, giving the M for ~5% exceeded and the M for
~0% exceeded from the SAME run. eval_tune/lazy_eval_margins/
aggregate_marginsafety_shards.py already sums bins across shards;
needs a small "exceeded rate at threshold M" query added alongside
hist_percentile.py's percentile query. Caveat (documented repeatedly
in eval.c's guess history): the recorded swing distribution is itself
shaped by whatever margin was actually compiled in during that
calibration run -- a different final M reshapes the search tree,
which changes which nodes even reach the swing-recording code -- so
this gives a very good first estimate for both variants, not a
substitute for the real match_play/ecm validation once each variant
is actually built.
2026-09-18: sharded ASSERT-fuzzing harness -- the technique that found
the CommonSearchInit PV-staleness bug (DEBUG binary + gdb, batch mode,
`script <shard>.ep_` at a shallow sd, let it run until either an
ASSERT/SIGSEGV or clean exit) generalizes well and should be run
routinely, not just once ad hoc. Shard tests/cross_sectional_200k.ep_
(or any large stratified pool) into N pieces and run N concurrent
DEBUG-build _typhoon processes under gdb, one per shard, each logging
to its own file; investigate any hit (assert message + gdb backtrace)
the same way this session did. Needs a full core budget (N cores) so
it collides with self-play match runs -- schedule it for whenever the
box isn't already saturated by match_play.py workers, not concurrently
with them.
2026-09-18: blunders.pgn review (typhoon vs Crafty losses).
2026-09-18: consider adding cLikelyUnstoppablePasser to the POSITION
structure and using it to extend searches. Just like we already do
with cTrapped and cEnprise -- signals from eval.c to affect search.
2026-10-01: It seems like a Q vs RR in a pawnful endgame... Q dominates.
In general, we should work on encouraging the engine to maintain a good
pawn structure -- read more of that pawn power in chess book. Work on
the pawn eval specifically. Maybe do eval learning on only the pawn
hashed eval terms.
2026-10-04: We need to go faster.
- Can we get rid of rgSquares[] completely and use bitboards everywher?
- Profile and speed hunt again?
- Eliminate SEE calls?
- profile a super-lazy-always (material-only Eval() exit, forced
unconditionally, throwaway diagnostic build a la the zero-
selection-budget move-ordering trick -- never commit it) binary
against Crafty at matched depth/positions to find out where our
nps actually goes once eval is ~free. Scott's educated guess is
SEE and the is-this-move-a-check / legal-reply-to-check generators
dominate once eval is out of the way, not eval itself -- also
suspect given the earlier `<=`-to-`<` SEE-skip experiment on line
~4178 (skip SEE on same-piece- for-same-piece captures) barely
changed speed but made trees fatter (more nodes needed), i.e. that
SEE call was doing real pruning work, not just costing
time. Before assuming check-detection costs are irreducible,
confirm "is this move a check" is answered via O(1) bitboard AND
against the enemy king square from the post-move attack bitboard,
not via a generate-and-filter pass -- Scott believes he already
does this "cheaply" but isn't sure it's the fastest available
method. Single-legal-reply-to- check detection is likely the
genuinely unavoidable cost of the three (needs a real legal-move
count in the child position) -- worth checking whether it
early-exits as soon as a 2nd legal move is found rather than
generating the full list.
2026-10-04: Reign in extensions the make the tree smaller.
2026-10-04: Modify self-play harness to record eval score / PVs per
move in the PGN output.
|