Bit-Trading-Company commited on
Commit
ac2fa62
·
verified ·
1 Parent(s): ac20a54

CI deploy local

Browse files
Files changed (1) hide show
  1. 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("gpu"))
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 caps.hardware == "gpu":
216
- # Only GPU-tier models take the GPU path. Routing a CPU-tier model
217
- # through it would spend someone's ZeroGPU quota to run a forecast that
218
- # finishes in 20ms on the CPU that is already sitting there.
 
 
 
 
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