Skip to content

feat!: standalone env servers - #3162

Merged
mikasenghaas merged 19 commits into
mainfrom
feat/no-env-server-sidecar
Aug 4, 2026
Merged

feat!: standalone env servers#3162
mikasenghaas merged 19 commits into
mainfrom
feat/no-env-server-sidecar

Conversation

@mikasenghaas

@mikasenghaas mikasenghaas commented Jul 30, 2026

Copy link
Copy Markdown
Member

Summary

Removes the env-server "sidecaring" path entirely: the orchestrator never spawns env servers via subprocess anymore — it always connects.

  • Addresses are derived, not configured per source (matching how transport wiring works): OrchestratorConfig.env_addresses maps each (split, name) source to tcp://127.0.0.1:<base + i> in config order (train → eval). There is no address field on the source's serve block — the launcher and the orchestrator independently derive the same answer from the config alone. The single wiring knob is orchestrator.env_server_base_port (default 5000, like inference.server.port): concurrent runs on one host give each run its own base, keeping orchestrator↔env-server 1:1.
  • uv run env-server serves one environment, and is now the only CLI for hosting an env server: the merged companion feat!: remove the serve CLI, rename ServingConfig to ServeConfig verifiers#2237 removed verifiers' unused serve CLI (and renamed ServingConfigServeConfig). The entrypoint moves to entrypoints/env_server.py next to the other console scripts, and its config (EnvServerConfig) is [env] + [serve] (vf.ServeConfig: pool, address, max_concurrent) + [legacy], plus prime-rl's [log] — and validates the env identity at parse time (a missing env or a v0 legacy.id mixed with a v1 env id fails immediately, as it does on orchestrator sources).
  • Orchestrator only connects, in parallel: envs.py loses all the multiprocessing spawn/teardown machinery; each Env gets its address at construction, Env.start() builds an EnvClient and polls health (600s budget), and Envs.start() connects all sources with a single asyncio.gather — since every address is known up front, spawn (launcher) and connect (orchestrator) happen in parallel.
  • Local rl launcher spawns the servers: write_subconfigs writes one EnvServerConfig TOML per source (configs/envs/{train,eval}/<name>.toml — the source's EnvConfig fields plus its derived address) and rl_local starts one env-server process per source, logged to logs/envs/{train,eval}/<name>.log (same paths as before) and monitored like inference/trainer/orchestrator.
  • Multinode template: multi_node_rl.sbatch.j2 launches the env servers on the orchestrator node right before the orchestrator (both regular and disaggregated layouts).
  • The source-level serve block is now prime-rl's own two-field config (pool, max_concurrent), consumed by the env-server process via the written per-source TOML.
  • The multi-run integration test starts one env server per orchestrator (1:1), each pair on its own env_server_base_port; a resumed run keeps its port and reconnects to the same server. Docs and the monitor-run skill are updated to the new process topology.
  • New debug config configs/debug/multi-env/rl.toml: two reverse-text train sources + one eval source — the minimal multi-source run exercising one env server per source.

Breaking

  • uv run orchestrator standalone no longer spawns env servers. Env servers must be running at each source's derived address — start one per source with uv run env-server (an [env] block matching the source, with serve.address set to the source's derived address; it defaults to tcp://127.0.0.1:5000, the first source's address at the default base port).
  • serve.address is removed from sources. Env-server locations are no longer configurable per source: previously None meant "spawn a subprocess on a free port" and a set value meant "connect to this external server"; now every source's server lives at its deterministic local address (offset by env_server_base_port), so pointing a source at a remote env server is no longer expressible. Concurrent runs on one host must set distinct env_server_base_ports.
  • env-server config restructured: [serve] and [legacy] are top-level blocks and [env] is the verifiers env block directly (previously everything nested under [env] as a source-shaped config).
  • The source-level ServeConfig is prime-rl's own two-field config (pool, max_concurrent) instead of a vf.ServeConfig subclass with an optional address.

Verification

All runs on 2 GPUs from this branch (with main merged in), via uv run rl:

  • reverse-text single env (configs/ci/integration/reverse-text/start.toml, 5 steps): launcher spawns PRIME-RL::EnvServer bound at tcp://127.0.0.1:5000, orchestrator connects ("Train environment(s) ready" before inference is even up), reward 0.16 → 0.28, "Training finished!", clean teardown (no leftover processes, exit 0).
  • multi-env debug run (configs/debug/multi-env/rl.toml, 10 steps): three env servers spawned in parallel at derived 5000/5001/5002, per-source configs under configs/envs/{train,eval}/ (each a vf-shaped [env]/[serve] TOML carrying its derived serve.address), per-source logs under logs/envs/{train,eval}/, both train sources contribute rollouts, evals run through the eval env server (reward 0.13 → 0.72 across steps 1/5/10), train reward 0.09 → 0.66, "Training finished!" with exit 0 and no leftover processes or bound ports.
  • Raising taskset exits the run immediately: same config with one train source's taskset pointed at a nonexistent dataset split. All three env servers come up healthy; the orchestrator raises ValueError: Unknown split "bogus" at taskset load (~5s after it starts), the full traceback lands in orchestrator.log and the launcher output (which tails it), the launcher logs Orchestrator failed with exit code 1Terminating all processes..., and exits 1. The env-server logs show clean startup then a graceful EnvServer down / EnvServerPool down from the launcher's teardown — no orphaned processes or bound ports.

tests/unit passes (484 passed after merging main; the test_qwen3_vl_e2e failure is pre-existing on main).


Note

High Risk
This is a breaking change to RL launch topology and standalone orchestrator workflows; misconfigured ports or missing env-server processes will cause rollout hangs or startup failures.

Overview
Breaking: The orchestrator no longer spawns env servers as child processes. Standalone uv run orchestrator requires one uv run env-server per source already bound at each source’s derived address.

Wiring: Each train/eval source gets tcp://127.0.0.1:<env_server_base_port + i> in config order (train then eval). Per-source serve.address is removed; concurrent runs on one host use distinct orchestrator.env_server_base_port values.

Launcher & config: uv run rl writes per-source EnvServerConfig TOMLs under configs/envs/{train,eval}/<name>.toml and starts monitored env-server processes (logs at logs/envs/{train,eval}/<name>.log). EnvServerConfig is now top-level [env], [serve], and [legacy] with parse-time validation. Source serve on the orchestrator side is only pool and max_concurrent for the written env-server configs.

Orchestrator: envs.py drops multiprocessing spawn/teardown; Env takes a fixed address, start() polls health (600s), and Envs.start() connects all sources in parallel with asyncio.gather.

Ops & tests: Multinode multi_node_rl.sbatch.j2 starts env servers on the orchestrator node before the orchestrator. Multi-run CI starts one env server per orchestrator with matching base ports. Adds configs/debug/multi-env/rl.toml and documents the new process topology.

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

mikasenghaas and others added 3 commits July 30, 2026 00:10
… addresses

The orchestrator never spawns env servers anymore — it always connects to
each source's serve.address and polls until the server is up. Addresses
are assigned deterministically at config validation (tcp://127.0.0.1:5000+
across train then eval sources), so the launcher (local rl + multinode
sbatch template) spawns the servers in parallel with the orchestrator and
both sides agree on where each server lives from the config alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… config

Addresses are wiring, not intent — like the transport blocks, they are no
longer configurable on sources. The orchestrator derives each server's
address from the source's position in the config; the env-server entrypoint
takes a required address the launcher writes per source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/prime-rl-configs/src/prime_rl/configs/orchestrator.py Outdated
Comment thread packages/prime-rl-configs/src/prime_rl/configs/env_server.py
Comment thread packages/prime-rl-configs/src/prime_rl/configs/orchestrator.py Outdated
Comment thread skills/training/monitor-run/SKILL.md Outdated
Comment thread src/prime_rl/entrypoints/rl.py Outdated
Comment thread src/prime_rl/entrypoints/rl.py Outdated
Comment thread tests/integration/test_reverse_text_multi_run.py Outdated
…e port

EnvServerConfig composes vf's env/serve/legacy blocks verbatim, so an
env-server TOML reads like a vf serve config. orchestrator.env_server_base_port
offsets the derived port range, giving concurrent runs on one host (e.g.
multi-run orchestrators) each their own 1:1 env server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikasenghaas mikasenghaas reopened this Aug 3, 2026
mikasenghaas and others added 6 commits August 3, 2026 21:13
… eval)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, env_server_addresses -> env_addresses

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EnvServerConfig -> configs/env.py:EnvConfig, source-level EnvConfig -> SourceConfig,
entrypoint moves to entrypoints/env.py, env_server_base_port -> env_base_port,
proctitle PRIME-RL::Env

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mikasenghaas and others added 7 commits August 3, 2026 23:00
Bumps deps/verifiers to PrimeIntellect-ai/verifiers#2237 (serve CLI removed,
ServingConfig renamed ServeConfig); prl's source-level ServingConfig follows suit

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…decar

