File size: 9,301 Bytes
df43f42
626bca9
df43f42
 
 
626bca9
df43f42
626bca9
df43f42
626bca9
 
 
 
 
df43f42
626bca9
df43f42
 
 
 
 
 
 
 
 
 
 
 
 
626bca9
 
df43f42
6f1eb44
 
df43f42
 
 
626bca9
 
 
 
 
 
6f1eb44
 
626bca9
 
 
 
359f2c4
626bca9
 
 
359f2c4
 
 
 
 
 
 
 
 
 
 
 
 
 
626bca9
 
 
 
 
 
 
 
 
df43f42
 
 
 
 
 
 
 
 
 
626bca9
 
 
 
 
e1640f8
 
 
 
 
 
 
 
 
 
 
 
626bca9
 
 
 
 
 
 
e1640f8
d50dad3
 
 
 
 
df43f42
626bca9
df43f42
 
 
626bca9
df43f42
 
 
 
 
626bca9
df43f42
626bca9
e1640f8
df43f42
 
 
 
 
e1640f8
df43f42
 
 
 
 
 
 
 
 
 
 
 
e1640f8
df43f42
 
 
e1640f8
df43f42
 
 
 
 
 
e1640f8
 
 
df43f42
 
 
 
626bca9
df43f42
 
 
 
 
 
 
 
e1640f8
 
df43f42
 
 
 
 
 
 
 
626bca9
df43f42
626bca9
df43f42
626bca9
 
 
df43f42
 
626bca9
df43f42
626bca9
 
df43f42
 
 
 
 
 
 
 
 
 
 
e1640f8
 
626bca9
 
 
 
 
 
 
 
 
6f1eb44
626bca9
 
df43f42
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
"""
clankerDiffusion — training loop (hybrid AR / masked-diffusion).

Hybrid objective (per step, mode chosen at random, p(AR)=0.5):
  AR   (mode 0): causal LM cross-entropy over the whole window.
  DIFF (mode 1): MDLM absorbing-state masked diffusion -- mask each token
                 independently with ratio r~U(0,1); reconstruct masked tokens
                 with bidirectional attention, conditioned on r via time embed.

Runs in bf16, AdamW + cosine LR, grad-clip, checkpoints locally and (optionally)
pushes each checkpoint to a HuggingFace repo via the `hf` CLI.

Used both locally and on Modal L4 (override --data-dir/--ckpt-dir/--hf-repo and
the model dimensions for a bigger model).
"""
import os, json, time, argparse, subprocess, threading
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

from model import YKDiff
from tokenizer import YKTokenizer

HERE = os.path.dirname(os.path.abspath(__file__))
DATADIR = os.path.join(HERE, "data")
CKPTDIR = os.path.join(HERE, "checkpoints")
os.makedirs(CKPTDIR, exist_ok=True)

# default architecture tuned for 16 GB (RTX 5060 Ti)
DEFAULT_CFG = dict(
    d_model=768, n_layers=12, n_heads=12, d_ff=2048,
    max_len=8192, vocab_size=32768,
    rope_scale=1.0,
)


def build_cfg(args):
    cfg = dict(DEFAULT_CFG)
    for k in ("d_model", "n_layers", "n_heads", "d_ff", "vocab_size", "max_len"):
        v = getattr(args, k, None)
        if v is not None:
            cfg[k] = v
    if getattr(args, "rope_scale", None) is not None:
        cfg["rope_scale"] = args.rope_scale
    return cfg


def _push_hf(path, repo):
    """Upload a single checkpoint file to HF (background thread)."""
    if not repo:
        return
    try:
        from huggingface_hub import HfApi
        token = os.environ.get("HF_TOKEN")
        if not token:
            for p in (os.path.join(HERE, ".env"),
                      os.path.join(os.path.dirname(HERE), ".env"),
                      os.path.join(os.path.expanduser("~"), ".env")):
                if os.path.exists(p):
                    for line in open(p, encoding="utf-8"):
                        if line.strip().startswith("HF_TOKEN"):
                            token = line.split("=", 1)[1].strip().strip('"').strip("'")
        api = HfApi(token=token)
        api.upload_file(path_or_fileobj=path,
                        path_in_repo=os.path.basename(path),
                        repo_id=repo, repo_type="model")
        print(f"[hf] pushed {os.path.basename(path)} -> {repo}", flush=True)
    except Exception as e:
        print(f"[hf] push failed for {path}: {e}", flush=True)


def load_data(data_dir):
    meta = json.load(open(os.path.join(data_dir, "meta.json")))
    arr = np.memmap(os.path.join(data_dir, "train.bin"), dtype=np.uint16, mode="r")
    return arr, meta["seq_len"], meta["vocab_size"], meta["n_tokens"]


def sample_batch(arr, seq_len, batch):
    N = len(arr)
    starts = np.random.randint(0, N - seq_len, size=batch)
    out = np.stack([arr[s:s + seq_len].astype(np.int64) for s in starts])
    return torch.from_numpy(out).long()


