243 lines
8.8 KiB
Python
243 lines
8.8 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""
|
|
Convert Hunyuan-GameCraft-1.0 weights from DeepSpeed format to FastVideo format.
|
|
|
|
The official GameCraft checkpoint (mp_rank_00_model_states.pt) uses slightly different
|
|
naming conventions than FastVideo. This script converts the weights to be compatible
|
|
with FastVideo's HunyuanGameCraft model.
|
|
|
|
Usage:
|
|
python scripts/checkpoint_conversion/convert_gamecraft_weights.py \
|
|
--input Hunyuan-GameCraft-1.0/weights/gamecraft_models/mp_rank_00_model_states.pt \
|
|
--output official_weights/hunyuan-gamecraft/transformer/diffusion_pytorch_model.safetensors
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from collections import OrderedDict
|
|
from pathlib import Path
|
|
|
|
import torch
|
|
from safetensors.torch import save_file
|
|
|
|
# Mapping from official GameCraft naming to FastVideo naming
|
|
# GameCraft weights are already close to FastVideo format, but need minor adjustments
|
|
PARAM_NAME_MAP: dict[str, str] = {
|
|
# MLP naming: fc1 -> fc_in, fc2 -> fc_out
|
|
r"^(.*)\.img_mlp\.fc1\.(.*)$": r"\1.img_mlp.fc_in.\2",
|
|
r"^(.*)\.img_mlp\.fc2\.(.*)$": r"\1.img_mlp.fc_out.\2",
|
|
r"^(.*)\.txt_mlp\.fc1\.(.*)$": r"\1.txt_mlp.fc_in.\2",
|
|
r"^(.*)\.txt_mlp\.fc2\.(.*)$": r"\1.txt_mlp.fc_out.\2",
|
|
|
|
# Single block MLP naming
|
|
r"^single_blocks\.(\d+)\.mlp\.fc1\.(.*)$": r"single_blocks.\1.mlp.fc_in.\2",
|
|
r"^single_blocks\.(\d+)\.mlp\.fc2\.(.*)$": r"single_blocks.\1.mlp.fc_out.\2",
|
|
|
|
# Token refiner naming - rename individual_token_refiner.blocks to refiner_blocks
|
|
# This is applied first, then subsequent mappings handle the sub-components
|
|
r"^txt_in\.individual_token_refiner\.blocks\.(\d+)\.(.*)$": r"txt_in.refiner_blocks.\1.\2",
|
|
|
|
# Vector in naming
|
|
r"^vector_in\.in_layer\.(.*)$": r"vector_in.fc_in.\1",
|
|
r"^vector_in\.out_layer\.(.*)$": r"vector_in.fc_out.\1",
|
|
|
|
# Time embedder naming
|
|
r"^time_in\.mlp\.0\.(.*)$": r"time_in.mlp.fc_in.\1",
|
|
r"^time_in\.mlp\.2\.(.*)$": r"time_in.mlp.fc_out.\1",
|
|
|
|
# Guidance embedder naming (if present)
|
|
r"^guidance_in\.mlp\.0\.(.*)$": r"guidance_in.mlp.fc_in.\1",
|
|
r"^guidance_in\.mlp\.2\.(.*)$": r"guidance_in.mlp.fc_out.\1",
|
|
|
|
# Final layer adaLN modulation
|
|
r"^final_layer\.adaLN_modulation\.1\.(.*)$": r"final_layer.adaLN_modulation.linear.\1",
|
|
|
|
# Text in c_embedder (context embedder) naming
|
|
r"^txt_in\.c_embedder\.linear_1\.(.*)$": r"txt_in.c_embedder.fc_in.\1",
|
|
r"^txt_in\.c_embedder\.linear_2\.(.*)$": r"txt_in.c_embedder.fc_out.\1",
|
|
|
|
# Text in t_embedder (time embedder) naming
|
|
r"^txt_in\.t_embedder\.mlp\.0\.(.*)$": r"txt_in.t_embedder.mlp.fc_in.\1",
|
|
r"^txt_in\.t_embedder\.mlp\.2\.(.*)$": r"txt_in.t_embedder.mlp.fc_out.\1",
|
|
|
|
# Refiner block MLP naming - need to match the actual checkpoint names
|
|
r"^txt_in\.refiner_blocks\.(\d+)\.mlp\.fc1\.(.*)$": r"txt_in.refiner_blocks.\1.mlp.fc_in.\2",
|
|
r"^txt_in\.refiner_blocks\.(\d+)\.mlp\.fc2\.(.*)$": r"txt_in.refiner_blocks.\1.mlp.fc_out.\2",
|
|
|
|
# Refiner block adaLN modulation naming - matches .adaLN_modulation.1.weight/bias
|
|
r"^txt_in\.refiner_blocks\.(\d+)\.adaLN_modulation\.1\.(.*)$":
|
|
r"txt_in.refiner_blocks.\1.adaLN_modulation.linear.\2",
|
|
|
|
# Camera net - keep as-is (already correct)
|
|
# r"^camera_net\.(.*)$": r"camera_net.\1",
|
|
}
|
|
|
|
|
|
def apply_mapping(key: str) -> str:
|
|
"""Apply name mapping to convert official key to FastVideo key.
|
|
|
|
Mappings are applied repeatedly until no more changes occur.
|
|
This allows for chained transformations.
|
|
"""
|
|
prev_key = None
|
|
while prev_key != key:
|
|
prev_key = key
|
|
for pattern, replacement in PARAM_NAME_MAP.items():
|
|
if re.match(pattern, key):
|
|
key = re.sub(pattern, replacement, key)
|
|
break # Apply only one mapping per iteration
|
|
return key
|
|
|
|
|
|
def load_deepspeed_checkpoint(path: Path) -> dict[str, torch.Tensor]:
|
|
"""Load weights from a DeepSpeed checkpoint file."""
|
|
checkpoint = torch.load(path, map_location="cpu")
|
|
|
|
if "module" in checkpoint:
|
|
state_dict = checkpoint["module"]
|
|
elif "state_dict" in checkpoint:
|
|
state_dict = checkpoint["state_dict"]
|
|
elif "model" in checkpoint:
|
|
state_dict = checkpoint["model"]
|
|
else:
|
|
state_dict = checkpoint
|
|
|
|
if not isinstance(state_dict, dict):
|
|
raise TypeError(f"Expected dict state_dict, got {type(state_dict)}")
|
|
|
|
return state_dict
|
|
|
|
|
|
def convert_weights(
|
|
input_path: Path,
|
|
output_dir: Path,
|
|
save_config: bool = True,
|
|
verbose: bool = False,
|
|
) -> dict[str, int]:
|
|
"""
|
|
Convert GameCraft weights to FastVideo format.
|
|
|
|
Returns:
|
|
dict with conversion statistics
|
|
"""
|
|
print(f"Loading checkpoint from {input_path}")
|
|
state_dict = load_deepspeed_checkpoint(input_path)
|
|
|
|
print(f"Found {len(state_dict)} parameters")
|
|
|
|
# Convert weights
|
|
converted_state_dict: OrderedDict[str, torch.Tensor] = OrderedDict()
|
|
renamed_count = 0
|
|
unchanged_count = 0
|
|
|
|
for key, value in state_dict.items():
|
|
new_key = apply_mapping(key)
|
|
if new_key != key:
|
|
if verbose:
|
|
print(f" {key} -> {new_key}")
|
|
renamed_count += 1
|
|
else:
|
|
unchanged_count += 1
|
|
converted_state_dict[new_key] = value
|
|
|
|
print(f"Renamed {renamed_count} parameters, {unchanged_count} unchanged")
|
|
|
|
# Create output directory structure
|
|
transformer_dir = output_dir / "transformer"
|
|
transformer_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Save as safetensors
|
|
output_path = transformer_dir / "diffusion_pytorch_model.safetensors"
|
|
print(f"Saving converted weights to {output_path}")
|
|
save_file(converted_state_dict, str(output_path))
|
|
|
|
# Save config if requested
|
|
if save_config:
|
|
config = {
|
|
"_class_name": "HunyuanGameCraftTransformer3DModel",
|
|
"_fastvideo_version": "0.1.0",
|
|
"in_channels": 33, # 16 + 16 + 1 for concat mode
|
|
"out_channels": 16,
|
|
"hidden_size": 3072,
|
|
"num_attention_heads": 24,
|
|
"attention_head_dim": 128,
|
|
"num_layers": 20,
|
|
"num_single_layers": 40,
|
|
"num_refiner_layers": 2,
|
|
"patch_size": [1, 2, 2],
|
|
"rope_axes_dim": [16, 56, 56],
|
|
"text_embed_dim": 4096,
|
|
"pooled_projection_dim": 768,
|
|
"mlp_ratio": 4.0,
|
|
"guidance_embeds": False,
|
|
"camera_net": True,
|
|
}
|
|
config_path = transformer_dir / "config.json"
|
|
with open(config_path, "w") as f:
|
|
json.dump(config, f, indent=2)
|
|
print(f"Saved config to {config_path}")
|
|
|
|
# Verify camera_net weights
|
|
camera_keys = [k for k in converted_state_dict.keys() if "camera_net" in k]
|
|
print(f"\nCamera net weights ({len(camera_keys)}):")
|
|
for k in camera_keys:
|
|
print(f" {k}: {converted_state_dict[k].shape}")
|
|
|
|
# Verify img_in weights (should be 33 channels)
|
|
if "img_in.proj.weight" in converted_state_dict:
|
|
img_in_shape = converted_state_dict["img_in.proj.weight"].shape
|
|
print(f"\nimg_in.proj.weight shape: {img_in_shape}")
|
|
expected_in_channels = 33
|
|
actual_in_channels = img_in_shape[1]
|
|
if actual_in_channels != expected_in_channels:
|
|
print(f" WARNING: Expected {expected_in_channels} input channels, got {actual_in_channels}")
|
|
else:
|
|
print(f" OK: 33 input channels (16 latent + 16 gt_latent + 1 mask)")
|
|
|
|
return {
|
|
"total": len(converted_state_dict),
|
|
"renamed": renamed_count,
|
|
"unchanged": unchanged_count,
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Convert Hunyuan-GameCraft-1.0 weights to FastVideo format.")
|
|
parser.add_argument("--input",
|
|
type=str,
|
|
required=True,
|
|
help="Path to official GameCraft checkpoint (mp_rank_00_model_states.pt)")
|
|
parser.add_argument("--output",
|
|
type=str,
|
|
default="official_weights/hunyuan-gamecraft",
|
|
help="Output directory for converted weights")
|
|
parser.add_argument("--no-config", action="store_true", help="Don't save config.json")
|
|
parser.add_argument("--verbose", "-v", action="store_true", help="Print all renamed parameters")
|
|
|
|
args = parser.parse_args()
|
|
|
|
input_path = Path(args.input)
|
|
output_dir = Path(args.output)
|
|
|
|
if not input_path.exists():
|
|
raise FileNotFoundError(f"Input file not found: {input_path}")
|
|
|
|
stats = convert_weights(
|
|
input_path=input_path,
|
|
output_dir=output_dir,
|
|
save_config=not args.no_config,
|
|
verbose=args.verbose,
|
|
)
|
|
|
|
print(f"\nConversion complete!")
|
|
print(f" Total parameters: {stats['total']}")
|
|
print(f" Renamed: {stats['renamed']}")
|
|
print(f" Unchanged: {stats['unchanged']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|