# Conflicts:
#	packages/prime-rl-configs/src/prime_rl/configs/orchestrator.py
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikasenghaas mikasenghaas changed the title feat!: remove env-server sidecaring, launcher spawns env servers at deterministic addresses feat!: standalone env servers Aug 4, 2026
@mikasenghaas
mikasenghaas marked this pull request as ready for review August 4, 2026 16:41
@mikasenghaas
mikasenghaas requested review from JannikSt and samsja August 4, 2026 16:42

@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 and found 2 potential issues.

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 ac2cd8d. Configure here.

Comment thread pyproject.toml Outdated
Comment thread packages/prime-rl-configs/src/prime_rl/configs/env_server.py
mikasenghaas and others added 2 commits August 4, 2026 17:15
The env console script shadows coreutils env in activated shells — back to
env-server as the sole entrypoint (still at entrypoints/, orchestrator source
class stays EnvConfig as on main). EnvServerConfig regains the identity
validation the nested source config used to provide (missing env, mixed v0/v1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The multi-node srun body is one single-quoted bash -c string, so the
apostrophe in "source's" closed it early: every rendered sbatch failed to
parse with a syntax error near unexpected token '(' and the job died before
any component started. Reword the comment and note the constraint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mikasenghaas
mikasenghaas merged commit 327f378 into main Aug 4, 2026
18 checks passed
eligotts added a commit that referenced this pull request Aug 5, 2026
Main's standalone env servers (#3162) moved env workers out of the
orchestrator process, so the orchestrator-side apply_run_asset_env no
longer reaches them. The run image-asset env now rides the launcher's
env-server spawn (the process that actually renders and offloads
images); the orchestrator-side apply and spawn injection are deleted as
dead plumbing. Standalone env servers set VF_RENDERER_IMAGE_OFFLOAD_DIR
themselves. Also imports EncodedTensor in trainer/batch.py, surfaced by
main's F821 scope fix (#3194).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants