Files
hao-ai-lab-FastVideo/tests/local_tests/transformers/test_gamecraft_parity.py

767 lines
29 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
Numerical parity test for HunyuanGameCraft transformer.
This test compares the FastVideo implementation against the official
Hunyuan-GameCraft-1.0 implementation to ensure numerical alignment.
Usage:
# Run with default paths
pytest tests/local_tests/transformers/test_gamecraft_parity.py -v
# Run with custom paths
GAMECRAFT_OFFICIAL_PATH=path/to/Hunyuan-GameCraft-1.0 \
GAMECRAFT_WEIGHTS_PATH=path/to/weights/gamecraft_models/mp_rank_00_model_states.pt \
pytest tests/local_tests/transformers/test_gamecraft_parity.py -v
# Enable debug logging
GAMECRAFT_DEBUG_LOGS=1 pytest tests/local_tests/transformers/test_gamecraft_parity.py -v
"""
import os
import sys
from pathlib import Path
import pytest
import torch
from torch.testing import assert_close
# Set up distributed environment defaults
os.environ.setdefault("MASTER_ADDR", "localhost")
os.environ.setdefault("MASTER_PORT", "29514")
os.environ.setdefault("DISABLE_SP", "1") # Disable sequence parallelism for testing
repo_root = Path(__file__).resolve().parents[3]
def _add_official_to_path():
"""Add official GameCraft implementation to Python path."""
official_path = Path(
os.getenv("GAMECRAFT_OFFICIAL_PATH", repo_root / "Hunyuan-GameCraft-1.0")
)
if official_path.exists() and str(official_path) not in sys.path:
sys.path.insert(0, str(official_path))
return official_path
def _format_sum(tensor: torch.Tensor | None) -> str:
"""Format tensor sum for logging."""
if tensor is None:
return "None"
return f"{tensor.float().sum().item():.6f}"
def _attach_block_logging(
model: torch.nn.Module,
log_path: Path,
label: str,
enabled: bool,
) -> None:
"""Attach forward hooks to log block outputs."""
if not enabled:
return
log_path.parent.mkdir(parents=True, exist_ok=True)
if log_path.exists():
log_path.unlink()
def _hook_factory(block_idx: int, block_type: str):
def _hook(_module, _inputs, outputs):
if isinstance(outputs, tuple):
img_out, txt_out = outputs
img_sum = _format_sum(img_out)
txt_sum = _format_sum(txt_out)
out_str = f"img_sum={img_sum},txt_sum={txt_sum}"
else:
out_str = f"out_sum={_format_sum(outputs)}"
with log_path.open("a", encoding="utf-8") as f:
f.write(f"{label}:{block_type}:{block_idx}:{out_str}\n")
return _hook
# Attach to double blocks
if hasattr(model, "double_blocks"):
for idx, block in enumerate(model.double_blocks):
block.register_forward_hook(_hook_factory(idx, "double"))
# Attach to single blocks
if hasattr(model, "single_blocks"):
for idx, block in enumerate(model.single_blocks):
block.register_forward_hook(_hook_factory(idx, "single"))
def _load_official_model(official_path: Path, weights_path: Path, device: torch.device, dtype: torch.dtype):
"""Load the official GameCraft model."""
try:
# Add official implementation to path
sys.path.insert(0, str(official_path))
from hymm_sp.modules.models import HYVideoDiffusionTransformer, HUNYUAN_VIDEO_CONFIG
from hymm_sp.config import parse_args
# Create a minimal args object
class MinimalArgs:
text_projection = "single_refiner"
text_states_dim = 4096
use_attention_mask = False
text_states_dim_2 = 768
args = MinimalArgs()
config = HUNYUAN_VIDEO_CONFIG['HYVideo-T/2']
# Initialize model
model = HYVideoDiffusionTransformer(
args=args,
patch_size=[1, 2, 2],
in_channels=16, # Will be adjusted for concat mode
out_channels=16,
hidden_size=config['hidden_size'],
mlp_width_ratio=config['mlp_width_ratio'],
num_heads=config['num_heads'],
depth_double_blocks=config['depth_double_blocks'],
depth_single_blocks=config['depth_single_blocks'],
rope_dim_list=config['rope_dim_list'],
guidance_embed=False,
dtype=dtype,
device='cpu', # Load on CPU first
multitask_mask_training_type="concat", # GameCraft uses concat mode
camera_in_channels=6,
camera_down_coef=8,
)
# Load weights
checkpoint = torch.load(weights_path, map_location='cpu')
state_dict = checkpoint['module']
model.load_state_dict(state_dict, strict=True)
model = model.to(device=device, dtype=dtype)
model.eval()
return model
except ImportError as e:
pytest.skip(f"Failed to import official GameCraft: {e}")
except Exception as e:
pytest.skip(f"Failed to load official GameCraft model: {e}")
def _create_test_inputs(batch_size: int, device: torch.device, dtype: torch.dtype):
"""Create test inputs for the transformer."""
# Video latent shape: [B, C, T, H, W]
# For GameCraft: C=33 (16 latent + 16 gt_latent + 1 mask)
frames = 9 # 9 latent frames = 33 video frames with temporal compression
height = 88 # 704 / 8
width = 160 # 1280 / 8
# Pure latents (what we're denoising)
latents = torch.randn(batch_size, 16, frames, height, width, device=device, dtype=dtype)
# Ground truth latents (history/reference)
gt_latents = torch.randn(batch_size, 16, frames, height, width, device=device, dtype=dtype)
# Mask (1 = use gt, 0 = generate)
mask = torch.zeros(batch_size, 1, frames, height, width, device=device, dtype=dtype)
mask[:, :, :frames//2] = 1.0 # First half is conditioned
# Concatenated input
x = torch.cat([latents, gt_latents, mask], dim=1) # [B, 33, T, H, W]
# Timestep
t = torch.tensor([500.0] * batch_size, device=device, dtype=dtype)
# Text embeddings (LLaMA)
text_states = torch.randn(batch_size, 77, 4096, device=device, dtype=dtype)
text_mask = torch.ones(batch_size, 77, device=device, dtype=dtype)
# CLIP embeddings
text_states_2 = torch.randn(batch_size, 768, device=device, dtype=dtype)
# Camera/action states: [B, T_video, 6, H_video, W_video]
# For 9 latent frames with temporal 4x compression -> ~36 video frames
# But camera net expects raw video resolution
video_frames = 33 # (9-1)*4 + 1 for 884 VAE
cam_latents = torch.randn(batch_size, video_frames, 6, 704, 1280, device=device, dtype=dtype)
return {
'x': x,
't': t,
'text_states': text_states,
'text_mask': text_mask,
'text_states_2': text_states_2,
'cam_latents': cam_latents,
}
def _get_rotary_embeddings(shape, hidden_size, num_heads, rope_dim_list, device, dtype):
"""Generate rotary position embeddings."""
# Simplified RoPE generation for testing
# In production, this uses the full RoPE implementation
t, h, w = shape
seq_len = t * h * w
head_dim = hidden_size // num_heads
freqs_cos = torch.ones(seq_len, head_dim, device=device, dtype=dtype)
freqs_sin = torch.zeros(seq_len, head_dim, device=device, dtype=dtype)
return freqs_cos, freqs_sin
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
def test_gamecraft_transformer_parity():
"""Test numerical parity between official and FastVideo GameCraft implementations."""
torch.manual_seed(42)
# Initialize distributed environment for FastVideo
from fastvideo.distributed.parallel_state import init_distributed_environment, initialize_model_parallel
init_distributed_environment(world_size=1, rank=0, local_rank=0)
initialize_model_parallel(tensor_model_parallel_size=1, sequence_model_parallel_size=1)
# Get paths
official_path = Path(
os.getenv("GAMECRAFT_OFFICIAL_PATH", repo_root / "Hunyuan-GameCraft-1.0")
)
weights_path = Path(
os.getenv(
"GAMECRAFT_WEIGHTS_PATH",
repo_root / "Hunyuan-GameCraft-1.0" / "weights" / "gamecraft_models" / "mp_rank_00_model_states.pt"
)
)
if not official_path.exists():
pytest.skip(f"Official GameCraft repo not found at {official_path}")
if not weights_path.exists():
pytest.skip(f"GameCraft weights not found at {weights_path}")
device = torch.device("cuda:0")
dtype = torch.bfloat16
batch_size = 1
# Load official model
print(f"\n[GameCraft TEST] Loading official model from {weights_path}")
official_model = _load_official_model(official_path, weights_path, device, dtype)
# Enable debug logging if requested
debug_logs = os.getenv("GAMECRAFT_DEBUG_LOGS", "0") == "1"
_attach_block_logging(
official_model,
repo_root / "gamecraft_debug" / "official.log",
"official",
debug_logs,
)
# Create test inputs
# IMPORTANT: latent_len must be 9, 10, or 18 for official model's camera conditioning
frames = 9 # Valid latent temporal length
height = 44 # 352 / 8 (small spatial resolution for testing)
width = 80 # 640 / 8
latents = torch.randn(batch_size, 16, frames, height, width, device=device, dtype=dtype)
gt_latents = torch.randn(batch_size, 16, frames, height, width, device=device, dtype=dtype)
mask = torch.zeros(batch_size, 1, frames, height, width, device=device, dtype=dtype)
mask[:, :, :4] = 1.0 # First 4 frames are conditioned (history)
x = torch.cat([latents, gt_latents, mask], dim=1) # [B, 33, T, H, W]
t = torch.tensor([500.0], device=device, dtype=dtype)
text_states = torch.randn(batch_size, 32, 4096, device=device, dtype=dtype)
text_mask = torch.ones(batch_size, 32, device=device, dtype=dtype)
text_states_2 = torch.randn(batch_size, 768, device=device, dtype=dtype)
# Camera states: for 9 latent frames -> 33 video frames with 884 VAE
video_frames = 33
cam_latents = torch.randn(batch_size, video_frames, 6, 352, 640, device=device, dtype=dtype)
# Get RoPE embeddings - after patchify with patch_size=[1,2,2]: T, H/2, W/2
tt, th, tw = frames, height // 2, width // 2
# Use FastVideo's RoPE implementation for both models
from fastvideo.layers.rotary_embedding import get_rotary_pos_embed
hidden_size = 3072
num_heads = 24
rope_dim_list = [16, 56, 56] # From GameCraft config
rope_theta = 256.0
freqs_cos, freqs_sin = get_rotary_pos_embed(
(tt, th, tw),
hidden_size,
num_heads,
rope_dim_list,
rope_theta,
)
freqs_cos = freqs_cos.to(device=device, dtype=dtype)
freqs_sin = freqs_sin.to(device=device, dtype=dtype)
print(f"[GameCraft TEST] Input shapes:")
print(f" x: {x.shape}")
print(f" text_states: {text_states.shape}")
print(f" cam_latents: {cam_latents.shape}")
# Run official model
with torch.no_grad():
official_output = official_model(
x=x,
t=t,
text_states=text_states,
text_mask=text_mask,
text_states_2=text_states_2,
freqs_cos=freqs_cos,
freqs_sin=freqs_sin,
cam_latents=cam_latents,
return_dict=True,
)
if isinstance(official_output, dict):
official_out = official_output['x']
else:
official_out = official_output
print(f"[GameCraft TEST] Official output shape: {official_out.shape}")
print(f"[GameCraft TEST] Official output stats: mean={official_out.mean():.6f}, std={official_out.std():.6f}")
# Load FastVideo GameCraft model
print("[GameCraft TEST] Loading FastVideo GameCraft model...")
from fastvideo.configs.models.dits.hunyuangamecraft import HunyuanGameCraftConfig
from fastvideo.models.dits.hunyuangamecraft import HunyuanGameCraftTransformer3DModel
# Create FastVideo config
fastvideo_config = HunyuanGameCraftConfig()
fastvideo_config.dtype = dtype
# Load converted weights
converted_weights_path = repo_root / "official_weights" / "hunyuan-gamecraft" / "transformer"
if not converted_weights_path.exists():
# Try to convert weights on-the-fly
print(f"[GameCraft TEST] Converting weights from {weights_path}...")
from scripts.checkpoint_conversion.convert_gamecraft_weights import convert_gamecraft_weights
convert_gamecraft_weights(
input_path=str(weights_path),
output_dir=str(converted_weights_path),
)
# Load hf_config from converted weights
import json
config_path = converted_weights_path / "config.json"
if config_path.exists():
with open(config_path) as f:
hf_config = json.load(f)
else:
# Create minimal hf_config
hf_config = {
"hidden_size": 3072,
"num_attention_heads": 24,
"num_layers": 20,
"num_single_layers": 40,
}
# Initialize FastVideo model
fastvideo_model = HunyuanGameCraftTransformer3DModel(
config=fastvideo_config,
hf_config=hf_config,
)
# Load weights
from safetensors.torch import load_file
safetensor_files = list(converted_weights_path.glob("*.safetensors"))
if safetensor_files:
state_dict = {}
for sf_file in safetensor_files:
state_dict.update(load_file(sf_file))
# Apply param_names_mapping in reverse
reverse_mapping = fastvideo_config.reverse_param_names_mapping
mapped_state_dict = {}
for key, value in state_dict.items():
mapped_key = key
for pattern, replacement in reverse_mapping.items():
import re
if re.match(pattern, key):
mapped_key = re.sub(pattern, replacement, key)
break
mapped_state_dict[mapped_key] = value
missing, unexpected = fastvideo_model.load_state_dict(mapped_state_dict, strict=False)
print(f"[GameCraft TEST] Missing keys: {len(missing)}")
print(f"[GameCraft TEST] Unexpected keys: {len(unexpected)}")
if missing:
print(f"[GameCraft TEST] First 5 missing: {missing[:5]}")
if unexpected:
print(f"[GameCraft TEST] First 5 unexpected: {unexpected[:5]}")
fastvideo_model = fastvideo_model.to(device=device, dtype=dtype)
fastvideo_model.eval()
_attach_block_logging(
fastvideo_model,
repo_root / "gamecraft_debug" / "fastvideo.log",
"fastvideo",
debug_logs,
)
# Prepare FastVideo inputs
# FastVideo expects encoder_hidden_states as [text_states, text_states_2]
fastvideo_encoder_hidden_states = [text_states, text_states_2]
# Import forward context
from fastvideo.forward_context import set_forward_context
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
forward_batch = ForwardBatch(data_type="video")
# Run FastVideo model
with torch.no_grad():
with set_forward_context(current_timestep=0, attn_metadata=None, forward_batch=forward_batch):
fastvideo_out = fastvideo_model(
x, # Already concatenated [B, 33, T, H, W]
fastvideo_encoder_hidden_states,
t,
camera_states=cam_latents,
encoder_attention_mask=[text_mask],
)
print(f"[GameCraft TEST] FastVideo output shape: {fastvideo_out.shape}")
print(f"[GameCraft TEST] FastVideo output stats: mean={fastvideo_out.mean():.6f}, std={fastvideo_out.std():.6f}")
# Compare outputs using appropriate metrics for numerical parity
print("[GameCraft TEST] Comparing outputs...")
# Compute various metrics
diff = (official_out - fastvideo_out).abs()
max_diff = diff.max().item()
mean_diff = diff.mean().item()
# Correlation (most important for numerical alignment)
corr = torch.corrcoef(torch.stack([
official_out.flatten().float(),
fastvideo_out.flatten().float()
]))[0, 1].item()
# Cosine similarity
cos_sim = torch.nn.functional.cosine_similarity(
official_out.flatten().unsqueeze(0).float(),
fastvideo_out.flatten().unsqueeze(0).float()
).item()
# Sum comparison (should match exactly)
off_sum = official_out.double().sum().item()
fv_sum = fastvideo_out.double().sum().item()
sum_diff_pct = abs(off_sum - fv_sum) / (abs(off_sum) + 1e-10) * 100
print(f"[GameCraft TEST] Max diff: {max_diff:.4f}")
print(f"[GameCraft TEST] Mean diff: {mean_diff:.6f}")
print(f"[GameCraft TEST] Correlation: {corr:.6f}")
print(f"[GameCraft TEST] Cosine similarity: {cos_sim:.6f}")
print(f"[GameCraft TEST] Sum diff: {sum_diff_pct:.4f}%")
# Pass criteria: correlation > 0.99 and cosine_sim > 0.99 and sum_diff < 10%
# Sum diff can be larger due to floating point accumulation differences
assert corr > 0.99, f"Correlation too low: {corr}"
assert cos_sim > 0.99, f"Cosine similarity too low: {cos_sim}"
assert sum_diff_pct < 10.0, f"Sum difference too high: {sum_diff_pct}%"
print("[GameCraft TEST] PASSED: Outputs are numerically aligned!")
print("[GameCraft TEST] Parity test complete!")
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
def test_gamecraft_layer_by_layer():
"""Test layer-by-layer parity to find divergence point."""
import os
os.environ['DISABLE_SP'] = '1'
torch.manual_seed(42)
from fastvideo.distributed.parallel_state import init_distributed_environment, initialize_model_parallel
init_distributed_environment(world_size=1, rank=0, local_rank=0)
initialize_model_parallel(tensor_model_parallel_size=1, sequence_model_parallel_size=1)
official_path = Path(
os.getenv("GAMECRAFT_OFFICIAL_PATH", repo_root / "Hunyuan-GameCraft-1.0")
)
weights_path = Path(
os.getenv(
"GAMECRAFT_WEIGHTS_PATH",
repo_root / "Hunyuan-GameCraft-1.0" / "weights" / "gamecraft_models" / "mp_rank_00_model_states.pt"
)
)
if not official_path.exists():
pytest.skip(f"Official GameCraft repo not found at {official_path}")
if not weights_path.exists():
pytest.skip(f"GameCraft weights not found at {weights_path}")
device = torch.device("cuda:0")
dtype = torch.bfloat16
batch_size = 1
frames = 9
height, width = 44, 80
text_seq_len = 32
# Load official model
print("\n[LAYER TEST] Loading official model...")
official_model = _load_official_model(official_path, weights_path, device, dtype)
# Load FastVideo model
print("[LAYER TEST] Loading FastVideo model...")
from fastvideo.configs.models.dits.hunyuangamecraft import HunyuanGameCraftConfig
from fastvideo.models.dits.hunyuangamecraft import HunyuanGameCraftTransformer3DModel
fastvideo_config = HunyuanGameCraftConfig()
converted_weights_path = repo_root / "official_weights" / "hunyuan-gamecraft" / "transformer"
import json
config_path = converted_weights_path / "config.json"
hf_config = {}
if config_path.exists():
with open(config_path) as f:
hf_config = json.load(f)
fastvideo_model = HunyuanGameCraftTransformer3DModel(config=fastvideo_config, hf_config=hf_config)
from safetensors.torch import load_file
safetensor_files = list(converted_weights_path.glob("*.safetensors"))
if safetensor_files:
state_dict = {}
for sf_file in safetensor_files:
state_dict.update(load_file(sf_file))
reverse_mapping = fastvideo_config.reverse_param_names_mapping
mapped_state_dict = {}
for key, value in state_dict.items():
mapped_key = key
for pattern, replacement in reverse_mapping.items():
import re
if re.match(pattern, key):
mapped_key = re.sub(pattern, replacement, key)
break
mapped_state_dict[mapped_key] = value
fastvideo_model.load_state_dict(mapped_state_dict, strict=False)
fastvideo_model = fastvideo_model.to(device=device, dtype=dtype)
fastvideo_model.eval()
# Create inputs
torch.manual_seed(42)
x = torch.randn(batch_size, 33, frames, height, width, device=device, dtype=dtype)
t = torch.tensor([500.0], device=device, dtype=dtype)
text_states = torch.randn(batch_size, text_seq_len, 4096, device=device, dtype=dtype)
text_states_2 = torch.randn(batch_size, 768, device=device, dtype=dtype)
cam_latents = torch.randn(batch_size, 33, 6, 352, 640, device=device, dtype=dtype)
# RoPE
tt, th, tw = frames, height // 2, width // 2
from fastvideo.layers.rotary_embedding import get_rotary_pos_embed
freqs_cos, freqs_sin = get_rotary_pos_embed((tt, th, tw), 3072, 24, [16, 56, 56], 256.0)
freqs_cos = freqs_cos.to(device=device, dtype=dtype)
freqs_sin = freqs_sin.to(device=device, dtype=dtype)
print("[LAYER TEST] Testing embedding layers...")
# Set up FastVideo context early (needed for txt_in)
from fastvideo.forward_context import set_forward_context
from fastvideo.attention.backends.flash_attn import FlashAttnMetadataBuilder
attn_metadata = FlashAttnMetadataBuilder().build(current_timestep=0, attn_mask=None)
with torch.no_grad(), set_forward_context(current_timestep=0, attn_metadata=attn_metadata):
# === img_in ===
off_img = official_model.img_in(x)
fv_img = fastvideo_model.img_in(x)
diff = (off_img - fv_img).abs().max().item()
print(f" img_in: max_diff={diff:.6f} {'OK' if diff < 1e-3 else 'DIFF!'}")
# === time_in ===
off_time = official_model.time_in(t)
fv_time = fastvideo_model.time_in(t)
diff = (off_time - fv_time).abs().max().item()
print(f" time_in: max_diff={diff:.6f} {'OK' if diff < 1e-3 else 'DIFF!'}")
# === vector_in ===
off_vec = official_model.vector_in(text_states_2)
fv_vec = fastvideo_model.vector_in(text_states_2)
diff = (off_vec - fv_vec).abs().max().item()
print(f" vector_in: max_diff={diff:.6f} {'OK' if diff < 1e-3 else 'DIFF!'}")
# === txt_in ===
off_txt = official_model.txt_in(text_states, t, None)
fv_txt = fastvideo_model.txt_in(text_states, t)
diff = (off_txt - fv_txt).abs().max().item()
print(f" txt_in: max_diff={diff:.6f} {'OK' if diff < 1e-3 else 'DIFF!'}")
# === camera_net ===
off_cam = official_model.camera_net(cam_latents)
fv_cam = fastvideo_model.camera_net(cam_latents)
diff = (off_cam - fv_cam).abs().max().item()
print(f" camera_net: max_diff={diff:.6f} {'OK' if diff < 1e-3 else 'DIFF!'}")
# Combined embeddings
off_img_combined = off_img + off_cam
fv_img_combined = fv_img + fv_cam
off_vec_combined = off_time + off_vec
fv_vec_combined = fv_time + fv_vec
print("\n[LAYER TEST] Testing double blocks (first 5)...")
# cu_seqlens for official model
seq_len = off_img_combined.shape[1] + off_txt.shape[1]
cu_seqlens = torch.tensor([0, seq_len, seq_len], device=device, dtype=torch.int32)
off_img_cur, off_txt_cur = off_img_combined, off_txt
fv_img_cur, fv_txt_cur = fv_img_combined, fv_txt
for i in range(min(5, len(official_model.double_blocks))):
# Official
off_img_out, off_txt_out = official_model.double_blocks[i](
off_img_cur, off_txt_cur, off_vec_combined,
cu_seqlens_q=cu_seqlens, cu_seqlens_kv=cu_seqlens,
max_seqlen_q=seq_len, max_seqlen_kv=seq_len,
freqs_cis=(freqs_cos, freqs_sin), use_sage=False
)
# FastVideo (context already set above)
fv_img_out, fv_txt_out = fastvideo_model.double_blocks[i](
fv_img_cur, fv_txt_cur, fv_vec_combined,
freqs_cis=(freqs_cos, freqs_sin)
)
img_diff = (off_img_out - fv_img_out).abs().max().item()
txt_diff = (off_txt_out - fv_txt_out).abs().max().item()
img_mean_diff = (off_img_out - fv_img_out).abs().mean().item()
status = 'OK' if img_diff < 0.1 else 'DIFF!'
print(f" double_block[{i}]: img_max_diff={img_diff:.4f}, img_mean_diff={img_mean_diff:.6f}, txt_max_diff={txt_diff:.4f} {status}")
# Use outputs for next layer
off_img_cur, off_txt_cur = off_img_out, off_txt_out
fv_img_cur, fv_txt_cur = fv_img_out, fv_txt_out
if img_diff > 1.0:
print(f" [STOPPING] Large divergence at double_block[{i}]")
break
print("\n[LAYER TEST] Done.")
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
def test_gamecraft_cameranet_parity():
"""Test CameraNet component in isolation."""
torch.manual_seed(42)
official_path = Path(
os.getenv("GAMECRAFT_OFFICIAL_PATH", repo_root / "Hunyuan-GameCraft-1.0")
)
weights_path = Path(
os.getenv(
"GAMECRAFT_WEIGHTS_PATH",
repo_root / "Hunyuan-GameCraft-1.0" / "weights" / "gamecraft_models" / "mp_rank_00_model_states.pt"
)
)
if not official_path.exists():
pytest.skip(f"Official GameCraft repo not found at {official_path}")
if not weights_path.exists():
pytest.skip(f"GameCraft weights not found at {weights_path}")
device = torch.device("cuda:0")
dtype = torch.bfloat16
# Add official to path
_add_official_to_path()
try:
from hymm_sp.modules.cameranet import CameraNet
except ImportError as e:
pytest.skip(f"Failed to import CameraNet: {e}")
# Load CameraNet weights
checkpoint = torch.load(weights_path, map_location='cpu')
state_dict = checkpoint['module']
# Extract CameraNet weights
camera_state_dict = {}
for k, v in state_dict.items():
if k.startswith('camera_net.'):
new_key = k.replace('camera_net.', '')
camera_state_dict[new_key] = v
# Initialize CameraNet
camera_net = CameraNet(
in_channels=6,
downscale_coef=8,
out_channels=16,
patch_size=[1, 2, 2],
hidden_size=3072,
)
camera_net.load_state_dict(camera_state_dict, strict=True)
camera_net = camera_net.to(device=device, dtype=dtype)
camera_net.eval()
# Test input
batch_size = 1
num_frames = 17
cam_input = torch.randn(batch_size, num_frames, 6, 352, 640, device=device, dtype=dtype)
with torch.no_grad():
cam_output = camera_net(cam_input)
print(f"[CameraNet TEST] Official input shape: {cam_input.shape}")
print(f"[CameraNet TEST] Official output shape: {cam_output.shape}")
print(f"[CameraNet TEST] Official output stats: mean={cam_output.mean():.6f}, std={cam_output.std():.6f}")
# Load FastVideo CameraNet
print("[CameraNet TEST] Loading FastVideo CameraNet...")
from fastvideo.models.dits.hunyuangamecraft import CameraNet as FastVideoCameraNet
# Initialize FastVideo CameraNet
fastvideo_camera_net = FastVideoCameraNet(
in_channels=6,
downscale_coef=8,
out_channels=16,
patch_size=[1, 2, 2],
hidden_size=3072,
dtype=dtype,
prefix="camera_net",
)
# Map official weights to FastVideo format
fastvideo_camera_state_dict = {}
for k, v in camera_state_dict.items():
new_key = k
# Map camera_in -> PatchEmbed format
if "camera_in.proj" in k:
new_key = k # Keep as-is for now
fastvideo_camera_state_dict[new_key] = v
missing, unexpected = fastvideo_camera_net.load_state_dict(fastvideo_camera_state_dict, strict=False)
print(f"[CameraNet TEST] Missing keys: {len(missing)}, Unexpected keys: {len(unexpected)}")
if missing:
print(f"[CameraNet TEST] Missing: {missing}")
if unexpected:
print(f"[CameraNet TEST] Unexpected: {unexpected}")
fastvideo_camera_net = fastvideo_camera_net.to(device=device, dtype=dtype)
fastvideo_camera_net.eval()
# Run FastVideo CameraNet
with torch.no_grad():
fastvideo_cam_output = fastvideo_camera_net(cam_input)
print(f"[CameraNet TEST] FastVideo output shape: {fastvideo_cam_output.shape}")
print(f"[CameraNet TEST] FastVideo output stats: mean={fastvideo_cam_output.mean():.6f}, std={fastvideo_cam_output.std():.6f}")
# Compare outputs
print("[CameraNet TEST] Comparing outputs...")
try:
assert_close(cam_output, fastvideo_cam_output, atol=0.0, rtol=0.0)
print("[CameraNet TEST] PASSED: CameraNet outputs match!")
except AssertionError as e:
diff = (cam_output - fastvideo_cam_output).abs()
print(f"[CameraNet TEST] Max diff: {diff.max():.6f}")
print(f"[CameraNet TEST] Mean diff: {diff.mean():.6f}")
print(f"[CameraNet TEST] FAILED: {e}")
raise
print("[CameraNet TEST] CameraNet parity test complete!")
if __name__ == "__main__":
# Run tests directly
test_gamecraft_cameranet_parity()
test_gamecraft_transformer_parity()