Skip to content
Go back

A Tiny VAE for FLUX 3 Klein: 30× Smaller, 11× Faster Decoding

Edit page

TL;DR. FLUX 3 Image Klein ships the FLUX.2 autoencoder unchanged, so a tiny decoder trained for that latent space works for FLUX 3. I fine-tuned TAEF2 on 25.8k real photos with L1 + LPIPS + a light PatchGAN loss and weight averaging. The result, Tiny Klein 3 VAE, has 2.7M parameters instead of 84M, decodes 11× faster at 1024² with about 4× less memory, and its perceptual gap to the full VAE (LPIPS) is 26–32% smaller than TAEF2’s on FLUX 3 generations. It is still visibly softer than the full VAE in the finest textures. It also works for image editing, including bounding-box edits, as long as the full VAE encodes the reference images. Drag the sliders below and judge for yourself.

Parameters (vs 84M)
2.7M
Faster decode, 1024²
11.2×
Less memory, 2048²
3.8×
LPIPS gap vs TAEF2
−26–32%

Table of contents

Open Table of contents

1. Why a tiny VAE?

FLUX 3 Image Klein is a 5.6B-parameter text-to-image and editing model. Like every latent diffusion model it never touches pixels while it denoises: the transformer works on a compressed latent grid, and a separate autoencoder (the VAE) turns that grid into an image at the end. FLUX 3 Klein’s VAE has 84M parameters (34M encoder, 50M decoder) and a self-attention block in the middle of the decoder.

For a single 1024² image from the 50-step base model the decode is cheap: about 0.16 s out of roughly 40 s of generation on an L40S. A tiny decoder is not about that case. It matters when:

The goal: a decoder small enough to run everywhere, reading exactly the same latents, losing as little quality as possible.

2. FLUX 3 Klein reuses the FLUX.2 VAE

A tiny decoder only works if it reads the same latent space the transformer writes. So the first question was whether FLUX 3 changed the autoencoder.

It did not. The Hugging Face LFS metadata gives the SHA-256 of each weight file without downloading it:

FileSHA-256
flux-3-image-klein-base/ae.safetensors868fe7b3…ce8f3
FLUX.2-dev/ae.safetensors868fe7b3…ce8f3

The diffusers copies (vae/diffusion_pytorch_model.safetensors) also match each other, and the two vae/config.json files differ only in the diffusers version stamp. Both describe a 32-channel latent at 1/8 resolution, patchified 2×2 into 128 channels for the transformer.

So anything trained against the FLUX.2 VAE (including TAEF2) already speaks FLUX 3 Klein’s latent format.

3. The latent-space trap

My first test fed the full VAE’s encoder output straight into TAEF2’s decoder. It scored 17.9 dB PSNR, which is badly broken. The latent spread gave it away: the encoder’s latents had a standard deviation of about 1.7, TAEF2’s own about 0.85.

The FLUX pipeline does not hand the raw VAE latent to the transformer. It patchifies it and normalizes each of the 128 channels with the VAE’s stored batch-norm statistics, and it undoes that just before decoding:

znorm=unpatchify⁡ ⁣(patchify⁡(z)−μbnσbn2+ϵ)z_{\text{norm}} = \operatorname{unpatchify}\!\left(\frac{\operatorname{patchify}(z) - \mu_{\text{bn}}}{\sqrt{\sigma^2_{\text{bn}} + \epsilon}}\right)

TAEF2 was trained on znormz_{\text{norm}}, the transformer’s space, not on the raw zz. Its diffusers wrapper swaps in an identity batch-norm, so the pipeline’s de-normalization becomes a no-op. Applying the normalization fixed it at once:

Input to TAEF2’s decoderPSNR (Kodak)LPIPS
raw VAE latent zz17.89 dB0.224
normalized latent znormz_{\text{norm}}30.69 dB0.038

Everything below uses znormz_{\text{norm}}.

4. Method

Architecture

I kept TAEF2’s architecture (the flux_2 variant of TAESD) and changed only its weights. The decoder is a plain convolutional network:

That is 1.34M parameters in the decoder, the same again in the encoder, against 49.6M in the full decoder.

Distillation setup

The full VAE encoder is the teacher: it turns each training photo xx into znormz_{\text{norm}}. The tiny decoder DθD_\theta is the student and learns to rebuild the original photo from those latents. Only the decoder is trained. The encoder stays as TAEF2 shipped it; it is only needed for image-to-image and editing.

L(θ)=∥Dθ(z~)−x∥1  +  0.5 LPIPSVGG(Dθ(z~),x)  +  0.01 Ladv\mathcal{L}(\theta) = \lVert D_\theta(\tilde z) - x \rVert_1 \;+\; 0.5\,\mathrm{LPIPS}_{\text{VGG}}\big(D_\theta(\tilde z), x\big) \;+\; 0.01\,\mathcal{L}_{\text{adv}}

Data. 800 DIV2K photos plus 24,997 Unsplash Lite photos at 2048 px. Real photos only, no FLUX outputs. Each sample is resized so its short side is random between the crop size and 3× the crop size, then randomly cropped and flipped. The model sees both fine native-resolution detail and whole-object structure.

Schedule (one NVIDIA L40S, bf16 autocast, AdamW, cosine decay):

StageStepsCropBatchPeak LR
1. warm-up fine-tune (L1 + LPIPS, DIV2K only)3,000512161e-4
2a. full data, GAN weight 0.052,000*512161e-4
2b. full data, GAN weight 0.011,000*512168e-5
3. + EMA, main run16,000512166e-5
4. high-resolution finish3,000102442e-5

*Stages 2a and 2b are the kept parts of two runs I stopped early; each later stage starts from the best checkpoint of the one before. Section 7 has the details. All in, about 6.5 GPU-hours including the abandoned steps, or roughly $6 of rented compute.

The checkpoint kept at each stage is the one with the lowest LPIPS on the 24 Kodak photos.

5. Results

Quality on real FLUX 3 generations

These are the numbers that matter. I generated images with FLUX 3 Klein base (50 steps), captured the exact latent the pipeline hands to its VAE, and decoded it three ways. The full-VAE decode is the reference. None of these images was used for training or for picking checkpoints.

DecoderSet A: 6 imagesSet B: 8 images (gallery)
PSNR ↑LPIPS ↓PSNR ↑LPIPS ↓
TAEF230.000.034228.870.0363
Tiny Klein 3 VAE (ours)31.450.023430.330.0267
gap closed vs TAEF2 (LPIPS)−32%−26%

Set A is mixed sizes from 1024² to 1088×1920; set B is the 8 gallery images below. Ours beats TAEF2 on all 14 images, by both metrics.

Quality on photographs (Kodak)

Here the reference is the original photo, so the full VAE’s own error is visible too. These numbers are a little optimistic for the tiny models, because Kodak was also used to pick checkpoints.

DecoderParamsPSNR ↑LPIPS ↓
Full FLUX 3 VAE49.6M (dec)34.380.0137
TAEF21.34M (dec)30.690.0380
Tiny Klein 3 VAE (ours)1.34M (dec)31.980.0306

Speed and memory

Decoder only, batch 1, bf16, NVIDIA L40S, no tiling. “Extra memory” is the peak above the model weights.

ResolutionFull VAETiny Klein 3 VAESpeed-up
1024²161 ms · 2.4 GB14.4 ms · 0.6 GB11.2×
2048²760 ms · 9.5 GB73 ms · 2.5 GB10.4×
4096²78,249 ms · 24.0 GB311 ms · 10.0 GB251×

The 4096² row is the full decoder’s attention block running out of headroom. A tiled decode would avoid most of it, but tiling the full VAE trades time for seams and complexity.

Each image is a FLUX 3 Klein base generation (seed 7, 50 steps). Left of the line: full VAE. Right: our tiny decoder. Drag to compare. Under each is the 384×384 region with the most fine detail, picked automatically, decoded both ways. Numbers are against the full-VAE decode.

6. Does it work for image editing?

FLUX 3 Klein also edits images: it takes up to 10 reference images and a caption, which can include a table of bounding boxes. Editing uses the VAE twice. The encoder turns each reference image into latents the transformer conditions on, and the decoder turns the result back into pixels. My model only replaces the decoder, so I tested three setups:

The tests: a plain edit (recolor a sweater), a text edit, a bounding-box add (put a cat inside a given box) and remove (take out a lamp), and a two-reference composition. “Preserved region” is a part of the image the edit should leave alone, compared with the reference. Same seed everywhere, 50 steps, FLUX 3 Klein base.

EditB vs AC vs APreserved region: A / B / C
Recolor sweater0.0210.0640.015 / 0.027 / 0.068
Change chalk text0.0250.0470.033 / 0.043 / 0.059
Bbox: add a cat0.0260.0400.012 / 0.028 / 0.031
Bbox: remove a lamp0.0250.0380.012 / 0.027 / 0.031
Two references0.0170.305—

All numbers are LPIPS, lower is better.

Reference, then the three setups. A and B share one generated latent; C is its own generation from tiny-encoded references.

Editing the same image again and again

Editing an edit compounds errors. I ran three edits in a row on the portrait (recolor the sweater → add glasses → swap the background), each round feeding its own output back as the next reference:

RoundB vs AC vs A
1. recolor0.0210.064
2. + glasses0.0550.098
3. + new background0.1050.193
Round 3 of chained edits, full VAEA · full VAE
Round 3 of chained edits, full encoder plus tiny decoderB · tiny decoder
Round 3 of chained edits, tiny encoder and decoderC · all tiny
After three rounds of editing. B and C have drifted in color and contrast: warmer skin, more orange hair, deeper shadows.

The drift is not just softness. By round 3, B and C show a visible color and contrast shift. Each round’s small decoder error becomes part of the next reference, and the model faithfully keeps it (“everything else stays unchanged”). For multi-round editing, use the tiny decoder for previews but feed the full-VAE decode back as the next reference. That breaks the loop for the cost of one full decode per round.

7. What worked and what didn’t

Kodak LPIPS of the best checkpoint after each change, lower is better:

StepKodak LPIPSPSNR
TAEF2, as released0.038030.69
+ fine-tune with L1 + LPIPS on DIV2K (3k steps)0.035832.05
+ full data and GAN weight 0.050.0356 → 0.037731.71 → 31.24
restart with GAN weight 0.010.033931.70
+ EMA, 16k steps0.031031.94
+ 1024px finish, 3k steps0.030631.98
EMA weightsraw weightsTAEF2 = 0.0380
Kodak LPIPS during stage 3 (steps 0–16k, 512px) and stage 4 (16k–19k, 1024px). Lower is better. The raw weights jump around while the EMA copy improves steadily.

8. Limitations

9. Reproduce it


Edit page
Share this post on:

Next Post
Inside a Blackwell 2-CTA GEMM: A Gluon Kernel Tour