Source code for tooluniverse.remote_resident.worker_base

"""The half of a resident worker that is the same for every model.

This file runs inside the *model's* environment, not ToolUniverse's, and is
loaded by path for the same reason ``protocol.py`` is: that environment holds a
pinned scientific stack and nothing of this package, and importing
``tooluniverse`` there would drag in a dependency set it has no reason to carry.
Its own imports are therefore the standard library only; the one function that
needs the model's stack (:func:`release_cublas_workspaces`) imports inside the
call.

A provider's worker script supplies what is genuinely its own — building the
model, and turning one request into one prediction — and calls :func:`serve`
with them. Everything below is what every model needs identically: the socket,
the request loop, a failure that does not take the model down with it, and the
two ways a held model gives the card back.

Giving the card back is the part worth being explicit about. A held model is
fast because it keeps weights, a CUDA context and compiled kernels resident,
and that is exactly why it must stop holding them when nobody is asking. The
supervisor retires an idle worker; that covers the ordinary case. It does not
cover its own death, and on this project SIGKILLing an MCP server left two
worker processes holding a GPU that nothing could then reach. So the worker
also watches for being orphaned — reparented to init — and exits once a grace
period has passed with no request, long enough that a prediction in flight when
the parent died still finishes and writes its answer.
"""

from __future__ import annotations

import importlib.util
import os
import socket
import sys
import threading
import time
import traceback
from pathlib import Path
from typing import Any, Callable, Dict, Optional

# The supervisor exports these so a worker never has to guess where the shared
# files are; the filesystem fallback keeps a hand-run worker working.
PROTOCOL_ENV = "TOOLUNIVERSE_REMOTE_PROTOCOL"
ORPHAN_GRACE_ENV = "TOOLUNIVERSE_RESIDENT_ORPHAN_GRACE_S"
DEFAULT_ORPHAN_GRACE_S = 120.0
# Set to "1" to stop :func:`serve` releasing cuBLAS workspaces after each
# request. Nothing in the fleet needs it; it is here so that the leak can be
# reproduced on a provider that no longer has it, which is how the fix was
# checked in the first place and how the next one will be.
NO_CUBLAS_RELEASE_ENV = "TOOLUNIVERSE_RESIDENT_NO_CUBLAS_RELEASE"


def _load_by_path(name: str, path: Path):
    spec = importlib.util.spec_from_file_location(name, path)
    if spec is None or spec.loader is None:
        raise SystemExit(f"[{name}] cannot be loaded from {path}")
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


[docs] def protocol(): """The wire protocol module, by path — never as a ToolUniverse import.""" override = os.environ.get(PROTOCOL_ENV) path = ( Path(override) if override else Path(__file__).resolve().parent / "protocol.py" ) if not path.is_file(): raise SystemExit( f"the wire protocol is not at {path}; set {PROTOCOL_ENV} to its path" ) return _load_by_path("_tu_remote_protocol", path)
[docs] def release_cublas_workspaces() -> int: """Give back the cuBLAS workspaces torch caches per (handle, stream). Returns the bytes freed, or 0 on a build with no such call and on CPU. WHY A HELD MODEL NEEDS THIS. torch allocates a cuBLAS workspace the first time a GEMM runs on a (handle, stream) pair and keeps it in a C++ static map that nothing clears. A kit that captures a CUDA graph creates a fresh side stream per request to warm up on before capturing, so every request abandons a stream and leaves its workspace behind -- measured at exactly ``torch.backends.cuda.blas_workspace_size()``, 32 MiB on this GB10, per request on byte-identical input. It is invisible to every ordinary check. The bytes are in ``memory_allocated`` but held by no Python object, so a census of every reachable CUDA tensor stays frozen while the number climbs, ``gc.collect()`` returns nothing, and ``empty_cache()`` returns nothing -- it only drops blocks the allocator is *caching*, and these are live. What it looks like from outside is one new fully-allocated allocator segment per request, on a stream id that was not there before. ``chai1`` and ``genie3`` each grew ~0.04 GiB per request until this call, which is 140 requests to chai1's whole declared budget; their manifests carry the measurements. A HELD GRAPH, MEASURED RATHER THAN FEARED. This used to say the call was safe only where no captured graph outlives the request, on the reasoning that a graph which captured cuBLAS calls holds the workspace pointer, and on that reasoning it was a call each provider had to make for itself. `rosettafold3` is why it finally got tested rather than repeated: its `fast` mode keeps pairformer-stack graphs across folds, so it could not use the argument. The reasoning is wrong twice over. * A replay cannot read a freed workspace's contents, because the workspace is scratch that each kernel writes before it reads. ``cudaMemset`` it to 0xAB and the replay returns the same bits, so "the output did not change" was never evidence of anything either. * The failure that WOULD be real is the other direction -- the replay still writes to that address, and writing over a tensor the allocator has since handed to someone else corrupts it. It does not happen: a workspace allocated during a capture belongs to that graph's PRIVATE memory pool (``segment_pool_id`` (1, 0), not (0, 0)), so clearing the map hands the block back to a pool only a capture into the same pool draws from. Checked on every torch in this fleet -- 2.7.0+cu128, 2.7.1+cu128, 2.8.0+cu129, 2.10.0+cu128, 2.11.0+cu128, 2.11.0+cu130, 2.12.0+cu130, 2.13.0+cu130 -- and on each one with a POSITIVE CONTROL first, because without one the test is vacuous and two of the first three written here were. The control reads the workspace bytes back to show that this build's replay does write to them; which GEMM does that is a function of the cuBLAS version and has to be searched for per build (fp32 ``mm`` works everywhere here; fp16 writes on 13.0 and not on 12.8, and a square ``1024**3`` writes on neither). With the control passing, 34 workspaces' worth of live tensors on the default stream and on fresh ones never landed on the address, twenty cleared replays scribbled on nothing, and with five graphs captured and four dropped the survivor still wrote only into its own private pool. The workspace is 32 MiB on torch >= 2.12 and 8.12 MiB before it, and on the older builds it sits INSIDE a 20 MiB segment -- a probe that filters segments by size rather than blocks finds nothing there and proves nothing, which is how the first pass missed it. So this is not a call a provider has to prove its graphs dead to make, and it is no longer a call a provider makes at all: :func:`serve` does it after every request, for everybody. See ``NO_CUBLAS_RELEASE_ENV`` for the way back out. """ # The one function here that touches the model's stack rather than the # standard library, and it imports inside the call so that the module's # import stays what the docstring says it is. try: import torch # noqa: PLC0415 except Exception: # noqa: BLE001 - a worker without torch has no workspaces return 0 clear = getattr(getattr(torch, "_C", None), "_cuda_clearCublasWorkspaces", None) if clear is None or not torch.cuda.is_available(): return 0 before = torch.cuda.memory_allocated() clear() return max(0, before - torch.cuda.memory_allocated())
def _fatal_reply(exc: BaseException) -> Dict[str, Any]: """The last thing a worker says when a request takes it down with it. A ``SystemExit`` raised out of ``on_predict`` is not a failed request. It is the process declaring that it must not answer another one, and three of the kits do exactly that: ``af3_jax``, ``rfdiffusion3`` and ``opendde`` each define a ``KernelsRefused(SystemExit)`` and raise it from inside a prediction when the kernel route they required is not the route that loaded. All three carry code **5**, which is this standard's own code for an expected accelerator absent (section 8). That verdict is about the PROCESS, not about the request, and honouring it is right: a provider that advertises a kit's kernels and then answers on stock ones is returning results whose provenance is a lie. **Swallowing it would not even keep the worker alive.** af3_jax's ``refuse_exit`` arms a ``threading.Timer`` that calls ``os._exit(5)`` sixty seconds later, in its own words "should anything swallow the SystemExit", and rfdiffusion3's ``finish_at_exit`` hook forces the same code at interpreter end. Catching the refusal and carrying on buys one minute of serving and then an abrupt death in the middle of whichever request is running by then -- strictly worse than dying now, and harder to attribute. So the worker still dies. What it must not do is die SILENTLY, which is what it did before this existed: the refusal unwound the whole loop, the client's ``recv_line`` hit EOF and raised ``ProtocolError: peer closed the connection mid-message``, and the KERNELS line naming the cause sat in a transcript nobody was pointed at. This builds the reply that names it; the loop sends it, then re-raises so the exit still happens. """ code = exc.code if isinstance(exc, SystemExit) else None # ``SystemExit(int)`` exits with that status; ``SystemExit(anything else)`` # prints it and exits 1; ``SystemExit()`` exits 0. KeyboardInterrupt has no # code at all. exit_code = code if isinstance(code, int) else (None if code is None else 1) # All three KernelsRefused classes pass the CODE to ``SystemExit``, so # ``str(exc)`` is the bare number and the readable part is on an attribute # instead: ``.reason`` on rfdiffusion3's, ``.line`` on opendde's. Prefer # whichever is there over printing "5" twice. This is a convenience, not a # protocol -- an exception with neither is reported as it prints. said = next( ( str(v).strip() for v in (getattr(exc, "reason", None), getattr(exc, "line", None)) if isinstance(v, str) and v.strip() ), str(exc), ) tail = ( f"the worker exits {exit_code} rather than answering again" if exit_code is not None else "the worker is exiting rather than answering again" ) return { "ok": False, "error": f"{type(exc).__name__}: {said} \u2014 {tail}", "reason": "worker_exiting", "exit_code": exit_code, "traceback": traceback.format_exc(limit=8), }
[docs] def serve( *, slug: str, socket_path: str, on_predict: Callable[[Dict[str, Any]], Dict[str, Any]], ready: Optional[Callable[[], Dict[str, Any]]] = None, on_reset: Optional[Callable[[], None]] = None, log: Optional[Callable[[str], None]] = None, orphan_grace_s: Optional[float] = None, release_cublas: bool = True, ) -> int: """Answer requests on ``socket_path`` until told to stop. ``on_predict`` receives the request dict and returns the body of a successful reply. Raising is fine and expected: the failure is named back to the caller and the model stays up, because a bad request must not cost the minutes it took to build one. ``on_reset`` runs just before each prediction (peak-memory counters and the like); ``ready`` adds fields to the ping. Raising something that is *not* an ``Exception`` is the other case, and it is not a failed request: the caller is told and then the worker goes down. :func:`_fatal_reply` is where that is argued, because the argument is the whole of why this loop has two except clauses instead of one. One request at a time, by construction. Two inside one CUDA context would interleave their allocations and make an out-of-memory attributable to neither, so this loop accepts and answers serially and callers queue. After each prediction it calls :func:`release_cublas_workspaces`, which is where the reasoning for doing that for everybody is written down. A provider that wants it off passes ``release_cublas=False``; so does ``TOOLUNIVERSE_RESIDENT_NO_CUBLAS_RELEASE=1``, which exists to reproduce the leak rather than to live in a deployment. """ P = protocol() say = log or (lambda msg: print(f"[{slug}-resident] {msg}", flush=True)) started = time.time() state = {"last_request": time.time()} grace = ( orphan_grace_s if orphan_grace_s is not None else float(os.environ.get(ORPHAN_GRACE_ENV, DEFAULT_ORPHAN_GRACE_S)) ) release = release_cublas and os.environ.get(NO_CUBLAS_RELEASE_ENV) != "1" def handle(req: Dict[str, Any]) -> Dict[str, Any]: op = req.get("op") if op == P.OP_PING: body = { "ok": True, "model": slug, "ready": True, "uptime_s": round(time.time() - started, 1), } if ready is not None: body.update(ready()) return body if op == P.OP_SHUTDOWN: return {"ok": True} if op == P.OP_PREDICT: state["last_request"] = time.time() if on_reset is not None: on_reset() result = None try: result = on_predict(req) finally: # After on_predict rather than inside it, which is the same # moment: the providers that used to do this themselves did # it on their last line. It does not disturb the peak they # report either -- clearing returns allocated bytes, and the # allocator goes on caching the blocks, so memory_reserved # does not move. # # In a `finally` because a request that RAISED can have got # far enough to capture a graph and abandon its stream, and a # worker answering a run of bad requests is exactly where # that would add up unnoticed. if release: freed = release_cublas_workspaces() if freed and isinstance(result, dict): result.setdefault( "cublas_workspaces_freed_mib", round(freed / 2**20, 1) ) return {"ok": True, "result": result} return {"ok": False, "error": f"unknown op {op!r}", "reason": "unknown_op"} def orphan_watchdog() -> None: while True: time.sleep(5) if os.getppid() != 1: continue idle = time.time() - state["last_request"] if idle < grace: continue say( f"orphaned (parent gone) and idle {idle:.0f}s >= {grace:.0f}s: " f"releasing the GPU and exiting" ) os._exit(0) # the loop is blocked in accept(); there is nothing to unwind if os.path.exists(socket_path): os.unlink(socket_path) srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) srv.bind(socket_path) os.chmod(socket_path, 0o600) # this uid only; the MCP server runs as the same user srv.listen(16) threading.Thread( target=orphan_watchdog, name="orphan-watchdog", daemon=True ).start() say(f"listening on {socket_path}") try: while True: conn, _ = srv.accept() buf = bytearray() try: while True: try: req = P.recv_line(conn, buf) except P.ProtocolError: break try: reply = handle(req) except Exception as exc: # noqa: BLE001 - never lose the model reply = { "ok": False, "error": f"{type(exc).__name__}: {exc}", "reason": "request_failed", "traceback": traceback.format_exc(limit=8), } except BaseException as exc: # noqa: BLE001 - see below # NOT an Exception, so the model is not staying up: # answer first, then let it go. :func:`_fatal_reply` # carries the reasoning for both halves of that. reply = _fatal_reply(exc) say(reply["error"]) try: P.send_line(conn, reply) except OSError: pass # the caller is gone; the exit is what matters raise state["last_request"] = time.time() P.send_line(conn, reply) if req.get("op") == P.OP_SHUTDOWN: say("shutdown requested") return 0 finally: conn.close() finally: srv.close() if os.path.exists(socket_path): try: os.unlink(socket_path) except OSError: pass
[docs] def bootstrap(worker_file: str): """Load this module from a provider's worker script. Every provider's worker needs the same three lines to reach here, so they live here instead:: import importlib.util, pathlib _wb = importlib.util.module_from_spec(...) # see the providers In practice a worker calls :func:`sys.path`-free path loading directly; this helper exists for the symmetric case where the base is found relative to the worker rather than through the environment. """ base = Path(worker_file).resolve().parents[3] / "remote_resident" / "worker_base.py" return _load_by_path("_tu_worker_base", base)
__all__ = [ "protocol", "serve", "bootstrap", "release_cublas_workspaces", "PROTOCOL_ENV", "ORPHAN_GRACE_ENV", "NO_CUBLAS_RELEASE_ENV", ]