"""Offline prebuild of the whole-clip VAE latent cache. Training encodes the first latents of a window fresh and slices the tail from this cache. Usage (single or multi-GPU sharding, resumable): PYTHONPATH=. python scripts/tools/precache_vae_latents.py \ --config configs/stage1_pretrain_bidir.yaml [--rank 0 --world 2] [--device cuda:0] [--limit N] Pixel processing mirrors the dataset: uniform_sample on the 24fps grid -> ToTensor -> bicubic(antialias) resize (H,W) → [-1,1] → StreamingVAEEncoder(chunk 33)。 """ from __future__ import annotations import argparse import os import sys import time import numpy as np import torch sys.path.insert(0, os.getcwd()) from alaya.config.loader import load_config from alaya.data.vae_latent_cache import entry_paths, save_entry from alaya.model.loader import load_vae from fastvideo.ltx2_streaming_vae import StreamingVAEEncoder def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--config", required=True) ap.add_argument("--rank", type=int, default=int(os.environ.get("RANK", 0))) ap.add_argument("--world", type=int, default=int(os.environ.get("WORLD_SIZE", 1))) ap.add_argument("--device", default=None) ap.add_argument("--limit", type=int, default=None, help="only process the first N videos (smoke test)") ap.add_argument("--batch_chunk", type=int, default=33) args = ap.parse_args() cfg = load_config(args.config) cache_dir = cfg.runtime.vae_latent_cache_dir assert cache_dir, "runtime.vae_latent_cache_dir must be set in the config" dev = torch.device(args.device or f"cuda:{args.rank % max(1, torch.cuda.device_count())}") dtype = torch.bfloat16 H, W = int(cfg.sample.height), int(cfg.sample.width) fps = float(cfg.sample.fps) stride = int(cfg.sample.temporal_stride) # Enumerate unique videos from the training dataset so source/video_id match training from alaya.data.dataloader import build_train_dataloader from alaya.utils.distributed import DistributedState dist_state = DistributedState(rank=0, local_rank=0, world_size=1, device=torch.device("cpu")) loader = build_train_dataloader(cfg, dist_state) ds = loader.dataset subsets = [ds] if hasattr(ds, "samples") else list(getattr(ds, "datasets", [])) videos: dict[tuple[str, str], str] = {} for sub in subsets: for video_path, _cap, _pose, source, video_id in sub.samples: videos.setdefault((str(source), str(video_id)), str(video_path)) items = sorted(videos.items()) if args.limit: items = items[: args.limit] missing = [ (k, v) for k, v in items if not os.path.exists(entry_paths(cache_dir, k[0], k[1], H, W, fps)[0]) ] shard = missing[args.rank :: max(1, args.world)] print( f"[VAECache] unique videos {len(items)}, already cached {len(items) - len(missing)}, " f"this rank ({args.rank}/{args.world}) will encode {len(shard)} (dir={cache_dir})", flush=True, ) if not shard: return vae_enc_raw, _dec = load_vae(cfg.paths.vae, device=dev, dtype=dtype) enc = StreamingVAEEncoder(vae_enc_raw, device=dev, dtype=dtype) import decord # noqa: E402 - imported after torch to avoid a thread conflict t0 = time.time() done = 0 total = len(shard) report_every = max(1, total // 100) for (source, video_id), video_path in shard: try: vr = decord.VideoReader(video_path, num_threads=2) video_fps = float(vr.get_avg_fps()) # raw value, matching the dataset fps ratio (no rounding) ratio = video_fps / fps total_src = len(vr) # Largest 24fps grid length n with round((n-1)*ratio) inside the clip and (n-1)%stride==0 n = int((total_src - 1) / ratio) + 1 n = ((n - 1) // stride) * stride + 1 if n < stride + 1: done += 1 continue ids = [min(int(j * ratio), total_src - 1) for j in range(n)] frames = vr.get_batch(ids).asnumpy() # [T,H0,W0,C] uint8 px = torch.from_numpy(frames).float().div_(255.0).permute(0, 3, 1, 2) # [T,C,H0,W0] px = torch.nn.functional.interpolate( px, size=(H, W), mode="bicubic", align_corners=False, antialias=True ) px = px.sub_(0.5).div_(0.5).permute(1, 0, 2, 3).to(dev, dtype) # [C,T,H,W] with torch.no_grad(): latent = enc.encode(px.unsqueeze(0), chunk_size=args.batch_chunk, verbose=False)[0] save_entry(cache_dir, source, video_id, H, W, fps, latent, ratio) del px, latent, frames except Exception as e: # one failed video must not abort the shard print(f"[VAECache] SKIP {source}/{video_id}: {type(e).__name__} {e}", flush=True) done += 1 if done % report_every == 0 or done == total: el = time.time() - t0 ips = done / max(el, 1e-6) eta = (total - done) / max(ips, 1e-6) bar_n = int(30 * done / total) print( f"[VAECache] rank{args.rank} |{'█' * bar_n}{'.' * (30 - bar_n)}| " f"{done}/{total} ({100 * done / total:.1f}%) {ips:.2f} vid/s ETA {eta / 3600:.2f}h", flush=True, ) print(f"[VAECache] rank{args.rank} done in {(time.time() - t0) / 60:.1f}min", flush=True) if __name__ == "__main__": main()