Add MiniMax Music 3 - #14456
Conversation
672f410 to
5698322
Compare
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
…I/MiniMax-Music-3
| @dataclass | ||
| class MiniMaxMusic3TransformerOutput(BaseOutput): | ||
| sample: torch.Tensor |
There was a problem hiding this comment.
Is it not possible to use the Transformer2DModelOutput here?
There was a problem hiding this comment.
Done — now returns Transformer2DModelOutput.
| return freqs.cos().contiguous(), freqs.sin().contiguous() | ||
|
|
||
| def forward(self, seq_len: int, device: torch.device) -> Tuple[torch.Tensor, torch.Tensor]: | ||
| return self._build(seq_len, device) |
There was a problem hiding this comment.
Consider folding into forward() so that things stay explicitly inline.
There was a problem hiding this comment.
Done — folded into forward() with the cache decorator on it directly.
| self.to_k = nn.Linear(dim, self.inner_dim, bias=False) | ||
| self.to_v = nn.Linear(dim, self.inner_dim, bias=False) | ||
| self.to_out = nn.ModuleList([nn.Linear(self.inner_dim, dim, bias=False), nn.Dropout(0.0)]) | ||
| self.set_processor(processor or MiniMaxMusic3AttnProcessor()) |
There was a problem hiding this comment.
| self.set_processor(processor or MiniMaxMusic3AttnProcessor()) | |
| if processor is None: | |
| processor = self._default_processor_cls() | |
| self.set_processor(processor) |
There was a problem hiding this comment.
Done (also applied to the depth decoder's attention).
| previous_latent = None | ||
| previous_condition = None | ||
| global_step = 0 | ||
| with self.progress_bar(total=self._num_timesteps) as progress_bar: |
There was a problem hiding this comment.
Why not do self.num_inference_steps or put num_inference_steps * len(chunk_starts) in a separate variable and use it here? This feels like an antipattern to me.
There was a problem hiding this comment.
Gone with the modular conversion — this file is removed; step accounting now lives in the loop blocks.
Per review: the conditioner moves to models/condition_embedders, the RVQ depth decoder and vocoder to models/transformers and models/autoencoders, and the standard pipeline is replaced by MiniMaxMusic3ModularPipeline (anima-style blocks, helios-style chunk loop, guider-abstracted CFG with a zeros unconditional branch). Index configs now reference all components under the diffusers library. Also applies the transformer review suggestions (Transformer2DModelOutput, inlined rotary forward, explicit processor default).
46564b9 to
bcc6bbf
Compare
|
Converted to a modular pipeline per @yiyixuxu's suggestion. Also applied Sayak's comments. |
There was a problem hiding this comment.
🤗 Serge says:
Overall this is a well-structured modular integration: the block decomposition (text encoder → AR semantic generation → chunk bookkeeping → per-chunk loop with condition/prepare/set-timesteps/denoise/update sub-blocks → vocoder decode) follows the established LoopSequentialPipelineBlocks pattern, the guider is wired the same way as other modular pipelines, and I verified the chunk geometry is self-consistent (200-frame windows at ~3.445 latents/frame → 689-latent windows, ~344.5-latent hop; the 86/258 crop constants tile the song exactly and the [L-344, L-172) carry aligns with the next window's first 172 latents).
Correctness / clarity
- The chunking comments in
before_denoise.py,denoise.py, anddecoders.pyall state that neighboring windows "share 172 latent frames", but at ~3.445 latents per frame a 100-frame hop on 200-frame windows gives ~344.5 shared latent frames; 172 is only the blended/carried prefix. The code is right, the comments are wrong — worth fixing since future readers will re-derive this. MiniMaxMusic3SemanticGenerationStepraises a misleading "the prompt ended generation immediately" error whenaudio_durationis shorter than one AR frame (< 0.04 s), sincemax_framesbecomes 0.
Style / docs
MiniMaxMusic3ChunkDenoiseInneruses rawtqdm(one new bar per chunk) instead of routing through the pipeline'sprogress_bar/set_progress_bar_configmachinery; helios has the same pattern, but a single bar or config-respecting bar would be nicer for a 14-chunk song.- Minor import-ordering slip in
src/diffusers/models/__init__.pyand a wrong autodoc path forTransformer2DModelOutputin the transformer docstring.
Description vs. diff
- The PR description's usage example (
from diffusers import MiniMaxMusic3Pipeline,.audios[0]) does not match the diff: onlyMiniMaxMusic3ModularPipeline/MiniMaxMusic3Blocksexist; there is no standardMiniMaxMusic3Pipeline. The in-repo docs correctly useModularPipeline.from_pretrained, but the description should be updated to avoid confusion.
Tests
pretrained_model_name_or_path = "diffusers-internal-dev/tiny-minimax-music3"is flagged as a placeholder (TODO to move tohf-internal-testing/); please make sure the tiny repo exists and the CI tests actually run before merge. Also note the fast tests only ever exercise a single chunk (audio_duration=0.2→ 5 frames), so the multi-chunk overlap/crop path — the trickiest part of the modular loop — has no automated coverage. A small multi-chunk shape test (e.g. enough frames for 2–3 windows with tiny configs) would be valuable.
serge v0.1.0 · model: claude-fable-5 · 18 LLM turns · 33 tool calls · 1708.7s · 1520108 in / 129503 out tokens
|
|
||
| logger = logging.get_logger(__name__) # pylint: disable=invalid-name | ||
|
|
||
| # Neighboring windows share 172 latent frames; the previous window's carry spans latent frames [L - 344, L - 172). |
There was a problem hiding this comment.
The "share 172 latent frames" claim is off: at 44100/512 ÷ 24000/960 ≈ 3.445 latents per frame, a 200-frame window is 689 latents and the 100-frame hop is ~344.5 latents, so neighboring windows share ~344 latent frames. 172 (_OVERLAP_LATENT_LENGTH) is only the carried/blended prefix of that shared span — the [L - 344, L - 172) carry is correct precisely because the shared region is ~344 long. The same "share 172" wording also appears in before_denoise.py and decoders.py; please fix all three so the (correct) constants aren't second-guessed later.
There was a problem hiding this comment.
Right — the windows share ~344 latents and 172 is only the blended carry prefix. Reworded in all three files in 1a258e3.
| "encoder_hidden_states": (block_state.condition, torch.zeros_like(block_state.condition)), | ||
| } | ||
|
|
||
| with tqdm(total=block_state.num_inference_steps) as progress_bar: |
There was a problem hiding this comment.
Nit: this creates a fresh raw tqdm bar per chunk (14 bars for a 60 s song) and bypasses set_progress_bar_config. Other modular pipelines route through the pipeline-level progress_bar helper on the loop wrapper; consider driving a single bar (e.g. len(chunk_starts) * num_inference_steps) from MiniMaxMusic3ChunkLoopWrapper.__call__, or at least labeling bars with the chunk index.
There was a problem hiding this comment.
Switched to a single pipeline-level progress_bar over chunks × steps, driven from the loop wrapper (1a258e3).
|
|
||
| @staticmethod | ||
| def check_inputs(block_state): | ||
| if block_state.audio_duration <= 0: |
There was a problem hiding this comment.
If audio_duration is positive but shorter than one AR frame (< 1/25 s), max_frames becomes 0, no hidden states are collected, and the user gets the misleading "the prompt ended generation immediately" error at the end of the step. Consider rejecting durations that round to zero frames here so the error names the actual cause.
There was a problem hiding this comment.
Added a guard where max_frames is computed (frame rate is config-derived, so check_inputs can't see it) — 1a258e3.
| _import_structure["transformers.dual_transformer_2d"] = ["DualTransformer2DModel"] | ||
| _import_structure["transformers.hunyuan_transformer_2d"] = ["HunyuanDiT2DModel"] | ||
| _import_structure["transformers.latte_transformer_3d"] = ["LatteTransformer3DModel"] | ||
| _import_structure["transformers.minimax_music3_rvq_depth_decoder"] = ["MiniMaxMusic3RVQDepthDecoder"] |
There was a problem hiding this comment.
Nit: this entry breaks the alphabetical ordering of the block — transformers.minimax_music3_rvq_depth_decoder should come after transformers.lumina_nextdit2d.
| Frame-aligned conditioning from `MiniMaxMusic3ConditionEncoder`. Pass zeros for the unconditional | ||
| branch of classifier-free guidance. | ||
| return_dict (`bool`, defaults to `True`): | ||
| Whether to return a [`~models.transformers.transformer_minimax_music3.Transformer2DModelOutput`] |
There was a problem hiding this comment.
Transformer2DModelOutput is imported from models.modeling_outputs, not defined in this module, so this autodoc cross-reference won't resolve.
| Whether to return a [`~models.transformers.transformer_minimax_music3.Transformer2DModelOutput`] | |
| Whether to return a [`~models.modeling_outputs.Transformer2DModelOutput`] |
…e-level progress bar, zero-frame duration guard, import ordering, docstring cross-reference
Adds MiniMax Music 3 (lyrics + music description → complete songs up to 5 minutes, 44.1 kHz stereo). An 8B Qwen3 autoregressive stage predicts per-frame audio codes; its hidden states condition a 2.4B flow-matching transformer that produces Flow-VAE latents in overlapping chunks; a DAC-style decoder renders the waveform. Structurally close to Ace-Step (DiT + pipeline-local submodels) and AudioLDM2 (language model as a pipeline component).
New:
MiniMaxMusic3Transformer1DModel,MiniMaxMusic3ConditionEncoder(inmodels/condition_embedders/),MiniMaxMusic3RVQDepthDecoder,MiniMaxMusic3Vocoder, andMiniMaxMusic3ModularPipeline(modular blocks: text encode → AR semantic generation → chunked flow-matching with guider-abstracted CFG → vocode/stitch), plus conversion script, docs, and model- and modular-pipeline tests. The scheduler is the existingFlowMatchEulerDiscreteScheduler(num_train_timesteps=1, shift=1.0, invert_sigmas=True), which reproduces the reference Euler loop exactly.Usage:
Parity vs the reference implementation: every converted component matches bitwise on CPU/fp32; full 30-step chunked generation matches at 57.8 dB SNR on GPU (residual comes from the fused→split QKV projections).
Weights: final home will be
MiniMaxAI/MiniMax-Music3when the model is outSome choices I made that would be great to double check:
generatordrives the whole pipeline - the reference derives a separate seed per stage and per chunk, imo it's fine to keep the diffusers convention here.prompt/lyricsare strings; the AR stage is internally batch-2 for CFG). If we want to optimize batched generation I think it may need an AR-loop redesign, which I would defer to a follow-up if someone would be interested?