Skip to content

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

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

[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

Adds ReplayBuffer.update_if_present, a generation-safe conditional mutation API for replay fields, on top of the opt-in generation stamps merged in #4046.

rb = TensorDictReplayBuffer(
    storage=LazyTensorStorage(100),
    writer=TensorDictRoundRobinWriter(track_generations=True),
)
rb.extend(data)
sample = rb.sample(32)
result = rb.update_if_present(
    sample,
    TensorDict({"priority": new_priority}, batch_size=sample.batch_size),
)

The API:

  • Validates the complete patch before making any writes, including keys, dtypes, and shapes.
  • Compares generations and applies multi-key patches while holding the replay-buffer and writer locks.
  • Skips entries whose slots were emptied or overwritten after sampling.
  • Keeps a handle reusable until its slot is actually rewritten.
  • Supports nested keys and replay buffers with ndim > 1.
  • Raises a clear RuntimeError for unsupported storages and writers without generation tracking.
  • Uses one actor RPC for Ray replay buffers; distributed replay buffers report the unsupported capability explicitly.
  • Returns a ConditionalUpdateResult with the applied mask and update/skip counts.

Version-comparison support remains in #4049.

Tests and documentation

  • Adds 14 focused TestUpdateIfPresent cases, including TensorDict patches, stale handles, validation, nested keys, and multidimensional storage.
  • Adds Ray transport coverage and a replay-buffer benchmark.
  • Documents the API and its generation-safe semantics in the replay-buffer reference.
  • Locally: 497 core replay-buffer tests passed, the focused Ray test and doctests passed, and all pre-commit hooks passed.
  • The broader replay-buffer run reached 4,376 passed and 891 skipped; the remaining failures require unavailable local dependencies or services (h5py, working Torch Inductor/OpenMP metadata, and a resolvable Gloo hostname).

Part of #4040. Builds on #4046.

@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 b8bf0a7 with merge base d7659c7 (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
@vmoens
vmoens force-pushed the feature/rb-update-if-present branch 2 times, most recently from 6b96677 to 6b16ca6 Compare August 5, 2026 16:23
theap06 and others added 4 commits August 9, 2026 01:37
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.
…ytorch#4046)

Drop this PR's own generation-stamp scaffold (writers.py machinery,
always-on tracking, sample-info insertions and their test patches) in
favor of the storage-owned, opt-in implementation from pytorch#4046:

- update_if_present keeps its contract but now requires a writer
  constructed with track_generations=True and raises otherwise; its
  generation comparison and patch masks are device-aware so CUDA/MPS
  storages and mixed-device handles work.
- TestUpdateIfPresent builds its buffers with tracking writers, gains a
  test for the non-tracking capability error; the superseded
  TestSlotGenerations and TestSampleGenerationInfo suites are removed
  (covered by pytorch#4046's TestWriterGeneration).
- The Ray test threads a tracking writer factory to the remote buffer.
- The wraparound benchmark enables tracking so it no longer measures a
  no-op; docs and docstrings describe the opt-in reality and link to
  the generation-stamp reference section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refresh update_if_present on the merged opt-in generation API. Add typed Ray delegation with CPU payload staging, delayed-init handling, compile-friendlier empty-live behavior, executable public examples, TensorDict-patch coverage, and a benchmark of the conditional update itself.
@theap06
theap06 force-pushed the feature/rb-update-if-present branch from 6b16ca6 to b8bf0a7 Compare August 9, 2026 08:51
@theap06
theap06 marked this pull request as ready for review August 9, 2026 08:51
@theap06

theap06 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and #4046, resolved the obsolete generation-stamp history, and marked this ready for review. The focused core, Ray, and doctest coverage passes locally. I do not have permission to request reviewers or apply labels; @vmoens, could you review and add the ci/optdeps label, then rerun CI after the label is present? This PR touches the Ray optional-dependency integration.

@vmoens

vmoens commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Reviewed rebased head b8bf0a7a0bc3ec5b995569aaf1414f86ac2553b7, including the latest generation-stamp reconciliation commit and the request for optional-dependency CI.

One blocking correctness issue is at torchrl/data/replay_buffers/replay_buffers.py in the live = current == generation comparison. Generation -1 explicitly means “no usable stamp”, but equality treats (-1, -1) as live. I reproduced this with a size-10 lazy storage containing only 5 records: update_if_present(index=tensor([7]), generation=tensor([-1]), patch={"x": tensor([99.])}) returns updated=True and writes slot 7 even though len(rb) == 5. Please exclude negative current/captured generations (and add the never-written-slot regression) so only genuine live handles can mutate storage.

Targeted result: all 14 TestUpdateIfPresent tests pass, which confirms the missing case. After the fix, the author-requested ci/optdeps label/rerun is still appropriate because the diff changes the Ray integration.

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.

2 participants