From 60ec5218f05e32ed5813b36a8af92b1bb6d0bac3 Mon Sep 17 00:00:00 2001 From: junlin-star Date: Wed, 10 Jun 2026 19:08:56 -0700 Subject: [PATCH 01/11] Add agentic-RL research scaffold with Modal sandbox and env adapters Introduce async_rl_research components: mini-swe-agent rollout updates, Modal-based sandbox, environment adapters (harbor, swe_gym) with slime conversion helpers, and supporting docs. Co-authored-by: Cursor --- async_rl_research/README.md | 34 ++ async_rl_research/agent/base.py | 296 +++++++++++ async_rl_research/agent/mini_swe_agent.py | 369 +++++++------- async_rl_research/architecture.png | Bin 0 -> 473758 bytes async_rl_research/data/.gitignore | 6 + async_rl_research/data/README.md | 41 ++ async_rl_research/env/__init__.py | 5 + async_rl_research/env/base.py | 204 ++++++++ .../env/convert2slime/__init__.py | 7 + async_rl_research/env/convert2slime/harbor.py | 297 +++++++++++ .../env/convert2slime/swe_gym.py | 172 +++++++ async_rl_research/env/harbor.py | 446 +++++++++++++++++ async_rl_research/env/swe_gym.py | 216 ++++++++ async_rl_research/generate.py | 310 ++++++------ async_rl_research/modal_sandbox.py | 469 ++++++++++++++++++ async_rl_research/sandbox.py | 0 16 files changed, 2542 insertions(+), 330 deletions(-) create mode 100644 async_rl_research/README.md create mode 100644 async_rl_research/agent/base.py create mode 100644 async_rl_research/architecture.png create mode 100644 async_rl_research/data/.gitignore create mode 100644 async_rl_research/data/README.md create mode 100644 async_rl_research/env/__init__.py create mode 100644 async_rl_research/env/base.py create mode 100644 async_rl_research/env/convert2slime/__init__.py create mode 100644 async_rl_research/env/convert2slime/harbor.py create mode 100644 async_rl_research/env/convert2slime/swe_gym.py create mode 100644 async_rl_research/env/harbor.py create mode 100644 async_rl_research/env/swe_gym.py create mode 100644 async_rl_research/modal_sandbox.py delete mode 100644 async_rl_research/sandbox.py diff --git a/async_rl_research/README.md b/async_rl_research/README.md new file mode 100644 index 0000000000..d86a13d6de --- /dev/null +++ b/async_rl_research/README.md @@ -0,0 +1,34 @@ +# async_rl_research + +Agentic-RL rollout package for slime. It runs an in-sandbox coding agent +(default: mini-swe-agent) against tasks on Modal, records exact SGLang tokens +via an HTTP adapter, and grades the result into a reward. Task families are +pluggable **envs**: SWE-Gym (git-diff grading in a clean sandbox) and harbor +datasets like USACO (in-place `test.sh` verification, multi-step aware). + +| Module | Role | +| --- | --- | +| `generate.py` | Per-sample rollout entrypoint (`--custom-generate-function-path async_rl_research.generate.generate`); orchestrates `runtime × env` | +| `agent/base.py` | `AgentRuntime` contract + shared launch/provision machinery + runtime registry | +| `agent/mini_swe_agent.py` | Default runtime (`mini-swe`): adapter choice, venv provisioning, headless runner | +| `env/base.py` | `RolloutEnv` contract (row schema, sandbox lifecycle, grading) + env registry; rows pick their env via `metadata.task_type` | +| `env/swe_gym.py` | SWE-Gym env: prebuilt image boot / pre_commands / git diff / clean-sandbox eval | +| `env/harbor.py` | Harbor env: Dockerfile boot, step loop, in-place verify (+ oracle-check CLI) | +| `env/convert2slime/` | Dataset converters, paired with their env by filename (see `data/README.md`) | +| `modal_sandbox.py` | Modal backend (boot concurrency, create retry; registry refs + Dockerfile builds) | +| `dashboard/` | Modal web app (Bun/TS) for browsing the rollout debug dumps as agent conversations (see `dashboard/README.md`) | + +## Setup + +Harbor datasets need two things at rollout time: `ASYNC_RL_TASK_ROOT` pointing at +the converter's out dir (on the slime-data volume), and ideally an oracle pass +first (`python -m async_rl_research.env.harbor --limit 3`, expect +reward=1.0) -- see `data/README.md` for the full flow. + +The rollout boot honors these env vars: + +| Env var | Purpose | +| --- | --- | +| `MODAL_REGISTRY_SECRET` | Modal secret for authenticated Docker Hub pulls (`dockerhub-creds`) | +| `MODAL_ENVIRONMENT` | Modal environment the images are cached in | +| `SLIME_AGENT_SANDBOX_ADD_PYTHON` | Add python to the image (must match rollout) | diff --git a/async_rl_research/agent/base.py b/async_rl_research/agent/base.py new file mode 100644 index 0000000000..e405b3dfc2 --- /dev/null +++ b/async_rl_research/agent/base.py @@ -0,0 +1,296 @@ +"""AgentRuntime: the contract between ``generate.py`` and one agent framework. + +A *runtime* packages everything specific to one in-sandbox agent framework +(mini-swe-agent, opencode, pi, ...): which slime adapter speaks its wire +protocol, how to provision the agent inside the work sandbox, and how to +launch it. Everything else -- adapter/HTTP lifecycle, task workspace prep, +grading, trajectory merge -- is the generic recipe in ``generate.py`` plus +the active task env (``env/base.py``) and never changes per agent. + +The shared launch machinery lives HERE (not in a separate module) on purpose: +``_detached_run`` creates scratch files under ``workdir`` (launcher script, +done marker, log), and the entity that creates scratch must be the entity +that excludes it from the captured diff. ``diff_exclude_all`` = the base's +launch scratch + the subclass's ``diff_exclude``, so "forgot to exclude the +launcher" is structurally impossible. + +Writing a new runtime +--------------------- +Subclass, declare the class attributes, implement ``run_agent`` by composing +the two helpers. Sketch of an opencode-style runtime:: + + class OpenCodeRuntime(AgentRuntime): + name = "opencode" + adapter_cls = OpenAIAdapter # or AnthropicAdapter + diff_exclude = ("opencode.json",) # extra scratch beyond launch files + + async def run_agent(self, sb, *, md, session_id, adapter_url, time_budget_sec): + await self._ensure_provisioned(sb, spec=..., marker_path=..., setup_script=...) + await sb.write_file(f"{md['workdir']}/opencode.json", _config(adapter_url)) + return await self._detached_run( + sb, workdir=md["workdir"], + command="opencode run ...", + env={"OPENAI_API_KEY": session_id}, + time_budget_sec=time_budget_sec, + ) + +Then register it in ``RUNTIMES`` below. + +On-policy rule every runtime must respect: the adapter applies the request +body OVER its per-session sampling defaults, so the agent must NOT send its +own temperature/top_p (a client-sent temperature silently turns rollouts +greedy -> zero-variance GRPO groups). Strip sampling knobs at the agent's +config layer (see mini_swe_agent's runner for the pattern). + +Runtimes are instantiated once per rollout worker (held by +``generate._State``) and must be stateless across samples -- ``run_agent`` +receives everything per-call. +""" + +from __future__ import annotations + +import asyncio +import importlib +import logging +import shlex +import time +from abc import ABC, abstractmethod +from typing import ClassVar + +from slime.agent.sandbox import Sandbox + +logger = logging.getLogger(__name__) + + +# run_agent return convention: the agent's exit code, or this when the +# wallclock budget elapsed before the done marker appeared. +EXIT_BUDGET_EXCEEDED = -2 + + +class AgentRuntime(ABC): + """One agent framework's integration: wire adapter + provision + launch. + + Required class attributes (validated at class-definition time): + + name registry key / log prefix, e.g. "mini-swe" + adapter_cls slime adapter class for the agent's wire protocol + (OpenAIAdapter / AnthropicAdapter). generate._State + constructs it as adapter_cls(tokenizer=, sglang_url=, + tool_parser=, reasoning_parser=). + + Optional class attributes: + + model_name model name advertised to the agent ("slime-actor"); + the adapter ignores it (routes to the served actor) -- + clients only need it to pick the right API dialect. + scratch_prefix prefix for the launch scratch files _detached_run + writes under workdir (".agent" -> .agent_run.sh / + .agent_done / .agent_log). + diff_exclude EXTRA scratch files the runtime writes under workdir + (runner scripts, configs, prompt-convention artifacts + like mini-swe's "patch.txt"). Launch scratch is + excluded automatically -- do not repeat it here. + + ``generate.py`` only ever touches: ``adapter_cls``, ``run_agent``, + ``diff_exclude_all``, ``name``. + """ + + name: ClassVar[str] + adapter_cls: ClassVar[type] + model_name: ClassVar[str] = "slime-actor" + scratch_prefix: ClassVar[str] = ".agent" + diff_exclude: ClassVar[tuple[str, ...]] = () + + def __init_subclass__(cls, **kwargs) -> None: + # Fail at import time, not mid-rollout: a runtime missing its + # declarations should never make it into a training run. + super().__init_subclass__(**kwargs) + missing = [a for a in ("name", "adapter_cls") if getattr(cls, a, None) is None] + if missing: + raise TypeError(f"{cls.__name__} must define class attribute(s) {missing!r} (see AgentRuntime)") + + @property + def diff_exclude_all(self) -> tuple[str, ...]: + """Everything to drop from the captured diff: launch scratch + extras.""" + return (*self._launch_scratch_files(), *self.diff_exclude) + + @abstractmethod + async def run_agent( + self, + sb: Sandbox, + *, + md: dict, + session_id: str, + adapter_url: str, + time_budget_sec: int, + ) -> int: + """Provision + launch the agent in the already-booted, task-prepped sandbox. + + The workspace is ready (the active env applied its setup and wrote + ``PROBLEM_FILE``) -- implementations only set up their own agent. The + agent must target ``adapter_url`` for model calls and send + ``session_id`` as its auth/bearer so the adapter groups its turns. + ``md`` is the env-normalized dataset row (``RolloutEnv + .normalize_metadata``); may be called multiple times per sample in the + SAME sandbox (multi-step episodes). Returns the agent's exit code or + ``EXIT_BUDGET_EXCEEDED``. + """ + + # ------------------------------------------------------------------ + # Shared machinery + # ------------------------------------------------------------------ + def _launch_scratch_files(self) -> tuple[str, str, str]: + """(launcher script, done marker, log) names under workdir.""" + p = self.scratch_prefix + return (f"{p}_run.sh", f"{p}_done", f"{p}_log") + + async def _detached_run( + self, + sb: Sandbox, + *, + workdir: str, + command: str, + env: dict[str, str] | None = None, + time_budget_sec: int, + poll_interval_sec: float = 5.0, + log_tag: str = "", + ) -> int: + """Launch ``command`` detached in ``workdir`` and poll a done-marker. + + Writes a launcher script (cd + ``env`` exports + command, stdout/err + to the log file, exit code to the done marker) and starts it in its + own session (``setsid ... &``) so the exec RPC returns immediately + rather than streaming a multi-minute foreground command. This is NOT + Modal's ``Sandbox.detach()`` -- the sandbox object stays live. + + We avoid a long-lived foreground exec because a worker stream reset + mid-run would be classified transient and re-launch the whole agent + (see ModalSandbox._is_transient / _retry); the poll RPCs are short and + idempotent, so a dropped poll just retries (and each one counts as + sandbox activity if an idle_timeout is configured). On nonzero exit + the log tail is surfaced host-side so an in-sandbox crash is never + just an opaque exit code. + + ``command`` is spliced into the script verbatim -- the caller quotes + its own paths (shlex.quote). Returns the command's exit code, or + ``EXIT_BUDGET_EXCEEDED`` if ``time_budget_sec`` elapses first. + """ + q = shlex.quote + launch, done, log = self._launch_scratch_files() + exports = "".join(f"export {k}={q(str(v))}\n" for k, v in (env or {}).items()) + launcher_body = ( + "#!/bin/bash\n" + f"cd {q(workdir)}\n" + f"{exports}" + f"{command} > {q(log)} 2>&1\n" + f"echo $? > {q(done)}\n" + ) + await sb.write_file(f"{workdir}/{launch}", launcher_body) + # rm the done marker BEFORE launching: a multi-leg episode (e.g. the + # harbor env's multi-step tasks) relaunches in the same sandbox, and a + # stale marker from the previous leg would satisfy the first poll + # while the new agent is still running. + await sb.exec(f"cd {q(workdir)} && rm -f {q(done)} && chmod +x {q(launch)}", check=False, timeout=30) + # Detach so the exec RPC returns immediately; the marker file is the signal. + await sb.exec( + f"cd {q(workdir)} && setsid bash {q(launch)} < /dev/null > /dev/null 2>&1 &", + check=False, + timeout=30, + ) + + done_path = f"{workdir}/{done}" + deadline = time.time() + time_budget_sec + exit_code = EXIT_BUDGET_EXCEEDED + while time.time() < deadline: + await asyncio.sleep(poll_interval_sec) + ec, out, _ = await sb.exec(f"test -f {q(done_path)} && cat {q(done_path)}", check=False, timeout=15) + if ec == 0: + try: + exit_code = int((out or "").strip() or "-1") + except ValueError: + exit_code = -1 + break + if exit_code != 0: + _, tail, _ = await sb.exec(f"tail -c 4000 {q(f'{workdir}/{log}')} 2>/dev/null", check=False, timeout=15) + if (tail or "").strip(): + logger.warning("[%s] %s exit=%s %s tail:\n%s", self.name, log_tag, exit_code, log, tail.strip()) + logger.info("[%s] %s exit=%s elapsed<=%ds", self.name, log_tag, exit_code, time_budget_sec) + return exit_code + + async def _ensure_provisioned( + self, + sb: Sandbox, + *, + spec: str, + marker_path: str, + setup_script: str, + check_cmd: str | None = None, + timeout: int = 900, + ) -> bool: + """Idempotent toolchain install keyed on a spec marker. + + If ``marker_path`` already holds exactly ``spec`` (and ``check_cmd``, + when given, exits 0), the install is skipped -- this is what lets a + pre-baked derived image (or a previous boot) short-circuit, while a + changed pin rebuilds stale pre-baked toolchains instead of silently + running the old agent. Otherwise ``setup_script`` runs (bash, checked), + ``check_cmd`` re-verifies the result, and the marker is written LAST so + a half-finished install is never mistaken for a complete one. + + Returns True if provisioning ran, False on a marker hit. Setup + typically needs outbound network, which the default + ``block_network=False`` allows. + """ + q = shlex.quote + probe = f"cat {q(marker_path)} 2>/dev/null" + if check_cmd: + probe = f"({check_cmd}) >/dev/null 2>&1 && " + probe + _, out, _ = await sb.exec(probe, check=False, timeout=60) + if (out or "").strip() == spec: + return False + + logger.info("[%s] provisioning spec=%s in sandbox %s", self.name, spec, sb.sandbox_id[:8]) + await sb.exec(setup_script, check=True, timeout=timeout) + if check_cmd: + await sb.exec(check_cmd, check=True, timeout=60) + await sb.exec(f"printf '%s' {q(spec)} > {q(marker_path)}", check=True, timeout=30) + return True + + +# --------------------------------------------------------------------------- +# Registry + loader +# --------------------------------------------------------------------------- +DEFAULT_RUNTIME = "mini-swe" + +# Short name -> "module:Class". Values are strings (not classes) so importing +# base.py never imports any runtime module. +RUNTIMES: dict[str, str] = { + "mini-swe": "async_rl_research.agent.mini_swe_agent:MiniSweAgentRuntime", +} + + +def load_runtime(spec: str | None = None) -> AgentRuntime: + """Resolve ``spec`` to an AgentRuntime instance, validating eagerly. + + Accepted forms: + * registry short name "mini-swe" + * explicit class "pkg.module:ClassName" + * module path exposing RUNTIME "pkg.module" (legacy driver form; + what existing ASYNC_RL_AGENT_DRIVER configs pass) + """ + spec = spec or DEFAULT_RUNTIME + target = RUNTIMES.get(spec, spec) + if ":" in target: + module_path, _, attr = target.partition(":") + else: + module_path, attr = target, "RUNTIME" + module = importlib.import_module(module_path) + cls = getattr(module, attr, None) + if cls is None: + raise ValueError( + f"agent runtime {spec!r}: module {module_path!r} does not expose {attr!r}; " + f"known short names: {sorted(RUNTIMES)}" + ) + if not (isinstance(cls, type) and issubclass(cls, AgentRuntime)): + raise TypeError(f"agent runtime {spec!r} resolved to {cls!r}, which is not an AgentRuntime subclass") + return cls() diff --git a/async_rl_research/agent/mini_swe_agent.py b/async_rl_research/agent/mini_swe_agent.py index 60f9b43f86..d4ceb24826 100644 --- a/async_rl_research/agent/mini_swe_agent.py +++ b/async_rl_research/agent/mini_swe_agent.py @@ -1,26 +1,24 @@ -"""mini-swe-agent driver. +"""mini-swe-agent runtime (the default AgentRuntime). -This module is the agent-specific half of the rollout. The generic recipe -(adapter/HTTP lifecycle, trajectory merge, abort isolation, dataset -normalization, sandbox boot / git_diff / evaluate) lives in -``async_rl_research.generate`` and ``async_rl_research.sandbox``. Here we -own only what is unique to mini-swe-agent: +The contract + shared machinery (detached launch/poll, idempotent +provisioning) live in ``agent/base.py``; the generic rollout recipe lives in +``async_rl_research.generate`` and the per-task-family envs in +``async_rl_research.env``. By the time ``run_agent`` is called the workspace +is already task-prepped (the active env applied its setup and wrote +``PROBLEM_FILE``). Here we own only what is unique to mini-swe-agent: - * ADAPTER_CLS / MODEL_NAME -- which slime adapter speaks this agent's wire - protocol (mini-swe-agent talks to litellm's OpenAI-compatible API, so we - intercept with OpenAIAdapter). + * which adapter speaks this agent's wire protocol (mini-swe-agent talks + to litellm's OpenAI-compatible API, so we intercept with OpenAIAdapter) * the in-sandbox **headless runner** (``MINI_RUNNER_PY``) -- stock mini-swe-agent (LitellmModel + LocalEnvironment + DefaultAgent) wired so - every model call dials back to the slime adapter. - * ``run_agent`` -- provision the package, upload the runner + task, launch - it detached, and poll a done-marker (sandbox gateways reset long-lived - HTTP/2 connections, so we cannot hold a multi-minute foreground exec). + every model call dials back to the slime adapter + * the isolated uv-venv provisioning spec (a standalone py3.11 + + ``mini-swe-agent`` that never touches the image's testbed conda env) + * the launch env/command wiring (litellm env vars, ``-P`` safe path) -Token capture + loss masking happen entirely host-side in the adapter; this +Token capture + loss masking happen entirely host-side in the adapter; the runner is "dumb" and never sees token ids. mini-swe-agent runs UNMODIFIED at -its public OpenAI boundary -- the only requirement on the sandbox image is -python + the ``mini-swe-agent`` package (prefer baking it in; the best-effort -pip install below is a fallback for dev). +its public OpenAI boundary. Design A wire flow per turn:: @@ -30,59 +28,76 @@ (return_logprob) -> record TurnRecord -> OpenAI JSON back in-sandbox: run bash tool-call locally -> append observation -> loop -The served model must support tool-call bash; set the matching SGLang parsers -on the launcher, e.g. ``--sglang-tool-call-parser qwen3_coder`` and -``--sglang-reasoning-parser qwen3``. +mini-swe-agent v2 drives bash through NATIVE tool-calls, so the adapter MUST +be given the served model's sglang tool-call parser or every response looks +tool-less and the agent format-errors in a loop. Set the matching parsers on +the launcher, e.g. ``--sglang-tool-call-parser qwen25`` (Qwen3 emits +hermes-style ```` JSON) and ``--sglang-reasoning-parser qwen3``. """ from __future__ import annotations -import asyncio -import logging import os import shlex -import time from slime.agent.adapters import OpenAIAdapter from slime.agent.sandbox import Sandbox -logger = logging.getLogger(__name__) +from .base import AgentRuntime - -# --- driver declaration (read by async_rl_research.generate._State) --------- -ADAPTER_CLS = OpenAIAdapter -# Advertised to litellm as "openai/". The adapter ignores the name -# (it routes to the SGLang-served actor); litellm only needs the provider -# prefix so it speaks the OpenAI dialect at our adapter_url. -MODEL_NAME = "slime-actor" +# Task-layer constant: the active env writes the problem statement here +# before run_agent is called; the runner reads it via MSWE_PROBLEM_FILE. +from ..env.base import PROBLEM_FILE # --- mini-swe-agent-specific knobs ------------------------------------------ MSWE_STEP_LIMIT = int(os.environ.get("MSWE_STEP_LIMIT", "50")) -# Prefer baking `pip install mini-swe-agent==` into the sandbox image. If -# MSWE_PIP_INSTALL=1, run_agent will best-effort install it at boot (needs the -# sandbox to have outbound PyPI access). -MSWE_PIP_INSTALL = os.environ.get("MSWE_PIP_INSTALL", "0") == "1" -MSWE_PIP_SPEC = os.environ.get("MSWE_PIP_SPEC", "mini-swe-agent") - -# Sandbox paths (kept under workdir; excluded from the captured diff). +# Which builtin mini-swe-agent YAML config (prompts!) the runner loads, +# relative to its packaged config dir. Resolution: MSWE_CONFIG env (global +# override) > the env's md["agent_config"] hint (swe_gym -> +# benchmarks/swebench.yaml, harbor -> mini.yaml) > the SWE-bench default. +MSWE_CONFIG = os.environ.get("MSWE_CONFIG", "") +MSWE_DEFAULT_CONFIG = "benchmarks/swebench.yaml" +# Exact-pinned: the scaffold's prompts + wire protocol are part of the RL task +# distribution (a PyPI drift mid-experiment silently changes the environment), +# and MINI_RUNNER_PY below is written against the v2 API. +MSWE_PIP_SPEC = os.environ.get("MSWE_PIP_SPEC", "mini-swe-agent==2.3.1") +# Prepended to PATH for the agent's bash commands (runner keeps only the dirs +# that exist in the image). LocalEnvironment runs commands via /bin/sh, so the +# swebench images' bashrc-based `conda activate testbed` never fires; putting +# the testbed env's bin first is how its python/pytest win. +MSWE_PATH_PREPEND = os.environ.get( + "MSWE_PATH_PREPEND", "/opt/miniconda3/envs/testbed/bin:/opt/miniconda3/bin" +) +# The agent runs in an isolated venv so the testbed conda env (often pinned to +# an old python) is never used or clobbered. Provisioned at boot with uv; can be +# pre-baked into a derived image (presence of the venv interpreter + matching +# spec marker skips it). +MSWE_AGENT_VENV = os.environ.get("MSWE_AGENT_VENV", "/opt/mswe-agent") +MSWE_AGENT_PYTHON_VERSION = os.environ.get("MSWE_AGENT_PYTHON_VERSION", "3.11") + +_VENV_PY = f"{MSWE_AGENT_VENV}/bin/python" + +# Runtime scratch under workdir beyond the base's launch files (which keep +# their historical .mswe_* names via scratch_prefix below). _RUNNER = ".mswe_runner.py" -_LAUNCH = ".mswe_run.sh" -_DONE = ".mswe_done" -_LOG = ".mswe_log" -_PROBLEM = "PROBLEM_STATEMENT.md" # --------------------------------------------------------------------------- # Headless in-sandbox runner. # -# NOTE: PIN + VERIFY the imports / kwargs against the mini-swe-agent version -# baked into the image -- class names and config kwargs have shifted across -# releases. The wiring that must hold regardless: litellm points at the slime -# adapter (OPENAI_API_BASE/OPENAI_API_KEY), and we DO NOT set temperature here -# (the adapter's per-session sampling defaults must win, keeping RL on-policy). +# Written against mini-swe-agent v2 (exact-pinned via MSWE_PIP_SPEC). The v2 +# specifics this depends on: default prompts ship as packaged YAML configs +# (we load the SWE-bench-tuned one instead of hand-rolling templates), bash is +# driven through NATIVE tool-calls, and cost tracking hard-fails on models +# litellm cannot price (hence cost_tracking="ignore_errors"). The wiring that +# must hold regardless of version: litellm points at the slime adapter +# (OPENAI_API_BASE/OPENAI_API_KEY), and NO sampling knobs reach the request +# body -- the adapter applies the body OVER its per-session defaults, so a +# client-sent temperature would silently turn rollouts greedy (zero-variance +# GRPO groups). # --------------------------------------------------------------------------- -MINI_RUNNER_PY = r'''"""Headless mini-swe-agent runner -- runs INSIDE the sandbox (design A).""" +MINI_RUNNER_PY = r'''"""Headless mini-swe-agent (v2) runner -- runs INSIDE the sandbox (design A).""" import os import sys import traceback @@ -90,21 +105,60 @@ WORKDIR = os.environ["MSWE_WORKDIR"] MODEL = os.environ.get("MSWE_MODEL", "slime-actor") STEP_LIMIT = int(os.environ.get("MSWE_STEP_LIMIT", "50")) +PATH_PREPEND = os.environ.get("MSWE_PATH_PREPEND", "") with open(os.environ["MSWE_PROBLEM_FILE"], encoding="utf-8") as fh: TASK = fh.read() try: + import yaml from minisweagent.agents.default import DefaultAgent + from minisweagent.config import builtin_config_dir from minisweagent.environments.local import LocalEnvironment from minisweagent.models.litellm_model import LitellmModel + # v2 ships its default prompts in packaged YAML configs; the host picks + # one per task family (MSWE_CONFIG: SWE -> the SWE-bench-tuned config's + # patch-submission protocol, harbor -> the generic default). Read the + # builtin path directly -- get_config_from_spec() would also try + # cwd-relative candidates, which a repo file could shadow. + cfg_path = builtin_config_dir / os.environ.get("MSWE_CONFIG", "benchmarks/swebench.yaml") + if not cfg_path.is_file(): + print("[runner] config %s not found; falling back to benchmarks/swebench.yaml" % cfg_path) + cfg_path = builtin_config_dir / "benchmarks" / "swebench.yaml" + cfg = yaml.safe_load(cfg_path.read_text()) + agent_cfg = dict(cfg.get("agent") or {}) + model_cfg = dict(cfg.get("model") or {}) + env_cfg = dict(cfg.get("environment") or {}) + # api_base / api_key come from OPENAI_API_BASE / OPENAI_API_KEY in the env - # (litellm's openai provider reads them). No temperature/top_p here. - model = LitellmModel(model_name="openai/" + MODEL) - env = LocalEnvironment(cwd=WORKDIR) - # cost_limit=0 disables cost tracking (meaningless against a local actor). - agent = DefaultAgent(model, env, step_limit=STEP_LIMIT, cost_limit=0.0) - agent.run(TASK) + # (litellm's openai provider reads them). The bundled config pins + # temperature=0.0 for benchmarking -- strip all sampling knobs so the + # adapter's per-session defaults (training's temperature) stay in force. + model_kwargs = dict(model_cfg.get("model_kwargs") or {}) + model_kwargs.pop("temperature", None) + model_kwargs.pop("top_p", None) + model_cfg.update( + model_name="openai/" + MODEL, + model_kwargs=model_kwargs, + # "openai/slime-actor" has no litellm price entry; the default mode + # would raise on the first successful completion. + cost_tracking="ignore_errors", + ) + agent_cfg.update(step_limit=STEP_LIMIT, cost_limit=0.0) # 0 disables the cost check + + # The agent's bash commands run via /bin/sh (dash on these images), so the + # config's BASH_ENV-based conda activation never fires; prepend the testbed + # env's bin dirs onto PATH instead. config.env wins over os.environ. + env_overrides = dict(env_cfg.get("env") or {}) + prepend = [p for p in PATH_PREPEND.split(":") if p and os.path.isdir(p)] + if prepend: + env_overrides["PATH"] = ":".join(prepend) + ":" + os.environ.get("PATH", "") + + model = LitellmModel(**model_cfg) + env = LocalEnvironment(cwd=WORKDIR, env=env_overrides, timeout=int(env_cfg.get("timeout") or 60)) + agent = DefaultAgent(model, env, **agent_cfg) + info = agent.run(TASK) + print("[runner] exit_status=%s" % info.get("exit_status")) sys.exit(0) except SystemExit: raise @@ -114,125 +168,94 @@ ''' -# --------------------------------------------------------------------------- -# run_agent: provision + launch + poll (the only entrypoint generate.py calls) -# --------------------------------------------------------------------------- -async def run_agent( - sb: Sandbox, - *, - md: dict, - session_id: str, - adapter_url: str, - time_budget_sec: int, -) -> int: - """Provision mini-swe-agent in ``sb``, run it on the task, poll to done. - - Returns the runner's exit code, or ``-2`` if the wallclock budget elapses - first. The agent dials back to ``adapter_url`` for every model call and - authenticates with ``session_id`` so the adapter groups its turns. - """ - workdir = md["workdir"] - await _prepare_workspace(sb, workdir, md) - await _ensure_installed(sb) - return await _launch_and_poll( - sb, - workdir=workdir, - session_id=session_id, - adapter_url=adapter_url, - time_budget_sec=time_budget_sec, - ) - - -async def _prepare_workspace(sb: Sandbox, workdir: str, md: dict) -> None: - # git operations inside the sandbox need the repo marked safe; the diff is - # captured by sandbox.git_diff later. - await sb.exec("git config --system --add safe.directory '*'", check=False, timeout=60) - if md.get("pre_commands"): - await _apply_pre_commands(sb, workdir, md["pre_commands"]) - await sb.write_file(f"{workdir}/{_PROBLEM}", md.get("problem_statement") or "") - await sb.write_file(f"{workdir}/{_RUNNER}", MINI_RUNNER_PY) - - -async def _apply_pre_commands(sb: Sandbox, workdir: str, pre) -> None: - # Keep the work sandbox baseline aligned with eval (sweb-style pre_commands - # are typically `git checkout -f`); skipping them makes the - # model's diff context mismatch the eval base -> apply failures. - body = pre.replace("\\n", "\n") if isinstance(pre, str) else "\n".join(c for c in (pre or []) if c) - await sb.write_file(f"{workdir}/.mswe_pre.sh", "set -e\n" + body) - await sb.exec(f"cd {shlex.quote(workdir)} && bash .mswe_pre.sh", check=False, timeout=600) - +# Provision mini-swe-agent into an isolated py3.11 uv venv. uv resolves and +# installs a standalone interpreter, so we don't depend on the image shipping +# py3.11 (prefer a baked uv; fall back to the astral installer). The base +# writes the spec marker only after this script AND the import check succeed. +_VENV_SETUP = ( + 'export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"\n' + "if ! command -v uv >/dev/null 2>&1; then\n" + " curl -LsSf https://astral.sh/uv/install.sh | sh\n" + ' export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"\n' + "fi\n" + f"rm -rf {shlex.quote(MSWE_AGENT_VENV)}\n" + f"uv venv --python {shlex.quote(MSWE_AGENT_PYTHON_VERSION)} {shlex.quote(MSWE_AGENT_VENV)}\n" + f"uv pip install --python {shlex.quote(_VENV_PY)} {shlex.quote(MSWE_PIP_SPEC)}\n" +) + +# MSWEA_SILENT_STARTUP suppresses the import-time banner that would otherwise +# corrupt the provisioning probe's marker comparison (and litter the run log). +_VENV_CHECK = f"MSWEA_SILENT_STARTUP=1 {shlex.quote(_VENV_PY)} -c 'import minisweagent'" + + +class MiniSweAgentRuntime(AgentRuntime): + name = "mini-swe" + adapter_cls = OpenAIAdapter + # Advertised to litellm as "openai/". The adapter ignores the + # name (it routes to the SGLang-served actor); litellm only needs the + # provider prefix so it speaks the OpenAI dialect at our adapter_url. + model_name = "slime-actor" + # Keep the historical scratch names (.mswe_run.sh / .mswe_done / .mswe_log). + scratch_prefix = ".mswe" + # "patch.txt" is the submission artifact the v2 swebench prompt instructs + # the agent to create; `git add -N .` would otherwise sweep it into the + # diff. (Launch scratch + the task-layer PROBLEM_FILE are excluded by the + # base / by the swe_gym env's git_diff itself.) + diff_exclude = (_RUNNER, "patch.txt") + + async def run_agent( + self, + sb: Sandbox, + *, + md: dict, + session_id: str, + adapter_url: str, + time_budget_sec: int, + ) -> int: + """Provision mini-swe-agent in ``sb``, run it on the task, poll to done.""" + workdir = md["workdir"] + await sb.write_file(f"{workdir}/{_RUNNER}", MINI_RUNNER_PY) + await self._ensure_provisioned( + sb, + spec=MSWE_PIP_SPEC, + marker_path=f"{MSWE_AGENT_VENV}/.mswe_spec", + setup_script=_VENV_SETUP, + check_cmd=_VENV_CHECK, + ) -async def _ensure_installed(sb: Sandbox) -> None: - ec, out, _ = await sb.exec( - 'python -c "import minisweagent" 2>/dev/null && echo MSWE_OK', check=False, timeout=60 - ) - if "MSWE_OK" in (out or ""): - return - if not MSWE_PIP_INSTALL: - raise RuntimeError( - "mini-swe-agent is not installed in the sandbox image. Bake " - f"`pip install {MSWE_PIP_SPEC}` into the image, or set " - "MSWE_PIP_INSTALL=1 to install at boot (needs outbound PyPI)." + base = f"{adapter_url}/v1" + env = { + # litellm's openai provider reads these for base URL + bearer auth. + "OPENAI_API_BASE": base, + "OPENAI_BASE_URL": base, + "OPENAI_API_KEY": session_id, + "MSWE_MODEL": self.model_name, + "MSWE_WORKDIR": workdir, + "MSWE_PROBLEM_FILE": f"{workdir}/{PROBLEM_FILE}", + "MSWE_CONFIG": MSWE_CONFIG or md.get("agent_config") or MSWE_DEFAULT_CONFIG, + "MSWE_STEP_LIMIT": str(MSWE_STEP_LIMIT), + "MSWE_PATH_PREPEND": MSWE_PATH_PREPEND, + # keep the v2 import-time banner out of the runner log. + "MSWEA_SILENT_STARTUP": "1", + } + # Run the agent with the ISOLATED venv interpreter. The runner still + # cd's into workdir + uses LocalEnvironment, so the agent's bash + # tool-calls (tests, git) run against the repo -- with the testbed + # env's bin on PATH (MSWE_PATH_PREPEND) -- only the agent process + # itself is isolated. -P (safe path, py3.11+) keeps the script dir + # (= workdir) off sys.path: a repo that shares a name with an agent + # dep (e.g. the pydantic instances) would otherwise shadow the venv's + # copy and crash the runner at import time, before any model call. + return await self._detached_run( + sb, + workdir=workdir, + command=f"{shlex.quote(_VENV_PY)} -P {shlex.quote(_RUNNER)}", + env=env, + time_budget_sec=time_budget_sec, + log_tag=f"session={session_id}", ) - logger.info("[mini_swe_agent] installing %s in sandbox %s", MSWE_PIP_SPEC, sb.sandbox_id[:8]) - await sb.exec(f"pip install --no-input {shlex.quote(MSWE_PIP_SPEC)}", check=True, timeout=600) - - -async def _launch_and_poll( - sb: Sandbox, - *, - workdir: str, - session_id: str, - adapter_url: str, - time_budget_sec: int, -) -> int: - """Launch the runner detached + poll a done-marker file. - - Sandbox gateways reset HTTP/2 around ~6.5 min, so we cannot keep a - long-lived foreground exec. The launcher writes the exit code into a marker - file; we poll it every 5s via short RPCs (which also keeps the sandbox - alive against idle GC). - """ - q = shlex.quote - base = q(f"{adapter_url}/v1") - launcher_body = ( - "#!/bin/bash\n" - f"cd {q(workdir)}\n" - # litellm's openai provider reads these for base URL + bearer auth. - f"export OPENAI_API_BASE={base}\n" - f"export OPENAI_BASE_URL={base}\n" - f"export OPENAI_API_KEY={q(session_id)}\n" - f"export MSWE_MODEL={q(MODEL_NAME)}\n" - f"export MSWE_WORKDIR={q(workdir)}\n" - f"export MSWE_PROBLEM_FILE={q(f'{workdir}/{_PROBLEM}')}\n" - f"export MSWE_STEP_LIMIT={q(str(MSWE_STEP_LIMIT))}\n" - f"python {q(_RUNNER)} > {q(_LOG)} 2>&1\n" - f"echo $? > {q(_DONE)}\n" - ) - await sb.write_file(f"{workdir}/{_LAUNCH}", launcher_body) - await sb.exec(f"cd {q(workdir)} && chmod +x {q(_LAUNCH)}", check=False, timeout=30) - # Detach so the exec RPC returns immediately; the marker file is the signal. - await sb.exec( - f"cd {q(workdir)} && setsid bash {q(_LAUNCH)} < /dev/null > /dev/null 2>&1 &", - check=False, - timeout=30, - ) - done_path = f"{workdir}/{_DONE}" - deadline = time.time() + time_budget_sec - exit_code = -2 # convention: -2 = budget exceeded - while time.time() < deadline: - await asyncio.sleep(5) - ec, out, _ = await sb.exec(f"test -f {q(done_path)} && cat {q(done_path)}", check=False, timeout=15) - if ec == 0: - try: - exit_code = int((out or "").strip() or "-1") - except ValueError: - exit_code = -1 - break - logger.info("[mini_swe_agent] session=%s exit=%s elapsed<=%ds", session_id, exit_code, time_budget_sec) - return exit_code - - -# sandbox.git_diff should exclude these scratch files from the captured diff: -DIFF_EXCLUDE = (_PROBLEM, _RUNNER, _LAUNCH, _DONE, _LOG, ".mswe_pre.sh") + +# Module export for dotted-module-path loading (legacy ASYNC_RL_AGENT_DRIVER +# configs pass "async_rl_research.agent.mini_swe_agent"; see base.load_runtime). +RUNTIME = MiniSweAgentRuntime diff --git a/async_rl_research/architecture.png b/async_rl_research/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..9c20b2a036c32b7978fc801fb7a264d65daa8ce2 GIT binary patch literal 473758 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
e0m62H;LVX->deec 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

_u^1B{Y$>PI`vo;PrZ5LLiJ)YXJoxJ*R`Wkb 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

QcT)*kc9mlWiw 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πN)~Be>~-M;8mhoe3x$lleKX#Gll461O(T=E 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
l; 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. Put the +out dir on the slime-data volume so head workers can read the task files. +Before training, sanity-check the plumbing with the reference solutions (no +model involved): + +```bash +export ASYNC_RL_TASK_ROOT=$(pwd)/async_rl_research/data/usaco +python -m async_rl_research.env.harbor $ASYNC_RL_TASK_ROOT/usaco.jsonl --limit 3 # expect reward=1.0 +``` diff --git a/async_rl_research/env/__init__.py b/async_rl_research/env/__init__.py new file mode 100644 index 0000000000..6595716ac5 --- /dev/null +++ b/async_rl_research/env/__init__.py @@ -0,0 +1,5 @@ +"""Task environments (env) + their dataset converters (env/convert2slime).""" + +from .base import PROBLEM_FILE, EnvMetadataError, RewardResult, RolloutEnv, load_env + +__all__ = ["PROBLEM_FILE", "EnvMetadataError", "RewardResult", "RolloutEnv", "load_env"] diff --git a/async_rl_research/env/base.py b/async_rl_research/env/base.py new file mode 100644 index 0000000000..db3d1010c8 --- /dev/null +++ b/async_rl_research/env/base.py @@ -0,0 +1,204 @@ +"""RolloutEnv: the contract between ``generate.py`` and one task family. + +An *env* packages everything specific to one task/dataset family (SWE-Gym, +harbor, ...): how to validate a dataset row, how to boot + prepare the task +sandbox, how to drive the agent across the task's step(s), and how to grade +the result into a reward. Everything else -- adapter/HTTP lifecycle, session +management, trajectory merge, abort/timeout isolation -- is the generic +recipe in ``generate.py`` and never changes per task family. + +This mirrors ``agent/base.py``'s AgentRuntime exactly: one ``generate()`` +orchestrates ``runtime x env``. The runtime knows *which agent* runs and how +to launch it; the env knows *what task* it runs on and what reward it earned. + +Schema-pair convention +---------------------- +``env/.py`` and ``env/convert2slime/.py`` are a pair: the +converter is the ONLY writer of the ``metadata`` dict for task type +```` and the env's ``normalize_metadata`` is the only reader. When a +field is added, both edits land in sibling files. Rows select their env via +``metadata.task_type`` (absent -> ``swe_gym``, the historical schema). + +Writing a new env +----------------- +Subclass, declare ``name``, implement ``normalize_metadata`` + ``rollout``, +register in ``ENVS``. ``rollout`` owns the sandbox lifecycle end-to-end +(boot, prep, agent run(s), grading) so each family can sequence them freely: +SWE grades a captured diff in a separate CLEAN sandbox after the work sandbox +closes; harbor verifies in-place inside the still-open agent sandbox (and +loops over steps for multi-step tasks). + +Envs are instantiated once per rollout worker (cached by ``load_env``) and +must be stateless across samples -- ``rollout`` receives everything per-call. +""" + +from __future__ import annotations + +import gzip +import importlib +import io +import logging +import shlex +import tarfile +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, ClassVar + +logger = logging.getLogger(__name__) + + +# Task-layer artifact shared by all envs: the problem statement lands in the +# workdir (agents/runners read it from disk via MSWE_PROBLEM_FILE etc.) and is +# always excluded from any captured diff. +PROBLEM_FILE = "PROBLEM_STATEMENT.md" + + +class EnvMetadataError(ValueError): + """A dataset row is unusable for this env; str(err) becomes the abort reason.""" + + +@dataclass(frozen=True) +class RewardResult: + """What an env's ``rollout`` hands back to the trajectory merge. + + ``reward`` is the scalar training signal; ``is_solved`` the boolean used + for run-level solve-rate logging; ``extra`` is env-specific diagnostics + merged into the trajectory metadata (SWE: ``applied_cleanly``; harbor: + the raw rewards dict + per-step results). + """ + + reward: float + is_solved: bool + extra: dict[str, Any] = field(default_factory=dict) + + +class RolloutEnv(ABC): + """One task family's integration: row schema + sandbox + grading. + + ``generate.py`` only ever touches: ``name``, ``normalize_metadata``, + ``rollout``. + """ + + name: ClassVar[str] + + def __init_subclass__(cls, **kwargs) -> None: + # Fail at import time, not mid-rollout (same rule as AgentRuntime). + super().__init_subclass__(**kwargs) + if getattr(cls, "name", None) is None: + raise TypeError(f"{cls.__name__} must define class attribute 'name' (see RolloutEnv)") + + @abstractmethod + def normalize_metadata(self, sample) -> dict[str, Any]: + """Normalize one dataset row (a slime ``Sample``) into the env's md dict. + + Must include ``instance_id`` (used for session ids + logging) and + ``workdir``/``agent_config`` if the agent runtime is expected to read + them. Raises ``EnvMetadataError`` (str = abort reason) for rows this + env cannot run. + """ + + @abstractmethod + async def rollout( + self, + md: dict[str, Any], + *, + runtime, + session_id: str, + adapter_url: str, + agent_time_budget_sec: int, + eval_timeout_sec: int, + ) -> RewardResult: + """Run the full task episode: boot, prep, agent run(s), grading. + + ``runtime`` is the active ``agent.base.AgentRuntime``; call + ``runtime.run_agent(sb, md=, session_id=, adapter_url=, + time_budget_sec=)`` for each agent leg (all legs share the one + adapter session, so a multi-step episode is still one trajectory). + ``agent_time_budget_sec`` bounds TOTAL agent wallclock across legs; + ``eval_timeout_sec`` caps each grading command. The caller wraps this + whole coroutine in a wall-clock guard -- exceeding + budget + eval + slack aborts the sample. + """ + + # ------------------------------------------------------------------ + # Shared sandbox helpers + # ------------------------------------------------------------------ + @staticmethod + async def write_problem_file(sb, workdir: str, text: str | None) -> None: + await sb.write_file(f"{workdir}/{PROBLEM_FILE}", text or "") + + @staticmethod + async def upload_dir(sb, host_dir: str | Path, sandbox_dir: str) -> None: + """Copy a host directory's CONTENTS into ``sandbox_dir`` (created fresh). + + Ships one gzipped tar through ``write_file`` instead of N round-trips; + task tests/solution dirs are small, so in-memory packing is fine. + """ + host_dir = Path(host_dir) + buf = io.BytesIO() + # mtime=0 so re-uploads of identical content are byte-identical. + with gzip.GzipFile(fileobj=buf, mode="wb", mtime=0) as gz: + with tarfile.open(fileobj=gz, mode="w") as tar: + tar.add(host_dir, arcname=".") + archive = f"/tmp/.upload_{abs(hash(str(host_dir))) % 10**8}.tgz" + await sb.write_file(archive, buf.getvalue()) + q = shlex.quote + await sb.exec( + f"rm -rf {q(sandbox_dir)} && mkdir -p {q(sandbox_dir)} && tar -xzf {q(archive)} -C {q(sandbox_dir)}", + check=True, + timeout=120, + ) + + +def coerce_prompt(prompt) -> str: + """Best-effort extraction of plain text from a slime prompt field.""" + if isinstance(prompt, str): + return prompt + if isinstance(prompt, list): + for m in prompt: + if isinstance(m, dict) and m.get("role") == "user": + c = m.get("content") + if isinstance(c, str): + return c + if isinstance(c, list): + return "\n".join(p.get("text", "") for p in c if isinstance(p, dict) and p.get("type") == "text") + return "" + + +# --------------------------------------------------------------------------- +# Registry + loader (mirrors agent.base.RUNTIMES / load_runtime) +# --------------------------------------------------------------------------- +DEFAULT_ENV = "swe_gym" + +# task_type -> "module:Class". Values are strings so importing base.py never +# imports any env module (env modules pull in provider backends). +ENVS: dict[str, str] = { + "swe_gym": "async_rl_research.env.swe_gym:SweGymEnv", + "harbor": "async_rl_research.env.harbor:HarborEnv", +} + +_ENV_CACHE: dict[str, RolloutEnv] = {} + + +def load_env(spec: str | None = None) -> RolloutEnv: + """Resolve ``spec`` (a row's ``metadata.task_type``) to a cached env instance. + + Accepted forms: a registry short name ("harbor"), or "pkg.module:Class" + for out-of-tree envs. Absent -> ``DEFAULT_ENV`` (the historical SWE rows + carry no ``task_type``). + """ + spec = spec or DEFAULT_ENV + cached = _ENV_CACHE.get(spec) + if cached is not None: + return cached + target = ENVS.get(spec, spec) + if ":" not in target: + raise ValueError(f"unknown task_type {spec!r}; known: {sorted(ENVS)} (or pass 'pkg.module:Class')") + module_path, _, attr = target.partition(":") + cls = getattr(importlib.import_module(module_path), attr, None) + if not (isinstance(cls, type) and issubclass(cls, RolloutEnv)): + raise TypeError(f"task_type {spec!r} resolved to {cls!r}, which is not a RolloutEnv subclass") + env = cls() + _ENV_CACHE[spec] = env + return env diff --git a/async_rl_research/env/convert2slime/__init__.py b/async_rl_research/env/convert2slime/__init__.py new file mode 100644 index 0000000000..ac19529042 --- /dev/null +++ b/async_rl_research/env/convert2slime/__init__.py @@ -0,0 +1,7 @@ +"""Dataset converters: one per env, paired by filename (see env/base.py). + +``env/convert2slime/.py`` is the only writer of the ``metadata`` schema +that ``env/.py`` reads. Converters run offline (laptop or head node) +and may carry heavy/optional dependencies (``datasets``, ``harbor``) that the +rollout runtime never imports. +""" diff --git a/async_rl_research/env/convert2slime/harbor.py b/async_rl_research/env/convert2slime/harbor.py new file mode 100644 index 0000000000..6dd3bc0c7b --- /dev/null +++ b/async_rl_research/env/convert2slime/harbor.py @@ -0,0 +1,297 @@ +"""Materialize a harbor dataset as slime prompt data + local task dirs. + +Schema pair of ``env/harbor.py`` (rows carry ``metadata.task_type: +"harbor"``). This converter is the ONLY writer of that schema: it parses each +task's ``task.toml`` offline (plain ``tomllib`` -- the ``harbor`` package is +needed only for ``--registry`` downloads) and bakes everything the rollout +needs into ``metadata``, so the rollout runtime never reads harbor config. + +Output layout (put ``--out-dir`` on the slime-data volume):: + + /.jsonl slime prompt data + /tasks// copied harbor task dirs (environment/ for + Dockerfile builds, tests/ + steps/ for + verification, solution/ for oracle checks) + +Rows reference tasks via ``metadata.task_path`` RELATIVE to the out dir; +export ``ASYNC_RL_TASK_ROOT=`` for the rollout / oracle. + +Sources:: + + # a directory whose subdirectories are harbor tasks (e.g. a + # harbor-datasets checkout subtree, or an adapter's generated tasks) + python -m async_rl_research.env.convert2slime.harbor \ + --tasks-dir ~/harbor-datasets/datasets/usaco --out-dir data/usaco + + # straight from a harbor registry (requires `pip install harbor`) + python -m async_rl_research.env.convert2slime.harbor \ + --registry ~/harbor/registry.json --dataset usaco --out-dir data/usaco + +v1 scope (anything else is skipped + logged): linux, single-container +Dockerfile or prebuilt docker_image, shared-environment verification, no +GPU/TPU, no MCP servers. Multi-step tasks ARE supported (per-step +instruction/tests/min_reward; see env/harbor.py for reward aggregation). +""" + +from __future__ import annotations + +import argparse +import json +import logging +import re +import shutil +import tomllib +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + +_COPY_IGNORE = shutil.ignore_patterns(".git", "__pycache__", ".DS_Store", ".venv", "node_modules") +_WORKDIR_RE = re.compile(r"^\s*WORKDIR\s+(.+?)\s*$", re.IGNORECASE | re.MULTILINE) + + +class SkipTask(Exception): + """Task is outside v1 scope; str(err) is the logged reason.""" + + +def _parse_dockerfile_workdir(dockerfile: Path) -> str | None: + matches = _WORKDIR_RE.findall(dockerfile.read_text(encoding="utf-8", errors="replace")) + if not matches: + return None + last = matches[-1].strip().strip('"').strip("'") + # Variable or relative WORKDIRs can't be resolved statically; let the + # rollout detect the cwd from the booted sandbox instead. + return last if last.startswith("/") and "$" not in last else None + + +def _check_verifier_shared(verifier_cfg: dict[str, Any], where: str) -> None: + if verifier_cfg.get("environment_mode") == "separate" or verifier_cfg.get("environment"): + raise SkipTask(f"separate verifier environment ({where})") + + +def _verifier_md(verifier_cfg: dict[str, Any]) -> dict[str, Any]: + md: dict[str, Any] = {} + if verifier_cfg.get("timeout_sec"): + md["timeout_sec"] = float(verifier_cfg["timeout_sec"]) + if verifier_cfg.get("env"): + md["env"] = dict(verifier_cfg["env"]) + return md + + +def _instruction(path: Path, where: str) -> str: + if not path.is_file(): + raise SkipTask(f"missing {where}") + return path.read_text(encoding="utf-8") + + +def _steps_md(cfg: dict[str, Any], task_dir: Path) -> list[dict[str, Any]]: + steps_md = [] + for step in cfg.get("steps") or []: + name = step.get("name") + if not name: + raise SkipTask("unnamed step") + step_dir = task_dir / "steps" / name + # Harbor falls back to the shared top-level tests/ when a step ships + # no tests of its own. + tests_path = f"steps/{name}/tests" if (step_dir / "tests" / "test.sh").is_file() else "tests" + if not (task_dir / tests_path / "test.sh").is_file(): + raise SkipTask(f"step {name!r} has no tests/test.sh (step or shared)") + _check_verifier_shared(step.get("verifier") or {}, f"step {name!r}") + steps_md.append( + { + "name": name, + "instruction": _instruction(step_dir / "instruction.md", f"steps/{name}/instruction.md"), + "tests_path": tests_path, + "verifier": _verifier_md(step.get("verifier") or {}), + "min_reward": step.get("min_reward"), + "agent_timeout_sec": (step.get("agent") or {}).get("timeout_sec"), + } + ) + return steps_md + + +def translate_task(task_dir: Path, *, dataset: str | None = None) -> dict[str, Any]: + """One harbor task dir -> one slime row (metadata.task_path filled by caller). + + ``dataset`` qualifies tasks whose task.toml carries no ``[task].name`` + (harbor-datasets tasks are often bare numeric dirs like ``usaco/84``). + Raises ``SkipTask`` for tasks outside v1 scope. + """ + config_path = task_dir / "task.toml" + if not config_path.is_file(): + raise SkipTask("no task.toml") + cfg = tomllib.loads(config_path.read_text(encoding="utf-8")) + + env_cfg = cfg.get("environment") or {} + if (env_cfg.get("os") or "linux") != "linux": + raise SkipTask(f"os={env_cfg.get('os')}") + if env_cfg.get("gpus") or env_cfg.get("gpu_types") or env_cfg.get("tpu"): + raise SkipTask("requires GPU/TPU") + if env_cfg.get("mcp_servers"): + raise SkipTask("requires MCP servers") + if env_cfg.get("network_mode") in ("no-network", "allowlist"): + # The Modal backend doesn't enforce per-phase network policies yet; + # running these would silently grant more network than the task allows. + raise SkipTask(f"network_mode={env_cfg['network_mode']}") + + docker_image = env_cfg.get("docker_image") + dockerfile = None + workdir = env_cfg.get("workdir") + if not docker_image: + if (task_dir / "environment" / "docker-compose.yaml").is_file() or ( + task_dir / "environment" / "docker-compose.yml" + ).is_file(): + raise SkipTask("docker-compose environment") + if not (task_dir / "environment" / "Dockerfile").is_file(): + raise SkipTask("no environment/Dockerfile or docker_image") + dockerfile = "environment/Dockerfile" + if not workdir: + workdir = _parse_dockerfile_workdir(task_dir / "environment" / "Dockerfile") + + verifier_cfg = cfg.get("verifier") or {} + _check_verifier_shared(verifier_cfg, "task") + + steps_md = _steps_md(cfg, task_dir) + if steps_md: + instruction_path = task_dir / "instruction.md" + instruction = instruction_path.read_text(encoding="utf-8") if instruction_path.is_file() else steps_md[0]["instruction"] + else: + instruction = _instruction(task_dir / "instruction.md", "instruction.md") + if not (task_dir / "tests" / "test.sh").is_file(): + raise SkipTask("no tests/test.sh") + + fallback = f"{dataset}/{task_dir.name}" if dataset else task_dir.name + task_name = ((cfg.get("task") or {}).get("name") or fallback).strip() + instance_id = re.sub(r"[^A-Za-z0-9_.-]+", "__", task_name) or task_dir.name + + metadata: dict[str, Any] = { + "task_type": "harbor", + "instance_id": instance_id, + "docker_image": docker_image, + "dockerfile": dockerfile, + "workdir": workdir, + "problem_statement": instruction, + "agent_timeout_sec": (cfg.get("agent") or {}).get("timeout_sec"), + "verifier": _verifier_md(verifier_cfg), + "steps": steps_md or None, + "reward_strategy": cfg.get("multi_step_reward_strategy"), + "cpus": env_cfg.get("cpus"), + "memory_mb": env_cfg.get("memory_mb"), + } + metadata = {k: v for k, v in metadata.items() if v is not None} + + return {"prompt": instruction, "label": task_name, "metadata": metadata} + + +def convert( + task_dirs: list[Path], out_dir: Path, *, name: str, dataset: str | None = None, limit: int | None = None +) -> tuple[int, int]: + """Copy tasks + write the JSONL. Returns (converted, skipped).""" + out_dir.mkdir(parents=True, exist_ok=True) + tasks_out = out_dir / "tasks" + rows: list[dict[str, Any]] = [] + skipped = 0 + + for task_dir in task_dirs: + if limit is not None and len(rows) >= limit: + break + try: + row = translate_task(task_dir, dataset=dataset) + except SkipTask as e: + skipped += 1 + logger.warning("[harbor2slime] skip %s: %s", task_dir.name, e) + continue + instance_id = row["metadata"]["instance_id"] + dest = tasks_out / instance_id + if dest.exists(): + shutil.rmtree(dest) + shutil.copytree(task_dir, dest, ignore=_COPY_IGNORE) + row["metadata"]["task_path"] = f"tasks/{instance_id}" + rows.append(row) + + jsonl_path = out_dir / f"{name}.jsonl" + with open(jsonl_path, "w", encoding="utf-8") as fh: + for row in rows: + fh.write(json.dumps(row, ensure_ascii=False) + "\n") + logger.info("[harbor2slime] wrote %d rows -> %s (skipped %d)", len(rows), jsonl_path, skipped) + return len(rows), skipped + + +def _discover_task_dirs(tasks_dir: Path) -> list[Path]: + """Direct subdirectories holding a task.toml, sorted for determinism.""" + if (tasks_dir / "task.toml").is_file(): + return [tasks_dir] + return sorted(p for p in tasks_dir.iterdir() if (p / "task.toml").is_file()) + + +def _download_from_registry(registry_spec: str, dataset: str, version: str | None, download_dir: Path) -> list[Path]: + """Fetch a dataset's tasks via the harbor package (optional dependency).""" + try: + from harbor.models.registry import Registry + from harbor.tasks.client import TaskClient + except ImportError as exc: + raise SystemExit("--registry mode needs the `harbor` package: pip install harbor") from exc + + import asyncio + + if registry_spec.startswith(("http://", "https://")): + registry = Registry.from_url(registry_spec) + else: + registry = Registry.from_path(Path(registry_spec)) + matches = [d for d in registry.datasets if d.name == dataset and (version is None or d.version == version)] + if not matches: + known = sorted({d.name for d in registry.datasets}) + raise SystemExit(f"dataset {dataset!r} not in registry; known: {known}") + spec = matches[-1] + task_ids = [t.to_source_task_id() for t in spec.tasks] + logger.info("[harbor2slime] downloading %d tasks for %s==%s", len(task_ids), spec.name, spec.version) + result = asyncio.run(TaskClient().download_tasks(task_ids, output_dir=download_dir)) + + paths: list[Path] = [] + items = result.values() if hasattr(result, "values") else result + for item in items: + path = getattr(item, "path", None) or getattr(item, "downloaded_path", None) + if path: + paths.append(Path(path)) + if not paths: + raise SystemExit(f"registry download produced no task paths (result type {type(result).__name__})") + return sorted(paths) + + +def main(argv: list[str] | None = None) -> int: + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") + parser = argparse.ArgumentParser(description="Materialize a harbor dataset as slime prompt JSONL + task dirs.") + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument("--tasks-dir", type=Path, help="directory of harbor task dirs (or a single task dir)") + source.add_argument("--registry", help="harbor registry.json path or URL (needs `pip install harbor`)") + parser.add_argument("--dataset", help="dataset name in the registry (with --registry)") + parser.add_argument("--dataset-version", help="dataset version in the registry (default: last match)") + parser.add_argument("--out-dir", type=Path, required=True, help="output dir (JSONL + tasks/); use the slime-data volume") + parser.add_argument("--name", help="JSONL filename stem (default: dataset or tasks-dir name)") + parser.add_argument("--limit", type=int, help="maximum tasks to convert") + args = parser.parse_args(argv) + + if args.registry: + if not args.dataset: + parser.error("--registry requires --dataset") + task_dirs = _download_from_registry(args.registry, args.dataset, args.dataset_version, args.out_dir / "downloads") + name = args.name or args.dataset + dataset = args.dataset + else: + task_dirs = _discover_task_dirs(args.tasks_dir) + name = args.name or args.tasks_dir.name + dataset = args.tasks_dir.name + if not task_dirs: + raise SystemExit("no task dirs found") + + converted, skipped = convert(task_dirs, args.out_dir, name=name, dataset=dataset, limit=args.limit) + out_dir = args.out_dir.resolve() + print(f"converted {converted} tasks ({skipped} skipped) -> {out_dir / (name + '.jsonl')}") + print("next steps:") + print(f" export ASYNC_RL_TASK_ROOT={out_dir}") + print(f" python -m async_rl_research.env.harbor {out_dir / (name + '.jsonl')} --limit 3 # oracle check") + return 0 if converted else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/async_rl_research/env/convert2slime/swe_gym.py b/async_rl_research/env/convert2slime/swe_gym.py new file mode 100644 index 0000000000..64043c23e5 --- /dev/null +++ b/async_rl_research/env/convert2slime/swe_gym.py @@ -0,0 +1,172 @@ +"""Translate SWE-Gym / SWE-Gym-Lite rows into slime prompt data. + +Schema pair of ``env/swe_gym.py`` (SWE rows carry no ``metadata.task_type``: +they are the default env). The output is one JSON object per line: + + { + "prompt": "...", + "label": "owner__repo-123", + "metadata": { + "instance_id": "...", + "image": "...", + "workdir": "/testbed", + "problem_statement": "...", + "eval_cmd": "echo ... | base64 -d > /tmp/swegym_eval.py && python /tmp/swegym_eval.py", + "pre_commands": ["git checkout -f"] + } + } + +The only SWE-Gym-specific choices here are deriving the prebuilt image name and +building a simple pytest-based reward command from ``test_patch`` + F2P/P2P +tests. +""" + +from __future__ import annotations + +import argparse +import base64 +import json +import os +from collections.abc import Iterable, Iterator +from itertools import islice +from pathlib import Path +from typing import Any + +HF_DATASET = "SWE-Gym/SWE-Gym" +HF_DATASET_LITE = "SWE-Gym/SWE-Gym-Lite" +IMAGE_PREFIX = os.environ.get("SWE_GYM_IMAGE_PREFIX", "docker.io/xingyaoww") +IMAGE_TAG = os.environ.get("SWE_GYM_IMAGE_TAG", "latest") +WORKDIR = "/testbed" + + +def image_for(instance_id: str) -> str: + name = "sweb.eval.x86_64." + instance_id.replace("__", "_s_") + return f"{IMAGE_PREFIX.rstrip('/')}/{name}:{IMAGE_TAG}" + + +def as_list(value: Any) -> list[str]: + if isinstance(value, str): + value = json.loads(value) if value.strip() else [] + return [str(item) for item in (value or [])] + + +def build_eval_cmd(test_patch: str, tests: list[str]) -> str: + patch_b64 = base64.b64encode((test_patch or "").encode()).decode("ascii") + script = "\n".join( + [ + "import base64", + "import pathlib", + "import subprocess", + "import sys", + "", + "PATCH_B64 = " + repr(patch_b64), + "TESTS = " + json.dumps(tests), + "patch_path = pathlib.Path('/tmp/swegym_test.patch')", + "patch_path.write_bytes(base64.b64decode(PATCH_B64))", + "if patch_path.stat().st_size:", + " commands = [", + " ['git', 'apply', '-v', str(patch_path)],", + " ['git', 'apply', '--3way', str(patch_path)],", + " ['patch', '-p1', '--no-backup-if-mismatch', '-i', str(patch_path)],", + " ]", + " for command in commands:", + " result = subprocess.run(command)", + " if result.returncode == 0:", + " break", + " else:", + " sys.exit(result.returncode)", + "", + "import pytest", + "sys.exit(pytest.main(['--no-header', '-rN', '-p', 'no:cacheprovider', *TESTS]))", + "", + ] + ) + script_b64 = base64.b64encode(script.encode()).decode("ascii") + return f"echo {script_b64} | base64 -d > /tmp/swegym_eval.py && python /tmp/swegym_eval.py" + + +def translate(raw: dict[str, Any]) -> dict[str, Any] | None: + instance_id = raw.get("instance_id") + if not instance_id: + return None + + tests = as_list(raw.get("FAIL_TO_PASS")) + as_list(raw.get("PASS_TO_PASS")) + if not tests: + return None + + problem = raw.get("problem_statement") or "" + metadata: dict[str, Any] = { + "instance_id": instance_id, + "image": image_for(instance_id), + "workdir": WORKDIR, + "problem_statement": problem, + "eval_cmd": build_eval_cmd(raw.get("test_patch") or "", tests), + "repo": raw.get("repo"), + "base_commit": raw.get("base_commit"), + "version": raw.get("version"), + } + if base_commit := raw.get("base_commit"): + metadata["pre_commands"] = [f"git checkout {base_commit} -f"] + + return { + "prompt": problem, + "label": instance_id, + "metadata": metadata, + } + + +def iter_jsonl(path: str | Path) -> Iterator[dict[str, Any]]: + with open(path, encoding="utf-8") as fh: + for line in fh: + line = line.strip() + if line: + yield json.loads(line) + + +def load_hf(split: str, *, lite: bool, limit: int | None) -> Iterator[dict[str, Any]]: + try: + from datasets import load_dataset + except ImportError as exc: + raise SystemExit("Install `datasets` to pull SWE-Gym from HuggingFace.") from exc + + dataset = HF_DATASET_LITE if lite else HF_DATASET + for index, row in enumerate(load_dataset(dataset, split=split)): + if limit is not None and index >= limit: + break + yield dict(row) + + +def write_jsonl(rows: Iterable[dict[str, Any]], out_path: str | Path) -> int: + count = 0 + with open(out_path, "w", encoding="utf-8") as fh: + for raw in rows: + row = translate(raw) + if row is None: + continue + fh.write(json.dumps(row, ensure_ascii=False) + "\n") + count += 1 + return count + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Translate SWE-Gym data to slime prompt JSONL.") + parser.add_argument("--out", required=True, help="output JSONL path") + parser.add_argument("--input", help="downloaded SWE-Gym JSONL to convert") + parser.add_argument("--split", default="train", help="HuggingFace split when --input is omitted") + parser.add_argument("--lite", action="store_true", help="use SWE-Gym-Lite when --input is omitted") + parser.add_argument("--limit", type=int, help="maximum rows to read") + args = parser.parse_args(argv) + + if args.input: + raw_rows: Iterable[dict[str, Any]] = iter_jsonl(args.input) + if args.limit is not None: + raw_rows = islice(raw_rows, args.limit) + else: + raw_rows = load_hf(args.split, lite=args.lite, limit=args.limit) + count = write_jsonl(raw_rows, args.out) + print(f"wrote {count} rows -> {args.out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/async_rl_research/env/harbor.py b/async_rl_research/env/harbor.py new file mode 100644 index 0000000000..b131221d6f --- /dev/null +++ b/async_rl_research/env/harbor.py @@ -0,0 +1,446 @@ +"""Harbor env: run harbor-format tasks (USACO, ...) as RL episodes on Modal. + +The schema pair of ``env/convert2slime/harbor.py`` (see ``base.py``). The +converter materializes harbor task directories next to the JSONL and bakes +everything the rollout needs into ``metadata`` -- this module never imports +the ``harbor`` package and never parses ``task.toml`` at rollout time. + +Episode shape (harbor "shared" verifier semantics): + + boot sandbox (task Dockerfile built on Modal, or prebuilt docker_image) + for each step (single-step tasks are one pseudo-step): + write the step instruction to {workdir}/PROBLEM_STATEMENT.md + agent leg: runtime.run_agent(...) against the shared adapter session + verify IN-PLACE: upload step tests/ -> /tests, run test.sh, + parse /logs/verifier/reward.{json,txt} + gate on the step's min_reward (abort remaining steps below threshold) + aggregate per-step rewards (mean | final) -> scalar training reward + +Verification runs inside the agent's sandbox (tests are uploaded only AFTER +the agent leg, so the agent cannot read them) -- weaker than swe_gym's +clean-sandbox diff grading, but it is harbor's contract: the agent's job is +to leave artifacts behind. Tasks demanding a separate verifier container are +filtered out by the converter. + +Rollout-side requirements: + ASYNC_RL_TASK_ROOT directory the JSONL's relative ``task_path``s resolve + against (the converter's --out-dir; put it on the + slime-data volume so head workers can read tests/). + +Oracle check (no model involved -- validates boot/prep/verify plumbing by +running each task's reference solution through the exact rollout path):: + + python -m async_rl_research.env.harbor out/usaco.jsonl --task-root out --limit 3 +""" + +from __future__ import annotations + +import json +import logging +import os +import re +import shlex +import time +from pathlib import Path +from typing import Any + +from ..modal_sandbox import DockerfileImage, ModalSandbox +from .base import EnvMetadataError, RewardResult, RolloutEnv, coerce_prompt + +logger = logging.getLogger(__name__) + +TASK_ROOT_ENV = "ASYNC_RL_TASK_ROOT" + +# ${VAR} / ${VAR:-default} templates in verifier/solution env values, resolved +# against the HEAD's os.environ at use time (harbor's resolve_env_vars +# semantics, except an unresolvable var is skipped with a warning instead of +# failing the rollout). +_ENV_TEMPLATE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-(.*))?\}$") + + +def _resolve_env_templates(env: dict[str, str] | None) -> dict[str, str]: + resolved: dict[str, str] = {} + for key, value in (env or {}).items(): + m = _ENV_TEMPLATE.fullmatch(str(value)) + if not m: + resolved[key] = str(value) + continue + name, default = m.group(1), m.group(2) + actual = os.environ.get(name, default) + if actual is None: + logger.warning("[harbor] env %s=${%s} unresolvable on this host; skipping", key, name) + continue + resolved[key] = actual + return resolved + + +def _scalar_reward(rewards: dict[str, Any] | None) -> float: + """Harbor 1D convention: the 'reward' key; a single-entry dict counts too.""" + if not rewards: + return 0.0 + if "reward" in rewards: + return float(rewards["reward"]) + if len(rewards) == 1: + return float(next(iter(rewards.values()))) + logger.warning("[harbor] multi-key rewards %s without 'reward' key; scalar=0", sorted(rewards)) + return 0.0 + + +def _meets_min_reward(rewards: dict[str, Any] | None, min_reward: float | dict[str, float] | None) -> bool: + """Harbor's step gate: missing rewards/keys are treated as -inf.""" + if min_reward is None: + return True + if isinstance(min_reward, dict): + return all(rewards is not None and key in rewards and float(rewards[key]) >= float(v) for key, v in min_reward.items()) + return rewards is not None and "reward" in rewards and float(rewards["reward"]) >= float(min_reward) + + +class HarborEnv(RolloutEnv): + name = "harbor" + # mini-swe-agent prompt config hint: the generic builtin, not the SWE-bench + # patch-submission one -- harbor tasks ask for artifacts, not patches. + # Per-row override via metadata.agent_config; global via MSWE_CONFIG. + agent_config = "mini.yaml" + + # ------------------------------------------------------------------ + # Row schema (written by env/convert2slime/harbor.py -- keep in sync) + # ------------------------------------------------------------------ + def normalize_metadata(self, sample) -> dict[str, Any]: + m = sample.metadata or {} + task_path = m.get("task_path") + if not task_path: + raise EnvMetadataError("missing_task_path") + task_dir = self._resolve_task_dir(task_path) + + docker_image = m.get("docker_image") + dockerfile = m.get("dockerfile") + if not docker_image and not dockerfile: + raise EnvMetadataError("missing_docker_image_or_dockerfile") + if dockerfile and not (task_dir / dockerfile).is_file(): + raise EnvMetadataError(f"dockerfile_missing:{task_dir / dockerfile}") + + steps = m.get("steps") or None + if steps: + for step in steps: + if not (task_dir / step["tests_path"] / "test.sh").is_file(): + raise EnvMetadataError(f"tests_missing:{step['tests_path']}") + elif not (task_dir / "tests" / "test.sh").is_file(): + raise EnvMetadataError("tests_missing:tests") + + return { + "instance_id": m.get("instance_id") or sample.label or task_dir.name, + "task_dir": str(task_dir), + "docker_image": docker_image, + "dockerfile": dockerfile, + "workdir": m.get("workdir"), # None -> detected from the booted sandbox + "problem_statement": m.get("problem_statement") or coerce_prompt(sample.prompt), + "agent_timeout_sec": m.get("agent_timeout_sec"), + "verifier": m.get("verifier") or {}, + "steps": steps, + "reward_strategy": m.get("reward_strategy"), + "cpus": m.get("cpus"), + "memory_mb": m.get("memory_mb"), + "agent_config": m.get("agent_config") or self.agent_config, + } + + @staticmethod + def _resolve_task_dir(task_path: str) -> Path: + p = Path(task_path) + if not p.is_absolute(): + root = os.environ.get(TASK_ROOT_ENV) + if not root: + raise EnvMetadataError(f"{TASK_ROOT_ENV}_unset") + p = Path(root) / p + if not p.is_dir(): + raise EnvMetadataError(f"task_dir_missing:{p}") + return p + + # ------------------------------------------------------------------ + # Episode + # ------------------------------------------------------------------ + def _image(self, md: dict[str, Any]) -> str | DockerfileImage: + if md["docker_image"]: + return md["docker_image"] + path = Path(md["task_dir"]) / md["dockerfile"] + return DockerfileImage(path=str(path), context_dir=str(path.parent)) + + def _sandbox(self, md: dict[str, Any]) -> ModalSandbox: + kwargs: dict[str, Any] = {} + if md["cpus"]: + kwargs["cpu"] = float(md["cpus"]) + if md["memory_mb"]: + kwargs["memory_mb"] = int(md["memory_mb"]) + if md["workdir"]: + kwargs["workdir"] = md["workdir"] + return ModalSandbox(self._image(md), **kwargs) + + def _step_specs(self, md: dict[str, Any]) -> list[dict[str, Any]]: + """Uniform step list; a single-step task becomes one pseudo-step.""" + if md["steps"]: + return md["steps"] + return [ + { + "name": None, + "instruction": md["problem_statement"], + "tests_path": "tests", + "verifier": md["verifier"], + "min_reward": None, + "agent_timeout_sec": None, + } + ] + + async def rollout( + self, + md: dict[str, Any], + *, + runtime, + session_id: str, + adapter_url: str, + agent_time_budget_sec: int, + eval_timeout_sec: int, + ) -> RewardResult: + async def agent_leg(sb, leg_md: dict[str, Any], budget_sec: int) -> None: + await runtime.run_agent( + sb, + md=leg_md, + session_id=session_id, + adapter_url=adapter_url, + time_budget_sec=budget_sec, + ) + + return await self._episode( + md, + run_leg=agent_leg, + agent_time_budget_sec=agent_time_budget_sec, + eval_timeout_sec=eval_timeout_sec, + ) + + async def _episode( + self, + md: dict[str, Any], + *, + run_leg, + agent_time_budget_sec: int, + eval_timeout_sec: int, + ) -> RewardResult: + """Shared by the RL rollout and the oracle check: only the leg differs.""" + task_dir = Path(md["task_dir"]) + steps = self._step_specs(md) + step_results: list[dict[str, Any]] = [] + deadline = time.monotonic() + agent_time_budget_sec + + async with self._sandbox(md) as sb: + workdir = md["workdir"] or await self._detect_workdir(sb) + q = shlex.quote + # Harbor mounts /logs/{agent,verifier,artifacts}; test scripts + # assume they exist. workdir may be absent on prebuilt images. + await sb.exec( + f"mkdir -p {q(workdir)} /logs/agent /logs/verifier /logs/artifacts", + check=True, + timeout=60, + ) + + for step in steps: + remaining = int(deadline - time.monotonic()) + if remaining <= 0: + logger.warning("[harbor] %s: agent budget exhausted before step %r", md["instance_id"], step["name"]) + break + budget = remaining + for cap in (step.get("agent_timeout_sec"), md["agent_timeout_sec"]): + if cap: + budget = min(budget, int(cap)) + + await self.write_problem_file(sb, workdir, step["instruction"]) + leg_md = {**md, "workdir": workdir} + await run_leg(sb, leg_md, budget) + + rewards = await self._verify( + sb, + tests_dir=task_dir / step["tests_path"], + workdir=workdir, + verifier={**md["verifier"], **(step.get("verifier") or {})}, + eval_timeout_sec=eval_timeout_sec, + instance_id=md["instance_id"], + ) + step_results.append({"name": step["name"], "rewards": rewards, "reward": _scalar_reward(rewards)}) + if not _meets_min_reward(rewards, step.get("min_reward")): + logger.info("[harbor] %s: step %r below min_reward; aborting remaining steps", md["instance_id"], step["name"]) + break + + reward = self._aggregate(steps, step_results, md["reward_strategy"]) + return RewardResult( + reward=reward, + is_solved=reward >= 1.0, + extra={ + "harbor_step_results": step_results, + "harbor_steps_completed": len(step_results), + "harbor_steps_total": len(steps), + }, + ) + + @staticmethod + def _aggregate(steps: list[dict], results: list[dict], strategy: str | None) -> float: + """Scalar episode reward from per-step scalars. + + 'mean' divides by ALL declared steps (an unexecuted/gated step counts + 0) -- stricter than harbor's job-level mean, which excludes steps + whose verifier never ran, but the conservative signal is what RL + wants. 'final' is the last DECLARED step's reward (0 if never + reached), matching harbor. + """ + if not results: + return 0.0 + if len(steps) == 1: + return results[0]["reward"] + if (strategy or "mean") == "final": + return results[-1]["reward"] if len(results) == len(steps) else 0.0 + return sum(r["reward"] for r in results) / len(steps) + + @staticmethod + async def _detect_workdir(sb) -> str: + # Prebuilt docker_image rows may not know their WORKDIR; ask the + # sandbox (Modal honors the image workdir for exec cwd). + ec, out, _ = await sb.exec("pwd", check=False, timeout=30) + detected = (out or "").strip().splitlines()[-1] if ec == 0 and (out or "").strip() else "" + return detected or "/app" + + # ------------------------------------------------------------------ + # In-place verification (harbor's shared-environment Verifier semantics) + # ------------------------------------------------------------------ + async def _verify( + self, + sb, + *, + tests_dir: Path, + workdir: str, + verifier: dict[str, Any], + eval_timeout_sec: int, + instance_id: str, + ) -> dict[str, Any] | None: + """Upload tests, run test.sh, parse the reward files. None = no verdict.""" + timeout = int(verifier.get("timeout_sec") or eval_timeout_sec) + if timeout > eval_timeout_sec: + logger.info( + "[harbor] %s: verifier timeout %ds capped to AGENT_EVAL_TIMEOUT_SEC=%ds", + instance_id, + timeout, + eval_timeout_sec, + ) + timeout = eval_timeout_sec + + await self.upload_dir(sb, tests_dir, "/tests") + q = shlex.quote + await sb.exec( + "chmod +x /tests/test.sh && rm -f /logs/verifier/reward.json /logs/verifier/reward.txt", + check=False, + timeout=60, + ) + env = _resolve_env_templates(verifier.get("env")) + ec, _, err = await sb.exec( + f"cd {q(workdir)} && bash /tests/test.sh", + env=env or None, + timeout=timeout, + check=False, + ) + + raw_json = await sb.read_file("/logs/verifier/reward.json") + if raw_json.strip(): + try: + return dict(json.loads(raw_json)) + except (ValueError, TypeError): + logger.warning("[harbor] %s: unparseable reward.json: %.200s", instance_id, raw_json) + return None + raw_txt = await sb.read_file("/logs/verifier/reward.txt") + if raw_txt.strip(): + try: + return {"reward": float(raw_txt.strip())} + except ValueError: + logger.warning("[harbor] %s: unparseable reward.txt: %.200s", instance_id, raw_txt) + return None + logger.warning( + "[harbor] %s: test.sh exit=%s wrote no reward file; stderr tail: %s", + instance_id, + ec, + (err or "")[-400:], + ) + return None + + # ------------------------------------------------------------------ + # Oracle check (reference solution through the exact rollout path) + # ------------------------------------------------------------------ + async def oracle_episode(self, md: dict[str, Any], *, solve_timeout_sec: int, eval_timeout_sec: int) -> RewardResult: + """Replace the agent leg with the task's solution/solve.sh. + + Legs run sequentially, so a simple counter maps each leg back to its + step (for per-step solution dirs on multi-step tasks). + """ + task_dir = Path(md["task_dir"]) + steps = self._step_specs(md) + state = {"i": 0} + + async def leg(sb, leg_md: dict[str, Any], budget_sec: int) -> None: + step = steps[state["i"]] + state["i"] += 1 + solution_dir = task_dir / "steps" / step["name"] / "solution" if step["name"] else task_dir / "solution" + if not (solution_dir / "solve.sh").is_file(): + solution_dir = task_dir / "solution" + if not (solution_dir / "solve.sh").is_file(): + raise FileNotFoundError(f"no solution/solve.sh under {task_dir}") + await self.upload_dir(sb, solution_dir, "/solution") + q = shlex.quote + await sb.exec("chmod +x /solution/solve.sh", check=False, timeout=30) + ec, _, err = await sb.exec( + f"cd {q(leg_md['workdir'])} && bash /solution/solve.sh", + timeout=min(budget_sec, solve_timeout_sec), + check=False, + ) + if ec != 0: + logger.warning("[harbor-oracle] solve.sh exit=%d stderr tail: %s", ec, (err or "")[-400:]) + + return await self._episode( + md, + run_leg=leg, + agent_time_budget_sec=solve_timeout_sec * max(1, len(steps)), + eval_timeout_sec=eval_timeout_sec, + ) + + +def _oracle_main() -> int: + import argparse + import asyncio + from types import SimpleNamespace + + parser = argparse.ArgumentParser(description="Run harbor reference solutions through the rollout path (reward should be 1.0).") + parser.add_argument("jsonl", help="converted slime prompt JSONL (env/convert2slime/harbor.py output)") + parser.add_argument("--task-root", help=f"task root (default: ${TASK_ROOT_ENV} or the JSONL's directory)") + parser.add_argument("--limit", type=int, default=1, help="how many rows to check (default 1)") + parser.add_argument("--index", type=int, help="check exactly this row") + parser.add_argument("--solve-timeout", type=int, default=600) + parser.add_argument("--eval-timeout", type=int, default=int(os.environ.get("AGENT_EVAL_TIMEOUT_SEC", "600"))) + args = parser.parse_args() + + root = args.task_root or os.environ.get(TASK_ROOT_ENV) or str(Path(args.jsonl).resolve().parent) + os.environ[TASK_ROOT_ENV] = root + + rows = [] + with open(args.jsonl, encoding="utf-8") as fh: + for line in fh: + if line.strip(): + rows.append(json.loads(line)) + picked = [rows[args.index]] if args.index is not None else rows[: args.limit] + + env = HarborEnv() + failures = 0 + for row in picked: + sample = SimpleNamespace(metadata=row.get("metadata"), prompt=row.get("prompt"), label=row.get("label")) + md = env.normalize_metadata(sample) + result = asyncio.run(env.oracle_episode(md, solve_timeout_sec=args.solve_timeout, eval_timeout_sec=args.eval_timeout)) + status = "OK " if result.is_solved else "FAIL" + print(f"[{status}] {md['instance_id']}: reward={result.reward:.2f} {result.extra}") + failures += 0 if result.is_solved else 1 + return 1 if failures else 0 + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") + raise SystemExit(_oracle_main()) diff --git a/async_rl_research/env/swe_gym.py b/async_rl_research/env/swe_gym.py new file mode 100644 index 0000000000..1ef1ffad1e --- /dev/null +++ b/async_rl_research/env/swe_gym.py @@ -0,0 +1,216 @@ +"""SWE-Gym env: git-diff capture + clean-sandbox grading on Modal. + +The schema pair of ``env/convert2slime/swe_gym.py`` (see ``base.py`` for the +convention). Each row carries a prebuilt per-instance registry image plus a +self-contained ``eval_cmd`` (or a ``swepro`` run/parse spec), and grading is +diff-transplant based: + + boot work sandbox -> pre_commands + problem file -> agent runs -> + capture git diff -> CLOSE work sandbox -> boot CLEAN sandbox -> + re-apply pre_commands -> apply diff -> run eval_cmd. + +No-test-cheating guarantee: the evaluator sandbox never sees the agent's +filesystem -- only the captured diff can affect reward. This is what +``rollout`` owning the whole lifecycle buys: the work sandbox is torn down +BEFORE the evaluator boots, exactly as the pre-refactor flow did. + +The provider backend is ``modal_sandbox.ModalSandbox``; boot concurrency and +create-retry live there, so the evaluator sandbox is gated and retried just +like the work sandbox. +""" + +from __future__ import annotations + +import json +import logging +import shlex +from pathlib import Path +from typing import Any + +from slime.agent.sandbox import Sandbox + +from ..modal_sandbox import ModalSandbox +from .base import PROBLEM_FILE, EnvMetadataError, RewardResult, RolloutEnv, coerce_prompt + +logger = logging.getLogger(__name__) + + +# Patch/pre scripts live under /tmp, outside the diff's reach. +_PATCH = "/tmp/__swe_patch__.diff" +_PRE = "/tmp/__swe_pre__.sh" + + +class SweGymEnv(RolloutEnv): + name = "swe_gym" + # mini-swe-agent prompt config hint: the SWE-bench-tuned builtin (patch + # submission protocol, capped observations). Rows may override via + # metadata.agent_config. + agent_config = "benchmarks/swebench.yaml" + + def normalize_metadata(self, sample) -> dict[str, Any]: + m = sample.metadata or {} + label = sample.label if (isinstance(sample.label, str) and len(sample.label) < 256) else None + md = { + "instance_id": m.get("instance_id") or label or "unknown", + "image": m.get("image"), + "workdir": m.get("workdir"), + "problem_statement": m.get("problem_statement") or coerce_prompt(sample.prompt), + "swepro": m.get("swepro"), + "eval_cmd": m.get("eval_cmd"), + "pre_commands": m.get("pre_commands"), + "agent_config": m.get("agent_config") or self.agent_config, + } + if not md["image"] or not md["workdir"]: + raise EnvMetadataError("missing_image_or_workdir") + return md + + async def rollout( + self, + md: dict[str, Any], + *, + runtime, + session_id: str, + adapter_url: str, + agent_time_budget_sec: int, + eval_timeout_sec: int, + ) -> RewardResult: + workdir = md["workdir"] + async with ModalSandbox(md["image"]) as sb: + await self._prepare_workspace(sb, md) + await runtime.run_agent( + sb, + md=md, + session_id=session_id, + adapter_url=adapter_url, + time_budget_sec=agent_time_budget_sec, + ) + diff_text = await self._git_diff(sb, workdir, exclude=runtime.diff_exclude_all) + + # Work sandbox is closed; grade the diff in a clean one. + reward, is_solved, applied = await self._evaluate(md, diff_text, timeout_sec=eval_timeout_sec) + return RewardResult( + reward=float(reward), + is_solved=bool(is_solved), + extra={"applied_cleanly": bool(applied)}, + ) + + # ------------------------------------------------------------------ + # Workspace prep (work sandbox; task-side, agent-agnostic) + # ------------------------------------------------------------------ + async def _prepare_workspace(self, sb: Sandbox, md: dict[str, Any]) -> None: + """Bring a freshly booted work sandbox to the task's start state. + + ``pre_commands`` must run in BOTH the work and eval sandboxes (see + ``_apply_pre_commands``): they are typically ``git checkout + -f``, and skipping either side makes the model's diff + context mismatch the eval base -> apply failures. + """ + # git operations inside the sandbox (pre_commands, the later diff + # capture) need the repo marked safe for root. + await sb.exec("git config --system --add safe.directory '*'", check=False, timeout=60) + if md["pre_commands"]: + await _apply_pre_commands(sb, md["workdir"], md["pre_commands"]) + await self.write_problem_file(sb, md["workdir"], md["problem_statement"]) + + # ------------------------------------------------------------------ + # Diff capture + # ------------------------------------------------------------------ + async def _git_diff(self, sb: Sandbox, workdir: str, *, exclude: tuple[str, ...] = ()) -> str: + """Capture the model's edits as a patch, excluding scratch files. + + ``git add -N .`` stages intent-to-add for new files so they show up in + the diff. ``exclude`` is the active runtime's ``diff_exclude_all``; + the task-layer ``PROBLEM_FILE`` is always excluded here. + """ + excludes = " ".join(f"':(exclude){f}'" for f in (PROBLEM_FILE, *exclude)) + cmd = f"cd {shlex.quote(workdir)} && git add -N . && git diff -- . {excludes}" + _, out, _ = await sb.exec(cmd, user="root", timeout=120, check=False) + return out + + # ------------------------------------------------------------------ + # Eval (fresh clean sandbox, apply diff, run dataset tests) + # ------------------------------------------------------------------ + async def _evaluate(self, md: dict[str, Any], diff_text: str, *, timeout_sec: int) -> tuple[float, bool, bool]: + """Grade ``diff_text`` in a CLEAN sandbox; returns (reward, solved, applied).""" + if not (md["swepro"] or md["eval_cmd"]): + logger.warning("[swe_gym.evaluate] no swepro/eval_cmd; reward=0") + return 0.0, False, True + + workdir = md["workdir"] + async with ModalSandbox(md["image"]) as ev: + if md["pre_commands"]: + await _apply_pre_commands(ev, workdir, md["pre_commands"]) + + applied = await _apply_diff(ev, workdir, diff_text) + if not applied: + return 0.0, False, False + + if md["swepro"]: + reward, solved = await _run_swepro(ev, workdir, md["swepro"], timeout_sec) + return reward, solved, True + reward, solved = await _run_eval_cmd(ev, workdir, md["eval_cmd"], timeout_sec) + return reward, solved, True + + +async def _apply_pre_commands(sb: Sandbox, workdir: str, pre: list[str] | str) -> None: + body = pre.replace("\\n", "\n") if isinstance(pre, str) else "\n".join(c for c in (pre or []) if c) + await sb.write_file(_PRE, "set -e\n" + body) + await sb.exec(f"cd {shlex.quote(workdir)} && bash {shlex.quote(_PRE)}", check=False, timeout=600) + + +async def _apply_diff(sb: Sandbox, workdir: str, diff_text: str) -> bool: + if not diff_text.strip(): + return True + await sb.write_file(_PATCH, diff_text) + wq = shlex.quote(workdir) + pq = shlex.quote(_PATCH) + for cmd in ( + f"cd {wq} && git apply --3way --whitespace=nowarn {pq}", + f"cd {wq} && git apply --whitespace=nowarn {pq}", + f"cd {wq} && patch -p1 --no-backup-if-mismatch < {pq}", + ): + ec, _, _ = await sb.exec(cmd, check=False, timeout=120) + if ec == 0: + return True + return False + + +async def _run_eval_cmd(sb: Sandbox, workdir: str, cmd: str, timeout: int) -> tuple[float, bool]: + # What SWE-Gym-Lite emits: a self-contained command whose exit code is the + # verdict (applies the test_patch, runs pytest on F2P/P2P). + ec, _, _ = await sb.exec(f"cd {shlex.quote(workdir)} && {cmd}", check=False, timeout=timeout) + return (1.0 if ec == 0 else 0.0), ec == 0 + + +async def _run_swepro(sb: Sandbox, workdir: str, swepro: dict, timeout: int) -> tuple[float, bool]: + # Forward-compat pass-through for swepro-style grading (SWE-Gym-Lite data + # carries none, but a richer dataset can supply a run/parse script pair). + swepro_dir = "/tmp/swepro_eval" + await sb.exec(f"mkdir -p {swepro_dir} && chmod 777 {swepro_dir}", check=True, timeout=30) + for key, dst in (("run_script_path", "run_script.sh"), ("parser_script_path", "parser.py")): + host_path = swepro.get(key) + if host_path: + await sb.write_file(f"{swepro_dir}/{dst}", Path(host_path).read_text()) + await sb.exec(f"chmod -R 755 {swepro_dir}", check=False, timeout=30) + + test_arg = ",".join(swepro.get("selected_test_files") or []) + stdout_f = f"{swepro_dir}/stdout.log" + stderr_f = f"{swepro_dir}/stderr.log" + result_f = f"{swepro_dir}/result.json" + await sb.exec( + f"cd {shlex.quote(workdir)} && bash {swepro_dir}/run_script.sh " + f"{shlex.quote(test_arg)} > {stdout_f} 2> {stderr_f} || true", + check=False, + timeout=timeout, + ) + await sb.exec( + f"python3 {swepro_dir}/parser.py {stdout_f} {stderr_f} {result_f}", + check=False, + timeout=120, + ) + raw = await sb.read_file(result_f) + parsed = json.loads(raw) if raw else {"tests": []} + passed = {t["name"] for t in parsed.get("tests", []) if t.get("status") == "PASSED"} + required = set(swepro.get("fail_to_pass") or []) | set(swepro.get("pass_to_pass") or []) + solved = bool(required) and required.issubset(passed) + return (1.0 if solved else 0.0), solved diff --git a/async_rl_research/generate.py b/async_rl_research/generate.py index 7c21afba70..aba7e00172 100644 --- a/async_rl_research/generate.py +++ b/async_rl_research/generate.py @@ -4,87 +4,81 @@ --custom-generate-function-path async_rl_research.generate.generate -This is the **agent-agnostic** per-sample orchestrator. It owns the parts that -are identical for any in-sandbox agent and delegates the agent-specific and -sandbox-specific work to two collaborators: +This is the **agent- and task-agnostic** per-sample orchestrator. It owns the +parts that are identical for any in-sandbox agent on any task family, and +delegates everything else to two orthogonal collaborators: - generate.py (this file) the rollout recipe + adapter/HTTP lifecycle + + generate.py (this file) adapter/HTTP lifecycle + session management + trajectory merge + abort/timeout isolation. - agent/.py everything specific to one agent (which adapter, - how to launch it in the sandbox, its prompt / - env wiring). Default: agent/mini_swe_agent.py. - sandbox.py sandbox backend + SWE eval (boot / git_diff / - evaluate). NOT built yet -- contract below. + agent/base.py the AgentRuntime contract + shared launch and + provisioning machinery + the runtime registry + (load_runtime). One subclass per agent + framework (default: mini_swe_agent). + env/base.py the RolloutEnv contract + the env registry + (load_env). One subclass per task family -- + row schema, sandbox boot/prep, agent-leg + sequencing, grading. env/swe_gym.py grades a + captured git diff in a CLEAN sandbox; + env/harbor.py verifies harbor tasks in-place + (multi-step aware). Rows pick their env via + metadata.task_type (absent -> swe_gym). Topology (design A -- "in-sandbox subprocess + HTTP adapter"): host generate(): - 1. _State (once/worker): build the driver's adapter (an aiohttp app that + 1. _State (once/worker): build the runtime's adapter (an aiohttp app that speaks the agent's wire API and records exact SGLang tokens) and serve it on a bg thread; expose adapter_url = http://$SLIME_HEAD_HOST:$PORT. 2. open an adapter session keyed by session_id. - 3. boot a sandbox; the driver launches the agent inside it as a - subprocess. The agent dials BACK to adapter_url for every model call; - the adapter renders messages -> input_ids, calls SGLang /generate - (return_logprob), and records (prompt_ids, output_ids, logprobs). - 4. capture git diff; score it in a CLEAN sandbox (no test-cheating). - 5. finish_session() drains the recorded token segments; merge -> Sample. - -Reward is computed inline (sandbox.evaluate) and written onto the sample, so + 3. env.rollout(): boot the task sandbox, prep the workspace, let the + runtime launch the agent inside it for each task step. The agent + dials BACK to adapter_url for every model call; the adapter renders + messages -> input_ids, calls SGLang /generate (return_logprob), and + records (prompt_ids, output_ids, logprobs). The env grades the + result into a RewardResult. + 4. finish_session() drains the recorded token segments; merge -> Sample. + +Reward is computed inline (env.rollout) and written onto the sample, so slime's default reward-model step is skipped (generate_and_rm only calls async_rm when sample.reward is None). ----------------------------------------------------------------------------- -Driver contract (a driver is a *module*; default async_rl_research.agent.mini_swe_agent) ----------------------------------------------------------------------------- - ADAPTER_CLS : type[BaseAdapter] - The slime adapter class for this agent's wire protocol - (OpenAIAdapter for mini-swe-agent / litellm, AnthropicAdapter for - claude-code). Constructed as - ADAPTER_CLS(tokenizer=, sglang_url=, tool_parser=, reasoning_parser=). - - async def run_agent(sb, *, md, session_id, adapter_url, time_budget_sec) -> int - Provision + launch the agent inside the already-booted sandbox `sb`, - wait for it to finish, return an exit code. The agent must send - `session_id` as its auth/bearer so the adapter groups its turns, and - must target `adapter_url` for model calls. `md` is the normalized - dataset row (see _metadata). - ----------------------------------------------------------------------------- -sandbox.py contract (async_rl_research.sandbox -- NOT built yet) ----------------------------------------------------------------------------- - @asynccontextmanager - async def boot_agent_sandbox(image: str) -> AsyncIterator[Sandbox]: ... - - async def git_diff(sb, workdir: str) -> str: ... - - async def evaluate(*, image, workdir, diff_text, swepro=None, eval_cmd=None, - pre_commands=None, timeout_sec=600) -> tuple[float, bool, bool]: - # (reward, solved, applied_cleanly); applies diff in a CLEAN sandbox. +The full contracts live on ``agent.base.AgentRuntime`` and +``env.base.RolloutEnv`` -- single sources of truth. This module only relies +on: ``runtime.adapter_cls`` (constructed as adapter_cls(tokenizer=, +sglang_url=, tool_parser=, reasoning_parser=)), ``env.normalize_metadata``, +and ``env.rollout``. Env knobs --------- - SLIME_HEAD_HOST public IP sandboxes use to reach the adapter (REQUIRED) + SLIME_HEAD_HOST public IP sandboxes use to reach the adapter + (REQUIRED unless MODAL_EXPOSE_ADAPTER=1) + MODAL_EXPOSE_ADAPTER 1 to publish the adapter through a modal.forward + tunnel (required on a Modal cluster: sandboxes are + network-isolated and can only dial a public URL) SHIM_BIND_HOST 0.0.0.0 adapter bind host on the head node SHIM_PORT 18002 adapter bind port - ASYNC_RL_AGENT_DRIVER dotted module path of the driver - (default async_rl_research.agent.mini_swe_agent) - AGENT_TIME_BUDGET_SEC 1800 wallclock budget for one agent run - AGENT_EVAL_TIMEOUT_SEC 600 wallclock cap on the evaluator sandbox + ASYNC_RL_AGENT_RUNTIME which agent runtime to use: a registry short name + ("mini-swe"), "module:Class", or a module path + exposing RUNTIME (default "mini-swe"; see + agent.base.load_runtime) + ASYNC_RL_AGENT_DRIVER legacy alias for ASYNC_RL_AGENT_RUNTIME + ASYNC_RL_TASK_ROOT root dir that relative metadata.task_path values + resolve against (harbor env; the converter's + --out-dir on the slime-data volume) + AGENT_TIME_BUDGET_SEC 1800 total agent wallclock budget per sample + (multi-step episodes share it) + AGENT_EVAL_TIMEOUT_SEC 600 wallclock cap per grading command AGENT_GENERATE_GUARD_SEC full generate() guard; default budget+eval+180 """ from __future__ import annotations import asyncio -import base64 -import importlib import logging import os import secrets import time import traceback -from dataclasses import dataclass from typing import Any from slime.agent.trajectory import TokenSegment, fan_out_sample_segments @@ -92,13 +86,13 @@ async def evaluate(*, image, workdir, diff_text, swepro=None, eval_cmd=None, from slime.utils.processing_utils import load_tokenizer from slime.utils.types import Sample +from .agent.base import AgentRuntime, load_runtime from .aiohttp_threaded import run_app_in_thread +from .env.base import EnvMetadataError, RewardResult, load_env logger = logging.getLogger(__name__) -DEFAULT_DRIVER = "async_rl_research.agent.mini_swe_agent" - AGENT_TIME_BUDGET_SEC = int(os.environ.get("AGENT_TIME_BUDGET_SEC", "1800")) AGENT_EVAL_TIMEOUT_SEC = int(os.environ.get("AGENT_EVAL_TIMEOUT_SEC", "600")) # Wall-clock guard for the entire generate() call. When exceeded, the in-flight @@ -109,27 +103,37 @@ async def evaluate(*, image, workdir, diff_text, swepro=None, eval_cmd=None, ) SHIM_BIND_HOST = os.environ.get("SHIM_BIND_HOST", "0.0.0.0") SHIM_PORT = int(os.environ.get("SHIM_PORT", "18002")) +# On a Modal cluster the head is itself a Modal container and the sandboxes are +# network-isolated (no i6pn/cluster routing), so they can only reach the +# adapter via a public modal.forward tunnel rather than a private cluster IP. +MODAL_EXPOSE_ADAPTER = os.environ.get("MODAL_EXPOSE_ADAPTER", "0").strip().lower() in ("1", "true", "yes") + +def _load_runtime(args) -> AgentRuntime: + """Resolve + instantiate the agent runtime (env > arg > registry default). -def _load_driver(args): - """Resolve the agent driver *module* (env > arg > default).""" - path = ( - os.environ.get("ASYNC_RL_AGENT_DRIVER") + Validation is eager (load_runtime / AgentRuntime.__init_subclass__), so a + misdeclared runtime fails the worker boot loudly instead of + AttributeError-ing mid-sample. + """ + spec = ( + os.environ.get("ASYNC_RL_AGENT_RUNTIME") + or os.environ.get("ASYNC_RL_AGENT_DRIVER") # legacy alias + or getattr(args, "agent_runtime", None) or getattr(args, "agent_driver", None) - or DEFAULT_DRIVER ) - return importlib.import_module(path) + return load_runtime(spec) # --------------------------------------------------------------------------- -# Singleton: tokenizer + driver-selected adapter + background HTTP server. -# SingletonMeta keys per class, so there is exactly one adapter + server per -# rollout worker process; trajectories stay isolated by session_id. +# Singleton: tokenizer + runtime-selected adapter + background HTTP server. +# SingletonMeta keys per class, so there is exactly one runtime + adapter + +# server per rollout worker process; trajectories stay isolated by session_id. # --------------------------------------------------------------------------- class _State(metaclass=SingletonMeta): def __init__(self, args) -> None: self.args = args - self.driver = _load_driver(args) + self.runtime = _load_runtime(args) self.tokenizer = load_tokenizer(args.hf_checkpoint, trust_remote_code=True) self.max_context_len = int(getattr(args, "rollout_max_context_len", 0) or 0) # Adapter reuses the SGLang parsers configured for the served model so @@ -140,15 +144,17 @@ def __init__(self, args) -> None: sglang_url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}" public_host = os.environ.get("SLIME_HEAD_HOST") - if not public_host: + if not public_host and not MODAL_EXPOSE_ADAPTER: raise RuntimeError( "SLIME_HEAD_HOST is not set. Export it to the host IP that " - "sandboxes can reach for the reverse-connection to the adapter. " - "Without it the in-sandbox agent cannot dial back and the " - "rollout will silently abort." + "sandboxes can reach for the reverse-connection to the adapter, " + "or set MODAL_EXPOSE_ADAPTER=1 to publish the adapter through a " + "modal.forward tunnel (required on a Modal cluster). Without " + "either the in-sandbox agent cannot dial back and the rollout " + "will silently abort." ) - self.adapter = self.driver.ADAPTER_CLS( + self.adapter = self.runtime.adapter_cls( tokenizer=self.tokenizer, sglang_url=sglang_url, tool_parser=self.tool_parser, @@ -165,28 +171,55 @@ def __init__(self, args) -> None: thread_name="agent-adapter", runner_kwargs={"handler_cancellation": True}, ) - # Base URL (no /v1). The driver appends whatever its wire API needs. - self.adapter_url = f"http://{public_host}:{self.app_handle.port}" + # Everything past the bind can still fail (e.g. modal.forward). The + # server thread is a daemon that holds SHIM_PORT for the process + # lifetime, and SingletonMeta only caches on a clean __init__, so a + # failure here would orphan the thread and make every later _State() + # collide on the port. Tear the server down on any failure so the bind + # is releasable and the next sample can retry. + try: + # Base URL (no /v1). The runtime appends whatever its wire API needs. + self._tunnel_cm = None + self.adapter_url = self._resolve_adapter_url(public_host) + except BaseException: + self.app_handle.stop() + raise logger.info( - "[async_rl] driver=%s adapter=%s tokenizer=%s tool_parser=%s reasoning_parser=%s", - self.driver.__name__, + "[async_rl] runtime=%s adapter=%s tokenizer=%s tool_parser=%s reasoning_parser=%s", + self.runtime.name, self.adapter_url, args.hf_checkpoint, self.tool_parser, self.reasoning_parser, ) + def _resolve_adapter_url(self, public_host: str | None) -> str: + """Pick the URL the in-sandbox agent dials back on. + + On a Modal cluster the adapter must be reached through a per-process + ``modal.forward`` tunnel (the head is a Modal container; sandboxes are + network-isolated). Each rollout-worker process is its own ``_State`` + singleton, so it opens its OWN tunnel -- a single static env can't cover + multiple data-parallel workers. The tunnel context is held on ``self`` + for the process lifetime; litellm speaks HTTPS, so the encrypted + ``https://.modal.host`` URL needs no port juggling. + """ + if not MODAL_EXPOSE_ADAPTER: + return f"http://{public_host}:{self.app_handle.port}" + + import modal + + # Blocking context manager (we're in sync __init__); never exited -- + # the process owns the tunnel until it dies. + self._tunnel_cm = modal.forward(self.app_handle.port) + tunnel = self._tunnel_cm.__enter__() + logger.info("[async_rl] modal.forward tunnel for adapter port %d -> %s", self.app_handle.port, tunnel.url) + return tunnel.url + # --------------------------------------------------------------------------- # Trajectory -> Sample # --------------------------------------------------------------------------- -@dataclass(frozen=True) -class RewardResult: - reward: float - is_solved: bool - applied_cleanly: bool - - def _start_session(state: _State, sample: Sample, md: dict[str, Any]) -> str: """Register the adapter session BEFORE the agent starts. @@ -200,8 +233,10 @@ def _start_session(state: _State, sample: Sample, md: dict[str, Any]) -> str: else: session_id = f"agent-{md['instance_id']}-{secrets.token_hex(8)}" sample.session_id = session_id - # sampling_defaults win over anything the agent sends, keeping the rollout - # on-policy (the adapter merges request body OVER these defaults). + # sampling_defaults are the rollout's baseline, but the adapter applies the + # request body OVER them (adapters/common._sampling_params): an agent that + # sends its own temperature/top_p would silently override training's. + # Runtimes must strip sampling knobs from agent requests to stay on-policy. state.adapter.open_session( session_id, sampling_defaults=_sampling_params(state.args), @@ -250,8 +285,10 @@ def _merge_samples( **(sample.metadata or {}), "instance_id": instance_id, "is_solved": reward_result.is_solved, - "applied_cleanly": reward_result.applied_cleanly, "elapsed_sec": elapsed_sec, + # Env-specific diagnostics (swe_gym: applied_cleanly; harbor: per-step + # rewards). Keys are env-namespaced or unambiguous by construction. + **reward_result.extra, } fanned = fan_out_sample_segments( sample, segments, reward_result.reward, state.tokenizer, metadata=trajectory_metadata @@ -259,13 +296,13 @@ def _merge_samples( if not fanned: raise ValueError("fan-out produced no samples") logger.info( - "[async_rl] %s: reward=%.2f solved=%s applied=%s elapsed=%.1fs segments=%d", + "[async_rl] %s: reward=%.2f solved=%s elapsed=%.1fs segments=%d extra=%s", instance_id, reward_result.reward, reward_result.is_solved, - reward_result.applied_cleanly, elapsed_sec, len(fanned), + {k: v for k, v in reward_result.extra.items() if not isinstance(v, (list, dict))}, ) return fanned @@ -277,45 +314,34 @@ async def generate(args, sample: Sample, sampling_params: dict[str, Any], evalua """Per-sample agent rollout with a wall-clock guard. Accepts ``evaluation`` (slime passes it when present in the signature) but - treats train and eval identically -- running the agent + grading its diff - is what eval wants too. + treats train and eval identically -- running the agent + grading the + result is what eval wants too. """ - # `sandbox` is intentionally lazy-imported: it is not built yet (its - # contract is documented above). Everything else in this module imports and - # runs without it. - from . import sandbox - state = _State(args) - md = _metadata(sample) - if not md["image"] or not md["workdir"]: - return _abort_result(sample, "missing_image_or_workdir") + # Row -> env dispatch. load_env imports the env module lazily, so this + # module stays importable on nodes that never boot a sandbox. A bad row + # (unknown task_type, unusable metadata) aborts THAT sample; systemic + # failures (an env module that doesn't import) still raise loudly. + try: + env = load_env((sample.metadata or {}).get("task_type")) + md = env.normalize_metadata(sample) + except EnvMetadataError as e: + return _abort_result(sample, str(e)) + except (ValueError, TypeError) as e: + return _abort_result(sample, f"env_dispatch_failed:{type(e).__name__}:{e}") instance_id = md["instance_id"] session_id = _start_session(state, sample, md) t0 = time.time() try: async with asyncio.timeout(AGENT_GENERATE_GUARD_SEC): - async with sandbox.boot_agent_sandbox(md["image"]) as sb: - await state.driver.run_agent( - sb, - md=md, - session_id=session_id, - adapter_url=state.adapter_url, - time_budget_sec=AGENT_TIME_BUDGET_SEC, - ) - diff_text = await sandbox.git_diff(sb, md["workdir"]) - - reward, is_solved, applied_cleanly = await sandbox.evaluate( - image=md["image"], - workdir=md["workdir"], - diff_text=diff_text, - swepro=md["swepro"], - eval_cmd=md["eval_cmd"], - pre_commands=md["pre_commands"], - timeout_sec=AGENT_EVAL_TIMEOUT_SEC, - ) - reward_result = RewardResult( - reward=float(reward), is_solved=bool(is_solved), applied_cleanly=bool(applied_cleanly) + reward_result: RewardResult = await env.rollout( + md, + runtime=state.runtime, + session_id=session_id, + adapter_url=state.adapter_url, + agent_time_budget_sec=AGENT_TIME_BUDGET_SEC, + eval_timeout_sec=AGENT_EVAL_TIMEOUT_SEC, ) segments = await state.adapter.finish_session(session_id) return _merge_samples( @@ -359,52 +385,22 @@ def _log_timeout_diagnostic(t0: float) -> None: pass -# --------------------------------------------------------------------------- -# Dataset-row normalization (agent-agnostic; SWE schema shared with the example) -# --------------------------------------------------------------------------- -def _wrap_f2p_script(script: str | None) -> str | None: - if not script: - return None - b64 = base64.b64encode(script.encode("utf-8")).decode("ascii") - return f"echo {b64} | base64 -d > /tmp/slime_f2p.py && python /tmp/slime_f2p.py" - - -def _metadata(sample: Sample) -> dict[str, Any]: - """Normalize the two dataset schemas (flat vs ``remote_env_info``).""" - m = sample.metadata or {} - rem = m.get("remote_env_info") or {} - label = sample.label if (isinstance(sample.label, str) and len(sample.label) < 256) else None - return { - "instance_id": m.get("instance_id") or rem.get("instance_id") or label or "unknown", - "image": m.get("image") or rem.get("image_url"), - "workdir": m.get("workdir") or rem.get("workdir"), - "problem_statement": m.get("problem_statement") or _coerce_prompt(sample.prompt), - "swepro": m.get("swepro"), - "eval_cmd": m.get("eval_cmd") or _wrap_f2p_script(rem.get("f2p_script")), - "pre_commands": m.get("pre_commands") or rem.get("pre_commands"), - } - - -def _coerce_prompt(prompt) -> str: - if isinstance(prompt, str): - return prompt - if isinstance(prompt, list): - for m in prompt: - if isinstance(m, dict) and m.get("role") == "user": - c = m.get("content") - if isinstance(c, str): - return c - if isinstance(c, list): - return "\n".join(p.get("text", "") for p in c if isinstance(p, dict) and p.get("type") == "text") - return "" - - def _abort(sample: Sample, reason: str) -> Sample: sample.tokens = [0, 0] sample.response = "" sample.response_length = 1 sample.loss_mask = [0] + # Per-token fields must stay shape-consistent with response_length: when + # any sample in the batch carries rollout_log_probs, the train actor + # slices it for EVERY sample (actor._get_rollout_data), and a None here + # crashes the whole train step. loss_mask is 0 so the value never trains. + sample.rollout_log_probs = [0.0] sample.reward = 0.0 + # Mirror fan_out_sample_segments' convention (rollout_id = sample.index): + # build_dp_schedule groups samples by rollout_id, so a None here collapses + # every aborted sample in the batch into ONE rollout group and the unique + # rollout count drops below global_batch_size, crashing the train step. + sample.rollout_id = sample.index sample.status = Sample.Status.ABORTED sample.metadata = {**(sample.metadata or {}), "abort_reason": reason} logger.warning("[async_rl] aborted: %s", reason) diff --git a/async_rl_research/modal_sandbox.py b/async_rl_research/modal_sandbox.py new file mode 100644 index 0000000000..add7966e70 --- /dev/null +++ b/async_rl_research/modal_sandbox.py @@ -0,0 +1,469 @@ +"""Modal sandbox backend for agent rollouts. + +``ModalSandbox`` is the local analog of ``slime.agent.sandbox.E2BSandbox``: a +drop-in implementation of the ``Sandbox`` protocol (``sandbox_id``, +``__aenter__``/``__aexit__``, ``exec``, ``write_file``, ``read_file``) backed by +``modal.Sandbox``. It is pure infrastructure -- it knows nothing about tasks, +agents, or grading -- so the env glue (``env/swe_gym.py``, ``env/harbor.py``) +and any agent runtime can build on top of it. The image is either a registry +ref (``str``) or a host-side Dockerfile build (``DockerfileImage``). + +``modal`` is imported lazily (inside ``__aenter__``) so this module stays +importable without Modal installed, mirroring the E2B backend's lazy +``e2b`` import. That keeps importing the env modules cheap on nodes that +never actually boot a sandbox. + +Boot concurrency and create-retry live here (not in the SWE glue) so that +*every* sandbox creation -- the work sandbox and the clean evaluator sandbox +alike -- is gated and retried uniformly. + +Env knobs +--------- + MODAL_BOOT_CONCURRENCY max concurrent sandbox creates (default 8) + MODAL_BOOT_RETRIES transient-create retries (default 2) + MODAL_RPC_RETRIES transient-exec retries (default 2) + SLIME_AGENT_SANDBOX_LIFETIME_SEC sandbox max lifetime (default 3600) + (legacy alias: MODAL_SANDBOX_LIFETIME_SEC) + SLIME_AGENT_SANDBOX_MODAL_APP Modal app name (default slime-agent-sandboxes) + (legacy alias: MODAL_SANDBOX_APP_NAME) + SLIME_AGENT_SANDBOX_BLOCK_NETWORK 1 to cut sandbox outbound network + (legacy alias: MODAL_SANDBOX_BLOCK_NETWORK) + SLIME_AGENT_SANDBOX_CPU fractional cpu cores (optional) + SLIME_AGENT_SANDBOX_MEMORY_MB memory in MB (optional) + SLIME_AGENT_SANDBOX_GPU gpu spec, e.g. "a10g" (optional) + SLIME_AGENT_SANDBOX_ADD_PYTHON add a python to the image (optional) + MODAL_REGISTRY_SECRET modal.Secret name for a private registry/ECR + MODAL_ENVIRONMENT modal environment name (optional) +""" + +from __future__ import annotations + +import asyncio +import codecs +import logging +import os +import shlex +import uuid +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + + +ExecResult = tuple[int, str, str] +FileContent = str | bytes | Path + + +@dataclass(frozen=True) +class DockerfileImage: + """Build-from-Dockerfile image spec (harbor-style task environments). + + ``path`` is the host-side Dockerfile, ``context_dir`` the build context + (defaults to the Dockerfile's directory). Modal content-hashes the + Dockerfile commands + context files, so identical task files are a cache + hit across rollout boots -- the same property the + registry path gets from immutable tags. Registry auth (the FROM pull) is + Modal's builder default; private base images are not supported here. + """ + + path: str + context_dir: str | None = None + + @property + def description(self) -> str: + return f"dockerfile:{self.path}" + +# Modal validates the total argv size of an exec against ARG_MAX (64 KiB) and +# raises InvalidError client-side. Dataset eval commands can blow well past it +# (SWE-Gym eval_cmds inline the entire test_patch as a heredoc), so commands +# above this threshold are shipped into the sandbox as a script file and run +# from there. Kept well under the limit to leave room for the bash/runuser +# wrapper argv. +_EXEC_ARGV_LIMIT_BYTES = 32_768 + + +def _normalize_image_ref(ref: str) -> str: + """Lowercase the repository name of a registry ref; preserve tag/digest. + + OCI/Docker repository names must be lowercase (``skopeo`` rejects mixed + case with "invalid reference format: repository name must be lowercase"). + Some SWE-bench task images carry mixed-case orgs/repos in the dataset + (e.g. ``Project-MONAI_s_MONAI-3326``) while the actual Docker Hub repo is + lowercase, so normalize here -- on the shared boot path -- so the rollout + builds the exact same (valid) ref. + + The tag (``:latest``) and digest (``@sha256:...``) are case-sensitive and + left untouched; only the name (host + path) is lowercased. The host is + already lowercase in practice and any ``:port`` is digits, so lowercasing + the whole name portion is safe. + """ + if not ref: + return ref + name, sep, suffix = ref, "", "" + if "@" in ref: # digest pin: name@sha256:... + name, _, digest = ref.partition("@") + sep, suffix = "@", digest + else: + slash = ref.rfind("/") + colon = ref.rfind(":") + if colon > slash: # a tag colon (not a registry :port, which precedes the last '/') + name, sep, suffix = ref[:colon], ":", ref[colon + 1 :] + return name.lower() + sep + suffix + + +def _getenv(*names: str, default: str = "") -> str: + for name in names: + value = os.environ.get(name) + if value is not None and value.strip(): + return value + return default + + +def _getenv_int(*names: str, default: int) -> int: + raw = _getenv(*names) + return int(raw) if raw else default + + +# Process-wide create gate + cached App. Created lazily on the running loop so +# importing this module never touches asyncio or modal. +_BOOT_SEM: asyncio.Semaphore | None = None +_APP_CACHE: dict[str, Any] = {} +_APP_LOCK: asyncio.Lock | None = None + + +def _boot_sem() -> asyncio.Semaphore: + global _BOOT_SEM + if _BOOT_SEM is None: + _BOOT_SEM = asyncio.Semaphore(_getenv_int("MODAL_BOOT_CONCURRENCY", default=8)) + return _BOOT_SEM + + +def _app_lock() -> asyncio.Lock: + global _APP_LOCK + if _APP_LOCK is None: + _APP_LOCK = asyncio.Lock() + return _APP_LOCK + + +class ModalSandbox: + """Async context manager around ``modal.Sandbox`` (the ``Sandbox`` protocol). + + Normal command failures surface as exit codes. Modal transport errors are + retried while transient and otherwise raised, so an infrastructure problem + is never silently scored as a failed SWE test. + """ + + default_lifetime_sec = 3600 + default_app_name = "slime-agent-sandboxes" + default_create_retries = 2 + default_rpc_retries = 2 + rpc_backoff_base_sec = 1.0 + # Per-stream output cap so a runaway command can't balloon host memory. + output_cap_chars = _getenv_int("MODAL_EXEC_OUTPUT_CAP", default=1_000_000) + + def __init__( + self, + image: str | DockerfileImage, + *, + timeout: int | None = None, + block_network: bool | None = None, + cpu: float | None = None, + memory_mb: int | None = None, + gpu: str | None = None, + registry_secret: str | None = None, + rpc_retries: int | None = None, + create_retries: int | None = None, + app_name: str | None = None, + add_python: str | None = None, + workdir: str | None = None, + ) -> None: + if isinstance(image, DockerfileImage): + self.image_spec: DockerfileImage | None = image + self.image = image.description # log/cache-key label only + else: + self.image_spec = None + self.image = _normalize_image_ref(image) + self.timeout = timeout if timeout is not None else self._lifetime_from_env() + self.block_network = block_network if block_network is not None else self._block_network_from_env() + self.cpu = cpu if cpu is not None else self._float_from_env("SLIME_AGENT_SANDBOX_CPU", "MODAL_SANDBOX_CPU") + self.memory_mb = ( + memory_mb + if memory_mb is not None + else self._int_from_env("SLIME_AGENT_SANDBOX_MEMORY_MB", "MODAL_SANDBOX_MEMORY_MB") + ) + self.gpu = gpu or (_getenv("SLIME_AGENT_SANDBOX_GPU", "MODAL_SANDBOX_GPU") or None) + self.registry_secret = registry_secret or (_getenv("MODAL_REGISTRY_SECRET") or None) + self.rpc_retries = rpc_retries if rpc_retries is not None else _getenv_int( + "MODAL_RPC_RETRIES", "SLIME_AGENT_SANDBOX_RPC_RETRIES", default=self.default_rpc_retries + ) + self.create_retries = create_retries if create_retries is not None else _getenv_int( + "MODAL_BOOT_RETRIES", default=self.default_create_retries + ) + self.app_name = app_name or _getenv( + "SLIME_AGENT_SANDBOX_MODAL_APP", "MODAL_SANDBOX_APP_NAME", default=self.default_app_name + ) + self.add_python = add_python or (_getenv("SLIME_AGENT_SANDBOX_ADD_PYTHON", "MODAL_SANDBOX_ADD_PYTHON") or None) + self.workdir = workdir or (_getenv("SLIME_AGENT_SANDBOX_WORKDIR", "MODAL_SANDBOX_WORKDIR") or None) + self._modal = None + self._sb = None + self.sandbox_id = "" + + # -- env helpers -------------------------------------------------------- + @classmethod + def _lifetime_from_env(cls) -> int: + return _getenv_int( + "SLIME_AGENT_SANDBOX_LIFETIME_SEC", "MODAL_SANDBOX_LIFETIME_SEC", default=cls.default_lifetime_sec + ) + + @staticmethod + def _block_network_from_env() -> bool: + return _getenv("SLIME_AGENT_SANDBOX_BLOCK_NETWORK", "MODAL_SANDBOX_BLOCK_NETWORK").strip().lower() in ( + "1", + "true", + "yes", + ) + + @staticmethod + def _float_from_env(*names: str) -> float | None: + raw = _getenv(*names) + return float(raw) if raw else None + + @staticmethod + def _int_from_env(*names: str) -> int | None: + raw = _getenv(*names) + return int(raw) if raw else None + + # -- transient-error classification ------------------------------------ + @staticmethod + def _is_transient(e: BaseException) -> bool: + """True if ``e`` is a Modal transport flap that is safe to retry. + + Command-level timeouts (the command itself ran too long) are NOT + transient -- retrying would just burn the budget again. + """ + name = type(e).__name__ + if "SandboxTimeout" in name or name == "TimeoutError": + return False + if name in { + "ConnectionError", + "ConnectionResetError", + "ConnectionAbortedError", + "GRPCError", + "StreamTerminatedError", + "InternalError", + "ServerError", + "RemoteError", + }: + return True + msg = str(e).lower() + return any(s in msg for s in ("connection", "unavailable", "stream terminated", "goaway", "reset")) + + async def _retry(self, op_name: str, attempts: int, coro_factory): + last_err: BaseException | None = None + for attempt in range(max(1, attempts)): + try: + return await coro_factory() + except Exception as e: + if not self._is_transient(e) or attempt + 1 >= max(1, attempts): + raise + last_err = e + backoff = self.rpc_backoff_base_sec * (2**attempt) + logger.debug( + "[modal_sandbox] %s transient %s, retry %d/%d in %.1fs: %s", + op_name, + type(e).__name__, + attempt + 1, + attempts, + backoff, + str(e)[:160], + ) + await asyncio.sleep(backoff) + assert last_err is not None + raise last_err + + # -- lifecycle ---------------------------------------------------------- + async def _get_app(self): + environment_name = _getenv("MODAL_ENVIRONMENT") or None + key = f"{self.app_name}\0{environment_name or ''}" + async with _app_lock(): + app = _APP_CACHE.get(key) + if app is None: + kwargs: dict[str, Any] = {"create_if_missing": True} + if environment_name: + kwargs["environment_name"] = environment_name + app = await self._modal.App.lookup.aio(self.app_name, **kwargs) + _APP_CACHE[key] = app + return app + + def _build_image(self): + modal = self._modal + kwargs: dict[str, Any] = {} + if self.add_python: + kwargs["add_python"] = self.add_python + if self.image_spec is not None: + spec = self.image_spec + context_dir = spec.context_dir or str(Path(spec.path).parent) + return modal.Image.from_dockerfile(spec.path, context_dir=context_dir, **kwargs) + secret = None + if self.registry_secret: + secret = modal.Secret.from_name(self.registry_secret) + if ".dkr.ecr." in self.image and secret is not None: + return modal.Image.from_aws_ecr(self.image, secret=secret, **kwargs) + return modal.Image.from_registry(self.image, secret=secret, **kwargs) + + async def __aenter__(self) -> "ModalSandbox": + import modal # lazy: keep this module importable without modal installed + + self._modal = modal + app = await self._get_app() + image = self._build_image() + + create_kwargs: dict[str, Any] = { + "app": app, + "image": image, + "timeout": self.timeout, + "block_network": self.block_network, + } + if self.cpu is not None: + create_kwargs["cpu"] = self.cpu + if self.memory_mb is not None: + create_kwargs["memory"] = self.memory_mb + if self.gpu: + create_kwargs["gpu"] = self.gpu + if self.workdir: + create_kwargs["workdir"] = self.workdir + + async def _create(): + async with _boot_sem(): + return await modal.Sandbox.create.aio(**create_kwargs) + + self._sb = await self._retry(f"create({self.image[:48]!r})", self.create_retries, _create) + self.sandbox_id = str(getattr(self._sb, "object_id", "") or "") + return self + + async def __aexit__(self, exc_type, exc, tb) -> None: + sb = self._sb + if sb is None: + return + try: + await sb.terminate.aio() + except Exception as e: + logger.warning("[modal_sandbox] terminate %s failed: %s", self.sandbox_id[:8], e) + try: + await sb.wait.aio(raise_on_termination=False) + except Exception: + pass + + # -- protocol surface --------------------------------------------------- + def _require_sandbox(self): + if self._sb is None: + raise RuntimeError("ModalSandbox has not been entered") + return self._sb + + async def exec( + self, + cmd: str, + *, + user: str = "root", + env: dict[str, str] | None = None, + timeout: int = 120, + check: bool = False, + ) -> ExecResult: + sb = self._require_sandbox() + # Honor user= so the backend is drop-in for agents that drop privileges + # (mini-swe-agent only ever runs as root). + if user and user != "root": + inner = f"runuser -u {shlex.quote(user)} -- bash -lc {shlex.quote(cmd)}" + else: + inner = cmd + if len(inner.encode("utf-8", errors="ignore")) > _EXEC_ARGV_LIMIT_BYTES: + # Too big for exec argv: stage the command as a script and run that. + # The script is left behind (no rm) so _retry can safely re-run the + # exec; sandboxes are ephemeral, so /tmp leftovers cost nothing. + script = f"/tmp/.modal_exec_{uuid.uuid4().hex}.sh" + await self.write_file(script, inner) + inner = f"bash {shlex.quote(script)}" + secrets = [self._modal.Secret.from_dict({str(k): str(v) for k, v in env.items()})] if env else [] + + async def _run() -> ExecResult: + # text=False: Modal's text mode decodes strictly inside the client, + # so one non-UTF8 byte in command output (a `git diff` over latin-1 + # sources, a `tail -c` that splits a multibyte char) raises + # UnicodeDecodeError mid-stream and kills the rollout. Take bytes + # and decode host-side with errors="replace" instead. + proc = await sb.exec.aio("bash", "-lc", inner, timeout=timeout, secrets=secrets, text=False) + # Start draining both streams BEFORE awaiting wait(): a full pipe + # buffer would otherwise block the command and deadlock wait(). + out_task = asyncio.create_task(_read_stream_capped(proc.stdout, self.output_cap_chars)) + err_task = asyncio.create_task(_read_stream_capped(proc.stderr, self.output_cap_chars)) + exit_code = int(await proc.wait.aio()) + stdout, stderr = await asyncio.gather(out_task, err_task) + return exit_code, stdout, stderr + + exit_code, stdout, stderr = await self._retry(f"exec({cmd[:48]!r})", self.rpc_retries, _run) + if check and exit_code != 0: + raise RuntimeError(f"modal exec failed (exit={exit_code}): {cmd[:120]}\n{stderr[:400]}") + return exit_code, stdout, stderr + + async def write_file(self, sandbox_path: str, content: FileContent, *, user: str = "root") -> None: + sb = self._require_sandbox() + fs = sb.filesystem + + async def _write(): + if isinstance(content, Path): + await fs.copy_from_local.aio(str(content), sandbox_path) + elif isinstance(content, bytes): + await fs.write_bytes.aio(content, sandbox_path) + else: + await fs.write_text.aio(str(content), sandbox_path) + + await self._retry(f"write_file({sandbox_path})", self.rpc_retries, _write) + if user and user != "root": + quoted = shlex.quote(user) + await self.exec(f"chown {quoted}:{quoted} {shlex.quote(sandbox_path)}", timeout=30, check=False) + + async def read_file(self, sandbox_path: str, *, user: str = "root") -> str: + del user # Modal file APIs read as the sandbox owner; user is advisory. + sb = self._require_sandbox() + try: + return await self._retry( + f"read_file({sandbox_path})", + self.rpc_retries, + lambda: sb.filesystem.read_text.aio(sandbox_path), + ) + except Exception: + return "" + + +async def _read_stream_capped(stream: Any, cap: int) -> str: + """Drain ``stream`` fully but keep at most ``cap`` chars. + + The whole stream is consumed (so the process can finish) while only the + first ``cap`` characters are retained; the tail is dropped with a marker. + Byte chunks (``exec`` runs with ``text=False``) are decoded incrementally + so a multibyte char split across chunks doesn't turn into mojibake, with + ``errors="replace"`` so genuinely non-UTF8 output can never raise. + """ + if stream is None: + return "" + decoder = codecs.getincrementaldecoder("utf-8")(errors="replace") + parts: list[str] = [] + total = 0 + truncated = False + async for chunk in stream: + if chunk is None: + continue + text = decoder.decode(chunk) if isinstance(chunk, (bytes, bytearray)) else chunk + if total < cap: + take = text[: cap - total] + parts.append(take) + total += len(take) + if total >= cap: + truncated = True + else: + truncated = True + out = "".join(parts) + if truncated: + out += "\n...[truncated]" + return out diff --git a/async_rl_research/sandbox.py b/async_rl_research/sandbox.py deleted file mode 100644 index e69de29bb2..0000000000 From 4df27cf62954614063fbc0c8b0daf617784625e8 Mon Sep 17 00:00:00 2001 From: junlin-star Date: Fri, 12 Jun 2026 11:24:47 -0700 Subject: [PATCH 02/11] Rename env package to environment async_rl_research.env shadowed the common .env/env-var association and collided with the repo .gitignore's env/ habits; rename the package to async_rl_research.environment and update every module path reference (ENVS registry strings, imports, oracle-check CLI invocations, docs). Co-Authored-By: Claude Fable 5 --- async_rl_research/README.md | 2 +- async_rl_research/agent/mini_swe_agent.py | 4 ++-- async_rl_research/{env => environment}/__init__.py | 0 async_rl_research/{env => environment}/base.py | 4 ++-- .../{env => environment}/convert2slime/__init__.py | 0 .../{env => environment}/convert2slime/harbor.py | 6 +++--- .../{env => environment}/convert2slime/swe_gym.py | 0 async_rl_research/{env => environment}/harbor.py | 2 +- async_rl_research/{env => environment}/swe_gym.py | 0 async_rl_research/generate.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) rename async_rl_research/{env => environment}/__init__.py (100%) rename async_rl_research/{env => environment}/base.py (98%) rename async_rl_research/{env => environment}/convert2slime/__init__.py (100%) rename async_rl_research/{env => environment}/convert2slime/harbor.py (98%) rename async_rl_research/{env => environment}/convert2slime/swe_gym.py (100%) rename async_rl_research/{env => environment}/harbor.py (99%) rename async_rl_research/{env => environment}/swe_gym.py (100%) diff --git a/async_rl_research/README.md b/async_rl_research/README.md index d86a13d6de..fba8afaea6 100644 --- a/async_rl_research/README.md +++ b/async_rl_research/README.md @@ -22,7 +22,7 @@ datasets like USACO (in-place `test.sh` verification, multi-step aware). Harbor datasets need two things at rollout time: `ASYNC_RL_TASK_ROOT` pointing at the converter's out dir (on the slime-data volume), and ideally an oracle pass -first (`python -m async_rl_research.env.harbor --limit 3`, expect +first (`python -m async_rl_research.environment.harbor --limit 3`, expect reward=1.0) -- see `data/README.md` for the full flow. The rollout boot honors these env vars: diff --git a/async_rl_research/agent/mini_swe_agent.py b/async_rl_research/agent/mini_swe_agent.py index d4ceb24826..fc047d0f51 100644 --- a/async_rl_research/agent/mini_swe_agent.py +++ b/async_rl_research/agent/mini_swe_agent.py @@ -3,7 +3,7 @@ The contract + shared machinery (detached launch/poll, idempotent provisioning) live in ``agent/base.py``; the generic rollout recipe lives in ``async_rl_research.generate`` and the per-task-family envs in -``async_rl_research.env``. By the time ``run_agent`` is called the workspace +``async_rl_research.environment``. By the time ``run_agent`` is called the workspace is already task-prepped (the active env applied its setup and wrote ``PROBLEM_FILE``). Here we own only what is unique to mini-swe-agent: @@ -47,7 +47,7 @@ # Task-layer constant: the active env writes the problem statement here # before run_agent is called; the runner reads it via MSWE_PROBLEM_FILE. -from ..env.base import PROBLEM_FILE +from ..environment.base import PROBLEM_FILE # --- mini-swe-agent-specific knobs ------------------------------------------ diff --git a/async_rl_research/env/__init__.py b/async_rl_research/environment/__init__.py similarity index 100% rename from async_rl_research/env/__init__.py rename to async_rl_research/environment/__init__.py diff --git a/async_rl_research/env/base.py b/async_rl_research/environment/base.py similarity index 98% rename from async_rl_research/env/base.py rename to async_rl_research/environment/base.py index db3d1010c8..093dab8dbd 100644 --- a/async_rl_research/env/base.py +++ b/async_rl_research/environment/base.py @@ -174,8 +174,8 @@ def coerce_prompt(prompt) -> str: # task_type -> "module:Class". Values are strings so importing base.py never # imports any env module (env modules pull in provider backends). ENVS: dict[str, str] = { - "swe_gym": "async_rl_research.env.swe_gym:SweGymEnv", - "harbor": "async_rl_research.env.harbor:HarborEnv", + "swe_gym": "async_rl_research.environment.swe_gym:SweGymEnv", + "harbor": "async_rl_research.environment.harbor:HarborEnv", } _ENV_CACHE: dict[str, RolloutEnv] = {} diff --git a/async_rl_research/env/convert2slime/__init__.py b/async_rl_research/environment/convert2slime/__init__.py similarity index 100% rename from async_rl_research/env/convert2slime/__init__.py rename to async_rl_research/environment/convert2slime/__init__.py diff --git a/async_rl_research/env/convert2slime/harbor.py b/async_rl_research/environment/convert2slime/harbor.py similarity index 98% rename from async_rl_research/env/convert2slime/harbor.py rename to async_rl_research/environment/convert2slime/harbor.py index 6dd3bc0c7b..47de4e0ee0 100644 --- a/async_rl_research/env/convert2slime/harbor.py +++ b/async_rl_research/environment/convert2slime/harbor.py @@ -20,11 +20,11 @@ # a directory whose subdirectories are harbor tasks (e.g. a # harbor-datasets checkout subtree, or an adapter's generated tasks) - python -m async_rl_research.env.convert2slime.harbor \ + python -m async_rl_research.environment.convert2slime.harbor \ --tasks-dir ~/harbor-datasets/datasets/usaco --out-dir data/usaco # straight from a harbor registry (requires `pip install harbor`) - python -m async_rl_research.env.convert2slime.harbor \ + python -m async_rl_research.environment.convert2slime.harbor \ --registry ~/harbor/registry.json --dataset usaco --out-dir data/usaco v1 scope (anything else is skipped + logged): linux, single-container @@ -289,7 +289,7 @@ def main(argv: list[str] | None = None) -> int: print(f"converted {converted} tasks ({skipped} skipped) -> {out_dir / (name + '.jsonl')}") print("next steps:") print(f" export ASYNC_RL_TASK_ROOT={out_dir}") - print(f" python -m async_rl_research.env.harbor {out_dir / (name + '.jsonl')} --limit 3 # oracle check") + print(f" python -m async_rl_research.environment.harbor {out_dir / (name + '.jsonl')} --limit 3 # oracle check") return 0 if converted else 1 diff --git a/async_rl_research/env/convert2slime/swe_gym.py b/async_rl_research/environment/convert2slime/swe_gym.py similarity index 100% rename from async_rl_research/env/convert2slime/swe_gym.py rename to async_rl_research/environment/convert2slime/swe_gym.py diff --git a/async_rl_research/env/harbor.py b/async_rl_research/environment/harbor.py similarity index 99% rename from async_rl_research/env/harbor.py rename to async_rl_research/environment/harbor.py index b131221d6f..6438ea17f7 100644 --- a/async_rl_research/env/harbor.py +++ b/async_rl_research/environment/harbor.py @@ -30,7 +30,7 @@ Oracle check (no model involved -- validates boot/prep/verify plumbing by running each task's reference solution through the exact rollout path):: - python -m async_rl_research.env.harbor out/usaco.jsonl --task-root out --limit 3 + python -m async_rl_research.environment.harbor out/usaco.jsonl --task-root out --limit 3 """ from __future__ import annotations diff --git a/async_rl_research/env/swe_gym.py b/async_rl_research/environment/swe_gym.py similarity index 100% rename from async_rl_research/env/swe_gym.py rename to async_rl_research/environment/swe_gym.py diff --git a/async_rl_research/generate.py b/async_rl_research/generate.py index aba7e00172..6f635af11c 100644 --- a/async_rl_research/generate.py +++ b/async_rl_research/generate.py @@ -88,7 +88,7 @@ from .agent.base import AgentRuntime, load_runtime from .aiohttp_threaded import run_app_in_thread -from .env.base import EnvMetadataError, RewardResult, load_env +from .environment.base import EnvMetadataError, RewardResult, load_env logger = logging.getLogger(__name__) From 638416c799534cc96e11228f8d54b8cb99bb0b3a Mon Sep 17 00:00:00 2001 From: junlin-star Date: Fri, 12 Jun 2026 11:26:34 -0700 Subject: [PATCH 03/11] Replace per-family builtin prompts with a repo-owned universal config mini-swe-agent prompts previously came from per-task-family builtin YAMLs (swe_gym -> benchmarks/swebench.yaml, harbor -> mini.yaml), so the prompt scaffold differed across task families and pinned task contracts (the swebench patch.txt ritual) that did not match what actually gets graded. Ship one repo-owned config (agent/config/universal.yaml) uploaded into the sandbox as the default for every family; the task-specific deliverable now lives in the instruction text the env writes (swe_gym appends an explicit git-diff deliverable contract; harbor instruction.md files already carry their own). Builtin configs remain reachable via the override ladder: MSWE_CONFIG env > metadata.agent_config > the uploaded universal config. Co-Authored-By: Claude Fable 5 --- async_rl_research/agent/config/universal.yaml | 173 ++++++++++++++++++ async_rl_research/agent/mini_swe_agent.py | 65 ++++--- async_rl_research/environment/harbor.py | 10 +- async_rl_research/environment/swe_gym.py | 29 ++- 4 files changed, 244 insertions(+), 33 deletions(-) create mode 100644 async_rl_research/agent/config/universal.yaml diff --git a/async_rl_research/agent/config/universal.yaml b/async_rl_research/agent/config/universal.yaml new file mode 100644 index 0000000000..5fb1355afe --- /dev/null +++ b/async_rl_research/agent/config/universal.yaml @@ -0,0 +1,173 @@ +# The universal mini-swe-agent config: ONE prompt scaffold for ALL task +# families. Owned by this repo (not the pip-installed package) so the prompt +# distribution is version-controlled here and changing it never requires a +# package bump. +# +# Based on mini-swe-agent v2.3.1's default.yaml, with the action rules and +# format_error_template rewritten for NATIVE tool-calls (the runner uses +# LitellmModel, and the slime adapter parses tool calls via sglang's parser; +# default.yaml's ```mswea_bash_command``` text format would be rejected every +# turn). +# +# Scope rule: this file carries only scaffold-universal content (response +# format, subshell semantics, how to finish). The task's DELIVERABLE (patch +# vs. artifacts vs. stdout) belongs in the instruction text the env writes to +# PROBLEM_STATEMENT.md, which is rendered here as {{task}} -- harbor +# instruction.md files already follow this; swe_gym appends its suffix in +# env/swe_gym.py. +# +# step_limit / cost_limit / sampling knobs are overridden host-side by the +# runner regardless of what this file says. +agent: + system_template: | + You are a helpful assistant that can interact with a computer. + instance_template: | + Please solve the following task: + + + {{task}} + + + You can execute bash commands and edit files to accomplish it. The task + description above defines what you must produce and any task-specific + submission steps; follow it exactly. + + ## Command Execution Rules + + You are operating in an environment where + + 1. You issue at least one command + 2. The system executes the command(s) in a subshell + 3. You see the result(s) + 4. You write your next command(s) + + Each response should include: + + 1. **Reasoning text** where you explain your analysis and plan + 2. At least one tool call with your command + + **CRITICAL REQUIREMENTS:** + + - Your response SHOULD include reasoning text explaining what you're doing + - Your response MUST include AT LEAST ONE bash tool call + - Directory or environment variable changes are not persistent. Every action is executed in a new subshell. + - However, you can prefix any action with `MY_ENV_VAR=MY_VALUE cd /path/to/working/dir && ...` or write/load environment variables from files + + Example of a CORRECT response: + + I need to understand the structure of the repository first. Let me check what files are in the current directory to get a better understanding of the codebase. + + [Makes bash tool call with {"command": "ls -la"} as arguments] + + + ## Environment Details + + - You have a full Linux shell environment + - Always use non-interactive flags (-y, -f) for commands + - Avoid interactive tools like vi, nano, or any that require user input + - You can create new tools or scripts to help you with the task; if a tool isn't available, you can install it + + + {{system}} {{release}} {{version}} {{machine}} + + + ## Useful command examples + + ### Create a new file: + + ```bash + cat <<'EOF' > newfile.py + import numpy as np + hello = "world" + print(hello) + EOF + ``` + + ### Edit files with sed: + + ```bash + # Replace all occurrences + sed -i 's/old_string/new_string/g' filename.py + + # Replace only first occurrence + sed -i 's/old_string/new_string/' filename.py + + # Replace first occurrence on line 1 + sed -i '1s/old_string/new_string/' filename.py + + # Replace all occurrences in lines 1-10 + sed -i '1,10s/old_string/new_string/g' filename.py + ``` + + ### View file content: + + ```bash + # View specific lines with numbers + nl -ba filename.py | sed -n '10,20p' + ``` + + ## Finishing + + Once you have verified your work and completed any submission steps the + task description requires, finish by issuing the following command: + `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`. + Do not combine it with any other command. After this command, + you cannot continue working on this task. + step_limit: 0 + cost_limit: 0. +environment: + timeout: 60 + env: + PAGER: cat + MANPAGER: cat + LESS: -R + PIP_PROGRESS_BAR: 'off' + TQDM_DISABLE: '1' +model: + observation_template: | + {% if output.exception_info -%} + {{output.exception_info}} + {% endif -%} + {{output.returncode}} + {% if output.output | length < 10000 -%} + + {{ output.output -}} + + {%- else -%} + + The output of your last command was too long. + Please try a different command that produces less output. + If you're looking at a file you can try use head, tail or sed to view a smaller number of lines selectively. + If you're using grep or find and it produced too much output, you can use a more selective search pattern. + If you really need to see something from the full command's output, you can redirect output to a file and then search in that file. + + {%- set elided_chars = output.output | length - 10000 -%} + + {{ output.output[:5000] }} + + + {{ elided_chars }} characters elided + + + {{ output.output[-5000:] }} + + {%- endif -%} + format_error_template: | + Tool call error: + + + {{error}} + + + Here is general guidance on how to submit correct toolcalls: + + Every response needs to use the 'bash' tool at least once to execute commands. + + Call the bash tool with your command as the argument: + - Tool: bash + - Arguments: {"command": "your_command_here"} + + If you want to end the task, please issue the following command: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT` + without any other command. + model_kwargs: + drop_params: true diff --git a/async_rl_research/agent/mini_swe_agent.py b/async_rl_research/agent/mini_swe_agent.py index fc047d0f51..57b88c3dae 100644 --- a/async_rl_research/agent/mini_swe_agent.py +++ b/async_rl_research/agent/mini_swe_agent.py @@ -39,6 +39,7 @@ import os import shlex +from pathlib import Path from slime.agent.adapters import OpenAIAdapter from slime.agent.sandbox import Sandbox @@ -52,12 +53,18 @@ # --- mini-swe-agent-specific knobs ------------------------------------------ MSWE_STEP_LIMIT = int(os.environ.get("MSWE_STEP_LIMIT", "50")) -# Which builtin mini-swe-agent YAML config (prompts!) the runner loads, -# relative to its packaged config dir. Resolution: MSWE_CONFIG env (global -# override) > the env's md["agent_config"] hint (swe_gym -> -# benchmarks/swebench.yaml, harbor -> mini.yaml) > the SWE-bench default. +# Which YAML config (prompts!) the runner loads. The DEFAULT is the repo-owned +# universal config below, uploaded into the sandbox -- ONE prompt scaffold for +# all task families; the task-specific deliverable lives in the instruction +# text the env writes (see config/universal.yaml's scope rule). Override +# ladder (both name a BUILTIN config relative to the package's config dir, +# e.g. "mini.yaml" or "benchmarks/swebench.yaml"): +# MSWE_CONFIG env (global, experiments) > metadata.agent_config (per-row) +# > the uploaded universal config. MSWE_CONFIG = os.environ.get("MSWE_CONFIG", "") -MSWE_DEFAULT_CONFIG = "benchmarks/swebench.yaml" +# Read at import: the prompt scaffold ships with this module and must be +# identical for every rollout in a run. +UNIVERSAL_CONFIG_YAML = (Path(__file__).parent / "config" / "universal.yaml").read_text(encoding="utf-8") # Exact-pinned: the scaffold's prompts + wire protocol are part of the RL task # distribution (a PyPI drift mid-experiment silently changes the environment), # and MINI_RUNNER_PY below is written against the v2 API. @@ -81,14 +88,16 @@ # Runtime scratch under workdir beyond the base's launch files (which keep # their historical .mswe_* names via scratch_prefix below). _RUNNER = ".mswe_runner.py" +_CONFIG_FILE = ".mswe_config.yaml" # --------------------------------------------------------------------------- # Headless in-sandbox runner. # # Written against mini-swe-agent v2 (exact-pinned via MSWE_PIP_SPEC). The v2 -# specifics this depends on: default prompts ship as packaged YAML configs -# (we load the SWE-bench-tuned one instead of hand-rolling templates), bash is +# specifics this depends on: prompts come from a YAML config in the same +# schema as the packaged ones (we upload the repo-owned universal config; +# builtins remain reachable via the MSWE_CONFIG override ladder), bash is # driven through NATIVE tool-calls, and cost tracking hard-fails on models # litellm cannot price (hence cost_tracking="ignore_errors"). The wiring that # must hold regardless of version: litellm points at the slime adapter @@ -101,6 +110,7 @@ import os import sys import traceback +from pathlib import Path WORKDIR = os.environ["MSWE_WORKDIR"] MODEL = os.environ.get("MSWE_MODEL", "slime-actor") @@ -116,15 +126,20 @@ from minisweagent.environments.local import LocalEnvironment from minisweagent.models.litellm_model import LitellmModel - # v2 ships its default prompts in packaged YAML configs; the host picks - # one per task family (MSWE_CONFIG: SWE -> the SWE-bench-tuned config's - # patch-submission protocol, harbor -> the generic default). Read the - # builtin path directly -- get_config_from_spec() would also try - # cwd-relative candidates, which a repo file could shadow. - cfg_path = builtin_config_dir / os.environ.get("MSWE_CONFIG", "benchmarks/swebench.yaml") - if not cfg_path.is_file(): - print("[runner] config %s not found; falling back to benchmarks/swebench.yaml" % cfg_path) - cfg_path = builtin_config_dir / "benchmarks" / "swebench.yaml" + # Prompt config: the host uploads the repo-owned UNIVERSAL config next to + # this runner (MSWE_CONFIG_FILE) -- the default for every task family. + # MSWE_CONFIG, when set (global env override or a row's agent_config), + # names a BUILTIN packaged config instead. Read the builtin path directly + # -- get_config_from_spec() would also try cwd-relative candidates, which + # a repo file could shadow. + cfg_path = Path(os.environ["MSWE_CONFIG_FILE"]) + builtin = os.environ.get("MSWE_CONFIG", "") + if builtin: + candidate = builtin_config_dir / builtin + if candidate.is_file(): + cfg_path = candidate + else: + print("[runner] builtin config %s not found; using the universal config" % candidate) cfg = yaml.safe_load(cfg_path.read_text()) agent_cfg = dict(cfg.get("agent") or {}) model_cfg = dict(cfg.get("model") or {}) @@ -197,11 +212,13 @@ class MiniSweAgentRuntime(AgentRuntime): model_name = "slime-actor" # Keep the historical scratch names (.mswe_run.sh / .mswe_done / .mswe_log). scratch_prefix = ".mswe" - # "patch.txt" is the submission artifact the v2 swebench prompt instructs - # the agent to create; `git add -N .` would otherwise sweep it into the - # diff. (Launch scratch + the task-layer PROBLEM_FILE are excluded by the - # base / by the swe_gym env's git_diff itself.) - diff_exclude = (_RUNNER, "patch.txt") + # "patch.txt" is the submission artifact the builtin swebench prompt + # instructs the agent to create -- the universal config doesn't, but an + # MSWE_CONFIG/agent_config override back to the builtin would, and + # `git add -N .` would sweep it into the diff. (Launch scratch + the + # task-layer PROBLEM_FILE are excluded by the base / by the swe_gym env's + # git_diff itself.) + diff_exclude = (_RUNNER, _CONFIG_FILE, "patch.txt") async def run_agent( self, @@ -215,6 +232,7 @@ async def run_agent( """Provision mini-swe-agent in ``sb``, run it on the task, poll to done.""" workdir = md["workdir"] await sb.write_file(f"{workdir}/{_RUNNER}", MINI_RUNNER_PY) + await sb.write_file(f"{workdir}/{_CONFIG_FILE}", UNIVERSAL_CONFIG_YAML) await self._ensure_provisioned( sb, spec=MSWE_PIP_SPEC, @@ -232,7 +250,10 @@ async def run_agent( "MSWE_MODEL": self.model_name, "MSWE_WORKDIR": workdir, "MSWE_PROBLEM_FILE": f"{workdir}/{PROBLEM_FILE}", - "MSWE_CONFIG": MSWE_CONFIG or md.get("agent_config") or MSWE_DEFAULT_CONFIG, + # Override ladder: global env > per-row builtin override > the + # uploaded universal config ("" -> the runner uses MSWE_CONFIG_FILE). + "MSWE_CONFIG": MSWE_CONFIG or md.get("agent_config") or "", + "MSWE_CONFIG_FILE": f"{workdir}/{_CONFIG_FILE}", "MSWE_STEP_LIMIT": str(MSWE_STEP_LIMIT), "MSWE_PATH_PREPEND": MSWE_PATH_PREPEND, # keep the v2 import-time banner out of the runner log. diff --git a/async_rl_research/environment/harbor.py b/async_rl_research/environment/harbor.py index 6438ea17f7..4c37bef842 100644 --- a/async_rl_research/environment/harbor.py +++ b/async_rl_research/environment/harbor.py @@ -97,10 +97,10 @@ def _meets_min_reward(rewards: dict[str, Any] | None, min_reward: float | dict[s class HarborEnv(RolloutEnv): name = "harbor" - # mini-swe-agent prompt config hint: the generic builtin, not the SWE-bench - # patch-submission one -- harbor tasks ask for artifacts, not patches. - # Per-row override via metadata.agent_config; global via MSWE_CONFIG. - agent_config = "mini.yaml" + # No agent_config default: the runtime's universal prompt scaffold is the + # default for all task families, and harbor instruction.md files already + # carry their own deliverable contract (what artifacts to leave where). + # Per-row builtin override via metadata.agent_config; global via MSWE_CONFIG. # ------------------------------------------------------------------ # Row schema (written by env/convert2slime/harbor.py -- keep in sync) @@ -140,7 +140,7 @@ def normalize_metadata(self, sample) -> dict[str, Any]: "reward_strategy": m.get("reward_strategy"), "cpus": m.get("cpus"), "memory_mb": m.get("memory_mb"), - "agent_config": m.get("agent_config") or self.agent_config, + "agent_config": m.get("agent_config"), } @staticmethod diff --git a/async_rl_research/environment/swe_gym.py b/async_rl_research/environment/swe_gym.py index 1ef1ffad1e..fb9b2cec7a 100644 --- a/async_rl_research/environment/swe_gym.py +++ b/async_rl_research/environment/swe_gym.py @@ -40,12 +40,29 @@ _PRE = "/tmp/__swe_pre__.sh" +# Appended to the row's problem statement: with the universal prompt scaffold +# the agent's YAML no longer carries a submission protocol, so the task +# instruction must say what the deliverable is. Reward here is the captured +# working-tree `git diff`, hence: edit in place, don't commit, no patch files +# (the builtin swebench prompt's patch.txt ritual was never what got graded). +_DELIVERABLE_SUFFIX = """ + +## Deliverable + +Fix the issue by editing the repository's source files in place. + +- Your work is collected as the uncommitted working-tree changes (`git diff`) of this repository when you finish: leave your edits uncommitted. +- Do NOT commit your changes and do NOT create patch files. +- Do NOT modify tests or configuration files (pyproject.toml, setup.cfg, etc.). +- Delete any reproduction scripts or scratch files you created before finishing. +""" + + class SweGymEnv(RolloutEnv): name = "swe_gym" - # mini-swe-agent prompt config hint: the SWE-bench-tuned builtin (patch - # submission protocol, capped observations). Rows may override via - # metadata.agent_config. - agent_config = "benchmarks/swebench.yaml" + # No agent_config default: the runtime's universal prompt scaffold is the + # default; the SWE deliverable contract is _DELIVERABLE_SUFFIX above. + # Per-row builtin override via metadata.agent_config; global via MSWE_CONFIG. def normalize_metadata(self, sample) -> dict[str, Any]: m = sample.metadata or {} @@ -58,7 +75,7 @@ def normalize_metadata(self, sample) -> dict[str, Any]: "swepro": m.get("swepro"), "eval_cmd": m.get("eval_cmd"), "pre_commands": m.get("pre_commands"), - "agent_config": m.get("agent_config") or self.agent_config, + "agent_config": m.get("agent_config"), } if not md["image"] or not md["workdir"]: raise EnvMetadataError("missing_image_or_workdir") @@ -110,7 +127,7 @@ async def _prepare_workspace(self, sb: Sandbox, md: dict[str, Any]) -> None: await sb.exec("git config --system --add safe.directory '*'", check=False, timeout=60) if md["pre_commands"]: await _apply_pre_commands(sb, md["workdir"], md["pre_commands"]) - await self.write_problem_file(sb, md["workdir"], md["problem_statement"]) + await self.write_problem_file(sb, md["workdir"], (md["problem_statement"] or "") + _DELIVERABLE_SUFFIX) # ------------------------------------------------------------------ # Diff capture From 5b3b547235c326c1a0cb4cde9166779488433f35 Mon Sep 17 00:00:00 2001 From: junlin-star Date: Fri, 12 Jun 2026 11:27:02 -0700 Subject: [PATCH 04/11] Harden harbor grading and conversion for terminal-bench-style datasets Fixes found while onboarding openthoughts-tblite: - Grade reward-file-less tasks from test.sh's exit code (0/1 only; other exit codes stay "no verdict" so infra breakage is not scored as a model failure) -- terminal-bench-style tasks end test.sh with a bare pytest run. - Tolerate float error in is_solved: weighted pytest fractions can sum to 0.999... for a fully-passing task. - Prefer task subdirectories over a stray template task.toml at the dataset root in the converter's discovery. - Provision uv via pip when the image ships no curl (python:*-slim). Co-Authored-By: Claude Fable 5 --- async_rl_research/agent/mini_swe_agent.py | 10 ++++++++-- .../environment/convert2slime/harbor.py | 18 ++++++++++++++++-- async_rl_research/environment/harbor.py | 13 ++++++++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/async_rl_research/agent/mini_swe_agent.py b/async_rl_research/agent/mini_swe_agent.py index 57b88c3dae..0bd9f7c526 100644 --- a/async_rl_research/agent/mini_swe_agent.py +++ b/async_rl_research/agent/mini_swe_agent.py @@ -190,8 +190,14 @@ _VENV_SETUP = ( 'export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"\n' "if ! command -v uv >/dev/null 2>&1; then\n" - " curl -LsSf https://astral.sh/uv/install.sh | sh\n" - ' export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"\n' + " if command -v curl >/dev/null 2>&1; then\n" + " curl -LsSf https://astral.sh/uv/install.sh | sh\n" + ' export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"\n' + " else\n" + " # slim images (e.g. harbor's python:*-slim) ship neither curl nor\n" + " # wget, but they do ship pip; uv's PyPI wheel lands on PATH.\n" + " python3 -m pip install --quiet uv\n" + " fi\n" "fi\n" f"rm -rf {shlex.quote(MSWE_AGENT_VENV)}\n" f"uv venv --python {shlex.quote(MSWE_AGENT_PYTHON_VERSION)} {shlex.quote(MSWE_AGENT_VENV)}\n" diff --git a/async_rl_research/environment/convert2slime/harbor.py b/async_rl_research/environment/convert2slime/harbor.py index 47de4e0ee0..cfb9e1ce17 100644 --- a/async_rl_research/environment/convert2slime/harbor.py +++ b/async_rl_research/environment/convert2slime/harbor.py @@ -218,10 +218,24 @@ def convert( def _discover_task_dirs(tasks_dir: Path) -> list[Path]: - """Direct subdirectories holding a task.toml, sorted for determinism.""" + """Direct subdirectories holding a task.toml, sorted for determinism. + + Falls back to treating ``tasks_dir`` itself as a single task only when no + subdirectory tasks exist: some published datasets (e.g. harbor-datasets' + openthoughts-tblite) ship a stray template task.toml at the dataset root. + """ + subdirs = sorted(p for p in tasks_dir.iterdir() if (p / "task.toml").is_file()) + if subdirs: + if (tasks_dir / "task.toml").is_file(): + logger.warning( + "[harbor2slime] %s has both a root task.toml and %d task subdirs; using the subdirs", + tasks_dir, + len(subdirs), + ) + return subdirs if (tasks_dir / "task.toml").is_file(): return [tasks_dir] - return sorted(p for p in tasks_dir.iterdir() if (p / "task.toml").is_file()) + return [] def _download_from_registry(registry_spec: str, dataset: str, version: str | None, download_dir: Path) -> list[Path]: diff --git a/async_rl_research/environment/harbor.py b/async_rl_research/environment/harbor.py index 4c37bef842..f0efcd5c3d 100644 --- a/async_rl_research/environment/harbor.py +++ b/async_rl_research/environment/harbor.py @@ -270,7 +270,9 @@ async def _episode( reward = self._aggregate(steps, step_results, md["reward_strategy"]) return RewardResult( reward=reward, - is_solved=reward >= 1.0, + # epsilon: weighted pytest fractions can sum to 0.999... for a + # fully-passing task (seen on openthoughts-tblite bash-log-processor-fix) + is_solved=reward >= 1.0 - 1e-6, extra={ "harbor_step_results": step_results, "harbor_steps_completed": len(step_results), @@ -357,6 +359,15 @@ async def _verify( except ValueError: logger.warning("[harbor] %s: unparseable reward.txt: %.200s", instance_id, raw_txt) return None + # No reward file: terminal-bench-style tasks (e.g. openthoughts-tblite) + # end test.sh with a bare pytest run and rely on the harness to grade + # it. Grade all-or-nothing on pytest's exit code (tb's resolved + # semantics): 0 = every test passed, 1 = test failures. Anything else + # (127 missing dep, 2-5 pytest infra, timeout) stays "no verdict" so + # infra breakage is not silently scored as a model failure. + if ec in (0, 1): + logger.info("[harbor] %s: no reward file; graded from test.sh exit=%d", instance_id, ec) + return {"reward": 1.0 if ec == 0 else 0.0, "graded_from": "exit_code"} logger.warning( "[harbor] %s: test.sh exit=%s wrote no reward file; stderr tail: %s", instance_id, From 8b300c7b9a7af53e17cbb81774ea1b3528d72005 Mon Sep 17 00:00:00 2001 From: junlin-star Date: Fri, 12 Jun 2026 11:27:48 -0700 Subject: [PATCH 05/11] Instrument rollouts with phase + per-turn timing Rollout wall-clock is the training bottleneck (~20 min/sample) and the dumps carried no attribution. Add profiles/profiling.py: a PhaseTimer for env-side phases (work_boot/prep/agent/diff/eval_boot/eval) plus aiohttp middleware on the adapter that counts turns and generation time per session (bearer token == session_id). Both land in one "timing" dict per sample (swe_gym also records diff size metrics) -> sample extra -> rollout dumps, where the offline analyzer can attribute time across phases. Co-Authored-By: Claude Fable 5 --- async_rl_research/environment/swe_gym.py | 47 +++++++--- async_rl_research/generate.py | 9 ++ async_rl_research/profiles/.gitignore | 4 + async_rl_research/profiles/__init__.py | 3 + async_rl_research/profiles/profiling.py | 104 +++++++++++++++++++++++ 5 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 async_rl_research/profiles/.gitignore create mode 100644 async_rl_research/profiles/__init__.py create mode 100644 async_rl_research/profiles/profiling.py diff --git a/async_rl_research/environment/swe_gym.py b/async_rl_research/environment/swe_gym.py index fb9b2cec7a..24fe0afa97 100644 --- a/async_rl_research/environment/swe_gym.py +++ b/async_rl_research/environment/swe_gym.py @@ -24,12 +24,14 @@ import json import logging import shlex +import time from pathlib import Path from typing import Any from slime.agent.sandbox import Sandbox from ..modal_sandbox import ModalSandbox +from ..profiles.profiling import PhaseTimer from .base import PROBLEM_FILE, EnvMetadataError, RewardResult, RolloutEnv, coerce_prompt logger = logging.getLogger(__name__) @@ -92,23 +94,39 @@ async def rollout( eval_timeout_sec: int, ) -> RewardResult: workdir = md["workdir"] + timer = PhaseTimer() + t0 = time.monotonic() async with ModalSandbox(md["image"]) as sb: - await self._prepare_workspace(sb, md) - await runtime.run_agent( - sb, - md=md, - session_id=session_id, - adapter_url=adapter_url, - time_budget_sec=agent_time_budget_sec, - ) - diff_text = await self._git_diff(sb, workdir, exclude=runtime.diff_exclude_all) + timer.record("work_boot", time.monotonic() - t0) + with timer.phase("prep"): + await self._prepare_workspace(sb, md) + with timer.phase("agent"): + await runtime.run_agent( + sb, + md=md, + session_id=session_id, + adapter_url=adapter_url, + time_budget_sec=agent_time_budget_sec, + ) + with timer.phase("diff"): + diff_text = await self._git_diff(sb, workdir, exclude=runtime.diff_exclude_all) # Work sandbox is closed; grade the diff in a clean one. - reward, is_solved, applied = await self._evaluate(md, diff_text, timeout_sec=eval_timeout_sec) + with timer.phase("eval"): + reward, is_solved, applied = await self._evaluate( + md, diff_text, timeout_sec=eval_timeout_sec, timer=timer + ) return RewardResult( reward=float(reward), is_solved=bool(is_solved), - extra={"applied_cleanly": bool(applied)}, + # diff_bytes/diff_files are SIZE metrics only (len + header count); + # the patch text itself is never stored. + extra={ + "applied_cleanly": bool(applied), + "diff_bytes": len(diff_text), + "diff_files": diff_text.count("diff --git"), + "timing": timer.as_dict(), + }, ) # ------------------------------------------------------------------ @@ -147,14 +165,19 @@ async def _git_diff(self, sb: Sandbox, workdir: str, *, exclude: tuple[str, ...] # ------------------------------------------------------------------ # Eval (fresh clean sandbox, apply diff, run dataset tests) # ------------------------------------------------------------------ - async def _evaluate(self, md: dict[str, Any], diff_text: str, *, timeout_sec: int) -> tuple[float, bool, bool]: + async def _evaluate( + self, md: dict[str, Any], diff_text: str, *, timeout_sec: int, timer: PhaseTimer | None = None + ) -> tuple[float, bool, bool]: """Grade ``diff_text`` in a CLEAN sandbox; returns (reward, solved, applied).""" if not (md["swepro"] or md["eval_cmd"]): logger.warning("[swe_gym.evaluate] no swepro/eval_cmd; reward=0") return 0.0, False, True workdir = md["workdir"] + t0 = time.monotonic() async with ModalSandbox(md["image"]) as ev: + if timer is not None: + timer.record("eval_boot", time.monotonic() - t0) if md["pre_commands"]: await _apply_pre_commands(ev, workdir, md["pre_commands"]) diff --git a/async_rl_research/generate.py b/async_rl_research/generate.py index 6f635af11c..3b521d3f52 100644 --- a/async_rl_research/generate.py +++ b/async_rl_research/generate.py @@ -86,6 +86,7 @@ from slime.utils.processing_utils import load_tokenizer from slime.utils.types import Sample +from .profiles import profiling from .agent.base import AgentRuntime, load_runtime from .aiohttp_threaded import run_app_in_thread from .environment.base import EnvMetadataError, RewardResult, load_env @@ -160,6 +161,9 @@ def __init__(self, args) -> None: tool_parser=self.tool_parser, reasoning_parser=self.reasoning_parser, ) + # Per-turn timing by session (bearer token == session_id); must be + # installed before the app starts. Feeds metadata["timing"] below. + profiling.install(self.adapter.app) # handler_cancellation=True so a client disconnect cancels the handler # coroutine, arming the adapter's fire-and-forget /abort_request. Without # it a cancelled client leaves an inflight sglang /generate that races @@ -343,6 +347,11 @@ async def generate(args, sample: Sample, sampling_params: dict[str, Any], evalua agent_time_budget_sec=AGENT_TIME_BUDGET_SEC, eval_timeout_sec=AGENT_EVAL_TIMEOUT_SEC, ) + # Fold the adapter's per-turn stats into the env's phase timing + # (one "timing" dict per sample -> dumps -> profile.py). + turn_stats = profiling.pop_session_stats(session_id) + if turn_stats: + reward_result.extra.setdefault("timing", {}).update(turn_stats) segments = await state.adapter.finish_session(session_id) return _merge_samples( sample=sample, diff --git a/async_rl_research/profiles/.gitignore b/async_rl_research/profiles/.gitignore new file mode 100644 index 0000000000..4a75feb3c0 --- /dev/null +++ b/async_rl_research/profiles/.gitignore @@ -0,0 +1,4 @@ +# Per-run profiling artifacts produced by profile.py; regenerate by re-running +# it against the W&B run + rollout dump. Keep only the code + methodology. +runs.jsonl +ATTRIBUTION.md diff --git a/async_rl_research/profiles/__init__.py b/async_rl_research/profiles/__init__.py new file mode 100644 index 0000000000..722fd9e727 --- /dev/null +++ b/async_rl_research/profiles/__init__.py @@ -0,0 +1,3 @@ +"""Rollout profiling: in-rollout instrumentation (profiling.py), offline +analyzer (profile.py), methodology (PERF.md), and generated artifacts +(runs.jsonl / ATTRIBUTION.md).""" diff --git a/async_rl_research/profiles/profiling.py b/async_rl_research/profiles/profiling.py new file mode 100644 index 0000000000..c4589ef913 --- /dev/null +++ b/async_rl_research/profiles/profiling.py @@ -0,0 +1,104 @@ +"""In-rollout profiling: phase timers + per-session adapter turn stats. + +Two collectors, both feeding ``sample.metadata["timing"]`` (and from there the +``rollout_{id}.pt`` debug dumps that ``profile.py`` aggregates offline): + +``PhaseTimer`` + Wall-clock spans for the env-side phases of one sample's rollout + (work_boot / prep / agent / diff / eval / eval_boot). Owned by the env's + ``rollout()``; serialized via ``as_dict()`` into ``RewardResult.extra``. + +``timing_middleware`` / ``pop_session_stats`` + An aiohttp middleware installed on the adapter app (``generate._State`` + owns both the adapter and the server start, so this needs NO slime-core + change). It times every generation request and attributes it to the + session via the bearer token -- which IS the slime session_id by the + adapter's auth convention. ``gen_s`` measures the whole adapter hop + (render -> tokenize -> SGLang /generate -> response build), so + ``gen_s/n_turns`` minus the engine-side e2e latency (W&B ``sgl_engine``) + is the adapter's own per-turn overhead. + +Caveats (fine for profiling, do not treat as exact accounting): + * the store is per-process (one rollout worker = one adapter = one store); + * a request in flight when a sample is aborted may be missing from, or + inflate, that sample's stats; + * dict mutation is GIL-atomic and the reader (``generate()``) only pops + after the agent finished, so no locking is needed. +""" + +from __future__ import annotations + +import time +from contextlib import contextmanager + +from aiohttp import web + +# Endpoints that represent one model turn (OpenAI chat/responses, Anthropic +# messages). Health checks and model listings must not count as turns. +_GENERATION_PATHS = ("/v1/chat/completions", "/v1/responses", "/v1/messages") + + +def _session_id(request: web.Request) -> str: + """Bearer token (slime's session auth convention), else x-api-key. + + Mirrors slime.agent.adapters.common.request_session_id without importing + it (that pulls the torch-heavy slime chain into this otherwise + dependency-free module). The body-based fallbacks there don't apply: our + runtimes always authenticate with session_id as the key. + """ + auth = request.headers.get("Authorization", "") + if auth.lower().startswith("bearer ") and auth[7:].strip(): + return auth[7:].strip() + return (request.headers.get("X-Api-Key") or "").strip() or "default" + + +class PhaseTimer: + """Accumulates named wall-clock spans for one sample's rollout.""" + + def __init__(self) -> None: + self._spans: dict[str, float] = {} + + @contextmanager + def phase(self, name: str): + t0 = time.monotonic() + try: + yield + finally: + self.record(name, time.monotonic() - t0) + + def record(self, name: str, seconds: float) -> None: + self._spans[name] = round(self._spans.get(name, 0.0) + seconds, 3) + + def as_dict(self) -> dict[str, float]: + return dict(self._spans) + + +# --------------------------------------------------------------------------- +# Adapter-side per-session turn stats +# --------------------------------------------------------------------------- +_SESSION_STATS: dict[str, dict[str, float]] = {} + + +@web.middleware +async def timing_middleware(request: web.Request, handler): + if request.method != "POST" or not request.path.endswith(_GENERATION_PATHS): + return await handler(request) + t0 = time.monotonic() + try: + return await handler(request) + finally: + sid = _session_id(request) + stats = _SESSION_STATS.setdefault(sid, {"n_turns": 0, "gen_s": 0.0}) + stats["n_turns"] += 1 + stats["gen_s"] = round(stats["gen_s"] + (time.monotonic() - t0), 3) + + +def install(app: web.Application) -> None: + """Idempotently add the timing middleware (call BEFORE the app starts).""" + if timing_middleware not in app.middlewares: + app.middlewares.append(timing_middleware) + + +def pop_session_stats(session_id: str) -> dict[str, float] | None: + """Drain one session's turn stats (None if the agent never dialed in).""" + return _SESSION_STATS.pop(session_id, None) From e4c4e91b493b60fe5d1d523a00e3a8f352eedfc7 Mon Sep 17 00:00:00 2001 From: junlin-star Date: Fri, 12 Jun 2026 11:28:39 -0700 Subject: [PATCH 06/11] Contain stray CancelledError to the failing sample A CancelledError raised inside a rollout (e.g. the Modal SDK's synchronicity bridge cancelling an in-flight .aio call on a client hiccup) is not caught by `except Exception` and propagated through generate_and_rm_group's gather, cancelling the whole generate_rollout_async task and crashing the training step. Catch it, abort only that sample, and re-raise only for a genuine external cancel (task left in a cancelling state). Aborted samples also keep whatever per-turn timing accrued, distinguishing 'agent alive but slow' from 'never dialed in'. Co-Authored-By: Claude Fable 5 --- async_rl_research/generate.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/async_rl_research/generate.py b/async_rl_research/generate.py index 3b521d3f52..6ce8eb7d07 100644 --- a/async_rl_research/generate.py +++ b/async_rl_research/generate.py @@ -364,9 +364,25 @@ async def generate(args, sample: Sample, sampling_params: dict[str, Any], evalua except asyncio.TimeoutError: _log_timeout_diagnostic(t0) + _attach_partial_timing(sample, session_id, t0) return _abort_result(sample, "wall_clock_timeout") + except asyncio.CancelledError: + # A stray CancelledError from inside the rollout (e.g. the Modal SDK's + # synchronicity bridge cancelling an in-flight .aio call on a client + # hiccup) is not caught by `except Exception` and would otherwise + # propagate through generate_and_rm_group's gather and cancel the + # WHOLE generate_rollout_async task, crashing the training step. A + # genuine external cancel (the asyncio.timeout guard converts its own + # to TimeoutError before this; Ray/loop teardown does not) leaves the + # task in a cancelling state -- re-raise only then. + if asyncio.current_task().cancelling(): + raise + logger.error("[async_rl] %s: stray CancelledError; aborting sample", instance_id) + _attach_partial_timing(sample, session_id, t0) + return _abort_result(sample, "exception:CancelledError") except Exception as e: logger.error("[async_rl] %s: rollout failed: %s\n%s", instance_id, e, traceback.format_exc()) + _attach_partial_timing(sample, session_id, t0) return _abort_result(sample, f"exception:{type(e).__name__}") finally: # Close the sid before the next train step's release_memory_occupation; @@ -374,6 +390,14 @@ async def generate(args, sample: Sample, sampling_params: dict[str, Any], evalua await state.adapter.finish_session(session_id) # idempotent +def _attach_partial_timing(sample: Sample, session_id: str, t0: float) -> None: + """On abort, keep whatever turn stats accrued -- distinguishes 'agent was + alive but slow' (turns accrued) from 'never dialed in' (no stats).""" + stats = profiling.pop_session_stats(session_id) or {} + stats["elapsed_at_abort"] = round(time.time() - t0, 1) + sample.metadata = {**(sample.metadata or {}), "timing": stats} + + def _log_timeout_diagnostic(t0: float) -> None: """Dump pending-task names when the wall-clock guard fires. Never crashes.""" try: From 74822a5f256775af5143972407b872c90d7dfdbe Mon Sep 17 00:00:00 2001 From: junlin-star Date: Fri, 12 Jun 2026 11:29:03 -0700 Subject: [PATCH 07/11] Add eval-set builder and honor per-dataset eval sampling overrides evalset.py turns a spec YAML into subsampled per-subset jsonls plus a manifest and a ready --eval-config block, so eval sets are versioned and pinned instead of hand-cut. Making the per-dataset overrides actually take effect required a generate.py fix: _sampling_params built the session defaults from the rollout_* args only, ignoring the sampling_params dict slime passes to generate() -- which on the eval path is what carries the eval_config temperature/top_p/top_k. Layer those overrides on top. Per-turn max_new_tokens deliberately stays adapter-governed. Document the eval flow (builder -> inline eval_config -> eval-only runs) in the README. Co-Authored-By: Claude Fable 5 --- async_rl_research/README.md | 34 +++++++ async_rl_research/evalset.py | 181 ++++++++++++++++++++++++++++++++++ async_rl_research/generate.py | 42 +++++--- 3 files changed, 242 insertions(+), 15 deletions(-) create mode 100644 async_rl_research/evalset.py diff --git a/async_rl_research/README.md b/async_rl_research/README.md index fba8afaea6..f1fa87e458 100644 --- a/async_rl_research/README.md +++ b/async_rl_research/README.md @@ -15,8 +15,12 @@ datasets like USACO (in-place `test.sh` verification, multi-step aware). | `env/swe_gym.py` | SWE-Gym env: prebuilt image boot / pre_commands / git diff / clean-sandbox eval | | `env/harbor.py` | Harbor env: Dockerfile boot, step loop, in-place verify (+ oracle-check CLI) | | `env/convert2slime/` | Dataset converters, paired with their env by filename (see `data/README.md`) | +| `evalset.py` | Eval-set builder: spec YAML → subsampled per-subset jsonl + manifest + ready `--eval-config` (see `data/README.md`) | | `modal_sandbox.py` | Modal backend (boot concurrency, create retry; registry refs + Dockerfile builds) | | `dashboard/` | Modal web app (Bun/TS) for browsing the rollout debug dumps as agent conversations (see `dashboard/README.md`) | +| `profiles/PERF.md` | Measured rollout-time attribution, ranked fixes, and a step-by-step profiling guide | +| `profiles/profiling.py` | In-rollout instrumentation: env phase timers + adapter middleware (per-session turn count / gen time) → `sample.metadata["timing"]` → dumps | +| `profiles/profile.py` | Offline analyzer: W&B run + rollout dump → one attribution row in `profiles/runs.jsonl` + regenerated `profiles/ATTRIBUTION.md` | ## Setup @@ -32,3 +36,33 @@ The rollout boot honors these env vars: | `MODAL_REGISTRY_SECRET` | Modal secret for authenticated Docker Hub pulls (`dockerhub-creds`) | | `MODAL_ENVIRONMENT` | Modal environment the images are cached in | | `SLIME_AGENT_SANDBOX_ADD_PYTHON` | Add python to the image (must match rollout) | + +## Eval + +Eval reuses the exact same `generate()` → `runtime × env` stack as training: +slime's eval path (`slime/rollout/sglang_rollout.py::eval_rollout`) iterates +`--eval-config` datasets and calls the custom generate function with +`evaluation=True` per sample. Mean reward per dataset lands in W&B as +`eval/{name}` (plus `-truncated_ratio`, response-len stats). + +Three pieces, in order: + +1. **Build an eval set** (subsampled, versioned, pinned by manifest): + `python -m async_rl_research.evalset spec.yaml --out-dir /data/evalsets/v0` + — see `data/README.md`. Oracle-check harbor subsets before burning GPU time. +2. **Wire it into the training config** as an inline `eval_config` dict (the + launcher materializes it to a temp YAML → `--eval-config`); set + `eval_interval`. train_async.py evals every `eval_interval` rollouts + (first at rollout `eval_interval` — no step-0 baseline in async mode; get + the base-model baseline from an eval-only run instead). Eval blocks the + train loop and shares the sglang engines — size subsets accordingly. +3. **Eval-only runs**: `num_rollout = 0` with `eval_interval` set routes + through `train.py`'s stock eval-only branch — one eval pass, then exit + (set `load` to a saved checkpoint to eval a trained model; use + `async_mode = False` in the experiment config so train.py is the + entrypoint). + +Per-dataset eval sampling overrides (`temperature`, `top_p`, `top_k`) flow +through `generate.py::_sampling_params` into the adapter's session defaults; +per-turn `max_new_tokens` stays adapter-governed regardless of +`eval_max_response_len`. diff --git a/async_rl_research/evalset.py b/async_rl_research/evalset.py new file mode 100644 index 0000000000..18a64cba93 --- /dev/null +++ b/async_rl_research/evalset.py @@ -0,0 +1,181 @@ +"""Build a versioned eval set by subsampling converted slime datasets. + +An *eval set* is a directory of per-subset JSONL files drawn from +already-converted datasets (the outputs of ``env/convert2slime/*``), plus a +manifest pinning exactly which instances were chosen. Build it once onto the +slime-data volume, then point the training config's inline ``eval_config`` +at the subset files. The rows are ordinary slime prompt rows, so eval drives +the same ``runtime x env`` stack as training. + +Spec YAML:: + + task_root: /data # optional: harbor metadata.task_path values are + # rewritten relative to this dir, so ONE + # ASYNC_RL_TASK_ROOT covers train + eval rows. + # Omit to inline absolute task paths instead. + subsets: + - name: swebench_verified_50 + source: /data/swebench_verified/swebench_verified.jsonl + n: 50 # omit -> keep all rows + seed: 0 # deterministic subsample (default 0) + - name: usaco_hard + source: /data/usaco/usaco.jsonl + ids: [usaco_829, ...] # optional instance_id allowlist, applied before n + +Usage:: + + python -m async_rl_research.evalset spec.yaml --out-dir /data/evalsets/v0 + +Outputs ``/.jsonl`` per subset, ``manifest.json`` (spec + +chosen instance ids), and ``eval_config.yaml`` (a ready ``--eval-config`` +file). It also prints the equivalent inline ``eval_config`` dict to paste +into a training config. Paths inside the spec should be the paths as seen at +*runtime* (e.g. ``/data/...`` on the cluster); run the builder where those +paths resolve (a Modal shell / function with the volume mounted) so the +harbor task-dir checks mean something. After building, oracle-check a subset: +``ASYNC_RL_TASK_ROOT= python -m async_rl_research.environment.harbor +/.jsonl --limit 3``. +""" + +from __future__ import annotations + +import argparse +import json +import random +import sys +from pathlib import Path +from typing import Any + + +def _load_spec(path: Path) -> dict[str, Any]: + import yaml + + spec = yaml.safe_load(path.read_text()) + if not isinstance(spec, dict) or not isinstance(spec.get("subsets"), list) or not spec["subsets"]: + raise SystemExit(f"{path}: spec must be a mapping with a non-empty `subsets` list") + names = [s.get("name") for s in spec["subsets"]] + if any(not n for n in names) or len(set(names)) != len(names): + raise SystemExit(f"{path}: every subset needs a unique `name` (got {names})") + for s in spec["subsets"]: + if not s.get("source"): + raise SystemExit(f"{path}: subset {s.get('name')!r} is missing `source`") + unknown = set(s) - {"name", "source", "n", "seed", "ids"} + if unknown: + raise SystemExit(f"{path}: subset {s['name']!r} has unknown keys {sorted(unknown)}") + return spec + + +def _instance_id(row: dict[str, Any], index: int) -> str: + return (row.get("metadata") or {}).get("instance_id") or row.get("label") or f"row-{index}" + + +def _rewrite_task_path(row: dict[str, Any], source_dir: Path, task_root: Path | None, problems: list[str]) -> None: + """Re-root a harbor row's relative task_path so it stays resolvable. + + Converted harbor rows carry task_path relative to their converter's + out dir; a subsampled copy lives elsewhere, so pin the path down: relative + to ``task_root`` when given (matching the run's single ASYNC_RL_TASK_ROOT), + absolute otherwise (env/harbor accepts absolute paths as-is). + """ + md = row.get("metadata") or {} + if md.get("task_type") != "harbor" or not md.get("task_path"): + return + task_dir = Path(md["task_path"]) + if not task_dir.is_absolute(): + task_dir = source_dir / task_dir + if task_root is not None: + try: + md["task_path"] = str(task_dir.relative_to(task_root)) + except ValueError: + problems.append(f"task dir {task_dir} is outside task_root {task_root}; kept absolute") + md["task_path"] = str(task_dir) + else: + md["task_path"] = str(task_dir) + if not task_dir.is_dir(): + problems.append(f"task dir not found: {task_dir}") + + +def _build_subset(subset: dict[str, Any], out_dir: Path, task_root: Path | None, strict: bool) -> dict[str, Any]: + source = Path(subset["source"]) + rows = [json.loads(line) for line in source.read_text().splitlines() if line.strip()] + ids = [_instance_id(row, i) for i, row in enumerate(rows)] + + if allow := subset.get("ids"): + missing = set(allow) - set(ids) + if missing: + raise SystemExit(f"subset {subset['name']!r}: ids not in {source}: {sorted(missing)}") + keep = [i for i, iid in enumerate(ids) if iid in set(allow)] + else: + keep = list(range(len(rows))) + + n = subset.get("n") + if n is not None and n < len(keep): + keep = sorted(random.Random(subset.get("seed", 0)).sample(keep, n)) + + problems: list[str] = [] + chosen = [] + for i in keep: + row = json.loads(json.dumps(rows[i])) # deep copy; never mutate the source rows + _rewrite_task_path(row, source.parent, task_root, problems) + chosen.append(row) + + if problems: + for p in problems: + print(f" [{subset['name']}] WARNING: {p}", file=sys.stderr) + if strict: + raise SystemExit(f"subset {subset['name']!r}: {len(problems)} problem(s) with --strict") + + out_path = out_dir / f"{subset['name']}.jsonl" + with out_path.open("w") as f: + for row in chosen: + f.write(json.dumps(row, ensure_ascii=False) + "\n") + print(f" {subset['name']}: {len(chosen)}/{len(rows)} rows from {source} -> {out_path}") + return { + "name": subset["name"], + "source": str(source), + "n_source_rows": len(rows), + "n_rows": len(chosen), + "seed": subset.get("seed", 0), + "path": str(out_path), + "instance_ids": [ids[i] for i in keep], + } + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("spec", type=Path, help="eval-set spec YAML (see module docstring)") + parser.add_argument("--out-dir", type=Path, required=True, help="eval-set output dir (one dir per version)") + parser.add_argument("--strict", action="store_true", help="fail on missing task dirs instead of warning") + args = parser.parse_args() + + spec = _load_spec(args.spec) + task_root = Path(spec["task_root"]) if spec.get("task_root") else None + # Resolve so the paths baked into the manifest / eval_config are absolute. + args.out_dir = args.out_dir.resolve() + args.out_dir.mkdir(parents=True, exist_ok=True) + + built = [_build_subset(s, args.out_dir, task_root, args.strict) for s in spec["subsets"]] + + manifest = {"spec": spec, "subsets": built} + (args.out_dir / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") + + datasets = [{"name": b["name"], "path": b["path"]} for b in built] + import yaml + + (args.out_dir / "eval_config.yaml").write_text(yaml.dump({"eval": {"datasets": datasets}}, sort_keys=False)) + + print(f"\nwrote {args.out_dir}/manifest.json and {args.out_dir}/eval_config.yaml") + if task_root is not None: + print(f"run with ASYNC_RL_TASK_ROOT={task_root} (harbor task_paths are relative to it)") + print("\ninline eval_config for the training config:\n") + print(" eval_config = {") + print(' "defaults": {"n_samples_per_eval_prompt": 1},') + print(' "datasets": [') + for d in datasets: + print(f' {{"name": "{d["name"]}", "path": "{d["path"]}"}},') + print(" ],") + print(" }") + + +if __name__ == "__main__": + main() diff --git a/async_rl_research/generate.py b/async_rl_research/generate.py index 6ce8eb7d07..4416ed839c 100644 --- a/async_rl_research/generate.py +++ b/async_rl_research/generate.py @@ -224,7 +224,7 @@ def _resolve_adapter_url(self, public_host: str | None) -> str: # --------------------------------------------------------------------------- # Trajectory -> Sample # --------------------------------------------------------------------------- -def _start_session(state: _State, sample: Sample, md: dict[str, Any]) -> str: +def _start_session(state: _State, sample: Sample, md: dict[str, Any], sampling_params: dict[str, Any]) -> str: """Register the adapter session BEFORE the agent starts. The in-sandbox agent sends ``session_id`` as its auth/bearer token so the @@ -243,26 +243,38 @@ def _start_session(state: _State, sample: Sample, md: dict[str, Any]) -> str: # Runtimes must strip sampling knobs from agent requests to stay on-policy. state.adapter.open_session( session_id, - sampling_defaults=_sampling_params(state.args), + sampling_defaults=_sampling_params(state.args, sampling_params), max_context_tokens=state.max_context_len, ) return session_id -def _sampling_params(args) -> dict[str, Any]: +def _sampling_params(args, overrides: dict[str, Any] | None = None) -> dict[str, Any]: # Kept tiny on purpose: the adapter fills the rest of its defaults. We only # pin the knobs that must match training. Extend as needed. - if args is None: - return {} - return { - k: v - for k, v in ( - ("temperature", getattr(args, "rollout_temperature", None)), - ("top_p", getattr(args, "rollout_top_p", None)), - ("top_k", getattr(args, "rollout_top_k", None)), - ) - if v is not None - } + # + # ``overrides`` is the sampling_params dict slime hands to generate(): on + # the train path it mirrors the rollout_* args, on the eval path it carries + # the per-dataset eval overrides (eval_config temperature/top_p/top_k), so + # honoring it here is what makes eval-time sampling settings take effect. + # Per-turn max_new_tokens deliberately stays adapter-governed. + params = ( + {} + if args is None + else { + k: v + for k, v in ( + ("temperature", getattr(args, "rollout_temperature", None)), + ("top_p", getattr(args, "rollout_top_p", None)), + ("top_k", getattr(args, "rollout_top_k", None)), + ) + if v is not None + } + ) + for k in ("temperature", "top_p", "top_k"): + if overrides and overrides.get(k) is not None: + params[k] = overrides[k] + return params def _merge_samples( @@ -335,7 +347,7 @@ async def generate(args, sample: Sample, sampling_params: dict[str, Any], evalua return _abort_result(sample, f"env_dispatch_failed:{type(e).__name__}:{e}") instance_id = md["instance_id"] - session_id = _start_session(state, sample, md) + session_id = _start_session(state, sample, md, sampling_params) t0 = time.time() try: async with asyncio.timeout(AGENT_GENERATE_GUARD_SEC): From 31a8e8dd04cfbaf399480644e212951e9391199e Mon Sep 17 00:00:00 2001 From: junlin-star Date: Mon, 15 Jun 2026 19:45:22 -0700 Subject: [PATCH 08/11] Refactor agentic-RL adapters, envs, and generation pipeline Add repo-owned Qwen OpenAI adapter and openthoughts_agent converter, rework agent/environment base classes and convert2slime adapters, and streamline generate.py/modal_sandbox sampling and rollout handling. Remove stale data/ and profiles/ scaffolding. Co-authored-by: Cursor --- async_rl_research/README.md | 8 +- async_rl_research/agent/adapters/__init__.py | 6 + async_rl_research/agent/adapters/qwen.py | 174 ++++++++++ async_rl_research/agent/base.py | 188 ++++------- async_rl_research/agent/config/universal.yaml | 60 ++-- async_rl_research/agent/mini_swe_agent.py | 210 +++++------- async_rl_research/aiohttp_threaded.py | 15 +- async_rl_research/data/.gitignore | 6 - async_rl_research/data/README.md | 41 --- async_rl_research/environment/base.py | 103 +++--- .../environment/convert2slime/__init__.py | 4 +- .../environment/convert2slime/harbor.py | 62 ++-- .../convert2slime/openthoughts_agent.py | 160 +++++++++ .../environment/convert2slime/swe_gym.py | 12 +- async_rl_research/environment/harbor.py | 194 +++++------ async_rl_research/environment/swe_gym.py | 63 ++-- async_rl_research/evalset.py | 34 +- async_rl_research/generate.py | 310 ++++++------------ async_rl_research/modal_sandbox.py | 160 ++++----- async_rl_research/profiles/.gitignore | 4 - async_rl_research/profiles/__init__.py | 3 - async_rl_research/profiles/profiling.py | 104 ------ 22 files changed, 899 insertions(+), 1022 deletions(-) create mode 100644 async_rl_research/agent/adapters/__init__.py create mode 100644 async_rl_research/agent/adapters/qwen.py delete mode 100644 async_rl_research/data/.gitignore delete mode 100644 async_rl_research/data/README.md create mode 100644 async_rl_research/environment/convert2slime/openthoughts_agent.py delete mode 100644 async_rl_research/profiles/.gitignore delete mode 100644 async_rl_research/profiles/__init__.py delete mode 100644 async_rl_research/profiles/profiling.py diff --git a/async_rl_research/README.md b/async_rl_research/README.md index f1fa87e458..b5b0790a78 100644 --- a/async_rl_research/README.md +++ b/async_rl_research/README.md @@ -63,6 +63,8 @@ Three pieces, in order: entrypoint). Per-dataset eval sampling overrides (`temperature`, `top_p`, `top_k`) flow -through `generate.py::_sampling_params` into the adapter's session defaults; -per-turn `max_new_tokens` stays adapter-governed regardless of -`eval_max_response_len`. +through `generate.py::_sampling_params` into the adapter's session defaults, +along with `max_new_tokens` (the per-turn generation cap). slime sets it to +`rollout_max_response_len` for train and `eval_max_response_len` for eval, so +that value bounds a single model turn, then the adapter further clamps it to the +remaining `rollout_max_context_len` budget. diff --git a/async_rl_research/agent/adapters/__init__.py b/async_rl_research/agent/adapters/__init__.py new file mode 100644 index 0000000000..597051eb20 --- /dev/null +++ b/async_rl_research/agent/adapters/__init__.py @@ -0,0 +1,6 @@ +"""Repo-owned slime adapter variants handling model-specific rendering quirks +without patching slime core. See ``qwen.py``.""" + +from .qwen import QwenOpenAIAdapter + +__all__ = ["QwenOpenAIAdapter"] diff --git a/async_rl_research/agent/adapters/qwen.py b/async_rl_research/agent/adapters/qwen.py new file mode 100644 index 0000000000..e6991e1f2c --- /dev/null +++ b/async_rl_research/agent/adapters/qwen.py @@ -0,0 +1,174 @@ +"""Qwen-family OpenAI adapter: render tool-call arguments as a dict. + +slime's ``OpenAIAdapter`` stringifies ``function.arguments`` before +``apply_chat_template``, but the Qwen3-Coder family (Qwen3.6-35B-A3B) template +iterates ``arguments | items`` and needs a mapping -- a string raises on turn +2+, capping every episode at one turn. This adapter renders ``arguments`` as a +dict on the inbound path only (the outbound OpenAI response stays string-form). + +It also splices each turn's raw ``output_ids`` into the next prompt rather than +re-rendering the parsed assistant message (see ``_build_prompt``): the +qwen3_coder parser strips trailing whitespace from tool-call arguments, so a +re-render is not token-identical to what the model generated, which makes +``merge_turns`` log "prefix drift" and mask whole turns out of training. + +slime renders through free functions with no method seam, so we register our +own ``/v1/chat/completions`` handler. ``_run_turn`` / ``_handle_chat_completions`` +below are faithful mirrors of slime's -- keep them in sync. +""" + +from __future__ import annotations + +import asyncio +import json +from typing import Any + +from aiohttp import web + +from slime.agent.adapters import openai as _slime_openai +from slime.agent.adapters.openai import OpenAIAdapter +from slime.agent.adapters.common import ADAPTER_KEY, BaseAdapter, TOKENIZER_KEY, render_token_ids + + +def _dictify_tool_arguments(messages: list[dict]) -> None: + """In place: parse each tool call's JSON-string ``function.arguments`` into a + dict so ``apply_chat_template`` can iterate it. Idempotent; non-JSON left as-is.""" + for msg in messages: + for call in msg.get("tool_calls") or []: + fn = call.get("function") + if not isinstance(fn, dict): + continue + args = fn.get("arguments") + if isinstance(args, str): + s = args.strip() + if not s: + fn["arguments"] = {} + continue + try: + fn["arguments"] = json.loads(s) + except (json.JSONDecodeError, ValueError): + pass + + +def _template_ids(tok, messages: list[dict], *, add_generation_prompt: bool) -> list[int]: + """``apply_chat_template`` -> flat token-id list (tolerating the 1-element + batch that some transformers versions return for ``tokenize=True``).""" + enc = tok.apply_chat_template( + messages, tools=None, tokenize=True, add_generation_prompt=add_generation_prompt + ) + ids = enc["input_ids"] if hasattr(enc, "__getitem__") and "input_ids" in enc else enc + ids = list(ids) + if ids and isinstance(ids[0], list): # transformers>=5 may return [[...ids...]] + ids = ids[0] + return ids + + +def _tool_continuation_ids(new_messages: list[dict], tok) -> list[int]: + """Token delta to append after the previous turn's raw ``output_ids``: the + tool-result/user message(s) mini-swe added this turn, plus the next + generation prompt. + + The model's raw ``output_ids`` stop at ``<|im_end|>``; the chat template + emits ``<|im_end|>\\n`` for a finished assistant turn, so the delta must + restore that single inter-turn newline. We anchor on a sentinel user message + and slice from its trailing newline onward. + """ + continuation = _slime_openai._translate_chat_messages( + [m for m in new_messages if isinstance(m, dict) and m.get("role") != "assistant"] + ) + if not continuation: + return [] + sentinel = [{"role": "user", "content": ""}] + base = _template_ids(tok, sentinel, add_generation_prompt=False) + full = _template_ids(tok, sentinel + continuation, add_generation_prompt=True) + if len(base) < 1 or full[: len(base)] != base: + return [] # unexpected render shape -> caller falls back to a full re-render + return full[len(base) - 1 :] # start at the sentinel's trailing "\n" (the inter-turn separator) + + +def _build_prompt(target, messages: list[dict], tools_schema: list[dict] | None, kind: str, tok) -> list[int]: + """Build the next prompt. + + On the ``append`` path, splice the previous turn's **raw** ``output_ids`` + into the prompt instead of re-rendering the parsed assistant message. The + qwen3_coder parser strips trailing whitespace from tool-call arguments, so a + re-render is not token-identical to what the model generated; that mismatch + makes ``merge_turns`` log "prefix drift" and mask whole turns out of training + (and makes the rollout subtly off-policy). Splicing the raw tokens keeps the + prompt == the training target by construction. The parsed message is still + returned to mini-swe for tool execution -- only prompt reconstruction here + changes. ``new``/``wipe`` (and the first turn) fall back to a full re-render. + """ + new_messages = messages[target.seen_msgs :] if kind == "append" else [] + (_slime_openai._extend_chat_messages if kind == "append" else _slime_openai._replace_chat_messages)( + target, messages, tools_schema + ) + if kind == "append" and target.turns: + continuation = _tool_continuation_ids(new_messages, tok) + if continuation: + last = target.turns[-1] + return list(last.prompt_ids) + list(last.output_ids) + continuation + _dictify_tool_arguments(target.chat_messages) + return render_token_ids(target, tok) + + +async def _run_turn( + request: web.Request, body: dict, messages: list[dict] +): + """Mirror of ``openai._run_turn`` calling the dict-args ``_build_prompt``.""" + sid = _slime_openai._request_session_id(request, body) + adapter = request.app[ADAPTER_KEY] + if sid in adapter.closed: + raise web.HTTPServiceUnavailable(text="session closed") + app = request.app + s = adapter.store.setdefault(sid, _slime_openai.Session()) + task = asyncio.current_task() + adapter.inflight.setdefault(sid, set()).add(task) + try: + async with s.lock: + target = s.main + tools_schema = _slime_openai._normalize_tools(body.get("tools")) + kind = _slime_openai._select_kind(s, messages) + prompt_ids = _build_prompt(target, messages, tools_schema, kind, app[TOKENIZER_KEY]) + turn = await _slime_openai._generate(prompt_ids, s, body, app, session_id=sid) + parsed = _slime_openai._parse_turn(target, turn, app) + target.turns.append(turn) + return turn, parsed, len(prompt_ids), len(turn.output_ids) + finally: + adapter.inflight.get(sid, set()).discard(task) + + +async def _handle_chat_completions(request: web.Request) -> web.StreamResponse: + """Mirror of ``openai._handle_chat_completions`` via the dict-args ``_run_turn``.""" + body = await request.json() + messages = body.get("messages") or [] + if not isinstance(messages, list): + raise web.HTTPBadRequest(text="messages must be a list") + turn, parsed, in_tok, out_tok = await _run_turn(request, body, messages) + if body.get("stream"): + return await _slime_openai._stream_chat_completion( + request, body, parsed, turn.finish_reason, in_tok, out_tok + ) + return web.json_response( + _slime_openai._chat_completion_response(body, parsed, turn.finish_reason, in_tok, out_tok) + ) + + +class QwenOpenAIAdapter(OpenAIAdapter): + """``OpenAIAdapter`` rendering tool-call arguments as a dict (see module + docstring); only the ``/v1/chat/completions`` handler differs.""" + + def __init__(self, *, tokenizer, sglang_url, tool_parser=None, reasoning_parser=None) -> None: + # Skip OpenAIAdapter.__init__: it binds slime's string-args handler and + # aiohttp can't re-bind a route. Do BaseAdapter setup, then our routes. + BaseAdapter.__init__( + self, + tokenizer=tokenizer, + sglang_url=sglang_url, + tool_parser=tool_parser, + reasoning_parser=reasoning_parser, + ) + self.app.router.add_post("/v1/chat/completions", _handle_chat_completions) + self.app.router.add_post("/v1/responses", _slime_openai._handle_responses) # mini-swe unused + self.app.router.add_get("/healthz", _slime_openai._ok) + self.app.router.add_get("/v1/models", _slime_openai._ok) diff --git a/async_rl_research/agent/base.py b/async_rl_research/agent/base.py index e405b3dfc2..9861c70e2b 100644 --- a/async_rl_research/agent/base.py +++ b/async_rl_research/agent/base.py @@ -1,50 +1,14 @@ """AgentRuntime: the contract between ``generate.py`` and one agent framework. A *runtime* packages everything specific to one in-sandbox agent framework -(mini-swe-agent, opencode, pi, ...): which slime adapter speaks its wire -protocol, how to provision the agent inside the work sandbox, and how to -launch it. Everything else -- adapter/HTTP lifecycle, task workspace prep, -grading, trajectory merge -- is the generic recipe in ``generate.py`` plus -the active task env (``env/base.py``) and never changes per agent. - -The shared launch machinery lives HERE (not in a separate module) on purpose: -``_detached_run`` creates scratch files under ``workdir`` (launcher script, -done marker, log), and the entity that creates scratch must be the entity -that excludes it from the captured diff. ``diff_exclude_all`` = the base's -launch scratch + the subclass's ``diff_exclude``, so "forgot to exclude the -launcher" is structurally impossible. - -Writing a new runtime ---------------------- +(mini-swe-agent, opencode, ...): its slime adapter, provisioning, and launch. Subclass, declare the class attributes, implement ``run_agent`` by composing -the two helpers. Sketch of an opencode-style runtime:: - - class OpenCodeRuntime(AgentRuntime): - name = "opencode" - adapter_cls = OpenAIAdapter # or AnthropicAdapter - diff_exclude = ("opencode.json",) # extra scratch beyond launch files - - async def run_agent(self, sb, *, md, session_id, adapter_url, time_budget_sec): - await self._ensure_provisioned(sb, spec=..., marker_path=..., setup_script=...) - await sb.write_file(f"{md['workdir']}/opencode.json", _config(adapter_url)) - return await self._detached_run( - sb, workdir=md["workdir"], - command="opencode run ...", - env={"OPENAI_API_KEY": session_id}, - time_budget_sec=time_budget_sec, - ) - -Then register it in ``RUNTIMES`` below. - -On-policy rule every runtime must respect: the adapter applies the request -body OVER its per-session sampling defaults, so the agent must NOT send its -own temperature/top_p (a client-sent temperature silently turns rollouts -greedy -> zero-variance GRPO groups). Strip sampling knobs at the agent's -config layer (see mini_swe_agent's runner for the pattern). - -Runtimes are instantiated once per rollout worker (held by -``generate._State``) and must be stateless across samples -- ``run_agent`` -receives everything per-call. +``_ensure_provisioned`` + ``_detached_run``, and register in ``RUNTIMES`` below. + +On-policy rule: the adapter applies the request body OVER its per-session +sampling defaults, so a runtime must strip the agent's own temperature/top_p +(a client-sent temperature silently turns rollouts greedy). Runtimes are +instantiated once per worker and must be stateless across samples. """ from __future__ import annotations @@ -55,44 +19,40 @@ async def run_agent(self, sb, *, md, session_id, adapter_url, time_budget_sec): import shlex import time from abc import ABC, abstractmethod -from typing import ClassVar +from typing import ClassVar, NamedTuple from slime.agent.sandbox import Sandbox logger = logging.getLogger(__name__) -# run_agent return convention: the agent's exit code, or this when the -# wallclock budget elapsed before the done marker appeared. +# run_agent return value when the wallclock budget elapsed before the done +# marker appeared (otherwise run_agent returns the agent's exit code). EXIT_BUDGET_EXCEEDED = -2 +class AgentRunResult(NamedTuple): + """Outcome of one agent leg: the process ``exit_code`` (or + ``EXIT_BUDGET_EXCEEDED``) plus, on a nonzero exit, the last few KB of the + agent's stdout/stderr (``tail``; empty on a clean exit). + + Persisted into sample metadata so a zero-turn ``adapter_session_empty`` + self-explains in the dump (e.g. exit=137 -> OOM-killed) instead of relying + on tail-only Modal logs that age out once the run finishes. + """ + + exit_code: int + tail: str = "" + + class AgentRuntime(ABC): """One agent framework's integration: wire adapter + provision + launch. - Required class attributes (validated at class-definition time): - - name registry key / log prefix, e.g. "mini-swe" - adapter_cls slime adapter class for the agent's wire protocol - (OpenAIAdapter / AnthropicAdapter). generate._State - constructs it as adapter_cls(tokenizer=, sglang_url=, - tool_parser=, reasoning_parser=). - - Optional class attributes: - - model_name model name advertised to the agent ("slime-actor"); - the adapter ignores it (routes to the served actor) -- - clients only need it to pick the right API dialect. - scratch_prefix prefix for the launch scratch files _detached_run - writes under workdir (".agent" -> .agent_run.sh / - .agent_done / .agent_log). - diff_exclude EXTRA scratch files the runtime writes under workdir - (runner scripts, configs, prompt-convention artifacts - like mini-swe's "patch.txt"). Launch scratch is - excluded automatically -- do not repeat it here. - - ``generate.py`` only ever touches: ``adapter_cls``, ``run_agent``, - ``diff_exclude_all``, ``name``. + Required attributes (validated at class-definition time): ``name`` (registry + key / log prefix) and ``adapter_cls`` (slime adapter for the wire protocol). + Optional: ``model_name`` (advertised to the agent), ``scratch_prefix`` + (launch scratch prefix under workdir), ``diff_exclude`` (extra scratch to + drop from the diff; launch scratch is excluded automatically). """ name: ClassVar[str] @@ -102,8 +62,7 @@ class AgentRuntime(ABC): diff_exclude: ClassVar[tuple[str, ...]] = () def __init_subclass__(cls, **kwargs) -> None: - # Fail at import time, not mid-rollout: a runtime missing its - # declarations should never make it into a training run. + # Fail at import time, not mid-rollout, on missing declarations. super().__init_subclass__(**kwargs) missing = [a for a in ("name", "adapter_cls") if getattr(cls, a, None) is None] if missing: @@ -123,17 +82,13 @@ async def run_agent( session_id: str, adapter_url: str, time_budget_sec: int, - ) -> int: - """Provision + launch the agent in the already-booted, task-prepped sandbox. - - The workspace is ready (the active env applied its setup and wrote - ``PROBLEM_FILE``) -- implementations only set up their own agent. The - agent must target ``adapter_url`` for model calls and send - ``session_id`` as its auth/bearer so the adapter groups its turns. - ``md`` is the env-normalized dataset row (``RolloutEnv - .normalize_metadata``); may be called multiple times per sample in the - SAME sandbox (multi-step episodes). Returns the agent's exit code or - ``EXIT_BUDGET_EXCEEDED``. + ) -> AgentRunResult: + """Provision + launch the agent in the booted, task-prepped sandbox. + + The agent must call ``adapter_url`` with ``session_id`` as its bearer so + the adapter groups its turns. May be called multiple times per sample in + the SAME sandbox (multi-step). Returns an ``AgentRunResult`` (exit code, + or ``EXIT_BUDGET_EXCEEDED``, plus a failure-only log tail). """ # ------------------------------------------------------------------ @@ -154,26 +109,13 @@ async def _detached_run( time_budget_sec: int, poll_interval_sec: float = 5.0, log_tag: str = "", - ) -> int: - """Launch ``command`` detached in ``workdir`` and poll a done-marker. - - Writes a launcher script (cd + ``env`` exports + command, stdout/err - to the log file, exit code to the done marker) and starts it in its - own session (``setsid ... &``) so the exec RPC returns immediately - rather than streaming a multi-minute foreground command. This is NOT - Modal's ``Sandbox.detach()`` -- the sandbox object stays live. - - We avoid a long-lived foreground exec because a worker stream reset - mid-run would be classified transient and re-launch the whole agent - (see ModalSandbox._is_transient / _retry); the poll RPCs are short and - idempotent, so a dropped poll just retries (and each one counts as - sandbox activity if an idle_timeout is configured). On nonzero exit - the log tail is surfaced host-side so an in-sandbox crash is never - just an opaque exit code. - - ``command`` is spliced into the script verbatim -- the caller quotes - its own paths (shlex.quote). Returns the command's exit code, or - ``EXIT_BUDGET_EXCEEDED`` if ``time_budget_sec`` elapses first. + ) -> AgentRunResult: + """Launch ``command`` detached in ``workdir`` (``setsid ... &``) and poll + a done-marker, so a foreground exec stream reset can't re-launch the + whole agent. ``command`` is spliced in verbatim (caller quotes paths). + Returns an ``AgentRunResult``: the exit code (or ``EXIT_BUDGET_EXCEEDED`` + if ``time_budget_sec`` elapses first) plus, on a nonzero exit, the last + 4 KB of the agent log (surfaced host-side AND returned for the dump). """ q = shlex.quote launch, done, log = self._launch_scratch_files() @@ -186,12 +128,9 @@ async def _detached_run( f"echo $? > {q(done)}\n" ) await sb.write_file(f"{workdir}/{launch}", launcher_body) - # rm the done marker BEFORE launching: a multi-leg episode (e.g. the - # harbor env's multi-step tasks) relaunches in the same sandbox, and a - # stale marker from the previous leg would satisfy the first poll - # while the new agent is still running. + # rm the done marker BEFORE launching: a stale marker from a prior leg + # would satisfy the first poll while the new agent still runs. await sb.exec(f"cd {q(workdir)} && rm -f {q(done)} && chmod +x {q(launch)}", check=False, timeout=30) - # Detach so the exec RPC returns immediately; the marker file is the signal. await sb.exec( f"cd {q(workdir)} && setsid bash {q(launch)} < /dev/null > /dev/null 2>&1 &", check=False, @@ -210,12 +149,14 @@ async def _detached_run( except ValueError: exit_code = -1 break + tail = "" if exit_code != 0: - _, tail, _ = await sb.exec(f"tail -c 4000 {q(f'{workdir}/{log}')} 2>/dev/null", check=False, timeout=15) - if (tail or "").strip(): - logger.warning("[%s] %s exit=%s %s tail:\n%s", self.name, log_tag, exit_code, log, tail.strip()) + _, raw_tail, _ = await sb.exec(f"tail -c 4000 {q(f'{workdir}/{log}')} 2>/dev/null", check=False, timeout=15) + tail = (raw_tail or "").strip() + if tail: + logger.warning("[%s] %s exit=%s %s tail:\n%s", self.name, log_tag, exit_code, log, tail) logger.info("[%s] %s exit=%s elapsed<=%ds", self.name, log_tag, exit_code, time_budget_sec) - return exit_code + return AgentRunResult(exit_code, tail) async def _ensure_provisioned( self, @@ -229,17 +170,10 @@ async def _ensure_provisioned( ) -> bool: """Idempotent toolchain install keyed on a spec marker. - If ``marker_path`` already holds exactly ``spec`` (and ``check_cmd``, - when given, exits 0), the install is skipped -- this is what lets a - pre-baked derived image (or a previous boot) short-circuit, while a - changed pin rebuilds stale pre-baked toolchains instead of silently - running the old agent. Otherwise ``setup_script`` runs (bash, checked), - ``check_cmd`` re-verifies the result, and the marker is written LAST so - a half-finished install is never mistaken for a complete one. - - Returns True if provisioning ran, False on a marker hit. Setup - typically needs outbound network, which the default - ``block_network=False`` allows. + Skips when ``marker_path`` already holds exactly ``spec`` (and + ``check_cmd`` exits 0), so a pre-baked image short-circuits while a + changed pin rebuilds. The marker is written LAST so a half-finished + install is never mistaken for complete. Returns True if it ran. """ q = shlex.quote probe = f"cat {q(marker_path)} 2>/dev/null" @@ -262,8 +196,7 @@ async def _ensure_provisioned( # --------------------------------------------------------------------------- DEFAULT_RUNTIME = "mini-swe" -# Short name -> "module:Class". Values are strings (not classes) so importing -# base.py never imports any runtime module. +# Short name -> "module:Class" (strings so importing base.py imports no runtime). RUNTIMES: dict[str, str] = { "mini-swe": "async_rl_research.agent.mini_swe_agent:MiniSweAgentRuntime", } @@ -272,11 +205,8 @@ async def _ensure_provisioned( def load_runtime(spec: str | None = None) -> AgentRuntime: """Resolve ``spec`` to an AgentRuntime instance, validating eagerly. - Accepted forms: - * registry short name "mini-swe" - * explicit class "pkg.module:ClassName" - * module path exposing RUNTIME "pkg.module" (legacy driver form; - what existing ASYNC_RL_AGENT_DRIVER configs pass) + Accepts a registry short name ("mini-swe"), "pkg.module:ClassName", or a + module path exposing ``RUNTIME`` (legacy driver form). """ spec = spec or DEFAULT_RUNTIME target = RUNTIMES.get(spec, spec) diff --git a/async_rl_research/agent/config/universal.yaml b/async_rl_research/agent/config/universal.yaml index 5fb1355afe..a06f734e24 100644 --- a/async_rl_research/agent/config/universal.yaml +++ b/async_rl_research/agent/config/universal.yaml @@ -44,21 +44,18 @@ agent: Each response should include: 1. **Reasoning text** where you explain your analysis and plan - 2. At least one tool call with your command + 2. At least one call to the `bash` tool with the shell command to run **CRITICAL REQUIREMENTS:** - Your response SHOULD include reasoning text explaining what you're doing - - Your response MUST include AT LEAST ONE bash tool call + - Your response MUST include AT LEAST ONE call to the `bash` tool - Directory or environment variable changes are not persistent. Every action is executed in a new subshell. - However, you can prefix any action with `MY_ENV_VAR=MY_VALUE cd /path/to/working/dir && ...` or write/load environment variables from files - Example of a CORRECT response: - - I need to understand the structure of the repository first. Let me check what files are in the current directory to get a better understanding of the codebase. - - [Makes bash tool call with {"command": "ls -la"} as arguments] - + Example of a CORRECT response: a short paragraph of reasoning ("I'll look at + the repo layout first."), followed by a call to the `bash` tool whose command + is `ls -la`. ## Environment Details @@ -71,40 +68,19 @@ agent: {{system}} {{release}} {{version}} {{machine}} - ## Useful command examples - - ### Create a new file: - - ```bash - cat <<'EOF' > newfile.py - import numpy as np - hello = "world" - print(hello) - EOF - ``` - - ### Edit files with sed: - - ```bash - # Replace all occurrences - sed -i 's/old_string/new_string/g' filename.py - - # Replace only first occurrence - sed -i 's/old_string/new_string/' filename.py - - # Replace first occurrence on line 1 - sed -i '1s/old_string/new_string/' filename.py - - # Replace all occurrences in lines 1-10 - sed -i '1,10s/old_string/new_string/g' filename.py - ``` + ## Useful shell idioms - ### View file content: + The following are commands you pass to the `bash` tool (its `command` + argument) -- they are NOT a response format. Adapt as needed. - ```bash - # View specific lines with numbers - nl -ba filename.py | sed -n '10,20p' - ``` + - Create a file with a heredoc: + cat <<'EOF' > newfile.py + import numpy as np + print("hello") + EOF + - Edit in place with sed: `sed -i 's/old/new/g' filename.py` (drop the `g` + for first-occurrence-only; prefix a range like `1,10` to scope to lines). + - View specific lines with numbers: `nl -ba filename.py | sed -n '10,20p'`. ## Finishing @@ -152,7 +128,8 @@ model: {{ output.output[-5000:] }} {%- endif -%} - format_error_template: | + format_error_template: |- + Tool call error: @@ -169,5 +146,6 @@ model: If you want to end the task, please issue the following command: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT` without any other command. + model_kwargs: drop_params: true diff --git a/async_rl_research/agent/mini_swe_agent.py b/async_rl_research/agent/mini_swe_agent.py index 0bd9f7c526..e75172c784 100644 --- a/async_rl_research/agent/mini_swe_agent.py +++ b/async_rl_research/agent/mini_swe_agent.py @@ -1,38 +1,11 @@ """mini-swe-agent runtime (the default AgentRuntime). -The contract + shared machinery (detached launch/poll, idempotent -provisioning) live in ``agent/base.py``; the generic rollout recipe lives in -``async_rl_research.generate`` and the per-task-family envs in -``async_rl_research.environment``. By the time ``run_agent`` is called the workspace -is already task-prepped (the active env applied its setup and wrote -``PROBLEM_FILE``). Here we own only what is unique to mini-swe-agent: - - * which adapter speaks this agent's wire protocol (mini-swe-agent talks - to litellm's OpenAI-compatible API, so we intercept with OpenAIAdapter) - * the in-sandbox **headless runner** (``MINI_RUNNER_PY``) -- stock - mini-swe-agent (LitellmModel + LocalEnvironment + DefaultAgent) wired so - every model call dials back to the slime adapter - * the isolated uv-venv provisioning spec (a standalone py3.11 + - ``mini-swe-agent`` that never touches the image's testbed conda env) - * the launch env/command wiring (litellm env vars, ``-P`` safe path) - -Token capture + loss masking happen entirely host-side in the adapter; the -runner is "dumb" and never sees token ids. mini-swe-agent runs UNMODIFIED at -its public OpenAI boundary. - -Design A wire flow per turn:: - - in-sandbox: litellm.completion(messages, tools=[BASH_TOOL]) - -> POST {adapter_url}/v1/chat/completions Bearer - host adapter: render messages -> input_ids -> SGLang /generate - (return_logprob) -> record TurnRecord -> OpenAI JSON back - in-sandbox: run bash tool-call locally -> append observation -> loop - -mini-swe-agent v2 drives bash through NATIVE tool-calls, so the adapter MUST -be given the served model's sglang tool-call parser or every response looks -tool-less and the agent format-errors in a loop. Set the matching parsers on -the launcher, e.g. ``--sglang-tool-call-parser qwen25`` (Qwen3 emits -hermes-style ```` JSON) and ``--sglang-reasoning-parser qwen3``. +Runs stock mini-swe-agent (v2) headless inside the sandbox in an isolated +uv-venv, with every model call dialing back to the slime adapter over litellm's +OpenAI-compatible API. Token capture + loss masking happen host-side; the +runner never sees token ids. The adapter MUST use the served model's sglang +tool-call parser (v2 drives bash via native tool-calls), e.g. +``--sglang-tool-call-parser qwen25 --sglang-reasoning-parser qwen3``. """ from __future__ import annotations @@ -41,71 +14,44 @@ import shlex from pathlib import Path -from slime.agent.adapters import OpenAIAdapter from slime.agent.sandbox import Sandbox -from .base import AgentRuntime +from .base import AgentRunResult, AgentRuntime +# Renders tool-call arguments as a dict so Qwen3.6's qwen3_coder chat template +# doesn't crash on turn 2+ (safe for hermes-style Qwen3 too). +from .adapters import QwenOpenAIAdapter -# Task-layer constant: the active env writes the problem statement here -# before run_agent is called; the runner reads it via MSWE_PROBLEM_FILE. from ..environment.base import PROBLEM_FILE -# --- mini-swe-agent-specific knobs ------------------------------------------ MSWE_STEP_LIMIT = int(os.environ.get("MSWE_STEP_LIMIT", "50")) -# Which YAML config (prompts!) the runner loads. The DEFAULT is the repo-owned -# universal config below, uploaded into the sandbox -- ONE prompt scaffold for -# all task families; the task-specific deliverable lives in the instruction -# text the env writes (see config/universal.yaml's scope rule). Override -# ladder (both name a BUILTIN config relative to the package's config dir, -# e.g. "mini.yaml" or "benchmarks/swebench.yaml"): -# MSWE_CONFIG env (global, experiments) > metadata.agent_config (per-row) -# > the uploaded universal config. +# Which YAML config (prompts) the runner loads. Override ladder: MSWE_CONFIG +# env (global) > metadata.agent_config (per-row) > universal config below. MSWE_CONFIG = os.environ.get("MSWE_CONFIG", "") -# Read at import: the prompt scaffold ships with this module and must be -# identical for every rollout in a run. +# Read at import: the scaffold must be identical for every rollout in a run. UNIVERSAL_CONFIG_YAML = (Path(__file__).parent / "config" / "universal.yaml").read_text(encoding="utf-8") -# Exact-pinned: the scaffold's prompts + wire protocol are part of the RL task -# distribution (a PyPI drift mid-experiment silently changes the environment), +# Exact-pinned: prompts + wire protocol are part of the RL task distribution, # and MINI_RUNNER_PY below is written against the v2 API. MSWE_PIP_SPEC = os.environ.get("MSWE_PIP_SPEC", "mini-swe-agent==2.3.1") -# Prepended to PATH for the agent's bash commands (runner keeps only the dirs -# that exist in the image). LocalEnvironment runs commands via /bin/sh, so the -# swebench images' bashrc-based `conda activate testbed` never fires; putting -# the testbed env's bin first is how its python/pytest win. +# Prepended to PATH for the agent's bash commands: LocalEnvironment runs via +# /bin/sh so `conda activate testbed` never fires; this is how its python wins. MSWE_PATH_PREPEND = os.environ.get( "MSWE_PATH_PREPEND", "/opt/miniconda3/envs/testbed/bin:/opt/miniconda3/bin" ) -# The agent runs in an isolated venv so the testbed conda env (often pinned to -# an old python) is never used or clobbered. Provisioned at boot with uv; can be -# pre-baked into a derived image (presence of the venv interpreter + matching -# spec marker skips it). +# Isolated venv so the testbed conda env is never used or clobbered. Provisioned +# at boot with uv; can be pre-baked into a derived image. MSWE_AGENT_VENV = os.environ.get("MSWE_AGENT_VENV", "/opt/mswe-agent") MSWE_AGENT_PYTHON_VERSION = os.environ.get("MSWE_AGENT_PYTHON_VERSION", "3.11") _VENV_PY = f"{MSWE_AGENT_VENV}/bin/python" -# Runtime scratch under workdir beyond the base's launch files (which keep -# their historical .mswe_* names via scratch_prefix below). _RUNNER = ".mswe_runner.py" _CONFIG_FILE = ".mswe_config.yaml" -# --------------------------------------------------------------------------- -# Headless in-sandbox runner. -# -# Written against mini-swe-agent v2 (exact-pinned via MSWE_PIP_SPEC). The v2 -# specifics this depends on: prompts come from a YAML config in the same -# schema as the packaged ones (we upload the repo-owned universal config; -# builtins remain reachable via the MSWE_CONFIG override ladder), bash is -# driven through NATIVE tool-calls, and cost tracking hard-fails on models -# litellm cannot price (hence cost_tracking="ignore_errors"). The wiring that -# must hold regardless of version: litellm points at the slime adapter -# (OPENAI_API_BASE/OPENAI_API_KEY), and NO sampling knobs reach the request -# body -- the adapter applies the body OVER its per-session defaults, so a -# client-sent temperature would silently turn rollouts greedy (zero-variance -# GRPO groups). -# --------------------------------------------------------------------------- +# Headless in-sandbox runner (mini-swe-agent v2, exact-pinned). NO sampling +# knobs reach the request body -- the adapter applies it OVER its per-session +# defaults, so a client-sent temperature would silently turn rollouts greedy. MINI_RUNNER_PY = r'''"""Headless mini-swe-agent (v2) runner -- runs INSIDE the sandbox (design A).""" import os import sys @@ -126,12 +72,9 @@ from minisweagent.environments.local import LocalEnvironment from minisweagent.models.litellm_model import LitellmModel - # Prompt config: the host uploads the repo-owned UNIVERSAL config next to - # this runner (MSWE_CONFIG_FILE) -- the default for every task family. - # MSWE_CONFIG, when set (global env override or a row's agent_config), - # names a BUILTIN packaged config instead. Read the builtin path directly - # -- get_config_from_spec() would also try cwd-relative candidates, which - # a repo file could shadow. + # Default to the uploaded universal config; MSWE_CONFIG (if set) names a + # BUILTIN packaged config. Read the builtin path directly -- the spec helper + # would also try cwd-relative candidates a repo file could shadow. cfg_path = Path(os.environ["MSWE_CONFIG_FILE"]) builtin = os.environ.get("MSWE_CONFIG", "") if builtin: @@ -145,10 +88,8 @@ model_cfg = dict(cfg.get("model") or {}) env_cfg = dict(cfg.get("environment") or {}) - # api_base / api_key come from OPENAI_API_BASE / OPENAI_API_KEY in the env - # (litellm's openai provider reads them). The bundled config pins - # temperature=0.0 for benchmarking -- strip all sampling knobs so the - # adapter's per-session defaults (training's temperature) stay in force. + # Strip all sampling knobs (the config pins temperature=0.0 for + # benchmarking) so the adapter's per-session defaults stay in force. model_kwargs = dict(model_cfg.get("model_kwargs") or {}) model_kwargs.pop("temperature", None) model_kwargs.pop("top_p", None) @@ -159,11 +100,10 @@ # would raise on the first successful completion. cost_tracking="ignore_errors", ) - agent_cfg.update(step_limit=STEP_LIMIT, cost_limit=0.0) # 0 disables the cost check + agent_cfg.update(step_limit=STEP_LIMIT, cost_limit=0.0) - # The agent's bash commands run via /bin/sh (dash on these images), so the - # config's BASH_ENV-based conda activation never fires; prepend the testbed - # env's bin dirs onto PATH instead. config.env wins over os.environ. + # Prepend the testbed env's bin dirs onto PATH (config.env wins over + # os.environ); conda activation never fires under /bin/sh. env_overrides = dict(env_cfg.get("env") or {}) prepend = [p for p in PATH_PREPEND.split(":") if p and os.path.isdir(p)] if prepend: @@ -183,47 +123,65 @@ ''' -# Provision mini-swe-agent into an isolated py3.11 uv venv. uv resolves and -# installs a standalone interpreter, so we don't depend on the image shipping -# py3.11 (prefer a baked uv; fall back to the astral installer). The base -# writes the spec marker only after this script AND the import check succeed. +# Provision mini-swe-agent into an isolated py3.11 uv venv. uv is our package +# manager; we NEVER fall back to the image's pip, because many task images +# (SWE-bench-Pro) ship a poisoned PIP_INDEX_URL pointing at a dead build-time +# mirror -> `pip install uv` hits "Connection refused .../uv/". Instead, mirror +# harbor's bootstrap: ensure curl via the OS package manager, install uv from +# the pinned astral script (falling back to pip ONLY with an explicit PyPI +# index), and retry network steps to absorb transient GitHub release resets. +# Measured across all 731 SWE-bench-Pro images at conc 50: 99.7% success (vs the +# original pip-fallback's ~52% on no-curl images). See +# profiles/PROVISIONING_VENV_VS_VOLUME.md. _VENV_SETUP = ( + "set -e\n" 'export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"\n' + 'retry() { for i in 1 2 3; do bash -c "$1" && return 0; sleep $((i*4)); done; return 1; }\n' "if ! command -v uv >/dev/null 2>&1; then\n" - " if command -v curl >/dev/null 2>&1; then\n" - " curl -LsSf https://astral.sh/uv/install.sh | sh\n" - ' export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"\n' - " else\n" - " # slim images (e.g. harbor's python:*-slim) ship neither curl nor\n" - " # wget, but they do ship pip; uv's PyPI wheel lands on PATH.\n" - " python3 -m pip install --quiet uv\n" - " fi\n" + # Ensure curl via whatever package manager the image ships (best-effort: + # if none works we still try the pip path below). + " command -v curl >/dev/null 2>&1 || retry \"apt-get update && apt-get install -y curl" + " || apk add --no-cache curl || yum install -y curl || dnf install -y curl\" || true\n" + # uv via the pinned astral script (bypasses the image's pip entirely); + # pip fallback forces a clean PyPI index so a poisoned image config can't win, + # then retries with --break-system-packages so PEP 668 ("externally-managed") + # images don't hard-fail. Try plain first: older pip (<23) lacks the flag but + # also doesn't enforce PEP 668, so the plain attempt already succeeds there. + ' retry "curl -LsSf https://astral.sh/uv/0.7.13/install.sh | sh"' + ' || retry "python3 -m pip install --index-url https://pypi.org/simple --root-user-action=ignore uv' + ' || python3 -m pip install --index-url https://pypi.org/simple --root-user-action=ignore --break-system-packages uv"\n' + ' export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"\n' "fi\n" f"rm -rf {shlex.quote(MSWE_AGENT_VENV)}\n" - f"uv venv --python {shlex.quote(MSWE_AGENT_PYTHON_VERSION)} {shlex.quote(MSWE_AGENT_VENV)}\n" - f"uv pip install --python {shlex.quote(_VENV_PY)} {shlex.quote(MSWE_PIP_SPEC)}\n" + f'retry "uv venv --python {MSWE_AGENT_PYTHON_VERSION} {shlex.quote(MSWE_AGENT_VENV)}"\n' + f'retry "uv pip install --python {shlex.quote(_VENV_PY)} {shlex.quote(MSWE_PIP_SPEC)}"\n' + # pydantic_core's compiled native module intermittently fails to land in the + # venv (partial wheel from a corrupt uv cache / index contention at high + # conc) -> the agent dies at `import minisweagent.agents.default` with zero + # turns (adapter_session_empty; ~46% of gravitational/teleport on one eval). + # Verify the agent's REAL import and force a clean reinstall (--reinstall + # --no-cache) to repair the partial wheel; a still-broken venv then fails + # LOUDLY here (set -e -> CalledProcessError) instead of as a silent empty. + f"for i in 1 2 3; do MSWEA_SILENT_STARTUP=1 {shlex.quote(_VENV_PY)} -c 'import minisweagent.agents.default' 2>/dev/null && break;" + f' retry "uv pip install --python {shlex.quote(_VENV_PY)} --reinstall --no-cache {shlex.quote(MSWE_PIP_SPEC)}" || true; done\n' + f"MSWEA_SILENT_STARTUP=1 {shlex.quote(_VENV_PY)} -c 'import minisweagent.agents.default'\n" ) # MSWEA_SILENT_STARTUP suppresses the import-time banner that would otherwise -# corrupt the provisioning probe's marker comparison (and litter the run log). -_VENV_CHECK = f"MSWEA_SILENT_STARTUP=1 {shlex.quote(_VENV_PY)} -c 'import minisweagent'" +# corrupt the provisioning probe's marker comparison. Import the agent's real +# entrypoint (not just the top package) so the probe also rejects a pre-baked +# venv whose pydantic_core native module is missing -> re-provision instead of +# launching a doomed agent. +_VENV_CHECK = f"MSWEA_SILENT_STARTUP=1 {shlex.quote(_VENV_PY)} -c 'import minisweagent.agents.default'" class MiniSweAgentRuntime(AgentRuntime): name = "mini-swe" - adapter_cls = OpenAIAdapter - # Advertised to litellm as "openai/". The adapter ignores the - # name (it routes to the SGLang-served actor); litellm only needs the - # provider prefix so it speaks the OpenAI dialect at our adapter_url. + adapter_cls = QwenOpenAIAdapter model_name = "slime-actor" - # Keep the historical scratch names (.mswe_run.sh / .mswe_done / .mswe_log). scratch_prefix = ".mswe" - # "patch.txt" is the submission artifact the builtin swebench prompt - # instructs the agent to create -- the universal config doesn't, but an - # MSWE_CONFIG/agent_config override back to the builtin would, and - # `git add -N .` would sweep it into the diff. (Launch scratch + the - # task-layer PROBLEM_FILE are excluded by the base / by the swe_gym env's - # git_diff itself.) + # "patch.txt": submission artifact the builtin swebench prompt tells the + # agent to create, which `git add -N .` would otherwise sweep into the diff. diff_exclude = (_RUNNER, _CONFIG_FILE, "patch.txt") async def run_agent( @@ -234,7 +192,7 @@ async def run_agent( session_id: str, adapter_url: str, time_budget_sec: int, - ) -> int: + ) -> AgentRunResult: """Provision mini-swe-agent in ``sb``, run it on the task, poll to done.""" workdir = md["workdir"] await sb.write_file(f"{workdir}/{_RUNNER}", MINI_RUNNER_PY) @@ -249,30 +207,21 @@ async def run_agent( base = f"{adapter_url}/v1" env = { - # litellm's openai provider reads these for base URL + bearer auth. "OPENAI_API_BASE": base, "OPENAI_BASE_URL": base, "OPENAI_API_KEY": session_id, "MSWE_MODEL": self.model_name, "MSWE_WORKDIR": workdir, "MSWE_PROBLEM_FILE": f"{workdir}/{PROBLEM_FILE}", - # Override ladder: global env > per-row builtin override > the - # uploaded universal config ("" -> the runner uses MSWE_CONFIG_FILE). + # Override ladder: global env > per-row builtin > universal config. "MSWE_CONFIG": MSWE_CONFIG or md.get("agent_config") or "", "MSWE_CONFIG_FILE": f"{workdir}/{_CONFIG_FILE}", "MSWE_STEP_LIMIT": str(MSWE_STEP_LIMIT), "MSWE_PATH_PREPEND": MSWE_PATH_PREPEND, - # keep the v2 import-time banner out of the runner log. "MSWEA_SILENT_STARTUP": "1", } - # Run the agent with the ISOLATED venv interpreter. The runner still - # cd's into workdir + uses LocalEnvironment, so the agent's bash - # tool-calls (tests, git) run against the repo -- with the testbed - # env's bin on PATH (MSWE_PATH_PREPEND) -- only the agent process - # itself is isolated. -P (safe path, py3.11+) keeps the script dir - # (= workdir) off sys.path: a repo that shares a name with an agent - # dep (e.g. the pydantic instances) would otherwise shadow the venv's - # copy and crash the runner at import time, before any model call. + # Run with the ISOLATED venv interpreter; -P keeps the workdir off + # sys.path so a repo sharing a name with an agent dep can't shadow it. return await self._detached_run( sb, workdir=workdir, @@ -283,6 +232,5 @@ async def run_agent( ) -# Module export for dotted-module-path loading (legacy ASYNC_RL_AGENT_DRIVER -# configs pass "async_rl_research.agent.mini_swe_agent"; see base.load_runtime). +# Module export for dotted-module-path loading (see load_runtime). RUNTIME = MiniSweAgentRuntime diff --git a/async_rl_research/aiohttp_threaded.py b/async_rl_research/aiohttp_threaded.py index 2b9d12c87f..fc9f737080 100644 --- a/async_rl_research/aiohttp_threaded.py +++ b/async_rl_research/aiohttp_threaded.py @@ -1,11 +1,6 @@ -"""Run an ``aiohttp.web.Application`` in a background daemon thread. - -This is the "http" piece: ``generate.py`` builds the slime adapter (an -``aiohttp`` app that speaks the agent's wire API on the front and SGLang -``/generate`` on the back) and serves it here, on a daemon thread, so the -synchronous slime rollout loop and the in-sandbox agent's HTTP callbacks can -run concurrently. Verbatim copy of -``examples/coding_agent_rl/aiohttp_threaded.py`` (generic, no SWE specifics). +"""Run an ``aiohttp.web.Application`` in a background daemon thread, so the +synchronous slime rollout loop and the agent's HTTP callbacks run concurrently. +Verbatim copy of ``examples/coding_agent_rl/aiohttp_threaded.py``. """ from __future__ import annotations @@ -54,9 +49,7 @@ def run_app_in_thread( ) -> AppHandle: """Spin up ``app`` on a daemon thread; block until it is listening. - ``runner_kwargs`` is forwarded to ``web.AppRunner`` (e.g. pass - ``{"handler_cancellation": True}`` to make a client disconnect cancel - the in-flight handler coroutine). + ``runner_kwargs`` is forwarded to ``web.AppRunner``. """ started = threading.Event() err_box: list[BaseException] = [] diff --git a/async_rl_research/data/.gitignore b/async_rl_research/data/.gitignore deleted file mode 100644 index 4c5605f5bf..0000000000 --- a/async_rl_research/data/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# Generated datasets are reproducible via env/convert2slime/*.py; don't commit them. -# Keep only the small hand-written example. -*.jsonl -!example.jsonl -# materialized harbor datasets (tasks/ copies + downloads) -*/ diff --git a/async_rl_research/data/README.md b/async_rl_research/data/README.md deleted file mode 100644 index 0b6821b9c2..0000000000 --- a/async_rl_research/data/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Prompt data (artifacts only) - -This directory holds generated dataset artifacts (`*.jsonl`, materialized -harbor task dirs). The converter code lives in -[`../env/convert2slime/`](../env/convert2slime/) — one converter per task -env, paired by filename (`env/swe_gym.py` ↔ `env/convert2slime/swe_gym.py`). - -## SWE-Gym - -```bash -python -m async_rl_research.env.convert2slime.swe_gym --lite --out async_rl_research/data/swe_gym_lite.jsonl -python -m async_rl_research.env.convert2slime.swe_gym --input raw_swe_gym.jsonl --out swe_gym.jsonl -``` - -Rows carry no `metadata.task_type` (SWE-Gym is the default env). Each row: -`prompt` = problem statement; `metadata` = `instance_id`, prebuilt `image`, -`workdir`, `eval_cmd`/`swepro`, `pre_commands`. See the converter docstring -for the field-by-field mapping. - -## Harbor (USACO, ...) - -```bash -# from a local harbor task tree (e.g. a harbor-datasets checkout subtree) -python -m async_rl_research.env.convert2slime.harbor \ - --tasks-dir ~/harbor-datasets/datasets/usaco --out-dir async_rl_research/data/usaco - -# or straight from a harbor registry (needs `pip install harbor`) -python -m async_rl_research.env.convert2slime.harbor \ - --registry ~/harbor/registry.json --dataset usaco --out-dir async_rl_research/data/usaco -``` - -Rows carry `metadata.task_type: "harbor"` and a `task_path` relative to the -out dir; the rollout resolves it via `ASYNC_RL_TASK_ROOT=`. Put the -out dir on the slime-data volume so head workers can read the task files. -Before training, sanity-check the plumbing with the reference solutions (no -model involved): - -```bash -export ASYNC_RL_TASK_ROOT=$(pwd)/async_rl_research/data/usaco -python -m async_rl_research.env.harbor $ASYNC_RL_TASK_ROOT/usaco.jsonl --limit 3 # expect reward=1.0 -``` diff --git a/async_rl_research/environment/base.py b/async_rl_research/environment/base.py index 093dab8dbd..bd5b278a03 100644 --- a/async_rl_research/environment/base.py +++ b/async_rl_research/environment/base.py @@ -1,35 +1,18 @@ """RolloutEnv: the contract between ``generate.py`` and one task family. -An *env* packages everything specific to one task/dataset family (SWE-Gym, -harbor, ...): how to validate a dataset row, how to boot + prepare the task -sandbox, how to drive the agent across the task's step(s), and how to grade -the result into a reward. Everything else -- adapter/HTTP lifecycle, session -management, trajectory merge, abort/timeout isolation -- is the generic -recipe in ``generate.py`` and never changes per task family. - -This mirrors ``agent/base.py``'s AgentRuntime exactly: one ``generate()`` -orchestrates ``runtime x env``. The runtime knows *which agent* runs and how -to launch it; the env knows *what task* it runs on and what reward it earned. - -Schema-pair convention ----------------------- -``env/.py`` and ``env/convert2slime/.py`` are a pair: the -converter is the ONLY writer of the ``metadata`` dict for task type -```` and the env's ``normalize_metadata`` is the only reader. When a -field is added, both edits land in sibling files. Rows select their env via -``metadata.task_type`` (absent -> ``swe_gym``, the historical schema). - -Writing a new env ------------------ -Subclass, declare ``name``, implement ``normalize_metadata`` + ``rollout``, -register in ``ENVS``. ``rollout`` owns the sandbox lifecycle end-to-end -(boot, prep, agent run(s), grading) so each family can sequence them freely: -SWE grades a captured diff in a separate CLEAN sandbox after the work sandbox -closes; harbor verifies in-place inside the still-open agent sandbox (and -loops over steps for multi-step tasks). - -Envs are instantiated once per rollout worker (cached by ``load_env``) and -must be stateless across samples -- ``rollout`` receives everything per-call. +An *env* packages everything task-family-specific (SWE-Gym, harbor, ...): row +validation, sandbox boot/prep, driving the agent across step(s), grading into a +reward. Mirrors ``agent/base.py``'s AgentRuntime: one ``generate()`` +orchestrates ``runtime x env``. + +Schema-pair convention: ``env/.py`` and ``env/convert2slime/.py`` +are paired -- the converter is the only writer of the ``metadata`` dict and +``normalize_metadata`` the only reader. Rows select their env via +``metadata.task_type`` (absent -> ``swe_gym``). + +Writing a new env: subclass, declare ``name``, implement ``normalize_metadata`` ++ ``rollout``, register in ``ENVS``. ``rollout`` owns the whole sandbox +lifecycle. Envs are cached once per worker and must be stateless across samples. """ from __future__ import annotations @@ -48,9 +31,7 @@ logger = logging.getLogger(__name__) -# Task-layer artifact shared by all envs: the problem statement lands in the -# workdir (agents/runners read it from disk via MSWE_PROBLEM_FILE etc.) and is -# always excluded from any captured diff. +# Problem statement written to the workdir; always excluded from captured diffs. PROBLEM_FILE = "PROBLEM_STATEMENT.md" @@ -60,12 +41,9 @@ class EnvMetadataError(ValueError): @dataclass(frozen=True) class RewardResult: - """What an env's ``rollout`` hands back to the trajectory merge. - - ``reward`` is the scalar training signal; ``is_solved`` the boolean used - for run-level solve-rate logging; ``extra`` is env-specific diagnostics - merged into the trajectory metadata (SWE: ``applied_cleanly``; harbor: - the raw rewards dict + per-step results). + """What an env's ``rollout`` hands back to the trajectory merge: a scalar + ``reward``, an ``is_solved`` flag for solve-rate logging, and ``extra`` + env-specific diagnostics merged into trajectory metadata. """ reward: float @@ -83,19 +61,17 @@ class RolloutEnv(ABC): name: ClassVar[str] def __init_subclass__(cls, **kwargs) -> None: - # Fail at import time, not mid-rollout (same rule as AgentRuntime). + # Fail at import time, not mid-rollout. super().__init_subclass__(**kwargs) if getattr(cls, "name", None) is None: raise TypeError(f"{cls.__name__} must define class attribute 'name' (see RolloutEnv)") @abstractmethod def normalize_metadata(self, sample) -> dict[str, Any]: - """Normalize one dataset row (a slime ``Sample``) into the env's md dict. + """Normalize one dataset row (slime ``Sample``) into the env's md dict. - Must include ``instance_id`` (used for session ids + logging) and - ``workdir``/``agent_config`` if the agent runtime is expected to read - them. Raises ``EnvMetadataError`` (str = abort reason) for rows this - env cannot run. + Must include ``instance_id``, plus ``workdir``/``agent_config`` if the + runtime reads them. Raises ``EnvMetadataError`` for unrunnable rows. """ @abstractmethod @@ -111,16 +87,22 @@ async def rollout( ) -> RewardResult: """Run the full task episode: boot, prep, agent run(s), grading. - ``runtime`` is the active ``agent.base.AgentRuntime``; call - ``runtime.run_agent(sb, md=, session_id=, adapter_url=, - time_budget_sec=)`` for each agent leg (all legs share the one - adapter session, so a multi-step episode is still one trajectory). + Call ``runtime.run_agent`` per agent leg; all legs share one adapter + session so a multi-step episode stays one trajectory. ``agent_time_budget_sec`` bounds TOTAL agent wallclock across legs; - ``eval_timeout_sec`` caps each grading command. The caller wraps this - whole coroutine in a wall-clock guard -- exceeding - budget + eval + slack aborts the sample. + ``eval_timeout_sec`` caps each grading command. """ + def effective_budgets(self, md: dict[str, Any], *, agent_time_budget_sec: int, eval_timeout_sec: int) -> dict[str, int]: + """Wall-clock budgets actually enforced this rollout (for the dump/dashboard).""" + from ..modal_sandbox import ModalSandbox + + return { + "boot_sec": ModalSandbox._boot_timeout_from_env(), + "agent_sec": agent_time_budget_sec, + "eval_sec": eval_timeout_sec, + } + # ------------------------------------------------------------------ # Shared sandbox helpers # ------------------------------------------------------------------ @@ -130,11 +112,8 @@ async def write_problem_file(sb, workdir: str, text: str | None) -> None: @staticmethod async def upload_dir(sb, host_dir: str | Path, sandbox_dir: str) -> None: - """Copy a host directory's CONTENTS into ``sandbox_dir`` (created fresh). - - Ships one gzipped tar through ``write_file`` instead of N round-trips; - task tests/solution dirs are small, so in-memory packing is fine. - """ + """Copy a host dir's CONTENTS into a fresh ``sandbox_dir`` via one + gzipped tar (task tests/solution dirs are small).""" host_dir = Path(host_dir) buf = io.BytesIO() # mtime=0 so re-uploads of identical content are byte-identical. @@ -171,8 +150,7 @@ def coerce_prompt(prompt) -> str: # --------------------------------------------------------------------------- DEFAULT_ENV = "swe_gym" -# task_type -> "module:Class". Values are strings so importing base.py never -# imports any env module (env modules pull in provider backends). +# task_type -> "module:Class" (strings so importing base.py imports no env module). ENVS: dict[str, str] = { "swe_gym": "async_rl_research.environment.swe_gym:SweGymEnv", "harbor": "async_rl_research.environment.harbor:HarborEnv", @@ -182,11 +160,10 @@ def coerce_prompt(prompt) -> str: def load_env(spec: str | None = None) -> RolloutEnv: - """Resolve ``spec`` (a row's ``metadata.task_type``) to a cached env instance. + """Resolve ``spec`` (a row's ``metadata.task_type``) to a cached env. - Accepted forms: a registry short name ("harbor"), or "pkg.module:Class" - for out-of-tree envs. Absent -> ``DEFAULT_ENV`` (the historical SWE rows - carry no ``task_type``). + Accepts a registry short name ("harbor") or "pkg.module:Class"; absent -> + ``DEFAULT_ENV``. """ spec = spec or DEFAULT_ENV cached = _ENV_CACHE.get(spec) diff --git a/async_rl_research/environment/convert2slime/__init__.py b/async_rl_research/environment/convert2slime/__init__.py index ac19529042..b556da7301 100644 --- a/async_rl_research/environment/convert2slime/__init__.py +++ b/async_rl_research/environment/convert2slime/__init__.py @@ -1,7 +1,5 @@ """Dataset converters: one per env, paired by filename (see env/base.py). ``env/convert2slime/.py`` is the only writer of the ``metadata`` schema -that ``env/.py`` reads. Converters run offline (laptop or head node) -and may carry heavy/optional dependencies (``datasets``, ``harbor``) that the -rollout runtime never imports. +``env/.py`` reads. Run offline; may carry heavy deps the rollout never imports. """ diff --git a/async_rl_research/environment/convert2slime/harbor.py b/async_rl_research/environment/convert2slime/harbor.py index cfb9e1ce17..c4836c10f7 100644 --- a/async_rl_research/environment/convert2slime/harbor.py +++ b/async_rl_research/environment/convert2slime/harbor.py @@ -1,36 +1,21 @@ """Materialize a harbor dataset as slime prompt data + local task dirs. -Schema pair of ``env/harbor.py`` (rows carry ``metadata.task_type: -"harbor"``). This converter is the ONLY writer of that schema: it parses each -task's ``task.toml`` offline (plain ``tomllib`` -- the ``harbor`` package is -needed only for ``--registry`` downloads) and bakes everything the rollout -needs into ``metadata``, so the rollout runtime never reads harbor config. - -Output layout (put ``--out-dir`` on the slime-data volume):: - - /.jsonl slime prompt data - /tasks// copied harbor task dirs (environment/ for - Dockerfile builds, tests/ + steps/ for - verification, solution/ for oracle checks) - -Rows reference tasks via ``metadata.task_path`` RELATIVE to the out dir; -export ``ASYNC_RL_TASK_ROOT=`` for the rollout / oracle. +Schema pair of ``env/harbor.py`` and the ONLY writer of that schema: parses +each ``task.toml`` offline and bakes everything into ``metadata`` so the rollout +never reads harbor config. Output goes under ``--out-dir`` (on the slime-data +volume): ``.jsonl`` + ``tasks//``, referenced via +``metadata.task_path`` relative to it (export ``ASYNC_RL_TASK_ROOT=``). Sources:: - # a directory whose subdirectories are harbor tasks (e.g. a - # harbor-datasets checkout subtree, or an adapter's generated tasks) python -m async_rl_research.environment.convert2slime.harbor \ --tasks-dir ~/harbor-datasets/datasets/usaco --out-dir data/usaco - - # straight from a harbor registry (requires `pip install harbor`) python -m async_rl_research.environment.convert2slime.harbor \ --registry ~/harbor/registry.json --dataset usaco --out-dir data/usaco -v1 scope (anything else is skipped + logged): linux, single-container -Dockerfile or prebuilt docker_image, shared-environment verification, no -GPU/TPU, no MCP servers. Multi-step tasks ARE supported (per-step -instruction/tests/min_reward; see env/harbor.py for reward aggregation). +v1 scope (else skipped + logged): linux, single-container Dockerfile or prebuilt +docker_image, shared-environment verification, no GPU/TPU, no MCP. Multi-step +tasks are supported. """ from __future__ import annotations @@ -59,8 +44,8 @@ def _parse_dockerfile_workdir(dockerfile: Path) -> str | None: if not matches: return None last = matches[-1].strip().strip('"').strip("'") - # Variable or relative WORKDIRs can't be resolved statically; let the - # rollout detect the cwd from the booted sandbox instead. + # Variable/relative WORKDIRs can't be resolved statically; let the rollout + # detect cwd from the sandbox. return last if last.startswith("/") and "$" not in last else None @@ -91,8 +76,7 @@ def _steps_md(cfg: dict[str, Any], task_dir: Path) -> list[dict[str, Any]]: if not name: raise SkipTask("unnamed step") step_dir = task_dir / "steps" / name - # Harbor falls back to the shared top-level tests/ when a step ships - # no tests of its own. + # Fall back to the shared top-level tests/ when a step ships none. tests_path = f"steps/{name}/tests" if (step_dir / "tests" / "test.sh").is_file() else "tests" if not (task_dir / tests_path / "test.sh").is_file(): raise SkipTask(f"step {name!r} has no tests/test.sh (step or shared)") @@ -111,11 +95,10 @@ def _steps_md(cfg: dict[str, Any], task_dir: Path) -> list[dict[str, Any]]: def translate_task(task_dir: Path, *, dataset: str | None = None) -> dict[str, Any]: - """One harbor task dir -> one slime row (metadata.task_path filled by caller). + """One harbor task dir -> one slime row (task_path filled by caller). - ``dataset`` qualifies tasks whose task.toml carries no ``[task].name`` - (harbor-datasets tasks are often bare numeric dirs like ``usaco/84``). - Raises ``SkipTask`` for tasks outside v1 scope. + ``dataset`` qualifies tasks whose task.toml has no ``[task].name``. Raises + ``SkipTask`` for tasks outside v1 scope. """ config_path = task_dir / "task.toml" if not config_path.is_file(): @@ -130,8 +113,8 @@ def translate_task(task_dir: Path, *, dataset: str | None = None) -> dict[str, A if env_cfg.get("mcp_servers"): raise SkipTask("requires MCP servers") if env_cfg.get("network_mode") in ("no-network", "allowlist"): - # The Modal backend doesn't enforce per-phase network policies yet; - # running these would silently grant more network than the task allows. + # Modal can't enforce per-phase network policies yet, so running these + # would grant more network than the task allows. raise SkipTask(f"network_mode={env_cfg['network_mode']}") docker_image = env_cfg.get("docker_image") @@ -172,6 +155,7 @@ def translate_task(task_dir: Path, *, dataset: str | None = None) -> dict[str, A "workdir": workdir, "problem_statement": instruction, "agent_timeout_sec": (cfg.get("agent") or {}).get("timeout_sec"), + "build_timeout_sec": env_cfg.get("build_timeout_sec"), "verifier": _verifier_md(verifier_cfg), "steps": steps_md or None, "reward_strategy": cfg.get("multi_step_reward_strategy"), @@ -180,7 +164,10 @@ def translate_task(task_dir: Path, *, dataset: str | None = None) -> dict[str, A } metadata = {k: v for k, v in metadata.items() if v is not None} - return {"prompt": instruction, "label": task_name, "metadata": metadata} + # prompt as a single-message list, NOT a raw string: slime's Dataset asserts + # a list when a HF processor loads for hf_checkpoint. Harmless: the agent + # reads metadata["problem_statement"], never sample.prompt. + return {"prompt": [{"role": "user", "content": instruction}], "label": task_name, "metadata": metadata} def convert( @@ -220,9 +207,8 @@ def convert( def _discover_task_dirs(tasks_dir: Path) -> list[Path]: """Direct subdirectories holding a task.toml, sorted for determinism. - Falls back to treating ``tasks_dir`` itself as a single task only when no - subdirectory tasks exist: some published datasets (e.g. harbor-datasets' - openthoughts-tblite) ship a stray template task.toml at the dataset root. + Falls back to ``tasks_dir`` as a single task only when no subdir tasks exist + (some datasets ship a stray template task.toml at the root). """ subdirs = sorted(p for p in tasks_dir.iterdir() if (p / "task.toml").is_file()) if subdirs: @@ -239,7 +225,7 @@ def _discover_task_dirs(tasks_dir: Path) -> list[Path]: def _download_from_registry(registry_spec: str, dataset: str, version: str | None, download_dir: Path) -> list[Path]: - """Fetch a dataset's tasks via the harbor package (optional dependency).""" + """Fetch a dataset's tasks via the harbor package (optional dep).""" try: from harbor.models.registry import Registry from harbor.tasks.client import TaskClient diff --git a/async_rl_research/environment/convert2slime/openthoughts_agent.py b/async_rl_research/environment/convert2slime/openthoughts_agent.py new file mode 100644 index 0000000000..1919b25c01 --- /dev/null +++ b/async_rl_research/environment/convert2slime/openthoughts_agent.py @@ -0,0 +1,160 @@ +"""Materialize ``open-thoughts/OpenThoughts-Agent-v1-RL`` as slime prompt data. + +Each HF row packs a harbor task dir as a gzipped tar (``task_binary``); this +unpacks each into a staging tree and hands off to the shared ``harbor.convert``. +Output matches ``harbor.convert`` (``.jsonl`` + ``tasks//``). +These tasks carry no ``[task].name``, so ``instance_id`` falls back to +``__``. + +These tasks deposit their deliverable in ``/output`` (e.g. ``cp ... +/output/command_capture.txt``), but harbor only provisions the workdir + +``/logs/{agent,verifier,artifacts}``. Rather than special-case ``/output`` in +the generic env, we remap it onto harbor's ``/logs/artifacts`` here so the data +is fully ``/logs``-native (the tasks already write their reward to +``/logs/verifier``); see ``conform_output_paths``. + + python -m async_rl_research.environment.convert2slime.openthoughts_agent \ + --out-dir data/openthoughts_agent +""" + +from __future__ import annotations + +import argparse +import base64 +import io +import logging +import re +import tarfile +from collections.abc import Iterable, Iterator +from pathlib import Path +from typing import Any + +from async_rl_research.environment.convert2slime import harbor + +logger = logging.getLogger(__name__) + +HF_DATASET = "open-thoughts/OpenThoughts-Agent-v1-RL" +DATASET_NAME = "openthoughts_agent" # instance_id prefix + JSONL stem + +# These tasks hardcode a /output capture dir; harbor only provisions the workdir +# + /logs/{agent,verifier,artifacts}. Remap /output onto harbor's artifacts dir. +_HARBOR_ARTIFACTS_DIR = "/logs/artifacts" +# Match the /output dir prefix only -- not lookalikes like /outputs or /output_dir. +_OUTPUT_DIR_RE = re.compile(r"/output(?![A-Za-z0-9_])") +# Runtime + agent-facing files only; never touch test data (e.g. the .txt holding +# tests/expected_output.txt, which could legitimately contain the literal /output). +_REMAP_SUFFIXES = (".sh", ".md", ".py") + + +def _archive_bytes(task_binary: bytes | str) -> bytes: + """The ``binary`` HF feature loads as bytes; JSON transports base64.""" + if isinstance(task_binary, str): + return base64.b64decode(task_binary) + return bytes(task_binary) + + +def unpack_tasks(rows: Iterable[dict[str, Any]], staging_dir: Path) -> list[Path]: + """Unpack each row's ``task_binary`` into ``staging_dir//``; returns + the sorted task dirs for ``harbor.convert``.""" + staging_dir.mkdir(parents=True, exist_ok=True) + task_dirs: list[Path] = [] + for row in rows: + path = row.get("path") + if not path: + logger.warning("[openthoughts2slime] skip row with no path") + continue + dest = staging_dir / str(path) + with tarfile.open(fileobj=io.BytesIO(_archive_bytes(row["task_binary"])), mode="r:gz") as tf: + # filter="data" blocks path traversal/unsafe members (py3.12+). + tf.extractall(dest, filter="data") + task_dirs.append(dest) + return sorted(task_dirs) + + +def conform_output_paths(task_dirs: Iterable[Path]) -> int: + """Remap the legacy ``/output`` capture dir onto harbor's ``/logs/artifacts`` + in each task's scripts + instructions (solve.sh, test.sh, instruction.md). + + Rewrites in-place under the staging tree before ``harbor.convert`` copies it. + Returns the count of files changed. Only ``_REMAP_SUFFIXES`` are touched, so + test data (``tests/expected_output.txt``) is never rewritten. + """ + rewritten = 0 + for task_dir in task_dirs: + for path in task_dir.rglob("*"): + if not path.is_file() or path.suffix not in _REMAP_SUFFIXES: + continue + try: + text = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + continue + new = _OUTPUT_DIR_RE.sub(_HARBOR_ARTIFACTS_DIR, text) + if new != text: + path.write_text(new, encoding="utf-8") + rewritten += 1 + return rewritten + + +def load_hf_rows(repo: str, split: str, limit: int | None) -> Iterator[dict[str, Any]]: + try: + from datasets import load_dataset + except ImportError as exc: + raise SystemExit("Install `datasets` to pull OpenThoughts-Agent from HuggingFace.") from exc + + for index, row in enumerate(load_dataset(repo, split=split)): + if limit is not None and index >= limit: + break + yield dict(row) + + +def materialize( + out_dir: Path, + *, + name: str = DATASET_NAME, + repo: str = HF_DATASET, + split: str = "train", + limit: int | None = None, +) -> tuple[int, int]: + """Download the HF dataset, unpack tasks, and run the harbor converter. + + Returns ``(converted, skipped)``. Staging lives under ``out_dir`` (one + filesystem for the converter's copytree) and is removed after. + """ + import shutil + + staging = out_dir / "_staging" + rows = load_hf_rows(repo, split, limit) + task_dirs = unpack_tasks(rows, staging) + logger.info("[openthoughts2slime] unpacked %d tasks -> %s", len(task_dirs), staging) + remapped = conform_output_paths(task_dirs) + logger.info("[openthoughts2slime] remapped /output -> %s in %d files", _HARBOR_ARTIFACTS_DIR, remapped) + try: + return harbor.convert(task_dirs, out_dir, name=name, dataset=DATASET_NAME) + finally: + shutil.rmtree(staging, ignore_errors=True) + + +def main(argv: list[str] | None = None) -> int: + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--out-dir", type=Path, required=True, help="output dir (JSONL + tasks/); use the slime-data volume") + parser.add_argument("--name", default=DATASET_NAME, help="JSONL filename stem") + parser.add_argument("--repo", default=HF_DATASET, help="HuggingFace dataset repo id") + parser.add_argument("--split", default="train", help="HuggingFace split") + parser.add_argument("--limit", type=int, help="maximum tasks to convert") + args = parser.parse_args(argv) + + converted, skipped = materialize( + args.out_dir, name=args.name, repo=args.repo, split=args.split, limit=args.limit + ) + out_dir = args.out_dir.resolve() + jsonl = out_dir / f"{args.name}.jsonl" + print(f"converted {converted} tasks ({skipped} skipped) -> {jsonl}") + print("next steps:") + print(f" export ASYNC_RL_TASK_ROOT={out_dir}") + print(f" python -m async_rl_research.environment.harbor {jsonl} --task-root {out_dir} --limit 3 # oracle check") + return 0 if converted else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/async_rl_research/environment/convert2slime/swe_gym.py b/async_rl_research/environment/convert2slime/swe_gym.py index 64043c23e5..a4707e7f1e 100644 --- a/async_rl_research/environment/convert2slime/swe_gym.py +++ b/async_rl_research/environment/convert2slime/swe_gym.py @@ -1,7 +1,7 @@ """Translate SWE-Gym / SWE-Gym-Lite rows into slime prompt data. Schema pair of ``env/swe_gym.py`` (SWE rows carry no ``metadata.task_type``: -they are the default env). The output is one JSON object per line: +the default env). Output is one JSON object per line: { "prompt": "...", @@ -16,9 +16,8 @@ } } -The only SWE-Gym-specific choices here are deriving the prebuilt image name and -building a simple pytest-based reward command from ``test_patch`` + F2P/P2P -tests. +SWE-Gym-specific: derive the prebuilt image name and build a pytest reward +command from ``test_patch`` + F2P/P2P tests. """ from __future__ import annotations @@ -108,8 +107,11 @@ def translate(raw: dict[str, Any]) -> dict[str, Any] | None: if base_commit := raw.get("base_commit"): metadata["pre_commands"] = [f"git checkout {base_commit} -f"] + # prompt as a single-message list, NOT a raw string: slime's Dataset asserts + # a list when a HF processor loads for hf_checkpoint. Harmless: the agent + # reads problem_statement from metadata, never sample.prompt. return { - "prompt": problem, + "prompt": [{"role": "user", "content": problem}], "label": instance_id, "metadata": metadata, } diff --git a/async_rl_research/environment/harbor.py b/async_rl_research/environment/harbor.py index f0efcd5c3d..7b2eefb65e 100644 --- a/async_rl_research/environment/harbor.py +++ b/async_rl_research/environment/harbor.py @@ -1,34 +1,16 @@ """Harbor env: run harbor-format tasks (USACO, ...) as RL episodes on Modal. -The schema pair of ``env/convert2slime/harbor.py`` (see ``base.py``). The -converter materializes harbor task directories next to the JSONL and bakes -everything the rollout needs into ``metadata`` -- this module never imports -the ``harbor`` package and never parses ``task.toml`` at rollout time. - -Episode shape (harbor "shared" verifier semantics): - - boot sandbox (task Dockerfile built on Modal, or prebuilt docker_image) - for each step (single-step tasks are one pseudo-step): - write the step instruction to {workdir}/PROBLEM_STATEMENT.md - agent leg: runtime.run_agent(...) against the shared adapter session - verify IN-PLACE: upload step tests/ -> /tests, run test.sh, - parse /logs/verifier/reward.{json,txt} - gate on the step's min_reward (abort remaining steps below threshold) - aggregate per-step rewards (mean | final) -> scalar training reward - -Verification runs inside the agent's sandbox (tests are uploaded only AFTER -the agent leg, so the agent cannot read them) -- weaker than swe_gym's -clean-sandbox diff grading, but it is harbor's contract: the agent's job is -to leave artifacts behind. Tasks demanding a separate verifier container are -filtered out by the converter. - -Rollout-side requirements: - ASYNC_RL_TASK_ROOT directory the JSONL's relative ``task_path``s resolve - against (the converter's --out-dir; put it on the - slime-data volume so head workers can read tests/). - -Oracle check (no model involved -- validates boot/prep/verify plumbing by -running each task's reference solution through the exact rollout path):: +Schema pair of ``env/convert2slime/harbor.py`` (see ``base.py``); the converter +bakes everything into ``metadata`` so this never reads ``task.toml`` at rollout. + +Episode (harbor "shared" verifier semantics): boot sandbox, then per step write +the instruction, run the agent leg against the shared session, verify IN-PLACE +(upload tests/, run test.sh, parse /logs/verifier/reward.{json,txt}), and gate +on min_reward. Per-step rewards aggregate (mean | final) to a scalar. Tests are +uploaded only AFTER the agent leg so the agent can't read them. + +Rollout needs ``ASYNC_RL_TASK_ROOT`` (dir relative ``task_path``s resolve +against). Oracle check (no model): python -m async_rl_research.environment.harbor out/usaco.jsonl --task-root out --limit 3 """ @@ -51,13 +33,20 @@ TASK_ROOT_ENV = "ASYNC_RL_TASK_ROOT" -# ${VAR} / ${VAR:-default} templates in verifier/solution env values, resolved -# against the HEAD's os.environ at use time (harbor's resolve_env_vars -# semantics, except an unresolvable var is skipped with a warning instead of -# failing the rollout). +# Per-task task.toml timeouts override the env defaults only when this is set; +# default (off) keeps the env vars (boot/agent/eval) the single source of truth. +TASK_TIMEOUT_OVERRIDE = os.environ.get("ASYNC_RL_TASK_TIMEOUT_OVERRIDE", "0").strip().lower() in ("1", "true", "yes") + +# ${VAR} / ${VAR:-default} templates in verifier env values, resolved against +# the HEAD's os.environ (unresolvable -> skipped with a warning). _ENV_TEMPLATE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-(.*))?\}$") +def _effective(env_val: int, task_val: Any) -> int: + """Task.toml timeout wins over the env default only under TASK_TIMEOUT_OVERRIDE.""" + return int(task_val) if (TASK_TIMEOUT_OVERRIDE and task_val) else int(env_val) + + def _resolve_env_templates(env: dict[str, str] | None) -> dict[str, str]: resolved: dict[str, str] = {} for key, value in (env or {}).items(): @@ -97,14 +86,10 @@ def _meets_min_reward(rewards: dict[str, Any] | None, min_reward: float | dict[s class HarborEnv(RolloutEnv): name = "harbor" - # No agent_config default: the runtime's universal prompt scaffold is the - # default for all task families, and harbor instruction.md files already - # carry their own deliverable contract (what artifacts to leave where). - # Per-row builtin override via metadata.agent_config; global via MSWE_CONFIG. + # No agent_config default: harbor instruction.md files carry their own + # deliverable contract. Override per-row via metadata.agent_config. - # ------------------------------------------------------------------ # Row schema (written by env/convert2slime/harbor.py -- keep in sync) - # ------------------------------------------------------------------ def normalize_metadata(self, sample) -> dict[str, Any]: m = sample.metadata or {} task_path = m.get("task_path") @@ -132,9 +117,10 @@ def normalize_metadata(self, sample) -> dict[str, Any]: "task_dir": str(task_dir), "docker_image": docker_image, "dockerfile": dockerfile, - "workdir": m.get("workdir"), # None -> detected from the booted sandbox + "workdir": m.get("workdir"), # None -> detected from the sandbox "problem_statement": m.get("problem_statement") or coerce_prompt(sample.prompt), "agent_timeout_sec": m.get("agent_timeout_sec"), + "build_timeout_sec": m.get("build_timeout_sec"), "verifier": m.get("verifier") or {}, "steps": steps, "reward_strategy": m.get("reward_strategy"), @@ -143,6 +129,13 @@ def normalize_metadata(self, sample) -> dict[str, Any]: "agent_config": m.get("agent_config"), } + def effective_budgets(self, md: dict[str, Any], *, agent_time_budget_sec: int, eval_timeout_sec: int) -> dict[str, int]: + return { + "boot_sec": _effective(ModalSandbox._boot_timeout_from_env(), md.get("build_timeout_sec")), + "agent_sec": _effective(agent_time_budget_sec, md.get("agent_timeout_sec")), + "eval_sec": _effective(eval_timeout_sec, (md.get("verifier") or {}).get("timeout_sec")), + } + @staticmethod def _resolve_task_dir(task_path: str) -> Path: p = Path(task_path) @@ -155,9 +148,7 @@ def _resolve_task_dir(task_path: str) -> Path: raise EnvMetadataError(f"task_dir_missing:{p}") return p - # ------------------------------------------------------------------ # Episode - # ------------------------------------------------------------------ def _image(self, md: dict[str, Any]) -> str | DockerfileImage: if md["docker_image"]: return md["docker_image"] @@ -172,6 +163,8 @@ def _sandbox(self, md: dict[str, Any]) -> ModalSandbox: kwargs["memory_mb"] = int(md["memory_mb"]) if md["workdir"]: kwargs["workdir"] = md["workdir"] + if TASK_TIMEOUT_OVERRIDE and md.get("build_timeout_sec"): + kwargs["boot_timeout"] = int(md["build_timeout_sec"]) return ModalSandbox(self._image(md), **kwargs) def _step_specs(self, md: dict[str, Any]) -> list[dict[str, Any]]: @@ -199,8 +192,8 @@ async def rollout( agent_time_budget_sec: int, eval_timeout_sec: int, ) -> RewardResult: - async def agent_leg(sb, leg_md: dict[str, Any], budget_sec: int) -> None: - await runtime.run_agent( + async def agent_leg(sb, leg_md: dict[str, Any], budget_sec: int): + return await runtime.run_agent( sb, md=leg_md, session_id=session_id, @@ -227,32 +220,42 @@ async def _episode( task_dir = Path(md["task_dir"]) steps = self._step_specs(md) step_results: list[dict[str, Any]] = [] - deadline = time.monotonic() + agent_time_budget_sec + # Last agent leg's exit code + failure tail (stays None for the oracle + # leg, which runs solve.sh rather than the agent). Surfaced in extra so + # a zero-turn adapter_session_empty self-explains in the rollout dump. + last_agent = None async with self._sandbox(md) as sb: workdir = md["workdir"] or await self._detect_workdir(sb) q = shlex.quote - # Harbor mounts /logs/{agent,verifier,artifacts}; test scripts - # assume they exist. workdir may be absent on prebuilt images. + # Test scripts assume /logs/{agent,verifier,artifacts} exist. await sb.exec( f"mkdir -p {q(workdir)} /logs/agent /logs/verifier /logs/artifacts", check=True, timeout=60, ) + # Start the agent clock only once the sandbox is booted and prepped: + # a cold per-instance image pull can take many minutes, and charging + # it against the agent budget would exhaust the window before any step + # runs (-> zero agent turns -> adapter_session_empty). Provisioning on + # the first leg likewise gets its own clock inside _detached_run. + deadline = time.monotonic() + _effective(agent_time_budget_sec, md.get("agent_timeout_sec")) + for step in steps: remaining = int(deadline - time.monotonic()) if remaining <= 0: logger.warning("[harbor] %s: agent budget exhausted before step %r", md["instance_id"], step["name"]) break budget = remaining - for cap in (step.get("agent_timeout_sec"), md["agent_timeout_sec"]): - if cap: - budget = min(budget, int(cap)) + if TASK_TIMEOUT_OVERRIDE and step.get("agent_timeout_sec"): + budget = min(budget, int(step["agent_timeout_sec"])) await self.write_problem_file(sb, workdir, step["instruction"]) leg_md = {**md, "workdir": workdir} - await run_leg(sb, leg_md, budget) + leg_result = await run_leg(sb, leg_md, budget) + if leg_result is not None: + last_agent = leg_result rewards = await self._verify( sb, @@ -268,27 +271,29 @@ async def _episode( break reward = self._aggregate(steps, step_results, md["reward_strategy"]) + extra: dict[str, Any] = { + "harbor_step_results": step_results, + "harbor_steps_completed": len(step_results), + "harbor_steps_total": len(steps), + } + if last_agent is not None: + extra["agent_exit_code"] = last_agent.exit_code + extra["agent_tail"] = last_agent.tail return RewardResult( reward=reward, - # epsilon: weighted pytest fractions can sum to 0.999... for a - # fully-passing task (seen on openthoughts-tblite bash-log-processor-fix) + # epsilon: weighted pytest fractions sum to 0.999... for a fully- + # passing task (seen on openthoughts-tblite bash-log-processor-fix) is_solved=reward >= 1.0 - 1e-6, - extra={ - "harbor_step_results": step_results, - "harbor_steps_completed": len(step_results), - "harbor_steps_total": len(steps), - }, + extra=extra, ) @staticmethod def _aggregate(steps: list[dict], results: list[dict], strategy: str | None) -> float: """Scalar episode reward from per-step scalars. - 'mean' divides by ALL declared steps (an unexecuted/gated step counts - 0) -- stricter than harbor's job-level mean, which excludes steps - whose verifier never ran, but the conservative signal is what RL - wants. 'final' is the last DECLARED step's reward (0 if never - reached), matching harbor. + 'mean' divides by ALL declared steps (gated steps count 0; stricter than + harbor's job-level mean, but the conservative signal is what RL wants). + 'final' is the last declared step's reward (0 if never reached). """ if not results: return 0.0 @@ -300,15 +305,12 @@ def _aggregate(steps: list[dict], results: list[dict], strategy: str | None) -> @staticmethod async def _detect_workdir(sb) -> str: - # Prebuilt docker_image rows may not know their WORKDIR; ask the - # sandbox (Modal honors the image workdir for exec cwd). + # Prebuilt docker_image rows may not know their WORKDIR; ask the sandbox. ec, out, _ = await sb.exec("pwd", check=False, timeout=30) detected = (out or "").strip().splitlines()[-1] if ec == 0 and (out or "").strip() else "" return detected or "/app" - # ------------------------------------------------------------------ # In-place verification (harbor's shared-environment Verifier semantics) - # ------------------------------------------------------------------ async def _verify( self, sb, @@ -320,15 +322,7 @@ async def _verify( instance_id: str, ) -> dict[str, Any] | None: """Upload tests, run test.sh, parse the reward files. None = no verdict.""" - timeout = int(verifier.get("timeout_sec") or eval_timeout_sec) - if timeout > eval_timeout_sec: - logger.info( - "[harbor] %s: verifier timeout %ds capped to AGENT_EVAL_TIMEOUT_SEC=%ds", - instance_id, - timeout, - eval_timeout_sec, - ) - timeout = eval_timeout_sec + timeout = _effective(eval_timeout_sec, verifier.get("timeout_sec")) await self.upload_dir(sb, tests_dir, "/tests") q = shlex.quote @@ -338,12 +332,20 @@ async def _verify( timeout=60, ) env = _resolve_env_templates(verifier.get("env")) - ec, _, err = await sb.exec( + ec, out, err = await sb.exec( f"cd {q(workdir)} && bash /tests/test.sh", env=env or None, timeout=timeout, check=False, ) + if os.environ.get("HARBOR_VERIFY_DEBUG"): + logger.info( + "[harbor-verify-debug] %s: test.sh exit=%s\n--- stdout tail ---\n%s\n--- stderr tail ---\n%s", + instance_id, + ec, + (out or "")[-3000:], + (err or "")[-3000:], + ) raw_json = await sb.read_file("/logs/verifier/reward.json") if raw_json.strip(): @@ -359,12 +361,9 @@ async def _verify( except ValueError: logger.warning("[harbor] %s: unparseable reward.txt: %.200s", instance_id, raw_txt) return None - # No reward file: terminal-bench-style tasks (e.g. openthoughts-tblite) - # end test.sh with a bare pytest run and rely on the harness to grade - # it. Grade all-or-nothing on pytest's exit code (tb's resolved - # semantics): 0 = every test passed, 1 = test failures. Anything else - # (127 missing dep, 2-5 pytest infra, timeout) stays "no verdict" so - # infra breakage is not silently scored as a model failure. + # No reward file: terminal-bench-style tasks end test.sh with a bare + # pytest run. Grade all-or-nothing on its exit code (0 pass, 1 fail); + # anything else stays "no verdict" so infra breakage isn't scored. if ec in (0, 1): logger.info("[harbor] %s: no reward file; graded from test.sh exit=%d", instance_id, ec) return {"reward": 1.0 if ec == 0 else 0.0, "graded_from": "exit_code"} @@ -376,15 +375,10 @@ async def _verify( ) return None - # ------------------------------------------------------------------ # Oracle check (reference solution through the exact rollout path) - # ------------------------------------------------------------------ async def oracle_episode(self, md: dict[str, Any], *, solve_timeout_sec: int, eval_timeout_sec: int) -> RewardResult: - """Replace the agent leg with the task's solution/solve.sh. - - Legs run sequentially, so a simple counter maps each leg back to its - step (for per-step solution dirs on multi-step tasks). - """ + """Replace the agent leg with the task's solution/solve.sh (a counter + maps each sequential leg to its step).""" task_dir = Path(md["task_dir"]) steps = self._step_specs(md) state = {"i": 0} @@ -400,13 +394,21 @@ async def leg(sb, leg_md: dict[str, Any], budget_sec: int) -> None: await self.upload_dir(sb, solution_dir, "/solution") q = shlex.quote await sb.exec("chmod +x /solution/solve.sh", check=False, timeout=30) - ec, _, err = await sb.exec( + ec, out, err = await sb.exec( f"cd {q(leg_md['workdir'])} && bash /solution/solve.sh", timeout=min(budget_sec, solve_timeout_sec), check=False, ) if ec != 0: logger.warning("[harbor-oracle] solve.sh exit=%d stderr tail: %s", ec, (err or "")[-400:]) + if os.environ.get("HARBOR_VERIFY_DEBUG"): + logger.info( + "[harbor-oracle-debug] %s: solve.sh exit=%s\n--- stdout tail ---\n%s\n--- stderr tail ---\n%s", + md["instance_id"], + ec, + (out or "")[-3000:], + (err or "")[-3000:], + ) return await self._episode( md, @@ -428,7 +430,14 @@ def _oracle_main() -> int: parser.add_argument("--index", type=int, help="check exactly this row") parser.add_argument("--solve-timeout", type=int, default=600) parser.add_argument("--eval-timeout", type=int, default=int(os.environ.get("AGENT_EVAL_TIMEOUT_SEC", "600"))) + parser.add_argument( + "--vm-runtime", + action="store_true", + help="boot VM sandboxes (experimental_options={'vm_runtime': True}) instead of gVisor", + ) args = parser.parse_args() + if args.vm_runtime: + os.environ["SLIME_AGENT_SANDBOX_VM_RUNTIME"] = "1" root = args.task_root or os.environ.get(TASK_ROOT_ENV) or str(Path(args.jsonl).resolve().parent) os.environ[TASK_ROOT_ENV] = root @@ -445,9 +454,10 @@ def _oracle_main() -> int: for row in picked: sample = SimpleNamespace(metadata=row.get("metadata"), prompt=row.get("prompt"), label=row.get("label")) md = env.normalize_metadata(sample) + t0 = time.monotonic() result = asyncio.run(env.oracle_episode(md, solve_timeout_sec=args.solve_timeout, eval_timeout_sec=args.eval_timeout)) status = "OK " if result.is_solved else "FAIL" - print(f"[{status}] {md['instance_id']}: reward={result.reward:.2f} {result.extra}") + print(f"[{status}] {md['instance_id']}: reward={result.reward:.2f} t={time.monotonic() - t0:.0f}s {result.extra}") failures += 0 if result.is_solved else 1 return 1 if failures else 0 diff --git a/async_rl_research/environment/swe_gym.py b/async_rl_research/environment/swe_gym.py index 24fe0afa97..4ee577eb56 100644 --- a/async_rl_research/environment/swe_gym.py +++ b/async_rl_research/environment/swe_gym.py @@ -1,22 +1,10 @@ """SWE-Gym env: git-diff capture + clean-sandbox grading on Modal. -The schema pair of ``env/convert2slime/swe_gym.py`` (see ``base.py`` for the -convention). Each row carries a prebuilt per-instance registry image plus a -self-contained ``eval_cmd`` (or a ``swepro`` run/parse spec), and grading is -diff-transplant based: - - boot work sandbox -> pre_commands + problem file -> agent runs -> - capture git diff -> CLOSE work sandbox -> boot CLEAN sandbox -> - re-apply pre_commands -> apply diff -> run eval_cmd. - -No-test-cheating guarantee: the evaluator sandbox never sees the agent's -filesystem -- only the captured diff can affect reward. This is what -``rollout`` owning the whole lifecycle buys: the work sandbox is torn down -BEFORE the evaluator boots, exactly as the pre-refactor flow did. - -The provider backend is ``modal_sandbox.ModalSandbox``; boot concurrency and -create-retry live there, so the evaluator sandbox is gated and retried just -like the work sandbox. +Schema pair of ``env/convert2slime/swe_gym.py`` (see ``base.py``). Grading is +diff-transplant: boot work sandbox -> pre_commands + problem file -> agent runs +-> capture git diff -> CLOSE work sandbox -> boot CLEAN sandbox -> re-apply +pre_commands -> apply diff -> run eval_cmd. The evaluator never sees the agent's +filesystem (only the captured diff affects reward), so tests can't be cheated. """ from __future__ import annotations @@ -42,11 +30,8 @@ _PRE = "/tmp/__swe_pre__.sh" -# Appended to the row's problem statement: with the universal prompt scaffold -# the agent's YAML no longer carries a submission protocol, so the task -# instruction must say what the deliverable is. Reward here is the captured -# working-tree `git diff`, hence: edit in place, don't commit, no patch files -# (the builtin swebench prompt's patch.txt ritual was never what got graded). +# Appended to the problem statement: the universal scaffold has no submission +# protocol, so spell out the deliverable (reward is the working-tree `git diff`). _DELIVERABLE_SUFFIX = """ ## Deliverable @@ -62,9 +47,8 @@ class SweGymEnv(RolloutEnv): name = "swe_gym" - # No agent_config default: the runtime's universal prompt scaffold is the - # default; the SWE deliverable contract is _DELIVERABLE_SUFFIX above. - # Per-row builtin override via metadata.agent_config; global via MSWE_CONFIG. + # No agent_config default: the universal scaffold + _DELIVERABLE_SUFFIX + # apply. Override per-row via metadata.agent_config; globally via MSWE_CONFIG. def normalize_metadata(self, sample) -> dict[str, Any]: m = sample.metadata or {} @@ -101,7 +85,7 @@ async def rollout( with timer.phase("prep"): await self._prepare_workspace(sb, md) with timer.phase("agent"): - await runtime.run_agent( + agent_run = await runtime.run_agent( sb, md=md, session_id=session_id, @@ -119,13 +103,14 @@ async def rollout( return RewardResult( reward=float(reward), is_solved=bool(is_solved), - # diff_bytes/diff_files are SIZE metrics only (len + header count); - # the patch text itself is never stored. + # diff_bytes/diff_files are SIZE metrics; patch text never stored. extra={ "applied_cleanly": bool(applied), "diff_bytes": len(diff_text), "diff_files": diff_text.count("diff --git"), "timing": timer.as_dict(), + "agent_exit_code": agent_run.exit_code, + "agent_tail": agent_run.tail, }, ) @@ -135,13 +120,10 @@ async def rollout( async def _prepare_workspace(self, sb: Sandbox, md: dict[str, Any]) -> None: """Bring a freshly booted work sandbox to the task's start state. - ``pre_commands`` must run in BOTH the work and eval sandboxes (see - ``_apply_pre_commands``): they are typically ``git checkout - -f``, and skipping either side makes the model's diff - context mismatch the eval base -> apply failures. + ``pre_commands`` (typically ``git checkout -f``) run in BOTH + work and eval sandboxes, else the diff context mismatches the eval base. """ - # git operations inside the sandbox (pre_commands, the later diff - # capture) need the repo marked safe for root. + # In-sandbox git ops need the repo marked safe for root. await sb.exec("git config --system --add safe.directory '*'", check=False, timeout=60) if md["pre_commands"]: await _apply_pre_commands(sb, md["workdir"], md["pre_commands"]) @@ -151,11 +133,8 @@ async def _prepare_workspace(self, sb: Sandbox, md: dict[str, Any]) -> None: # Diff capture # ------------------------------------------------------------------ async def _git_diff(self, sb: Sandbox, workdir: str, *, exclude: tuple[str, ...] = ()) -> str: - """Capture the model's edits as a patch, excluding scratch files. - - ``git add -N .`` stages intent-to-add for new files so they show up in - the diff. ``exclude`` is the active runtime's ``diff_exclude_all``; - the task-layer ``PROBLEM_FILE`` is always excluded here. + """Capture the model's edits as a patch (``git add -N .`` so new files + appear), excluding ``PROBLEM_FILE`` + the runtime's ``diff_exclude_all``. """ excludes = " ".join(f"':(exclude){f}'" for f in (PROBLEM_FILE, *exclude)) cmd = f"cd {shlex.quote(workdir)} && git add -N . && git diff -- . {excludes}" @@ -216,15 +195,13 @@ async def _apply_diff(sb: Sandbox, workdir: str, diff_text: str) -> bool: async def _run_eval_cmd(sb: Sandbox, workdir: str, cmd: str, timeout: int) -> tuple[float, bool]: - # What SWE-Gym-Lite emits: a self-contained command whose exit code is the - # verdict (applies the test_patch, runs pytest on F2P/P2P). + # SWE-Gym-Lite's self-contained command whose exit code is the verdict. ec, _, _ = await sb.exec(f"cd {shlex.quote(workdir)} && {cmd}", check=False, timeout=timeout) return (1.0 if ec == 0 else 0.0), ec == 0 async def _run_swepro(sb: Sandbox, workdir: str, swepro: dict, timeout: int) -> tuple[float, bool]: - # Forward-compat pass-through for swepro-style grading (SWE-Gym-Lite data - # carries none, but a richer dataset can supply a run/parse script pair). + # Forward-compat pass-through for swepro-style run/parse grading. swepro_dir = "/tmp/swepro_eval" await sb.exec(f"mkdir -p {swepro_dir} && chmod 777 {swepro_dir}", check=True, timeout=30) for key, dst in (("run_script_path", "run_script.sh"), ("parser_script_path", "parser.py")): diff --git a/async_rl_research/evalset.py b/async_rl_research/evalset.py index 18a64cba93..90864394ad 100644 --- a/async_rl_research/evalset.py +++ b/async_rl_research/evalset.py @@ -1,11 +1,8 @@ """Build a versioned eval set by subsampling converted slime datasets. -An *eval set* is a directory of per-subset JSONL files drawn from -already-converted datasets (the outputs of ``env/convert2slime/*``), plus a -manifest pinning exactly which instances were chosen. Build it once onto the -slime-data volume, then point the training config's inline ``eval_config`` -at the subset files. The rows are ordinary slime prompt rows, so eval drives -the same ``runtime x env`` stack as training. +Writes per-subset JSONL files drawn from already-converted datasets plus a +manifest pinning the chosen instances; point the training config's inline +``eval_config`` at the subset files. Spec YAML:: @@ -26,15 +23,10 @@ python -m async_rl_research.evalset spec.yaml --out-dir /data/evalsets/v0 -Outputs ``/.jsonl`` per subset, ``manifest.json`` (spec + -chosen instance ids), and ``eval_config.yaml`` (a ready ``--eval-config`` -file). It also prints the equivalent inline ``eval_config`` dict to paste -into a training config. Paths inside the spec should be the paths as seen at -*runtime* (e.g. ``/data/...`` on the cluster); run the builder where those -paths resolve (a Modal shell / function with the volume mounted) so the -harbor task-dir checks mean something. After building, oracle-check a subset: -``ASYNC_RL_TASK_ROOT= python -m async_rl_research.environment.harbor -/.jsonl --limit 3``. +Outputs per-subset ``.jsonl``, ``manifest.json``, and +``eval_config.yaml``, and prints the inline ``eval_config`` dict. Spec paths +must be the *runtime* paths (e.g. ``/data/...``); run the builder where they +resolve so harbor task-dir checks mean something. """ from __future__ import annotations @@ -70,12 +62,8 @@ def _instance_id(row: dict[str, Any], index: int) -> str: def _rewrite_task_path(row: dict[str, Any], source_dir: Path, task_root: Path | None, problems: list[str]) -> None: - """Re-root a harbor row's relative task_path so it stays resolvable. - - Converted harbor rows carry task_path relative to their converter's - out dir; a subsampled copy lives elsewhere, so pin the path down: relative - to ``task_root`` when given (matching the run's single ASYNC_RL_TASK_ROOT), - absolute otherwise (env/harbor accepts absolute paths as-is). + """Re-root a harbor row's relative task_path so it stays resolvable: pin it + relative to ``task_root`` when given, absolute otherwise. """ md = row.get("metadata") or {} if md.get("task_type") != "harbor" or not md.get("task_path"): @@ -115,7 +103,7 @@ def _build_subset(subset: dict[str, Any], out_dir: Path, task_root: Path | None, problems: list[str] = [] chosen = [] for i in keep: - row = json.loads(json.dumps(rows[i])) # deep copy; never mutate the source rows + row = json.loads(json.dumps(rows[i])) # deep copy _rewrite_task_path(row, source.parent, task_root, problems) chosen.append(row) @@ -150,7 +138,7 @@ def main() -> None: spec = _load_spec(args.spec) task_root = Path(spec["task_root"]) if spec.get("task_root") else None - # Resolve so the paths baked into the manifest / eval_config are absolute. + # Resolve so manifest / eval_config paths are absolute. args.out_dir = args.out_dir.resolve() args.out_dir.mkdir(parents=True, exist_ok=True) diff --git a/async_rl_research/generate.py b/async_rl_research/generate.py index 4416ed839c..b47d3301ea 100644 --- a/async_rl_research/generate.py +++ b/async_rl_research/generate.py @@ -1,74 +1,30 @@ """Generic agentic-RL rollout entrypoint for slime (design A: HTTP adapter). -Wire-up:: - - --custom-generate-function-path async_rl_research.generate.generate - -This is the **agent- and task-agnostic** per-sample orchestrator. It owns the -parts that are identical for any in-sandbox agent on any task family, and -delegates everything else to two orthogonal collaborators: - - generate.py (this file) adapter/HTTP lifecycle + session management + - trajectory merge + abort/timeout isolation. - agent/base.py the AgentRuntime contract + shared launch and - provisioning machinery + the runtime registry - (load_runtime). One subclass per agent - framework (default: mini_swe_agent). - env/base.py the RolloutEnv contract + the env registry - (load_env). One subclass per task family -- - row schema, sandbox boot/prep, agent-leg - sequencing, grading. env/swe_gym.py grades a - captured git diff in a CLEAN sandbox; - env/harbor.py verifies harbor tasks in-place - (multi-step aware). Rows pick their env via - metadata.task_type (absent -> swe_gym). - -Topology (design A -- "in-sandbox subprocess + HTTP adapter"): - - host generate(): - 1. _State (once/worker): build the runtime's adapter (an aiohttp app that - speaks the agent's wire API and records exact SGLang tokens) and serve - it on a bg thread; expose adapter_url = http://$SLIME_HEAD_HOST:$PORT. - 2. open an adapter session keyed by session_id. - 3. env.rollout(): boot the task sandbox, prep the workspace, let the - runtime launch the agent inside it for each task step. The agent - dials BACK to adapter_url for every model call; the adapter renders - messages -> input_ids, calls SGLang /generate (return_logprob), and - records (prompt_ids, output_ids, logprobs). The env grades the - result into a RewardResult. - 4. finish_session() drains the recorded token segments; merge -> Sample. - -Reward is computed inline (env.rollout) and written onto the sample, so -slime's default reward-model step is skipped (generate_and_rm only calls -async_rm when sample.reward is None). - -The full contracts live on ``agent.base.AgentRuntime`` and -``env.base.RolloutEnv`` -- single sources of truth. This module only relies -on: ``runtime.adapter_cls`` (constructed as adapter_cls(tokenizer=, -sglang_url=, tool_parser=, reasoning_parser=)), ``env.normalize_metadata``, -and ``env.rollout``. +Wire-up: ``--custom-generate-function-path async_rl_research.generate.generate``. + +Agent- and task-agnostic per-sample orchestrator. Owns the parts identical for +any in-sandbox agent on any task family (adapter/HTTP lifecycle, session +management, trajectory merge, abort/timeout isolation) and delegates the rest to +a runtime (``agent/base.py``) and an env (``env/base.py``, picked per row by +metadata.task_type). Per sample: ``_State`` serves the runtime's adapter on a bg +thread, a session is opened keyed by session_id, ``env.rollout`` runs the agent +(which dials back to the adapter per model call), and the recorded token +segments merge into Sample(s). Reward is computed inline so slime's reward-model +step is skipped. Env knobs --------- SLIME_HEAD_HOST public IP sandboxes use to reach the adapter (REQUIRED unless MODAL_EXPOSE_ADAPTER=1) MODAL_EXPOSE_ADAPTER 1 to publish the adapter through a modal.forward - tunnel (required on a Modal cluster: sandboxes are - network-isolated and can only dial a public URL) + tunnel (required on a Modal cluster) SHIM_BIND_HOST 0.0.0.0 adapter bind host on the head node SHIM_PORT 18002 adapter bind port - ASYNC_RL_AGENT_RUNTIME which agent runtime to use: a registry short name - ("mini-swe"), "module:Class", or a module path - exposing RUNTIME (default "mini-swe"; see - agent.base.load_runtime) + ASYNC_RL_AGENT_RUNTIME agent runtime spec (default "mini-swe") ASYNC_RL_AGENT_DRIVER legacy alias for ASYNC_RL_AGENT_RUNTIME - ASYNC_RL_TASK_ROOT root dir that relative metadata.task_path values - resolve against (harbor env; the converter's - --out-dir on the slime-data volume) - AGENT_TIME_BUDGET_SEC 1800 total agent wallclock budget per sample - (multi-step episodes share it) - AGENT_EVAL_TIMEOUT_SEC 600 wallclock cap per grading command - AGENT_GENERATE_GUARD_SEC full generate() guard; default budget+eval+180 + ASYNC_RL_TASK_ROOT root dir relative metadata.task_path resolve against + AGENT_TIME_BUDGET_SEC 1800 total agent wallclock budget per sample + AGENT_EVAL_TIMEOUT_SEC 600 wallclock cap per grading command """ from __future__ import annotations @@ -90,32 +46,24 @@ from .agent.base import AgentRuntime, load_runtime from .aiohttp_threaded import run_app_in_thread from .environment.base import EnvMetadataError, RewardResult, load_env +from .modal_sandbox import SandboxBootTimeout logger = logging.getLogger(__name__) AGENT_TIME_BUDGET_SEC = int(os.environ.get("AGENT_TIME_BUDGET_SEC", "1800")) AGENT_EVAL_TIMEOUT_SEC = int(os.environ.get("AGENT_EVAL_TIMEOUT_SEC", "600")) -# Wall-clock guard for the entire generate() call. When exceeded, the in-flight -# sample is aborted (`wall_clock_timeout`) and the rest of the rollout -# continues -- isolates one hung trajectory from the whole training step. -AGENT_GENERATE_GUARD_SEC = int(os.environ.get("AGENT_GENERATE_GUARD_SEC", "0") or 0) or ( - AGENT_TIME_BUDGET_SEC + AGENT_EVAL_TIMEOUT_SEC + 180 -) SHIM_BIND_HOST = os.environ.get("SHIM_BIND_HOST", "0.0.0.0") SHIM_PORT = int(os.environ.get("SHIM_PORT", "18002")) -# On a Modal cluster the head is itself a Modal container and the sandboxes are -# network-isolated (no i6pn/cluster routing), so they can only reach the -# adapter via a public modal.forward tunnel rather than a private cluster IP. +# On a Modal cluster sandboxes are network-isolated and reach the adapter only +# via a public modal.forward tunnel. MODAL_EXPOSE_ADAPTER = os.environ.get("MODAL_EXPOSE_ADAPTER", "0").strip().lower() in ("1", "true", "yes") def _load_runtime(args) -> AgentRuntime: """Resolve + instantiate the agent runtime (env > arg > registry default). - Validation is eager (load_runtime / AgentRuntime.__init_subclass__), so a - misdeclared runtime fails the worker boot loudly instead of - AttributeError-ing mid-sample. + Validation is eager so a misdeclared runtime fails the worker boot loudly. """ spec = ( os.environ.get("ASYNC_RL_AGENT_RUNTIME") @@ -126,20 +74,14 @@ def _load_runtime(args) -> AgentRuntime: return load_runtime(spec) -# --------------------------------------------------------------------------- -# Singleton: tokenizer + runtime-selected adapter + background HTTP server. -# SingletonMeta keys per class, so there is exactly one runtime + adapter + -# server per rollout worker process; trajectories stay isolated by session_id. -# --------------------------------------------------------------------------- +# Singleton per worker process: tokenizer + adapter + bg HTTP server. class _State(metaclass=SingletonMeta): def __init__(self, args) -> None: self.args = args self.runtime = _load_runtime(args) self.tokenizer = load_tokenizer(args.hf_checkpoint, trust_remote_code=True) self.max_context_len = int(getattr(args, "rollout_max_context_len", 0) or 0) - # Adapter reuses the SGLang parsers configured for the served model so - # tool-call bash / reasoning are parsed correctly (e.g. - # --sglang-tool-call-parser qwen3_coder, --sglang-reasoning-parser qwen3). + # Reuse the served model's SGLang parsers so tool-call / reasoning parse. self.tool_parser = getattr(args, "sglang_tool_call_parser", None) or None self.reasoning_parser = getattr(args, "sglang_reasoning_parser", None) or None @@ -161,13 +103,10 @@ def __init__(self, args) -> None: tool_parser=self.tool_parser, reasoning_parser=self.reasoning_parser, ) - # Per-turn timing by session (bearer token == session_id); must be - # installed before the app starts. Feeds metadata["timing"] below. + # Per-turn timing by session; install before the app starts. profiling.install(self.adapter.app) - # handler_cancellation=True so a client disconnect cancels the handler - # coroutine, arming the adapter's fire-and-forget /abort_request. Without - # it a cancelled client leaves an inflight sglang /generate that races - # the next release_memory_occupation and trips sglang's idle assertion. + # handler_cancellation=True: a client disconnect cancels the handler and + # arms /abort_request, else an inflight /generate trips sglang's idle assert. self.app_handle = run_app_in_thread( self.adapter.app, host=SHIM_BIND_HOST, @@ -175,14 +114,9 @@ def __init__(self, args) -> None: thread_name="agent-adapter", runner_kwargs={"handler_cancellation": True}, ) - # Everything past the bind can still fail (e.g. modal.forward). The - # server thread is a daemon that holds SHIM_PORT for the process - # lifetime, and SingletonMeta only caches on a clean __init__, so a - # failure here would orphan the thread and make every later _State() - # collide on the port. Tear the server down on any failure so the bind - # is releasable and the next sample can retry. + # Work past the bind can still fail (e.g. modal.forward); tear down so + # the orphaned daemon thread doesn't hold SHIM_PORT against retries. try: - # Base URL (no /v1). The runtime appends whatever its wire API needs. self._tunnel_cm = None self.adapter_url = self._resolve_adapter_url(public_host) except BaseException: @@ -200,21 +134,16 @@ def __init__(self, args) -> None: def _resolve_adapter_url(self, public_host: str | None) -> str: """Pick the URL the in-sandbox agent dials back on. - On a Modal cluster the adapter must be reached through a per-process - ``modal.forward`` tunnel (the head is a Modal container; sandboxes are - network-isolated). Each rollout-worker process is its own ``_State`` - singleton, so it opens its OWN tunnel -- a single static env can't cover - multiple data-parallel workers. The tunnel context is held on ``self`` - for the process lifetime; litellm speaks HTTPS, so the encrypted - ``https://.modal.host`` URL needs no port juggling. + On a Modal cluster: a per-process ``modal.forward`` tunnel (one static + env can't cover multiple data-parallel workers), held on ``self`` for + the process lifetime. """ if not MODAL_EXPOSE_ADAPTER: return f"http://{public_host}:{self.app_handle.port}" import modal - # Blocking context manager (we're in sync __init__); never exited -- - # the process owns the tunnel until it dies. + # Blocking CM, never exited -- the process owns the tunnel until death. self._tunnel_cm = modal.forward(self.app_handle.port) tunnel = self._tunnel_cm.__enter__() logger.info("[async_rl] modal.forward tunnel for adapter port %d -> %s", self.app_handle.port, tunnel.url) @@ -225,11 +154,8 @@ def _resolve_adapter_url(self, public_host: str | None) -> str: # Trajectory -> Sample # --------------------------------------------------------------------------- def _start_session(state: _State, sample: Sample, md: dict[str, Any], sampling_params: dict[str, Any]) -> str: - """Register the adapter session BEFORE the agent starts. - - The in-sandbox agent sends ``session_id`` as its auth/bearer token so the - adapter groups all of its turns under one chain. - """ + """Register the adapter session BEFORE the agent starts (it sends + ``session_id`` as its bearer token to group its turns).""" if sample.session_id: session_id = sample.session_id elif sample.index is not None and sample.group_index is not None: @@ -237,10 +163,8 @@ def _start_session(state: _State, sample: Sample, md: dict[str, Any], sampling_p else: session_id = f"agent-{md['instance_id']}-{secrets.token_hex(8)}" sample.session_id = session_id - # sampling_defaults are the rollout's baseline, but the adapter applies the - # request body OVER them (adapters/common._sampling_params): an agent that - # sends its own temperature/top_p would silently override training's. - # Runtimes must strip sampling knobs from agent requests to stay on-policy. + # Adapter applies the request body OVER sampling_defaults; runtimes must + # strip the agent's own sampling knobs to stay on-policy. state.adapter.open_session( session_id, sampling_defaults=_sampling_params(state.args, sampling_params), @@ -250,14 +174,14 @@ def _start_session(state: _State, sample: Sample, md: dict[str, Any], sampling_p def _sampling_params(args, overrides: dict[str, Any] | None = None) -> dict[str, Any]: - # Kept tiny on purpose: the adapter fills the rest of its defaults. We only - # pin the knobs that must match training. Extend as needed. - # - # ``overrides`` is the sampling_params dict slime hands to generate(): on - # the train path it mirrors the rollout_* args, on the eval path it carries - # the per-dataset eval overrides (eval_config temperature/top_p/top_k), so - # honoring it here is what makes eval-time sampling settings take effect. - # Per-turn max_new_tokens deliberately stays adapter-governed. + # Pin the knobs that must match training; the adapter fills the rest. These + # become the session defaults the adapter applies UNDER each request. + # ``overrides`` (slime's sampling_params) carries the per-call values: + # the eval temperature/top_p AND ``max_new_tokens`` -- which slime sets to + # rollout_max_response_len for train and eval_max_response_len for eval. + # Forwarding it makes that the adapter's per-turn generation cap (still + # further clamped to the remaining rollout_max_context_len budget); dropping + # it would silently fall back to the adapter's hardcoded per-turn default. params = ( {} if args is None @@ -267,11 +191,12 @@ def _sampling_params(args, overrides: dict[str, Any] | None = None) -> dict[str, ("temperature", getattr(args, "rollout_temperature", None)), ("top_p", getattr(args, "rollout_top_p", None)), ("top_k", getattr(args, "rollout_top_k", None)), + ("max_new_tokens", getattr(args, "rollout_max_response_len", None)), ) if v is not None } ) - for k in ("temperature", "top_p", "top_k"): + for k in ("temperature", "top_p", "top_k", "max_new_tokens"): if overrides and overrides.get(k) is not None: params[k] = overrides[k] return params @@ -288,22 +213,23 @@ def _merge_samples( ): """Fan TokenSegments + reward out into Sample(s). - A single linear agent chain yields one ("final") segment -> K=1 -> one - Sample. Routing through ``fan_out_sample_segments`` (which handles K==1) - keeps it correct if an agent later adds context compaction ("wipe" - segments): reward is split reward/K and siblings share ``rollout_id`` so - the per-rollout loss reducer counts the trajectory once. + A linear chain yields one Sample; routing through ``fan_out_sample_segments`` + stays correct if an agent later adds context-compaction "wipe" segments + (reward split reward/K, siblings share ``rollout_id``). """ if not segments: - return _abort_result(sample, "adapter_session_empty") + # Carry the agent's exit code + failure tail (set by the env in + # reward_result.extra) onto the abort, so a zero-turn rollout self- + # explains in the dump instead of needing tail-only Modal logs. + diag = {k: reward_result.extra[k] for k in ("agent_exit_code", "agent_tail") if k in reward_result.extra} + return _abort_result(sample, "adapter_session_empty", extra=diag) trajectory_metadata = { **(sample.metadata or {}), "instance_id": instance_id, "is_solved": reward_result.is_solved, "elapsed_sec": elapsed_sec, - # Env-specific diagnostics (swe_gym: applied_cleanly; harbor: per-step - # rewards). Keys are env-namespaced or unambiguous by construction. + # Env-specific diagnostics (swe_gym: applied_cleanly; harbor: per-step). **reward_result.extra, } fanned = fan_out_sample_segments( @@ -318,7 +244,7 @@ def _merge_samples( reward_result.is_solved, elapsed_sec, len(fanned), - {k: v for k, v in reward_result.extra.items() if not isinstance(v, (list, dict))}, + {k: v for k, v in reward_result.extra.items() if k != "agent_tail" and not isinstance(v, (list, dict))}, ) return fanned @@ -329,15 +255,11 @@ def _merge_samples( async def generate(args, sample: Sample, sampling_params: dict[str, Any], evaluation: bool = False): """Per-sample agent rollout with a wall-clock guard. - Accepts ``evaluation`` (slime passes it when present in the signature) but - treats train and eval identically -- running the agent + grading the - result is what eval wants too. + Treats train and eval identically (run the agent + grade). """ state = _State(args) - # Row -> env dispatch. load_env imports the env module lazily, so this - # module stays importable on nodes that never boot a sandbox. A bad row - # (unknown task_type, unusable metadata) aborts THAT sample; systemic - # failures (an env module that doesn't import) still raise loudly. + # Row -> env dispatch (lazy import). A bad row aborts THAT sample; an env + # module that won't import still raises loudly. try: env = load_env((sample.metadata or {}).get("task_type")) md = env.normalize_metadata(sample) @@ -347,46 +269,46 @@ async def generate(args, sample: Sample, sampling_params: dict[str, Any], evalua return _abort_result(sample, f"env_dispatch_failed:{type(e).__name__}:{e}") instance_id = md["instance_id"] + # Enforced budgets (env defaults, or task.toml under override): recorded up + # front so the dump self-reports them even when the sample aborts. + sample.metadata = { + **(sample.metadata or {}), + "budgets": env.effective_budgets( + md, agent_time_budget_sec=AGENT_TIME_BUDGET_SEC, eval_timeout_sec=AGENT_EVAL_TIMEOUT_SEC + ), + } session_id = _start_session(state, sample, md, sampling_params) t0 = time.time() try: - async with asyncio.timeout(AGENT_GENERATE_GUARD_SEC): - reward_result: RewardResult = await env.rollout( - md, - runtime=state.runtime, - session_id=session_id, - adapter_url=state.adapter_url, - agent_time_budget_sec=AGENT_TIME_BUDGET_SEC, - eval_timeout_sec=AGENT_EVAL_TIMEOUT_SEC, - ) - # Fold the adapter's per-turn stats into the env's phase timing - # (one "timing" dict per sample -> dumps -> profile.py). - turn_stats = profiling.pop_session_stats(session_id) - if turn_stats: - reward_result.extra.setdefault("timing", {}).update(turn_stats) - segments = await state.adapter.finish_session(session_id) - return _merge_samples( - sample=sample, - state=state, - segments=segments, - reward_result=reward_result, - elapsed_sec=time.time() - t0, - instance_id=instance_id, - ) + reward_result: RewardResult = await env.rollout( + md, + runtime=state.runtime, + session_id=session_id, + adapter_url=state.adapter_url, + agent_time_budget_sec=AGENT_TIME_BUDGET_SEC, + eval_timeout_sec=AGENT_EVAL_TIMEOUT_SEC, + ) + # Fold adapter per-turn stats into the env's phase timing. + turn_stats = profiling.pop_session_stats(session_id) + if turn_stats: + reward_result.extra.setdefault("timing", {}).update(turn_stats) + segments = await state.adapter.finish_session(session_id) + return _merge_samples( + sample=sample, + state=state, + segments=segments, + reward_result=reward_result, + elapsed_sec=time.time() - t0, + instance_id=instance_id, + ) - except asyncio.TimeoutError: - _log_timeout_diagnostic(t0) + except SandboxBootTimeout as e: _attach_partial_timing(sample, session_id, t0) - return _abort_result(sample, "wall_clock_timeout") + return _abort_result(sample, f"boot_timeout:{e.timeout_sec}s") except asyncio.CancelledError: - # A stray CancelledError from inside the rollout (e.g. the Modal SDK's - # synchronicity bridge cancelling an in-flight .aio call on a client - # hiccup) is not caught by `except Exception` and would otherwise - # propagate through generate_and_rm_group's gather and cancel the - # WHOLE generate_rollout_async task, crashing the training step. A - # genuine external cancel (the asyncio.timeout guard converts its own - # to TimeoutError before this; Ray/loop teardown does not) leaves the - # task in a cancelling state -- re-raise only then. + # A stray CancelledError from inside the rollout (e.g. Modal's + # synchronicity bridge) would crash the whole training step. Only a + # genuine external cancel leaves the task cancelling -- re-raise then. if asyncio.current_task().cancelling(): raise logger.error("[async_rl] %s: stray CancelledError; aborting sample", instance_id) @@ -397,61 +319,37 @@ async def generate(args, sample: Sample, sampling_params: dict[str, Any], evalua _attach_partial_timing(sample, session_id, t0) return _abort_result(sample, f"exception:{type(e).__name__}") finally: - # Close the sid before the next train step's release_memory_occupation; - # stragglers from this trajectory would otherwise race its idle assert. + # Close the sid before the next step's release_memory_occupation, else + # stragglers race its idle assert. await state.adapter.finish_session(session_id) # idempotent def _attach_partial_timing(sample: Sample, session_id: str, t0: float) -> None: - """On abort, keep whatever turn stats accrued -- distinguishes 'agent was - alive but slow' (turns accrued) from 'never dialed in' (no stats).""" + """On abort, keep accrued turn stats (distinguishes 'alive but slow' from + 'never dialed in').""" stats = profiling.pop_session_stats(session_id) or {} stats["elapsed_at_abort"] = round(time.time() - t0, 1) sample.metadata = {**(sample.metadata or {}), "timing": stats} -def _log_timeout_diagnostic(t0: float) -> None: - """Dump pending-task names when the wall-clock guard fires. Never crashes.""" - try: - elapsed = time.time() - t0 - pending = [t for t in asyncio.all_tasks() if not t.done()] - stuck = [] - for t in pending[:5]: - coro = getattr(t, "_coro", None) - stuck.append(getattr(coro, "__qualname__", repr(coro))) - logger.warning( - "[async_rl] generate() wall_clock_timeout after %.1fs (guard=%ds); %d tasks pending; stuck: %s", - elapsed, - AGENT_GENERATE_GUARD_SEC, - len(pending), - stuck, - ) - except Exception: # pragma: no cover - pass - - -def _abort(sample: Sample, reason: str) -> Sample: +def _abort(sample: Sample, reason: str, extra: dict[str, Any] | None = None) -> Sample: sample.tokens = [0, 0] sample.response = "" sample.response_length = 1 sample.loss_mask = [0] - # Per-token fields must stay shape-consistent with response_length: when - # any sample in the batch carries rollout_log_probs, the train actor - # slices it for EVERY sample (actor._get_rollout_data), and a None here - # crashes the whole train step. loss_mask is 0 so the value never trains. + # Shape-consistent with response_length: the train actor slices + # rollout_log_probs for every sample, so a None here crashes the step. sample.rollout_log_probs = [0.0] sample.reward = 0.0 - # Mirror fan_out_sample_segments' convention (rollout_id = sample.index): - # build_dp_schedule groups samples by rollout_id, so a None here collapses - # every aborted sample in the batch into ONE rollout group and the unique - # rollout count drops below global_batch_size, crashing the train step. + # Mirror fan_out_sample_segments: build_dp_schedule groups by rollout_id, so + # a None collapses all aborts into one group and drops below global_batch_size. sample.rollout_id = sample.index sample.status = Sample.Status.ABORTED - sample.metadata = {**(sample.metadata or {}), "abort_reason": reason} + sample.metadata = {**(sample.metadata or {}), "abort_reason": reason, **(extra or {})} logger.warning("[async_rl] aborted: %s", reason) return sample -def _abort_result(sample: Sample, reason: str): +def _abort_result(sample: Sample, reason: str, extra: dict[str, Any] | None = None): """Uniform list shape for this (potentially fan-out) generate function.""" - return [_abort(sample, reason)] + return [_abort(sample, reason, extra)] diff --git a/async_rl_research/modal_sandbox.py b/async_rl_research/modal_sandbox.py index add7966e70..7daebfe5fe 100644 --- a/async_rl_research/modal_sandbox.py +++ b/async_rl_research/modal_sandbox.py @@ -1,26 +1,17 @@ """Modal sandbox backend for agent rollouts. -``ModalSandbox`` is the local analog of ``slime.agent.sandbox.E2BSandbox``: a -drop-in implementation of the ``Sandbox`` protocol (``sandbox_id``, -``__aenter__``/``__aexit__``, ``exec``, ``write_file``, ``read_file``) backed by -``modal.Sandbox``. It is pure infrastructure -- it knows nothing about tasks, -agents, or grading -- so the env glue (``env/swe_gym.py``, ``env/harbor.py``) -and any agent runtime can build on top of it. The image is either a registry -ref (``str``) or a host-side Dockerfile build (``DockerfileImage``). - -``modal`` is imported lazily (inside ``__aenter__``) so this module stays -importable without Modal installed, mirroring the E2B backend's lazy -``e2b`` import. That keeps importing the env modules cheap on nodes that -never actually boot a sandbox. - -Boot concurrency and create-retry live here (not in the SWE glue) so that -*every* sandbox creation -- the work sandbox and the clean evaluator sandbox -alike -- is gated and retried uniformly. +``ModalSandbox`` is a drop-in ``Sandbox`` protocol impl backed by +``modal.Sandbox`` (the local analog of ``E2BSandbox``). Pure infrastructure, so +the env glue and agent runtimes build on it. Image is a registry ref or a +host-side Dockerfile build (``DockerfileImage``). ``modal`` is imported lazily +so this stays importable without Modal installed. Boot concurrency and +create-retry live here so every sandbox creation is gated/retried uniformly. Env knobs --------- MODAL_BOOT_CONCURRENCY max concurrent sandbox creates (default 8) MODAL_BOOT_RETRIES transient-create retries (default 2) + MODAL_BOOT_TIMEOUT_SEC cap on sandbox boot/image-pull (default 600) MODAL_RPC_RETRIES transient-exec retries (default 2) SLIME_AGENT_SANDBOX_LIFETIME_SEC sandbox max lifetime (default 3600) (legacy alias: MODAL_SANDBOX_LIFETIME_SEC) @@ -31,6 +22,9 @@ SLIME_AGENT_SANDBOX_CPU fractional cpu cores (optional) SLIME_AGENT_SANDBOX_MEMORY_MB memory in MB (optional) SLIME_AGENT_SANDBOX_GPU gpu spec, e.g. "a10g" (optional) + SLIME_AGENT_SANDBOX_VM_RUNTIME 1 to boot a VM sandbox instead of gVisor + (real kernel, allowlisted workspaces only; VM memory is static, floored + at MODAL_VM_MEMORY_FLOOR_MB, default 2048) SLIME_AGENT_SANDBOX_ADD_PYTHON add a python to the image (optional) MODAL_REGISTRY_SECRET modal.Secret name for a private registry/ECR MODAL_ENVIRONMENT modal environment name (optional) @@ -55,16 +49,21 @@ FileContent = str | bytes | Path +class SandboxBootTimeout(Exception): + """Sandbox create/image-pull exceeded its boot budget (MODAL_BOOT_TIMEOUT_SEC).""" + + def __init__(self, timeout_sec: int, image: str = "") -> None: + self.timeout_sec = timeout_sec + super().__init__(f"sandbox boot exceeded {timeout_sec}s: {image}") + + @dataclass(frozen=True) class DockerfileImage: """Build-from-Dockerfile image spec (harbor-style task environments). - ``path`` is the host-side Dockerfile, ``context_dir`` the build context - (defaults to the Dockerfile's directory). Modal content-hashes the - Dockerfile commands + context files, so identical task files are a cache - hit across rollout boots -- the same property the - registry path gets from immutable tags. Registry auth (the FROM pull) is - Modal's builder default; private base images are not supported here. + ``context_dir`` defaults to the Dockerfile's dir. Modal content-hashes the + Dockerfile + context so identical task files cache-hit across boots. FROM + pull uses Modal's default builder auth (no private base images). """ path: str @@ -74,29 +73,17 @@ class DockerfileImage: def description(self) -> str: return f"dockerfile:{self.path}" -# Modal validates the total argv size of an exec against ARG_MAX (64 KiB) and -# raises InvalidError client-side. Dataset eval commands can blow well past it -# (SWE-Gym eval_cmds inline the entire test_patch as a heredoc), so commands -# above this threshold are shipped into the sandbox as a script file and run -# from there. Kept well under the limit to leave room for the bash/runuser -# wrapper argv. +# Modal validates exec argv against ARG_MAX (64 KiB) client-side; larger +# commands are staged as a script file. Under the limit to leave room for the +# bash/runuser wrapper argv. _EXEC_ARGV_LIMIT_BYTES = 32_768 def _normalize_image_ref(ref: str) -> str: - """Lowercase the repository name of a registry ref; preserve tag/digest. - - OCI/Docker repository names must be lowercase (``skopeo`` rejects mixed - case with "invalid reference format: repository name must be lowercase"). - Some SWE-bench task images carry mixed-case orgs/repos in the dataset - (e.g. ``Project-MONAI_s_MONAI-3326``) while the actual Docker Hub repo is - lowercase, so normalize here -- on the shared boot path -- so the rollout - builds the exact same (valid) ref. - - The tag (``:latest``) and digest (``@sha256:...``) are case-sensitive and - left untouched; only the name (host + path) is lowercased. The host is - already lowercase in practice and any ``:port`` is digits, so lowercasing - the whole name portion is safe. + """Lowercase a registry ref's repository name; preserve tag/digest. + + OCI repo names must be lowercase, but some SWE-bench dataset images carry + mixed-case orgs/repos. Tag/digest are case-sensitive and left untouched. """ if not ref: return ref @@ -107,7 +94,7 @@ def _normalize_image_ref(ref: str) -> str: else: slash = ref.rfind("/") colon = ref.rfind(":") - if colon > slash: # a tag colon (not a registry :port, which precedes the last '/') + if colon > slash: # a tag colon, not a registry :port name, sep, suffix = ref[:colon], ":", ref[colon + 1 :] return name.lower() + sep + suffix @@ -125,8 +112,7 @@ def _getenv_int(*names: str, default: int) -> int: return int(raw) if raw else default -# Process-wide create gate + cached App. Created lazily on the running loop so -# importing this module never touches asyncio or modal. +# Process-wide create gate + cached App, lazily created on the running loop. _BOOT_SEM: asyncio.Semaphore | None = None _APP_CACHE: dict[str, Any] = {} _APP_LOCK: asyncio.Lock | None = None @@ -149,15 +135,18 @@ def _app_lock() -> asyncio.Lock: class ModalSandbox: """Async context manager around ``modal.Sandbox`` (the ``Sandbox`` protocol). - Normal command failures surface as exit codes. Modal transport errors are - retried while transient and otherwise raised, so an infrastructure problem - is never silently scored as a failed SWE test. + Command failures surface as exit codes; transient Modal transport errors are + retried so infra problems are never scored as a failed test. """ default_lifetime_sec = 3600 + default_boot_timeout_sec = 600 default_app_name = "slime-agent-sandboxes" default_create_retries = 2 default_rpc_retries = 2 + # Main process that keeps the sandbox alive (exec runs separate processes). + # Required for images that blank their entrypoint (see __aenter__). + keepalive_command = ("sleep", "infinity") rpc_backoff_base_sec = 1.0 # Per-stream output cap so a runaway command can't balloon host memory. output_cap_chars = _getenv_int("MODAL_EXEC_OUTPUT_CAP", default=1_000_000) @@ -177,14 +166,17 @@ def __init__( app_name: str | None = None, add_python: str | None = None, workdir: str | None = None, + vm_runtime: bool | None = None, + boot_timeout: int | None = None, ) -> None: if isinstance(image, DockerfileImage): self.image_spec: DockerfileImage | None = image - self.image = image.description # log/cache-key label only + self.image = image.description # label only else: self.image_spec = None self.image = _normalize_image_ref(image) self.timeout = timeout if timeout is not None else self._lifetime_from_env() + self.boot_timeout = boot_timeout if boot_timeout is not None else self._boot_timeout_from_env() self.block_network = block_network if block_network is not None else self._block_network_from_env() self.cpu = cpu if cpu is not None else self._float_from_env("SLIME_AGENT_SANDBOX_CPU", "MODAL_SANDBOX_CPU") self.memory_mb = ( @@ -193,6 +185,10 @@ def __init__( else self._int_from_env("SLIME_AGENT_SANDBOX_MEMORY_MB", "MODAL_SANDBOX_MEMORY_MB") ) self.gpu = gpu or (_getenv("SLIME_AGENT_SANDBOX_GPU", "MODAL_SANDBOX_GPU") or None) + self.vm_runtime = vm_runtime if vm_runtime is not None else self._vm_runtime_from_env() + if self.vm_runtime and self.memory_mb is None: + # VM memory is static; Modal's 128MB default OOMs a VM. + self.memory_mb = _getenv_int("MODAL_VM_MEMORY_FLOOR_MB", default=2048) self.registry_secret = registry_secret or (_getenv("MODAL_REGISTRY_SECRET") or None) self.rpc_retries = rpc_retries if rpc_retries is not None else _getenv_int( "MODAL_RPC_RETRIES", "SLIME_AGENT_SANDBOX_RPC_RETRIES", default=self.default_rpc_retries @@ -216,6 +212,18 @@ def _lifetime_from_env(cls) -> int: "SLIME_AGENT_SANDBOX_LIFETIME_SEC", "MODAL_SANDBOX_LIFETIME_SEC", default=cls.default_lifetime_sec ) + @classmethod + def _boot_timeout_from_env(cls) -> int: + return _getenv_int("MODAL_BOOT_TIMEOUT_SEC", default=cls.default_boot_timeout_sec) + + @staticmethod + def _vm_runtime_from_env() -> bool: + return _getenv("SLIME_AGENT_SANDBOX_VM_RUNTIME", "MODAL_SANDBOX_VM_RUNTIME").strip().lower() in ( + "1", + "true", + "yes", + ) + @staticmethod def _block_network_from_env() -> bool: return _getenv("SLIME_AGENT_SANDBOX_BLOCK_NETWORK", "MODAL_SANDBOX_BLOCK_NETWORK").strip().lower() in ( @@ -237,11 +245,8 @@ def _int_from_env(*names: str) -> int | None: # -- transient-error classification ------------------------------------ @staticmethod def _is_transient(e: BaseException) -> bool: - """True if ``e`` is a Modal transport flap that is safe to retry. - - Command-level timeouts (the command itself ran too long) are NOT - transient -- retrying would just burn the budget again. - """ + """True if ``e`` is a Modal transport flap safe to retry (command-level + timeouts are NOT transient).""" name = type(e).__name__ if "SandboxTimeout" in name or name == "TimeoutError": return False @@ -313,7 +318,7 @@ def _build_image(self): return modal.Image.from_registry(self.image, secret=secret, **kwargs) async def __aenter__(self) -> "ModalSandbox": - import modal # lazy: keep this module importable without modal installed + import modal # lazy self._modal = modal app = await self._get_app() @@ -333,12 +338,25 @@ async def __aenter__(self) -> "ModalSandbox": create_kwargs["gpu"] = self.gpu if self.workdir: create_kwargs["workdir"] = self.workdir + if self.vm_runtime: + create_kwargs["experimental_options"] = {"vm_runtime": True} async def _create(): async with _boot_sem(): - return await modal.Sandbox.create.aio(**create_kwargs) + # Explicit keepalive command. Some task images (SWE-bench-Pro) + # blank the entrypoint (`ENTRYPOINT []`) expecting an external + # `sleep infinity` from docker-compose; with no command Modal runs + # the now-empty entrypoint and the container exits immediately + # (rc 128) -> every later exec hits "sandbox already shut down". + # `exec` spawns its own processes, so this is inert for images + # that already stay up (SWE-bench verified). + return await modal.Sandbox.create.aio(*self.keepalive_command, **create_kwargs) - self._sb = await self._retry(f"create({self.image[:48]!r})", self.create_retries, _create) + try: + async with asyncio.timeout(self.boot_timeout): + self._sb = await self._retry(f"create({self.image[:48]!r})", self.create_retries, _create) + except TimeoutError: + raise SandboxBootTimeout(self.boot_timeout, self.image[:48]) from None self.sandbox_id = str(getattr(self._sb, "object_id", "") or "") return self @@ -371,30 +389,24 @@ async def exec( check: bool = False, ) -> ExecResult: sb = self._require_sandbox() - # Honor user= so the backend is drop-in for agents that drop privileges - # (mini-swe-agent only ever runs as root). + # Honor user= for agents that drop privileges. if user and user != "root": inner = f"runuser -u {shlex.quote(user)} -- bash -lc {shlex.quote(cmd)}" else: inner = cmd if len(inner.encode("utf-8", errors="ignore")) > _EXEC_ARGV_LIMIT_BYTES: - # Too big for exec argv: stage the command as a script and run that. - # The script is left behind (no rm) so _retry can safely re-run the - # exec; sandboxes are ephemeral, so /tmp leftovers cost nothing. + # Too big for exec argv: stage as a script. Left behind so _retry can + # re-run; sandboxes are ephemeral. script = f"/tmp/.modal_exec_{uuid.uuid4().hex}.sh" await self.write_file(script, inner) inner = f"bash {shlex.quote(script)}" secrets = [self._modal.Secret.from_dict({str(k): str(v) for k, v in env.items()})] if env else [] async def _run() -> ExecResult: - # text=False: Modal's text mode decodes strictly inside the client, - # so one non-UTF8 byte in command output (a `git diff` over latin-1 - # sources, a `tail -c` that splits a multibyte char) raises - # UnicodeDecodeError mid-stream and kills the rollout. Take bytes - # and decode host-side with errors="replace" instead. + # text=False: Modal's text mode decodes strictly, so one non-UTF8 + # byte kills the rollout. Take bytes, decode host-side with replace. proc = await sb.exec.aio("bash", "-lc", inner, timeout=timeout, secrets=secrets, text=False) - # Start draining both streams BEFORE awaiting wait(): a full pipe - # buffer would otherwise block the command and deadlock wait(). + # Drain both streams BEFORE wait(): a full pipe buffer deadlocks wait(). out_task = asyncio.create_task(_read_stream_capped(proc.stdout, self.output_cap_chars)) err_task = asyncio.create_task(_read_stream_capped(proc.stderr, self.output_cap_chars)) exit_code = int(await proc.wait.aio()) @@ -424,7 +436,7 @@ async def _write(): await self.exec(f"chown {quoted}:{quoted} {shlex.quote(sandbox_path)}", timeout=30, check=False) async def read_file(self, sandbox_path: str, *, user: str = "root") -> str: - del user # Modal file APIs read as the sandbox owner; user is advisory. + del user # advisory: Modal reads as the sandbox owner sb = self._require_sandbox() try: return await self._retry( @@ -437,13 +449,9 @@ async def read_file(self, sandbox_path: str, *, user: str = "root") -> str: async def _read_stream_capped(stream: Any, cap: int) -> str: - """Drain ``stream`` fully but keep at most ``cap`` chars. - - The whole stream is consumed (so the process can finish) while only the - first ``cap`` characters are retained; the tail is dropped with a marker. - Byte chunks (``exec`` runs with ``text=False``) are decoded incrementally - so a multibyte char split across chunks doesn't turn into mojibake, with - ``errors="replace"`` so genuinely non-UTF8 output can never raise. + """Drain ``stream`` fully but keep only the first ``cap`` chars (tail dropped + with a marker). Decodes byte chunks incrementally with ``errors="replace"`` + so a split multibyte char doesn't mojibake and non-UTF8 never raises. """ if stream is None: return "" diff --git a/async_rl_research/profiles/.gitignore b/async_rl_research/profiles/.gitignore deleted file mode 100644 index 4a75feb3c0..0000000000 --- a/async_rl_research/profiles/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Per-run profiling artifacts produced by profile.py; regenerate by re-running -# it against the W&B run + rollout dump. Keep only the code + methodology. -runs.jsonl -ATTRIBUTION.md diff --git a/async_rl_research/profiles/__init__.py b/async_rl_research/profiles/__init__.py deleted file mode 100644 index 722fd9e727..0000000000 --- a/async_rl_research/profiles/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Rollout profiling: in-rollout instrumentation (profiling.py), offline -analyzer (profile.py), methodology (PERF.md), and generated artifacts -(runs.jsonl / ATTRIBUTION.md).""" diff --git a/async_rl_research/profiles/profiling.py b/async_rl_research/profiles/profiling.py deleted file mode 100644 index c4589ef913..0000000000 --- a/async_rl_research/profiles/profiling.py +++ /dev/null @@ -1,104 +0,0 @@ -"""In-rollout profiling: phase timers + per-session adapter turn stats. - -Two collectors, both feeding ``sample.metadata["timing"]`` (and from there the -``rollout_{id}.pt`` debug dumps that ``profile.py`` aggregates offline): - -``PhaseTimer`` - Wall-clock spans for the env-side phases of one sample's rollout - (work_boot / prep / agent / diff / eval / eval_boot). Owned by the env's - ``rollout()``; serialized via ``as_dict()`` into ``RewardResult.extra``. - -``timing_middleware`` / ``pop_session_stats`` - An aiohttp middleware installed on the adapter app (``generate._State`` - owns both the adapter and the server start, so this needs NO slime-core - change). It times every generation request and attributes it to the - session via the bearer token -- which IS the slime session_id by the - adapter's auth convention. ``gen_s`` measures the whole adapter hop - (render -> tokenize -> SGLang /generate -> response build), so - ``gen_s/n_turns`` minus the engine-side e2e latency (W&B ``sgl_engine``) - is the adapter's own per-turn overhead. - -Caveats (fine for profiling, do not treat as exact accounting): - * the store is per-process (one rollout worker = one adapter = one store); - * a request in flight when a sample is aborted may be missing from, or - inflate, that sample's stats; - * dict mutation is GIL-atomic and the reader (``generate()``) only pops - after the agent finished, so no locking is needed. -""" - -from __future__ import annotations - -import time -from contextlib import contextmanager - -from aiohttp import web - -# Endpoints that represent one model turn (OpenAI chat/responses, Anthropic -# messages). Health checks and model listings must not count as turns. -_GENERATION_PATHS = ("/v1/chat/completions", "/v1/responses", "/v1/messages") - - -def _session_id(request: web.Request) -> str: - """Bearer token (slime's session auth convention), else x-api-key. - - Mirrors slime.agent.adapters.common.request_session_id without importing - it (that pulls the torch-heavy slime chain into this otherwise - dependency-free module). The body-based fallbacks there don't apply: our - runtimes always authenticate with session_id as the key. - """ - auth = request.headers.get("Authorization", "") - if auth.lower().startswith("bearer ") and auth[7:].strip(): - return auth[7:].strip() - return (request.headers.get("X-Api-Key") or "").strip() or "default" - - -class PhaseTimer: - """Accumulates named wall-clock spans for one sample's rollout.""" - - def __init__(self) -> None: - self._spans: dict[str, float] = {} - - @contextmanager - def phase(self, name: str): - t0 = time.monotonic() - try: - yield - finally: - self.record(name, time.monotonic() - t0) - - def record(self, name: str, seconds: float) -> None: - self._spans[name] = round(self._spans.get(name, 0.0) + seconds, 3) - - def as_dict(self) -> dict[str, float]: - return dict(self._spans) - - -# --------------------------------------------------------------------------- -# Adapter-side per-session turn stats -# --------------------------------------------------------------------------- -_SESSION_STATS: dict[str, dict[str, float]] = {} - - -@web.middleware -async def timing_middleware(request: web.Request, handler): - if request.method != "POST" or not request.path.endswith(_GENERATION_PATHS): - return await handler(request) - t0 = time.monotonic() - try: - return await handler(request) - finally: - sid = _session_id(request) - stats = _SESSION_STATS.setdefault(sid, {"n_turns": 0, "gen_s": 0.0}) - stats["n_turns"] += 1 - stats["gen_s"] = round(stats["gen_s"] + (time.monotonic() - t0), 3) - - -def install(app: web.Application) -> None: - """Idempotently add the timing middleware (call BEFORE the app starts).""" - if timing_middleware not in app.middlewares: - app.middlewares.append(timing_middleware) - - -def pop_session_stats(session_id: str) -> dict[str, float] | None: - """Drain one session's turn stats (None if the agent never dialed in).""" - return _SESSION_STATS.pop(session_id, None) From 7f92f58497f7ce389fdbc7f203d6276b89801cd6 Mon Sep 17 00:00:00 2001 From: junlin-star Date: Mon, 15 Jun 2026 19:52:21 -0700 Subject: [PATCH 09/11] Apply pre-commit formatting (ruff, isort, black) Co-authored-by: Cursor --- async_rl_research/agent/adapters/qwen.py | 15 +++----- async_rl_research/agent/base.py | 10 +++--- async_rl_research/agent/mini_swe_agent.py | 15 ++++---- async_rl_research/environment/base.py | 4 ++- .../environment/convert2slime/harbor.py | 15 +++++--- .../convert2slime/openthoughts_agent.py | 8 ++--- async_rl_research/environment/harbor.py | 34 ++++++++++++++----- async_rl_research/environment/swe_gym.py | 4 +-- async_rl_research/generate.py | 2 +- async_rl_research/modal_sandbox.py | 15 +++++--- 10 files changed, 70 insertions(+), 52 deletions(-) diff --git a/async_rl_research/agent/adapters/qwen.py b/async_rl_research/agent/adapters/qwen.py index e6991e1f2c..08269037eb 100644 --- a/async_rl_research/agent/adapters/qwen.py +++ b/async_rl_research/agent/adapters/qwen.py @@ -21,13 +21,12 @@ import asyncio import json -from typing import Any from aiohttp import web from slime.agent.adapters import openai as _slime_openai +from slime.agent.adapters.common import ADAPTER_KEY, TOKENIZER_KEY, BaseAdapter, render_token_ids from slime.agent.adapters.openai import OpenAIAdapter -from slime.agent.adapters.common import ADAPTER_KEY, BaseAdapter, TOKENIZER_KEY, render_token_ids def _dictify_tool_arguments(messages: list[dict]) -> None: @@ -53,9 +52,7 @@ def _dictify_tool_arguments(messages: list[dict]) -> None: def _template_ids(tok, messages: list[dict], *, add_generation_prompt: bool) -> list[int]: """``apply_chat_template`` -> flat token-id list (tolerating the 1-element batch that some transformers versions return for ``tokenize=True``).""" - enc = tok.apply_chat_template( - messages, tools=None, tokenize=True, add_generation_prompt=add_generation_prompt - ) + enc = tok.apply_chat_template(messages, tools=None, tokenize=True, add_generation_prompt=add_generation_prompt) ids = enc["input_ids"] if hasattr(enc, "__getitem__") and "input_ids" in enc else enc ids = list(ids) if ids and isinstance(ids[0], list): # transformers>=5 may return [[...ids...]] @@ -112,9 +109,7 @@ def _build_prompt(target, messages: list[dict], tools_schema: list[dict] | None, return render_token_ids(target, tok) -async def _run_turn( - request: web.Request, body: dict, messages: list[dict] -): +async def _run_turn(request: web.Request, body: dict, messages: list[dict]): """Mirror of ``openai._run_turn`` calling the dict-args ``_build_prompt``.""" sid = _slime_openai._request_session_id(request, body) adapter = request.app[ADAPTER_KEY] @@ -146,9 +141,7 @@ async def _handle_chat_completions(request: web.Request) -> web.StreamResponse: raise web.HTTPBadRequest(text="messages must be a list") turn, parsed, in_tok, out_tok = await _run_turn(request, body, messages) if body.get("stream"): - return await _slime_openai._stream_chat_completion( - request, body, parsed, turn.finish_reason, in_tok, out_tok - ) + return await _slime_openai._stream_chat_completion(request, body, parsed, turn.finish_reason, in_tok, out_tok) return web.json_response( _slime_openai._chat_completion_response(body, parsed, turn.finish_reason, in_tok, out_tok) ) diff --git a/async_rl_research/agent/base.py b/async_rl_research/agent/base.py index 9861c70e2b..d1a7a8edfc 100644 --- a/async_rl_research/agent/base.py +++ b/async_rl_research/agent/base.py @@ -121,11 +121,7 @@ async def _detached_run( launch, done, log = self._launch_scratch_files() exports = "".join(f"export {k}={q(str(v))}\n" for k, v in (env or {}).items()) launcher_body = ( - "#!/bin/bash\n" - f"cd {q(workdir)}\n" - f"{exports}" - f"{command} > {q(log)} 2>&1\n" - f"echo $? > {q(done)}\n" + "#!/bin/bash\n" f"cd {q(workdir)}\n" f"{exports}" f"{command} > {q(log)} 2>&1\n" f"echo $? > {q(done)}\n" ) await sb.write_file(f"{workdir}/{launch}", launcher_body) # rm the done marker BEFORE launching: a stale marker from a prior leg @@ -151,7 +147,9 @@ async def _detached_run( break tail = "" if exit_code != 0: - _, raw_tail, _ = await sb.exec(f"tail -c 4000 {q(f'{workdir}/{log}')} 2>/dev/null", check=False, timeout=15) + _, raw_tail, _ = await sb.exec( + f"tail -c 4000 {q(f'{workdir}/{log}')} 2>/dev/null", check=False, timeout=15 + ) tail = (raw_tail or "").strip() if tail: logger.warning("[%s] %s exit=%s %s tail:\n%s", self.name, log_tag, exit_code, log, tail) diff --git a/async_rl_research/agent/mini_swe_agent.py b/async_rl_research/agent/mini_swe_agent.py index e75172c784..c1828308c3 100644 --- a/async_rl_research/agent/mini_swe_agent.py +++ b/async_rl_research/agent/mini_swe_agent.py @@ -16,13 +16,12 @@ from slime.agent.sandbox import Sandbox -from .base import AgentRunResult, AgentRuntime +from ..environment.base import PROBLEM_FILE + # Renders tool-call arguments as a dict so Qwen3.6's qwen3_coder chat template # doesn't crash on turn 2+ (safe for hermes-style Qwen3 too). from .adapters import QwenOpenAIAdapter - -from ..environment.base import PROBLEM_FILE - +from .base import AgentRunResult, AgentRuntime MSWE_STEP_LIMIT = int(os.environ.get("MSWE_STEP_LIMIT", "50")) # Which YAML config (prompts) the runner loads. Override ladder: MSWE_CONFIG @@ -35,9 +34,7 @@ MSWE_PIP_SPEC = os.environ.get("MSWE_PIP_SPEC", "mini-swe-agent==2.3.1") # Prepended to PATH for the agent's bash commands: LocalEnvironment runs via # /bin/sh so `conda activate testbed` never fires; this is how its python wins. -MSWE_PATH_PREPEND = os.environ.get( - "MSWE_PATH_PREPEND", "/opt/miniconda3/envs/testbed/bin:/opt/miniconda3/bin" -) +MSWE_PATH_PREPEND = os.environ.get("MSWE_PATH_PREPEND", "/opt/miniconda3/envs/testbed/bin:/opt/miniconda3/bin") # Isolated venv so the testbed conda env is never used or clobbered. Provisioned # at boot with uv; can be pre-baked into a derived image. MSWE_AGENT_VENV = os.environ.get("MSWE_AGENT_VENV", "/opt/mswe-agent") @@ -140,8 +137,8 @@ "if ! command -v uv >/dev/null 2>&1; then\n" # Ensure curl via whatever package manager the image ships (best-effort: # if none works we still try the pip path below). - " command -v curl >/dev/null 2>&1 || retry \"apt-get update && apt-get install -y curl" - " || apk add --no-cache curl || yum install -y curl || dnf install -y curl\" || true\n" + ' command -v curl >/dev/null 2>&1 || retry "apt-get update && apt-get install -y curl' + ' || apk add --no-cache curl || yum install -y curl || dnf install -y curl" || true\n' # uv via the pinned astral script (bypasses the image's pip entirely); # pip fallback forces a clean PyPI index so a poisoned image config can't win, # then retries with --break-system-packages so PEP 668 ("externally-managed") diff --git a/async_rl_research/environment/base.py b/async_rl_research/environment/base.py index bd5b278a03..7db242e1a5 100644 --- a/async_rl_research/environment/base.py +++ b/async_rl_research/environment/base.py @@ -93,7 +93,9 @@ async def rollout( ``eval_timeout_sec`` caps each grading command. """ - def effective_budgets(self, md: dict[str, Any], *, agent_time_budget_sec: int, eval_timeout_sec: int) -> dict[str, int]: + def effective_budgets( + self, md: dict[str, Any], *, agent_time_budget_sec: int, eval_timeout_sec: int + ) -> dict[str, int]: """Wall-clock budgets actually enforced this rollout (for the dump/dashboard).""" from ..modal_sandbox import ModalSandbox diff --git a/async_rl_research/environment/convert2slime/harbor.py b/async_rl_research/environment/convert2slime/harbor.py index c4836c10f7..05a91c942c 100644 --- a/async_rl_research/environment/convert2slime/harbor.py +++ b/async_rl_research/environment/convert2slime/harbor.py @@ -25,10 +25,11 @@ import logging import re import shutil -import tomllib from pathlib import Path from typing import Any +import tomllib + logger = logging.getLogger(__name__) _COPY_IGNORE = shutil.ignore_patterns(".git", "__pycache__", ".DS_Store", ".venv", "node_modules") @@ -137,7 +138,9 @@ def translate_task(task_dir: Path, *, dataset: str | None = None) -> dict[str, A steps_md = _steps_md(cfg, task_dir) if steps_md: instruction_path = task_dir / "instruction.md" - instruction = instruction_path.read_text(encoding="utf-8") if instruction_path.is_file() else steps_md[0]["instruction"] + instruction = ( + instruction_path.read_text(encoding="utf-8") if instruction_path.is_file() else steps_md[0]["instruction"] + ) else: instruction = _instruction(task_dir / "instruction.md", "instruction.md") if not (task_dir / "tests" / "test.sh").is_file(): @@ -266,7 +269,9 @@ def main(argv: list[str] | None = None) -> int: source.add_argument("--registry", help="harbor registry.json path or URL (needs `pip install harbor`)") parser.add_argument("--dataset", help="dataset name in the registry (with --registry)") parser.add_argument("--dataset-version", help="dataset version in the registry (default: last match)") - parser.add_argument("--out-dir", type=Path, required=True, help="output dir (JSONL + tasks/); use the slime-data volume") + parser.add_argument( + "--out-dir", type=Path, required=True, help="output dir (JSONL + tasks/); use the slime-data volume" + ) parser.add_argument("--name", help="JSONL filename stem (default: dataset or tasks-dir name)") parser.add_argument("--limit", type=int, help="maximum tasks to convert") args = parser.parse_args(argv) @@ -274,7 +279,9 @@ def main(argv: list[str] | None = None) -> int: if args.registry: if not args.dataset: parser.error("--registry requires --dataset") - task_dirs = _download_from_registry(args.registry, args.dataset, args.dataset_version, args.out_dir / "downloads") + task_dirs = _download_from_registry( + args.registry, args.dataset, args.dataset_version, args.out_dir / "downloads" + ) name = args.name or args.dataset dataset = args.dataset else: diff --git a/async_rl_research/environment/convert2slime/openthoughts_agent.py b/async_rl_research/environment/convert2slime/openthoughts_agent.py index 1919b25c01..b02fb8694b 100644 --- a/async_rl_research/environment/convert2slime/openthoughts_agent.py +++ b/async_rl_research/environment/convert2slime/openthoughts_agent.py @@ -137,16 +137,16 @@ def materialize( def main(argv: list[str] | None = None) -> int: logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument("--out-dir", type=Path, required=True, help="output dir (JSONL + tasks/); use the slime-data volume") + parser.add_argument( + "--out-dir", type=Path, required=True, help="output dir (JSONL + tasks/); use the slime-data volume" + ) parser.add_argument("--name", default=DATASET_NAME, help="JSONL filename stem") parser.add_argument("--repo", default=HF_DATASET, help="HuggingFace dataset repo id") parser.add_argument("--split", default="train", help="HuggingFace split") parser.add_argument("--limit", type=int, help="maximum tasks to convert") args = parser.parse_args(argv) - converted, skipped = materialize( - args.out_dir, name=args.name, repo=args.repo, split=args.split, limit=args.limit - ) + converted, skipped = materialize(args.out_dir, name=args.name, repo=args.repo, split=args.split, limit=args.limit) out_dir = args.out_dir.resolve() jsonl = out_dir / f"{args.name}.jsonl" print(f"converted {converted} tasks ({skipped} skipped) -> {jsonl}") diff --git a/async_rl_research/environment/harbor.py b/async_rl_research/environment/harbor.py index 7b2eefb65e..f78cbf7f4d 100644 --- a/async_rl_research/environment/harbor.py +++ b/async_rl_research/environment/harbor.py @@ -80,7 +80,9 @@ def _meets_min_reward(rewards: dict[str, Any] | None, min_reward: float | dict[s if min_reward is None: return True if isinstance(min_reward, dict): - return all(rewards is not None and key in rewards and float(rewards[key]) >= float(v) for key, v in min_reward.items()) + return all( + rewards is not None and key in rewards and float(rewards[key]) >= float(v) for key, v in min_reward.items() + ) return rewards is not None and "reward" in rewards and float(rewards["reward"]) >= float(min_reward) @@ -129,7 +131,9 @@ def normalize_metadata(self, sample) -> dict[str, Any]: "agent_config": m.get("agent_config"), } - def effective_budgets(self, md: dict[str, Any], *, agent_time_budget_sec: int, eval_timeout_sec: int) -> dict[str, int]: + def effective_budgets( + self, md: dict[str, Any], *, agent_time_budget_sec: int, eval_timeout_sec: int + ) -> dict[str, int]: return { "boot_sec": _effective(ModalSandbox._boot_timeout_from_env(), md.get("build_timeout_sec")), "agent_sec": _effective(agent_time_budget_sec, md.get("agent_timeout_sec")), @@ -245,7 +249,9 @@ async def _episode( for step in steps: remaining = int(deadline - time.monotonic()) if remaining <= 0: - logger.warning("[harbor] %s: agent budget exhausted before step %r", md["instance_id"], step["name"]) + logger.warning( + "[harbor] %s: agent budget exhausted before step %r", md["instance_id"], step["name"] + ) break budget = remaining if TASK_TIMEOUT_OVERRIDE and step.get("agent_timeout_sec"): @@ -267,7 +273,11 @@ async def _episode( ) step_results.append({"name": step["name"], "rewards": rewards, "reward": _scalar_reward(rewards)}) if not _meets_min_reward(rewards, step.get("min_reward")): - logger.info("[harbor] %s: step %r below min_reward; aborting remaining steps", md["instance_id"], step["name"]) + logger.info( + "[harbor] %s: step %r below min_reward; aborting remaining steps", + md["instance_id"], + step["name"], + ) break reward = self._aggregate(steps, step_results, md["reward_strategy"]) @@ -376,7 +386,9 @@ async def _verify( return None # Oracle check (reference solution through the exact rollout path) - async def oracle_episode(self, md: dict[str, Any], *, solve_timeout_sec: int, eval_timeout_sec: int) -> RewardResult: + async def oracle_episode( + self, md: dict[str, Any], *, solve_timeout_sec: int, eval_timeout_sec: int + ) -> RewardResult: """Replace the agent leg with the task's solution/solve.sh (a counter maps each sequential leg to its step).""" task_dir = Path(md["task_dir"]) @@ -423,7 +435,9 @@ def _oracle_main() -> int: import asyncio from types import SimpleNamespace - parser = argparse.ArgumentParser(description="Run harbor reference solutions through the rollout path (reward should be 1.0).") + parser = argparse.ArgumentParser( + description="Run harbor reference solutions through the rollout path (reward should be 1.0)." + ) parser.add_argument("jsonl", help="converted slime prompt JSONL (env/convert2slime/harbor.py output)") parser.add_argument("--task-root", help=f"task root (default: ${TASK_ROOT_ENV} or the JSONL's directory)") parser.add_argument("--limit", type=int, default=1, help="how many rows to check (default 1)") @@ -455,9 +469,13 @@ def _oracle_main() -> int: sample = SimpleNamespace(metadata=row.get("metadata"), prompt=row.get("prompt"), label=row.get("label")) md = env.normalize_metadata(sample) t0 = time.monotonic() - result = asyncio.run(env.oracle_episode(md, solve_timeout_sec=args.solve_timeout, eval_timeout_sec=args.eval_timeout)) + result = asyncio.run( + env.oracle_episode(md, solve_timeout_sec=args.solve_timeout, eval_timeout_sec=args.eval_timeout) + ) status = "OK " if result.is_solved else "FAIL" - print(f"[{status}] {md['instance_id']}: reward={result.reward:.2f} t={time.monotonic() - t0:.0f}s {result.extra}") + print( + f"[{status}] {md['instance_id']}: reward={result.reward:.2f} t={time.monotonic() - t0:.0f}s {result.extra}" + ) failures += 0 if result.is_solved else 1 return 1 if failures else 0 diff --git a/async_rl_research/environment/swe_gym.py b/async_rl_research/environment/swe_gym.py index 4ee577eb56..9816350b04 100644 --- a/async_rl_research/environment/swe_gym.py +++ b/async_rl_research/environment/swe_gym.py @@ -97,9 +97,7 @@ async def rollout( # Work sandbox is closed; grade the diff in a clean one. with timer.phase("eval"): - reward, is_solved, applied = await self._evaluate( - md, diff_text, timeout_sec=eval_timeout_sec, timer=timer - ) + reward, is_solved, applied = await self._evaluate(md, diff_text, timeout_sec=eval_timeout_sec, timer=timer) return RewardResult( reward=float(reward), is_solved=bool(is_solved), diff --git a/async_rl_research/generate.py b/async_rl_research/generate.py index b47d3301ea..a986b15158 100644 --- a/async_rl_research/generate.py +++ b/async_rl_research/generate.py @@ -42,11 +42,11 @@ from slime.utils.processing_utils import load_tokenizer from slime.utils.types import Sample -from .profiles import profiling from .agent.base import AgentRuntime, load_runtime from .aiohttp_threaded import run_app_in_thread from .environment.base import EnvMetadataError, RewardResult, load_env from .modal_sandbox import SandboxBootTimeout +from .profiles import profiling logger = logging.getLogger(__name__) diff --git a/async_rl_research/modal_sandbox.py b/async_rl_research/modal_sandbox.py index 7daebfe5fe..44190a0b36 100644 --- a/async_rl_research/modal_sandbox.py +++ b/async_rl_research/modal_sandbox.py @@ -73,6 +73,7 @@ class DockerfileImage: def description(self) -> str: return f"dockerfile:{self.path}" + # Modal validates exec argv against ARG_MAX (64 KiB) client-side; larger # commands are staged as a script file. Under the limit to leave room for the # bash/runuser wrapper argv. @@ -190,11 +191,15 @@ def __init__( # VM memory is static; Modal's 128MB default OOMs a VM. self.memory_mb = _getenv_int("MODAL_VM_MEMORY_FLOOR_MB", default=2048) self.registry_secret = registry_secret or (_getenv("MODAL_REGISTRY_SECRET") or None) - self.rpc_retries = rpc_retries if rpc_retries is not None else _getenv_int( - "MODAL_RPC_RETRIES", "SLIME_AGENT_SANDBOX_RPC_RETRIES", default=self.default_rpc_retries + self.rpc_retries = ( + rpc_retries + if rpc_retries is not None + else _getenv_int("MODAL_RPC_RETRIES", "SLIME_AGENT_SANDBOX_RPC_RETRIES", default=self.default_rpc_retries) ) - self.create_retries = create_retries if create_retries is not None else _getenv_int( - "MODAL_BOOT_RETRIES", default=self.default_create_retries + self.create_retries = ( + create_retries + if create_retries is not None + else _getenv_int("MODAL_BOOT_RETRIES", default=self.default_create_retries) ) self.app_name = app_name or _getenv( "SLIME_AGENT_SANDBOX_MODAL_APP", "MODAL_SANDBOX_APP_NAME", default=self.default_app_name @@ -317,7 +322,7 @@ def _build_image(self): return modal.Image.from_aws_ecr(self.image, secret=secret, **kwargs) return modal.Image.from_registry(self.image, secret=secret, **kwargs) - async def __aenter__(self) -> "ModalSandbox": + async def __aenter__(self) -> ModalSandbox: import modal # lazy self._modal = modal From f8e5e9a87c42c6a2855b7c690dc8384e4c143627 Mon Sep 17 00:00:00 2001 From: junlin-star Date: Mon, 15 Jun 2026 19:54:08 -0700 Subject: [PATCH 10/11] Fix isort import grouping in cispo loss test Co-authored-by: Cursor --- tests/test_policy_loss_modes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_policy_loss_modes.py b/tests/test_policy_loss_modes.py index 367e5de6c5..60c57a589d 100644 --- a/tests/test_policy_loss_modes.py +++ b/tests/test_policy_loss_modes.py @@ -5,7 +5,8 @@ from slime.utils.misc import load_function from slime.utils.ppo_utils import compute_policy_loss -from slime_plugins.losses.cispo import cispo_policy_loss_function, compute_policy_loss as compute_cispo_policy_loss +from slime_plugins.losses.cispo import cispo_policy_loss_function +from slime_plugins.losses.cispo import compute_policy_loss as compute_cispo_policy_loss @pytest.mark.unit From 9bd66615a631871da236b459e904b447e77cdb12 Mon Sep 17 00:00:00 2001 From: junlin-star Date: Tue, 16 Jun 2026 17:23:55 -0700 Subject: [PATCH 11/11] Improve agentic RL runtime diagnostics Co-authored-by: Cursor --- .claude/skills/profile-run/SKILL.md | 133 ++++++++++ .gitignore | 1 - async_rl_research/agent/mini_swe_agent.py | 75 +++++- async_rl_research/environment/harbor.py | 39 +-- async_rl_research/modal_sandbox.py | 2 +- .../scripts_agenticRL/.gitignore | 6 + .../scripts_agenticRL/qwen3_6_swe_eval.sh | 9 + .../scripts_agenticRL/qwen3_dapo_og.sh | 15 ++ slime/backends/megatron_utils/actor.py | 20 ++ slime/ray/actor_group.py | 4 + slime/utils/logging_utils.py | 24 +- slime/utils/wandb_utils.py | 250 +++++++++++++----- tests/test_adapter_session_empty_diag.py | 168 ++++++++++++ tests/test_qwen_adapter_splice.py | 248 +++++++++++++++++ train.py | 29 +- uv.lock | 3 + 16 files changed, 926 insertions(+), 100 deletions(-) create mode 100644 .claude/skills/profile-run/SKILL.md create mode 100644 async_rl_research/scripts_agenticRL/.gitignore create mode 100755 async_rl_research/scripts_agenticRL/qwen3_6_swe_eval.sh create mode 100644 async_rl_research/scripts_agenticRL/qwen3_dapo_og.sh create mode 100644 tests/test_adapter_session_empty_diag.py create mode 100644 tests/test_qwen_adapter_splice.py create mode 100644 uv.lock diff --git a/.claude/skills/profile-run/SKILL.md b/.claude/skills/profile-run/SKILL.md new file mode 100644 index 0000000000..44eb1c3d8c --- /dev/null +++ b/.claude/skills/profile-run/SKILL.md @@ -0,0 +1,133 @@ +--- +name: profile-run +description: Profile/debug a Modal training or eval run for this async-RL project — access Modal app logs, fetch and analyze rollout_*.pt dumps from the slime-checkpoints volume, read the rollout dashboard, and find the W&B run. Use whenever the user reports a bug, shares a log/dashboard link, or asks why a run behaved a certain way. +--- + +# Profile a training/eval run + +How to ground any bug report for this project in **actual observed data** instead of speculation. + +## Grounding rule (most important) + +When the user reports a bug, shares a log line, or asks "why did X happen": +1. **Pull the real artifact first** — Modal logs, the `rollout_*.pt` dump, and/or W&B — before forming a root-cause claim. +2. **Quote what you actually observe** (counts, token spans, decoded text), not what the code "should" do. This project has burned multiple wrong hypotheses (reasoning-content loss, openai-SDK dropping fields) that the data disproved. +3. If a hypothesis contradicts the data, **say so and revise** — don't defend it. +4. If the needed artifact isn't accessible (e.g. historical logs aged out, W&B entity unknown), **ask the user a specific question** ("what's the app id / W&B entity?") rather than guessing. +5. Verify fixes against the real data path (decode with the real tokenizer / run the actual merge logic), not a synthetic stub. + +## Where to look first (efficiency) + +Cheapest → most expensive. Don't download a 70–120 MB dump to answer a question W&B already shows. +1. **W&B** (no download): trends over steps — reward/solve (`rollout/raw_reward`), collapse, grad_norm, KL, step_time, cache-hit, eval. Start here for "is it learning / did it collapse / how's throughput." +2. **Modal logs** (stream): live failures and per-sample summaries on the *current* step (tail-only, can't see old steps). +3. **Rollout dump** (download once, reuse): per-sample / token-level forensics — loss-mask spans, turn structure, drift/reset detection, decoded conversations. Only when you need what W&B can't show. + +Reuse a single analysis venv across the session (don't reinstall torch/transformers each time); download each dump once. + +## Environment facts + +- `modal` is **not** on PATH — use **`uvx modal …`** with args **unquoted** (`uvx modal app list --env junlin-dev`, not a single quoted string). Almost everything needs **`--env junlin-dev`**. So-called "modal failures" are nearly always one of: missing `uvx`, missing `--env` (→ empty output or "No such file"), the `app logs` stream auto-disconnecting (~15 min, expected), or arg-quoting — not flaky auth. When correctly invoked it's reliable (verified). First `uvx` call may be slow (downloads modal once, then cached). +- Runs launch from `multinode-training-guide/` via `EXPERIMENT_CONFIG= uv run --no-dev modal run -d slime/modal_train.py::train`. Configs live in `multinode-training-guide/slime/configs/`. +- Run tag / W&B group / dump subdir all equal `_RUN_TAG` (default e.g. `qwen3.6-35b-a3b-swe-gym-lite-colocate-1n`). Strip ANSI from CLI output with `sed -E 's/\x1b\[[0-9;]*m//g'`. + +## 1. Find the run's app + +```bash +cd /Users/junlin/Documents/Research/async-rl/multinode-training-guide +uvx modal app list --env junlin-dev 2>&1 | sed -E 's/\x1b\[[0-9;]*m//g' | grep -iE "w_qwen|ephemeral|running" +``` +The training run is the `ephemeral` app named after the config (e.g. `w_qwen3_6_…`). Note its `ap-…` id. + +## 2. Modal logs + +```bash +uvx modal app logs ap-XXXX --env junlin-dev 2>err | sed -E 's/\x1b\[[0-9;]*m//g' > /tmp/logs.txt +``` +Caveats (learned the hard way): +- It **streams from ~now**, tail-only — you **cannot scroll back** to an old step's startup. For step-0 history you usually need the dump instead. +- The stream **auto-disconnects ~every 15 min**. For a durable watch, use a Monitor with a self-reconnecting `while true; do uvx modal app logs …; sleep 3; done` loop and a tight `grep` filter (e.g. `adapter_session_empty|aborted:|wall_clock_timeout|\[mini-swe\].*tail:|Traceback`). Don't filter to only the happy path — include failure signatures or a crash looks identical to silence. + +Useful greps: `[async_rl] … reward=` (per-sample summaries), `[mini-swe] … exit=N … tail:` (in-sandbox agent failures, nonzero exit only), `[harbor]` (env/grading), `[trajectory] merge prompt base changed` (trajectory drift), `agent budget exhausted before step` (boot/budget). + +## 3. Rollout dumps — the main triage tool + +Dumps are on the `slime-checkpoints` volume, written per step (config `save_debug_rollout_data`). Train = `rollout_.pt`, eval = `rollout_eval_.pt`. Same `_RUN_TAG` relaunch **overwrites** them. + +```bash +uvx modal volume ls slime-checkpoints /swe_rollout_dumps/ --env junlin-dev # list + mtimes +uvx modal volume get slime-checkpoints /swe_rollout_dumps//rollout_0.pt /tmp/r0.pt --env junlin-dev --force +``` +Load (plain dicts — no slime import needed): +```python +import torch +dump = torch.load("/tmp/r0.pt", map_location="cpu", weights_only=False) # {"rollout_id", "samples":[...]} +s = dump["samples"][0] # dict: tokens, loss_mask, response_length, response (decoded str), + # prompt, rollout_log_probs, metadata{instance_id,is_solved,abort_reason,...}, + # status, reward, weight_versions, trace +``` +Key invariants: `tokens = prompt_ids + response_ids`; `loss_mask`/`rollout_log_probs` align with the **response** portion (last `response_length` tokens). `mask=1` = trained (assistant output), `mask=0` = context (tool results / re-rendered history). + +### Analysis recipes (verified) +```python +# trained fraction & turn count +trained = sum(s["loss_mask"]); frac = trained / s["response_length"] +turns = s["response"].count("<|im_start|>assistant") + 1 # +1 for the head turn + +# trajectory-merge RESET detector: base task prompt is ~1.0-1.5k tokens; a much larger +# prompt means early turns were dropped into the UNTRAINED prompt (line-107 reset). +prompt_len = len(s["tokens"]) - s["response_length"] +is_reset = prompt_len > 4000 + +# GRPO signal check: groups with zero reward variance give no advantage +# decode token spans / mask runs with the real tokenizer (see venv below) +``` + +### Throwaway analysis venv (tokenizer/torch without polluting anything) +```bash +uv venv --python 3.11 /tmp/dbg && \ +uv pip install --python /tmp/dbg/bin/python -q transformers jinja2 wandb && \ +uv pip install --python /tmp/dbg/bin/python -q torch --index-url https://download.pytorch.org/whl/cpu +# tokenizer-only (Qwen3.6 is public; don't download weights): +/tmp/dbg/bin/python -c "from huggingface_hub import snapshot_download as d; from transformers import AutoTokenizer; \ +AutoTokenizer.from_pretrained(d('Qwen/Qwen3.6-35B-A3B', allow_patterns=['tokenizer*','*.json']))" +``` +`apply_chat_template(..., tokenize=False)` then `tok.encode(...)` to get clean id lists (tokenize=True returns an Encoding in transformers 5.x). The chat template + `reasoning_parser=qwen3` / `tool_call_parser=qwen3_coder` are the source of multi-turn render quirks — see [[trajectory-drift-formaterror]]. + +## 4. Rollout dashboard (web) + +`async_rl_research/dashboard/` serves the same volume. URL pattern: +`https://modal-labs-junlin-dev--swe-rollout-dashboard-dashboard.modal.run/#/.pt/` +`convert.py` reconstructs turns by splitting the decoded `response` on `<|im_start|>` and parsing ``/``/``. The first (head) turn renders without an opening `` because the prompt prefilled it — that's expected, not a bug. + +## 5. W&B (verified — use this FIRST for trends; no big download) + +- **Entity `junlinwang`, project `Modal`** (= `WANDB_PROJECT`), run name = group = `_RUN_TAG` (suffix disabled, so one run per relaunch; relaunches make a *new* run with the same name). +- Auth: `wandb` reads `~/.netrc` automatically — no key needed in code (it's already logged in). On a fresh machine: `wandb login` or set `WANDB_API_KEY`. `wandb` isn't installed by default; `uv pip install wandb` into the analysis venv. + +```python +import wandb +api = wandb.Api() # loads creds from ~/.netrc +print(api.default_entity) # -> junlinwang +runs = list(api.runs("junlinwang/Modal", filters={"group": ""})) +runs.sort(key=lambda r: r.created_at) # latest = newest relaunch +run = runs[-1] +print(run.id, run.state) # e.g. crashed/running/finished +val = run.summary.get("rollout/raw_reward") # last-logged scalar +h = run.history(keys=["rollout/step","rollout/raw_reward","train/grad_norm","rollout/kl"], + samples=5000, pandas=True) # time series (use history, NOT scan_history) +``` + +**Metric semantics that bite (verified):** +- **`rollout/raw_reward` = the actual mean reward / solve signal** (step 0 ≈ 0.48 matched 122/252 solved in the dump). **`rollout/rewards` is the GRPO-centered advantage ≈ 0 by construction — do NOT use it to judge solve rate.** +- `train/grad_norm`, `train/kl_loss`, `train/ppo_kl`, `perf/step_time`, `sgl_engine/sglang_cache_hit_rate`, `eval//...`. ~193 keys; engine gauges are per-DP-rank means (×8) — see [[swe-rl-perf-profile]]. +- `run.history(...)` returns a sampled DataFrame; `_step` is the W&B logging step, use the `rollout/step`/`train/step` columns for the real step. `scan_history(keys=...)` gave all-None here — prefer `history(keys=..., samples=N, pandas=True)`. + +W&B alone shows trends without any download: e.g. `rollout/raw_reward` 0.48 → ~0.0 after step 0 is the **policy collapse**, visible instantly. + +## Quick interpretation map + +- `adapter_session_empty` (0 turns) → in-sandbox agent never completed a turn; check `[mini-swe] … tail:` and `agent budget exhausted before step` (see [[adapter-session-empty-budget-bug]]). +- `exit=-2` → `EXIT_BUDGET_EXCEEDED` (agent ran full `AGENT_TIME_BUDGET_SEC`). +- Large prompt / dropped turns / `merge prompt base changed` → trajectory drift ([[trajectory-drift-formaterror]]). +- `mean turns/sample` collapsing across steps (e.g. 34→1 after one update) → policy collapse; suspect rollout-logprob/EAGLE mismatch, recompute train-side logprobs vs `rollout_log_probs`. diff --git a/.gitignore b/.gitignore index 91d9db40fd..e9a268d45d 100644 --- a/.gitignore +++ b/.gitignore @@ -193,4 +193,3 @@ glm/ _examples_synced/ .env .DS_Store -scripts_agenticRL/ diff --git a/async_rl_research/agent/mini_swe_agent.py b/async_rl_research/agent/mini_swe_agent.py index c1828308c3..5d1e7054db 100644 --- a/async_rl_research/agent/mini_swe_agent.py +++ b/async_rl_research/agent/mini_swe_agent.py @@ -24,6 +24,10 @@ from .base import AgentRunResult, AgentRuntime MSWE_STEP_LIMIT = int(os.environ.get("MSWE_STEP_LIMIT", "50")) +# Consecutive no-tool-call model turns before the runner ends the episode: a +# stuck model that never reaches the context wall would otherwise format-error +# its way to MSWE_STEP_LIMIT. See _StopAwareModel in MINI_RUNNER_PY. +MSWE_MAX_EMPTY_TURNS = int(os.environ.get("MSWE_MAX_EMPTY_TURNS", "3")) # Which YAML config (prompts) the runner loads. Override ladder: MSWE_CONFIG # env (global) > metadata.agent_config (per-row) > universal config below. MSWE_CONFIG = os.environ.get("MSWE_CONFIG", "") @@ -58,6 +62,7 @@ WORKDIR = os.environ["MSWE_WORKDIR"] MODEL = os.environ.get("MSWE_MODEL", "slime-actor") STEP_LIMIT = int(os.environ.get("MSWE_STEP_LIMIT", "50")) +MAX_EMPTY_TURNS = int(os.environ.get("MSWE_MAX_EMPTY_TURNS", "3")) PATH_PREPEND = os.environ.get("MSWE_PATH_PREPEND", "") with open(os.environ["MSWE_PROBLEM_FILE"], encoding="utf-8") as fh: TASK = fh.read() @@ -68,6 +73,42 @@ from minisweagent.config import builtin_config_dir from minisweagent.environments.local import LocalEnvironment from minisweagent.models.litellm_model import LitellmModel + from minisweagent.exceptions import FormatError, LimitsExceeded + + class _StopAwareModel(LitellmModel): + """End the episode instead of looping when the model can't progress. + + A no-tool-call response surfaces as FormatError from super().query() + (LitellmModel stashes the raw response, incl. finish_reason, on it). We + stop on finish_reason='length' (adapter signalled context/output budget + exhausted) or after MAX_EMPTY_TURNS consecutive no-tool-call turns, + raising LimitsExceeded -- mini-swe's own graceful 'exit' path, the same + one step_limit uses. Without this mini-swe retries the format error every + turn and burns the whole context to step_limit (49 dead turns seen on + eval); finish_reason is otherwise never inspected. + """ + + _empty = 0 + + def query(self, messages, **kwargs): + try: + msg = super().query(messages, **kwargs) + except FormatError as e: + resp = (e.messages[0].get("extra") or {}).get("response") or {} + fr = ((resp.get("choices") or [{}])[0] or {}).get("finish_reason") + self._empty += 1 + if fr == "length" or self._empty >= MAX_EMPTY_TURNS: + status = "ContextLengthExceeded" if fr == "length" else "NoProgress" + raise LimitsExceeded( + { + "role": "exit", + "content": f"ending session: finish_reason={fr}, no-tool-call streak={self._empty}", + "extra": {"exit_status": status, "submission": ""}, + } + ) + raise + self._empty = 0 + return msg # Default to the uploaded universal config; MSWE_CONFIG (if set) names a # BUILTIN packaged config. Read the builtin path directly -- the spec helper @@ -106,7 +147,7 @@ if prepend: env_overrides["PATH"] = ":".join(prepend) + ":" + os.environ.get("PATH", "") - model = LitellmModel(**model_cfg) + model = _StopAwareModel(**model_cfg) env = LocalEnvironment(cwd=WORKDIR, env=env_overrides, timeout=int(env_cfg.get("timeout") or 60)) agent = DefaultAgent(model, env, **agent_cfg) info = agent.run(TASK) @@ -152,24 +193,33 @@ f"rm -rf {shlex.quote(MSWE_AGENT_VENV)}\n" f'retry "uv venv --python {MSWE_AGENT_PYTHON_VERSION} {shlex.quote(MSWE_AGENT_VENV)}"\n' f'retry "uv pip install --python {shlex.quote(_VENV_PY)} {shlex.quote(MSWE_PIP_SPEC)}"\n' - # pydantic_core's compiled native module intermittently fails to land in the - # venv (partial wheel from a corrupt uv cache / index contention at high - # conc) -> the agent dies at `import minisweagent.agents.default` with zero - # turns (adapter_session_empty; ~46% of gravitational/teleport on one eval). - # Verify the agent's REAL import and force a clean reinstall (--reinstall - # --no-cache) to repair the partial wheel; a still-broken venv then fails - # LOUDLY here (set -e -> CalledProcessError) instead of as a silent empty. - f"for i in 1 2 3; do MSWEA_SILENT_STARTUP=1 {shlex.quote(_VENV_PY)} -c 'import minisweagent.agents.default' 2>/dev/null && break;" + # Verify the agent's REAL import (the top package doesn't pull in pydantic). + # Two distinct failure modes this guards against: + # * `-P` (MANDATORY): the exec runs with cwd = the image WORKDIR (e.g. + # /testbed), so a task repo whose root *is* an agent dependency shadows + # the venv. The pydantic SWE-gym tasks ship /testbed/pydantic/, which the + # bare `python -c` imports instead of the venv's pydantic -> repo-pydantic + # vs venv-pydantic_core skew crashes ("no attribute 'dict_not_none'"). + # -P drops cwd from sys.path -- the SAME guard the runner launch uses + # (see run_agent) -- so the venv wins. Deterministic: was hitting 100% of + # pydantic tasks as a loud `exception:RuntimeError` from _ensure_provisioned. + # * reinstall: a partial wheel (corrupt uv cache / index contention at high + # conc) can leave a native ext unimportable; --reinstall --no-cache + # repairs it. A still-broken venv then fails LOUDLY here (set -e) instead + # of as a silent zero-turn adapter_session_empty. + f"for i in 1 2 3; do MSWEA_SILENT_STARTUP=1 {shlex.quote(_VENV_PY)} -P -c 'import minisweagent.agents.default' 2>/dev/null && break;" f' retry "uv pip install --python {shlex.quote(_VENV_PY)} --reinstall --no-cache {shlex.quote(MSWE_PIP_SPEC)}" || true; done\n' - f"MSWEA_SILENT_STARTUP=1 {shlex.quote(_VENV_PY)} -c 'import minisweagent.agents.default'\n" + f"MSWEA_SILENT_STARTUP=1 {shlex.quote(_VENV_PY)} -P -c 'import minisweagent.agents.default'\n" ) # MSWEA_SILENT_STARTUP suppresses the import-time banner that would otherwise # corrupt the provisioning probe's marker comparison. Import the agent's real # entrypoint (not just the top package) so the probe also rejects a pre-baked # venv whose pydantic_core native module is missing -> re-provision instead of -# launching a doomed agent. -_VENV_CHECK = f"MSWEA_SILENT_STARTUP=1 {shlex.quote(_VENV_PY)} -c 'import minisweagent.agents.default'" +# launching a doomed agent. `-P` keeps the image WORKDIR (e.g. /testbed) off +# sys.path so a repo named like an agent dep (pydantic tasks) can't shadow the +# venv and fail the probe; see _VENV_SETUP and the runner launch. +_VENV_CHECK = f"MSWEA_SILENT_STARTUP=1 {shlex.quote(_VENV_PY)} -P -c 'import minisweagent.agents.default'" class MiniSweAgentRuntime(AgentRuntime): @@ -214,6 +264,7 @@ async def run_agent( "MSWE_CONFIG": MSWE_CONFIG or md.get("agent_config") or "", "MSWE_CONFIG_FILE": f"{workdir}/{_CONFIG_FILE}", "MSWE_STEP_LIMIT": str(MSWE_STEP_LIMIT), + "MSWE_MAX_EMPTY_TURNS": str(MSWE_MAX_EMPTY_TURNS), "MSWE_PATH_PREPEND": MSWE_PATH_PREPEND, "MSWEA_SILENT_STARTUP": "1", } diff --git a/async_rl_research/environment/harbor.py b/async_rl_research/environment/harbor.py index f78cbf7f4d..5092d98ea1 100644 --- a/async_rl_research/environment/harbor.py +++ b/async_rl_research/environment/harbor.py @@ -27,6 +27,7 @@ from typing import Any from ..modal_sandbox import DockerfileImage, ModalSandbox +from ..profiles.profiling import PhaseTimer from .base import EnvMetadataError, RewardResult, RolloutEnv, coerce_prompt logger = logging.getLogger(__name__) @@ -228,16 +229,20 @@ async def _episode( # leg, which runs solve.sh rather than the agent). Surfaced in extra so # a zero-turn adapter_session_empty self-explains in the rollout dump. last_agent = None + timer = PhaseTimer() + t0 = time.monotonic() async with self._sandbox(md) as sb: + timer.record("work_boot", time.monotonic() - t0) workdir = md["workdir"] or await self._detect_workdir(sb) q = shlex.quote # Test scripts assume /logs/{agent,verifier,artifacts} exist. - await sb.exec( - f"mkdir -p {q(workdir)} /logs/agent /logs/verifier /logs/artifacts", - check=True, - timeout=60, - ) + with timer.phase("prep"): + await sb.exec( + f"mkdir -p {q(workdir)} /logs/agent /logs/verifier /logs/artifacts", + check=True, + timeout=60, + ) # Start the agent clock only once the sandbox is booted and prepped: # a cold per-instance image pull can take many minutes, and charging @@ -257,20 +262,23 @@ async def _episode( if TASK_TIMEOUT_OVERRIDE and step.get("agent_timeout_sec"): budget = min(budget, int(step["agent_timeout_sec"])) - await self.write_problem_file(sb, workdir, step["instruction"]) + with timer.phase("prep"): + await self.write_problem_file(sb, workdir, step["instruction"]) leg_md = {**md, "workdir": workdir} - leg_result = await run_leg(sb, leg_md, budget) + with timer.phase("agent"): + leg_result = await run_leg(sb, leg_md, budget) if leg_result is not None: last_agent = leg_result - rewards = await self._verify( - sb, - tests_dir=task_dir / step["tests_path"], - workdir=workdir, - verifier={**md["verifier"], **(step.get("verifier") or {})}, - eval_timeout_sec=eval_timeout_sec, - instance_id=md["instance_id"], - ) + with timer.phase("verifier"): + rewards = await self._verify( + sb, + tests_dir=task_dir / step["tests_path"], + workdir=workdir, + verifier={**md["verifier"], **(step.get("verifier") or {})}, + eval_timeout_sec=eval_timeout_sec, + instance_id=md["instance_id"], + ) step_results.append({"name": step["name"], "rewards": rewards, "reward": _scalar_reward(rewards)}) if not _meets_min_reward(rewards, step.get("min_reward")): logger.info( @@ -285,6 +293,7 @@ async def _episode( "harbor_step_results": step_results, "harbor_steps_completed": len(step_results), "harbor_steps_total": len(steps), + "timing": timer.as_dict(), } if last_agent is not None: extra["agent_exit_code"] = last_agent.exit_code diff --git a/async_rl_research/modal_sandbox.py b/async_rl_research/modal_sandbox.py index 44190a0b36..459f60650a 100644 --- a/async_rl_research/modal_sandbox.py +++ b/async_rl_research/modal_sandbox.py @@ -420,7 +420,7 @@ async def _run() -> ExecResult: exit_code, stdout, stderr = await self._retry(f"exec({cmd[:48]!r})", self.rpc_retries, _run) if check and exit_code != 0: - raise RuntimeError(f"modal exec failed (exit={exit_code}): {cmd[:120]}\n{stderr[:400]}") + raise RuntimeError(f"modal exec failed (exit={exit_code}): {cmd[:120]}\n{stderr[-1000:]}") return exit_code, stdout, stderr async def write_file(self, sandbox_path: str, content: FileContent, *, user: str = "root") -> None: diff --git a/async_rl_research/scripts_agenticRL/.gitignore b/async_rl_research/scripts_agenticRL/.gitignore new file mode 100644 index 0000000000..3493937931 --- /dev/null +++ b/async_rl_research/scripts_agenticRL/.gitignore @@ -0,0 +1,6 @@ +# local launch scripts — machine-specific, never tracked +* +!.gitignore +!qwen3_dapo_og.sh +!qwen3_6_swe_eval.sh +!qwen3_6_colocate.sh diff --git a/async_rl_research/scripts_agenticRL/qwen3_6_swe_eval.sh b/async_rl_research/scripts_agenticRL/qwen3_6_swe_eval.sh new file mode 100755 index 0000000000..49fc68ca5c --- /dev/null +++ b/async_rl_research/scripts_agenticRL/qwen3_6_swe_eval.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail +cd "${GUIDE:-$HOME/Documents/Research/async-rl/multinode-training-guide}" +export EXPERIMENT_CONFIG=w_qwen3_6_swe_eval_2n +export MODAL_ENVIRONMENT=${MODAL_ENVIRONMENT:-junlin-dev} WANDB_PROJECT=${WANDB_PROJECT:-Modal} + +# uv run --no-dev modal run slime/modal_train.py::download_data +uv run --no-dev modal run -d slime/modal_train.py::train diff --git a/async_rl_research/scripts_agenticRL/qwen3_dapo_og.sh b/async_rl_research/scripts_agenticRL/qwen3_dapo_og.sh new file mode 100644 index 0000000000..94aa4a9eb7 --- /dev/null +++ b/async_rl_research/scripts_agenticRL/qwen3_dapo_og.sh @@ -0,0 +1,15 @@ +export EXPERIMENT_CONFIG=qwen3_dapo +export MODAL_ENVIRONMENT=junlin-dev +export WANDB_PROJECT=Modal +export WANDB_GROUP=qwen3-30b-a3b-dapo-math-1n + + + +cd /Users/junlin/Documents/Research/async-rl/multinode-training-guide + + + +# uv run --no-dev modal run slime/modal_train.py::download_model +# uv run --no-dev modal run slime/modal_train.py::download_data +# uv run --no-dev modal run slime/modal_train.py::convert_hf_to_megatron_checkpoint +uv run --no-dev modal run -d slime/modal_train.py::train diff --git a/slime/backends/megatron_utils/actor.py b/slime/backends/megatron_utils/actor.py index 74680e2ada..e378c27528 100644 --- a/slime/backends/megatron_utils/actor.py +++ b/slime/backends/megatron_utils/actor.py @@ -591,6 +591,26 @@ def save_model(self, rollout_id: int, force_sync: bool = False) -> None: if self.args.offload_train: self.sleep() + def save_hf(self, rollout_id: int = 0) -> None: + """DEBUG: dump HF via the real resync converter only (no megatron ckpt). + + Used by the qwen3.6 resync probe to test the live TP/EP gather+convert in + isolation. Resolves hf_checkpoint to a local dir (raw save_hf requires it). + """ + import os + + if self.args.offload_train: + self.wake_up() + if not os.path.isdir(self.args.hf_checkpoint): + from huggingface_hub import snapshot_download + + self.args.hf_checkpoint = snapshot_download(self.args.hf_checkpoint, local_files_only=True) + from slime.backends.megatron_utils.model import save_hf_model + + save_hf_model(self.args, rollout_id, self.model) + if self.args.offload_train: + self.sleep() + @timer def update_weights(self) -> None: if self.args.debug_train_only or self.args.debug_rollout_only: diff --git a/slime/ray/actor_group.py b/slime/ray/actor_group.py index 27ad610ad9..4186581b10 100644 --- a/slime/ray/actor_group.py +++ b/slime/ray/actor_group.py @@ -141,6 +141,10 @@ def save_model(self, rollout_id, force_sync=False): """Save actor model""" return ray.get([actor.save_model.remote(rollout_id, force_sync=force_sync) for actor in self._actor_handlers]) + def save_hf(self, rollout_id=0): + """DEBUG (qwen3.6 resync probe): dump HF via the real resync converter.""" + return ray.get([actor.save_hf.remote(rollout_id) for actor in self._actor_handlers]) + def update_weights(self): """Broadcast weights from rank 0 to all other ranks.""" return ray.get([actor.update_weights.remote() for actor in self._actor_handlers]) diff --git a/slime/utils/logging_utils.py b/slime/utils/logging_utils.py index 11348a4074..3eff1514b4 100644 --- a/slime/utils/logging_utils.py +++ b/slime/utils/logging_utils.py @@ -38,6 +38,14 @@ def update_tracking_open_metrics(args, router_addr): def finish_tracking(args): if not args.use_wandb: return + try: + logger_actor = wandb_utils.get_logger_actor() + if logger_actor is not None: + import ray + + ray.get(logger_actor.finish.remote(), timeout=120) + except Exception: + logging.getLogger(__name__).exception("Failed to finish wandb logger actor") try: if wandb.run is not None: wandb.finish() @@ -48,7 +56,21 @@ def finish_tracking(args): # TODO further refactor, e.g. put TensorBoard init to the "init" part def log(args, metrics, step_key: str): if args.use_wandb: - wandb.log(metrics) + # All history must go through the single primary writer; metrics + # logged from shared-mode secondary processes are ingested hours + # late (or dropped) by the W&B backend. See wandb_utils. The call is + # synchronous (it is cheap and infrequent) so that no metric can be + # lost in a shutdown race and actor failures are surfaced here. + logger_actor = wandb_utils.get_logger_actor() + if logger_actor is not None: + try: + import ray + + ray.get(logger_actor.log.remote(metrics), timeout=60) + except Exception: + logging.getLogger(__name__).exception("Failed to log metrics via wandb logger actor") + elif wandb.run is not None: + wandb.log(metrics) if args.use_tensorboard: metrics_except_step = {k: v for k, v in metrics.items() if k != step_key} diff --git a/slime/utils/wandb_utils.py b/slime/utils/wandb_utils.py index 81dbe9f124..4ba52f0878 100644 --- a/slime/utils/wandb_utils.py +++ b/slime/utils/wandb_utils.py @@ -1,11 +1,30 @@ import logging +import math import os +import socket +import threading from copy import deepcopy import wandb logger = logging.getLogger(__name__) +# Name of the Ray actor that owns the W&B run and performs ALL history writes. +# +# Why a single writer: on the current W&B backend, history logged by +# ``mode="shared"`` secondary writers (``x_primary=False``) is not ingested in +# real time — it lands hours late via a backfill path, or is dropped entirely +# when the writer process dies before flushing. The same delayed path swallows +# everything logged after a run is finished and re-initialized with +# ``resume="allow"``. So the run looks completely empty in the UI during (and +# long after) training. Funneling every ``wandb.log`` through the one primary +# writer that created the run keeps metrics on the real-time path. Secondary +# processes still attach in shared mode, but only for console logs and +# per-node system metrics. +LOGGER_ACTOR_NAME = "slime_wandb_logger" + +_logger_actor = None + def _is_offline_mode(args) -> bool: """Detect whether W&B should run in offline mode. @@ -19,28 +38,8 @@ def _is_offline_mode(args) -> bool: return os.environ.get("WANDB_MODE") == "offline" -def init_wandb_primary(args): - if not args.use_wandb: - args.wandb_run_id = None - return - - # Set W&B mode if specified (overrides WANDB_MODE env var) - if args.wandb_mode: - os.environ["WANDB_MODE"] = args.wandb_mode - if args.wandb_mode == "offline": - logger.info("W&B offline mode enabled. Data will be saved locally.") - elif args.wandb_mode == "disabled": - logger.info("W&B disabled mode enabled. No data will be logged.") - elif args.wandb_mode == "online": - logger.info("W&B online mode enabled. Data will be uploaded to cloud.") - - offline = _is_offline_mode(args) - - # Only perform explicit login when NOT offline - if (not offline) and args.wandb_key is not None: - wandb.login(key=args.wandb_key, host=args.wandb_host) - - # Prepare wandb init parameters +def _primary_init_kwargs(args): + """Build the wandb.init kwargs shared by the offline path and the logger actor.""" # add random 6 length string with characters if args.wandb_random_suffix: group = args.wandb_group + "_" + wandb.util.generate_id() @@ -49,7 +48,6 @@ def init_wandb_primary(args): group = args.wandb_group run_name = args.wandb_group - # Prepare wandb init parameters init_kwargs = { "entity": args.wandb_team, "project": args.wandb_project, @@ -58,34 +56,174 @@ def init_wandb_primary(args): "config": _compute_config_for_logging(args), } - # Configure settings based on offline/online mode - if offline: - init_kwargs["settings"] = wandb.Settings(mode="offline") - else: - init_kwargs["settings"] = wandb.Settings(mode="shared", x_primary=True) - - # Add custom directory if specified if args.wandb_dir: # Ensure directory exists to avoid backend crashes os.makedirs(args.wandb_dir, exist_ok=True) init_kwargs["dir"] = args.wandb_dir logger.info(f"W&B logs will be stored in: {args.wandb_dir}") - wandb.init(**init_kwargs) + return init_kwargs - _init_wandb_common() +class WandbLoggerActor: + """Ray actor that owns the W&B run and is its only history writer. + + See the comment on ``LOGGER_ACTOR_NAME`` for why all metrics must flow + through this single process. + """ + + def __init__(self, args): + self.args = args + self._scraper_thread = None + self._stop = threading.Event() + + if args.wandb_mode: + os.environ["WANDB_MODE"] = args.wandb_mode + if args.wandb_key is not None: + wandb.login(key=args.wandb_key, host=args.wandb_host) + + init_kwargs = _primary_init_kwargs(args) + init_kwargs["settings"] = wandb.Settings(mode="shared", x_primary=True, x_label="primary-logger") + wandb.init(**init_kwargs) + _init_wandb_common() + + def get_run_id(self): + return wandb.run.id + + def log(self, metrics: dict): + if wandb.run is not None: + wandb.log(metrics) + + def start_open_metrics(self, router_addr: str): + """Poll the sglang router's metrics endpoint and log it as history. + + Replaces the previous finish + re-init with + ``x_stats_open_metrics_endpoints``: resuming a finished shared-mode + run sends all subsequent metric streams down the W&B backfill path + (hours of delay), which made runs look empty. + """ + if self._scraper_thread is not None: + return + url = f"{router_addr}/engine_metrics" + self._scraper_thread = threading.Thread(target=self._scrape_loop, args=(url,), daemon=True) + self._scraper_thread.start() + logger.info(f"Scraping SGLang engine metrics from {url}.") + + def _scrape_loop(self, url): + import urllib.request + + while not self._stop.wait(30): + try: + with urllib.request.urlopen(url, timeout=10) as resp: + text = resp.read().decode("utf-8", errors="replace") + except Exception: + continue + metrics = _parse_prometheus_text(text) + if metrics and wandb.run is not None: + wandb.log({f"sgl_engine/{name}": value for name, value in metrics.items()}) + + def finish(self): + # Idempotent: called from RolloutManager.dispose() and again from the + # driver's finish_tracking(). + self._stop.set() + if wandb.run is not None: + wandb.finish() + + +def _parse_prometheus_text(text): + """Parse Prometheus text exposition into {metric_name: mean across series}.""" + sums: dict[str, float] = {} + counts: dict[str, int] = {} + for line in text.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if "{" in line: + name = line[: line.index("{")] + rest = line[line.index("}") + 1 :].split() + else: + parts = line.split() + name, rest = parts[0], parts[1:] + if not rest: + continue + try: + value = float(rest[0]) + except ValueError: + continue + if math.isnan(value) or math.isinf(value): + continue + sums[name] = sums.get(name, 0.0) + value + counts[name] = counts.get(name, 0) + 1 + return {name: sums[name] / counts[name] for name in sums} + + +def get_logger_actor(): + """Return the primary logger actor handle, or None (e.g. offline mode).""" + global _logger_actor + if _logger_actor is None: + try: + import ray + + if not ray.is_initialized(): + return None + _logger_actor = ray.get_actor(LOGGER_ACTOR_NAME) + except Exception: + return None + return _logger_actor + + +def init_wandb_primary(args): + if not args.use_wandb: + args.wandb_run_id = None + return + + # Set W&B mode if specified (overrides WANDB_MODE env var) + if args.wandb_mode: + os.environ["WANDB_MODE"] = args.wandb_mode + if args.wandb_mode == "offline": + logger.info("W&B offline mode enabled. Data will be saved locally.") + elif args.wandb_mode == "disabled": + logger.info("W&B disabled mode enabled. No data will be logged.") + elif args.wandb_mode == "online": + logger.info("W&B online mode enabled. Data will be uploaded to cloud.") + + if _is_offline_mode(args) or args.wandb_mode == "disabled": + # Offline/disabled: every process writes locally (or not at all); no + # actor needed. For "disabled", the WANDB_MODE env var set above must + # stay in charge — an explicit Settings(mode=...) would override it. + init_kwargs = _primary_init_kwargs(args) + if _is_offline_mode(args): + init_kwargs["settings"] = wandb.Settings(mode="offline") + wandb.init(**init_kwargs) + _init_wandb_common() + args.wandb_run_id = wandb.run.id + return + + if args.wandb_key is not None: + wandb.login(key=args.wandb_key, host=args.wandb_host) + + import ray + + global _logger_actor + _logger_actor = ray.remote(num_cpus=0)(WandbLoggerActor).options(name=LOGGER_ACTOR_NAME).remote(args) # Set wandb_run_id in args for easy access throughout the training process - args.wandb_run_id = wandb.run.id + args.wandb_run_id = ray.get(_logger_actor.get_run_id.remote()) + + # Attach the driver as a shared-mode secondary so its console output and + # head-node system metrics still reach the run. + init_wandb_secondary(args, role="driver") def reinit_wandb_primary_with_open_metrics(args, router_addr): - """Re-initialize the primary W&B run with open metrics endpoints. + """Start uploading SGLang engine metrics now that the router is up. The primary wandb init happens before rollout servers start (to obtain - ``wandb_run_id`` for secondary processes). This function is called - *after* servers are up so the router address is available for scraping - SGLang Prometheus metrics via the primary process's stats monitor. + ``wandb_run_id`` for secondary processes). This function is called + *after* servers are up so the router address is available. The logger + actor scrapes the router's Prometheus endpoint itself — the previous + finish + re-init with ``x_stats_open_metrics_endpoints`` made the W&B + backend route all subsequent metrics through its hours-delayed backfill + path, so runs looked empty. """ if not args.use_wandb or _is_offline_mode(args): return @@ -93,8 +231,7 @@ def reinit_wandb_primary_with_open_metrics(args, router_addr): return if router_addr is None: return - wandb_run_id = getattr(args, "wandb_run_id", None) - if wandb_run_id is None: + if getattr(args, "wandb_run_id", None) is None: return import sglang_router @@ -105,34 +242,10 @@ def reinit_wandb_primary_with_open_metrics(args, router_addr): ) return - logger.info(f"Re-initializing primary W&B with SGLang metrics at {router_addr}.") - - wandb.finish() - - init_kwargs = { - "id": wandb_run_id, - "entity": args.wandb_team, - "project": args.wandb_project, - "resume": "allow", - "reinit": True, - "settings": wandb.Settings( - mode="shared", - x_primary=True, - x_stats_open_metrics_endpoints={ - "sgl_engine": f"{router_addr}/engine_metrics", - }, - x_stats_open_metrics_filters={ - "sgl_engine.*": {}, - }, - ), - } - - if args.wandb_dir: - os.makedirs(args.wandb_dir, exist_ok=True) - init_kwargs["dir"] = args.wandb_dir - - wandb.init(**init_kwargs) - _init_wandb_common() + actor = get_logger_actor() + if actor is None: + return + actor.start_open_metrics.remote(router_addr) def _compute_config_for_logging(args): @@ -198,6 +311,9 @@ def init_wandb_secondary(args, role=None): mode="shared", x_primary=False, x_update_finish_state=False, + # Distinct labels keep per-process system metrics and console + # logs from clobbering each other on the W&B backend. + x_label=f"{role or 'worker'}-{socket.gethostname()}-{os.getpid()}", ) init_kwargs = { diff --git a/tests/test_adapter_session_empty_diag.py b/tests/test_adapter_session_empty_diag.py new file mode 100644 index 0000000000..36b9b98a6b --- /dev/null +++ b/tests/test_adapter_session_empty_diag.py @@ -0,0 +1,168 @@ +"""Regression tests for persisting the agent exit code + log tail. + +A zero-turn rollout aborts as ``adapter_session_empty`` on the graceful success +path (the agent process launched but made no adapter calls). Previously the dump +could not say *why* turns=0 — ``_detached_run`` only logged the exit code/tail to +tail-only Modal logs that age out. These tests pin the wiring that now carries +``agent_exit_code`` (+ a failure-only ``agent_tail``) into the abort sample's +metadata so the dump self-explains (e.g. exit=137 -> OOM-killed). + +- ``test_detached_run_returns_exit_and_tail`` (unit): fake sandbox, asserts + ``_detached_run`` returns the parsed exit code with the log tail on a nonzero + exit and an empty tail on a clean exit. +- ``test_empty_session_carries_agent_diag`` (unit): ``_merge_samples`` empty + path copies ``agent_exit_code``/``agent_tail`` from ``reward_result.extra`` + onto the aborted sample's metadata, and stays clean when they're absent. +- ``test_abort_result_merges_extra`` (unit): ``_abort_result`` merges an extra + dict without clobbering ``abort_reason``. +""" +import asyncio +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from async_rl_research.agent.base import EXIT_BUDGET_EXCEEDED, AgentRunResult, AgentRuntime +from async_rl_research.environment.base import RewardResult +from async_rl_research.generate import _abort_result, _merge_samples +from slime.utils.types import Sample + +NUM_GPUS = 0 + + +def test_agent_run_result_defaults(): + assert AgentRunResult(0).tail == "" + assert AgentRunResult(137, "boom").exit_code == 137 + assert EXIT_BUDGET_EXCEEDED == -2 + + +# -------------------------------------------------------------------------- +# _detached_run: the new exit-code + failure-tail capture +# -------------------------------------------------------------------------- +class _FakeRuntime(AgentRuntime): + name = "fake" + adapter_cls = object # non-None is all __init_subclass__ requires + + async def run_agent(self, *a, **k): # unused; abstract method must exist + raise NotImplementedError + + +class _FakeSandbox: + """Minimal sandbox: the done-marker poll yields ``exit_code``; the tail + command yields ``tail_text``; everything else (rm/chmod/setsid) is a no-op.""" + + def __init__(self, exit_code, tail_text): + self._exit = exit_code + self._tail = tail_text + + async def write_file(self, path, body): + return None + + async def exec(self, cmd, check=False, timeout=None): + if "cat" in cmd and "_done" in cmd: # poll: `test -f .. && cat ..` + return (0, str(self._exit), "") + if "tail -c 4000" in cmd: + return (0, self._tail, "") + return (0, "", "") # rm / chmod / setsid launch + + +def _run_detached(exit_code, tail_text): + rt = _FakeRuntime() + sb = _FakeSandbox(exit_code, tail_text) + return asyncio.run( + rt._detached_run( + sb, + workdir="/app", + command="true", + time_budget_sec=5, + poll_interval_sec=0.01, # don't sleep 5s in a test + ) + ) + + +def test_detached_run_returns_exit_and_tail(): + # nonzero exit -> tail captured and returned (137 == 128 + SIGKILL == OOM) + res = _run_detached(137, "fatal: Out of memory\nKilled") + assert isinstance(res, AgentRunResult) + assert res.exit_code == 137 + assert "Out of memory" in res.tail + + # clean exit -> no tail read (empty), per "tail only on failure" + ok = _run_detached(0, "should-not-be-read") + assert ok.exit_code == 0 + assert ok.tail == "" + + +# -------------------------------------------------------------------------- +# _merge_samples empty path carries the diag onto the abort +# -------------------------------------------------------------------------- +def _merge_empty(extra): + # On the empty path _merge_samples returns before touching `state`, so a + # dummy is safe. + return _merge_samples( + sample=Sample(index=0, prompt="x"), + state=None, + segments=[], + reward_result=RewardResult(reward=0.0, is_solved=False, extra=extra), + elapsed_sec=1.0, + instance_id="gravitational__teleport-deadbeef", + ) + + +def test_empty_session_carries_agent_diag(): + out = _merge_empty({"agent_exit_code": 137, "agent_tail": "Killed (OOM)", "harbor_steps_total": 1}) + assert len(out) == 1 + md = out[0].metadata + assert md["abort_reason"] == "adapter_session_empty" + assert md["agent_exit_code"] == 137 + assert md["agent_tail"] == "Killed (OOM)" + # only the agent diag is copied, not unrelated extra keys + assert "harbor_steps_total" not in md + + +def test_empty_session_without_diag_is_clean(): + # e.g. budget exhausted before any leg ran -> extra has no agent_* keys + out = _merge_empty({}) + md = out[0].metadata + assert md["abort_reason"] == "adapter_session_empty" + assert "agent_exit_code" not in md + assert "agent_tail" not in md + + +def test_venv_setup_hardens_pydantic_core_import(): + # Provisioning verifies the agent's deep import `import minisweagent.agents.default` + # and force-reinstalls (--reinstall --no-cache) to repair a partial wheel. + # `-P` is load-bearing: the check runs with cwd = the image WORKDIR (e.g. + # /testbed), so without it a task repo named like an agent dep (the pydantic + # SWE-gym tasks ship /testbed/pydantic/) shadows the venv and crashes the + # import -> a deterministic `exception:RuntimeError`. Verified fixed on + # pydantic-6104/6043/8511 (rc 1 -> 0). See profiles/provisioning_repro_pydantic.py. + from async_rl_research.agent.mini_swe_agent import _VENV_CHECK, _VENV_SETUP + + assert "minisweagent.agents.default" in _VENV_SETUP + assert "--reinstall" in _VENV_SETUP and "--no-cache" in _VENV_SETUP + assert "minisweagent.agents.default" in _VENV_CHECK + # the import checks MUST use `-P` (keep cwd/workdir off sys.path) + assert "-P -c 'import minisweagent.agents.default'" in _VENV_CHECK + assert _VENV_SETUP.count("-P -c 'import minisweagent.agents.default'") == 2 + # the repair script must be valid bash (it's assembled as a Python string) + import shutil + import subprocess + + bash = shutil.which("bash") + if bash: + r = subprocess.run([bash, "-n"], input=_VENV_SETUP, text=True, capture_output=True) + assert r.returncode == 0, f"_VENV_SETUP is not valid bash:\n{r.stderr}" + + +def test_abort_result_merges_extra(): + out = _abort_result(Sample(index=1, prompt="x"), "adapter_session_empty", extra={"agent_exit_code": 1}) + md = out[0].metadata + assert md["abort_reason"] == "adapter_session_empty" + assert md["agent_exit_code"] == 1 + # extra is optional: other abort reasons still work + out2 = _abort_result(Sample(index=2, prompt="x"), "boot_timeout:600s") + assert out2[0].metadata["abort_reason"] == "boot_timeout:600s" + assert "agent_exit_code" not in out2[0].metadata diff --git a/tests/test_qwen_adapter_splice.py b/tests/test_qwen_adapter_splice.py new file mode 100644 index 0000000000..7d876f1df8 --- /dev/null +++ b/tests/test_qwen_adapter_splice.py @@ -0,0 +1,248 @@ +"""Regression tests for the QwenOpenAIAdapter prompt-splice fix. + +The qwen3_coder tool-call parser strips trailing whitespace from tool-call +arguments, so re-rendering the parsed assistant message is not token-identical to +the model's raw ``output_ids``. When ``_build_prompt`` re-rendered, that mismatch +made ``merge_turns`` log "prefix drift" and mask whole assistant turns out of +training. The fix splices the previous turn's raw ``output_ids`` into the next +prompt so prompt == training target by construction. + +- ``test_splice_invariant_no_drift`` (unit): fake tokenizer, asserts every + appended prompt starts with ``prompt_{i-1} + output_{i-1}`` and ``merge_turns`` + trains 100% of output tokens with zero drift warnings. +- ``test_real_template_trailing_whitespace_*`` (integration): real Qwen3.6 + tokenizer, reproduces the trailing-whitespace drift and shows the splice path + eliminates it while the old re-render path does not. +""" +import json +import logging +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from slime.agent.adapters import openai as O +from slime.agent.adapters.common import AdapterChain, render_token_ids +from slime.agent.trajectory import TurnRecord, merge_turns +from async_rl_research.agent.adapters.qwen import _build_prompt as splice_build_prompt +from async_rl_research.agent.adapters.qwen import _dictify_tool_arguments + +NUM_GPUS = 0 + +TOOLS = [ + { + "type": "function", + "function": { + "name": "bash", + "description": "Run a bash command", + "parameters": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}, + }, + } +] + + +class _Session: + """Minimal stand-in for the adapter Session that ``_select_kind`` needs.""" + + def __init__(self): + self.main = AdapterChain() + self.segments = [] + + +class _DriftCapture: + """Capture warnings emitted by ``merge_turns``.""" + + def __enter__(self): + self.msgs = [] + self._handler = logging.Handler() + self._handler.emit = lambda record: self.msgs.append(record.getMessage()) + self._logger = logging.getLogger("slime.agent.trajectory") + self._old_level = self._logger.level + self._logger.addHandler(self._handler) + self._logger.setLevel(logging.WARNING) + return self + + def __exit__(self, *exc): + self._logger.removeHandler(self._handler) + self._logger.setLevel(self._old_level) + + +def _drive_episode(tok, raw_outputs, build_prompt, echo_for): + """Run the adapter's per-turn loop. ``raw_outputs[i]`` is (output_ids, raw_text); + ``echo_for(raw_text)`` returns the OpenAI assistant message handed back to the + harness (models the parser). Returns (chain, merged_segment, drift_warnings).""" + s = _Session() + target = s.main + messages = [ + {"role": "system", "content": "You are a helpful assistant that can interact with a computer."}, + {"role": "user", "content": "Please solve the task. Use the bash tool."}, + ] + for i, (output_ids, raw_text) in enumerate(raw_outputs): + kind = O._select_kind(s, messages) + prompt_ids = build_prompt(target, messages, TOOLS, kind, tok) + target.turns.append( + TurnRecord( + prompt_ids=list(prompt_ids), + output_ids=list(output_ids), + finish_reason="tool_calls", + output_log_probs=[-0.01] * len(output_ids), + ) + ) + messages = messages + [ + echo_for(raw_text), + {"role": "tool", "tool_call_id": "call_0", "content": f"0\n{i}"}, + ] + with _DriftCapture() as cap: + seg = merge_turns(target.turns) + return target, seg, list(cap.msgs) + + +# --------------------------------------------------------------------------- # +# unit: fake tokenizer, splice invariant + clean merge # +# --------------------------------------------------------------------------- # + +_IM_START, _IM_END, _NL = 800, 801, 802 +_ROLE = {"system": 810, "user": 811, "assistant": 812, "tool": 813} + + +class _FakeTokenizer: + """Prefix-consistent chat template: each message renders to a fixed block + ending in ``<|im_end|>`` + ``\\n``; the generation prompt appends its own block. + Models enough structure for ``_tool_continuation_ids``' sentinel-and-slice.""" + + def _block(self, m): + content = m.get("content") or "" + ctoks = [(ord(c) % 50) + 100 for c in str(content)[:5]] + calls = m.get("tool_calls") or [] + ctoks += [777] * len(calls) # a fixed per-tool-call marker + return [_IM_START, _ROLE.get(m.get("role"), 811)] + ctoks + [_IM_END, _NL] + + def apply_chat_template(self, messages, tools=None, tokenize=True, add_generation_prompt=True): + ids = [] + for m in messages: + ids += self._block(m) + if add_generation_prompt: + ids += [_IM_START, _ROLE["assistant"], 900] # "\n" + return ids + + def decode(self, ids, skip_special_tokens=False): + return "" + + +@pytest.mark.unit +def test_splice_invariant_no_drift(): + tok = _FakeTokenizer() + # three turns; output_ids are arbitrary but each ends in <|im_end|> + raw_outputs = [([700 + i, 701 + i, 702 + i, _IM_END], f"raw{i}") for i in range(3)] + + def echo_for(_raw): + return { + "role": "assistant", + "content": "ok", + "tool_calls": [{"id": "call_0", "type": "function", "function": {"name": "bash", "arguments": '{"command": "ls"}'}}], + } + + target, seg, warns = _drive_episode(tok, raw_outputs, splice_build_prompt, echo_for) + + assert warns == [], f"splice path should not drift, got: {warns}" + # every appended prompt must start with prompt_{i-1} + output_{i-1} + for i in range(1, len(target.turns)): + prev = list(target.turns[i - 1].prompt_ids) + list(target.turns[i - 1].output_ids) + assert target.turns[i].prompt_ids[: len(prev)] == prev, f"turn {i} prompt does not extend prior turn" + # 100% of generated output tokens are trained (mask=1); context is mask=0 + total_output = sum(len(o) for o, _ in raw_outputs) + assert sum(seg.loss_mask) == total_output + assert len(seg.loss_mask) == seg.response_ids.__len__() + + +# --------------------------------------------------------------------------- # +# integration: real Qwen3.6 template, trailing-whitespace drift # +# --------------------------------------------------------------------------- # + + +def _real_tokenizer(): + transformers = pytest.importorskip("transformers") + try: + return transformers.AutoTokenizer.from_pretrained("Qwen/Qwen3.6-35B-A3B") + except Exception as exc: # offline + not cached, gated, etc. + pytest.skip(f"Qwen3.6 tokenizer unavailable: {exc}") + + +def _raw_output_text(reasoning, cmd): + # cmd carries a trailing '\n' -> the parser strips it -> re-render drifts + return ( + f"{reasoning}\n\n\n\n\n\n" + f"{cmd}\n\n\n<|im_end|>" + ) + + +_TRAJECTORY = [ + ("Check the python version.", 'python3 -c "\nimport sys\nprint(sys.version)\n"\n'), + ("Run the failing case.", 'cd /testbed && python3 -c "\nimport numpy as np\nprint(np.zeros((2,2)))\n"\n'), + ("Apply the fix and re-run.", 'cd /testbed && python3 -c "\nprint(1 + 1)\n"\n'), +] + + +def _old_build_prompt(target, messages, tools_schema, kind, tok): + """Pre-fix behavior: extend/replace, dict-ify, full re-render.""" + (O._extend_chat_messages if kind == "append" else O._replace_chat_messages)(target, messages, tools_schema) + _dictify_tool_arguments(target.chat_messages) + return render_token_ids(target, tok) + + +def _build_real_episode_inputs(tok): + raw_outputs = [] + echo_map = {} + for reasoning, cmd in _TRAJECTORY: + raw_text = _raw_output_text(reasoning, cmd) + raw_outputs.append((tok.encode(raw_text, add_special_tokens=False), raw_text)) + # model the qwen3_coder parser: tool value has its trailing whitespace + # stripped, and arguments are returned to the harness as a JSON string + # (exactly what _chat_message/_json_arguments produce). + echo_map[raw_text] = { + "role": "assistant", + "content": None, + "reasoning_content": reasoning, + "tool_calls": [ + { + "id": "call_0", + "type": "function", + "function": {"name": "bash", "arguments": json.dumps({"command": cmd.rstrip()})}, + } + ], + } + return raw_outputs, (lambda raw: echo_map[raw]) + + +@pytest.mark.integration +def test_real_template_trailing_whitespace_old_path_drifts(): + tok = _real_tokenizer() + raw_outputs, echo_for = _build_real_episode_inputs(tok) + _, seg, warns = _drive_episode(tok, raw_outputs, _old_build_prompt, echo_for) + total_output = sum(len(o) for o, _ in raw_outputs) + # baseline: the old re-render path drifts and masks turns out of training + assert any("prefix drift" in w for w in warns), "expected the pre-fix path to drift" + assert sum(seg.loss_mask) < total_output, "expected the pre-fix path to mask some output" + + +@pytest.mark.integration +def test_real_template_trailing_whitespace_splice_is_clean(): + tok = _real_tokenizer() + raw_outputs, echo_for = _build_real_episode_inputs(tok) + target, seg, warns = _drive_episode(tok, raw_outputs, splice_build_prompt, echo_for) + total_output = sum(len(o) for o, _ in raw_outputs) + # the fix: no drift, every output token trained + assert warns == [], f"splice path must not drift, got: {warns}" + assert sum(seg.loss_mask) == total_output, "splice path must train 100% of output tokens" + # and every appended prompt contains the prior turn's raw output verbatim + for i in range(1, len(target.turns)): + prev = list(target.turns[i - 1].prompt_ids) + list(target.turns[i - 1].output_ids) + assert target.turns[i].prompt_ids[: len(prev)] == prev + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__, "-v"])) diff --git a/train.py b/train.py index 2404a0bbd1..e3c7f50dcd 100644 --- a/train.py +++ b/train.py @@ -1,3 +1,5 @@ +import os + import ray from slime.ray.placement_group import create_placement_groups, create_rollout_manager, create_training_models @@ -16,13 +18,34 @@ def train(args): # need to initialize rollout manager first to calculate num_rollout rollout_manager, num_rollout_per_epoch = create_rollout_manager(args, pgs["rollout"]) - # Update primary W&B with SGLang metrics endpoint now that servers are up. - router_addr = ray.get(rollout_manager.get_metrics_router_addr.remote()) - update_tracking_open_metrics(args, router_addr) + # DEBUG (qwen3.6 resync probe): skip sglang-metrics wiring for the HF-dump probes. + _hf_probe = os.environ.get("SLIME_SAVE_HF_AND_EXIT") or os.environ.get("SLIME_SAVE_HF_AFTER_TRAIN") + if not _hf_probe: + # Update primary W&B with SGLang metrics endpoint now that servers are up. + router_addr = ray.get(rollout_manager.get_metrics_router_addr.remote()) + update_tracking_open_metrics(args, router_addr) # create the actor and critic models actor_model, critic_model = create_training_models(args, pgs, rollout_manager) + # DEBUG (qwen3.6 resync probe): the model is loaded at the real TP/EP layout; + # dump HF via the live resync converter and exit BEFORE any train — tests the + # gather+convert on the clean model only. + if os.environ.get("SLIME_SAVE_HF_AND_EXIT"): + actor_model.save_hf(0) + finish_tracking(args) + return + + # DEBUG (qwen3.6 resync probe): train EXACTLY ONE step on dumped rollout data + # (load_debug_rollout_data → no sglang), then dump HF — tests whether the + # optimizer/backward step corrupts the Megatron weights. + if os.environ.get("SLIME_SAVE_HF_AFTER_TRAIN"): + rollout_data_ref = ray.get(rollout_manager.generate.remote(0)) + ray.get(actor_model.async_train(0, rollout_data_ref)) + actor_model.save_hf(0) + finish_tracking(args) + return + if args.offload_rollout: ray.get(rollout_manager.onload_weights.remote()) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..7518fc90bf --- /dev/null +++ b/uv.lock @@ -0,0 +1,3 @@ +version = 1 +revision = 3 +requires-python = ">=3.12"