def train(args):
    data_dir = args.data_dir or DATADIR
    ckpt_dir = args.ckpt_dir or CKPTDIR
    os.makedirs(ckpt_dir, exist_ok=True)
    cfg = build_cfg(args)

    # ---- device resolution (cuda / xla / cpu) ----
    if getattr(args, "device", "cuda") == "xla":
        import torch_xla.core.xla_model as xm
        device = xm.xla_device()
        print(f"[train] device = TPU:XLA ({device})")
    elif getattr(args, "device", "cuda") == "cpu":
        device = torch.device("cpu")
        print("[train] device = CPU")
    else:
        device = torch.device("cuda")
        print(f"[train] device = {device}")

    tok = YKTokenizer.load(os.path.join(data_dir, "tokenizer.json"))
    arr, seq_len, vocab, n_tokens = load_data(data_dir)
    cfg["vocab_size"] = vocab
    cfg["max_len"] = seq_len
    print(f"[train] data n_tokens={n_tokens:,} seq_len={seq_len} vocab={vocab}")
    print(f"[train] model params = {sum(p.numel() for p in YKDiff(cfg).parameters())/1e6:.1f}M")

    model = YKDiff(cfg).to(device)
    # Train in bf16 weights so a large model fits a 22 GB L4: fp32 weights would
    # need ~21.6 GB just for params+grads+Adam states (OOM). bf16 halves that.
    if device.type in ("cuda", "xla"):
        model = model.to(torch.bfloat16)
        print("[train] using bfloat16 weights")
    n_params = sum(p.numel() for p in model.parameters())
    print(f"[train] allocated params = {n_params/1e6:.1f}M")

    optim = torch.optim.AdamW(model.parameters(), lr=args.lr, betas=(0.9, 0.95),
                              weight_decay=0.1)
    V = cfg["vocab_size"]
    pad_id = tok.pad_id
    mask_id = tok.mask_id

    # resume
    step0 = 0
    ckpts = sorted([f for f in os.listdir(ckpt_dir) if f.endswith(".pt")])
    if ckpts and not args.fresh:
        path = os.path.join(ckpt_dir, ckpts[-1])
        sd = torch.load(path, map_location=device)
        model.load_state_dict(sd["model"]); optim.load_state_dict(sd["optim"])
        step0 = sd["step"]
        print(f"[train] resumed from {path} step={step0}")

    model.train()
    amp = torch.amp.autocast(device_type=device.type, dtype=torch.bfloat16)
    t0 = time.time()
    limit = args.hours * 3600.0
    step = step0
    running = 0.0

    while True:
        if time.time() - t0 > limit:
            print(f"[train] wall-clock limit {args.hours}h reached at step {step}")
            break

        optim.zero_grad(set_to_none=True)
        mode_ar = (torch.rand(1).item() < 0.5)
        idx = sample_batch(arr, seq_len, args.batch).to(device)

        with amp:
            if mode_ar:
                m = torch.zeros(args.batch, dtype=torch.long, device=device)
                logits = model(idx, m, t=None)                 # causal
                loss = F.cross_entropy(
                    logits[:, :-1].reshape(-1, V),
                    idx[:, 1:].reshape(-1), ignore_index=pad_id)
                mname = "AR"
            else:
                m = torch.ones(args.batch, dtype=torch.long, device=device)
                r = torch.rand(args.batch, device=device)       # per-sample ratio
                is_mask = torch.rand(args.batch, seq_len, device=device) < r[:, None]
                not_pad = idx != pad_id
                masked = idx.clone(); masked[is_mask] = mask_id
                logits = model(masked, m, t=r)
                ce = F.cross_entropy(logits.reshape(-1, V), idx.reshape(-1),
                                     reduction="none", ignore_index=-100)
                ce = ce * is_mask.reshape(-1) * not_pad.reshape(-1)
                denom = (is_mask & not_pad).reshape(-1).sum().clamp(min=1)
                loss = ce.sum() / denom
                mname = "DIFF"

        loss.backward()
        nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        optim.step()
        if device.type == "xla":
            xm.mark_step()

        running = running * 0.9 + float(loss.item()) * 0.1
        step += 1
        if step % args.log_every == 0:
            print(f"[train] step {step} [{mname}] loss={running:.3f} "
                  f"t={(time.time()-t0)/60:.1f}m", flush=True)

        if step % args.ckpt_every == 0:
            path = os.path.join(ckpt_dir, f"clanker_{step:07d}.pt")
            torch.save({"model": model.state_dict(), "optim": optim.state_dict(),
                        "step": step, "cfg": cfg, "vocab": V}, path)
            print(f"[train] checkpoint -> {path}", flush=True)
            if args.hf_repo:
                threading.Thread(target=_push_hf, args=(path, args.hf_repo),
                                 daemon=True).start()

    # final save
    path = os.path.join(ckpt_dir, f"clanker_{step:07d}_final.pt")
    torch.save({"model": model.state_dict(), "optim": optim.state_dict(),
                "step": step, "cfg": cfg, "vocab": V}, path)
    json.dump(cfg, open(os.path.join(ckpt_dir, "config.json"), "w"))
    print(f"[train] DONE final={path} steps={step}")


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--hours", type=float, default=5.0)
    ap.add_argument("--batch", type=int, default=32)
    ap.add_argument("--lr", type=float, default=3e-4)
    ap.add_argument("--log-every", type=int, default=25)
    ap.add_argument("--ckpt-every", type=int, default=500)
    ap.add_argument("--fresh", action="store_true")
    ap.add_argument("--device", default="cuda", choices=["cuda", "xla", "cpu"],
                    help="training device (cuda default; xla for TPU; cpu for tests)")
    ap.add_argument("--data-dir", default=None)
    ap.add_argument("--ckpt-dir", default=None)
    ap.add_argument("--hf-repo", default=None,
                    help="HuggingFace repo id to push checkpoints to (via `hf` CLI)")
    # model overrides (for a bigger Modal model)
    ap.add_argument("--d-model", type=int, default=None)
    ap.add_argument("--n-layers", type=int, default=None)
    ap.add_argument("--n-heads", type=int, default=None)
    ap.add_argument("--d-ff", type=int, default=None)
    ap.add_argument("--rope-scale", type=float, default=None)
    ap.add_argument("--vocab-size", type=int, default=None)
    ap.add_argument("--seq-len", type=int, default=None)
    train(ap.parse_args())