Skip to content

Filter zero-advantage groups in AsyncGRPO - #7609

Open
AmineDiro wants to merge 11 commits into
mainfrom
async-grpo-filter-zero-advantage
Open

AmineDiro wants to merge 11 commits into
mainfrom
async-grpo-filter-zero-advantage

Conversation

@AmineDiro

@AmineDiro AmineDiro commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

The rollout worker now drops a group whose rollouts all got the same reward, or none of which could be scored, before it reaches the trainer. Every advantage in such a group is zero, so it costs a forward and backward and moves nothing. This is DAPO's dynamic sampling without resampling to a fixed group count.

The loop does not replace a dropped group, so batch/groups_per_step goes down while the batch keeps samples_per_step samples. Filtered groups never reach the collator, so an epoch takes more prompts than len(train_dataset).

Metrics: rollout/groups_filtered_zero_advantage counts dropped groups and reward_filtered is their mean reward. reward covers trained groups only, so the two are meant to be read together.


Note

Medium Risk
Changes rollout queue protocol, epoch/checkpoint counting, and training batch composition; mis-handling dropped group IDs could skip prompts or skew metrics, but behavior is covered by new tests.

Overview
Async GRPO skips prompt groups with no learning signal (DAPO-style dynamic sampling): after scoring, the rollout worker drops groups where every scorable rollout got the same reward (zero advantage everywhere) instead of enqueueing RolloutSamples. Dropped groups are signaled on rollout_buffer as a bare group_id int; the trainer records them in _dropped_groups so they still count as consumed for num_train_epochs, checkpoint rollout_state.json resume cursor, and epoch-stop logic—without running forward/backward on useless batches. Training steps still fill samples_per_step from kept groups, so heavy filtering means more generation per step.

Metrics: per-sample trained reward is renamed to reward_kept; aggregate reward still reflects all scored rollouts (including dropped groups); frac_reward_zero_std tracks how often groups are filtered. num_generations < 2 is now rejected at config validation.

Reviewed by Cursor Bugbot for commit c882082. Bugbot is set up for automated code reviews on this repo. Configure here.

@bot-ci-comment

bot-ci-comment Bot commented Oct 9, 2026

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread trl/experimental/async_grpo/async_rollout_worker.py
Comment thread trl/experimental/async_grpo/async_rollout_worker.py Outdated
@cursor

cursor Bot commented Oct 10, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@qgallouedec
qgallouedec requested a review from lewtun October 12, 2026 05:12
@qgallouedec

Copy link
Copy Markdown
Member

thanks, as discussed yesterday, please link/close all the issues that asked for "dynamic sampling"

@AmineDiro

Copy link
Copy Markdown
Member Author

Closes #3708. Also requested in #3973 and #4764 (closed as duplicates), with an earlier GRPOTrainer attempt in #3758.

Also Related: #6367.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread trl/experimental/async_grpo/async_grpo_trainer.py Outdated
…o-advantage

# Conflicts:
#	tests/experimental/test_async_grpo_trainer.py
…o-advantage

# Conflicts:
#	docs/source/async_grpo_trainer.md

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit aa84c8a. Configure here.

Comment thread trl/experimental/async_grpo/async_rollout_worker.py

@qgallouedec qgallouedec left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, lgtm overall. A few things before merging:

  1. reward_std == 0.0 (L1224) misses some constant groups: with a constant reward of 0.1 and 3, 6, 7, 12 or 64 generations, numpy gives a std around 1e-17 🥲. Checking that all scored rewards are equal is exact.
  2. As planned in #7841 and #7835, let's log GRPO's frac_reward_zero_std (a fraction of groups) instead of rollout/zero_advantage_groups_total.
  3. reward now covers every scored group, but reward_std and rewards/<func> still come from the kept samples (L1259-L1260), so reward_std reads higher than in GRPO. Can we compute them in the worker before the filter, like reward, and drop reward_kept?
  4. The description still has the old metric names and says reward covers trained groups only. "Closes #3708" also needs to be in the description

@lewtun lewtun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for adding this @AmineDiro ! I left a few comments on top of Quentin's review.

Turbo nit on naming: we use "filtered" in the code (_filtered_groups, TestFilterZeroAdvantageGroups), "dropped" in the docs and "zero advantage" in the metric. Should we stick to just one wording consistently (e.g. dropped)?

Comment thread docs/source/async_grpo_trainer.md Outdated
| `rollout/vllm_preemptions_total` | requests the server preempted during the logging window (vLLM's `num_preemptions_total`, summed over engines). Reported only: a weight sync resets the server's caches, which preempts every running request, so it is not a pressure signal |
| `rollout/zero_advantage_groups_total`, `reward_kept` | groups dropped because every rollout got the same reward, and the mean reward of the groups that were trained on. `reward` is the policy's reward over every scored group, dropped ones included, so it is the one to track |

A group whose rollouts all received the same reward (or none of which could be scored) has zero advantage everywhere, so the worker drops it instead of enqueuing it: it would cost a forward and backward and move nothing. The loop does not resample to replace a dropped group, so `batch/groups_per_step` goes down while the batch keeps its `samples_per_step` samples. A dropped group still counts toward an epoch, which stays one pass over the prompts.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are you sure batch/groups_per_step goes down? IIUC each step still fills up to samples_per_step from the kept groups, and the metric counts trained groups

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You're right! I mainly asked claude to right this feature it seemed to be easy enough to one shot 🫠 🫠 🫠 Thanks for reviewiing I should looked at this closer.

Fixed

self._rates["reward"][1] += int(scored_mask.sum())
if not scored_mask.any() or reward_std == 0.0:
self._counters["rollout/zero_advantage_groups_total"] += 1
self._filtered_groups.put_nowait(group.group_id)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IIUC a dropped group is counted as soon as it's scored, but a kept group only once it's trained?

An alternative would be to put the dropped group_id on rollout_buffer instead and have RolloutQueueDataset.__iter__ record it and continue. Then drops are counted in order and we don't need the filtered_groups queue at all.

FYI the new filtered_groups arg also breaks the _HarnessRolloutLoop(...) calls in test_openenv_tito.py. Not sure if the OpenEnv integration is considered critical since it's not tested in the CI :)

self._rates["reward"][0] += float(np.nansum(rewards))
self._rates["reward"][1] += int(scored_mask.sum())
if not scored_mask.any() or reward_std == 0.0:
self._counters["rollout/zero_advantage_groups_total"] += 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

FYI if we switch to frac_reward_zero_std as @qgallouedec suggested, GRPO doesn't count groups with zero or one scored completions as zero std, since nanstd returns NaN for them. We drop these groups here, so the metric would undercount the drops.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ok, I see. I kept the GRPO naming but defined it as the fraction of groups the worker actually drops which is slighttly different indeed:

The drop here checks if all scored rewards are equal, so a group with 1 scored completion, where nanstd would give NaN, is counted as well (something like [None, 1, None, None]). I think dropping it makes sense, since it also has zero advantage for all samples.

updated the docs 👍🏼

Comment thread tests/experimental/test_async_grpo_trainer.py

This branch has not been deployed

No deployments
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