Mage flow pipeline - #14295
Conversation
Integrate Microsoft's Mage-Flow 4B text-to-image model based on the NR-MMDiT dual-stream architecture into diffusers. This adds: - MageFlowTransformer2DModel: dual-stream MMDiT with complex RoPE, symmetric frequency scaling, and joint attention via dispatch_attention_fn - AutoencoderMageVAE: DConv encoder + CoD decoder with 128-channel latents and 16x spatial downsampling, including adaLN constant folding - MageFlowPipeline: text-to-image pipeline using Qwen3-VL text encoder with ChatML prompt template, FlowMatchEulerDiscreteScheduler, and CFG - Weight conversion script for original HuggingFace checkpoint format - Pipeline and model tests (66 passed, 5 skipped edge cases) - API documentation for pipeline and transformer model Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace diffusers' built-in Timesteps (float32 frequency table) with a custom sinusoidal embedding that downcasts the frequency table to the input dtype before computing the embedding. The model was trained with this exact bf16 rounding — using float32 frequencies produces visibly degraded (blurry) outputs because the small per-step error compounds over 30 denoising steps. Before fix: bf16 transformer forward cos_sim=0.65 vs original After fix: bf16 transformer forward cos_sim=0.99999994 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused `encoder_hidden_states_mask` and `txt_ids` params from transformer forward (models.md: don't declare params you ignore) - Remove dead `temb + zeros` no-op in transformer forward - Add `latent_height`/`latent_width` params to avoid .item() GPU-CPU sync in RoPE computation (torch.compile compatibility) - Add comment explaining custom sigma schedule in pipeline - Fix pipeline docstring: AutoencoderKL -> AutoencoderMageVAE - Fix test imports to use public API (from diffusers import ...) - Replace hardcoded developer paths in conversion script docstring Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Transformer: - Store RoPE frequencies as real-valued registered buffers instead of plain complex attributes (NB1: model.to(device) now works correctly) - Remove dead scale_rope=False branch (always True under default config) - Add main_input_name class attribute for layerwise casting tests - Use num_layers=2 in test config for processor count validation VAE: - Add docstring to _MageVAEYEmbedder explaining weight-key compatibility - Replace channel-count dispatch with explicit is_latent parameter (NB3) - Add _no_split_modules class attribute (NB4) - Add generator parameter to encode() for reproducibility (NB5) - Replace torch.bmm with F.scaled_dot_product_attention in AttnBlock (NB9) - Remove dead nn.Identity() layer from MageVAEDecoder - Remove duplicate forward() method; add forward() that delegates to decode() so model_cpu_offload hooks trigger correctly Pipeline: - Fall back to no-CFG when text_encoder unavailable and no negative_prompt_embeds provided (fixes test_encode_prompt_isolation) - Call self.vae() instead of self.vae.decode() for CPU offload compat Tests: - Skip test_model_parallelism (fixed-size RoPE buffers dominate the tiny test model, preventing meaningful multi-GPU split) Result: 70 passed, 0 failed, 9 skipped Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Override test_gradient_checkpointing_is_applied with correct expected_set. Result: 71 passed, 0 failed, 8 skipped (torch.compile needs 2.7.1+) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Isolate .item()-based grid size inference into a static method called before pos_embed.forward, and skip fullgraph torch.compile tests that require pure-tensor RoPE (to be addressed in a follow-up). Also restore .item() fallback in PosEmbed for non-pipeline callers. Result: 73 passed, 0 failed, 3 skipped Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
We have reviewed the PR and confirmed that the implementation is correct. The Mage-Flow integration looks good, including the transformer, VAE, pipeline, weight conversion, tests. From our side, the PR is ready and can be merged into Diffusers. Thanks for the contribution! |
There was a problem hiding this comment.
🤗 Serge says:
Thorough port, but there are several correctness gaps and repo-convention violations that need fixing before merge.
Correctness
prompt_embeds_maskis computed but never used.encode_promptreturns a mask,check_inputswarns loudly about providing it, but the denoising loop never passes anyattention_maskto the transformer —MageFlowAttnProcessoracceptsattention_maskyet the pipeline callsself.transformer(...)without it. With batched prompts of differing lengths the padded text tokens will participate in joint attention. Either wire the mask through (joint_attention_kwargs/an explicit arg) or drop the mask plumbing and the warnings entirely (the repo rules forbid unused code paths).prompt_embeds_maskreshape is wrong.prompt_embeds_mask.repeat(1, num_images_per_prompt, 1)is applied to a 2-D[B, S]tensor —repeatwith 3 dims will unsqueeze to[1, B, S*num_images]and then the.view(batch*num_images, seq_len)only accidentally works fornum_images_per_prompt == 1. QwenImage has the same latent bug but it is guarded differently; here it will raise/mis-shape fornum_images_per_prompt > 1.negative_prompt_embeds_maskis discarded. In__call__the negative branch assigns into localnegative_prompt_embeds_maskbut it is never used afterwards, same as above.- VAE
encodeis dead code and off-convention.AutoencoderMageVAE.encodereturns a bare tensor (all other autoencoders returnEncoderOutput/AutoencoderKLOutputwithreturn_dict, and are decorated with@apply_forward_hook), and no caller in the repo uses it.forwardalso just forwards todecodeinstead of the encode→decode round trip every other autoencoder implements — this will silently break generic tooling/tests that assume the standard VAE interface. freeze_adaln()is a manual, mutating post-load step. The docstring in the PR description requires users to callvae.freeze_adaln()after loading. This mutates module structure (replacingadaLN_modulationwith a buffer holder), which breakssave_pretrained/reload round trips and offloading/device_mappaths, and it is never called from the pipeline. Either fold this into the model at construction time based on config, or drop it.MageFlowPosEmbed.forwardignoresimg_idsexcept for.device, and the grid size is recovered via.item()onimg_idsin the modelforward— which is exactly why threeTorchCompileTesterMixintests had to be disabled. Passing latentheight/widthdown from the pipeline (which already computeslatent_h/latent_w) would remove both the host sync and the disabled tests.- Hard-coded 4096 RoPE tables cap resolution silently:
_compute_video_freqsslicesfreqs_pos[1][: height // 2], so heights/widths above 8192 latent units silently truncate instead of erroring. Also these non-persistent buffers inflate the tiny test model enough thattest_model_parallelismhad to be disabled — a symptom worth fixing rather than skipping. _apply_rotary_emb_complexforces fp32 complex math per block per step; fine functionally, but notex_passisNone-checked via a branch that the rules discourage — minor.
Repo conventions / release plumbing
- Missing dummy objects.
MageFlowTransformer2DModel/AutoencoderMageVAEare not inutils/dummy_pt_objects.pyandMageFlowPipeline/MageFlowPipelineOutputare not inutils/dummy_torch_and_transformers_objects.py.make fix-copies/python utils/check_dummies.pywill fail CI. - Alphabetical ordering broken in
src/diffusers/__init__.py:MageFlowPipeline/MageFlowPipelineOutputare inserted beforeLumina2Pipelinein both the_import_structurelist and theTYPE_CHECKINGblock. AutoencoderMageVAEhas no doc page but is exported from the public init —utils/check_repo.py::check_all_objects_are_documentedwill flag it (and there is no_toctree.ymlentry for it).- Missing
AutoencoderMageVAEmodel tests..ai/testing.mdrequires model-level tests for every new model class; only the transformer has one. Notests/models/autoencoders/test_models_autoencoder_mage_vae.pywas added, contrary to the description's "tests (70 passed / 0 failed)" claim covering the new components. pretrained_model_name_or_pathleft as""with aTODOin the transformer tester config, and the docstring example points atmicrosoft/Mage-Flow-4Bwhich is not referenced anywhere else — the PR ships no usable checkpoint reference.- Typing/import nits:
pipeline_mage_flow.pyimportsnumpyonly for thenp.linspacesigma default;transformer_mage_flow.pyuses a mutable defaultaxes_dim: list[int] = [16, 48, 48]. Docstring defaults (context_in_dim=3584,num_layers=32) disagree with the conversion script's real config (2560,12). - Import order in the pipeline test (
Qwen3VLConfig, Qwen3VLForConditionalGeneration, Qwen2Tokenizer) isn't isort-clean;make stylehasn't been run. - Conversion script style:
key[len(VAE_ENCODER_PREFIX):]slices will be reformatted by ruff (E203-style spacing), and_diffusers_version: "0.37.0"is hard-coded rather than read fromdiffusers.__version__.
Minor
docs/source/en/api/pipelines/mage_flow.mduses.mdsuffixes in doc links (../../using-diffusers/schedulers.md); the prevailing convention in this folder is extension-less links, and it uses<Tip>where newer pages use>blockquotes.MageFlowAttnProcessor.__init__raising on missingF.scaled_dot_product_attentionis defensive dead code — torch>=2 is a hard requirement.MageVAENerfEmbedder._pos_cacheis an unbounded dict keyed by(patch_size, device, dtype); harmless in practice but it's mutable state on a module that won't move with.to().
serge v0.1.0 · model: claude-opus-5 · 24 LLM turns · 36 tool calls · 1069.6s · 2205133 in / 82286 out tokens
|
|
||
| if prompt_embeds_mask is not None: | ||
| prompt_embeds_mask = prompt_embeds_mask[:, :max_sequence_length] | ||
| prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_images_per_prompt, 1) |
There was a problem hiding this comment.
prompt_embeds_mask is 2-D ([batch, seq_len]), so repeat(1, num_images_per_prompt, 1) prepends a broadcast dim and produces [1, batch, seq_len * num_images_per_prompt]. The following .view(batch_size * num_images_per_prompt, seq_len) then only happens to work when num_images_per_prompt == 1 and will mis-shape (or raise) otherwise.
| prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_images_per_prompt, 1) | |
| prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_images_per_prompt) |
| encoder_hidden_states=prompt_embeds, | ||
| timestep=timestep / 1000, | ||
| img_ids=img_ids, | ||
| joint_attention_kwargs=self.attention_kwargs, |
There was a problem hiding this comment.
The masks returned by encode_prompt (prompt_embeds_mask, negative_prompt_embeds_mask) are never forwarded to the transformer, so padded text tokens fully participate in joint attention whenever a batch contains prompts of different lengths. MageFlowAttnProcessor already accepts attention_mask, and check_inputs warns users about supplying the mask — but nothing consumes it.
Either thread the mask through to the attention processor, or remove the mask plumbing and the associated check_inputs warnings entirely (the project rules disallow unused code paths).
| bottleneck_dim=decoder_bottleneck_dim, | ||
| ) | ||
|
|
||
| def encode(self, x: torch.Tensor, generator: torch.Generator | None = None) -> torch.Tensor: |
There was a problem hiding this comment.
encode is not called anywhere in the repo (the pipeline is T2I-only and uses self.vae(...) for decode), and it deviates from the autoencoder contract used everywhere else: no return_dict, no EncoderOutput/AutoencoderKLOutput wrapper, and no @apply_forward_hook. Either bring it in line with e.g. AutoencoderRAE.encode (return EncoderOutput, support return_dict, add the forward hook) or drop it for the initial integration.
Relatedly, forward on line 709 just delegates to decode, whereas every other autoencoder's forward does encode→decode. Generic VAE tooling/tests that assume the standard interface will silently do the wrong thing here.
| return (sample,) | ||
| return DecoderOutput(sample=sample) | ||
|
|
||
| def freeze_adaln(self): |
There was a problem hiding this comment.
freeze_adaln() mutates the module tree after load (swapping adaLN_modulation MLPs for a buffer holder). That breaks save_pretrained/from_pretrained round trips (the folded state dict no longer matches the class-constructed module), and interacts badly with device_map/offloading hooks. It's also never called from MageFlowPipeline, so users must remember the manual step documented in the PR description.
Please either perform the folding at construction time driven by config, or drop it — a post-hoc structural mutation isn't something the rest of the library supports.
| freqs = torch.cat([freqs_frame, freqs_height, freqs_width], dim=-1).reshape(seq_len, -1) | ||
| return freqs.clone().contiguous() | ||
|
|
||
| def forward(self, img_ids: torch.Tensor, height: int = None, width: int = None) -> torch.Tensor: |
There was a problem hiding this comment.
forward ignores img_ids entirely except for .device, taking the grid from the height/width args instead. Combined with _infer_grid_size calling .item() at line 601, this is what forces three torch.compile tests to be disabled and adds a host sync every step.
The pipeline already knows latent_h/latent_w — pass them through as explicit arguments (like Flux's img_shapes) and drop img_ids/_infer_grid_size rather than round-tripping the grid through a tensor and back.
|
|
||
| @property | ||
| def pretrained_model_name_or_path(self): | ||
| return "" # TODO: Set Hub repository ID |
There was a problem hiding this comment.
pretrained_model_name_or_path is still an unfilled TODO, which means any tester relying on it (e.g. the from_pretrained-based paths) can't actually load anything. Please point this at a tiny repo (a personal one is fine for review; a maintainer can move it under hf-internal-testing/ later) — .ai/testing.md calls this out explicitly.
Separately: .ai/testing.md requires model-level tests for every new model class, and AutoencoderMageVAE has none (tests/models/autoencoders/test_models_autoencoder_mage_vae.py is missing). The description's claim of full test coverage isn't supported by the diff.
| # test model's parameter count, making it impossible to split the model across | ||
| # GPUs at the granularity accelerate uses. Skip multi-GPU parallelism for the | ||
| # tiny test config; real-size models split correctly. | ||
| test_model_parallelism = None |
There was a problem hiding this comment.
Disabling test_model_parallelism because the fixed 4096-entry RoPE buffers dominate the tiny model is treating a design issue as a test problem. Those buffers are non-persistent and sized independently of the config; computing frequencies on demand for the requested grid (or sizing the table from config) would both fix the parallelism split and remove the compile-time .item() graph break flagged below.
| # limitations under the License. | ||
|
|
||
| import torch | ||
| from transformers import Qwen3VLConfig, Qwen3VLForConditionalGeneration, Qwen2Tokenizer |
There was a problem hiding this comment.
Import order isn't isort-clean (Qwen2Tokenizer should precede Qwen3VLConfig), which suggests make style wasn't run before opening the PR.
| from transformers import Qwen3VLConfig, Qwen3VLForConditionalGeneration, Qwen2Tokenizer | |
| from transformers import Qwen2Tokenizer, Qwen3VLConfig, Qwen3VLForConditionalGeneration |
|
|
||
| <Tip> | ||
|
|
||
| Make sure to check out the Schedulers [guide](../../using-diffusers/schedulers.md) to learn how to explore the tradeoff between scheduler speed and quality, and see the [reuse components across pipelines](../../using-diffusers/loading.md#reuse-a-pipeline) section to learn how to efficiently load the same components into multiple pipelines. |
There was a problem hiding this comment.
Doc links in this folder are written without the .md extension (../../using-diffusers/schedulers, ../../using-diffusers/loading#reuse-a-pipeline) — the .md suffix produces broken links in the built docs. Newer pages also use a > blockquote rather than <Tip>.
|
|
||
| TRANSFORMER_DIFFUSERS_CONFIG = { | ||
| "_class_name": "MageFlowTransformer2DModel", | ||
| "_diffusers_version": "0.37.0", |
There was a problem hiding this comment.
_diffusers_version is hard-coded here and in VAE_DIFFUSERS_CONFIG/MODEL_INDEX; read it from diffusers.__version__ instead so converted repos don't claim a stale version.
Also key[len(VAE_ENCODER_PREFIX):] (lines 105/111) will be reformatted by ruff — please run make style over this script.
|
Is edit pipeline included? |
|
thanks so much for the PR @chenyangzhu1 ! Could you convert the pipeline to Modular Diffusers? We are currently trying to do modular-only releases whenever possible — as long as we're not under release time pressure, we prefer new integrations to be released in modular pipeline from the start (it should be a pretty straightforward task for your AI agent if you're using one! ) |
| "LTXLatentUpsamplePipeline", | ||
| "LTXPipeline", | ||
| "LucyEditPipeline", | ||
| "MageFlowPipeline", |
There was a problem hiding this comment.
Note that you should also include this in https://github.com/alvarobartt/diffusers/blob/a8c1c08f42f321ca853f8f4349ba3ede0ca4358b/src/diffusers/pipelines/auto_pipeline.py#L138-L191 so that the AutoPipelineForText2Image can be leveraged for this model; it's really just about adding this entry:
("mage-flow", MageFlowPipeline),cc @asomoza for visibility
What does this PR do?
Add Mage-Flow text-to-image pipeline to diffusers. Mage-Flow is a 4B parameter model from Microsoft's Mage team, based on the NR-MMDiT (Native-Resolution Multimodal DiT) dual-stream architecture with Qwen3-VL text encoder and a custom MageVAE (128-channel latents, 16x downsampling). Supports any resolution from 512 to 2048 at arbitrary aspect ratios.
This PR adds:
MageFlowTransformer2DModel— dual-stream MMDiT with complex RoPE and joint attentionAutoencoderMageVAE— DConv encoder + CoD decoder with adaLN constant foldingMageFlowPipeline— T2I pipeline with Qwen3-VL text encoding and FlowMatchEuler schedulingUsage
Quality Verification
All images 1024x1024, 30 steps, CFG=5.0. The original pipeline uses Gaussian-Shading watermark noise and packed varlen text encoding, so exact pixel-level reproduction is not expected — but visual quality and prompt adherence are equivalent.
Mountain Landscape (seed=88)
Cat on Windowsill (seed=123)
City Skyline (seed=77)
Numerical Parity
Implementation Details
Transformer (
MageFlowTransformer2DModel)model.to(device)behaviordispatch_attention_fn(non-packed [txt, img] concatenation)latent_height/latent_widthforward params to avoid GPU-CPU sync in RoPE (torch.compile compatible)VAE (
AutoencoderMageVAE)freeze_adaln()for inferenceF.scaled_dot_product_attentionin CoD decoder attention blocksforward()delegates todecode()for CPU offload hook compatibilityPipeline (
MageFlowPipeline)Qwen3VLForConditionalGeneration) text encoder with ChatML prompt templateFlowMatchEulerDiscreteScheduler(shift=6.0)with custom base sigmaslinspace(1, 1/N, N)model_cpu_offload_seq = "text_encoder->transformer->vae"Weight Conversion
scripts/convert_mage_flow_to_diffusers.pyimg_in→x_embedder,txt_norm→context_embedder_norm,txt_in→context_embedderstudent.dconv_encoder.*→encoder.*,pipeline.*→decoder.*(excludingy_embedder.encoder.*)Test Results
The 3 skipped tests:
test_save_load_optional_components: Pipeline has no optional components (correct skip)test_keep_in_fp32_modules: Mage-Flow does not need_keep_in_fp32_modules(correct skip)Self-Review
Ran
.ai/skills/self-reviewagainst.ai/review-rules.md,.ai/models.md,.ai/pipelines.md,.ai/testing.md, and.ai/skills/model-integration/pitfalls.md.Blocking issues found and resolved
encoder_hidden_states_maskparam but never used ittxt_idsparamtemb + torch.zeros(...)no-op.item()calls in RoPE break torch.compilelatent_height/latent_widthparamsAutoencoderKLAutoencoderMageVAENon-blocking issues found and resolved
_MageVAEYEmbedderwrapper undocumentedis_latentparameter_no_split_modulesencode()lacked generator for reproducibilitygeneratorparam withrandn_tensorMageVAEAttnBlockusedtorch.bmmF.scaled_dot_product_attentionDead code removed
temb + zerosblockscale_rope=Falsebranchscale_ropealwaysTrueencoder_hidden_states_mask/txt_idsparamsMageVAEDecoder.ada = nn.Identity()forward()methodFiles Changed
New Files
src/diffusers/models/transformers/transformer_mage_flow.pysrc/diffusers/models/autoencoders/autoencoder_mage_vae.pysrc/diffusers/pipelines/mage_flow/pipeline_mage_flow.pysrc/diffusers/pipelines/mage_flow/pipeline_output.pysrc/diffusers/pipelines/mage_flow/__init__.pyscripts/convert_mage_flow_to_diffusers.pytests/pipelines/mage_flow/test_pipeline_mage_flow.pytests/models/transformers/test_models_transformer_mage_flow.pytests/pipelines/mage_flow/__init__.pydocs/source/en/api/pipelines/mage_flow.mddocs/source/en/api/models/mage_flow_transformer2d.mdModified Files
src/diffusers/__init__.py— Register new componentssrc/diffusers/models/__init__.py— Register modelssrc/diffusers/models/transformers/__init__.pysrc/diffusers/models/autoencoders/__init__.pysrc/diffusers/pipelines/__init__.py— Register pipelinedocs/source/en/_toctree.yml— Add doc entriesFixes #14270
Before submitting
self-reviewskill on the diff?documentation guidelines, and
here are tips on formatting docstrings.
Who can review?
@sayakpaul @yiyixuxu @DN6 @dg845 @asomoza