Repository navigation
Move the late move reductions and pruning into one module - #265
Conversation
|
Review round 1 (Opus reviewer, own build directory, both sides rebuilt from
Every number in the commit body was checked against the diff by the reviewer and holds. |
Speed against
|
…module The decision about a quiet move its ordering put late was spread through `engine.rs`: twenty constants among the search's other figures, two predicates with six and nine parameters, a staging field on the engine, and two derivations of the features the attention model reads, which a third in the fit's python had to agree with. It is now `arche-core/src/late_move.rs`, 1164 lines, private to the crate, and `engine.rs` loses 1161 lines for 188. What moved, with its comments and its fit provenance as they were: the five reduction figures with their floors, the thirteen `ATTENTION_*` weights and the two thresholds, `AttentionFeatures`, `attention_score`, and the bodies of `reduces` and `model_gate`. The scout stays in `windowed`, the recorder hooks stay in `engine.rs`, and `census::Table` stays in `census.rs`, which the module's doc says and why. The move loop's two calls become one. `decide(&Search, &mut Node, m, searched)` answers a `Verdict`: `Scout(n)` for a scout n plies shallower, zero meaning the full search, or `Skip`. `Search` is three references, the board, the ordering and the config, so the module's interface names what the decision reads and a test builds one with no engine behind it. `Node` carries the node's facts and borrows the two dear features. They are lazy separately: the static evaluation is computed by the first move the gate scores, since only the score wants it, and the history denominator by the first move the gate scores or the ledger stages. A node neither reaches pays for neither, armed or not. The `Gate` enum goes: the plies say what its three words said, and a move the reduction never touches is `Scout(0)` where it used to be a separate bool. The engine's two feature derivations become one. `late_move::features` is the one place they are derived, in the units the ledger prints; the gate reads its history fraction off that value and the staging asks the same function over the same node, so the two readings cannot diverge. The second history walk in `staged_reduction` goes with it. They used to agree only because the gate clamped `history` and left `history_max` signed while the ledger clamped the other one, and the guard on the denominator made the pair equal; the gate now reads the clamped denominator, which the guard leaves worth the same. The denominator is also read once for the node and held, as the gate always held it, so a row records the largest the score was read against rather than the largest by the time the staging ran: a child search between two of a node's moves can teach the history, and INSTRUMENTS.md says which of the two the column is. The fit's python is the third derivation and stays as it is: it reads printed rows, and those rows now say what the gate saw. The `staged` field goes. `search_child` and `windowed` take an `Option<&reduction::Staged>`, `None` at the two sites that never reduce, so a reduced move inside a scout cannot consume the staging of the move being scouted: a parameter is not reachable from a deeper node, where a field was. The `Recorded::disarm` override that cleared the field goes, and with nothing left overriding it the provided method goes too. `search_child` is one argument past clippy's limit and carries the allow; `ledger_skip` drops the one it had. `census::Table::of` is read once for the node instead of at the four sites that asked for it. Fourteen tests stand in the new module, twelve of them moved and two new: that the gate's threshold edge sits where the recorded row's own score puts it, and that the denominator is walked once for the node. The two exemption tests that also ran a search are split, the predicate half moved and the node-for-node half left beside the search under a name that says what it measures. The leak-path test goes with the field. `engine.rs` holds 121 tests where it held 132, and the suite counts 478 in debug where it counted 475, 476 in release where it counted 473. `A_CAPTURE_AND_QUIETS` moves to `board::fens`, which both modules read. No search behaviour changes. The bench is the same 4102609 nodes at depth seven position by position, rebased over the king attack fit and the mobility walk that took its counts (the branch benched 3868987 on the 4b1b80f it was cut from), and the tactical and strategic totals do not move. Five interleaved rounds against 4b1b80f read faster after the change, +0.8% on the medians and +0.4% on the fastest rounds, on a spread of 4.2%: inside the spread, so the clock makes no claim either way and the staging travels as a parameter rather than as a field. Callgrind over the identical bench prices it at +3,110,295 instructions, 4,024,717,023 against 4,027,827,318, which is +0.077% and well under this box's clock floor. It lands as +1,906,840 in `alpha_beta`, about half an instruction a node, which is the `Node` literal the loop writes for each move, against -1,637,407 in `windowed`, where the parameter costs less than the field's read and take did. Bench: 4102609 Elo: not measured Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1f5ed96 to
37fe452
Compare
Pass 6 of the architecture review, candidate F1 (pass 5's E2 re-read). A shape change with no search change: the bench is master's 4102609 on both sides, position by position (3868987 before the rebase over the king attack fit), and the tactics and strategy pins do not move.
The decision about a quiet move ordered late was spread through
engine.rs: twenty constants, two predicates with six and nine parameters, a staging field on the engine, and two derivations of the features the attention model reads (with a third infit_attention.pythat had to agree). It is nowarche-core/src/late_move.rs. The move loop asks one question,decide(&Search, &mut Node, m, searched), and gets Skip or the plies to scout.Searchis three references (board, ordering, config), so the module names what it reads and a test builds one with no engine.The gate and the reduction ledger now read the features from one function over the same node. The
stagedfield goes: the staging travels as anOption<&Staged>parameter throughsearch_childandwindowed, so a reduced move inside a scout cannot consume its parent's staging. The ledger'shistory_maxcolumn becomes the node's held denominator, as the gate always read it, rather than a fresh walk per staging; INSTRUMENTS.md says so and a test holds the cache.Callgrind on the identical bench: +0.077% instructions, the per-move
Nodeliteral against a cheaper parameter, under the clock floor. Tests 475 to 478 debug, 473 to 476 release.The handoff with the eight grilled decisions is in the planning repo (
handoffs/late-move-module.md). One Opus review round, both sides rebuilt: no behavioural defect, nine findings folded into the commit.🤖 Generated with Claude Code