File size: 6,888 Bytes
af8c481 | 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 | """Train FACT from scratch on every valid K-block anchor of one episode."""
from __future__ import annotations
import argparse
import json
import math
import random
import time
from pathlib import Path
import numpy as np
import torch
from torch.utils.data import DataLoader
from fact import (
FactAnchorDataset,
FactConfig,
FactEpisode,
FactPolicy,
collate,
count_parameters,
)
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser()
p.add_argument("--dataset-root", required=True)
p.add_argument("--episode", type=int, default=0)
p.add_argument("--cache", required=True)
p.add_argument("--out", required=True)
p.add_argument("--steps", type=int, default=3000)
p.add_argument("--batch", type=int, default=8)
p.add_argument("--blocks", type=int, default=4)
p.add_argument("--lr", type=float, default=2e-4)
p.add_argument("--min-lr-ratio", type=float, default=0.1)
p.add_argument("--weight-decay", type=float, default=0.01)
p.add_argument("--warmup", type=int, default=150)
p.add_argument("--clip", type=float, default=1.0)
p.add_argument("--seed", type=int, default=42)
p.add_argument("--workers", type=int, default=4)
p.add_argument("--log-every", type=int, default=25)
p.add_argument("--save-every", type=int, default=1000)
p.add_argument("--bf16", action="store_true")
p.add_argument("--config-json", default=None, help="JSON overrides for FactConfig")
# common config shortcuts
p.add_argument("--dim", type=int, default=None)
p.add_argument("--layers", type=int, default=None)
p.add_argument("--heads", type=int, default=None)
p.add_argument("--ffn", type=int, default=None)
p.add_argument("--no-future-state", action="store_true")
p.add_argument("--no-force", action="store_true")
p.add_argument("--train-fraction", type=float, default=None,
help="use only anchors whose K-block span ends before this fraction of the episode")
return p.parse_args()
def build_config(args: argparse.Namespace) -> FactConfig:
cfg = FactConfig()
if args.config_json:
for key, value in json.loads(args.config_json).items():
if not hasattr(cfg, key):
raise KeyError(key)
setattr(cfg, key, value)
if args.dim:
cfg.dim = args.dim
if args.layers:
cfg.num_layers = args.layers
if args.heads:
cfg.num_heads = args.heads
if args.ffn:
cfg.ffn_dim = args.ffn
if args.no_future_state:
cfg.predict_future_state = False
if args.no_force:
cfg.use_force = False
return cfg
def lr_at(step: int, args: argparse.Namespace) -> float:
if step < args.warmup:
return args.lr * (step + 1) / args.warmup
progress = (step - args.warmup) / max(1, args.steps - args.warmup)
cosine = 0.5 * (1 + math.cos(math.pi * min(1.0, progress)))
return args.lr * (args.min_lr_ratio + (1 - args.min_lr_ratio) * cosine)
def main() -> None:
args = parse_args()
torch.manual_seed(args.seed)
np.random.seed(args.seed)
random.seed(args.seed)
device = torch.device("cuda")
out = Path(args.out)
out.mkdir(parents=True, exist_ok=True)
cfg = build_config(args)
model = FactPolicy(cfg).to(device)
n_total = count_parameters(model)
n_coarse = count_parameters(model.coarse)
n_force = count_parameters(model.force) if model.force is not None else 0
print(f"params: total={n_total/1e6:.2f}M coarse={n_coarse/1e6:.2f}M force={n_force/1e6:.2f}M", flush=True)
(out / "fact_config.json").write_text(cfg.to_json())
(out / "train_args.json").write_text(json.dumps(vars(args), indent=1))
episode = FactEpisode(args.dataset_root, args.episode, latent_cache=args.cache)
dataset = FactAnchorDataset(episode, blocks=args.blocks, train_fraction=args.train_fraction)
print(f"episode {args.episode}: {episode.length} frames, {len(dataset)} anchors (K={args.blocks})", flush=True)
generator = torch.Generator().manual_seed(args.seed)
loader = DataLoader(
dataset,
batch_size=args.batch,
shuffle=True,
drop_last=True,
num_workers=args.workers,
collate_fn=collate,
generator=generator,
persistent_workers=args.workers > 0,
)
decay, no_decay = [], []
for name, param in model.named_parameters():
if not param.requires_grad:
continue
(no_decay if param.ndim < 2 or "embedding" in name or "modulation" in name else decay).append(param)
optimizer = torch.optim.AdamW(
[{"params": decay, "weight_decay": args.weight_decay}, {"params": no_decay, "weight_decay": 0.0}],
lr=args.lr,
betas=(0.9, 0.95),
)
log_path = out / "loss_log.jsonl"
log_file = log_path.open("w")
model.train()
step = 0
start = time.time()
running: dict[str, float] = {}
while step < args.steps:
for batch in loader:
if step >= args.steps:
break
lr = lr_at(step, args)
for group in optimizer.param_groups:
group["lr"] = lr
with torch.autocast("cuda", dtype=torch.bfloat16, enabled=args.bf16):
losses = model.forward_core(batch)
loss = losses["loss"]
optimizer.zero_grad(set_to_none=True)
loss.backward()
grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), args.clip)
optimizer.step()
step += 1
record = {k: float(v) for k, v in losses.items()}
record.update({"step": step, "lr": lr, "grad_norm": float(grad_norm)})
log_file.write(json.dumps(record) + "\n")
for key, value in record.items():
running[key] = running.get(key, 0.0) + value
if step % args.log_every == 0:
avg = {k: v / args.log_every for k, v in running.items()}
running = {}
elapsed = time.time() - start
print(
f"step {step}/{args.steps} loss {avg['loss']:.4f} act {avg['action_loss']:.4f} "
f"state {avg['state_loss']:.4f} force {avg['force_loss']:.4f} vq {avg['vq_loss']:.4f} "
f"gn {avg['grad_norm']:.2f} lr {lr:.2e} {elapsed/step:.2f}s/it",
flush=True,
)
log_file.flush()
if step % args.save_every == 0 or step == args.steps:
model.save(out / f"checkpoint-{step}", extra={"step": step, "params_total": n_total})
log_file.close()
model.save(out / "final", extra={"step": step, "params_total": n_total, "params_coarse": n_coarse, "params_force": n_force})
print(f"done in {(time.time() - start)/60:.1f} min; saved {out/'final'}", flush=True)
if __name__ == "__main__":
main()
|