fix: stop the fully-async rollout dropping completed groups - #2238
Open
keepkeen wants to merge 2 commits into
Open
fix: stop the fully-async rollout dropping completed groups#2238keepkeen wants to merge 2 commits into
keepkeen wants to merge 2 commits into
Conversation
Two coupled defects in the fully-async worker's output queue: 1. `_generate_rollout_async` drained the entire queue on every poll but returned only `[:rollout_batch_size]`. Everything past the slice was discarded — fully generated, reward-scored groups whose prompts were already consumed from the data buffer. With the shipped example config the worker keeps `sglang_server_concurrency * num_engines` (1536) groups in flight against a target of 8, so nearly every completed group was thrown away. The module's own contract says the opposite: "each generate_rollout call drains until it has rollout_batch_size groups", "the queue stays warm", and `queue_left=` is logged as if leftovers persisted. 2. The task done-callback called blocking `Queue.put` on a bounded queue (maxsize=1000) from the event-loop thread. When the queue filled, the callback blocked the loop itself, freezing every in-flight generation. They have to be fixed together: keeping the surplus queued (1) makes a standing backlog normal, which turns the blocking put (2) from an occasional stall into a guaranteed freeze. - `get_completed_groups` takes a `limit`; the rollout pulls only what it still needs, and the surplus stays queued for the next call (FIFO). - The queue is unbounded so the callback can never block the loop; real backpressure moves to `_loop`, which stops pulling new prompts while a full pool of completed groups is already waiting. - The loop's sleep becomes `self.poll_interval` so the CPU test can drive iterations quickly. Adds tests/test_fully_async_rollout.py (CPU CI): surplus retention + FIFO, `limit` semantics, callback completing past the old 1000-item cap, and the queue plateauing near `concurrency` instead of absorbing the whole dataset.
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.
What
The fully-async rollout throws away almost every completed group, and its output queue can freeze the worker's event loop. The two defects are coupled, so this fixes both.
Bug 1: completed groups are dropped
_generate_rollout_asyncdrains the entire output queue on every poll, then returns only a slice:The dropped groups are fully generated and reward-scored, and their prompts were already consumed from
data_buffer(never requeued) — burned out of the epoch, GPU work wasted.This is not a rare edge: the worker is sized at
sglang_server_concurrency * num_enginesin-flight groups. With the shippedexamples/fully_async/run-qwen2.5-0.5B-fully_async.shthat is 512 × 3 = 1536 in flight against--rollout-batch-size 8. The module's own documentation states the opposite contract three times: "The worker is shared across all subsequentgenerate_rolloutcalls so its queue stays warm", "eachgenerate_rolloutcall drains until it hasrollout_batch_sizegroups and returns them", andqueue_left=is logged at the end as if leftovers persisted.Reproduction (real
_generate_rollout_async): 10 groups ready,target=4→ 4 returned, queue left0, groups 4–9 gone.Bug 2: blocking
puton the event-loop threadThe task done-callback runs on the worker's event-loop thread and calls blocking
Queue.puton a bounded queue (maxsize=1000). Once the queue fills — sized 1536-in-flight vs cap 1000, this happens on the first rollout of the example config — the callback blocks the loop itself, freezing every in-flightgenerate_and_rm_groupcoroutine until the training side happens to drain.Why one PR
Fixing bug 1 alone makes bug 2 strictly worse: keeping the surplus queued makes a standing backlog the normal state, turning the blocking put from an occasional stall into a guaranteed freeze.
Fix
get_completed_groups(limit=None): the rollout pulls onlytarget - len(collected)per poll; the surplus stays queued FIFO for the next rollout (the documented warm-queue behaviour). The now-dead[:target]slice is removed._loop's top-up gate: no new prompts are pulled while a full pool (concurrency) of completed groups is already waiting to be consumed. Memory stays bounded at ~2 × concurrency groups, same order as today's in-flight pool._loop's sleep becomesself.poll_interval(default 1.0, unchanged) so the CPU test can drive iterations quickly.Test
New CPU test
tests/test_fully_async_rollout.py, registered in thecpu-unittestjob (sglang_router / transformers stubbed the same way astests/test_agent/test_agent_rollout_cpu.py):_generate_rollout_asyncreturns exactlytargetgroups FIFO and leaves the surplus queued — fails onmainwithassert 0 == 6.get_completed_groupslimit semantics.mainwithqueue grew to 9 with concurrency=3.