Skip to content

Add baseline filtering and scoring options to ReplayBuffer#524

Merged
josephdviviano merged 2 commits into
masterfrom
feat/replay-buffer-perf
May 29, 2026
Merged

Add baseline filtering and scoring options to ReplayBuffer#524
josephdviviano merged 2 commits into
masterfrom
feat/replay-buffer-perf

Conversation

@chirayuharyan

@chirayuharyan chirayuharyan commented May 7, 2026

Copy link
Copy Markdown
Collaborator
  • I've read the .github/CONTRIBUTING.md file
  • My code follows the typing guidelines
  • I've added appropriate tests
  • I've run pre-commit hooks locally

Description

TLDR

  • Added baseline_filtering to send only trajectories whose log-reward exceeds the current remote buffer baseline.
  • Added scoring_only mode to send only terminating states and log-rewards instead of full trajectory data when trajectory storage is unnecessary.

More Details

This pull request introduces two major new features to the replay buffer system: baseline filtering and scoring-only communication. These changes allow agents to avoid sending low-value trajectories and to reduce communication costs by sending only essential data when appropriate. The buffer manager now also supports operation without local storage, and tracks/reporting for these new modes has been added.

Replay Buffer Improvements:

  • Added baseline_filtering option to ReplayBuffer to filter out trajectories with log-reward below the current baseline (worst item in the remote buffer); includes logic for updating and applying the baseline, and tracking statistics on filtered/skipped sends. [1] [2] [3] [4] [5] [6] [7]
  • Added scoring_only option to ReplayBuffer, allowing agents to send only terminating states and log-rewards (not full trajectories) to the remote buffer manager, reducing communication and serialization overhead. [1] [2] [3] [4]

Replay Buffer Manager Enhancements:

  • Added store_locally option to ReplayBufferManager to allow operation without storing data locally; data is only scored and not retained if this is set to False. [1] [2] [3]
  • Buffer manager now injects a baseline_log_reward field into score responses (when full) to communicate the current baseline to agents using baseline filtering. [1] [2] [3]

Monitoring and Logging:

  • Enhanced timing logs in ReplayBuffer to report statistics on baseline filtering: proportion of items kept/filtered and number of sends skipped due to filtering.

- Introduced baseline_filtering to send only trajectories with log-reward above the baseline.
- Added scoring_only to send only terminating states and log-rewards.
- Updated methods to handle filtering and baseline updates accordingly.

Co-authored-by: Copilot <copilot@github.com>
@codecov

codecov Bot commented May 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.48598% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.39%. Comparing base (f8c97f8) to head (2b75e91).

Files with missing lines Patch % Lines
src/gfn/containers/replay_buffer.py 62.50% 24 Missing and 3 partials ⚠️
src/gfn/containers/replay_buffer_manager.py 68.57% 8 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #524      +/-   ##
==========================================
+ Coverage   73.92%   74.39%   +0.47%     
==========================================
  Files          59       59              
  Lines       10539    10636      +97     
  Branches     1523     1546      +23     
