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