RL environments for LLMs: how they're built, scored and scaled

RL environments for LLMs: how they're built, scored and scaled
6frameworks, with the same environment built in all of them
11components that make up an environment
4ways to wire the reward
128 → 16,384concurrent sessions on two nodes, after tuning the load balancer
RL environments for LLMs: the ultimate guide to building, scoring and scaling them
I also explain it in this video, on my Articulator EN channel.

Why environments matterlink

Reinforcement learning (RL) has become the main driver of capability gains for reasoning models and agents: it's where supervised fine-tuning hits a ceiling and RL keeps lifting performance past it. And its core piece is the environment: the place where a model practices, gets graded, and learns from interaction over long horizons.

To hit capability targets, environment counts have exploded:

  • Qwen3 trained on roughly 20 general-domain tasks.
  • Qwen3-Coder pushed that to 20,000 parallel environments on Alibaba Cloud.
  • MiniMax's Forge trains M2.5 across hundreds of thousands of real-world environments.
  • Qwen3.5 reports million-agent environments with progressively more complex task distributions.
The scale of environments: from about 20 tasks in Qwen3 to 20,000 parallel environments in Qwen3-Coder, hundreds of thousands in Forge and millions of agents in Qwen3.5.
From twenty tasks to millions of agents.

The Qwen team is blunt about it in the Qwen3.5 release notes: most of the gain over Qwen3 comes from “extensive scaling of virtually all RL tasks and environments we could conceive”, deliberately raising their difficulty and generality instead of optimizing for narrow benchmarks.

So the bottleneck is no longer “can I set up an environment?” but “how do I run 100,000 of them, keep them honest and feed them into a training loop?”. Frameworks are emerging to standardize that, alongside catalogs of ready-made environments: the largest sits on Hugging Face Spaces, with over 4,000 MCP-compatible environments, and Prime Intellect's Environments Hub and openreward.ai add several thousand more.

The authors' conclusion, up front: each framework does the same thing its own way. The differences are about how the environment wires into the rest of training, not what it can ultimately do. The same environment can be replicated in all six, so you won't miss anything fundamental by picking one over another: what changes is convenience, and that depends on what's already in your stack. All the code lives in their RL_Envs_101 repo.

What an RL environment is in the LLM eralink

In classical RL (Atari, robotics), an environment is small and self-contained. The textbook example is CartPole: a cart on a track with a pole balanced on top. The agent observes four numbers (cart position and velocity, pole angle and angular velocity), picks one of two actions (push left or right), and the environment ticks one physics step forward. The reward is +1 for every step the pole stays upright. The environment is the physics simulator plus the reward rule.

CartPole: a cart on a track with a balanced pole, four state numbers, two actions and one point per step.
CartPole: four numbers, two actions, one point per step.

With language models the picture gets more complex, and there's no canonical implementation. One common shape: the agent is the language model; the environment is a sandbox that runs shell commands or executes code; and the action space is whatever tools the framework exposes. Each rollout is a multi-turn conversation: the model writes, runs a tool, reads the output, decides what to try next, and eventually submits an answer. The environment scores the completed rollout and returns a reward, and each training step pulls in a group of rollouts per task to learn from.

The loop with a language model: the model calls a tool in the sandbox, reads the observation and gets a reward at the end.
Same skeleton as CartPole, different scale.

Many other shapes exist (single-turn reasoning, web tasks, code-repo agents, multi-agent setups), but they're all variations on the same skeleton. What changes is the tools, the observations and the reward rule.

The five stages, and who covers whatlink

Every RL training system for LLMs sits on the same five-stage spine: tasks to solve, a harness that lets the model interact, a reward that scores the behavior, a rollout collector that gathers complete episodes, and a trainer that turns those episodes into a policy update. What changes from framework to framework is which slice of that spine comes in the box:

FrameworkTasksHarnessRewardRolloutTrainer
OpenEnv–✓✓––
ORS✓✓✓––
NeMo Gym✓✓✓✓–
Verifiers + Prime RL✓✓✓✓✓
SkyRL Gym–✓✓✓✓
GEM✓✓✓––
The five stages in a row (tasks, harness, reward, rollout and trainer) with colored brackets marking the slice each framework covers.
Each framework covers a different slice of the spine.

OpenEnv gives you the harness (the tools the model can call) and the reward, but you write the trainer yourself. Verifiers, on its own, describes the task, the tools, the harness and the reward; the actual training is handled by Prime RL, which it pairs with. That's why a flat “X vs Y” comparison is hard: each was built to slot into a different ecosystem, and they don't live at the same level of the stack. For a complementary cut, sync versus async training, the authors point to Hugging Face's async RL training landscape.

The eleven components of an environmentlink

After enough time inside all six frameworks, the same pieces kept showing up: the ones you actually have to think about when you build or pick an environment for an agent. Each answers a question:

