Commit e0f4dc2
authored
feat: add partial_rollout recipe (#96)
## Summary
Adds `partial_rollout/` to the recipe submodule: APRIL-style
([paper](https://arxiv.org/pdf/2509.18521)) synchronous RL with
cross-step rollout interruption + resume to reclaim long-tail GPU
bubbles. Aborted gens carry their conversation state across the step
boundary and resume on the next step while their KV cache may still be
live on the rollout server.
Based on upstream `verl-project/verl@8ebccd44` (full pin in
[`recipe/partial_rollout/REQUIRED_VERL.txt`](https://github.com/startju/verl-recipe/blob/partial_rollout/partial_rollout/REQUIRED_VERL.txt)).
## Relationship to #58
Open PR #58 (`mamazi0131:main`, 2026-03-01) lands the same recipe
directory and was the starting point for this work. This PR is
materially different on four architectural axes:
1. **`LLMServerManager` / `AgentLoopManager` split** (verl#6117).
Current upstream separates the rollout server manager from the
agent-loop manager. This recipe ships `llm_server.py`
(`PartialRolloutLLMServerManager`) so cancel/resume fan-out lives on the
new server-manager surface; a small symbol-swap in
`ray_trainer.init_workers` injects it because upstream
`RayPPOTrainer.init_workers` hardcodes `LLMServerManager` with no FQN
config knob (unlike the parallel `agent_loop_manager_class` knob it does
have). #58's tree doesn't import on current `main`.
2. **Cancel/retry path absorbed inside `FullyLLMServerClient`**
(upstream verl#5631). Current upstream's
[`FullyLLMServerClient.generate()`](https://github.com/verl-project/verl/blob/main/verl/workers/rollout/llm_server.py#L160)
already has an abort-then-retry loop — when a generate is aborted
mid-flight by `cancel()`, it parks and resumes against the next weight
version's accumulated context without ever returning to AgentLoop. This
recipe **gates** that retry branch with
`+async_training.partial_rollout=True` and **forces**
`get_client(fully_async=True)` for every caller. Net effect: the
recipe's `AgentLoop` only handles pull/push and trajectory-grained pull
pacing (see axis 3); validation goes through upstream's untouched
`AgentLoopWorker.generate_sequences`. #58 instead returns an ABORT
sentinel to the agent loop, which then re-enqueues the prompt into
`pending_queue` — a structurally heavier path that requires a forked
`tool_agent_loop` for state snapshot/restore.
3. **Trajectory-grained pull pacing**.
`PartialRolloutAgentLoopWorker.generate_for_prompt` replaces upstream's
trailing `outputs = await asyncio.gather(*tasks)` with an
`asyncio.wait(FIRST_COMPLETED)` loop that decrements
`self.inflight_traj` and signals `self._slot_event` after every
per-trajectory completion. The `run_continuous` outer loop then pulls
the next prompt as soon as `inflight_traj + n <= max_inflight_prompts *
n` — long-tail trajectories inside one prompt don't block new prompts
from entering across the budget freed by other in-flight prompts'
completions. Pull RPC, `_run_one` tasks, and the slot-wait sentinel
share a single `asyncio.wait(running)` set; identity checks dispatch the
three task kinds. Validation keeps the simpler upstream gather path (we
add `generate_for_prompt` as a new method rather than overriding
`generate_sequences`). #58 reaches a similar trajectory-level effect via
`pending_queue` re-enqueue + `last_agent_loop_output` snapshot/restore —
heavier mechanism, requires forked agent loops.
4. **Engine-level cancel via Python `_resume_event` +
`abort_all_requests` drain** (vLLM 0.11 stopgap). vLLM <0.12 doesn't
expose `pause_generation`, so `PartialRolloutvLLMHttpServer` adds a
Python-side `_resume_event` gate around `generate()` plus an
`inflight`-counted `abort_all_requests(reset_prefix_cache=False)` drain
loop in `cancel()`. Deletable once verl moves to vLLM ≥0.12. #58 uses
per-request `asyncio.Event` + `Lock` — every in-flight generate awaits
its own cancel handle; PartialRollout substitutes one engine-core batch
call.
In addition this PR adds:
- `gsm8k_tool_config.yaml` (recovered after upstream #6126 deletion of
`examples/sglang_multiturn/config/tool_config/gsm8k_tool_config.yaml`).
- `run_qwen3-0.6b_gsm8k_grpo_tool{,_baseline}.sh` plus non-tool 0.6B
variants for laptop-scale repro, under `recipe/partial_rollout/run/`.
- `README_zh.md`, `REFERENCE.md`, `REQUIRED_VERL.txt`.
Happy to fold these into #58 if @mamazi0131 prefers — opening separately
because #58 does not apply to current `verl-project/verl@main` and the
rebase is nontrivial.
## Test plan
Full 1-epoch chain on 2× RTX 3090, Qwen3-0.6B, gsm8k, GRPO + token-level
rollout-IS, `max_response_length=4096`, `max_model_len=4608`, batch=8,
TP=1.
### Single-turn, PR vs baseline (completed, 934 steps each)
```bash
bash recipe/partial_rollout/run/run_qwen3-0.6b_gsm8k_grpo.sh
bash recipe/partial_rollout/run/run_qwen3-0.6b_gsm8k_grpo_baseline.sh
```
- [x] 934 steps each, no OOM, no hang
- [x] PR `timing_s/gen` avg **18.5s** vs baseline **26.1s** — **−29% gen
time**
- [x] PR `perf/throughput` avg **1060** tok/s/GPU vs baseline **851** —
**+24%**
- [x] Learning curves overlap — no learning regression
- [x] `pre-commit run --all-files` (ruff, ruff-format) clean.
## Test Result
<img width="2780" height="1230" alt="image"
src="https://github.com/user-attachments/assets/e719d604-9272-41ac-8c4d-e183867c75bd"
/>
Single-turn 934-step run on 2× RTX 3090, Qwen3-0.6B, gsm8k, GRPO +
token-level rollout-IS, `max_response_length=4096`, batch=8, 1 epoch.
Six panels — **green = baseline, pink = partial_rollout**.
### Learning-quality panels (top-left, bottom-left, bottom-right)
These three panels exist to falsify "PR breaks the algorithm." If PR
shifted training dynamics, one of these would diverge.
- **`critic/rewards/mean`** — both runs climb from ~0 to ~0.8 by step
~200, then track together at 0.7–0.9 for the rest of training. **No
reward divergence**; PR's cross-step interrupt + resume does not
introduce bias into the policy gradient.
- **`response_length/mean`** — both rise from ~600 to ~1100–1200 over
the run, near-overlapping. PR is marginally higher (matches the 1142 vs
1068 averages reported in the comparison comment), within noise.
- **`actor/entropy`** — both decay from ~0.4 to ~0.15 along the same
trajectory. **Same exploration / collapse rate**.
→ PR is policy-correctness-neutral. The cancel-resume mechanism doesn't
perturb the optimization.
### Performance panels (top-middle, top-right, bottom-middle)
These show the actual speedup.
- **`timing_s/gen`** ⭐ — the panel that matters most. **baseline sits at
~25–35s, PR sits at ~15–20s, consistently and across the entire 934
steps**. The two curves almost never cross. Spikes at multiples of 50
are `test_freq=50` validation steps (validation goes through upstream's
no-PR path, so both runs pay the same validation cost — those spikes
overlap). Excluding warmup, PR averages 18.5s, baseline 26.1s —
**−29%**.
- **`perf/throughput`** (tok/s/GPU) — mirror of gen timing. **PR
~1100–1400, baseline ~800–1000**, sustained gap, **+30% throughput**.
Both curves are noisy step-to-step (batch=8 means high per-step variance
— a single long-tail prompt dominates), but the bands clearly separate.
- **`timing_s/step`** — total step wall time. **PR ~30–40s, baseline
~40–50s**. Same direction as gen but smaller relative gap (≈ −16%)
because non-gen phases (`update_actor` ~12s, `ref` + `old_log_prob` ~5s,
etc.) are unchanged by PR. PR's win is concentrated entirely inside the
gen phase; the rest is identical work.
### Why the chart is convincing
1. **Sustained, not warmup-bounded**: the gap shows up by step 5 and
stays for 900 more steps. Not an outlier of a particular batch.
2. **Two curves never cross on `timing_s/gen`**: any single step PR ≤
baseline (modulo the shared validation spikes). System-level effect, not
statistical noise.
3. **Learning curves overlap pixel-for-pixel**: the speedup is **not**
"PR took shortcuts and generated less / worse." Reward, length, entropy
match.
### Headline
**29% faster gen, 30% higher throughput, no learning-curve regression.**
At this scale (`max_response=4096`, batch=8, long-tail driven) PR is in
its design sweet spot.
## AI-assistance disclosure
This PR was drafted with AI assistance (Claude Opus 4.7, 1M context
window). The commit carries a `Co-authored-by: Claude` trailer. The
submitting human (@startju) reviewed every changed line, ran the test
above, and is the accountable owner of this change end-to-end.1 parent a809c5c commit e0f4dc2
24 files changed
Lines changed: 2370 additions & 0 deletions
File tree
- partial_rollout
- agent_loop
- run
- tools
- vllm_rollout
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
| 83 | + | |
83 | 84 | | |
84 | 85 | | |
85 | 86 | | |
| |||
97 | 98 | | |
98 | 99 | | |
99 | 100 | | |
| 101 | + | |
100 | 102 | | |
101 | 103 | | |
102 | 104 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
0 commit comments