File size: 14,776 Bytes
9c6799a | 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 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | """Text LM training from a world-model (iVideoGPT) init vs. controls.
Every arm uses the same LlamaConfig as iVideoGPT's transformer, re-sized to the
GPT-2 vocab. Only the body (everything but embed_tokens / lm_head) differs:
--init scratch HF default random init
--init ivgpt iVideoGPT body weights; embeddings + head freshly initialized
--init shuffled iVideoGPT body with each tensor's entries randomly permuted
(same per-tensor weight statistics, no learned structure)
--init rescaled iVideoGPT body with every matrix scaled to std 0.02 (the scratch
init scale); keeps learned structure, removes the ~2.5x larger scale
--init sp shrink-and-perturb (Ash & Adams 2020): lambda * iVideoGPT + fresh init
(--sp_lambda); RMSNorm gains become lambda * g + (1 - lambda)
--init nobody no pretrained body at all: only the --extra_layers (baseline for them)
--freeze body train only embed_tokens, lm_head and RMSNorm gains (+ LoRA / extra layers)
--lora_rank R LoRA on every body linear (frozen base); merged into the weights at save
--extra_layers K append K freshly initialized trainable layers on top of the body
Launch: torchrun --nproc_per_node=N wp/train.py --init ivgpt --lr 1e-3 ...
"""
import argparse
import json
import math
import os
import time
from contextlib import nullcontext
from pathlib import Path
import numpy as np
import torch
import torch.distributed as dist
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file
from transformers import AutoTokenizer, LlamaConfig, LlamaForCausalLM
GPT2_EOT = 50256
VOCAB = 50304 # GPT-2 vocab padded to a multiple of 128
IO_PREFIXES = ("model.embed_tokens.", "lm_head.")
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--init", choices=["scratch", "ivgpt", "shuffled", "rescaled", "sp", "nobody"], required=True)
p.add_argument("--freeze", choices=["none", "body"], default="none")
p.add_argument("--repo", default="thuml/ivideogpt-oxe-64-act-free-medium")
p.add_argument("--data_dir", default="/data/fineweb-edu-gpt2")
p.add_argument("--out_dir", required=True)
p.add_argument("--ckpt_dir", default=None, help="rolling checkpoint location (default: out_dir)")
p.add_argument("--final_dir", default=None, help="save final bf16 HF model here")
p.add_argument("--tokens", type=float, default=2e9, help="training token budget")
p.add_argument("--batch_tokens", type=int, default=524288, help="global tokens per step")
p.add_argument("--seq_len", type=int, default=1024)
p.add_argument("--micro_bs", type=int, default=32)
p.add_argument("--lr", type=float, default=1e-3)
p.add_argument("--min_lr_frac", type=float, default=0.1)
p.add_argument("--warmup_steps", type=int, default=200, help="-1: 5%% of steps, capped at 200")
p.add_argument("--wd", type=float, default=0.1)
p.add_argument("--grad_clip", type=float, default=1.0)
p.add_argument("--eval_every", type=int, default=100)
p.add_argument("--val_tokens", type=int, default=4_000_000)
p.add_argument("--ckpt_every", type=int, default=500)
p.add_argument("--seed", type=int, default=0)
p.add_argument("--no_compile", action="store_true")
p.add_argument("--sp_lambda", type=float, default=0.5)
p.add_argument("--sp_only", choices=["all", "attn", "mlp", "lower", "upper"], default="all",
help="apply S&P only to these weights; the rest keep the scratch init")
p.add_argument("--sp_shuffle", action="store_true", help="shrink-and-perturb the shuffled body (statistics-matched control)")
p.add_argument("--lora_rank", type=int, default=0)
p.add_argument("--extra_layers", type=int, default=0)
return p.parse_args()
def build_model(args):
cfg = LlamaConfig.from_pretrained(args.repo, subfolder="transformer")
n_body = 0 if args.init == "nobody" else cfg.num_hidden_layers
cfg.num_hidden_layers = n_body + args.extra_layers
cfg.vocab_size = VOCAB
cfg.bos_token_id = cfg.eos_token_id = GPT2_EOT
cfg.attention_dropout = 0.0
cfg.max_position_embeddings = args.seq_len
cfg.tie_word_embeddings = False
torch.manual_seed(args.seed)
model = LlamaForCausalLM(cfg)
model.n_body = n_body
if args.init in ("scratch", "nobody"):
return model
sd = load_file(hf_hub_download(args.repo, "transformer/model.safetensors"))
# action-conditioned checkpoints nest the Llama under "llm." and add an action input layer
sd = {k.removeprefix("llm."): v for k, v in sd.items() if not k.startswith("action_linear.")}
body = {k: v for k, v in sd.items() if not k.startswith(IO_PREFIXES) and "rotary_emb" not in k}
if args.init == "shuffled" or (args.init == "sp" and args.sp_shuffle):
g = torch.Generator().manual_seed(args.seed + 1)
body = {k: v.flatten()[torch.randperm(v.numel(), generator=g)].view_as(v) for k, v in body.items()}
if args.init == "rescaled":
body = {k: v * (cfg.initializer_range / v.std()) if v.dim() == 2 else v for k, v in body.items()}
if args.init == "sp":
fresh = model.state_dict() # the random init drawn above is the perturbation
lam = args.sp_lambda
def selected(k):
layer = int(k.split(".")[2]) if k.startswith("model.layers.") else -1
return {"all": True, "attn": "self_attn" in k or "input_layernorm" in k,
"mlp": ".mlp." in k or "post_attention_layernorm" in k,
"lower": 0 <= layer < cfg.num_hidden_layers // 2,
"upper": layer >= cfg.num_hidden_layers // 2}[args.sp_only]
body = {k: (lam * v + (fresh[k] if v.dim() == 2 else (1 - lam))) if selected(k) else fresh[k]
for k, v in body.items()}
missing, unexpected = model.load_state_dict(body, strict=False)
assert not unexpected, f"unexpected keys: {unexpected}"
is_extra = lambda k: k.startswith("model.layers.") and int(k.split(".")[2]) >= n_body
assert all(k.startswith(IO_PREFIXES) or is_extra(k) for k in missing), f"missing body keys: {missing}"
return model
class LoRALinear(torch.nn.Module):
"""y = W x + (B A x) * alpha/r with W frozen; B starts at zero so the init function is unchanged."""
def __init__(self, base, r, alpha):
super().__init__()
self.base = base
self.lora_A = torch.nn.Parameter(torch.empty(r, base.in_features))
torch.nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
self.lora_B = torch.nn.Parameter(torch.zeros(base.out_features, r))
self.scale = alpha / r
def forward(self, x):
return self.base(x) + (x @ self.lora_A.t()) @ self.lora_B.t() * self.scale
def merged(self):
lin = self.base
lin.weight.data += (self.lora_B @ self.lora_A).to(lin.weight.dtype) * self.scale
return lin
LORA_TARGETS = {"self_attn": ("q_proj", "k_proj", "v_proj", "o_proj"), "mlp": ("gate_proj", "up_proj", "down_proj")}
def add_lora(model, r):
for layer in model.model.layers[: model.n_body]:
for parent, names in LORA_TARGETS.items():
mod = getattr(layer, parent)
for n in names:
setattr(mod, n, LoRALinear(getattr(mod, n), r, alpha=r))
def merge_lora(model):
for layer in model.model.layers:
for parent, names in LORA_TARGETS.items():
mod = getattr(layer, parent)
for n in names:
if isinstance(getattr(mod, n), LoRALinear):
setattr(mod, n, getattr(mod, n).merged())
def apply_freeze(model, freeze):
if freeze == "none":
return
for name, p in model.named_parameters():
extra = name.startswith("model.layers.") and int(name.split(".")[2]) >= model.n_body
p.requires_grad = name.startswith(IO_PREFIXES) or "norm" in name or "lora_" in name or extra
class Batches:
"""Random seq_len+1 windows from a uint16 token memmap; same order for every arm."""
def __init__(self, path, seq_len, micro_bs, seed, rank):
self.data = np.memmap(path, dtype=np.uint16, mode="r")
self.seq_len, self.micro_bs = seq_len, micro_bs
self.rng = np.random.default_rng([seed, rank])
def next(self, device):
idx = self.rng.integers(0, len(self.data) - self.seq_len - 1, self.micro_bs)
x = np.stack([self.data[i : i + self.seq_len + 1] for i in idx]).astype(np.int64)
x = torch.from_numpy(x).pin_memory().to(device, non_blocking=True)
return x[:, :-1], x[:, 1:]
@torch.no_grad()
def evaluate(model, val, args, rank, world, device):
"""Mean loss over a fixed set of non-overlapping windows from the start of val.bin."""
model.eval()
n_seq = args.val_tokens // args.seq_len
starts = np.arange(n_seq)[rank::world] * args.seq_len
tot, cnt = torch.zeros((), device=device), torch.zeros((), device=device)
for i in range(0, len(starts), args.micro_bs):
x = np.stack([val[s : s + args.seq_len + 1] for s in starts[i : i + args.micro_bs]]).astype(np.int64)
x = torch.from_numpy(x).to(device)
with torch.autocast("cuda", dtype=torch.bfloat16):
logits = model(input_ids=x[:, :-1]).logits
tot += torch.nn.functional.cross_entropy(
logits.float().flatten(0, 1), x[:, 1:].flatten(), reduction="sum"
)
cnt += x[:, 1:].numel()
if world > 1:
dist.all_reduce(tot)
dist.all_reduce(cnt)
model.train()
return (tot / cnt).item()
def lr_at(step, total, args):
if step < args.warmup_steps:
return args.lr * (step + 1) / args.warmup_steps
t = (step - args.warmup_steps) / max(1, total - args.warmup_steps)
return args.lr * (args.min_lr_frac + (1 - args.min_lr_frac) * 0.5 * (1 + math.cos(math.pi * t)))
def main():
args = parse_args()
ddp = "RANK" in os.environ
if ddp:
dist.init_process_group("nccl")
rank, world = dist.get_rank(), dist.get_world_size()
local = int(os.environ["LOCAL_RANK"])
else:
rank, world, local = 0, 1, 0
device = torch.device("cuda", local)
torch.cuda.set_device(device)
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
master = rank == 0
out = Path(args.out_dir)
if master:
out.mkdir(parents=True, exist_ok=True)
tok_per_micro = args.micro_bs * args.seq_len * world
assert args.batch_tokens % tok_per_micro == 0, "batch_tokens must divide micro_bs*seq_len*world"
accum = args.batch_tokens // tok_per_micro
total_steps = int(args.tokens // args.batch_tokens)
if args.warmup_steps < 0:
args.warmup_steps = min(200, max(10, int(0.05 * total_steps)))
model = build_model(args)
if args.lora_rank:
add_lora(model, args.lora_rank)
model = model.to(device)
apply_freeze(model, args.freeze)
params = [p for p in model.parameters() if p.requires_grad]
n_total = sum(p.numel() for p in model.parameters())
n_train = sum(p.numel() for p in params)
decay = [p for p in params if p.dim() >= 2]
no_decay = [p for p in params if p.dim() < 2]
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=True,
)
step = 0
ckpt_path = Path(args.ckpt_dir or out) / "ckpt.pt"
if master:
ckpt_path.parent.mkdir(parents=True, exist_ok=True)
if ckpt_path.exists():
ck = torch.load(ckpt_path, map_location=device)
model.load_state_dict(ck["model"])
opt.load_state_dict(ck["opt"])
step = ck["step"]
if master:
print(f"resumed from step {step}")
fwd = model if args.no_compile else torch.compile(model)
if ddp:
fwd = torch.nn.parallel.DistributedDataParallel(fwd, device_ids=[local])
train = Batches(Path(args.data_dir) / "train.bin", args.seq_len, args.micro_bs, args.seed, rank)
for _ in range(step * accum): # replay the sampler so resumed runs see the same data
train.rng.integers(0, len(train.data) - args.seq_len - 1, args.micro_bs)
val = np.memmap(Path(args.data_dir) / "val.bin", dtype=np.uint16, mode="r")
def log(rec):
if master:
print(json.dumps(rec), flush=True)
with open(out / "metrics.jsonl", "a") as f:
f.write(json.dumps(rec) + "\n")
if master and step == 0:
(out / "args.json").write_text(json.dumps(vars(args) | {"world": world, "accum": accum}, indent=2))
log({"step": 0, "tokens": 0, "val_loss": evaluate(model, val, args, rank, world, device),
"n_params": n_total, "n_trainable": n_train})
elif step == 0:
evaluate(model, val, args, rank, world, device)
t0 = time.time()
while step < total_steps:
lr = lr_at(step, total_steps, args)
for g in opt.param_groups:
g["lr"] = lr
loss_acc = 0.0
for micro in range(accum):
x, y = train.next(device)
sync = nullcontext() if (not ddp or micro == accum - 1) else fwd.no_sync()
with sync, torch.autocast("cuda", dtype=torch.bfloat16):
logits = fwd(input_ids=x).logits
loss = torch.nn.functional.cross_entropy(logits.float().flatten(0, 1), y.flatten())
(loss / accum).backward()
loss_acc += loss.item() / accum
gnorm = torch.nn.utils.clip_grad_norm_(params, args.grad_clip).item()
opt.step()
opt.zero_grad(set_to_none=True)
step += 1
if step % 10 == 0 and master:
dt = time.time() - t0
t0 = time.time()
log({"step": step, "tokens": step * args.batch_tokens, "train_loss": round(loss_acc, 4),
"lr": lr, "gnorm": round(gnorm, 3), "tok_per_s": int(10 * args.batch_tokens / dt)})
if step % args.eval_every == 0 or step == total_steps:
vl = evaluate(model, val, args, rank, world, device)
log({"step": step, "tokens": step * args.batch_tokens, "val_loss": vl})
if master and (step % args.ckpt_every == 0 or step == total_steps):
torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "step": step}, ckpt_path.with_suffix(".tmp"))
os.replace(ckpt_path.with_suffix(".tmp"), ckpt_path)
if master:
if args.final_dir:
merge_lora(model)
model.to(torch.bfloat16).save_pretrained(args.final_dir)
AutoTokenizer.from_pretrained("gpt2").save_pretrained(args.final_dir)
(out / "DONE").touch()
if ddp:
dist.destroy_process_group()
if __name__ == "__main__":
main()
|