From 76a47a1c4734f1e5ea9c29c4d6fd3da6fe6bce8a Mon Sep 17 00:00:00 2001 From: shinfxh Date: Mon, 6 Apr 2026 02:35:46 -0400 Subject: [PATCH 1/6] Add Reverso foundation model for zero-shot time series forecasting Reverso is a lightweight (~3M params) foundation model combining long convolutions with DeltaNet linear attention. Includes torch-native implementation, HuggingFace integration, and unit tests for all three variants (nano, small, full). Closes #3034 --- CHANGELOG.md | 1 + darts/models/__init__.py | 5 +- darts/models/components/reverso_submodels.py | 386 +++++++++++++ darts/models/forecasting/__init__.py | 1 + darts/models/forecasting/reverso_model.py | 532 ++++++++++++++++++ .../reverso/tiny_reverso_full/config.json | 15 + .../tiny_reverso_full/model.safetensors | Bin 0 -> 36940 bytes .../reverso/tiny_reverso_nano/config.json | 15 + .../tiny_reverso_nano/model.safetensors | Bin 0 -> 10828 bytes .../reverso/tiny_reverso_small/config.json | 15 + .../tiny_reverso_small/model.safetensors | Bin 0 -> 18900 bytes .../models/forecasting/test_foundation.py | 137 ++++- 12 files changed, 1105 insertions(+), 2 deletions(-) create mode 100644 darts/models/components/reverso_submodels.py create mode 100644 darts/models/forecasting/reverso_model.py create mode 100644 darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_full/config.json create mode 100644 darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_full/model.safetensors create mode 100644 darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_nano/config.json create mode 100644 darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_nano/model.safetensors create mode 100644 darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_small/config.json create mode 100644 darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_small/model.safetensors diff --git a/CHANGELOG.md b/CHANGELOG.md index d8291bf29e..2120abd08d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ but cannot always guarantee backwards compatibility. Changes that may **break co ### For users of the library: +- Added `ReversoModel`, a new foundation model for zero-shot time series forecasting. Reverso is a highly parameter-efficient model (600K-3M params) that matches accuracy of models 100x its size. [#3034](https://github.com/unit8co/darts/issues/3034) by [Xinghong Fu](https://github.com/shinfxh). - Added native multi-quantile support for `CatBoostModel` by using CatBoost’s `MultiQuantile` loss for faster training and inference. Set `likelihood="multiquantile"` to enable this feature. [#3032](https://github.com/unit8co/darts/pull/3032) by [Zhihao Dai](https://github.com/daidahao) **Fixed** diff --git a/darts/models/__init__.py b/darts/models/__init__.py index ef933b8876..763c224cfd 100644 --- a/darts/models/__init__.py +++ b/darts/models/__init__.py @@ -5,7 +5,7 @@ A comprehensive collection of forecasting and filtering models, including baseline models (NaiveSeasonal, NaiveMovingAverage, ...), statistical models (ARIMA, exponential smoothing, ...), machine learning models (LightGBM, CatBoost, sklearn-based, ...), neural network models (RNN, -N-BEATS, TiDE...), and foundation models (Chronos-2, TimesFM 2.5). +N-BEATS, TiDE...), and foundation models (Chronos-2, Reverso, TimesFM 2.5). """ from darts.logging import get_logger @@ -89,9 +89,11 @@ try: from darts.models.forecasting.chronos2_model import Chronos2Model + from darts.models.forecasting.reverso_model import ReversoModel from darts.models.forecasting.timesfm2p5_model import TimesFM2p5Model except ModuleNotFoundError: Chronos2Model = NotImportedModule(module_name="(Py)Torch", warn=False) + ReversoModel = NotImportedModule(module_name="(Py)Torch", warn=False) TimesFM2p5Model = NotImportedModule(module_name="(Py)Torch", warn=False) try: @@ -210,6 +212,7 @@ "ConformalNaiveModel", "ConformalQRModel", "Chronos2Model", + "ReversoModel", "TimesFM2p5Model", "NeuralForecastModel", ] diff --git a/darts/models/components/reverso_submodels.py b/darts/models/components/reverso_submodels.py new file mode 100644 index 0000000000..6c636dec8e --- /dev/null +++ b/darts/models/components/reverso_submodels.py @@ -0,0 +1,386 @@ +""" +Reverso Submodels +----------------- + +--- +title: Reverso Submodels +summary: This module contains the submodules used in the Reverso model. +--- + +# License and Attribution + +MIT License from https://github.com/shinfxh/reverso/blob/main/LICENSE + +Copyright (c) 2026 Xinghong Fu, Yanhong Li, Georgios Papaioannou, Yoon Kim + +Ported from https://github.com/shinfxh/reverso (reverso_torch/model.py and reverso/model.py). + +# Modifications for Darts + +Adapted for Darts with custom `PLForecastingModule` and `FoundationModel` integration: +- Remove autoregressive rollout logic (handled by `PLForecastingModule`). +- Replace FlashFFTConv with FFT-based circular convolution (pure PyTorch). +- Replace fla.layers.DeltaNet with pure-PyTorch delta-rule linear attention. +- Add `_PositionalEmbedding` for `use_output_pe` support (from reverso/model.py). +- Prefix all class names with `_` for internal use. +""" + +import math + +import torch +import torch.nn.functional as F +from torch import nn + +from darts.logging import get_logger + +logger = get_logger(__name__) + + +class _RMSNorm(nn.Module): + """RMS normalization matching fla.modules.layernorm.RMSNorm weight layout.""" + + def __init__(self, hidden_size: int, eps: float = 1e-5): + super().__init__() + self.eps = eps + self.weight = nn.Parameter(torch.ones(hidden_size)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + dtype = x.dtype + x = x.float() + rms = x.pow(2).mean(-1, keepdim=True).add(self.eps).rsqrt() + return (x * rms * self.weight.float()).to(dtype) + + +class _PositionalEmbedding(nn.Module): + """Sinusoidal positional embedding for the output decoder head.""" + + def __init__(self, d_model: int, max_len: int = 6500): + super().__init__() + pe = torch.zeros(max_len, d_model).float() + position = torch.arange(0, max_len).float().unsqueeze(1) + div_term = ( + torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model) + ).exp() + pe[:, 0::2] = torch.sin(position * div_term) + pe[:, 1::2] = torch.cos(position * div_term) + pe = pe.unsqueeze(0) + self.register_buffer("pe", pe) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.pe[:, : x.size(1)] + + +class _Gating(nn.Module): + """Gated short convolution block.""" + + def __init__(self, channels: int, temporal_kernel: int = 3): + super().__init__() + self.net = nn.Sequential( + nn.Conv1d( + channels, + channels, + kernel_size=temporal_kernel, + padding=temporal_kernel // 2, + groups=channels, + ), + nn.SiLU(), + nn.Conv1d(channels, channels, kernel_size=1), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.sigmoid(self.net(x)) + + +class _MLPBlock(nn.Module): + """Feed-forward block with skip connection and LayerNorm.""" + + def __init__(self, d_in: int, d_out: int, d_intermediate: int = 0): + super().__init__() + self.norm = nn.LayerNorm(d_out) + if d_intermediate and d_intermediate > 0: + self.linear = nn.Linear(d_in, d_intermediate) + self.linear_final = nn.Linear(d_intermediate, d_out) + else: + self.linear = nn.Linear(d_in, d_out) + self.linear_final = nn.Identity() + self.activation = nn.ReLU() + self.skip_linear = ( + nn.Linear(d_in, d_out) if d_in != d_out else nn.Identity() + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if x.ndim == 3: + x = x.permute(0, 2, 1) + residual = self.skip_linear(x) + y = self.linear(x) + y = self.activation(y) + y = self.linear_final(y) + y = self.norm(y) + y = residual + y + if y.ndim == 3: + y = y.permute(0, 2, 1) + return y + + +class _CNNBlock(nn.Module): + """Long convolution via FFT (replaces FlashFFTConv).""" + + def __init__(self, channels: int, seq_len: int, gating_kernel_size: int = 3): + super().__init__() + self.seq_len = seq_len + self.k = nn.Parameter(torch.randn(channels, seq_len, dtype=torch.float32)) + self.pregate = _Gating(channels, gating_kernel_size) + self.activation = nn.ReLU() + self.norm = nn.LayerNorm(channels) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + residual = x + x_conv = x.contiguous().to(torch.bfloat16) + pregate = self.pregate(x_conv.float()).to(x_conv.dtype) + x_gated = (pregate * x_conv).float() + + # Circular convolution via FFT (matches FlashFFTConv behaviour) + X = torch.fft.rfft(x_gated, n=self.seq_len, dim=-1) + K = torch.fft.rfft(self.k.float(), n=self.seq_len, dim=-1) + out = torch.fft.irfft(X * K.unsqueeze(0), n=self.seq_len, dim=-1) + + out = self.activation(out) + out = out.transpose(1, 2) + out = self.norm(out) + out = out.transpose(1, 2) + out = out + residual + return out + + +class _TorchDeltaNet(nn.Module): + """Pure-PyTorch delta-rule linear attention. + + Weight-compatible with ``fla.layers.DeltaNet`` (same parameter names and shapes) + so that pre-trained checkpoints load directly. + """ + + def __init__( + self, + d_model: int | None = None, + hidden_size: int = 1024, + mode: str = "chunk", + expand_k: float = 1.0, + expand_v: float = 1.0, + num_heads: int = 4, + use_beta: bool = True, + use_gate: bool = False, + use_short_conv: bool = True, + conv_size: int = 4, + conv_bias: bool = False, + allow_neg_eigval: bool = False, + qk_activation: str = "silu", + qk_norm: str = "l2", + norm_eps: float = 1e-5, + **kwargs, + ): + super().__init__() + + if d_model is not None: + hidden_size = d_model + self.hidden_size = hidden_size + self.num_heads = num_heads + self.key_dim = int(hidden_size * expand_k) + self.value_dim = int(hidden_size * expand_v) + self.head_k_dim = self.key_dim // num_heads + self.head_v_dim = self.value_dim // num_heads + self.use_beta = use_beta + self.use_gate = use_gate + self.use_short_conv = use_short_conv + self.allow_neg_eigval = allow_neg_eigval + self.qk_activation = qk_activation + self.qk_norm = qk_norm + + # projections (match fla naming) + self.q_proj = nn.Linear(hidden_size, self.key_dim, bias=False) + self.k_proj = nn.Linear(hidden_size, self.key_dim, bias=False) + self.v_proj = nn.Linear(hidden_size, self.value_dim, bias=False) + + if use_short_conv: + self.q_conv1d = nn.Conv1d( + self.key_dim, + self.key_dim, + conv_size, + padding=conv_size - 1, + groups=self.key_dim, + bias=conv_bias, + ) + self.k_conv1d = nn.Conv1d( + self.key_dim, + self.key_dim, + conv_size, + padding=conv_size - 1, + groups=self.key_dim, + bias=conv_bias, + ) + self.v_conv1d = nn.Conv1d( + self.value_dim, + self.value_dim, + conv_size, + padding=conv_size - 1, + groups=self.value_dim, + bias=conv_bias, + ) + + if use_beta: + self.b_proj = nn.Linear(hidden_size, num_heads, bias=False) + + self.o_norm = _RMSNorm(self.head_v_dim, eps=norm_eps) + self.o_proj = nn.Linear(self.value_dim, hidden_size, bias=False) + + def _causal_conv1d( + self, x: torch.Tensor, conv: nn.Conv1d, apply_silu: bool = True + ) -> torch.Tensor: + """Causal depthwise conv1d: (B, L, D) -> (B, L, D).""" + y = conv(x.transpose(1, 2)) # (B, D, L + pad) + y = y[..., : x.shape[1]].transpose(1, 2) # truncate to causal + if apply_silu: + y = F.silu(y) + return y + + @staticmethod + def _delta_rule_recurrent( + q: torch.Tensor, # (B, H, L, K) + k: torch.Tensor, # (B, H, L, K) + v: torch.Tensor, # (B, H, L, V) + beta: torch.Tensor, # (B, H, L) + ) -> torch.Tensor: + B, H, L, K = q.shape + V = v.shape[-1] + device, dtype = q.device, q.dtype + + h = q.new_zeros(B, H, K, V) # recurrent state + o = torch.empty(B, H, L, V, device=device, dtype=dtype) + + for t in range(L): + k_t = k[:, :, t] # (B, H, K) + v_t = v[:, :, t] # (B, H, V) + q_t = q[:, :, t] # (B, H, K) + b_t = beta[:, :, t, None, None] # (B, H, 1, 1) + + kv = k_t.unsqueeze(-1) * v_t.unsqueeze(-2) # (B, H, K, V) + kk = k_t.unsqueeze(-1) * k_t.unsqueeze(-2) # (B, H, K, K) + + # delta rule: h = h + β (kv − kk @ h) + h = h + b_t * (kv - torch.matmul(kk, h)) + + # read: o_t = h^T q_t + o[:, :, t] = torch.einsum("bhkv,bhk->bhv", h, q_t) + + return o # (B, H, L, V) + + def forward( + self, hidden_states: torch.Tensor, attention_mask=None, **kwargs + ) -> tuple[torch.Tensor, None, None]: + B, L, _ = hidden_states.shape + + if self.use_short_conv: + q = self._causal_conv1d( + self.q_proj(hidden_states), + self.q_conv1d, + apply_silu=(self.qk_activation == "silu"), + ) + k = self._causal_conv1d( + self.k_proj(hidden_states), + self.k_conv1d, + apply_silu=(self.qk_activation == "silu"), + ) + v = self._causal_conv1d( + self.v_proj(hidden_states), self.v_conv1d, apply_silu=True + ) + else: + q = self.q_proj(hidden_states) + k = self.k_proj(hidden_states) + v = self.v_proj(hidden_states) + if self.qk_activation == "silu": + q = F.silu(q) + k = F.silu(k) + v = F.silu(v) + + # reshape to multi-head: (B, L, H, D) + q = q.view(B, L, self.num_heads, self.head_k_dim) + k = k.view(B, L, self.num_heads, self.head_k_dim) + v = v.view(B, L, self.num_heads, self.head_v_dim) + + # L2 normalization per head + if self.qk_norm == "l2": + q = q / (q.norm(2, dim=-1, keepdim=True).pow(2).add(1e-6)).sqrt() + k = k / (k.norm(2, dim=-1, keepdim=True).pow(2).add(1e-6)).sqrt() + + # beta + if self.use_beta: + beta = self.b_proj(hidden_states).sigmoid() # (B, L, H) + else: + beta = q.new_ones(B, L, self.num_heads) + if self.allow_neg_eigval: + beta = beta * 2.0 + + # -> (B, H, L, D) + q = q.permute(0, 2, 1, 3) + k = k.permute(0, 2, 1, 3) + v = v.permute(0, 2, 1, 3) + beta = beta.permute(0, 2, 1) # (B, H, L) + + q = q * (self.head_k_dim**-0.5) + + o = self._delta_rule_recurrent(q, k, v, beta) # (B, H, L, V) + + # -> (B, L, H, V) then RMSNorm per head + o = o.permute(0, 2, 1, 3) + o = self.o_norm(o) + + # merge heads and project + o = o.reshape(B, L, self.value_dim) + o = self.o_proj(o) + return o, None, None + + +class _AttentionBlock(nn.Module): + """DeltaNet attention block with optional state weaving.""" + + def __init__( + self, + d_model: int, + expand_v: float, + state_weaving: bool = False, + is_intermediate: bool = False, + ): + super().__init__() + self.state_weaving = state_weaving + self.is_intermediate = is_intermediate + self.attention = _TorchDeltaNet( + mode="chunk", + d_model=d_model, + expand_k=1.0, + expand_v=expand_v, + num_heads=4, + use_beta=True, + use_gate=False, + use_short_conv=True, + conv_size=4, + allow_neg_eigval=False, + qk_activation="silu", + qk_norm="l2", + layer_idx=0, + ) + self.norm = nn.LayerNorm(d_model) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x_t = x.transpose(1, 2) + residual = x_t + if self.state_weaving and self.is_intermediate: + x_t = x_t.clone() + x_t[:, 0:1, :] = x_t[:, 0:1, :] + x_t[:, -1:, :] + attn_out = self.attention(hidden_states=x_t, attention_mask=None) + if isinstance(attn_out, tuple): + out = attn_out[0] + else: + out = attn_out + out = self.norm(out) + out = out + residual + out = out.transpose(1, 2) + return out diff --git a/darts/models/forecasting/__init__.py b/darts/models/forecasting/__init__.py index a29ff6076b..02a773cfd4 100644 --- a/darts/models/forecasting/__init__.py +++ b/darts/models/forecasting/__init__.py @@ -54,6 +54,7 @@ - :class:`~darts.models.forecasting.nf_model.NeuralForecastModel` Foundation Models (`GlobalForecastingModel `__) - :class:`~darts.models.forecasting.chronos2_model.Chronos2Model` + - :class:`~darts.models.forecasting.reverso_model.ReversoModel` - :class:`~darts.models.forecasting.timesfm2p5_model.TimesFM2p5Model` Ensemble Models (`GlobalForecastingModel `__) - :class:`~darts.models.forecasting.baselines.NaiveEnsembleModel` diff --git a/darts/models/forecasting/reverso_model.py b/darts/models/forecasting/reverso_model.py new file mode 100644 index 0000000000..fd14b55c60 --- /dev/null +++ b/darts/models/forecasting/reverso_model.py @@ -0,0 +1,532 @@ +""" +Reverso +------- + +Reverso is a highly parameter efficient model that achieves comparable performance with models 100x its size. + +A combination of long convolutions and DeltaNet sequence mixing modules are used. + +Reverso can be used the same way as other foundation models (e.g. Chronos2, TimesFM 2.5), with the exception +that it does not yet support any type of covariates or probabilistic forecasts. + +For detailed examples and tutorials, check out the Chronos2 notebook: + +* `Chronos-2 Foundation Model Examples + `__ +* `Fine-Tuning Examples + `__ +""" + +import os +from typing import Any + +import torch +import torch.nn.functional as F +from torch import nn + +from darts.logging import get_logger, raise_log +from darts.models.components.huggingface_connector import HuggingFaceConnector +from darts.models.components.reverso_submodels import ( + _AttentionBlock, + _CNNBlock, + _MLPBlock, + _PositionalEmbedding, +) +from darts.models.forecasting.foundation_model import FoundationModel +from darts.models.forecasting.pl_forecasting_module import ( + PLForecastingModule, + io_processor, +) +from darts.utils.data.torch_datasets.utils import PLModuleInput, TorchTrainingSample + +logger = get_logger(__name__) + + +class _ReversoModule(PLForecastingModule): + def __init__( + self, + seq_len: int = 2048, + input_token_len: int = 2048, + output_token_len: int = 48, + d_model: int = 64, + d_intermediate: int = 256, + output_bottleneck_dim: int = 48, + expand_v: float = 1.0, + state_weaving: int | bool = False, + gating_kernel_size: int = 3, + main_module: str = "conv,attn,conv,attn", + use_norm: int | bool = True, + learn_bias: int | bool = True, + use_output_pe: int | bool = False, + **kwargs, + ): + """PyTorch module implementing the Reverso model, ported from + `shinfxh/reverso `_ and + adapted for Darts :class:`PLForecastingModule` interface. + + Parameters + ---------- + seq_len + Context window length. + input_token_len + Input sequence length (must equal seq_len). + output_token_len + Number of time steps predicted per forward pass. + d_model + Model embedding dimension. + d_intermediate + MLP hidden dimension. + output_bottleneck_dim + Bottleneck dimension in the decoder head. + expand_v + Value dimension expansion factor for DeltaNet. + state_weaving + Whether to use state weaving in intermediate attention blocks. + gating_kernel_size + Kernel size for gating convolutions. + main_module + Comma-separated layer types, e.g. "conv,attn,conv,attn". + use_norm + Whether to apply min-max normalization to inputs. + learn_bias + Whether to use bias in the decoder head linear layer. + use_output_pe + Whether to use positional embeddings in the decoder head. + **kwargs + All parameters required for :class:`PLForecastingModule` base class. + """ + kwargs.pop("enable_finetuning", False) + super().__init__(**kwargs) + + self.seq_len = seq_len + self.input_token_len = input_token_len + self.output_token_len = output_token_len + self.d_model = d_model + self.use_norm = bool(use_norm) + self.use_output_pe = bool(use_output_pe) + + # embedding + self.embedding = nn.Linear(1, d_model, bias=False) + + # build encoder layers + state_weaving = bool(state_weaving) + module_list = [m.strip() for m in main_module.split(",")] + e_layers = len(module_list) + + layers = [] + for i, layer_type in enumerate(module_list): + if layer_type == "conv": + layers.append(_CNNBlock(d_model, seq_len, gating_kernel_size)) + elif layer_type == "attn": + is_intermediate = (i > 0) and (i < e_layers - 1) + layers.append( + _AttentionBlock(d_model, expand_v, state_weaving, is_intermediate) + ) + else: + raise_log( + ValueError(f"Invalid layer type: {layer_type}"), + logger, + ) + layers.append(_MLPBlock(d_model, d_model, d_intermediate)) + self.layers = nn.Sequential(*layers) + + # decoder head + self.head = nn.Linear( + input_token_len, output_bottleneck_dim, bias=bool(learn_bias) + ) + self.simple_q_proj = nn.Linear(d_model, d_model) + self.key_proj = nn.Linear(d_model, d_model) + self.value_proj = nn.Linear(d_model, d_model) + self.out_proj = nn.Linear(d_model, 1) + + # optional positional embedding for decoder head (used by full Reverso model) + if self.use_output_pe: + pe_max_len = seq_len + output_token_len + self.output_position_embedding = _PositionalEmbedding( + d_model, max_len=pe_max_len + ) + self.post_pe_q_proj = nn.Linear(d_model, d_model) + + # slice for output_chunk_shift / output_chunk_length + self.future_slice = slice( + self.output_chunk_shift, + self.output_chunk_shift + (self.output_chunk_length or 0), + ) + + def _reverso_forward(self, x: torch.Tensor) -> torch.Tensor: + """Core Reverso forward pass. + + Parameters + ---------- + x + Input tensor of shape (batch, seq_len, 1). + + Returns + ------- + torch.Tensor + Predictions of shape (batch, output_token_len, 1). + """ + # min-max normalization + if self.use_norm: + x_min = x.min(1, keepdim=True)[0].detach() + x_max = x.max(1, keepdim=True)[0].detach() + x_range = torch.clamp(x_max - x_min, min=1e-5).detach() + x = (x - x_min) / x_range + means = x_min + stdev = x_range + + # embedding: (B, seq_len, 1) -> (B, d_model, seq_len) + x = self.embedding(x).transpose(1, 2) + + # encoder layers + dec_out = self.layers(x) + + # decoder head + temp_out = self.head(dec_out).permute(0, 2, 1) + q = self.simple_q_proj(temp_out) + + dec_out_perm = dec_out.permute(0, 2, 1) + + if self.use_output_pe: + full_hidden = torch.cat([dec_out_perm, q], dim=1) + full_hidden = full_hidden + self.output_position_embedding(full_hidden) + dec_out_pe = full_hidden[:, : dec_out_perm.shape[1], :] + q = self.post_pe_q_proj(full_hidden[:, dec_out_perm.shape[1] :, :]) + k = self.key_proj(dec_out_pe) + v = self.value_proj(dec_out_pe) + else: + k = self.key_proj(dec_out_perm) + v = self.value_proj(dec_out_perm) + + attn = F.scaled_dot_product_attention(q, k, v) + dec_out = self.out_proj(attn) + + # inverse normalization + if self.use_norm: + dec_out = dec_out * stdev + means + + return dec_out + + @io_processor + def forward(self, x_in: PLModuleInput, *args, **kwargs) -> Any: + """Reverso model forward pass. + + Parameters + ---------- + x_in + Comes as tuple ``(x_past, x_future, x_static)`` where ``x_past`` is the + input/past chunk. Input dimensions are ``(n_samples, n_time_steps, n_variables)``. + + Returns + ------- + torch.Tensor + The output tensor of shape ``(n_samples, n_time_steps, n_targets, 1)`` + for deterministic forecasts. + """ + # B: batch size, L: input chunk length, C: target components + x_past, _, _ = x_in + B, L, C = x_past.shape + + # channel independence: (B, L, C) -> (B*C, L) + x = x_past.permute(0, 2, 1).reshape(-1, L) + + # left-pad with per-series first value to seq_len + if L < self.seq_len: + first_val = x[:, :1] # (B*C, 1) + x = torch.cat( + [first_val.expand(-1, self.seq_len - L), x], dim=1 + ) # (B*C, seq_len) + + # (B*C, seq_len) -> (B*C, seq_len, 1) + x = x.unsqueeze(-1) + + # core forward pass -> (B*C, output_token_len, 1) + out = self._reverso_forward(x) + + # reshape back: (B*C, T, 1) -> (B, C, T, 1) -> (B, T, C, 1) + out = out.reshape(B, C, self.output_token_len, 1) + out = out.permute(0, 2, 1, 3) + + # truncate to output_chunk_length with output_chunk_shift + out = out[:, self.future_slice, :, :] + + return out + + +class ReversoModel(FoundationModel): + def __init__( + self, + input_chunk_length: int, + output_chunk_length: int, + output_chunk_shift: int = 0, + hub_model_name: str = "shinfxh/reverso-small", + hub_model_revision: str | None = None, + local_dir: str | os.PathLike | None = None, + **kwargs, + ): + """Reverso Model for zero-shot forecasting. + + This is an implementation of the Reverso model, ported from + `shinfxh/reverso `_ with adaptations to use the Darts API. + Reverso is an efficient time-series foundation model combining long convolutions with + DeltaNet (delta-rule linear attention) layers. With approximately 3 million parameters, + it achieves performance parity with foundation models over 100x its size. + + This model supports either univariate or multivariate time series, but does not support covariates + or probabilistic forecasts. For multivariate time series, the model is applied independently to each + component. + + Using this model will automatically download and cache the pre-trained model from HuggingFace Hub. + Alternatively, you can specify a local directory containing the model config and weights using the + ``local_dir`` parameter. + + .. tip:: + You can perform full or partial fine-tuning of the model by setting the ``enable_finetuning`` parameter. + Read more in the parameter description below and in the `Fine-Tuning Examples + `__. + + Parameters + ---------- + input_chunk_length + Number of time steps in the past to take as a model input (per chunk). Applies to the target + series. For Reverso, ``input_chunk_length`` must be less than or equal to the model's context + length (2048 for all Reverso variants). + output_chunk_length + Number of time steps predicted at once (per chunk) by the internal model. It is not the same + as forecast horizon ``n`` used in ``predict()``, which is the desired number of prediction points + generated using either a one-shot- or autoregressive forecast. Setting ``n <= output_chunk_length`` + prevents auto-regression. + For Reverso, ``output_chunk_length + output_chunk_shift`` must be less than or equal to the + model's output token length (48 for all Reverso variants). + output_chunk_shift + Optionally, the number of steps to shift the start of the output chunk into the future (relative to the + input chunk end). This will create a gap between the input and output. Predictions will start + ``output_chunk_shift`` steps after the end of the target ``series``. If ``output_chunk_shift`` is set, + the model cannot generate autoregressive predictions (``n > output_chunk_length``). + hub_model_name + The model ID on HuggingFace Hub. Default: ``"shinfxh/reverso-small"``. + hub_model_revision + The model version to use. This can be a branch name, tag name, or commit hash. + local_dir + Optional local directory to load the pre-downloaded model. If specified and the directory is empty, the + model will be downloaded from HuggingFace Hub and saved to this directory. Default is ``None``, which will + use a cache directory managed by ``huggingface_hub`` instead. Note that this is different from the + ``work_dir`` parameter used for saving model checkpoints during fine-tuning. + **kwargs + Optional arguments to initialize the pytorch_lightning.Module, pytorch_lightning.Trainer, and + Darts' :class:`TorchForecastingModel`. + + loss_fn + PyTorch loss function used for fine-tuning. Default: ``nn.MSELoss()``. + torch_metrics + A torch metric or a ``MetricCollection`` used for evaluation. A full list of available metrics can be found + at https://torchmetrics.readthedocs.io/en/latest/. Default: ``None``. + optimizer_cls + The PyTorch optimizer class to be used. Default: ``torch.optim.Adam``. + optimizer_kwargs + Optionally, some keyword arguments for the PyTorch optimizer (e.g., ``{'lr': 1e-3}`` + for specifying a learning rate). Otherwise, the default values of the selected ``optimizer_cls`` + will be used. Default: ``None``. + lr_scheduler_cls + Optionally, the PyTorch learning rate scheduler class to be used. Specifying ``None`` corresponds + to using a constant learning rate. Default: ``None``. + lr_scheduler_kwargs + Optionally, some keyword arguments for the PyTorch learning rate scheduler. Default: ``None``. + batch_size + Number of time series (input and output sequences) used in each training pass. Default: ``32``. + n_epochs + Number of epochs over which to train the model. Default: ``100``. + model_name + Name of the model. Used for creating checkpoints and saving tensorboard data. If not specified, + defaults to the following string ``"YYYY-mm-dd_HH_MM_SS_torch_model_run_PID"``, where the initial part + of the name is formatted with the local date and time, while PID is the processed ID (preventing models + spawned at the same time by different processes to share the same model_name). E.g., + ``"2021-06-14_09_53_32_torch_model_run_44607"``. + work_dir + Path of the working directory, where to save checkpoints and Tensorboard summaries. + Default: current working directory. + log_tensorboard + If set, use Tensorboard to log the different parameters. The logs will be located in: + ``"{work_dir}/darts_logs/{model_name}/logs/"``. Default: ``False``. + nr_epochs_val_period + Number of epochs to wait before evaluating the validation loss (if a validation + ``TimeSeries`` is passed to the :func:`fit()` method). Default: ``1``. + force_reset + If set to ``True``, any previously-existing model with the same name will be reset (all checkpoints will + be discarded). Default: ``False``. + save_checkpoints + Whether to automatically save the untrained model and checkpoints from training. + To load the model from checkpoint, call :func:`MyModelClass.load_from_checkpoint()`, where + :class:`MyModelClass` is the :class:`TorchForecastingModel` class that was used (such as :class:`TFTModel`, + :class:`NBEATSModel`, etc.). If set to ``False``, the model can still be manually saved using + :func:`save()` and loaded using :func:`load()`. Default: ``False``. + add_encoders + A large number of past and future covariates can be automatically generated with `add_encoders`. + This can be done by adding multiple pre-defined index encoders and/or custom user-made functions that + will be used as index encoders. Additionally, a transformer such as Darts' :class:`Scaler` can be added to + transform the generated covariates. This happens all under one hood and only needs to be specified at + model creation. + Read :meth:`SequentialEncoder ` to find out more about + ``add_encoders``. Default: ``None``. An example showing some of ``add_encoders`` features: + + .. highlight:: python + .. code-block:: python + + def encode_year(idx): + return (idx.year - 1950) / 50 + + add_encoders={ + 'cyclic': {'future': ['month']}, + 'datetime_attribute': {'future': ['hour', 'dayofweek']}, + 'position': {'past': ['relative'], 'future': ['relative']}, + 'custom': {'past': [encode_year]}, + 'transformer': Scaler(), + 'tz': 'CET' + } + .. + random_state + Controls the randomness of the weights initialization and reproducible forecasting. + pl_trainer_kwargs + By default :class:`TorchForecastingModel` creates a PyTorch Lightning Trainer with several useful presets + that performs the training, validation and prediction processes. These presets include automatic + checkpointing, tensorboard logging, setting the torch device and more. + With ``pl_trainer_kwargs`` you can add additional kwargs to instantiate the PyTorch Lightning trainer + object. Check the `PL Trainer documentation + `__ for more information about the + supported kwargs. Default: ``None``. + Running on GPU(s) is also possible using ``pl_trainer_kwargs`` by specifying keys ``"accelerator", + "devices", and "auto_select_gpus"``. Some examples for setting the devices inside the ``pl_trainer_kwargs`` + dict: + + - ``{"accelerator": "cpu"}`` for CPU, + - ``{"accelerator": "gpu", "devices": [i]}`` to use only GPU ``i`` (``i`` must be an integer), + - ``{"accelerator": "gpu", "devices": -1, "auto_select_gpus": True}`` to use all available GPUS. + + For more info, see here: + https://pytorch-lightning.readthedocs.io/en/stable/common/trainer.html#trainer-flags , and + https://pytorch-lightning.readthedocs.io/en/stable/accelerators/gpu_basic.html#train-on-multiple-gpus + + With parameter ``"callbacks"`` you can add custom or PyTorch-Lightning built-in callbacks to Darts' + :class:`TorchForecastingModel`. Below is an example for adding EarlyStopping to the training process. + The model will stop training early if the validation loss `val_loss` does not improve beyond + specifications. For more information on callbacks, visit: + `PyTorch Lightning Callbacks + `__ + + .. highlight:: python + .. code-block:: python + + from pytorch_lightning.callbacks.early_stopping import EarlyStopping + + # stop training when validation loss does not decrease more than 0.05 (`min_delta`) over + # a period of 5 epochs (`patience`) + my_stopper = EarlyStopping( + monitor="val_loss", + patience=5, + min_delta=0.05, + mode='min', + ) + + pl_trainer_kwargs={"callbacks": [my_stopper]} + .. + + Note that you can also use a custom PyTorch Lightning Trainer for training and prediction with optional + parameter ``trainer`` in :func:`fit()` and :func:`predict()`. + show_warnings + whether to show warnings raised from PyTorch Lightning. Useful to detect potential issues of + your forecasting use case. Default: ``False``. + enable_finetuning + Enables model fine-tuning. Only effective if not ``None``. + If a bool, specifies whether to perform full fine-tuning / training (all parameters are updated) or keep + all parameters frozen. If a dict, specifies which parameters to fine-tune. Must only contain one key-value + record. Can be used to: + + - Unfreeze specific parameters, while keeping everything else frozen: + ``{"unfreeze": ["param.name.patterns.*"]}`` + - Freeze specific parameters, while keeping everything else unfrozen: + ``{"freeze": ["param.name.patterns.*"]}`` + + Default: ``None``. + + References + ---------- + .. [1] X. Fu, Y. Li, G. Papaioannou, Y. Kim. "Reverso: Efficient Time Series Foundation Models for + Zero-shot Forecasting", 2026. arXiv https://arxiv.org/abs/2602.17634. + + Examples + -------- + >>> from darts.datasets import WeatherDataset + >>> from darts.models import ReversoModel + >>> # load data in float32 format (macOS issues with float64 and PyTorch) + >>> series = WeatherDataset().load().astype("float32") + >>> # predicting atmospheric pressure + >>> target = series['p (mbar)'][:200] + >>> model = ReversoModel( + >>> input_chunk_length=96, + >>> output_chunk_length=48, + >>> ) + >>> # calling fit is still mandatory to ensure consistent number of components; however, + >>> # ReversoModel is training-free and the model weights are not updated + >>> model.fit(target) + >>> pred = model.predict(48) + + .. note:: + Reverso is licensed under the `MIT License `_, + Copyright (c) 2026 Xinghong Fu, Yanhong Li, Georgios Papaioannou, Yoon Kim. + By using this model, you agree to the terms and conditions of the license. + .. note:: + Reverso does not support covariates natively. For multivariate time series, each component + is forecasted independently. + .. warning:: + CPU inference is significantly slower than GPU due to the use of torch Conv instead of flashfft and sequential delta-rule + computation instead of fla implementation. GPU is recommended for production use. See https://github.com/shinfxh/reverso/. + """ + hf_connector = HuggingFaceConnector( + model_name=hub_model_name, + model_revision=hub_model_revision, + local_dir=local_dir, + ) + + # load model config for validation + config = hf_connector.load_config() + + # validate input_chunk_length against model's context length + context_length = config["seq_len"] + if input_chunk_length > context_length: + raise_log( + ValueError( + f"`input_chunk_length` {input_chunk_length} cannot be greater than " + f"model's context length {context_length}" + ), + logger, + ) + + # validate output_chunk_length + output_chunk_shift against model's output length + prediction_length = config["output_token_len"] + if output_chunk_length + output_chunk_shift > prediction_length: + raise_log( + ValueError( + f"`output_chunk_length` {output_chunk_length} plus `output_chunk_shift` " + f"{output_chunk_shift} cannot be greater than model's maximum prediction " + f"length {prediction_length}" + ), + logger, + ) + + self.hf_connector = hf_connector + super().__init__(**kwargs) + + def _create_model(self, train_sample: TorchTrainingSample) -> PLForecastingModule: + pl_module_params = self.pl_module_params or {} + return self.hf_connector.load_model( + module_class=_ReversoModule, + pl_module_params=pl_module_params, + ) + + @property + def supports_past_covariates(self) -> bool: + return False + + @property + def supports_future_covariates(self) -> bool: + return False diff --git a/darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_full/config.json b/darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_full/config.json new file mode 100644 index 0000000000..14b47613d1 --- /dev/null +++ b/darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_full/config.json @@ -0,0 +1,15 @@ +{ + "seq_len": 32, + "input_token_len": 32, + "output_token_len": 8, + "d_model": 8, + "d_intermediate": 16, + "output_bottleneck_dim": 8, + "expand_v": 1.0, + "state_weaving": 1, + "gating_kernel_size": 3, + "main_module": "conv,attn,conv,attn,conv,attn,conv,attn", + "use_norm": true, + "learn_bias": 1, + "use_output_pe": true +} \ No newline at end of file diff --git a/darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_full/model.safetensors b/darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_full/model.safetensors new file mode 100644 index 0000000000000000000000000000000000000000..ee71e46adcb211fc7ec8d7f36c4a4b77f9da1f59 GIT binary patch literal 36940 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 literal 0 HcmV?d00001 diff --git a/darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_small/config.json b/darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_small/config.json new file mode 100644 index 0000000000..4dc0f5c472 --- /dev/null +++ b/darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_small/config.json @@ -0,0 +1,15 @@ +{ + "seq_len": 32, + "input_token_len": 32, + "output_token_len": 8, + "d_model": 8, + "d_intermediate": 16, + "output_bottleneck_dim": 8, + "expand_v": 1.0, + "state_weaving": 1, + "gating_kernel_size": 3, + "main_module": "conv,attn,conv,attn", + "use_norm": true, + "learn_bias": 1, + "use_output_pe": false +} \ No newline at end of file diff --git a/darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_small/model.safetensors b/darts/tests/models/forecasting/artefacts/reverso/tiny_reverso_small/model.safetensors new file mode 100644 index 0000000000000000000000000000000000000000..e6dd1fef89f4e8121f8a75b4097ff17178c9371c GIT binary patch literal 18900 zcmb`N2~Amr zd1$S1TjJsAr);)Xd9h!hubZ-&@+5sdWuEdXPuH%0%{SrcI`WhkyZX60FL!re<>t3a z@Jk(@zMkVcp0cN#>td~iOI!uN`;XuM`?vM=ct!?)ef`%01lRhT0wV*Su8xUG7sbmh z(Ajt8@}>W~kS+pGS65G`8|K%7O#ZH?i^9{@H#X`*dAkO>tz4z0qviE~>`T`c{%u)Z z1fHIO&UZzBVU{gl>GQv@t_vez{c)$dvwpe6zboqk321-ZxW8z=E8RR?{oJ&cx%ml} z`#+zAE}j5t+I`o$Lw~82r_0m-nO;-DP8yniKc_#{tM?mv1w5Xfk>2;y`yZgc*89_t z>Z0=WOiX^f^SWBzOP0C0uKa~iU8A2YUqIsNnHv14^e^16Br+eoK}w(^~k4nf;1v@UvUgh2s1{&(1||moX-8K4(;XL}c>;s{p6UJ!kRP?L`0q~3&v#sa#yo<>*GVW$+`hS@3 z|6Fc#VR%L+-46VptY3=zyBT(Y1hj7X(BOYB`ML-^i&54{$%~e z0=qy0TDKa)K&yLo_CMc)E}j6|&F23d`b(`pFZsHtJQJO6k=Gsl8zo;Ck7uH*^JDV( zcj&M6{hQIF=T_m1~LATTW7w%UQf9@1rG@glRH}m-y@^_u03nyT9 z%V__`{FT_BJ4F|hXKK*Rf((B%kh(BDQ^Rh7^e5|=;{NUwT_6FiTOb+ze=7O9usqZ5 z>E=iFKP~yX&^%LvyR&~^_;;>=-n|n3 zk^WEXzVB?o6%4v%>F!tfC;A0k0o}Npt^G*S3Eud3Z+aI;K-2FQ0)NteDe&(e z|DDvu`y*K{U+wqb3?+XuTm6pc0{zjC`~~`zCf%Pk)!#8)us`-})e;|HZ#U;Zo5ugS zcKgl|&HBF5`kf^p8vSux{9V0QyZtx&_LIKrJERNsN2~Ay>Nk}MKmweg z5^!9%PU-tc;)N*}HOv&O%k^3NS(+SA;{tvR2DV8=l6w$4kxPlxz#D@U@J7Ox%Df

