patchfm 2.0.1__tar.gz → 2.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {patchfm-2.0.1/src/patchfm.egg-info → patchfm-2.1.1}/PKG-INFO +1 -1
- {patchfm-2.0.1 → patchfm-2.1.1}/pyproject.toml +1 -1
- patchfm-2.1.1/src/patchfm/__init__.py +2 -0
- {patchfm-2.0.1 → patchfm-2.1.1}/src/patchfm/configs/model_config.py +4 -3
- {patchfm-2.0.1 → patchfm-2.1.1}/src/patchfm/inference/forecaster.py +37 -111
- {patchfm-2.0.1 → patchfm-2.1.1}/src/patchfm/inference/modules.py +12 -2
- {patchfm-2.0.1 → patchfm-2.1.1/src/patchfm.egg-info}/PKG-INFO +1 -1
- patchfm-2.0.1/src/patchfm/__init__.py +0 -2
- {patchfm-2.0.1 → patchfm-2.1.1}/LICENSE +0 -0
- {patchfm-2.0.1 → patchfm-2.1.1}/README.md +0 -0
- {patchfm-2.0.1 → patchfm-2.1.1}/setup.cfg +0 -0
- {patchfm-2.0.1 → patchfm-2.1.1}/src/patchfm.egg-info/SOURCES.txt +0 -0
- {patchfm-2.0.1 → patchfm-2.1.1}/src/patchfm.egg-info/dependency_links.txt +0 -0
- {patchfm-2.0.1 → patchfm-2.1.1}/src/patchfm.egg-info/requires.txt +0 -0
- {patchfm-2.0.1 → patchfm-2.1.1}/src/patchfm.egg-info/top_level.txt +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
from dataclasses import dataclass, field
|
|
1
|
+
from dataclasses import asdict, dataclass, field
|
|
2
|
+
|
|
2
3
|
|
|
3
4
|
@dataclass
|
|
4
5
|
class PatchFMConfig:
|
|
5
6
|
max_seq_len: int = 1024
|
|
6
7
|
patch_len: int = 32
|
|
7
|
-
d_model: int =
|
|
8
|
-
n_heads: int =
|
|
8
|
+
d_model: int = 1024
|
|
9
|
+
n_heads: int = 32
|
|
9
10
|
n_layers_encoder: int = 6
|
|
10
11
|
quantiles: list[float] = field(default_factory=lambda: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
|
|
11
12
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import numpy as np
|
|
1
2
|
import torch
|
|
2
3
|
import torch.nn as nn
|
|
3
4
|
from einops import rearrange
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
from patchfm.inference.modules import (CausalRevIN, PatchFM, ResidualBlock,
|
|
7
|
+
SeqTypeConverter, TransformerEncoder)
|
|
5
8
|
|
|
6
9
|
|
|
7
10
|
# --- Forecaster Model ---
|
|
@@ -20,7 +23,7 @@ class Forecaster(nn.Module):
|
|
|
20
23
|
self.max_patches = self.max_seq_len // self.patch_len
|
|
21
24
|
|
|
22
25
|
print("Loading base model from HuggingFace Hub...")
|
|
23
|
-
base_model = PatchFM.from_pretrained("vilhess/PatchFM")
|
|
26
|
+
base_model = PatchFM.from_pretrained("vilhess/PatchFM-CausalRevIN-asinh")
|
|
24
27
|
self._init_from_base(base_model)
|
|
25
28
|
|
|
26
29
|
self.eval()
|
|
@@ -59,91 +62,13 @@ class Forecaster(nn.Module):
|
|
|
59
62
|
self.proj_output = base_model.proj_output
|
|
60
63
|
|
|
61
64
|
@torch.inference_mode()
|
|
62
|
-
def
|
|
63
|
-
x = self.converter.convert(x)
|
|
64
|
-
assert x.ndim in (1, 2), f"Input dimension must be 1D (time) or 2D (batch, time), got {x.ndim}D."
|
|
65
|
-
|
|
66
|
-
batch_dim=True
|
|
67
|
-
if x.ndim != 2:
|
|
68
|
-
x = x.unsqueeze(0)
|
|
69
|
-
batch_dim=False
|
|
70
|
-
bs, ws = x.size()
|
|
71
|
-
|
|
72
|
-
x = x.to(self.device)
|
|
73
|
-
|
|
74
|
-
if ws > self.max_seq_len:
|
|
75
|
-
print(f"Warning: Input length {ws} exceeds max_seq_len {self.max_seq_len}. Truncating input.")
|
|
76
|
-
x = x[:, -self.max_seq_len:]
|
|
77
|
-
ws = self.max_seq_len
|
|
65
|
+
def auto_regressive_quantile_decoding(self, x: torch.Tensor, forecast_horizon: int | None = None, quantiles: list[float] | None = None) -> torch.Tensor:
|
|
78
66
|
|
|
79
|
-
|
|
80
|
-
pad = (self.patch_len - ws % self.patch_len) % self.patch_len
|
|
81
|
-
if pad > 0:
|
|
82
|
-
x = torch.cat([x[:, :1].repeat(1, pad), x], dim=1)
|
|
67
|
+
q = torch.tensor(self.quantiles, device=self.device)
|
|
83
68
|
|
|
84
69
|
# Default horizon = patch_len
|
|
85
70
|
forecast_horizon = forecast_horizon or self.patch_len
|
|
86
71
|
|
|
87
|
-
# Reshape into patches
|
|
88
|
-
x = rearrange(x, "b (pn pl) -> b pn pl", pl=self.patch_len)
|
|
89
|
-
|
|
90
|
-
rollouts = -(-forecast_horizon // self.patch_len) # ceil division
|
|
91
|
-
predictions = []
|
|
92
|
-
|
|
93
|
-
for _ in range(rollouts):
|
|
94
|
-
|
|
95
|
-
if x.size(1) > self.max_patches:
|
|
96
|
-
x = x[:, -self.max_patches:, :]
|
|
97
|
-
|
|
98
|
-
# Forward pass
|
|
99
|
-
x = self.revin(x, mode="norm")
|
|
100
|
-
x = self.proj_embedding(x)
|
|
101
|
-
x = self.transformer_encoder(x)
|
|
102
|
-
x = x[:, -1:, :] # Keep only the last patch for autoregressive forecasting
|
|
103
|
-
forecasting = self.proj_output(x)
|
|
104
|
-
forecasting = self.revin(forecasting, mode="denorm")
|
|
105
|
-
|
|
106
|
-
# Reshape to (bs, patch_num, patch_len, n_quantiles)
|
|
107
|
-
forecasting = rearrange(
|
|
108
|
-
forecasting, "b 1 (pl q) -> b 1 pl q",
|
|
109
|
-
pl=self.patch_len, q=self.n_quantiles
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
# Take median quantile (index 4)
|
|
113
|
-
patch_median = forecasting[:, -1:, :, 4].detach()
|
|
114
|
-
predictions.append(forecasting[:, -1, :, :])
|
|
115
|
-
|
|
116
|
-
# Append median patch for next rollout
|
|
117
|
-
x = patch_median.clone()
|
|
118
|
-
|
|
119
|
-
pred_quantiles = torch.cat(predictions, dim=1)
|
|
120
|
-
pred_quantiles = pred_quantiles[:, :forecast_horizon, :]
|
|
121
|
-
pred_median = pred_quantiles[:, :, 4]
|
|
122
|
-
|
|
123
|
-
pred_quantiles = pred_quantiles[..., [self.quantiles.index(q) for q in quantiles]] if quantiles is not None else pred_quantiles
|
|
124
|
-
|
|
125
|
-
self.clear_cache()
|
|
126
|
-
|
|
127
|
-
if torch.any(torch.isnan(pred_median)) or torch.any(torch.isinf(pred_median)):
|
|
128
|
-
print("Warning: NaN or Inf values detected in predictions. Returning zeros.")
|
|
129
|
-
pred_median = torch.zeros_like(pred_median)
|
|
130
|
-
pred_quantiles = torch.zeros_like(pred_quantiles)
|
|
131
|
-
|
|
132
|
-
if not batch_dim:
|
|
133
|
-
pred_median = pred_median.squeeze(0)
|
|
134
|
-
pred_quantiles = pred_quantiles.squeeze(0)
|
|
135
|
-
|
|
136
|
-
pred_median, pred_quantiles = self.converter.deconvert(pred_median, pred_quantiles)
|
|
137
|
-
return pred_median, pred_quantiles
|
|
138
|
-
|
|
139
|
-
@torch.inference_mode()
|
|
140
|
-
def auto_regressive_quantile_decoding(self, x: torch.Tensor, forecast_horizon: int | None = None, quantiles: list[float] | None = None) -> torch.Tensor:
|
|
141
|
-
|
|
142
|
-
if forecast_horizon<=self.patch_len:
|
|
143
|
-
print("Warning: forecast_horizon is less than or equal to patch_len. Using standard forecast method instead.")
|
|
144
|
-
return self.forecast(x, forecast_horizon, quantiles)
|
|
145
|
-
|
|
146
|
-
print("Using Auto-Regressive Quantile Decoding... This may be slower because no KV-Caching.")
|
|
147
72
|
x = self.converter.convert(x)
|
|
148
73
|
assert x.ndim in (1, 2), f"Input dimension must be 1D (time) or 2D (batch, time), got {x.ndim}D."
|
|
149
74
|
|
|
@@ -165,12 +90,8 @@ class Forecaster(nn.Module):
|
|
|
165
90
|
if pad > 0:
|
|
166
91
|
x = torch.cat([x[:, :1].repeat(1, pad), x], dim=1)
|
|
167
92
|
|
|
168
|
-
# Default horizon = patch_len
|
|
169
|
-
forecast_horizon = forecast_horizon or self.patch_len
|
|
170
|
-
|
|
171
93
|
# Reshape into patches
|
|
172
|
-
x = rearrange(x, "b (pn pl) -> b pn pl", pl=self.patch_len)
|
|
173
|
-
context = x.clone()
|
|
94
|
+
x = rearrange(x, "b (pn pl) -> b pn pl", pl=self.patch_len)
|
|
174
95
|
|
|
175
96
|
rollouts = -(-forecast_horizon // self.patch_len) # ceil division
|
|
176
97
|
predictions = []
|
|
@@ -188,18 +109,10 @@ class Forecaster(nn.Module):
|
|
|
188
109
|
forecasting = rearrange(
|
|
189
110
|
forecasting, "b 1 (pl q) -> b 1 pl q",
|
|
190
111
|
pl=self.patch_len, q=self.n_quantiles
|
|
191
|
-
)
|
|
112
|
+
)
|
|
113
|
+
x = forecasting.permute(0, 3, 1, 2).reshape(forecasting.size(0)*self.n_quantiles, 1, self.patch_len)
|
|
192
114
|
|
|
193
|
-
|
|
194
|
-
base_context_expanded = torch.cat((context_expanded, forecasting), dim=1)
|
|
195
|
-
context_expanded = base_context_expanded.permute(0, 3, 1, 2).reshape(bs*self.n_quantiles, base_context_expanded.size(1), self.patch_len)
|
|
196
|
-
|
|
197
|
-
x = context_expanded
|
|
198
|
-
q = torch.tensor(self.quantiles, device=self.device)
|
|
199
|
-
|
|
200
|
-
predictions.append(forecasting[:, -1, :, :])
|
|
201
|
-
|
|
202
|
-
self.clear_cache()
|
|
115
|
+
predictions.append(forecasting[:, -1, :, :].detach())
|
|
203
116
|
|
|
204
117
|
for _ in range(rollouts-1):
|
|
205
118
|
|
|
@@ -223,15 +136,11 @@ class Forecaster(nn.Module):
|
|
|
223
136
|
)
|
|
224
137
|
forecasting = forecasting.permute(0, 2, 3, 1, 4).flatten(start_dim=-2) # batch x 1 x patch_len x n_quantiles**2
|
|
225
138
|
forecasting = torch.quantile(forecasting, q, dim=-1) # n_quantiles x batch x 1 x patch_len
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
context_expanded = base_context_expanded.permute(0, 3, 1, 2).reshape(bs*self.n_quantiles, base_context_expanded.size(1), self.patch_len)
|
|
230
|
-
|
|
231
|
-
x = context_expanded
|
|
232
|
-
predictions.append(forecasting[:, -1, :, :])
|
|
139
|
+
|
|
140
|
+
x = forecasting.permute(1, 0, 2, 3).reshape(-1, 1, self.patch_len)
|
|
141
|
+
predictions.append(forecasting.permute(1, 2, 3, 0)[:, 0].detach())
|
|
233
142
|
|
|
234
|
-
|
|
143
|
+
self.clear_cache()
|
|
235
144
|
|
|
236
145
|
pred_quantiles = torch.cat(predictions, dim=1)
|
|
237
146
|
pred_quantiles = pred_quantiles[:, :forecast_horizon, :]
|
|
@@ -251,13 +160,30 @@ class Forecaster(nn.Module):
|
|
|
251
160
|
pred_median, pred_quantiles = self.converter.deconvert(pred_median, pred_quantiles)
|
|
252
161
|
return pred_median, pred_quantiles
|
|
253
162
|
|
|
254
|
-
def __call__(self, context: torch.Tensor, forecast_horizon: int | None = None, quantiles: list[float] | None = None,
|
|
255
|
-
if
|
|
256
|
-
|
|
163
|
+
def __call__(self, context: torch.Tensor, forecast_horizon: int | None = None, quantiles: list[float] | None = None, flip_equivariance: bool = False) -> torch.Tensor:
|
|
164
|
+
if flip_equivariance:
|
|
165
|
+
print("Flip equivariance enabled: forecast = (f(x) - f(-x)) / 2. This requires multiplying by 2 the batch size (Reverso: Efficient Time Series Foundation Models for Zero-shot Forecasting).")
|
|
166
|
+
bs = context.size(0)
|
|
167
|
+
context_flipped = -context
|
|
168
|
+
concat_context = torch.cat([context, context_flipped], dim=0)
|
|
169
|
+
pred_median_full, pred_quantiles_full = self.auto_regressive_quantile_decoding(concat_context, forecast_horizon, quantiles)
|
|
170
|
+
pred_median, pred_quantiles = pred_median_full[:bs], pred_quantiles_full[:bs]
|
|
171
|
+
pred_median2, pred_quantiles2 = pred_median_full[bs:], pred_quantiles_full[bs:]
|
|
172
|
+
pred_median = (pred_median - pred_median2) / 2
|
|
173
|
+
pred_quantiles = (pred_quantiles - flip_last_dim(pred_quantiles2)) / 2
|
|
257
174
|
else:
|
|
258
|
-
|
|
175
|
+
pred_median, pred_quantiles = self.auto_regressive_quantile_decoding(context, forecast_horizon, quantiles)
|
|
176
|
+
return pred_median, pred_quantiles
|
|
259
177
|
|
|
260
178
|
def clear_cache(self):
|
|
261
179
|
self.revin.clear_cache()
|
|
262
180
|
for layer in self.transformer_encoder.layers:
|
|
263
|
-
layer.attn.clear_cache()
|
|
181
|
+
layer.attn.clear_cache()
|
|
182
|
+
|
|
183
|
+
def flip_last_dim(x):
|
|
184
|
+
if isinstance(x, torch.Tensor):
|
|
185
|
+
return torch.flip(x, dims=[-1])
|
|
186
|
+
elif isinstance(x, np.ndarray):
|
|
187
|
+
return np.flip(x, axis=-1)
|
|
188
|
+
else:
|
|
189
|
+
raise TypeError(f"Unsupported type: {type(x)}")
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Modules efficient for inference with caching
|
|
2
2
|
|
|
3
|
+
import numpy as np
|
|
3
4
|
import torch
|
|
4
5
|
import torch.nn as nn
|
|
5
6
|
from einops import rearrange
|
|
6
|
-
from rotary_embedding_torch import RotaryEmbedding
|
|
7
7
|
from huggingface_hub import PyTorchModelHubMixin
|
|
8
|
-
|
|
8
|
+
from rotary_embedding_torch import RotaryEmbedding
|
|
9
|
+
|
|
9
10
|
|
|
10
11
|
class SeqTypeConverter:
|
|
11
12
|
def __init__(self):
|
|
@@ -136,11 +137,14 @@ class CausalRevIN(nn.Module):
|
|
|
136
137
|
counts = counts - nan_counts
|
|
137
138
|
|
|
138
139
|
if self.cached_counts is not None:
|
|
140
|
+
factor = B//self.cached_counts.size(0)
|
|
141
|
+
self.cached_counts = self.cached_counts.repeat_interleave(factor, dim=0)
|
|
139
142
|
counts += self.cached_counts
|
|
140
143
|
self.cached_counts = counts[:, -1:, :]
|
|
141
144
|
|
|
142
145
|
cumsum_x = torch.cumsum(x.nansum(dim=-1, keepdim=True), dim=1)
|
|
143
146
|
if self.cached_cumsum_x is not None:
|
|
147
|
+
self.cached_cumsum_x = self.cached_cumsum_x.repeat_interleave(factor, dim=0)
|
|
144
148
|
cumsum_x += self.cached_cumsum_x
|
|
145
149
|
self.cached_cumsum_x = cumsum_x[:, -1:, :]
|
|
146
150
|
|
|
@@ -149,6 +153,7 @@ class CausalRevIN(nn.Module):
|
|
|
149
153
|
|
|
150
154
|
cumsum_x2 = torch.cumsum((x**2).nansum(dim=-1, keepdim=True), dim=1)
|
|
151
155
|
if self.cached_cumsum_x2 is not None:
|
|
156
|
+
self.cached_cumsum_x2 = self.cached_cumsum_x2.repeat_interleave(factor, dim=0)
|
|
152
157
|
cumsum_x2 += self.cached_cumsum_x2
|
|
153
158
|
self.cached_cumsum_x2 = cumsum_x2[:, -1:, :]
|
|
154
159
|
|
|
@@ -161,6 +166,8 @@ class CausalRevIN(nn.Module):
|
|
|
161
166
|
self.cached_cumsum_x = None
|
|
162
167
|
self.cached_cumsum_x2 = None
|
|
163
168
|
self.cached_counts = None
|
|
169
|
+
self.cached_mean = None
|
|
170
|
+
self.cached_std = None
|
|
164
171
|
|
|
165
172
|
class ResidualBlock(nn.Module):
|
|
166
173
|
def __init__(self, in_dim, hid_dim, out_dim):
|
|
@@ -218,6 +225,9 @@ class MultiHeadAttention(nn.Module):
|
|
|
218
225
|
if self.k_cache is not None and self.v_cache is not None:
|
|
219
226
|
offset += self.k_cache.size(2)
|
|
220
227
|
is_causal = False
|
|
228
|
+
factor = q.size(0) // self.k_cache.size(0)
|
|
229
|
+
self.k_cache = self.k_cache.repeat_interleave(factor, dim=0)
|
|
230
|
+
self.v_cache = self.v_cache.repeat_interleave(factor, dim=0)
|
|
221
231
|
k = torch.cat([self.k_cache, k], dim=2)
|
|
222
232
|
v = torch.cat([self.v_cache, v], dim=2)
|
|
223
233
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|