Repository navigation
feat(training): train with verl through an AgentEnv agent loop - #422
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p on verl itself Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut special tokens; verl CI job Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efile Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…are trl's Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s; keep the extras test's subset rule Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se a prompt verl would cut, raise on an unscored episode, bound replays qwen3_coder and deepseek_v4 read tool.function.* on the schemas; built unvalidated they were dicts, and qwen3_coder swallowed the AttributeError and dropped every tool call. verl left-truncates a first prompt over rollout.prompt_length, cutting the system prompt and tools, and its length filter doesn't count a row's tools. An episode that ends unscored without failing is a configuration error, not an infra failure to replay. Replays stop once reset_timeout has passed since the rollout began. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ternals named and guarded; one read-and-probe for both writers The loop called Session's underscore methods from outside; _Session now owns start, tool, finish and close, as the TRL and NeMo Gym helpers' subclasses do. The class docstring names the verl internals the loop leans on, and run refuses a second rollout on one instance, since verl building a loop per trajectory is what keeps their state apart. tasks_and_tools in _openenv replaces the read, split-name check and probe both writers repeated. The verl tier fails envs through the fake env and a broken server rather than by patching Session, serves an unscored task through the serve fixture's new reward, and checks the config's loop path names the class. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… prompt check before writing, a warning on cut answers A rollout the env fails on every attempt now raises RolloutFailed: verl's V1 trainer (the default) records a session that raised as failed and leaves its trajectory out of the loss, its group's GRPO baseline and the eval reward, where the masked reward 0 still entered both. build_app(token=...) requires Authorization: Bearer on every request and WebSocket but /health, so the env server can run where other machines reach it. The session layer sends it; the verl writer and loop read it from the environment variable --token-var names, which agent_loop.yaml records, so the token is in no file. The writer with --tokenizer counts each row's prompt with its tools in the model's tokens and refuses rows over --max-prompt-length before writing; verl's own length filter doesn't count a row's tools. The loop warns once a tool when an env answer is over max_tool_response_length, which verl cuts silently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tions while it restarts A refused connection counted as nothing listening, so a server restarted mid-run failed every rollout in flight on all its attempts within seconds. The verl loop's sessions now wait it out within reset_timeout, as they wait out a server at its limit; the writer read the server first, so a refusal then is a server coming back. A server that refuses the token still fails at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te settings --chat-template-kwargs, verl's data.apply_chat_template_kwargs, so a prompt near the limit is counted as verl renders it (Qwen3 with thinking off renders differently). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…econds in reward_extra_info verl reports reward_extra_info per eval as val-aux metrics and in its dumps: each of the episode's scores (score/<step id>), the attempts it took, and its seconds in the model and in the env (deploying, calling tools and scoring). The end-of-episode scoring is timed as verl's compute_score. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
Train on a bundle's tasks with verl, through verl's own agent loop. verl 0.9 runs multi-turn tool rollouts in
verl.experimental.agent_loop: per row, an agent loop generates on verl's vLLM, parses the model's tool calls, adds their answers to the context and returns the tokens, the loss mask and, optionally, the rollout'sreward_score, in which case verl skips its reward function. A custom loop is registered throughactor_rollout_ref.rollout.agent.agent_loop_config_path, and each row picks it byagent_name.agent_env.training.verl, behind a newverlextra (openenv,pyarrow,transformers; verl, vLLM and torch stay the trainer's to pin).The writer
python -m agent_env.training.verl URL OUTwrites the tasks of a bundle's OpenEnv server (agent_env.openenv.build_app):OUT/<split>.parquet: a row per task, with an explicit schema. A row has:agent_name: agentenvanddata_source: agentenv/<env>;extra_infowith the split, index, task id and the row's tools as JSON.OUT/agent_loop.yaml: registers the loop with the server's URL, the deadlines,attemptsand the token's variable.A row's tools come from playing one task of each toolset, as the NeMo Gym writer does; both writers share
_openenv.tasks_and_tools.Its options:
--token-var NAMEnames the environment variable holding the token a server asks for. The writer reads it now and the loop in every verl worker, so the token is in no file.--tokenizer MODELcounts each row's prompt with its tools as the model's chat template renders it. Pass--chat-template-kwargswith verl'sdata.apply_chat_template_kwargsso the count matches verl's rendering.--max-prompt-length Nrefuses rows overNtokens before anything is written. verl would cut such a prompt from the left, losing the system prompt and the tools, and its own overlong filter doesn't count a row's tools.The loop
agent_env.training.verl.loop.AgentEnvAgentLoopis verl'sToolAgentLoopwith the row's tools in place of verl's configured ones, so verl's tokenization, tool parsers, turn caps and truncation are its own.Sessionlayer the TRL helper and the NeMo Gym server use. verl's parsers get typed tool schemas (qwen3_coderconverts arguments by them), and the chat template renders the row's full JSON Schema.reward_score.attemptstimes, but none starts oncereset_timeouthas passed since the rollout began.RolloutFailed. verl's V1 trainer (trainer.use_v1, the default on 0.9.1 and main) records a session that raised as failed and leaves its trajectory out of the loss, its group's GRPO baseline (padding gets its own uid) and the eval reward.trainer.v1.sampler.sync_refill_failed_groups=True, a group left with no trajectories is replaced by a fresh prompt instead of stopping the run.Shared.waits_for_server) withinreset_timeout. The writer read the server first, so a refusal then is a server coming back; a server that refuses the token fails at once.rollout.prompt_length(refused rather than cut);multi_turn.max_tool_response_length, which verl truncates silently (256 characters by default).reward_extra_infoholds each score by the step id that recorded it (score/<step>), its attempts, and its seconds generating (model_seconds) and in the env (env_seconds: waiting for a session, deploying, calling tools, ending turns, scoring). verl's V1 trainer averages them per eval asval-aux/<data_source>/<key>/mean@Nand writes them per rollout totrainer.validation_data_dir. It doesn't readreward_extra_infoin training steps, nor dump it torollout_data_dir, so GDPO'sgdpo_reward_keysreach it only in verl's deprecated trainer.The env server
build_app(token=...)requiresAuthorization: Bearer <token>on every request and WebSocket but/health: a request without it gets 401, and a WebSocket is closed before it opens. That lets the env server run where other machines, verl's workers on any node, reach it. Without a token, anyone who reaches the server deploys and drives its envs. The session layer sends the token (Shared.token,opened,read_tasks,probe).Test infrastructure
tst/unit/test_optional_extras_stay_optional.py:verl's distributions are alltrl's (pyarrow comes with datasets, and transformers), so the per-extra check has nothing to block for it._COVEREDrecords that and a test asserts it; the no-extras check covers the module.Tests
tst/unit/training/verl_test.py, the writer on the fake bundle: the rows of each split and the loop config; tools per toolset, each probed once; an empty split keeping a row's columns; a server that asks for a token, read with the one its variable holds, with the token in no file and a wrong one refused; and what's refused before the server is read.tst/unit/openenv/app_test.py: a server given a token refuses HTTP and WebSocket without it, but answers/health.tst/unit/training/sessions_test.py: a session waits out a restarting server only when asked, and never one that refuses the token.tst/verl/loop_test.py,make verl-test):AgentEnvAgentLoop.runon verl itself, with Qwen3's tokenizer and a stand-in rollout server, on the fake bundle. Env failures come from the fake env and a broken server, not from patchingSession. It covers:reward_extra_info(its keys, the attempt count after a replay, env seconds covering the scoring), through hermes andqwen3_coder;RolloutFailedwhen every attempt fails;enable_thinking, and its refusal over the limit;.github/workflows/verl.ymlruns the verl tier on verl 0.9.1 and main with CPU torch; it allows no skips and isn't a required check.GPU runs
GRPO on the GridSim v1 bundle through
AgentEnvAgentLoop.Setup:
verlai/verl:uv.cu130.dev5(torch 2.13, vLLM 0.29) on one L40S (g6e.4xlarge), FSDP actor and vLLM rollout sharing it, verl's V1 trainer.Round 2, on this PR's final code. The env server ran on its own VM (c7i.4xlarge):
build_app(..., token=...)on 0.0.0.0:8200, reachable only inside a temporary VPC. The trainer wrote the rows from it over the network:The verl tier passed there too (13 tests on verl main in the CUDA image).
Run C had no failed or replayed rollouts and no cut answers; it took about 75 s a step.
Failures injected in round 2:
attempts: 1, a graceful stop 8 s into a step's episodes:submit_schedule: ConnectionClosedError: received 1012 (service restart)) and raisedRolloutFailed.rollout_failure/evicted_samples: 4).reset_timeout(600 s) for it, then raisedRolloutFailed.ValueError: Received an empty list as keys).Round 3, on 39791fa. One g6e.4xlarge, the env servers on the same machine. Runs A (8 steps,
sync) and B (8 steps,colocate_async) on the GridSim rows and SFT model above; run D on a two-turn test bundle (four questions about a three-email inbox, a regexagent_prompt_response_verifiera turn,answer-1andanswer-2, served withreward=their mean), on Qwen3.5 withmulti_turn.format=qwen3_coder.reward_extra_infoin verl's metrics (run A's step-0 eval, 200 days):val-core/agentenv/gridsim-v1/reward/mean@1: 0.5345,val-aux/.../score/grade/mean@1: 0.5345,attempts 1.0,model_seconds 49.6,env_seconds 71.5. With 200 rollouts and 64 sessions, most of the env seconds are waiting for a session.Where a step's time goes (run A, median 75 s): generation 42.7, old log-probs 7, actor update 23, weight sync 2.5. A probe of 32 GridSim episodes at once, no model, put a rollout's env time at about 25 s: open and deploy 18–20 s median, a tool call 0.7 s, scoring 4 s. So the deploy is most of the env's share; the page's limits say so. verl V1's replay buffer polls every 2 s, which is why generation times come in 2 s steps.
colocate_asyncagainstsync:sync(A)colocate_async(B)The loop needs nothing for it: rollouts still running at a step's end pause their generation while the actor trains, their episodes open, and go on with the new weights (
trajectory_staleness_worst1,aborted_ratio0). It keepsnum_warmup_batches(1) more batch in flight, so the server holds twice the sessions: 64 here.Multi-turn and
qwen3_coder(run D): Qwen3.5-4B's step-0 eval scored 0.75,score/answer-11.0 andscore/answer-20.5, in 7 turns on average, so the parser's calls reached the episode andend_turnhanded over the second turn. Qwen3.5-2B trained 5 GRPO steps through the loop (eval 0.375 → 0.5, no failed rollouts); its per-turn scores show it never calledend_turn, so never reached turn 2, which the reward alone doesn't show.Found on the way, neither in the loop:
all predefined address pools have been fully subnetted). The loop replayed the failed deploys, then raisedRolloutFailedfor the 2 rollouts that failed all 3 attempts, and verl left them out. Wideningdefault-address-poolsindaemon.jsonfixed it; the docs page has it under limits. GridSim's env doesn't take a network per episode.verl settings this needs (the round-2 command line):
The token reaches every Ray worker through the environment the trainer starts in.
Known limits:
reset_timeout. It's upstream behaviour the loop can't avoid, since verl doesn't tell a loop whether it's in an eval.uv.locks point flash-attn at a wheelhouse URL that now 404s, souv sync --extra fsdpfails; verl's image works. The CPU tier runs on PyPI's verl 0.9.1.🤖 Generated with Claude Code


Confidence Score: 5/5
The changes appear safe to merge; no new blocking issue was found.Summary
Adds training through verl’s agent loop, with task datasets, episode rewards, retries, and optional server authentication.
Reviews (8) · Last reviewed commit: "feat(training): a verl rollout's scores,..." · Reviewed by Greptile