vNgT 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 literal 0 HcmV?d00001 diff --git a/darts/tests/models/forecasting/test_foundation.py b/darts/tests/models/forecasting/test_foundation.py index 093fb7ddd5..d3b297bd6f 100644 --- a/darts/tests/models/forecasting/test_foundation.py +++ b/darts/tests/models/forecasting/test_foundation.py @@ -18,7 +18,7 @@ allow_module_level=True, ) -from darts.models import Chronos2Model, TimesFM2p5Model +from darts.models import Chronos2Model, ReversoModel, TimesFM2p5Model def generate_series(n_variables: int, length: int, prefix: str): @@ -456,3 +456,138 @@ def test_finetuning_all_models(self, config): preds = model.predict(n=6, predict_likelihood_parameters=True) assert preds.shape == (6, self.series.n_components * len(quantiles), 1) + + +reverso_artefacts_dir = ( + Path(__file__).parent / "artefacts" / "reverso" +).absolute() + +reverso_variant_dirs = { + "shinfxh/reverso-nano": reverso_artefacts_dir / "tiny_reverso_nano", + "shinfxh/reverso-small": reverso_artefacts_dir / "tiny_reverso_small", + "shinfxh/reverso-darts": reverso_artefacts_dir / "tiny_reverso_full", +} + +# default variant for tests that don't parametrize +reverso_local_dir = reverso_variant_dirs["shinfxh/reverso-small"] + + +def reverso_mock_download( + repo_id: str, + filename: str, + revision: str | None, + local_dir: str | Path | None, + **kwargs, +): + variant_dir = reverso_variant_dirs.get(repo_id, reverso_local_dir) + path = variant_dir / filename + if local_dir is None: + return str(path) + else: + dest_path = Path(local_dir) / filename + shutil.copy(path, dest_path) + return str(dest_path) + + +class TestReversoModel: + series = generate_series(n_variables=2, length=100, prefix="A") + + @patch( + "darts.models.components.huggingface_connector.hf_hub_download", + side_effect=reverso_mock_download, + ) + @pytest.mark.parametrize( + "hub_model_name", + ["shinfxh/reverso-nano", "shinfxh/reverso-small", "shinfxh/reverso-darts"], + ) + def test_all_variants(self, mock_method, hub_model_name): + """Test that all three Reverso variants can load, fit, and predict.""" + model = ReversoModel( + input_chunk_length=12, + output_chunk_length=6, + hub_model_name=hub_model_name, + **tfm_kwargs, + ) + mock_method.assert_called() + + with patch("pytorch_lightning.Trainer.fit") as mock_fit: + model.fit(series=self.series) + mock_fit.assert_not_called() + + assert model.model_created + assert not model.supports_probabilistic_prediction + + pred = model.predict(n=6) + assert isinstance(pred, TimeSeries) + assert len(pred) == 6 + assert pred.n_components == self.series.n_components + + @patch( + "darts.models.components.huggingface_connector.hf_hub_download", + side_effect=reverso_mock_download, + ) + def test_invalid_params(self, mock_method): + # input_chunk_length exceeds model's seq_len (32) + with pytest.raises(ValueError, match="cannot be greater than"): + _ = ReversoModel( + input_chunk_length=64, + output_chunk_length=6, + **tfm_kwargs, + ) + + # output_chunk_length exceeds model's output_token_len (8) + with pytest.raises(ValueError, match="cannot be greater than"): + _ = ReversoModel( + input_chunk_length=12, + output_chunk_length=10, + **tfm_kwargs, + ) + + @patch( + "darts.models.components.huggingface_connector.hf_hub_download", + side_effect=reverso_mock_download, + ) + def test_local_dir(self, mock_method): + model = ReversoModel( + input_chunk_length=12, + output_chunk_length=6, + local_dir=reverso_local_dir, + **tfm_kwargs, + ) + model.fit(series=self.series) + pred = model.predict(n=6) + assert isinstance(pred, TimeSeries) + assert len(pred) == 6 + assert pred.n_components == self.series.n_components + + @patch( + "darts.models.components.huggingface_connector.hf_hub_download", + side_effect=reverso_mock_download, + ) + def test_no_covariates(self, mock_method): + model = ReversoModel( + input_chunk_length=12, + output_chunk_length=6, + **tfm_kwargs, + ) + assert model.supports_past_covariates is False + assert model.supports_future_covariates is False + + @patch( + "darts.models.components.huggingface_connector.hf_hub_download", + side_effect=reverso_mock_download, + ) + def test_autoregressive_prediction(self, mock_method): + """Test that predictions with n > output_chunk_length work via autoregression.""" + model = ReversoModel( + input_chunk_length=12, + output_chunk_length=6, + **tfm_kwargs, + ) + model.fit(series=self.series) + + # n=12 > output_chunk_length=6, requires autoregression + pred = model.predict(n=12) + assert isinstance(pred, TimeSeries) + assert len(pred) == 12 + assert pred.n_components == self.series.n_components From ed5ec9c0075ec5d94d7b414a214f0772067a7aa3 Mon Sep 17 00:00:00 2001 From: shinfxh Date: Mon, 6 Apr 2026 17:19:58 -0400 Subject: [PATCH 2/6] add chunking. recommended chunk sizes: nano 512, small 128, base 64 --- darts/models/components/reverso_submodels.py | 88 ++++++++++++++++---- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/darts/models/components/reverso_submodels.py b/darts/models/components/reverso_submodels.py index 6c636dec8e..c744ee5951 100644 --- a/darts/models/components/reverso_submodels.py +++ b/darts/models/components/reverso_submodels.py @@ -21,7 +21,6 @@ - Remove autoregressive rollout logic (handled by `PLForecastingModule`). - Replace FlashFFTConv with FFT-based circular convolution (pure PyTorch). - Replace fla.layers.DeltaNet with pure-PyTorch delta-rule linear attention. -- Add `_PositionalEmbedding` for `use_output_pe` support (from reverso/model.py). - Prefix all class names with `_` for internal use. """ @@ -249,27 +248,82 @@ def _delta_rule_recurrent( v: torch.Tensor, # (B, H, L, V) beta: torch.Tensor, # (B, H, L) ) -> torch.Tensor: + """Chunked parallel-scan delta rule. + + Replaces the naive step-by-step recurrence with a Hillis-Steele + parallel prefix scan *within* fixed-size chunks, while propagating + the recurrent state sequentially *across* chunks. This is + numerically equivalent (max diff ~4e-7 in float32) but 2-9x faster + on CPU because each scan step is a single batched matmul instead of + a Python for-loop over time steps. + + The chunk size is chosen automatically based on K (head dimension) + to balance the O(K^2) scan matmul cost against Python-loop overhead. + """ B, H, L, K = q.shape V = v.shape[-1] device, dtype = q.device, q.dtype - h = q.new_zeros(B, H, K, V) # recurrent state - o = torch.empty(B, H, L, V, device=device, dtype=dtype) - - for t in range(L): - k_t = k[:, :, t] # (B, H, K) - v_t = v[:, :, t] # (B, H, V) - q_t = q[:, :, t] # (B, H, K) - b_t = beta[:, :, t, None, None] # (B, H, 1, 1) - - kv = k_t.unsqueeze(-1) * v_t.unsqueeze(-2) # (B, H, K, V) - kk = k_t.unsqueeze(-1) * k_t.unsqueeze(-2) # (B, H, K, K) - - # delta rule: h = h + β (kv − kk @ h) - h = h + b_t * (kv - torch.matmul(kk, h)) + # Larger K → smaller optimal chunk (scan matmuls are O(K^2) each). + # Values determined empirically across reverso-nano/small/full. + if K <= 8: + chunk_size = 512 + elif K <= 16: + chunk_size = 128 + else: + chunk_size = 64 - # read: o_t = h^T q_t - o[:, :, t] = torch.einsum("bhkv,bhk->bhv", h, q_t) + eye = torch.eye(K, device=device, dtype=dtype) + o = torch.empty(B, H, L, V, device=device, dtype=dtype) + h = q.new_zeros(B, H, K, V) # inter-chunk recurrent state + + num_chunks = (L + chunk_size - 1) // chunk_size + for c in range(num_chunks): + start = c * chunk_size + end = min(start + chunk_size, L) + clen = end - start + + q_c = q[:, :, start:end] + k_c = k[:, :, start:end] + v_c = v[:, :, start:end] + b_c = beta[:, :, start:end] + + # Build per-step transition matrices and bias vectors: + # A_t = I - β_t k_t k_t^T (B, H, clen, K, K) + # b_t = β_t k_t v_t^T (B, H, clen, K, V) + beta_exp = b_c.unsqueeze(-1).unsqueeze(-1) + As = eye - beta_exp * (k_c.unsqueeze(-1) * k_c.unsqueeze(-2)) + bs = beta_exp * (k_c.unsqueeze(-1) * v_c.unsqueeze(-2)) + + # Hillis-Steele inclusive prefix scan within chunk. + # After ceil(log2(clen)) steps of batched matmul: + # As[t] = A_t @ A_{t-1} @ ... @ A_0 (cumulative transition) + # bs[t] = cumulative bias such that state = As[t] @ h_prev + bs[t] + scan_steps = int(math.ceil(math.log2(clen))) + for d in range(scan_steps): + stride = 2**d + if stride >= clen: + break + new_A = torch.matmul( + As[:, :, stride:], As[:, :, : clen - stride] + ) + new_b = ( + torch.matmul(As[:, :, stride:], bs[:, :, : clen - stride]) + + bs[:, :, stride:] + ) + As = torch.cat([As[:, :, :stride], new_A], dim=2) + bs = torch.cat([bs[:, :, :stride], new_b], dim=2) + + # Materialize all states: h_t = As[t] @ h_prev + bs[t] + states = torch.matmul( + As, h.unsqueeze(2).expand(-1, -1, clen, -1, -1) + ) + bs + + # Readout: o_t = q_t^T @ h_t + o[:, :, start:end] = torch.einsum("bhlk,bhlkv->bhlv", q_c, states) + + # Propagate state to next chunk + h = As[:, :, -1] @ h + bs[:, :, -1] return o # (B, H, L, V) From e229743ede96b7e6685f7d830873a19d36c9c39c Mon Sep 17 00:00:00 2001 From: shinfxh Date: Mon, 6 Apr 2026 17:32:05 -0400 Subject: [PATCH 3/6] fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2120abd08d..1c2df014ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ but cannot always guarantee backwards compatibility. Changes that may **break co ### For users of the library: -- Added `ReversoModel`, a new foundation model for zero-shot time series forecasting. Reverso is a highly parameter-efficient model (600K-3M params) that matches accuracy of models 100x its size. [#3034](https://github.com/unit8co/darts/issues/3034) by [Xinghong Fu](https://github.com/shinfxh). +- Added `ReversoModel`, a new foundation model for zero-shot time series forecasting. Reverso is a highly parameter-efficient model (200K-2.6M params) that matches accuracy of models 100x its size. [#3034](https://github.com/unit8co/darts/issues/3034) by [Xinghong Fu](https://github.com/shinfxh). - Added native multi-quantile support for `CatBoostModel` by using CatBoost’s `MultiQuantile` loss for faster training and inference. Set `likelihood="multiquantile"` to enable this feature. [#3032](https://github.com/unit8co/darts/pull/3032) by [Zhihao Dai](https://github.com/daidahao) **Fixed** From 71624f7cc42bbe4991c41e2ddfb9a72be8a4a21a Mon Sep 17 00:00:00 2001 From: shinfxh Date: Mon, 6 Apr 2026 17:38:39 -0400 Subject: [PATCH 4/6] fix formatting --- darts/models/components/reverso_submodels.py | 12 +++--------- darts/models/forecasting/reverso_model.py | 5 +++-- darts/tests/models/forecasting/test_foundation.py | 4 +--- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/darts/models/components/reverso_submodels.py b/darts/models/components/reverso_submodels.py index c744ee5951..5cb7081a1f 100644 --- a/darts/models/components/reverso_submodels.py +++ b/darts/models/components/reverso_submodels.py @@ -103,9 +103,7 @@ def __init__(self, d_in: int, d_out: int, d_intermediate: int = 0): self.linear = nn.Linear(d_in, d_out) self.linear_final = nn.Identity() self.activation = nn.ReLU() - self.skip_linear = ( - nn.Linear(d_in, d_out) if d_in != d_out else nn.Identity() - ) + self.skip_linear = nn.Linear(d_in, d_out) if d_in != d_out else nn.Identity() def forward(self, x: torch.Tensor) -> torch.Tensor: if x.ndim == 3: @@ -304,9 +302,7 @@ def _delta_rule_recurrent( stride = 2**d if stride >= clen: break - new_A = torch.matmul( - As[:, :, stride:], As[:, :, : clen - stride] - ) + new_A = torch.matmul(As[:, :, stride:], As[:, :, : clen - stride]) new_b = ( torch.matmul(As[:, :, stride:], bs[:, :, : clen - stride]) + bs[:, :, stride:] @@ -315,9 +311,7 @@ def _delta_rule_recurrent( bs = torch.cat([bs[:, :, :stride], new_b], dim=2) # Materialize all states: h_t = As[t] @ h_prev + bs[t] - states = torch.matmul( - As, h.unsqueeze(2).expand(-1, -1, clen, -1, -1) - ) + bs + states = torch.matmul(As, h.unsqueeze(2).expand(-1, -1, clen, -1, -1)) + bs # Readout: o_t = q_t^T @ h_t o[:, :, start:end] = torch.einsum("bhlk,bhlkv->bhlv", q_c, states) diff --git a/darts/models/forecasting/reverso_model.py b/darts/models/forecasting/reverso_model.py index fd14b55c60..f9bc2116ef 100644 --- a/darts/models/forecasting/reverso_model.py +++ b/darts/models/forecasting/reverso_model.py @@ -478,8 +478,9 @@ def encode_year(idx): Reverso does not support covariates natively. For multivariate time series, each component is forecasted independently. .. warning:: - CPU inference is significantly slower than GPU due to the use of torch Conv instead of flashfft and sequential delta-rule - computation instead of fla implementation. GPU is recommended for production use. See https://github.com/shinfxh/reverso/. + CPU inference is significantly slower than GPU due to the use of torch Conv instead of + flashfft and sequential delta-rule computation instead of fla implementation. GPU is + recommended for production use. See https://github.com/shinfxh/reverso/. """ hf_connector = HuggingFaceConnector( model_name=hub_model_name, diff --git a/darts/tests/models/forecasting/test_foundation.py b/darts/tests/models/forecasting/test_foundation.py index d3b297bd6f..65984356f1 100644 --- a/darts/tests/models/forecasting/test_foundation.py +++ b/darts/tests/models/forecasting/test_foundation.py @@ -458,9 +458,7 @@ def test_finetuning_all_models(self, config): assert preds.shape == (6, self.series.n_components * len(quantiles), 1) -reverso_artefacts_dir = ( - Path(__file__).parent / "artefacts" / "reverso" -).absolute() +reverso_artefacts_dir = (Path(__file__).parent / "artefacts" / "reverso").absolute() reverso_variant_dirs = { "shinfxh/reverso-nano": reverso_artefacts_dir / "tiny_reverso_nano", From f08f4195cd687a99d86531098c8509bb83be5d04 Mon Sep 17 00:00:00 2001 From: shinfxh Date: Mon, 6 Apr 2026 18:01:59 -0400 Subject: [PATCH 5/6] model links --- darts/models/forecasting/reverso_model.py | 9 +++++++++ darts/tests/models/forecasting/test_foundation.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/darts/models/forecasting/reverso_model.py b/darts/models/forecasting/reverso_model.py index f9bc2116ef..d69c81ae43 100644 --- a/darts/models/forecasting/reverso_model.py +++ b/darts/models/forecasting/reverso_model.py @@ -280,6 +280,14 @@ def __init__( Alternatively, you can specify a local directory containing the model config and weights using the ``local_dir`` parameter. + Three variants are available on HuggingFace Hub: + + - `shinfxh/reverso-nano `_: 200K parameters + - `shinfxh/reverso-small `_: 550K parameters (default) + - `shinfxh/reverso-base `_: 2.6M parameters + + To use a different variant, specify the ``hub_model_name`` parameter. + .. tip:: You can perform full or partial fine-tuning of the model by setting the ``enable_finetuning`` parameter. Read more in the parameter description below and in the `Fine-Tuning Examples @@ -305,6 +313,7 @@ def __init__( the model cannot generate autoregressive predictions (``n > output_chunk_length``). hub_model_name The model ID on HuggingFace Hub. Default: ``"shinfxh/reverso-small"``. + Other available variants: ``"shinfxh/reverso-nano"`` and ``"shinfxh/reverso-base"``. hub_model_revision The model version to use. This can be a branch name, tag name, or commit hash. local_dir diff --git a/darts/tests/models/forecasting/test_foundation.py b/darts/tests/models/forecasting/test_foundation.py index 65984356f1..4c5cbc5796 100644 --- a/darts/tests/models/forecasting/test_foundation.py +++ b/darts/tests/models/forecasting/test_foundation.py @@ -463,7 +463,7 @@ def test_finetuning_all_models(self, config): reverso_variant_dirs = { "shinfxh/reverso-nano": reverso_artefacts_dir / "tiny_reverso_nano", "shinfxh/reverso-small": reverso_artefacts_dir / "tiny_reverso_small", - "shinfxh/reverso-darts": reverso_artefacts_dir / "tiny_reverso_full", + "shinfxh/reverso-base": reverso_artefacts_dir / "tiny_reverso_full", } # default variant for tests that don't parametrize @@ -496,7 +496,7 @@ class TestReversoModel: ) @pytest.mark.parametrize( "hub_model_name", - ["shinfxh/reverso-nano", "shinfxh/reverso-small", "shinfxh/reverso-darts"], + ["shinfxh/reverso-nano", "shinfxh/reverso-small", "shinfxh/reverso-base"], ) def test_all_variants(self, mock_method, hub_model_name): """Test that all three Reverso variants can load, fit, and predict.""" From dfcf28adea824646206811b5d8903da1bc9437bd Mon Sep 17 00:00:00 2001 From: shinfxh Date: Tue, 7 Apr 2026 15:46:01 -0400 Subject: [PATCH 6/6] update documentation --- README.md | 1 + docs/source/index.rst | 6 ++++++ docs/userguide/covariates.md | 1 + 3 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 6dfc227e3f..6a09f1631f 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,7 @@ Here's a breakdown of the forecasting models currently implemented in Darts. Our | **Foundation Models**
([GlobalForecastingModel](https://unit8co.github.io/darts/userguide/covariates.html#global-forecasting-models-gfms)): No training required | | | | | | | [Chronos2Model](https://unit8co.github.io/darts/generated_api/darts.models.forecasting.chronos2_model.html#darts.models.forecasting.chronos2_model.Chronos2Model) | [Chronos-2 report](https://arxiv.org/abs/2510.15821), [Amazon blog post](https://www.amazon.science/blog/introducing-chronos-2-from-univariate-to-universal-forecasting) | ✅ ✅ | ✅ ✅ 🔴 | ✅ ✅ | ✅ | | [TimesFM2p5Model](https://unit8co.github.io/darts/generated_api/darts.models.forecasting.timesfm2p5_model.html#darts.models.forecasting.timesfm2p5_model.TimesFM2p5Model) | [TimesFM 1.0 paper](https://arxiv.org/abs/2310.10688), [Google blog post](https://research.google/blog/a-decoder-only-foundation-model-for-time-series-forecasting) | ✅ ✅ | 🔴 🔴 🔴 | ✅ ✅ | ✅ | +| [ReversoModel](https://unit8co.github.io/darts/generated_api/darts.models.forecasting.reverso_model.html#darts.models.forecasting.reverso_model.ReversoModel) | [Reverso paper](https://arxiv.org/abs/2602.17634), [GitHub](https://github.com/shinfxh/reverso) | ✅ 🔴 | 🔴 🔴 🔴 | 🔴 🔴 | ✅ | | **Ensemble Models**
([GlobalForecastingModel](https://unit8co.github.io/darts/userguide/covariates.html#global-forecasting-models-gfms)): Model support is dependent on ensembled forecasting models and the ensemble model itself | | | | | | | [NaiveEnsembleModel](https://unit8co.github.io/darts/generated_api/darts.models.forecasting.baselines.html#darts.models.forecasting.baselines.NaiveEnsembleModel) | | ✅ ✅ | ✅ ✅ ✅ | ✅ ✅ | ✅ | | [RegressionEnsembleModel](https://unit8co.github.io/darts/generated_api/darts.models.forecasting.regression_ensemble_model.html#darts.models.forecasting.regression_ensemble_model.RegressionEnsembleModel) | | ✅ ✅ | ✅ ✅ ✅ | ✅ ✅ | ✅ | diff --git a/docs/source/index.rst b/docs/source/index.rst index 900c189ae1..f0b3656903 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -675,6 +675,12 @@ Our regression models are designed to predict continuous numerical values, makin - ✅ ✅ - ✅ - `TimesFM 1.0 paper `_, `Google blog post `_ + * - `ReversoModel `_ + - ✅ 🔴 + - 🔴 🔴 🔴 + - 🔴 🔴 + - ✅ + - `Reverso paper `_, `GitHub `_ * - **Ensemble Models** (`GlobalForecastingModel `_): Model support is dependent on ensembled forecasting models and the ensemble model itself - - diff --git a/docs/userguide/covariates.md b/docs/userguide/covariates.md index 89a493134d..28db308510 100644 --- a/docs/userguide/covariates.md +++ b/docs/userguide/covariates.md @@ -159,6 +159,7 @@ GFMs are models that can be trained on multiple target (and covariate) time seri | [NeuralForecastModel](https://unit8co.github.io/darts/generated_api/darts.models.forecasting.nf_model.html#darts.models.forecasting.nf_model.NeuralForecastModel) (g) | ✅ | ✅ | ✅ | | [Chronos2Model](https://unit8co.github.io/darts/generated_api/darts.models.forecasting.chronos2_model.html#darts.models.forecasting.chronos2_model.Chronos2Model) | ✅ | ✅ | | | [TimesFM2p5Model](https://unit8co.github.io/darts/generated_api/darts.models.forecasting.timesfm2p5_model.html#darts.models.forecasting.timesfm2p5_model.TimesFM2p5Model) | | | | +| [ReversoModel](https://unit8co.github.io/darts/generated_api/darts.models.forecasting.reverso_model.html#darts.models.forecasting.reverso_model.ReversoModel) | | | | | Ensemble Models (h) | ✅ | ✅ | ✅ | | Conformal Prediction Models (i) | ✅ | ✅ | ✅ |