Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Download app/mimo/runner/core.py from FineEnvs/RL-Explorer: direct link, hf CLI and curl.
- Browser
- Download file 18.1 kB
-
https://huggingface.co/spaces/FineEnvs/RL-Explorer/resolve/main/app/mimo/runner/core.py
- Command line
-
hf download hf://spaces/FineEnvs/RL-Explorer/app/mimo/runner/core.py
-
curl -L -o core.py https://huggingface.co/spaces/FineEnvs/RL-Explorer/resolve/main/app/mimo/runner/core.py
18.1 kB
| """One rollout, start to finish: sandbox up, environment set, agent runs, task's own verifier grades. | |
| Everything a rollout does becomes an *event*, kept in memory for live viewers and appended to | |
| `events.jsonl` in batches. Event kinds the UI understands: | |
| phase {name, status: "start"|"done"|"error", detail} | |
| text {text} an assistant message | |
| tool {tool, title, input, output, status, ms} | |
| step {tokens: {...}, cost} one model call finished | |
| log {text} setup / verifier output worth showing | |
| checks {checks: [{id, passed, score, method, tier, weight, message}], reward, summary} | |
| error {text} | |
| The user's token is held here for the rollout's lifetime only. It never reaches an event or a file. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import re | |
| import secrets | |
| import threading | |
| import time | |
| import traceback | |
| import uuid | |
| from concurrent.futures import ThreadPoolExecutor | |
| from .. import catalog, config, models, store, version | |
| _pool = ThreadPoolExecutor(max_workers=config.MAX_ACTIVE_ROLLOUTS, thread_name_prefix="rollout") | |
| _live: dict[str, "Rollout"] = {} | |
| _caps: dict[str, "Rollout"] = {} # model-proxy capability -> its rollout, while it runs | |
| _live_lock = threading.Lock() | |
| # key-shaped: a known prefix, then a run with an uppercase letter or a digit (code names like hf_raise_for_status don't match) | |
| SECRET_RE = re.compile(r"\b(hf_|sk-|sk_|api_key=|Bearer )(?=[A-Za-z0-9\-]*[A-Z0-9])[A-Za-z0-9\-]{4,}(?![a-z_])") | |
| ACTIVE_STATES = {"queued", "starting", "setup", "running", "verifying"} | |
| class Cancelled(Exception): | |
| pass | |
| class Rollout: | |
| def __init__(self, run: dict, token: str, agent_key: str | None = None): | |
| self.run = run | |
| self.id = run["id"] | |
| self.token = token | |
| self.agent_key = agent_key # a bring-your-own endpoint's key: memory only, like the HF token | |
| # what the sandbox gets instead of credentials (see config.PUBLIC_URL): valid while this rollout is live | |
| self.cap = secrets.token_urlsafe(32) | |
| self.stream: dict | None = None # the model reply streaming through the proxy right now, counted as it goes | |
| self.events: list[dict] = store.read_events(self.id) | |
| self._pending: list[dict] = [] | |
| self._last_flush = time.time() | |
| self._lock = threading.Lock() | |
| self.t0 = time.time() | |
| self.sandbox = None | |
| self.cancelled = threading.Event() | |
| self.tokens = {"input": 0, "output": 0, "reasoning": 0, "cache_read": 0, "cache_write": 0} | |
| self.model_cost = 0.0 | |
| # ββ events βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _redact(self, v): | |
| """Nothing secret reaches a trace: the user's HF token, an endpoint key, or anything shaped like one | |
| (the General verifier, for one, prints the first characters of its judge key).""" | |
| if isinstance(v, str): | |
| for secret in (self.token, self.agent_key, getattr(self, "cap", None)): | |
| if secret: | |
| v = v.replace(secret, "[redacted]") | |
| return SECRET_RE.sub(lambda m: m.group(1) + "[redacted]", v) | |
| if isinstance(v, dict): | |
| return {k: self._redact(x) for k, x in v.items()} | |
| if isinstance(v, list): | |
| return [self._redact(x) for x in v] | |
| return v | |
| def emit(self, kind: str, **data) -> None: | |
| ev = {"i": len(self.events), "t": round(time.time() - self.t0, 2), "kind": kind, **self._redact(data)} | |
| with self._lock: | |
| self.events.append(ev) | |
| self._pending.append(ev) | |
| if time.time() - self._last_flush > 1.0 or kind in ("phase", "checks", "error", "prompt"): | |
| self._flush() | |
| def _flush(self) -> None: | |
| pending, self._pending = self._pending, [] | |
| self._last_flush = time.time() | |
| store.append_events(self.id, pending) | |
| def phase(self, name: str, status: str = "start", detail: str = "") -> None: | |
| self.emit("phase", name=name, status=status, detail=detail) | |
| if status == "start": | |
| self.update(status={"sandbox": "starting", "setup": "setup", "agent": "running", | |
| "verify": "verifying"}.get(name, self.run.get("status")), phase=name) | |
| def log(self, text: str) -> None: | |
| if text and text.strip(): | |
| self.emit("log", text=text.strip()[-6000:]) | |
| def add_tokens(self, t: dict) -> None: | |
| for k in self.tokens: | |
| self.tokens[k] += int(t.get(k) or 0) | |
| ep = self.run.get("endpoint") | |
| if ep: # your own endpoint: metered only if you told us its prices | |
| t = self.tokens | |
| self.model_cost = ((t["input"] + t["cache_read"] + t["cache_write"]) * (ep.get("price_in") or 0) | |
| + (t["output"] + t["reasoning"]) * (ep.get("price_out") or 0)) / 1e6 | |
| else: | |
| self.model_cost = models.cost(self.run["model"], self.run.get("provider"), self.tokens) | |
| self.update(tokens=self.tokens, cost=self.cost_now()) | |
| def cost_now(self) -> dict: | |
| hours = (time.time() - self.t0) / 3600 | |
| sandbox = hours * config.FLAVOR_PRICE_PER_HOUR.get(self.run.get("flavor") or "", 0.0) | |
| judge = self.run.get("judge_cost") or 0.0 | |
| return {"model": round(self.model_cost, 5), "judge": round(judge, 5), "sandbox": round(sandbox, 5), | |
| "total": round(self.model_cost + judge + sandbox, 5)} | |
| def update(self, **fields) -> None: | |
| fields = self._redact(fields) | |
| self.run.update(fields) | |
| store.update(self.id, **fields) | |
| def check_cancel(self) -> None: | |
| if self.cancelled.is_set(): | |
| raise Cancelled() | |
| # ββ lifecycle ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def start_sandbox(self, image: str, flavor: str) -> "object": | |
| from huggingface_hub import Sandbox | |
| self.phase("sandbox", detail=f"{image.rsplit(':', 1)[-1]} on {flavor}") | |
| self.update(flavor=flavor) | |
| t = time.time() | |
| self.sandbox = Sandbox.create(image=image, flavor=flavor, idle_timeout=config.SANDBOX_IDLE_TIMEOUT, | |
| forward_hf_token=False, token=self.token, start_timeout=900, | |
| labels={"app": "rl-explorer", "runner": "mimo", "run": self.id}) | |
| self.update(sandbox_id=self.sandbox.id) | |
| self.phase("sandbox", "done", f"ready in {time.time() - t:.0f}s") | |
| return self.sandbox | |
| def agent_api(self) -> tuple[str, str | None, str]: | |
| """(base URL, key, model string) the agent's model calls go to.""" | |
| ep = self.run.get("endpoint") | |
| if ep: | |
| return ep["base_url"], self.agent_key, self.run["model"] | |
| mid = f"{self.run['model']}:{self.run['provider']}" if self.run.get("provider") else self.run["model"] | |
| return config.ROUTER, self.token, mid | |
| def llm_base(self) -> str | None: | |
| """Only a per-run capability reaches the sandbox, including local previews.""" | |
| if config.PUBLIC_URL: | |
| return f"{config.PUBLIC_URL}/api/llm/{self.cap}/v1" | |
| from ...runner import _proxy_url | |
| return f"{_proxy_url().rstrip('/')}/rlx/llm/{self.cap}/v1" | |
| def check_model_network(self): | |
| """Check the exact relay path from the sandbox, without a model call.""" | |
| url = self.llm_base().removesuffix("/v1") + "/health" | |
| script = ("import urllib.request,urllib.error\n" | |
| f"url={url!r}\n" | |
| "try:\n" | |
| " r=urllib.request.urlopen(url,timeout=15)\n" | |
| " print('relay HTTP',r.status)\n" | |
| "except urllib.error.HTTPError as e:\n" | |
| " print('relay HTTP',e.code);raise SystemExit(1)\n" | |
| "except Exception as e:\n" | |
| " print(type(e).__name__);raise SystemExit(1)\n") | |
| import shlex | |
| result = self.sh("python3 -c " + shlex.quote(script), timeout=25) | |
| if result.exit_code: | |
| raise RuntimeError("The sandbox cannot reach the model relay. Check HTTPS egress from the sandbox and ingress to the explorer. " | |
| "No account token was forwarded. " + (result.stdout or "")[-120:]) | |
| self.log("Network check passed: sandbox β model relay. HF account token stays on the explorer.") | |
| def upstream(self, model: str) -> tuple[str, str | None] | None: | |
| """For the model proxy: (base URL, key) for a model this rollout may call, else None.""" | |
| base, key, mid = self.agent_api() | |
| if model == mid: | |
| return base, key | |
| if self.run.get("judge") and model == self.run["judge"]: | |
| return config.ROUTER, self.token | |
| return None | |
| def sh(self, cmd: str, timeout: float = 600, **kw): | |
| """Run a shell command in the sandbox; never raises on a non-zero exit.""" | |
| self.check_cancel() | |
| return self.sandbox.run(cmd, shell=True, check=False, timeout=timeout, **kw) | |
| def settle_model(self) -> None: | |
| """Check, with tiny requests, that the model takes this rollout's settings, and adjust rather than fail. | |
| Some models take no thinking level at all (Kimi-K2-Instruct answers HTTP 400 to reasoning_effort=low): run | |
| those with thinking off. A provider that caps replies below the agent's output limit gets its own cap.""" | |
| p = self.run.get("params") or {} | |
| base, key, mid = self.agent_api() | |
| byo = bool(self.run.get("endpoint")) | |
| eff = p.get("thinking") | |
| if eff in ("low", "medium", "high") and _refused(base, key, mid, byo, {"reasoning_effort": eff}, r"reason|think|effort"): | |
| p = {**p, "thinking": "none", "thinking_requested": eff} | |
| self.update(params=p) | |
| self.log(f"{self.run['model']} doesn't take a thinking level (it refused reasoning_effort={eff}), " | |
| "so this rollout runs with thinking off.") | |
| if self.run["domain"] != "music" and not p.get("max_tokens"): | |
| from .opencode import OUTPUT_LIMIT | |
| if _refused(base, key, mid, byo, {"max_tokens": OUTPUT_LIMIT}, r"token|length|context|max"): | |
| self.update(params={**p, "output_limit": 32000}) | |
| self.log(f"The provider doesn't allow {OUTPUT_LIMIT:,}-token replies for this model; " | |
| "the agent's replies are capped at 32,000 tokens.") | |
| def execute(self) -> None: | |
| from .domains import ADAPTERS | |
| if self.run.get("runner") == "nemo-gym": | |
| from ... import nemo_runner as adapter | |
| else: | |
| adapter = ADAPTERS[self.run["domain"]] | |
| try: | |
| self.update(status="starting", started_at=time.time()) | |
| self.settle_model() | |
| result = adapter.run(self) | |
| self.update(status="done", phase="done", reward=result.get("reward"), reward_error=result.get("error"), | |
| graded=result.get("reward") is not None, wall_s=round(time.time() - self.t0, 2), | |
| finished_at=time.time(), cost=self.cost_now()) | |
| self.phase("done", "done", f"reward {result.get('reward')}" if result.get("reward") is not None | |
| else f"not scored: {result.get('error')}") | |
| except Cancelled: | |
| self.update(status="cancelled", finished_at=time.time(), cost=self.cost_now()) | |
| self.phase("done", "error", "cancelled") | |
| except Exception as e: # noqa: BLE001 | |
| if self.cancelled.is_set(): # killing the sandbox makes the blocked call fail: that is the cancel, not a crash | |
| self.update(status="cancelled", finished_at=time.time(), cost=self.cost_now()) | |
| else: | |
| msg = friendly(e) or f"{type(e).__name__}: {str(e)[:300]}" | |
| self.emit("error", text=msg, trace=traceback.format_exc()[-3000:]) | |
| self.update(status="failed", error=msg, finished_at=time.time(), cost=self.cost_now()) | |
| finally: | |
| with self._lock: | |
| self._flush() | |
| self.stop_sandbox() | |
| self.agent_key = None | |
| self.native_task = None | |
| with _live_lock: | |
| _caps.pop(self.cap, None) | |
| with _live_lock: | |
| _live.pop(self.id, None) | |
| self.token = "" | |
| def stop_sandbox(self) -> None: | |
| if self.sandbox is None: | |
| return | |
| try: | |
| from huggingface_hub import Sandbox | |
| Sandbox.kill(self.sandbox.id, token=self.token) | |
| except Exception: # it idles out on its own anyway | |
| pass | |
| self.sandbox = None | |
| def friendly(e: Exception) -> str | None: | |
| """The Hub errors people actually hit, said so they know what to do.""" | |
| status = getattr(getattr(e, "response", None), "status_code", None) | |
| text = str(e) | |
| if status == 402 or "402 Payment Required" in text: | |
| return ("Your Hugging Face account has no prepaid credit for sandboxes, so none could start. Add credit at " | |
| "https://huggingface.co/settings/billing and run again. Nothing was charged.") | |
| if status in (401, 403) and "/api/jobs" in text: | |
| return ("Your sign-in isn't allowed to start sandboxes. Sign in again and allow Jobs, or use a write token.") | |
| if status == 429: | |
| return "Hugging Face is rate-limiting sandbox starts for your account. Wait a minute and run again." | |
| return None | |
| # ββ public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def active_for(user: str) -> int: | |
| with _live_lock: | |
| return sum(1 for r in _live.values() if r.run["user"] == user and not r.cancelled.is_set()) | |
| def submit(user: str, token: str, task: dict, model: str, provider: str | None, judge: str | None, | |
| endpoint: dict | None = None, agent_key: str | None = None, params: dict | None = None, | |
| visibility: str = "public", extra: dict | None = None, reserved: bool = False) -> dict: | |
| """`extra`: fields the explorer adds to the record (its dataset, ref, environment and runner). `reserved`: the | |
| explorer already holds this rollout's place (app/runner._slot, with the admin's limits), so no check of its own.""" | |
| if not reserved and active_for(user) >= config.MAX_ACTIVE_PER_USER: | |
| raise RuntimeError(f"You already have {config.MAX_ACTIVE_PER_USER} rollouts running. Wait for one to finish.") | |
| with _live_lock: | |
| if not reserved and len(_live) >= config.MAX_ACTIVE_ROLLOUTS: | |
| raise RuntimeError("The explorer is at capacity right now. Try again in a few minutes.") | |
| run = store.create({ | |
| "id": time.strftime("%Y%m%d-%H%M%S-") + uuid.uuid4().hex[:6], | |
| "user": user, "task_id": task["id"], "domain": task["domain"], "title": task["title"], | |
| "facets": task.get("facets"), "model": model, "provider": provider, "judge": judge, | |
| "status": "queued", "harness": "opencode", "endpoint": endpoint, "params": params or {}, | |
| "visibility": "private" if visibility == "private" else "public", | |
| "image": catalog.image_for(task["id"]) if task["domain"] not in ("music", "nemo-gym") else None, | |
| "provenance": version.provenance(), | |
| **(extra or {}), | |
| }) | |
| r = Rollout(run, token, agent_key) | |
| r.native_task = task.get("native_task") # reference action stays in memory; never in the run record | |
| with _live_lock: | |
| _live[r.id] = r | |
| _caps[r.cap] = r | |
| _pool.submit(r.execute) | |
| return run | |
| _refusals: dict[tuple, bool] = {} | |
| def _refused(base: str, key: str | None, mid: str, byo: bool, extra: dict, about: str) -> bool: | |
| """Does the model answer HTTP 400/422, about `about`, to a two-word request carrying `extra`? Cached.""" | |
| k = (base, mid, json.dumps(extra, sort_keys=True)) | |
| if k not in _refusals: | |
| import httpx | |
| from .. import endpoints | |
| body = {"model": mid, "max_tokens": 16, **extra, "messages": [{"role": "user", "content": "Say OK."}]} | |
| try: | |
| if byo: # the user's endpoint: pinned connection, capped reply | |
| r = endpoints.request("POST", f"{base}/chat/completions", key, timeout=60, json=body) | |
| else: | |
| r = httpx.post(f"{base}/chat/completions", json=body, timeout=60, | |
| headers={"Authorization": f"Bearer {key}"} if key else {}) | |
| except Exception: | |
| return False # can't tell; let the rollout itself report a real connection problem | |
| refused = r.status_code in (400, 422) and bool(re.search(about, r.text, re.I)) | |
| if r.status_code in (200, 400, 422): # only cache real answers, not rate limits or outages | |
| _refusals[k] = refused | |
| return refused | |
| return _refusals[k] | |
| def by_cap(cap: str) -> Rollout | None: | |
| with _live_lock: | |
| r = _caps.get(cap) | |
| return r if r is not None and not r.cancelled.is_set() else None | |
| def live(run_id: str) -> Rollout | None: | |
| with _live_lock: | |
| return _live.get(run_id) | |
| def events(run_id: str, after: int = 0) -> list[dict]: | |
| r = live(run_id) | |
| if r is not None: | |
| with r._lock: | |
| return r.events[after:] | |
| return store.read_events(run_id, after) | |
| def cancel(run_id: str) -> bool: | |
| r = live(run_id) | |
| if r is None: | |
| return False | |
| r.cancelled.set() | |
| with _live_lock: | |
| _caps.pop(r.cap, None) | |
| # Say so at once: the worker may sit in a blocking sandbox call for a while after the kill. | |
| r.update(status="cancelled", finished_at=time.time(), cost=r.cost_now()) | |
| r.phase("done", "error", "stopped by you") | |
| threading.Thread(target=r.stop_sandbox, daemon=True).start() | |
| return True | |