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()