Spaces:
Running on Zero
Running on Zero
CI deploy local
Browse files- src/adapters/timesfm.py +52 -2
- src/runtime.py +8 -9
src/adapters/timesfm.py
CHANGED
|
@@ -21,6 +21,8 @@ that -- which is why `_ensure_compiled` exists rather than a line in `load`.
|
|
| 21 |
from __future__ import annotations
|
| 22 |
|
| 23 |
import logging
|
|
|
|
|
|
|
| 24 |
|
| 25 |
import numpy as np
|
| 26 |
import pandas as pd
|
|
@@ -49,6 +51,43 @@ COMPILE_MAX_HORIZON = 256
|
|
| 49 |
# The quantile grid TimesFM returns: column 0 is the mean, then 0.1 .. 0.9.
|
| 50 |
QUANTILE_GRID = (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9)
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
class TimesFMAdapter(ForecastAdapter):
|
| 54 |
"""`google/timesfm-2.5-*` quantile forecasters."""
|
|
@@ -116,14 +155,20 @@ class TimesFMAdapter(ForecastAdapter):
|
|
| 116 |
)
|
| 117 |
|
| 118 |
self.resolve_revision()
|
| 119 |
-
|
| 120 |
-
self.model_id,
|
| 121 |
revision=self._resolved_revision,
|
| 122 |
# torch.compile spends tens of seconds warming up and buys little
|
| 123 |
# on a CPU box with batch size 1. It is the difference between
|
| 124 |
# clearing the cold-start budget and blowing straight through it.
|
| 125 |
torch_compile=(self.device == "cuda"),
|
| 126 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
return self
|
| 128 |
|
| 129 |
def _ensure_compiled(self) -> None:
|
|
@@ -131,6 +176,11 @@ class TimesFMAdapter(ForecastAdapter):
|
|
| 131 |
return
|
| 132 |
import timesfm
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
self._model.compile(timesfm.ForecastConfig(
|
| 135 |
max_context=self._max_context,
|
| 136 |
max_horizon=COMPILE_MAX_HORIZON,
|
|
|
|
| 21 |
from __future__ import annotations
|
| 22 |
|
| 23 |
import logging
|
| 24 |
+
import threading
|
| 25 |
+
from contextlib import contextmanager
|
| 26 |
|
| 27 |
import numpy as np
|
| 28 |
import pandas as pd
|
|
|
|
| 51 |
# The quantile grid TimesFM returns: column 0 is the mean, then 0.1 .. 0.9.
|
| 52 |
QUANTILE_GRID = (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9)
|
| 53 |
|
| 54 |
+
# Serialises the CPU pin below, which mutates a torch global.
|
| 55 |
+
_PIN_LOCK = threading.Lock()
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@contextmanager
|
| 59 |
+
def _null_context():
|
| 60 |
+
yield
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@contextmanager
|
| 64 |
+
def _pinned_to_cpu():
|
| 65 |
+
"""Hide CUDA from TimesFM's loader for the duration of a CPU load.
|
| 66 |
+
|
| 67 |
+
`TimesFM_2p5_200M_torch.load_checkpoint` decides its own device:
|
| 68 |
+
|
| 69 |
+
if torch.cuda.is_available():
|
| 70 |
+
self.device = torch.device("cuda:0")
|
| 71 |
+
|
| 72 |
+
There is no argument to override it. On ZeroGPU that probe runs outside a
|
| 73 |
+
`@spaces.GPU` function and trips a low-level CUDA init the emulation layer
|
| 74 |
+
cannot intercept, which fails the load outright -- so the probe has to not
|
| 75 |
+
happen. Patching the lookup TimesFM performs is narrower than making the
|
| 76 |
+
whole process CUDA-blind, and it is reverted immediately.
|
| 77 |
+
"""
|
| 78 |
+
import torch
|
| 79 |
+
|
| 80 |
+
with _PIN_LOCK:
|
| 81 |
+
original_available = torch.cuda.is_available
|
| 82 |
+
original_count = torch.cuda.device_count
|
| 83 |
+
torch.cuda.is_available = lambda: False
|
| 84 |
+
torch.cuda.device_count = lambda: 1
|
| 85 |
+
try:
|
| 86 |
+
yield
|
| 87 |
+
finally:
|
| 88 |
+
torch.cuda.is_available = original_available
|
| 89 |
+
torch.cuda.device_count = original_count
|
| 90 |
+
|
| 91 |
|
| 92 |
class TimesFMAdapter(ForecastAdapter):
|
| 93 |
"""`google/timesfm-2.5-*` quantile forecasters."""
|
|
|
|
| 155 |
)
|
| 156 |
|
| 157 |
self.resolve_revision()
|
| 158 |
+
load_kwargs = dict(
|
|
|
|
| 159 |
revision=self._resolved_revision,
|
| 160 |
# torch.compile spends tens of seconds warming up and buys little
|
| 161 |
# on a CPU box with batch size 1. It is the difference between
|
| 162 |
# clearing the cold-start budget and blowing straight through it.
|
| 163 |
torch_compile=(self.device == "cuda"),
|
| 164 |
)
|
| 165 |
+
if self.device == "cuda":
|
| 166 |
+
self._model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
|
| 167 |
+
self.model_id, **load_kwargs)
|
| 168 |
+
else:
|
| 169 |
+
with _pinned_to_cpu():
|
| 170 |
+
self._model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
|
| 171 |
+
self.model_id, **load_kwargs)
|
| 172 |
return self
|
| 173 |
|
| 174 |
def _ensure_compiled(self) -> None:
|
|
|
|
| 176 |
return
|
| 177 |
import timesfm
|
| 178 |
|
| 179 |
+
with (_pinned_to_cpu() if self.device != "cuda"
|
| 180 |
+
else _null_context()):
|
| 181 |
+
self._compile(timesfm)
|
| 182 |
+
|
| 183 |
+
def _compile(self, timesfm) -> None:
|
| 184 |
self._model.compile(timesfm.ForecastConfig(
|
| 185 |
max_context=self._max_context,
|
| 186 |
max_horizon=COMPILE_MAX_HORIZON,
|
src/runtime.py
CHANGED
|
@@ -235,20 +235,19 @@ def run_forecast(store: ArenaStore, model_slug: str, asset: str, timeframe: str,
|
|
| 235 |
warm(entry["model_id"], entry.get("revision"))
|
| 236 |
|
| 237 |
started = time.time()
|
| 238 |
-
if
|
| 239 |
-
#
|
| 240 |
-
#
|
| 241 |
-
#
|
| 242 |
-
#
|
| 243 |
-
#
|
| 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
|
|
|
|
| 235 |
warm(entry["model_id"], entry.get("revision"))
|
| 236 |
|
| 237 |
started = time.time()
|
| 238 |
+
if caps.hardware == "gpu":
|
| 239 |
+
# Only GPU-tier models take the GPU path. Routing CPU-tier models
|
| 240 |
+
# through it was tried and is wrong: ZeroGPU's anonymous run limit is
|
| 241 |
+
# exhausted in a couple of calls, so a visitor who clicked Forecast
|
| 242 |
+
# twice on a model that runs in 20ms on CPU got locked out of the GPU
|
| 243 |
+
# models they actually needed it for.
|
|
|
|
|
|
|
| 244 |
result = _predict(entry["family"], entry["model_id"],
|
| 245 |
entry.get("revision"), context, horizon, n_samples,
|
| 246 |
seed, issued_ts, caps.hardware)
|
| 247 |
else:
|
| 248 |
adapter = adapter_for(entry["family"], entry["model_id"],
|
| 249 |
+
revision=entry.get("revision"),
|
| 250 |
+
device=_device("cpu"))
|
| 251 |
result = adapter.predict(context, horizon=horizon, n_samples=n_samples,
|
| 252 |
seed=int(seed), issued_ts=issued_ts)
|
| 253 |
elapsed = time.time() - started
|