Skip to content

[Feature] Add ReplayBuffer.update_if_present for generation-safe conditional updates - #4043

Draft
theap06 wants to merge 4 commits into
pytorch:mainfrom
theap06:feature/rb-update-if-present
Draft

[Feature] Add ReplayBuffer.update_if_present for generation-safe conditional updates#4043
theap06 wants to merge 4 commits into
pytorch:mainfrom
theap06:feature/rb-update-if-present

Conversation

@theap06

@theap06 theap06 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

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_present closes that race optimistically instead of pinning the buffer:

sample = buffer.sample()
result = buffer.update_if_present(
    index=sample["index"],
    generation=sample["index_generation"],
    patch={"recurrent_state": refreshed_state},
)
result.updated        # bool mask aligned with the input index order
result.updated_count  # live records patched
result.stale_count    # recycled/emptied records skipped, content untouched

Semantics, matching the RFC's acceptance criteria:

  • Validate-then-write: the whole patch is checked against the storage (key existence -> 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.
  • Per-record atomicity: the generation comparison and the patch write share one replay-lock acquisition, so a concurrent extend cannot 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.
  • Handles are not consumed: updating refreshes a record's content, not its identity; the same (index, generation) pair keeps working until the slot is rewritten.
  • Capability-gated: tensor storages advertise supports_conditional_update; ListStorage and writers without generation tracking raise a clear capability error instead of performing an unsafe raw-index write.
  • Nested keys and multidimensional (ndim>1) storages are supported.

Remote buffers: RayReplayBuffer.update_if_present delegates 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 at transport="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. Full test/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.

theap06 added 4 commits July 24, 2026 00:21
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.
@pytorch-bot

pytorch-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

🔗 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.

⚠️ 15 Awaiting Approval

As of commit 11715f6 with merge base ae421b9 (image):

AWAITING APPROVAL - The following workflows need approval before CI can run:

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@github-actions github-actions Bot added Feature New feature Documentation Improvements or additions to documentation Benchmarks rl/benchmark changes ReplayBuffers and removed Feature New feature labels Jul 24, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 24, 2026
@theap06
theap06 marked this pull request as draft July 24, 2026 08:08
@github-actions github-actions Bot added the Feature New feature label Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Benchmarks rl/benchmark changes CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Documentation Improvements or additions to documentation Feature New feature ReplayBuffers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant