Buckets:

Mercity/FluxDistill / CLAUDE.md
Pranav2748's picture
|
download
raw
27.2 kB

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.pyscripts/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.dequantizecosine ≈ 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 .pt checkpoints are (278 GB combined). To pull a usable working copy without the giant checkpoints, exclude *.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:

  1. 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.
  2. 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, for Flux2*), transformers 5.10.2, torchao 0.17. Pip cache: /workspace/.cache/pip.
  3. Model weights re-downloaded 2026-06-10 to models/klein-4b/ (~23 GB, public, no token — hf download, NOT the deprecated huggingface-cli). HF cache pinned to /workspace/.cache.
  4. 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).
  5. scripts/13_eval_svdquant.py got 32-GB memory fixes (CPU-side state load; text encoder parked on CPU during the loss step; numerics untouched) and scripts/run_cell.sh now takes optional [SMOOTH] [ALPHA] args 5–6 and exports PYTORCH_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)
  • 12 env vars: RANK ALPHA WBITS ABITS WGROUP N_CALIB WHITEN REFINE CALIB_DIR MB OUT. Always set OUT when sweeping: the default dir tag is only _{plain|whiten} (ignores refine), so refine on/off would collide. The grid uses outputs/abl_c300_r{R}_{plain|whiten|plain_refine|whiten_refine}. Falls back to data/monet_cache if CALIB_DIR is absent (warns).
  • 13 eval = SAME held-out first-16 metric as the surgery runs → directly comparable in RESULTS.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.pt before quant_config.json, so wait for the DONE -> 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.py env vars: STUDENT_DIR (which built student to recover), SCHED (cosine|constant), MB/ACCUM (micro-batch / grad-accum). It logs to outputs/train_<tag>/train.log and saves student_best.pt/student_final.pt + per-step samples.
  • To watch a long run, tail the eval_vel_loss= lines in its train.log.
  • make_report_assets.py regenerates 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-root M of the smoothed-act Gram Ĝ=X̂ᵀX̂ (SVD of Ŵ·M, map back by M⁻¹). Uses torch.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-fit L to Ŵ−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 to scripts/13. Whiten builds (28 GB) are borderline — prefer plain cells or do the Gram-offload TODO.
  • 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/12 saves the 7.8 GB quant_state.pt then quant_config.json; launching 13 too early hits FileNotFoundError on the config. Wait for the DONE -> 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. SMOOTH env on scripts/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 is SMOOTH=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 OUT per run. The build's default dir tag ignores REFINE, 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, not huggingface-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_importanceattach_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-token x + B·σ(A·x). Cannot mix tokens → fine for warm-start but collapses under aggressive pruning (this is the key finding). Closed-form lstsq_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 in torch.autocast("cuda", dtype=torch.bfloat16) (see sample() in 08). 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.1 is required for the Qwen3 chat template (older versions crash encode_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_latents is 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.