Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Licenses verified against Hugging Face model cards. Check the model card for you

- **Apache-2.0**, **MIT**: generally fine for commercial production (retain notices; T0† is also gated on Hugging Face: accept terms and set `HF_TOKEN`).
- **CC-BY-NC-4.0** (Moirai), **CC-BY-NC-SA-4.0** (PatchTST-FM): **non-commercial** only; not for revenue-generating production without a separate agreement from the rights holder.
- **TimesFM 3.0**: weights for `google/timesfm-3.0-pytorch` are [non-commercial](https://huggingface.co/google/timesfm-3.0-pytorch/blob/main/LICENSE); TimesFM 1.0–2.5 checkpoints remain Apache-2.0.
- **TabPFN NC**‡: TabPFN-2.6+ weights are non-commercial; production requires a [Prior Labs commercial license or API](https://docs.priorlabs.ai/models). First use also requires accepting terms at [ux.priorlabs.ai](https://ux.priorlabs.ai) (`TABPFN_TOKEN`).
- **Community / Apache-2.0** (TiRex): TiRex 1.0 uses the [NXAI Community License](https://huggingface.co/NX-AI/TiRex/blob/main/LICENSE) (commercial limits for large enterprises); TiRex 2.0 is Apache-2.0.
- **Nixtla API**§: hosted service via `NIXTLA_API_KEY`; production under [Nixtla terms/pricing](https://www.nixtla.io/docs), not open weights.
Expand All @@ -107,7 +108,7 @@ Some models require specific Python versions (e.g. FlowState 3.11-3.13, TabPFN &
- **Tafsut:** `Tafsut-FM/tafsut-univariate-base`
- **TiRex:** `NX-AI/TiRex`, `NX-AI/TiRex-2`
- **TimeGPT:** pass `model=` to `TimeGPT()`, e.g. `timegpt-1`, [`timegpt-1-long-horizon`](https://www.nixtla.io/docs/forecasting/model-version/longhorizon_model), `timegpt-2-mini`, `timegpt-2`, `timegpt-2-pro`
- **TimesFM:** `google/timesfm-{1.0-200m,2.0-500m,2.5-200m}-pytorch`
- **TimesFM:** `google/timesfm-{1.0-200m,2.0-500m,2.5-200m,3.0}-pytorch` (3.0 weights are [non-commercial](https://huggingface.co/google/timesfm-3.0-pytorch/blob/main/LICENSE))
- **Toto:** `Datadog/Toto-Open-Base-1.0`, `Datadog/Toto-2.0-{4m,22m,313m,1B,2.5B}`

</details>
Expand Down
717 changes: 717 additions & 0 deletions docs/examples/timesfm-family.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/model-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ Below is the list of available foundation models. Click a model name for API det
- [Tafsut](api/models/foundation/models.md#foundationforecast.models.tafsut.Tafsut) ([GitHub](https://github.com/Tafsut-FM/tafsut))
- [TiRex / TiRex-2](api/models/foundation/models.md#foundationforecast.models.tirex.TiRex) ([arXiv:2505.23719](https://arxiv.org/abs/2505.23719), [arXiv:2607.01204](https://arxiv.org/abs/2607.01204)) — Python 3.11+
- [TimeGPT](api/models/foundation/models.md#foundationforecast.models.timegpt.TimeGPT) ([arXiv:2310.03589](https://arxiv.org/abs/2310.03589)) — requires `NIXTLA_API_KEY`
- [TimesFM](api/models/foundation/models.md#foundationforecast.models.timesfm.TimesFM) ([arXiv:2310.10688](https://arxiv.org/abs/2310.10688))
- [TimesFM](api/models/foundation/models.md#foundationforecast.models.timesfm.TimesFM) ([arXiv:2310.10688](https://arxiv.org/abs/2310.10688)) — supports 1.0, 2.0, 2.5, and 3.0; 3.0 weights are [non-commercial](https://huggingface.co/google/timesfm-3.0-pytorch/blob/main/LICENSE)
- [Toto](api/models/foundation/models.md#foundationforecast.models.toto.Toto) ([arXiv:2505.14766](https://arxiv.org/abs/2505.14766))
3 changes: 3 additions & 0 deletions experiments/gift-eval/configs/ci_subset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ jobs:
- model_key: google--timesfm-2.5-200m-pytorch
dataset_name: m4_weekly
term: short
- model_key: google--timesfm-3.0-pytorch
dataset_name: m4_weekly
term: short
- model_key: NX-AI--TiRex-1.1-gifteval
dataset_name: m4_weekly
term: short
Expand Down
8 changes: 8 additions & 0 deletions experiments/gift-eval/configs/models.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ models:
repo_id: google/timesfm-2.5-200m-pytorch
batch_size: 64

google--timesfm-3.0-pytorch:
class: foundationforecast.models.timesfm.TimesFM
reference_slug: TimesFM-3
kwargs:
repo_id: google/timesfm-3.0-pytorch
alias: TimesFM-3
batch_size: 64

# --- TiRex (GIFT-Eval-specific HF weights) ---
NX-AI--TiRex-1.1-gifteval:
class: foundationforecast.models.tirex.TiRex
Expand Down
8 changes: 4 additions & 4 deletions experiments/gift-eval/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

138 changes: 131 additions & 7 deletions foundationforecast/models/timesfm.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from contextlib import contextmanager
from typing import Any

import numpy as np
import pandas as pd
Expand All @@ -8,6 +9,7 @@
import torch
from huggingface_hub import repo_exists
from timesfm import TimesFM_2p5_200M_torch
from timesfm3 import ModelConfig, TimesFM3Evaluator
from timesfm_v1.timesfm_base import DEFAULT_QUANTILES as DEFAULT_QUANTILES_TFM
from tqdm import tqdm

Expand Down Expand Up @@ -136,7 +138,7 @@ def __init__(
context_length: int,
batch_size: int,
alias: str,
**kwargs: dict,
**kwargs: Any,
):
self.repo_id = repo_id
self.context_length = context_length
Expand Down Expand Up @@ -235,6 +237,114 @@ def forecast(
return fcst_df


class _TimesFMV3(Forecaster):
def __init__(
self,
repo_id: str,
context_length: int,
batch_size: int,
alias: str,
**kwargs: Any,
):
self.repo_id = repo_id
self.context_length = context_length
self.batch_size = batch_size
self.alias = alias
self.kwargs = kwargs

@contextmanager
def _get_predictor(self, prediction_length: int) -> TimesFM3Evaluator:
if os.path.exists(self.repo_id) or repo_exists(self.repo_id):
config = ModelConfig(
checkpoint_path=self.repo_id,
per_core_batch_size=self.batch_size,
**(self.kwargs or {}),
)
forecaster = TimesFM3Evaluator(config)
else:
raise OSError(
f"Failed to load model. Searched for '{self.repo_id}' "
"as a local path to model directory and as a Hugging Face repo_id."
)
try:
yield forecaster
finally:
del forecaster
torch.cuda.empty_cache()

def _series_to_context(self, series: torch.Tensor) -> np.ndarray:
arr = series.numpy().astype(np.float32)
if len(arr) > self.context_length:
arr = arr[-self.context_length :]
return arr

def _predict(
self,
forecaster: TimesFM3Evaluator,
dataset: TimeSeriesDataset,
h: int,
) -> tuple[np.ndarray, np.ndarray]:
fcsts_mean: list[np.ndarray] = []
fcsts_quantiles: list[np.ndarray] = []
for batch in tqdm(dataset):
contexts = [self._series_to_context(series) for series in batch]
outputs = list(
forecaster.predict_batch(
contexts=contexts,
horizon=h,
return_quantiles=True,
use_symmetric_averaging=True,
make_positive=True,
sort_quantiles=True,
)
)
fcsts_mean.extend(output.forecast for output in outputs)
fcsts_quantiles.extend(output.quantiles for output in outputs)
fcsts_mean_np = np.stack(fcsts_mean)
fcsts_quantiles_np = np.stack(fcsts_quantiles)
return fcsts_mean_np, fcsts_quantiles_np

def forecast(
self,
df: pd.DataFrame,
h: int,
freq: str | None = None,
level: list[int | float] | None = None,
quantiles: list[float] | None = None,
) -> pd.DataFrame:
freq = self._maybe_infer_freq(df, freq)
qc = QuantileConverter(level=level, quantiles=quantiles)
if qc.quantiles is not None and len(qc.quantiles) != len(DEFAULT_QUANTILES_TFM):
raise ValueError(
"TimesFM only supports the default quantiles, "
"please use the default quantiles or default level, "
"see https://github.com/google-research/timesfm/issues/286"
)
dataset = TimeSeriesDataset.from_df(
df,
batch_size=self.batch_size,
dtype=torch.float32,
)
fcst_df = dataset.make_future_dataframe(h=h, freq=freq)
with self._get_predictor(prediction_length=h) as forecaster:
fcsts_mean_np, fcsts_quantiles_np = self._predict(
forecaster,
dataset,
h,
)
fcst_df[self.alias] = fcsts_mean_np.reshape(-1, 1)
if qc.quantiles is not None:
for i, q in enumerate(qc.quantiles):
fcst_df[f"{self.alias}-q-{int(q * 100)}"] = fcsts_quantiles_np[
..., i
].reshape(-1, 1)
fcst_df = qc.maybe_convert_quantiles_to_level(
fcst_df,
models=[self.alias],
)
return fcst_df


class TimesFM(Forecaster):
"""
TimesFM is a large time series model for time series forecasting, supporting both
Expand All @@ -248,7 +358,7 @@ def __new__(
context_length: int = 2048,
batch_size: int = 64,
alias: str = "TimesFM",
**kwargs: dict,
**kwargs: Any,
):
if "pytorch" not in repo_id and repo_id not in _GIFT_EVAL_LEGACY_REPOS:
legacy = ", ".join(_GIFT_EVAL_LEGACY_REPOS)
Expand All @@ -272,9 +382,17 @@ def __new__(
alias=alias,
**kwargs,
)
elif "3.0" in repo_id:
return _TimesFMV3(
repo_id=repo_id,
context_length=context_length,
batch_size=batch_size,
alias=alias,
**kwargs,
)
else:
raise ValueError(
"TimesFM only supports 1.0, 2.0 and 2.5 models, please use a "
"TimesFM only supports 1.0, 2.0, 2.5 and 3.0 models, please use a "
"valid model id"
)

Expand All @@ -284,7 +402,7 @@ def __init__(
context_length: int = 2048,
batch_size: int = 64,
alias: str = "TimesFM",
kwargs: dict | None = None,
**kwargs: Any,
):
"""
Args:
Expand All @@ -298,17 +416,19 @@ def __init__(
- `google/timesfm-1.0-200m-pytorch`
- `google/timesfm-2.0-500m-pytorch`
- `google/timesfm-2.5-200m-pytorch`
- `google/timesfm-3.0-pytorch`
context_length (int, optional): Maximum context length (input window size)
for the model. Defaults to 2048. For TimesFM 2.0 models, max is 2048
(must be a multiple of 32). For TimesFM 1.0 models, max is 512. See
(must be a multiple of 32). For TimesFM 1.0 models, max is 512. For
TimesFM 3.0 models, max is 15360. See
[TimesFM docs](https://github.com/google-research/timesfm#loading-the-
model) for details.
batch_size (int, optional): Batch size for inference. Defaults to 64.
Adjust based on available memory and model size.
alias (str, optional): Name to use for the model in output DataFrames and
logs. Defaults to `TimesFM`.
kwargs (dict, optional): Additional keyword arguments to pass to the model.
Defaults to None. Only used for TimesFM 2.5 models.
**kwargs (Any): Extra keyword arguments forwarded to the backend
model config. Used for TimesFM 2.5 and 3.0 models.

Notes:
**Academic Reference:**
Expand All @@ -325,11 +445,15 @@ def __init__(
- Only PyTorch checkpoints are currently supported. JAX is not supported.
- The model is loaded onto the best available device (GPU if available,
otherwise CPU).
- TimesFM 3.0 pretrained weights are distributed under a
[non-commercial license](https://huggingface.co/google/timesfm-3.0-pytorch/blob/main/LICENSE)
and are restricted to non-commercial, non-production use.

**Supported Models:**

- `google/timesfm-1.0-200m-pytorch`
- `google/timesfm-2.0-500m-pytorch`
- `google/timesfm-2.5-200m-pytorch`
- `google/timesfm-3.0-pytorch`
"""
pass
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ dependencies = [
"tfc-t0>=0.2.3 ; python_full_version >= '3.11' and python_full_version < '3.14'",
"timecopilot-chronos-forecasting>=0.2.2",
"timecopilot-granite-tsfm>=0.2.1 ; python_full_version >= '3.11' and python_full_version < '3.14'",
"timecopilot-timesfm>=0.3.0",
"timecopilot-timesfm>=0.4.0",
"timecopilot-tirex2>=0.1.0 ; python_full_version >= '3.11'",
"timecopilot-tirex>=0.1.1 ; python_full_version >= '3.11'",
"timecopilot-toto-2>=0.1.1",
Expand Down
4 changes: 4 additions & 0 deletions tests/models/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
repo_id="google/timesfm-2.5-200m-pytorch",
context_length=256,
),
TimesFM(
repo_id="google/timesfm-3.0-pytorch",
context_length=256,
),
Moirai(
context_length=256,
batch_size=2,
Expand Down
8 changes: 8 additions & 0 deletions tests/models/test_timesfm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
TimesFM,
_TimesFMV1,
_TimesFMV2_p5,
_TimesFMV3,
)

pytestmark = pytest.mark.models

MODEL_PARAMS = [
_TimesFMV1,
_TimesFMV2_p5,
_TimesFMV3,
]


Expand All @@ -27,6 +29,12 @@ def test_timesfm_accepts_pytorch_repos():
assert isinstance(model, _TimesFMV1)


def test_timesfm_routes_3_0_repo():
model = TimesFM(repo_id="google/timesfm-3.0-pytorch")
assert isinstance(model, _TimesFMV3)
assert model.repo_id == "google/timesfm-3.0-pytorch"


def test_timesfm_rejects_non_pytorch_repo():
with pytest.raises(ValueError, match="JAX backends are not supported"):
TimesFM(repo_id="google/timesfm-2.0-500m")
Expand Down
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading