Download code/offload/dspark/spark/remote_dspark/proxy_proc.py from ProprietaryLegal/dsv41-sampled-drafting: direct link, hf CLI and curl.
- Browser
- Download file 26 kB
-
https://huggingface.co/ProprietaryLegal/dsv41-sampled-drafting/resolve/main/code/offload/dspark/spark/remote_dspark/proxy_proc.py
- Command line
-
hf download hf://ProprietaryLegal/dsv41-sampled-drafting/code/offload/dspark/spark/remote_dspark/proxy_proc.py
-
curl -L -o proxy_proc.py https://huggingface.co/ProprietaryLegal/dsv41-sampled-drafting/resolve/main/code/offload/dspark/spark/remote_dspark/proxy_proc.py
26 kB
| # SPDX-License-Identifier: Apache-2.0 | |
| """Out-of-process async proxy (VLLM_DSPARK_REMOTE_ASYNC_PROXY=process). torch-free: the proxy process imports this | |
| package as a top-level package and never imports vllm or torch. | |
| Why a process (directive 2026-10-23 §3): the thread proxy's success path - wait for the gather, network round, | |
| publish - is Python, so it needs the engine's GIL; during prefill the engine thread holds the GIL for long stretches | |
| and the round's publication waits for it (GB10: +4-8 % TTFT, 31 watchdog-preempted rounds of 5,113 in the async | |
| compact set). Here the same reviewed round code (roundcore.RoundCore, unchanged) runs in a child interpreter with its | |
| own GIL; it shares nothing with the engine but the memory region (shmregion.py) and the C hand-off words (hostflag.c). | |
| Its success path is: acquire-wait on the descriptor and gathered words, the round, one GIL-free C publish into the | |
| shared page. No step needs the engine's GIL, a CUDA call or an engine-held lock. | |
| engine thread: gather -> D2H into the region -> descriptor t -> desc_head = t -> [stream: gathered = t, | |
| wait released >= t, H2D reply] -> return (never blocks) | |
| proxy process: wait desc_head >= t -> copy descriptor, desc_tail = t -> wait gathered >= t -> RoundCore round | |
| -> publish(t) (claim, copy, release: hostflag.c) | |
| engine watchdog (C thread in the engine process): a round gathered but unreleased past the deadline is claimed, | |
| failed and released, exactly as with the thread proxy. | |
| Ownership of round t's completion is the claim word (one winner: proxy publish, watchdog, or a fail-all). Failure of | |
| the proxy process is seen by the engine at the next round (Popen.poll): fail-all far ahead, then target-only rounds, | |
| the same end state as a dead proxy thread (S9 F2). The proxy watches its parent (getppid, and PR_SET_PDEATHSIG when | |
| started from the main thread) and exits when the engine is gone. | |
| """ | |
| from __future__ import annotations | |
| import ctypes | |
| import dataclasses | |
| import json | |
| import logging | |
| import os | |
| import signal | |
| import subprocess | |
| import sys | |
| import time | |
| import numpy as np | |
| from . import wire | |
| from .breaker import CircuitBreaker | |
| from .client import RemoteDraftClient | |
| from .config import RemoteConfig | |
| from .hostflag import _M32, _V, HostFlagCore, set_timer_slack | |
| from .roundcore import STATUS_ROUND_FAILED, BatchView, ReplyLayout, RoundCore, collect_meta | |
| from .shmregion import (C_DESC_HEAD, C_DESC_TAIL, C_DONE, C_ENGINE_SEQ, C_FLUSH_ACK, C_FLUSH_REQ, C_HEARTBEAT, C_PID, | |
| C_START, C_STATE, C_STOP, C_WD, F_ENQUEUE_FAILED, R_FAILED, R_OK, R_PENDING, S_FAILED, | |
| S_RUNNING, S_STOPPED, RegionLayout, SharedRegion) | |
| from .state import SlotTable | |
| from .stats import Stats | |
| from .transport import Endpoint | |
| log = logging.getLogger("vllm.remote_dspark") | |
| PR_SET_PDEATHSIG = 1 | |
| DESC_SPIN_NS = 200_000 # descriptor wait: spin 200 us, then 100 us naps (the gather leaves that much slack) | |
| DESC_NAP_NS = 100_000 | |
| POLL_S = 0.1 # every wait wakes this often to check stop / parent / flush requests | |
| START_TIMEOUT_S = 60.0 # interpreter start + imports (+ connect and HELLO when strict) | |
| # ---------------------------------------------------------------------------------------------------- init blob | |
| def _enc(v): | |
| if isinstance(v, bytes): | |
| return {"__hex__": v.hex()} | |
| if isinstance(v, dict): | |
| return {k: _enc(x) for k, x in v.items()} | |
| if isinstance(v, (list, tuple)): | |
| return [_enc(x) for x in v] | |
| return v | |
| def _dec(v): | |
| if isinstance(v, dict): | |
| if set(v) == {"__hex__"}: | |
| return bytes.fromhex(v["__hex__"]) | |
| return {k: _dec(x) for k, x in v.items()} | |
| if isinstance(v, list): | |
| return [_dec(x) for x in v] | |
| return v | |
| def encode_init(cfg: RemoteConfig, hello: wire.Hello, layout: RegionLayout, *, n_out_by_reqs, lib_path: str, | |
| spin_us: int, nap_us: int, watchdog_ms: int, strict: bool, parent_pid: int, pdeathsig: bool, | |
| start_round: int = 1, session: int | None = None, base_counters: dict | None = None, | |
| generation: int = 0) -> bytes: | |
| """start_round: the first round this proxy serves (a respawned proxy continues the engine's numbering); | |
| session: its worker session id; base_counters: its predecessors' counters, carried on in its stats file; | |
| generation: 0 for the first proxy, k for the k-th respawn.""" | |
| return json.dumps(_enc({ | |
| "cfg": dataclasses.asdict(cfg), "hello": dataclasses.asdict(hello), "layout": layout.to_dict(), | |
| "n_out_by_reqs": list(n_out_by_reqs) if n_out_by_reqs else None, "lib_path": lib_path, | |
| "spin_us": int(spin_us), "nap_us": int(nap_us), "watchdog_ms": int(watchdog_ms), "strict": bool(strict), | |
| "parent_pid": int(parent_pid), "pdeathsig": bool(pdeathsig), "start_round": int(start_round), | |
| "session": session, "base_counters": dict(base_counters or {}), "generation": int(generation)})).encode() | |
| def decode_init(data: bytes) -> dict: | |
| d = _dec(json.loads(data.decode())) | |
| c = d["cfg"] | |
| c["endpoint"] = Endpoint(**c["endpoint"]) | |
| d["cfg"] = RemoteConfig(**c) | |
| h = d["hello"] | |
| h["tap_layer_ids"] = tuple(h["tap_layer_ids"]) | |
| d["hello"] = wire.Hello(**h) | |
| d["layout"] = RegionLayout(**d["layout"]) | |
| return d | |
| ENGINE_OWNED = ("async_dead",) # the engine decides whether a proxy serves (a dying proxy also records its reason) | |
| def merge_snapshots(proxy: dict, engine: dict | None) -> dict: | |
| """One stats file from the proxy's snapshot and the engine side's (counters add up; histograms and extras are | |
| disjoint by construction, the proxy's win on a clash except ENGINE_OWNED).""" | |
| if not engine: | |
| return proxy | |
| out = {k: v for k, v in engine.items() if k not in ("counters", "latency", "uptime_s")} | |
| out.update({k: v for k, v in proxy.items() if k not in ("counters", "latency")}) | |
| for k in ENGINE_OWNED: | |
| if k in engine: | |
| out[k] = engine[k] | |
| cnt = dict(engine.get("counters") or {}) | |
| for k, v in (proxy.get("counters") or {}).items(): | |
| cnt[k] = cnt.get(k, 0) + v | |
| out["counters"] = dict(sorted(cnt.items())) | |
| lat = dict(engine.get("latency") or {}) | |
| lat.update(proxy.get("latency") or {}) | |
| out["latency"] = dict(sorted(lat.items())) | |
| # what the engine side added, so that the engine can rewrite the file without counting it twice | |
| out["engine_share"] = {"counters": dict(engine.get("counters") or {}), | |
| "latency": sorted((engine.get("latency") or {}).keys())} | |
| return out | |
| def unmerge_snapshot(merged: dict) -> dict: | |
| """The proxy's part of a file merge_snapshots() wrote (the inverse of the engine share).""" | |
| share = merged.get("engine_share") or {} | |
| out = {k: v for k, v in merged.items() if k != "engine_share"} | |
| eng_c = share.get("counters") or {} | |
| out["counters"] = {k: v - eng_c.get(k, 0) for k, v in (merged.get("counters") or {}).items() | |
| if v - eng_c.get(k, 0)} | |
| eng_l = set(share.get("latency") or ()) | |
| out["latency"] = {k: v for k, v in (merged.get("latency") or {}).items() if k not in eng_l} | |
| return out | |
| def write_json_atomic(path: str, obj: dict) -> None: | |
| tmp = f"{path}.tmp.{os.getpid()}" | |
| with open(tmp, "w") as f: | |
| json.dump(obj, f, indent=1, default=str) | |
| os.replace(tmp, path) | |
| # ---------------------------------------------------------------------------------------------------- proxy side | |
| class _ProxyFlags(HostFlagCore): | |
| """The proxy process's view of the engine's hand-off page: wait, publish, fail-all. The watchdog runs in the | |
| engine process; its stats are mirrored into the control block every round.""" | |
| def __init__(self, region: SharedRegion, lib_path: str, spin_us: int, nap_us: int, reply_bytes: int, | |
| watchdog_ms: int): | |
| super().__init__(lib_path, spin_us, nap_us, page=region.page_addr) | |
| self.configure_reply(reply_bytes, STATUS_ROUND_FAILED) | |
| self.region = region | |
| self.deadline_ms = int(watchdog_ms) | |
| def attach(self, *a, **k) -> None: | |
| raise RuntimeError("the watchdog belongs to the engine process") | |
| def watchdog_stats(self) -> dict: | |
| fires, last, late = (int(x) for x in self.region.u64(C_WD, 3)) | |
| return {"running": True, "deadline_ms": self.deadline_ms, "fires": fires, "last_failed_round": last, | |
| "max_late_ms": round(late / 1e6, 3), "mirrored": True} | |
| def describe(self) -> dict: | |
| d = super().describe() | |
| d["mechanism"] = "host_flags_shared" | |
| return d | |
| class _RegionRows: | |
| """What RoundCore reads of the gatherer, from the region: the STEP message buffer and the gather metadata.""" | |
| def __init__(self, region: SharedRegion): | |
| L = region.layout | |
| self.region = region | |
| self.msg_np = region.msg_np | |
| self.max_rows = L.max_rows | |
| self.row_bytes = L.row_bytes | |
| def collect(self, n: int, n_candidates: int, meta_slot: int = 0): | |
| return collect_meta(self.region.meta_np[meta_slot], n, n_candidates) | |
| def gather_from_ring(self, n_req, spans): | |
| raise RuntimeError("synchronous resync in the process proxy") | |
| def monotonic_ms() -> int: | |
| """CLOCK_MONOTONIC in ms as a u32 (the heartbeat's clock; system-wide, so the engine compares it with its own).""" | |
| return int(time.monotonic() * 1000) & _M32 | |
| class _Housekeeping: | |
| """RoundCore._stop for the proxy process: polled at least every POLL_S while it waits and once per round. Writes | |
| the heartbeat (C_HEARTBEAT, S11 P-3), serves stats flush requests; reports stop when the engine asked for it or | |
| the engine process is gone.""" | |
| def __init__(self, rnd: "ProcessRound"): | |
| self.r = rnd | |
| self.lib = rnd.sync.lib | |
| self._stop_w = _V(rnd.region.word(C_STOP)) | |
| self._flush_w = _V(rnd.region.word(C_FLUSH_REQ)) | |
| self._hb_w = _V(rnd.region.word(C_HEARTBEAT)) | |
| self._flushed = 0 | |
| def beat(self) -> None: | |
| self.lib.dsv41_hf_set(self._hb_w, monotonic_ms()) | |
| def is_set(self) -> bool: | |
| self.beat() | |
| req = int(self.lib.dsv41_hf_load(self._flush_w)) | |
| if req != self._flushed: | |
| self._flushed = req | |
| try: | |
| self.r.write_stats() | |
| finally: | |
| self.lib.dsv41_hf_store(_V(self.r.region.word(C_FLUSH_ACK)), req) | |
| if self.lib.dsv41_hf_load(self._stop_w): | |
| return True | |
| return os.getppid() != self.r.parent_pid | |
| class ProcessRound(RoundCore): | |
| """The proxy process's round: RoundCore fed from the descriptor ring instead of the thread proxy's queue.""" | |
| def __init__(self, region: SharedRegion, cfg: RemoteConfig, hello: wire.Hello, *, n_out_by_reqs, lib_path: str, | |
| spin_us: int, nap_us: int, watchdog_ms: int, parent_pid: int, background: bool, | |
| transport_factory=None, start_round: int = 1, session: int | None = None, | |
| base_counters: dict | None = None, generation: int = 0): | |
| self.cfg = cfg | |
| self.hello = hello | |
| self.region = region | |
| self.parent_pid = parent_pid | |
| self.start_round = int(start_round) | |
| self.stats = Stats() | |
| for k, v in (base_counters or {}).items(): # a respawned proxy carries its predecessors' counts | |
| self.stats.inc(k, int(v)) | |
| self.N, self.W, self.M = hello.num_spec, hello.window, hello.max_slots | |
| self.layout = ReplyLayout(self.M, self.N, cfg.sampled_top32) | |
| self.bcast_np = np.zeros(self.layout.size, dtype=np.int32) | |
| self.n_out_by_reqs = list(n_out_by_reqs) if n_out_by_reqs else None | |
| self.rounds = self.start_round - 1 | |
| self._last_epoch = 0 | |
| self._last_log = time.monotonic() | |
| self._err_logged = 0.0 | |
| self.is_sender = True | |
| self.async_mode = True | |
| L = region.layout | |
| if (L.max_slots, L.window, L.row_bytes, L.max_rows) != (self.M, self.W, hello.row_bytes, | |
| hello.max_rows_per_step): | |
| raise ValueError(f"region layout {L} does not match the HELLO") | |
| self.gatherer = _RegionRows(region) | |
| self._meta_slots = L.ring | |
| self.resync_np = region.resync_np | |
| self.slots = SlotTable(self.M, self.W) | |
| self.breaker = CircuitBreaker(cfg.trip, cfg.cooldown) | |
| self.stats.set("breaker", self.breaker.snapshot()) | |
| self.sync = _ProxyFlags(region, lib_path, spin_us, nap_us, self.layout.size * 4, watchdog_ms) | |
| self.lib = self.sync.lib | |
| self._staged: frozenset[int] = frozenset() | |
| self._dead: str | None = None | |
| self.client = RemoteDraftClient(cfg, hello, stats=self.stats, transport_factory=transport_factory, | |
| background=background, session=session) | |
| self.stats.set("contract_version", wire.CONTRACT_VERSION) | |
| self.stats.set("policy", cfg.policy) | |
| self.stats.set("endpoint", str(cfg.endpoint)) | |
| self.stats.set("n_out_by_reqs", self.n_out_by_reqs) | |
| # the engine's "async_proxy" (region, mlock, current pid) stays authoritative in the merged stats file | |
| self.stats.set("async_proxy_process", {"pid": os.getpid(), "generation": int(generation), | |
| "start_round": self.start_round}) | |
| self._stop = _Housekeeping(self) | |
| self._engine_seq = 0 | |
| self._engine_snap: dict | None = None | |
| # resync requests: one byte per slot in the control block (engine reads them at submit) | |
| def _request_resync(self, slots: set[int]) -> None: | |
| f = self.region.resync_flags | |
| for sl in slots: | |
| f[sl] = 1 | |
| def _clear_resync(self, staged: frozenset[int]) -> None: | |
| f = self.region.resync_flags | |
| for sl in staged: | |
| f[sl] = 0 | |
| def _wait_descriptor(self, t: int) -> bool: | |
| head = _V(self.region.word(C_DESC_HEAD)) | |
| while not self._stop.is_set(): | |
| if self.lib.dsv41_hf_wait_geq(head, t & _M32, int(POLL_S * 1e9), DESC_SPIN_NS, DESC_NAP_NS) == 0: | |
| return True | |
| return False | |
| def _take(self, t: int): | |
| d = self.region.read_descriptor(t) | |
| if d.flags & F_ENQUEUE_FAILED: | |
| return (t, None, 0, frozenset()) | |
| bv = BatchView(req_ids=d.owners, slots=d.slots, qsl=np.zeros(0, dtype=np.int64), | |
| need_draft=d.need_draft, k_window=d.k_window, max_q=d.max_q) | |
| return (t, bv, d.R, d.staged) | |
| def serve(self) -> None: | |
| """Rounds start_round, start_round + 1, ... until stopped. Raises on a broken hand-off (the caller fails | |
| everything closed).""" | |
| t = self.start_round | |
| tail = _V(self.region.word(C_DESC_TAIL)) | |
| done = _V(self.region.word(C_DONE)) | |
| while True: | |
| if not self._wait_descriptor(t): | |
| return | |
| item = self._take(t) | |
| self.rounds = t | |
| if not self._async_round(item): | |
| return | |
| # slot t % K (descriptor, metadata) may be reused only now (shmregion.py) | |
| self.lib.dsv41_hf_store(tail, t & _M32) | |
| self.lib.dsv41_hf_store(done, t & _M32) | |
| t += 1 | |
| def _engine_snapshot(self) -> dict | None: | |
| seq = int(self.lib.dsv41_hf_load(_V(self.region.word(C_ENGINE_SEQ)))) | |
| if seq and seq != self._engine_seq: | |
| try: | |
| self._engine_snap = json.loads(self.region.read_engine_blob(seq).decode()) | |
| self._engine_seq = seq | |
| except (ValueError, UnicodeDecodeError): | |
| pass # overwritten while read (stats only): the previous snapshot stands | |
| return self._engine_snap | |
| def write_stats(self) -> None: | |
| if not self.cfg.stats_file: | |
| return | |
| self.stats.set("rounds", self.rounds) | |
| self.stats.set("breaker", self.breaker.snapshot()) | |
| self.stats.set("async_watchdog", self.sync.watchdog_stats()) | |
| self.stats.set("proxy_torch_loaded", "torch" in sys.modules) # evidence: the proxy never imports it | |
| try: | |
| write_json_atomic(self.cfg.stats_file, merge_snapshots(self.stats.snapshot(), self._engine_snapshot())) | |
| except OSError as e: | |
| log.warning("remote DSpark stats file %s: %s", self.cfg.stats_file, e) | |
| def _pdeathsig() -> None: | |
| try: | |
| libc = ctypes.CDLL(None, use_errno=True) | |
| libc.prctl(PR_SET_PDEATHSIG, signal.SIGKILL, 0, 0, 0) | |
| except (OSError, AttributeError): | |
| pass | |
| def _store(region: SharedRegion, lib, off: int, value: int) -> None: | |
| lib.dsv41_hf_set(_V(region.word(off)), value & _M32) | |
| def main(fd: int, init: bytes | None = None, transport_factory=None) -> int: | |
| """Proxy process entry (BOOT below): the init blob comes on stdin.""" | |
| d = decode_init(init if init is not None else sys.stdin.buffer.read()) | |
| if d["pdeathsig"]: | |
| _pdeathsig() | |
| set_timer_slack() # short naps (ASYNC_NAP_US > 0) wake on time | |
| if os.getppid() != d["parent_pid"]: | |
| return 5 # the engine is already gone | |
| logging.basicConfig(level=logging.INFO, stream=sys.stderr, | |
| format="(remote-dspark proxy pid=%(process)d) %(levelname)s %(asctime)s %(message)s") | |
| region = SharedRegion.attach(d["layout"], fd) | |
| region.u32(C_PID)[0] = os.getpid() | |
| cfg: RemoteConfig = d["cfg"] | |
| rnd = None | |
| lib = None | |
| try: | |
| rnd = ProcessRound(region, cfg, d["hello"], n_out_by_reqs=d["n_out_by_reqs"], lib_path=d["lib_path"], | |
| spin_us=d["spin_us"], nap_us=d["nap_us"], watchdog_ms=d["watchdog_ms"], | |
| parent_pid=d["parent_pid"], background=not d["strict"], | |
| transport_factory=transport_factory, start_round=d.get("start_round", 1), | |
| session=d.get("session"), base_counters=d.get("base_counters"), | |
| generation=d.get("generation", 0)) | |
| lib = rnd.lib | |
| if d["strict"]: | |
| if not rnd.client.connect_now(): | |
| raise RuntimeError(f"connect/HELLO to {cfg.endpoint} failed at startup " | |
| f"({rnd.client.disabled_reason or rnd.client.last_error})") | |
| rnd.client.start_background() | |
| except BaseException as e: # noqa: BLE001 -- the engine reads the reason and raises | |
| region.set_start_reason(f"{type(e).__name__}: {e}") | |
| if lib is not None: | |
| _store(region, lib, C_START, R_FAILED) | |
| _store(region, lib, C_STATE, S_FAILED) | |
| else: | |
| region.u32(C_START)[0] = R_FAILED | |
| region.u32(C_STATE)[0] = S_FAILED | |
| if rnd is not None: | |
| rnd.client.close() | |
| return 2 | |
| rnd._stop.beat() | |
| _store(region, lib, C_START, R_OK) | |
| _store(region, lib, C_STATE, S_RUNNING) | |
| log.info("remote DSpark proxy process running (engine pid %d, %s)", d["parent_pid"], rnd.sync.describe()) | |
| code = 0 | |
| try: | |
| rnd.serve() | |
| except BaseException as e: # noqa: BLE001 -- whatever ends the proxy must not park the GPU | |
| rnd._die(f"proxy process: {type(e).__name__}: {e}") | |
| code = 4 | |
| finally: | |
| try: | |
| rnd.write_stats() | |
| finally: | |
| rnd.client.close() | |
| _store(region, lib, C_STATE, S_FAILED if code else S_STOPPED) | |
| return code | |
| # ---------------------------------------------------------------------------------------------------- engine side | |
| class ProxyStartError(RuntimeError): | |
| """The proxy process did not start (spawn, interpreter, imports, start timeout; strict: connect + HELLO). The engine | |
| then runs synchronous rounds unless the start is strict (round.make_remote_round, S11 P-2).""" | |
| # the proxy's interpreter imports this package from its directory as the top-level package "remote_dspark" (never | |
| # through vllm: no torch, no CUDA in the proxy) | |
| BOOT = """import importlib.util, sys | |
| pkg, fd = sys.argv[1], int(sys.argv[2]) | |
| spec = importlib.util.spec_from_file_location("remote_dspark", pkg + "/__init__.py", submodule_search_locations=[pkg]) | |
| mod = importlib.util.module_from_spec(spec) | |
| sys.modules["remote_dspark"] = mod | |
| spec.loader.exec_module(mod) | |
| from remote_dspark.proxy_proc import main | |
| sys.exit(main(fd)) | |
| """ | |
| class ProxyProcess: | |
| """Engine side: start the proxy process on a region, watch it, stop it. CPU only.""" | |
| def __init__(self, region: SharedRegion, lib, init: bytes, *, python: str | None = None): | |
| self.region = region | |
| self.lib = lib | |
| pkg = os.path.dirname(os.path.abspath(__file__)) | |
| # -I: isolated (no PYTHONPATH / user site / working directory on sys.path): the proxy imports only this | |
| # package (by path) and the interpreter's own site packages | |
| self.proc = subprocess.Popen([python or sys.executable, "-I", "-c", BOOT, pkg, str(region.fd)], | |
| stdin=subprocess.PIPE, pass_fds=(region.fd,), close_fds=True) | |
| try: | |
| self.proc.stdin.write(init) | |
| self.proc.stdin.close() | |
| except OSError: | |
| pass # died at once: wait_started() reports it | |
| self._flush = 0 | |
| self._engine_seq = 0 | |
| self._blob_warned = False | |
| def pid(self) -> int: | |
| return self.proc.pid | |
| def word(self, off: int) -> int: | |
| return int(self.lib.dsv41_hf_load(_V(self.region.word(off)))) | |
| def start_status(self) -> tuple[str, str]: | |
| """Without waiting: ("ok", ""), ("pending", "") or ("failed", why) (it reported a failure or exited).""" | |
| r = self.word(C_START) | |
| if r == R_OK: | |
| return "ok", "" | |
| if r == R_FAILED: | |
| return "failed", f"remote DSpark proxy process failed to start: {self.region.start_reason()}" | |
| rc = self.proc.poll() | |
| if rc is not None: | |
| return "failed", f"remote DSpark proxy process exited with {rc} before it started" | |
| return "pending", "" | |
| def wait_started(self, timeout_s: float = START_TIMEOUT_S) -> None: | |
| """Until the proxy reports its start (client built; when strict: connected + HELLO). Raises ProxyStartError | |
| when it failed, exited or did not report in time; the process is then gone or killed.""" | |
| t_end = time.monotonic() + timeout_s | |
| while True: | |
| state, why = self.start_status() | |
| if state == "ok": | |
| return | |
| if state == "failed": | |
| self.kill(2.0) | |
| raise ProxyStartError(why) | |
| if time.monotonic() > t_end: | |
| self.kill() | |
| raise ProxyStartError(f"remote DSpark proxy process did not start within {timeout_s:.0f} s") | |
| time.sleep(0.005) | |
| def exit_code(self) -> int | None: | |
| return self.proc.poll() | |
| def tail(self) -> int: | |
| return self.word(C_DESC_TAIL) | |
| def heartbeat_age_ms(self) -> int: | |
| """Milliseconds since the proxy's last housekeeping poll (0 if its clock read is a hair ahead of ours).""" | |
| d = (monotonic_ms() - self.word(C_HEARTBEAT)) & _M32 | |
| return d if d < (1 << 31) else 0 | |
| def publish_head(self, t: int) -> None: | |
| self.lib.dsv41_hf_store(_V(self.region.word(C_DESC_HEAD)), t & _M32) | |
| def resync_requests(self) -> list[int]: | |
| return [int(i) for i in np.flatnonzero(self.region.resync_flags)] | |
| def publish_engine_stats(self, snapshot: dict) -> None: | |
| data = json.dumps(snapshot, default=str).encode() | |
| seq = self._engine_seq + 1 | |
| if self.region.write_engine_blob(seq, data): | |
| self._engine_seq = seq | |
| self.lib.dsv41_hf_store(_V(self.region.word(C_ENGINE_SEQ)), seq & _M32) | |
| elif not self._blob_warned: | |
| self._blob_warned = True | |
| log.warning("remote DSpark engine-side stats (%d B) do not fit the shared region's blob; the stats file " | |
| "keeps the last engine snapshot that did", len(data)) | |
| def flush(self, timeout_s: float = 2.0) -> bool: | |
| """Ask the proxy to write the stats file now; True once it did.""" | |
| if self.proc.poll() is not None: | |
| return False | |
| self._flush += 1 | |
| self.lib.dsv41_hf_store(_V(self.region.word(C_FLUSH_REQ)), self._flush & _M32) | |
| r = self.lib.dsv41_hf_wait_geq(_V(self.region.word(C_FLUSH_ACK)), self._flush & _M32, int(timeout_s * 1e9), | |
| 200_000, 1_000_000) | |
| return r == 0 | |
| def stop(self, timeout_s: float = 5.0) -> int | None: | |
| self.lib.dsv41_hf_store(_V(self.region.word(C_STOP)), 1) | |
| try: | |
| return self.proc.wait(timeout=timeout_s) | |
| except subprocess.TimeoutExpired: | |
| self.kill() | |
| return None | |
| def signal_kill(self) -> None: | |
| """SIGKILL, without waiting: the engine thread must not block on a child that may sit in uninterruptible | |
| sleep (S11 P-6). exit_code() reaps it later. A killed process runs no more user code, so it can no longer | |
| write the region (it may still be finishing a system call).""" | |
| if self.proc.poll() is None: | |
| self.proc.kill() | |
| def kill(self, wait_s: float = 5.0) -> None: | |
| """SIGKILL and wait for the exit at most wait_s (startup and shutdown only; the round path uses signal_kill).""" | |
| self.signal_kill() | |
| if self.proc.returncode is None: | |
| try: | |
| self.proc.wait(timeout=wait_s) | |
| except subprocess.TimeoutExpired: | |
| log.error("remote DSpark proxy process %d did not exit within %.0f s of SIGKILL", self.pid, wait_s) | |
| __all__ = ["ProcessRound", "ProxyProcess", "ProxyStartError", "encode_init", "decode_init", "merge_snapshots", "main", | |
| "R_PENDING"] | |