bora-v1 / code /bora_run.py
Ami1ne's picture
Bora V1 final: ema_step50000
dd1362a verified
Raw History Blame Contribute Delete
12.7 kB
# bora_run.py -- command-line runner for a rented GPU box (same steps as bora_train.ipynb, no Jupyter needed).
#
# python bora_run.py download --data /ephemeral/bora_data
# python bora_run.py overfit --data /ephemeral/bora_data --runs /ephemeral/bora_runs
# python bora_run.py train --data /ephemeral/bora_data --runs /ephemeral/bora_runs --batch 32 --steps 50000
# python bora_run.py sample --data ... --runs ... --prompts "ocean waves crashing" "rocket launch"
# python bora_run.py push --runs ... --repo Ami1ne/bora-v1 (token from HF_TOKEN or ~/.cache/huggingface/token)
#
# Run train inside tmux so it survives disconnects. It resumes from <runs>/<name>/ckpt_last.pt.
import argparse
import json
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
os.environ.setdefault("HF_HUB_DOWNLOAD_TIMEOUT", "60")
os.environ.setdefault("HF_HUB_ETAG_TIMEOUT", "60")
sys.path.insert(0, str(Path(__file__).resolve().parent))
REPO_ID, REPO_TYPE = "Ami1ne/Bora", "dataset"
LAT_DIR, LAT_META_DIR, LAT_MERGED = "latents_256", "latents_256_manifests", "latents_256_manifest.csv"
LABELS_DIR = "text_t5v11base_32"
LABEL_FILES = ["latents_256_manifest_labeled.csv", "captions.json", "text_t5v11base_32.safetensors"]
VAE_REPO, VAE_SUBFOLDER = "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", "vae"
SAMPLE_CAPTIONS = ("ocean waves crashing", "fog rolling through valley", "motorcycle overtaking car", "rocket launch")
def log_to(path):
def log(msg):
line = f"[{time.strftime('%m-%d %H:%M:%S')}] {msg}"
print(line, flush=True)
with open(path, "a") as f:
f.write(line + "\n")
return log
def with_retries(fn, what, log=print, tries=6, base=15, cap=600):
for i in range(1, tries + 1):
try:
return fn()
except KeyboardInterrupt:
raise
except Exception as e:
st = getattr(getattr(e, "response", None), "status_code", None)
if st in (401, 403, 404) or i == tries:
raise
wait = min(cap, base * 2 ** (i - 1)) if st != 429 else max(300, base * 2 ** (i - 1))
log(f" retry {i}/{tries - 1} for {what}: {type(e).__name__}: {str(e)[:160]} -> sleep {wait}s")
time.sleep(wait)
# ----------------------------------------------------------------------------- download
def cmd_download(a):
from huggingface_hub import HfApi, snapshot_download, hf_hub_download
from huggingface_hub.hf_api import RepoFile
data = Path(a.data); data.mkdir(parents=True, exist_ok=True)
log = log_to(data / "download.log")
api = HfApi()
remote = {}
for folder in (LAT_DIR, LABELS_DIR):
remote.update({it.path: it.size for it in with_retries(lambda: list(api.list_repo_tree(REPO_ID, path_in_repo=folder, repo_type=REPO_TYPE)), f"list {folder}", log)
if isinstance(it, RepoFile)})
remote = {p: s for p, s in remote.items() if p.endswith((".safetensors", ".csv", ".json"))}
missing = lambda: [p for p, s in remote.items() if not (data / p).exists() or (data / p).stat().st_size != s]
GB = 1024 ** 3
todo = missing()
log(f"{len(remote)} files on the Hub, {sum(remote.values()) / GB:.1f} GB | to download: {len(todo)} files, {sum(remote[p] for p in todo) / GB:.1f} GB")
t0 = time.time()
if todo:
with_retries(lambda: snapshot_download(REPO_ID, repo_type=REPO_TYPE, local_dir=str(data), max_workers=16,
allow_patterns=[f"{LAT_DIR}/*", f"{LAT_META_DIR}/*", LAT_MERGED, f"{LABELS_DIR}/*"]),
"snapshot_download", log, tries=5, base=20)
for p in missing():
log(f"size mismatch or missing: {p} -> downloading again")
with_retries(lambda: hf_hub_download(REPO_ID, p, repo_type=REPO_TYPE, local_dir=str(data), force_download=True), f"download {p}", log, tries=4, base=20)
assert not missing(), f"still wrong: {missing()[:5]}"
for f in LABEL_FILES:
shutil.copy(data / LABELS_DIR / f, data / f)
log(f"ok in {time.time() - t0:.0f}s: {sum(1 for p in remote if p.startswith(LAT_DIR))} shards + label files in {data}")
# ----------------------------------------------------------------------------- VAE decode -> mp4
class Decoder:
def __init__(self, data):
import torch
from safetensors import safe_open
with safe_open(str(next((Path(data) / LAT_DIR).glob("shard_*.safetensors"))), framework="pt") as f:
meta = f.metadata()
self.mean = torch.tensor(json.loads(meta["latents_mean"])).view(1, -1, 1, 1, 1)
self.std = torch.tensor(json.loads(meta["latents_std"])).view(1, -1, 1, 1, 1)
self.vae = None
def decode(self, lat):
import numpy as np
import torch
if self.vae is None:
from diffusers import AutoencoderKLWan
self.vae = AutoencoderKLWan.from_pretrained(VAE_REPO, subfolder=VAE_SUBFOLDER, torch_dtype=torch.float32).eval()
vae = self.vae.to("cuda")
out = []
with torch.inference_mode():
for z in lat:
x = vae.decode(z[None].float().cuda() * self.std.cuda() + self.mean.cuda()).sample.float()
out.append(((x[0] + 1) * 127.5).round().clamp(0, 255).to(torch.uint8).permute(1, 2, 3, 0).cpu().numpy())
vae.to("cpu"); torch.cuda.empty_cache()
return np.stack(out)
@staticmethod
def write_mp4(frames, path, fps=8):
import numpy as np
both = np.concatenate(list(frames), axis=2)
h, w = both.shape[1:3]
subprocess.run(["ffmpeg", "-nostdin", "-v", "error", "-y", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", str(fps), "-i", "-",
"-c:v", "libx264", "-crf", "16", "-pix_fmt", "yuv420p", str(path)], input=both.tobytes(), check=True, timeout=300)
return path
def decode_fn(self, out_dir, log=print):
def fn(lat, prompts, step):
try:
p = self.write_mp4(self.decode(lat), Path(out_dir) / "samples" / f"step{step}.mp4")
log(f" samples at step {step}: {p.name} | {[x[:40] for x in prompts]}")
except Exception as e: # a failed preview must never kill the run
log(f" sample decode failed at step {step}: {type(e).__name__}: {e}")
return fn
# ----------------------------------------------------------------------------- overfit / train
def cmd_overfit(a):
import csv
from safetensors.torch import load_file
from bora_train import TrainConfig, train
out = Path(a.runs) / "overfit"
shutil.rmtree(out, ignore_errors=True); out.mkdir(parents=True)
log = log_to(out / "train.log")
dec = Decoder(a.data)
cfg = TrainConfig(data_root=a.data, out_dir=str(out), preset=a.preset, model=json.loads(a.model), overfit_batch=a.batch, max_steps=a.steps,
warmup=50, lr=3e-4, min_lr_frac=1.0, p_uncond=0.0, p_cond=0.5, num_workers=0, log_every=25, val_every=0,
sample_every=a.steps, sample_steps=8, sample_cfg=1.0, save_every=0, ema=0.99, compile=a.compile)
train(cfg, dec.decode_fn(out, log), log=log)
truth = load_file(str(out / "overfit_batch.safetensors"))["latents"][:4]
dec.write_mp4(dec.decode(truth), out / "samples" / "truth.mp4")
rows = list(csv.DictReader(open(out / "log.csv")))
first, last = float(rows[0]["loss"]), float(rows[-1]["loss"])
log(f"OVERFIT RESULT: loss {first:.3f} at step {rows[0]['step']} -> {last:.3f} at step {rows[-1]['step']} | "
f"{float(rows[-1]['samples_per_s']):.1f} samples/s | videos: {out / 'samples'}/step{a.steps}.mp4 and truth.mp4")
def cmd_train(a):
from bora_train import TrainConfig, train
out = Path(a.runs) / a.name
out.mkdir(parents=True, exist_ok=True)
log = log_to(out / "train.log")
dec = Decoder(a.data)
cfg = TrainConfig(data_root=a.data, out_dir=str(out), preset=a.preset, model=json.loads(a.model), batch_size=a.batch, max_steps=a.steps,
lr=a.lr, warmup=a.warmup, num_workers=a.workers, grad_checkpoint=a.grad_checkpoint, compile=a.compile,
log_every=50, val_every=1000, sample_every=2000, save_every=1000, sample_captions=SAMPLE_CAPTIONS)
train(cfg, dec.decode_fn(out, log), log=log)
# ----------------------------------------------------------------------------- sample / push
def cmd_sample(a):
import torch
from transformers import AutoTokenizer, T5EncoderModel
from bora_train import load_ema_model, load_null_text
from bora_dit import sample_video
out = Path(a.runs) / a.name
ema = Path(a.ema) if a.ema else sorted(out.glob("ema_step*.safetensors"), key=lambda p: int(p.stem[8:]))[-1]
model = load_ema_model(ema, "cuda")
tok = AutoTokenizer.from_pretrained("google/t5-v1_1-base")
enc = T5EncoderModel.from_pretrained("google/t5-v1_1-base", torch_dtype=torch.float32).cuda().eval()
with torch.inference_mode():
t = tok(a.prompts, max_length=32, padding="max_length", truncation=True, return_tensors="pt").to("cuda")
text = enc(input_ids=t.input_ids, attention_mask=t.attention_mask).last_hidden_state * t.attention_mask[..., None]
del enc; torch.cuda.empty_cache()
nt, nm = load_null_text(Path(a.data) / "text_t5v11base_32.safetensors")
B = len(a.prompts)
g = torch.Generator(device="cuda").manual_seed(a.seed)
lat = sample_video(model, text, t.attention_mask.bool(), nt[None].expand(B, -1, -1).cuda(), nm[None].expand(B, -1).cuda(),
n_blocks=3, steps=a.sample_steps, cfg_scale=a.cfg, shift=model.cfg.time_shift, generator=g).float().cpu()
dec = Decoder(a.data)
p = dec.write_mp4(dec.decode(lat), out / "samples" / f"prompts_{ema.stem}_s{a.seed}.mp4")
print(p)
def cmd_push(a):
from huggingface_hub import HfApi, CommitOperationAdd
out = Path(a.runs) / a.name
api = HfApi()
print("logged in as", api.whoami()["name"])
api.create_repo(a.repo, repo_type="model", exist_ok=True)
ema = sorted(out.glob("ema_step*.safetensors"), key=lambda p: int(p.stem[8:]))[-1]
here = Path(__file__).resolve().parent
files = {ema.name: ema, "ema_latest.safetensors": ema, "config.json": out / "config.json", "log.csv": out / "log.csv",
"val.csv": out / "val.csv", "train.log": out / "train.log"}
for n in ("bora_dit.py", "bora_dataset.py", "bora_train.py", "bora_run.py"):
files[n] = here / n
for p in sorted((out / "samples").glob("*.mp4"), key=lambda p: p.stat().st_mtime)[-4:]:
files[f"samples/{p.name}"] = p
ops = [CommitOperationAdd(path_in_repo=k, path_or_fileobj=str(v)) for k, v in files.items() if v.exists()]
api.create_commit(a.repo, repo_type="model", operations=ops, commit_message=f"{a.name}: {ema.stem}")
print(f"ok: https://huggingface.co/{a.repo}")
def main():
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
def common(p, runs=True):
p.add_argument("--data", default="/ephemeral/bora_data")
if runs:
p.add_argument("--runs", default="/ephemeral/bora_runs")
p.add_argument("--name", default="v1")
p = sub.add_parser("download"); common(p, runs=False)
p = sub.add_parser("overfit"); common(p)
p.add_argument("--preset", default="v1"); p.add_argument("--model", default="{}")
p.add_argument("--batch", type=int, default=8); p.add_argument("--steps", type=int, default=600); p.add_argument("--compile", action="store_true")
p = sub.add_parser("train"); common(p)
p.add_argument("--preset", default="v1"); p.add_argument("--model", default="{}")
p.add_argument("--batch", type=int, default=32); p.add_argument("--steps", type=int, default=50000)
p.add_argument("--lr", type=float, default=1e-4); p.add_argument("--warmup", type=int, default=1000)
p.add_argument("--workers", type=int, default=8); p.add_argument("--grad-checkpoint", action="store_true"); p.add_argument("--compile", action="store_true")
p = sub.add_parser("sample"); common(p)
p.add_argument("--prompts", nargs="+", required=True); p.add_argument("--ema", default=None)
p.add_argument("--sample-steps", type=int, default=8); p.add_argument("--cfg", type=float, default=4.0); p.add_argument("--seed", type=int, default=0)
p = sub.add_parser("push"); common(p); p.add_argument("--repo", default="Ami1ne/bora-v1")
a = ap.parse_args()
{"download": cmd_download, "overfit": cmd_overfit, "train": cmd_train, "sample": cmd_sample, "push": cmd_push}[a.cmd](a)
if __name__ == "__main__":
main()