mirror of
https://github.com/huggingface/diffusers.git
synced 2026-10-24 20:59:27 +03:00
* Add Anima pipeline * Fix empty Anima negative prompts * Fix Anima registration * Clean up Anima conditioner * Refactor Anima to modular * Use modular loader in Anima docs * Move Anima text conditioner * Apply style fixes * Address Anima review nits * Fix Anima rotary autocast * Clean up Anima LoRA checks * Update Anima model id * Document Anima LoRA loader * Document Anima conditioner forward --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: YiYi Xu <yixu310@gmail.com>
317 lines
13 KiB
Python
317 lines
13 KiB
Python
"""
|
|
Convert Anima checkpoints to Diffusers format.
|
|
|
|
Example:
|
|
```bash
|
|
python scripts/convert_anima_to_diffusers.py \
|
|
--transformer_ckpt_path anima_model/anima-preview3-base.safetensors \
|
|
--text_encoder_ckpt_path anima_model/qwen_3_06b_base.safetensors \
|
|
--vae_ckpt_path anima_model/qwen_image_vae.safetensors \
|
|
--qwen_tokenizer_path path/to/qwen25_tokenizer \
|
|
--t5_tokenizer_path path/to/t5_tokenizer \
|
|
--output_path anima_model/anima-preview3-diffusers \
|
|
--save_pipeline
|
|
```
|
|
"""
|
|
|
|
import argparse
|
|
import pathlib
|
|
import sys
|
|
from typing import Any
|
|
|
|
import torch
|
|
from accelerate import init_empty_weights
|
|
from convert_cosmos_to_diffusers import convert_transformer
|
|
from safetensors.torch import load_file
|
|
from transformers import AutoTokenizer, Qwen3Config, Qwen3Model, T5TokenizerFast
|
|
|
|
from diffusers import (
|
|
AnimaAutoBlocks,
|
|
AnimaTextConditioner,
|
|
AutoencoderKLQwenImage,
|
|
FlowMatchEulerDiscreteScheduler,
|
|
)
|
|
|
|
|
|
DTYPE_MAPPING = {
|
|
"fp32": torch.float32,
|
|
"fp16": torch.float16,
|
|
"bf16": torch.bfloat16,
|
|
}
|
|
|
|
|
|
def rename_residual_key(key: str) -> str:
|
|
replacements = {
|
|
".residual.0.": ".norm1.",
|
|
".residual.2.": ".conv1.",
|
|
".residual.3.": ".norm2.",
|
|
".residual.6.": ".conv2.",
|
|
".shortcut.": ".conv_shortcut.",
|
|
}
|
|
for old, new in replacements.items():
|
|
key = key.replace(old, new)
|
|
return key
|
|
|
|
|
|
def rename_mid_key(key: str) -> str:
|
|
replacements = {
|
|
".middle.0.": ".mid_block.resnets.0.",
|
|
".middle.1.": ".mid_block.attentions.0.",
|
|
".middle.2.": ".mid_block.resnets.1.",
|
|
}
|
|
for old, new in replacements.items():
|
|
key = key.replace(old, new)
|
|
return rename_residual_key(key)
|
|
|
|
|
|
def rename_decoder_upsample_key(key: str) -> str:
|
|
prefix = "decoder.upsamples."
|
|
suffix = key.removeprefix(prefix)
|
|
index_str, rest = suffix.split(".", 1)
|
|
index = int(index_str)
|
|
|
|
if index in (3, 7, 11):
|
|
block_index = (index - 3) // 4
|
|
new_key = f"decoder.up_blocks.{block_index}.upsamplers.0.{rest}"
|
|
else:
|
|
block_index = index // 4
|
|
resnet_index = index % 4
|
|
new_key = f"decoder.up_blocks.{block_index}.resnets.{resnet_index}.{rest}"
|
|
|
|
return rename_residual_key(new_key)
|
|
|
|
|
|
def convert_qwen_image_vae_state_dict(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
|
|
converted_state_dict = {}
|
|
for key, value in state_dict.items():
|
|
if key.startswith("conv1."):
|
|
new_key = key.replace("conv1.", "quant_conv.", 1)
|
|
elif key.startswith("conv2."):
|
|
new_key = key.replace("conv2.", "post_quant_conv.", 1)
|
|
elif key.startswith("encoder.conv1."):
|
|
new_key = key.replace("encoder.conv1.", "encoder.conv_in.", 1)
|
|
elif key.startswith("decoder.conv1."):
|
|
new_key = key.replace("decoder.conv1.", "decoder.conv_in.", 1)
|
|
elif key.startswith("encoder.downsamples."):
|
|
new_key = rename_residual_key(key.replace("encoder.downsamples.", "encoder.down_blocks.", 1))
|
|
elif key.startswith("decoder.upsamples."):
|
|
new_key = rename_decoder_upsample_key(key)
|
|
elif key.startswith("encoder.middle.") or key.startswith("decoder.middle."):
|
|
new_key = rename_mid_key(key)
|
|
elif key.startswith("encoder.head.0."):
|
|
new_key = key.replace("encoder.head.0.", "encoder.norm_out.", 1)
|
|
elif key.startswith("encoder.head.2."):
|
|
new_key = key.replace("encoder.head.2.", "encoder.conv_out.", 1)
|
|
elif key.startswith("decoder.head.0."):
|
|
new_key = key.replace("decoder.head.0.", "decoder.norm_out.", 1)
|
|
elif key.startswith("decoder.head.2."):
|
|
new_key = key.replace("decoder.head.2.", "decoder.conv_out.", 1)
|
|
else:
|
|
new_key = rename_residual_key(key)
|
|
|
|
if new_key in converted_state_dict:
|
|
raise ValueError(f"Duplicate converted VAE key: {new_key}")
|
|
converted_state_dict[new_key] = value
|
|
|
|
return converted_state_dict
|
|
|
|
|
|
def convert_qwen_image_vae(state_dict: dict[str, torch.Tensor]) -> AutoencoderKLQwenImage:
|
|
converted_state_dict = convert_qwen_image_vae_state_dict(state_dict)
|
|
with init_empty_weights():
|
|
vae = AutoencoderKLQwenImage()
|
|
|
|
expected_keys = set(vae.state_dict().keys())
|
|
converted_keys = set(converted_state_dict.keys())
|
|
missing_keys = expected_keys - converted_keys
|
|
unexpected_keys = converted_keys - expected_keys
|
|
if missing_keys or unexpected_keys:
|
|
if missing_keys:
|
|
print(f"ERROR: missing VAE keys ({len(missing_keys)}):", file=sys.stderr)
|
|
for key in sorted(missing_keys):
|
|
print(key, file=sys.stderr)
|
|
if unexpected_keys:
|
|
print(f"ERROR: unexpected VAE keys ({len(unexpected_keys)}):", file=sys.stderr)
|
|
for key in sorted(unexpected_keys):
|
|
print(key, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
vae.load_state_dict(converted_state_dict, strict=True, assign=True)
|
|
return vae
|
|
|
|
|
|
def infer_text_conditioner_config(state_dict: dict[str, torch.Tensor]) -> dict[str, Any]:
|
|
model_dim = state_dict["blocks.0.self_attn.q_proj.weight"].shape[0]
|
|
source_dim = state_dict["blocks.0.cross_attn.k_proj.weight"].shape[1]
|
|
target_vocab_size, target_dim = state_dict["embed.weight"].shape
|
|
attention_head_dim = state_dict["blocks.0.self_attn.q_norm.weight"].shape[0]
|
|
num_layers = 1 + max(int(key.split(".")[1]) for key in state_dict if key.startswith("blocks."))
|
|
|
|
return {
|
|
"source_dim": source_dim,
|
|
"target_dim": target_dim,
|
|
"model_dim": model_dim,
|
|
"num_layers": num_layers,
|
|
"num_attention_heads": model_dim // attention_head_dim,
|
|
"target_vocab_size": target_vocab_size,
|
|
}
|
|
|
|
|
|
def convert_text_conditioner(state_dict: dict[str, torch.Tensor]) -> AnimaTextConditioner:
|
|
config = infer_text_conditioner_config(state_dict)
|
|
with init_empty_weights():
|
|
text_conditioner = AnimaTextConditioner(**config)
|
|
|
|
expected_keys = set(text_conditioner.state_dict().keys())
|
|
converted_keys = set(state_dict.keys())
|
|
missing_keys = expected_keys - converted_keys
|
|
unexpected_keys = converted_keys - expected_keys
|
|
if missing_keys or unexpected_keys:
|
|
if missing_keys:
|
|
print(f"ERROR: missing text conditioner keys ({len(missing_keys)}):", file=sys.stderr)
|
|
for key in sorted(missing_keys):
|
|
print(key, file=sys.stderr)
|
|
if unexpected_keys:
|
|
print(f"ERROR: unexpected text conditioner keys ({len(unexpected_keys)}):", file=sys.stderr)
|
|
for key in sorted(unexpected_keys):
|
|
print(key, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
text_conditioner.load_state_dict(state_dict, strict=True, assign=True)
|
|
return text_conditioner
|
|
|
|
|
|
def infer_qwen3_config(state_dict: dict[str, torch.Tensor]) -> Qwen3Config:
|
|
vocab_size, hidden_size = state_dict["embed_tokens.weight"].shape
|
|
intermediate_size = state_dict["layers.0.mlp.gate_proj.weight"].shape[0]
|
|
num_hidden_layers = 1 + max(int(key.split(".")[1]) for key in state_dict if key.startswith("layers."))
|
|
head_dim = state_dict["layers.0.self_attn.q_norm.weight"].shape[0]
|
|
num_attention_heads = state_dict["layers.0.self_attn.q_proj.weight"].shape[0] // head_dim
|
|
num_key_value_heads = state_dict["layers.0.self_attn.k_proj.weight"].shape[0] // head_dim
|
|
|
|
return Qwen3Config(
|
|
vocab_size=vocab_size,
|
|
hidden_size=hidden_size,
|
|
intermediate_size=intermediate_size,
|
|
num_hidden_layers=num_hidden_layers,
|
|
num_attention_heads=num_attention_heads,
|
|
num_key_value_heads=num_key_value_heads,
|
|
max_position_embeddings=32768,
|
|
rms_norm_eps=1e-6,
|
|
rope_theta=1000000.0,
|
|
head_dim=head_dim,
|
|
attention_bias=False,
|
|
tie_word_embeddings=False,
|
|
)
|
|
|
|
|
|
def convert_text_encoder(state_dict: dict[str, torch.Tensor]) -> Qwen3Model:
|
|
state_dict = {key.removeprefix("model."): value for key, value in state_dict.items()}
|
|
config = infer_qwen3_config(state_dict)
|
|
with init_empty_weights():
|
|
text_encoder = Qwen3Model(config)
|
|
|
|
expected_keys = set(text_encoder.state_dict().keys())
|
|
converted_keys = set(state_dict.keys())
|
|
missing_keys = expected_keys - converted_keys
|
|
unexpected_keys = converted_keys - expected_keys
|
|
if missing_keys or unexpected_keys:
|
|
if missing_keys:
|
|
print(f"ERROR: missing Qwen3 keys ({len(missing_keys)}):", file=sys.stderr)
|
|
for key in sorted(missing_keys):
|
|
print(key, file=sys.stderr)
|
|
if unexpected_keys:
|
|
print(f"ERROR: unexpected Qwen3 keys ({len(unexpected_keys)}):", file=sys.stderr)
|
|
for key in sorted(unexpected_keys):
|
|
print(key, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
text_encoder.load_state_dict(state_dict, strict=True, assign=True)
|
|
return text_encoder
|
|
|
|
|
|
def split_anima_transformer_checkpoint(
|
|
state_dict: dict[str, torch.Tensor],
|
|
) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]:
|
|
transformer_state_dict = {}
|
|
text_conditioner_state_dict = {}
|
|
adapter_prefix = "net.llm_adapter."
|
|
|
|
for key, value in state_dict.items():
|
|
if key.startswith(adapter_prefix):
|
|
text_conditioner_state_dict[key.removeprefix(adapter_prefix)] = value
|
|
else:
|
|
transformer_state_dict[key] = value
|
|
|
|
return transformer_state_dict, text_conditioner_state_dict
|
|
|
|
|
|
def save_pipeline(args, transformer, text_conditioner, text_encoder, vae):
|
|
tokenizer = AutoTokenizer.from_pretrained(args.qwen_tokenizer_path)
|
|
t5_tokenizer = T5TokenizerFast.from_pretrained(args.t5_tokenizer_path)
|
|
scheduler = FlowMatchEulerDiscreteScheduler(shift=3.0)
|
|
|
|
pipe = AnimaAutoBlocks().init_pipeline()
|
|
pipe.update_components(
|
|
text_encoder=text_encoder,
|
|
tokenizer=tokenizer,
|
|
t5_tokenizer=t5_tokenizer,
|
|
text_conditioner=text_conditioner,
|
|
transformer=transformer,
|
|
vae=vae,
|
|
scheduler=scheduler,
|
|
)
|
|
pipe.save_pretrained(args.output_path, safe_serialization=True, max_shard_size=args.max_shard_size)
|
|
|
|
|
|
def get_args():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--transformer_ckpt_path", type=str, required=True, help="Path to Anima DiT safetensors")
|
|
parser.add_argument("--text_encoder_ckpt_path", type=str, required=True, help="Path to Qwen3 text encoder")
|
|
parser.add_argument("--vae_ckpt_path", type=str, required=True, help="Path to Qwen-Image VAE safetensors")
|
|
parser.add_argument("--qwen_tokenizer_path", type=str, default=None)
|
|
parser.add_argument("--t5_tokenizer_path", type=str, default=None)
|
|
parser.add_argument("--output_path", type=str, required=True)
|
|
parser.add_argument("--save_pipeline", action="store_true")
|
|
parser.add_argument("--dtype", default="bf16", choices=list(DTYPE_MAPPING.keys()))
|
|
parser.add_argument("--max_shard_size", default="5GB")
|
|
return parser.parse_args()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = get_args()
|
|
output_path = pathlib.Path(args.output_path)
|
|
dtype = DTYPE_MAPPING[args.dtype]
|
|
|
|
raw_transformer_state_dict = load_file(args.transformer_ckpt_path, device="cpu")
|
|
transformer_state_dict, text_conditioner_state_dict = split_anima_transformer_checkpoint(
|
|
raw_transformer_state_dict
|
|
)
|
|
transformer = convert_transformer(
|
|
"Cosmos-2.0-Diffusion-2B-Text2Image", state_dict=transformer_state_dict, weights_only=True
|
|
).to(dtype=dtype)
|
|
text_conditioner = convert_text_conditioner(text_conditioner_state_dict).to(dtype=dtype)
|
|
|
|
text_encoder_state_dict = load_file(args.text_encoder_ckpt_path, device="cpu")
|
|
text_encoder = convert_text_encoder(text_encoder_state_dict).to(dtype=dtype)
|
|
|
|
vae_state_dict = load_file(args.vae_ckpt_path, device="cpu")
|
|
vae = convert_qwen_image_vae(vae_state_dict).to(dtype=dtype)
|
|
|
|
if args.save_pipeline:
|
|
if args.qwen_tokenizer_path is None or args.t5_tokenizer_path is None:
|
|
raise ValueError("`--qwen_tokenizer_path` and `--t5_tokenizer_path` are required with `--save_pipeline`.")
|
|
save_pipeline(args, transformer, text_conditioner, text_encoder, vae)
|
|
else:
|
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
transformer.save_pretrained(
|
|
output_path / "transformer", safe_serialization=True, max_shard_size=args.max_shard_size
|
|
)
|
|
text_conditioner.save_pretrained(
|
|
output_path / "text_conditioner", safe_serialization=True, max_shard_size=args.max_shard_size
|
|
)
|
|
text_encoder.save_pretrained(
|
|
output_path / "text_encoder", safe_serialization=True, max_shard_size=args.max_shard_size
|
|
)
|
|
vae.save_pretrained(output_path / "vae", safe_serialization=True, max_shard_size=args.max_shard_size)
|