Skip to content

Fit the king attack zone weights, and take its counts in the mobility walk - #264

Merged
aywrite merged 2 commits into
masterfrom
eval/king-attack-fit
Sep 17, 2026
Merged

aywrite merged 2 commits into
masterfrom
eval/king-attack-fit

Conversation

@aywrite

@aywrite aywrite commented Sep 16, 2026

Copy link
Copy Markdown
Owner

The second half of the king attack zone, whose counts landed at zero weight in #258. Two commits.

Fit the king attack zone weights on the archive. Knight pack(11, 3), bishop pack(20, -2), rook pack(27, -3), queen pack(17, 9), fitted on 50,677 games with the tables, material, mobility, shelter and pawn structure held. The midgame half is positive for all four and the endgame half near zero except the queen, so the taper came out of the fit. The held-out games read -0.000143 ±0.000059 (2.42 standard errors), which is the figure quoted: the selection group read -0.000395 and sat 3.2 standard errors away, so it overstated the fit. The match passed [0, 10] at 10+0.1 on 8moves_v3: +35 ±16 over 1,000 games, LLR 4.21, against 4b1b80f.

Take the king attack counts in the mobility walk. Standalone the term was 11.51% of the bench run's instructions. Now one walk per side reads each attack set twice, against mobility's scope and against the enemy king's ring. The shipped build is 7.64% fewer instructions than the first commit over the same tree, and 4.34% more a node than master before the term. Each term keeps its own counts for the tuner, and a test holds the shared walk to both over the bench, tactical and strategic suites.

Bench 3868987 to 4102609. Tactical 224 to 237 and strategic 99,938 to 100,176, which the suites cannot read as strength. The match was played on the tip of this branch; the commits were reworded afterwards to carry their trailers, with the trees unchanged.

Each commit builds, tests and benches on its own, and carries its own Bench:, Elo: and Speed: trailers, so a squash merge would fold them into one message.

🤖 Generated with Claude Code

aywrite and others added 2 commits September 16, 2026 23:51
The king attack zone landed at zero weight, counted but not scored. These are
its eight weights, fitted on the whole archive with every other term held:

    knight  11 / 3       bishop  20 / -2
    rook    27 / -3      queen   17 / 9

310 artifacts and 50,677 games, whose 6,623,970 post-book plies gave
6,227,777 positions and 2,890,773 quiet rows at the 820 slot layout. The
corpus is sha256 a0798d51, built with the sealed pairs named, and those are
sha256 a723887f. The rows are sha256 6001063b and the fitted vector sha256
f65aa57e. K was held at 1.0821. The games split by pair: 16,300 pairs and
32,516 games trained, 5,528 pairs and 11,030 games chose the ridge, and 3,498
pairs and 6,985 games were sealed. The seal file names 3,501 pairs over 6,995
games. Three of those pairs reach no row, which accounts for at most six of
the ten missing games, and where the other four went is not established. The
768 table entries, the six material values and the mobility, shelter and pawn
structure weights were all held, so these eight are the only thing that moved.

The fit's figure is the sealed group's. It was opened once, at
2026-09-16T10:28:31Z, after the vector was frozen. Over 417,778 positions it
reads -0.000143 against a standard error of 0.000059 at a design factor of
3.2. That is 2.42 standard errors from zero, outside its interval.

The selection group reads -0.000395 against a standard error of 0.000052 at
the same design factor: 0.085901 at zero, 0.085506 at the fit before rounding
and 0.085496 after. By phase it is -0.000261 at six pieces or fewer, -0.000811
from seven to twelve and +0.000008 at thirteen or more. The two readings are
about 3.2 standard errors apart. By the rule the fit was registered with, that
a disagreement wider than the mobility refit's 1.85 standard errors means the
sealed figure is the one quoted, the selection group's -0.000395 ±0.000052
overstated the fit.

Two findings in the sealed reading were written down before any game was
played. The first is its size: about a third of the selection group's, and
about 3.2 standard errors from it, where the mobility refit's two readings
were 1.85 standard errors apart and the king safety fit's 0.68. Most of the
sealed games are the lazy mobility arms, whose candidates skipped the mobility
term at most quiescence stand pats and so reached positions a different
evaluation chose. The second is where it pays. By phase the sealed reading is
-0.000088 at six pieces or fewer, -0.000414 from seven to twelve and +0.000366
at thirteen or more, so on those games the fullest boards read slightly worse
at these weights. No standard error is printed per bucket.

The ridge is zero, which the grid ranked first. It would have been overruled
had the largest weight sat on the column with the least support. It did not:
the largest is the rook's 27, and the rook column is the best supported of the
four. A midgame coefficient is written in 16.2% of the training rows for the
knight, 27.0% for the bishop, 30.5% for the rook and 17.4% for the queen, and
little of that is on a full board. A sample of every twentieth of all
2,890,773 rows, across all three groups and reading positions only, finds a
rook bearing on the enemy ring from 0.6% of the sides with thirteen or more
pieces left, against 21.7% over the sampled rows. Because the sample spans
every group, it is a different population from the training row shares.

The learning curve is flat from an eighth of the training pairs. Five draws of
2,038 pairs read -0.000346 to -0.000397 against the whole set's -0.000395, so
the corpus is not what limits eight weights.

The match, played on this commit with the shared walk of the one after it,
passed [0, 10] at 10+0.1 on 8moves_v3 over 1,000 games in two batches: +35
±16 elo pooled against 4b1b80f, which plays the zero weight tree. The
batches read +36 ±23 and +33 ±24, and the likelihood ratio reached 4.21
against a bound of 2.94. Under +40, so it is not re-measured.

## What moved

The bench goes from 3,868,987 to 4,102,609, which is 6.04% more. Sixteen of
the eighteen default pins and sixteen of the eighteen reference ones moved.
`king and pawn` and `trebuchet` are unmoved in both sets.

    default                    before      after
    start                      68,341     73,075
    italian                   150,035    132,136
    ruy lopez                 161,892    127,220
    kiwipete                  243,106    275,925
    perft 4                   110,373    122,259
    promotions                 54,363     46,622
    middlegame                100,507    167,457
    sharp middlegame          180,608    184,701
    bratko kopec 1            571,361    671,225
    wac 4                   1,944,432  1,946,406
    rook and pawns             29,021     29,112
    tarrasch                   89,870     53,975
    lucena                     15,468     16,701
    philidor                   61,191     60,589
    minor endgame              25,367     30,918
    queen endgame              56,696    157,932
    king and pawn               3,522      3,522
    trebuchet                   2,834      2,834

    reference                  before      after
    start                      28,328     33,729
    italian                   227,641    120,260
    ruy lopez                 120,190    118,515
    kiwipete                  178,402    224,244
    perft 4                   232,961    255,435
    promotions                 95,254     95,210
    middlegame                167,371    164,325
    sharp middlegame          277,951    241,920
    bratko kopec 1             44,544     52,545
    wac 4                      89,394     89,428
    rook and pawns             24,020     22,615
    tarrasch                   33,182     48,576
    lucena                     21,127     24,290
    philidor                   51,857     29,763
    minor endgame              34,141     30,627
    queen endgame              96,523    175,117
    king and pawn               1,601      1,601
    trebuchet                     968        968

The tactical suite goes from 224 to 237, which puts back more than the mobility
refit took. The floor's comment now counts the history with 237 in it and the
empty list of accepted losses counts sixty three misses.

The strategic suite goes from 99,938 to 100,176. Eight themes rise and seven
fall. AKPC rises most, by 432, then Undermine by 275 and Square Vacancy by 235.
Pawn Play in the Center falls 322, Offer of Simplification 311 and Advancement
of a/b/c pawns 222.

The uci session's node budget moves with the tree, as its comment says it will.
The swap is still at depth ten, which now ends at 2,114,707 and reports the new
move from about 1,995,000, so the budget goes from 2,800,000 to 2,050,000. No
other test moved: the pins in `engine.rs`, `reduction.rs`, `residual.rs` and
the reductions session all pass as they stand.

## What it costs

Callgrind over `arche bench` at depth 7, on a build with `king_attack::fold`,
`king_attack::counts_of` and `mobility::fold` forced out of line:

    king_attack::fold, inclusive     558,628,090   11.51% of the run
      of which counts_of             512,071,008   10.55%
    mobility::fold, inclusive        553,880,961   11.41%
    the run                        4,854,634,752

Those three attributes cost 33,558,174 instructions of call overhead the
shipped build does not pay: the shipped build is 4,821,016,788. What the
figure prices is this arrangement, a second walk over both sides' pieces at
every leaf beside mobility's, and it costs about what mobility's walk costs,
136 instructions for each node the bench counts against mobility's 135. The
run is 4,024,717,460 instructions over 3,868,987 nodes before this and
4,821,016,788 over 4,102,609 after, which is 1,040 a node against 1,175, or
13.0% more.

That is over the 5% rule of thumb. The figure is recorded here and the counts
are left as their own walk. Taking both readings in the mobility walk is a
separate change and is not made here.

On the clock, `scripts/speed.sh` against 4b1b80f over 21 interleaved rounds
reads -7.6% on the median nodes per second and -8.8% on the fastest, against a
spread of 8.9%. The tree is 6.0% larger, so the bench takes 14.8% longer to
reach depth 7. The box was shared during the run, so the instruction count is
the claim.

## What went

The scaffolding the zero weights needed. The equality on the term's name in
`every_term_writes_the_counts_its_width_claims` is back to asserting every
term is priced. `the_table_names_the_terms_the_sum_adds` reads the shipped
fold again rather than trial weights, on the same position, where all four
folds are non-zero. `fold_with` is private again, and the fold test no longer
asserts that the shipped fold is zero. The doc blocks and comments that said
the term was unfitted, in `king_attack.rs`, `eval/mod.rs`, `tune.rs`, the
architecture overview and the instruments guide, say what it is now. The
readme counts four fitted leaf terms. The roadmap says what the term counts
and what the fit read, and gives its cost.

Bench: 4102609
Elo: +35 ±16 (sprt [0, 10] passed, 1000 games, 10+0.1, vs 4b1b80f)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mobility and the king attack zone each walked both sides' knights, bishops,
rooks and queens at every leaf, and each probed the same attack sets: the
knight table and the two magic lookups over the whole occupancy. Now one walk
per side takes each piece's attack set once and reads it twice, against
mobility's scope for the mobility counts and against the enemy king's ring
for the king attack counts. The probes and the loops over the pieces are what
is shared. The masks, the columns, the weights and the folds are not: each
term folds its own counts through a new `fold_counts`.

The skips still hold. The walk takes mobility's `SCORED_KINDS` and the king
attack `SCORED` as compile time parameters, so a count that is not asked for
is not compiled, down to the loop over a kind neither term wants. Read off the
disassembly of the walk forced out of line: all four kinds with the ring is
227 instructions with 8 popcounts, without the ring 161 with 4, the knight
alone without the ring 61 with 1 and no magic multiply, the knight with the
ring 188 with 5, and nothing at all 9.

The walk lives in `eval/mod.rs`, beside the sum, which is its one caller. It
is the sum's arrangement of two terms rather than a part of either, and
putting it in `mobility.rs` would have that file compute the other term's
counts (and import from a file that already imports from it). A separate
`eval/attacks.rs` was the other choice and was declined: it would be a file
for one function with one caller and no counts, weights or fold of its own.

`mobility::counts_of` and `king_attack::counts_of` are unchanged. The tuner
still reads them through `TERMS`, the hand counts still pin them, and they
are now what the walk is tested against. `fold` in each term file is kept for
the tests, as the fold over the standalone counts the sum used to answer.

## What it costs

Callgrind over `arche bench` at depth 7. The attributed builds force the
measured functions out of line: at 02432c9 the two folds and
`king_attack::counts_of`, here the walk and the two `fold_counts`. A third
build is this commit with the walk's ring turned off and the king attack
term read by its own walk again, which gives the walk without the ring over
the same tree.

    attributed                          02432c9         this commit
    the run                       4,854,574,730       4,600,409,835
    per node                            1,183.3             1,121.3
    king_attack::fold             558,628,090 11.51%
      of which counts_of          512,071,008 10.55%
    mobility::fold                553,880,961 11.41%
    the shared walk                                 777,417,417 16.90%
    the walk without the ring                       576,199,267
    the ring                                        201,218,150  4.37%
    king_attack::fold_counts                         21,162,310  0.46%
    mobility::fold_counts                            21,162,310  0.46%

    shipped                             02432c9         this commit
    the run                       4,821,016,788       4,452,853,888
    per node                            1,175.1             1,085.4

The node count is 4,102,609 in every build. The shipped run is 368,162,900
instructions shorter, 7.64%. What the ring adds to the walk is 49 instructions
a node, and 54 with its fold, against the 125 the standalone counts cost. The
walk without the ring costs more than 02432c9's `mobility::fold` because it is
called once per side rather than once per leaf in that build and returns both
arrays, so the attributed builds overstate the walk a little and the shipped
figures are the ones to compare.

Against 4b1b80f, before the king attack zone, measured again here on its
shipped build at 4,024,751,355 instructions over 3,868,987 nodes, 1,040.3 a
node, this commit is 4.34% more a node. The fit alone was 12.96% more. The
ring's share of the attributed run is 4.37%, and 4.83% with its fold. Both are
under the 5% rule of thumb.

