Download code/finetune_tiny_vae.py from Patil/tiny-flux-klien-3-vae: direct link, hf CLI and curl.
- Browser
- Download file 10 kB
-
https://huggingface.co/Patil/tiny-flux-klien-3-vae/resolve/main/code/finetune_tiny_vae.py
- Command line
-
hf download hf://Patil/tiny-flux-klien-3-vae/code/finetune_tiny_vae.py
-
curl -L -o finetune_tiny_vae.py https://huggingface.co/Patil/tiny-flux-klien-3-vae/resolve/main/code/finetune_tiny_vae.py
10 kB
| """Fine-tune TAEF2 into a better tiny VAE for FLUX 3 Klein (same VAE as FLUX.2). | |
| Teacher: the full FLUX 3 Klein / FLUX.2 VAE encoder makes the target latents (in the | |
| BN-normalized space the transformer uses). Student: TAEF2's decoder learns to turn those | |
| latents back into the real image. Optionally the student encoder is trained to match the | |
| teacher latents too. | |
| python finetune_tiny_vae.py --data data/div2k --steps 3000 --batch 16 --res 512 | |
| """ | |
| import argparse, copy, glob, math, os, random, time | |
| import lpips | |
| import safetensors.torch as stt | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from PIL import Image | |
| from torch.utils.data import DataLoader, Dataset | |
| from torchvision.transforms import functional as TF | |
| from diffusers import AutoencoderKLFlux2 | |
| from eval_tiny_vae import LatentNorm, load_images, load_tiny, psnr | |
| class ImageFolder(Dataset): | |
| def __init__(self, roots, res): | |
| exts = (".png", ".jpg", ".jpeg", ".webp") | |
| self.files = [p for root in roots for p in glob.glob(os.path.join(root, "**", "*"), recursive=True) | |
| if p.lower().endswith(exts)] | |
| assert self.files, f"no images under {roots}" | |
| print(f"{len(self.files)} training images", flush=True) | |
| self.res = res | |
| def __len__(self): | |
| return len(self.files) * 50 # many random crops per high-res image | |
| def __getitem__(self, i): | |
| try: | |
| im = Image.open(self.files[i % len(self.files)]).convert("RGB") | |
| except Exception: # skip broken downloads | |
| return self[random.randrange(len(self.files))] | |
| # random downscale so the model sees both fine detail and whole-object structure | |
| short = min(im.size) | |
| target = random.randint(self.res, max(self.res, min(short, self.res * 3))) | |
| if target != short: | |
| im = TF.resize(im, target, antialias=True) | |
| w, h = im.size | |
| x0, y0 = random.randint(0, w - self.res), random.randint(0, h - self.res) | |
| im = im.crop((x0, y0, x0 + self.res, y0 + self.res)) | |
| if random.random() < 0.5: | |
| im = TF.hflip(im) | |
| return TF.to_tensor(im) | |
| class PatchDisc(nn.Module): | |
| """Small PatchGAN discriminator for optional adversarial sharpening.""" | |
| def __init__(self, ch=64): | |
| super().__init__() | |
| sn = nn.utils.spectral_norm | |
| self.net = nn.Sequential( | |
| sn(nn.Conv2d(3, ch, 4, 2, 1)), nn.LeakyReLU(0.2), | |
| sn(nn.Conv2d(ch, ch * 2, 4, 2, 1)), nn.LeakyReLU(0.2), | |
| sn(nn.Conv2d(ch * 2, ch * 4, 4, 2, 1)), nn.LeakyReLU(0.2), | |
| sn(nn.Conv2d(ch * 4, 1, 3, 1, 1)), | |
| ) | |
| def forward(self, x): | |
| return self.net(x * 2 - 1) | |
| def evaluate(student, vae, ln, lp, imgs, dev): | |
| student.eval() | |
| ps, ls = [], [] | |
| for x in imgs: | |
| x = x[None].to(dev) | |
| zn = ln.to_norm(vae.encode((x * 2 - 1).to(torch.bfloat16)).latent_dist.mode().float()) | |
| with torch.autocast("cuda", torch.bfloat16): | |
| d = student.decoder(zn) | |
| d = d.float().clamp(0, 1) | |
| ps.append(psnr(d, x)); ls.append(lp(d * 2 - 1, x * 2 - 1).item()) | |
| student.train() | |
| return sum(ps) / len(ps), sum(ls) / len(ls) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--data", nargs="+", required=True) | |
| ap.add_argument("--eval_images", default="data/kodak") | |
| ap.add_argument("--vae", default="weights/flux3-klein-base/vae") | |
| ap.add_argument("--init", default="weights/taef2.safetensors") | |
| ap.add_argument("--out", default="runs/ft") | |
| ap.add_argument("--steps", type=int, default=3000) | |
| ap.add_argument("--batch", type=int, default=16) | |
| ap.add_argument("--res", type=int, default=512) | |
| ap.add_argument("--lr", type=float, default=1e-4) | |
| ap.add_argument("--lpips_w", type=float, default=0.5) | |
| ap.add_argument("--gan_w", type=float, default=0.0, help="e.g. 0.05 to add adversarial loss") | |
| ap.add_argument("--gan_start", type=int, default=1000) | |
| ap.add_argument("--disc_init", default=None, help="resume the discriminator from a previous run's disc.pt") | |
| ap.add_argument("--noise_p", type=float, default=0.3, help="prob. of jittering latents (robust to diffusion outputs)") | |
| ap.add_argument("--train_encoder", action="store_true") | |
| ap.add_argument("--eval_every", type=int, default=500) | |
| ap.add_argument("--workers", type=int, default=32) | |
| ap.add_argument("--ema", type=float, default=0.999, help="EMA decay for the student weights (0 = off)") | |
| args = ap.parse_args() | |
| torch.backends.cuda.matmul.allow_tf32 = True | |
| torch.backends.cudnn.allow_tf32 = True | |
| dev = "cuda" | |
| os.makedirs(args.out, exist_ok=True) | |
| vae = AutoencoderKLFlux2.from_pretrained(args.vae, torch_dtype=torch.bfloat16).to(dev).eval().requires_grad_(False) | |
| ln = LatentNorm(vae) | |
| student = load_tiny(args.init, dev).train().requires_grad_(True) | |
| student.encoder.requires_grad_(args.train_encoder) | |
| lp = lpips.LPIPS(net="vgg", verbose=False).to(dev).eval().requires_grad_(False) | |
| lp_eval = lpips.LPIPS(net="alex", verbose=False).to(dev).eval().requires_grad_(False) | |
| params = [p for p in student.parameters() if p.requires_grad] | |
| opt = torch.optim.AdamW(params, lr=args.lr, betas=(0.9, 0.99), weight_decay=0.0) | |
| sched = torch.optim.lr_scheduler.LambdaLR( | |
| opt, lambda s: min(1.0, (s + 1) / 200) * (0.5 * (1 + math.cos(math.pi * s / args.steps)) * 0.9 + 0.1)) | |
| disc = d_opt = None | |
| if args.gan_w > 0: | |
| disc = PatchDisc().to(dev) | |
| d_opt = torch.optim.AdamW(disc.parameters(), lr=args.lr, betas=(0.5, 0.99)) | |
| if args.disc_init: | |
| ck = torch.load(args.disc_init, map_location=dev) | |
| disc.load_state_dict(ck["disc"]); d_opt.load_state_dict(ck["opt"]) | |
| ema = None | |
| if args.ema > 0: | |
| ema = copy.deepcopy(student).eval().requires_grad_(False) | |
| eval_imgs, _ = load_images(args.eval_images) | |
| p0, l0 = evaluate(student, vae, ln, lp_eval, eval_imgs, dev) | |
| print(f"step 0 eval PSNR {p0:.2f} LPIPS {l0:.4f}", flush=True) | |
| best = l0 | |
| # the starting weights are the best so far until something beats them | |
| stt.save_file({k: v.detach().float().contiguous().cpu() for k, v in student.state_dict().items()}, | |
| os.path.join(args.out, "best.safetensors")) | |
| dl = DataLoader(ImageFolder(args.data, args.res), batch_size=args.batch, shuffle=True, | |
| num_workers=args.workers, pin_memory=True, drop_last=True, persistent_workers=True) | |
| it = iter(dl) | |
| t0 = time.time() | |
| for step in range(1, args.steps + 1): | |
| try: | |
| x = next(it) | |
| except StopIteration: | |
| it = iter(dl); x = next(it) | |
| x = x.to(dev, non_blocking=True) | |
| with torch.no_grad(): | |
| zn = ln.to_norm(vae.encode((x * 2 - 1).to(torch.bfloat16)).latent_dist.mode().float()) | |
| z_in = zn | |
| if args.noise_p > 0: | |
| mask = (torch.rand(zn.shape[0], 1, 1, 1, device=dev) < args.noise_p).float() | |
| z_in = zn + mask * torch.rand_like(mask) * 0.1 * torch.randn_like(zn) | |
| with torch.autocast("cuda", torch.bfloat16): | |
| out = student.decoder(z_in) | |
| out = out.float() | |
| l1 = F.l1_loss(out, x) | |
| lpl = lp(out.clamp(0, 1) * 2 - 1, x * 2 - 1).mean() | |
| loss = l1 + args.lpips_w * lpl | |
| if args.train_encoder: | |
| zt = student.encoder(x).float() | |
| loss = loss + F.mse_loss(zt, zn) | |
| g_loss = torch.zeros((), device=dev) | |
| if disc is not None and step >= args.gan_start: | |
| g_loss = -disc(out).mean() | |
| loss = loss + args.gan_w * g_loss | |
| opt.zero_grad(set_to_none=True) | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(params, 1.0) | |
| opt.step(); sched.step() | |
| if disc is not None and step >= args.gan_start - 200: # warm the discriminator up a bit first | |
| with torch.autocast("cuda", torch.bfloat16): | |
| d_loss = F.relu(1 - disc(x)).mean() + F.relu(1 + disc(out.detach())).mean() | |
| d_opt.zero_grad(set_to_none=True); d_loss.backward(); d_opt.step() | |
| if step % 50 == 0: | |
| ips = step * args.batch / (time.time() - t0) | |
| print(f"step {step:6d} loss {loss.item():.4f} l1 {l1.item():.4f} lpips {lpl.item():.4f} " | |
| f"gan {g_loss.item():.3f} lr {sched.get_last_lr()[0]:.2e} {ips:.0f} img/s", flush=True) | |
| if ema is not None: | |
| d = min(args.ema, (1 + step) / (10 + step)) | |
| with torch.no_grad(): | |
| for pe, ps in zip(ema.parameters(), student.parameters()): | |
| pe.lerp_(ps, 1 - d) | |
| if step % args.eval_every == 0 or step == args.steps: | |
| sd_of = lambda m: {k: v.detach().float().contiguous().cpu() for k, v in m.state_dict().items()} | |
| p, l = evaluate(student, vae, ln, lp_eval, eval_imgs, dev) | |
| print(f"step {step} eval PSNR {p:.2f} ({p - p0:+.2f}) LPIPS {l:.4f} ({l - l0:+.4f})", flush=True) | |
| cands = [(l, sd_of(student))] | |
| stt.save_file(cands[0][1], os.path.join(args.out, "last.safetensors")) | |
| if ema is not None: | |
| pe_, le_ = evaluate(ema, vae, ln, lp_eval, eval_imgs, dev) | |
| print(f"step {step} ema eval PSNR {pe_:.2f} ({pe_ - p0:+.2f}) LPIPS {le_:.4f} ({le_ - l0:+.4f})", flush=True) | |
| cands.append((le_, sd_of(ema))) | |
| stt.save_file(cands[1][1], os.path.join(args.out, "ema.safetensors")) | |
| if disc is not None: | |
| torch.save({"disc": disc.state_dict(), "opt": d_opt.state_dict()}, os.path.join(args.out, "disc.pt")) | |
| lc, sdc = min(cands, key=lambda c: c[0]) | |
| if lc < best: | |
| best = lc | |
| stt.save_file(sdc, os.path.join(args.out, "best.safetensors")) | |
| print(f"step {step} new best LPIPS {lc:.4f}", flush=True) | |
| if __name__ == "__main__": | |
| main() | |