File size: 11,084 Bytes
51bb0b7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | """Train Modular-TS from scratch with DHF + pinball loss.
python -m modular_ts.train --size 2.5M --ctx 4096 --tokens_per_batch 65536 --steps 20000 \
--lr 2e-3 --warmup 1000 --out runs/smoke --max_minutes 35 [--compile] [--data synthetic]
Logs JSONL to <out>/log.jsonl (step, loss, val, lr, enc tokens/s, grad norm),
checkpoints to <out>/ckpt.pt and an inference export (config.json + model.safetensors,
loadable by timestral.TimestralForecaster) to <out>/export/.
"""
from __future__ import annotations
import argparse
import json
import math
import os
import time
import torch
from modular_ts.model import ModularTS, count_params, make_config
from modular_ts.synth import SynthStream
def export(model: ModularTS, out_dir: str) -> None:
from safetensors.torch import save_file
os.makedirs(out_dir, exist_ok=True)
sd = {k: v.detach().float().cpu().contiguous() for k, v in model.state_dict().items()}
save_file(sd, os.path.join(out_dir, "model.safetensors"))
model.config.save(os.path.join(out_dir, "config.json"))
def lr_at(step, total, warmup, peak, final_frac=0.05):
if step < warmup:
return peak * (step + 1) / warmup
prog = min(1.0, (step - warmup) / max(1, total - warmup))
return peak * (final_frac + (1 - final_frac) * 0.5 * (1 + math.cos(math.pi * prog)))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--size", default="2.5M")
ap.add_argument("--ctx", type=int, default=4096)
ap.add_argument("--tokens_per_batch", type=int, default=65536, help="encoder patch tokens per step")
ap.add_argument("--steps", type=int, default=20000, help="schedule length (cosine horizon)")
ap.add_argument("--max_minutes", type=float, default=0, help="stop early after this wall time (0 = off)")
ap.add_argument("--lr", type=float, default=2e-3)
ap.add_argument("--warmup", type=int, default=1000)
ap.add_argument("--wd", type=float, default=0.05)
ap.add_argument("--clip", type=float, default=1.0)
ap.add_argument("--data", default="synthetic", choices=["synthetic", "synthetic+gifteval"])
ap.add_argument("--real_frac", type=float, default=0.5)
ap.add_argument("--gifteval_subset", default="", help="comma list of GiftEvalPretrain sub-datasets (default all kept)")
ap.add_argument("--gep_selection", default="", help="json {dataset: [shard files]} -> fast GPU-resident GiftEvalPretrain stream")
ap.add_argument("--synth", default="v1", choices=["v1", "v2"], help="v2 = modular_ts.synth2 prior (v3)")
ap.add_argument("--p_new", type=float, default=0.55, help="synth v2: share of new generator families")
ap.add_argument("--real_v2", action="store_true", help="v3 real stream: resolution sampling + TSMixup + ds_temp 0.3")
ap.add_argument("--min_lr_frac", type=float, default=0.05)
ap.add_argument("--time_schedule", action="store_true", help="cosine progress = elapsed / max_minutes (smoke runs)")
ap.add_argument("--compile", action="store_true")
ap.add_argument("--bf16", action="store_true", default=True)
ap.add_argument("--no_bf16", dest="bf16", action="store_false")
ap.add_argument("--log_every", type=int, default=25)
ap.add_argument("--val_every", type=int, default=250)
ap.add_argument("--ckpt_every", type=int, default=1000)
ap.add_argument("--out", default="runs/dev")
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--resume", action="store_true")
ap.add_argument("--resume_from", default="", help="ckpt to resume model+opt from (default <out>/ckpt.pt)")
ap.add_argument("--reset_schedule", action="store_true", help="on resume, restart warmup+cosine at the resumed step")
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
args = ap.parse_args()
os.makedirs(args.out, exist_ok=True)
torch.manual_seed(args.seed)
dev = torch.device(args.device)
if dev.type == "cuda":
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
cfg = make_config(args.size, context_length=args.ctx)
model = ModularTS(cfg).to(dev)
pc = count_params(model)
print("params", pc, flush=True)
if args.synth == "v2":
from modular_ts.synth2 import SynthStream2
synth = SynthStream2(args.ctx, args.tokens_per_batch, dev, seed=args.seed, p_new=args.p_new)
else:
synth = SynthStream(args.ctx, args.tokens_per_batch, dev, seed=args.seed)
stream = synth
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)"}
if args.data == "synthetic+gifteval":
from modular_ts.realdata import GiftEvalPretrainStream, MixedStream
sub = [s for s in args.gifteval_subset.split(",") if s] or None
if args.gep_selection:
from modular_ts.realdata import FastGiftEvalPretrain
if args.real_v2:
from modular_ts.realdata2 import FastGiftEvalPretrain2 as FastGiftEvalPretrain
real = FastGiftEvalPretrain(args.ctx, args.tokens_per_batch, dev, json.load(open(args.gep_selection)), seed=args.seed)
else:
real = GiftEvalPretrainStream(args.ctx, args.tokens_per_batch, dev, seed=args.seed, datasets_subset=sub)
stream = MixedStream(synth, real, args.real_frac, seed=args.seed)
manifest.update(real.manifest())
json.dump(manifest, open(os.path.join(args.out, "data_manifest.json"), "w"), indent=1)
# fixed held-out validation batches from a different seed (one per V bucket)
vgen = SynthStream(args.ctx, args.tokens_per_batch // 2, dev, seed=10_000 + args.seed, chunk=1)
val_batches = [vgen.next_batch(V) for V in (1, 4, 16)]
decay, no_decay = [], []
for n, p in model.named_parameters():
(decay if p.dim() >= 2 else no_decay).append(p)
opt = torch.optim.AdamW([{"params": decay, "weight_decay": args.wd}, {"params": no_decay, "weight_decay": 0.0}],
lr=args.lr, betas=(0.9, 0.95), eps=1e-8, fused=dev.type == "cuda")
step = 0
ck = os.path.join(args.out, "ckpt.pt")
rck = args.resume_from or ck
step0 = 0
prev_elapsed = 0.0
if args.resume and os.path.exists(rck):
st = torch.load(rck, map_location=dev)
model.load_state_dict(st["model"])
opt.load_state_dict(st["opt"])
step = st["step"]
if args.reset_schedule:
step0 = step
elif args.time_schedule and "elapsed_min" in st:
prev_elapsed = st["elapsed_min"] # continue the same time-based schedule after a crash
print("resumed at", step, "from", rck, flush=True)
fwd = model.forward_train
if args.compile:
try:
fwd = torch.compile(model.forward_train, dynamic=False)
except Exception as e: # pragma: no cover
print("compile failed, eager:", e, flush=True)
amp = torch.autocast(device_type=dev.type, dtype=torch.bfloat16, enabled=args.bf16 and dev.type == "cuda")
def evaluate():
model.eval()
tot = 0.0
with torch.no_grad(), amp:
for b in val_batches:
l, _ = model.forward_train(b["values"], b["observed"], b["roles"], b["s"])
tot += l.item()
model.train()
return tot / len(val_batches)
log = open(os.path.join(args.out, "log.jsonl"), "a")
t0 = time.time() - prev_elapsed * 60
t_last, tok_acc, gen_t = time.time(), 0, 0.0
ema = None
model.train()
while step < args.steps:
if args.max_minutes and (time.time() - t0) / 60 > args.max_minutes:
print("time limit reached", flush=True)
break
prof = step % args.log_every == 0 # measure data-gen share only on log steps (sync is costly)
if prof and dev.type == "cuda":
torch.cuda.synchronize()
tg = time.time()
b = stream.next_batch()
if prof and dev.type == "cuda":
torch.cuda.synchronize()
if prof:
gen_t += (time.time() - tg) * args.log_every
ss = step - step0
if args.time_schedule and args.max_minutes:
frac = (time.time() - t0) / 60 / args.max_minutes
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)
else:
lr = lr_at(ss, args.steps, args.warmup, args.lr, args.min_lr_frac)
for g in opt.param_groups:
g["lr"] = lr
try:
with amp:
loss, st = fwd(b["values"], b["observed"], b["roles"], b["s"])
except Exception as e:
if fwd is model.forward_train:
raise
print("compiled forward failed, falling back to eager:", repr(e)[:500], flush=True)
fwd = model.forward_train
with amp:
loss, st = fwd(b["values"], b["observed"], b["roles"], b["s"])
if not torch.isfinite(loss):
print(f"non-finite loss at step {step}; skipping batch", flush=True)
opt.zero_grad(set_to_none=True)
step += 1
continue
loss.backward()
gn = torch.nn.utils.clip_grad_norm_(model.parameters(), args.clip)
opt.step()
opt.zero_grad(set_to_none=True)
tok_acc += b["values"].shape[0] * b["values"].shape[1] * (args.ctx // cfg.patch_size)
step += 1
lv = loss.item() if step % args.log_every == 0 or step == 1 else None
if lv is not None:
ema = lv if ema is None else 0.9 * ema + 0.1 * lv
if dev.type == "cuda":
torch.cuda.synchronize()
dt = time.time() - t_last
rec = {"step": step, "loss": lv, "loss_ema": ema, "lr": lr, "gnorm": float(gn),
"enc_tok_s": tok_acc / dt, "gen_frac": gen_t / dt, "elapsed_min": (time.time() - t0) / 60}
if step % args.val_every == 0:
rec["val"] = evaluate()
if dev.type == "cuda":
rec["mem_gb"] = torch.cuda.max_memory_allocated() / 1e9
print(json.dumps(rec), flush=True)
log.write(json.dumps(rec) + "\n")
log.flush()
t_last, tok_acc, gen_t = time.time(), 0, 0.0
if step % args.ckpt_every == 0:
torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "step": step, "args": vars(args), "elapsed_min": (time.time() - t0) / 60}, ck)
export(model, os.path.join(args.out, "export"))
torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "step": step, "args": vars(args), "elapsed_min": (time.time() - t0) / 60}, ck)
export(model, os.path.join(args.out, "export"))
fin = {"final_step": step, "val": evaluate(), "params": pc, "minutes": (time.time() - t0) / 60}
print("FINAL", json.dumps(fin), flush=True)
json.dump(fin, open(os.path.join(args.out, "final.json"), "w"))
if __name__ == "__main__":
main()
|