UncleanCode commited on
Commit
51bb0b7
·
verified ·
1 Parent(s): ce7e676

Upload via Autoresearch export_data

Browse files
README.md CHANGED
@@ -1,3 +1,86 @@
1
  ---
2
- license: apache-2.0
 
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ license: cc-by-nc-4.0
3
+ library_name: pytorch
4
+ pipeline_tag: time-series-forecasting
5
+ tags:
6
+ - time-series
7
+ - forecasting
8
+ - foundation-model
9
+ - zero-shot
10
+ - probabilistic-forecasting
11
+ - gift-eval
12
  ---
13
+
14
+ # Modular-TS-2.4M
15
+
16
+ **A 2.4M-parameter time-series foundation model, pretrained from scratch by [Modular Computing](https://huggingface.co/Modularcomputing).**
17
+
18
+ Modular-TS-2.4M is a small, fast, zero-shot probabilistic forecaster: give it any univariate or multivariate series and it returns 21 quantiles for every future step, with no fine-tuning. It is built for the GPU-poor: it runs comfortably on a CPU or a phone-class device.
19
+
20
+ ## Results (GIFT-Eval, full 97 configurations, zero-shot)
21
+
22
+ Geometric mean relative to Seasonal Naive (lower is better).
23
+
24
+ | Model | Params | MASE | CRPS |
25
+ |---|---|---|---|
26
+ | **Modular-TS-2.4M (ours)** | **2.41M** | **0.745** | **0.511** |
27
+ | IBM TTM-R3 (PT) | ~1–5M | 0.724 | 0.520 |
28
+ | TimeSSM-2.5M | 2.5M | 0.754 | 0.513 |
29
+ | Seasonal Naive | – | 1.000 | 1.000 |
30
+
31
+ - Best CRPS (probabilistic accuracy) of any open model at 2.5M parameters or fewer that we evaluated.
32
+ - Beats TimeSSM-2.5M on both metrics and TTM-R3 on CRPS; TTM-R3 is still ahead on MASE.
33
+ - Our numbers are strictly zero-shot: the model never sees a test dataset's own training split at inference time. (TTM-R3-PT uses each dataset's train split for backtest ensembling.)
34
+ - Evaluated with the official GIFT-Eval harness at leaderboard settings; the same harness reproduces published leaderboard scores for reference models exactly.
35
+
36
+ ## Model
37
+
38
+ | | |
39
+ |---|---|
40
+ | Architecture | Decoder-style patch transformer (Timestral-style), d_model 128, 2 heads, MLP 320, 8 layers alternating time and variate attention, patch 32 |
41
+ | Parameters | 2,407,680 (embed 107,392 / trunk 1,804,672 / head 495,616) |
42
+ | Context | up to 8,192 steps |
43
+ | Output | 21 monotone quantiles per step (plus mean) |
44
+
45
+ The architecture code adapts layers from Mistral AI's Apache-2.0 `timestral` inference package. **No Timestral weights were loaded, fine-tuned, or distilled at any point.** Every weight in this repo was trained from random initialisation.
46
+
47
+ ## Training data
48
+
49
+ - **Synthetic (majority):** a GPU-vectorised generator with Gaussian-process kernels, regime-switching Ornstein–Uhlenbeck processes, waveform and load-curve templates, steps, growth curves, intermittent counts, multivariate lead-lag mixing, covariates and missingness, followed by an augmentation cascade.
50
+ - **Real (~40% in the final stage):** GiftEvalPretrain, resolution-sampled with TSMixup, after a contamination filter that removes any dataset overlapping the GIFT-Eval test sets.
51
+
52
+ ## Usage
53
+
54
+ ```python
55
+ from huggingface_hub import snapshot_download
56
+ path = snapshot_download("Modularcomputing/Modular-TS-2.4M")
57
+ # path/export/model.safetensors + path/export/config.json
58
+ # Load with the bundled modular_ts package (see modular_ts/README.md),
59
+ # which wraps the Apache-2.0 timestral forecaster around these weights.
60
+ ```
61
+
62
+ Files:
63
+ - `export/model.safetensors`, `export/config.json`: inference weights and config
64
+ - `modular_ts/`: training, synthetic data generation and evaluation code
65
+ - `results/all_results.csv`: per-configuration GIFT-Eval scores
66
+
67
+ ## Licence
68
+
69
+ - **Weights:** CC BY-NC 4.0 (research and non-commercial use), because the real-data portion of training mixes datasets under several licences.
70
+ - **Code (`modular_ts/`):** Apache-2.0, including the adapted `timestral` components (see `modular_ts/NOTICE`).
71
+
72
+ ## Limitations
73
+
74
+ - Trails TTM-R3 on point accuracy (MASE); weakest on some long-horizon, high-frequency energy and weather series.
75
+ - A small model: it captures common seasonal and trend structure well but can miss rare regime changes.
76
+
77
+ ## Citation
78
+
79
+ ```bibtex
80
+ @misc{modularts2026,
81
+ title = {Modular-TS-2.4M: a small time-series foundation model trained from scratch},
82
+ author = {Akuegbo, Chima Emmanuel and Modular Computing},
83
+ year = {2026},
84
+ url = {https://huggingface.co/Modularcomputing/Modular-TS-2.4M}
85
+ }
86
+ ```
export/config.json ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "patch_size": 32,
3
+ "d_model": 128,
4
+ "num_heads": 2,
5
+ "mlp_hidden_dim": 320,
6
+ "block_schedule": [
7
+ "time",
8
+ "time",
9
+ "time",
10
+ "time_variate",
11
+ "time",
12
+ "time",
13
+ "time",
14
+ "time_variate"
15
+ ],
16
+ "quantile_knots": [
17
+ 0.01,
18
+ 0.05,
19
+ 0.1,
20
+ 0.15,
21
+ 0.2,
22
+ 0.25,
23
+ 0.3,
24
+ 0.35,
25
+ 0.4,
26
+ 0.45,
27
+ 0.5,
28
+ 0.55,
29
+ 0.6,
30
+ 0.65,
31
+ 0.7,
32
+ 0.75,
33
+ 0.8,
34
+ 0.85,
35
+ 0.9,
36
+ 0.95,
37
+ 0.99
38
+ ],
39
+ "block_size": 32,
40
+ "context_length": 8192,
41
+ "rope_theta": 10000.0,
42
+ "rope_partial_factor": 0.5,
43
+ "norm_eps": 1e-06,
44
+ "attn_bias": false,
45
+ "mlp_bias": false,
46
+ "time_attn_gate": "sdpa_sigmoid",
47
+ "variate_attn_gate": "sdpa_sigmoid",
48
+ "minimum_scale": 1e-06,
49
+ "use_asinh": true,
50
+ "stabilize_with_global": false,
51
+ "covariate_enabled": true,
52
+ "split_context_horizon_attn": true,
53
+ "attend_missing_patches": true,
54
+ "quantile_width_factor": 3.0
55
+ }
export/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:04d4c57329c8fe0a67716bff5207adea92c632d69810d33e877d64c9299bf4dc
3
+ size 9638720
modular_ts/NOTICE ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ Modular-TS — Apache-2.0.
2
+ This package imports and adapts code from the `timestral` inference package
3
+ (https://github.com/mistralai/timestral, Copyright Mistral AI, Apache-2.0; see TIMESTRAL_LICENSE):
4
+ layers, masks, scaler, and the forecaster used at evaluation time.
5
+ No Timestral model weights are loaded, fine-tuned or distilled anywhere in this code.
6
+ All weights are trained from scratch.
modular_ts/TIMESTRAL_LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
modular_ts/distill.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stage B: fine-tune a Stage-A checkpoint with ground-truth pinball + teacher-ensemble pinball (TimesFM-2.5 + Chronos-2).
2
+
3
+ python -m modular_ts.distill --init runs/run2_8k/ckpt.pt --teach teach/synth,teach/gep --lam 0.5 --minutes 45 --out runs/run3
4
+
5
+ Batches: with prob --p_distill a teacher batch (V=1, T=C+F windows, min-context split s=C/32;
6
+ synthetic rows: GT pinball + teacher pinball; GiftEvalPretrain rows: teacher pinball ONLY, their
7
+ future is filled with the teacher-ensemble median just for the normalisation frame and never used
8
+ as a target), else a fresh synthetic Stage-A batch (GT pinball only) to avoid forgetting.
9
+ Teacher ensemble = mean of the two teachers' quantiles (Vincentisation) at 0.1..0.9.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import glob
16
+ import json
17
+ import math
18
+ import os
19
+ import time
20
+
21
+ import numpy as np
22
+ import torch
23
+
24
+ from modular_ts.model import ModularTS, count_params, make_config
25
+ from modular_ts.synth import SynthStream
26
+ from modular_ts.train import export, lr_at
27
+
28
+
29
+ def load_teach(dirs, dev, max_per_dir=0):
30
+ ctx, fut, q, src = [], [], [], []
31
+ for si, d in enumerate(dirs):
32
+ files = sorted(glob.glob(os.path.join(d, "shard_*.npz")))
33
+ n = 0
34
+ for f in files:
35
+ z = np.load(f)
36
+ qe = 0.5 * (z["q_tfm"].astype(np.float32) + z["q_cr2"].astype(np.float32))
37
+ qe = np.sort(qe, -1)
38
+ ctx.append(z["ctx"]); fut.append(z["fut"]); q.append(qe); src.append(np.full(len(qe), si))
39
+ n += len(qe)
40
+ if max_per_dir and n >= max_per_dir:
41
+ break
42
+ print(f"[teach] {d}: {n} windows from {len(files)} shards", flush=True)
43
+ c = torch.from_numpy(np.concatenate(ctx)).to(dev)
44
+ f = torch.from_numpy(np.concatenate(fut)).to(dev)
45
+ qq = torch.from_numpy(np.concatenate(q)).to(dev)
46
+ s = torch.from_numpy(np.concatenate(src)).to(dev)
47
+ return c, f, qq, s
48
+
49
+
50
+ def main():
51
+ ap = argparse.ArgumentParser()
52
+ ap.add_argument("--init", required=True, help="Stage-A ckpt.pt (model weights only are used)")
53
+ ap.add_argument("--teach", required=True, help="comma list of teacher shard dirs; dirs containing 'gep' are teacher-only")
54
+ ap.add_argument("--lam", type=float, default=0.5)
55
+ ap.add_argument("--p_distill", type=float, default=0.6)
56
+ ap.add_argument("--lr", type=float, default=3e-4)
57
+ ap.add_argument("--warmup", type=int, default=200)
58
+ ap.add_argument("--minutes", type=float, default=45)
59
+ ap.add_argument("--tokens_per_batch", type=int, default=262144)
60
+ ap.add_argument("--ctx", type=int, default=0, help="model context_length for export (0 = from init ckpt)")
61
+ ap.add_argument("--compile", action="store_true")
62
+ ap.add_argument("--log_every", type=int, default=100)
63
+ ap.add_argument("--seed", type=int, default=7)
64
+ ap.add_argument("--out", required=True)
65
+ args = ap.parse_args()
66
+ os.makedirs(args.out, exist_ok=True)
67
+ dev = torch.device("cuda")
68
+ torch.manual_seed(args.seed)
69
+ torch.backends.cuda.matmul.allow_tf32 = True
70
+ st = torch.load(args.init, map_location=dev)
71
+ ctx_len = args.ctx or st["args"]["ctx"]
72
+ cfg = make_config(st["args"].get("size", "2.5M"), context_length=ctx_len)
73
+ model = ModularTS(cfg).to(dev)
74
+ model.load_state_dict(st["model"])
75
+ print("params", count_params(model), "init", args.init, "step", st["step"], flush=True)
76
+
77
+ dirs = [d for d in args.teach.split(",") if d]
78
+ tc, tf, tq, tsrc = load_teach(dirs, dev)
79
+ teacher_only = torch.tensor(["gep" in d for d in dirs], device=dev)[tsrc]
80
+ C, F, H = tc.shape[1], tf.shape[1], tq.shape[1]
81
+ T = C + F
82
+ P = cfg.patch_size
83
+ Bd = max(1, args.tokens_per_batch // (T // P))
84
+ nT = tc.shape[0]
85
+ print(f"[teach] total {nT} windows (teacher-only {int(teacher_only.sum())}), C={C} F={F} H={H}, rows/batch {Bd}", flush=True)
86
+ # hold out 2% of synthetic teacher windows for a GT/KD validation number
87
+ g = torch.Generator(device=dev); g.manual_seed(args.seed)
88
+ perm = torch.randperm(nT, generator=g, device=dev)
89
+ syn_idx = perm[~teacher_only[perm]]
90
+ val_idx = syn_idx[:min(4096, max(1, len(syn_idx) // 50))]
91
+ trmask = torch.ones(nT, dtype=torch.bool, device=dev); trmask[val_idx] = False
92
+ tr_idx = torch.nonzero(trmask).squeeze(1)
93
+
94
+ def distill_batch(idx):
95
+ c, f, q, to = tc[idx], tf[idx], tq[idx], teacher_only[idx]
96
+ med = q[:, :, 4]
97
+ fill = torch.full_like(f, float("nan"))
98
+ fill[:, :H] = med
99
+ f = torch.where(to[:, None], fill, f)
100
+ vals = torch.cat([c, f], 1)
101
+ obs = torch.isfinite(vals)
102
+ vals = torch.where(obs, vals, torch.zeros_like(vals))
103
+ teach = torch.zeros(len(idx), T, q.shape[-1], device=dev)
104
+ teach[:, C:C + H] = q
105
+ tobs = torch.zeros(len(idx), T, dtype=torch.bool, device=dev)
106
+ tobs[:, C:C + H] = True
107
+ R = len(idx)
108
+ return dict(values=vals[:, None], observed=obs[:, None], roles=torch.zeros(R, 1, dtype=torch.long, device=dev),
109
+ s=torch.full((R,), C // P, dtype=torch.long, device=dev), teacher=teach, teacher_obs=tobs,
110
+ gt_row=(~to).float())
111
+
112
+ synth = SynthStream(4096, args.tokens_per_batch, dev, seed=args.seed + 99)
113
+
114
+ decay, no_decay = [], []
115
+ for n, p in model.named_parameters():
116
+ (decay if p.dim() >= 2 else no_decay).append(p)
117
+ opt = torch.optim.AdamW([{"params": decay, "weight_decay": 0.05}, {"params": no_decay, "weight_decay": 0.0}],
118
+ lr=args.lr, betas=(0.9, 0.95), eps=1e-8, fused=True)
119
+ fwd = torch.compile(model.forward_train, dynamic=False) if args.compile else model.forward_train
120
+ amp = torch.autocast(device_type="cuda", dtype=torch.bfloat16)
121
+ lam = args.lam
122
+
123
+ def evaluate():
124
+ model.eval()
125
+ out = {"val_gt": 0.0, "val_kd": 0.0}
126
+ with torch.no_grad(), amp:
127
+ for i in range(0, len(val_idx), Bd):
128
+ b = distill_batch(val_idx[i:i + Bd])
129
+ l, s_ = model.forward_train(b["values"], b["observed"], b["roles"], b["s"], teacher=b["teacher"],
130
+ teacher_obs=b["teacher_obs"], gt_row=b["gt_row"], lam=lam)
131
+ out["val_gt"] += float(s_["pinball"]); out["val_kd"] += float(s_["kd"])
132
+ n = math.ceil(len(val_idx) / Bd)
133
+ model.train()
134
+ return {k: v / n for k, v in out.items()}
135
+
136
+ log = open(os.path.join(args.out, "log.jsonl"), "a")
137
+ rng = np.random.default_rng(args.seed)
138
+ t0 = time.time(); step = 0
139
+ rec0 = {"step": 0, **evaluate()}
140
+ print(json.dumps(rec0), flush=True); log.write(json.dumps(rec0) + "\n")
141
+ model.train()
142
+ while (time.time() - t0) / 60 < args.minutes:
143
+ frac = (time.time() - t0) / 60 / args.minutes
144
+ lr = lr_at(step, max(step + 1, int(step / max(frac, 1e-6))) if step > args.warmup else 10**8, args.warmup, args.lr)
145
+ for gp in opt.param_groups:
146
+ gp["lr"] = lr
147
+ is_d = rng.random() < args.p_distill
148
+ with amp:
149
+ if is_d:
150
+ idx = tr_idx[torch.randint(0, len(tr_idx), (Bd,), device=dev)]
151
+ b = distill_batch(idx)
152
+ loss, s_ = fwd(b["values"], b["observed"], b["roles"], b["s"], False, b["teacher"], b["teacher_obs"], b["gt_row"], lam)
153
+ else:
154
+ b = synth.next_batch()
155
+ loss, s_ = fwd(b["values"], b["observed"], b["roles"], b["s"])
156
+ if not torch.isfinite(loss):
157
+ opt.zero_grad(set_to_none=True); step += 1
158
+ continue
159
+ loss.backward()
160
+ gn = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
161
+ opt.step(); opt.zero_grad(set_to_none=True)
162
+ step += 1
163
+ if step % args.log_every == 0:
164
+ rec = {"step": step, "lr": lr, "loss": float(loss), "kind": "distill" if is_d else "synth",
165
+ "kd": float(s_.get("kd", float("nan"))), "gnorm": float(gn), "min": (time.time() - t0) / 60}
166
+ if step % (args.log_every * 10) == 0:
167
+ rec.update(evaluate())
168
+ print(json.dumps(rec), flush=True); log.write(json.dumps(rec) + "\n"); log.flush()
169
+ torch.save({"model": model.state_dict(), "step": step, "args": {**st["args"], "distill": vars(args)}}, os.path.join(args.out, "ckpt.pt"))
170
+ export(model, os.path.join(args.out, "export"))
171
+ fin = {"steps": step, **evaluate(), "minutes": (time.time() - t0) / 60, "lam": lam}
172
+ print("FINAL", json.dumps(fin), flush=True)
173
+ json.dump(fin, open(os.path.join(args.out, "final.json"), "w"))
174
+
175
+
176
+ if __name__ == "__main__":
177
+ main()
modular_ts/eval_gift.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GIFT-Eval harness for Modular-TS (official gift-eval Dataset + gluonts evaluate_model).
2
+
3
+ Adapted from gift-eval/notebooks/timestral.ipynb (Apache-2.0). Our weights load into
4
+ the Apache-2.0 `timestral.TimestralForecaster` (block-AR decoding, advancing refine frame,
5
+ clampsym TTA). Metrics, settings and the leaderboard aggregate match the official board:
6
+ evaluate_model(axis=None, mask_invalid_label=True, allow_nan_forecast=False), 9
7
+ quantiles 0.1..0.9 for CRPS (mean_weighted_sum_quantile_loss), then the geometric mean
8
+ over configs of (model / Seasonal_Naive) for MASE and CRPS.
9
+
10
+ python -m modular_ts.eval_gift --export runs/smoke/export --mode quick --out runs/smoke/gift_quick
11
+ python -m modular_ts.eval_gift --export ... --mode full # all 97 configs -> all_results.csv
12
+
13
+ Requires: pip install --no-deps git+https://github.com/SalesforceAIResearch/gift-eval
14
+ pip install "gluonts~=0.15.1" python-dotenv toolz orjson datasets pyarrow pandas
15
+ Data: Salesforce/GiftEval is downloaded (only the needed folders) to $GIFT_EVAL.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import json
22
+ import logging
23
+ import math
24
+ import os
25
+ import time
26
+ import warnings
27
+ from pathlib import Path
28
+
29
+ import numpy as np
30
+ import pandas as pd
31
+ import torch
32
+
33
+ import modular_ts.model # noqa: F401 (puts the vendored timestral package on sys.path)
34
+
35
+ ROOT = Path(__file__).resolve().parent.parent
36
+ LB_DIR = ROOT / "gift-eval" / "results"
37
+ PROPS = ROOT / "gift-eval" / "notebooks" / "dataset_properties.json"
38
+
39
+ SHORT_DATASETS = "m4_yearly m4_quarterly m4_monthly m4_weekly m4_daily m4_hourly electricity/15T electricity/H electricity/D electricity/W solar/10T solar/H solar/D solar/W hospital covid_deaths us_births/D us_births/M us_births/W saugeenday/D saugeenday/M saugeenday/W temperature_rain_with_missing kdd_cup_2018_with_missing/H kdd_cup_2018_with_missing/D car_parts_with_missing restaurant hierarchical_sales/D hierarchical_sales/W LOOP_SEATTLE/5T LOOP_SEATTLE/H LOOP_SEATTLE/D SZ_TAXI/15T SZ_TAXI/H M_DENSE/H M_DENSE/D ett1/15T ett1/H ett1/D ett1/W ett2/15T ett2/H ett2/D ett2/W jena_weather/10T jena_weather/H jena_weather/D bitbrains_fast_storage/5T bitbrains_fast_storage/H bitbrains_rnd/5T bitbrains_rnd/H bizitobs_application bizitobs_service bizitobs_l2c/5T bizitobs_l2c/H"
40
+ MED_LONG_DATASETS = "electricity/15T electricity/H solar/10T solar/H kdd_cup_2018_with_missing/H LOOP_SEATTLE/5T LOOP_SEATTLE/H SZ_TAXI/15T M_DENSE/H ett1/15T ett1/H ett2/15T ett2/H jena_weather/10T jena_weather/H bitbrains_fast_storage/5T bitbrains_rnd/5T bizitobs_application bizitobs_service bizitobs_l2c/5T bizitobs_l2c/H"
41
+ PRETTY = {"saugeenday": "saugeen", "temperature_rain_with_missing": "temperature_rain",
42
+ "kdd_cup_2018_with_missing": "kdd_cup_2018", "car_parts_with_missing": "car_parts"}
43
+ # Quick subset: small, domain-diverse configs (12 short + 2 long). Used for smoke checks only.
44
+ QUICK = [("m4_hourly", "short"), ("m4_weekly", "short"), ("us_births/D", "short"), ("saugeenday/D", "short"),
45
+ ("hospital", "short"), ("covid_deaths", "short"), ("ett1/H", "short"), ("jena_weather/H", "short"),
46
+ ("bizitobs_service", "short"), ("car_parts_with_missing", "short"), ("electricity/D", "short"),
47
+ ("LOOP_SEATTLE/D", "short"), ("ett2/H", "long"), ("bizitobs_l2c/H", "long")]
48
+ QUANTILE_LEVELS = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
49
+ METRIC_COLUMNS = ["MSE[mean]", "MSE[0.5]", "MAE[0.5]", "MASE[0.5]", "MAPE[0.5]", "sMAPE[0.5]", "MSIS",
50
+ "RMSE[mean]", "NRMSE[mean]", "ND[0.5]", "mean_weighted_sum_quantile_loss"]
51
+ COMPARE = ["Seasonal_Naive", "TimeSSM-2.5M", "Timestral-5M", "TTM-R3-PT", "TinyCast"]
52
+
53
+
54
+ def all_configs(properties):
55
+ short, ml = SHORT_DATASETS.split(), MED_LONG_DATASETS.split()
56
+ out = []
57
+ for name in dict.fromkeys(short + ml):
58
+ for term in ["short", "medium", "long"]:
59
+ if term != "short" and name not in ml:
60
+ continue
61
+ out.append((name, term, config_key(name, term, properties)))
62
+ assert len(out) == 97
63
+ return out
64
+
65
+
66
+ def config_key(name, term, properties):
67
+ if "/" in name:
68
+ k, f = name.split("/")
69
+ k = PRETTY.get(k.lower(), k.lower())
70
+ else:
71
+ k = PRETTY.get(name.lower(), name.lower())
72
+ f = properties[k]["frequency"]
73
+ return f"{k}/{f}/{term}"
74
+
75
+
76
+ def ensure_data(names, root):
77
+ from huggingface_hub import snapshot_download
78
+ tops = sorted({n.split("/")[0] for n in names})
79
+ need = [t for t in tops if not (Path(root) / t).exists()]
80
+ if need:
81
+ snapshot_download("Salesforce/GiftEval", repo_type="dataset", local_dir=root,
82
+ allow_patterns=[f"{t}/**" for t in need])
83
+
84
+
85
+ class Predictor:
86
+ def __init__(self, fc, prediction_length, use_past_covariates, max_rows=1024, max_tokens=2**24):
87
+ self.fc, self.h, self.cov = fc, prediction_length, use_past_covariates
88
+ self.max_rows, self.max_tokens = max_rows, max_tokens
89
+ lv = list(fc.quantile_levels)
90
+ self.qi = [next(i for i, l in enumerate(lv) if abs(l - q) < 1e-9) for q in QUANTILE_LEVELS]
91
+ self.keys = [str(q) for q in QUANTILE_LEVELS] + ["mean"]
92
+
93
+ def predict(self, test_input):
94
+ from gluonts.model.forecast import QuantileForecast
95
+ entries = list(test_input)
96
+ mc = self.fc.max_context
97
+ tg, cv = [], []
98
+ for e in entries:
99
+ t = np.asarray(e["target"], dtype=np.float32)
100
+ tg.append((t[None] if t.ndim == 1 else t)[:, -mc:])
101
+ c = e.get("past_feat_dynamic_real") if self.cov else None
102
+ if c is not None:
103
+ c = np.asarray(c, dtype=np.float32)
104
+ c = (c[None] if c.ndim == 1 else c)[:, -mc:]
105
+ cv.append(c)
106
+ nrows = tg[0].shape[0] + (cv[0].shape[0] if cv[0] is not None else 0)
107
+ outs = [None] * len(entries)
108
+ order = sorted(range(len(entries)), key=lambda i: tg[i].shape[-1])
109
+ s = 0
110
+ while s < len(order):
111
+ e_ = s + 1
112
+ while e_ < len(order):
113
+ L = tg[order[e_]].shape[-1] + self.h
114
+ if (e_ + 1 - s) * nrows > self.max_rows or (e_ + 1 - s) * nrows * L > self.max_tokens:
115
+ break
116
+ e_ += 1
117
+ self._batch(order[s:e_], tg, cv, outs)
118
+ s = e_
119
+ res = []
120
+ for e, q in zip(entries, outs):
121
+ arr = np.concatenate([q, q[..., QUANTILE_LEVELS.index(0.5), None]], -1).transpose(2, 1, 0)
122
+ if np.asarray(e["target"]).ndim == 1:
123
+ arr = arr[..., 0]
124
+ res.append(QuantileForecast(forecast_arrays=arr, forecast_keys=self.keys,
125
+ start_date=e["start"] + np.asarray(e["target"]).shape[-1], item_id=e.get("item_id")))
126
+ return res
127
+
128
+ def _batch(self, idx, tg, cv, outs):
129
+ try:
130
+ L = max(tg[i].shape[-1] for i in idx)
131
+ ctx = np.stack([_lpad(tg[i], L) for i in idx])
132
+ past = np.stack([_lpad(cv[i], L) for i in idx]) if cv[idx[0]] is not None else None
133
+ f = self.fc.forecast(ctx, self.h, past_covariates=past)
134
+ q = f.quantiles[..., self.qi].float().cpu().numpy()
135
+ except torch.cuda.OutOfMemoryError:
136
+ torch.cuda.empty_cache()
137
+ if len(idx) == 1:
138
+ raise
139
+ hlf = len(idx) // 2
140
+ self._batch(idx[:hlf], tg, cv, outs)
141
+ self._batch(idx[hlf:], tg, cv, outs)
142
+ return
143
+ for j, i in enumerate(idx):
144
+ outs[i] = q[j]
145
+
146
+
147
+ def _lpad(x, L):
148
+ p = L - x.shape[-1]
149
+ return x if p == 0 else np.concatenate([np.full((x.shape[0], p), np.nan, np.float32), x], -1)
150
+
151
+
152
+ def evaluate_config(fc, name, term):
153
+ from gift_eval.data import Dataset
154
+ from gluonts.ev.metrics import MAE, MAPE, MASE, MSE, MSIS, ND, NRMSE, RMSE, SMAPE, MeanWeightedSumQuantileLoss
155
+ from gluonts.model import evaluate_model
156
+ from gluonts.time_feature import get_seasonality
157
+ metrics = [MSE(forecast_type="mean"), MSE(forecast_type=0.5), MAE(), MASE(), MAPE(), SMAPE(), MSIS(), RMSE(),
158
+ NRMSE(), ND(), MeanWeightedSumQuantileLoss(quantile_levels=QUANTILE_LEVELS)]
159
+ ds = Dataset(name=name, term=term, to_univariate=False)
160
+ pred = Predictor(fc, ds.prediction_length, bool(ds.past_feat_dynamic_real_dim), max_rows=int(os.environ.get("MAX_ROWS", "1024")))
161
+ res = evaluate_model(pred, test_data=ds.test_data, metrics=metrics, batch_size=512, axis=None,
162
+ mask_invalid_label=True, allow_nan_forecast=False, seasonality=get_seasonality(ds.freq))
163
+ return {k: float(res[k].iloc[0]) for k in res.columns}
164
+
165
+
166
+ def gmean(x):
167
+ return float(math.exp(np.log(np.asarray(x, dtype=float)).mean()))
168
+
169
+
170
+ def aggregate(per_cfg: dict[str, dict]) -> dict:
171
+ keys = sorted(per_cfg)
172
+ sn = pd.read_csv(LB_DIR / "Seasonal_Naive" / "all_results.csv").set_index("dataset")
173
+ out = {}
174
+ m = [per_cfg[k]["MASE[0.5]"] / sn.loc[k, "eval_metrics/MASE[0.5]"] for k in keys]
175
+ c = [per_cfg[k]["mean_weighted_sum_quantile_loss"] / sn.loc[k, "eval_metrics/mean_weighted_sum_quantile_loss"] for k in keys]
176
+ out["ours"] = {"MASE": gmean(m), "CRPS": gmean(c)}
177
+ for name in COMPARE:
178
+ p = LB_DIR / name / "all_results.csv"
179
+ if not p.exists():
180
+ continue
181
+ df = pd.read_csv(p).set_index("dataset")
182
+ if not all(k in df.index for k in keys):
183
+ continue
184
+ out[name] = {
185
+ "MASE": gmean([df.loc[k, "eval_metrics/MASE[0.5]"] / sn.loc[k, "eval_metrics/MASE[0.5]"] for k in keys]),
186
+ "CRPS": gmean([df.loc[k, "eval_metrics/mean_weighted_sum_quantile_loss"] / sn.loc[k, "eval_metrics/mean_weighted_sum_quantile_loss"] for k in keys]),
187
+ }
188
+ out["n_configs"] = len(keys)
189
+ return out
190
+
191
+
192
+ def main():
193
+ ap = argparse.ArgumentParser()
194
+ ap.add_argument("--export", required=True)
195
+ ap.add_argument("--mode", default="quick", choices=["quick", "full"])
196
+ ap.add_argument("--configs", default="", help="override: 'name:term,name:term'")
197
+ ap.add_argument("--out", required=True)
198
+ ap.add_argument("--gift_eval_dir", default=os.environ.get("GIFT_EVAL", str(ROOT / "data" / "gift_eval")))
199
+ ap.add_argument("--max_context", type=int, default=0, help="0 = model's trained context_length")
200
+ ap.add_argument("--model_name", default="Modular-TS-2.5M")
201
+ ap.add_argument("--tta", default="", help="v3 test-time ensemble spec, e.g. 'c8192,c2048,d4' (modular_ts.tta)")
202
+ ap.add_argument("--tta_alpha", type=float, default=0.5)
203
+ ap.add_argument("--tta_backtest", action="store_true")
204
+ ap.add_argument("--tta_tau", type=float, default=0.5)
205
+ ap.add_argument("--tta_bt_windows", type=int, default=1)
206
+ ap.add_argument("--tta_prior", type=float, default=0.0)
207
+ ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
208
+ args = ap.parse_args()
209
+
210
+ logging.getLogger("gluonts.model.forecast").setLevel(logging.ERROR)
211
+ warnings.filterwarnings("ignore")
212
+ os.environ["GIFT_EVAL"] = args.gift_eval_dir
213
+ if os.environ.get("EVAL_MEM_FRAC") and torch.cuda.is_available(): # share the GPU with a training job
214
+ torch.cuda.set_per_process_memory_fraction(float(os.environ["EVAL_MEM_FRAC"]))
215
+ props = json.load(open(PROPS))
216
+ if args.configs:
217
+ cfgs = [tuple(x.split(":")) for x in args.configs.split(",")]
218
+ elif args.mode == "quick":
219
+ cfgs = QUICK
220
+ else:
221
+ cfgs = [(n, t) for n, t, _ in all_configs(props)]
222
+ ensure_data([n for n, _ in cfgs], args.gift_eval_dir)
223
+
224
+ from timestral import TimestralForecaster
225
+ fc = TimestralForecaster.from_pretrained(args.export, device=args.device,
226
+ max_context=args.max_context or None)
227
+ if args.tta:
228
+ from modular_ts.tta import EnsembleForecaster
229
+ fc = EnsembleForecaster(fc, args.tta, alpha=args.tta_alpha, backtest=args.tta_backtest, tau=args.tta_tau, bt_windows=args.tta_bt_windows, prior=args.tta_prior)
230
+ out = Path(args.out)
231
+ (out / "per_config").mkdir(parents=True, exist_ok=True)
232
+ per = {}
233
+ for name, term in cfgs:
234
+ key = config_key(name, term, props)
235
+ p = out / "per_config" / (key.replace("/", "__") + ".json")
236
+ if p.exists():
237
+ per[key] = json.loads(p.read_text())["metrics"]
238
+ continue
239
+ t = time.time()
240
+ try:
241
+ met = evaluate_config(fc, name, term)
242
+ except Exception as e: # keep going; a failed config is reported, not silently dropped
243
+ print(f"{key}: Error {e!r}"[:400], flush=True)
244
+ continue
245
+ p.write_text(json.dumps({"dataset": key, "metrics": met, "seconds": time.time() - t}, indent=1))
246
+ per[key] = met
247
+ print(f"{key}: MASE={met['MASE[0.5]']:.4f} CRPS={met['mean_weighted_sum_quantile_loss']:.4f} ({time.time()-t:.0f}s)", flush=True)
248
+ agg = aggregate(per)
249
+ json.dump(agg, open(out / "summary.json", "w"), indent=1)
250
+ print("SUMMARY", json.dumps(agg), flush=True)
251
+ if args.mode == "full":
252
+ rows = []
253
+ for k, met in sorted(per.items()):
254
+ ds_key = k.split("/")[0]
255
+ rows.append({"dataset": k, "model": args.model_name, **{f"eval_metrics/{c}": met[c] for c in METRIC_COLUMNS},
256
+ "domain": props[ds_key]["domain"], "num_variates": props[ds_key]["num_variates"]})
257
+ pd.DataFrame(rows).to_csv(out / "all_results.csv", index=False)
258
+
259
+
260
+ if __name__ == "__main__":
261
+ main()
modular_ts/model.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Modular-TS training model: a Timestral-style network trained from scratch with DHF.
2
+
3
+ The network *is* `timestral.model.TimestralModel` (Apache-2.0 architecture code, random
4
+ init), so parameter names match the open inference package and a trained checkpoint
5
+ exports to `config.json` + `model.safetensors` that `timestral.TimestralForecaster`
6
+ loads unchanged. We add `forward_train`, the Decoupled-Horizon-Forecasting training pass
7
+ (our reconstruction; Mistral has not published it):
8
+
9
+ - batch layout (B groups, V rows per group, T = N * patch steps); rows are target,
10
+ past-only covariate, known-future covariate, or padding;
11
+ - Enc = all N patch embeddings (causal per patch, past-only scaling + asinh);
12
+ Dec = N copies of the learned forecast query; per group a min-context s, decoder
13
+ blocks of K patches anchored at s, s+K, ...; Dec token in block b reads Enc[< b] plus
14
+ the causal Dec chain of its own block (timestral.masks.build_two_stream_time_mask);
15
+ - variate attention per patch slot over the V rows of a group with the covariate
16
+ firewall (covariates never read targets; known-future rows never read causal rows);
17
+ - target of Dec token p = true patch p normalized in the causal frame of patches < p
18
+ (the "advancing refine frame" the inference code decodes in), asinh-squashed;
19
+ - loss = pinball over the 21 quantile knots on observed target values of active patches.
20
+
21
+ NEVER load Timestral weights into this model (Modified-MIT licence, see NOTICE).
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import math
27
+ import os
28
+ import sys
29
+
30
+ import torch
31
+ import torch.nn.functional as F
32
+ from torch import Tensor
33
+
34
+ _HERE = os.path.dirname(os.path.abspath(__file__))
35
+ _TS_SRC = os.path.join(os.path.dirname(_HERE), "timestral", "src")
36
+ if os.path.isdir(_TS_SRC) and _TS_SRC not in sys.path:
37
+ sys.path.insert(0, _TS_SRC)
38
+
39
+ from timestral.config import TimestralConfig # noqa: E402
40
+ from timestral.layers import Attention, _apply_rotary # noqa: E402
41
+ from timestral.masks import build_two_stream_time_mask, compute_enc_key_validity # noqa: E402
42
+ from timestral.model import ROLE_KNOWN_COV, ROLE_PAST_COV, ROLE_TARGET, TimestralModel # noqa: E402
43
+ from timestral.scaler import apply_affine_asinh_masked, causal_patch_loc_scale # noqa: E402
44
+
45
+ ROLE_PAD = -1
46
+ QUANTILES_21 = (0.01, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5,
47
+ 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)
48
+
49
+
50
+ def make_config(size: str = "2.5M", context_length: int = 4096) -> TimestralConfig:
51
+ presets = {
52
+ # d_model, heads, mlp, layers-groups
53
+ "2.5M": (128, 2, 320, 2),
54
+ "5M": (192, 3, 512, 2),
55
+ "28M": (384, 6, 1024, 3),
56
+ "tiny": (64, 1, 128, 2), # CPU tests only
57
+ }
58
+ d, h, mlp, k = presets[size]
59
+ return TimestralConfig(
60
+ patch_size=32, d_model=d, num_heads=h, mlp_hidden_dim=mlp,
61
+ block_schedule=("time", "time", "time", "time_variate") * k,
62
+ quantile_knots=QUANTILES_21, block_size=32, context_length=context_length,
63
+ rope_theta=1e4, rope_partial_factor=0.5, norm_eps=1e-6, attn_bias=False, mlp_bias=False,
64
+ time_attn_gate="sdpa_sigmoid", variate_attn_gate="sdpa_sigmoid", minimum_scale=1e-6,
65
+ use_asinh=True, stabilize_with_global=False, covariate_enabled=True,
66
+ split_context_horizon_attn=True, attend_missing_patches=True, quantile_width_factor=3.0,
67
+ )
68
+
69
+
70
+ def count_params(model: torch.nn.Module) -> dict:
71
+ tot = sum(p.numel() for p in model.parameters())
72
+ emb = sum(p.numel() for n, p in model.named_parameters() if n.startswith("representation"))
73
+ head = sum(p.numel() for n, p in model.named_parameters() if n.startswith("head"))
74
+ return {"total": tot, "embed": emb, "trunk": tot - emb - head, "head": head}
75
+
76
+
77
+ def _variate_attention(attn: Attention, x: Tensor, mask: Tensor, B: int, V: int) -> Tensor:
78
+ """Variate attention per (group, slot) over V rows. x: (B*V, S, D); mask: (B*S, 1, V, V)."""
79
+ R, S, D = x.shape
80
+ inp = x.view(B, V, S, D).permute(0, 2, 1, 3).reshape(B * S, V, D)
81
+ q, k, v = attn._qkv(inp) # (B*S, V, H, hd)
82
+ out = F.scaled_dot_product_attention(q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), attn_mask=mask)
83
+ out = out.transpose(1, 2).reshape(B * S, V, D)
84
+ if attn._gate is not None:
85
+ out = out * attn._gate(inp).sigmoid()
86
+ out = attn.wO(out)
87
+ return out.view(B, S, V, D).permute(0, 2, 1, 3).reshape(R, S, D)
88
+
89
+
90
+ def _time_attention(attn: Attention, x: Tensor, mask: Tensor, cos: Tensor, sin: Tensor) -> Tensor:
91
+ q, k, v = attn._qkv(x)
92
+ q, k = _apply_rotary(q, k, cos, sin)
93
+ out = F.scaled_dot_product_attention(q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), attn_mask=mask)
94
+ out = out.transpose(1, 2).reshape(x.shape[0], x.shape[1], -1)
95
+ if attn._gate is not None:
96
+ out = out * attn._gate(x).sigmoid()
97
+ return attn.wO(out)
98
+
99
+
100
+ class ModularTS(TimestralModel):
101
+ """TimestralModel + DHF training pass (identical parameter names)."""
102
+
103
+ def __init__(self, config: TimestralConfig) -> None:
104
+ super().__init__(config)
105
+ self.register_buffer("q_levels", torch.tensor(config.quantile_knots, dtype=torch.float32), persistent=False)
106
+ self._init_weights()
107
+
108
+ def _init_weights(self) -> None:
109
+ for name, p in self.named_parameters():
110
+ if p.dim() == 2:
111
+ torch.nn.init.normal_(p, std=0.02)
112
+ if name.endswith("wO.weight") or name.endswith("w3.weight"):
113
+ p.data.mul_(1.0 / math.sqrt(2 * len(self.config.block_schedule)))
114
+ elif name.endswith(".bias"):
115
+ torch.nn.init.zeros_(p)
116
+ torch.nn.init.normal_(self.representation.forecast_query, std=0.02)
117
+
118
+ # ------------------------------------------------------------------ masks
119
+ def build_masks(self, observed: Tensor, roles: Tensor, s: Tensor):
120
+ """observed (B,V,T) bool, roles (B,V) long, s (B,) long -> masks/positions/key validity."""
121
+ c = self.config
122
+ B, V, T = observed.shape
123
+ P, N, R = c.patch_size, T // c.patch_size, B * V
124
+ dev = observed.device
125
+ obs_pp = observed.view(R, N, P).any(-1)
126
+ not_pad = (roles != ROLE_PAD).view(R)
127
+ key_valid = compute_enc_key_validity(obs_pp, c.attend_missing_patches) & not_pad[:, None]
128
+ known = (roles == ROLE_KNOWN_COV).view(R)
129
+ target = (roles == ROLE_TARGET).view(R)
130
+ s_rows = s.repeat_interleave(V)
131
+ time_mask = build_two_stream_time_mask(s_rows, N, c.block_size, key_valid, bidirectional_rows=known)
132
+ # variate mask (B*2N, 1, V, V)
133
+ slot_valid = torch.cat([key_valid, not_pad[:, None].expand(R, N)], dim=1).view(B, V, 2 * N)
134
+ keyok = slot_valid.permute(0, 2, 1)[:, :, None, :] # (B, 2N, 1, Vk)
135
+ tg = target.view(B, V)
136
+ kn = known.view(B, V)
137
+ forbid = ((~tg)[:, :, None] & tg[:, None, :]) | (kn[:, :, None] & (~kn)[:, None, :]) # (B, Vq, Vk)
138
+ vmask = keyok & ~forbid[:, None]
139
+ eye = torch.eye(V, dtype=torch.bool, device=dev)
140
+ vmask = (vmask | eye).reshape(B * 2 * N, 1, V, V)
141
+ # positions anchored at each group's first observed patch
142
+ grp_obs = obs_pp.view(B, V, N).any(1) # (B, N)
143
+ first = torch.where(grp_obs.any(-1), grp_obs.float().argmax(-1), torch.zeros(B, device=dev)).long()
144
+ p = torch.arange(N, device=dev)
145
+ pos = (p[None] - first[:, None]).repeat_interleave(V, 0)
146
+ positions = torch.cat([pos, pos], dim=1)
147
+ return time_mask, vmask, positions, obs_pp, target, known
148
+
149
+ # ---------------------------------------------------------------- forward
150
+ def forward_train(self, values: Tensor, observed: Tensor, roles: Tensor, s: Tensor, return_preds: bool = False,
151
+ teacher: Tensor | None = None, teacher_obs: Tensor | None = None, gt_row: Tensor | None = None,
152
+ lam: float = 0.5, teacher_q_idx: tuple = (2, 4, 6, 8, 10, 12, 14, 16, 18)):
153
+ """values (B,V,T) float (anything where unobserved), observed (B,V,T) bool,
154
+ roles (B,V) long in {-1,0,1,2}, s (B,) min-context patches. Returns (loss, stats)."""
155
+ c = self.config
156
+ B, V, T = values.shape
157
+ P, N, R = c.patch_size, T // c.patch_size, B * V
158
+ x = torch.where(observed, values, torch.zeros_like(values)).float().view(R, T)
159
+ obs = observed.view(R, T)
160
+
161
+ with torch.no_grad():
162
+ time_mask, vmask, positions, obs_pp, target_rows, known_rows = self.build_masks(observed, roles, s)
163
+ loc, scale = causal_patch_loc_scale(x[:, None], obs[:, None], P, c.minimum_scale, False)
164
+ loc, scale = loc[:, 0], scale[:, 0]
165
+ scaled = apply_affine_asinh_masked(x, loc, scale, obs, use_asinh=c.use_asinh)
166
+ scaled, _, _ = self._known_cov_scaling(x, obs, scaled, loc, scale, known_rows)
167
+ # DHF targets in the frame of patches < p
168
+ loc_pp = loc.view(R, N, P)[..., 0]
169
+ sc_pp = scale.view(R, N, P)[..., 0]
170
+ f_loc = torch.cat([loc_pp[:, :1], loc_pp[:, :-1]], dim=1)
171
+ f_sc = torch.cat([sc_pp[:, :1], sc_pp[:, :-1]], dim=1)
172
+ y = torch.asinh((x.view(R, N, P) - f_loc[..., None]) / f_sc[..., None].clamp_min(1e-6))
173
+ cnt_before = torch.cumsum(obs.view(R, N, P).sum(-1), 1) - obs.view(R, N, P).sum(-1)
174
+ p_idx = torch.arange(N, device=x.device)
175
+ active = p_idx[None] >= s.repeat_interleave(V)[:, None]
176
+ w = (obs.view(R, N, P) & target_rows[:, None, None] & active[..., None] & (cnt_before >= 2)[..., None]).float()
177
+ if gt_row is not None: # rows whose ground truth may not be used (teacher-only inputs)
178
+ w = w * gt_row.view(R, 1, 1).float()
179
+ y = torch.where(w > 0, y, torch.zeros_like(y))
180
+ if teacher is not None:
181
+ # teacher (R, T, Qt) raw-space quantiles (anything where ~teacher_obs) -> same advancing frame + asinh
182
+ Qt = teacher.shape[-1]
183
+ tv = torch.where(teacher_obs.view(R, T, 1), teacher.view(R, T, Qt).float(), torch.zeros(1, device=x.device))
184
+ yt = torch.asinh((tv.view(R, N, P, Qt) - f_loc[..., None, None]) / f_sc[..., None, None].clamp_min(1e-6))
185
+ wt = (teacher_obs.view(R, N, P) & target_rows[:, None, None] & active[..., None] & (cnt_before >= 2)[..., None]).float()
186
+ yt = torch.where(wt[..., None] > 0, yt, torch.zeros_like(yt))
187
+
188
+ x_enc = self.representation(scaled, obs)
189
+ x_dec = self.representation.forecast_tokens(R, N)
190
+ h = torch.cat([x_enc, x_dec.to(x_enc.dtype)], dim=1)
191
+ cos, sin = self.transformer.rope(h, positions)
192
+ for layer in self.transformer.layers:
193
+ for norm, attn in zip(layer.attn_norms, layer.attns):
194
+ if attn.is_time:
195
+ h = h + _time_attention(attn, norm(h), time_mask, cos, sin)
196
+ else:
197
+ h = h + _variate_attention(attn, norm(h), vmask, B, V)
198
+ h = h + layer.mlp(layer.mlp_norm(h))
199
+ h = self.transformer.out_norm(h)
200
+ preds = self.head(h[:, N:]).view(R, N, P, -1).float() # (R,N,P,Q) normalized, monotone
201
+
202
+ if return_preds:
203
+ return preds, f_loc, f_sc
204
+ err = y[..., None] - preds
205
+ tau = self.q_levels.view(1, 1, 1, -1)
206
+ pin = torch.maximum(tau * err, (tau - 1) * err).mean(-1)
207
+ denom = w.sum().clamp_min(1.0)
208
+ loss = (pin * w).sum() / denom
209
+ with torch.no_grad():
210
+ mae = ((y - preds[..., c.median_index]).abs() * w).sum() / denom
211
+ stats = {"pinball": loss.detach(), "mae_med": mae, "n_tgt": denom}
212
+ if teacher is not None:
213
+ pq = preds[..., list(teacher_q_idx)]
214
+ tq = self.q_levels[list(teacher_q_idx)].view(1, 1, 1, -1)
215
+ et = yt - pq
216
+ kd = torch.maximum(tq * et, (tq - 1) * et).mean(-1)
217
+ kd_loss = (kd * wt).sum() / wt.sum().clamp_min(1.0)
218
+ loss = (1 - lam) * loss + lam * kd_loss
219
+ stats["kd"] = kd_loss.detach()
220
+ return loss, stats
modular_ts/realdata.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Optional real-data source: Salesforce/GiftEvalPretrain (OFF by default).
2
+
3
+ GiftEvalPretrain's card says Apache-2.0 but "for research purposes only"; the user has
4
+ not chosen between synthetic-only and synthetic+GiftEvalPretrain, so train.py keeps this
5
+ behind `--data synthetic+gifteval` (default `--data synthetic`).
6
+
7
+ Contamination filter: every GiftEvalPretrain sub-dataset whose normalised name matches
8
+ (or is an explicit alias of) a dataset in the GIFT-Eval test pool, fev-bench tasks or the
9
+ TIME benchmark is dropped. The resolved include/exclude lists are written next to each
10
+ checkpoint (data_manifest.json) for the model card's contamination audit.
11
+
12
+ Data layout on the Hub: one folder per sub-dataset holding HF `save_to_disk` arrow
13
+ shards (`data-0000k-of-0000n.arrow`) with columns item_id, start, freq, target
14
+ (1-D or 2-D list). We download shards lazily, memory-map them, and crop random windows
15
+ on a background thread.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import queue
21
+ import math
22
+ import random
23
+ import re
24
+ import threading
25
+
26
+ import numpy as np
27
+ import torch
28
+
29
+ REPO = "Salesforce/GiftEvalPretrain"
30
+
31
+ GIFT_EVAL_TEST = [
32
+ "m4_yearly", "m4_quarterly", "m4_monthly", "m4_weekly", "m4_daily", "m4_hourly", "electricity", "solar",
33
+ "hospital", "covid_deaths", "us_births", "saugeenday", "temperature_rain_with_missing",
34
+ "kdd_cup_2018_with_missing", "car_parts_with_missing", "restaurant", "hierarchical_sales", "LOOP_SEATTLE",
35
+ "SZ_TAXI", "M_DENSE", "ett1", "ett2", "jena_weather", "bitbrains_fast_storage", "bitbrains_rnd",
36
+ "bizitobs_application", "bizitobs_service", "bizitobs_l2c",
37
+ ]
38
+ FEV_BENCH = [
39
+ "ETT", "LOOP_SEATTLE", "M_DENSE", "SZ_TAXI", "australian_tourism", "bizitobs_l2c", "boomlet", "ecdc_ili",
40
+ "entsoe", "epf_be", "epf_de", "epf_fr", "epf_np", "epf_pjm", "ercot", "favorita_stores", "favorita_transactions",
41
+ "fred_md_2025", "fred_qd_2025", "gvar", "hermes", "hierarchical_sales", "hospital", "hospital_admissions",
42
+ "jena_weather", "kdd_cup_2022", "m5", "proenfo_gfc12", "proenfo_gfc14", "proenfo_gfc17", "redset", "restaurant",
43
+ "rohlik_orders", "rohlik_sales", "rossmann", "solar", "solar_with_weather", "uci_air_quality", "uk_covid_nation",
44
+ "uk_covid_utla", "us_consumption", "walmart", "world_co2_emissions", "world_life_expectancy", "world_tourism",
45
+ ]
46
+ TIME_BENCH = [
47
+ "Australia_Solar", "Auto_Production_SF", "CPHL", "Coastal_T_S", "Commodity_Import", "Commodity_Production",
48
+ "Crypto", "ECDC_COVID", "EWELD_Load", "Finland_Traffic", "Global_Influenza", "Global_Price", "Housing_Inventory",
49
+ "JOLTS", "Job_Claims", "MetroPT-3", "NE_China_Wind", "Oil_Price", "Online_Retail_2_UCI", "OpenElectricity_NEM",
50
+ "Port_Activity", "SG_Carpark", "SG_PM25", "SG_Weather", "Smart_Manufacturing", "Supply_Chain_Customer",
51
+ "Supply_Chain_Location", "US_Labor", "US_Term_Structure", "Uncertainty_1M", "Vehicle_Sales", "Vehicle_Supply",
52
+ "WUI_Global", "Water_Quality_Darwin", "azure2019", "current_velocity", "epf_electricity_price",
53
+ ]
54
+ # GiftEvalPretrain names that are the same source as a benchmark dataset under another name.
55
+ ALIASES_EXCLUDE = {
56
+ "favorita_sales": "fev favorita_stores", "favorita_transactions": "fev favorita_transactions",
57
+ "m5": "fev m5", "kdd2022": "fev kdd_cup_2022", "gfc12_load": "fev proenfo_gfc12",
58
+ "gfc14_load": "fev proenfo_gfc14", "gfc17_load": "fev proenfo_gfc17", "fred_md": "fev fred_md_2025",
59
+ "solar_power": "GIFT-Eval solar (conservative)", "ecdc_ili": "fev ecdc_ili",
60
+ }
61
+
62
+
63
+ def _norm(s: str) -> str:
64
+ s = s.lower()
65
+ s = re.sub(r"_with_missing$", "", s)
66
+ s = re.sub(r"_(\d+[a-z]+|daily|hourly|weekly|monthly|yearly|quarterly)$", "", s)
67
+ return re.sub(r"[^a-z0-9]", "", s)
68
+
69
+
70
+ def list_pretrain_datasets() -> list[str]:
71
+ from huggingface_hub import HfApi
72
+ tree = HfApi().list_repo_tree(REPO, repo_type="dataset")
73
+ return sorted(t.path for t in tree if "." not in t.path)
74
+
75
+
76
+ def filter_datasets(names: list[str]) -> tuple[list[str], dict[str, str]]:
77
+ bench = {}
78
+ for pool, lst in (("GIFT-Eval", GIFT_EVAL_TEST), ("fev-bench", FEV_BENCH), ("TIME", TIME_BENCH)):
79
+ for b in lst:
80
+ bench[_norm(b)] = f"{pool} {b}"
81
+ keep, dropped = [], {}
82
+ for n in names:
83
+ k = _norm(n)
84
+ hit = ALIASES_EXCLUDE.get(n) or bench.get(k)
85
+ if hit is None:
86
+ hit = next((v for bk, v in bench.items() if len(bk) >= 5 and (bk in k or k in bk)), None)
87
+ if hit:
88
+ dropped[n] = hit
89
+ else:
90
+ keep.append(n)
91
+ return keep, dropped
92
+
93
+
94
+ class GiftEvalPretrainStream:
95
+ """Random fixed-length windows from GiftEvalPretrain, batched as (B, V, T) like SynthStream.
96
+
97
+ Univariate series give V=1 groups; multivariate series give up to `max_v` variates.
98
+ `datasets` is optional; used only to memory-map arrow shards.
99
+ """
100
+
101
+ def __init__(self, T: int, tokens_per_batch: int, device="cpu", seed=0, datasets_subset: list[str] | None = None,
102
+ max_shards_per_dataset: int = 2, max_v: int = 16, patch=32, min_ctx=4, cache_dir=None, prefetch=4):
103
+ from datasets import Dataset as HFDataset
104
+ from huggingface_hub import HfApi, hf_hub_download
105
+
106
+ names = datasets_subset or list_pretrain_datasets()
107
+ self.keep, self.dropped = filter_datasets(names)
108
+ self.T, self.N, self.tok, self.patch = T, T // patch, tokens_per_batch, patch
109
+ self.dev, self.min_ctx, self.max_v = torch.device(device), min_ctx, max_v
110
+ self.rng = random.Random(seed)
111
+ api = HfApi()
112
+ self.tables = []
113
+ for name in self.keep:
114
+ files = sorted(f for f in api.list_repo_files(REPO, repo_type="dataset") if f.startswith(name + "/") and f.endswith(".arrow"))
115
+ for f in files[:max_shards_per_dataset]:
116
+ p = hf_hub_download(REPO, f, repo_type="dataset", cache_dir=cache_dir)
117
+ self.tables.append((name, HFDataset.from_file(p)))
118
+ assert self.tables, "no GiftEvalPretrain shards left after filtering"
119
+ self.q: queue.Queue = queue.Queue(maxsize=prefetch)
120
+ threading.Thread(target=self._worker, daemon=True).start()
121
+
122
+ def manifest(self) -> dict:
123
+ return {"source": REPO, "included": self.keep, "excluded": self.dropped}
124
+
125
+ def _window(self):
126
+ name, tab = self.rng.choice(self.tables)
127
+ row = tab[self.rng.randrange(len(tab))]
128
+ x = np.asarray(row["target"], dtype=np.float32)
129
+ x = x[None] if x.ndim == 1 else x
130
+ if x.shape[0] > self.max_v:
131
+ x = x[self.rng.sample(range(x.shape[0]), self.max_v)]
132
+ L = x.shape[1]
133
+ if L >= self.T:
134
+ st = self.rng.randrange(L - self.T + 1)
135
+ w = x[:, st : st + self.T]
136
+ else: # left-pad with NaN (unobserved)
137
+ w = np.concatenate([np.full((x.shape[0], self.T - L), np.nan, np.float32), x], 1)
138
+ return w
139
+
140
+ def _make(self):
141
+ # bucket by V: draw one window, then fill the batch with windows of the same V
142
+ first = self._window()
143
+ V = first.shape[0]
144
+ Bg = max(1, self.tok // (V * self.N))
145
+ ws = [first]
146
+ tries = 0
147
+ while len(ws) < Bg and tries < 20 * Bg:
148
+ w = self._window()
149
+ tries += 1
150
+ if w.shape[0] == V:
151
+ ws.append(w)
152
+ arr = np.stack(ws)
153
+ obs = np.isfinite(arr)
154
+ vals = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)
155
+ return vals, obs
156
+
157
+ def _worker(self):
158
+ while True:
159
+ self.q.put(self._make())
160
+
161
+ def next_batch(self, V=None) -> dict:
162
+ vals, obs = self.q.get()
163
+ Bg, V, _ = vals.shape
164
+ s = torch.randint(self.min_ctx, self.N - 1, (Bg,))
165
+ return {
166
+ "values": torch.from_numpy(vals).to(self.dev, non_blocking=True),
167
+ "observed": torch.from_numpy(obs).to(self.dev, non_blocking=True),
168
+ "roles": torch.zeros(Bg, V, dtype=torch.long, device=self.dev),
169
+ "s": s.to(self.dev),
170
+ }
171
+
172
+
173
+ class MixedStream:
174
+ """Draw from `real` with probability `real_frac`, else from `synth`."""
175
+
176
+ def __init__(self, synth, real, real_frac: float, seed=0):
177
+ self.synth, self.real, self.p = synth, real, real_frac
178
+ self.rng = random.Random(seed)
179
+
180
+ def next_batch(self, V=None):
181
+ return self.real.next_batch() if self.rng.random() < self.p else self.synth.next_batch(V)
182
+
183
+
184
+ # ---------------------------------------------------------------------------------------------
185
+ # Fast GPU-resident stream (used for run1). The thread/row-based stream above cannot keep up
186
+ # with ~12+ steps/s x 1024 windows; this one loads a size-capped selection of shards once into
187
+ # one flat float32 tensor on the GPU and crops whole batches with a single vectorised gather.
188
+ # ---------------------------------------------------------------------------------------------
189
+
190
+ def _arrow_channels(path):
191
+ """Return (values float32 1-D, ch_off int64, ch_len int64, row_first_ch, row_nvar)."""
192
+ import pyarrow as pa
193
+ try:
194
+ tab = pa.ipc.open_stream(path).read_all()
195
+ except Exception:
196
+ tab = pa.ipc.open_file(path).read_all()
197
+ col = tab.column("target").combine_chunks()
198
+ t = col.type
199
+ if pa.types.is_list(t) and not (pa.types.is_list(t.value_type) or pa.types.is_fixed_size_list(t.value_type)):
200
+ inner, nvar = col, np.ones(len(col), np.int64)
201
+ elif pa.types.is_fixed_size_list(t):
202
+ inner, nvar = col.flatten(), np.full(len(col), t.list_size, np.int64)
203
+ else: # list<list<float>>
204
+ inner = col.flatten()
205
+ off = np.asarray(col.offsets)
206
+ nvar = (off[1:] - off[:-1]).astype(np.int64)
207
+ off = np.asarray(inner.offsets).astype(np.int64)
208
+ vals = inner.values.to_numpy(zero_copy_only=False).astype(np.float32, copy=False)
209
+ base = off[0]
210
+ vals = vals[base:off[-1]]
211
+ ch_off = off[:-1] - base
212
+ ch_len = off[1:] - off[:-1]
213
+ row_first = np.concatenate([[0], np.cumsum(nvar)[:-1]]).astype(np.int64)
214
+ return vals, ch_off, ch_len, row_first, nvar
215
+
216
+
217
+ class FastGiftEvalPretrain:
218
+ def __init__(self, T, tokens_per_batch, device, selection: dict, seed=0, patch=32, cache_dir=None,
219
+ mv_frac=0.3, max_v=16, short_ctx_p=0.3, ds_temp=0.5, workers=16):
220
+ from concurrent.futures import ThreadPoolExecutor
221
+ from huggingface_hub import hf_hub_download
222
+ names = sorted(selection)
223
+ self.keep, self.dropped = filter_datasets(names)
224
+ assert set(self.keep) == set(names), f"selection contains filtered datasets: {set(names) - set(self.keep)}"
225
+ self.T, self.P, self.N, self.tok = T, patch, T // patch, tokens_per_batch
226
+ self.dev, self.max_v, self.mv_frac, self.short_ctx_p = torch.device(device), max_v, mv_frac, short_ctx_p
227
+ self.g = torch.Generator(device=self.dev); self.g.manual_seed(seed + 777)
228
+ jobs = [(n, f) for n in names for f in selection[n]]
229
+ with ThreadPoolExecutor(workers) as ex:
230
+ paths = list(ex.map(lambda nf: hf_hub_download(REPO, nf[1], repo_type="dataset", cache_dir=cache_dir), jobs))
231
+ print(f"[gep] downloaded {len(paths)} shards", flush=True)
232
+ with ThreadPoolExecutor(8) as ex:
233
+ parsed = list(ex.map(_arrow_channels, paths))
234
+ vals_all, ch_off_all, ch_len_all, ch_ds, mv_rows = [], [], [], [], [] # mv_rows: (ds, first_ch_global, nvar)
235
+ base, nch = 0, 0
236
+ ds_idx = {n: i for i, n in enumerate(names)}
237
+ self.ds_points = np.zeros(len(names))
238
+ for (n, _), (v, co, cl, rf, nv) in zip(jobs, parsed):
239
+ keep = cl >= 8
240
+ d = ds_idx[n]
241
+ vals_all.append(v)
242
+ ch_off_all.append(co + base); ch_len_all.append(cl); ch_ds.append(np.full(len(cl), d, np.int64))
243
+ m = (nv >= 2)
244
+ if m.any():
245
+ mv_rows.append(np.stack([np.full(m.sum(), d), rf[m] + nch, nv[m]], 1))
246
+ self.ds_points[d] += cl[keep].sum()
247
+ base += len(v); nch += len(cl)
248
+ flat = np.concatenate(vals_all)
249
+ ch_off, ch_len, ch_ds = map(np.concatenate, (ch_off_all, ch_len_all, ch_ds))
250
+ good = ch_len >= 8
251
+ print(f"[gep] {len(names)} datasets, {nch} channels, {len(flat)/1e9:.2f}B points", flush=True)
252
+ self.flat = torch.from_numpy(flat).to(self.dev)
253
+ del flat, vals_all
254
+ # univariate channel pool grouped by dataset
255
+ order = np.argsort(ch_ds[good], kind="stable")
256
+ gi = np.nonzero(good)[0][order]
257
+ self.u_off = torch.from_numpy(ch_off[gi]).to(self.dev)
258
+ self.u_len = torch.from_numpy(ch_len[gi]).to(self.dev)
259
+ cnt = np.bincount(ch_ds[gi], minlength=len(names))
260
+ self.u_start = torch.from_numpy(np.concatenate([[0], np.cumsum(cnt)[:-1]])).to(self.dev)
261
+ self.u_cnt = torch.from_numpy(cnt).to(self.dev)
262
+ w = np.where(cnt > 0, self.ds_points ** ds_temp, 0.0)
263
+ self.u_w = torch.tensor(w / w.sum(), dtype=torch.float, device=self.dev)
264
+ self.weights = {n: float(x) for n, x in zip(names, w / w.sum())}
265
+ # multivariate rows
266
+ self.ch_off_t = torch.from_numpy(ch_off).to(self.dev)
267
+ self.ch_len_t = torch.from_numpy(ch_len).to(self.dev)
268
+ self.mv = None
269
+ if mv_rows:
270
+ mvr = np.concatenate(mv_rows)
271
+ mvr = mvr[ch_len[mvr[:, 1]] >= 8]
272
+ self.mv = mvr
273
+ self.mv_by_v = {}
274
+ for V in (2, 4, 8, 16):
275
+ sub = mvr[mvr[:, 2] >= V]
276
+ if len(sub) == 0:
277
+ continue
278
+ dcnt = np.bincount(sub[:, 0], minlength=len(names))
279
+ o = np.argsort(sub[:, 0], kind="stable"); sub = sub[o]
280
+ wv = np.where(dcnt > 0, self.ds_points ** ds_temp, 0.0)
281
+ self.mv_by_v[V] = dict(first=torch.from_numpy(sub[:, 1]).to(self.dev), nvar=torch.from_numpy(sub[:, 2]).to(self.dev),
282
+ start=torch.from_numpy(np.concatenate([[0], np.cumsum(dcnt)[:-1]])).to(self.dev),
283
+ cnt=torch.from_numpy(dcnt).to(self.dev), w=torch.tensor(wv / wv.sum(), dtype=torch.float, device=self.dev))
284
+ self.rng = random.Random(seed)
285
+ self.ar = torch.arange(T, device=self.dev)
286
+
287
+ def manifest(self):
288
+ try:
289
+ excl = filter_datasets(list_pretrain_datasets())[1]
290
+ except Exception as e: # offline
291
+ excl = f"unavailable: {e!r}"
292
+ return {"source": REPO, "selection": "size-capped shard selection (gep_selection.json)", "included": self.keep,
293
+ "excluded_by_contamination_filter": excl, "sampling_weights": self.weights, "points_per_dataset": {n: int(x) for n, x in zip(sorted(self.weights), self.ds_points)}}
294
+
295
+ def _rand(self, n):
296
+ return torch.rand(n, generator=self.g, device=self.dev)
297
+
298
+ def _pick(self, w, start, cnt, n):
299
+ d = torch.multinomial(w, n, replacement=True, generator=self.g)
300
+ return start[d] + (self._rand(n) * cnt[d]).long().clamp_max(cnt[d] - 1)
301
+
302
+ def _crop(self, off, L):
303
+ """off, L: (R,) -> values (R,T), observed (R,T). Same start for rows of one group handled by caller."""
304
+ T = self.T
305
+ st = torch.where(L >= T, (self._rand(L.shape[0]) * (L - T + 1)).long(), L - T)
306
+ return st
307
+
308
+ def _gather(self, off, L, st, keep_len):
309
+ pos = st[:, None] + self.ar[None]
310
+ valid = (pos >= 0) & (pos < L[:, None]) & (self.ar[None] >= (self.T - keep_len)[:, None])
311
+ idx = (off[:, None] + pos.clamp(min=0)).clamp_max(self.flat.numel() - 1)
312
+ v = self.flat[idx]
313
+ obs = valid & torch.isfinite(v)
314
+ return torch.where(obs, v, torch.zeros_like(v)), obs
315
+
316
+ def _s(self, obs_bv):
317
+ # obs_bv (B,V,T): first observed patch per group -> min-context split after it
318
+ B = obs_bv.shape[0]
319
+ any_p = obs_bv.view(B, -1, self.N, self.P).any(-1).any(1) # (B,N)
320
+ fs = torch.where(any_p.any(1), any_p.float().argmax(1), torch.zeros(B, dtype=torch.long, device=self.dev))
321
+ span = (self.N - 2 - fs).clamp_min(1)
322
+ s = fs + 1 + (self._rand(B) * span).long()
323
+ return s.clamp(1, self.N - 2)
324
+
325
+ def _keep_len(self, n):
326
+ k = torch.full((n,), self.T, device=self.dev, dtype=torch.long)
327
+ short = self._rand(n) < self.short_ctx_p
328
+ kl = torch.exp(math.log(64) + self._rand(n) * (math.log(self.T) - math.log(64))).long()
329
+ return torch.where(short, kl, k)
330
+
331
+ @torch.no_grad()
332
+ def next_batch(self, V=None):
333
+ if self.mv is not None and self.rng.random() < self.mv_frac:
334
+ V = self.rng.choice(sorted(self.mv_by_v))
335
+ m = self.mv_by_v[V]
336
+ Bg = max(1, self.tok // (V * self.N))
337
+ r = self._pick(m["w"], m["start"], m["cnt"], Bg)
338
+ first, nvar = m["first"][r], m["nvar"][r]
339
+ j0 = (self._rand(Bg) * (nvar - V + 1)).long()
340
+ ch = first[:, None] + j0[:, None] + torch.arange(V, device=self.dev)[None] # (Bg,V)
341
+ off, L = self.ch_off_t[ch], self.ch_len_t[ch]
342
+ Lmin = L.min(1).values
343
+ st = self._crop(None, Lmin)
344
+ kl = self._keep_len(Bg)
345
+ v, o = self._gather(off.reshape(-1), L.reshape(-1), st.repeat_interleave(V), kl.repeat_interleave(V))
346
+ v, o = v.view(Bg, V, self.T), o.view(Bg, V, self.T)
347
+ else:
348
+ V = 1
349
+ Bg = max(1, self.tok // self.N)
350
+ c = self._pick(self.u_w, self.u_start, self.u_cnt, Bg)
351
+ off, L = self.u_off[c], self.u_len[c]
352
+ st = self._crop(off, L)
353
+ v, o = self._gather(off, L, st, self._keep_len(Bg))
354
+ v, o = v[:, None], o[:, None]
355
+ return {"values": v, "observed": o, "roles": torch.zeros(Bg, V, dtype=torch.long, device=self.dev), "s": self._s(o)}
356
+
357
+
358
+ if __name__ == "__main__":
359
+ names = list_pretrain_datasets()
360
+ keep, dropped = filter_datasets(names)
361
+ print(f"{len(names)} sub-datasets; keep {len(keep)}; drop {len(dropped)}")
362
+ for k, v in dropped.items():
363
+ print(f" DROP {k:40s} <- {v}")
modular_ts/realdata2.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GiftEvalPretrain stream v2 (Modular-TS v3): resolution + mixup augmentation on real data.
2
+
3
+ Adds to FastGiftEvalPretrain (run1):
4
+ * TTM-style "diverse resolution sampling" (Ekambaram et al., arXiv 2401.03955): with prob
5
+ `ds_p` a univariate batch is drawn at a coarser resolution, mean-pooling k in {2,4,8}
6
+ consecutive raw steps (NaN-aware), creating new frequencies from the same corpus;
7
+ * TSMixup (Chronos, arXiv 2403.07815): with prob `mix_p` rows are replaced by a convex
8
+ combination of 2-3 standardised real series (Dirichlet weights), rescaled;
9
+ * flatter dataset temperature (ds_temp 0.3 instead of 0.5) so small datasets are seen more.
10
+ Contamination filter and GPU-resident layout are inherited unchanged.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import torch
16
+
17
+ from modular_ts.realdata import FastGiftEvalPretrain
18
+
19
+
20
+ class FastGiftEvalPretrain2(FastGiftEvalPretrain):
21
+ def __init__(self, *a, ds_p=0.3, ds_factors=(2, 4, 8), mix_p=0.2, **k):
22
+ k.setdefault("ds_temp", 0.3)
23
+ super().__init__(*a, **k)
24
+ self.ds_p, self.ds_factors, self.mix_p = ds_p, ds_factors, mix_p
25
+
26
+ def _gather_k(self, off, L, kf):
27
+ """Gather T*kf raw steps ending at a random point and mean-pool by kf."""
28
+ T, R = self.T, off.shape[0]
29
+ TK = T * kf
30
+ st = torch.where(L >= TK, (self._rand(R) * (L - TK + 1)).long(), L - TK)
31
+ ar = torch.arange(TK, device=self.dev)
32
+ pos = st[:, None] + ar[None]
33
+ valid = (pos >= 0) & (pos < L[:, None])
34
+ idx = (off[:, None] + pos.clamp(min=0)).clamp_max(self.flat.numel() - 1)
35
+ v = self.flat[idx]
36
+ ok = valid & torch.isfinite(v)
37
+ v = torch.where(ok, v, torch.zeros_like(v)).view(R, T, kf)
38
+ c = ok.view(R, T, kf).sum(-1)
39
+ out = v.sum(-1) / c.clamp_min(1)
40
+ obs = c > 0
41
+ return torch.where(obs, out, torch.zeros_like(out)), obs
42
+
43
+ def _mixup(self, v, o):
44
+ R, T = v.shape
45
+ both = o & o.roll(1, 0) & o.roll(2, 0)
46
+ def zs(x, m):
47
+ n = m.sum(-1, keepdim=True).clamp_min(1)
48
+ mu = (x * m).sum(-1, keepdim=True) / n
49
+ sd = (((x - mu) ** 2) * m).sum(-1, keepdim=True).div(n).sqrt().clamp_min(1e-6)
50
+ return (x - mu) / sd, sd, mu
51
+ z0, sd0, mu0 = zs(v, o)
52
+ z1, _, _ = zs(v.roll(1, 0), o.roll(1, 0))
53
+ z2, _, _ = zs(v.roll(2, 0), o.roll(2, 0))
54
+ w = torch.distributions.Dirichlet(torch.full((3,), 1.5, device=self.dev)).sample((R,))
55
+ w[:, 2] *= (self._rand(R) < 0.5).float()
56
+ w = w / w.sum(-1, keepdim=True)
57
+ m = w[:, :1] * z0 + w[:, 1:2] * z1 + w[:, 2:3] * z2
58
+ m = m * sd0 + mu0
59
+ sel = (self._rand(R) < self.mix_p)[:, None]
60
+ return torch.where(sel, torch.where(both, m, 0.0), v), torch.where(sel, both, o)
61
+
62
+ @torch.no_grad()
63
+ def next_batch(self, V=None):
64
+ if (self.mv is not None and self.rng.random() < self.mv_frac) or self.rng.random() >= self.ds_p:
65
+ b = super().next_batch(V)
66
+ if b["values"].shape[1] == 1 and self.mix_p > 0:
67
+ v, o = self._mixup(b["values"][:, 0], b["observed"][:, 0])
68
+ b["values"], b["observed"] = v[:, None], o[:, None]
69
+ b["s"] = self._s(b["observed"])
70
+ return b
71
+ kf = self.rng.choice(self.ds_factors)
72
+ Bg = max(1, self.tok // self.N)
73
+ c = self._pick(self.u_w, self.u_start, self.u_cnt, Bg)
74
+ off, L = self.u_off[c], self.u_len[c]
75
+ v, o = self._gather_k(off, L, kf)
76
+ o = o & (torch.arange(self.T, device=self.dev)[None] >= (self.T - self._keep_len(Bg))[:, None])
77
+ v = torch.where(o, v, torch.zeros_like(v))
78
+ if self.mix_p > 0:
79
+ v, o = self._mixup(v, o)
80
+ v, o = v[:, None], o[:, None]
81
+ return {"values": v, "observed": o, "roles": torch.zeros(Bg, 1, dtype=torch.long, device=self.dev), "s": self._s(o)}
modular_ts/synth.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Streamed, fully vectorised synthetic time-series generator (torch; runs on GPU).
2
+
3
+ Univariate bases (each switched on per series with its own probability, summed or
4
+ multiplied):
5
+ * KernelSynth-style GP components via random Fourier features: RBF (log-uniform
6
+ length-scale), rational-quadratic (scale-mixture of RBFs), periodic kernels at
7
+ frequency-typical periods with random harmonics, linear (trend), white noise;
8
+ * TSI-style trends: linear / quadratic / piecewise-linear with change points / damped;
9
+ * AR(1) / random walk (FFT convolution with phi^k kernels);
10
+ * heteroscedastic noise (log-GP volatility, GARCH-like clustering), student-t tails;
11
+ * level shifts, spikes / outliers;
12
+ * intermittent (Croston-like zero-inflated count) and bursty (Poisson bursts with
13
+ exponential decay) observability-style signals;
14
+ * non-negativity transforms, random offset / scale over many orders of magnitude.
15
+ Multivariatizers (Chronos-2 / CauKer style): latent univariate drivers mixed into each
16
+ row by a random sparse matrix through random nonlinearities with lead-lag delays, plus
17
+ idiosyncratic noise; optional covariate tasks where some rows are past-only or
18
+ known-future covariates (incl. 0/1 event / calendar indicators) and targets respond to
19
+ them through random lagged nonlinear effects.
20
+ Masks: iid missing points, contiguous gaps, late series starts (left padding).
21
+
22
+ `SynthStream(...).next_batch()` returns dict(values, observed, roles, s) ready for
23
+ `ModularTS.forward_train`.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import math
29
+
30
+ import torch
31
+ from torch import Tensor
32
+
33
+ PERIOD_BANK = torch.tensor([4, 5, 7, 12, 24, 48, 52, 60, 96, 144, 168, 288, 336, 365, 672, 1008, 1440, 2016], dtype=torch.float32)
34
+
35
+
36
+ def _std(x: Tensor) -> Tensor:
37
+ return x.std(-1, keepdim=True).clamp_min(1e-6)
38
+
39
+
40
+ def _z(x: Tensor) -> Tensor:
41
+ return (x - x.mean(-1, keepdim=True)) / _std(x)
42
+
43
+
44
+ class SynthGen:
45
+ def __init__(self, T: int, device: str | torch.device = "cpu", seed: int = 0, n_rff: int = 32):
46
+ self.T = T
47
+ self.dev = torch.device(device)
48
+ self.g = torch.Generator(device=self.dev)
49
+ self.g.manual_seed(seed)
50
+ self.n_rff = n_rff
51
+ self.t = torch.arange(T, device=self.dev, dtype=torch.float32)
52
+
53
+ # -- random helpers
54
+ def U(self, *shape, lo=0.0, hi=1.0):
55
+ return lo + (hi - lo) * torch.rand(*shape, generator=self.g, device=self.dev)
56
+
57
+ def N(self, *shape):
58
+ return torch.randn(*shape, generator=self.g, device=self.dev)
59
+
60
+ def B(self, p, *shape):
61
+ return (self.U(*shape) < p).float()
62
+
63
+ def logU(self, *shape, lo, hi):
64
+ return torch.exp(self.U(*shape, lo=math.log(lo), hi=math.log(hi)))
65
+
66
+ # -- components (n, T)
67
+ def rff(self, n, lengthscale: Tensor) -> Tensor:
68
+ M = self.n_rff
69
+ w = self.N(n, M, 1) / lengthscale.view(n, 1, 1)
70
+ b = self.U(n, M, 1, hi=2 * math.pi)
71
+ a = self.N(n, M, 1)
72
+ return (a * torch.cos(w * self.t + b)).sum(1) * math.sqrt(2.0 / M)
73
+
74
+ def rbf(self, n):
75
+ return self.rff(n, self.logU(n, lo=self.T / 400, hi=self.T / 1.5))
76
+
77
+ def rq(self, n): # scale mixture: sum of RBFs at several length-scales
78
+ out = 0
79
+ for _ in range(3):
80
+ out = out + self.U(n, 1) * self.rff(n, self.logU(n, lo=self.T / 1000, hi=self.T / 4))
81
+ return out
82
+
83
+ def periodic(self, n):
84
+ k = len(PERIOD_BANK)
85
+ idx = torch.randint(0, k, (n,), generator=self.g, device=self.dev)
86
+ per = PERIOD_BANK.to(self.dev)[idx]
87
+ rnd = self.logU(n, lo=3.0, hi=max(4.0, self.T / 3))
88
+ per = torch.where(self.U(n) < 0.25, rnd, per)
89
+ per = per * self.U(n, lo=0.97, hi=1.03) * torch.where(self.U(n) < 0.1, 1.0 + 0.2 * self.N(n), torch.ones(n, device=self.dev))
90
+ H = 6
91
+ h = torch.arange(1, H + 1, device=self.dev, dtype=torch.float32).view(1, H, 1)
92
+ decay = self.U(n, 1, 1, lo=0.3, hi=1.5)
93
+ amp = self.N(n, H, 1) / h.pow(decay) * self.B(0.8, n, H, 1).clamp_min((h == 1).float())
94
+ ph = self.U(n, H, 1, hi=2 * math.pi)
95
+ sig = (amp * torch.sin(2 * math.pi * h * self.t / per.view(n, 1, 1) + ph)).sum(1)
96
+ # optional slowly varying amplitude (multiplicative modulation)
97
+ mod = 1 + 0.3 * self.B(0.3, n, 1) * torch.tanh(self.rbf(n))
98
+ return sig * mod
99
+
100
+ def trend(self, n):
101
+ tt = self.t / self.T
102
+ lin = self.N(n, 1) * tt
103
+ quad = self.N(n, 1) * 0.5 * tt**2 * self.B(0.3, n, 1)
104
+ # piecewise linear with up to 4 change points
105
+ K = 4
106
+ cps = self.U(n, K, 1)
107
+ slopes = self.N(n, K, 1) * self.B(0.5, n, K, 1)
108
+ pw = (slopes * torch.relu(tt.view(1, 1, -1) - cps)).sum(1)
109
+ damp = self.N(n, 1) * (1 - torch.exp(-tt / self.U(n, 1, lo=0.05, hi=0.5))) * self.B(0.2, n, 1)
110
+ return lin + quad + pw + damp
111
+
112
+ def ar(self, n):
113
+ phi = torch.where(self.U(n) < 0.3, torch.ones(n, device=self.dev), self.U(n, lo=0.3, hi=0.995))
114
+ e = self.N(n, self.T)
115
+ L = min(self.T, 1024)
116
+ k = phi.view(n, 1) ** torch.arange(L, device=self.dev, dtype=torch.float32)
117
+ nfft = 1 << (self.T + L - 1).bit_length()
118
+ y = torch.fft.irfft(torch.fft.rfft(e, nfft) * torch.fft.rfft(k, nfft), nfft)[:, : self.T]
119
+ rw = torch.cumsum(e, -1)
120
+ return torch.where((phi == 1).view(n, 1), rw, y)
121
+
122
+ def noise(self, n):
123
+ z = self.N(n, self.T)
124
+ # heavy tails: scale mixture of normals (generator-friendly stand-in for student-t)
125
+ heavy = self.B(0.3, n, 1)
126
+ tz = z * (1 + heavy * (self.N(n, self.T).abs() ** 2) * 0.5)
127
+ vol = torch.where(self.U(n, 1) < 0.25, torch.exp(0.7 * self.rbf(n).clamp(-3, 3)), torch.ones(n, 1, device=self.dev))
128
+ return tz * vol
129
+
130
+ def level_shifts(self, n):
131
+ K = 3
132
+ loc = self.U(n, K, 1) * self.T
133
+ mag = self.N(n, K, 1) * self.B(0.6, n, K, 1)
134
+ return (mag * (self.t.view(1, 1, -1) > loc).float()).sum(1)
135
+
136
+ def spikes(self, n):
137
+ p = self.logU(n, 1, lo=1e-3, hi=2e-2)
138
+ return (self.U(n, self.T) < p).float() * self.N(n, self.T) * self.U(n, 1, lo=3, hi=10)
139
+
140
+ def bursts(self, n):
141
+ rate = self.logU(n, 1, lo=1e-3, hi=2e-2)
142
+ ev = (self.U(n, self.T) < rate).float() * self.logU(n, self.T, lo=1.0, hi=20.0)
143
+ tau = self.logU(n, lo=2.0, hi=60.0)
144
+ L = min(self.T, 512)
145
+ k = torch.exp(-torch.arange(L, device=self.dev).float().view(1, -1) / tau.view(n, 1))
146
+ nfft = 1 << (self.T + L - 1).bit_length()
147
+ return torch.fft.irfft(torch.fft.rfft(ev, nfft) * torch.fft.rfft(k, nfft), nfft)[:, : self.T]
148
+
149
+ # -- univariate
150
+ def univariate(self, n) -> Tensor:
151
+ z = torch.zeros(n, self.T, device=self.dev)
152
+ on = lambda p: self.B(p, n, 1) # noqa: E731
153
+ w = lambda: self.logU(n, 1, lo=0.1, hi=2.0) # noqa: E731
154
+ trend = on(0.6) * w() * _z(self.trend(n))
155
+ seas = on(0.7) * w() * _z(self.periodic(n)) + on(0.25) * w() * _z(self.periodic(n))
156
+ smooth = on(0.5) * w() * _z(self.rbf(n)) + on(0.25) * w() * _z(self.rq(n))
157
+ arp = on(0.4) * w() * _z(self.ar(n))
158
+ sig = trend + seas + smooth + arp + z
159
+ mult = on(0.25)
160
+ sig = torch.where(mult > 0, (1 + 0.4 * torch.tanh(trend + smooth)) * seas + trend + arp, sig)
161
+ sig = sig + on(0.15) * w() * self.level_shifts(n)
162
+ noise_lvl = self.logU(n, 1, lo=0.01, hi=1.0)
163
+ sig = sig + noise_lvl * self.noise(n)
164
+ sig = sig + on(0.12) * self.spikes(n)
165
+ # intermittent / bursty regimes (non-negative count-like)
166
+ inter = on(0.10)
167
+ lam = torch.exp(0.8 * _z(sig).clamp(-4, 4) + self.U(n, 1, lo=-3, hi=1.5))
168
+ p0 = self.U(n, 1, lo=0.2, hi=0.9)
169
+ cnt = torch.poisson(lam, generator=self.g) * (self.U(n, self.T) > p0).float()
170
+ sig = torch.where(inter > 0, cnt, sig)
171
+ burst = on(0.07)
172
+ sig = torch.where(burst > 0, self.U(n, 1, lo=0, hi=2) + 0.1 * self.noise(n).abs() + self.bursts(n), sig)
173
+ # non-negativity transforms on some continuous series
174
+ nn_ = on(0.2) * (1 - inter) * (1 - burst)
175
+ sig = torch.where(nn_ > 0, torch.nn.functional.softplus(sig * self.U(n, 1, lo=0.5, hi=3)), sig)
176
+ return sig
177
+
178
+ def affine(self, x: Tensor) -> Tensor:
179
+ n = x.shape[0]
180
+ scale = self.logU(n, 1, lo=1e-2, hi=1e4)
181
+ off = self.N(n, 1) * scale * self.logU(n, 1, lo=0.01, hi=20.0) * self.B(0.6, n, 1)
182
+ sgn = torch.where(self.U(n, 1) < 0.5, -1.0, 1.0)
183
+ pos = (x.min(-1, keepdim=True).values >= 0).float() # keep count-like series non-negative
184
+ return torch.where(pos > 0, x * scale, sgn * x * scale + off)
185
+
186
+ # -- multivariate group batch
187
+ def group_batch(self, Bg: int, V: int, cov_prob: float = 0.35):
188
+ """Returns values (Bg,V,T), roles (Bg,V)."""
189
+ T = self.T
190
+ R = Bg * V
191
+ if V == 1:
192
+ vals = self.affine(self.univariate(Bg)).view(Bg, 1, T)
193
+ return vals, torch.zeros(Bg, 1, dtype=torch.long, device=self.dev)
194
+ K = max(1, V // 2)
195
+ lat = _z(self.univariate(Bg * K)).view(Bg, K, T)
196
+ # lead-lag delays per (row, latent)
197
+ lag = torch.randint(0, 48, (Bg, V, K), generator=self.g, device=self.dev)
198
+ lag = lag * self.B(0.5, Bg, V, K).long()
199
+ idx = (self.t.long().view(1, 1, 1, T) - lag[..., None]).clamp_min(0)
200
+ shifted = torch.gather(lat[:, None].expand(Bg, V, K, T), 3, idx)
201
+ W = self.N(Bg, V, K, 1) * self.B(0.6, Bg, V, K, 1)
202
+ W[:, :, 0] = W[:, :, 0] + 0.5
203
+ mixed = (W * shifted).sum(2)
204
+ f = torch.randint(0, 5, (Bg, V, 1), generator=self.g, device=self.dev)
205
+ nl = torch.stack([mixed, torch.tanh(mixed), mixed.abs(), torch.relu(mixed), mixed + 0.3 * mixed**2], 0)
206
+ mixed = torch.gather(nl, 0, f[None].expand(1, Bg, V, T))[0]
207
+ own = _z(self.univariate(R)).view(Bg, V, T)
208
+ a = self.U(Bg, V, 1, lo=0.0, hi=1.0)
209
+ indep = self.B(0.15, Bg, 1, 1)
210
+ rows = torch.where(indep > 0, own, (1 - a) * _z(mixed) + a * own)
211
+ roles = torch.zeros(Bg, V, dtype=torch.long, device=self.dev)
212
+
213
+ # covariate tasks
214
+ cov_g = self.U(Bg) < cov_prob
215
+ if cov_g.any():
216
+ n_cov = torch.randint(1, V, (Bg,), generator=self.g, device=self.dev)
217
+ ridx = torch.arange(V, device=self.dev).view(1, V)
218
+ is_cov = (ridx >= (V - n_cov).view(Bg, 1)) & cov_g.view(Bg, 1)
219
+ known = is_cov & (self.U(Bg, V) < 0.6)
220
+ # some covariates are 0/1 event indicators (holiday / promo / calendar)
221
+ ev_rate = self.logU(Bg, V, 1, lo=0.005, hi=0.15)
222
+ per = PERIOD_BANK.to(self.dev)[torch.randint(0, len(PERIOD_BANK), (Bg, V, 1), generator=self.g, device=self.dev)]
223
+ cal = ((self.t.view(1, 1, T) % per) < torch.clamp(per * self.U(Bg, V, 1, lo=0.05, hi=0.3), min=1)).float()
224
+ evt = (self.U(Bg, V, T) < ev_rate).float()
225
+ ind = torch.where(self.U(Bg, V, 1) < 0.5, cal, evt)
226
+ binary = is_cov & (self.U(Bg, V) < 0.4)
227
+ cov_vals = torch.where(binary[..., None], ind, rows)
228
+ # target response to covariates: lagged nonlinear effects
229
+ beta = self.N(Bg, V, 1) * self.U(Bg, 1, 1, lo=0.5, hi=2.5)
230
+ clag = torch.randint(0, 8, (Bg, V, 1), generator=self.g, device=self.dev) * (~known[..., None]).long()
231
+ cidx = (self.t.long().view(1, 1, T) - clag).clamp_min(0)
232
+ cl = torch.gather(cov_vals, 2, cidx)
233
+ eff_in = torch.where(binary[..., None], cl, _z(cl))
234
+ eff = (beta * torch.where(self.U(Bg, V, 1) < 0.5, eff_in, torch.tanh(eff_in)) * is_cov[..., None].float()).sum(1, keepdim=True)
235
+ tgt_mask = (~is_cov)[..., None].float()
236
+ rows = torch.where(is_cov[..., None], cov_vals, rows + tgt_mask * eff * cov_g.view(Bg, 1, 1).float())
237
+ roles = torch.where(is_cov, torch.where(known, 2, 1), roles)
238
+ bin_rows = binary.view(R)
239
+ else:
240
+ bin_rows = torch.zeros(R, dtype=torch.bool, device=self.dev)
241
+ flat = rows.reshape(R, T)
242
+ aff = self.affine(flat)
243
+ flat = torch.where(bin_rows[:, None], flat, aff)
244
+ return flat.view(Bg, V, T), roles
245
+
246
+ def masks(self, vals: Tensor, roles: Tensor) -> Tensor:
247
+ Bg, V, T = vals.shape
248
+ obs = torch.ones(Bg, V, T, dtype=torch.bool, device=self.dev)
249
+ # iid missing
250
+ pm = self.U(Bg, V, 1, hi=0.15) * self.B(0.2, Bg, V, 1)
251
+ obs &= self.U(Bg, V, T) >= pm
252
+ # contiguous gaps
253
+ gap = self.B(0.15, Bg, V, 1) > 0
254
+ g0 = self.U(Bg, V, 1) * T
255
+ gl = self.logU(Bg, V, 1, lo=8, hi=T / 4)
256
+ in_gap = (self.t.view(1, 1, T) >= g0) & (self.t.view(1, 1, T) < g0 + gl)
257
+ obs &= ~(gap & in_gap)
258
+ # late start (left padding), per group
259
+ late = self.B(0.25, Bg, 1, 1) > 0
260
+ st = (self.U(Bg, 1, 1) * T * 0.8)
261
+ obs &= ~(late & (self.t.view(1, 1, T) < st))
262
+ return obs
263
+
264
+
265
+ class SynthStream:
266
+ """Endless batches of roughly `tokens_per_batch` encoder patch tokens."""
267
+
268
+ V_CHOICES = (1, 2, 4, 8, 16)
269
+ V_PROBS = (0.45, 0.1, 0.15, 0.15, 0.15)
270
+
271
+ def __init__(self, T: int, tokens_per_batch: int, device="cpu", seed=0, patch=32, block=32, min_ctx=4, chunk=8):
272
+ self.gen = SynthGen(T, device, seed)
273
+ self.chunk = chunk # batches generated per call (amortises kernel-launch overhead on GPU)
274
+ self.cache: dict[int, list] = {}
275
+ self.T, self.patch, self.N = T, patch, T // patch
276
+ self.tok = tokens_per_batch
277
+ self.min_ctx = min_ctx
278
+ self.dev = torch.device(device)
279
+ self.rng = torch.Generator().manual_seed(seed + 1)
280
+
281
+ def next_batch(self, V: int | None = None) -> dict:
282
+ if V is None:
283
+ i = torch.multinomial(torch.tensor(self.V_PROBS), 1, generator=self.rng).item()
284
+ V = self.V_CHOICES[i]
285
+ Bg = max(1, self.tok // (V * self.N))
286
+ if not self.cache.get(V):
287
+ vals, roles = self.gen.group_batch(Bg * self.chunk, V)
288
+ obs = self.gen.masks(vals, roles)
289
+ vals = torch.nan_to_num(vals, nan=0.0, posinf=0.0, neginf=0.0)
290
+ s = torch.randint(self.min_ctx, self.N - 1, (Bg * self.chunk,), generator=self.gen.g, device=self.dev)
291
+ self.cache[V] = [
292
+ {"values": vals[i * Bg:(i + 1) * Bg], "observed": obs[i * Bg:(i + 1) * Bg],
293
+ "roles": roles[i * Bg:(i + 1) * Bg], "s": s[i * Bg:(i + 1) * Bg]}
294
+ for i in range(self.chunk)
295
+ ]
296
+ return self.cache[V].pop()
modular_ts/synth2.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Synthetic prior v2 (Modular-TS v3): new generator families + augmentation cascade.
2
+
3
+ Motivated by the run2/run3 GIFT-Eval diagnosis (v3/INSIGHTS.md) and TempoPFN's
4
+ leave-one-out prior ablation (arXiv 2510.25502, Table 3: removing the regime-switching
5
+ OU/SDE prior costs the most, then CauKer, spikes, stochastic rhythms, step functions,
6
+ sawtooth; the augmentation cascade is worth ~5% CRPS):
7
+
8
+ New univariate families (all vectorised torch, run on the GPU):
9
+ * ou_regime - Ornstein-Uhlenbeck with Markov regime switching of mean / vol / speed
10
+ * waveform - non-sinusoidal periodic waves: sawtooth, square (random duty), triangle,
11
+ spike trains, with optional slow period drift (phase-drifting cycles)
12
+ * template - "load-curve" seasonality: a random smooth+spiky daily profile tiled at a
13
+ bank period, optionally modulated by a weekly profile (24x7, 96x7, 288x7)
14
+ and multiplicative trend; near-deterministic, low-noise (m4_hourly-like)
15
+ * steps - piecewise-constant levels with Poisson change points (+ small noise)
16
+ * growth - integrated / cumulative processes: cumsum of a positive rate (covid-like),
17
+ logistic / Gompertz growth with noise, compound-interest trends
18
+ Augmentations (applied after generation, each with its own probability):
19
+ time reversal, integration (cumsum), differencing, quantisation to a random grid,
20
+ censoring (clip at a random quantile), random smoothing conv filter, sample-and-hold
21
+ resampling artefacts, amplitude modulation, TSMixup of two series.
22
+
23
+ `SynthGen2.univariate` returns a mixture of the v1 prior (`SynthGen.univariate`) and the new
24
+ families; everything downstream (multivariatizers, covariates, masks) is inherited.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import math
30
+
31
+ import torch
32
+ from torch import Tensor
33
+
34
+ from modular_ts.synth import PERIOD_BANK, SynthGen, SynthStream, _z
35
+
36
+
37
+ def _fft_filter(e: Tensor, phi: Tensor, L: int) -> Tensor:
38
+ """y_t = sum_k phi^k e_{t-k} for each row (AR(1) filter via FFT), phi (n,)."""
39
+ n, T = e.shape
40
+ L = min(T, L)
41
+ k = phi.view(n, 1) ** torch.arange(L, device=e.device, dtype=torch.float32)
42
+ nfft = 1 << (T + L - 1).bit_length()
43
+ return torch.fft.irfft(torch.fft.rfft(e, nfft) * torch.fft.rfft(k, nfft), nfft)[:, :T]
44
+
45
+
46
+ class SynthGen2(SynthGen):
47
+ def __init__(self, *a, p_new: float = 0.55, p_aug: float = 0.5, **k):
48
+ super().__init__(*a, **k)
49
+ self.p_new, self.p_aug = p_new, p_aug
50
+
51
+ # ------------------------------------------------------------------ families
52
+ def _regimes(self, n, rate_lo=1e-4, rate_hi=3e-3):
53
+ """Piecewise-constant regime index field (n,T) in [0,1) via Poisson change points."""
54
+ T = self.T
55
+ rate = self.logU(n, 1, lo=rate_lo, hi=rate_hi)
56
+ jumps = (self.U(n, T) < rate).float()
57
+ seg = torch.cumsum(jumps, -1).long() % 64 # segment id
58
+ table = self.U(n, 64)
59
+ return torch.gather(table, 1, seg)
60
+
61
+ def ou_regime(self, n):
62
+ r = self._regimes(n)
63
+ mu = (r - 0.5) * self.U(n, 1, lo=0.0, hi=6.0)
64
+ vol = torch.exp(self.U(n, 1, lo=-1.0, hi=1.0) * (self._regimes(n) - 0.5) * 2)
65
+ theta = self.logU(n, lo=1e-3, hi=0.3)
66
+ e = theta.view(n, 1) * mu + 0.3 * vol * self.N(n, self.T)
67
+ return _fft_filter(e, 1 - theta, 2048)
68
+
69
+ def _period(self, n):
70
+ idx = torch.randint(0, len(PERIOD_BANK), (n,), generator=self.g, device=self.dev)
71
+ per = PERIOD_BANK.to(self.dev)[idx]
72
+ rnd = self.logU(n, lo=3.0, hi=max(4.0, self.T / 4))
73
+ return torch.where(self.U(n) < 0.3, rnd, per)
74
+
75
+ def _phase(self, n, per):
76
+ """Cycle phase in [0,1) with optional slow period drift (phase-drifting cycles)."""
77
+ drift = torch.where(self.U(n) < 0.3, self.N(n) * 0.02, torch.zeros(n, device=self.dev))
78
+ inst = 1.0 / per.view(n, 1) * (1 + drift.view(n, 1) * torch.tanh(self.rbf(n)))
79
+ return torch.frac(torch.cumsum(inst.expand(n, self.T), -1) + self.U(n, 1))
80
+
81
+ def waveform(self, n):
82
+ per = self._period(n)
83
+ ph = self._phase(n, per)
84
+ kind = torch.randint(0, 4, (n, 1), generator=self.g, device=self.dev)
85
+ duty = self.U(n, 1, lo=0.1, hi=0.9)
86
+ saw = ph * 2 - 1
87
+ sq = torch.where(ph < duty, 1.0, -1.0)
88
+ tri = 1 - 4 * (ph - 0.5).abs()
89
+ spk = torch.exp(-((ph - 0.5) ** 2) / (2 * self.logU(n, 1, lo=1e-4, hi=1e-2)))
90
+ w = torch.where(kind == 0, saw, torch.where(kind == 1, sq, torch.where(kind == 2, tri, spk)))
91
+ w = torch.where(self.U(n, 1) < 0.5, -w, w)
92
+ return w
93
+
94
+ def template(self, n):
95
+ """Tiled random daily profile (smooth + bumps) x optional weekly profile."""
96
+ T = self.T
97
+ per = PERIOD_BANK.to(self.dev)[torch.randint(4, len(PERIOD_BANK), (n,), generator=self.g, device=self.dev)]
98
+ per = torch.where(self.U(n) < 0.6, torch.tensor([24.0, 96.0, 288.0, 48.0, 12.0, 7.0], device=self.dev)[
99
+ torch.randint(0, 6, (n,), generator=self.g, device=self.dev)], per)
100
+ ph = self._phase(n, per) # (n,T) in [0,1)
101
+ # profile = sum of K random bumps on the circle + low-order Fourier terms
102
+ K = 4
103
+ c = self.U(n, K, 1)
104
+ wdt = self.logU(n, K, 1, lo=0.02, hi=0.25)
105
+ a = self.N(n, K, 1).abs() * torch.where(self.U(n, K, 1) < 0.2, -1.0, 1.0)
106
+ d = torch.minimum((ph[:, None] - c).abs(), 1 - (ph[:, None] - c).abs())
107
+ prof = (a * torch.exp(-0.5 * (d / wdt) ** 2)).sum(1)
108
+ h = torch.arange(1, 4, device=self.dev, dtype=torch.float32).view(1, 3, 1)
109
+ prof = prof + (0.3 * self.N(n, 3, 1) / h * torch.sin(2 * math.pi * h * ph[:, None] + self.U(n, 3, 1, hi=6.3))).sum(1)
110
+ # weekly modulation for intraday periods
111
+ wk = self.U(n, 7, lo=0.5, hi=1.2)
112
+ wk = torch.where(self.U(n, 1) < 0.5, wk, torch.ones_like(wk))
113
+ day = (torch.cumsum((ph[:, 1:] < ph[:, :-1]).float(), -1)).long() % 7
114
+ day = torch.cat([torch.zeros(n, 1, dtype=torch.long, device=self.dev), day], -1)
115
+ mod = torch.gather(wk, 1, day)
116
+ sig = prof * mod
117
+ # slowly varying level and tiny noise (near-deterministic series)
118
+ sig = sig + self.B(0.5, n, 1) * 0.3 * _z(self.trend(n)) + self.logU(n, 1, lo=1e-3, hi=0.2) * self.N(n, T)
119
+ return sig
120
+
121
+ def steps(self, n):
122
+ r = self._regimes(n, 2e-4, 1e-2)
123
+ lv = torch.where(self.U(n, 1) < 0.5, torch.round(r * self.U(n, 1, lo=2, hi=20)), r)
124
+ return lv + self.logU(n, 1, lo=1e-3, hi=0.1) * self.N(n, self.T)
125
+
126
+ def growth(self, n):
127
+ T = self.T
128
+ tt = self.t / T
129
+ kind = torch.randint(0, 3, (n, 1), generator=self.g, device=self.dev)
130
+ # cumulative counts: cumsum of positive, slowly varying rate (+ noise)
131
+ rate = torch.exp(0.8 * _z(self.rbf(n)).clamp(-3, 3)) * (1 + 0.3 * self.N(n, T).abs() * self.B(0.5, n, 1))
132
+ cum = torch.cumsum(rate, -1)
133
+ # logistic / gompertz
134
+ t0 = self.U(n, 1, lo=-0.2, hi=1.2)
135
+ k = self.logU(n, 1, lo=2.0, hi=30.0)
136
+ logi = torch.sigmoid(k * (tt - t0))
137
+ gomp = torch.exp(-torch.exp(-k * (tt - t0)))
138
+ sig = torch.where(kind == 0, cum, torch.where(kind == 1, logi, gomp))
139
+ sig = sig * (1 + self.logU(n, 1, lo=1e-3, hi=0.05) * self.N(n, T))
140
+ return sig
141
+
142
+ # ------------------------------------------------------------------ augmentations
143
+ def augment(self, x: Tensor) -> Tensor:
144
+ n, T = x.shape
145
+ on = lambda p: self.U(n, 1) < p * self.p_aug * 2 # noqa: E731
146
+ # time reversal
147
+ x = torch.where(on(0.15), x.flip(-1), x)
148
+ # integration / differencing
149
+ xi = torch.cumsum(x - x.mean(-1, keepdim=True), -1)
150
+ x = torch.where(on(0.06), xi, x)
151
+ xd = torch.cat([x[:, :1] * 0, x[:, 1:] - x[:, :-1]], -1)
152
+ x = torch.where(on(0.04), xd, x)
153
+ # amplitude modulation
154
+ x = torch.where(on(0.1), x * (1 + 0.5 * torch.tanh(self.rbf(n))), x)
155
+ # random smoothing conv filter (moving average of random width via cumsum trick)
156
+ w = torch.randint(2, 16, (n, 1), generator=self.g, device=self.dev)
157
+ cs = torch.cumsum(torch.nn.functional.pad(x, (1, 0)), -1)
158
+ idx = torch.arange(1, T + 1, device=self.dev).view(1, T)
159
+ lo = (idx - w).clamp_min(0)
160
+ ma = (cs[:, 1:] - torch.gather(cs, 1, lo)) / (idx - lo).float()
161
+ x = torch.where(on(0.1), ma, x)
162
+ # sample-and-hold resampling artefacts
163
+ k = torch.randint(2, 8, (n, 1), generator=self.g, device=self.dev)
164
+ hold = torch.gather(x, 1, (torch.arange(T, device=self.dev).view(1, T) // k) * k)
165
+ x = torch.where(on(0.05), hold, x)
166
+ # TSMixup with a rolled partner
167
+ lam = self.U(n, 1, lo=0.2, hi=0.8)
168
+ xm = lam * _z(x) + (1 - lam) * _z(x.roll(1, 0))
169
+ x = torch.where(on(0.1), xm, x)
170
+ # censoring at a random quantile (one side)
171
+ qv = torch.quantile(x[:, :: max(1, T // 512)], 0.8 + 0.19 * torch.rand(1, device=self.dev).item(), dim=-1, keepdim=True)
172
+ x = torch.where(on(0.05), torch.minimum(x, qv), x)
173
+ return x
174
+
175
+ def quantize(self, x: Tensor) -> Tensor:
176
+ """Round some series to a coarse grid (integer-valued / low-resolution sensors)."""
177
+ n = x.shape[0]
178
+ step = x.std(-1, keepdim=True).clamp_min(1e-6) * self.logU(n, 1, lo=0.02, hi=0.5)
179
+ q = torch.round(x / step) * step
180
+ return torch.where(self.U(n, 1) < 0.08, q, x)
181
+
182
+ # ------------------------------------------------------------------ mixture
183
+ def univariate(self, n) -> Tensor:
184
+ base = super().univariate(n)
185
+ fam = torch.randint(0, 5, (n, 1), generator=self.g, device=self.dev)
186
+ new = torch.zeros(n, self.T, device=self.dev)
187
+ gens = (self.ou_regime, self.waveform, self.template, self.steps, self.growth)
188
+ for i, g in enumerate(gens):
189
+ new = torch.where(fam == i, _z(g(n)), new)
190
+ # optional combination of the new family with a v1 component (additive)
191
+ comb = self.U(n, 1) < 0.35
192
+ new = torch.where(comb, new + self.logU(n, 1, lo=0.05, hi=1.0) * _z(base), new)
193
+ use_new = self.U(n, 1) < self.p_new
194
+ x = torch.where(use_new, new, base)
195
+ x = self.augment(x)
196
+ x = torch.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)
197
+ return x
198
+
199
+ def affine(self, x: Tensor) -> Tensor:
200
+ return self.quantize(super().affine(x))
201
+
202
+
203
+ class SynthStream2(SynthStream):
204
+ def __init__(self, T, tokens_per_batch, device="cpu", seed=0, patch=32, block=32, min_ctx=4, chunk=8, p_new=0.55):
205
+ super().__init__(T, tokens_per_batch, device, seed, patch, block, min_ctx, chunk)
206
+ self.gen = SynthGen2(T, device, seed, p_new=p_new)
modular_ts/teacher_gen.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stage B: cache teacher quantile forecasts (TimesFM-2.5-200M + Chronos-2) for distillation.
2
+
3
+ Run in the teacher venv (~/tvenv, system torch):
4
+ ~/tvenv/bin/python -m modular_ts.teacher_gen --source synth --n 200000 --out teach/synth
5
+ ~/tvenv/bin/python -m modular_ts.teacher_gen --source gep --n 100000 --out teach/gep --gep_selection gep_selection.json
6
+
7
+ Each shard teach/<src>/shard_XXXX.npz holds
8
+ ctx (n, C) float32 context, NaN = unobserved (left-padded for short contexts)
9
+ fut (n, F) float32 ground-truth continuation (synthetic only; NaN for GEP = teacher-only input)
10
+ q_tfm, q_cr2 (n, H, 9) float32 teacher quantiles 0.1..0.9 for the first H future steps
11
+ Teacher weights: google/timesfm-2.5-200m-pytorch (Apache-2.0), amazon/chronos-2 (Apache-2.0).
12
+ GiftEvalPretrain inputs use the same GIFT-Eval/fev/TIME leakage filter as run1 (realdata.filter_datasets).
13
+ NEVER uses Timestral weights.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import math
21
+ import os
22
+ import time
23
+
24
+ import numpy as np
25
+ import torch
26
+
27
+ Q9 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
28
+
29
+
30
+ def interp_nans(x: np.ndarray) -> np.ndarray:
31
+ m = np.isfinite(x)
32
+ if m.all():
33
+ return x
34
+ idx = np.arange(len(x))
35
+ return np.interp(idx, idx[m], x[m]).astype(np.float32)
36
+
37
+
38
+ class Teachers:
39
+ def __init__(self, C, H, dev="cuda", tfm_bs=256, cr_bs=512):
40
+ import timesfm
41
+ from chronos import Chronos2Pipeline
42
+ torch.set_float32_matmul_precision("high")
43
+ self.H = H
44
+ self.cr = Chronos2Pipeline.from_pretrained("amazon/chronos-2", device_map=dev)
45
+ self.cr_bs = cr_bs
46
+ self.tfm = timesfm.TimesFM_2p5_200M_torch.from_pretrained("google/timesfm-2.5-200m-pytorch")
47
+ self.tfm.compile(timesfm.ForecastConfig(max_context=C, max_horizon=H, normalize_inputs=True, per_core_batch_size=tfm_bs,
48
+ use_continuous_quantile_head=True, force_flip_invariance=True,
49
+ infer_is_positive=True, fix_quantile_crossing=True))
50
+
51
+ def __call__(self, ctx: np.ndarray):
52
+ """ctx (n, C) with NaN -> q_tfm, q_cr2 (n, H, 9)."""
53
+ seqs = []
54
+ for row in ctx:
55
+ f = np.isfinite(row)
56
+ first = int(np.argmax(f)) if f.any() else len(row) - 1
57
+ seqs.append(row[first:])
58
+ with torch.no_grad():
59
+ qs, _ = self.cr.predict_quantiles([torch.from_numpy(s) for s in seqs], prediction_length=self.H,
60
+ quantile_levels=Q9, batch_size=self.cr_bs)
61
+ q_cr2 = torch.stack([q[0] for q in qs]).float().cpu().numpy()
62
+ _, qt = self.tfm.forecast(horizon=self.H, inputs=[interp_nans(s) for s in seqs])
63
+ q_tfm = np.asarray(qt)[:, :self.H, 1:10].astype(np.float32)
64
+ return q_tfm, q_cr2
65
+
66
+
67
+ def synth_windows(gen, n, C, F, rng):
68
+ vals, roles = gen.group_batch(n, 1)
69
+ v = vals[:, 0].float().cpu().numpy()
70
+ T = C + F
71
+ # context length: 50% full C, else log-uniform in [48, C]; 15% of series get iid missing values
72
+ clen = np.where(rng.random(n) < 0.5, C, np.exp(rng.uniform(math.log(48), math.log(C), n))).astype(int)
73
+ obs = np.ones((n, T), bool)
74
+ obs[np.arange(T)[None] < (C - clen)[:, None]] = False
75
+ miss = rng.random(n) < 0.15
76
+ pm = rng.uniform(0, 0.1, n)[:, None]
77
+ obs &= ~(miss[:, None] & (rng.random((n, T)) < pm))
78
+ v = np.where(obs, v, np.nan).astype(np.float32)
79
+ return v[:, :C], v[:, C:]
80
+
81
+
82
+ def main():
83
+ ap = argparse.ArgumentParser()
84
+ ap.add_argument("--source", choices=["synth", "gep"], required=True)
85
+ ap.add_argument("--n", type=int, default=100000)
86
+ ap.add_argument("--shard", type=int, default=8192)
87
+ ap.add_argument("--C", type=int, default=2048)
88
+ ap.add_argument("--F", type=int, default=1024)
89
+ ap.add_argument("--H", type=int, default=512)
90
+ ap.add_argument("--out", required=True)
91
+ ap.add_argument("--seed", type=int, default=123)
92
+ ap.add_argument("--gep_selection", default="gep_selection.json")
93
+ ap.add_argument("--max_minutes", type=float, default=0)
94
+ args = ap.parse_args()
95
+ os.makedirs(args.out, exist_ok=True)
96
+ dev = torch.device("cuda")
97
+ rng = np.random.default_rng(args.seed)
98
+ T = args.C + args.F
99
+ if args.source == "synth":
100
+ from modular_ts.synth import SynthGen
101
+ gen = SynthGen(T, dev, seed=args.seed)
102
+ else:
103
+ from modular_ts.realdata import FastGiftEvalPretrain
104
+ gep = FastGiftEvalPretrain(T, args.shard * 2 * (T // 32), dev, json.load(open(args.gep_selection)), seed=args.seed,
105
+ mv_frac=0.0, short_ctx_p=0.3)
106
+ json.dump(gep.manifest(), open(os.path.join(args.out, "gep_manifest.json"), "w"), indent=1)
107
+ tea = Teachers(args.C, args.H)
108
+ t0, done, k = time.time(), 0, 0
109
+ while done < args.n:
110
+ if args.max_minutes and (time.time() - t0) / 60 > args.max_minutes:
111
+ print("time limit", flush=True)
112
+ break
113
+ p = os.path.join(args.out, f"shard_{k:04d}.npz")
114
+ if os.path.exists(p):
115
+ done += args.shard; k += 1
116
+ continue
117
+ if args.source == "synth":
118
+ gen = SynthGen(T, dev, seed=args.seed * 1000 + k) # per-shard seed -> resumable without duplicates
119
+ rng = np.random.default_rng(args.seed * 1000 + k)
120
+ ctx, fut = synth_windows(gen, args.shard, args.C, args.F, rng)
121
+ else:
122
+ gep.g.manual_seed(args.seed * 1000 + k); gep.rng.seed(args.seed * 1000 + k)
123
+ b = gep.next_batch()
124
+ v = torch.where(b["observed"][:, 0], b["values"][:, 0], torch.full_like(b["values"][:, 0], float("nan"))).cpu().numpy()
125
+ ok = np.isfinite(v[:, :args.C]).sum(1) >= 48
126
+ v = v[ok][:args.shard]
127
+ ctx, fut = v[:, :args.C], np.full((len(v), args.F), np.nan, np.float32)
128
+ ts = time.time()
129
+ q_tfm, q_cr2 = tea(ctx)
130
+ good = np.isfinite(q_tfm).all((1, 2)) & np.isfinite(q_cr2).all((1, 2))
131
+ np.savez(p, ctx=ctx[good], fut=fut[good], q_tfm=q_tfm[good].astype(np.float32), q_cr2=q_cr2[good].astype(np.float32))
132
+ done += int(good.sum()); k += 1
133
+ print(json.dumps({"shard": k, "n": int(good.sum()), "dropped": int((~good).sum()), "teach_s": round(time.time() - ts, 1),
134
+ "done": done, "min": round((time.time() - t0) / 60, 2)}), flush=True)
135
+ print("TEACH_DONE", done, flush=True)
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()
modular_ts/train.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Train Modular-TS from scratch with DHF + pinball loss.
2
+
3
+ python -m modular_ts.train --size 2.5M --ctx 4096 --tokens_per_batch 65536 --steps 20000 \
4
+ --lr 2e-3 --warmup 1000 --out runs/smoke --max_minutes 35 [--compile] [--data synthetic]
5
+
6
+ Logs JSONL to <out>/log.jsonl (step, loss, val, lr, enc tokens/s, grad norm),
7
+ checkpoints to <out>/ckpt.pt and an inference export (config.json + model.safetensors,
8
+ loadable by timestral.TimestralForecaster) to <out>/export/.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import math
16
+ import os
17
+ import time
18
+
19
+ import torch
20
+
21
+ from modular_ts.model import ModularTS, count_params, make_config
22
+ from modular_ts.synth import SynthStream
23
+
24
+
25
+ def export(model: ModularTS, out_dir: str) -> None:
26
+ from safetensors.torch import save_file
27
+ os.makedirs(out_dir, exist_ok=True)
28
+ sd = {k: v.detach().float().cpu().contiguous() for k, v in model.state_dict().items()}
29
+ save_file(sd, os.path.join(out_dir, "model.safetensors"))
30
+ model.config.save(os.path.join(out_dir, "config.json"))
31
+
32
+
33
+ def lr_at(step, total, warmup, peak, final_frac=0.05):
34
+ if step < warmup:
35
+ return peak * (step + 1) / warmup
36
+ prog = min(1.0, (step - warmup) / max(1, total - warmup))
37
+ return peak * (final_frac + (1 - final_frac) * 0.5 * (1 + math.cos(math.pi * prog)))
38
+
39
+
40
+ def main():
41
+ ap = argparse.ArgumentParser()
42
+ ap.add_argument("--size", default="2.5M")
43
+ ap.add_argument("--ctx", type=int, default=4096)
44
+ ap.add_argument("--tokens_per_batch", type=int, default=65536, help="encoder patch tokens per step")
45
+ ap.add_argument("--steps", type=int, default=20000, help="schedule length (cosine horizon)")
46
+ ap.add_argument("--max_minutes", type=float, default=0, help="stop early after this wall time (0 = off)")
47
+ ap.add_argument("--lr", type=float, default=2e-3)
48
+ ap.add_argument("--warmup", type=int, default=1000)
49
+ ap.add_argument("--wd", type=float, default=0.05)
50
+ ap.add_argument("--clip", type=float, default=1.0)
51
+ ap.add_argument("--data", default="synthetic", choices=["synthetic", "synthetic+gifteval"])
52
+ ap.add_argument("--real_frac", type=float, default=0.5)
53
+ ap.add_argument("--gifteval_subset", default="", help="comma list of GiftEvalPretrain sub-datasets (default all kept)")
54
+ ap.add_argument("--gep_selection", default="", help="json {dataset: [shard files]} -> fast GPU-resident GiftEvalPretrain stream")
55
+ ap.add_argument("--synth", default="v1", choices=["v1", "v2"], help="v2 = modular_ts.synth2 prior (v3)")
56
+ ap.add_argument("--p_new", type=float, default=0.55, help="synth v2: share of new generator families")
57
+ ap.add_argument("--real_v2", action="store_true", help="v3 real stream: resolution sampling + TSMixup + ds_temp 0.3")
58
+ ap.add_argument("--min_lr_frac", type=float, default=0.05)
59
+ ap.add_argument("--time_schedule", action="store_true", help="cosine progress = elapsed / max_minutes (smoke runs)")
60
+ ap.add_argument("--compile", action="store_true")
61
+ ap.add_argument("--bf16", action="store_true", default=True)
62
+ ap.add_argument("--no_bf16", dest="bf16", action="store_false")
63
+ ap.add_argument("--log_every", type=int, default=25)
64
+ ap.add_argument("--val_every", type=int, default=250)
65
+ ap.add_argument("--ckpt_every", type=int, default=1000)
66
+ ap.add_argument("--out", default="runs/dev")
67
+ ap.add_argument("--seed", type=int, default=0)
68
+ ap.add_argument("--resume", action="store_true")
69
+ ap.add_argument("--resume_from", default="", help="ckpt to resume model+opt from (default <out>/ckpt.pt)")
70
+ ap.add_argument("--reset_schedule", action="store_true", help="on resume, restart warmup+cosine at the resumed step")
71
+ ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
72
+ args = ap.parse_args()
73
+
74
+ os.makedirs(args.out, exist_ok=True)
75
+ torch.manual_seed(args.seed)
76
+ dev = torch.device(args.device)
77
+ if dev.type == "cuda":
78
+ torch.backends.cuda.matmul.allow_tf32 = True
79
+ torch.backends.cudnn.allow_tf32 = True
80
+
81
+ cfg = make_config(args.size, context_length=args.ctx)
82
+ model = ModularTS(cfg).to(dev)
83
+ pc = count_params(model)
84
+ print("params", pc, flush=True)
85
+
86
+ if args.synth == "v2":
87
+ from modular_ts.synth2 import SynthStream2
88
+ synth = SynthStream2(args.ctx, args.tokens_per_batch, dev, seed=args.seed, p_new=args.p_new)
89
+ else:
90
+ synth = SynthStream(args.ctx, args.tokens_per_batch, dev, seed=args.seed)
91
+ stream = synth
92
+ manifest = {"data": args.data, "synth_version": args.synth, "real_v2": args.real_v2, "synthetic": "modular_ts.synth (KernelSynth-style GP + TSI + AR + intermittent/bursty + multivariatizers + covariate tasks)"}
93
+ if args.data == "synthetic+gifteval":
94
+ from modular_ts.realdata import GiftEvalPretrainStream, MixedStream
95
+ sub = [s for s in args.gifteval_subset.split(",") if s] or None
96
+ if args.gep_selection:
97
+ from modular_ts.realdata import FastGiftEvalPretrain
98
+ if args.real_v2:
99
+ from modular_ts.realdata2 import FastGiftEvalPretrain2 as FastGiftEvalPretrain
100
+ real = FastGiftEvalPretrain(args.ctx, args.tokens_per_batch, dev, json.load(open(args.gep_selection)), seed=args.seed)
101
+ else:
102
+ real = GiftEvalPretrainStream(args.ctx, args.tokens_per_batch, dev, seed=args.seed, datasets_subset=sub)
103
+ stream = MixedStream(synth, real, args.real_frac, seed=args.seed)
104
+ manifest.update(real.manifest())
105
+ json.dump(manifest, open(os.path.join(args.out, "data_manifest.json"), "w"), indent=1)
106
+
107
+ # fixed held-out validation batches from a different seed (one per V bucket)
108
+ vgen = SynthStream(args.ctx, args.tokens_per_batch // 2, dev, seed=10_000 + args.seed, chunk=1)
109
+ val_batches = [vgen.next_batch(V) for V in (1, 4, 16)]
110
+
111
+ decay, no_decay = [], []
112
+ for n, p in model.named_parameters():
113
+ (decay if p.dim() >= 2 else no_decay).append(p)
114
+ opt = torch.optim.AdamW([{"params": decay, "weight_decay": args.wd}, {"params": no_decay, "weight_decay": 0.0}],
115
+ lr=args.lr, betas=(0.9, 0.95), eps=1e-8, fused=dev.type == "cuda")
116
+ step = 0
117
+ ck = os.path.join(args.out, "ckpt.pt")
118
+ rck = args.resume_from or ck
119
+ step0 = 0
120
+ prev_elapsed = 0.0
121
+ if args.resume and os.path.exists(rck):
122
+ st = torch.load(rck, map_location=dev)
123
+ model.load_state_dict(st["model"])
124
+ opt.load_state_dict(st["opt"])
125
+ step = st["step"]
126
+ if args.reset_schedule:
127
+ step0 = step
128
+ elif args.time_schedule and "elapsed_min" in st:
129
+ prev_elapsed = st["elapsed_min"] # continue the same time-based schedule after a crash
130
+ print("resumed at", step, "from", rck, flush=True)
131
+
132
+ fwd = model.forward_train
133
+ if args.compile:
134
+ try:
135
+ fwd = torch.compile(model.forward_train, dynamic=False)
136
+ except Exception as e: # pragma: no cover
137
+ print("compile failed, eager:", e, flush=True)
138
+
139
+ amp = torch.autocast(device_type=dev.type, dtype=torch.bfloat16, enabled=args.bf16 and dev.type == "cuda")
140
+
141
+ def evaluate():
142
+ model.eval()
143
+ tot = 0.0
144
+ with torch.no_grad(), amp:
145
+ for b in val_batches:
146
+ l, _ = model.forward_train(b["values"], b["observed"], b["roles"], b["s"])
147
+ tot += l.item()
148
+ model.train()
149
+ return tot / len(val_batches)
150
+
151
+ log = open(os.path.join(args.out, "log.jsonl"), "a")
152
+ t0 = time.time() - prev_elapsed * 60
153
+ t_last, tok_acc, gen_t = time.time(), 0, 0.0
154
+ ema = None
155
+ model.train()
156
+ while step < args.steps:
157
+ if args.max_minutes and (time.time() - t0) / 60 > args.max_minutes:
158
+ print("time limit reached", flush=True)
159
+ break
160
+ prof = step % args.log_every == 0 # measure data-gen share only on log steps (sync is costly)
161
+ if prof and dev.type == "cuda":
162
+ torch.cuda.synchronize()
163
+ tg = time.time()
164
+ b = stream.next_batch()
165
+ if prof and dev.type == "cuda":
166
+ torch.cuda.synchronize()
167
+ if prof:
168
+ gen_t += (time.time() - tg) * args.log_every
169
+ ss = step - step0
170
+ if args.time_schedule and args.max_minutes:
171
+ frac = (time.time() - t0) / 60 / args.max_minutes
172
+ lr = lr_at(ss, max(ss + 1, int(ss / max(frac, 1e-6))) if ss > args.warmup else args.steps, args.warmup, args.lr, args.min_lr_frac)
173
+ else:
174
+ lr = lr_at(ss, args.steps, args.warmup, args.lr, args.min_lr_frac)
175
+ for g in opt.param_groups:
176
+ g["lr"] = lr
177
+ try:
178
+ with amp:
179
+ loss, st = fwd(b["values"], b["observed"], b["roles"], b["s"])
180
+ except Exception as e:
181
+ if fwd is model.forward_train:
182
+ raise
183
+ print("compiled forward failed, falling back to eager:", repr(e)[:500], flush=True)
184
+ fwd = model.forward_train
185
+ with amp:
186
+ loss, st = fwd(b["values"], b["observed"], b["roles"], b["s"])
187
+ if not torch.isfinite(loss):
188
+ print(f"non-finite loss at step {step}; skipping batch", flush=True)
189
+ opt.zero_grad(set_to_none=True)
190
+ step += 1
191
+ continue
192
+ loss.backward()
193
+ gn = torch.nn.utils.clip_grad_norm_(model.parameters(), args.clip)
194
+ opt.step()
195
+ opt.zero_grad(set_to_none=True)
196
+ tok_acc += b["values"].shape[0] * b["values"].shape[1] * (args.ctx // cfg.patch_size)
197
+ step += 1
198
+ lv = loss.item() if step % args.log_every == 0 or step == 1 else None
199
+ if lv is not None:
200
+ ema = lv if ema is None else 0.9 * ema + 0.1 * lv
201
+ if dev.type == "cuda":
202
+ torch.cuda.synchronize()
203
+ dt = time.time() - t_last
204
+ rec = {"step": step, "loss": lv, "loss_ema": ema, "lr": lr, "gnorm": float(gn),
205
+ "enc_tok_s": tok_acc / dt, "gen_frac": gen_t / dt, "elapsed_min": (time.time() - t0) / 60}
206
+ if step % args.val_every == 0:
207
+ rec["val"] = evaluate()
208
+ if dev.type == "cuda":
209
+ rec["mem_gb"] = torch.cuda.max_memory_allocated() / 1e9
210
+ print(json.dumps(rec), flush=True)
211
+ log.write(json.dumps(rec) + "\n")
212
+ log.flush()
213
+ t_last, tok_acc, gen_t = time.time(), 0, 0.0
214
+ if step % args.ckpt_every == 0:
215
+ torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "step": step, "args": vars(args), "elapsed_min": (time.time() - t0) / 60}, ck)
216
+ export(model, os.path.join(args.out, "export"))
217
+ torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "step": step, "args": vars(args), "elapsed_min": (time.time() - t0) / 60}, ck)
218
+ export(model, os.path.join(args.out, "export"))
219
+ fin = {"final_step": step, "val": evaluate(), "params": pc, "minutes": (time.time() - t0) / 60}
220
+ print("FINAL", json.dumps(fin), flush=True)
221
+ json.dump(fin, open(os.path.join(args.out, "final.json"), "w"))
222
+
223
+
224
+ if __name__ == "__main__":
225
+ main()
modular_ts/tta.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Test-time ensembling for Modular-TS (v3): context-length ensembles, temporal-hierarchy
2
+ (multi-resolution) reconciliation and in-context backtest weighting. Inference only.
3
+
4
+ Members (spec string, comma separated):
5
+ cN - the base forecaster with max_context N (e.g. c8192, c2048, c512)
6
+ dK - downsampled member: context mean-pooled by K (aligned to the forecast origin, so it
7
+ sees up to K x the history at the same token budget), forecast ceil(H/K) coarse
8
+ steps, step-upsampled back. Used as a *level corrector* for the fine forecasts
9
+ (temporal hierarchies / THieF, Athanasopoulos et al. 2017): fine + a*(coarse - pool(fine))
10
+ instead of being averaged in directly, so intra-period shape comes from full res.
11
+ Combination of the c-members: quantile averaging (Vincentisation), uniform or, with
12
+ backtest=True, per-series softmax weights from each member's scaled MAE on the last H
13
+ observed steps of the context (a zero-shot, in-context model selection - it only uses the
14
+ series' own history).
15
+
16
+ fc = EnsembleForecaster(TimestralForecaster.from_pretrained(exp), "c8192,c2048,d4", alpha=0.5)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import math
22
+ from dataclasses import dataclass
23
+
24
+ import numpy as np
25
+ import torch
26
+
27
+
28
+ @dataclass
29
+ class _F:
30
+ quantiles: torch.Tensor
31
+
32
+
33
+ def _pool(x: np.ndarray | None, k: int) -> np.ndarray | None:
34
+ """(B,V,T) NaN-aware mean pool by k aligned to the end."""
35
+ if x is None:
36
+ return None
37
+ B, V, T = x.shape
38
+ n = T // k
39
+ if n == 0:
40
+ return None
41
+ y = x[..., T - n * k:].reshape(B, V, n, k)
42
+ with np.errstate(all="ignore"):
43
+ cnt = np.isfinite(y).sum(-1)
44
+ s = np.nansum(y, -1)
45
+ out = np.where(cnt > 0, s / np.maximum(cnt, 1), np.nan)
46
+ return out.astype(np.float32)
47
+
48
+
49
+ class EnsembleForecaster:
50
+ def __init__(self, fc, spec: str = "c8192", alpha: float = 0.5, backtest: bool = False, tau: float = 0.5, bt_windows: int = 1, prior: float = 0.0,
51
+ min_coarse_ctx: int = 256, min_extra: float = 1.5, min_ratio: float = 8.0):
52
+ self.fc = fc
53
+ self.members = [m.strip() for m in spec.split(",") if m.strip()]
54
+ cl = fc.model.config.context_length # never exceed the trained context (RoPE does not extrapolate)
55
+ self.ctxs = [min(int(m[1:]), cl) for m in self.members if m[0] == "c"] or [fc.max_context]
56
+ self.ks = [int(m[1:]) for m in self.members if m[0] == "d"]
57
+ self.alpha, self.backtest, self.tau = alpha, backtest, tau
58
+ self.bt_windows, self.prior = bt_windows, prior # prior: logit bonus for the first (longest) member
59
+ self.min_coarse_ctx, self.min_extra, self.min_ratio = min_coarse_ctx, min_extra, min_ratio # member ctx >= min_ratio*H
60
+ self.base_ctx = fc.max_context
61
+ self.max_context = max(self.ctxs + [self.base_ctx * k for k in self.ks] + [self.base_ctx])
62
+ self.quantile_levels = fc.quantile_levels
63
+ self.mi = list(fc.quantile_levels).index(0.5) if 0.5 in list(fc.quantile_levels) else len(fc.quantile_levels) // 2
64
+
65
+ def _one(self, ctx, h, past, mc):
66
+ self.fc.max_context = mc
67
+ try:
68
+ return self.fc.forecast(ctx, h, past_covariates=past).quantiles.float().cpu()
69
+ finally:
70
+ self.fc.max_context = self.base_ctx
71
+
72
+ def _members(self, ctx, h, past):
73
+ T = ctx.shape[-1]
74
+ outs, used = [], set()
75
+ for i, c in enumerate(self.ctxs):
76
+ if i > 0 and c < self.min_ratio * h: # a short context cannot support a long horizon
77
+ continue
78
+ eff = min(c, T)
79
+ if eff in used: # a shorter series makes several members identical
80
+ continue
81
+ used.add(eff)
82
+ outs.append(self._one(ctx[..., -c:], h, None if past is None else past[..., -c:], c))
83
+ return outs
84
+
85
+ def forecast(self, context, prediction_length, past_covariates=None):
86
+ ctx = np.asarray(context, np.float32)
87
+ if ctx.ndim == 2:
88
+ ctx = ctx[:, None]
89
+ past = None if past_covariates is None else np.asarray(past_covariates, np.float32)
90
+ h = prediction_length
91
+ T = ctx.shape[-1]
92
+ outs = self._members(ctx, h, past)
93
+ if len(outs) == 1:
94
+ q = outs[0]
95
+ elif self.backtest and T - h >= 2 * 32:
96
+ E, ok = 0.0, True
97
+ nw = 0
98
+ for wdw in range(1, self.bt_windows + 1):
99
+ if T - wdw * h < 2 * 32:
100
+ break
101
+ cut = T - wdw * h
102
+ bt = self._members(ctx[..., :cut], h, None if past is None else past[..., :cut])
103
+ if len(bt) != len(outs):
104
+ ok = False
105
+ break
106
+ y = torch.from_numpy(ctx[..., cut:cut + h])
107
+ hist = torch.from_numpy(ctx[..., :cut])
108
+ dd = hist[..., 1:] - hist[..., :-1]
109
+ fin = torch.isfinite(dd)
110
+ scale = (torch.where(fin, dd.abs(), torch.zeros_like(dd)).sum(-1) / fin.sum(-1).clamp_min(1)).clamp_min(1e-8)
111
+ errs = []
112
+ for b in bt:
113
+ e = (b[..., self.mi] - y).abs()
114
+ fe = torch.isfinite(e)
115
+ e = torch.where(fe, e, torch.zeros_like(e)).sum(-1) / fe.sum(-1).clamp_min(1) / scale
116
+ errs.append(e.mean(-1))
117
+ E = E + torch.stack(errs, 0)
118
+ nw += 1
119
+ if not ok or nw == 0:
120
+ q = torch.stack(outs).mean(0)
121
+ else:
122
+ E = E / nw
123
+ logit = -E / (self.tau * E.min(0, keepdim=True).values.clamp_min(1e-3))
124
+ logit[0] = logit[0] + self.prior
125
+ w = torch.softmax(logit, 0)
126
+ q = (torch.stack(outs) * w[:, :, None, None, None]).sum(0)
127
+ else:
128
+ q = torch.stack(outs).mean(0)
129
+ # temporal-hierarchy level correction from downsampled members
130
+ for k in self.ks:
131
+ hk = math.ceil(h / k)
132
+ pooled = _pool(ctx, k)
133
+ if pooled is None or pooled.shape[-1] < self.min_coarse_ctx:
134
+ continue
135
+ # only worth it when the coarse view sees materially more history than the fine one
136
+ if T < self.min_extra * self.base_ctx and hk < 2:
137
+ continue
138
+ pp = _pool(past, k) if past is not None else None
139
+ cq = self._one(pooled[..., -self.base_ctx:], hk, None if pp is None else pp[..., -self.base_ctx:], self.base_ctx)
140
+ cmed = cq[..., self.mi] # (B,V,hk)
141
+ fmed = q[..., self.mi]
142
+ pad = hk * k - h
143
+ fm = torch.cat([fmed, fmed[..., -1:].expand(*fmed.shape[:-1], pad)], -1) if pad else fmed
144
+ fpool = fm.view(*fm.shape[:-1], hk, k).mean(-1)
145
+ delta = (cmed - fpool).repeat_interleave(k, -1)[..., :h]
146
+ q = q + self.alpha / len(self.ks) * delta[..., None]
147
+ return _F(q)
results/all_results.csv ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ dataset,model,eval_metrics/MSE[mean],eval_metrics/MSE[0.5],eval_metrics/MAE[0.5],eval_metrics/MASE[0.5],eval_metrics/MAPE[0.5],eval_metrics/sMAPE[0.5],eval_metrics/MSIS,eval_metrics/RMSE[mean],eval_metrics/NRMSE[mean],eval_metrics/ND[0.5],eval_metrics/mean_weighted_sum_quantile_loss,domain,num_variates
2
+ bitbrains_fast_storage/5T/long,Modular-TS-2.4M-v3_M1,3678060.0939310347,3678060.0939310347,375.4961381185165,0.9167681594477102,4.714564867919659,0.7251918487783172,15.841790583442892,1917.826919701315,5.068243351659421,0.9923240653485829,0.6959523690328288,Web/CloudOps,2
3
+ bitbrains_fast_storage/5T/medium,Modular-TS-2.4M-v3_M1,2794097.8588460106,2794097.8588460106,301.5700941306518,1.248188037721358,4.705945084704349,0.7298914125066623,23.65806409550792,1671.5555207189532,5.078647577538354,0.9162532796732381,0.674956798323308,Web/CloudOps,2
4
+ bitbrains_fast_storage/5T/short,Modular-TS-2.4M-v3_M1,1789367.908427151,1789367.908427151,170.94836712944672,0.7464365081982969,2.2107092294123354,0.6847539590206893,14.157463028441592,1337.6725714565396,4.199961010964384,0.5367355899732063,0.4188247283434475,Web/CloudOps,2
5
+ bitbrains_fast_storage/H/short,Modular-TS-2.4M-v3_M1,2857251.909621366,2857251.909621366,324.96432151762076,1.0518938446957118,3.765954716264035,0.5135839053460005,19.937172636928338,1690.340767307399,4.818020494788251,0.9262539195814864,0.6779741560340291,Web/CloudOps,2
6
+ bitbrains_rnd/5T/long,Modular-TS-2.4M-v3_M1,2335002.1897918363,2335002.1897918363,202.03773498891655,3.340738878079062,2.732445501293894,0.6418363842517235,116.04497928950197,1528.0713955152214,5.853697246156131,0.7739610442256082,0.6664847615792028,Web/CloudOps,2
7
+ bitbrains_rnd/5T/medium,Modular-TS-2.4M-v3_M1,2197458.6406868207,2197458.6406868207,166.13444570183873,4.413797604303608,1.2794170885289742,0.6578795845955763,160.08923169435977,1482.3827578216162,6.237140002367876,0.699012310815642,0.6454088859518597,Web/CloudOps,2
8
+ bitbrains_rnd/5T/short,Modular-TS-2.4M-v3_M1,1717739.5983574833,1717739.5983574833,129.86860276201207,1.7188269009296444,1.114256719613909,0.6338544360460594,53.535910841778204,1310.6256514953013,5.362701787992878,0.5313848294067788,0.4247043445566765,Web/CloudOps,2
9
+ bitbrains_rnd/H/short,Modular-TS-2.4M-v3_M1,1760008.1728826563,1760008.1728826563,156.42778973615788,5.9169358495513436,1.6377402633165954,0.5359674461057023,204.34510912470066,1326.6529964096326,5.806082855626521,0.684604573003369,0.6252948443035078,Web/CloudOps,2
10
+ bizitobs_application/10S/long,Modular-TS-2.4M-v3_M1,5275426.702222222,5275426.702222222,1418.387638888889,3.0462642085000406,0.04374827490912543,0.044643965827094186,34.158310869955976,2296.8297068398915,0.08857954110209905,0.054701541774522924,0.04655703725886792,Web/CloudOps,2
11
+ bizitobs_application/10S/medium,Modular-TS-2.4M-v3_M1,3028799.36,3028799.36,970.73,2.2233368341100945,0.031767988204956056,0.03231649398803711,19.42404117346401,1740.344609552947,0.06833821067690701,0.0381177100709006,0.03148191823098797,Web/CloudOps,2
12
+ bizitobs_application/10S/short,Modular-TS-2.4M-v3_M1,629297.9911111111,629297.9911111111,429.53465277777775,1.232171827879832,0.017635783089531794,0.01781905280219184,8.829611381439575,793.2830460252577,0.03059375624223889,0.016565434658564034,0.012766240040952294,Web/CloudOps,2
13
+ bizitobs_l2c/5T/long,Modular-TS-2.4M-v3_M1,92.45116071428572,92.45116071428572,6.359086061507936,0.6284250899642787,0.5096323568723897,0.7791392328354297,3.653899407257271,9.615152662037442,0.5398764472943504,0.35705317550293086,0.2669678027002204,Web/CloudOps,7
14
+ bizitobs_l2c/5T/medium,Modular-TS-2.4M-v3_M1,80.05903486394558,80.05903486394558,5.660879703443878,0.5515346011572464,0.38841309866420504,0.6572395147049294,3.432537186556386,8.947571450619748,0.47077383803909884,0.297845519242355,0.228644277283759,Web/CloudOps,7
15
+ bizitobs_l2c/5T/short,Modular-TS-2.4M-v3_M1,19.891878255208333,19.891878255208333,2.5970302036830355,0.26936388067215716,0.14014765634067433,0.16574777941924232,2.293561869201875,4.460031194420991,0.15317552406028445,0.08919252917942418,0.07143140322408012,Web/CloudOps,7
16
+ bizitobs_l2c/H/long,Modular-TS-2.4M-v3_M1,80.8273685515873,80.8273685515873,5.249859716021826,0.5630390129701736,0.6261864244101014,0.7163555186958651,5.3501780426093,8.990404248507812,0.5491598394034193,0.32067658348283234,0.26036159148559485,Web/CloudOps,7
17
+ bizitobs_l2c/H/medium,Modular-TS-2.4M-v3_M1,75.64733072916667,75.64733072916667,5.013981701078869,0.5068527865795754,0.5128123874838597,0.6639396817558852,3.3392014091576043,8.697547397350972,0.5266497057254618,0.3036042078011892,0.23915307887691264,Web/CloudOps,7
18
+ bizitobs_l2c/H/short,Modular-TS-2.4M-v3_M1,57.78172278025794,57.78172278025794,4.74778578016493,0.47055571124351386,0.441387441804369,0.5357121708376895,2.600709930494657,7.601428995936089,0.40973653327113746,0.255917839569219,0.19489947674121633,Web/CloudOps,7
19
+ bizitobs_service/10S/long,Modular-TS-2.4M-v3_M1,130062.9671957672,130062.9671957672,75.6047619047619,1.3220944818431355,0.08974954917948082,0.09647402162905093,17.90537812216811,360.64243676495863,0.2671754466043876,0.0560104246425329,0.051381319986138645,Web/CloudOps,2
20
+ bizitobs_service/10S/medium,Modular-TS-2.4M-v3_M1,37549.83365079365,37549.83365079365,44.102624007936505,1.0659872190913706,0.07613135443793403,0.07608071947854662,8.761180060093817,193.77779452453692,0.14584171713131502,0.03319267014621623,0.02467642624887996,Web/CloudOps,2
21
+ bizitobs_service/10S/short,Modular-TS-2.4M-v3_M1,3464.6414814814816,3464.6414814814816,21.6134126984127,0.8154586392523805,0.07128501932457011,0.05466276300016534,6.416263373524299,58.861205233001144,0.04360626258204015,0.01601190709042046,0.012609605005872552,Web/CloudOps,2
22
+ car_parts/M/short,Modular-TS-2.4M-v3_M1,1.305600452790527,1.305600452790527,0.5138089770543491,0.9264228697044318,0.7229986347697147,1.815765007228728,13.205239812801091,1.1426287467023255,2.7399065232330053,1.232061220225577,1.0199670264999154,Sales,1
23
+ covid_deaths/D/short,Modular-TS-2.4M-v3_M1,390569.1909774436,390569.1909774436,124.35499686716793,36.53481413039805,0.10018785763625955,0.1363243285045092,572.0192411858611,624.9553511871417,0.2351424922666607,0.04678917275228266,0.03960264190873637,Healthcare,1
24
+ electricity/15T/long,Modular-TS-2.4M-v3_M1,387027.38936486485,387027.38936486485,61.74539771021021,0.8999086727862419,0.13413104448677174,0.12631141205794552,7.4110272826156764,622.1152540846952,0.9814631068825673,0.0974109370803379,0.07878154973566881,Energy,1
25
+ electricity/15T/medium,Modular-TS-2.4M-v3_M1,324140.55405292794,324140.55405292794,57.261868524774776,0.8602740891433444,0.12796559968104762,0.1250541946200342,7.40904239422722,569.3334295937029,0.9810188500772348,0.0986679676504098,0.08014002920089768,Energy,1
26
+ electricity/15T/short,Modular-TS-2.4M-v3_M1,159011.70030686937,159011.70030686937,58.58624731665259,1.0991763686633387,0.17856490707210804,0.16560820949532176,12.54439727462825,398.76271178091537,0.7860399099717378,0.11548504212115139,0.09610354583362528,Energy,1
27
+ electricity/D/short,Modular-TS-2.4M-v3_M1,1324453454.931027,1324453454.931027,4420.498882882883,1.4642161417491881,0.5259458230883358,0.0996492496888516,11.876414361582455,36393.04129817989,0.5980585584955973,0.07264348060574302,0.05708177686933791,Energy,1
28
+ electricity/H/long,Modular-TS-2.4M-v3_M1,7372469.746162162,7372469.746162162,268.9585945945946,1.1839789805436962,0.294873560923189,0.13517404831015878,9.636234992963349,2715.229225343997,1.0708856186022901,0.10607719166485649,0.0846881171621528,Energy,1
29
+ electricity/H/medium,Modular-TS-2.4M-v3_M1,5435094.20972973,5435094.20972973,246.52309403153154,1.1023261199038497,0.24864662046620367,0.1266396394450642,8.89635907075197,2331.328850619262,0.9099944376457132,0.09622608336027569,0.07738372993314419,Energy,1
30
+ electricity/H/short,Modular-TS-2.4M-v3_M1,1876482.3957207208,1876482.3957207208,198.68760152730857,1.048592430136885,0.2125099229835893,0.12904541010787124,10.67673108397427,1369.847581200449,0.6466658874640627,0.09379473740952059,0.07583874279304725,Energy,1
31
+ electricity/W/short,Modular-TS-2.4M-v3_M1,63847587411.94955,63847587411.94955,35648.821621621624,1.8002616835096779,0.26463992427529304,0.107129864744331,12.682172214599442,252680.80143127128,0.5765342039285258,0.08133884679086692,0.06238429748467943,Energy,1
32
+ ett1/15T/long,Modular-TS-2.4M-v3_M1,8.64753410218254,8.64753410218254,1.6084292844742063,1.0242327428538225,0.7725774866299528,0.3994036380828373,7.362441077699103,2.940668988883744,0.560494416846029,0.3065682119433832,0.23851112316934234,Energy,7
33
+ ett1/15T/medium,Modular-TS-2.4M-v3_M1,8.946388268849207,8.946388268849207,1.612916976686508,1.0142896203616822,0.7179076932443985,0.4008093067956349,6.9522545201894985,2.99105136513053,0.5700972801996723,0.3074235341848529,0.23753058892474854,Energy,7
34
+ ett1/15T/short,Modular-TS-2.4M-v3_M1,4.493948800223214,4.493948800223214,1.0467429024832589,0.6744832518361066,0.4831348848598496,0.23317115420386905,4.85612060712067,2.1198935822873786,0.40074808391760935,0.19787795766233757,0.15605220939196554,Energy,7
35
+ ett1/D/short,Modular-TS-2.4M-v3_M1,41473.67619047619,41473.67619047619,136.99885912698412,1.6938340222853872,1.5330467587425596,0.47015250069754466,11.039966279309825,203.65086837643534,0.5348957609590255,0.3598320477956078,0.27754000750616026,Energy,7
36
+ ett1/H/long,Modular-TS-2.4M-v3_M1,190.66746031746032,190.66746031746032,8.587733548280424,1.5527086794374731,5316620674772.501,0.5386861488301918,8.817916161074745,13.808238856474794,0.6612466522675325,0.41124795988760454,0.316052518207568,Energy,7
37
+ ett1/H/medium,Modular-TS-2.4M-v3_M1,162.93642113095237,162.93642113095237,7.93248523530506,1.426481905997391,4927664332637.245,0.4986232212611607,8.261870012206636,12.764655151274255,0.6113967232377264,0.37994723888118587,0.2912179401657243,Energy,7
38
+ ett1/H/short,Modular-TS-2.4M-v3_M1,101.40430617559524,101.40430617559524,5.343412853422619,0.8934073410569346,0.5035806565057663,0.2850081307547433,5.960154567403204,10.06997051513038,0.4700067218968723,0.24939893867666016,0.1950639093326618,Energy,7
39
+ ett1/W/short,Modular-TS-2.4M-v3_M1,1106313.857142857,1106313.857142857,817.4416155133929,1.4995751755326385,0.6967250960213798,0.5054152693067279,9.828438170708507,1051.8145545403224,0.41854211569731004,0.3252795293040285,0.25851552338781847,Energy,7
40
+ ett2/15T/long,Modular-TS-2.4M-v3_M1,12.21656746031746,12.21656746031746,2.289798332093254,0.9465275728595461,0.14278422674529917,0.17554123651413692,7.384640025056532,3.4952206597463142,0.18626662345647246,0.1220274898885009,0.09823390820536648,Energy,7
41
+ ett2/15T/medium,Modular-TS-2.4M-v3_M1,11.67773933531746,11.67773933531746,2.1916136532738095,0.9034327604282267,0.1354037787471785,0.166792728484623,6.875634428994166,3.417270743637012,0.1821125316019545,0.11679505097284643,0.09285252044934728,Energy,7
42
+ ett2/15T/short,Modular-TS-2.4M-v3_M1,7.374314081101191,7.374314081101191,1.7099256243024554,0.7340474423392392,0.10527994779395827,0.12591985974993025,5.222691349888079,2.7155688319578997,0.1289177129693672,0.08117625237061474,0.06418560197946685,Energy,7
43
+ ett2/D/short,Modular-TS-2.4M-v3_M1,105054.83174603175,105054.83174603175,196.08004712301587,1.2984840916817926,0.4009605286613343,0.13548792278955854,12.586524599283038,324.12163109862286,0.18550334007335306,0.11222177162249401,0.09302339248607955,Energy,7
44
+ ett2/H/long,Modular-TS-2.4M-v3_M1,250.87258597883599,250.87258597883599,9.74873511904762,1.0779024421450007,0.1888666196931598,0.17896481468563988,7.821690076035867,15.83895785646379,0.21690233728199415,0.13350142427467618,0.10684645595914224,Energy,7
45
+ ett2/H/medium,Modular-TS-2.4M-v3_M1,232.85926339285714,232.85926339285714,9.49784923735119,1.0546525783918868,0.184112399827706,0.17202008565266927,7.232294896354205,15.259726845289766,0.20563461951715362,0.12798961829102862,0.10264518121230139,Energy,7
46
+ ett2/H/short,Modular-TS-2.4M-v3_M1,110.36870349702382,110.36870349702382,6.621943591889881,0.7615127738607653,0.12385460601547361,0.1129866463797433,5.21876475954453,10.505651026805708,0.13324088597847747,0.08398466966317847,0.06666944395920799,Energy,7
47
+ ett2/W/short,Modular-TS-2.4M-v3_M1,3541620.5714285714,3541620.5714285714,1345.2218191964287,0.9547622085461569,0.1449977159500122,0.15665956905909947,11.340939022920598,1881.919384944151,0.15786415525532294,0.11284346599404156,0.09697388059895506,Energy,7
48
+ hierarchical_sales/D/short,Modular-TS-2.4M-v3_M1,28.936732637453492,28.936732637453492,2.3071339525630425,0.7453639144930956,0.6165533933772638,1.0357622986776354,6.798132751789339,5.379287372640868,1.6503057939121941,0.7078031466790955,0.5757825059383115,Sales,1
49
+ hierarchical_sales/W/short,Modular-TS-2.4M-v3_M1,477.0483315677966,477.0483315677966,9.167869438559322,0.7432259541889759,0.5635383778173443,0.4685462046477754,6.674547896652912,21.84143611504968,1.002751015482967,0.42090137026274516,0.35890123597072937,Sales,1
50
+ hospital/M/short,Modular-TS-2.4M-v3_M1,3125.2115384615386,3125.2115384615386,18.713556945349847,0.767503522036095,0.192932274796454,0.1733501290093811,5.795029146932404,55.90359146299581,0.20294301861673855,0.06793455726470443,0.0545404261615041,Healthcare,1
51
+ jena_weather/10T/long,Modular-TS-2.4M-v3_M1,1443.2055555555555,1443.2055555555555,10.116731770833333,0.7593568382098755,1.100752820766555,0.49222818745996527,5.903263138358273,37.98954534547045,0.23246454209202339,0.06190601643657213,0.0515347532084138,Nature,21
52
+ jena_weather/10T/medium,Modular-TS-2.4M-v3_M1,1446.1549783549783,1446.1549783549783,9.601532061688312,0.6357975527659445,0.9295913873316204,0.4859170127441035,5.517174917876551,38.02834440723101,0.23337927368603792,0.058924431598551034,0.04895998333926589,Nature,21
53
+ jena_weather/10T/short,Modular-TS-2.4M-v3_M1,752.6180555555555,752.6180555555555,5.232061089409722,0.27035257312197475,0.3470003990971884,0.42719930557880287,2.2899825126672466,27.433885170634426,0.17039882350965418,0.03249765932243417,0.027317755948203546,Nature,21
54
+ jena_weather/D/short,Modular-TS-2.4M-v3_M1,358.2728670634921,358.2728670634921,9.786906312003968,1.1468402426850592,0.7556325844701777,0.4527134486607143,7.451680589994401,18.92809729115666,0.11397808223330899,0.05893317195492466,0.0474915151842114,Nature,21
55
+ jena_weather/H/long,Modular-TS-2.4M-v3_M1,1242.4757936507935,1242.4757936507935,10.863038401124339,0.8130128803640841,2.96361049556809,0.5416168209643959,6.04634515756844,35.24877010125025,0.2122366701561103,0.06540753312555465,0.054081654231405804,Nature,21
56
+ jena_weather/H/medium,Modular-TS-2.4M-v3_M1,1334.2667658730159,1334.2667658730159,10.666871279761905,0.7757337503072408,3.033535544209986,0.6149863686895949,5.766084326924138,36.52761648223185,0.22351894657938667,0.06527247221044566,0.05414278224434971,Nature,21
57
+ jena_weather/H/short,Modular-TS-2.4M-v3_M1,1118.265873015873,1118.265873015873,8.77562395572264,0.5575230640846863,2.1529343567204986,0.5692911840960002,4.7894401276209715,33.440482547593014,0.2049880216277734,0.053794014206363795,0.04447337600489246,Nature,21
58
+ kdd_cup_2018/D/short,Modular-TS-2.4M-v3_M1,2937.0604625889355,2937.0604625889355,21.22017127214035,1.1948516372972833,0.511777291233548,0.46432526976086136,9.314079710765023,54.19465345021532,1.2140178020228125,0.47535437623965665,0.3776508562461224,Nature,1
59
+ kdd_cup_2018/H/long,Modular-TS-2.4M-v3_M1,4117.359196395615,4117.359196395615,23.66399017941364,1.003633075334812,1.0376693110834994,0.6020821082508269,7.688290315191491,64.16665174680392,1.5056089083022801,0.5552528213672531,0.44193315910112674,Nature,1
60
+ kdd_cup_2018/H/medium,Modular-TS-2.4M-v3_M1,5299.258908786141,5299.258908786141,25.391270108120917,1.0468693908626807,0.9595902537350729,0.5642321082769655,8.73280114304173,72.79600887951304,1.5237522924414542,0.531485264518917,0.42880623103540333,Nature,1
61
+ kdd_cup_2018/H/short,Modular-TS-2.4M-v3_M1,4584.175843487953,4584.175843487953,22.749892904784264,0.95232434344199,0.890807433928544,0.5088257350694734,7.662817227431168,67.70654210257642,1.4172205403315912,0.4761964577390122,0.3829445880060442,Nature,1
62
+ loop_seattle/5T/long,Modular-TS-2.4M-v3_M1,77.26814915147345,77.26814915147345,5.110872122606352,0.80926892145504,0.1596513534141727,0.11667130228713378,5.974691681224069,8.790230324142449,0.15541307062057544,0.0903612648171242,0.07131325128347951,Transport,1
63
+ loop_seattle/5T/medium,Modular-TS-2.4M-v3_M1,65.26931017801857,65.26931017801857,4.754036780830753,0.7570855931174018,0.15041783068079206,0.10974240849261682,5.513218410195204,8.078942392294834,0.1437409880385142,0.08458408426080426,0.06666107906952709,Transport,1
64
+ loop_seattle/5T/short,Modular-TS-2.4M-v3_M1,42.28828225780444,42.28828225780444,3.693087041045859,0.584577183016778,0.09555870071899288,0.07878280734130104,4.261568654004953,6.502944122303715,0.11151113514682257,0.06332828952513796,0.04999500278718841,Transport,1
65
+ loop_seattle/D/short,Modular-TS-2.4M-v3_M1,19.247128160474716,19.247128160474716,3.069865941853715,0.9222919478266817,0.05698735145592468,0.056664226520172216,6.831595807621177,4.387154905001044,0.07840432343320114,0.05486260855008722,0.04409556422971058,Transport,1
66
+ loop_seattle/H/long,Modular-TS-2.4M-v3_M1,61.99619453044376,61.99619453044376,4.348210206505848,0.9057515030505758,0.1377619010786195,0.10102539482418516,7.543673818871382,7.8737662227452345,0.1407147465779697,0.07770833930892147,0.062404691134790226,Transport,1
67
+ loop_seattle/H/medium,Modular-TS-2.4M-v3_M1,65.77274735552116,65.77274735552116,4.492230331688597,0.921160909128047,0.12503000895182292,0.10257727246043359,7.348116805943949,8.11003991084638,0.14334404367348244,0.07939966608496138,0.06444749087666797,Transport,1
68
+ loop_seattle/H/short,Modular-TS-2.4M-v3_M1,61.149788382285045,61.149788382285045,4.268878590541999,0.8667326508127984,0.12220229219881393,0.09891576271249822,7.2416823477751615,7.81983301498728,0.13841899749822176,0.07556349270015796,0.061376456341542145,Transport,1
69
+ m4_daily/D/short,Modular-TS-2.4M-v3_M1,392397.3660481936,392397.3660481936,181.16333851895976,3.3476633916432093,0.03983545669163223,0.03301116227579889,26.376863236475945,626.4162881408765,0.09676400471083228,0.02798472912303381,0.02220331743107636,Econ/Fin,1
70
+ m4_hourly/H/short,Modular-TS-2.4M-v3_M1,1324520.2962962964,1324520.2962962964,237.69789150563608,1.081845233732063,0.11952784441519475,0.10734349249257749,8.872149782593194,1150.8780544854856,0.15712025744314306,0.032451008828867055,0.02570204011312775,Econ/Fin,1
71
+ m4_monthly/M/short,Modular-TS-2.4M-v3_M1,1934938.9624814815,1934938.9624814815,585.6214935619213,1.0030714222778911,0.16685686432873761,0.1375792439955252,8.349537856486776,1391.0208346683673,0.2891018823308095,0.12171225038659843,0.09736162749611771,Econ/Fin,1
72
+ m4_quarterly/Q/short,Modular-TS-2.4M-v3_M1,1874165.072,1874165.072,603.2542565104167,1.316495197534114,0.12405512468020122,0.10885652160644531,9.885131678659691,1369.001487216139,0.22914674122762205,0.10097413940152244,0.08072033966550607,Econ/Fin,1
73
+ m4_weekly/W/short,Modular-TS-2.4M-v3_M1,335642.3844011142,335642.3844011142,290.3625991000643,2.4103909647970867,0.07060589247810424,0.07127483360563532,19.266476231809886,579.346514964157,0.10554795669707111,0.05289956570837176,0.04221483984337579,Econ/Fin,1
74
+ m4_yearly/A/short,Modular-TS-2.4M-v3_M1,3688188.9997388353,3688188.9997388353,936.5003173877717,3.5548652369615543,0.17382724042770525,0.1558134202350262,29.47083697666009,1920.465828839148,0.3079555980318765,0.15017216706871636,0.12110404592475733,Econ/Fin,1
75
+ m_dense/D/short,Modular-TS-2.4M-v3_M1,10159.522962962963,10159.522962962963,53.3965625,0.7801702443914319,0.13779236247046422,0.1134519788953993,7.483199837382785,100.79445898938573,0.17449011796416458,0.09243734807373728,0.07591254278413982,Transport,1
76
+ m_dense/H/long,Modular-TS-2.4M-v3_M1,36196.61432098765,36196.61432098765,87.6086574074074,0.7551546647160053,0.3013599794595034,0.21297898553485448,7.125273549664825,190.2540783294478,0.3293579342551789,0.1516635368867529,0.1240570907774525,Transport,1
77
+ m_dense/H/medium,Modular-TS-2.4M-v3_M1,37816.72888888889,37816.72888888889,88.91314236111111,0.7605924991518306,0.2978922554395164,0.21514732452903898,7.291474351876949,194.46523825323868,0.33834494648820845,0.15469763472633177,0.1258787633837597,Transport,1
78
+ m_dense/H/short,Modular-TS-2.4M-v3_M1,48915.275555555556,48915.275555555556,100.25403645833333,0.8735990399490393,0.3199904790093199,0.23692531760679472,8.501058392911478,221.16798040303112,0.39229688570665994,0.17782567897241194,0.14547168121836587,Transport,1
79
+ restaurant/D/short,Modular-TS-2.4M-v3_M1,150.43012748669096,150.43012748669096,7.4691381806294945,0.7131750289869679,0.7115707333330414,0.4073099264791725,4.815310569254752,12.264996024731968,0.5644635347684277,0.3437470448918538,0.26815136235708237,Sales,1
80
+ saugeen/D/short,Modular-TS-2.4M-v3_M1,1255.1364583333334,1255.1364583333334,13.298326009114584,2.9542435988005167,0.29903676350911457,0.33566294352213544,25.569303454472077,35.42790507965908,1.147518286774784,0.43073594796643183,0.3473857615991493,Nature,1
81
+ saugeen/M/short,Modular-TS-2.4M-v3_M1,398.76102120535717,398.76102120535717,12.247690836588541,0.7174716361510609,0.342522212437221,0.35552256447928293,4.534491556471845,19.969001507470452,0.5995293316977024,0.3677124216427659,0.28603135309760525,Nature,1
82
+ saugeen/W/short,Modular-TS-2.4M-v3_M1,1000.85078125,1000.85078125,15.224264526367188,1.2349741504575213,0.3950361728668213,0.4176785469055176,11.806592395561532,31.636225774418794,0.9549657813238388,0.45955708408994433,0.37746652189469726,Nature,1
83
+ solar/10T/long,Modular-TS-2.4M-v3_M1,23.032447219687754,23.032447219687754,1.9713294753966444,0.8621459578505782,3.446850755508605,1.3579222325089273,4.592986812658656,4.799213187563953,1.0372731537285507,0.4260713292089982,0.3338359879421979,Energy,1
84
+ solar/10T/medium,Modular-TS-2.4M-v3_M1,23.399660611590356,23.399660611590356,1.9647820587259457,0.8598613118615777,3.9067165151966625,1.3391397518249897,4.916194768083654,4.837319568892504,1.0734204229639002,0.43599294164316454,0.3459019924979136,Energy,1
85
+ solar/10T/short,Modular-TS-2.4M-v3_M1,33.723401740229626,33.723401740229626,2.723277234451034,1.1968402326041616,6.204456295299063,1.5001876465957038,5.100924566936104,5.8071853543889596,1.6893095907126325,0.7922010525891977,0.6068997591975502,Energy,1
86
+ solar/D/short,Modular-TS-2.4M-v3_M1,135535.72749391728,135535.72749391728,250.01791362530415,0.9752235465681917,1.1072755094282238,0.4214751380493461,6.18232538588262,368.15177236286297,0.5318539169598063,0.3611907279932196,0.2825565340308264,Energy,1
87
+ solar/H/long,Modular-TS-2.4M-v3_M1,778.3379562043796,778.3379562043796,13.486013534063261,1.0291823076641544,5.149863372380082,1.276069171517705,5.4384003716373455,27.898708862676415,0.9672990802210861,0.4675846667872307,0.3518338960682997,Energy,1
88
+ solar/H/medium,Modular-TS-2.4M-v3_M1,736.2314476885645,736.2314476885645,12.329110591545012,0.9441481311907173,4.591132831562747,1.2722219049250862,5.789483995718961,27.13358523469695,0.9774150702516566,0.44412333979240187,0.3478506380884181,Energy,1
89
+ solar/H/short,Modular-TS-2.4M-v3_M1,715.8867962847355,715.8867962847355,11.702336005905646,0.9044365748556973,4.6407299556635095,1.1701262443745015,6.501452797930489,26.756060926166533,0.9862373044853525,0.4313519972351118,0.3508833002839567,Energy,1
90
+ solar/W/short,Modular-TS-2.4M-v3_M1,2389976.7591240876,2389976.7591240876,1241.9268932481752,1.3624889021851072,0.2861955183265853,0.2382669936131387,8.87072004666349,1545.954966719305,0.3155680594890949,0.2535083286166676,0.18192297054488138,Energy,1
91
+ sz_taxi/15T/long,Modular-TS-2.4M-v3_M1,15.928574608262108,15.928574608262108,2.7806888243411683,0.5244470608982269,6650318795664.556,0.41094389912749285,4.537647377087957,3.9910618396940567,0.3694055745349831,0.25737560429218964,0.20908997649672162,Transport,1
92
+ sz_taxi/15T/medium,Modular-TS-2.4M-v3_M1,16.384969284188035,16.384969284188035,2.7931724091880343,0.5450982003341567,9861828586658.79,0.4125929351545807,4.257375272013037,4.047835135499967,0.37671862381263804,0.25995131492201445,0.20716505142307973,Transport,1
93
+ sz_taxi/15T/short,Modular-TS-2.4M-v3_M1,16.7533195970696,16.7533195970696,2.753968011765253,0.5500745426844621,1067602223315.3171,0.4005391792791085,3.952657790711455,4.093081919174059,0.3827206648290862,0.2575077873333941,0.20260036043551058,Transport,1
94
+ sz_taxi/H/short,Modular-TS-2.4M-v3_M1,7.632657042935363,7.632657042935363,1.927211402827858,0.5869074094720954,1.2931420741937099,0.305347410022703,4.3569741325911115,2.7627263785860814,0.2573650106397508,0.1795316347787159,0.14225309101194755,Transport,1
95
+ temperature_rain/D/short,Modular-TS-2.4M-v3_M1,189.0777322179368,189.0777322179368,5.9719531694435695,1.3929113280636496,31.342278410802678,1.4597783411489158,18.62782191179909,13.75055388767801,1.6188037505079726,0.7030567835682812,0.5648490663882909,Nature,1
96
+ us_births/D/short,Modular-TS-2.4M-v3_M1,208488.02666666667,208488.02666666667,290.93140625,0.42816387846273435,0.0278005854288737,0.027437305450439452,4.140300088215134,456.60489119879855,0.04280300341490426,0.027272458563743076,0.02229785791913427,Healthcare,1
97
+ us_births/M/short,Modular-TS-2.4M-v3_M1,88368842.66666667,88368842.66666667,7829.018229166667,0.8869402107460148,0.023900558551152546,0.02429335316022237,6.003885858475542,9400.47034284278,0.029197796793272567,0.024316877242219714,0.018599029498638203,Healthcare,1
98
+ us_births/W/short,Modular-TS-2.4M-v3_M1,4173123.4285714286,4173123.4285714286,1518.942103794643,1.3805942500939652,0.02031937667301723,0.020534809146608626,9.69547943307617,2042.8224172872758,0.027731114006895916,0.020619490120016678,0.0164939195793787,Healthcare,1