Skip to content

reuse census sheets across uneven generations#97

Merged
andy-k merged 4 commits into
mainfrom
census-grow-sheet-reuse
Jul 17, 2026
Merged

reuse census sheets across uneven generations#97
andy-k merged 4 commits into
mainfrom
census-grow-sheet-reuse

Conversation

@andy-k

@andy-k andy-k commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

The census values leaves on sampled boards, and building a board's play sheet is the
costly step, so later generations reuse the sheets earlier ones built. That reuse only
worked when every generation used the same board count: a growing spec like 256,512,1024
fell back to rebuilding every generation, paying for the sum (1792) instead of the largest
(1024). This makes the reuse work for uneven specs, stops the cache holding sheets nothing
will read, and updates the recommended recipe.

Changes

  • Reuse sheets across uneven generations: track the running maximum board count and build
    only the slots at or above it.
  • Cache only the slots a later generation will actually read (live_after, a suffix
    maximum of the board counts -- a slot must survive a dip, so the lookahead cannot stop at
    the next generation), and size the cache to the highest slot any generation keeps.
  • Free the sheets no later generation will read, at the generation boundary.
  • Document both recipes: 4x256 (about 44s) and 3x256,2048 (about 142s).

Performance

Peak memory on spec 64,512: 15.55 GB -> 6.40 GB. 256,256,256,2048 now keeps the 256
slots its early generations share rather than a slot per board of its 2048-board
generation. Freeing does not lower the peak and cannot -- the live set only shrinks, so the
cache peaks before the first shrink -- but it hands the memory back during the run.

Correctness

Byte-identical leaves on growing, uniform, narrowing and grow-then-narrow specs, with
sheet-reuse on and off. Two tests pin the lookahead, including that a slot survives a dip
(400,100,300: the 300-board generation still reads slots 0..300).

Test plan

  • census output byte-identical vs the previous binary on 8,32,16,12 / 4x8 / 8,32,12 /
    32,8,12 / 12,40,20,16, sheet-reuse on and off.
  • new tests: census_sheet_reuse_plan_looks_past_the_next_generation,
    census_sheet_reuse_plan_never_reads_an_uncached_slot.

andy-k and others added 4 commits July 16, 2026 01:28
The census values leaves on sampled boards, one board per slot, and
each board is fixed by its slot number -- so every generation sees the
same boards. Building a board's play sheet is the costly step, so later
generations reuse the sheets that earlier ones built. That reuse only
kicked in when every generation used the same board count; a growing
spec such as 256,512,1024 fell back to rebuilding every generation,
doing the board work of the sum (1792) instead of the largest (1024).

Track the largest board count seen so far and let each generation reuse
every slot below it, building only the new ones on top. A growing spec
now costs the largest count once, not the sum. The output is
byte-identical -- same slots, same sheets, only the wasted rebuilds
removed -- so a cheap early generation can settle the table before a
costly last one pins it down.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The census values leaves on boards it samples, and building a
board's play sheet is the costly step, so a sheet is cached and
reused by the generations that follow. But every board cached its
sheet -- including the boards of the LAST generation, which no
later generation exists to read. A sheet holds one entry per rack
the lattice knows, tens of megabytes each, so those dead sheets
dominated the run's memory while buying nothing.

Work out, per generation, how many boards the generations after it
will use: live_after, a suffix maximum of the board counts. A slot
at or above it is dead the moment its generation ends, so a
generation fills the cache only below it, and the last generation's
count is zero, so it caches nothing at all. Size the cache itself
the same way -- the highest slot any generation keeps -- so a spec
whose largest generation comes last stops parking a slot per board
of it: 200,1000,400,300 needs 400 slots, not 1000, and
256,256,256,2048 needs 256, not 2048.

The lookahead is a suffix maximum rather than the next generation's
count because a slot has to survive a dip: in 400,100,300 the
300-board generation still reads slots 0..300, so the 400-board
generation must hold them across the 100. Looking only at the next
count would keep 100 and rebuild 200 sheets. Two tests pin this
down -- one on the arithmetic, one walking each spec to check that
no generation ever reuses a slot that was never cached.

A slot is never starved: a generation only reads a slot it also has
a board for, so any later reader of slot b forces b below the
live_after of the generation that built it.

Peak memory for a spec such as 256,256,256,2048 becomes the 256
slots the early generations share, not the 2048 the last one
builds. Measured on 64,512: 15.55 GB -> 6.40 GB, with the leaves
byte-identical (sheet-reuse on and off both give
08b86888304617a4d88e32f10e141fc7 on 8,32,16,12).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A generation now caches only the board slots a later generation
will read back, but it still held those sheets until the whole run
ended. Once a generation finishes, every slot at or above its
live_after is dead -- no generation still to come has that many
boards -- so there is no reason to keep paying for it.

Free them at the generation boundary. A spec that narrows, say
200,1000,400,300, sheds its tail as it goes: the 400-board
generation is the last to want slots 300..400, so those go before
the 300-board generation starts. The final generation frees the
lot, ahead of the confidence-interval report and the klv2 write.
It is safe there because every worker has passed the barrier, so
the generation's reads are done, and they wait on the next barrier
until the leader has finished.

This does NOT lower peak memory, and cannot: live_after only ever
falls as the run goes on, so the cache reaches its high-water mark
before the first shrink and never grows past it again. Measured on
64,512,256,128, peak is 10.42 GB either way. What it buys is
giving the memory back DURING the run instead of at exit, which is
what a census sharing a machine with other jobs actually needs.

The leaves are byte-identical: 8,32,16,12 / 4x8 / 8,32,12 all
match the values from before this change, and sheet-reuse on and
off still agree.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The doc has recommended four generations of 256 boards and told the
reader that adding boards or generations no longer moves the table.
The second half is not supported: it rested on a comparison at two
hundred thousand game pairs that came out a tie, which is far too
few pairs to resolve an edge this small. The tie was the measurement
failing, not the table having settled.

With enough pairs the edge shows, and it always points the same way.
4096 boards beat 256 about 50.02 to 49.98 over five million game
pairs at roughly 94 percent confidence, and 3x256,2048 edges 4x256
the same way -- 50.0064 at five million, 50.0103 and 50.0098 at one
million on two different comparison seeds. Four readings, one side,
every time.

The edge is about a hundredth of a percentage point and 4x256 is
three times faster (about 44 seconds against 142), so neither recipe
is simply the answer and the doc now carries both with the trade
stated: 4x256 while iterating, 3x256,2048 for a table worth keeping.
Calling 4x256 the best would misdirect, since it loses every time it
is measured; dropping it would cost the reader a table that is very
nearly as good for a third of the wait. The cost gap is structural,
not incidental -- every board is fixed by its position, so 4x256
builds 256 boards and reuses them, while 3x256,2048 must build all
2048 for its last generation.

Below 2048 boards in the final generation the edge goes away, and a
fifth generation measured no better than the third.

to size a run for an unfamiliar tile set. Measured, it is not: asked
to stop once nine leaves in ten had settled, it stopped after 576
boards and produced a table losing to 3x256,2048 by about 0.14 of a
percentage point at almost total confidence. Settling by that
measure is not the same as playing as well. Say so, rather than keep
recommending it. That run changed two things at once -- the stop
also requires WOLGES_CENSUS_RACK_SUMMARY=1, which makes the census
write a rack summary to decompose rather than a table directly, and
runs as a single generation -- so which of them cost the 0.14 is
recorded as unpinned rather than guessed at.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@andy-k
andy-k merged commit de77bde into main Jul 17, 2026
4 checks passed
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