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

CI deploy local

Browse files
Files changed (2) hide show
  1. src/adapters/timesfm.py +52 -2
  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
- self._model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
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 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
 
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