These price instructions over one tree, so they are what the arrangement
saves and nothing about strength. The fit commit's attributed build reproduces
its own figures to within 60,022 instructions of the run. It was measured as
4379b1b, whose code 02432c9 carries unchanged: the two differ in comments and
docs alone.

On the clock, `scripts/speed.sh` against 02432c9 over 21 interleaved rounds
reads +5.7% on the median nodes per second and +5.3% on the fastest, against a
spread of 12.2%, which is two slow candidate rounds (7 and 18). The box was
shared, so the instruction count is the claim, and the clock agrees with it in
sign.

## Tests

`the_shared_walk_counts_what_each_term_counts_alone` holds the walk to the two
standalone counts for both colours on every position of the bench, tactical
and strategic suites and the shared fens. It asks five instantiations: all
four kinds with the ring, the live constants, two that leave out different
kinds with the ring and without it, and no kind without the ring. A skipped
count has to answer zero. The hand count tests of both terms run the same
check on their positions.

Two mutations were run against it. Reading the ring through mobility's scope
fails it, and so does skipping the queens. Each also fails both hand count
tests and `the_table_names_the_terms_the_sum_adds`.

The ARCHITECTURE entries say the two terms share one walk at the leaf and keep
their own counts for the tuner. The doc comments that said `eval` and the
tuner read one function for these counts now name the shared walk and the test
that holds it to them. The roadmap's budget bullet carries the fused figures,
with the standalone 11.5% kept as history.

Bench: 4102609
Speed: +5.7% (bench nps, 21 interleaved rounds vs 02432c9, spread 12.2%)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@aywrite

aywrite commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

Review rounds, all folded before the match was played.

The fit commit was read by one reviewer on its own, then both commits by a second that rebuilt 4b1b80f, the fit and the shared walk in its own clone. Neither found a defect in the code. What it established: 4,102,609 nodes on both commits with every bench row identical but the times; the shipped instruction counts reproduced to within 35,000 of about four and a half billion on all three builds; every provenance figure matched the raw fit outputs; seven mutations of the shared walk (an x-ray through the blocker, the knight's ring through the scope mask, the wrong king, the queen read as a bishop, the skip parameters swapped, the sides' arrays swapped both ways) were each caught or did not compile.

Findings, folded into the commit each belonged to:

  • the fit was registered to quote the held-out reading when the two readings disagree by more than 1.85 standard errors; they disagree by 3.2 and the records led with the selection figure
  • the seal's ten missing games were written as explained; three pairs with no row account for at most six
  • doc comments in mobility.rs and tune.rs still described eval reading the standalone counts, after the second commit changed that
  • the 5% cost figure written as an allowance, where it is a rule of thumb nothing enforces
  • a claim that the box was idle during the timing runs, when other work was running on it
  • a sample of the ring counts described as over training rows when it spanned all three groups
  • the storm sentence in the roadmap separated from its antecedent, three-part parallels, and wrapping

One test was added: the shared walk with no kind and no ring, where every count must answer zero. No production code changed in the fold. Airgap and trailers checked on both commits.

@github-actions

Copy link
Copy Markdown
Contributor

Speed against 4b1b80fd

Both sides built and run on this runner in this job, the way
scripts/speed.sh measures a perf commit. The change is between
medians, and the spread beside it is what the change has to be
read against. The node counts are the search's: they move when
the search does, and a speed change leaves them alone.

round     base nps  candidate nps
    1      8366553        8250463
    2      8441911        8056716
    3      8328874        8062400
    4      7945624        8029154
    5      8006495        7955556
    6      7987388        7810506
    7      8271997        8111978
    8      8291000        8150884
    9      7918402        7757046

             nodes    time  median nps  fastest nps
base       3868987  0.47 s     8271997      8441911
candidate  4102609  0.51 s     8056716      8250463
change       +6.0%   +8.9%       -2.6%        -2.3%

the node counts differ, so this is a search change as well as a speed
one. nps only says what a node costs. Time to depth is what the change is
worth at this depth, and whether the new tree is the right one is for
games to say.

Speed: -2.6% (bench nps, 9 interleaved rounds vs 4b1b80fd, spread 6.3%)

@aywrite
aywrite merged commit 7991f40 into master Sep 17, 2026
19 of 20 checks passed
@aywrite
aywrite deleted the eval/king-attack-fit branch September 17, 2026 04:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant