WeMM-JEV / inference /process.py
Rover00's picture
Publish WeMM-JEV model and inference
7b5e901 verified
Raw History Blame Contribute Delete
5.15 kB
"""Spawn one D3 process per GPU so FLA/Triton mutable state is not shared."""
import multiprocessing
import threading
import traceback
from concurrent.futures import ProcessPoolExecutor
from ._compat import Engine, NativeAbstention, Unsupported
TARGET = f"{__package__}.engine:D3Engine"
RUN_SEED = 20260919
_ENGINE = None
_FAILURE = None
_START_LOCK = threading.Lock()
class WorkerError(RuntimeError):
def __init__(self, record):
super().__init__(record["error"])
self.exception_name = record["exception"]
self.remote_traceback = record["traceback"]
def _invoke(target, options, method, args, kwargs):
global _ENGINE, _FAILURE
if _FAILURE is not None:
return False, _FAILURE
try:
if _ENGINE is None:
from importlib import import_module
if target == TARGET:
import torch
torch.manual_seed(RUN_SEED)
module, name = target.split(":")
_ENGINE = getattr(import_module(module), name)(**options)
if method == "info":
value = (_ENGINE.provenance, _ENGINE.runtime(), _ENGINE.latency)
else:
value = getattr(_ENGINE, method)(*args, **kwargs)
if method == "__call__":
_ENGINE.synchronize()
return True, value
except Exception as exc:
record = {"error": str(exc), "exception": type(exc).__name__,
"traceback": traceback.format_exc()}
if isinstance(exc, NativeAbstention):
record.update(kind="abstained", raw=exc.raw)
elif isinstance(exc, Unsupported):
record["kind"] = "unsupported"
if _ENGINE is None or record["exception"] in ("OutOfMemoryError", "AcceleratorError") or "device-side assert" in str(exc):
_FAILURE = record
return False, record
class _Child:
def __init__(self, options, target=TARGET):
self.options = options
self.target = target
self.lock = threading.Lock()
self.pool = ProcessPoolExecutor(max_workers=1, mp_context=multiprocessing.get_context("spawn"))
try:
self.info = self.call("info")
except BaseException:
self.pool.shutdown(wait=True, cancel_futures=True)
raise
def call(self, method, *args, **kwargs):
with self.lock:
ok, value = self.pool.submit(_invoke, self.target, self.options, method, args, kwargs).result()
if ok:
return value
if value.get("kind") == "unsupported":
raise Unsupported(value["error"])
if value.get("kind") == "abstained":
raise NativeAbstention(value["raw"])
raise WorkerError(value)
def close(self):
# Process exit releases the model and all CUDA allocations, even after an error.
self.pool.shutdown(wait=True, cancel_futures=True)
class D3ProcessEngine(Engine):
name = "d3"
def __init__(self, device=None, replicas=None, **options):
import torch
gpus = torch.cuda.device_count() if torch.cuda.is_available() else 0
if replicas is not None and int(replicas) > 1:
if int(replicas) > gpus:
raise ValueError(f"replicas={replicas} needs more than {gpus} CUDA devices")
self.devices = [f"cuda:{i}" for i in range(int(replicas))]
elif device in (None, "cuda"):
self.devices = [f"cuda:{i}" for i in range(gpus)] or ["cpu"]
else:
self.devices = [str(device)]
super().__init__(device=device, **options)
self._options = options
self._children = {}
self._local = threading.local()
self._lock = threading.Lock()
self._next = 0
child = self._child(0)
provenance, runtime, self.latency = child.info
self.provenance = {**provenance, "gpu_execution": "one spawned process per GPU"}
self._runtime = runtime
def _child(self, slot):
child = self._children.get(slot)
if child is not None:
return child
with self._lock:
if slot not in self._children:
# Avoid simultaneous checkpoint loads exhausting host RAM.
with _START_LOCK:
self._children[slot] = _Child({**self._options, "device": self.devices[slot]})
return self._children[slot]
def __call__(self, *args, **kwargs):
slot = getattr(self._local, "slot", None)
if slot is None:
with self._lock:
slot = self._next
self._next = (self._next + 1) % len(self.devices)
self._local.slot = slot
return self._child(slot).call("__call__", *args, **kwargs)
def warmup(self):
self._child(0).call("warmup")
def synchronize(self):
for child in list(self._children.values()):
child.call("synchronize")
def runtime(self):
return {**self._runtime, "gpu_execution": "spawn", "devices": self.devices}
def close(self):
for child in self._children.values():
child.close()
self._children.clear()