==========================================
+ Hits         7791     7913     +122     
+ Misses       2252     2219      -33     
- Partials      496      504       +8     
Files with missing lines Coverage Δ
src/gfn/containers/replay_buffer_manager.py 36.90% <68.57%> (+36.90%) ⬆️
src/gfn/containers/replay_buffer.py 60.56% <62.50%> (+6.93%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@younik younik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this!

My only concern is that this code is a bit overfitted to our usecase, but we are merging to broader torchgfn.
I would land these changes anyway, but we need at least a full test coverage for all the Container types.

Comment on lines +345 to +355
terminating_states = container.terminating_states
log_rewards = container.log_rewards
n = len(terminating_states)
return StatesContainer(
env=self.env,
states=terminating_states,
is_terminating=torch.ones(
n, dtype=torch.bool, device=terminating_states.device
),
log_rewards=log_rewards,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this doesn't work with full Trajectories. The script we are running are using terminal states as container, thus log_rewards refer to them, but in general, if container are trajectories, container.terminating_states and container.log_rewards have different shapes

Comment on lines +308 to +328
log_rewards = container.log_rewards
if log_rewards is None:
return container # Can't filter without rewards.

mask = log_rewards >= self._baseline_log_reward

n_total = len(container)
n_kept = int(mask.sum().item())
if self.timing:
self._baseline_total += n_total
self._baseline_kept += n_kept

if mask.all():
return container # All above baseline — send everything.
if not mask.any():
if self.timing:
self._baseline_skipped_sends += 1
return None # All below baseline — skip send.

indices = torch.where(mask)[0]
return container[indices]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want to have this for general-use, we should keep the container type consistent.

Think about what happen with the current code if container is a trajectory, it seems to me we select only the states with reward higher to baseline, but I believe we should return the full trajectories based on a baseline constraint (e.g. sum of rewards in the trajectory).
Maybe container[indices] even crashes for trajectories because bad indexing, not sure

Address review feedback on the baseline-filtering and scoring-only paths:

Worker (src/gfn/containers/replay_buffer.py)
- Fix mangled docstring: separate the lazy_sort / baseline_filtering /
  scoring_only entries and put the closing triple-quote on its own line.
- Reject Transitions in _filter_by_baseline and _prepare_for_remote:
  Transitions.log_rewards is per-row with -inf for non-terminating
  steps, so per-row masking would silently drop intermediate transitions
  and break DB/SubTB. Document Trajectories / StatesContainer as the
  supported payloads and assert at the entry points.
- Assert log_rewards shape and reject backward Trajectories in
  _prepare_for_remote (no log_rewards => cannot build the lean
  StatesContainer payload).
- Cross-validate at init: baseline_filtering=True now raises if
  remote_manager_rank is None.
- Track filter counters unconditionally (no longer gated on timing=True).
- Add baseline_refresh_after (default 10): after this many consecutive
  fully-filtered batches the worker bypasses the filter on the next send
  so it can receive a fresh baseline. Prevents the worker from
  permanently starving the manager once the baseline drifts above the
  worker's current support.
- Forward the three new flags through
  NormBasedDiversePrioritizedReplayBuffer.__init__.

Manager (src/gfn/containers/replay_buffer_manager.py)
- Add class docstring documenting store_locally and the baseline
  injection contract.
- New args: baseline_strategy ("min" | "percentile" | "ema", default
  "min"), baseline_percentile=0.1, baseline_ema_alpha=0.1. Strategy is
  validated at init.
- _inject_baseline_log_reward now takes the incoming container and
  maintains an EMA of incoming batch minima (with torch.isfinite
  filtering so Transitions' -inf entries do not poison the statistic).
  Dispatch:
    - store_locally=True and buffer at capacity => min or percentile of
      the buffer (per strategy).
    - otherwise => EMA fallback.
  This makes store_locally=False + baseline_filtering=True compose
  correctly; previously the combination was a silent no-op because the
  manager's empty buffer never tripped the len >= capacity gate.

Tests (testing/test_replay_buffer.py)
- 15 new tests covering: init-time validation, _filter_by_baseline
  paths (no-baseline-yet / all-above / all-below / partial /
  force-refresh-after-N-empties / rejects Transitions),
  _prepare_for_remote paths (Trajectories -> StatesContainer /
  StatesContainer no-op / rejects Transitions), and manager-side
  baseline injection under min / percentile / EMA-fallback /
  non-finite-skip strategies.

Pre-commit (black, flake8, pyright) clean; full pytest passes
(1927 passed, 735 skipped; no regressions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chirayuharyan

chirayuharyan commented May 25, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you Joseph for implementing these changes; the updates look great and really enhance the feature. Good to go on my end.
And thanks, Omar, for the thorough review and helpful feedback. You made a great catch regarding the potential errors with different Container Types versus Trajectories, and Joseph's changes resolve it nicely.

@younik younik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@josephdviviano
josephdviviano merged commit 654b828 into master May 29, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants