# SPDX-License-Identifier: Apache-2.0 """Environment configuration of the remote DSpark proxy (Spark side). The overlay is inactive unless VLLM_DSPARK_REMOTE is set (then the image's own code path runs byte-identically). Every value is validated at startup; a bad value raises. VLLM_DSPARK_REMOTE endpoint for rank 0: o1rdma://10.202.28.1:18620 (lane O1's ofl, RDMA), o1tcp://... (ofl's TCP backend) or tcp://... (the contract's TCP stub) VLLM_DSPARK_REMOTE_BIND this side's direct-link IPv4 (10.202.28.2); same as ?bind= in the endpoint, without a '?' that a launcher shell could glob VLLM_DSPARK_REMOTE_OFL_DIR directory holding libofl.so + ofl.py (o1tcp/o1rdma endpoints) VLLM_DSPARK_REMOTE_POLICY shadow (default) | resilience | capacity VLLM_DSPARK_REMOTE_IDENTITY 64-hex drafter identity (CONTRACT 6.7); required VLLM_DSPARK_REMOTE_MARKOV_IDENTITY 64-hex; default: sha256 of the VLLM_DSPARK_MARKOV_CACHE file, zeros if that variable is unset VLLM_DSPARK_REMOTE_TIMEOUT_MS per-round deadline (default 50) VLLM_DSPARK_REMOTE_CONNECT_TIMEOUT_MS connect + HELLO deadline (default 1000) VLLM_DSPARK_REMOTE_BACKOFF_MS first reconnect delay after a failure (default 1000, doubles up to VLLM_DSPARK_REMOTE_BACKOFF_MAX_MS, default 30000) VLLM_DSPARK_REMOTE_ON_FAIL target_only (default) | raise (capacity policy) VLLM_DSPARK_REMOTE_TRIP circuit breaker (CONTRACT 10, breaker.py): consecutive failed attempts before the remote is skipped (default 3; 0 = off) VLLM_DSPARK_REMOTE_COOLDOWN rounds without network after a trip, then one probe (default 256) VLLM_DSPARK_REMOTE_STRICT_START 1: raise at startup if connect + HELLO fails (default 0) VLLM_DSPARK_REMOTE_SHADOW_USE local (default) | remote (shadow policy) VLLM_DSPARK_REMOTE_WANT_MARGIN 1: ask the worker for top1-top2 margins (default 1 in shadow) VLLM_DSPARK_REMOTE_DRAFT_MS remote draft round cost for the depth policy (ms, finite >= 0); replaces VLLM_DSPARK_DRAFT_MS in capacity/resilience VLLM_DSPARK_REMOTE_ROUND_MS verify-only forward table for remote mode: ms for depth 0..k (k + 1 finite values > 0, non-decreasing); replaces VLLM_DSPARK_ROUND_MS (whose live value includes the local drafter) in capacity/resilience. With VLLM_DSPARK_DEPTH_POLICY on, capacity/resilience need BOTH remote costs (no mixed cost model) and refuse "confr:" (its own table includes the local drafter). Shadow keeps the local measurements. VLLM_DSPARK_REMOTE_ASYNC 1: capacity policy only - rank 0 does not block its CPU on the round; the engine stream and the round's proxy thread hand each round over through two words in registered host memory (streamsync.HostFlagSync); the proxy thread makes no CUDA call. Self-tested at startup; falls back to the synchronous round with a warning. Default 0. VLLM_DSPARK_REMOTE_ASYNC_PROXY async mode: where the round's proxy runs. thread (default): a Python thread of the engine process (needs the engine's GIL to publish). process: a torch-free child process with its own interpreter (proxy_proc.py); the gather staging, the descriptor ring and the hand-off words share one registered memfd region (shmregion.py), so the success path never waits for the engine's GIL; the switch interval is not changed VLLM_DSPARK_REMOTE_HOSTFLAG_LIB async mode: path of libdsv41_hostflag.so (built from remote_dspark/hostflag.c). Default: VLLM_DSPARK_REMOTE_OFL_DIR's copy, else hostflag.c compiled with cc at startup. VLLM_DSPARK_REMOTE_ASYNC_NAP_US async mode: the proxy thread spins on the gathered word; > 0 = sleep this many us between checks after VLLM_DSPARK_REMOTE_ASYNC_SPIN_US of spinning (default 0: spin; one rank-0 core while a round is pending) VLLM_DSPARK_REMOTE_ASYNC_SPIN_US see ASYNC_NAP_US (default 1000) VLLM_DSPARK_REMOTE_ASYNC_WATCHDOG_MS async mode: a native watchdog fails and releases a round the engine stream gathered but nobody released within this many ms (zero drafts for that round), so a proxy thread that cannot run never parks the GPU for good. Default 0 = auto: max(4 x VLLM_DSPARK_REMOTE_TIMEOUT_MS, 200) VLLM_DSPARK_REMOTE_ASYNC_RESPAWNS async process mode: new proxy processes the engine may start after its proxy died or stalled (default 3; 0 = none: target-only rounds for the rest of the run). Between the loss and the new proxy's start every round is target-only (zero drafts); the new proxy starts with a new session, so the worker drops its windows and every slot is re-announced (NEW_GEN). Spent: target-only rounds, one error log line VLLM_DSPARK_REMOTE_ASYNC_RESPAWN_RESET_S a proxy process that served this long restores the full respawn budget (default 3600; 0 = never) VLLM_DSPARK_REMOTE_SWITCH_INTERVAL_US async thread mode: Python GIL switch interval while the proxy thread runs (default 500; the interpreter default is 5000; 0 = unchanged); ignored with ASYNC_PROXY=process VLLM_DSPARK_REMOTE_FUSED_GATHER auto (default: on CUDA, after a startup self-check against the torch path) | 0 | 1: the one-launch Triton row gather (fused_gather.py) VLLM_DSPARK_REMOTE_N_OUT auto (default) | 0: ReqEntry.n_out (CONTRACT 6.3). auto = cap[n] of the dynamic speculation schedule when the depth policy masks positions >= cap (conf/confb/confr, no VLLM_DSPARK_DEPTH_FILE) in capacity/resilience; otherwise, and in shadow, always all N positions VLLM_DSPARK_REMOTE_LOG_EVERY rounds between stats log lines (default 1024, 0 = off) VLLM_DSPARK_REMOTE_STATS_FILE JSON stats written by rank 0 (default /tmp/dsv41-remote-dspark.json, inside the container) """ from __future__ import annotations import hashlib import math import os from dataclasses import dataclass from . import wire from .transport import Endpoint, parse_endpoint POLICIES = ("shadow", "resilience", "capacity") ENV_ENDPOINT = "VLLM_DSPARK_REMOTE" def remote_requested(environ=None) -> bool: env = os.environ if environ is None else environ return bool((env.get(ENV_ENDPOINT) or "").strip()) def _int(env, key: str, default: int, lo: int = 0) -> int: raw = env.get(key) if raw is None or raw.strip() == "": return default try: v = int(raw) except ValueError as e: raise ValueError(f"{key}={raw!r}: not an integer") from e if v < lo: raise ValueError(f"{key}={v}: must be >= {lo}") return v def _choice(env, key: str, default: str, choices: tuple[str, ...]) -> str: v = (env.get(key) or default).strip().lower() if v not in choices: raise ValueError(f"{key}={v!r}: expected one of {choices}") return v def _bool(env, key: str, default: bool) -> bool: raw = env.get(key) if raw is None or raw.strip() == "": return default v = raw.strip().lower() if v in ("1", "true", "on", "yes"): return True if v in ("0", "false", "off", "no"): return False raise ValueError(f"{key}={raw!r}: expected 0 or 1") def _float_list(env, key: str) -> list[float] | None: raw = env.get(key) if raw is None or raw.strip() == "": return None try: vals = [float(x) for x in raw.split(",") if x.strip()] except ValueError as e: raise ValueError(f"{key}={raw!r}: expected comma-separated numbers") from e if not vals or not all(math.isfinite(v) for v in vals): raise ValueError(f"{key}={raw!r}: expected finite numbers") if any(v < 0 for v in vals): raise ValueError(f"{key}={raw!r}: expected non-negative numbers") return vals @dataclass class RemoteConfig: endpoint: Endpoint policy: str drafter_identity: bytes markov_identity: bytes timeout_ms: int connect_timeout_ms: int backoff_ms: int backoff_max_ms: int on_fail: str strict_start: bool shadow_use: str want_margin: bool remote_draft_ms: float | None remote_round_ms: list[float] | None log_every: int stats_file: str async_mode: bool = False switch_interval_us: int = 500 hostflag_lib: str = "" async_nap_us: int = 0 async_spin_us: int = 1000 async_watchdog_ms: int = 0 async_proxy: str = "thread" async_respawns: int = 3 async_respawn_reset_s: int = 3600 fused_gather: bool | None = None n_out: str = "auto" trip: int = 3 cooldown: int = 256 sampled_top32: bool = False @property def watchdog_ms(self) -> int: """The async watchdog deadline actually used.""" return self.async_watchdog_ms or max(4 * self.timeout_ms, 200) @property def keeps_local_drafter(self) -> bool: return self.policy in ("shadow", "resilience") @classmethod def from_env(cls, environ=None) -> "RemoteConfig": env = os.environ if environ is None else environ if not remote_requested(env): raise ValueError(f"{ENV_ENDPOINT} is not set") endpoint = parse_endpoint(env[ENV_ENDPOINT]) bind = (env.get("VLLM_DSPARK_REMOTE_BIND") or "").strip() if bind: if endpoint.options.get("bind", bind) != bind: raise ValueError(f"VLLM_DSPARK_REMOTE_BIND={bind} conflicts with the endpoint's " f"?bind={endpoint.options['bind']}") endpoint.options["bind"] = bind policy = _choice(env, "VLLM_DSPARK_REMOTE_POLICY", "shadow", POLICIES) ident_raw = env.get("VLLM_DSPARK_REMOTE_IDENTITY", "") if not ident_raw.strip(): raise ValueError( "VLLM_DSPARK_REMOTE_IDENTITY is required: the 64-hex drafter identity " "(offload/dspark/spark/tools/identity.py over lane S2's DSPARK_COPY_DONE.json)") drafter_identity = wire.parse_hex32(ident_raw, "VLLM_DSPARK_REMOTE_IDENTITY") markov_raw = env.get("VLLM_DSPARK_REMOTE_MARKOV_IDENTITY", "") if markov_raw.strip(): markov_identity = wire.parse_hex32(markov_raw, "VLLM_DSPARK_REMOTE_MARKOV_IDENTITY") else: markov_identity = markov_identity_from_env(env) draft_ms_list = _float_list(env, "VLLM_DSPARK_REMOTE_DRAFT_MS") if draft_ms_list is not None and len(draft_ms_list) != 1: raise ValueError("VLLM_DSPARK_REMOTE_DRAFT_MS: one number (ms)") cfg = cls( endpoint=endpoint, policy=policy, drafter_identity=drafter_identity, markov_identity=markov_identity, timeout_ms=_int(env, "VLLM_DSPARK_REMOTE_TIMEOUT_MS", 50, lo=1), connect_timeout_ms=_int(env, "VLLM_DSPARK_REMOTE_CONNECT_TIMEOUT_MS", 1000, lo=1), backoff_ms=_int(env, "VLLM_DSPARK_REMOTE_BACKOFF_MS", 1000, lo=1), backoff_max_ms=_int(env, "VLLM_DSPARK_REMOTE_BACKOFF_MAX_MS", 30000, lo=1), on_fail=_choice(env, "VLLM_DSPARK_REMOTE_ON_FAIL", "target_only", ("target_only", "raise")), strict_start=_bool(env, "VLLM_DSPARK_REMOTE_STRICT_START", False), shadow_use=_choice(env, "VLLM_DSPARK_REMOTE_SHADOW_USE", "local", ("local", "remote")), want_margin=_bool(env, "VLLM_DSPARK_REMOTE_WANT_MARGIN", policy == "shadow"), remote_draft_ms=None if draft_ms_list is None else draft_ms_list[0], remote_round_ms=_float_list(env, "VLLM_DSPARK_REMOTE_ROUND_MS"), log_every=_int(env, "VLLM_DSPARK_REMOTE_LOG_EVERY", 1024, lo=0), stats_file=(env.get("VLLM_DSPARK_REMOTE_STATS_FILE") or "/tmp/dsv41-remote-dspark.json"), async_mode=_bool(env, "VLLM_DSPARK_REMOTE_ASYNC", False), switch_interval_us=_int(env, "VLLM_DSPARK_REMOTE_SWITCH_INTERVAL_US", 500, lo=0), hostflag_lib=(env.get("VLLM_DSPARK_REMOTE_HOSTFLAG_LIB") or "").strip(), async_nap_us=_int(env, "VLLM_DSPARK_REMOTE_ASYNC_NAP_US", 0, lo=0), async_spin_us=_int(env, "VLLM_DSPARK_REMOTE_ASYNC_SPIN_US", 1000, lo=0), async_watchdog_ms=_int(env, "VLLM_DSPARK_REMOTE_ASYNC_WATCHDOG_MS", 0, lo=0), async_proxy=_choice(env, "VLLM_DSPARK_REMOTE_ASYNC_PROXY", "thread", ("thread", "process")), async_respawns=_int(env, "VLLM_DSPARK_REMOTE_ASYNC_RESPAWNS", 3, lo=0), async_respawn_reset_s=_int(env, "VLLM_DSPARK_REMOTE_ASYNC_RESPAWN_RESET_S", 3600, lo=0), fused_gather={"auto": None, "1": True, "0": False}[ _choice(env, "VLLM_DSPARK_REMOTE_FUSED_GATHER", "auto", ("auto", "0", "1"))], n_out=_choice(env, "VLLM_DSPARK_REMOTE_N_OUT", "auto", ("auto", "0")), sampled_top32=_bool(env, "VLLM_DSPARK_SAMPLED_TOP32", False), trip=_int(env, "VLLM_DSPARK_REMOTE_TRIP", 3, lo=0), cooldown=_int(env, "VLLM_DSPARK_REMOTE_COOLDOWN", 256, lo=1), ) if cfg.async_mode and cfg.policy != "capacity": raise ValueError("VLLM_DSPARK_REMOTE_ASYNC=1 needs VLLM_DSPARK_REMOTE_POLICY=capacity " "(resilience/shadow decide on the host every round)") if cfg.async_mode and cfg.on_fail == "raise": raise ValueError("VLLM_DSPARK_REMOTE_ASYNC=1 cannot raise on a failed round " "(the host never waits for it); use ON_FAIL=target_only") if cfg.backoff_max_ms < cfg.backoff_ms: raise ValueError("VLLM_DSPARK_REMOTE_BACKOFF_MAX_MS < VLLM_DSPARK_REMOTE_BACKOFF_MS") return cfg def markov_identity_from_env(env) -> bytes: path = (env.get("VLLM_DSPARK_MARKOV_CACHE") or "").strip() if not path: return bytes(32) try: with open(path, "rb") as f: return hashlib.sha256(f.read()).digest() except OSError as e: raise ValueError( f"VLLM_DSPARK_MARKOV_CACHE={path!r} unreadable ({e}); set " "VLLM_DSPARK_REMOTE_MARKOV_IDENTITY explicitly") from e def apply_cost_overrides(cfg: RemoteConfig, environ=None, depth_policy_on: bool = False, depth_mode: str | None = None, num_spec: int = 5, depth_file: str | None = None) -> dict[str, str]: """Make the remote round cost an explicit input of the depth policy (CONTRACT 10, brief "recalibration"). Writes VLLM_DSPARK_DRAFT_MS / VLLM_DSPARK_ROUND_MS in the process environment before the model runner builds DSparkDepthPolicy (it reads them in initialize_kv_cache, after the speculator exists; it is built only when VLLM_DSPARK_DEPTH_POLICY is on). Same env on both ranks, so the cost tables stay identical across TP ranks. Returns what was changed; raises before writing anything. The policy prices depth d as ROUND_MS[d] + DRAFT_MS (depth_policy.round_cost_ms). The live ROUND_MS includes the local drafter, so with the policy on, capacity/resilience need both remote inputs: a remote DRAFT_MS over the live table counts the drafter twice, a verify-only table under the live DRAFT_MS counts the wrong drafter. confr's table is part of the policy string and cannot take them at all. fixed: never prices a depth, so it needs them only when a depth file can switch the mode at runtime.""" env = os.environ if environ is None else environ changed: dict[str, str] = {} if cfg.policy == "shadow": return changed # local drafter runs every round; timing is not representative if depth_policy_on and not (depth_mode == "fixed" and not depth_file): missing = [k for k, v in (("VLLM_DSPARK_REMOTE_DRAFT_MS", cfg.remote_draft_ms), ("VLLM_DSPARK_REMOTE_ROUND_MS", cfg.remote_round_ms)) if v is None] if missing: raise ValueError( f"remote DSpark ({cfg.policy}) with VLLM_DSPARK_DEPTH_POLICY needs both " "VLLM_DSPARK_REMOTE_DRAFT_MS (measured remote round, ms) and " "VLLM_DSPARK_REMOTE_ROUND_MS (verify-only table, ms by depth 0..k): missing " + " and ".join(missing) + ". The live VLLM_DSPARK_ROUND_MS includes the local " "drafter, so a partial override mixes two cost models") if depth_mode == "confr": raise ValueError( f"remote DSpark ({cfg.policy}): VLLM_DSPARK_DEPTH_POLICY=confr: carries " "its own step table (measured with the local drafter), which the remote cost " "inputs cannot replace; use conf or confb with VLLM_DSPARK_REMOTE_DRAFT_MS / " "VLLM_DSPARK_REMOTE_ROUND_MS") if cfg.remote_round_ms is not None: t = cfg.remote_round_ms if len(t) != num_spec + 1: raise ValueError(f"VLLM_DSPARK_REMOTE_ROUND_MS: {len(t)} values, expected " f"{num_spec + 1} (verify ms for depth 0..{num_spec})") if any(v <= 0 for v in t): raise ValueError("VLLM_DSPARK_REMOTE_ROUND_MS: verify times must be > 0 ms") if any(b < a for a, b in zip(t, t[1:])): raise ValueError("VLLM_DSPARK_REMOTE_ROUND_MS: must be non-decreasing with the " f"depth (more live rows never verify faster): {t}") changed["VLLM_DSPARK_ROUND_MS"] = ",".join(f"{v:g}" for v in t) if cfg.remote_draft_ms is not None: changed["VLLM_DSPARK_DRAFT_MS"] = f"{cfg.remote_draft_ms:g}" for k, v in changed.items(): env[k] = v return changed # depth-policy modes that zero the confidences at positions >= cap[num_reqs] before choosing # depths (DSparkDepthPolicy.record); "fixed" and "off" verify by the scheduler window only CAP_MASKING_DEPTH_MODES = ("conf", "confb", "confr") def n_out_table(cfg: RemoteConfig, num_spec: int, depth_mode: str, depth_file: str | None, dense_cap: list[int] | None) -> tuple[list[int] | None, str]: """ReqEntry.n_out by request count (CONTRACT 6.3), or None (always all N positions), with the reason. dense_cap is build_dynamic_sd_schedule_lookup's table (index = number of requests, index 0 unused), as the model runner passes it to DSparkDepthPolicy. Positions >= cap[n] are only safe to drop when nothing reads them: the depth policy must zero them (a cap-masking mode, fixed for the life of the serve, so no depth file that could switch it to "fixed"/"off"), and shadow compares every position.""" if cfg.policy == "shadow": return None, "shadow compares every position" if cfg.n_out == "0": return None, "VLLM_DSPARK_REMOTE_N_OUT=0" if depth_mode not in CAP_MASKING_DEPTH_MODES: return None, f"depth policy {depth_mode!r} does not mask positions >= cap" if depth_file: return None, "VLLM_DSPARK_DEPTH_FILE can switch the depth policy at runtime" if not dense_cap: return None, "no dynamic speculation schedule" table = [0 if i == 0 else min(num_spec, int(c)) for i, c in enumerate(dense_cap)] if all(c >= num_spec for c in table[1:]): return None, "schedule never caps below N" return table, "cap by request count"