Skip to content

Phase 2: consolidation — one registry, one backbone, real MM-DiT, one trainer#12

Open
AshishKumar4 wants to merge 6 commits into
revamp/phase-1-correctnessfrom
revamp/phase-2-consolidation
Open

Phase 2: consolidation — one registry, one backbone, real MM-DiT, one trainer#12
AshishKumar4 wants to merge 6 commits into
revamp/phase-1-correctnessfrom
revamp/phase-2-consolidation

Conversation

@AshishKumar4

@AshishKumar4 AshishKumar4 commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Third PR of the revamp series — stacked on #11 (which stacks on #10). Review in order; this diff shows only Phase 2 commits.

What

Deletions first (chore: delete dead code, −1,984 lines): favor_fastattn (722 lines, unimportable), unet_3d* (950 lines, unreachable), dead layers (AdaLNZero, PositionalEncoding, TimeEmbedding, PixelShuffle, utils.RMSNorm), the never-called broken map_nested_config, duplicated global-array helpers, dead tensorboard stubs, an if/else with identical branches.

One model registry: training.py and inference each had their own architecture table, three drifted copies of the dtype/precision/activation maps (one had a 'highes' typo), and two independent +2d/+hilbert/+zigzag parsers — a model was constructed through different code paths at train and inference time. flaxdiff.models.registry now owns all of it; configs stay plain strings into the wandb log and build_model resolves them identically on both sides. The hand-drifted --architecture choices list is gone.

