Repository navigation
feat(training): train with verl through an AgentEnv agent loop - #422
Open
earakely-scale wants to merge 12 commits into
Open
earakely-scale wants to merge 12 commits into
earakely-scale wants to merge 12 commits into
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>
This branch has not been deployed
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).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:qwen3_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 run
GRPO on the GridSim v1 bundle through
AgentEnvAgentLoop, on the branch, with the TRL, NeMo RL and prime-rl runs' settings where verl has them:build_app, 64 episodes at once).verlai/verl:uv.cu130.dev5image (torch 2.13, vLLM 0.29), withagentenv-framework[verl]from this branch installed into its venv.What it took to run, none of it in the code:
uv.lockpoint flash-attn at a wheel URL that now 404s, souv sync --extra fsdpfails; verl's image, whose uv cache holds the wheel, works.rollout.layered_summon=Truefails (FSDP refuses CPU offload withNO_SHARD); it ran withFalse.multi_turn.max_tool_response_lengthmust cover the env's answers; verl's default of 256 characters would cut them.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; no actionable issues remain from this review.Summary
Adds a verl dataset writer and an agent loop that plays AgentEnv episodes through OpenEnv.
No new actionable issues were found. Tests were inspected, not run.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A[OpenEnv task server] --> B[Dataset writer] B --> C[Parquet rows and loop config] C --> D[verl agent loop] D --> E[Model generates replies and tool calls] E --> F[AgentEnv episode] F --> D D --> G[Episode reward returned to verl]Reviews (7) · Last reviewed commit: "fix(training): the verl writer counts pr..." · Reviewed by Greptile