Spaces:
Running on Zero
Running on Zero
CI deploy local
Browse files- src/runtime.py +42 -16
src/runtime.py
CHANGED
|
@@ -22,7 +22,7 @@ from __future__ import annotations
|
|
| 22 |
|
| 23 |
import logging
|
| 24 |
import time
|
| 25 |
-
from dataclasses import dataclass, field
|
| 26 |
|
| 27 |
import pandas as pd
|
| 28 |
|
|
@@ -52,17 +52,31 @@ _ADAPTERS: dict[tuple, ForecastAdapter] = {}
|
|
| 52 |
|
| 53 |
|
| 54 |
def _device(tier: str) -> str | None:
|
| 55 |
-
"""The device to hand an adapter, or None to let it decide.
|
| 56 |
-
|
| 57 |
-
On ZeroGPU the answer is never "work it out yourself": a CUDA probe outside
|
| 58 |
-
a `@spaces.GPU` function is fatal. Everywhere else -- local, CI, cpu-basic
|
| 59 |
-
-- the adapter's own detection is right.
|
| 60 |
-
"""
|
| 61 |
if not gpu_dispatch.HAS_SPACES:
|
| 62 |
return None
|
| 63 |
return "cuda" if tier == "gpu" else "cpu"
|
| 64 |
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
def adapter_for(family: str, model_id: str, revision: str | None = None,
|
| 67 |
**kwargs) -> ForecastAdapter:
|
| 68 |
"""A loaded adapter, cached. Raises `ForecastUnavailable` on load failure."""
|
|
@@ -99,11 +113,11 @@ def clear_adapter_cache() -> None:
|
|
| 99 |
@gpu_dispatch.gpu()
|
| 100 |
def _predict(family: str, model_id: str, revision: str | None,
|
| 101 |
context_ohlcv, horizon: int, n_samples: int, seed: int,
|
| 102 |
-
issued_ts) -> ForecastResult:
|
| 103 |
# Inside the GPU function the device is known, so it is stated rather than
|
| 104 |
# probed -- see `base.default_device` for why probing is not an option.
|
| 105 |
adapter = adapter_for(family, model_id, revision=revision,
|
| 106 |
-
device=_device(
|
| 107 |
return adapter.predict(context_ohlcv, horizon=horizon, n_samples=n_samples,
|
| 108 |
seed=int(seed), issued_ts=issued_ts)
|
| 109 |
|
|
@@ -193,6 +207,12 @@ def run_forecast(store: ArenaStore, model_slug: str, asset: str, timeframe: str,
|
|
| 193 |
# context ZeroGPU forbids, which failed even for CPU-tier models.
|
| 194 |
caps = get_adapter(entry["family"], entry["model_id"],
|
| 195 |
revision=entry.get("revision")).capabilities()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
|
| 197 |
# The smaller of what the model can take and what the Arena spends. See
|
| 198 |
# config.DEFAULT_CONTEXT_BARS for why the ceiling is not the model's own.
|
|
@@ -211,18 +231,24 @@ def run_forecast(store: ArenaStore, model_slug: str, asset: str, timeframe: str,
|
|
| 211 |
f"currently have. CPU-tier models are unaffected.",
|
| 212 |
kind="no_gpu")
|
| 213 |
|
|
|
|
|
|
|
|
|
|
| 214 |
started = time.time()
|
| 215 |
-
if
|
| 216 |
-
#
|
| 217 |
-
#
|
| 218 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
result = _predict(entry["family"], entry["model_id"],
|
| 220 |
entry.get("revision"), context, horizon, n_samples,
|
| 221 |
-
seed, issued_ts)
|
| 222 |
else:
|
| 223 |
adapter = adapter_for(entry["family"], entry["model_id"],
|
| 224 |
-
revision=entry.get("revision")
|
| 225 |
-
device=_device("cpu"))
|
| 226 |
result = adapter.predict(context, horizon=horizon, n_samples=n_samples,
|
| 227 |
seed=int(seed), issued_ts=issued_ts)
|
| 228 |
elapsed = time.time() - started
|
|
|
|
| 22 |
|
| 23 |
import logging
|
| 24 |
import time
|
| 25 |
+
from dataclasses import dataclass, field, replace
|
| 26 |
|
| 27 |
import pandas as pd
|
| 28 |
|
|
|
|
| 52 |
|
| 53 |
|
| 54 |
def _device(tier: str) -> str | None:
|
| 55 |
+
"""The device to hand an adapter, or None to let it decide."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
if not gpu_dispatch.HAS_SPACES:
|
| 57 |
return None
|
| 58 |
return "cuda" if tier == "gpu" else "cpu"
|
| 59 |
|
| 60 |
|
| 61 |
+
def warm(model_id: str, revision: str | None) -> None:
|
| 62 |
+
"""Pull a model's weights to local disk, outside any GPU call.
|
| 63 |
+
|
| 64 |
+
Downloading is network and disk, not CUDA, so it is legal anywhere -- and
|
| 65 |
+
doing it here means the GPU call spends its 60-second budget on compute
|
| 66 |
+
instead of on a 400 MB download it might not finish.
|
| 67 |
+
"""
|
| 68 |
+
if model_id.startswith("baseline/"):
|
| 69 |
+
return
|
| 70 |
+
try:
|
| 71 |
+
from huggingface_hub import snapshot_download
|
| 72 |
+
|
| 73 |
+
snapshot_download(model_id, revision=revision,
|
| 74 |
+
allow_patterns=["*.json", "*.safetensors", "*.ckpt"])
|
| 75 |
+
except Exception as e: # pragma: no cover
|
| 76 |
+
# A failed prefetch is not fatal: the loader will fetch what it needs.
|
| 77 |
+
log.info("could not prefetch %s: %s", model_id, e)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
def adapter_for(family: str, model_id: str, revision: str | None = None,
|
| 81 |
**kwargs) -> ForecastAdapter:
|
| 82 |
"""A loaded adapter, cached. Raises `ForecastUnavailable` on load failure."""
|
|
|
|
| 113 |
@gpu_dispatch.gpu()
|
| 114 |
def _predict(family: str, model_id: str, revision: str | None,
|
| 115 |
context_ohlcv, horizon: int, n_samples: int, seed: int,
|
| 116 |
+
issued_ts, tier: str = "gpu") -> ForecastResult:
|
| 117 |
# Inside the GPU function the device is known, so it is stated rather than
|
| 118 |
# probed -- see `base.default_device` for why probing is not an option.
|
| 119 |
adapter = adapter_for(family, model_id, revision=revision,
|
| 120 |
+
device=_device(tier))
|
| 121 |
return adapter.predict(context_ohlcv, horizon=horizon, n_samples=n_samples,
|
| 122 |
seed=int(seed), issued_ts=issued_ts)
|
| 123 |
|
|
|
|
| 207 |
# context ZeroGPU forbids, which failed even for CPU-tier models.
|
| 208 |
caps = get_adapter(entry["family"], entry["model_id"],
|
| 209 |
revision=entry.get("revision")).capabilities()
|
| 210 |
+
# The registry's recorded hardware wins over the adapter's declared
|
| 211 |
+
# default: it is the one that was measured on real hardware, and a
|
| 212 |
+
# demotion recorded there must actually govern who can run the model.
|
| 213 |
+
recorded = (entry.get("capabilities") or {}).get("hardware")
|
| 214 |
+
if recorded in ("cpu", "gpu") and recorded != caps.hardware:
|
| 215 |
+
caps = replace(caps, hardware=recorded)
|
| 216 |
|
| 217 |
# The smaller of what the model can take and what the Arena spends. See
|
| 218 |
# config.DEFAULT_CONTEXT_BARS for why the ceiling is not the model's own.
|
|
|
|
| 231 |
f"currently have. CPU-tier models are unaffected.",
|
| 232 |
kind="no_gpu")
|
| 233 |
|
| 234 |
+
# Weights land on disk before the GPU clock starts.
|
| 235 |
+
warm(entry["model_id"], entry.get("revision"))
|
| 236 |
+
|
| 237 |
started = time.time()
|
| 238 |
+
if gpu_dispatch.HAS_SPACES:
|
| 239 |
+
# On ZeroGPU *everything* goes through the GPU function, CPU-tier
|
| 240 |
+
# models included. Outside one, ZeroGPU runs a CUDA emulation that does
|
| 241 |
+
# not intercept every operation -- TimesFM's loader trips straight
|
| 242 |
+
# through it into a low-level init and takes the request down. Inside,
|
| 243 |
+
# every torch operation is legal, and these calls are seconds long.
|
| 244 |
+
#
|
| 245 |
+
# The tier still decides who may run what; it no longer decides where.
|
| 246 |
result = _predict(entry["family"], entry["model_id"],
|
| 247 |
entry.get("revision"), context, horizon, n_samples,
|
| 248 |
+
seed, issued_ts, caps.hardware)
|
| 249 |
else:
|
| 250 |
adapter = adapter_for(entry["family"], entry["model_id"],
|
| 251 |
+
revision=entry.get("revision"))
|
|
|
|
| 252 |
result = adapter.predict(context, horizon=horizon, n_samples=n_samples,
|
| 253 |
seed=int(seed), issued_ts=issued_ts)
|
| 254 |
elapsed = time.time() - started
|