Download modular_ts/teacher_gen.py from Modularcomputing/Modular-TS-2.4M: direct link, hf CLI and curl.
- Browser
- Download file 6.45 kB
-
https://huggingface.co/Modularcomputing/Modular-TS-2.4M/resolve/main/modular_ts/teacher_gen.py
- Command line
-
hf download hf://Modularcomputing/Modular-TS-2.4M/modular_ts/teacher_gen.py
-
curl -L -o teacher_gen.py https://huggingface.co/Modularcomputing/Modular-TS-2.4M/resolve/main/modular_ts/teacher_gen.py
6.45 kB
| """Stage B: cache teacher quantile forecasts (TimesFM-2.5-200M + Chronos-2) for distillation. | |
| Run in the teacher venv (~/tvenv, system torch): | |
| ~/tvenv/bin/python -m modular_ts.teacher_gen --source synth --n 200000 --out teach/synth | |
| ~/tvenv/bin/python -m modular_ts.teacher_gen --source gep --n 100000 --out teach/gep --gep_selection gep_selection.json | |
| Each shard teach/<src>/shard_XXXX.npz holds | |
| ctx (n, C) float32 context, NaN = unobserved (left-padded for short contexts) | |
| fut (n, F) float32 ground-truth continuation (synthetic only; NaN for GEP = teacher-only input) | |
| q_tfm, q_cr2 (n, H, 9) float32 teacher quantiles 0.1..0.9 for the first H future steps | |
| Teacher weights: google/timesfm-2.5-200m-pytorch (Apache-2.0), amazon/chronos-2 (Apache-2.0). | |
| GiftEvalPretrain inputs use the same GIFT-Eval/fev/TIME leakage filter as run1 (realdata.filter_datasets). | |
| NEVER uses Timestral weights. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import math | |
| import os | |
| import time | |
| import numpy as np | |
| import torch | |
| Q9 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] | |
| def interp_nans(x: np.ndarray) -> np.ndarray: | |
| m = np.isfinite(x) | |
| if m.all(): | |
| return x | |
| idx = np.arange(len(x)) | |
| return np.interp(idx, idx[m], x[m]).astype(np.float32) | |
| class Teachers: | |
| def __init__(self, C, H, dev="cuda", tfm_bs=256, cr_bs=512): | |
| import timesfm | |
| from chronos import Chronos2Pipeline | |
| torch.set_float32_matmul_precision("high") | |
| self.H = H | |
| self.cr = Chronos2Pipeline.from_pretrained("amazon/chronos-2", device_map=dev) | |
| self.cr_bs = cr_bs | |
| self.tfm = timesfm.TimesFM_2p5_200M_torch.from_pretrained("google/timesfm-2.5-200m-pytorch") | |
| self.tfm.compile(timesfm.ForecastConfig(max_context=C, max_horizon=H, normalize_inputs=True, per_core_batch_size=tfm_bs, | |
| use_continuous_quantile_head=True, force_flip_invariance=True, | |
| infer_is_positive=True, fix_quantile_crossing=True)) | |
| def __call__(self, ctx: np.ndarray): | |
| """ctx (n, C) with NaN -> q_tfm, q_cr2 (n, H, 9).""" | |
| seqs = [] | |
| for row in ctx: | |
| f = np.isfinite(row) | |
| first = int(np.argmax(f)) if f.any() else len(row) - 1 | |
| seqs.append(row[first:]) | |
| with torch.no_grad(): | |
| qs, _ = self.cr.predict_quantiles([torch.from_numpy(s) for s in seqs], prediction_length=self.H, | |
| quantile_levels=Q9, batch_size=self.cr_bs) | |
| q_cr2 = torch.stack([q[0] for q in qs]).float().cpu().numpy() | |
| _, qt = self.tfm.forecast(horizon=self.H, inputs=[interp_nans(s) for s in seqs]) | |
| q_tfm = np.asarray(qt)[:, :self.H, 1:10].astype(np.float32) | |
| return q_tfm, q_cr2 | |
| def synth_windows(gen, n, C, F, rng): | |
| vals, roles = gen.group_batch(n, 1) | |
| v = vals[:, 0].float().cpu().numpy() | |
| T = C + F | |
| # context length: 50% full C, else log-uniform in [48, C]; 15% of series get iid missing values | |
| clen = np.where(rng.random(n) < 0.5, C, np.exp(rng.uniform(math.log(48), math.log(C), n))).astype(int) | |
| obs = np.ones((n, T), bool) | |
| obs[np.arange(T)[None] < (C - clen)[:, None]] = False | |
| miss = rng.random(n) < 0.15 | |
| pm = rng.uniform(0, 0.1, n)[:, None] | |
| obs &= ~(miss[:, None] & (rng.random((n, T)) < pm)) | |
| v = np.where(obs, v, np.nan).astype(np.float32) | |
| return v[:, :C], v[:, C:] | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--source", choices=["synth", "gep"], required=True) | |
| ap.add_argument("--n", type=int, default=100000) | |
| ap.add_argument("--shard", type=int, default=8192) | |
| ap.add_argument("--C", type=int, default=2048) | |
| ap.add_argument("--F", type=int, default=1024) | |
| ap.add_argument("--H", type=int, default=512) | |
| ap.add_argument("--out", required=True) | |
| ap.add_argument("--seed", type=int, default=123) | |
| ap.add_argument("--gep_selection", default="gep_selection.json") | |
| ap.add_argument("--max_minutes", type=float, default=0) | |
| args = ap.parse_args() | |
| os.makedirs(args.out, exist_ok=True) | |
| dev = torch.device("cuda") | |
| rng = np.random.default_rng(args.seed) | |
| T = args.C + args.F | |
| if args.source == "synth": | |
| from modular_ts.synth import SynthGen | |
| gen = SynthGen(T, dev, seed=args.seed) | |
| else: | |
| from modular_ts.realdata import FastGiftEvalPretrain | |
| gep = FastGiftEvalPretrain(T, args.shard * 2 * (T // 32), dev, json.load(open(args.gep_selection)), seed=args.seed, | |
| mv_frac=0.0, short_ctx_p=0.3) | |
| json.dump(gep.manifest(), open(os.path.join(args.out, "gep_manifest.json"), "w"), indent=1) | |
| tea = Teachers(args.C, args.H) | |
| t0, done, k = time.time(), 0, 0 | |
| while done < args.n: | |
| if args.max_minutes and (time.time() - t0) / 60 > args.max_minutes: | |
| print("time limit", flush=True) | |
| break | |
| p = os.path.join(args.out, f"shard_{k:04d}.npz") | |
| if os.path.exists(p): | |
| done += args.shard; k += 1 | |
| continue | |
| if args.source == "synth": | |
| gen = SynthGen(T, dev, seed=args.seed * 1000 + k) # per-shard seed -> resumable without duplicates | |
| rng = np.random.default_rng(args.seed * 1000 + k) | |
| ctx, fut = synth_windows(gen, args.shard, args.C, args.F, rng) | |
| else: | |
| gep.g.manual_seed(args.seed * 1000 + k); gep.rng.seed(args.seed * 1000 + k) | |
| b = gep.next_batch() | |
| v = torch.where(b["observed"][:, 0], b["values"][:, 0], torch.full_like(b["values"][:, 0], float("nan"))).cpu().numpy() | |
| ok = np.isfinite(v[:, :args.C]).sum(1) >= 48 | |
| v = v[ok][:args.shard] | |
| ctx, fut = v[:, :args.C], np.full((len(v), args.F), np.nan, np.float32) | |
| ts = time.time() | |
| q_tfm, q_cr2 = tea(ctx) | |
| good = np.isfinite(q_tfm).all((1, 2)) & np.isfinite(q_cr2).all((1, 2)) | |
| 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)) | |
| done += int(good.sum()); k += 1 | |
| print(json.dumps({"shard": k, "n": int(good.sum()), "dropped": int((~good).sum()), "teach_s": round(time.time() - ts, 1), | |
| "done": done, "min": round((time.time() - t0) / 60, 2)}), flush=True) | |
| print("TEACH_DONE", done, flush=True) | |
| if __name__ == "__main__": | |
| main() | |