Repository navigation
CircularBuffer: move added elements into their slots; compact loaded buffers - #1637
Open
lemony-fresh wants to merge 1 commit into
Open
lemony-fresh wants to merge 1 commit into
lemony-fresh wants to merge 1 commit into
Conversation
…buffers Once full, Add copy-assigned the new element into the oldest slot, and a copy-assigned std::vector keeps its storage when that is large enough, so each slot kept the storage of the largest element it ever held. Add now takes the element by value and moves it into the slot. SerializableCircularBuffer::LoadBuffer: libnop reads vectors of non-integral elements one push_back at a time, leaving up to twice the capacity they need; each loaded element is now replaced by a copy of itself.
lemony-fresh
added a commit
to lemony-fresh/open_spiel
that referenced
this pull request
Oct 6, 2026
…at keeps its size - watch_run.py, the evaluation watcher: a checkpoint's ladder rating per side, beside a run or after it (--all); matches in the idle class, only with memory to spare. The load test (load_toggle.py, load_test_analysis.py) found it costs the trainer ~25-35% of its self-play, mostly efficiency: evaluate between runs. - analyse_games.py, the game analyser: replays the actors' games (captures per side, first captures, openings); checked against logged returns and endings, and the trainer's per-step margins. - Our trainer's replay buffer (thud/az/replay_buffer.h) moves positions into their slots and compacts itself after loading: upstream's kept each slot at the largest position it held (+72%) and loaded with 45% spare. replay_buffer_check: same contents, samples and file as upstream's, exact memory; controls. Actor and evaluator logs append. - az_search_cost.cc, the CPU study; ladder.py's fit as a function. - Docs: run W, G' to step 73 (the dwarfs' narrowing), the GPU plan, the conventions on diagnostics and safety flags, PR google-deepmind#1637. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lemony-fresh
added a commit
to lemony-fresh/open_spiel
that referenced
this pull request
Oct 18, 2026
…epmind#1637 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Problem
Once a
CircularBufferis full,Addcopy-assigns the new element into the slot of theoldest one. For elements that hold vectors, such as AlphaZero's
TrainInputs, acopy-assigned
std::vectorkeeps its existing storage whenever that is large enough, soeach slot keeps the storage of the largest element it ever held. With elements of varying
size, the buffer's memory keeps growing long after the buffer is full.
SerializableCircularBuffer::LoadBufferadds to this: libnop reads vectors of non-integralelements (the
floatobservations and the policy's(action, probability)pairs) onepush_backat a time, deliberately withoutreserve(), which leaves them with up to twicethe capacity they need.
We ran into this training the C++ AlphaZero with a replay buffer of 458,752 positions
(4.7 GB as saved) on a game whose two players have very different numbers of legal actions
(a median of about 20 for one, about 240 for the other): the learner's heap kept growing
after the buffer was full, and right after resuming from a saved buffer the buffer held
about 45% more than its elements needed.
The overhead is a share of the buffer's own size, so it matters most where memory is
tight: on machines with 32 GB or less, where the buffer competes with everything else
running. With more memory it wastes the same share, but is easier to afford.
Change
CircularBuffer::Addtakes the element by value and moves it into its slot, so a slotholds the new element's storage. Callers are unchanged: an lvalue is copied into the
parameter, as before; an rvalue is moved.
Why by value, not by
const T&: copy-assigning into the slot is what keeps the slot'sold storage, so the fix needs a copy into a fresh object, then a move into the slot
(from a reference:
data_[i] = T(value)). Taken by value, the caller's argument is thatfresh object: copied from an lvalue, as before, or moved from an rvalue without any
copy — one function instead of two overloads (
const T&andT&&). Abseil'sTip #117 recommends this for arguments a function stores,
and names the one trade-off, which is the point here: copy-assignment "can reuse
existing buffers". That reuse is usually welcome, but with elements of very different
sizes it makes every slot keep the largest. Where sizes do not vary (DQN's transitions),
the change costs one allocation per added element, small next to producing the element.
SerializableCircularBuffer::LoadBufferreplaces each loaded element by a copy of itself,which holds only what it needs.
No new dependencies: the change uses only the standard library (
std::move).Measurements
With elements shaped like
TrainInputs(6 x 15 x 15 float observations, positionsalternating between 150-330 and 10-40 legal actions) in a buffer of 2,000:
AddThe contents, samples drawn with the same seed, and the saved file are identical before and
after. In training, with the same change in our copy of the trainer, the learner's heap
stayed flat at 5.7 GB overnight; without it, it was at 6.1-6.4 GB and growing.
Tests
New:
circular_buffer_test: a slot that held a large vector does not keep its capacity when asmall one replaces it.
serializable_circular_buffer_test: a loaded vector of 1,025 floats holds less than 1.5times its size in capacity.
Both new tests fail without the change and pass with it.
Run with the change (Linux aarch64, clang 21.1.8, LibTorch 2.10.0):
circular_buffer_testandserializable_circular_buffer_testthroughctest, in thedefault build; both test programs also under AddressSanitizer and
UndefinedBehaviorSanitizer, without a report.
torch_integration_test,torch_model_test,torch_vpnet_test(AlphaZero) anddqn_torch_test(DQN).alpha_zero_torch_exampleon tic-tac-toe: 2 steps with a buffer that is full by thesecond, then resumed from its
config.jsonto a third step — loading the saved bufferand replacing elements of a full one.
🤖 Generated with Claude Code