Modular-TS-2.4M / modular_ts /teacher_gen.py
UncleanCode's picture
Upload via Autoresearch export_data
51bb0b7 verified
Raw History Blame Contribute Delete
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()