[Feature] Add ReplayBuffer.update_if_present for generation-safe conditional updates - #4043
Draft
theap06 wants to merge 4 commits into
Draft
[Feature] Add ReplayBuffer.update_if_present for generation-safe conditional updates#4043theap06 wants to merge 4 commits into
theap06 wants to merge 4 commits into
Conversation
Executable spec for step 1 of the conditional replay-update RFC: round-robin writers stamp each slot with an int64 generation exposed via writer.generations_of(index); first write is generation 0, every slot reuse increments it, empty() never revives old handles, and generations persist through state_dict and dumps with legacy checkpoints still loading. Sampling exposes the stamps as an index_generation info entry / TensorDict key aligned with index. Tests are expected to fail until the implementation lands.
Round-robin writers now stamp each storage slot with an int64 generation counter: the first write of a slot has generation 0 and every round-robin reuse increments it, so a captured (index, generation) pair identifies one specific record and becomes detectably stale once the slot is recycled or the buffer is emptied. Generations are exposed through writer.generations_of(index), persist through state_dict and dumps/loads (legacy checkpoints still load), and are reported at sampling time as an index_generation info entry, which TensorDict buffers surface as a sample key next to index. Writers that do not track reuse report -1. Positional write_at and raw __setitem__ writes patch records in place and do not advance generations. A wraparound-heavy extend benchmark covers the hot-path cost. First step of the conditional replay-update RFC (pytorch#4040). Addresses pytorch#4041.
Executable spec for step 2 of the conditional replay-update RFC: rb.update_if_present(index=, generation=, patch=) applies every patch key to records whose (index, generation) is still live, skips reused or emptied slots without touching their content, and returns a result with an updated mask aligned to the input order plus updated/stale counts. The whole patch is validated before any write (KeyError for unknown keys, ValueError for shape or dtype mismatches, storage untouched in both cases), handles survive repeated updates, nested keys are supported, ListStorage raises a capability error, multidim storages round-trip, sampled handles flow straight into the call, and a concurrent writer/updater stress test pins non-torn multi-key visibility. RayReplayBuffer delegates the call to the actor. Tests are expected to fail until the implementation lands.
…itional updates Adds a best-effort conditional mutation API for stored replay fields. update_if_present(index=, generation=, patch=) applies a patch only to records whose (index, generation) pair still matches the writer's current slot generation, skipping records whose slot was recycled or emptied instead of corrupting them, and returns a ConditionalUpdateResult with a per-record updated mask plus updated/stale counts. The whole patch is validated (key existence, shape, dtype) before any write; validation failures leave storage untouched. The generation comparison and the patch write share one replay-lock acquisition, giving per-record atomicity against concurrent extends, and updating a record does not consume its handle. Tensor storages advertise supports_conditional_update; unsupported backends such as ListStorage raise a capability error instead of performing an unsafe raw-index write. RayReplayBuffer delegates the call to the actor in a single RPC (validation and write run inside the actor under its own lock); the distributed transport raises a clear capability error. Nested keys and multidimensional storages are supported. Second step of the conditional replay-update RFC. Closes pytorch#4040.
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/rl/4043
Note: Links to docs will display an error until the docs builds have been completed.
|
theap06
marked this pull request as draft
July 24, 2026 08:08
6 tasks
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.
Description
Second step of the conditional replay-update RFC: a best-effort conditional mutation API for stored replay fields, built on generation-stamped replay slots (#4041).
Some algorithms update stored data after sampling (refreshed recurrent states, cached embeddings, asynchronously computed labels). With a round-robin writer, the physical slot sampled from may hold a different record by write-back time, so an unconditional index write corrupts fresh data.
update_if_presentcloses that race optimistically instead of pinning the buffer:Semantics, matching the RFC's acceptance criteria:
KeyError, shape/dtype ->ValueError) before any mutation; a failed validation leaves storage byte-for-byte untouched, including multi-key patches where only one key is invalid.extendcannot interleave between validation of a record and its write; a stress test with a continuously extending writer thread verifies multi-key patches are never observed torn.(index, generation)pair keeps working until the slot is rewritten.supports_conditional_update;ListStorageand writers without generation tracking raise a clear capability error instead of performing an unsafe raw-index write.ndim>1) storages are supported.Remote buffers:
RayReplayBuffer.update_if_presentdelegates to the actor in a single RPC, with validation, comparison and write all executing inside the actor under its own lock; the distributed tensor transport raises a capability error pointing attransport="ray".Deferred per the RFC, with the keyword-only signature left room to grow: the compare-version facility (
version_key/require_newer), conditional priority updates (sampler metadata stays on its own API), and a declared-mutable-fields schema (any existing tensor field is currently patchable, validated at call time).Testing
TestUpdateIfPresent, written spec-first: live updates, stale-skip with content verification, mask alignment under shuffled mixed batches, repeated updates on one handle, validation-before-write (including the mixed-validity patch case), nested keys, capability errors,empty()invalidation, sampled-handle round-trip, multidim storages, and the concurrent no-torn-records stress test. A Ray-gated test covers the one-RPC delegation with a wraparound. Fulltest/rb/suite passes (4205 tests).Note
Draft, deliberately. The generation-stamp implementation (step 1, #4041) is being built by @harryfrzz; the stamp commits currently present in this branch are reference scaffolding only and are NOT part of this PR's claimed scope. Once the #4041 implementation lands, this PR will be rebased onto it so its diff contains only the conditional-update layer:
update_if_present,ConditionalUpdateResult, the storage capability/validation helpers, and the Ray delegation.Part of #4040: the compare-version facility and checkpoint round-trip coverage remain open on the issue for other contributors.