Source code for tooluniverse.remote_resident.supervisor

"""Start, reuse and retire one provider's resident model worker.

A GPU remote tool costs most of its wall clock in work that is identical for
every request: loading weights, building the CUDA context, capturing the
kernels a first pass compiles. Measured for Boltz-2 on this project's GB10 box,
a fresh worker per request spends ~102 s where a held one spends ~47 s, and the
structure pass alone falls from ~23 s to ~5.8 s because CUDA graphs captured on
one request are replayed on the next. That is what this module exists to hold
on to.

Holding a model resident spends device memory the whole time, which is the
opposite of what a box running several models wants. Both are served by the
same object: a worker is started on the first request, reused while requests
keep arriving, and shut down once ``idle_timeout_s`` passes with none -- so a
provider nobody is calling is not occupying the card. ``vram_budget_gb`` from
the provider's manifest is what a scheduler above this reads to decide how many
providers may be resident at once; this module reports it and never enforces it.

One request at a time per worker, by construction. Two requests sharing one
CUDA context would interleave their allocations and make an out-of-memory
attributable to neither, so callers queue on :attr:`ResidentWorker.lock` rather
than racing.
"""

from __future__ import annotations

import os
import socket
import subprocess
import threading
import time
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Tuple

from . import protocol as P


[docs] class WorkerStartError(RuntimeError): """The worker could not be started, or did not bind its socket in time."""
[docs] class WorkerRequestError(RuntimeError): """The worker answered, and the answer was a failure."""
[docs] def __init__(self, message: str, reason: str = "request_failed"): super().__init__(message) self.reason = reason
# STANDARD section 8: the exit codes are stable and branchable, which is only # worth anything if what reads them says what they mean. A bare number in an # error message makes an operator look the table up; these sentences are the # table, and they are here once so the start path and the request path cannot # drift apart on what a 5 is. EXIT_HINTS = { 3: "that is a refusal (exit 3), so its last line names what it refused and why", 5: ( "that is an expected accelerator absent (exit 5): a kit's kernel " "census refuses this way when the route it required is not the route " "that loaded, and the KERNELS line above names it" ), }
[docs] def exit_hint(rc: Optional[int]) -> str: """The clause naming what ``rc`` means, or nothing when it has no name.""" text = EXIT_HINTS.get(rc) if isinstance(rc, int) else None return f" \u2014 {text}" if text else ""
# (argv, extra environment) for one worker process. A provider builds it: the # interpreter must be the model environment's own, never the one running # ToolUniverse, because the two do not share a dependency set. # The mapping is applied over the operator's environment; a None value # REMOVES that name from the child's environment rather than setting it.
[docs] def child_env( extra_env: Dict[str, Optional[str]], environ: Dict[str, str] ) -> Dict[str, str]: """The child's environment: `environ`, with `extra_env` applied over it. **A launcher removes a name by mapping it to None.** It cannot remove one by returning a pruned copy of `environ`, and this function exists as a separate, testable thing because that distinction was got wrong and the wrong version passed review. af3_jax's launcher pruned a copy; the supervisor then rebuilt the child's environment from `os.environ` and applied the pruned copy over it, so every name the launcher had removed came straight back. An operator with a stray `AF3_JAX_BOGUS` exported got a worker that exited 3 at `import alphafold3` -- exactly the failure the pruning existed to prevent. The check that missed it read `build_command`'s RETURN VALUE, which is the one place a pruned copy looks correct. A launcher's intent has to be expressed in something the supervisor acts on. """ env = dict(environ) env.update({k: v for k, v in extra_env.items() if v is not None}) for name in [k for k, v in extra_env.items() if v is None]: env.pop(name, None) return env
CommandBuilder = Callable[ ["ResidentWorker"], Tuple[List[str], Dict[str, Optional[str]]] ]
[docs] @dataclass class ResidentWorker: """One provider's model, held in a process of its own.""" slug: str build_command: CommandBuilder # Cold start is weights plus CUDA plus, for some providers, a first-pass # kernel compile; minutes, not seconds. start_timeout_s: float = 900.0 request_timeout_s: float = P.DEFAULT_REQUEST_TIMEOUT_S # 0 disables retirement: the worker holds the card until it is stopped. idle_timeout_s: float = 0.0 vram_budget_gb: Optional[float] = None # Whether `ensure_started` consults `admission` before spending the budget. # True is the default because the alternative has wedged this host; a host # with its own scheduler above this one turns it off and owns the decision. admit: bool = True log_path: Optional[str] = None # The provider's manifest, so a command builder has the declaration it was # built from without a second load. manifest: Any = None lock: threading.RLock = field(default_factory=threading.RLock, repr=False) _proc: Optional[subprocess.Popen] = field(default=None, repr=False) _log: Any = field(default=None, repr=False) _last_used: float = field(default=0.0, repr=False) _reaper: Optional[threading.Thread] = field(default=None, repr=False) _stopping: bool = field(default=False, repr=False) # ------------------------------------------------------------------ state @property def socket_path(self) -> str: return P.socket_path(self.slug)
[docs] def is_running(self) -> bool: return self._proc is not None and self._proc.poll() is None
[docs] def ping(self) -> Optional[Dict[str, Any]]: """The worker's own answer, or None when nothing is listening.""" try: return self._roundtrip({"op": P.OP_PING}, timeout=P.CONNECT_TIMEOUT_S) except (OSError, P.ProtocolError, WorkerRequestError): return None
# ------------------------------------------------------------------ start
[docs] def ensure_started(self) -> None: """Start the worker if it is not already serving. Holds :attr:`lock`. STARTING IS WHERE THE BUDGET IS SPENT, so it is where the budget is checked. `admission.room_for` adds up what is already held and reads the machine, and a refusal raises :class:`AdmissionRefused` rather than starting a worker the host cannot carry -- this project wedged the host once by starting one anyway, with the numbers available. `admit` is the escape hatch and it is an ATTRIBUTE, not an environment variable: a stray name in the environment is the class of mistake this module's siblings are full of, and a host that genuinely wants an unchecked start says so in the object it is holding. """ with self.lock: if self.is_running() and os.path.exists(self.socket_path): return if self.is_running(): # Process alive but no socket: it died between bind and now, or # is still coming up from an earlier call that timed out. self._terminate() # No declared budget means nothing is spent from the pool, and the # manifest layer already guarantees that reading: a resident provider # whose `[gpu] policy` is not "none" cannot load without a # `vram_budget_gb`. So None here is esm_if1's case -- CPU only -- and # admitting it is not a decision anyone needs to make. if self.admit and self.vram_budget_gb is not None: from . import admission admission.room_for(self.slug, self.vram_budget_gb).raise_if_refused() self._start()
def _start(self) -> None: argv, extra_env = self.build_command(self) env = child_env(extra_env, os.environ) # A socket left by a previous process would be connected to and never # answer; the worker unlinks it too, but not if it was killed. if os.path.exists(self.socket_path): os.unlink(self.socket_path) if self.log_path: os.makedirs(os.path.dirname(self.log_path) or ".", exist_ok=True) self._log = open(self.log_path, "a", buffering=1) out = self._log else: out = subprocess.DEVNULL self._proc = subprocess.Popen( argv, env=env, stdout=out, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True, # a Ctrl-C to the MCP server is not the model's ) deadline = time.time() + self.start_timeout_s while not os.path.exists(self.socket_path): rc = self._proc.poll() if rc is not None: where = self._transcript() # A named code per STANDARD section 8, said out loud: a # refusal has a cause an operator can act on and its last # stderr line names it, where an unnamed non-zero code is a # crash whose transcript has to be read. why = exit_hint(rc) raise WorkerStartError( f"{self.slug}: the resident worker exited {rc} before it bound " f"{self.socket_path}{where}{why}" ) if time.time() > deadline: self._terminate() raise WorkerStartError( f"{self.slug}: the resident worker did not bind {self.socket_path} " f"within {self.start_timeout_s:.0f}s" ) time.sleep(0.5) self._last_used = time.time() self._start_reaper() # ------------------------------------------------------------------- call
[docs] def request(self, payload: Dict[str, Any]) -> Dict[str, Any]: """One request, serialized against every other on this worker. A worker that dies answering is named rather than guessed at, in both of the shapes that takes. Usually it manages a last reply first -- ``worker_base`` sends one for any ``SystemExit`` out of a prediction, which is how the kits' ``KernelsRefused`` arrives -- and then the only thing missing is what the worker could not know: where its transcript is, and what its exit code is called. Sometimes it cannot answer at all (``os._exit``, the OOM killer, a segfault), and then the socket just ends; a bare ``ProtocolError: peer closed the connection mid-message`` is true and useless, so the code it exited with goes in its place. """ with self.lock: self.ensure_started() self._last_used = time.time() try: reply = self._roundtrip(payload, timeout=self.request_timeout_s) except (OSError, P.ProtocolError) as exc: raise self._died_mid_request(exc) from exc finally: self._last_used = time.time() if not reply.get("ok"): message = reply.get("error") or "the worker reported a failure" reason = reply.get("reason", "request_failed") if reason == "worker_exiting": message = ( f"{self.slug}: {message}{self._transcript()}" f"{exit_hint(reply.get('exit_code'))}" ) raise WorkerRequestError(message, reason=reason) return reply
def _transcript(self) -> str: return f" (its transcript is in {self.log_path})" if self.log_path else "" def _died_mid_request(self, exc: BaseException) -> BaseException: """``exc``, or a better-named error when the worker is the reason. The wait is short and bounded and only ever runs on this path: a process that has just closed its socket is milliseconds from being reapable, but ``poll()`` asked immediately still answers None often enough to lose the code. A worker that is still alive -- a request that simply outran ``request_timeout_s``, say -- gets ``exc`` back unchanged, because then the socket failed and the worker did not. """ if self._proc is None: return exc try: rc = self._proc.wait(timeout=2.0) except subprocess.TimeoutExpired: return exc return WorkerRequestError( f"{self.slug}: the resident worker exited {rc} during the request " f"without answering{self._transcript()}{exit_hint(rc)}", reason="worker_died", ) def _roundtrip(self, payload: Dict[str, Any], *, timeout: float) -> Dict[str, Any]: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(timeout) try: sock.connect(self.socket_path) P.send_line(sock, payload) return P.recv_line(sock, bytearray()) finally: sock.close() # ------------------------------------------------------------------- stop
[docs] def stop(self, *, grace_s: float = 30.0) -> None: """Ask the worker to exit, then make sure it did. Frees the device.""" with self.lock: self._stopping = True try: if self.is_running() and os.path.exists(self.socket_path): try: self._roundtrip({"op": P.OP_SHUTDOWN}, timeout=grace_s) except (OSError, P.ProtocolError): pass # it is going away either way if self._proc is not None: try: self._proc.wait(timeout=grace_s) except subprocess.TimeoutExpired: self._terminate() finally: self._stopping = False self._proc = None if self._log is not None: self._log.close() self._log = None if os.path.exists(self.socket_path): try: os.unlink(self.socket_path) except OSError: pass
def _terminate(self) -> None: proc = self._proc if proc is None: return proc.terminate() try: proc.wait(timeout=15) except subprocess.TimeoutExpired: proc.kill() proc.wait(timeout=15) # ----------------------------------------------------------------- reaper def _start_reaper(self) -> None: if self.idle_timeout_s <= 0 or (self._reaper and self._reaper.is_alive()): return def run() -> None: while True: time.sleep(min(30.0, max(1.0, self.idle_timeout_s / 4))) if self._stopping or not self.is_running(): return idle = time.time() - self._last_used if idle < self.idle_timeout_s: continue # A request in flight holds the lock for its whole duration, so # a non-blocking acquire is what distinguishes "idle" from # "busy for longer than the timeout". if not self.lock.acquire(blocking=False): continue try: if time.time() - self._last_used >= self.idle_timeout_s: self.stop() return finally: self.lock.release() self._reaper = threading.Thread( target=run, name=f"resident-reaper-{self.slug}", daemon=True ) self._reaper.start()