ComponentThe question it answersExamples
Tasks / datasetWhat problems should the model solve?Coding tasks, math problems, game instances
Initial stateHow is per-episode state set up at the start?reset(), setup() lifecycle, seed_session endpoint, setup_state hook
Prompt templateHow is the task presented to the model?System prompt + user instruction + tool descriptions
Tool definitionWhat can the model do in the world?execute_code(), search(), submit_answer()
Observation formatWhat does the model see after an action?Raw text, a structured ToolOutput with blocks, the Gymnasium 5-tuple
Execution backendWhere do actions actually run?Cloud sandbox, Docker container, Python eval(), a game simulator, or none (pure Python)
State managementHow is state tracked across turns?Session cookies, sandbox persistence, conversation history
Reward / rubricHow do we score the behavior?String matching, LLM-as-judge, unit tests, a verify() endpoint
Done / terminationHow does the episode end?finished per tool call, terminated/truncated, max turns, verify() decides
Episode controlWho drives the loop and decides when to stop?The trainer (TRL), the environment (Atropos) or the framework (Verifiers)
Transport / protocolHow does the model talk to the environment?HTTP REST, WebSockets, JSON-RPC (MCP), SSE, in-process Python calls
The environment box opened up, with its eleven components: tasks, initial state, prompt, tools, observation, execution, state, reward, done, control and transport.
Eleven components, across all six frameworks.

Why frameworks exist, and the six they pickedlink

Why have environment frameworks at all? Mostly for standardization. If there's an agreed protocol for how a trainer talks to an environment, like MCP for tools, any training loop can plug into any environment, researchers across different domains follow the same shape, and someone else's environments become reusable for your run instead of one-off scripts.

There's no standard protocol yet, so each framework answers four questions its own way:

  1. What is an environment? For some, just a reward function; for others, also tools, state and the full multi-turn loop; others bundle the whole training pipeline.
  2. Where does it run? As an HTTP server (Docker, HF Spaces), scaling independently from training, or in-process inside the training venv, with no network hop but no isolation either.
  3. How much trainer comes with it? Some ship their own (Prime RL, NeMo RL, SkyRL); others need adapters to plug into external loops like TRL.
  4. When does the reward fire? Per tool call, per step with a rubric, in a post-episode verify, or in an external scoring function.

Out of 16 they surveyed, they picked six to build the same environment in and compare head to head:

FrameworkCreatorTypePackagePython
OpenEnvMeta PyTorchHTTP server (MCP)openenv-core≥3.11
ORSGeneral ReasoningHTTP server (REST + SSE)ors-sdk≥3.10
NeMo GymNVIDIAHTTP server (REST)nemo_gym (from git)≥3.12
VerifiersPrimeIntellectIn-process (Python)verifiers≥3.11
SkyRL GymNovaSky-AI · BerkeleyIn-process (Gym)skyrl-gym≥3.10
GEMAxon-RLIn-process (Gymnasium)gem-llm≥3.10
The six frameworks: OpenEnv, ORS and NeMo Gym as HTTP servers; Verifiers, SkyRL Gym and GEM inside the training process.
Three servers, and three that live inside training.

The 16 split into three tiers:

  • Tier 1, pure task libraries: problems and verifiers, with no transport, tools or state (RLVE, Reasoning Gym).
  • Tier 2, environment frameworks: they define how to build an environment and leave the trainer to you (OpenEnv, ORS, NeMo Gym, RL-Factory).
  • Tier 3, environment and training bundled: environment definition, rollouts and training in one package (Verifiers, SkyRL Gym, GEM, Atropos, Harbor, RAGEN, rLLM, Open-Instruct, TextArena, LlamaGym).

They focused on tiers 2 and 3, the ones that support multi-turn tool-calling environments. The ones left out, and why:

FrameworkCreatorWhy it was excluded
AtroposNous ResearchDifferent paradigm: environments own inference and POST scored batches to a central API. Not compatible with TRL's turn-by-turn tool calling.
HarborLaude InstituteEval and rollout-generation framework, the official harness for Terminal-Bench 2.0: it runs full agent harnesses (Claude Code, Codex CLI, OpenHands) in containers.
RLVEZhiyuan ZengA pure verifier library (445 tasks), generate() → verify(), with no transport, tools or state.
Reasoning GymOpen ThoughtProcedural task generators and verifiers, stateless and single-turn.
RAGENZihan WangFull stack (env + StarPO + veRL), tightly coupled to its own training loop.
rLLMAgenticaA decorator pattern that wraps existing agent code; there's no environment class.
RL-FactorySimple-EfficientAny MCP server becomes an environment through config; interesting, but very early.
Open-InstructAllen AIA training framework with environment hooks; environments are reward functions, not multi-turn agents.
TextArenaLeon GuertlerGame-specific multi-agent environments, a narrow domain.
LlamaGymKhoomeiKA Gymnasium wrapper for LLMs, an early prototype no longer maintained.

What each one ships out of the boxlink

This is the key table. Each framework ships a different subset of components: ✅ a first-class API for that component; ⚙️ it works, but you lean on a convention or a trainer hook; 🔧 bring your own.

ComponentOpenEnvORSNeMo GymVerifiersSkyRL GymGEM
Tasks🔧✅ list_tasks(split)✅ JSONL + ng_prepare_data✅ HF Dataset🔧 (JSONL)✅ 24+ envs
Initial state✅ reset()✅ setup()✅ /seed_session✅ setup_state✅ init(prompt)✅ reset(seed)
Prompt template🔧✅ get_prompt()✅ in the JSONL✅ system_prompt🔧✅ per-env instructions
Tools✅ @mcp.tool✅ @tool✅ POST endpoints✅ Python functions✅ ToolGroup✅ ToolEnvWrapper
Observation✅ str✅ ToolOutput✅ Response✅ str✅ BaseTextEnvStepOutput✅ 5-tuple
Execution backend🔧🔧🔧🔧🔧🔧
State management✅ MCP sessions✅ X-Session-ID✅ cookies⚙️ in-process state✅ self.turns counter✅ reset()/step()
Reward✅ Rubric✅ ToolOutput.reward✅ /verify✅ Rubric + JudgeRubric✅ returned by step()✅ returned by step()
Done✅ Observation.done✅ finished⚙️ verify() decides✅ @vf.stop / max turns✅ step().done✅ terminated/truncated
Episode control⚙️ trainer⚙️ trainer✅ Agent Server✅ env.evaluate()⚙️ trainer⚙️ trainer
Transport✅ HTTP + JSON-RPC✅ REST + SSE✅ REST + cookies⚙️ in-process only⚙️ in-process only⚙️ in-process only
Deployment✅ Docker / HF Spaces✅ Docker / Spaces / OpenReward✅ Docker / Ray⚙️ training venv⚙️ training venv⚙️ training venv
Native training⚙️ via TRL⚙️ multi-trainer✅ Megatron / NeMo RL✅ Prime RL✅ SkyRL⚙️ multi-trainer
The table of what each framework ships, with the execution backend row empty in all six.
One row none of them fills: where actions actually run.
  • Most bundled: Verifiers ships dataset, tools, rubric, rollout harness and training. You define the problem and it handles the rest, with the trade-off that you live inside its API surface.
  • Least bundled: OpenEnv ships a protocol (MCP), session management and a rubric system. Tasks and execution are yours: more setup, more control.
  • In between: ORS and NeMo Gym ship a deployment protocol and a reward mechanism, but you bring execution and tasks. GEM ships built-in environments and the Gymnasium API, and you bring the trainer.

And one thing jumps out of the table right away: none of them ships the execution backend. The sandbox is always on you.

Same concept, six nameslink

Reading two of these frameworks' docs back to back feels like reading two different APIs for the same concept, because that's basically what it is. A small Rosetta stone:

ConceptOpenEnvORSNeMo GymVerifiersSkyRL GymGEM
Reward functionRubricreward field on ToolOutputverify() endpointRubricwhatever step() returnswhatever step() returns
End of episodedonefinished on every callverify() decides@vf.stopdoneterminated / truncated
One taskwhatever you bringa task in a splita JSONL linea Dataset rowwhatever you bringan instance of a built-in env
The same idea, the reward, with a different name in each framework: Rubric, ToolOutput.reward, verify() and step().
The reward goes by a different name in almost every framework.

Two things stand out with everything in one view: every framework has something for the initial state and for tool definitions, because every multi-turn environment needs both, but tasks are only filled in by frameworks that bundle their own task source. And for episode state, the HTTP frameworks all use sessions while the in-process ones lean on plain Python.

Dimension 1: what code you writelink

The first thing you run into is the API surface: what you subclass, how you declare a tool and how much boilerplate sits around the actual logic. The same toy environment comes out six different ways:

OpenEnvORSNeMo GymVerifiersSkyRL GymGEM
Base classMCPEnvironmentors.EnvironmentSimpleResourcesServernone (plain functions)BaseTextEnvgem.Env
Tool syntax@mcp.tool@tool + Pydantic inputapp.post("/name")def func(arg: type)inside step()inside step()
ReturnsstrToolOutput(blocks, reward, finished)Response(output=str)strBaseTextEnvStepOutput(obs, reward, done, trunc, info)
Entry pointcreate_app(Env)Server([Env]).run()Env.run_webserver()vf.ToolEnv(tools=[...])skyrl_gym.register(...)gem.register(...)

To see the contrast, here's the environment in OpenEnv, with its MCP tools registered inside the class:

class MyEnv(MCPEnvironment):
    def __init__(self):
        mcp = FastMCP("my_env")

        @mcp.tool
        def do_action(input: str) -> str:
            """Execute an action. Returns observation."""
            return execute(input)

        super().__init__(mcp)

    def reset(self, **kwargs) -> Observation:
        self._state = fresh_state()
        return Observation(done=False, reward=None, metadata={"status": "ready"})

In Verifiers, plain Python functions and a rubric are enough:

def do_action(input: str) -> str:
    """Execute an action. Returns observation."""
    return execute(input)

async def correctness(completion, answer, **kwargs) -> float:
    return 1.0 if answer in completion[-1]["content"] else 0.0

env = vf.ToolEnv(
    tools=[do_action],
    rubric=vf.Rubric(funcs=[correctness]),
    dataset=my_dataset,
)

And in GEM everything goes through Gymnasium-style reset() and step(), with spawn() to fork the environment for parallel GRPO rollouts:

class MyGEM(gem.Env):
    def reset(self, seed=None):
        super().reset(seed)
        self._state = fresh_state()
        return "Your task: ...", {"suffix": "Begin."}

    def step(self, action):
        parsed = parse_action(action)
        result = execute(self._state, parsed)
        terminated = is_done()
        return result, score(result), terminated, False, {"suffix": "Continue."}

    def spawn(self, same_state=False, **kwargs):
        return MyGEM(...)
The toy environment written in GEM, with Gymnasium-style reset() and step(), and below it its base class, how it declares tools and what it returns.
Same environment, six APIs: here, GEM.

Dimension 2: HTTP server or in-processlink