One DiT backbone: the patchify/scan/sincos/conditioning/output sandwich was repeated near-verbatim across 4 model files, and the adaLN-Zero block was copy-pasted 3×. Now: dit_common.py owns the sandwich + a single ModulatedBlock (mixer = attention | S5), s5.py owns the SSM layers. SimpleDiT went 291→88 lines, HybridSSMAttentionDiT 597→139. Non-square patch grids now work (the audit's last xfail — the suite is now xfail-free), and attention gains optional SD3-style qk-norm.

Real MM-DiT: the old SimpleMMDiT mean-pooled text into the adaLN vector (text never entered the token sequence) and ran attn+MLP in parallel off one norm — a text-modulated DiT at half its nominal depth, verified by ablation. Now a true dual-stream block: per-modality qkv/mlp/modulation, joint attention over concat([txt, img]), RoPE on image tokens only, adaLN-modulated final layer. HierarchicalMMDiT is ported onto it (raster-only: its merge/expand reshapes assume row-major order, so the old hilbert option was scrambling the merged neighborhoods). AdaLNParams gains the SiLU the DiT paper specifies. A behavior test pins the dual-stream property.

One concrete trainer: the legacy DiffusionTrainer middle class was a fork whose train/validation overrides were dead (its validation couldn't even construct a sampler) — only TrainState/generate_states/fit were live, and those now sit on GeneralDiffusionTrainer directly. The broken AutoEncoderTrainer (copy-pasted diffusion step referencing attributes that were never set) is deleted. Trainer smoke tests now run the real trainer end to end and assert step counter, adam moments and EMA all survive a save/restore round-trip.

Conversion script (scripts/convert_legacy_checkpoint.py) remaps old DiT-family checkpoints to the new param layout.

Scope notes (deviations from the plan, deliberate)

  • The Objective seam is deferred to the JEPA PR where its second implementation exists — an interface with one implementer fails the deletion test today.
  • Grain iterator checkpointing rides the Phase 3 PR: it builds on the same orbax rework Phase 3 does for sharded checkpoints.

Verification

pytest -m "not network": 61 passed, 0 xfailed — every bug documented in Phase 0's strict xfails is now fixed or the feature deleted. Net diff for the phase: +1,331 −4,096.

🤖 Generated with Claude Code

AshishKumar4 and others added 6 commits July 22, 2026 23:59
- favor_fastattn.py (722 lines, zero importers, hard-depends on
  undeclared gin/absl, contains a live typo)
- unet_3d.py + unet_3d_blocks.py (950 lines, unreachable since the
  models/__init__ import was commented out; video support will be
  rebuilt from the shared backbone)
- vit_common: AdaLNZero and PositionalEncoding (nothing constructs them)
- common: TimeEmbedding (superseded by FourierEmbedding, NaN for
  features=2) and PixelShuffle (only reference was a commented line)
- utils: RMSNorm (nn.RMSNorm is used everywhere), the never-called
  map_nested_config that silently dropped every non-string value, and
  the string maps that fed it
- trainer: the duplicated form_global_array/convert_to_global_tree copy
  (the flaxdiff.utils one is the survivor), dead tensorboard stubs,
  write-only last_save_time, a 39-line commented-out block
- simple_vit: an if/else with identical branches and the dead use_remat
  flag

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uction

training.py and the inference pipeline each carried their own
architecture table, their own dtype/precision/activation string maps
(three copies total, already drifted: a 'highes' typo on one side,
extra keys on another), and their own +2d/+hilbert/+zigzag suffix
parser. A model was therefore constructed through two different code
paths at train and inference time.

flaxdiff.models.registry now owns the architecture table, the string
mapping, suffix canonicalization, and field-filtered construction.
Model configs stay as plain strings all the way into the logged wandb
config, and build_model resolves them identically on both sides - the
logged config IS the construction input. The hand-maintained
--architecture choices list (which had already drifted from the
inference table) is gone; the registry validates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The patchify/scan-order/sincos/conditioning/final-head skeleton was
repeated near-verbatim in SimpleDiT, HybridSSMAttentionDiT, SimpleUDiT
and SimpleMMDiT, and the adaLN-Zero block body was copy-pasted three
times (attention, SSM, and dual-cond variants). dit_common.py now owns
the sandwich (PatchSequenceEmbed / ConditioningEmbed /
PatchSequenceOutput) and a single ModulatedBlock with a pluggable mixer
(attention | S5 ssm); the S5 layers move to s5.py so the block can use
them without importing the full hybrid model. SimpleDiT (291 -> 90
lines) and HybridSSMAttentionDiT (597 -> 142) are now just block
arrangements; SimpleUDiT builds its U-skips from the same block.

Also: unpatchify takes an explicit patch grid so non-square inputs work
(the last remaining xfail from the audit - the whole suite is now
xfail-free), and attention gains optional SD3-style qk-norm (RMSNorm on
q/k) for bf16 logit safety, exposed on every DiT-family model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SimpleMMDiT was MM-DiT in name only: text was mean-pooled into the adaLN
vector and never entered the token sequence, attention and MLP ran in
parallel off a single norm (verified by ablation - zeroing attention
changed the MLP contribution by ~2e-6), so it was a text-modulated DiT at
roughly half its nominal depth. The block is now a true SD3-style
dual-stream MM-DiT: per-modality qkv/mlp/modulation weights, joint
attention over concat([txt, img]) with RoPE on the image tokens only,
sequential attn -> mlp, and an adaLN-modulated final layer.
HierarchicalMMDiT is ported onto the same block (raster only - its
merge/expand grid reshapes assume row-major order, so the old hilbert
option was scrambling the neighborhoods being merged). AdaLNParams gains
the missing SiLU before its zero-init projection, per the DiT paper.

A behavior test pins the dual-stream property: changing text tokens must
reach the image stream through joint attention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iner

GeneralDiffusionTrainer overrode every method the legacy DiffusionTrainer
middle class added except TrainState/generate_states/fit - the middle
class was a fork whose train/validation code was dead (its validation
step could not even construct a sampler post the API change) yet still
shipped. Those three live pieces now sit directly on
GeneralDiffusionTrainer, the middle class and the broken
AutoEncoderTrainer (a copy-paste of the diffusion step referencing
attributes that were never set) are deleted, and SimpleTrainer keeps
only generic infra with abstract step definitions instead of toy
classification steps no real path used.

The trainer smoke tests now run the real GeneralDiffusionTrainer end to
end (tiny DiT, unconditional synthetic data): fit without wandb, and a
save/restore round-trip asserting step counter, adam moments and EMA
params all survive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remaps the top-level module paths the backbone consolidation moved
(embed/, conditioning/, output/) so pre-consolidation DiT-family
checkpoints load against the new models. UNet/UViT need no conversion;
MMDiT checkpoints are refused implicitly since the architecture itself
changed. Documents the FourierEmbedding frequency provenance caveat for
pre-jax-0.5 checkpoints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant