reuse census sheets across uneven generations#97
Merged
Conversation
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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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,1024fell 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
only the slots at or above it.
live_after, a suffixmaximum 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.
4x256(about 44s) and3x256,2048(about 142s).Performance
Peak memory on spec
64,512: 15.55 GB -> 6.40 GB.256,256,256,2048now keeps the 256slots 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
8,32,16,12/4x8/8,32,12/32,8,12/12,40,20,16, sheet-reuse on and off.census_sheet_reuse_plan_looks_past_the_next_generation,census_sheet_reuse_plan_never_reads_an_uncached_slot.