The most fundamental split: does the environment run as a separate HTTP server or inside the training process? Almost everything else falls out of that single choice. It shows in three ways:

  • Where it runs. A server lives on its own machine, often a cheap CPU box, or in a Hugging Face Space. The in-process kind shares the training GPU node.
  • What you install on the trainer side. HTTP needs only an SDK or requests. In-process pulls the full framework package into the training venv.
  • How it scales. HTTP adds server replicas behind a load balancer. In-process adds identical training workers, each with its own copy of the environment.
OpenEnvORSNeMo GymVerifiersSkyRL GymGEM
TypeHTTP serverHTTP serverHTTP serverin-processin-processin-process
ProtocolJSON-RPC 2.0 (MCP), WebSocketREST + SSEREST + cookiesPython callsPython callsPython calls
Dockeryesyesyesnonono
Scales independentlyyesyesyesnonono
Deps on the traineropenenv-corerequests onlyrequests onlyfull packagefull packagefull package
AuthnoneX-API-Keycookies–––
Where does the environment run? Two branches: as a separate HTTP server or inside the training process; almost everything else follows from that choice.
A separate server or inside training: the choice that orders everything else.

An important nuance: in practice, most environments don't do the heavy work themselves. They delegate it to a sandbox provider like E2B, Modal or a custom container backend. That changes the scaling story for both shapes: the HTTP server becomes a thin router holding sessions and forwarding tool calls into the sandbox, so adding replicas is cheap; and the in-process class becomes a thin client over the sandbox, so the per-worker footprint stays small even with hundreds of workers. The deployment shape still matters (HTTP isolates dependencies, in-process shares them), but the heavy compute lives behind whichever sandbox you picked.

Dimension 3: tools and actionslink

All six end up exposing the same thing to the model: a list of callable tools with names, descriptions and typed parameters. What differs is where the tool definition lives and how the trainer discovers it. HTTP frameworks define tools on the server and ship a discovery endpoint the client hits at runtime; in-process frameworks define tools as Python functions registered when the environment is constructed. After discovery, both shapes are identical: send a tool call, get an observation back.

Tool discovery: over HTTP they're fetched from the server at runtime; in-process they're registered when the environment is built; then, call and observation.
After discovery, it's all the same.
  • Schema portability. HTTP frameworks ship JSON Schema (MCP, ORS ToolSpec, OpenAPI), which any OpenAI-compatible model knows how to call: the same tool spec hops between models cleanly. In-process frameworks derive schemas from Python type hints: faster to write, harder to lift into another framework or a model that expects a different schema dialect.
  • Authoring cost per tool. A @tool decorator on a function is one line; a FastAPI endpoint plus its Pydantic body model is closer to a small file. Multiply by your tool count.
  • The multi-turn loop is universal. Once the tools are wired up, the model-side experience is identical: name and typed args in, observation out.

Dimension 4: the rewardlink

Who decides how good the model's behavior was, and when? This is where frameworks differ most philosophically. There are four patterns:

PatternHow it worksUsed by
1. External rewardThe training script decides: the environment just returns text, and a function you write compares the result with the expected output.SkyRL Gym, GEM
2. Server-embeddedEvery tool response carries a reward; the server evaluates as it goes and the trainer just reads env.reward.ORS
3. Post-episode verificationA separate /verify endpoint is called after the episode and evaluates the full trajectory.NeMo Gym
4. Environment-embedded rubricA composable Rubric object (modeled after nn.Module) computes rewards from actions and observations, called automatically on each step().OpenEnv, Verifiers
The four reward patterns on the same trajectory: external, per tool call, post-episode verify and a rubric on every step.
Four moments to hand out the score.

The composable rubric supports composition (WeightedSum, Sequential, Gate), an LLM as judge (LLMJudge) and trajectory-level scoring.

The composable rubric: several functions added with weights, chained, a gate that cuts the score and a model acting as judge.
A rubric composes like a PyTorch module.

What goes inside a reward

The four patterns are about when the reward is computed and who owns the code. What goes inside is a separate question, with three flavors:

  • Procedural or verifiable. Deterministic checks: does the final answer match, did the unit test pass, does the JSON parse, does the math evaluate to the right number? This is the RLVR (reinforcement learning with verifiable rewards) pattern that DeepSeek-R1 scaled up: the model only earns a reward when its output passes a programmatic check, typically a 0/1 signal. Scalable, hard to game, well suited to math, code and logic. The catch: it needs a clear ground-truth oracle.
  • LLM-as-judge (sometimes called RLAIF). For what code can't check: creative writing, open-ended reasoning, summary quality, agent coherence. Another model reads the trajectory and scores it, often guided by a rubric (“rate factual accuracy 1–5, clarity 1–5, weight them”). That's what LLMJudge and JudgeRubric do. The catch is reward hacking: the policy learns to produce outputs the judge over-rewards, even though a stricter gold-standard judge would penalize them. Decomposed rubrics and “thinking judges” (J1-style) that reason before scoring push back on this.
  • Dense versus sparse. A separate axis. A sparse reward fires only at the end (pass/fail, final score); a dense one fires per step, per token or per sub-goal, giving the trainer a much richer credit-assignment signal. External and post-episode rewards are sparse by default; server-embedded and rubric rewards can be either, and the rubric is the most natural place to make them dense. Dense rewards train faster and more stably; sparse ones are simpler to define and harder to game.
Reward hacking: the model learns to give the judge what it likes instead of doing the task well.
With a lenient judge, the model learns to sweep dust under the rug.

