File size: 7,465 Bytes
cfa2b7d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Compare TAEF2 (or a fine-tuned copy) against the full FLUX 3 Klein VAE.

Reports reconstruction PSNR / LPIPS on a folder of images, latent agreement,
decode speed, parameter counts, and saves a side-by-side grid.

python eval_tiny_vae.py --images data/kodak --tiny weights/taef2.safetensors
"""
import argparse, glob, os, time

import lpips
import numpy as np
import safetensors.torch as stt
import torch
import torch.nn.functional as F
from PIL import Image
from diffusers import AutoencoderKLFlux2

from taesd import TAESD


def convert_diffusers_sd_to_taesd(sd):
    # taef2.safetensors uses "encoder.layers.N" names; taesd.py uses "encoder.N" (decoder offset by 1)
    out = {}
    for k, v in sd.items():
        if not k.startswith(("encoder.layers.", "decoder.layers.")):
            out[k] = v
            continue
        encdec, _layers, index, *suffix = k.split(".")
        offset = 1 if encdec == "decoder" else 0
        out[".".join([encdec, str(int(index) + offset), *suffix])] = v
    return out


def load_tiny(path, device, dtype=torch.float32):
    m = TAESD(encoder_path=None, decoder_path=None, latent_channels=32, arch_variant="flux_2")
    sd = stt.load_file(path)
    if any(k.startswith(("encoder.layers.", "decoder.layers.")) for k in sd):
        sd = convert_diffusers_sd_to_taesd(sd)
    m.load_state_dict(sd)
    return m.to(device, dtype).eval().requires_grad_(False)


def patchify(z):
    b, c, h, w = z.shape
    return z.view(b, c, h // 2, 2, w // 2, 2).permute(0, 1, 3, 5, 2, 4).reshape(b, c * 4, h // 2, w // 2)


def unpatchify(z):
    b, c, h, w = z.shape
    return z.reshape(b, c // 4, 2, 2, h, w).permute(0, 1, 4, 2, 5, 3).reshape(b, c // 4, h * 2, w * 2)


class LatentNorm:
    """Raw VAE latents <-> the BN-normalized space the FLUX transformer (and TAEF2) work in."""

    def __init__(self, vae):
        self.mean = vae.bn.running_mean.view(1, -1, 1, 1).float()
        self.std = torch.sqrt(vae.bn.running_var.view(1, -1, 1, 1).float() + vae.config.batch_norm_eps)

    def to_norm(self, z):
        return unpatchify((patchify(z) - self.mean.to(z)) / self.std.to(z))

    def to_raw(self, z):
        return unpatchify(patchify(z) * self.std.to(z) + self.mean.to(z))


def load_images(folder, mult=16):
    xs, names = [], []
    for p in sorted(glob.glob(os.path.join(folder, "*"))):
        if not p.lower().endswith((".png", ".jpg", ".jpeg", ".webp")):
            continue
        im = Image.open(p).convert("RGB")
        w, h = im.size
        im = im.crop((0, 0, w - w % mult, h - h % mult))
        xs.append(torch.from_numpy(np.asarray(im)).permute(2, 0, 1).float() / 255.0)
        names.append(os.path.basename(p))
    return xs, names


def psnr(a, b):
    return (10 * torch.log10(1.0 / F.mse_loss(a, b))).item()


def timed(fn, x, n=10):
    fn(x); torch.cuda.synchronize()
    t = time.perf_counter()
    for _ in range(n):
        fn(x)
    torch.cuda.synchronize()
    return (time.perf_counter() - t) / n * 1000


@torch.no_grad()
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--images", default="data/kodak")
    ap.add_argument("--vae", default="weights/flux3-klein-base/vae")
    ap.add_argument("--tiny", nargs="+", default=["weights/taef2.safetensors"])
    ap.add_argument("--grid", default="out/compare.jpg")
    ap.add_argument("--latent_space", choices=["norm", "raw"], default="norm",
                    help="space the tiny AE reads/writes: BN-normalized (TAEF2) or raw VAE latents")
    args = ap.parse_args()

    dev = "cuda"
    vae = AutoencoderKLFlux2.from_pretrained(args.vae, torch_dtype=torch.float32).to(dev).eval()
    ln = LatentNorm(vae)
    to_tiny = ln.to_norm if args.latent_space == "norm" else (lambda z: z)
    from_tiny = ln.to_raw if args.latent_space == "norm" else (lambda z: z)
    tinies = {os.path.basename(p): load_tiny(p, dev) for p in args.tiny}
    lp = lpips.LPIPS(net="alex", verbose=False).to(dev)

    nparam = lambda m: sum(p.numel() for p in m.parameters()) / 1e6
    print(f"full VAE   : {nparam(vae):7.2f}M params (enc {nparam(vae.encoder):.2f}M / dec {nparam(vae.decoder):.2f}M)")
    for n, t in tinies.items():
        print(f"{n:11s}: {nparam(t):7.2f}M params (enc {nparam(t.encoder):.2f}M / dec {nparam(t.decoder):.2f}M)")

    xs, names = load_images(args.images)
    res = {k: [] for k in ["full"] + [f"{n}|dec" for n in tinies] + [f"{n}|enc" for n in tinies] + [f"{n}|both" for n in tinies]}
    lat_stats = {n: [] for n in tinies}
    grid_rows = []
    for i, x in enumerate(xs):
        x = x[None].to(dev)
        z = vae.encode(x * 2 - 1).latent_dist.mode()  # raw 32-ch latent at /8, what the pipeline unpatchifies to
        full = vae.decode(z).sample.add(1).div(2).clamp(0, 1)
        res["full"].append((psnr(full, x), lp(full * 2 - 1, x * 2 - 1).item()))
        row = [x, full]
        for n, t in tinies.items():
            zn = to_tiny(z)
            d = t.decoder(zn).clamp(0, 1)
            res[f"{n}|dec"].append((psnr(d, x), lp(d * 2 - 1, x * 2 - 1).item()))
            zt = t.encoder(x)
            e = vae.decode(from_tiny(zt)).sample.add(1).div(2).clamp(0, 1)
            res[f"{n}|enc"].append((psnr(e, x), lp(e * 2 - 1, x * 2 - 1).item()))
            b = t.decoder(zt).clamp(0, 1)
            res[f"{n}|both"].append((psnr(b, x), lp(b * 2 - 1, x * 2 - 1).item()))
            cos = F.cosine_similarity(zt.flatten(), zn.flatten(), dim=0).item()
            lat_stats[n].append((F.mse_loss(zt, zn).item(), cos, zn.std().item(), zt.std().item()))
            row.append(d)
        if i < 4:
            grid_rows.append(torch.cat([r[..., :512, :512] for r in row], -1))

    print("\nreconstruction vs original image (mean over", len(xs), "images)")
    print(f"{'path':34s} {'PSNR dB':>8s} {'LPIPS':>7s}")
    for k, v in res.items():
        v = np.array(v)
        label = {"full": "full VAE enc -> full VAE dec"}.get(k, k.replace("|dec", ": full enc -> TINY dec").replace("|enc", ": TINY enc -> full dec").replace("|both", ": TINY enc -> TINY dec"))
        print(f"{label:34s} {v[:,0].mean():8.2f} {v[:,1].mean():7.4f}")
    for n, s in lat_stats.items():
        s = np.array(s)
        print(f"\n{n} encoder vs full encoder latents: MSE {s[:,0].mean():.4f}  cosine {s[:,1].mean():.4f}  std full {s[:,2].mean():.3f} / tiny {s[:,3].mean():.3f}")

    print("\ndecode latency (bf16, batch 1)")
    vae_bf = vae.to(torch.bfloat16)
    for res_px in (1024, 2048):
        z = torch.randn(1, 32, res_px // 8, res_px // 8, device=dev, dtype=torch.bfloat16)
        torch.cuda.reset_peak_memory_stats()
        tf = timed(lambda z: vae_bf.decode(z).sample, z, 5)
        mf = torch.cuda.max_memory_allocated() / 2**30
        line = f"  {res_px}px  full VAE {tf:8.1f} ms ({mf:.1f} GB peak)"
        for n, t in tinies.items():
            tb = t.to(torch.bfloat16)
            torch.cuda.reset_peak_memory_stats()
            tt = timed(tb.decoder, z, 10)
            mt = torch.cuda.max_memory_allocated() / 2**30
            line += f" | {n} {tt:6.1f} ms ({mt:.1f} GB) -> {tf/tt:.1f}x faster"
            t.float()
        print(line)

    os.makedirs(os.path.dirname(args.grid) or ".", exist_ok=True)
    g = torch.cat(grid_rows, -2)[0].permute(1, 2, 0).float().cpu().numpy()
    Image.fromarray((g * 255).round().astype(np.uint8)).save(args.grid, quality=92)
    print("\ngrid columns: original | full VAE |", " | ".join(tinies), "->", args.grid)


if __name__ == "__main__":
    main()