Files
2026-07-13 13:16:54 +08:00

464 lines
19 KiB
Python

# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# coding: utf-8
import warnings
warnings.filterwarnings("ignore", message=".*pkg_resources is deprecated.*", category=UserWarning)
warnings.filterwarnings("ignore", category=FutureWarning, module="diffusers.models.transformers.transformer_2d")
import os
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
import os.path as osp
from copy import deepcopy
import json
from typing import Tuple, cast, Optional
import torch
import torch.distributed as dist
from torch.utils.data import DataLoader
from transformers import HfArgumentParser, set_seed
from transformers.models.qwen2_5_vl.configuration_qwen2_5_vl import Qwen2_5_VLVisionConfig
from safetensors.torch import load_file
from PIL import Image
from torchvision.utils import make_grid
import numpy as np
from tqdm import trange
from data.dataset_base import DataConfig, simple_custom_collate
from data.data_utils import add_special_tokens
from modeling.vae.wan.model import WanVideoVAE
from modeling.lance import LanceConfig, Lance, Qwen2ForCausalLM
from modeling.qwen2 import Qwen2Tokenizer
from modeling.qwen2.modeling_qwen2 import Qwen2Config
from modeling.vit.qwen2_5_vl_vit import Qwen2_5_VisionTransformerPretrainedModel
from common.utils.misc import tuple_mul, AutoEncoderParams
from common.val.utils import make_padded_latent
from data.datasets_custom import ValidationDataset
from config.config_factory import ModelArguments, DataArguments, EvaluationArguments, get_model_path
def init_from_model_path_if_needed(model: Qwen2ForCausalLM, model_args: ModelArguments):
# Always load the trained Lance checkpoint from model_path.
path_dir = model_args.model_path
ema_path = osp.join(path_dir, "ema.safetensors")
model_path = osp.join(path_dir, "model.safetensors")
model_path_ft = None
if osp.exists(model_path):
model_path_ft = model_path
elif osp.exists(ema_path):
model_path_ft = ema_path
if model_path_ft:
model_state_dict = load_file(model_path_ft, device="cpu")
else:
raise FileNotFoundError(
f"Fine-tuning failed: No valid checkpoint ('ema.safetensors' or 'model.safetensors') found in {path_dir}"
)
# NOTE: position embeds are fixed sinusoidal embeddings, so we can just pop it off,
# which makes it easier to adapt to different resolutions.
if 'latent_pos_embed.pos_embed' in model_state_dict:
model_state_dict.pop('latent_pos_embed.pos_embed')
model.load_state_dict(model_state_dict, strict=False)
clean_memory(model_state_dict)
def clean_memory(*objects):
"""清理内存并释放 GPU 缓存"""
for obj in objects:
del obj
import gc
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
def decode_video_tensor_for_geneval(v_list):
"""
专门为 GenEVAL 解码视频张量,保持原有的保存格式
"""
N_target = len(v_list)
if N_target != 1:
from einops import rearrange
padded_videos_latent = [v.permute(1, 0, 2, 3) for v in v_list]
v_tc_hw = rearrange(padded_videos_latent, "n t c h w -> t c h (n w)")
else:
v_tc_hw = v_list[0].permute(1, 0, 2, 3)
v_tc_hw = v_tc_hw.float().clip(-1, 1).mul_(0.5).add_(0.5).mul_(255).round().clamp(0, 255).to(torch.uint8)
return v_tc_hw
def resolve_geneval_paths(
model_args: ModelArguments,
data_args: DataArguments,
) -> None:
if not model_args.model_path:
raise ValueError("GenEVAL requires --model_path to be provided explicitly.")
if not model_args.vit_path:
model_args.vit_path = get_model_path("vit.qwen2_5_vl")
if not data_args.val_dataset_config_file:
data_args.val_dataset_config_file = get_model_path("geneval.data")
def build_runtime_dataset_config(
model_args: ModelArguments,
inference_args: EvaluationArguments,
vae_config: Optional[AutoEncoderParams],
) -> DataConfig:
"""
当前推理链不再依赖 dataset_config_file,运行期 DataConfig 由显式参数拼装。
"""
dataset_config = DataConfig()
dataset_config.num_frames = inference_args.num_frames
dataset_config.H = inference_args.video_height
dataset_config.W = inference_args.video_width
dataset_config.task = inference_args.task
dataset_config.resolution = inference_args.resolution
dataset_config.text_template = inference_args.text_template
dataset_config.max_duration = inference_args.max_duration
dataset_config.system_prompt_type = inference_args.system_prompt_type
if inference_args.visual_und:
dataset_config.vit_patch_size = model_args.vit_patch_size
dataset_config.vit_patch_size_temporal = model_args.vit_patch_size_temporal
dataset_config.vit_max_num_patch_per_side = model_args.vit_max_num_patch_per_side
if inference_args.visual_gen and vae_config:
assert len(model_args.latent_patch_size) == 3, "len(latent_patch_size) must be 3"
dataset_config.latent_patch_size = model_args.latent_patch_size
dataset_config.vae_downsample = tuple_mul(
model_args.latent_patch_size,
(vae_config.downsample_temporal, vae_config.downsample_spatial, vae_config.downsample_spatial),
)
dataset_config.max_latent_size = model_args.max_latent_size
dataset_config.max_num_frames = model_args.max_num_frames
dataset_config.text_cond_dropout_prob = model_args.text_cond_dropout_prob
dataset_config.vae_cond_dropout_prob = model_args.vae_cond_dropout_prob
dataset_config.vit_cond_dropout_prob = model_args.vit_cond_dropout_prob
return dataset_config
def validate_on_fixed_batch(
fsdp_model: Lance,
vae_model: Optional[WanVideoVAE],
val_data_cpu: dict,
model_args: ModelArguments,
inference_args: EvaluationArguments,
new_token_ids,
image_token_id: int,
device: int,
save_source_video: bool = False,
save_path_gen: str = "",
sample_num_per_prompt: int = 1,
):
"""
验证逻辑,保持与原文件相同的保存格式
"""
# Check whether distributed execution has been initialized.
if dist.is_initialized():
is_rank0 = (dist.get_rank() == 0)
else:
is_rank0 = True
val_data = val_data_cpu.cuda(device).to_dict()
with torch.no_grad(), torch.amp.autocast("cuda", enabled=True, dtype=torch.bfloat16):
# Compute padded_latent.
if "padded_videos" in val_data.keys():
val_data["padded_latent"] = make_padded_latent(val_data["padded_videos"], val_data["vae_data_mode"], vae_model)
# Create an output folder from val_data["index"] first.
index_save = val_data["index"]
index_save = f"{index_save:05d}"
os.makedirs(os.path.join(save_path_gen, index_save), exist_ok=True)
os.makedirs(os.path.join(save_path_gen, index_save, "samples"), exist_ok=True)
# Save metadata.jsonl.
metadata = val_data["additional_info"]
with open(os.path.join(save_path_gen, index_save, "metadata.jsonl"), 'w') as f:
f.write(json.dumps(metadata, ensure_ascii=False) + "\n")
# -------------------- GEN branch --------------------
tensor_list_for_grid = []
loop_iterator = trange(sample_num_per_prompt) if is_rank0 else range(sample_num_per_prompt)
for sample_num_per_prompt_index in loop_iterator:
# Sample generations.
params = {
"val_packed_text_ids": val_data["packed_text_ids"],
"val_packed_text_indexes": val_data["packed_text_indexes"],
"val_sample_lens": val_data["sample_lens"],
"val_packed_position_ids": val_data["packed_position_ids"],
"val_split_lens": val_data["split_lens"],
"val_attn_modes": val_data["attn_modes"],
"val_sample_N_target": val_data["sample_N_target"],
"val_packed_vae_token_indexes": val_data["packed_vae_token_indexes"],
"timestep_shift": inference_args.validation_timestep_shift,
"num_timesteps": inference_args.validation_num_timesteps,
"val_mse_loss_indexes": val_data.get("mse_loss_indexes", None),
"val_padded_latent": val_data["padded_latent"],
"video_sizes": val_data["video_sizes"],
"cfg_text_scale": model_args.cfg_text_scale,
"cfg_interval": inference_args.cfg_interval,
"cfg_renorm_min": inference_args.cfg_renorm_min,
"cfg_renorm_type": inference_args.cfg_renorm_type,
"device": device,
"dtype": torch.bfloat16,
"new_token_ids": new_token_ids,
"max_samples": inference_args.validation_max_samples,
"validation_noise_seed": inference_args.validation_noise_seed + sample_num_per_prompt_index,
"apply_chat_template": inference_args.apply_chat_template,
"apply_qwen_2_5_vl_pos_emb": inference_args.apply_qwen_2_5_vl_pos_emb,
"image_token_id": image_token_id,
"val_packed_vit_token_indexes": val_data.get("packed_vit_token_indexes", None),
"val_packed_vit_tokens": val_data.get("packed_vit_tokens", None),
"vit_video_grid_thw": val_data.get("vit_video_grid_thw", None),
"vae_video_grid_thw": val_data["vae_video_grid_thw"],
"video_grid_thw": val_data.get("video_grid_thw", None),
"caption": val_data.get("caption", None),
"sample_task": val_data["sample_task"],
"sample_modality": val_data["sample_modality"],
"cfg_type": inference_args.cfg_type,
"cfg_uncond_token_id": inference_args.cfg_uncond_token_id,
"index": val_data["index"],
"val_padded_videos": val_data["padded_videos"] if save_source_video else None,
}
if inference_args.use_KVcache:
denoise_latent, _, _, _ = fsdp_model.validation_gen_KVcache(**params)
else:
denoise_latent, _, _, _ = fsdp_model.validation_gen(**params)
# Decode and save.
for latent in denoise_latent:
v_list = [vae_model.vae_decode([latent_])[0] for latent_ in latent]
# Keep the original save format.
v_thwc = decode_video_tensor_for_geneval(v_list)
# Use frame 0 directly.
if v_thwc.shape[0] == 1:
tensor_list_for_grid.append(v_thwc.squeeze(0).cpu())
# Save a single image.
save_name = f"{save_path_gen}/{index_save}/samples/{sample_num_per_prompt_index}.png"
Image.fromarray((v_thwc.squeeze(0).permute(1, 2, 0).cpu().numpy()).astype('uint8')).save(save_name)
else:
raise NotImplementedError("需要保存图像")
# Save the grid image.
save_name = f"{save_path_gen}/{index_save}/grid.png"
grid_tensor = make_grid(tensor_list_for_grid, nrow=int(np.sqrt(sample_num_per_prompt)), padding=0, pad_value=255)
grid_numpy = grid_tensor.permute(1, 2, 0).numpy()
Image.fromarray(grid_numpy).save(save_name)
def main():
# ========================= Env setup ==============================
assert torch.cuda.is_available()
if "RANK" in os.environ and "WORLD_SIZE" in os.environ:
dist.init_process_group("nccl")
GLOBAL_RANK = dist.get_rank()
WORLD_SIZE = dist.get_world_size()
else:
GLOBAL_RANK = 0
WORLD_SIZE = 1
LOCAL_RANK = GLOBAL_RANK % torch.cuda.device_count()
DEVICE = LOCAL_RANK
torch.cuda.set_device(DEVICE)
# ========================= Args and logger setup ==============================
parser = HfArgumentParser((ModelArguments, DataArguments, EvaluationArguments))
model_args, data_args, inference_args = cast(Tuple[ModelArguments, DataArguments, EvaluationArguments], parser.parse_args_into_dataclasses())
# ========================= GenEVAL path resolution ==============================
resolve_geneval_paths(model_args, data_args)
# NOTE: validation_noise_seed matches validation_data_seed.
inference_args.validation_noise_seed = inference_args.evaluation_seed
inference_args.validation_data_seed = inference_args.evaluation_seed
# Set seed:
seed = inference_args.global_seed * WORLD_SIZE + GLOBAL_RANK
set_seed(seed)
log_rank0 = print if GLOBAL_RANK == 0 else (lambda *_: None)
# ========================= LLM model setup ==============================
llm_config: Qwen2Config = Qwen2Config.from_json_file(osp.join(model_args.model_path, "llm_config.json"))
llm_config.layer_module = model_args.layer_module
llm_config.qk_norm = model_args.llm_qk_norm
llm_config.qk_norm_und = model_args.llm_qk_norm_und
llm_config.qk_norm_gen = model_args.llm_qk_norm_gen
llm_config.tie_word_embeddings = model_args.tie_word_embeddings
llm_config.freeze_und = inference_args.freeze_und
llm_config.apply_qwen_2_5_vl_pos_emb = inference_args.apply_qwen_2_5_vl_pos_emb
language_model: Qwen2ForCausalLM = Qwen2ForCausalLM(llm_config)
if inference_args.visual_und:
if model_args.vit_type in ("qwen2_5_vl", "qwen_2_5_vl_original"):
vit_config = Qwen2_5_VLVisionConfig.from_pretrained(model_args.vit_path)
vit_model = Qwen2_5_VisionTransformerPretrainedModel(vit_config)
vit_weights = load_file(osp.join(model_args.vit_path, "vit.safetensors"))
vit_model.load_state_dict(vit_weights, strict=True)
else:
raise ValueError(f"Unsupported vit_type: {model_args.vit_type}")
clean_memory(vit_weights)
if inference_args.visual_gen:
vae_model = WanVideoVAE()
vae_config: AutoEncoderParams = deepcopy(vae_model.vae_config)
else:
vae_model = None
vae_config = None
# Lance config.
config = LanceConfig(
visual_gen=inference_args.visual_gen,
visual_und=inference_args.visual_und,
llm_config=llm_config,
vit_config=vit_config if inference_args.visual_und else None,
vae_config=vae_config if inference_args.visual_gen else None,
latent_patch_size=model_args.latent_patch_size,
max_num_frames=model_args.max_num_frames,
max_latent_size=model_args.max_latent_size,
vit_max_num_patch_per_side=model_args.vit_max_num_patch_per_side,
connector_act=model_args.connector_act,
interpolate_pos=model_args.interpolate_pos,
timestep_shift=inference_args.timestep_shift,
)
model: Lance = Lance(
language_model=language_model,
vit_model=vit_model if inference_args.visual_und else None,
vit_type=model_args.vit_type,
config=config,
training_args=inference_args,
)
model = model.to(DEVICE)
# Setup tokenizer for model:
tokenizer: Qwen2Tokenizer = Qwen2Tokenizer.from_pretrained(model_args.model_path)
tokenizer, new_token_ids, num_new_tokens = add_special_tokens(tokenizer)
# Initialize MoE before loading the checkpoint.
if inference_args.copy_init_moe:
language_model.init_moe()
init_from_model_path_if_needed(model, model_args)
# Resize after loading the checkpoint.
if num_new_tokens > 0:
model.language_model.resize_token_embeddings(len(tokenizer))
model.config.llm_config.vocab_size = len(tokenizer)
model.language_model.config.vocab_size = len(tokenizer)
if model_args.vit_type.lower() == "qwen2_5_vl":
from common.model.hacks import hack_qwen2_5_vl_config
language_model = hack_qwen2_5_vl_config(language_model)
image_token_id = language_model.config.video_token_id
new_token_ids.update({"image_token_id": image_token_id})
model.update_tokenizer(tokenizer=tokenizer)
if model_args.tie_word_embeddings:
model.language_model.untie_lm_head()
model.language_model.copy_new_token_rows_to_lm_head(num_new_tokens)
model_args.tie_word_embeddings = False
llm_config.tie_word_embeddings = False
else:
assert model.language_model.get_input_embeddings().weight.data.data_ptr() != model.language_model.get_output_embeddings().weight.data.data_ptr(), 'tie_world_embeddings 冲突'
model = model.to(device=DEVICE, dtype=torch.bfloat16)
model.eval()
# Some VAE wrappers (e.g. `WanVideoVAE`) are plain helper objects rather
# than `nn.Module`s, and their internal model is already switched to eval.
if vae_model is not None and hasattr(vae_model, "eval"):
vae_model.eval()
dataset_config = build_runtime_dataset_config(
model_args=model_args,
inference_args=inference_args,
vae_config=vae_config,
)
# Create dataset.
val_dataset = ValidationDataset(
jsonl_path= data_args.val_dataset_config_file,
tokenizer=tokenizer,
data_args=data_args,
model_args=model_args,
training_args=inference_args,
new_token_ids=new_token_ids,
dataset_config=dataset_config,
local_rank=GLOBAL_RANK,
world_size=WORLD_SIZE,
)
val_loader = DataLoader(
val_dataset,
batch_size=1,
num_workers=0,
pin_memory=True,
collate_fn=simple_custom_collate,
drop_last=True,
prefetch_factor=None,
persistent_workers=False,
multiprocessing_context=None,
)
val_loader_iter = iter(val_loader)
if not os.path.exists(inference_args.save_path_gen):
os.makedirs(inference_args.save_path_gen, exist_ok=True)
# Main loop.
for _ in trange(len(val_loader), desc="Validating", unit="batch", leave=True, ncols=80, disable=(GLOBAL_RANK != 0)):
val_data_cpu = next(val_loader_iter)
validate_on_fixed_batch(
fsdp_model=model,
vae_model=vae_model,
val_data_cpu=val_data_cpu,
model_args=model_args,
inference_args=inference_args,
new_token_ids=new_token_ids,
image_token_id=image_token_id,
device=DEVICE,
save_source_video=False,
save_path_gen=inference_args.save_path_gen,
sample_num_per_prompt=inference_args.sample_num_per_prompt,
)
if dist.is_initialized():
dist.destroy_process_group()
if __name__ == "__main__":
main()