In practice you almost always mix them: a procedural part for what you can verify (the test passed, the JSON is valid, the answer matches) and a judge for what you can't (was the explanation good, did the agent stay on task), composed with weights into a single scalar. The frameworks only differ on where you write that composition and when it fires.

Dimension 5: who drives the loop, and when it stopslink

A multi-turn rollout is a tight cycle: the model generates an action, the tool gets called, the observation goes back to the model, repeat. Two things must be decided: who runs that cycle and how it knows it's done.

In every framework they tested, the trainer drives: it generates, parses tool calls, executes them through the adapter and generates again. The environment is passive: it responds to tool calls but never initiates anything. So the trainer controls episode length, tool-call parsing and retries. Some frameworks they didn't test flip this: in Atropos, the environment owns its own vLLM instance and generates rollouts autonomously; in Verifiers' env.evaluate() mode, the framework manages the loop. It matters because it changes how you think about batching, parallelism and resource allocation.

The trainer generates, parses the call, runs it in the environment through the adapter and generates again; the environment only responds.
The trainer drives; the environment responds.

The other half is the stop signal:

FrameworkTermination signalGranularityWho drives
OpenEnvObservation.done · final_answer toolper step + trainer max lengthtrainer
ORSToolOutput.finishedper calltrainer
NeMo Gymverify() after the rolloutpost-episodetrainer
Verifiers@vf.stop · is_completed()per steptrainer or env
SkyRL Gymstep().doneper steptrainer
GEMterminated · truncatedterminated / truncatedtrainer

Two examples with a coding-agent environment using a Claude Code style tool set: file ops (read, write, edit, multi_edit, ls, glob, grep), shell (bash(command, timeout=30)), planning (todo_write(todos)) and a terminator, submit_solution().

  • Tool-based control (trainer-driven). The environment just exposes the tools, and the episode ends when the model calls submit_solution() or max_completion_length cuts it off. All six support this out of the box and it ports almost identically across them. For most tool environments, it's the path of least resistance.
  • Harness-controlled rollout (env-driven). Sometimes you don't want the trainer to drive at all: you want a real coding harness (OpenCode, OpenClaw, Aider, Claude Code style) running in the sandbox and talking to the model directly. The harness owns the conversation: it manages the file tree, compresses context, retries on tool failures and knows when the task is done; the environment just hosts it and hands back the trajectory. You get benchmark-grade fidelity, but collecting training data becomes its own problem: the harness must emit, on every turn, the action, observation, reward and logprobs your loss needs, and that's harness-specific work each time.
A full coding agent running inside the sandbox, managing files, compressing context and retrying, which has to log action, observation, reward and probabilities on every turn.
When the harness drives, collecting training data is a job of its own.

Dimension 6: tasks and datasetslink

Every rollout starts with a task: the prompt, the data the model works on and, for scoring, the expected answer or the test that decides success. After the reward, it's the most varied dimension:

  • Verifiers integrates with Hugging Face Datasets.
  • GEM has a registry of 24+ built-in environments.
  • ORS keeps tasks on the server (list_tasks(split)).
  • NeMo Gym preprocesses JSONL through a CLI (ng_prepare_data).
  • OpenEnv and SkyRL Gym leave it to you.

Frameworks with a dataset abstraction also give you splits and versioning for free; with the others, you wire that yourself.

A task is more than a row

A row is one input plus one expected output. A task, especially for agents and coding, is a bundle of files that has to land in the sandbox before the rollout starts. The cleanest example is SETA, on OpenReward: 1,376 terminal-agent tasks on the Open Reward Standard, each shipping a task.yaml (prompt and metadata), the data files, a tests/test_outputs.py run after submission, and a weights.json that turns test outcomes into a scalar reward. SWE-Bench rows go further: each carries repo, base_commit, problem_statement and test_patch; the environment clones the repo at that commit, applies the test patch and grades the agent's diff with FAIL_TO_PASS and PASS_TO_PASS.

A SETA task as a bundle: task.yaml, data, tests and weights.json, copied into the sandbox before the rollout.
The task travels to the sandbox with its data and tests.

Those bundles usually live behind the dataset row (in S3, an HF dataset repo or a tarball): the environment pulls them on episode start, drops them into the sandbox, scores and tears down.

Coupled or decoupled: who owns the task spec

  • Coupled. Verifiers expects a Dataset with a prompt column and optional answer or info; GEM ships its own loaders; ORS and NeMo Gym pin the schema on the server. The Environments Hub and OpenReward go further and standardize the whole package (layout, scoring contract, even wheel-based packaging), so any task that fits the spec runs in any environment that follows it.
  • BYO. OpenEnv and SkyRL Gym leave the dataset to you: prompts come from anywhere, but every new source costs a little integration.

Coupling means the environment dictates the spec and you transform your data. For example: turning SWE-Bench JSONL into a Verifiers Dataset with a prompt column and the patch in info; fanning a Kaggle Q&A dump out into OpenReward-style folders, one per row, with task.yaml, data/ and tests/; or, for HumanEval or GSM8K, nothing at all, because the framework already ships a loader. You write the mapping once and, in return, the same environment runs unchanged on every task that fits the spec. If you care about one task, the spec is overhead; if you train across many or pull in benchmark suites, it's what makes it work.

Dimension 7: ecosystem and maturitylink

