Download code/bora_train.py from Ami1ne/bora-v1: direct link, hf CLI and curl.
- Browser
- Download file 16.7 kB
-
https://huggingface.co/Ami1ne/bora-v1/resolve/main/code/bora_train.py
- Command line
-
hf download hf://Ami1ne/bora-v1/code/bora_train.py
-
curl -L -o bora_train.py https://huggingface.co/Ami1ne/bora-v1/resolve/main/code/bora_train.py
16.7 kB
| # bora_train.py -- training loop for Bora V1 (bora_dit.BoraDiT on bora_dataset.BoraLatents). | |
| # | |
| # train(cfg, decode_fn=None) runs until cfg.max_steps, resumes from <out_dir>/ckpt_last.pt if it exists, and writes: | |
| # log.csv step, loss, lr, samples_per_s (every log_every steps) | |
| # val.csv step, val_loss, val_loss@t for a fixed t grid (every val_every steps, EMA weights, fixed noise) | |
| # ckpt_last.pt model + ema + optimizer + step (every save_every steps; ~5.4 GB for v1, keep it on local disk) | |
| # ema_step{N}.safetensors + config.json (every save_every steps; bf16 EMA weights, ~0.7 GB for v1) | |
| # samples/step{N}.safetensors (every sample_every steps: latents for cfg.sample_captions) | |
| # decode_fn(latents [B,16,10,32,32] float32 cpu, prompts list[str], step int) is called after each sample, e.g. to write an mp4. | |
| import csv | |
| import json | |
| import math | |
| import os | |
| import time | |
| from dataclasses import dataclass, asdict, field | |
| from pathlib import Path | |
| import torch | |
| from safetensors import safe_open | |
| from safetensors.torch import save_file, load_file | |
| from torch.utils.data import DataLoader | |
| from bora_dataset import BoraLatents | |
| from bora_dit import DiTConfig, BoraDiT, PRESETS, build, count_params, config_dict, flow_loss, shift_t, sample_video | |
| class TrainConfig: | |
| data_root: str = "/content/bora_data" | |
| out_dir: str = "/content/bora_runs/v1" | |
| text_embeds: str = "text_t5v11base_32.safetensors" # relative to data_root | |
| preset: str = "v1" | |
| model: dict = field(default_factory=dict) # DiTConfig overrides, e.g. {"attn": "linear"} | |
| # data | |
| batch_size: int = 32 | |
| num_workers: int = 4 | |
| block_len: int = 4 | |
| overlap: int = 1 | |
| val_mod: int = 100 | |
| primary_p: float = 0.5 | |
| # objective | |
| p_cond: float = 0.5 # fraction of samples that get the clean first frame | |
| p_uncond: float = 0.1 # fraction of samples whose text is replaced by the empty caption (for CFG) | |
| time_shift: float = 3.0 | |
| # optimisation | |
| lr: float = 1e-4 | |
| warmup: int = 1000 | |
| max_steps: int = 50000 | |
| min_lr_frac: float = 0.1 # cosine decay from lr to lr * min_lr_frac over max_steps (after warmup) | |
| weight_decay: float = 0.0 | |
| betas: tuple = (0.9, 0.95) | |
| grad_clip: float = 1.0 | |
| ema: float = 0.9999 | |
| bf16: bool = True | |
| grad_checkpoint: bool = False | |
| compile: bool = False | |
| seed: int = 0 | |
| # bookkeeping | |
| log_every: int = 50 | |
| val_every: int = 1000 | |
| val_batches: int = 8 | |
| val_t: tuple = (0.25, 0.5, 0.75, 0.9) # fixed timesteps for the validation loss (after the shift) | |
| sample_every: int = 2000 | |
| sample_steps: int = 8 | |
| sample_cfg: float = 4.0 | |
| sample_captions: tuple = () # caption strings to sample; empty -> 4 captions from the val set | |
| sample_blocks: int = 3 | |
| save_every: int = 1000 | |
| keep_ema_every: int = 10000 # keep every Nth ema_step file, delete the others | |
| overfit_batch: int = 0 # >0: train on one fixed batch of this size (pipeline test) | |
| resume: bool = True | |
| # ----------------------------------------------------------------------------- helpers | |
| def set_seed(seed): | |
| torch.manual_seed(seed) | |
| torch.cuda.manual_seed_all(seed) | |
| def lr_at(step, cfg): | |
| if step < cfg.warmup: | |
| return cfg.lr * (step + 1) / cfg.warmup | |
| p = min(1.0, (step - cfg.warmup) / max(1, cfg.max_steps - cfg.warmup)) | |
| return cfg.lr * (cfg.min_lr_frac + (1 - cfg.min_lr_frac) * 0.5 * (1 + math.cos(math.pi * p))) | |
| def load_null_text(path): | |
| with safe_open(str(path), framework="pt") as f: | |
| return f.get_slice("emb")[0:1][0].float(), f.get_slice("mask")[0:1][0].bool() # [L,D], [L] | |
| class EMA: | |
| def __init__(self, model, decay): | |
| self.decay = decay | |
| self.shadow = {k: v.detach().clone().float() for k, v in model.state_dict().items()} | |
| def update(self, model, step=None): | |
| # warmup: decay = min(decay, (1 + step) / (10 + step)), so the EMA tracks the model early instead of staying near the init | |
| d = self.decay if step is None else min(self.decay, (1 + step) / (10 + step)) | |
| for k, v in model.state_dict().items(): | |
| s = self.shadow[k] | |
| if s.dtype.is_floating_point: | |
| s.mul_(d).add_(v.detach().float(), alpha=1 - d) | |
| else: | |
| s.copy_(v) | |
| def state_dict(self): | |
| return self.shadow | |
| def load_state_dict(self, sd): | |
| self.shadow = {k: v.clone() for k, v in sd.items()} | |
| def to_model(self, model_cfg, device, dtype=torch.bfloat16): | |
| m = BoraDiT(model_cfg) | |
| m.load_state_dict({k: v for k, v in self.shadow.items()}) | |
| return m.to(device, dtype).eval() | |
| def make_model(cfg, device): | |
| model, mcfg = build(cfg.preset, time_shift=cfg.time_shift, **cfg.model) | |
| model.grad_checkpoint = cfg.grad_checkpoint | |
| model.to(device) | |
| return model, mcfg | |
| def make_data(cfg): | |
| root = Path(cfg.data_root) | |
| emb = root / cfg.text_embeds | |
| kw = dict(mode="block", block_len=cfg.block_len, overlap=cfg.overlap, val_mod=cfg.val_mod, text_embeds=emb, dtype=torch.float32) | |
| train_ds = BoraLatents(root, "train", random_block=True, primary_p=cfg.primary_p, **kw) | |
| val_ds = BoraLatents(root, "val", random_block=False, **kw) | |
| return train_ds, val_ds | |
| def infinite(loader): | |
| while True: | |
| for b in loader: | |
| yield b | |
| def to_device(batch, device): | |
| return {k: (v.to(device, non_blocking=True) if torch.is_tensor(v) else v) for k, v in batch.items()} | |
| def apply_uncond(text, mask, p, null_text, null_mask): | |
| """Replace a random fraction p of the text rows by the empty caption (classifier-free guidance training).""" | |
| if p <= 0: | |
| return text, mask | |
| drop = torch.rand(text.shape[0], device=text.device) < p | |
| text = torch.where(drop[:, None, None], null_text[None].to(text.dtype), text) | |
| mask = torch.where(drop[:, None], null_mask[None], mask) | |
| return text, mask | |
| def evaluate(model, val_dl, cfg, device, null_text, null_mask, n_batches): | |
| """Loss of `model` on the val set at fixed timesteps with fixed noise. Returns {"val_loss": .., "t=0.25": .., ...}.""" | |
| model.eval() | |
| g = torch.Generator(device=device).manual_seed(1234) | |
| sums = {t: 0.0 for t in cfg.val_t} | |
| n = 0 | |
| for i, batch in enumerate(val_dl): | |
| if i >= n_batches: | |
| break | |
| batch = to_device(batch, device) | |
| x = batch["latents"] | |
| text, mask = batch["text_emb"].float(), batch["text_mask"] | |
| noise = torch.randn(x.shape, device=device, generator=g) | |
| for t in cfg.val_t: | |
| tt = torch.full((x.shape[0],), t, device=device) | |
| with torch.autocast(device_type=device.type, dtype=torch.bfloat16, enabled=cfg.bf16 and device.type == "cuda"): | |
| loss, _ = flow_loss(model, x, text, mask, p_cond=cfg.p_cond, shift=cfg.time_shift, t=tt, noise=noise) | |
| sums[t] += loss.item() * x.shape[0] | |
| n += x.shape[0] | |
| model.train() | |
| out = {f"t={t}": sums[t] / max(1, n) for t in cfg.val_t} | |
| out["val_loss"] = sum(out.values()) / len(cfg.val_t) | |
| return out | |
| def pick_sample_captions(cfg, val_ds, train_ds): | |
| if cfg.sample_captions: | |
| caps = train_ds.captions | |
| ids = [] | |
| for c in cfg.sample_captions: | |
| assert c in caps, f"caption not in captions.json: {c!r} (sample any string by encoding it with T5 in the notebook)" | |
| ids.append(caps.index(c)) | |
| return ids | |
| step = max(1, len(val_ds) // (3 * 4)) # 4 clips spread over the val set (3 blocks per clip) | |
| return [val_ds[i * 3 * step]["caption_id"] for i in range(4)] | |
| def sample_captions(ema_model, caption_ids, emb_path, cfg, device, null_text, null_mask, seed=0): | |
| with safe_open(str(emb_path), framework="pt") as f: | |
| text = torch.stack([f.get_slice("emb")[i:i + 1][0] for i in caption_ids]).float().to(device) | |
| mask = torch.stack([f.get_slice("mask")[i:i + 1][0] for i in caption_ids]).bool().to(device) | |
| B = len(caption_ids) | |
| g = torch.Generator(device=device).manual_seed(seed) | |
| return sample_video(ema_model, text, mask, null_text[None].expand(B, -1, -1).to(device), null_mask[None].expand(B, -1).to(device), | |
| n_blocks=cfg.sample_blocks, overlap=cfg.overlap, steps=cfg.sample_steps, cfg_scale=cfg.sample_cfg, | |
| shift=cfg.time_shift, generator=g).float().cpu() | |
| def save_checkpoint(out, model, ema, opt, step, cfg, mcfg): | |
| tmp = out / "ckpt_last.pt.tmp" | |
| torch.save({"model": model.state_dict(), "ema": ema.state_dict(), "opt": opt.state_dict(), "step": step, | |
| "cfg": asdict(cfg), "model_cfg": config_dict(mcfg)}, tmp) | |
| os.replace(tmp, out / "ckpt_last.pt") | |
| ema_sd = {k: v.to(torch.bfloat16) for k, v in ema.state_dict().items() if v.dtype.is_floating_point} | |
| ema_sd.update({k: v for k, v in ema.state_dict().items() if not v.dtype.is_floating_point}) | |
| save_file(ema_sd, str(out / f"ema_step{step}.safetensors"), metadata={"step": str(step), "model_cfg": json.dumps(config_dict(mcfg))}) | |
| (out / "config.json").write_text(json.dumps({"train": asdict(cfg), "model": config_dict(mcfg), "step": step}, indent=1)) | |
| for p in sorted(out.glob("ema_step*.safetensors")): # prune | |
| s = int(p.stem[len("ema_step"):]) | |
| if s != step and s % cfg.keep_ema_every != 0: | |
| p.unlink() | |
| def load_ema_model(path, device, dtype=torch.bfloat16): | |
| """ema_stepN.safetensors -> BoraDiT in eval mode.""" | |
| with safe_open(str(path), framework="pt") as f: | |
| mcfg = json.loads(f.metadata()["model_cfg"]) | |
| mcfg["patch"] = tuple(mcfg["patch"]) | |
| m = BoraDiT(DiTConfig(**mcfg)) | |
| m.load_state_dict(load_file(str(path))) | |
| return m.to(device, dtype).eval() | |
| # ----------------------------------------------------------------------------- the loop | |
| def train(cfg: TrainConfig, decode_fn=None, log=print): | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| set_seed(cfg.seed) | |
| out = Path(cfg.out_dir) | |
| out.mkdir(parents=True, exist_ok=True) | |
| (out / "samples").mkdir(exist_ok=True) | |
| torch.backends.cuda.matmul.allow_tf32 = True | |
| torch.backends.cudnn.allow_tf32 = True | |
| # data | |
| train_ds, val_ds = make_data(cfg) | |
| emb_path = Path(cfg.data_root) / cfg.text_embeds | |
| null_text, null_mask = load_null_text(emb_path) | |
| null_text, null_mask = null_text.to(device), null_mask.to(device) | |
| pin = device.type == "cuda" | |
| if cfg.overfit_batch: | |
| fixed = next(iter(DataLoader(train_ds, batch_size=cfg.overfit_batch, shuffle=True, num_workers=0))) | |
| fixed["text_emb"] = fixed["text_emb"].float() | |
| train_it = iter(lambda: fixed, None) | |
| save_file({"latents": fixed["latents"].contiguous(), "global_idx": fixed["global_idx"], "start": fixed["start"]}, | |
| str(out / "overfit_batch.safetensors"), metadata={"prompts": json.dumps(fixed["text"])}) | |
| log(f"overfit mode: one fixed batch of {cfg.overfit_batch} | captions: {[t[:40] for t in fixed['text']]}") | |
| else: | |
| train_dl = DataLoader(train_ds, batch_size=cfg.batch_size, shuffle=True, num_workers=cfg.num_workers, drop_last=True, | |
| pin_memory=pin, persistent_workers=cfg.num_workers > 0, prefetch_factor=4 if cfg.num_workers > 0 else None) | |
| train_it = infinite(train_dl) | |
| val_dl = DataLoader(val_ds, batch_size=cfg.batch_size, shuffle=False, num_workers=min(2, cfg.num_workers), pin_memory=pin) | |
| log(f"data: train {len(train_ds)} clips / {train_ds.n_videos} videos | val {len(val_ds)} blocks / {val_ds.n_videos} videos | captions {len(train_ds.captions) - 1}") | |
| # model | |
| model, mcfg = make_model(cfg, device) | |
| log(f"model {cfg.preset}: {count_params(model) / 1e6:.1f}M params | dim {mcfg.dim} depth {mcfg.depth} heads {mcfg.heads} attn {mcfg.attn} | tokens {mcfg.tokens}") | |
| opt = torch.optim.AdamW(model.parameters(), lr=cfg.lr, betas=cfg.betas, weight_decay=cfg.weight_decay, fused=device.type == "cuda") | |
| ema = EMA(model, cfg.ema) | |
| step = 0 | |
| ck = out / "ckpt_last.pt" | |
| if cfg.resume and ck.exists(): | |
| sd = torch.load(ck, map_location=device, weights_only=False) | |
| model.load_state_dict(sd["model"]); ema.load_state_dict(sd["ema"]); opt.load_state_dict(sd["opt"]); step = sd["step"] | |
| log(f"resumed from {ck} at step {step}") | |
| fwd = torch.compile(model) if cfg.compile else model | |
| sample_ids = pick_sample_captions(cfg, val_ds, train_ds) if not cfg.overfit_batch else [int(i) for i in fixed["caption_id"][:4]] | |
| sample_prompts = [train_ds.captions[i] for i in sample_ids] | |
| log(f"sample prompts: {[p[:50] for p in sample_prompts]}") | |
| def write_row(name, row): | |
| p = out / name | |
| new = not p.exists() | |
| with open(p, "a", newline="") as f: | |
| w = csv.DictWriter(f, fieldnames=list(row.keys())) | |
| if new: | |
| w.writeheader() | |
| w.writerow(row) | |
| def do_sample(step): | |
| ema_model = ema.to_model(mcfg, device) | |
| lat = sample_captions(ema_model, sample_ids, emb_path, cfg, device, null_text, null_mask) | |
| save_file({"latents": lat}, str(out / "samples" / f"step{step}.safetensors"), metadata={"prompts": json.dumps(sample_prompts)}) | |
| del ema_model | |
| if decode_fn is not None: | |
| decode_fn(lat, sample_prompts, step) | |
| torch.cuda.empty_cache() if device.type == "cuda" else None | |
| # loop | |
| model.train() | |
| t0, n_since, loss_acc, loss_n = time.time(), 0, 0.0, 0 | |
| log(f"training from step {step} to {cfg.max_steps} | batch {cfg.overfit_batch or cfg.batch_size} | bf16 {cfg.bf16} | grad_checkpoint {cfg.grad_checkpoint} | device {device}") | |
| while step < cfg.max_steps: | |
| batch = to_device(next(train_it), device) | |
| x = batch["latents"] | |
| text, mask = apply_uncond(batch["text_emb"].float(), batch["text_mask"], cfg.p_uncond, null_text, null_mask) | |
| for g in opt.param_groups: | |
| g["lr"] = lr_at(step, cfg) | |
| with torch.autocast(device_type=device.type, dtype=torch.bfloat16, enabled=cfg.bf16 and device.type == "cuda"): | |
| loss, parts = flow_loss(fwd, x, text, mask, p_cond=cfg.p_cond, shift=cfg.time_shift) | |
| opt.zero_grad(set_to_none=True) | |
| loss.backward() | |
| gn = torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.grad_clip) if cfg.grad_clip else torch.tensor(0.0) | |
| if not torch.isfinite(loss): | |
| log(f"step {step}: non-finite loss, batch skipped") | |
| continue | |
| opt.step() | |
| ema.update(model, step) | |
| step += 1 | |
| loss_acc += loss.item(); loss_n += 1; n_since += x.shape[0] | |
| if step % cfg.log_every == 0: | |
| dt = time.time() - t0 | |
| row = {"step": step, "loss": loss_acc / loss_n, "lr": lr_at(step - 1, cfg), "grad_norm": float(gn), "samples_per_s": n_since / dt, "t_mean": parts["t_mean"]} | |
| write_row("log.csv", row) | |
| log(f"step {step} | loss {row['loss']:.4f} | lr {row['lr']:.2e} | gn {row['grad_norm']:.2f} | {row['samples_per_s']:.1f} samples/s") | |
| t0, n_since, loss_acc, loss_n = time.time(), 0, 0.0, 0 | |
| if cfg.val_every and step % cfg.val_every == 0 and not cfg.overfit_batch: | |
| ema_model = ema.to_model(mcfg, device, dtype=torch.float32) | |
| v = evaluate(ema_model, val_dl, cfg, device, null_text, null_mask, cfg.val_batches) | |
| del ema_model | |
| write_row("val.csv", {"step": step, **v}) | |
| log(f"step {step} | val {v['val_loss']:.4f} | " + " ".join(f"{k} {val:.3f}" for k, val in v.items() if k != "val_loss")) | |
| if cfg.sample_every and step % cfg.sample_every == 0: | |
| do_sample(step) | |
| if cfg.save_every and step % cfg.save_every == 0: | |
| save_checkpoint(out, model, ema, opt, step, cfg, mcfg) | |
| log(f"step {step} | saved ckpt_last.pt and ema_step{step}.safetensors") | |
| if not (cfg.save_every and step % cfg.save_every == 0): | |
| save_checkpoint(out, model, ema, opt, step, cfg, mcfg) | |
| if cfg.sample_every and step % cfg.sample_every != 0: | |
| do_sample(step) | |
| log(f"done at step {step}") | |
| return out | |