1
0
mirror of https://github.com/huggingface/diffusers.git synced 2026-10-24 20:59:27 +03:00
Files
diffusers/scripts/convert_minimax_music3_to_diffusers.py
apolinário 2da7040be1 Add MiniMax Music 3 (#14456)
* Add MiniMax Music 3 transformer, pipeline sub-models, and conversion script

* Add MiniMax Music 3 pipeline, tests, and docs

* Wire example docstring, point diffusers-format references at MiniMaxAI/MiniMax-Music-3

* Use MiniMaxAI/MiniMax-Music3 as the official diffusers weights repo id

* Apply doc-builder style

* Use dtype in examples, demonstrate the structured prompt format

* Use soundfile in examples like the other audio pipelines

* Convert to a modular pipeline and move models under src/diffusers/models

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).

* Point modular index loading specs at the Hub repo in the converter

* Apply doc-builder style to the modular blocks docstrings

* Address review-bot comments: overlap comment accuracy, single pipeline-level progress bar, zero-frame duration guard, import ordering, docstring cross-reference

* Support CPU offloading in the AR step: trigger offload hooks before submodule calls (minimax_h3 workaround), clear error when the AR models cannot co-reside

* Docs: PR-install tip and memory-usage section with measured offloading numbers

* Address review comments on the block assembly

Take the individual models rather than `components` in `_generate_depth_codes` and
`_embed_audio_frame`; `num_codebooks` / `audio_vocab_size` come off the depth decoder's
config, so the two pipeline properties that existed only for them are gone.

Assemble the blockset as three top-level children, each owning models and runnable on its
own — generate the semantics once and denoise it with different settings:

    semantic_generator  MiniMaxMusic3SemanticGenerationStep  [tokenize, generate]
    denoise             MiniMaxMusic3CoreDenoiseStep         [prepare_chunks, denoise]
    decode              MiniMaxMusic3VocoderDecodeStep

`MiniMaxMusic3TextEncoderStep` is renamed `MiniMaxMusic3TokenizeStep` (it only tokenizes)
and the autoregressive leaf `MiniMaxMusic3AutoregressiveStep`, freeing its old name for
the sequential.

Point the tests at `hf-internal-testing/tiny-minimax-music3-modular-pipe`; the previous
fixture repo's component specs pointed at a local path, so nothing loaded off that machine.
Drop the PR-branch install tip from the docs.

20s and 60s generations are bit-identical to before these changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: yiyixuxu <yixu310@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 13:47:57 -10:00

270 lines
13 KiB
Python