OpenEnvORSNeMo GymVerifiersSkyRL GymGEM
CreatorMeta PyTorchGeneral ReasoningNVIDIAPrimeIntellectNovaSky-AI · BerkeleyAxon-RL · ICLR 2026
Built-in envsexample in repoexample in repo50+example in repofew (search, SQL)24+ (games, math, code)
Community hub4k+ on HF Spaces330+ on openreward.ai–~1k on the Environments Hub––
Production usageMetaOpenReward platformNVIDIA · NemotronPrimeIntellect · INTELLECT-3SkyRL-Agent · SWE-Benchresearch
Who's behind each framework, how many environments its catalog holds and where it's used in production.
From research to production.

Dimension 8: what a new environment costslink

How hard is it to go from “I have an idea for an environment” to “it's running in training”? It matters whether a human writes it or an agent does.

OpenEnvORSNeMo GymVerifiersSkyRL GymGEM
Files4-6 (env, app, models, Dockerfile, pyproject)3-4 (server, Dockerfile, pyproject)4-5 (server, YAML, Dockerfile)1-21-21-2
ConceptsMCPEnvironment, create_app, Observation, Rubric, FastMCPEnvironment, @tool, ToolOutput, Server, SplitSimpleResourcesServer, setup_webserver, verify, sessionsToolEnv, RubricBaseTextEnv, init/step/closegem.Env, reset/step, spawn
CLI scaffoldopenenv initorwd init–prime env init––
Time to first test~30 min (Docker / server)~20 min (server + curl)~30 min (server + config)~5 min (pytest)~5 min (pytest)~5 min (pytest)
Files and concepts per framework: in-process ones at the bottom left, with one or two files and five minutes to the first test; server ones at the top right.
A couple of files and instant tests versus Docker and deployment.

And they tested it with an agent: Claude Code scaffolded a complete Wordle environment across all six frameworks in a single session. The in-process ones (Verifiers, SkyRL, GEM) were done in minutes; the HTTP ones (ORS, NeMo Gym, OpenEnv) needed extra rounds for Dockerfiles and deployment configs. A single Python file, no Docker, no server and instant pytest feedback is exactly what suits an agent best.

Dimension 9: local and cluster setuplink

OpenEnvORSNeMo GymVerifiersSkyRL GymGEM
Needs Dockerfor productionfor productionfor productionnonono
Installpip install openenv-core fastmcppip install ors-sdkpip install nemo_gym (from git, pulls Ray)pip install verifierspip install skyrl-gympip install gem-llm
Heavy depsFastMCP, FastAPI, GradioFastAPI, uvicornRay, OmegaConf, aiohttpminimalminimalminimal
Python≥3.11≥3.10≥3.12≥3.11≥3.10≥3.10
Dependency conflict risklowlowhigh (Ray pins many deps)lowlowlow

On a Slurm cluster, the question is who lives where. HTTP frameworks put the environment on its own node, often CPU-only, and point any number of GPU training nodes at it, with a load balancer fronting a pool of replicas you can grow. In-process frameworks put trainer and environment in the same venv on the same GPU node: simpler to operate, but scaling the environment is whatever orchestration story your training framework already has.

HTTP frameworksIn-process frameworks
Environment deploymentHF Spaces or a separate node; training nodes just need requestsFramework package installed in the training venv
GPU usageThe server uses no GPUsShares the GPU node with training
Dependency isolationClean: its own Docker or venvMust coexist with TRL, vLLM, DeepSpeed…
Multi-nodeServer on one node, training on othersSingle node

Dimension 10: scalinglink

RL generates several rollouts per prompt, ideally in parallel, so you're talking to many environments at once. In GRPO that's num_generations (typically 4-16) per prompt: with 64 prompts and num_generations=8, you have 512 concurrent environment instances per step.

In-process

Verifiers, SkyRL Gym and GEM create Python objects: startup takes under a millisecond, so 512, or 10,000, are no problem, and parallelism comes from the training framework (each worker gets its own instance). The real limit is orchestration, not architecture: if the environment does its own compute (heavy parsing, simulation), it shares CPU with training, and scaling it means scaling training nodes, paying for GPUs to do CPU work; if it delegates to a sandbox or remote service (E2B, Modal, a custom HTTP backend), the class is a thin client and the environment scales independently, but the orchestration is yours: picking the provider, managing replicas, failover, load balancing. HTTP frameworks bake that in; in-process ones leave it to you. Two things stay constant: environment dependencies must coexist with training dependencies (a framework pinning a conflicting numpy or torch can break training), and a crash in pure in-process environment code can take down the training worker, while sandboxed execution isolates failures naturally.

With a server: the benchmark

The openenv-scaling benchmark deployed an environment as a FastAPI app in a Docker container (the same image HF Spaces uses) across five infrastructures. OpenEnv, ORS and NeMo Gym all follow that shape (an app with per-session state in a container), so the numbers apply to all three. Maximum concurrent environments at ≥95% success:

InfrastructureCoresMax concurrentPer core
Multi-node Slurm (2 nodes + Envoy load balancer)9616,384170.7
Local uvicorn (8 workers)82,048256
Local Docker (same image as HF Spaces)82,048256
Single-node Slurm (48 workers)4851210.7
Hugging Face Spaces (free, cpu-basic)212864
Concurrent sessions per infrastructure: 128 on a free Space, 2,048 on a laptop with or without Docker and 16,384 with two nodes and Envoy.
From a free Space to sixteen thousand sessions.

And where does the time go at full load? The median of each phase (connect, reset and step, with a simulated 1 s tool wait) and the p99 total:

InfrastructureConnectResetStepp99 total
Local uvicorn0.58 s0.08 s1.05 s1.95 s
Local Docker1.38 s0.19 s1.05 s2.90 s
HF Spaces0.79 s0.10 s1.10 s2.48 s
Slurm, single node0.26 s0.04 s1.00 s1.33 s
Slurm, two nodes (16,384 sessions)17.50 s2.25 s2.42 s26.30 s
Stacked bars with connect, reset and step per infrastructure: the step dominates, except at 16,384 sessions, where connecting climbs to 17.5 s.
The step, where the tool runs, takes almost all the time.

The multi-node p99 reflects connection queuing for 16,384 sessions through the Envoy proxy, even though its absolute throughput (518 requests per second) is the highest of any configuration. The five takeaways:

  1. Docker adds no meaningful overhead: local Docker and uvicorn reach the same 2,048.
  2. Load-balancer configuration matters: before fixing Envoy, the two nodes reached only 128; after, 16,384, a 128× improvement.
  3. HF Spaces caps at about 128 sessions: enough for development and demos, and convenient because it's the largest catalog of pre-built environments.
  4. The server is rarely the bottleneck: even a laptop handles 2,048 sessions. Execution (sandbox creation, tool execution) dominates per-step latency regardless of framework.
  5. Horizontal scaling is a load-balancer config problem, not a protocol problem: the 128 → 16,384 jump came from tuning Envoy, not from changing the wire format. Sticky sessions, which WebSocket forces, make it harder; for thousands of environments, stateless requests with a session ID have fewer footguns.
A toll plaza opening every booth: ×128 from tuning the load balancer; what changed was the configuration, not the protocol.
Over a hundred times more, just by changing the configuration.
OpenEnvORSNeMo GymVerifiersSkyRL GymGEM
Scaling modelmulti-session WS servermulti-session SSE/HTTP servermulti-session server + Rayper-worker instancesper-worker instancesper-worker instances
Horizontal scaleEnvoy + more nodesEnvoy + more nodesRay autoscalingmore GPU workersmore GPU workersAsyncVectorEnv
State isolationper-session dictper session (X-Session-ID)per session (cookies)per instanceper instanceper instance

The global tablelink

Fold the ten dimensions into one table, and three things come out:

  • No framework dominates every row. Each wins a column or two and loses elsewhere. The pick comes down to the two or three dimensions you actually care about: usually deployment shape, episode control and how strict the task spec is.
  • The loudest split is still deployment. HTTP server (OpenEnv, ORS, NeMo Gym) versus in-process (Verifiers, SkyRL Gym, GEM) cuts through almost every other row. Pick that axis first and the table narrows fast.
  • Tooling maturity is uneven. CLI scaffolding, dataset registries and observability are strong in some frameworks and absent in others, even ones that look identical on API surface. Worth checking before you commit.

The six, one by onelink

OpenEnv (Meta PyTorch)

An MCP-based contract: protocol, session and transport layer (WebSocket) and a composable reward system (LLMJudge, WeightedSum, trajectory rubrics). Tasks, datasets and execution are yours. Good for thin, swappable environment interfaces that need to stay MCP-compatible.

ORS (General Reasoning)

A standard API for tools, tasks and rewards: @tool decorators, ToolOutput responses with inline per-step rewards and server-side tasks via list_tasks(split). openreward.ai hosts 330+ environments on the spec. Good for plugging into a large pre-built catalog.

NeMo Gym (NVIDIA)

FastAPI tool servers and a separate /verify endpoint for post-episode scoring, with 50+ built-in environments and tight integration with NeMo and Megatron. Good for trajectory-level scoring and for teams already on the NVIDIA stack.

Verifiers (PrimeIntellect)

The most bundled: datasets, tools, rubrics, rollout harness, trainer and the prime env init scaffold. Its Environments Hub shares environments as wheels. Good for going from zero to training fast with a full toolchain.

SkyRL Gym (NovaSky · Berkeley)

A Gym-style API with BaseTextEnv and ToolGroup, minimal dependencies, and the same library used to train SkyRL-Agent for SWE-Bench. Good for full control over the rollout loop with a familiar Gym mental model.

GEM (Axon-RL)

The closest to Gymnasium: reset() returns an observation, step() returns the 5-tuple (obs, reward, terminated, truncated, info) and AsyncVectorEnv vectorizes. Ships 24+ game, math and code environments. Good for carrying a Gymnasium/Atari mental model over to text agents.

What stood out, and which one to picklink

  • The “environment” boundary is a design choice, not a standard. Some (OpenEnv, SkyRL Gym) give you a thin tool interface and leave dataset, reward and trainer wiring to you; others (Verifiers, GEM) bundle it all. Neither is wrong: it's control versus time-to-running.
  • HTTP versus in-process is the loudest fork. Sandboxed execution (code, shell, browser) wants HTTP, so environment compute scales independently from training. Pure Python (games, math, text reasoning) wants in-process: zero RPC overhead, no infrastructure to babysit.
  • Dataset coupling cuts both ways. Bundled (Verifiers, ORS), environment and dataset come as one unit and you can't swap one without the other. Decoupled (OpenEnv, SkyRL Gym), you wire datasets yourself, but any task fits any environment.
  • Reward timing matters more than reward content. Per-call scoring is clean for per-step feedback and awkward when the reward only makes sense at the end; post-episode verification is clean for trajectory-level scoring but gives no in-episode signal.
  • The execution backend dominates per-step latency, regardless of framework.

And if you just want a recommendation, their decision tree has four yes/no questions:

  1. Where should the environment run? On its own server (OpenEnv, ORS, NeMo Gym) or inside the trainer (Verifiers, SkyRL Gym, GEM).
  2. Do you want a bundled trainer? Yes (NeMo RL, Prime RL or SkyRL) or no, because you'll plug in your own (TRL or custom).
  3. Do you need a large catalog of pre-built environments?
  4. Do you need a strict Gymnasium-style API? The 5-tuple from step(), drop-in for classical RL code.

For example: with your own server and your own trainer, you're left with OpenEnv and ORS; with a server and a bundled trainer, NeMo Gym; and if you need strict Gymnasium, only GEM.

The four-question tree with the example of your own server and your own trainer: two frameworks are left, OpenEnv and ORS.
Four questions to pick one.

What bit themlink

FrameworkGotcha
OpenEnvThe MCP protocol is still evolving: API changes between versions may need adapter updates.
ORSaiohttp base_url handling in the SDK client requires a raw HTTP workaround.
NeMo GymRequires Python 3.12, and strict Pydantic validation on /verify returns 422 on unexpected trajectory shapes.
VerifiersThe dataset is required at init time: environment and dataset are coupled.
SkyRL Gymstep() returns a dict, not a dataclass: a type inconsistency to handle.
GEMgem-llm may not be installed: conditional imports needed.

It's a snapshot from May 2026. All six launched in 2025 and are moving fast: six frameworks doing the same thing with different APIs is what early-stage exploration looks like. The authors expect consolidation around fewer protocols (MCP, ORS) over the next year.

Two details that don't add uplink

Comparing the article's interactive figures against each other, two things don't quite match:

  • GEM and the trainer. The tier map puts GEM in tier 3 (environment and training bundled), but the text says GEM “provides built-in environments + Gymnasium API but you bring the trainer”, and the ship matrix marks its training as partial (“multi-trainer”).
  • NeMo Gym and training. In the five-stage diagram, NeMo Gym stops at rollout, with no trainer; in the ship matrix, though, it has native training (Megatron / NeMo RL), and the decision tree also counts it among the frameworks with a bundled trainer.

None of this changes the conclusions, but it's worth knowing if that cell is what you're choosing by.

Wrap-uplink

An RL environment for an LLM is tasks, tools, a reward and a loop, spread across eleven components that the six frameworks cover in different ways. The decision that orders everything is whether the environment lives on its own server or inside training; then come when the reward fires, who drives the loop and how strict the task format is. And when you scale, the server is rarely the bottleneck: the sandbox is, along with a well-configured load balancer.

In one sentence

The six frameworks are the same thing wearing different clothes: the same environment ports across all of them, so pick the one that fits your stack.

What environment would you like to train?

Sourceslink

Continue reading

Last posts -->

Have you seen these projects?

Gymnasia

Gymnasia Gymnasia
Expo
React Native
TypeScript
OpenAI
Anthropic

Fitness app with two agents that run entirely on the device, with no backend, so the user's data never leaves the phone. A BYOK conversational coach with local tools and a remote system prompt with offline fallback, plus a food estimator that pulls macronutrients out of a photo of the plate, with barcode scanning against OpenFoodFacts.

LangGraph Deep Researcher

LangGraph Deep Researcher LangGraph Deep Researcher
Python
LangGraph
FastAPI
React
TypeScript
Docker

Multi-agent research system built with LangGraph. A supervisor breaks your question down into topics and launches search sub-agents in parallel; each one compresses its findings before handing them to a writer agent that produces the final sourced markdown report. Live streaming over WebSockets, a configurable model per role and bring-your-own API keys that are never persisted server-side.

Tau

Tau Tau
Python
LangChain

Multi-agent tutoring system for secondary school students, with one agent per subject and course material written and validated by a team of teachers. It was used with real students at a private school in Spain and at a secondary school in Colombia.

View all projects -->
>_ Available for projects

Do you have an AI project?

Let's talk.

maximofn@gmail.com

Machine Learning and AI specialist. I develop solutions with generative AI, intelligent agents and custom models.

Do you want to watch any talk?

Last talks -->

Do you want to improve with these tips?

Last tips -->

Use this locally

Hugging Face spaces allow us to run models with very simple demos, but what if the demo breaks? Or if the user deletes it? That's why I've created docker containers with some interesting spaces, to be able to use them locally, whatever happens. In fact, if you click on any project view button, it may take you to a space that doesn't work.

Flow edit

Flow edit Flow edit

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
View all containers -->
>_ Available for projects

Do you have an AI project?

Let's talk.

maximofn@gmail.com

Machine Learning and AI specialist. I develop solutions with generative AI, intelligent agents and custom models.

Do you want to train your model with these datasets?

short-jokes-dataset

HuggingFace

Dataset with jokes in English

Use: Fine-tuning text generation models for humor

231K rows 2 columns 45 MB
View on HuggingFace →

opus100

HuggingFace

Dataset with translations from English to Spanish

Use: Training English-Spanish translation models

1M rows 2 columns 210 MB
View on HuggingFace →

netflix_titles

HuggingFace

Dataset with Netflix movies and series

Use: Netflix catalog analysis and recommendation systems

8.8K rows 12 columns 3.5 MB
View on HuggingFace →
View more datasets -->