Buckets:
% FLUX.2 [klein] 4B → Compressed Student — Distillation Deep-Dive Report % Single-day ablation study · 1× A100-80GB % 2026-05-31
Executive Summary
We set out to compress FLUX.2 [klein] distilled 4B (a 4-step, CFG-free rectified-flow MM-DiT) into a smaller, faster text-to-image student via depth-pruning + warm-started surrogate blocks + short distillation recovery, on a single A100-80GB. Over one day we ran a full ablation sweep: block-selection strategies, four generations of surrogate architecture, recovery-recipe fixes (including a divergence we diagnosed and corrected), an optimizer A/B, a drop-count sweep, and a measured compute breakdown that re-pointed the strategy.
Headline result — the speed↔quality frontier:
| Config | Params | Smaller | Wall-clock speedup | Transformer-FLOP | Eval-loss | Verdict |
|---|---|---|---|---|---|---|
| teacher 4B | 3.876B | — | 1.00× | 1.00× | — | reference |
| v1 per-token, drop-12 | 2.441B | 37% | ~1.45× | ~1.64× | — | collapsed |
| per-token, drop-6 | 3.158B | 19% | 1.19× | 1.24× | 0.308 | ok, soft |
| linattn drop-6 (simple) | 3.177B | 18% | 1.15× | 1.23× | 0.253 | ok |
| linattn drop-6 (RoPE+conv+warmstart) | 3.177B | 18% | 1.15× | 1.23× | 0.231 | best quality |
| linattn drop-8 (+focused+FFN) | 2.995B | 23% | 1.20× | 1.28× | 0.269 | best perceptual |
| linattn drop-10 (mixed FFN) | 2.737B | 29% | 1.26× | 1.42× | ~0.322 | aggressive |
Eval-loss = held-out velocity-matching loss vs the teacher (lower = closer to teacher).
Three load-bearing conclusions:
- A per-token surrogate cannot replace an attention block (it can't mix tokens) → aggressive pruning collapses. Linear-attention surrogates (token-mixing) recover far better and are the key unlock.
- There is a steep speed↔quality frontier: best quality (0.231) sits at only 1.15× wall-clock; pushing to 1.26× costs ~40% more loss. The frontier is set by how many single blocks you remove, not surrogate cleverness.
- The 5 double-stream blocks are only ~21% of compute (measured) and do the fragile cross-modal binding — a bad target. Single blocks are 78% of compute and are the correct lever, but they're now near their useful limit. The real path to ~2× is step reduction (4→2), a separate project.
0. Block-Indexing Convention (read this first)
Throughout this report, to avoid the ambiguity that bit us a few times:
Single-stream blocks are numbered
S0 … S19(20 blocks), 0-indexed, counting from the START of the single-stream stack — i.e.S0= first / shallowest (immediately after the 5 double blocks),S19= last / deepest (immediately before the output projection).Double-stream blocks are
D0 … D4(5 blocks), same convention (D0= first).Data flows:
input → D0…D4 (double) → S0…S19 (single) → norm_out → proj_out → output. "Deepest N" always means the highest-numbered blocks (e.g. deepest-4 of the single stack =S16,S17,S18,S19). Lower importance = safer to drop.
Every experiment below lists its dropped / FFN block sets explicitly by these IDs.
1. Objective & Setup
- Teacher:
black-forest-labs/FLUX.2-klein-4B— the distilled checkpoint (4-step, guidance- distilled → CFG-free,is_distilled=true), Apache-2.0. We deliberately use the distilled (not the 50-step base) teacher: pure velocity matching cannot compress step count, so a 4-step student needs a 4-step teacher. - Hardware: 1× A100-80GB. No FlashAttention (Blackwell-only path) → PyTorch SDPA. This is a prototyping rig; the design is meant to lift onto a B200 for the full run.
- Goal: a smaller / faster student that stays recoverable, plus a clean map of the size/speed/quality tradeoffs.
- Eval metric: a fixed held-out batch (16 cached latents) on which we measure the velocity- matching loss between student and teacher. It is a clean, apples-to-apples proxy; it tracks perceptual quality well but not perfectly (see §6.6).
2. Architecture (verified from the real config + measured)
Flux2Transformer2DModel: d = 3072 (24 heads × 128), 5 double-stream + 20 single-stream
blocks, mlp_ratio=3, joint_attention_dim=7680 (Qwen3-4B conditioning), in_channels=128
(packed VAE latent), guidance_embeds=false. Transformer = 3.876B params. VAE
AutoencoderKLFlux2 (32 latent channels, batch-norm-normalized) decodes once at the end; the
transformer runs 4× (once per step); the Qwen3-4B text encoder runs once.
2.1 Double vs single blocks — internals & measured compute
Double block (Flux2TransformerBlock, 245.4M): an MM-DiT dual-stream block — it keeps image
and text as separate streams, joined only in attention:
attn (75.5M) joint cross-modal attention: image to_q/k/v/out (4×d²) +
text add_q/k/v_proj/to_add_out (4×d²); both streams' Q/K/V concatenated
→ every image token attends to every text token (and vice-versa)
ff (84.9M) image-stream FFN (its own)
ff_context(84.9M)text-stream FFN (its own, separate)
This is where cross-modal binding happens ("a red cube on a blue sphere").
Single block (Flux2ParallelSelfAttention, 122.7M): text+image merged into one stream, a
fused parallel attention+MLP with shared params over all 1536 tokens. These mostly refine.
Measured compute split (one transformer forward, batch-2):
full transformer forward: 136 ms
20 SINGLE blocks: 106 ms → 78% of compute
5 DOUBLE blocks: 28 ms → 21% of compute
per-block: double 5.71 ms vs single 5.30 ms → only 1.08× (NOT 2× despite 2× params)
Why params ≠ compute: a double block's 245M params are split across two shorter streams (image FFN on ~1024 tokens, text FFN on ~512), so it does roughly the same work as a single block's 122.7M running on all 1536 tokens. Takeaway: the 20 single blocks dominate compute → they are the correct surgery target. The 5 double blocks are a small, fragile slice (see §6.5).
3. Methodology
- Surgery: select least-useful single blocks, replace each with a cheaper surrogate that
preserves the residual shortcut (
out = x + surrogate(x)); keep the rest full. - Block selection: v1 used SVD-energy of each block's residual (picked a bad contiguous set); v2+ used leave-one-out importance ablation — skip each block, measure the relative change in the final latent, drop the lowest-impact blocks. Importance is much better (§4.2).
- Surrogate types (evolved across the day): per-token low-rank → linear-attention → +RoPE + depthwise-conv + warm-start → +focused feature map + FFN (§4.4).
- Recovery: freeze the entire pretrained network, train only the surrogates (~0.6–3% of params), AdamW @1e-4 with cosine-to-floor LR + grad-clip + fp32 master on the trained params, bf16 autocast compute. Primary loss = velocity matching to the frozen teacher (+ a light real-data flow-matching grounding term).
4. Experiments & Ablations
4.1 Surgery v1 — per-token low-rank, drop-12 → COLLAPSE
| Surrogate | per-token low-rank x + B·σ(A·x), rank 512 |
| Dropped (SVD-energy) | S0–S11 (first 12) |
| Kept full | S12–S19 |
| Params | 2.441B (−37%) |
A 7-agent visual-review workflow scored the warm-start student 5/5 severity (destroyed) in every category — flat color fields, no structure or text:
Root cause (the key architectural finding): a per-token low-rank map is applied independently per token — it cannot move information between token positions. But a dropped block was an attention block, whose whole job is token-mixing. So the surrogate can only mimic the MLP-ish per-token part (warm-start residual rel-err stuck at ~0.9 ≈ "predict nothing"). Removing 12 of 20 token-mixing layers severs the chain → collapse. This single finding redirected the entire project toward token-mixing surrogates.
4.2 Surgery v2 — importance-selected drop-6 → FUNCTIONAL
| Surrogate | per-token low-rank (same as v1) |
| Dropped (importance) | S12–S17 (6 least-important, middle-deep) |
| Kept full | S0–S11, S18, S19 |
| Params | 3.158B (−19%) |
Switching to importance ablation + dropping only 6 gave a functional (if soft) student even with near-identity surrogates. Importance also revealed low redundancy: every single block shifts the final latent by ≥43% when skipped — there are no "free" blocks.
4.3 Recovery recipe — divergence → fix → optimizer A/B
Failure first. Our initial recovery trained all weights with Muon at lr 0.02. It diverged:
Loss was stable ~0.83 to step ~120, then blew up to 8.08; samples went to cyan checkerboard noise. Diagnosis: Muon's 0.02 is a bulk-pretraining LR; applied to the well-pretrained kept blocks it corrupts them faster than the surrogates can learn. Never train the frozen-worthy weights.
The fix. Freeze the whole pretrained net; train only the surrogates (≈0.6% of params); AdamW @1e-4 (the diffusion/adapter LR regime), cosine LR with a floor, grad-clip 1.0, fp32 master on the trained params. Result: 0.476 → 0.308 (−35%), monotonic, zero divergence, 28.7 GB (vs 53 GB training all).
Optimizer A/B (controlled — identical recipe, only the optimizer swapped):
AdamW@1e-4 → 0.3081, Muon@2e-3 → 0.3056 — a statistical tie. The earlier blow-up was not Muon; it was lr-on-the-wrong-params. For a handful of adapter modules, AdamW is the simpler right tool; Muon is reserved for a later full-network recovery.
4.4 Surrogate evolution — the heart of the study
We rebuilt the surrogate four times, each time at the same dropped positions (S12–S17) for a controlled comparison, then swept drop-count.
(a) Linear-attention surrogate — O(N) linear attention (elu+1 feature map), multi-head. Unlike
per-token, it moves information between tokens. Drop-6 → 0.253, and it crossed the per-token
surrogate's converged floor by step 50. This validated token-mixing as the unlock.
(b) + RoPE + depthwise-conv + warm-start — RoPE (head_dim 128, the model's rotary) for spatial awareness; a depthwise-conv local branch for high-frequency detail; and a warm-start that fits each surrogate to mimic its teacher block. Drop-6 → 0.231 (best quality). Warm-start cut residual rel-err 1.0 → ~0.65 (the per-token surrogate never got below ~0.9 — see breakdown graph, §2.1).
(c) + focused feature map + FFN — a focused (FLatten) feature map with a learnable sharpening exponent makes linear attention's weights peaky/softmax-like instead of blurry; an FFN makes the surrogate a real linear-attn block. Warm-start improved further to 0.47–0.59. This version gave the best perceptual colors / local detail (your eye preferred it even though its loss number is higher — the metric doesn't fully capture perceptual sharpness).
Per-surrogate-generation comparison (drop-6, final samples):
4.5 Drop-count sweep + per-block FFN placement
The recovery curves for every surrogate/drop config:
Exact block tracking per run (single-block IDs, S0=first … S19=last):
| Run | Surrogate | Dropped block IDs | FFN on (IDs) | Kept full | Params | Speedup | Loss |
|---|---|---|---|---|---|---|---|
| v1 | per-token | S0–S11 | none | S12–S19 | 2.441B | ~1.45× | collapsed |
| v2 | per-token | S12–S17 | none | S0–S11,S18,S19 | 3.158B | 1.19× | 0.308 |
| linattn drop-6 simple | linattn (elu+1) | S12–S17 | none | S0–S11,S18,S19 | 3.177B | 1.15× | 0.253 |
| linattn drop-6 upgraded | linattn+RoPE+conv+ws | S12–S17 | none | S0–S11,S18,S19 | 3.177B | 1.15× | 0.231 |
| linattn drop-8 | linattn+focused+FFN | S10–S17 | S10–S17 (all 8) | S0–S9,S18,S19 | 2.995B | 1.20× | 0.269 |
| linattn drop-10 mixed | linattn+focused | S8–S17 | S14,S15,S16,S17 (deepest 4) | S0–S7,S18,S19 | 2.737B | 1.26× | ~0.322 |
Note the importance ablation consistently picks the middle-deep single blocks (S8–S17) as least important; the very last two (S18, S19) and the early ones (S0–S7) are always kept.
Drop-8 vs drop-10 final samples:
On FFN placement (drop-10): we put the FFN on the deepest 4 dropped blocks (S14–S17) and left S8–S13 light. Rationale: deeper blocks have more direct influence on the output, and the last dropped block should always carry an FFN. Warm-start confirmed it — the FFN blocks (S14–S17) reached rel-err 0.54–0.56 vs 0.61–0.70 for the light ones.
5. Results & The Frontier
- Best quality: drop-6 + RoPE+conv+warmstart → 0.231, 3.18B, 1.15× wall (~1.23× FLOP).
- Best perceptual / balanced: drop-8 +focused+FFN → 0.269, 3.00B, 1.20×.
- Most compression/speed: drop-10 mixed → ~0.322, 2.74B, 1.26× wall (1.42× FLOP).
- Failed: v1 per-token drop-12 (collapsed) — same aggressive drop a token-mixing surrogate might survive, but per-token cannot.
Wall-clock understates the real compression. At batch-1/512px, the Qwen3 text-encode (once) + VAE decode (once) are fixed overhead (~22% of wall-clock) that no denoiser surgery touches. The transformer-FLOP column is what the target hardware (B200, larger batch, cached text embeddings) would approach — e.g. drop-10's 1.26× wall ≈ 1.42× on the real run.
6. Key Findings & Lessons
- Function class is everything. A per-token surrogate can't do attention's token-mixing → it caps recovery and collapses under aggressive pruning. Linear attention restores it (§4.1, §4.4).
- Steep speed↔quality frontier set by drop-count. 0.231@drop-6 → 0.322@drop-10. The surrogate architecture shifts the curve a little; how many blocks you remove sets where you are on it.
- Recovery recipe: freezing the base is mandatory (training all weights diverged to noise); AdamW ≈ Muon for adapter-style recovery; LR must be the adapter regime (~1e-4), not Muon's 0.02.
- LR schedule: decaying cosine to exactly 0 wastes the tail and breaks resume; we floor it at 30% of base (1e-4 → 3e-5) so it decays visibly and settles (a constant LR jittered at the floor).
- Single blocks (78% of compute) are the target; the 5 double blocks (21%) are not. Per-block, double ≈ single in speed (1.08×); the double blocks do fragile cross-modal binding (the first thing to degrade) — a bad place to cut for little gain (§6.5).
- The eval metric ≠ perception. drop-8 had a higher velocity-loss (0.269) but the best colors / local detail by eye — the focused feature map + FFN improved perceptual quality the metric under-credits.
- Warm-start is limited by sequential mismatch. We fit each surrogate on the teacher's block inputs, but at inference downstream surrogates see student inputs — so the local rel-err gains (→0.5) don't fully transfer end-to-end. A progressive warm-start would fix it.
- FFN buys quality, not speed. It improved warm-start and perception but is heavy — at drop-8 it cut the speedup back to ~drop-6 levels. Capability (FFN) and speed (more drops) pull against each other.
6.5 Why we did NOT touch the double blocks (measured)
Shrinking the FFN on 2 of the 5 double blocks by 50% would touch 2/5 × 21% × ~60%(FFN) × 50% ≈ 2%
of compute → ~1.02× speedup. Even all five → ~1.05×. The leverage isn't there, and the cross-modal
attention is the most fragile thing in the model. Rejected on the math.
7. Speculations & Ideas (explored + future)
Surrogate-side (to push the frontier):
- Gating (GLA-style) data-dependent forget gates; width (heads × head_dim) as a capacity dial; 2D depthwise conv on the image-token grid (vs current 1D over the sequence); Hedgehog learnable feature map trained to mimic softmax.
- Progressive/sequential warm-start (fit S-i, re-capture, fit S-(i+1)…) to fix the §6.7 mismatch.
Training-signal upgrades (explained, not yet run):
- Trajectory velocity matching on the 4 schedule σ's (vs current σ~U(0,1)): instead of matching at random noise levels off real-image interpolants, roll out the teacher's actual 4-step sampler and match at the 4 on-trajectory points the student will really visit — kills few-step exposure bias.
- Feature matching on retained blocks (masked KD): also match intermediate hidden states of the kept blocks (denser supervision; no projector since widths match), masking the few extreme "massive activations" so they don't dominate the loss.
The real 2× levers (different axes):
- Step reduction (4→2) — halves all transformer passes at once; needs step-distillation (DMD / consistency). This is the biggest lever and the recommended next project.
- Text-encoder caching / batching — removes the fixed ~22% wall-clock overhead.
- B200 + FlashAttention + fp8 + larger batch + 300k-image data + offline latent shards (the original plan's scale-up).
8. Recommendations / Where Things Stand
This was not "it'll never work" — we mapped a real tradeoff and built a working surgery + recovery
- eval pipeline.
- If you want one balanced single-block model: lock drop-7 with 3 FFN (FFN on the deepest 3 dropped incl. the last) — projected ~3.08B, ~1.17× wall, loss ~0.25 (the midpoint of drop-6/drop-8).
- If quality is paramount: drop-6 + RoPE+conv+warmstart (0.231, 1.15×).
- Single-block surgery is near its useful limit (~1.15–1.26× on this setup); squeezing further is diminishing returns.
- For a genuine ~2×: the next project is step reduction (4→2 via step-distillation), plus text-encoder caching and the B200 scale-up — not more block surgery.
Appendix A — Reproduction
scripts/01_inspect_model.py # verify architecture
scripts/02_teacher_smoke.py # teacher 4-step generation
scripts/03_build_student.py # v1 surgery (SVD-energy, per-token, drop-12)
scripts/05_build_student_v2.py 6 # v2 surgery (importance, per-token, drop-6)
scripts/09_build_linattn.py 10 4 # linattn build: drop_k=10, FFN on deepest 4
scripts/08_train_recover.py 300 adamw 1e-4 # surrogate-only recovery (env: STUDENT_DIR, SCHED, MB)
scripts/10_bench.py # inference speedup
scripts/make_report_assets.py # graphs + montages
Data: 400 monet images cached @512 (scripts/06_cache_data.py 400). Held-out eval batch = first 16.
Appendix B — Surrogate config (final linear-attn block)
heads=4 × head_dim=128 (=512 inner, RoPE-compatible), elu+1→focused with learnable exponent,
depthwise-conv kernel=5 local branch, optional FFN (hidden=1024) placed per-block via ffn_idx.
Output zero-init = identity start; warm-started to the teacher block. Per-block params: ~6.3M light,
~12.6M with FFN.
Xet Storage Details
- Size:
- 20 kB
- Xet hash:
- 6fb2f330cd33f2dbd5886571ee6eca69bd8b7ccc1ae50a26ab9e8d7c2f0e1531
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.