# Conversion script for MiniMax Music 3 (https://huggingface.co/MiniMaxAI/MiniMax-Music3).
#
# Original checkpoint layout:
# flowmatching_vae.pth flow-matching DiT + condition projection
# dav.pth Flow-VAE (DAC-style) decoder
# qwen_7B/qwen_7B/ Qwen3 backbone + audio embedding + RVQ depth decoder (sharded safetensors)
# qwen_7B/qwen3-8B-tokenizer-music/ music tokenizer
#
# Usage:
# python scripts/convert_minimax_music3_to_diffusers.py \
# --checkpoint_dir MiniMaxAI/MiniMax-Music3 --output_path ./minimax-music3-diffusers
import argparse
import json
import os
import torch
from safetensors.torch import load_file
from diffusers import (
FlowMatchEulerDiscreteScheduler,
MiniMaxMusic3ConditionEncoder,
MiniMaxMusic3RVQDepthDecoder,
MiniMaxMusic3Transformer1DModel,
MiniMaxMusic3Vocoder,
)
def load_dit_state_dict(checkpoint_dir: str) -> dict:
return torch.load(os.path.join(checkpoint_dir, "flowmatching_vae.pth"), map_location="cpu", weights_only=True)
def load_dav_state_dict(checkpoint_dir: str) -> dict:
return torch.load(os.path.join(checkpoint_dir, "dav.pth"), map_location="cpu", weights_only=True)
def load_qwen_state_dict(checkpoint_dir: str) -> dict:
qwen_dir = os.path.join(checkpoint_dir, "qwen_7B", "qwen_7B")
with open(os.path.join(qwen_dir, "model.safetensors.index.json")) as f:
index = json.load(f)
state_dict = {}
for filename in sorted(set(index["weight_map"].values())):
state_dict.update(load_file(os.path.join(qwen_dir, filename), device="cpu"))
return state_dict
def convert_transformer(dit_state_dict: dict) -> MiniMaxMusic3Transformer1DModel:
prefix = "diffusion_transformer."
converted = {
"time_proj.weight": dit_state_dict[prefix + "timestep_features.weight"],
"time_embed.linear_1.weight": dit_state_dict[prefix + "to_timestep_embed.0.weight"],
"time_embed.linear_1.bias": dit_state_dict[prefix + "to_timestep_embed.0.bias"],
"time_embed.linear_2.weight": dit_state_dict[prefix + "to_timestep_embed.2.weight"],
"time_embed.linear_2.bias": dit_state_dict[prefix + "to_timestep_embed.2.bias"],
"preprocess_conv.weight": dit_state_dict[prefix + "preprocess_conv.weight"],
"postprocess_conv.weight": dit_state_dict[prefix + "postprocess_conv.weight"],
"proj_in.weight": dit_state_dict[prefix + "transformer.project_in.weight"],
"proj_out.weight": dit_state_dict[prefix + "transformer.project_out.weight"],
}
num_layers = 0
while prefix + f"transformer.layers.{num_layers}.pre_norm.gamma" in dit_state_dict:
num_layers += 1
for i in range(num_layers):
original = prefix + f"transformer.layers.{i}."
target = f"transformer_blocks.{i}."
converted[target + "norm1.weight"] = dit_state_dict[original + "pre_norm.gamma"]
converted[target + "norm1.bias"] = dit_state_dict[original + "pre_norm.beta"]
query, key, value = dit_state_dict[original + "self_attn.to_qkv.weight"].chunk(3, dim=0)
converted[target + "attn.to_q.weight"] = query
converted[target + "attn.to_k.weight"] = key
converted[target + "attn.to_v.weight"] = value
converted[target + "attn.to_out.0.weight"] = dit_state_dict[original + "self_attn.to_out.weight"]
converted[target + "norm2.weight"] = dit_state_dict[original + "ff_norm.gamma"]
converted[target + "norm2.bias"] = dit_state_dict[original + "ff_norm.beta"]
converted[target + "ff_in.weight"] = dit_state_dict[original + "ff.ff.0.proj.weight"]
converted[target + "ff_in.bias"] = dit_state_dict[original + "ff.ff.0.proj.bias"]
converted[target + "ff_out.weight"] = dit_state_dict[original + "ff.ff.2.weight"]
converted[target + "ff_out.bias"] = dit_state_dict[original + "ff.ff.2.bias"]
with torch.device("meta"):
transformer = MiniMaxMusic3Transformer1DModel(num_layers=num_layers)
transformer.load_state_dict(converted, strict=True, assign=True)
return transformer
def convert_condition_encoder(dit_state_dict: dict) -> MiniMaxMusic3ConditionEncoder:
converted = {
"layer_weight_logits": dit_state_dict["cond_layer_logits"],
"layer_scale": dit_state_dict["cond_layer_scale"],
"proj.weight": dit_state_dict["latent_conditioners.0.weight"],
"proj.bias": dit_state_dict["latent_conditioners.0.bias"],
}
with torch.device("meta"):
condition_encoder = MiniMaxMusic3ConditionEncoder()
condition_encoder.load_state_dict(converted, strict=True, assign=True)
return condition_encoder
def convert_vocoder(dav_state_dict: dict) -> MiniMaxMusic3Vocoder:
converted = {
"dec_in_proj.weight": dav_state_dict["dec_in_proj.weight"],
"dec_in_proj.bias": dav_state_dict["dec_in_proj.bias"],
}
# The reference decoder is one nn.Sequential: [conv_in, block*4, snake, conv_out, tanh].
for suffix in ("weight_g", "weight_v", "bias"):
converted[f"conv_in.{suffix}"] = dav_state_dict[f"decoder.model.0.{suffix}"]
converted[f"conv_out.{suffix}"] = dav_state_dict[f"decoder.model.6.{suffix}"]
converted["snake_out.alpha"] = dav_state_dict["decoder.model.5.alpha"]
for block_index in range(4):
original = f"decoder.model.{block_index + 1}.block."
target = f"blocks.{block_index}."
converted[target + "snake1.alpha"] = dav_state_dict[original + "0.alpha"]
for suffix in ("weight_g", "weight_v", "bias"):
converted[target + f"conv_t1.{suffix}"] = dav_state_dict[original + f"1.{suffix}"]
for unit_index, unit_name in ((2, "res_unit1"), (3, "res_unit2"), (4, "res_unit3")):
converted[target + f"{unit_name}.snake1.alpha"] = dav_state_dict[original + f"{unit_index}.block.0.alpha"]
converted[target + f"{unit_name}.snake2.alpha"] = dav_state_dict[original + f"{unit_index}.block.2.alpha"]
for suffix in ("weight_g", "weight_v", "bias"):
converted[target + f"{unit_name}.conv1.{suffix}"] = dav_state_dict[
original + f"{unit_index}.block.1.{suffix}"
]
converted[target + f"{unit_name}.conv2.{suffix}"] = dav_state_dict[
original + f"{unit_index}.block.3.{suffix}"
]
vocoder = MiniMaxMusic3Vocoder()
vocoder.load_state_dict(converted, strict=True)
return vocoder
def convert_rvq_depth_decoder(qwen_state_dict: dict, model_config: dict) -> MiniMaxMusic3RVQDepthDecoder:
prefix = "model.audio_decoder."
converted = {
"audio_embeddings.weight": qwen_state_dict["model.audio_extra_embedding.weight"],
"projection.weight": qwen_state_dict[prefix + "projection.weight"],
"pos_embedding.weight": qwen_state_dict[prefix + "pos_embedding.weight"],
"norm.weight": qwen_state_dict[prefix + "norm.weight"],
}
num_codebooks = int(model_config["audio_num_codebooks"])
for i in range(num_codebooks - 1):
converted[f"audio_heads.{i}.weight"] = qwen_state_dict[prefix + f"audio_heads.{i}.weight"]
num_layers = int(model_config["decoder_num_layers"])
for i in range(num_layers):
original = prefix + f"layers.{i}."
target = f"layers.{i}."
converted[target + "input_layernorm.weight"] = qwen_state_dict[original + "input_layernorm.weight"]
converted[target + "post_attention_layernorm.weight"] = qwen_state_dict[
original + "post_attention_layernorm.weight"
]
converted[target + "attn.to_q.weight"] = qwen_state_dict[original + "self_attn.q_proj.weight"]
converted[target + "attn.to_k.weight"] = qwen_state_dict[original + "self_attn.k_proj.weight"]
converted[target + "attn.to_v.weight"] = qwen_state_dict[original + "self_attn.v_proj.weight"]
converted[target + "attn.to_out.weight"] = qwen_state_dict[original + "self_attn.o_proj.weight"]
for proj in ("gate_proj", "up_proj", "down_proj"):
converted[target + proj + ".weight"] = qwen_state_dict[original + f"mlp.{proj}.weight"]
with torch.device("meta"):
rvq_depth_decoder = MiniMaxMusic3RVQDepthDecoder(
hidden_size=int(model_config["hidden_size"]),
num_layers=num_layers,
num_attention_heads=int(model_config["decoder_num_heads"]),
intermediate_size=int(model_config["decoder_intermediate_size"]),
audio_vocab_size=int(model_config["audio_vocab_size"]),
num_codebooks=num_codebooks,
)
rvq_depth_decoder.load_state_dict(converted, strict=True, assign=True)
return rvq_depth_decoder
def convert_language_model(qwen_state_dict: dict, model_config: dict):
from transformers import Qwen3Config, Qwen3ForCausalLM
config = Qwen3Config(
vocab_size=model_config["vocab_size"],
hidden_size=model_config["hidden_size"],
intermediate_size=model_config["intermediate_size"],
num_hidden_layers=model_config["num_hidden_layers"],
num_attention_heads=model_config["num_attention_heads"],
num_key_value_heads=model_config["num_key_value_heads"],
head_dim=model_config["head_dim"],
max_position_embeddings=model_config.get("max_position_embeddings", 10240),
rope_theta=model_config.get("rope_theta", 1000000),
rms_norm_eps=model_config.get("rms_norm_eps", 1e-6),
tie_word_embeddings=model_config.get("tie_word_embeddings", False),
)
backbone_state_dict = {
key: value
for key, value in qwen_state_dict.items()
if not key.startswith(("model.audio_extra_embedding", "model.audio_decoder."))
}
with torch.device("meta"):
language_model = Qwen3ForCausalLM(config)
language_model.load_state_dict(backbone_state_dict, strict=True, assign=True)
return language_model
def main(args):
checkpoint_dir = args.checkpoint_dir
if not os.path.isdir(checkpoint_dir):
from huggingface_hub import snapshot_download
checkpoint_dir = snapshot_download(checkpoint_dir)
with open(os.path.join(checkpoint_dir, "qwen_7B", "qwen_7B", "config.json")) as f:
model_config = json.load(f)
dit_state_dict = load_dit_state_dict(checkpoint_dir)
transformer = convert_transformer(dit_state_dict).to(args.dtype)
condition_encoder = convert_condition_encoder(dit_state_dict).to(args.dtype)
del dit_state_dict
vocoder = convert_vocoder(load_dav_state_dict(checkpoint_dir)).to(args.dtype)
qwen_state_dict = load_qwen_state_dict(checkpoint_dir)
rvq_depth_decoder = convert_rvq_depth_decoder(qwen_state_dict, model_config).to(torch.bfloat16)
language_model = convert_language_model(qwen_state_dict, model_config)
del qwen_state_dict
from transformers import AutoTokenizer
from diffusers import MiniMaxMusic3Blocks
tokenizer = AutoTokenizer.from_pretrained(os.path.join(checkpoint_dir, "qwen_7B", "qwen3-8B-tokenizer-music"))
# num_train_timesteps=1 keeps `scheduler.timesteps` equal to the flow-matching time in [0, 1] that the
# transformer's Fourier embedding expects.
scheduler = FlowMatchEulerDiscreteScheduler(num_train_timesteps=1, shift=1.0, invert_sigmas=True)
pipeline = MiniMaxMusic3Blocks().init_pipeline()
pipeline.update_components(
language_model=language_model,
rvq_depth_decoder=rvq_depth_decoder,
condition_encoder=condition_encoder,
transformer=transformer,
vocoder=vocoder,
tokenizer=tokenizer,
scheduler=scheduler,
)
pipeline.save_pretrained(args.output_path, safe_serialization=True, max_shard_size="5GB")
# save_pretrained bakes the local output path into the modular index's loading specs; point them at the
# Hub repo the components will be uploaded to instead.
index_path = os.path.join(args.output_path, "modular_model_index.json")
with open(index_path) as f:
index = json.load(f)
for entry in index.values():
if isinstance(entry, list) and len(entry) == 3 and isinstance(entry[2], dict):
if entry[2].get("pretrained_model_name_or_path") == args.output_path:
entry[2]["pretrained_model_name_or_path"] = args.repo_id
with open(index_path, "w") as f:
json.dump(index, f, indent=2, sort_keys=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--checkpoint_dir",
type=str,
default="MiniMaxAI/MiniMax-Music3",
help="Local directory or Hugging Face Hub repo id of the original checkpoint.",
)
parser.add_argument("--output_path", type=str, required=True)
parser.add_argument(
"--repo_id",
type=str,
default="MiniMaxAI/MiniMax-Music3",
help="Hub repo id the converted components will live in (written into the modular index loading specs).",
)
parser.add_argument("--dtype", type=lambda name: getattr(torch, name), default="float32")
args = parser.parse_args()
main(args)