Buckets:
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
A research/ablation codebase for compressing FLUX.2 [klein] distilled 4B (a 4-step, CFG-free rectified-flow MM-DiT text-to-image model) into a smaller, faster model. Dev rig (since 2026-06-10) is 1× RTX PRO 4500 Blackwell 32 GB (sm_120, SDPA; previously an A100-80GB — see the env section, several old assumptions changed); designed to lift onto a B200.
ACTIVE TRACK — W4A8 SVDQuant (quantization). Our own fake-quant implementation of SVDQuant
(4-bit weights + 8-bit activations, with a high-precision low-rank branch that absorbs outliers):
calibrate activation stats → per-Linear smooth → (whitened) SVD low-rank + iterative refine → 4-bit residual → eval against the frozen teacher. Code in flux2distill/svdquant.py; pipeline =
scripts/11–13 (drive the grid one cell at a time with scripts/run_cell.sh).
Best result (2026-06-10, new box/new eval axis): r64 plain+refine SMOOTH=0 = 0.0297 eval-loss @ 3.43× (smoothed twin re-evals at 0.0325; the surgery frontier sat at 0.23–0.32). Key findings:
(1) the upgrades are NOT the monotonic win the old "L-shape" ablation implied — refine is the
reliable lever; whitening alone is non-monotonic in rank (overfits the 300-img Gram; hurts r16 &
r64, helps r32). (2) ⚠️ SmoothQuant is HARMFUL at W4A8 — CONFIRMED at all ranks (2026-06-10):
SMOOTH=0 beats α=0.5 by −8.6% (r64/r32) to −14.1% (r16); 8-bit activations don't need migration,
and smoothing just widens the 4-bit-weight distribution (worst-layer wrecon 0.15→0.26). SMOOTH=0 is
the default now (α sweep: U-shaped, off best, α=0.1 worst). (3) W4A4 works ONLY with per-group
activations (AGROUP=64, the paper's actual spec): per-token A4 is catastrophic (0.51); g64 acts
→ 0.0610 @ r128 no-smooth (~2× the A8 champion). Same clean recipe at every bit-width:
plain+refine, no smooth, per-group W+A. NB the 2026-06-01 grid numbers (best
0.0446) are the OLD eval axis — not comparable to new-box numbers (see RESULTS.md 2026-06-10).
Full grid + math in report/QUANT_REPORT.md (+ report/QUANT_REPORT.pdf with all per-cell montages).
★ NOW (2026-06-13) — NVFP4 + a DEPLOYABLE fused model on real Blackwell kernels. Added NVFP4
(Blackwell-native FP4: E2M1 + group-16 + FP8 scales) to svdquant.py (w_fmt/a_fmt). NVFP4 beats
INT4 on BOTH quality and speed. Quality (same axis): NVFP4 W4A4 g16 r128 = 0.0303 (≈ the INT4
W4A8 champ, but full 4-bit acts; ~2× better than INT4 W4A4); NVFP4-W + FP8-A r128 = 0.0169 = new
overall champion. Speed (real Nunchaku kernels, built from source for sm_120): NVFP4 W4A4 = 2.49–2.75×
per-layer vs bf16; INT4 is BROKEN on Blackwell (1677 ms/step, slower than bf16 — no INT4 tensor
cores). Wrote our own bf16→Nunchaku NVFP4 exporter (flux2distill/nunchaku_export.py, no
DeepCompressor; convention validated to 2.99% in scripts/26) and shipped a deployable fused
klein-4B: outputs/nvfp4/deploy/klein4b_nvfp4_fused.safetensors (2.9 GB), correct images on the real
FP4 kernel, 1.74×@512 / 1.90×@1024 end-to-end, −24% VRAM. Full write-up + figures:
report/NVFP4_REPORT.md (report/figures/GRID_overview.png). Setup/footguns:
docs/CUDA_SETUP_RUNBOOK.md. Next-step kernel optimization plan: docs/SPEEDUP_IDEAS.md.
Scripts 20–28 (profile/bench/convert), scripts/make_nvfp4_figures.py. Nunchaku source build +
recipe in /workspace/build_nunchaku/do_build.sh.
★★ NOW (2026-06-14) — NVFP4 HEAD-TO-HEAD (matched, paired) + the BFL-official comparison, closed.
Ran the deployable model vs the field on N=512 MJHQ-30k, 512px, 4 steps, guidance 1.0, seed=idx,
with image-space fidelity-to-teacher metrics (this is a closer-to-teacher claim, not "prettier").
Master result: the SVDQuant low-rank branch helps at NVFP4 W4A4 — r0→r128 = LPIPS −19.7%,
PSNR +1.27 dB, FID-vs-teacher −14.7% (PickScore/CLIP flat → no semantic loss), and the real
Nunchaku FP4 kernel reproduces the fake-quant (LPIPS 0.167 vs 0.173 → the gain is real on the
deployed model). Models compared: A teacher bf16 · C ours NVFP4 W4A4 r128 (real Nunchaku
kernel; 0.352 s/img · 42.6 ms/step · 12.6 GB) · D plain-NVFP4 r0 (our controlled "plain NVFP4"
stand-in, identical recipe minus the low-rank branch) · E BFL official FP8 (W8A8, the real
public baseline; closest to teacher at 0.080/23.0 dB as 8-bit should be). FID-vs-real is flat
(88–90 incl. teacher — it tracks klein-vs-MJHQ style, not quant; reported but not discriminating).
Numbers: RESULTS.md (2026-06-14) + outputs/eval/h2h/metrics.json; speed/VRAM
outputs/nvfp4/benchmark_headtohead.json; full write-up + figures report/HEADTOHEAD_klein4b_nvfp4.md
(report/figures/h2h_*.png, incl. the teacher|ours|r0|BFL-fp8 montage on the text/hand/eggs probes).
Pipeline (one process per model, per-run logs): scripts/run_h2h.sh (A/D/fake-q-r128/C/E gen) →
scripts/34_metrics.py → scripts/42_h2h_figures.py; sensitive probes scripts/run_probes.sh
(uses PROMPTS_JSON=outputs/eval/probes.json, an env override now on scripts 32/35/41).
⛔ BFL official NVFP4 (black-forest-labs/FLUX.2-klein-4b-nvfp4) is NOT runnable on this box — DO
NOT re-attempt the manual-dequant path. It is the SAME klein-4b weights (per-tensor scale matches the
teacher to 0.1% on multiple layers), NVIDIA-ModelOpt NVFP4 W4A4, but stored in the cutlass/TensorRT
tensor-core SWIZZLED layout (packed-U8 weights AND FP8 group-16 block-scales jointly interleaved).
Plain modelopt.NVFP4QTensor.dequantize → cosine ≈ 0 vs teacher (fused and non-fused layers);
modelopt defers to tensorrt_llm (cutlass_fp4_scale_to_modelopt_fp4_scale) to de-swizzle, not viable
on sm_120 (no FLUX TRT runtime). The diffusers from_single_file + NVIDIAModelOptConfig path also
fails (FLUX.2 single-file converter isn't modelopt-aware: torch.chunks scalar scale tensors, drops
every input_scale). → Reproducing BFL's NVFP4 quality independently needs THEIR runtime; this is
itself a finding (report/HEADTOHEAD_klein4b_nvfp4.md §5). The BFL-NVFP4 attempt is scripts/40_load_bfl.py.
BFL's FP8 IS plain (not swizzled) — weight.float()·weight_scale → cos 0.9997 vs teacher — so it
loads faithfully via scripts/41_gen_bfl_fp8.py (dequant + diffusers key-remap + static-fp8 activation
hooks = true W8A8). For a "plain NVFP4" comparator, use our own rank-0 (scripts/32_gen_eval.py fq:0),
the clean one-variable ablation.
SHELVED TRACK — block surgery. The prior approach (depth-prune single-stream blocks → replace
with cheaper "surrogate" blocks → freeze the rest and recover via short distillation) topped out at
~1.15–1.26× and was quality-bounded. Kept for record: block_surgery_plan.md,
block_surgery_todo.md, report/REPORT.{md,pdf}, scripts 03/05/07/08/09, and
flux2distill/{surgery,surrogate,calibration}.py. Quantization is a better, orthogonal axis.
Read these first (real decision history + results): RESULTS.md (both tracks on one axis +
all metrics tables), report/QUANT_REPORT_2026-06-10.{md,pdf} (the LATEST campaign: SMOOTH=0
verdict, α U-sweep, W4A4 per-token→per-group fix, converged recipe, 20-cell montage appendix —
built by scripts/build_report_2026_06_10.py), report/QUANT_REPORT.md / .pdf (2026-05-31
A100-era: methodology + math: whitening, Cholesky→eigh, refinement, the 4×3 grid), plan.md
(active quant design), TODO.md (quant backlog: the 2000-img calib re-sweep is the next experiment),
README.md (run order). block_surgery_*.md + report/REPORT.pdf hold the shelved-track deep-dive.
init-plan.md is the original proposal, superseded.
Remote bucket & where the storage weight is (hf://buckets/Mercity/FluxDistill)
/workspace is mirrored to the Xet bucket Mercity/FluxDistill (hf sync). The full bucket is
~310–340 GB, but the bytes are NOT spread out — they are almost entirely large .pt files
under outputs/. Know this before any hf sync/hf download, or you will pull hundreds of GB by
accident (a naive hf sync … ./ downloads the whole thing).
Storage map (approx, 2026-06-14):
| Path | Size | What / why it's heavy |
|---|---|---|
outputs/**/quant_state.pt |
~169 GB (23 files) | the ~7.3–7.6 GB fake-quant state per grid cell — dead weight after eval (loss + montages are already saved); safe to delete |
outputs/train_*/**.pt |
~108 GB (19 files) | recovery-training checkpoints (student_best.pt/student_final.pt) |
models/klein-4b/ |
~23 GB | teacher weights (public, re-downloadable via hf download) |
outputs/**/*.safetensors |
~10 GB | incl. the deployable fused NVFP4 model |
.venv/ |
~4 GB | NOTE: no .venv is used on the box since 2026-06-10; this is a stale dir in the bucket |
.cache/ |
~1 GB | HF/pip cache |
build_nunchaku/ |
~0.8 GB | Nunchaku source build tree |
everything else (code, eval imgs/.png/.jpg, logs, json, report/, docs/, scripts/, flux2distill/) |
~1.3 GB | the actual source + results you usually want |
Takeaway: mjhq_ref (eval reference images, 0.1 GB) is NOT the weight — the 278 GB combined). To pull a usable working copy without the giant checkpoints, exclude
.pt checkpoints
are (*.pt (and the stale env/build dirs). The "code + models + eval artifacts, no checkpoints" recipe
(~34 GB) is:
hf sync hf://buckets/Mercity/FluxDistill ./ \
--exclude "outputs/*.pt" \
--exclude "outputs/eval/imgs/mjhq_ref/*" \
--exclude "build_nunchaku/*" \
--exclude ".venv/*" --exclude ".cache/*" \
--exclude "*__pycache__*" --exclude "*.pyc"
Drop --exclude "outputs/*.pt" to also fetch checkpoints (adds ~278 GB — slow link, many hours).
hf sync matching is Python fnmatch, where * crosses /, so outputs/*.pt matches
outputs/<exp>/quant_state.pt at any depth. Always --dry-run first and aggregate the plan by
size (not by guessing patterns) to confirm what's actually heavy before committing to a download.
Environment & commands
No build/lint/test suite — this is a script-driven research repo. Everything runs with
PYTHONPATH=. from the repo root.
⚠️ BOX REBUILT 2026-06-10 (third box). What changed vs the docs/reports written before:
- GPU: A100-80GB → RTX PRO 4500 Blackwell 32 GB (sm_120), driver CUDA 13.0. Plain/RTN/refine
builds (
18 GB) fit; eval peaks ~31 GB (fits only thanks to the 13_eval memory fixes below); the **whiten path (28 GB Gram on-GPU) is borderline** — prefer no-whiten cells here, or implement the TODO Gram-offload first. Upside: Blackwell IS the real INT4/FP4 kernel target. - NO
.venv— system python 3.11 (deliberate choice). torch 2.12.0+cu130 (the old cu126 advice is obsolete — cu126 has no sm_120 kernels and won't run this GPU), diffusers 0.39.dev (git, forFlux2*), transformers 5.10.2, torchao 0.17. Pip cache:/workspace/.cache/pip. - Model weights re-downloaded 2026-06-10 to
models/klein-4b/(~23 GB, public, no token —hf download, NOT the deprecatedhuggingface-cli). HF cache pinned to/workspace/.cache. - The eval axis SHIFTED (new kernels + transformers bump → different prompt embeddings / eval draws): the unchanged grid-best checkpoint re-evals at 0.0325, not its recorded 0.0446 (load verified key-complete). Old-box numbers are NOT comparable to new-box numbers — compare only within a box era; re-anchored baselines live in RESULTS.md (2026-06-10 section).
scripts/13_eval_svdquant.pygot 32-GB memory fixes (CPU-side state load; text encoder parked on CPU during the loss step; numerics untouched) andscripts/run_cell.shnow takes optional[SMOOTH] [ALPHA]args 5–6 and exportsPYTORCH_CUDA_ALLOC_CONF=expandable_segments:True.
⚠️ THIS IS AN EPHEMERAL POD — ONLY /workspace PERSISTS (it's the repo, backed by the HF bucket).
A restart wipes models/, the Python site-packages, AND the agent's ~/.claude/.../memory/ store.
So agent "memory" does NOT survive here — record every durable fact in THIS file (CLAUDE.md) or the
repo docs (README.md/RESULTS.md/TODO.md/report//docs/), which sync to
hf://buckets/Mercity/FluxDistill. Do not rely on /remember-style memory on this box.
After a restart, models/ and the Python stack must be rebuilt. To recover:
(a) hf download black-forest-labs/FLUX.2-klein-4B --local-dir models/klein-4b (~23 GB, public);
(b) reinstall torch==2.12.0 --index-url https://download.pytorch.org/whl/cu130, then
transformers==5.10.2 torchao==0.17 accelerate safetensors and diffusers from git (Flux2). Gotcha:
a leftover cu124 torchvision/torchaudio breaks the Qwen3ForCausalLM import (dead
torchvision::nms op) → Flux2KleinPipeline import fails; pip uninstall -y torchvision torchaudio
fixes it. Use PIP_CACHE_DIR=/workspace/.cache/pip. Builds are bit-deterministic on a fixed
box/stack: the 4 deleted W4A4 g64 champions rebuilt to exactly 0.0610/0.0620/0.0742/0.0759
(2026-06-13). NOTE: many quant_state.pt survive on disk (≈18 W4A8 + the 4 rebuilt W4A4), so
TODO.md's "only 3 SMOOTH=0 states survive" line is stale.
Caches: data/monet_cache/ (400 latents — used for BOTH training/eval AND the 300-img quant
calibration) survived the rebuild. data/monet_calib/ (the 2000-img full-res calib) does NOT
exist yet — only needed for the deferred 2k re-sweep (scripts/11). All outputs/abl_c300_*
grid cells incl. quant_state.pt survived. PDF-report deps (markdown+weasyprint + apt
libpango/cairo/poppler) are NOT reinstalled yet on this box.
NOTE: the shelved block-surgery .pt model states were deleted to reclaim space; their sample
images / logs / selection.json were kept (so block-surgery montages can be regenerated, weights cannot).
ACTIVE pipeline — W4A8 SVDQuant (scripts 11–13):
export PYTHONPATH=. # system python (no .venv since 2026-06-10)
# Preferred: one grid cell (build + eval) at a time, each with its own logs:
bash scripts/run_cell.sh 64 plain_refine 0 3 # args: RANK variant WHITEN REFINE [SMOOTH] [ALPHA]
bash scripts/run_cell.sh 64 plain_refine_nosmooth 0 3 0 # SMOOTH=0 (s=1, no migration)
# (variant name is just the output-dir tag; WHITEN/REFINE/SMOOTH/ALPHA are the real knobs)
# Or call the build/eval scripts directly (set OUT explicitly to avoid name collisions):
RANK=64 ALPHA=0.5 WGROUP=64 N_CALIB=300 WHITEN=0 REFINE=3 CALIB_DIR=data/monet_cache \
OUT=outputs/abl_c300_r64_plain_refine python3 -u scripts/12_build_svdquant.py
python3 -u scripts/13_eval_svdquant.py outputs/abl_c300_r64_plain_refine # vel-loss + 8 montages
python3 scripts/make_quant_report_assets.py # regen the analysis figures from the grid numbers
python3 scripts/build_report_pdf.py # assemble report/QUANT_REPORT.pdf (+ per-cell montages)
12env vars:RANK ALPHA WBITS ABITS WGROUP N_CALIB WHITEN REFINE CALIB_DIR MB OUT. Always setOUTwhen sweeping: the default dir tag is only_{plain|whiten}(ignores refine), so refine on/off would collide. The grid usesoutputs/abl_c300_r{R}_{plain|whiten|plain_refine|whiten_refine}. Falls back todata/monet_cacheifCALIB_DIRis absent (warns).13eval = SAME held-out first-16 metric as the surgery runs → directly comparable inRESULTS.md; it also renders 8 probe prompts (storefront text, mountain lake, fisherman, neon street, chalkboard text, breakfast counting flat-lay, hand/fingers, spiderweb macro) as teacher|quant montages.- Run experiments ONE AT A TIME with per-run logs + a Monitor (no batched bg loops); see the
saved memory. Plain build ~4 min, whiten ~7, whiten+refine ~10 (300-calib); eval ~2 min. The build
writes the 7.3 GB
quant_state.ptbeforequant_config.json, so wait for theDONE ->line before eval (race). The fake-quant state file is dead weight after eval (loss+montages are saved) — safe to delete to reclaim space if quota is tight.
SHELVED pipeline — block surgery (scripts 01–10), run in order:
export PYTHONPATH=.
python3 scripts/01_inspect_model.py # introspect transformer (block names, param counts)
python3 scripts/02_teacher_smoke.py # sanity: teacher 4-step generation
python3 scripts/09_build_linattn.py 10 4 # BUILD student: drop_k=10, FFN on deepest 4 blocks
python3 scripts/04_gen_eval.py baseline # teacher-vs-student image pairs across prompts
python3 scripts/06_cache_data.py 400 # cache N monet images @512 -> VAE latents
python3 scripts/10_bench.py # measure inference speedup vs teacher
# RECOVERY TRAINING (the core loop). Knobs are env vars + positional args:
STUDENT_DIR=outputs/student_linattn4 SCHED=cosine MB=8 ACCUM=2 \
python3 -u scripts/08_train_recover.py 300 adamw 1e-4
# ^steps ^opt(adamw|muon) ^base_lr
- Always run training with
python3 -u(unbuffered) so the per-step log file updates live. 08_train_recover.pyenv vars:STUDENT_DIR(which built student to recover),SCHED(cosine|constant),MB/ACCUM(micro-batch / grad-accum). It logs tooutputs/train_<tag>/train.logand savesstudent_best.pt/student_final.pt+ per-step samples.- To watch a long run, tail the
eval_vel_loss=lines in itstrain.log. make_report_assets.pyregenerates the report graphs/montages from the logs.
There are two superseded scripts kept as record: 03_build_student.py (v1 per-token surgery, produced
a collapsed model) and 07_train.py (the flawed run that trained all weights and diverged). Use
05_build_student_v2.py / 09_build_linattn.py for builds and 08_train_recover.py for training.
Architecture — ACTIVE quant track (flux2distill/svdquant.py)
The quant pipeline is calibrate → decompose → quantize → eval, all PTQ (no training):
1. SVDQuantLinear replaces an nn.Linear. Forward = (x̂·L₁·L₂)[bf16 low-rank] + Q8(x̂)·Q4(R)[4-bit residual] + b, where x̂ = x/smooth. Buffers: smooth (in,), lora_down
(r,in), lora_up (out,r), w_res (out,in, fake-quant'd 4-bit). compressed_bytes() reports the
size a real low-bit packing would reach (the fake-quant buffers themselves are bf16).
2. from_linear does the decomposition (smooth → low-rank → 4-bit residual): the s_j smoothing
factor, then the low-rank fit, then group-wise 4-bit residual. Two SVD modes + refinement:
- whitened SVD (
whiten=True, default): minimizes OUTPUT error‖X̂(Ŵ−L)‖via an eigen square-rootMof the smoothed-act GramĜ=X̂ᵀX̂(SVD ofŴ·M, map back byM⁻¹). Usestorch.linalg.eigh+ eigenvalue clamp, NOT Cholesky (the bf16 Gram is non-PD → Cholesky crashes). Per-layer try/except → plain-SVD fallback.whiten=False= plain SVD ofŴ(base paper). - iterative refinement (
refine_iters, default 3, SVDQuant §4.2): re-fitLtoŴ−Q(R)to absorb the 4-bit rounding error, keep the best iterate. Free at inference, build-time only.
3. collect_act_stats registers forward-pre-hooks that accumulate per-channel absmax (for
smooth) AND the raw Gram XᵀX (for whitening) in ONE calibration pass — the smoothed Gram is
Ĝ = Gᵣ⊘(s·sᵀ) since smoothing is diagonal. Gram is ~18 GB fp32 on-GPU for the 100 layers.
4. apply_svdquant_from_stats quantizes every target Linear (the 100 inside the 25 blocks;
embedders/norms/proj_out stay bf16). quant_config.json is the build↔load contract — records
per-layer specs (shape/rank); apply_svdquant_empty rebuilds empty modules from it so the saved
state_dict round-trips via load_state_dict.
Critical gotchas — quant track
- System python since 2026-06-10 (no
.venv); torch 2.12.0+cu130. Old/new eval numbers don't mix — the axis shifted with the box (see RESULTS.md 2026-06-10); compare within-box only. - 32 GB VRAM ceiling. Eval peaks
31 GB even after the memory fixes; don't add residents to28 GB) are borderline — prefer plain cells or do the Gram-offload TODO.scripts/13. Whiten builds ( - Cholesky is wrong here. The smoothed-act Gram is non-PD (bf16 accumulation + dead channels) →
_LinAlgError. Use the symmetric eigen square-root with eigenvalue clamping (same math, robust). - Build/eval race.
scripts/12saves the 7.8 GBquant_state.ptthenquant_config.json; launching13too early hitsFileNotFoundErroron the config. Wait for theDONE ->log line. - rank = quality knob, NOT speed. Low-rank branch is ~r/in ≈ 0.5–2% of the GEMM; rank costs memory (bf16 branch), not compute. The SPEED knob is bit-width (W4A8 vs W4A4). No real speedup on this A100 — fake-quant measures quality; throughput needs Ada/Blackwell fused kernels.
- SmoothQuant is NOT a free win at W4A8 — it hurts (α=0.5). Migration helps low-bit activations;
at A8 the acts are already easy, so smoothing only widens the 4-bit weight spread → worse. RTN
(
SMOOTH=0, s=1) beat smoothed rank-0 by 21% and beat smoothed SVD cells.SMOOTHenv onscripts/12(default 1;from_linear(..., smooth=False)sets s=1). Also note: α=0 ≠ no-smooth (α=0 →s=1/max|W|, the all-on-activations extreme); true no-smooth isSMOOTH=0. Tune α / disable smoothing before trusting any W4A8 number. The α=0.5 grid is mis-tuned (open: re-run no-smooth/low-α). - Whitening is NOT a free win at low calib. The 2026-06-01 full grid showed whitening-alone is non-monotonic in rank (hurts r16 & r64, helps r32) because it overfits the noisy 300-img Gram. Refine is the dependable upgrade; whitening only helps paired with refine or at moderate rank. Don't assume "more upgrades = better" — verify per (rank, calib-size). The open question (TODO ★): does a 2000-img calib stabilize whitening? Until then, at high rank prefer plain+refine (no Gram).
- Set
OUTper run. The build's default dir tag ignoresREFINE, so plain vs plain_refine (or whiten vs whiten_refine) silently overwrite each other. The grid encodes all knobs in the dir name. hf download, nothuggingface-cli(deprecated, refuses to run) — and pin torch to cu130 (this box: CUDA-13.0 driver, Blackwell sm_120; the older cu126 advice was for the destroyed A100 box).
Architecture — SHELVED surgery track (the big picture, requires reading multiple files)
The whole system is a 3-stage loop over the flux2distill/ package: build a student (surgery) →
recover it (training) → evaluate. The non-obvious glue:
1. The teacher is a Flux2KleinPipeline; we operate on pipe.transformer. Its denoiser has
transformer_blocks (5 double-stream blocks) and single_transformer_blocks (20 single-stream
blocks), d=3072. All surgery targets the single blocks (they are ~78% of compute; the 5 double
blocks do fragile cross-modal binding and are left untouched). Single blocks are referenced by index
S0..S19 (S0 = first/shallowest, S19 = last/deepest — see plan.md's block convention).
2. Surgery (surgery.py) = pick least-useful single blocks and swap them for surrogates:
importance_by_ablation (leave-one-out: skip each block, measure final-latent change — the preferred
selector) → select_blocks_by_importance → attach_surrogates replaces single_transformer_blocks[i]
in place. A surrogate must satisfy the single-block forward contract (in the model loop it's
called with encoder_hidden_states=None, split_hidden_states=False, and must return a single
tensor — see how LinearAttentionSurrogate.forward mirrors Flux2SingleTransformerBlock).
3. Surrogate types (surrogate.py) — two kinds via make_surrogate(kind=...):
lowrank: per-tokenx + B·σ(A·x). Cannot mix tokens → fine for warm-start but collapses under aggressive pruning (this is the key finding). Closed-formlstsq_lowrank_init.linear_attention: O(N) linear attention with RoPE (head_dim 128, reuses the model's rotary), a depthwise-conv local branch, a focused (learnable-exponent) feature map, and an optional FFN (use_ffn). This is the working surrogate. Output projection zero-inits to identity.
4. selection.json is the contract between build and load. Every build writes
outputs/<student>/{student_state.pt, selection.json}. selection.json records surrogate_idx,
kind, and per-surrogate params (heads, head_dim, conv_kernel, ffn_hidden, and ffn_idx = the
subset of dropped blocks that get an FFN). Loaders (eval_utils.load_student, 08_train_recover.py)
reconstruct the student structure from selection.json via attach_surrogates, then
load_state_dict — so the saved state_dict only round-trips if you attach with the same config.
5. Recovery training (08_train_recover.py) — the critical rule: FREEZE everything, train ONLY the
surrogates. It loads the frozen teacher (pipe.transformer) and a separate student, sets
requires_grad=False on the whole student, then re-enables grad only on the surrogate modules
(casting those to fp32 = master weights; bf16 autocast compute). Loss = velocity matching to the
teacher (losses.py) + a light real-data flow-matching term. Optimizer split lives in
model_utils.build_param_groups (Muon on 2D weights, AdamW on the rest) but for surrogate-only
recovery a single AdamW @1e-4 is the validated default. The metric is a fixed held-out velocity-
matching loss logged as eval_vel_loss=.
Critical gotchas / hard-won rules — SHELVED surgery track
- Never train all weights. Training the pretrained (kept) blocks at any meaningful LR diverges to noise. Freeze the base; only surrogates train. AdamW @1e-4 (adapter regime), not Muon's 0.02.
- fp32-master surrogates + the pipeline → dtype clash. The student's surrogates are fp32 but the
pipeline runs other modules in bf16, so any
pipe(...)generation with that student must be wrapped intorch.autocast("cuda", dtype=torch.bfloat16)(seesample()in08). Training forwards already are. - Cosine LR floors at 30% of base (
MIN_LR_FRAC), never decays to 0 (a dead tail breaks resume/extend; constant LR jitters at the floor). - Surrogate quality vs speed/drop-count is a real frontier. Linear-attn recovers far better than
per-token; the FFN improves quality but is heavy (kills the speedup from dropping more). More
dropped blocks = faster + worse. See the frontier table in
TODO.md/report/. jinja2>=3.1is required for the Qwen3 chat template (older versions crashencode_prompt).- At 512px the token sequence is fixed: 512 text + 1024 image = 1536; the VAE latent is 32-channel,
packed to 128-ch / 1024 tokens.
pipe._encode_vae_image+pipe._pack_latentsis the encode path.
Xet Storage Details
- Size:
- 27.2 kB
- Xet hash:
- 1dc1c3dc13d0d76b74163f8c1297d5c9cb00738b998cce1cd81c3ee688dcd82d
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.