Spaces:
Running on Zero
Running on Zero
Initial Lip Forcing 14B streaming demo
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +4 -0
- OmniAvatar/configs/__init__.py +0 -0
- OmniAvatar/configs/model_config.py +664 -0
- OmniAvatar/models/audio_pack.py +40 -0
- OmniAvatar/models/model_manager.py +444 -0
- OmniAvatar/models/wan_video_dit.py +611 -0
- OmniAvatar/models/wan_video_text_encoder.py +269 -0
- OmniAvatar/models/wan_video_vae.py +938 -0
- OmniAvatar/models/wav2vec.py +209 -0
- OmniAvatar/prompters/__init__.py +1 -0
- OmniAvatar/prompters/base_prompter.py +70 -0
- OmniAvatar/prompters/wan_prompter.py +109 -0
- OmniAvatar/utils/args_config.py +122 -0
- OmniAvatar/utils/io_utils.py +245 -0
- OmniAvatar/utils/latentsync/__init__.py +19 -0
- OmniAvatar/utils/latentsync/affine_transform.py +151 -0
- OmniAvatar/utils/latentsync/face_detector.py +93 -0
- OmniAvatar/utils/latentsync/image_processor.py +109 -0
- README.md +25 -5
- app.py +373 -0
- examples/example1_audio.wav +3 -0
- examples/example1_video.mp4 +3 -0
- examples/example2_audio.wav +3 -0
- examples/example2_video.mp4 +3 -0
- lipforcing/__init__.py +2 -0
- lipforcing/callbacks/__init__.py +2 -0
- lipforcing/callbacks/callback.py +183 -0
- lipforcing/callbacks/ct_schedule.py +83 -0
- lipforcing/callbacks/ema.py +169 -0
- lipforcing/callbacks/forced_weight_norm.py +28 -0
- lipforcing/callbacks/gpu_mem_profiler.py +134 -0
- lipforcing/callbacks/gpu_stats.py +92 -0
- lipforcing/callbacks/grad_clip.py +219 -0
- lipforcing/callbacks/param_count.py +116 -0
- lipforcing/callbacks/stdout_logger.py +33 -0
- lipforcing/callbacks/train_profiler.py +138 -0
- lipforcing/callbacks/wandb.py +773 -0
- lipforcing/configs/__init__.py +2 -0
- lipforcing/configs/callbacks.py +65 -0
- lipforcing/configs/config.py +282 -0
- lipforcing/configs/config_utils.py +266 -0
- lipforcing/configs/discriminator.py +23 -0
- lipforcing/configs/experiments/OmniAvatar/__init__.py +0 -0
- lipforcing/configs/experiments/OmniAvatar/config_df.py +247 -0
- lipforcing/configs/experiments/OmniAvatar/config_sf.py +395 -0
- lipforcing/configs/methods/__init__.py +2 -0
- lipforcing/configs/methods/config_dmd2.py +79 -0
- lipforcing/configs/methods/config_omniavatar_df.py +53 -0
- lipforcing/configs/methods/config_omniavatar_sf.py +102 -0
- lipforcing/configs/methods/config_self_forcing.py +59 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
examples/example1_audio.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
examples/example1_video.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
examples/example2_audio.wav filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
examples/example2_video.mp4 filter=lfs diff=lfs merge=lfs -text
|
OmniAvatar/configs/__init__.py
ADDED
|
File without changes
|
OmniAvatar/configs/model_config.py
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing_extensions import Literal, TypeAlias
|
| 2 |
+
from ..models.wan_video_dit import WanModel
|
| 3 |
+
from ..models.wan_video_text_encoder import WanTextEncoder
|
| 4 |
+
from ..models.wan_video_vae import WanVideoVAE
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
model_loader_configs = [
|
| 8 |
+
# These configs are provided for detecting model type automatically.
|
| 9 |
+
# The format is (state_dict_keys_hash, state_dict_keys_hash_with_shape, model_names, model_classes, model_resource)
|
| 10 |
+
(None, "9269f8db9040a9d860eaca435be61814", ["wan_video_dit"], [WanModel], "civitai"),
|
| 11 |
+
(None, "aafcfd9672c3a2456dc46e1cb6e52c70", ["wan_video_dit"], [WanModel], "civitai"),
|
| 12 |
+
(None, "6bfcfb3b342cb286ce886889d519a77e", ["wan_video_dit"], [WanModel], "civitai"),
|
| 13 |
+
(None, "cb104773c6c2cb6df4f9529ad5c60d0b", ["wan_video_dit"], [WanModel], "diffusers"),
|
| 14 |
+
(None, "9c8818c2cbea55eca56c7b447df170da", ["wan_video_text_encoder"], [WanTextEncoder], "civitai"),
|
| 15 |
+
(None, "1378ea763357eea97acdef78e65d6d96", ["wan_video_vae"], [WanVideoVAE], "civitai"),
|
| 16 |
+
(None, "ccc42284ea13e1ad04693284c7a09be6", ["wan_video_vae"], [WanVideoVAE], "civitai"),
|
| 17 |
+
]
|
| 18 |
+
huggingface_model_loader_configs = [
|
| 19 |
+
# These configs are provided for detecting model type automatically.
|
| 20 |
+
# The format is (architecture_in_huggingface_config, huggingface_lib, model_name, redirected_architecture)
|
| 21 |
+
("ChatGLMModel", "diffsynth.models.kolors_text_encoder", "kolors_text_encoder", None),
|
| 22 |
+
("MarianMTModel", "transformers.models.marian.modeling_marian", "translator", None),
|
| 23 |
+
("BloomForCausalLM", "transformers.models.bloom.modeling_bloom", "beautiful_prompt", None),
|
| 24 |
+
("Qwen2ForCausalLM", "transformers.models.qwen2.modeling_qwen2", "qwen_prompt", None),
|
| 25 |
+
# ("LlamaForCausalLM", "transformers.models.llama.modeling_llama", "omost_prompt", None),
|
| 26 |
+
("T5EncoderModel", "diffsynth.models.flux_text_encoder", "flux_text_encoder_2", "FluxTextEncoder2"),
|
| 27 |
+
("CogVideoXTransformer3DModel", "diffsynth.models.cog_dit", "cog_dit", "CogDiT"),
|
| 28 |
+
("SiglipModel", "transformers.models.siglip.modeling_siglip", "siglip_vision_model", "SiglipVisionModel"),
|
| 29 |
+
("LlamaForCausalLM", "diffsynth.models.hunyuan_video_text_encoder", "hunyuan_video_text_encoder_2", "HunyuanVideoLLMEncoder"),
|
| 30 |
+
("LlavaForConditionalGeneration", "diffsynth.models.hunyuan_video_text_encoder", "hunyuan_video_text_encoder_2", "HunyuanVideoMLLMEncoder"),
|
| 31 |
+
("Step1Model", "diffsynth.models.stepvideo_text_encoder", "stepvideo_text_encoder_2", "STEP1TextEncoder"),
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
preset_models_on_huggingface = {
|
| 35 |
+
"HunyuanDiT": [
|
| 36 |
+
("Tencent-Hunyuan/HunyuanDiT", "t2i/clip_text_encoder/pytorch_model.bin", "models/HunyuanDiT/t2i/clip_text_encoder"),
|
| 37 |
+
("Tencent-Hunyuan/HunyuanDiT", "t2i/mt5/pytorch_model.bin", "models/HunyuanDiT/t2i/mt5"),
|
| 38 |
+
("Tencent-Hunyuan/HunyuanDiT", "t2i/model/pytorch_model_ema.pt", "models/HunyuanDiT/t2i/model"),
|
| 39 |
+
("Tencent-Hunyuan/HunyuanDiT", "t2i/sdxl-vae-fp16-fix/diffusion_pytorch_model.bin", "models/HunyuanDiT/t2i/sdxl-vae-fp16-fix"),
|
| 40 |
+
],
|
| 41 |
+
"stable-video-diffusion-img2vid-xt": [
|
| 42 |
+
("stabilityai/stable-video-diffusion-img2vid-xt", "svd_xt.safetensors", "models/stable_video_diffusion"),
|
| 43 |
+
],
|
| 44 |
+
"ExVideo-SVD-128f-v1": [
|
| 45 |
+
("ECNU-CILab/ExVideo-SVD-128f-v1", "model.fp16.safetensors", "models/stable_video_diffusion"),
|
| 46 |
+
],
|
| 47 |
+
# Stable Diffusion
|
| 48 |
+
"StableDiffusion_v15": [
|
| 49 |
+
("benjamin-paine/stable-diffusion-v1-5", "v1-5-pruned-emaonly.safetensors", "models/stable_diffusion"),
|
| 50 |
+
],
|
| 51 |
+
"DreamShaper_8": [
|
| 52 |
+
("Yntec/Dreamshaper8", "dreamshaper_8.safetensors", "models/stable_diffusion"),
|
| 53 |
+
],
|
| 54 |
+
# Textual Inversion
|
| 55 |
+
"TextualInversion_VeryBadImageNegative_v1.3": [
|
| 56 |
+
("gemasai/verybadimagenegative_v1.3", "verybadimagenegative_v1.3.pt", "models/textual_inversion"),
|
| 57 |
+
],
|
| 58 |
+
# Stable Diffusion XL
|
| 59 |
+
"StableDiffusionXL_v1": [
|
| 60 |
+
("stabilityai/stable-diffusion-xl-base-1.0", "sd_xl_base_1.0.safetensors", "models/stable_diffusion_xl"),
|
| 61 |
+
],
|
| 62 |
+
"BluePencilXL_v200": [
|
| 63 |
+
("frankjoshua/bluePencilXL_v200", "bluePencilXL_v200.safetensors", "models/stable_diffusion_xl"),
|
| 64 |
+
],
|
| 65 |
+
"StableDiffusionXL_Turbo": [
|
| 66 |
+
("stabilityai/sdxl-turbo", "sd_xl_turbo_1.0_fp16.safetensors", "models/stable_diffusion_xl_turbo"),
|
| 67 |
+
],
|
| 68 |
+
# Stable Diffusion 3
|
| 69 |
+
"StableDiffusion3": [
|
| 70 |
+
("stabilityai/stable-diffusion-3-medium", "sd3_medium_incl_clips_t5xxlfp16.safetensors", "models/stable_diffusion_3"),
|
| 71 |
+
],
|
| 72 |
+
"StableDiffusion3_without_T5": [
|
| 73 |
+
("stabilityai/stable-diffusion-3-medium", "sd3_medium_incl_clips.safetensors", "models/stable_diffusion_3"),
|
| 74 |
+
],
|
| 75 |
+
# ControlNet
|
| 76 |
+
"ControlNet_v11f1p_sd15_depth": [
|
| 77 |
+
("lllyasviel/ControlNet-v1-1", "control_v11f1p_sd15_depth.pth", "models/ControlNet"),
|
| 78 |
+
("lllyasviel/Annotators", "dpt_hybrid-midas-501f0c75.pt", "models/Annotators")
|
| 79 |
+
],
|
| 80 |
+
"ControlNet_v11p_sd15_softedge": [
|
| 81 |
+
("lllyasviel/ControlNet-v1-1", "control_v11p_sd15_softedge.pth", "models/ControlNet"),
|
| 82 |
+
("lllyasviel/Annotators", "ControlNetHED.pth", "models/Annotators")
|
| 83 |
+
],
|
| 84 |
+
"ControlNet_v11f1e_sd15_tile": [
|
| 85 |
+
("lllyasviel/ControlNet-v1-1", "control_v11f1e_sd15_tile.pth", "models/ControlNet")
|
| 86 |
+
],
|
| 87 |
+
"ControlNet_v11p_sd15_lineart": [
|
| 88 |
+
("lllyasviel/ControlNet-v1-1", "control_v11p_sd15_lineart.pth", "models/ControlNet"),
|
| 89 |
+
("lllyasviel/Annotators", "sk_model.pth", "models/Annotators"),
|
| 90 |
+
("lllyasviel/Annotators", "sk_model2.pth", "models/Annotators")
|
| 91 |
+
],
|
| 92 |
+
"ControlNet_union_sdxl_promax": [
|
| 93 |
+
("xinsir/controlnet-union-sdxl-1.0", "diffusion_pytorch_model_promax.safetensors", "models/ControlNet/controlnet_union"),
|
| 94 |
+
("lllyasviel/Annotators", "dpt_hybrid-midas-501f0c75.pt", "models/Annotators")
|
| 95 |
+
],
|
| 96 |
+
# AnimateDiff
|
| 97 |
+
"AnimateDiff_v2": [
|
| 98 |
+
("guoyww/animatediff", "mm_sd_v15_v2.ckpt", "models/AnimateDiff"),
|
| 99 |
+
],
|
| 100 |
+
"AnimateDiff_xl_beta": [
|
| 101 |
+
("guoyww/animatediff", "mm_sdxl_v10_beta.ckpt", "models/AnimateDiff"),
|
| 102 |
+
],
|
| 103 |
+
|
| 104 |
+
# Qwen Prompt
|
| 105 |
+
"QwenPrompt": [
|
| 106 |
+
("Qwen/Qwen2-1.5B-Instruct", "config.json", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 107 |
+
("Qwen/Qwen2-1.5B-Instruct", "generation_config.json", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 108 |
+
("Qwen/Qwen2-1.5B-Instruct", "model.safetensors", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 109 |
+
("Qwen/Qwen2-1.5B-Instruct", "special_tokens_map.json", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 110 |
+
("Qwen/Qwen2-1.5B-Instruct", "tokenizer.json", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 111 |
+
("Qwen/Qwen2-1.5B-Instruct", "tokenizer_config.json", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 112 |
+
("Qwen/Qwen2-1.5B-Instruct", "merges.txt", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 113 |
+
("Qwen/Qwen2-1.5B-Instruct", "vocab.json", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 114 |
+
],
|
| 115 |
+
# Beautiful Prompt
|
| 116 |
+
"BeautifulPrompt": [
|
| 117 |
+
("alibaba-pai/pai-bloom-1b1-text2prompt-sd", "config.json", "models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd"),
|
| 118 |
+
("alibaba-pai/pai-bloom-1b1-text2prompt-sd", "generation_config.json", "models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd"),
|
| 119 |
+
("alibaba-pai/pai-bloom-1b1-text2prompt-sd", "model.safetensors", "models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd"),
|
| 120 |
+
("alibaba-pai/pai-bloom-1b1-text2prompt-sd", "special_tokens_map.json", "models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd"),
|
| 121 |
+
("alibaba-pai/pai-bloom-1b1-text2prompt-sd", "tokenizer.json", "models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd"),
|
| 122 |
+
("alibaba-pai/pai-bloom-1b1-text2prompt-sd", "tokenizer_config.json", "models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd"),
|
| 123 |
+
],
|
| 124 |
+
# Omost prompt
|
| 125 |
+
"OmostPrompt":[
|
| 126 |
+
("lllyasviel/omost-llama-3-8b-4bits", "model-00001-of-00002.safetensors", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 127 |
+
("lllyasviel/omost-llama-3-8b-4bits", "model-00002-of-00002.safetensors", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 128 |
+
("lllyasviel/omost-llama-3-8b-4bits", "tokenizer.json", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 129 |
+
("lllyasviel/omost-llama-3-8b-4bits", "tokenizer_config.json", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 130 |
+
("lllyasviel/omost-llama-3-8b-4bits", "config.json", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 131 |
+
("lllyasviel/omost-llama-3-8b-4bits", "generation_config.json", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 132 |
+
("lllyasviel/omost-llama-3-8b-4bits", "model.safetensors.index.json", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 133 |
+
("lllyasviel/omost-llama-3-8b-4bits", "special_tokens_map.json", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 134 |
+
],
|
| 135 |
+
# Translator
|
| 136 |
+
"opus-mt-zh-en": [
|
| 137 |
+
("Helsinki-NLP/opus-mt-zh-en", "config.json", "models/translator/opus-mt-zh-en"),
|
| 138 |
+
("Helsinki-NLP/opus-mt-zh-en", "generation_config.json", "models/translator/opus-mt-zh-en"),
|
| 139 |
+
("Helsinki-NLP/opus-mt-zh-en", "metadata.json", "models/translator/opus-mt-zh-en"),
|
| 140 |
+
("Helsinki-NLP/opus-mt-zh-en", "pytorch_model.bin", "models/translator/opus-mt-zh-en"),
|
| 141 |
+
("Helsinki-NLP/opus-mt-zh-en", "source.spm", "models/translator/opus-mt-zh-en"),
|
| 142 |
+
("Helsinki-NLP/opus-mt-zh-en", "target.spm", "models/translator/opus-mt-zh-en"),
|
| 143 |
+
("Helsinki-NLP/opus-mt-zh-en", "tokenizer_config.json", "models/translator/opus-mt-zh-en"),
|
| 144 |
+
("Helsinki-NLP/opus-mt-zh-en", "vocab.json", "models/translator/opus-mt-zh-en"),
|
| 145 |
+
],
|
| 146 |
+
# IP-Adapter
|
| 147 |
+
"IP-Adapter-SD": [
|
| 148 |
+
("h94/IP-Adapter", "models/image_encoder/model.safetensors", "models/IpAdapter/stable_diffusion/image_encoder"),
|
| 149 |
+
("h94/IP-Adapter", "models/ip-adapter_sd15.bin", "models/IpAdapter/stable_diffusion"),
|
| 150 |
+
],
|
| 151 |
+
"IP-Adapter-SDXL": [
|
| 152 |
+
("h94/IP-Adapter", "sdxl_models/image_encoder/model.safetensors", "models/IpAdapter/stable_diffusion_xl/image_encoder"),
|
| 153 |
+
("h94/IP-Adapter", "sdxl_models/ip-adapter_sdxl.bin", "models/IpAdapter/stable_diffusion_xl"),
|
| 154 |
+
],
|
| 155 |
+
"SDXL-vae-fp16-fix": [
|
| 156 |
+
("madebyollin/sdxl-vae-fp16-fix", "diffusion_pytorch_model.safetensors", "models/sdxl-vae-fp16-fix")
|
| 157 |
+
],
|
| 158 |
+
# Kolors
|
| 159 |
+
"Kolors": [
|
| 160 |
+
("Kwai-Kolors/Kolors", "text_encoder/config.json", "models/kolors/Kolors/text_encoder"),
|
| 161 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model.bin.index.json", "models/kolors/Kolors/text_encoder"),
|
| 162 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00001-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 163 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00002-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 164 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00003-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 165 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00004-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 166 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00005-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 167 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00006-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 168 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00007-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 169 |
+
("Kwai-Kolors/Kolors", "unet/diffusion_pytorch_model.safetensors", "models/kolors/Kolors/unet"),
|
| 170 |
+
("Kwai-Kolors/Kolors", "vae/diffusion_pytorch_model.safetensors", "models/kolors/Kolors/vae"),
|
| 171 |
+
],
|
| 172 |
+
# FLUX
|
| 173 |
+
"FLUX.1-dev": [
|
| 174 |
+
("black-forest-labs/FLUX.1-dev", "text_encoder/model.safetensors", "models/FLUX/FLUX.1-dev/text_encoder"),
|
| 175 |
+
("black-forest-labs/FLUX.1-dev", "text_encoder_2/config.json", "models/FLUX/FLUX.1-dev/text_encoder_2"),
|
| 176 |
+
("black-forest-labs/FLUX.1-dev", "text_encoder_2/model-00001-of-00002.safetensors", "models/FLUX/FLUX.1-dev/text_encoder_2"),
|
| 177 |
+
("black-forest-labs/FLUX.1-dev", "text_encoder_2/model-00002-of-00002.safetensors", "models/FLUX/FLUX.1-dev/text_encoder_2"),
|
| 178 |
+
("black-forest-labs/FLUX.1-dev", "text_encoder_2/model.safetensors.index.json", "models/FLUX/FLUX.1-dev/text_encoder_2"),
|
| 179 |
+
("black-forest-labs/FLUX.1-dev", "ae.safetensors", "models/FLUX/FLUX.1-dev"),
|
| 180 |
+
("black-forest-labs/FLUX.1-dev", "flux1-dev.safetensors", "models/FLUX/FLUX.1-dev"),
|
| 181 |
+
],
|
| 182 |
+
"InstantX/FLUX.1-dev-IP-Adapter": {
|
| 183 |
+
"file_list": [
|
| 184 |
+
("InstantX/FLUX.1-dev-IP-Adapter", "ip-adapter.bin", "models/IpAdapter/InstantX/FLUX.1-dev-IP-Adapter"),
|
| 185 |
+
("google/siglip-so400m-patch14-384", "model.safetensors", "models/IpAdapter/InstantX/FLUX.1-dev-IP-Adapter/image_encoder"),
|
| 186 |
+
("google/siglip-so400m-patch14-384", "config.json", "models/IpAdapter/InstantX/FLUX.1-dev-IP-Adapter/image_encoder"),
|
| 187 |
+
],
|
| 188 |
+
"load_path": [
|
| 189 |
+
"models/IpAdapter/InstantX/FLUX.1-dev-IP-Adapter/ip-adapter.bin",
|
| 190 |
+
"models/IpAdapter/InstantX/FLUX.1-dev-IP-Adapter/image_encoder",
|
| 191 |
+
],
|
| 192 |
+
},
|
| 193 |
+
# RIFE
|
| 194 |
+
"RIFE": [
|
| 195 |
+
("AlexWortega/RIFE", "flownet.pkl", "models/RIFE"),
|
| 196 |
+
],
|
| 197 |
+
# CogVideo
|
| 198 |
+
"CogVideoX-5B": [
|
| 199 |
+
("THUDM/CogVideoX-5b", "text_encoder/config.json", "models/CogVideo/CogVideoX-5b/text_encoder"),
|
| 200 |
+
("THUDM/CogVideoX-5b", "text_encoder/model.safetensors.index.json", "models/CogVideo/CogVideoX-5b/text_encoder"),
|
| 201 |
+
("THUDM/CogVideoX-5b", "text_encoder/model-00001-of-00002.safetensors", "models/CogVideo/CogVideoX-5b/text_encoder"),
|
| 202 |
+
("THUDM/CogVideoX-5b", "text_encoder/model-00002-of-00002.safetensors", "models/CogVideo/CogVideoX-5b/text_encoder"),
|
| 203 |
+
("THUDM/CogVideoX-5b", "transformer/config.json", "models/CogVideo/CogVideoX-5b/transformer"),
|
| 204 |
+
("THUDM/CogVideoX-5b", "transformer/diffusion_pytorch_model.safetensors.index.json", "models/CogVideo/CogVideoX-5b/transformer"),
|
| 205 |
+
("THUDM/CogVideoX-5b", "transformer/diffusion_pytorch_model-00001-of-00002.safetensors", "models/CogVideo/CogVideoX-5b/transformer"),
|
| 206 |
+
("THUDM/CogVideoX-5b", "transformer/diffusion_pytorch_model-00002-of-00002.safetensors", "models/CogVideo/CogVideoX-5b/transformer"),
|
| 207 |
+
("THUDM/CogVideoX-5b", "vae/diffusion_pytorch_model.safetensors", "models/CogVideo/CogVideoX-5b/vae"),
|
| 208 |
+
],
|
| 209 |
+
# Stable Diffusion 3.5
|
| 210 |
+
"StableDiffusion3.5-large": [
|
| 211 |
+
("stabilityai/stable-diffusion-3.5-large", "sd3.5_large.safetensors", "models/stable_diffusion_3"),
|
| 212 |
+
("stabilityai/stable-diffusion-3.5-large", "text_encoders/clip_l.safetensors", "models/stable_diffusion_3/text_encoders"),
|
| 213 |
+
("stabilityai/stable-diffusion-3.5-large", "text_encoders/clip_g.safetensors", "models/stable_diffusion_3/text_encoders"),
|
| 214 |
+
("stabilityai/stable-diffusion-3.5-large", "text_encoders/t5xxl_fp16.safetensors", "models/stable_diffusion_3/text_encoders"),
|
| 215 |
+
],
|
| 216 |
+
}
|
| 217 |
+
preset_models_on_modelscope = {
|
| 218 |
+
# Hunyuan DiT
|
| 219 |
+
"HunyuanDiT": [
|
| 220 |
+
("modelscope/HunyuanDiT", "t2i/clip_text_encoder/pytorch_model.bin", "models/HunyuanDiT/t2i/clip_text_encoder"),
|
| 221 |
+
("modelscope/HunyuanDiT", "t2i/mt5/pytorch_model.bin", "models/HunyuanDiT/t2i/mt5"),
|
| 222 |
+
("modelscope/HunyuanDiT", "t2i/model/pytorch_model_ema.pt", "models/HunyuanDiT/t2i/model"),
|
| 223 |
+
("modelscope/HunyuanDiT", "t2i/sdxl-vae-fp16-fix/diffusion_pytorch_model.bin", "models/HunyuanDiT/t2i/sdxl-vae-fp16-fix"),
|
| 224 |
+
],
|
| 225 |
+
# Stable Video Diffusion
|
| 226 |
+
"stable-video-diffusion-img2vid-xt": [
|
| 227 |
+
("AI-ModelScope/stable-video-diffusion-img2vid-xt", "svd_xt.safetensors", "models/stable_video_diffusion"),
|
| 228 |
+
],
|
| 229 |
+
# ExVideo
|
| 230 |
+
"ExVideo-SVD-128f-v1": [
|
| 231 |
+
("ECNU-CILab/ExVideo-SVD-128f-v1", "model.fp16.safetensors", "models/stable_video_diffusion"),
|
| 232 |
+
],
|
| 233 |
+
"ExVideo-CogVideoX-LoRA-129f-v1": [
|
| 234 |
+
("ECNU-CILab/ExVideo-CogVideoX-LoRA-129f-v1", "ExVideo-CogVideoX-LoRA-129f-v1.safetensors", "models/lora"),
|
| 235 |
+
],
|
| 236 |
+
# Stable Diffusion
|
| 237 |
+
"StableDiffusion_v15": [
|
| 238 |
+
("AI-ModelScope/stable-diffusion-v1-5", "v1-5-pruned-emaonly.safetensors", "models/stable_diffusion"),
|
| 239 |
+
],
|
| 240 |
+
"DreamShaper_8": [
|
| 241 |
+
("sd_lora/dreamshaper_8", "dreamshaper_8.safetensors", "models/stable_diffusion"),
|
| 242 |
+
],
|
| 243 |
+
"AingDiffusion_v12": [
|
| 244 |
+
("sd_lora/aingdiffusion_v12", "aingdiffusion_v12.safetensors", "models/stable_diffusion"),
|
| 245 |
+
],
|
| 246 |
+
"Flat2DAnimerge_v45Sharp": [
|
| 247 |
+
("sd_lora/Flat-2D-Animerge", "flat2DAnimerge_v45Sharp.safetensors", "models/stable_diffusion"),
|
| 248 |
+
],
|
| 249 |
+
# Textual Inversion
|
| 250 |
+
"TextualInversion_VeryBadImageNegative_v1.3": [
|
| 251 |
+
("sd_lora/verybadimagenegative_v1.3", "verybadimagenegative_v1.3.pt", "models/textual_inversion"),
|
| 252 |
+
],
|
| 253 |
+
# Stable Diffusion XL
|
| 254 |
+
"StableDiffusionXL_v1": [
|
| 255 |
+
("AI-ModelScope/stable-diffusion-xl-base-1.0", "sd_xl_base_1.0.safetensors", "models/stable_diffusion_xl"),
|
| 256 |
+
],
|
| 257 |
+
"BluePencilXL_v200": [
|
| 258 |
+
("sd_lora/bluePencilXL_v200", "bluePencilXL_v200.safetensors", "models/stable_diffusion_xl"),
|
| 259 |
+
],
|
| 260 |
+
"StableDiffusionXL_Turbo": [
|
| 261 |
+
("AI-ModelScope/sdxl-turbo", "sd_xl_turbo_1.0_fp16.safetensors", "models/stable_diffusion_xl_turbo"),
|
| 262 |
+
],
|
| 263 |
+
"SDXL_lora_zyd232_ChineseInkStyle_SDXL_v1_0": [
|
| 264 |
+
("sd_lora/zyd232_ChineseInkStyle_SDXL_v1_0", "zyd232_ChineseInkStyle_SDXL_v1_0.safetensors", "models/lora"),
|
| 265 |
+
],
|
| 266 |
+
# Stable Diffusion 3
|
| 267 |
+
"StableDiffusion3": [
|
| 268 |
+
("AI-ModelScope/stable-diffusion-3-medium", "sd3_medium_incl_clips_t5xxlfp16.safetensors", "models/stable_diffusion_3"),
|
| 269 |
+
],
|
| 270 |
+
"StableDiffusion3_without_T5": [
|
| 271 |
+
("AI-ModelScope/stable-diffusion-3-medium", "sd3_medium_incl_clips.safetensors", "models/stable_diffusion_3"),
|
| 272 |
+
],
|
| 273 |
+
# ControlNet
|
| 274 |
+
"ControlNet_v11f1p_sd15_depth": [
|
| 275 |
+
("AI-ModelScope/ControlNet-v1-1", "control_v11f1p_sd15_depth.pth", "models/ControlNet"),
|
| 276 |
+
("sd_lora/Annotators", "dpt_hybrid-midas-501f0c75.pt", "models/Annotators")
|
| 277 |
+
],
|
| 278 |
+
"ControlNet_v11p_sd15_softedge": [
|
| 279 |
+
("AI-ModelScope/ControlNet-v1-1", "control_v11p_sd15_softedge.pth", "models/ControlNet"),
|
| 280 |
+
("sd_lora/Annotators", "ControlNetHED.pth", "models/Annotators")
|
| 281 |
+
],
|
| 282 |
+
"ControlNet_v11f1e_sd15_tile": [
|
| 283 |
+
("AI-ModelScope/ControlNet-v1-1", "control_v11f1e_sd15_tile.pth", "models/ControlNet")
|
| 284 |
+
],
|
| 285 |
+
"ControlNet_v11p_sd15_lineart": [
|
| 286 |
+
("AI-ModelScope/ControlNet-v1-1", "control_v11p_sd15_lineart.pth", "models/ControlNet"),
|
| 287 |
+
("sd_lora/Annotators", "sk_model.pth", "models/Annotators"),
|
| 288 |
+
("sd_lora/Annotators", "sk_model2.pth", "models/Annotators")
|
| 289 |
+
],
|
| 290 |
+
"ControlNet_union_sdxl_promax": [
|
| 291 |
+
("AI-ModelScope/controlnet-union-sdxl-1.0", "diffusion_pytorch_model_promax.safetensors", "models/ControlNet/controlnet_union"),
|
| 292 |
+
("sd_lora/Annotators", "dpt_hybrid-midas-501f0c75.pt", "models/Annotators")
|
| 293 |
+
],
|
| 294 |
+
"Annotators:Depth": [
|
| 295 |
+
("sd_lora/Annotators", "dpt_hybrid-midas-501f0c75.pt", "models/Annotators"),
|
| 296 |
+
],
|
| 297 |
+
"Annotators:Softedge": [
|
| 298 |
+
("sd_lora/Annotators", "ControlNetHED.pth", "models/Annotators"),
|
| 299 |
+
],
|
| 300 |
+
"Annotators:Lineart": [
|
| 301 |
+
("sd_lora/Annotators", "sk_model.pth", "models/Annotators"),
|
| 302 |
+
("sd_lora/Annotators", "sk_model2.pth", "models/Annotators"),
|
| 303 |
+
],
|
| 304 |
+
"Annotators:Normal": [
|
| 305 |
+
("sd_lora/Annotators", "scannet.pt", "models/Annotators"),
|
| 306 |
+
],
|
| 307 |
+
"Annotators:Openpose": [
|
| 308 |
+
("sd_lora/Annotators", "body_pose_model.pth", "models/Annotators"),
|
| 309 |
+
("sd_lora/Annotators", "facenet.pth", "models/Annotators"),
|
| 310 |
+
("sd_lora/Annotators", "hand_pose_model.pth", "models/Annotators"),
|
| 311 |
+
],
|
| 312 |
+
# AnimateDiff
|
| 313 |
+
"AnimateDiff_v2": [
|
| 314 |
+
("Shanghai_AI_Laboratory/animatediff", "mm_sd_v15_v2.ckpt", "models/AnimateDiff"),
|
| 315 |
+
],
|
| 316 |
+
"AnimateDiff_xl_beta": [
|
| 317 |
+
("Shanghai_AI_Laboratory/animatediff", "mm_sdxl_v10_beta.ckpt", "models/AnimateDiff"),
|
| 318 |
+
],
|
| 319 |
+
# RIFE
|
| 320 |
+
"RIFE": [
|
| 321 |
+
("Damo_XR_Lab/cv_rife_video-frame-interpolation", "flownet.pkl", "models/RIFE"),
|
| 322 |
+
],
|
| 323 |
+
# Qwen Prompt
|
| 324 |
+
"QwenPrompt": {
|
| 325 |
+
"file_list": [
|
| 326 |
+
("qwen/Qwen2-1.5B-Instruct", "config.json", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 327 |
+
("qwen/Qwen2-1.5B-Instruct", "generation_config.json", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 328 |
+
("qwen/Qwen2-1.5B-Instruct", "model.safetensors", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 329 |
+
("qwen/Qwen2-1.5B-Instruct", "special_tokens_map.json", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 330 |
+
("qwen/Qwen2-1.5B-Instruct", "tokenizer.json", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 331 |
+
("qwen/Qwen2-1.5B-Instruct", "tokenizer_config.json", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 332 |
+
("qwen/Qwen2-1.5B-Instruct", "merges.txt", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 333 |
+
("qwen/Qwen2-1.5B-Instruct", "vocab.json", "models/QwenPrompt/qwen2-1.5b-instruct"),
|
| 334 |
+
],
|
| 335 |
+
"load_path": [
|
| 336 |
+
"models/QwenPrompt/qwen2-1.5b-instruct",
|
| 337 |
+
],
|
| 338 |
+
},
|
| 339 |
+
# Beautiful Prompt
|
| 340 |
+
"BeautifulPrompt": {
|
| 341 |
+
"file_list": [
|
| 342 |
+
("AI-ModelScope/pai-bloom-1b1-text2prompt-sd", "config.json", "models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd"),
|
| 343 |
+
("AI-ModelScope/pai-bloom-1b1-text2prompt-sd", "generation_config.json", "models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd"),
|
| 344 |
+
("AI-ModelScope/pai-bloom-1b1-text2prompt-sd", "model.safetensors", "models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd"),
|
| 345 |
+
("AI-ModelScope/pai-bloom-1b1-text2prompt-sd", "special_tokens_map.json", "models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd"),
|
| 346 |
+
("AI-ModelScope/pai-bloom-1b1-text2prompt-sd", "tokenizer.json", "models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd"),
|
| 347 |
+
("AI-ModelScope/pai-bloom-1b1-text2prompt-sd", "tokenizer_config.json", "models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd"),
|
| 348 |
+
],
|
| 349 |
+
"load_path": [
|
| 350 |
+
"models/BeautifulPrompt/pai-bloom-1b1-text2prompt-sd",
|
| 351 |
+
],
|
| 352 |
+
},
|
| 353 |
+
# Omost prompt
|
| 354 |
+
"OmostPrompt": {
|
| 355 |
+
"file_list": [
|
| 356 |
+
("Omost/omost-llama-3-8b-4bits", "model-00001-of-00002.safetensors", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 357 |
+
("Omost/omost-llama-3-8b-4bits", "model-00002-of-00002.safetensors", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 358 |
+
("Omost/omost-llama-3-8b-4bits", "tokenizer.json", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 359 |
+
("Omost/omost-llama-3-8b-4bits", "tokenizer_config.json", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 360 |
+
("Omost/omost-llama-3-8b-4bits", "config.json", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 361 |
+
("Omost/omost-llama-3-8b-4bits", "generation_config.json", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 362 |
+
("Omost/omost-llama-3-8b-4bits", "model.safetensors.index.json", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 363 |
+
("Omost/omost-llama-3-8b-4bits", "special_tokens_map.json", "models/OmostPrompt/omost-llama-3-8b-4bits"),
|
| 364 |
+
],
|
| 365 |
+
"load_path": [
|
| 366 |
+
"models/OmostPrompt/omost-llama-3-8b-4bits",
|
| 367 |
+
],
|
| 368 |
+
},
|
| 369 |
+
# Translator
|
| 370 |
+
"opus-mt-zh-en": {
|
| 371 |
+
"file_list": [
|
| 372 |
+
("moxying/opus-mt-zh-en", "config.json", "models/translator/opus-mt-zh-en"),
|
| 373 |
+
("moxying/opus-mt-zh-en", "generation_config.json", "models/translator/opus-mt-zh-en"),
|
| 374 |
+
("moxying/opus-mt-zh-en", "metadata.json", "models/translator/opus-mt-zh-en"),
|
| 375 |
+
("moxying/opus-mt-zh-en", "pytorch_model.bin", "models/translator/opus-mt-zh-en"),
|
| 376 |
+
("moxying/opus-mt-zh-en", "source.spm", "models/translator/opus-mt-zh-en"),
|
| 377 |
+
("moxying/opus-mt-zh-en", "target.spm", "models/translator/opus-mt-zh-en"),
|
| 378 |
+
("moxying/opus-mt-zh-en", "tokenizer_config.json", "models/translator/opus-mt-zh-en"),
|
| 379 |
+
("moxying/opus-mt-zh-en", "vocab.json", "models/translator/opus-mt-zh-en"),
|
| 380 |
+
],
|
| 381 |
+
"load_path": [
|
| 382 |
+
"models/translator/opus-mt-zh-en",
|
| 383 |
+
],
|
| 384 |
+
},
|
| 385 |
+
# IP-Adapter
|
| 386 |
+
"IP-Adapter-SD": [
|
| 387 |
+
("AI-ModelScope/IP-Adapter", "models/image_encoder/model.safetensors", "models/IpAdapter/stable_diffusion/image_encoder"),
|
| 388 |
+
("AI-ModelScope/IP-Adapter", "models/ip-adapter_sd15.bin", "models/IpAdapter/stable_diffusion"),
|
| 389 |
+
],
|
| 390 |
+
"IP-Adapter-SDXL": [
|
| 391 |
+
("AI-ModelScope/IP-Adapter", "sdxl_models/image_encoder/model.safetensors", "models/IpAdapter/stable_diffusion_xl/image_encoder"),
|
| 392 |
+
("AI-ModelScope/IP-Adapter", "sdxl_models/ip-adapter_sdxl.bin", "models/IpAdapter/stable_diffusion_xl"),
|
| 393 |
+
],
|
| 394 |
+
# Kolors
|
| 395 |
+
"Kolors": {
|
| 396 |
+
"file_list": [
|
| 397 |
+
("Kwai-Kolors/Kolors", "text_encoder/config.json", "models/kolors/Kolors/text_encoder"),
|
| 398 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model.bin.index.json", "models/kolors/Kolors/text_encoder"),
|
| 399 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00001-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 400 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00002-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 401 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00003-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 402 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00004-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 403 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00005-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 404 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00006-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 405 |
+
("Kwai-Kolors/Kolors", "text_encoder/pytorch_model-00007-of-00007.bin", "models/kolors/Kolors/text_encoder"),
|
| 406 |
+
("Kwai-Kolors/Kolors", "unet/diffusion_pytorch_model.safetensors", "models/kolors/Kolors/unet"),
|
| 407 |
+
("Kwai-Kolors/Kolors", "vae/diffusion_pytorch_model.safetensors", "models/kolors/Kolors/vae"),
|
| 408 |
+
],
|
| 409 |
+
"load_path": [
|
| 410 |
+
"models/kolors/Kolors/text_encoder",
|
| 411 |
+
"models/kolors/Kolors/unet/diffusion_pytorch_model.safetensors",
|
| 412 |
+
"models/kolors/Kolors/vae/diffusion_pytorch_model.safetensors",
|
| 413 |
+
],
|
| 414 |
+
},
|
| 415 |
+
"SDXL-vae-fp16-fix": [
|
| 416 |
+
("AI-ModelScope/sdxl-vae-fp16-fix", "diffusion_pytorch_model.safetensors", "models/sdxl-vae-fp16-fix")
|
| 417 |
+
],
|
| 418 |
+
# FLUX
|
| 419 |
+
"FLUX.1-dev": {
|
| 420 |
+
"file_list": [
|
| 421 |
+
("AI-ModelScope/FLUX.1-dev", "text_encoder/model.safetensors", "models/FLUX/FLUX.1-dev/text_encoder"),
|
| 422 |
+
("AI-ModelScope/FLUX.1-dev", "text_encoder_2/config.json", "models/FLUX/FLUX.1-dev/text_encoder_2"),
|
| 423 |
+
("AI-ModelScope/FLUX.1-dev", "text_encoder_2/model-00001-of-00002.safetensors", "models/FLUX/FLUX.1-dev/text_encoder_2"),
|
| 424 |
+
("AI-ModelScope/FLUX.1-dev", "text_encoder_2/model-00002-of-00002.safetensors", "models/FLUX/FLUX.1-dev/text_encoder_2"),
|
| 425 |
+
("AI-ModelScope/FLUX.1-dev", "text_encoder_2/model.safetensors.index.json", "models/FLUX/FLUX.1-dev/text_encoder_2"),
|
| 426 |
+
("AI-ModelScope/FLUX.1-dev", "ae.safetensors", "models/FLUX/FLUX.1-dev"),
|
| 427 |
+
("AI-ModelScope/FLUX.1-dev", "flux1-dev.safetensors", "models/FLUX/FLUX.1-dev"),
|
| 428 |
+
],
|
| 429 |
+
"load_path": [
|
| 430 |
+
"models/FLUX/FLUX.1-dev/text_encoder/model.safetensors",
|
| 431 |
+
"models/FLUX/FLUX.1-dev/text_encoder_2",
|
| 432 |
+
"models/FLUX/FLUX.1-dev/ae.safetensors",
|
| 433 |
+
"models/FLUX/FLUX.1-dev/flux1-dev.safetensors"
|
| 434 |
+
],
|
| 435 |
+
},
|
| 436 |
+
"FLUX.1-schnell": {
|
| 437 |
+
"file_list": [
|
| 438 |
+
("AI-ModelScope/FLUX.1-dev", "text_encoder/model.safetensors", "models/FLUX/FLUX.1-dev/text_encoder"),
|
| 439 |
+
("AI-ModelScope/FLUX.1-dev", "text_encoder_2/config.json", "models/FLUX/FLUX.1-dev/text_encoder_2"),
|
| 440 |
+
("AI-ModelScope/FLUX.1-dev", "text_encoder_2/model-00001-of-00002.safetensors", "models/FLUX/FLUX.1-dev/text_encoder_2"),
|
| 441 |
+
("AI-ModelScope/FLUX.1-dev", "text_encoder_2/model-00002-of-00002.safetensors", "models/FLUX/FLUX.1-dev/text_encoder_2"),
|
| 442 |
+
("AI-ModelScope/FLUX.1-dev", "text_encoder_2/model.safetensors.index.json", "models/FLUX/FLUX.1-dev/text_encoder_2"),
|
| 443 |
+
("AI-ModelScope/FLUX.1-dev", "ae.safetensors", "models/FLUX/FLUX.1-dev"),
|
| 444 |
+
("AI-ModelScope/FLUX.1-schnell", "flux1-schnell.safetensors", "models/FLUX/FLUX.1-schnell"),
|
| 445 |
+
],
|
| 446 |
+
"load_path": [
|
| 447 |
+
"models/FLUX/FLUX.1-dev/text_encoder/model.safetensors",
|
| 448 |
+
"models/FLUX/FLUX.1-dev/text_encoder_2",
|
| 449 |
+
"models/FLUX/FLUX.1-dev/ae.safetensors",
|
| 450 |
+
"models/FLUX/FLUX.1-schnell/flux1-schnell.safetensors"
|
| 451 |
+
],
|
| 452 |
+
},
|
| 453 |
+
"InstantX/FLUX.1-dev-Controlnet-Union-alpha": [
|
| 454 |
+
("InstantX/FLUX.1-dev-Controlnet-Union-alpha", "diffusion_pytorch_model.safetensors", "models/ControlNet/InstantX/FLUX.1-dev-Controlnet-Union-alpha"),
|
| 455 |
+
],
|
| 456 |
+
"jasperai/Flux.1-dev-Controlnet-Depth": [
|
| 457 |
+
("jasperai/Flux.1-dev-Controlnet-Depth", "diffusion_pytorch_model.safetensors", "models/ControlNet/jasperai/Flux.1-dev-Controlnet-Depth"),
|
| 458 |
+
],
|
| 459 |
+
"jasperai/Flux.1-dev-Controlnet-Surface-Normals": [
|
| 460 |
+
("jasperai/Flux.1-dev-Controlnet-Surface-Normals", "diffusion_pytorch_model.safetensors", "models/ControlNet/jasperai/Flux.1-dev-Controlnet-Surface-Normals"),
|
| 461 |
+
],
|
| 462 |
+
"jasperai/Flux.1-dev-Controlnet-Upscaler": [
|
| 463 |
+
("jasperai/Flux.1-dev-Controlnet-Upscaler", "diffusion_pytorch_model.safetensors", "models/ControlNet/jasperai/Flux.1-dev-Controlnet-Upscaler"),
|
| 464 |
+
],
|
| 465 |
+
"alimama-creative/FLUX.1-dev-Controlnet-Inpainting-Alpha": [
|
| 466 |
+
("alimama-creative/FLUX.1-dev-Controlnet-Inpainting-Alpha", "diffusion_pytorch_model.safetensors", "models/ControlNet/alimama-creative/FLUX.1-dev-Controlnet-Inpainting-Alpha"),
|
| 467 |
+
],
|
| 468 |
+
"alimama-creative/FLUX.1-dev-Controlnet-Inpainting-Beta": [
|
| 469 |
+
("alimama-creative/FLUX.1-dev-Controlnet-Inpainting-Beta", "diffusion_pytorch_model.safetensors", "models/ControlNet/alimama-creative/FLUX.1-dev-Controlnet-Inpainting-Beta"),
|
| 470 |
+
],
|
| 471 |
+
"Shakker-Labs/FLUX.1-dev-ControlNet-Depth": [
|
| 472 |
+
("Shakker-Labs/FLUX.1-dev-ControlNet-Depth", "diffusion_pytorch_model.safetensors", "models/ControlNet/Shakker-Labs/FLUX.1-dev-ControlNet-Depth"),
|
| 473 |
+
],
|
| 474 |
+
"Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro": [
|
| 475 |
+
("Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro", "diffusion_pytorch_model.safetensors", "models/ControlNet/Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro"),
|
| 476 |
+
],
|
| 477 |
+
"InstantX/FLUX.1-dev-IP-Adapter": {
|
| 478 |
+
"file_list": [
|
| 479 |
+
("InstantX/FLUX.1-dev-IP-Adapter", "ip-adapter.bin", "models/IpAdapter/InstantX/FLUX.1-dev-IP-Adapter"),
|
| 480 |
+
("AI-ModelScope/siglip-so400m-patch14-384", "model.safetensors", "models/IpAdapter/InstantX/FLUX.1-dev-IP-Adapter/image_encoder"),
|
| 481 |
+
("AI-ModelScope/siglip-so400m-patch14-384", "config.json", "models/IpAdapter/InstantX/FLUX.1-dev-IP-Adapter/image_encoder"),
|
| 482 |
+
],
|
| 483 |
+
"load_path": [
|
| 484 |
+
"models/IpAdapter/InstantX/FLUX.1-dev-IP-Adapter/ip-adapter.bin",
|
| 485 |
+
"models/IpAdapter/InstantX/FLUX.1-dev-IP-Adapter/image_encoder",
|
| 486 |
+
],
|
| 487 |
+
},
|
| 488 |
+
# ESRGAN
|
| 489 |
+
"ESRGAN_x4": [
|
| 490 |
+
("AI-ModelScope/Real-ESRGAN", "RealESRGAN_x4.pth", "models/ESRGAN"),
|
| 491 |
+
],
|
| 492 |
+
# RIFE
|
| 493 |
+
"RIFE": [
|
| 494 |
+
("AI-ModelScope/RIFE", "flownet.pkl", "models/RIFE"),
|
| 495 |
+
],
|
| 496 |
+
# Omnigen
|
| 497 |
+
"OmniGen-v1": {
|
| 498 |
+
"file_list": [
|
| 499 |
+
("BAAI/OmniGen-v1", "vae/diffusion_pytorch_model.safetensors", "models/OmniGen/OmniGen-v1/vae"),
|
| 500 |
+
("BAAI/OmniGen-v1", "model.safetensors", "models/OmniGen/OmniGen-v1"),
|
| 501 |
+
("BAAI/OmniGen-v1", "config.json", "models/OmniGen/OmniGen-v1"),
|
| 502 |
+
("BAAI/OmniGen-v1", "special_tokens_map.json", "models/OmniGen/OmniGen-v1"),
|
| 503 |
+
("BAAI/OmniGen-v1", "tokenizer_config.json", "models/OmniGen/OmniGen-v1"),
|
| 504 |
+
("BAAI/OmniGen-v1", "tokenizer.json", "models/OmniGen/OmniGen-v1"),
|
| 505 |
+
],
|
| 506 |
+
"load_path": [
|
| 507 |
+
"models/OmniGen/OmniGen-v1/vae/diffusion_pytorch_model.safetensors",
|
| 508 |
+
"models/OmniGen/OmniGen-v1/model.safetensors",
|
| 509 |
+
]
|
| 510 |
+
},
|
| 511 |
+
# CogVideo
|
| 512 |
+
"CogVideoX-5B": {
|
| 513 |
+
"file_list": [
|
| 514 |
+
("ZhipuAI/CogVideoX-5b", "text_encoder/config.json", "models/CogVideo/CogVideoX-5b/text_encoder"),
|
| 515 |
+
("ZhipuAI/CogVideoX-5b", "text_encoder/model.safetensors.index.json", "models/CogVideo/CogVideoX-5b/text_encoder"),
|
| 516 |
+
("ZhipuAI/CogVideoX-5b", "text_encoder/model-00001-of-00002.safetensors", "models/CogVideo/CogVideoX-5b/text_encoder"),
|
| 517 |
+
("ZhipuAI/CogVideoX-5b", "text_encoder/model-00002-of-00002.safetensors", "models/CogVideo/CogVideoX-5b/text_encoder"),
|
| 518 |
+
("ZhipuAI/CogVideoX-5b", "transformer/config.json", "models/CogVideo/CogVideoX-5b/transformer"),
|
| 519 |
+
("ZhipuAI/CogVideoX-5b", "transformer/diffusion_pytorch_model.safetensors.index.json", "models/CogVideo/CogVideoX-5b/transformer"),
|
| 520 |
+
("ZhipuAI/CogVideoX-5b", "transformer/diffusion_pytorch_model-00001-of-00002.safetensors", "models/CogVideo/CogVideoX-5b/transformer"),
|
| 521 |
+
("ZhipuAI/CogVideoX-5b", "transformer/diffusion_pytorch_model-00002-of-00002.safetensors", "models/CogVideo/CogVideoX-5b/transformer"),
|
| 522 |
+
("ZhipuAI/CogVideoX-5b", "vae/diffusion_pytorch_model.safetensors", "models/CogVideo/CogVideoX-5b/vae"),
|
| 523 |
+
],
|
| 524 |
+
"load_path": [
|
| 525 |
+
"models/CogVideo/CogVideoX-5b/text_encoder",
|
| 526 |
+
"models/CogVideo/CogVideoX-5b/transformer",
|
| 527 |
+
"models/CogVideo/CogVideoX-5b/vae/diffusion_pytorch_model.safetensors",
|
| 528 |
+
],
|
| 529 |
+
},
|
| 530 |
+
# Stable Diffusion 3.5
|
| 531 |
+
"StableDiffusion3.5-large": [
|
| 532 |
+
("AI-ModelScope/stable-diffusion-3.5-large", "sd3.5_large.safetensors", "models/stable_diffusion_3"),
|
| 533 |
+
("AI-ModelScope/stable-diffusion-3.5-large", "text_encoders/clip_l.safetensors", "models/stable_diffusion_3/text_encoders"),
|
| 534 |
+
("AI-ModelScope/stable-diffusion-3.5-large", "text_encoders/clip_g.safetensors", "models/stable_diffusion_3/text_encoders"),
|
| 535 |
+
("AI-ModelScope/stable-diffusion-3.5-large", "text_encoders/t5xxl_fp16.safetensors", "models/stable_diffusion_3/text_encoders"),
|
| 536 |
+
],
|
| 537 |
+
"StableDiffusion3.5-medium": [
|
| 538 |
+
("AI-ModelScope/stable-diffusion-3.5-medium", "sd3.5_medium.safetensors", "models/stable_diffusion_3"),
|
| 539 |
+
("AI-ModelScope/stable-diffusion-3.5-large", "text_encoders/clip_l.safetensors", "models/stable_diffusion_3/text_encoders"),
|
| 540 |
+
("AI-ModelScope/stable-diffusion-3.5-large", "text_encoders/clip_g.safetensors", "models/stable_diffusion_3/text_encoders"),
|
| 541 |
+
("AI-ModelScope/stable-diffusion-3.5-large", "text_encoders/t5xxl_fp16.safetensors", "models/stable_diffusion_3/text_encoders"),
|
| 542 |
+
],
|
| 543 |
+
"StableDiffusion3.5-large-turbo": [
|
| 544 |
+
("AI-ModelScope/stable-diffusion-3.5-large-turbo", "sd3.5_large_turbo.safetensors", "models/stable_diffusion_3"),
|
| 545 |
+
("AI-ModelScope/stable-diffusion-3.5-large", "text_encoders/clip_l.safetensors", "models/stable_diffusion_3/text_encoders"),
|
| 546 |
+
("AI-ModelScope/stable-diffusion-3.5-large", "text_encoders/clip_g.safetensors", "models/stable_diffusion_3/text_encoders"),
|
| 547 |
+
("AI-ModelScope/stable-diffusion-3.5-large", "text_encoders/t5xxl_fp16.safetensors", "models/stable_diffusion_3/text_encoders"),
|
| 548 |
+
],
|
| 549 |
+
"HunyuanVideo":{
|
| 550 |
+
"file_list": [
|
| 551 |
+
("AI-ModelScope/clip-vit-large-patch14", "model.safetensors", "models/HunyuanVideo/text_encoder"),
|
| 552 |
+
("DiffSynth-Studio/HunyuanVideo_MLLM_text_encoder", "model-00001-of-00004.safetensors", "models/HunyuanVideo/text_encoder_2"),
|
| 553 |
+
("DiffSynth-Studio/HunyuanVideo_MLLM_text_encoder", "model-00002-of-00004.safetensors", "models/HunyuanVideo/text_encoder_2"),
|
| 554 |
+
("DiffSynth-Studio/HunyuanVideo_MLLM_text_encoder", "model-00003-of-00004.safetensors", "models/HunyuanVideo/text_encoder_2"),
|
| 555 |
+
("DiffSynth-Studio/HunyuanVideo_MLLM_text_encoder", "model-00004-of-00004.safetensors", "models/HunyuanVideo/text_encoder_2"),
|
| 556 |
+
("DiffSynth-Studio/HunyuanVideo_MLLM_text_encoder", "config.json", "models/HunyuanVideo/text_encoder_2"),
|
| 557 |
+
("DiffSynth-Studio/HunyuanVideo_MLLM_text_encoder", "model.safetensors.index.json", "models/HunyuanVideo/text_encoder_2"),
|
| 558 |
+
("AI-ModelScope/HunyuanVideo", "hunyuan-video-t2v-720p/vae/pytorch_model.pt", "models/HunyuanVideo/vae"),
|
| 559 |
+
("AI-ModelScope/HunyuanVideo", "hunyuan-video-t2v-720p/transformers/mp_rank_00_model_states.pt", "models/HunyuanVideo/transformers")
|
| 560 |
+
],
|
| 561 |
+
"load_path": [
|
| 562 |
+
"models/HunyuanVideo/text_encoder/model.safetensors",
|
| 563 |
+
"models/HunyuanVideo/text_encoder_2",
|
| 564 |
+
"models/HunyuanVideo/vae/pytorch_model.pt",
|
| 565 |
+
"models/HunyuanVideo/transformers/mp_rank_00_model_states.pt"
|
| 566 |
+
],
|
| 567 |
+
},
|
| 568 |
+
"HunyuanVideoI2V":{
|
| 569 |
+
"file_list": [
|
| 570 |
+
("AI-ModelScope/clip-vit-large-patch14", "model.safetensors", "models/HunyuanVideoI2V/text_encoder"),
|
| 571 |
+
("AI-ModelScope/llava-llama-3-8b-v1_1-transformers", "model-00001-of-00004.safetensors", "models/HunyuanVideoI2V/text_encoder_2"),
|
| 572 |
+
("AI-ModelScope/llava-llama-3-8b-v1_1-transformers", "model-00002-of-00004.safetensors", "models/HunyuanVideoI2V/text_encoder_2"),
|
| 573 |
+
("AI-ModelScope/llava-llama-3-8b-v1_1-transformers", "model-00003-of-00004.safetensors", "models/HunyuanVideoI2V/text_encoder_2"),
|
| 574 |
+
("AI-ModelScope/llava-llama-3-8b-v1_1-transformers", "model-00004-of-00004.safetensors", "models/HunyuanVideoI2V/text_encoder_2"),
|
| 575 |
+
("AI-ModelScope/llava-llama-3-8b-v1_1-transformers", "config.json", "models/HunyuanVideoI2V/text_encoder_2"),
|
| 576 |
+
("AI-ModelScope/llava-llama-3-8b-v1_1-transformers", "model.safetensors.index.json", "models/HunyuanVideoI2V/text_encoder_2"),
|
| 577 |
+
("AI-ModelScope/HunyuanVideo-I2V", "hunyuan-video-i2v-720p/vae/pytorch_model.pt", "models/HunyuanVideoI2V/vae"),
|
| 578 |
+
("AI-ModelScope/HunyuanVideo-I2V", "hunyuan-video-i2v-720p/transformers/mp_rank_00_model_states.pt", "models/HunyuanVideoI2V/transformers")
|
| 579 |
+
],
|
| 580 |
+
"load_path": [
|
| 581 |
+
"models/HunyuanVideoI2V/text_encoder/model.safetensors",
|
| 582 |
+
"models/HunyuanVideoI2V/text_encoder_2",
|
| 583 |
+
"models/HunyuanVideoI2V/vae/pytorch_model.pt",
|
| 584 |
+
"models/HunyuanVideoI2V/transformers/mp_rank_00_model_states.pt"
|
| 585 |
+
],
|
| 586 |
+
},
|
| 587 |
+
"HunyuanVideo-fp8":{
|
| 588 |
+
"file_list": [
|
| 589 |
+
("AI-ModelScope/clip-vit-large-patch14", "model.safetensors", "models/HunyuanVideo/text_encoder"),
|
| 590 |
+
("DiffSynth-Studio/HunyuanVideo_MLLM_text_encoder", "model-00001-of-00004.safetensors", "models/HunyuanVideo/text_encoder_2"),
|
| 591 |
+
("DiffSynth-Studio/HunyuanVideo_MLLM_text_encoder", "model-00002-of-00004.safetensors", "models/HunyuanVideo/text_encoder_2"),
|
| 592 |
+
("DiffSynth-Studio/HunyuanVideo_MLLM_text_encoder", "model-00003-of-00004.safetensors", "models/HunyuanVideo/text_encoder_2"),
|
| 593 |
+
("DiffSynth-Studio/HunyuanVideo_MLLM_text_encoder", "model-00004-of-00004.safetensors", "models/HunyuanVideo/text_encoder_2"),
|
| 594 |
+
("DiffSynth-Studio/HunyuanVideo_MLLM_text_encoder", "config.json", "models/HunyuanVideo/text_encoder_2"),
|
| 595 |
+
("DiffSynth-Studio/HunyuanVideo_MLLM_text_encoder", "model.safetensors.index.json", "models/HunyuanVideo/text_encoder_2"),
|
| 596 |
+
("AI-ModelScope/HunyuanVideo", "hunyuan-video-t2v-720p/vae/pytorch_model.pt", "models/HunyuanVideo/vae"),
|
| 597 |
+
("DiffSynth-Studio/HunyuanVideo-safetensors", "model.fp8.safetensors", "models/HunyuanVideo/transformers")
|
| 598 |
+
],
|
| 599 |
+
"load_path": [
|
| 600 |
+
"models/HunyuanVideo/text_encoder/model.safetensors",
|
| 601 |
+
"models/HunyuanVideo/text_encoder_2",
|
| 602 |
+
"models/HunyuanVideo/vae/pytorch_model.pt",
|
| 603 |
+
"models/HunyuanVideo/transformers/model.fp8.safetensors"
|
| 604 |
+
],
|
| 605 |
+
},
|
| 606 |
+
}
|
| 607 |
+
Preset_model_id: TypeAlias = Literal[
|
| 608 |
+
"HunyuanDiT",
|
| 609 |
+
"stable-video-diffusion-img2vid-xt",
|
| 610 |
+
"ExVideo-SVD-128f-v1",
|
| 611 |
+
"ExVideo-CogVideoX-LoRA-129f-v1",
|
| 612 |
+
"StableDiffusion_v15",
|
| 613 |
+
"DreamShaper_8",
|
| 614 |
+
"AingDiffusion_v12",
|
| 615 |
+
"Flat2DAnimerge_v45Sharp",
|
| 616 |
+
"TextualInversion_VeryBadImageNegative_v1.3",
|
| 617 |
+
"StableDiffusionXL_v1",
|
| 618 |
+
"BluePencilXL_v200",
|
| 619 |
+
"StableDiffusionXL_Turbo",
|
| 620 |
+
"ControlNet_v11f1p_sd15_depth",
|
| 621 |
+
"ControlNet_v11p_sd15_softedge",
|
| 622 |
+
"ControlNet_v11f1e_sd15_tile",
|
| 623 |
+
"ControlNet_v11p_sd15_lineart",
|
| 624 |
+
"AnimateDiff_v2",
|
| 625 |
+
"AnimateDiff_xl_beta",
|
| 626 |
+
"RIFE",
|
| 627 |
+
"BeautifulPrompt",
|
| 628 |
+
"opus-mt-zh-en",
|
| 629 |
+
"IP-Adapter-SD",
|
| 630 |
+
"IP-Adapter-SDXL",
|
| 631 |
+
"StableDiffusion3",
|
| 632 |
+
"StableDiffusion3_without_T5",
|
| 633 |
+
"Kolors",
|
| 634 |
+
"SDXL-vae-fp16-fix",
|
| 635 |
+
"ControlNet_union_sdxl_promax",
|
| 636 |
+
"FLUX.1-dev",
|
| 637 |
+
"FLUX.1-schnell",
|
| 638 |
+
"InstantX/FLUX.1-dev-Controlnet-Union-alpha",
|
| 639 |
+
"jasperai/Flux.1-dev-Controlnet-Depth",
|
| 640 |
+
"jasperai/Flux.1-dev-Controlnet-Surface-Normals",
|
| 641 |
+
"jasperai/Flux.1-dev-Controlnet-Upscaler",
|
| 642 |
+
"alimama-creative/FLUX.1-dev-Controlnet-Inpainting-Alpha",
|
| 643 |
+
"alimama-creative/FLUX.1-dev-Controlnet-Inpainting-Beta",
|
| 644 |
+
"Shakker-Labs/FLUX.1-dev-ControlNet-Depth",
|
| 645 |
+
"Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro",
|
| 646 |
+
"InstantX/FLUX.1-dev-IP-Adapter",
|
| 647 |
+
"SDXL_lora_zyd232_ChineseInkStyle_SDXL_v1_0",
|
| 648 |
+
"QwenPrompt",
|
| 649 |
+
"OmostPrompt",
|
| 650 |
+
"ESRGAN_x4",
|
| 651 |
+
"RIFE",
|
| 652 |
+
"OmniGen-v1",
|
| 653 |
+
"CogVideoX-5B",
|
| 654 |
+
"Annotators:Depth",
|
| 655 |
+
"Annotators:Softedge",
|
| 656 |
+
"Annotators:Lineart",
|
| 657 |
+
"Annotators:Normal",
|
| 658 |
+
"Annotators:Openpose",
|
| 659 |
+
"StableDiffusion3.5-large",
|
| 660 |
+
"StableDiffusion3.5-medium",
|
| 661 |
+
"HunyuanVideo",
|
| 662 |
+
"HunyuanVideo-fp8",
|
| 663 |
+
"HunyuanVideoI2V",
|
| 664 |
+
]
|
OmniAvatar/models/audio_pack.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from typing import Tuple, Union
|
| 3 |
+
import torch
|
| 4 |
+
from einops import rearrange
|
| 5 |
+
from torch import nn
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def make_triple(value: Union[int, Tuple[int, int, int]]) -> Tuple[int, int, int]:
|
| 9 |
+
value = (value,) * 3 if isinstance(value, int) else value
|
| 10 |
+
assert len(value) == 3
|
| 11 |
+
return value
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class AudioPack(nn.Module):
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
in_channels: int,
|
| 18 |
+
patch_size: Union[int, Tuple[int, int, int]],
|
| 19 |
+
dim: int,
|
| 20 |
+
layernorm=False,
|
| 21 |
+
):
|
| 22 |
+
super().__init__()
|
| 23 |
+
t, h, w = make_triple(patch_size)
|
| 24 |
+
self.patch_size = t, h, w
|
| 25 |
+
self.proj = nn.Linear(in_channels * t * h * w, dim)
|
| 26 |
+
if layernorm:
|
| 27 |
+
self.norm_out = nn.LayerNorm(dim)
|
| 28 |
+
else:
|
| 29 |
+
self.norm_out = None
|
| 30 |
+
|
| 31 |
+
def forward(
|
| 32 |
+
self,
|
| 33 |
+
vid: torch.Tensor,
|
| 34 |
+
) -> torch.Tensor:
|
| 35 |
+
t, h, w = self.patch_size
|
| 36 |
+
vid = rearrange(vid, "b c (T t) (H h) (W w) -> b T H W (t h w c)", t=t, h=h, w=w)
|
| 37 |
+
vid = self.proj(vid)
|
| 38 |
+
if self.norm_out is not None:
|
| 39 |
+
vid = self.norm_out(vid)
|
| 40 |
+
return vid
|
OmniAvatar/models/model_manager.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, torch, json, importlib
|
| 2 |
+
from typing import List
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
from ..configs.model_config import model_loader_configs, huggingface_model_loader_configs
|
| 5 |
+
from ..utils.io_utils import load_state_dict, init_weights_on_device, hash_state_dict_keys, split_state_dict_with_prefix, smart_load_weights
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def load_model_from_single_file(state_dict, model_names, model_classes, model_resource, torch_dtype, device, infer):
|
| 9 |
+
loaded_model_names, loaded_models = [], []
|
| 10 |
+
for model_name, model_class in zip(model_names, model_classes):
|
| 11 |
+
print(f" model_name: {model_name} model_class: {model_class.__name__}")
|
| 12 |
+
state_dict_converter = model_class.state_dict_converter()
|
| 13 |
+
if model_resource == "civitai":
|
| 14 |
+
state_dict_results = state_dict_converter.from_civitai(state_dict)
|
| 15 |
+
elif model_resource == "diffusers":
|
| 16 |
+
state_dict_results = state_dict_converter.from_diffusers(state_dict)
|
| 17 |
+
if isinstance(state_dict_results, tuple):
|
| 18 |
+
model_state_dict, extra_kwargs = state_dict_results
|
| 19 |
+
print(f" This model is initialized with extra kwargs: {extra_kwargs}")
|
| 20 |
+
else:
|
| 21 |
+
model_state_dict, extra_kwargs = state_dict_results, {}
|
| 22 |
+
torch_dtype = torch.float32 if extra_kwargs.get("upcast_to_float32", False) else torch_dtype
|
| 23 |
+
with init_weights_on_device():
|
| 24 |
+
model = model_class(**extra_kwargs)
|
| 25 |
+
if hasattr(model, "eval"):
|
| 26 |
+
model = model.eval()
|
| 27 |
+
if not infer: # 训练才初始化
|
| 28 |
+
model = model.to_empty(device=torch.device("cpu"))
|
| 29 |
+
for name, param in model.named_parameters():
|
| 30 |
+
if param.dim() > 1: # 通常只对权重矩阵而不是偏置做初始化
|
| 31 |
+
nn.init.xavier_uniform_(param, gain=0.05)
|
| 32 |
+
else:
|
| 33 |
+
nn.init.zeros_(param)
|
| 34 |
+
else:
|
| 35 |
+
model = model.to_empty(device=device)
|
| 36 |
+
model, _, _ = smart_load_weights(model, model_state_dict)
|
| 37 |
+
# model.load_state_dict(model_state_dict, assign=True, strict=False)
|
| 38 |
+
model = model.to(dtype=torch_dtype, device=device)
|
| 39 |
+
loaded_model_names.append(model_name)
|
| 40 |
+
loaded_models.append(model)
|
| 41 |
+
return loaded_model_names, loaded_models
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def load_model_from_huggingface_folder(file_path, model_names, model_classes, torch_dtype, device):
|
| 45 |
+
loaded_model_names, loaded_models = [], []
|
| 46 |
+
for model_name, model_class in zip(model_names, model_classes):
|
| 47 |
+
if torch_dtype in [torch.float32, torch.float16, torch.bfloat16]:
|
| 48 |
+
model = model_class.from_pretrained(file_path, torch_dtype=torch_dtype).eval()
|
| 49 |
+
else:
|
| 50 |
+
model = model_class.from_pretrained(file_path).eval().to(dtype=torch_dtype)
|
| 51 |
+
if torch_dtype == torch.float16 and hasattr(model, "half"):
|
| 52 |
+
model = model.half()
|
| 53 |
+
try:
|
| 54 |
+
model = model.to(device=device)
|
| 55 |
+
except:
|
| 56 |
+
pass
|
| 57 |
+
loaded_model_names.append(model_name)
|
| 58 |
+
loaded_models.append(model)
|
| 59 |
+
return loaded_model_names, loaded_models
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def load_single_patch_model_from_single_file(state_dict, model_name, model_class, base_model, extra_kwargs, torch_dtype, device):
|
| 63 |
+
print(f" model_name: {model_name} model_class: {model_class.__name__} extra_kwargs: {extra_kwargs}")
|
| 64 |
+
base_state_dict = base_model.state_dict()
|
| 65 |
+
base_model.to("cpu")
|
| 66 |
+
del base_model
|
| 67 |
+
model = model_class(**extra_kwargs)
|
| 68 |
+
model.load_state_dict(base_state_dict, strict=False)
|
| 69 |
+
model.load_state_dict(state_dict, strict=False)
|
| 70 |
+
model.to(dtype=torch_dtype, device=device)
|
| 71 |
+
return model
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def load_patch_model_from_single_file(state_dict, model_names, model_classes, extra_kwargs, model_manager, torch_dtype, device):
|
| 75 |
+
loaded_model_names, loaded_models = [], []
|
| 76 |
+
for model_name, model_class in zip(model_names, model_classes):
|
| 77 |
+
while True:
|
| 78 |
+
for model_id in range(len(model_manager.model)):
|
| 79 |
+
base_model_name = model_manager.model_name[model_id]
|
| 80 |
+
if base_model_name == model_name:
|
| 81 |
+
base_model_path = model_manager.model_path[model_id]
|
| 82 |
+
base_model = model_manager.model[model_id]
|
| 83 |
+
print(f" Adding patch model to {base_model_name} ({base_model_path})")
|
| 84 |
+
patched_model = load_single_patch_model_from_single_file(
|
| 85 |
+
state_dict, model_name, model_class, base_model, extra_kwargs, torch_dtype, device)
|
| 86 |
+
loaded_model_names.append(base_model_name)
|
| 87 |
+
loaded_models.append(patched_model)
|
| 88 |
+
model_manager.model.pop(model_id)
|
| 89 |
+
model_manager.model_path.pop(model_id)
|
| 90 |
+
model_manager.model_name.pop(model_id)
|
| 91 |
+
break
|
| 92 |
+
else:
|
| 93 |
+
break
|
| 94 |
+
return loaded_model_names, loaded_models
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class ModelDetectorTemplate:
|
| 99 |
+
def __init__(self):
|
| 100 |
+
pass
|
| 101 |
+
|
| 102 |
+
def match(self, file_path="", state_dict={}):
|
| 103 |
+
return False
|
| 104 |
+
|
| 105 |
+
def load(self, file_path="", state_dict={}, device="cuda", torch_dtype=torch.float16, **kwargs):
|
| 106 |
+
return [], []
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class ModelDetectorFromSingleFile:
|
| 111 |
+
def __init__(self, model_loader_configs=[]):
|
| 112 |
+
self.keys_hash_with_shape_dict = {}
|
| 113 |
+
self.keys_hash_dict = {}
|
| 114 |
+
for metadata in model_loader_configs:
|
| 115 |
+
self.add_model_metadata(*metadata)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def add_model_metadata(self, keys_hash, keys_hash_with_shape, model_names, model_classes, model_resource):
|
| 119 |
+
self.keys_hash_with_shape_dict[keys_hash_with_shape] = (model_names, model_classes, model_resource)
|
| 120 |
+
if keys_hash is not None:
|
| 121 |
+
self.keys_hash_dict[keys_hash] = (model_names, model_classes, model_resource)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def match(self, file_path="", state_dict={}):
|
| 125 |
+
if isinstance(file_path, str) and os.path.isdir(file_path):
|
| 126 |
+
return False
|
| 127 |
+
if len(state_dict) == 0:
|
| 128 |
+
state_dict = load_state_dict(file_path)
|
| 129 |
+
keys_hash_with_shape = hash_state_dict_keys(state_dict, with_shape=True)
|
| 130 |
+
if keys_hash_with_shape in self.keys_hash_with_shape_dict:
|
| 131 |
+
return True
|
| 132 |
+
keys_hash = hash_state_dict_keys(state_dict, with_shape=False)
|
| 133 |
+
if keys_hash in self.keys_hash_dict:
|
| 134 |
+
return True
|
| 135 |
+
return False
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def load(self, file_path="", state_dict={}, device="cuda", torch_dtype=torch.float16, infer=False, **kwargs):
|
| 139 |
+
if len(state_dict) == 0:
|
| 140 |
+
state_dict = load_state_dict(file_path)
|
| 141 |
+
|
| 142 |
+
# Load models with strict matching
|
| 143 |
+
keys_hash_with_shape = hash_state_dict_keys(state_dict, with_shape=True)
|
| 144 |
+
if keys_hash_with_shape in self.keys_hash_with_shape_dict:
|
| 145 |
+
model_names, model_classes, model_resource = self.keys_hash_with_shape_dict[keys_hash_with_shape]
|
| 146 |
+
loaded_model_names, loaded_models = load_model_from_single_file(state_dict, model_names, model_classes, model_resource, torch_dtype, device, infer)
|
| 147 |
+
return loaded_model_names, loaded_models
|
| 148 |
+
|
| 149 |
+
# Load models without strict matching
|
| 150 |
+
# (the shape of parameters may be inconsistent, and the state_dict_converter will modify the model architecture)
|
| 151 |
+
keys_hash = hash_state_dict_keys(state_dict, with_shape=False)
|
| 152 |
+
if keys_hash in self.keys_hash_dict:
|
| 153 |
+
model_names, model_classes, model_resource = self.keys_hash_dict[keys_hash]
|
| 154 |
+
loaded_model_names, loaded_models = load_model_from_single_file(state_dict, model_names, model_classes, model_resource, torch_dtype, device, infer)
|
| 155 |
+
return loaded_model_names, loaded_models
|
| 156 |
+
|
| 157 |
+
return loaded_model_names, loaded_models
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
class ModelDetectorFromSplitedSingleFile(ModelDetectorFromSingleFile):
|
| 162 |
+
def __init__(self, model_loader_configs=[]):
|
| 163 |
+
super().__init__(model_loader_configs)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def match(self, file_path="", state_dict={}):
|
| 167 |
+
if isinstance(file_path, str) and os.path.isdir(file_path):
|
| 168 |
+
return False
|
| 169 |
+
if len(state_dict) == 0:
|
| 170 |
+
state_dict = load_state_dict(file_path)
|
| 171 |
+
splited_state_dict = split_state_dict_with_prefix(state_dict)
|
| 172 |
+
for sub_state_dict in splited_state_dict:
|
| 173 |
+
if super().match(file_path, sub_state_dict):
|
| 174 |
+
return True
|
| 175 |
+
return False
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def load(self, file_path="", state_dict={}, device="cuda", torch_dtype=torch.float16, **kwargs):
|
| 179 |
+
# Split the state_dict and load from each component
|
| 180 |
+
splited_state_dict = split_state_dict_with_prefix(state_dict)
|
| 181 |
+
valid_state_dict = {}
|
| 182 |
+
for sub_state_dict in splited_state_dict:
|
| 183 |
+
if super().match(file_path, sub_state_dict):
|
| 184 |
+
valid_state_dict.update(sub_state_dict)
|
| 185 |
+
if super().match(file_path, valid_state_dict):
|
| 186 |
+
loaded_model_names, loaded_models = super().load(file_path, valid_state_dict, device, torch_dtype)
|
| 187 |
+
else:
|
| 188 |
+
loaded_model_names, loaded_models = [], []
|
| 189 |
+
for sub_state_dict in splited_state_dict:
|
| 190 |
+
if super().match(file_path, sub_state_dict):
|
| 191 |
+
loaded_model_names_, loaded_models_ = super().load(file_path, valid_state_dict, device, torch_dtype)
|
| 192 |
+
loaded_model_names += loaded_model_names_
|
| 193 |
+
loaded_models += loaded_models_
|
| 194 |
+
return loaded_model_names, loaded_models
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class ModelDetectorFromHuggingfaceFolder:
|
| 199 |
+
def __init__(self, model_loader_configs=[]):
|
| 200 |
+
self.architecture_dict = {}
|
| 201 |
+
for metadata in model_loader_configs:
|
| 202 |
+
self.add_model_metadata(*metadata)
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def add_model_metadata(self, architecture, huggingface_lib, model_name, redirected_architecture):
|
| 206 |
+
self.architecture_dict[architecture] = (huggingface_lib, model_name, redirected_architecture)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def match(self, file_path="", state_dict={}):
|
| 210 |
+
if not isinstance(file_path, str) or os.path.isfile(file_path):
|
| 211 |
+
return False
|
| 212 |
+
file_list = os.listdir(file_path)
|
| 213 |
+
if "config.json" not in file_list:
|
| 214 |
+
return False
|
| 215 |
+
with open(os.path.join(file_path, "config.json"), "r") as f:
|
| 216 |
+
config = json.load(f)
|
| 217 |
+
if "architectures" not in config and "_class_name" not in config:
|
| 218 |
+
return False
|
| 219 |
+
return True
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def load(self, file_path="", state_dict={}, device="cuda", torch_dtype=torch.float16, **kwargs):
|
| 223 |
+
with open(os.path.join(file_path, "config.json"), "r") as f:
|
| 224 |
+
config = json.load(f)
|
| 225 |
+
loaded_model_names, loaded_models = [], []
|
| 226 |
+
architectures = config["architectures"] if "architectures" in config else [config["_class_name"]]
|
| 227 |
+
for architecture in architectures:
|
| 228 |
+
huggingface_lib, model_name, redirected_architecture = self.architecture_dict[architecture]
|
| 229 |
+
if redirected_architecture is not None:
|
| 230 |
+
architecture = redirected_architecture
|
| 231 |
+
model_class = importlib.import_module(huggingface_lib).__getattribute__(architecture)
|
| 232 |
+
loaded_model_names_, loaded_models_ = load_model_from_huggingface_folder(file_path, [model_name], [model_class], torch_dtype, device)
|
| 233 |
+
loaded_model_names += loaded_model_names_
|
| 234 |
+
loaded_models += loaded_models_
|
| 235 |
+
return loaded_model_names, loaded_models
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
class ModelDetectorFromPatchedSingleFile:
|
| 240 |
+
def __init__(self, model_loader_configs=[]):
|
| 241 |
+
self.keys_hash_with_shape_dict = {}
|
| 242 |
+
for metadata in model_loader_configs:
|
| 243 |
+
self.add_model_metadata(*metadata)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def add_model_metadata(self, keys_hash_with_shape, model_name, model_class, extra_kwargs):
|
| 247 |
+
self.keys_hash_with_shape_dict[keys_hash_with_shape] = (model_name, model_class, extra_kwargs)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def match(self, file_path="", state_dict={}):
|
| 251 |
+
if not isinstance(file_path, str) or os.path.isdir(file_path):
|
| 252 |
+
return False
|
| 253 |
+
if len(state_dict) == 0:
|
| 254 |
+
state_dict = load_state_dict(file_path)
|
| 255 |
+
keys_hash_with_shape = hash_state_dict_keys(state_dict, with_shape=True)
|
| 256 |
+
if keys_hash_with_shape in self.keys_hash_with_shape_dict:
|
| 257 |
+
return True
|
| 258 |
+
return False
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def load(self, file_path="", state_dict={}, device="cuda", torch_dtype=torch.float16, model_manager=None, **kwargs):
|
| 262 |
+
if len(state_dict) == 0:
|
| 263 |
+
state_dict = load_state_dict(file_path)
|
| 264 |
+
|
| 265 |
+
# Load models with strict matching
|
| 266 |
+
loaded_model_names, loaded_models = [], []
|
| 267 |
+
keys_hash_with_shape = hash_state_dict_keys(state_dict, with_shape=True)
|
| 268 |
+
if keys_hash_with_shape in self.keys_hash_with_shape_dict:
|
| 269 |
+
model_names, model_classes, extra_kwargs = self.keys_hash_with_shape_dict[keys_hash_with_shape]
|
| 270 |
+
loaded_model_names_, loaded_models_ = load_patch_model_from_single_file(
|
| 271 |
+
state_dict, model_names, model_classes, extra_kwargs, model_manager, torch_dtype, device)
|
| 272 |
+
loaded_model_names += loaded_model_names_
|
| 273 |
+
loaded_models += loaded_models_
|
| 274 |
+
return loaded_model_names, loaded_models
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class ModelManager:
|
| 279 |
+
def __init__(
|
| 280 |
+
self,
|
| 281 |
+
torch_dtype=torch.float16,
|
| 282 |
+
device="cuda",
|
| 283 |
+
model_id_list: List = [],
|
| 284 |
+
downloading_priority: List = ["ModelScope", "HuggingFace"],
|
| 285 |
+
file_path_list: List[str] = [],
|
| 286 |
+
infer: bool = False
|
| 287 |
+
):
|
| 288 |
+
self.torch_dtype = torch_dtype
|
| 289 |
+
self.device = device
|
| 290 |
+
self.model = []
|
| 291 |
+
self.model_path = []
|
| 292 |
+
self.model_name = []
|
| 293 |
+
self.infer = infer
|
| 294 |
+
downloaded_files = []
|
| 295 |
+
self.model_detector = [
|
| 296 |
+
ModelDetectorFromSingleFile(model_loader_configs),
|
| 297 |
+
ModelDetectorFromSplitedSingleFile(model_loader_configs),
|
| 298 |
+
ModelDetectorFromHuggingfaceFolder(huggingface_model_loader_configs),
|
| 299 |
+
]
|
| 300 |
+
self.load_models(downloaded_files + file_path_list)
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def load_model_from_single_file(self, file_path="", state_dict={}, model_names=[], model_classes=[], model_resource=None):
|
| 304 |
+
print(f"Loading models from file: {file_path}")
|
| 305 |
+
if len(state_dict) == 0:
|
| 306 |
+
state_dict = load_state_dict(file_path)
|
| 307 |
+
model_names, models = load_model_from_single_file(state_dict, model_names, model_classes, model_resource, self.torch_dtype, self.device, self.infer)
|
| 308 |
+
for model_name, model in zip(model_names, models):
|
| 309 |
+
self.model.append(model)
|
| 310 |
+
self.model_path.append(file_path)
|
| 311 |
+
self.model_name.append(model_name)
|
| 312 |
+
print(f" The following models are loaded: {model_names}.")
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def load_model_from_huggingface_folder(self, file_path="", model_names=[], model_classes=[]):
|
| 316 |
+
print(f"Loading models from folder: {file_path}")
|
| 317 |
+
model_names, models = load_model_from_huggingface_folder(file_path, model_names, model_classes, self.torch_dtype, self.device)
|
| 318 |
+
for model_name, model in zip(model_names, models):
|
| 319 |
+
self.model.append(model)
|
| 320 |
+
self.model_path.append(file_path)
|
| 321 |
+
self.model_name.append(model_name)
|
| 322 |
+
print(f" The following models are loaded: {model_names}.")
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def load_patch_model_from_single_file(self, file_path="", state_dict={}, model_names=[], model_classes=[], extra_kwargs={}):
|
| 326 |
+
print(f"Loading patch models from file: {file_path}")
|
| 327 |
+
model_names, models = load_patch_model_from_single_file(
|
| 328 |
+
state_dict, model_names, model_classes, extra_kwargs, self, self.torch_dtype, self.device)
|
| 329 |
+
for model_name, model in zip(model_names, models):
|
| 330 |
+
self.model.append(model)
|
| 331 |
+
self.model_path.append(file_path)
|
| 332 |
+
self.model_name.append(model_name)
|
| 333 |
+
print(f" The following patched models are loaded: {model_names}.")
|
| 334 |
+
|
| 335 |
+
def load_model(self, file_path, model_names=None, device=None, torch_dtype=None):
|
| 336 |
+
print(f"Loading models from: {file_path}")
|
| 337 |
+
if device is None: device = self.device ## cpu
|
| 338 |
+
if torch_dtype is None: torch_dtype = self.torch_dtype
|
| 339 |
+
if isinstance(file_path, list): ## <-------- file_path = ["pretrained_models/Wan2.1-T2V-14B/diffusion_pytorch_model-00001-of-00006.safetensors", ..., "...-00006-of-00006.safetensors"
|
| 340 |
+
state_dict = {}
|
| 341 |
+
"""
|
| 342 |
+
state_dict:
|
| 343 |
+
{
|
| 344 |
+
"patch_embedding.weight": tensor(...), # from shard 1
|
| 345 |
+
"patch_embedding.bias": tensor(...), # from shard 1
|
| 346 |
+
"blocks.0.self_attn.q.weight": tensor(...), # from shard 1
|
| 347 |
+
"blocks.0.self_attn.k.weight": tensor(...), # from shard 1
|
| 348 |
+
...
|
| 349 |
+
"blocks.20.ffn.2.weight": tensor(...), # from shard 3 or 4
|
| 350 |
+
...
|
| 351 |
+
"blocks.39.ffn.2.weight": tensor(...), # from shard 6
|
| 352 |
+
"head.head.weight": tensor(...), # from shard 6
|
| 353 |
+
}
|
| 354 |
+
"""
|
| 355 |
+
for path in file_path:
|
| 356 |
+
state_dict.update(load_state_dict(path))
|
| 357 |
+
elif os.path.isfile(file_path):
|
| 358 |
+
"""
|
| 359 |
+
For T5 (models_t5_umt5-xxl-enc-bf16.pth):
|
| 360 |
+
{
|
| 361 |
+
"encoder.embed_tokens.weight": tensor(...),
|
| 362 |
+
"encoder.block.0.layer.0.SelfAttention.q.weight": tensor(...),
|
| 363 |
+
...
|
| 364 |
+
"encoder.block.23.layer.1.DenseReluDense.wi_1.weight": tensor(...),
|
| 365 |
+
"encoder.final_layer_norm.weight": tensor(...),
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
For VAE (Wan2.1_VAE.pth):
|
| 369 |
+
{
|
| 370 |
+
"encoder.conv_in.conv.weight": tensor(...),
|
| 371 |
+
"encoder.mid.attn_1.norm.weight": tensor(...),
|
| 372 |
+
...
|
| 373 |
+
"decoder.conv_out.conv.weight": tensor(...),
|
| 374 |
+
}
|
| 375 |
+
"""
|
| 376 |
+
state_dict = load_state_dict(file_path)
|
| 377 |
+
else:
|
| 378 |
+
state_dict = None
|
| 379 |
+
|
| 380 |
+
"""
|
| 381 |
+
It calls into ModelDetectorFromSingleFile.load() (line 138), which looks up the matched model class from the hash table, then calls load_model_from_single_file() (line 8). That function
|
| 382 |
+
does:
|
| 383 |
+
|
| 384 |
+
1. Gets the model class — e.g. WanVideoModel for DiT, WanVideoVAE for VAE
|
| 385 |
+
2. state_dict_converter — converts key names from the checkpoint format to the model's internal format (e.g. HuggingFace naming → DiffSynth naming)
|
| 386 |
+
3. model_class(**extra_kwargs) — constructs the model with empty weights. For the DiT, extra_kwargs includes in_dim from args.model_config (which is 49 for V2V)
|
| 387 |
+
4. Since infer=False (lines 27-33):
|
| 388 |
+
- Moves to CPU with to_empty(device="cpu")
|
| 389 |
+
- Xavier-inits all weight matrices (gain=0.05), zeros all biases
|
| 390 |
+
5. smart_load_weights(model, model_state_dict) — overlays the Wan 2.1 weights onto the xavier-initialized model. For mismatched shapes (like 16ch checkpoint into 49ch patch_embedding), it
|
| 391 |
+
copies what fits and leaves the rest xavier-initialized
|
| 392 |
+
6. Casts to target dtype/device — model.to(dtype=torch_dtype, device="cpu")
|
| 393 |
+
|
| 394 |
+
So the model starts fully xavier-initialized, then gets the base Wan 2.1 weights overlaid on top. New parameters (extra patch_embedding channels, audio module placeholders) keep their
|
| 395 |
+
xavier values.
|
| 396 |
+
"""
|
| 397 |
+
for model_detector in self.model_detector:
|
| 398 |
+
if model_detector.match(file_path, state_dict):
|
| 399 |
+
model_names, models = model_detector.load(
|
| 400 |
+
file_path, state_dict,
|
| 401 |
+
device=device, torch_dtype=torch_dtype,
|
| 402 |
+
allowed_model_names=model_names, model_manager=self, infer=self.infer
|
| 403 |
+
)
|
| 404 |
+
for model_name, model in zip(model_names, models):
|
| 405 |
+
self.model.append(model)
|
| 406 |
+
self.model_path.append(file_path)
|
| 407 |
+
self.model_name.append(model_name)
|
| 408 |
+
print(f" The following models are loaded: {model_names}.")
|
| 409 |
+
break
|
| 410 |
+
else:
|
| 411 |
+
print(f" We cannot detect the model type. No models are loaded.")
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
def load_models(self, file_path_list, model_names=None, device=None, torch_dtype=None):
|
| 415 |
+
for file_path in file_path_list:
|
| 416 |
+
self.load_model(file_path, model_names, device=device, torch_dtype=torch_dtype)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def fetch_model(self, model_name, file_path=None, require_model_path=False):
|
| 420 |
+
fetched_models = []
|
| 421 |
+
fetched_model_paths = []
|
| 422 |
+
for model, model_path, model_name_ in zip(self.model, self.model_path, self.model_name):
|
| 423 |
+
if file_path is not None and file_path != model_path:
|
| 424 |
+
continue
|
| 425 |
+
if model_name == model_name_:
|
| 426 |
+
fetched_models.append(model)
|
| 427 |
+
fetched_model_paths.append(model_path)
|
| 428 |
+
if len(fetched_models) == 0:
|
| 429 |
+
print(f"No {model_name} models available.")
|
| 430 |
+
return None
|
| 431 |
+
if len(fetched_models) == 1:
|
| 432 |
+
print(f"Using {model_name} from {fetched_model_paths[0]}.")
|
| 433 |
+
else:
|
| 434 |
+
print(f"More than one {model_name} models are loaded in model manager: {fetched_model_paths}. Using {model_name} from {fetched_model_paths[0]}.")
|
| 435 |
+
if require_model_path:
|
| 436 |
+
return fetched_models[0], fetched_model_paths[0]
|
| 437 |
+
else:
|
| 438 |
+
return fetched_models[0]
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
def to(self, device):
|
| 442 |
+
for model in self.model:
|
| 443 |
+
model.to(device)
|
| 444 |
+
|
OmniAvatar/models/wan_video_dit.py
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
import math
|
| 5 |
+
from typing import Tuple, Optional
|
| 6 |
+
from einops import rearrange
|
| 7 |
+
from ..utils.io_utils import hash_state_dict_keys
|
| 8 |
+
from .audio_pack import AudioPack
|
| 9 |
+
from ..utils.args_config import args
|
| 10 |
+
|
| 11 |
+
# IMPORTANT: Import flash_attn_interface BEFORE xfuser to avoid operator conflicts
|
| 12 |
+
# xfuser registers simplified flash_attn_3 operators that would override the real ones
|
| 13 |
+
try:
|
| 14 |
+
import flash_attn_interface
|
| 15 |
+
FLASH_ATTN_3_AVAILABLE = True
|
| 16 |
+
except ModuleNotFoundError:
|
| 17 |
+
flash_attn_interface = None
|
| 18 |
+
FLASH_ATTN_3_AVAILABLE = False
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
+
import flash_attn
|
| 22 |
+
FLASH_ATTN_2_AVAILABLE = True
|
| 23 |
+
except ModuleNotFoundError:
|
| 24 |
+
FLASH_ATTN_2_AVAILABLE = False
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
from sageattention import sageattn
|
| 28 |
+
SAGE_ATTN_AVAILABLE = True
|
| 29 |
+
except ModuleNotFoundError:
|
| 30 |
+
SAGE_ATTN_AVAILABLE = False
|
| 31 |
+
|
| 32 |
+
# Lazy import for xfuser - import AFTER flash_attn to avoid operator conflicts
|
| 33 |
+
_xfuser_imported = False
|
| 34 |
+
_xfuser_get_sequence_parallel_rank = None
|
| 35 |
+
_xfuser_get_sequence_parallel_world_size = None
|
| 36 |
+
_xfuser_get_sp_group = None
|
| 37 |
+
|
| 38 |
+
def _lazy_import_xfuser():
|
| 39 |
+
"""Lazily import xfuser only when context parallel is actually used."""
|
| 40 |
+
global _xfuser_imported, _xfuser_get_sequence_parallel_rank
|
| 41 |
+
global _xfuser_get_sequence_parallel_world_size, _xfuser_get_sp_group
|
| 42 |
+
if not _xfuser_imported:
|
| 43 |
+
from xfuser.core.distributed import (
|
| 44 |
+
get_sequence_parallel_rank,
|
| 45 |
+
get_sequence_parallel_world_size,
|
| 46 |
+
get_sp_group
|
| 47 |
+
)
|
| 48 |
+
_xfuser_get_sequence_parallel_rank = get_sequence_parallel_rank
|
| 49 |
+
_xfuser_get_sequence_parallel_world_size = get_sequence_parallel_world_size
|
| 50 |
+
_xfuser_get_sp_group = get_sp_group
|
| 51 |
+
_xfuser_imported = True
|
| 52 |
+
|
| 53 |
+
def get_sequence_parallel_rank():
|
| 54 |
+
_lazy_import_xfuser()
|
| 55 |
+
return _xfuser_get_sequence_parallel_rank()
|
| 56 |
+
|
| 57 |
+
def get_sequence_parallel_world_size():
|
| 58 |
+
_lazy_import_xfuser()
|
| 59 |
+
return _xfuser_get_sequence_parallel_world_size()
|
| 60 |
+
|
| 61 |
+
def get_sp_group():
|
| 62 |
+
_lazy_import_xfuser()
|
| 63 |
+
return _xfuser_get_sp_group()
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def flash_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, num_heads: int, compatibility_mode=False):
|
| 67 |
+
# When context parallel is enabled (sp_size > 1), xfuser conflicts with flash_attn_3
|
| 68 |
+
# so we must fall back to flash_attn_2 or SDPA
|
| 69 |
+
use_flash_attn_3 = FLASH_ATTN_3_AVAILABLE and (args is None or getattr(args, 'sp_size', 1) <= 1)
|
| 70 |
+
|
| 71 |
+
if compatibility_mode:
|
| 72 |
+
q = rearrange(q, "b s (n d) -> b n s d", n=num_heads)
|
| 73 |
+
k = rearrange(k, "b s (n d) -> b n s d", n=num_heads)
|
| 74 |
+
v = rearrange(v, "b s (n d) -> b n s d", n=num_heads)
|
| 75 |
+
x = F.scaled_dot_product_attention(q, k, v)
|
| 76 |
+
x = rearrange(x, "b n s d -> b s (n d)", n=num_heads)
|
| 77 |
+
elif use_flash_attn_3:
|
| 78 |
+
q = rearrange(q, "b s (n d) -> b s n d", n=num_heads)
|
| 79 |
+
k = rearrange(k, "b s (n d) -> b s n d", n=num_heads)
|
| 80 |
+
v = rearrange(v, "b s (n d) -> b s n d", n=num_heads)
|
| 81 |
+
x = flash_attn_interface.flash_attn_func(q, k, v)
|
| 82 |
+
x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
|
| 83 |
+
elif FLASH_ATTN_2_AVAILABLE:
|
| 84 |
+
q = rearrange(q, "b s (n d) -> b s n d", n=num_heads)
|
| 85 |
+
k = rearrange(k, "b s (n d) -> b s n d", n=num_heads)
|
| 86 |
+
v = rearrange(v, "b s (n d) -> b s n d", n=num_heads)
|
| 87 |
+
x = flash_attn.flash_attn_func(q, k, v)
|
| 88 |
+
x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
|
| 89 |
+
elif SAGE_ATTN_AVAILABLE:
|
| 90 |
+
q = rearrange(q, "b s (n d) -> b n s d", n=num_heads)
|
| 91 |
+
k = rearrange(k, "b s (n d) -> b n s d", n=num_heads)
|
| 92 |
+
v = rearrange(v, "b s (n d) -> b n s d", n=num_heads)
|
| 93 |
+
x = sageattn(q, k, v)
|
| 94 |
+
x = rearrange(x, "b n s d -> b s (n d)", n=num_heads)
|
| 95 |
+
else:
|
| 96 |
+
q = rearrange(q, "b s (n d) -> b n s d", n=num_heads)
|
| 97 |
+
k = rearrange(k, "b s (n d) -> b n s d", n=num_heads)
|
| 98 |
+
v = rearrange(v, "b s (n d) -> b n s d", n=num_heads)
|
| 99 |
+
x = F.scaled_dot_product_attention(q, k, v)
|
| 100 |
+
x = rearrange(x, "b n s d -> b s (n d)", n=num_heads)
|
| 101 |
+
return x
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def modulate(x: torch.Tensor, shift: torch.Tensor, scale: torch.Tensor):
|
| 105 |
+
return (x * (1 + scale) + shift)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def sinusoidal_embedding_1d(dim, position):
|
| 109 |
+
sinusoid = torch.outer(position.type(torch.float64), torch.pow(
|
| 110 |
+
10000, -torch.arange(dim//2, dtype=torch.float64, device=position.device).div(dim//2)))
|
| 111 |
+
x = torch.cat([torch.cos(sinusoid), torch.sin(sinusoid)], dim=1)
|
| 112 |
+
return x.to(position.dtype)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def precompute_freqs_cis_3d(dim: int, end: int = 1024, theta: float = 10000.0):
|
| 116 |
+
# 3d rope precompute
|
| 117 |
+
f_freqs_cis = precompute_freqs_cis(dim - 2 * (dim // 3), end, theta)
|
| 118 |
+
h_freqs_cis = precompute_freqs_cis(dim // 3, end, theta)
|
| 119 |
+
w_freqs_cis = precompute_freqs_cis(dim // 3, end, theta)
|
| 120 |
+
return f_freqs_cis, h_freqs_cis, w_freqs_cis
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def precompute_freqs_cis(dim: int, end: int = 1024, theta: float = 10000.0):
|
| 124 |
+
# 1d rope precompute
|
| 125 |
+
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)
|
| 126 |
+
[: (dim // 2)].double() / dim))
|
| 127 |
+
freqs = torch.outer(torch.arange(end, device=freqs.device), freqs)
|
| 128 |
+
freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # complex64
|
| 129 |
+
return freqs_cis
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def rope_apply(x, freqs, num_heads):
|
| 133 |
+
x = rearrange(x, "b s (n d) -> b s n d", n=num_heads)
|
| 134 |
+
x_out = torch.view_as_complex(x.to(torch.float64).reshape(
|
| 135 |
+
x.shape[0], x.shape[1], x.shape[2], -1, 2))
|
| 136 |
+
x_out = torch.view_as_real(x_out * freqs).flatten(2)
|
| 137 |
+
return x_out.to(x.dtype)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class RMSNorm(nn.Module):
|
| 141 |
+
def __init__(self, dim, eps=1e-5):
|
| 142 |
+
super().__init__()
|
| 143 |
+
self.eps = eps
|
| 144 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 145 |
+
|
| 146 |
+
def norm(self, x):
|
| 147 |
+
return x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
|
| 148 |
+
|
| 149 |
+
def forward(self, x):
|
| 150 |
+
dtype = x.dtype
|
| 151 |
+
return self.norm(x.float()).to(dtype) * self.weight
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
class AttentionModule(nn.Module):
|
| 155 |
+
def __init__(self, num_heads):
|
| 156 |
+
super().__init__()
|
| 157 |
+
self.num_heads = num_heads
|
| 158 |
+
|
| 159 |
+
def forward(self, q, k, v):
|
| 160 |
+
x = flash_attention(q=q, k=k, v=v, num_heads=self.num_heads)
|
| 161 |
+
return x
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
class SelfAttention(nn.Module):
|
| 165 |
+
def __init__(self, dim: int, num_heads: int, eps: float = 1e-6):
|
| 166 |
+
super().__init__()
|
| 167 |
+
self.dim = dim
|
| 168 |
+
self.num_heads = num_heads
|
| 169 |
+
self.head_dim = dim // num_heads
|
| 170 |
+
|
| 171 |
+
self.q = nn.Linear(dim, dim)
|
| 172 |
+
self.k = nn.Linear(dim, dim)
|
| 173 |
+
self.v = nn.Linear(dim, dim)
|
| 174 |
+
self.o = nn.Linear(dim, dim)
|
| 175 |
+
self.norm_q = RMSNorm(dim, eps=eps)
|
| 176 |
+
self.norm_k = RMSNorm(dim, eps=eps)
|
| 177 |
+
|
| 178 |
+
self.attn = AttentionModule(self.num_heads)
|
| 179 |
+
|
| 180 |
+
def forward(self, x, freqs):
|
| 181 |
+
q = self.norm_q(self.q(x))
|
| 182 |
+
k = self.norm_k(self.k(x))
|
| 183 |
+
v = self.v(x)
|
| 184 |
+
q = rope_apply(q, freqs, self.num_heads)
|
| 185 |
+
k = rope_apply(k, freqs, self.num_heads)
|
| 186 |
+
x = self.attn(q, k, v)
|
| 187 |
+
return self.o(x)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class CrossAttention(nn.Module):
|
| 191 |
+
def __init__(self, dim: int, num_heads: int, eps: float = 1e-6, has_image_input: bool = False):
|
| 192 |
+
super().__init__()
|
| 193 |
+
self.dim = dim
|
| 194 |
+
self.num_heads = num_heads
|
| 195 |
+
self.head_dim = dim // num_heads
|
| 196 |
+
|
| 197 |
+
self.q = nn.Linear(dim, dim)
|
| 198 |
+
self.k = nn.Linear(dim, dim)
|
| 199 |
+
self.v = nn.Linear(dim, dim)
|
| 200 |
+
self.o = nn.Linear(dim, dim)
|
| 201 |
+
self.norm_q = RMSNorm(dim, eps=eps)
|
| 202 |
+
self.norm_k = RMSNorm(dim, eps=eps)
|
| 203 |
+
self.has_image_input = has_image_input
|
| 204 |
+
if has_image_input:
|
| 205 |
+
self.k_img = nn.Linear(dim, dim)
|
| 206 |
+
self.v_img = nn.Linear(dim, dim)
|
| 207 |
+
self.norm_k_img = RMSNorm(dim, eps=eps)
|
| 208 |
+
|
| 209 |
+
self.attn = AttentionModule(self.num_heads)
|
| 210 |
+
|
| 211 |
+
def forward(self, x: torch.Tensor, y: torch.Tensor):
|
| 212 |
+
if self.has_image_input:
|
| 213 |
+
img = y[:, :257]
|
| 214 |
+
ctx = y[:, 257:]
|
| 215 |
+
else:
|
| 216 |
+
ctx = y
|
| 217 |
+
q = self.norm_q(self.q(x))
|
| 218 |
+
k = self.norm_k(self.k(ctx))
|
| 219 |
+
v = self.v(ctx)
|
| 220 |
+
x = self.attn(q, k, v)
|
| 221 |
+
if self.has_image_input:
|
| 222 |
+
k_img = self.norm_k_img(self.k_img(img))
|
| 223 |
+
v_img = self.v_img(img)
|
| 224 |
+
y = flash_attention(q, k_img, v_img, num_heads=self.num_heads)
|
| 225 |
+
x = x + y
|
| 226 |
+
return self.o(x)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
class GateModule(nn.Module):
|
| 230 |
+
def __init__(self,):
|
| 231 |
+
super().__init__()
|
| 232 |
+
|
| 233 |
+
def forward(self, x, gate, residual):
|
| 234 |
+
return x + gate * residual
|
| 235 |
+
|
| 236 |
+
class DiTBlock(nn.Module):
|
| 237 |
+
def __init__(self, has_image_input: bool, dim: int, num_heads: int, ffn_dim: int, eps: float = 1e-6):
|
| 238 |
+
super().__init__()
|
| 239 |
+
self.dim = dim
|
| 240 |
+
self.num_heads = num_heads
|
| 241 |
+
self.ffn_dim = ffn_dim
|
| 242 |
+
|
| 243 |
+
self.self_attn = SelfAttention(dim, num_heads, eps)
|
| 244 |
+
self.cross_attn = CrossAttention(
|
| 245 |
+
dim, num_heads, eps, has_image_input=has_image_input)
|
| 246 |
+
self.norm1 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
|
| 247 |
+
self.norm2 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
|
| 248 |
+
self.norm3 = nn.LayerNorm(dim, eps=eps)
|
| 249 |
+
self.ffn = nn.Sequential(nn.Linear(dim, ffn_dim), nn.GELU(
|
| 250 |
+
approximate='tanh'), nn.Linear(ffn_dim, dim))
|
| 251 |
+
self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
|
| 252 |
+
self.gate = GateModule()
|
| 253 |
+
|
| 254 |
+
def forward(self, x, context, t_mod, freqs):
|
| 255 |
+
# msa: multi-head self-attention mlp: multi-layer perceptron
|
| 256 |
+
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
|
| 257 |
+
self.modulation.to(dtype=t_mod.dtype, device=t_mod.device) + t_mod).chunk(6, dim=1)
|
| 258 |
+
input_x = modulate(self.norm1(x), shift_msa, scale_msa)
|
| 259 |
+
x = self.gate(x, gate_msa, self.self_attn(input_x, freqs))
|
| 260 |
+
x = x + self.cross_attn(self.norm3(x), context)
|
| 261 |
+
input_x = modulate(self.norm2(x), shift_mlp, scale_mlp)
|
| 262 |
+
x = self.gate(x, gate_mlp, self.ffn(input_x))
|
| 263 |
+
return x
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
class MLP(torch.nn.Module):
|
| 267 |
+
def __init__(self, in_dim, out_dim):
|
| 268 |
+
super().__init__()
|
| 269 |
+
self.proj = torch.nn.Sequential(
|
| 270 |
+
nn.LayerNorm(in_dim),
|
| 271 |
+
nn.Linear(in_dim, in_dim),
|
| 272 |
+
nn.GELU(),
|
| 273 |
+
nn.Linear(in_dim, out_dim),
|
| 274 |
+
nn.LayerNorm(out_dim)
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
def forward(self, x):
|
| 278 |
+
return self.proj(x)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
class Head(nn.Module):
|
| 282 |
+
def __init__(self, dim: int, out_dim: int, patch_size: Tuple[int, int, int], eps: float):
|
| 283 |
+
super().__init__()
|
| 284 |
+
self.dim = dim
|
| 285 |
+
self.patch_size = patch_size
|
| 286 |
+
self.norm = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
|
| 287 |
+
self.head = nn.Linear(dim, out_dim * math.prod(patch_size))
|
| 288 |
+
self.modulation = nn.Parameter(torch.randn(1, 2, dim) / dim**0.5)
|
| 289 |
+
|
| 290 |
+
def forward(self, x, t_mod):
|
| 291 |
+
# t_mod: [B, dim] -> [B, 1, dim] for broadcasting with modulation [1, 2, dim]
|
| 292 |
+
if t_mod.dim() == 2:
|
| 293 |
+
t_mod = t_mod.unsqueeze(1)
|
| 294 |
+
shift, scale = (self.modulation.to(dtype=t_mod.dtype, device=t_mod.device) + t_mod).chunk(2, dim=1)
|
| 295 |
+
x = (self.head(self.norm(x) * (1 + scale) + shift))
|
| 296 |
+
return x
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
class WanModel(torch.nn.Module):
|
| 301 |
+
def __init__(
|
| 302 |
+
self,
|
| 303 |
+
dim: int,
|
| 304 |
+
in_dim: int,
|
| 305 |
+
ffn_dim: int,
|
| 306 |
+
out_dim: int,
|
| 307 |
+
text_dim: int,
|
| 308 |
+
freq_dim: int,
|
| 309 |
+
eps: float,
|
| 310 |
+
patch_size: Tuple[int, int, int],
|
| 311 |
+
num_heads: int,
|
| 312 |
+
num_layers: int,
|
| 313 |
+
has_image_input: bool,
|
| 314 |
+
audio_hidden_size: int=32,
|
| 315 |
+
):
|
| 316 |
+
super().__init__()
|
| 317 |
+
self.dim = dim
|
| 318 |
+
self.freq_dim = freq_dim
|
| 319 |
+
self.has_image_input = has_image_input
|
| 320 |
+
self.patch_size = patch_size
|
| 321 |
+
|
| 322 |
+
self.patch_embedding = nn.Conv3d(
|
| 323 |
+
in_dim, dim, kernel_size=patch_size, stride=patch_size)
|
| 324 |
+
# nn.LayerNorm(dim)
|
| 325 |
+
self.text_embedding = nn.Sequential(
|
| 326 |
+
nn.Linear(text_dim, dim),
|
| 327 |
+
nn.GELU(approximate='tanh'),
|
| 328 |
+
nn.Linear(dim, dim)
|
| 329 |
+
)
|
| 330 |
+
self.time_embedding = nn.Sequential(
|
| 331 |
+
nn.Linear(freq_dim, dim),
|
| 332 |
+
nn.SiLU(),
|
| 333 |
+
nn.Linear(dim, dim)
|
| 334 |
+
)
|
| 335 |
+
self.time_projection = nn.Sequential(
|
| 336 |
+
nn.SiLU(), nn.Linear(dim, dim * 6))
|
| 337 |
+
self.blocks = nn.ModuleList([
|
| 338 |
+
DiTBlock(has_image_input, dim, num_heads, ffn_dim, eps)
|
| 339 |
+
for _ in range(num_layers)
|
| 340 |
+
])
|
| 341 |
+
self.head = Head(dim, out_dim, patch_size, eps)
|
| 342 |
+
head_dim = dim // num_heads
|
| 343 |
+
self.freqs = precompute_freqs_cis_3d(head_dim)
|
| 344 |
+
|
| 345 |
+
if has_image_input:
|
| 346 |
+
self.img_emb = MLP(1280, dim) # clip_feature_dim = 1280
|
| 347 |
+
|
| 348 |
+
if 'use_audio' in args:
|
| 349 |
+
self.use_audio = args.use_audio
|
| 350 |
+
else:
|
| 351 |
+
self.use_audio = False
|
| 352 |
+
if self.use_audio:
|
| 353 |
+
audio_input_dim = 10752
|
| 354 |
+
audio_out_dim = dim
|
| 355 |
+
self.audio_proj = AudioPack(audio_input_dim, [4, 1, 1], audio_hidden_size, layernorm=True)
|
| 356 |
+
self.audio_cond_projs = nn.ModuleList()
|
| 357 |
+
for d in range(num_layers // 2 - 1):
|
| 358 |
+
l = nn.Linear(audio_hidden_size, audio_out_dim)
|
| 359 |
+
self.audio_cond_projs.append(l)
|
| 360 |
+
|
| 361 |
+
def patchify(self, x: torch.Tensor):
|
| 362 |
+
grid_size = x.shape[2:]
|
| 363 |
+
x = rearrange(x, 'b c f h w -> b (f h w) c').contiguous()
|
| 364 |
+
return x, grid_size # x, grid_size: (f, h, w)
|
| 365 |
+
|
| 366 |
+
def unpatchify(self, x: torch.Tensor, grid_size: torch.Tensor):
|
| 367 |
+
return rearrange(
|
| 368 |
+
x, 'b (f h w) (x y z c) -> b c (f x) (h y) (w z)',
|
| 369 |
+
f=grid_size[0], h=grid_size[1], w=grid_size[2],
|
| 370 |
+
x=self.patch_size[0], y=self.patch_size[1], z=self.patch_size[2]
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
def forward(self,
|
| 374 |
+
x: torch.Tensor,
|
| 375 |
+
timestep: torch.Tensor,
|
| 376 |
+
context: torch.Tensor,
|
| 377 |
+
clip_feature: Optional[torch.Tensor] = None,
|
| 378 |
+
y: Optional[torch.Tensor] = None,
|
| 379 |
+
use_gradient_checkpointing: bool = False,
|
| 380 |
+
audio_emb: Optional[torch.Tensor] = None,
|
| 381 |
+
use_gradient_checkpointing_offload: bool = False,
|
| 382 |
+
tea_cache = None,
|
| 383 |
+
**kwargs,
|
| 384 |
+
):
|
| 385 |
+
t = self.time_embedding(
|
| 386 |
+
sinusoidal_embedding_1d(self.freq_dim, timestep))
|
| 387 |
+
t_mod = self.time_projection(t).unflatten(1, (6, self.dim))
|
| 388 |
+
context = self.text_embedding(context)
|
| 389 |
+
lat_h, lat_w = x.shape[-2], x.shape[-1]
|
| 390 |
+
|
| 391 |
+
if audio_emb != None and self.use_audio: # TODO cache
|
| 392 |
+
audio_emb = audio_emb.permute(0, 2, 1)[:, :, :, None, None]
|
| 393 |
+
audio_emb = torch.cat([audio_emb[:, :, :1].repeat(1, 1, 3, 1, 1), audio_emb], 2) # 1, 768, 44, 1, 1
|
| 394 |
+
audio_emb = self.audio_proj(audio_emb)
|
| 395 |
+
|
| 396 |
+
audio_emb = torch.stack([audio_cond_proj(audio_emb) for audio_cond_proj in self.audio_cond_projs], dim=1)
|
| 397 |
+
|
| 398 |
+
x = torch.cat([x, y], dim=1)
|
| 399 |
+
x = self.patch_embedding(x)
|
| 400 |
+
x, (f, h, w) = self.patchify(x)
|
| 401 |
+
|
| 402 |
+
freqs = torch.cat([
|
| 403 |
+
self.freqs[0][:f].view(f, 1, 1, -1).expand(f, h, w, -1),
|
| 404 |
+
self.freqs[1][:h].view(1, h, 1, -1).expand(f, h, w, -1),
|
| 405 |
+
self.freqs[2][:w].view(1, 1, w, -1).expand(f, h, w, -1)
|
| 406 |
+
], dim=-1).reshape(f * h * w, 1, -1).to(x.device)
|
| 407 |
+
|
| 408 |
+
def create_custom_forward(module):
|
| 409 |
+
def custom_forward(*inputs):
|
| 410 |
+
return module(*inputs)
|
| 411 |
+
return custom_forward
|
| 412 |
+
|
| 413 |
+
if tea_cache is not None:
|
| 414 |
+
tea_cache_update = tea_cache.check(self, x, t_mod)
|
| 415 |
+
else:
|
| 416 |
+
tea_cache_update = False
|
| 417 |
+
ori_x_len = x.shape[1]
|
| 418 |
+
if tea_cache_update:
|
| 419 |
+
x = tea_cache.update(x)
|
| 420 |
+
else:
|
| 421 |
+
if args.sp_size > 1:
|
| 422 |
+
# Context Parallel
|
| 423 |
+
sp_size = get_sequence_parallel_world_size()
|
| 424 |
+
pad_size = 0
|
| 425 |
+
if ori_x_len % sp_size != 0:
|
| 426 |
+
pad_size = sp_size - ori_x_len % sp_size
|
| 427 |
+
x = torch.cat([x, torch.zeros_like(x[:, -1:]).repeat(1, pad_size, 1)], 1)
|
| 428 |
+
x = torch.chunk(x, sp_size, dim=1)[get_sequence_parallel_rank()]
|
| 429 |
+
|
| 430 |
+
# audio_emb is already [B, num_projs, C, T, 1, 1] from torch.stack(..., dim=1)
|
| 431 |
+
|
| 432 |
+
for layer_i, block in enumerate(self.blocks):
|
| 433 |
+
# audio cond
|
| 434 |
+
if self.use_audio:
|
| 435 |
+
au_idx = None
|
| 436 |
+
if (layer_i <= len(self.blocks) // 2 and layer_i > 1): # < len(self.blocks) - 1:
|
| 437 |
+
au_idx = layer_i - 2
|
| 438 |
+
audio_emb_tmp = audio_emb[:, au_idx].repeat(1, 1, lat_h // 2, lat_w // 2, 1) # 1, 11, 45, 25, 128
|
| 439 |
+
audio_cond_tmp = self.patchify(audio_emb_tmp.permute(0, 4, 1, 2, 3))[0]
|
| 440 |
+
if args.sp_size > 1:
|
| 441 |
+
if pad_size > 0:
|
| 442 |
+
audio_cond_tmp = torch.cat([audio_cond_tmp, torch.zeros_like(audio_cond_tmp[:, -1:]).repeat(1, pad_size, 1)], 1)
|
| 443 |
+
audio_cond_tmp = torch.chunk(audio_cond_tmp, sp_size, dim=1)[get_sequence_parallel_rank()]
|
| 444 |
+
x = audio_cond_tmp + x
|
| 445 |
+
|
| 446 |
+
if self.training and use_gradient_checkpointing:
|
| 447 |
+
if use_gradient_checkpointing_offload:
|
| 448 |
+
with torch.autograd.graph.save_on_cpu():
|
| 449 |
+
x = torch.utils.checkpoint.checkpoint(
|
| 450 |
+
create_custom_forward(block),
|
| 451 |
+
x, context, t_mod, freqs,
|
| 452 |
+
use_reentrant=False,
|
| 453 |
+
)
|
| 454 |
+
else:
|
| 455 |
+
x = torch.utils.checkpoint.checkpoint(
|
| 456 |
+
create_custom_forward(block),
|
| 457 |
+
x, context, t_mod, freqs,
|
| 458 |
+
use_reentrant=False,
|
| 459 |
+
)
|
| 460 |
+
else:
|
| 461 |
+
x = block(x, context, t_mod, freqs)
|
| 462 |
+
if tea_cache is not None:
|
| 463 |
+
x_cache = get_sp_group().all_gather(x, dim=1) # TODO: the size should be devided by sp_size
|
| 464 |
+
x_cache = x_cache[:, :ori_x_len]
|
| 465 |
+
tea_cache.store(x_cache)
|
| 466 |
+
|
| 467 |
+
x = self.head(x, t)
|
| 468 |
+
if args.sp_size > 1:
|
| 469 |
+
# Context Parallel
|
| 470 |
+
x = get_sp_group().all_gather(x, dim=1) # TODO: the size should be devided by sp_size
|
| 471 |
+
x = x[:, :ori_x_len]
|
| 472 |
+
|
| 473 |
+
x = self.unpatchify(x, (f, h, w))
|
| 474 |
+
return x
|
| 475 |
+
|
| 476 |
+
@staticmethod
|
| 477 |
+
def state_dict_converter():
|
| 478 |
+
return WanModelStateDictConverter()
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
class WanModelStateDictConverter:
|
| 482 |
+
def __init__(self):
|
| 483 |
+
pass
|
| 484 |
+
|
| 485 |
+
def from_diffusers(self, state_dict):
|
| 486 |
+
rename_dict = {
|
| 487 |
+
"blocks.0.attn1.norm_k.weight": "blocks.0.self_attn.norm_k.weight",
|
| 488 |
+
"blocks.0.attn1.norm_q.weight": "blocks.0.self_attn.norm_q.weight",
|
| 489 |
+
"blocks.0.attn1.to_k.bias": "blocks.0.self_attn.k.bias",
|
| 490 |
+
"blocks.0.attn1.to_k.weight": "blocks.0.self_attn.k.weight",
|
| 491 |
+
"blocks.0.attn1.to_out.0.bias": "blocks.0.self_attn.o.bias",
|
| 492 |
+
"blocks.0.attn1.to_out.0.weight": "blocks.0.self_attn.o.weight",
|
| 493 |
+
"blocks.0.attn1.to_q.bias": "blocks.0.self_attn.q.bias",
|
| 494 |
+
"blocks.0.attn1.to_q.weight": "blocks.0.self_attn.q.weight",
|
| 495 |
+
"blocks.0.attn1.to_v.bias": "blocks.0.self_attn.v.bias",
|
| 496 |
+
"blocks.0.attn1.to_v.weight": "blocks.0.self_attn.v.weight",
|
| 497 |
+
"blocks.0.attn2.norm_k.weight": "blocks.0.cross_attn.norm_k.weight",
|
| 498 |
+
"blocks.0.attn2.norm_q.weight": "blocks.0.cross_attn.norm_q.weight",
|
| 499 |
+
"blocks.0.attn2.to_k.bias": "blocks.0.cross_attn.k.bias",
|
| 500 |
+
"blocks.0.attn2.to_k.weight": "blocks.0.cross_attn.k.weight",
|
| 501 |
+
"blocks.0.attn2.to_out.0.bias": "blocks.0.cross_attn.o.bias",
|
| 502 |
+
"blocks.0.attn2.to_out.0.weight": "blocks.0.cross_attn.o.weight",
|
| 503 |
+
"blocks.0.attn2.to_q.bias": "blocks.0.cross_attn.q.bias",
|
| 504 |
+
"blocks.0.attn2.to_q.weight": "blocks.0.cross_attn.q.weight",
|
| 505 |
+
"blocks.0.attn2.to_v.bias": "blocks.0.cross_attn.v.bias",
|
| 506 |
+
"blocks.0.attn2.to_v.weight": "blocks.0.cross_attn.v.weight",
|
| 507 |
+
"blocks.0.ffn.net.0.proj.bias": "blocks.0.ffn.0.bias",
|
| 508 |
+
"blocks.0.ffn.net.0.proj.weight": "blocks.0.ffn.0.weight",
|
| 509 |
+
"blocks.0.ffn.net.2.bias": "blocks.0.ffn.2.bias",
|
| 510 |
+
"blocks.0.ffn.net.2.weight": "blocks.0.ffn.2.weight",
|
| 511 |
+
"blocks.0.norm2.bias": "blocks.0.norm3.bias",
|
| 512 |
+
"blocks.0.norm2.weight": "blocks.0.norm3.weight",
|
| 513 |
+
"blocks.0.scale_shift_table": "blocks.0.modulation",
|
| 514 |
+
"condition_embedder.text_embedder.linear_1.bias": "text_embedding.0.bias",
|
| 515 |
+
"condition_embedder.text_embedder.linear_1.weight": "text_embedding.0.weight",
|
| 516 |
+
"condition_embedder.text_embedder.linear_2.bias": "text_embedding.2.bias",
|
| 517 |
+
"condition_embedder.text_embedder.linear_2.weight": "text_embedding.2.weight",
|
| 518 |
+
"condition_embedder.time_embedder.linear_1.bias": "time_embedding.0.bias",
|
| 519 |
+
"condition_embedder.time_embedder.linear_1.weight": "time_embedding.0.weight",
|
| 520 |
+
"condition_embedder.time_embedder.linear_2.bias": "time_embedding.2.bias",
|
| 521 |
+
"condition_embedder.time_embedder.linear_2.weight": "time_embedding.2.weight",
|
| 522 |
+
"condition_embedder.time_proj.bias": "time_projection.1.bias",
|
| 523 |
+
"condition_embedder.time_proj.weight": "time_projection.1.weight",
|
| 524 |
+
"patch_embedding.bias": "patch_embedding.bias",
|
| 525 |
+
"patch_embedding.weight": "patch_embedding.weight",
|
| 526 |
+
"scale_shift_table": "head.modulation",
|
| 527 |
+
"proj_out.bias": "head.head.bias",
|
| 528 |
+
"proj_out.weight": "head.head.weight",
|
| 529 |
+
}
|
| 530 |
+
state_dict_ = {}
|
| 531 |
+
for name, param in state_dict.items():
|
| 532 |
+
if name in rename_dict:
|
| 533 |
+
state_dict_[rename_dict[name]] = param
|
| 534 |
+
else:
|
| 535 |
+
name_ = ".".join(name.split(".")[:1] + ["0"] + name.split(".")[2:])
|
| 536 |
+
if name_ in rename_dict:
|
| 537 |
+
name_ = rename_dict[name_]
|
| 538 |
+
name_ = ".".join(name_.split(".")[:1] + [name.split(".")[1]] + name_.split(".")[2:])
|
| 539 |
+
state_dict_[name_] = param
|
| 540 |
+
if hash_state_dict_keys(state_dict) == "cb104773c6c2cb6df4f9529ad5c60d0b":
|
| 541 |
+
config = {
|
| 542 |
+
"model_type": "t2v",
|
| 543 |
+
"patch_size": (1, 2, 2),
|
| 544 |
+
"text_len": 512,
|
| 545 |
+
"in_dim": 16,
|
| 546 |
+
"dim": 5120,
|
| 547 |
+
"ffn_dim": 13824,
|
| 548 |
+
"freq_dim": 256,
|
| 549 |
+
"text_dim": 4096,
|
| 550 |
+
"out_dim": 16,
|
| 551 |
+
"num_heads": 40,
|
| 552 |
+
"num_layers": 40,
|
| 553 |
+
"window_size": (-1, -1),
|
| 554 |
+
"qk_norm": True,
|
| 555 |
+
"cross_attn_norm": True,
|
| 556 |
+
"eps": 1e-6,
|
| 557 |
+
}
|
| 558 |
+
else:
|
| 559 |
+
config = {}
|
| 560 |
+
return state_dict_, config
|
| 561 |
+
|
| 562 |
+
def from_civitai(self, state_dict):
|
| 563 |
+
if hash_state_dict_keys(state_dict) == "9269f8db9040a9d860eaca435be61814":
|
| 564 |
+
config = {
|
| 565 |
+
"has_image_input": False,
|
| 566 |
+
"patch_size": [1, 2, 2],
|
| 567 |
+
"in_dim": 16,
|
| 568 |
+
"dim": 1536,
|
| 569 |
+
"ffn_dim": 8960,
|
| 570 |
+
"freq_dim": 256,
|
| 571 |
+
"text_dim": 4096,
|
| 572 |
+
"out_dim": 16,
|
| 573 |
+
"num_heads": 12,
|
| 574 |
+
"num_layers": 30,
|
| 575 |
+
"eps": 1e-6
|
| 576 |
+
}
|
| 577 |
+
elif hash_state_dict_keys(state_dict) == "aafcfd9672c3a2456dc46e1cb6e52c70":
|
| 578 |
+
config = {
|
| 579 |
+
"has_image_input": False,
|
| 580 |
+
"patch_size": [1, 2, 2],
|
| 581 |
+
"in_dim": 16,
|
| 582 |
+
"dim": 5120,
|
| 583 |
+
"ffn_dim": 13824,
|
| 584 |
+
"freq_dim": 256,
|
| 585 |
+
"text_dim": 4096,
|
| 586 |
+
"out_dim": 16,
|
| 587 |
+
"num_heads": 40,
|
| 588 |
+
"num_layers": 40,
|
| 589 |
+
"eps": 1e-6
|
| 590 |
+
}
|
| 591 |
+
elif hash_state_dict_keys(state_dict) == "6bfcfb3b342cb286ce886889d519a77e":
|
| 592 |
+
config = {
|
| 593 |
+
"has_image_input": True,
|
| 594 |
+
"patch_size": [1, 2, 2],
|
| 595 |
+
"in_dim": 36,
|
| 596 |
+
"dim": 5120,
|
| 597 |
+
"ffn_dim": 13824,
|
| 598 |
+
"freq_dim": 256,
|
| 599 |
+
"text_dim": 4096,
|
| 600 |
+
"out_dim": 16,
|
| 601 |
+
"num_heads": 40,
|
| 602 |
+
"num_layers": 40,
|
| 603 |
+
"eps": 1e-6
|
| 604 |
+
}
|
| 605 |
+
else:
|
| 606 |
+
config = {}
|
| 607 |
+
if hasattr(args, "model_config"):
|
| 608 |
+
model_config = args.model_config
|
| 609 |
+
if model_config is not None:
|
| 610 |
+
config.update(model_config)
|
| 611 |
+
return state_dict, config
|
OmniAvatar/models/wan_video_text_encoder.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def fp16_clamp(x):
|
| 9 |
+
if x.dtype == torch.float16 and torch.isinf(x).any():
|
| 10 |
+
clamp = torch.finfo(x.dtype).max - 1000
|
| 11 |
+
x = torch.clamp(x, min=-clamp, max=clamp)
|
| 12 |
+
return x
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class GELU(nn.Module):
|
| 16 |
+
|
| 17 |
+
def forward(self, x):
|
| 18 |
+
return 0.5 * x * (1.0 + torch.tanh(
|
| 19 |
+
math.sqrt(2.0 / math.pi) * (x + 0.044715 * torch.pow(x, 3.0))))
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class T5LayerNorm(nn.Module):
|
| 23 |
+
|
| 24 |
+
def __init__(self, dim, eps=1e-6):
|
| 25 |
+
super(T5LayerNorm, self).__init__()
|
| 26 |
+
self.dim = dim
|
| 27 |
+
self.eps = eps
|
| 28 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 29 |
+
|
| 30 |
+
def forward(self, x):
|
| 31 |
+
x = x * torch.rsqrt(x.float().pow(2).mean(dim=-1, keepdim=True) +
|
| 32 |
+
self.eps)
|
| 33 |
+
if self.weight.dtype in [torch.float16, torch.bfloat16]:
|
| 34 |
+
x = x.type_as(self.weight)
|
| 35 |
+
return self.weight * x
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class T5Attention(nn.Module):
|
| 39 |
+
|
| 40 |
+
def __init__(self, dim, dim_attn, num_heads, dropout=0.1):
|
| 41 |
+
assert dim_attn % num_heads == 0
|
| 42 |
+
super(T5Attention, self).__init__()
|
| 43 |
+
self.dim = dim
|
| 44 |
+
self.dim_attn = dim_attn
|
| 45 |
+
self.num_heads = num_heads
|
| 46 |
+
self.head_dim = dim_attn // num_heads
|
| 47 |
+
|
| 48 |
+
# layers
|
| 49 |
+
self.q = nn.Linear(dim, dim_attn, bias=False)
|
| 50 |
+
self.k = nn.Linear(dim, dim_attn, bias=False)
|
| 51 |
+
self.v = nn.Linear(dim, dim_attn, bias=False)
|
| 52 |
+
self.o = nn.Linear(dim_attn, dim, bias=False)
|
| 53 |
+
self.dropout = nn.Dropout(dropout)
|
| 54 |
+
|
| 55 |
+
def forward(self, x, context=None, mask=None, pos_bias=None):
|
| 56 |
+
"""
|
| 57 |
+
x: [B, L1, C].
|
| 58 |
+
context: [B, L2, C] or None.
|
| 59 |
+
mask: [B, L2] or [B, L1, L2] or None.
|
| 60 |
+
"""
|
| 61 |
+
# check inputs
|
| 62 |
+
context = x if context is None else context
|
| 63 |
+
b, n, c = x.size(0), self.num_heads, self.head_dim
|
| 64 |
+
|
| 65 |
+
# compute query, key, value
|
| 66 |
+
q = self.q(x).view(b, -1, n, c)
|
| 67 |
+
k = self.k(context).view(b, -1, n, c)
|
| 68 |
+
v = self.v(context).view(b, -1, n, c)
|
| 69 |
+
|
| 70 |
+
# attention bias
|
| 71 |
+
attn_bias = x.new_zeros(b, n, q.size(1), k.size(1))
|
| 72 |
+
if pos_bias is not None:
|
| 73 |
+
attn_bias += pos_bias
|
| 74 |
+
if mask is not None:
|
| 75 |
+
assert mask.ndim in [2, 3]
|
| 76 |
+
mask = mask.view(b, 1, 1,
|
| 77 |
+
-1) if mask.ndim == 2 else mask.unsqueeze(1)
|
| 78 |
+
attn_bias.masked_fill_(mask == 0, torch.finfo(x.dtype).min)
|
| 79 |
+
|
| 80 |
+
# compute attention (T5 does not use scaling)
|
| 81 |
+
attn = torch.einsum('binc,bjnc->bnij', q, k) + attn_bias
|
| 82 |
+
attn = F.softmax(attn.float(), dim=-1).type_as(attn)
|
| 83 |
+
x = torch.einsum('bnij,bjnc->binc', attn, v)
|
| 84 |
+
|
| 85 |
+
# output
|
| 86 |
+
x = x.reshape(b, -1, n * c)
|
| 87 |
+
x = self.o(x)
|
| 88 |
+
x = self.dropout(x)
|
| 89 |
+
return x
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class T5FeedForward(nn.Module):
|
| 93 |
+
|
| 94 |
+
def __init__(self, dim, dim_ffn, dropout=0.1):
|
| 95 |
+
super(T5FeedForward, self).__init__()
|
| 96 |
+
self.dim = dim
|
| 97 |
+
self.dim_ffn = dim_ffn
|
| 98 |
+
|
| 99 |
+
# layers
|
| 100 |
+
self.gate = nn.Sequential(nn.Linear(dim, dim_ffn, bias=False), GELU())
|
| 101 |
+
self.fc1 = nn.Linear(dim, dim_ffn, bias=False)
|
| 102 |
+
self.fc2 = nn.Linear(dim_ffn, dim, bias=False)
|
| 103 |
+
self.dropout = nn.Dropout(dropout)
|
| 104 |
+
|
| 105 |
+
def forward(self, x):
|
| 106 |
+
x = self.fc1(x) * self.gate(x)
|
| 107 |
+
x = self.dropout(x)
|
| 108 |
+
x = self.fc2(x)
|
| 109 |
+
x = self.dropout(x)
|
| 110 |
+
return x
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class T5SelfAttention(nn.Module):
|
| 114 |
+
|
| 115 |
+
def __init__(self,
|
| 116 |
+
dim,
|
| 117 |
+
dim_attn,
|
| 118 |
+
dim_ffn,
|
| 119 |
+
num_heads,
|
| 120 |
+
num_buckets,
|
| 121 |
+
shared_pos=True,
|
| 122 |
+
dropout=0.1):
|
| 123 |
+
super(T5SelfAttention, self).__init__()
|
| 124 |
+
self.dim = dim
|
| 125 |
+
self.dim_attn = dim_attn
|
| 126 |
+
self.dim_ffn = dim_ffn
|
| 127 |
+
self.num_heads = num_heads
|
| 128 |
+
self.num_buckets = num_buckets
|
| 129 |
+
self.shared_pos = shared_pos
|
| 130 |
+
|
| 131 |
+
# layers
|
| 132 |
+
self.norm1 = T5LayerNorm(dim)
|
| 133 |
+
self.attn = T5Attention(dim, dim_attn, num_heads, dropout)
|
| 134 |
+
self.norm2 = T5LayerNorm(dim)
|
| 135 |
+
self.ffn = T5FeedForward(dim, dim_ffn, dropout)
|
| 136 |
+
self.pos_embedding = None if shared_pos else T5RelativeEmbedding(
|
| 137 |
+
num_buckets, num_heads, bidirectional=True)
|
| 138 |
+
|
| 139 |
+
def forward(self, x, mask=None, pos_bias=None):
|
| 140 |
+
e = pos_bias if self.shared_pos else self.pos_embedding(
|
| 141 |
+
x.size(1), x.size(1))
|
| 142 |
+
x = fp16_clamp(x + self.attn(self.norm1(x), mask=mask, pos_bias=e))
|
| 143 |
+
x = fp16_clamp(x + self.ffn(self.norm2(x)))
|
| 144 |
+
return x
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
class T5RelativeEmbedding(nn.Module):
|
| 148 |
+
|
| 149 |
+
def __init__(self, num_buckets, num_heads, bidirectional, max_dist=128):
|
| 150 |
+
super(T5RelativeEmbedding, self).__init__()
|
| 151 |
+
self.num_buckets = num_buckets
|
| 152 |
+
self.num_heads = num_heads
|
| 153 |
+
self.bidirectional = bidirectional
|
| 154 |
+
self.max_dist = max_dist
|
| 155 |
+
|
| 156 |
+
# layers
|
| 157 |
+
self.embedding = nn.Embedding(num_buckets, num_heads)
|
| 158 |
+
|
| 159 |
+
def forward(self, lq, lk):
|
| 160 |
+
device = self.embedding.weight.device
|
| 161 |
+
# rel_pos = torch.arange(lk).unsqueeze(0).to(device) - \
|
| 162 |
+
# torch.arange(lq).unsqueeze(1).to(device)
|
| 163 |
+
rel_pos = torch.arange(lk, device=device).unsqueeze(0) - \
|
| 164 |
+
torch.arange(lq, device=device).unsqueeze(1)
|
| 165 |
+
rel_pos = self._relative_position_bucket(rel_pos)
|
| 166 |
+
rel_pos_embeds = self.embedding(rel_pos)
|
| 167 |
+
rel_pos_embeds = rel_pos_embeds.permute(2, 0, 1).unsqueeze(
|
| 168 |
+
0) # [1, N, Lq, Lk]
|
| 169 |
+
return rel_pos_embeds.contiguous()
|
| 170 |
+
|
| 171 |
+
def _relative_position_bucket(self, rel_pos):
|
| 172 |
+
# preprocess
|
| 173 |
+
if self.bidirectional:
|
| 174 |
+
num_buckets = self.num_buckets // 2
|
| 175 |
+
rel_buckets = (rel_pos > 0).long() * num_buckets
|
| 176 |
+
rel_pos = torch.abs(rel_pos)
|
| 177 |
+
else:
|
| 178 |
+
num_buckets = self.num_buckets
|
| 179 |
+
rel_buckets = 0
|
| 180 |
+
rel_pos = -torch.min(rel_pos, torch.zeros_like(rel_pos))
|
| 181 |
+
|
| 182 |
+
# embeddings for small and large positions
|
| 183 |
+
max_exact = num_buckets // 2
|
| 184 |
+
rel_pos_large = max_exact + (torch.log(rel_pos.float() / max_exact) /
|
| 185 |
+
math.log(self.max_dist / max_exact) *
|
| 186 |
+
(num_buckets - max_exact)).long()
|
| 187 |
+
rel_pos_large = torch.min(
|
| 188 |
+
rel_pos_large, torch.full_like(rel_pos_large, num_buckets - 1))
|
| 189 |
+
rel_buckets += torch.where(rel_pos < max_exact, rel_pos, rel_pos_large)
|
| 190 |
+
return rel_buckets
|
| 191 |
+
|
| 192 |
+
def init_weights(m):
|
| 193 |
+
if isinstance(m, T5LayerNorm):
|
| 194 |
+
nn.init.ones_(m.weight)
|
| 195 |
+
elif isinstance(m, T5FeedForward):
|
| 196 |
+
nn.init.normal_(m.gate[0].weight, std=m.dim**-0.5)
|
| 197 |
+
nn.init.normal_(m.fc1.weight, std=m.dim**-0.5)
|
| 198 |
+
nn.init.normal_(m.fc2.weight, std=m.dim_ffn**-0.5)
|
| 199 |
+
elif isinstance(m, T5Attention):
|
| 200 |
+
nn.init.normal_(m.q.weight, std=(m.dim * m.dim_attn)**-0.5)
|
| 201 |
+
nn.init.normal_(m.k.weight, std=m.dim**-0.5)
|
| 202 |
+
nn.init.normal_(m.v.weight, std=m.dim**-0.5)
|
| 203 |
+
nn.init.normal_(m.o.weight, std=(m.num_heads * m.dim_attn)**-0.5)
|
| 204 |
+
elif isinstance(m, T5RelativeEmbedding):
|
| 205 |
+
nn.init.normal_(
|
| 206 |
+
m.embedding.weight, std=(2 * m.num_buckets * m.num_heads)**-0.5)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class WanTextEncoder(torch.nn.Module):
|
| 210 |
+
|
| 211 |
+
def __init__(self,
|
| 212 |
+
vocab=256384,
|
| 213 |
+
dim=4096,
|
| 214 |
+
dim_attn=4096,
|
| 215 |
+
dim_ffn=10240,
|
| 216 |
+
num_heads=64,
|
| 217 |
+
num_layers=24,
|
| 218 |
+
num_buckets=32,
|
| 219 |
+
shared_pos=False,
|
| 220 |
+
dropout=0.1):
|
| 221 |
+
super(WanTextEncoder, self).__init__()
|
| 222 |
+
self.dim = dim
|
| 223 |
+
self.dim_attn = dim_attn
|
| 224 |
+
self.dim_ffn = dim_ffn
|
| 225 |
+
self.num_heads = num_heads
|
| 226 |
+
self.num_layers = num_layers
|
| 227 |
+
self.num_buckets = num_buckets
|
| 228 |
+
self.shared_pos = shared_pos
|
| 229 |
+
|
| 230 |
+
# layers
|
| 231 |
+
self.token_embedding = vocab if isinstance(vocab, nn.Embedding) \
|
| 232 |
+
else nn.Embedding(vocab, dim)
|
| 233 |
+
self.pos_embedding = T5RelativeEmbedding(
|
| 234 |
+
num_buckets, num_heads, bidirectional=True) if shared_pos else None
|
| 235 |
+
self.dropout = nn.Dropout(dropout)
|
| 236 |
+
self.blocks = nn.ModuleList([
|
| 237 |
+
T5SelfAttention(dim, dim_attn, dim_ffn, num_heads, num_buckets,
|
| 238 |
+
shared_pos, dropout) for _ in range(num_layers)
|
| 239 |
+
])
|
| 240 |
+
self.norm = T5LayerNorm(dim)
|
| 241 |
+
|
| 242 |
+
# initialize weights
|
| 243 |
+
self.apply(init_weights)
|
| 244 |
+
|
| 245 |
+
def forward(self, ids, mask=None):
|
| 246 |
+
x = self.token_embedding(ids)
|
| 247 |
+
x = self.dropout(x)
|
| 248 |
+
e = self.pos_embedding(x.size(1),
|
| 249 |
+
x.size(1)) if self.shared_pos else None
|
| 250 |
+
for block in self.blocks:
|
| 251 |
+
x = block(x, mask, pos_bias=e)
|
| 252 |
+
x = self.norm(x)
|
| 253 |
+
x = self.dropout(x)
|
| 254 |
+
return x
|
| 255 |
+
|
| 256 |
+
@staticmethod
|
| 257 |
+
def state_dict_converter():
|
| 258 |
+
return WanTextEncoderStateDictConverter()
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
class WanTextEncoderStateDictConverter:
|
| 262 |
+
def __init__(self):
|
| 263 |
+
pass
|
| 264 |
+
|
| 265 |
+
def from_diffusers(self, state_dict):
|
| 266 |
+
return state_dict
|
| 267 |
+
|
| 268 |
+
def from_civitai(self, state_dict):
|
| 269 |
+
return state_dict
|
OmniAvatar/models/wan_video_vae.py
ADDED
|
@@ -0,0 +1,938 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from einops import rearrange, repeat
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
from tqdm import tqdm
|
| 7 |
+
|
| 8 |
+
CACHE_T = 2
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def check_is_instance(model, module_class):
|
| 12 |
+
if isinstance(model, module_class):
|
| 13 |
+
return True
|
| 14 |
+
if hasattr(model, "module") and isinstance(model.module, module_class):
|
| 15 |
+
return True
|
| 16 |
+
return False
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def block_causal_mask(x, block_size):
|
| 20 |
+
# params
|
| 21 |
+
b, n, s, _, device = *x.size(), x.device
|
| 22 |
+
assert s % block_size == 0
|
| 23 |
+
num_blocks = s // block_size
|
| 24 |
+
|
| 25 |
+
# build mask
|
| 26 |
+
mask = torch.zeros(b, n, s, s, dtype=torch.bool, device=device)
|
| 27 |
+
for i in range(num_blocks):
|
| 28 |
+
mask[:, :,
|
| 29 |
+
i * block_size:(i + 1) * block_size, :(i + 1) * block_size] = 1
|
| 30 |
+
return mask
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class CausalConv3d(nn.Conv3d):
|
| 34 |
+
"""
|
| 35 |
+
Causal 3d convolusion.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
def __init__(self, *args, **kwargs):
|
| 39 |
+
super().__init__(*args, **kwargs)
|
| 40 |
+
self._padding = (self.padding[2], self.padding[2], self.padding[1],
|
| 41 |
+
self.padding[1], 2 * self.padding[0], 0)
|
| 42 |
+
self.padding = (0, 0, 0)
|
| 43 |
+
|
| 44 |
+
def forward(self, x, cache_x=None):
|
| 45 |
+
padding = list(self._padding)
|
| 46 |
+
if cache_x is not None and self._padding[4] > 0:
|
| 47 |
+
cache_x = cache_x.to(x.device)
|
| 48 |
+
x = torch.cat([cache_x, x], dim=2)
|
| 49 |
+
padding[4] -= cache_x.shape[2]
|
| 50 |
+
x = F.pad(x, padding)
|
| 51 |
+
|
| 52 |
+
return super().forward(x)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class RMS_norm(nn.Module):
|
| 56 |
+
|
| 57 |
+
def __init__(self, dim, channel_first=True, images=True, bias=False):
|
| 58 |
+
super().__init__()
|
| 59 |
+
broadcastable_dims = (1, 1, 1) if not images else (1, 1)
|
| 60 |
+
shape = (dim, *broadcastable_dims) if channel_first else (dim,)
|
| 61 |
+
|
| 62 |
+
self.channel_first = channel_first
|
| 63 |
+
self.scale = dim**0.5
|
| 64 |
+
self.gamma = nn.Parameter(torch.ones(shape))
|
| 65 |
+
self.bias = nn.Parameter(torch.zeros(shape)) if bias else 0.
|
| 66 |
+
|
| 67 |
+
def forward(self, x):
|
| 68 |
+
return F.normalize(
|
| 69 |
+
x, dim=(1 if self.channel_first else
|
| 70 |
+
-1)) * self.scale * self.gamma + self.bias
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class Upsample(nn.Upsample):
|
| 74 |
+
|
| 75 |
+
def forward(self, x):
|
| 76 |
+
"""
|
| 77 |
+
Fix bfloat16 support for nearest neighbor interpolation.
|
| 78 |
+
"""
|
| 79 |
+
return super().forward(x.float()).type_as(x)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class Resample(nn.Module):
|
| 83 |
+
|
| 84 |
+
def __init__(self, dim, mode):
|
| 85 |
+
assert mode in ('none', 'upsample2d', 'upsample3d', 'downsample2d',
|
| 86 |
+
'downsample3d')
|
| 87 |
+
super().__init__()
|
| 88 |
+
self.dim = dim
|
| 89 |
+
self.mode = mode
|
| 90 |
+
|
| 91 |
+
# layers
|
| 92 |
+
if mode == 'upsample2d':
|
| 93 |
+
self.resample = nn.Sequential(
|
| 94 |
+
Upsample(scale_factor=(2., 2.), mode='nearest-exact'),
|
| 95 |
+
nn.Conv2d(dim, dim // 2, 3, padding=1))
|
| 96 |
+
elif mode == 'upsample3d':
|
| 97 |
+
self.resample = nn.Sequential(
|
| 98 |
+
Upsample(scale_factor=(2., 2.), mode='nearest-exact'),
|
| 99 |
+
nn.Conv2d(dim, dim // 2, 3, padding=1))
|
| 100 |
+
self.time_conv = CausalConv3d(dim,
|
| 101 |
+
dim * 2, (3, 1, 1),
|
| 102 |
+
padding=(1, 0, 0))
|
| 103 |
+
|
| 104 |
+
elif mode == 'downsample2d':
|
| 105 |
+
self.resample = nn.Sequential(
|
| 106 |
+
nn.ZeroPad2d((0, 1, 0, 1)),
|
| 107 |
+
nn.Conv2d(dim, dim, 3, stride=(2, 2)))
|
| 108 |
+
elif mode == 'downsample3d':
|
| 109 |
+
self.resample = nn.Sequential(
|
| 110 |
+
nn.ZeroPad2d((0, 1, 0, 1)),
|
| 111 |
+
nn.Conv2d(dim, dim, 3, stride=(2, 2)))
|
| 112 |
+
self.time_conv = CausalConv3d(dim,
|
| 113 |
+
dim, (3, 1, 1),
|
| 114 |
+
stride=(2, 1, 1),
|
| 115 |
+
padding=(0, 0, 0))
|
| 116 |
+
|
| 117 |
+
else:
|
| 118 |
+
self.resample = nn.Identity()
|
| 119 |
+
|
| 120 |
+
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
| 121 |
+
b, c, t, h, w = x.size()
|
| 122 |
+
if self.mode == 'upsample3d':
|
| 123 |
+
if feat_cache is not None:
|
| 124 |
+
idx = feat_idx[0]
|
| 125 |
+
if feat_cache[idx] is None:
|
| 126 |
+
feat_cache[idx] = 'Rep'
|
| 127 |
+
feat_idx[0] += 1
|
| 128 |
+
else:
|
| 129 |
+
|
| 130 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 131 |
+
if cache_x.shape[2] < 2 and feat_cache[
|
| 132 |
+
idx] is not None and feat_cache[idx] != 'Rep':
|
| 133 |
+
# cache last frame of last two chunk
|
| 134 |
+
cache_x = torch.cat([
|
| 135 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
|
| 136 |
+
cache_x.device), cache_x
|
| 137 |
+
],
|
| 138 |
+
dim=2)
|
| 139 |
+
if cache_x.shape[2] < 2 and feat_cache[
|
| 140 |
+
idx] is not None and feat_cache[idx] == 'Rep':
|
| 141 |
+
cache_x = torch.cat([
|
| 142 |
+
torch.zeros_like(cache_x).to(cache_x.device),
|
| 143 |
+
cache_x
|
| 144 |
+
],
|
| 145 |
+
dim=2)
|
| 146 |
+
if feat_cache[idx] == 'Rep':
|
| 147 |
+
x = self.time_conv(x)
|
| 148 |
+
else:
|
| 149 |
+
x = self.time_conv(x, feat_cache[idx])
|
| 150 |
+
feat_cache[idx] = cache_x
|
| 151 |
+
feat_idx[0] += 1
|
| 152 |
+
|
| 153 |
+
x = x.reshape(b, 2, c, t, h, w)
|
| 154 |
+
x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]),
|
| 155 |
+
3)
|
| 156 |
+
x = x.reshape(b, c, t * 2, h, w)
|
| 157 |
+
t = x.shape[2]
|
| 158 |
+
x = rearrange(x, 'b c t h w -> (b t) c h w')
|
| 159 |
+
x = self.resample(x)
|
| 160 |
+
x = rearrange(x, '(b t) c h w -> b c t h w', t=t)
|
| 161 |
+
|
| 162 |
+
if self.mode == 'downsample3d':
|
| 163 |
+
if feat_cache is not None:
|
| 164 |
+
idx = feat_idx[0]
|
| 165 |
+
if feat_cache[idx] is None:
|
| 166 |
+
feat_cache[idx] = x.clone()
|
| 167 |
+
feat_idx[0] += 1
|
| 168 |
+
else:
|
| 169 |
+
cache_x = x[:, :, -1:, :, :].clone()
|
| 170 |
+
x = self.time_conv(
|
| 171 |
+
torch.cat([feat_cache[idx][:, :, -1:, :, :], x], 2))
|
| 172 |
+
feat_cache[idx] = cache_x
|
| 173 |
+
feat_idx[0] += 1
|
| 174 |
+
return x
|
| 175 |
+
|
| 176 |
+
def init_weight(self, conv):
|
| 177 |
+
conv_weight = conv.weight
|
| 178 |
+
nn.init.zeros_(conv_weight)
|
| 179 |
+
c1, c2, t, h, w = conv_weight.size()
|
| 180 |
+
one_matrix = torch.eye(c1, c2)
|
| 181 |
+
init_matrix = one_matrix
|
| 182 |
+
nn.init.zeros_(conv_weight)
|
| 183 |
+
conv_weight.data[:, :, 1, 0, 0] = init_matrix
|
| 184 |
+
conv.weight.data.copy_(conv_weight)
|
| 185 |
+
nn.init.zeros_(conv.bias.data)
|
| 186 |
+
|
| 187 |
+
def init_weight2(self, conv):
|
| 188 |
+
conv_weight = conv.weight.data
|
| 189 |
+
nn.init.zeros_(conv_weight)
|
| 190 |
+
c1, c2, t, h, w = conv_weight.size()
|
| 191 |
+
init_matrix = torch.eye(c1 // 2, c2)
|
| 192 |
+
conv_weight[:c1 // 2, :, -1, 0, 0] = init_matrix
|
| 193 |
+
conv_weight[c1 // 2:, :, -1, 0, 0] = init_matrix
|
| 194 |
+
conv.weight.data.copy_(conv_weight)
|
| 195 |
+
nn.init.zeros_(conv.bias.data)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class ResidualBlock(nn.Module):
|
| 199 |
+
|
| 200 |
+
def __init__(self, in_dim, out_dim, dropout=0.0):
|
| 201 |
+
super().__init__()
|
| 202 |
+
self.in_dim = in_dim
|
| 203 |
+
self.out_dim = out_dim
|
| 204 |
+
|
| 205 |
+
# layers
|
| 206 |
+
self.residual = nn.Sequential(
|
| 207 |
+
RMS_norm(in_dim, images=False), nn.SiLU(),
|
| 208 |
+
CausalConv3d(in_dim, out_dim, 3, padding=1),
|
| 209 |
+
RMS_norm(out_dim, images=False), nn.SiLU(), nn.Dropout(dropout),
|
| 210 |
+
CausalConv3d(out_dim, out_dim, 3, padding=1))
|
| 211 |
+
self.shortcut = CausalConv3d(in_dim, out_dim, 1) \
|
| 212 |
+
if in_dim != out_dim else nn.Identity()
|
| 213 |
+
|
| 214 |
+
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
| 215 |
+
h = self.shortcut(x)
|
| 216 |
+
for layer in self.residual:
|
| 217 |
+
if check_is_instance(layer, CausalConv3d) and feat_cache is not None:
|
| 218 |
+
idx = feat_idx[0]
|
| 219 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 220 |
+
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
|
| 221 |
+
# cache last frame of last two chunk
|
| 222 |
+
cache_x = torch.cat([
|
| 223 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
|
| 224 |
+
cache_x.device), cache_x
|
| 225 |
+
],
|
| 226 |
+
dim=2)
|
| 227 |
+
x = layer(x, feat_cache[idx])
|
| 228 |
+
feat_cache[idx] = cache_x
|
| 229 |
+
feat_idx[0] += 1
|
| 230 |
+
else:
|
| 231 |
+
x = layer(x)
|
| 232 |
+
return x + h
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
class AttentionBlock(nn.Module):
|
| 236 |
+
"""
|
| 237 |
+
Causal self-attention with a single head.
|
| 238 |
+
"""
|
| 239 |
+
|
| 240 |
+
def __init__(self, dim):
|
| 241 |
+
super().__init__()
|
| 242 |
+
self.dim = dim
|
| 243 |
+
|
| 244 |
+
# layers
|
| 245 |
+
self.norm = RMS_norm(dim)
|
| 246 |
+
self.to_qkv = nn.Conv2d(dim, dim * 3, 1)
|
| 247 |
+
self.proj = nn.Conv2d(dim, dim, 1)
|
| 248 |
+
|
| 249 |
+
# zero out the last layer params
|
| 250 |
+
nn.init.zeros_(self.proj.weight)
|
| 251 |
+
|
| 252 |
+
def forward(self, x):
|
| 253 |
+
identity = x
|
| 254 |
+
b, c, t, h, w = x.size()
|
| 255 |
+
x = rearrange(x, 'b c t h w -> (b t) c h w')
|
| 256 |
+
x = self.norm(x)
|
| 257 |
+
# compute query, key, value
|
| 258 |
+
q, k, v = self.to_qkv(x).reshape(b * t, 1, c * 3, -1).permute(
|
| 259 |
+
0, 1, 3, 2).contiguous().chunk(3, dim=-1)
|
| 260 |
+
|
| 261 |
+
# apply attention
|
| 262 |
+
x = F.scaled_dot_product_attention(
|
| 263 |
+
q,
|
| 264 |
+
k,
|
| 265 |
+
v,
|
| 266 |
+
#attn_mask=block_causal_mask(q, block_size=h * w)
|
| 267 |
+
)
|
| 268 |
+
x = x.squeeze(1).permute(0, 2, 1).reshape(b * t, c, h, w)
|
| 269 |
+
|
| 270 |
+
# output
|
| 271 |
+
x = self.proj(x)
|
| 272 |
+
x = rearrange(x, '(b t) c h w-> b c t h w', t=t)
|
| 273 |
+
return x + identity
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
class Encoder3d(nn.Module):
|
| 277 |
+
|
| 278 |
+
def __init__(self,
|
| 279 |
+
dim=128,
|
| 280 |
+
z_dim=4,
|
| 281 |
+
dim_mult=[1, 2, 4, 4],
|
| 282 |
+
num_res_blocks=2,
|
| 283 |
+
attn_scales=[],
|
| 284 |
+
temperal_downsample=[True, True, False],
|
| 285 |
+
dropout=0.0):
|
| 286 |
+
super().__init__()
|
| 287 |
+
self.dim = dim
|
| 288 |
+
self.z_dim = z_dim
|
| 289 |
+
self.dim_mult = dim_mult
|
| 290 |
+
self.num_res_blocks = num_res_blocks
|
| 291 |
+
self.attn_scales = attn_scales
|
| 292 |
+
self.temperal_downsample = temperal_downsample
|
| 293 |
+
|
| 294 |
+
# dimensions
|
| 295 |
+
dims = [dim * u for u in [1] + dim_mult]
|
| 296 |
+
scale = 1.0
|
| 297 |
+
|
| 298 |
+
# init block
|
| 299 |
+
self.conv1 = CausalConv3d(3, dims[0], 3, padding=1)
|
| 300 |
+
|
| 301 |
+
# downsample blocks
|
| 302 |
+
downsamples = []
|
| 303 |
+
for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
|
| 304 |
+
# residual (+attention) blocks
|
| 305 |
+
for _ in range(num_res_blocks):
|
| 306 |
+
downsamples.append(ResidualBlock(in_dim, out_dim, dropout))
|
| 307 |
+
if scale in attn_scales:
|
| 308 |
+
downsamples.append(AttentionBlock(out_dim))
|
| 309 |
+
in_dim = out_dim
|
| 310 |
+
|
| 311 |
+
# downsample block
|
| 312 |
+
if i != len(dim_mult) - 1:
|
| 313 |
+
mode = 'downsample3d' if temperal_downsample[
|
| 314 |
+
i] else 'downsample2d'
|
| 315 |
+
downsamples.append(Resample(out_dim, mode=mode))
|
| 316 |
+
scale /= 2.0
|
| 317 |
+
self.downsamples = nn.Sequential(*downsamples)
|
| 318 |
+
|
| 319 |
+
# middle blocks
|
| 320 |
+
self.middle = nn.Sequential(ResidualBlock(out_dim, out_dim, dropout),
|
| 321 |
+
AttentionBlock(out_dim),
|
| 322 |
+
ResidualBlock(out_dim, out_dim, dropout))
|
| 323 |
+
|
| 324 |
+
# output blocks
|
| 325 |
+
self.head = nn.Sequential(RMS_norm(out_dim, images=False), nn.SiLU(),
|
| 326 |
+
CausalConv3d(out_dim, z_dim, 3, padding=1))
|
| 327 |
+
|
| 328 |
+
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
| 329 |
+
if feat_cache is not None:
|
| 330 |
+
idx = feat_idx[0]
|
| 331 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 332 |
+
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
|
| 333 |
+
# cache last frame of last two chunk
|
| 334 |
+
cache_x = torch.cat([
|
| 335 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
|
| 336 |
+
cache_x.device), cache_x
|
| 337 |
+
],
|
| 338 |
+
dim=2)
|
| 339 |
+
x = self.conv1(x, feat_cache[idx])
|
| 340 |
+
feat_cache[idx] = cache_x
|
| 341 |
+
feat_idx[0] += 1
|
| 342 |
+
else:
|
| 343 |
+
x = self.conv1(x)
|
| 344 |
+
|
| 345 |
+
## downsamples
|
| 346 |
+
for layer in self.downsamples:
|
| 347 |
+
if feat_cache is not None:
|
| 348 |
+
x = layer(x, feat_cache, feat_idx)
|
| 349 |
+
else:
|
| 350 |
+
x = layer(x)
|
| 351 |
+
|
| 352 |
+
## middle
|
| 353 |
+
for layer in self.middle:
|
| 354 |
+
if check_is_instance(layer, ResidualBlock) and feat_cache is not None:
|
| 355 |
+
x = layer(x, feat_cache, feat_idx)
|
| 356 |
+
else:
|
| 357 |
+
x = layer(x)
|
| 358 |
+
|
| 359 |
+
## head
|
| 360 |
+
for layer in self.head:
|
| 361 |
+
if check_is_instance(layer, CausalConv3d) and feat_cache is not None:
|
| 362 |
+
idx = feat_idx[0]
|
| 363 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 364 |
+
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
|
| 365 |
+
# cache last frame of last two chunk
|
| 366 |
+
cache_x = torch.cat([
|
| 367 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
|
| 368 |
+
cache_x.device), cache_x
|
| 369 |
+
],
|
| 370 |
+
dim=2)
|
| 371 |
+
x = layer(x, feat_cache[idx])
|
| 372 |
+
feat_cache[idx] = cache_x
|
| 373 |
+
feat_idx[0] += 1
|
| 374 |
+
else:
|
| 375 |
+
x = layer(x)
|
| 376 |
+
return x
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
class Decoder3d(nn.Module):
|
| 380 |
+
|
| 381 |
+
def __init__(self,
|
| 382 |
+
dim=128,
|
| 383 |
+
z_dim=4,
|
| 384 |
+
dim_mult=[1, 2, 4, 4],
|
| 385 |
+
num_res_blocks=2,
|
| 386 |
+
attn_scales=[],
|
| 387 |
+
temperal_upsample=[False, True, True],
|
| 388 |
+
dropout=0.0):
|
| 389 |
+
super().__init__()
|
| 390 |
+
self.dim = dim
|
| 391 |
+
self.z_dim = z_dim
|
| 392 |
+
self.dim_mult = dim_mult
|
| 393 |
+
self.num_res_blocks = num_res_blocks
|
| 394 |
+
self.attn_scales = attn_scales
|
| 395 |
+
self.temperal_upsample = temperal_upsample
|
| 396 |
+
|
| 397 |
+
# dimensions
|
| 398 |
+
dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
|
| 399 |
+
scale = 1.0 / 2**(len(dim_mult) - 2)
|
| 400 |
+
|
| 401 |
+
# init block
|
| 402 |
+
self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
|
| 403 |
+
|
| 404 |
+
# middle blocks
|
| 405 |
+
self.middle = nn.Sequential(ResidualBlock(dims[0], dims[0], dropout),
|
| 406 |
+
AttentionBlock(dims[0]),
|
| 407 |
+
ResidualBlock(dims[0], dims[0], dropout))
|
| 408 |
+
|
| 409 |
+
# upsample blocks
|
| 410 |
+
upsamples = []
|
| 411 |
+
for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
|
| 412 |
+
# residual (+attention) blocks
|
| 413 |
+
if i == 1 or i == 2 or i == 3:
|
| 414 |
+
in_dim = in_dim // 2
|
| 415 |
+
for _ in range(num_res_blocks + 1):
|
| 416 |
+
upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
|
| 417 |
+
if scale in attn_scales:
|
| 418 |
+
upsamples.append(AttentionBlock(out_dim))
|
| 419 |
+
in_dim = out_dim
|
| 420 |
+
|
| 421 |
+
# upsample block
|
| 422 |
+
if i != len(dim_mult) - 1:
|
| 423 |
+
mode = 'upsample3d' if temperal_upsample[i] else 'upsample2d'
|
| 424 |
+
upsamples.append(Resample(out_dim, mode=mode))
|
| 425 |
+
scale *= 2.0
|
| 426 |
+
self.upsamples = nn.Sequential(*upsamples)
|
| 427 |
+
|
| 428 |
+
# output blocks
|
| 429 |
+
self.head = nn.Sequential(RMS_norm(out_dim, images=False), nn.SiLU(),
|
| 430 |
+
CausalConv3d(out_dim, 3, 3, padding=1))
|
| 431 |
+
|
| 432 |
+
# Gradient checkpointing support (default off, enable for aux loss training)
|
| 433 |
+
self.gradient_checkpointing = False
|
| 434 |
+
|
| 435 |
+
def forward(self, x, feat_cache=None, feat_idx=[0]):
|
| 436 |
+
if torch.is_grad_enabled() and self.gradient_checkpointing:
|
| 437 |
+
def create_custom_forward(module):
|
| 438 |
+
def custom_forward(*inputs):
|
| 439 |
+
return module._decode(*inputs)
|
| 440 |
+
return custom_forward
|
| 441 |
+
return torch.utils.checkpoint.checkpoint(
|
| 442 |
+
create_custom_forward(self),
|
| 443 |
+
x, feat_cache, feat_idx,
|
| 444 |
+
use_reentrant=False
|
| 445 |
+
)
|
| 446 |
+
else:
|
| 447 |
+
return self._decode(x, feat_cache, feat_idx)
|
| 448 |
+
|
| 449 |
+
def _decode(self, x, in_cache=None, feat_idx=[0]):
|
| 450 |
+
# Copy input cache to prevent mutations during gradient checkpoint recomputation
|
| 451 |
+
feat_cache = in_cache.copy() if in_cache is not None else None
|
| 452 |
+
|
| 453 |
+
## conv1
|
| 454 |
+
if feat_cache is not None:
|
| 455 |
+
idx = feat_idx[0]
|
| 456 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 457 |
+
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
|
| 458 |
+
# cache last frame of last two chunk
|
| 459 |
+
cache_x = torch.cat([
|
| 460 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
|
| 461 |
+
cache_x.device), cache_x
|
| 462 |
+
],
|
| 463 |
+
dim=2)
|
| 464 |
+
x = self.conv1(x, feat_cache[idx])
|
| 465 |
+
feat_cache[idx] = cache_x
|
| 466 |
+
feat_idx[0] += 1
|
| 467 |
+
else:
|
| 468 |
+
x = self.conv1(x)
|
| 469 |
+
|
| 470 |
+
## middle
|
| 471 |
+
for layer in self.middle:
|
| 472 |
+
if check_is_instance(layer, ResidualBlock) and feat_cache is not None:
|
| 473 |
+
x = layer(x, feat_cache, feat_idx)
|
| 474 |
+
else:
|
| 475 |
+
x = layer(x)
|
| 476 |
+
|
| 477 |
+
## upsamples
|
| 478 |
+
for layer in self.upsamples:
|
| 479 |
+
if feat_cache is not None:
|
| 480 |
+
x = layer(x, feat_cache, feat_idx)
|
| 481 |
+
else:
|
| 482 |
+
x = layer(x)
|
| 483 |
+
|
| 484 |
+
## head
|
| 485 |
+
for layer in self.head:
|
| 486 |
+
if check_is_instance(layer, CausalConv3d) and feat_cache is not None:
|
| 487 |
+
idx = feat_idx[0]
|
| 488 |
+
cache_x = x[:, :, -CACHE_T:, :, :].clone()
|
| 489 |
+
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
|
| 490 |
+
# cache last frame of last two chunk
|
| 491 |
+
cache_x = torch.cat([
|
| 492 |
+
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
|
| 493 |
+
cache_x.device), cache_x
|
| 494 |
+
],
|
| 495 |
+
dim=2)
|
| 496 |
+
x = layer(x, feat_cache[idx])
|
| 497 |
+
feat_cache[idx] = cache_x
|
| 498 |
+
feat_idx[0] += 1
|
| 499 |
+
else:
|
| 500 |
+
x = layer(x)
|
| 501 |
+
|
| 502 |
+
# Reset index counter for consistent state across checkpoint recomputations
|
| 503 |
+
feat_idx[0] = 0
|
| 504 |
+
return x, feat_cache
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
def count_conv3d(model):
|
| 508 |
+
count = 0
|
| 509 |
+
for m in model.modules():
|
| 510 |
+
if check_is_instance(m, CausalConv3d):
|
| 511 |
+
count += 1
|
| 512 |
+
return count
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
class VideoVAE_(nn.Module):
|
| 516 |
+
|
| 517 |
+
def __init__(self,
|
| 518 |
+
dim=96,
|
| 519 |
+
z_dim=16,
|
| 520 |
+
dim_mult=[1, 2, 4, 4],
|
| 521 |
+
num_res_blocks=2,
|
| 522 |
+
attn_scales=[],
|
| 523 |
+
temperal_downsample=[False, True, True],
|
| 524 |
+
dropout=0.0):
|
| 525 |
+
super().__init__()
|
| 526 |
+
self.dim = dim
|
| 527 |
+
self.z_dim = z_dim
|
| 528 |
+
self.dim_mult = dim_mult
|
| 529 |
+
self.num_res_blocks = num_res_blocks
|
| 530 |
+
self.attn_scales = attn_scales
|
| 531 |
+
self.temperal_downsample = temperal_downsample
|
| 532 |
+
self.temperal_upsample = temperal_downsample[::-1]
|
| 533 |
+
|
| 534 |
+
# modules
|
| 535 |
+
self.encoder = Encoder3d(dim, z_dim * 2, dim_mult, num_res_blocks,
|
| 536 |
+
attn_scales, self.temperal_downsample, dropout)
|
| 537 |
+
self.conv1 = CausalConv3d(z_dim * 2, z_dim * 2, 1)
|
| 538 |
+
self.conv2 = CausalConv3d(z_dim, z_dim, 1)
|
| 539 |
+
self.decoder = Decoder3d(dim, z_dim, dim_mult, num_res_blocks,
|
| 540 |
+
attn_scales, self.temperal_upsample, dropout)
|
| 541 |
+
|
| 542 |
+
def forward(self, x):
|
| 543 |
+
mu, log_var = self.encode(x)
|
| 544 |
+
z = self.reparameterize(mu, log_var)
|
| 545 |
+
x_recon = self.decode(z)
|
| 546 |
+
return x_recon, mu, log_var
|
| 547 |
+
|
| 548 |
+
def encode(self, x, scale, keep_cache: bool = False):
|
| 549 |
+
"""Encode video frames to latents.
|
| 550 |
+
|
| 551 |
+
Args:
|
| 552 |
+
keep_cache: if True, do not call clear_cache() at start. The caller
|
| 553 |
+
must drive cache state across calls. When True, x is treated as
|
| 554 |
+
a single 4-frame chunk (or 1-frame if cache is empty).
|
| 555 |
+
"""
|
| 556 |
+
if not keep_cache:
|
| 557 |
+
self.clear_cache()
|
| 558 |
+
## cache
|
| 559 |
+
t = x.shape[2]
|
| 560 |
+
iter_ = 1 + (t - 1) // 4
|
| 561 |
+
|
| 562 |
+
for i in range(iter_):
|
| 563 |
+
self._enc_conv_idx = [0]
|
| 564 |
+
if i == 0:
|
| 565 |
+
out = self.encoder(x[:, :, :1, :, :],
|
| 566 |
+
feat_cache=self._enc_feat_map,
|
| 567 |
+
feat_idx=self._enc_conv_idx)
|
| 568 |
+
else:
|
| 569 |
+
out_ = self.encoder(x[:, :, 1 + 4 * (i - 1):1 + 4 * i, :, :],
|
| 570 |
+
feat_cache=self._enc_feat_map,
|
| 571 |
+
feat_idx=self._enc_conv_idx)
|
| 572 |
+
out = torch.cat([out, out_], 2)
|
| 573 |
+
mu, log_var = self.conv1(out).chunk(2, dim=1)
|
| 574 |
+
if isinstance(scale[0], torch.Tensor):
|
| 575 |
+
scale = [s.to(dtype=mu.dtype, device=mu.device) for s in scale]
|
| 576 |
+
mu = (mu - scale[0].view(1, self.z_dim, 1, 1, 1)) * scale[1].view(
|
| 577 |
+
1, self.z_dim, 1, 1, 1)
|
| 578 |
+
else:
|
| 579 |
+
scale = scale.to(dtype=mu.dtype, device=mu.device)
|
| 580 |
+
mu = (mu - scale[0]) * scale[1]
|
| 581 |
+
return mu
|
| 582 |
+
|
| 583 |
+
def streaming_encode_step(self, x_chunk, scale):
|
| 584 |
+
"""Encode one chunk of video frames using the live feat_cache state.
|
| 585 |
+
|
| 586 |
+
Caller invariant:
|
| 587 |
+
- First call after reset_encode_cache(): x_chunk has 1 frame
|
| 588 |
+
- Subsequent calls: x_chunk has 4 frames each
|
| 589 |
+
Output: 1 latent frame per call.
|
| 590 |
+
|
| 591 |
+
Caller is responsible for invoking clear_cache() (or
|
| 592 |
+
clear_encode_cache()) once before the first chunk; cache is preserved
|
| 593 |
+
across calls.
|
| 594 |
+
"""
|
| 595 |
+
self._enc_conv_idx = [0]
|
| 596 |
+
out = self.encoder(x_chunk, feat_cache=self._enc_feat_map,
|
| 597 |
+
feat_idx=self._enc_conv_idx)
|
| 598 |
+
mu, log_var = self.conv1(out).chunk(2, dim=1)
|
| 599 |
+
if isinstance(scale[0], torch.Tensor):
|
| 600 |
+
scale = [s.to(dtype=mu.dtype, device=mu.device) for s in scale]
|
| 601 |
+
mu = (mu - scale[0].view(1, self.z_dim, 1, 1, 1)) * scale[1].view(
|
| 602 |
+
1, self.z_dim, 1, 1, 1)
|
| 603 |
+
else:
|
| 604 |
+
scale = scale.to(dtype=mu.dtype, device=mu.device)
|
| 605 |
+
mu = (mu - scale[0]) * scale[1]
|
| 606 |
+
return mu
|
| 607 |
+
|
| 608 |
+
def decode(self, z, scale, keep_cache: bool = False):
|
| 609 |
+
"""Decode latents to video frames.
|
| 610 |
+
|
| 611 |
+
Args:
|
| 612 |
+
keep_cache: if True, do not call clear_cache() at start. Use for
|
| 613 |
+
streaming inference where the caller drives state across calls.
|
| 614 |
+
The caller is responsible for invoking clear_cache() before the
|
| 615 |
+
first chunk and not in between.
|
| 616 |
+
"""
|
| 617 |
+
if not keep_cache:
|
| 618 |
+
self.clear_cache()
|
| 619 |
+
# z: [b,c,t,h,w]
|
| 620 |
+
if isinstance(scale[0], torch.Tensor):
|
| 621 |
+
scale = [s.to(dtype=z.dtype, device=z.device) for s in scale]
|
| 622 |
+
z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
|
| 623 |
+
1, self.z_dim, 1, 1, 1)
|
| 624 |
+
else:
|
| 625 |
+
scale = scale.to(dtype=z.dtype, device=z.device)
|
| 626 |
+
z = z / scale[1] + scale[0]
|
| 627 |
+
iter_ = z.shape[2]
|
| 628 |
+
x = self.conv2(z)
|
| 629 |
+
for i in range(iter_):
|
| 630 |
+
self._conv_idx = [0]
|
| 631 |
+
if i == 0:
|
| 632 |
+
out, self._feat_map = self.decoder(
|
| 633 |
+
x[:, :, i:i + 1, :, :],
|
| 634 |
+
feat_cache=self._feat_map,
|
| 635 |
+
feat_idx=self._conv_idx)
|
| 636 |
+
else:
|
| 637 |
+
out_, self._feat_map = self.decoder(
|
| 638 |
+
x[:, :, i:i + 1, :, :],
|
| 639 |
+
feat_cache=self._feat_map,
|
| 640 |
+
feat_idx=self._conv_idx)
|
| 641 |
+
out = torch.cat([out, out_], 2)
|
| 642 |
+
return out
|
| 643 |
+
|
| 644 |
+
def reparameterize(self, mu, log_var):
|
| 645 |
+
std = torch.exp(0.5 * log_var)
|
| 646 |
+
eps = torch.randn_like(std)
|
| 647 |
+
return eps * std + mu
|
| 648 |
+
|
| 649 |
+
def sample(self, imgs, deterministic=False):
|
| 650 |
+
mu, log_var = self.encode(imgs)
|
| 651 |
+
if deterministic:
|
| 652 |
+
return mu
|
| 653 |
+
std = torch.exp(0.5 * log_var.clamp(-30.0, 20.0))
|
| 654 |
+
return mu + std * torch.randn_like(std)
|
| 655 |
+
|
| 656 |
+
def clear_cache(self):
|
| 657 |
+
self._conv_num = count_conv3d(self.decoder)
|
| 658 |
+
self._conv_idx = [0]
|
| 659 |
+
self._feat_map = [None] * self._conv_num
|
| 660 |
+
# cache encode
|
| 661 |
+
self._enc_conv_num = count_conv3d(self.encoder)
|
| 662 |
+
self._enc_conv_idx = [0]
|
| 663 |
+
self._enc_feat_map = [None] * self._enc_conv_num
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
class WanVideoVAE(nn.Module):
|
| 667 |
+
|
| 668 |
+
def __init__(self, z_dim=16):
|
| 669 |
+
super().__init__()
|
| 670 |
+
|
| 671 |
+
mean = [
|
| 672 |
+
-0.7571, -0.7089, -0.9113, 0.1075, -0.1745, 0.9653, -0.1517, 1.5508,
|
| 673 |
+
0.4134, -0.0715, 0.5517, -0.3632, -0.1922, -0.9497, 0.2503, -0.2921
|
| 674 |
+
]
|
| 675 |
+
std = [
|
| 676 |
+
2.8184, 1.4541, 2.3275, 2.6558, 1.2196, 1.7708, 2.6052, 2.0743,
|
| 677 |
+
3.2687, 2.1526, 2.8652, 1.5579, 1.6382, 1.1253, 2.8251, 1.9160
|
| 678 |
+
]
|
| 679 |
+
self.mean = torch.tensor(mean)
|
| 680 |
+
self.std = torch.tensor(std)
|
| 681 |
+
self.scale = [self.mean, 1.0 / self.std]
|
| 682 |
+
|
| 683 |
+
# init model
|
| 684 |
+
self.model = VideoVAE_(z_dim=z_dim).eval().requires_grad_(False)
|
| 685 |
+
self.upsampling_factor = 8
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
def build_1d_mask(self, length, left_bound, right_bound, border_width):
|
| 689 |
+
x = torch.ones((length,))
|
| 690 |
+
if not left_bound:
|
| 691 |
+
x[:border_width] = (torch.arange(border_width) + 1) / border_width
|
| 692 |
+
if not right_bound:
|
| 693 |
+
x[-border_width:] = torch.flip((torch.arange(border_width) + 1) / border_width, dims=(0,))
|
| 694 |
+
return x
|
| 695 |
+
|
| 696 |
+
|
| 697 |
+
def build_mask(self, data, is_bound, border_width):
|
| 698 |
+
_, _, _, H, W = data.shape
|
| 699 |
+
h = self.build_1d_mask(H, is_bound[0], is_bound[1], border_width[0])
|
| 700 |
+
w = self.build_1d_mask(W, is_bound[2], is_bound[3], border_width[1])
|
| 701 |
+
|
| 702 |
+
h = repeat(h, "H -> H W", H=H, W=W)
|
| 703 |
+
w = repeat(w, "W -> H W", H=H, W=W)
|
| 704 |
+
|
| 705 |
+
mask = torch.stack([h, w]).min(dim=0).values
|
| 706 |
+
mask = rearrange(mask, "H W -> 1 1 1 H W")
|
| 707 |
+
return mask
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
def tiled_decode(self, hidden_states, device, tile_size, tile_stride):
|
| 711 |
+
_, _, T, H, W = hidden_states.shape
|
| 712 |
+
size_h, size_w = tile_size
|
| 713 |
+
stride_h, stride_w = tile_stride
|
| 714 |
+
|
| 715 |
+
# Split tasks
|
| 716 |
+
tasks = []
|
| 717 |
+
for h in range(0, H, stride_h):
|
| 718 |
+
if (h-stride_h >= 0 and h-stride_h+size_h >= H): continue
|
| 719 |
+
for w in range(0, W, stride_w):
|
| 720 |
+
if (w-stride_w >= 0 and w-stride_w+size_w >= W): continue
|
| 721 |
+
h_, w_ = h + size_h, w + size_w
|
| 722 |
+
tasks.append((h, h_, w, w_))
|
| 723 |
+
|
| 724 |
+
data_device = "cpu"
|
| 725 |
+
computation_device = device
|
| 726 |
+
|
| 727 |
+
out_T = T * 4 - 3
|
| 728 |
+
weight = torch.zeros((1, 1, out_T, H * self.upsampling_factor, W * self.upsampling_factor), dtype=hidden_states.dtype, device=data_device)
|
| 729 |
+
values = torch.zeros((1, 3, out_T, H * self.upsampling_factor, W * self.upsampling_factor), dtype=hidden_states.dtype, device=data_device)
|
| 730 |
+
|
| 731 |
+
for h, h_, w, w_ in tasks:
|
| 732 |
+
hidden_states_batch = hidden_states[:, :, :, h:h_, w:w_].to(computation_device)
|
| 733 |
+
hidden_states_batch = self.model.decode(hidden_states_batch, self.scale).to(data_device)
|
| 734 |
+
|
| 735 |
+
mask = self.build_mask(
|
| 736 |
+
hidden_states_batch,
|
| 737 |
+
is_bound=(h==0, h_>=H, w==0, w_>=W),
|
| 738 |
+
border_width=((size_h - stride_h) * self.upsampling_factor, (size_w - stride_w) * self.upsampling_factor)
|
| 739 |
+
).to(dtype=hidden_states.dtype, device=data_device)
|
| 740 |
+
|
| 741 |
+
target_h = h * self.upsampling_factor
|
| 742 |
+
target_w = w * self.upsampling_factor
|
| 743 |
+
values[
|
| 744 |
+
:,
|
| 745 |
+
:,
|
| 746 |
+
:,
|
| 747 |
+
target_h:target_h + hidden_states_batch.shape[3],
|
| 748 |
+
target_w:target_w + hidden_states_batch.shape[4],
|
| 749 |
+
] += hidden_states_batch * mask
|
| 750 |
+
weight[
|
| 751 |
+
:,
|
| 752 |
+
:,
|
| 753 |
+
:,
|
| 754 |
+
target_h: target_h + hidden_states_batch.shape[3],
|
| 755 |
+
target_w: target_w + hidden_states_batch.shape[4],
|
| 756 |
+
] += mask
|
| 757 |
+
values = values / weight
|
| 758 |
+
values = values.clamp_(-1, 1)
|
| 759 |
+
return values
|
| 760 |
+
|
| 761 |
+
|
| 762 |
+
def tiled_encode(self, video, device, tile_size, tile_stride):
|
| 763 |
+
_, _, T, H, W = video.shape
|
| 764 |
+
size_h, size_w = tile_size
|
| 765 |
+
stride_h, stride_w = tile_stride
|
| 766 |
+
|
| 767 |
+
# Split tasks
|
| 768 |
+
tasks = []
|
| 769 |
+
for h in range(0, H, stride_h):
|
| 770 |
+
if (h-stride_h >= 0 and h-stride_h+size_h >= H): continue
|
| 771 |
+
for w in range(0, W, stride_w):
|
| 772 |
+
if (w-stride_w >= 0 and w-stride_w+size_w >= W): continue
|
| 773 |
+
h_, w_ = h + size_h, w + size_w
|
| 774 |
+
tasks.append((h, h_, w, w_))
|
| 775 |
+
|
| 776 |
+
data_device = "cpu"
|
| 777 |
+
computation_device = device
|
| 778 |
+
|
| 779 |
+
out_T = (T + 3) // 4
|
| 780 |
+
weight = torch.zeros((1, 1, out_T, H // self.upsampling_factor, W // self.upsampling_factor), dtype=video.dtype, device=data_device)
|
| 781 |
+
values = torch.zeros((1, 16, out_T, H // self.upsampling_factor, W // self.upsampling_factor), dtype=video.dtype, device=data_device)
|
| 782 |
+
|
| 783 |
+
for h, h_, w, w_ in tasks:
|
| 784 |
+
hidden_states_batch = video[:, :, :, h:h_, w:w_].to(computation_device)
|
| 785 |
+
hidden_states_batch = self.model.encode(hidden_states_batch, self.scale).to(data_device)
|
| 786 |
+
|
| 787 |
+
mask = self.build_mask(
|
| 788 |
+
hidden_states_batch,
|
| 789 |
+
is_bound=(h==0, h_>=H, w==0, w_>=W),
|
| 790 |
+
border_width=((size_h - stride_h) // self.upsampling_factor, (size_w - stride_w) // self.upsampling_factor)
|
| 791 |
+
).to(dtype=video.dtype, device=data_device)
|
| 792 |
+
|
| 793 |
+
target_h = h // self.upsampling_factor
|
| 794 |
+
target_w = w // self.upsampling_factor
|
| 795 |
+
values[
|
| 796 |
+
:,
|
| 797 |
+
:,
|
| 798 |
+
:,
|
| 799 |
+
target_h:target_h + hidden_states_batch.shape[3],
|
| 800 |
+
target_w:target_w + hidden_states_batch.shape[4],
|
| 801 |
+
] += hidden_states_batch * mask
|
| 802 |
+
weight[
|
| 803 |
+
:,
|
| 804 |
+
:,
|
| 805 |
+
:,
|
| 806 |
+
target_h: target_h + hidden_states_batch.shape[3],
|
| 807 |
+
target_w: target_w + hidden_states_batch.shape[4],
|
| 808 |
+
] += mask
|
| 809 |
+
values = values / weight
|
| 810 |
+
return values
|
| 811 |
+
|
| 812 |
+
|
| 813 |
+
def single_encode(self, video, device):
|
| 814 |
+
video = video.to(device)
|
| 815 |
+
x = self.model.encode(video, self.scale)
|
| 816 |
+
return x
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
def single_decode(self, hidden_state, device):
|
| 820 |
+
hidden_state = hidden_state.to(device)
|
| 821 |
+
video = self.model.decode(hidden_state, self.scale)
|
| 822 |
+
return video.clamp_(-1, 1)
|
| 823 |
+
|
| 824 |
+
# ------------------------------------------------------------------
|
| 825 |
+
# Streaming encode/decode API
|
| 826 |
+
# ------------------------------------------------------------------
|
| 827 |
+
def reset_encode_cache(self):
|
| 828 |
+
"""Reset encoder feat_cache before starting a new streaming encode."""
|
| 829 |
+
self.model.clear_cache()
|
| 830 |
+
|
| 831 |
+
def save_encode_cache_state(self):
|
| 832 |
+
"""Snapshot the current encoder feat_cache (shallow list of tensors)."""
|
| 833 |
+
return list(self.model._enc_feat_map)
|
| 834 |
+
|
| 835 |
+
def load_encode_cache_state(self, state):
|
| 836 |
+
"""Restore a previously snapshotted encoder feat_cache."""
|
| 837 |
+
if state is None:
|
| 838 |
+
self.reset_encode_cache()
|
| 839 |
+
else:
|
| 840 |
+
self.model._enc_feat_map = list(state)
|
| 841 |
+
|
| 842 |
+
def streaming_encode_chunk(self, video_chunk, device):
|
| 843 |
+
"""Encode one chunk of video frames while preserving feat_cache state.
|
| 844 |
+
|
| 845 |
+
Caller must invoke reset_encode_cache() (or reset_decode_cache(), they
|
| 846 |
+
share state) once before the first chunk. The chunking convention
|
| 847 |
+
matches Wan VAE's internal pattern:
|
| 848 |
+
- First call after reset: video_chunk = [c, 1, h, w] (1 frame)
|
| 849 |
+
- Subsequent calls: video_chunk = [c, 4, h, w] (4 frames)
|
| 850 |
+
Each call returns 1 latent frame.
|
| 851 |
+
|
| 852 |
+
Args:
|
| 853 |
+
video_chunk: [c, t_chunk, h, w] (t_chunk == 1 first, 4 after)
|
| 854 |
+
device: target compute device
|
| 855 |
+
|
| 856 |
+
Returns:
|
| 857 |
+
latent: [1, z_dim, 1, h_lat, w_lat]
|
| 858 |
+
"""
|
| 859 |
+
x = video_chunk.unsqueeze(0).to(device)
|
| 860 |
+
return self.model.streaming_encode_step(x, self.scale)
|
| 861 |
+
|
| 862 |
+
def reset_decode_cache(self):
|
| 863 |
+
"""Reset the decoder feat_cache before starting a new streaming decode."""
|
| 864 |
+
self.model.clear_cache()
|
| 865 |
+
|
| 866 |
+
def streaming_decode_chunk(self, hidden_state_chunk, device):
|
| 867 |
+
"""Decode one chunk of latents while preserving feat_cache state.
|
| 868 |
+
|
| 869 |
+
The caller must invoke reset_decode_cache() once before the first chunk.
|
| 870 |
+
Subsequent calls reuse the cached temporal context, so the result is
|
| 871 |
+
bit-identical (modulo float order) to a single full-length decode of
|
| 872 |
+
the concatenated chunks.
|
| 873 |
+
|
| 874 |
+
Args:
|
| 875 |
+
hidden_state_chunk: [c, t_chunk, h, w] latent tensor
|
| 876 |
+
device: target device for compute
|
| 877 |
+
|
| 878 |
+
Returns:
|
| 879 |
+
video: [1, 3, t_video, H, W] tensor in [-1, 1]; t_video equals the
|
| 880 |
+
number of video frames produced by this chunk under Wan VAE 4x
|
| 881 |
+
temporal upsampling (1 latent → 1 video frame on the very first
|
| 882 |
+
call, then 4 per latent thereafter).
|
| 883 |
+
"""
|
| 884 |
+
hidden_state = hidden_state_chunk.unsqueeze(0).to(device)
|
| 885 |
+
video = self.model.decode(hidden_state, self.scale, keep_cache=True)
|
| 886 |
+
return video.clamp_(-1, 1)
|
| 887 |
+
|
| 888 |
+
|
| 889 |
+
def encode(self, videos, device, tiled=False, tile_size=(34, 34), tile_stride=(18, 16)):
|
| 890 |
+
|
| 891 |
+
videos = [video.to("cpu") for video in videos]
|
| 892 |
+
hidden_states = []
|
| 893 |
+
for video in videos:
|
| 894 |
+
video = video.unsqueeze(0)
|
| 895 |
+
if tiled:
|
| 896 |
+
tile_size = (tile_size[0] * 8, tile_size[1] * 8)
|
| 897 |
+
tile_stride = (tile_stride[0] * 8, tile_stride[1] * 8)
|
| 898 |
+
hidden_state = self.tiled_encode(video, device, tile_size, tile_stride)
|
| 899 |
+
else:
|
| 900 |
+
hidden_state = self.single_encode(video, device)
|
| 901 |
+
hidden_state = hidden_state.squeeze(0)
|
| 902 |
+
hidden_states.append(hidden_state)
|
| 903 |
+
hidden_states = torch.stack(hidden_states)
|
| 904 |
+
return hidden_states
|
| 905 |
+
|
| 906 |
+
|
| 907 |
+
def decode(self, hidden_states, device, tiled=False, tile_size=(34, 34), tile_stride=(18, 16)):
|
| 908 |
+
hidden_states = [hidden_state.to("cpu") for hidden_state in hidden_states]
|
| 909 |
+
videos = []
|
| 910 |
+
for hidden_state in hidden_states:
|
| 911 |
+
hidden_state = hidden_state.unsqueeze(0)
|
| 912 |
+
if tiled:
|
| 913 |
+
video = self.tiled_decode(hidden_state, device, tile_size, tile_stride)
|
| 914 |
+
else:
|
| 915 |
+
video = self.single_decode(hidden_state, device)
|
| 916 |
+
video = video.squeeze(0)
|
| 917 |
+
videos.append(video)
|
| 918 |
+
videos = torch.stack(videos)
|
| 919 |
+
return videos
|
| 920 |
+
|
| 921 |
+
|
| 922 |
+
@staticmethod
|
| 923 |
+
def state_dict_converter():
|
| 924 |
+
return WanVideoVAEStateDictConverter()
|
| 925 |
+
|
| 926 |
+
|
| 927 |
+
class WanVideoVAEStateDictConverter:
|
| 928 |
+
|
| 929 |
+
def __init__(self):
|
| 930 |
+
pass
|
| 931 |
+
|
| 932 |
+
def from_civitai(self, state_dict):
|
| 933 |
+
state_dict_ = {}
|
| 934 |
+
if 'model_state' in state_dict:
|
| 935 |
+
state_dict = state_dict['model_state']
|
| 936 |
+
for name in state_dict:
|
| 937 |
+
state_dict_['model.' + name] = state_dict[name]
|
| 938 |
+
return state_dict_
|
OmniAvatar/models/wav2vec.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pylint: disable=R0901
|
| 2 |
+
# src/models/wav2vec.py
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
This module defines the Wav2Vec model, which is a pre-trained model for speech recognition and understanding.
|
| 6 |
+
It inherits from the Wav2Vec2Model class in the transformers library and provides additional functionalities
|
| 7 |
+
such as feature extraction and encoding.
|
| 8 |
+
|
| 9 |
+
Classes:
|
| 10 |
+
Wav2VecModel: Inherits from Wav2Vec2Model and adds additional methods for feature extraction and encoding.
|
| 11 |
+
|
| 12 |
+
Functions:
|
| 13 |
+
linear_interpolation: Interpolates the features based on the sequence length.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
from transformers import Wav2Vec2Model
|
| 18 |
+
from transformers.modeling_outputs import BaseModelOutput
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class Wav2VecModel(Wav2Vec2Model):
|
| 22 |
+
"""
|
| 23 |
+
Wav2VecModel is a custom model class that extends the Wav2Vec2Model class from the transformers library.
|
| 24 |
+
It inherits all the functionality of the Wav2Vec2Model and adds additional methods for feature extraction and encoding.
|
| 25 |
+
...
|
| 26 |
+
|
| 27 |
+
Attributes:
|
| 28 |
+
base_model (Wav2Vec2Model): The base Wav2Vec2Model object.
|
| 29 |
+
|
| 30 |
+
Methods:
|
| 31 |
+
forward(input_values, seq_len, attention_mask=None, mask_time_indices=None
|
| 32 |
+
, output_attentions=None, output_hidden_states=None, return_dict=None):
|
| 33 |
+
Forward pass of the Wav2VecModel.
|
| 34 |
+
It takes input_values, seq_len, and other optional parameters as input and returns the output of the base model.
|
| 35 |
+
|
| 36 |
+
feature_extract(input_values, seq_len):
|
| 37 |
+
Extracts features from the input_values using the base model.
|
| 38 |
+
|
| 39 |
+
encode(extract_features, attention_mask=None, mask_time_indices=None, output_attentions=None, output_hidden_states=None, return_dict=None):
|
| 40 |
+
Encodes the extracted features using the base model and returns the encoded features.
|
| 41 |
+
"""
|
| 42 |
+
def forward(
|
| 43 |
+
self,
|
| 44 |
+
input_values,
|
| 45 |
+
seq_len,
|
| 46 |
+
attention_mask=None,
|
| 47 |
+
mask_time_indices=None,
|
| 48 |
+
output_attentions=None,
|
| 49 |
+
output_hidden_states=None,
|
| 50 |
+
return_dict=None,
|
| 51 |
+
):
|
| 52 |
+
"""
|
| 53 |
+
Forward pass of the Wav2Vec model.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
self: The instance of the model.
|
| 57 |
+
input_values: The input values (waveform) to the model.
|
| 58 |
+
seq_len: The sequence length of the input values.
|
| 59 |
+
attention_mask: Attention mask to be used for the model.
|
| 60 |
+
mask_time_indices: Mask indices to be used for the model.
|
| 61 |
+
output_attentions: If set to True, returns attentions.
|
| 62 |
+
output_hidden_states: If set to True, returns hidden states.
|
| 63 |
+
return_dict: If set to True, returns a BaseModelOutput instead of a tuple.
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
The output of the Wav2Vec model.
|
| 67 |
+
"""
|
| 68 |
+
self.config.output_attentions = True
|
| 69 |
+
|
| 70 |
+
output_hidden_states = (
|
| 71 |
+
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
|
| 72 |
+
)
|
| 73 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 74 |
+
|
| 75 |
+
extract_features = self.feature_extractor(input_values)
|
| 76 |
+
extract_features = extract_features.transpose(1, 2)
|
| 77 |
+
extract_features = linear_interpolation(extract_features, seq_len=seq_len)
|
| 78 |
+
|
| 79 |
+
if attention_mask is not None:
|
| 80 |
+
# compute reduced attention_mask corresponding to feature vectors
|
| 81 |
+
attention_mask = self._get_feature_vector_attention_mask(
|
| 82 |
+
extract_features.shape[1], attention_mask, add_adapter=False
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
hidden_states, extract_features = self.feature_projection(extract_features)
|
| 86 |
+
hidden_states = self._mask_hidden_states(
|
| 87 |
+
hidden_states, mask_time_indices=mask_time_indices, attention_mask=attention_mask
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
encoder_outputs = self.encoder(
|
| 91 |
+
hidden_states,
|
| 92 |
+
attention_mask=attention_mask,
|
| 93 |
+
output_attentions=output_attentions,
|
| 94 |
+
output_hidden_states=output_hidden_states,
|
| 95 |
+
return_dict=return_dict,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
hidden_states = encoder_outputs[0]
|
| 99 |
+
|
| 100 |
+
if self.adapter is not None:
|
| 101 |
+
hidden_states = self.adapter(hidden_states)
|
| 102 |
+
|
| 103 |
+
if not return_dict:
|
| 104 |
+
return (hidden_states, ) + encoder_outputs[1:]
|
| 105 |
+
return BaseModelOutput(
|
| 106 |
+
last_hidden_state=hidden_states,
|
| 107 |
+
hidden_states=encoder_outputs.hidden_states,
|
| 108 |
+
attentions=encoder_outputs.attentions,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def feature_extract(
|
| 113 |
+
self,
|
| 114 |
+
input_values,
|
| 115 |
+
seq_len,
|
| 116 |
+
):
|
| 117 |
+
"""
|
| 118 |
+
Extracts features from the input values and returns the extracted features.
|
| 119 |
+
|
| 120 |
+
Parameters:
|
| 121 |
+
input_values (torch.Tensor): The input values to be processed.
|
| 122 |
+
seq_len (torch.Tensor): The sequence lengths of the input values.
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
extracted_features (torch.Tensor): The extracted features from the input values.
|
| 126 |
+
"""
|
| 127 |
+
extract_features = self.feature_extractor(input_values)
|
| 128 |
+
extract_features = extract_features.transpose(1, 2)
|
| 129 |
+
extract_features = linear_interpolation(extract_features, seq_len=seq_len)
|
| 130 |
+
|
| 131 |
+
return extract_features
|
| 132 |
+
|
| 133 |
+
def encode(
|
| 134 |
+
self,
|
| 135 |
+
extract_features,
|
| 136 |
+
attention_mask=None,
|
| 137 |
+
mask_time_indices=None,
|
| 138 |
+
output_attentions=None,
|
| 139 |
+
output_hidden_states=None,
|
| 140 |
+
return_dict=None,
|
| 141 |
+
):
|
| 142 |
+
"""
|
| 143 |
+
Encodes the input features into the output space.
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
extract_features (torch.Tensor): The extracted features from the audio signal.
|
| 147 |
+
attention_mask (torch.Tensor, optional): Attention mask to be used for padding.
|
| 148 |
+
mask_time_indices (torch.Tensor, optional): Masked indices for the time dimension.
|
| 149 |
+
output_attentions (bool, optional): If set to True, returns the attention weights.
|
| 150 |
+
output_hidden_states (bool, optional): If set to True, returns all hidden states.
|
| 151 |
+
return_dict (bool, optional): If set to True, returns a BaseModelOutput instead of the tuple.
|
| 152 |
+
|
| 153 |
+
Returns:
|
| 154 |
+
The encoded output features.
|
| 155 |
+
"""
|
| 156 |
+
self.config.output_attentions = True
|
| 157 |
+
|
| 158 |
+
output_hidden_states = (
|
| 159 |
+
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
|
| 160 |
+
)
|
| 161 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 162 |
+
|
| 163 |
+
if attention_mask is not None:
|
| 164 |
+
# compute reduced attention_mask corresponding to feature vectors
|
| 165 |
+
attention_mask = self._get_feature_vector_attention_mask(
|
| 166 |
+
extract_features.shape[1], attention_mask, add_adapter=False
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
hidden_states, extract_features = self.feature_projection(extract_features)
|
| 170 |
+
hidden_states = self._mask_hidden_states(
|
| 171 |
+
hidden_states, mask_time_indices=mask_time_indices, attention_mask=attention_mask
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
encoder_outputs = self.encoder(
|
| 175 |
+
hidden_states,
|
| 176 |
+
attention_mask=attention_mask,
|
| 177 |
+
output_attentions=output_attentions,
|
| 178 |
+
output_hidden_states=output_hidden_states,
|
| 179 |
+
return_dict=return_dict,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
hidden_states = encoder_outputs[0]
|
| 183 |
+
|
| 184 |
+
if self.adapter is not None:
|
| 185 |
+
hidden_states = self.adapter(hidden_states)
|
| 186 |
+
|
| 187 |
+
if not return_dict:
|
| 188 |
+
return (hidden_states, ) + encoder_outputs[1:]
|
| 189 |
+
return BaseModelOutput(
|
| 190 |
+
last_hidden_state=hidden_states,
|
| 191 |
+
hidden_states=encoder_outputs.hidden_states,
|
| 192 |
+
attentions=encoder_outputs.attentions,
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def linear_interpolation(features, seq_len):
|
| 197 |
+
"""
|
| 198 |
+
Transpose the features to interpolate linearly.
|
| 199 |
+
|
| 200 |
+
Args:
|
| 201 |
+
features (torch.Tensor): The extracted features to be interpolated.
|
| 202 |
+
seq_len (torch.Tensor): The sequence lengths of the features.
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
torch.Tensor: The interpolated features.
|
| 206 |
+
"""
|
| 207 |
+
features = features.transpose(1, 2)
|
| 208 |
+
output_features = F.interpolate(features, size=seq_len, align_corners=True, mode='linear')
|
| 209 |
+
return output_features.transpose(1, 2)
|
OmniAvatar/prompters/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .wan_prompter import WanPrompter
|
OmniAvatar/prompters/base_prompter.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ..models.model_manager import ModelManager
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def tokenize_long_prompt(tokenizer, prompt, max_length=None):
|
| 7 |
+
# Get model_max_length from self.tokenizer
|
| 8 |
+
length = tokenizer.model_max_length if max_length is None else max_length
|
| 9 |
+
|
| 10 |
+
# To avoid the warning. set self.tokenizer.model_max_length to +oo.
|
| 11 |
+
tokenizer.model_max_length = 99999999
|
| 12 |
+
|
| 13 |
+
# Tokenize it!
|
| 14 |
+
input_ids = tokenizer(prompt, return_tensors="pt").input_ids
|
| 15 |
+
|
| 16 |
+
# Determine the real length.
|
| 17 |
+
max_length = (input_ids.shape[1] + length - 1) // length * length
|
| 18 |
+
|
| 19 |
+
# Restore tokenizer.model_max_length
|
| 20 |
+
tokenizer.model_max_length = length
|
| 21 |
+
|
| 22 |
+
# Tokenize it again with fixed length.
|
| 23 |
+
input_ids = tokenizer(
|
| 24 |
+
prompt,
|
| 25 |
+
return_tensors="pt",
|
| 26 |
+
padding="max_length",
|
| 27 |
+
max_length=max_length,
|
| 28 |
+
truncation=True
|
| 29 |
+
).input_ids
|
| 30 |
+
|
| 31 |
+
# Reshape input_ids to fit the text encoder.
|
| 32 |
+
num_sentence = input_ids.shape[1] // length
|
| 33 |
+
input_ids = input_ids.reshape((num_sentence, length))
|
| 34 |
+
|
| 35 |
+
return input_ids
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class BasePrompter:
|
| 40 |
+
def __init__(self):
|
| 41 |
+
self.refiners = []
|
| 42 |
+
self.extenders = []
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def load_prompt_refiners(self, model_manager: ModelManager, refiner_classes=[]):
|
| 46 |
+
for refiner_class in refiner_classes:
|
| 47 |
+
refiner = refiner_class.from_model_manager(model_manager)
|
| 48 |
+
self.refiners.append(refiner)
|
| 49 |
+
|
| 50 |
+
def load_prompt_extenders(self,model_manager:ModelManager,extender_classes=[]):
|
| 51 |
+
for extender_class in extender_classes:
|
| 52 |
+
extender = extender_class.from_model_manager(model_manager)
|
| 53 |
+
self.extenders.append(extender)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@torch.no_grad()
|
| 57 |
+
def process_prompt(self, prompt, positive=True):
|
| 58 |
+
if isinstance(prompt, list):
|
| 59 |
+
prompt = [self.process_prompt(prompt_, positive=positive) for prompt_ in prompt]
|
| 60 |
+
else:
|
| 61 |
+
for refiner in self.refiners:
|
| 62 |
+
prompt = refiner(prompt, positive=positive)
|
| 63 |
+
return prompt
|
| 64 |
+
|
| 65 |
+
@torch.no_grad()
|
| 66 |
+
def extend_prompt(self, prompt:str, positive=True):
|
| 67 |
+
extended_prompt = dict(prompt=prompt)
|
| 68 |
+
for extender in self.extenders:
|
| 69 |
+
extended_prompt = extender(extended_prompt)
|
| 70 |
+
return extended_prompt
|
OmniAvatar/prompters/wan_prompter.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .base_prompter import BasePrompter
|
| 2 |
+
from ..models.wan_video_text_encoder import WanTextEncoder
|
| 3 |
+
from transformers import AutoTokenizer
|
| 4 |
+
import os, torch
|
| 5 |
+
import ftfy
|
| 6 |
+
import html
|
| 7 |
+
import string
|
| 8 |
+
import regex as re
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def basic_clean(text):
|
| 12 |
+
text = ftfy.fix_text(text)
|
| 13 |
+
text = html.unescape(html.unescape(text))
|
| 14 |
+
return text.strip()
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def whitespace_clean(text):
|
| 18 |
+
text = re.sub(r'\s+', ' ', text)
|
| 19 |
+
text = text.strip()
|
| 20 |
+
return text
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def canonicalize(text, keep_punctuation_exact_string=None):
|
| 24 |
+
text = text.replace('_', ' ')
|
| 25 |
+
if keep_punctuation_exact_string:
|
| 26 |
+
text = keep_punctuation_exact_string.join(
|
| 27 |
+
part.translate(str.maketrans('', '', string.punctuation))
|
| 28 |
+
for part in text.split(keep_punctuation_exact_string))
|
| 29 |
+
else:
|
| 30 |
+
text = text.translate(str.maketrans('', '', string.punctuation))
|
| 31 |
+
text = text.lower()
|
| 32 |
+
text = re.sub(r'\s+', ' ', text)
|
| 33 |
+
return text.strip()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class HuggingfaceTokenizer:
|
| 37 |
+
|
| 38 |
+
def __init__(self, name, seq_len=None, clean=None, **kwargs):
|
| 39 |
+
assert clean in (None, 'whitespace', 'lower', 'canonicalize')
|
| 40 |
+
self.name = name
|
| 41 |
+
self.seq_len = seq_len
|
| 42 |
+
self.clean = clean
|
| 43 |
+
|
| 44 |
+
# init tokenizer
|
| 45 |
+
self.tokenizer = AutoTokenizer.from_pretrained(name, **kwargs)
|
| 46 |
+
self.vocab_size = self.tokenizer.vocab_size
|
| 47 |
+
|
| 48 |
+
def __call__(self, sequence, **kwargs):
|
| 49 |
+
return_mask = kwargs.pop('return_mask', False)
|
| 50 |
+
|
| 51 |
+
# arguments
|
| 52 |
+
_kwargs = {'return_tensors': 'pt'}
|
| 53 |
+
if self.seq_len is not None:
|
| 54 |
+
_kwargs.update({
|
| 55 |
+
'padding': 'max_length',
|
| 56 |
+
'truncation': True,
|
| 57 |
+
'max_length': self.seq_len
|
| 58 |
+
})
|
| 59 |
+
_kwargs.update(**kwargs)
|
| 60 |
+
|
| 61 |
+
# tokenization
|
| 62 |
+
if isinstance(sequence, str):
|
| 63 |
+
sequence = [sequence]
|
| 64 |
+
if self.clean:
|
| 65 |
+
sequence = [self._clean(u) for u in sequence]
|
| 66 |
+
ids = self.tokenizer(sequence, **_kwargs)
|
| 67 |
+
|
| 68 |
+
# output
|
| 69 |
+
if return_mask:
|
| 70 |
+
return ids.input_ids, ids.attention_mask
|
| 71 |
+
else:
|
| 72 |
+
return ids.input_ids
|
| 73 |
+
|
| 74 |
+
def _clean(self, text):
|
| 75 |
+
if self.clean == 'whitespace':
|
| 76 |
+
text = whitespace_clean(basic_clean(text))
|
| 77 |
+
elif self.clean == 'lower':
|
| 78 |
+
text = whitespace_clean(basic_clean(text)).lower()
|
| 79 |
+
elif self.clean == 'canonicalize':
|
| 80 |
+
text = canonicalize(basic_clean(text))
|
| 81 |
+
return text
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class WanPrompter(BasePrompter):
|
| 85 |
+
|
| 86 |
+
def __init__(self, tokenizer_path=None, text_len=512):
|
| 87 |
+
super().__init__()
|
| 88 |
+
self.text_len = text_len
|
| 89 |
+
self.text_encoder = None
|
| 90 |
+
self.fetch_tokenizer(tokenizer_path)
|
| 91 |
+
|
| 92 |
+
def fetch_tokenizer(self, tokenizer_path=None):
|
| 93 |
+
if tokenizer_path is not None:
|
| 94 |
+
self.tokenizer = HuggingfaceTokenizer(name=tokenizer_path, seq_len=self.text_len, clean='whitespace')
|
| 95 |
+
|
| 96 |
+
def fetch_models(self, text_encoder: WanTextEncoder = None):
|
| 97 |
+
self.text_encoder = text_encoder
|
| 98 |
+
|
| 99 |
+
def encode_prompt(self, prompt, positive=True, device="cuda"):
|
| 100 |
+
prompt = self.process_prompt(prompt, positive=positive)
|
| 101 |
+
|
| 102 |
+
ids, mask = self.tokenizer(prompt, return_mask=True, add_special_tokens=True)
|
| 103 |
+
ids = ids.to(device)
|
| 104 |
+
mask = mask.to(device)
|
| 105 |
+
seq_lens = mask.gt(0).sum(dim=1).long()
|
| 106 |
+
prompt_emb = self.text_encoder(ids, mask)
|
| 107 |
+
for i, v in enumerate(seq_lens):
|
| 108 |
+
prompt_emb[:, v:] = 0
|
| 109 |
+
return prompt_emb
|
OmniAvatar/utils/args_config.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import argparse
|
| 4 |
+
import yaml
|
| 5 |
+
args = None
|
| 6 |
+
|
| 7 |
+
def parse_hp_string(hp_string):
|
| 8 |
+
result = {}
|
| 9 |
+
for pair in hp_string.split(','):
|
| 10 |
+
if not pair:
|
| 11 |
+
continue
|
| 12 |
+
key, value = pair.split('=')
|
| 13 |
+
try:
|
| 14 |
+
# 自动转换为 int / float / str
|
| 15 |
+
ori_value = value
|
| 16 |
+
value = float(value)
|
| 17 |
+
if '.' not in str(ori_value):
|
| 18 |
+
value = int(value)
|
| 19 |
+
except ValueError:
|
| 20 |
+
pass
|
| 21 |
+
|
| 22 |
+
if value in ['true', 'True']:
|
| 23 |
+
value = True
|
| 24 |
+
if value in ['false', 'False']:
|
| 25 |
+
value = False
|
| 26 |
+
if '.' in key:
|
| 27 |
+
keys = key.split('.')
|
| 28 |
+
keys = keys
|
| 29 |
+
current = result
|
| 30 |
+
for key in keys[:-1]:
|
| 31 |
+
if key not in current or not isinstance(current[key], dict):
|
| 32 |
+
current[key] = {}
|
| 33 |
+
current = current[key]
|
| 34 |
+
current[keys[-1]] = value
|
| 35 |
+
else:
|
| 36 |
+
result[key.strip()] = value
|
| 37 |
+
return result
|
| 38 |
+
|
| 39 |
+
def parse_args():
|
| 40 |
+
global args
|
| 41 |
+
parser = argparse.ArgumentParser(description="Simple example of a training script.")
|
| 42 |
+
parser.add_argument("--config", type=str, required=True, help="Path to YAML config file.")
|
| 43 |
+
|
| 44 |
+
# 定义 argparse 参数
|
| 45 |
+
parser.add_argument("--exp_path", type=str, help="Path to save the model.")
|
| 46 |
+
parser.add_argument("--input_file", type=str, help="Path to inference txt.")
|
| 47 |
+
parser.add_argument("--debug", action='store_true', default=None)
|
| 48 |
+
parser.add_argument("--infer", action='store_true')
|
| 49 |
+
parser.add_argument("-hp", "--hparams", type=str, default="")
|
| 50 |
+
|
| 51 |
+
args = parser.parse_args()
|
| 52 |
+
|
| 53 |
+
# 读取 YAML 配置(如果提供了 --config 参数)
|
| 54 |
+
if args.config:
|
| 55 |
+
with open(args.config, "r") as f:
|
| 56 |
+
yaml_config = yaml.safe_load(f)
|
| 57 |
+
|
| 58 |
+
# 遍历 YAML 配置,将其添加到 args(如果 argparse 里没有定义)
|
| 59 |
+
for key, value in yaml_config.items():
|
| 60 |
+
if not hasattr(args, key): # argparse 没有的参数
|
| 61 |
+
setattr(args, key, value)
|
| 62 |
+
elif getattr(args, key) is None: # argparse 有但值为空
|
| 63 |
+
setattr(args, key, value)
|
| 64 |
+
|
| 65 |
+
args.rank = int(os.getenv("RANK", "0"))
|
| 66 |
+
args.world_size = int(os.getenv("WORLD_SIZE", "1"))
|
| 67 |
+
args.local_rank = int(os.getenv("LOCAL_RANK", "0")) # torchrun
|
| 68 |
+
args.device = f'cuda:{args.local_rank}'
|
| 69 |
+
args.num_nodes = int(os.getenv("NNODES", "1"))
|
| 70 |
+
debug = args.debug
|
| 71 |
+
|
| 72 |
+
# Apply hparams BEFORE reload_cfg so -hp exp_path=... takes effect first
|
| 73 |
+
if len(args.hparams) > 0:
|
| 74 |
+
hp_dict = parse_hp_string(args.hparams)
|
| 75 |
+
for key, value in hp_dict.items():
|
| 76 |
+
if not hasattr(args, key):
|
| 77 |
+
setattr(args, key, value)
|
| 78 |
+
else:
|
| 79 |
+
if isinstance(value, dict):
|
| 80 |
+
ori_v = getattr(args, key)
|
| 81 |
+
ori_v.update(value)
|
| 82 |
+
setattr(args, key, ori_v)
|
| 83 |
+
else:
|
| 84 |
+
setattr(args, key, value)
|
| 85 |
+
|
| 86 |
+
if not os.path.exists(args.exp_path):
|
| 87 |
+
args.exp_path = f'checkpoints/{args.exp_path}'
|
| 88 |
+
|
| 89 |
+
if hasattr(args, 'reload_cfg') and args.reload_cfg:
|
| 90 |
+
# 重新加载配置文件
|
| 91 |
+
conf_path = os.path.join(args.exp_path, "config.json")
|
| 92 |
+
if os.path.exists(conf_path):
|
| 93 |
+
print('| Reloading config from:', conf_path)
|
| 94 |
+
args = reload(args, conf_path)
|
| 95 |
+
args.debug = debug
|
| 96 |
+
dict_args = convert_namespace_to_dict(args)
|
| 97 |
+
if args.local_rank == 0:
|
| 98 |
+
print(dict_args)
|
| 99 |
+
return args
|
| 100 |
+
|
| 101 |
+
def reload(args, conf_path):
|
| 102 |
+
"""重新加载配置文件,不覆盖已有的参数"""
|
| 103 |
+
with open(conf_path, "r") as f:
|
| 104 |
+
yaml_config = yaml.safe_load(f)
|
| 105 |
+
# 遍历 YAML 配置,将其添加到 args(如果 argparse 里没有定义)
|
| 106 |
+
for key, value in yaml_config.items():
|
| 107 |
+
if not hasattr(args, key): # argparse 没有的参数
|
| 108 |
+
setattr(args, key, value)
|
| 109 |
+
elif getattr(args, key) is None: # argparse 有但值为空
|
| 110 |
+
setattr(args, key, value)
|
| 111 |
+
return args
|
| 112 |
+
|
| 113 |
+
def convert_namespace_to_dict(namespace):
|
| 114 |
+
"""将 argparse.Namespace 转为字典,并处理不可序列化对象"""
|
| 115 |
+
result = {}
|
| 116 |
+
for key, value in vars(namespace).items():
|
| 117 |
+
try:
|
| 118 |
+
json.dumps(value) # 检查是否可序列化
|
| 119 |
+
result[key] = value
|
| 120 |
+
except (TypeError, OverflowError):
|
| 121 |
+
result[key] = str(value) # 将不可序列化的对象转为字符串表示
|
| 122 |
+
return result
|
OmniAvatar/utils/io_utils.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import torch, os
|
| 3 |
+
from safetensors import safe_open
|
| 4 |
+
from OmniAvatar.utils.args_config import args
|
| 5 |
+
from contextlib import contextmanager
|
| 6 |
+
|
| 7 |
+
import re
|
| 8 |
+
import tempfile
|
| 9 |
+
import numpy as np
|
| 10 |
+
import imageio
|
| 11 |
+
from glob import glob
|
| 12 |
+
import soundfile as sf
|
| 13 |
+
from einops import rearrange
|
| 14 |
+
import hashlib
|
| 15 |
+
|
| 16 |
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 17 |
+
|
| 18 |
+
@contextmanager
|
| 19 |
+
def init_weights_on_device(device = torch.device("meta"), include_buffers :bool = False):
|
| 20 |
+
|
| 21 |
+
old_register_parameter = torch.nn.Module.register_parameter
|
| 22 |
+
if include_buffers:
|
| 23 |
+
old_register_buffer = torch.nn.Module.register_buffer
|
| 24 |
+
|
| 25 |
+
def register_empty_parameter(module, name, param):
|
| 26 |
+
old_register_parameter(module, name, param)
|
| 27 |
+
if param is not None:
|
| 28 |
+
param_cls = type(module._parameters[name])
|
| 29 |
+
kwargs = module._parameters[name].__dict__
|
| 30 |
+
kwargs["requires_grad"] = param.requires_grad
|
| 31 |
+
module._parameters[name] = param_cls(module._parameters[name].to(device), **kwargs)
|
| 32 |
+
|
| 33 |
+
def register_empty_buffer(module, name, buffer, persistent=True):
|
| 34 |
+
old_register_buffer(module, name, buffer, persistent=persistent)
|
| 35 |
+
if buffer is not None:
|
| 36 |
+
module._buffers[name] = module._buffers[name].to(device)
|
| 37 |
+
|
| 38 |
+
def patch_tensor_constructor(fn):
|
| 39 |
+
def wrapper(*args, **kwargs):
|
| 40 |
+
kwargs["device"] = device
|
| 41 |
+
return fn(*args, **kwargs)
|
| 42 |
+
|
| 43 |
+
return wrapper
|
| 44 |
+
|
| 45 |
+
if include_buffers:
|
| 46 |
+
tensor_constructors_to_patch = {
|
| 47 |
+
torch_function_name: getattr(torch, torch_function_name)
|
| 48 |
+
for torch_function_name in ["empty", "zeros", "ones", "full"]
|
| 49 |
+
}
|
| 50 |
+
else:
|
| 51 |
+
tensor_constructors_to_patch = {}
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
torch.nn.Module.register_parameter = register_empty_parameter
|
| 55 |
+
if include_buffers:
|
| 56 |
+
torch.nn.Module.register_buffer = register_empty_buffer
|
| 57 |
+
for torch_function_name in tensor_constructors_to_patch.keys():
|
| 58 |
+
setattr(torch, torch_function_name, patch_tensor_constructor(getattr(torch, torch_function_name)))
|
| 59 |
+
yield
|
| 60 |
+
finally:
|
| 61 |
+
torch.nn.Module.register_parameter = old_register_parameter
|
| 62 |
+
if include_buffers:
|
| 63 |
+
torch.nn.Module.register_buffer = old_register_buffer
|
| 64 |
+
for torch_function_name, old_torch_function in tensor_constructors_to_patch.items():
|
| 65 |
+
setattr(torch, torch_function_name, old_torch_function)
|
| 66 |
+
|
| 67 |
+
def load_state_dict_from_folder(file_path, torch_dtype=None):
|
| 68 |
+
state_dict = {}
|
| 69 |
+
for file_name in os.listdir(file_path):
|
| 70 |
+
if "." in file_name and file_name.split(".")[-1] in [
|
| 71 |
+
"safetensors", "bin", "ckpt", "pth", "pt"
|
| 72 |
+
]:
|
| 73 |
+
state_dict.update(load_state_dict(os.path.join(file_path, file_name), torch_dtype=torch_dtype))
|
| 74 |
+
return state_dict
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def load_state_dict(file_path, torch_dtype=None):
|
| 78 |
+
if file_path.endswith(".safetensors"):
|
| 79 |
+
return load_state_dict_from_safetensors(file_path, torch_dtype=torch_dtype)
|
| 80 |
+
else:
|
| 81 |
+
return load_state_dict_from_bin(file_path, torch_dtype=torch_dtype)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def load_state_dict_from_safetensors(file_path, torch_dtype=None):
|
| 85 |
+
state_dict = {}
|
| 86 |
+
with safe_open(file_path, framework="pt", device="cpu") as f:
|
| 87 |
+
for k in f.keys():
|
| 88 |
+
state_dict[k] = f.get_tensor(k)
|
| 89 |
+
if torch_dtype is not None:
|
| 90 |
+
state_dict[k] = state_dict[k].to(torch_dtype)
|
| 91 |
+
return state_dict
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def load_state_dict_from_bin(file_path, torch_dtype=None):
|
| 95 |
+
state_dict = torch.load(file_path, map_location="cpu", weights_only=True)
|
| 96 |
+
if torch_dtype is not None:
|
| 97 |
+
for i in state_dict:
|
| 98 |
+
if isinstance(state_dict[i], torch.Tensor):
|
| 99 |
+
state_dict[i] = state_dict[i].to(torch_dtype)
|
| 100 |
+
return state_dict
|
| 101 |
+
|
| 102 |
+
def smart_load_weights(model, ckpt_state_dict):
|
| 103 |
+
model_state_dict = model.state_dict()
|
| 104 |
+
new_state_dict = {}
|
| 105 |
+
|
| 106 |
+
for name, param in model_state_dict.items():
|
| 107 |
+
if name in ckpt_state_dict:
|
| 108 |
+
ckpt_param = ckpt_state_dict[name]
|
| 109 |
+
if param.shape == ckpt_param.shape:
|
| 110 |
+
new_state_dict[name] = ckpt_param
|
| 111 |
+
else:
|
| 112 |
+
# 自动修剪维度以匹配
|
| 113 |
+
if all(p >= c for p, c in zip(param.shape, ckpt_param.shape)):
|
| 114 |
+
print(f"[Truncate] {name}: ckpt {ckpt_param.shape} -> model {param.shape}")
|
| 115 |
+
# 创建新张量,拷贝旧数据
|
| 116 |
+
new_param = param.clone()
|
| 117 |
+
slices = tuple(slice(0, s) for s in ckpt_param.shape)
|
| 118 |
+
new_param[slices] = ckpt_param
|
| 119 |
+
new_state_dict[name] = new_param
|
| 120 |
+
else:
|
| 121 |
+
print(f"[Skip] {name}: ckpt {ckpt_param.shape} is larger than model {param.shape}")
|
| 122 |
+
|
| 123 |
+
# 更新 state_dict,只更新那些匹配的
|
| 124 |
+
missing_keys, unexpected_keys = model.load_state_dict(new_state_dict, assign=True, strict=False)
|
| 125 |
+
return model, missing_keys, unexpected_keys
|
| 126 |
+
|
| 127 |
+
def save_wav(audio, audio_path):
|
| 128 |
+
if isinstance(audio, torch.Tensor):
|
| 129 |
+
audio = audio.float().detach().cpu().numpy()
|
| 130 |
+
|
| 131 |
+
if audio.ndim == 1:
|
| 132 |
+
audio = np.expand_dims(audio, axis=0) # (1, samples)
|
| 133 |
+
|
| 134 |
+
sf.write(audio_path, audio.T, 16000)
|
| 135 |
+
|
| 136 |
+
return True
|
| 137 |
+
|
| 138 |
+
def save_video_as_grid_and_mp4(video_batch: torch.Tensor, save_path: str, fps: float = 5,prompt=None, prompt_path=None, audio=None, audio_path=None, prefix=None):
|
| 139 |
+
os.makedirs(save_path, exist_ok=True)
|
| 140 |
+
out_videos = []
|
| 141 |
+
with tempfile.TemporaryDirectory() as tmp_path:
|
| 142 |
+
for i, vid in enumerate(video_batch):
|
| 143 |
+
gif_frames = []
|
| 144 |
+
for frame in vid:
|
| 145 |
+
frame = rearrange(frame, "c h w -> h w c")
|
| 146 |
+
frame = (255.0 * frame).cpu().numpy().astype(np.uint8)
|
| 147 |
+
gif_frames.append(frame)
|
| 148 |
+
if prefix is not None:
|
| 149 |
+
now_save_path = os.path.join(save_path, f"{prefix}_{i:03d}.mp4")
|
| 150 |
+
tmp_save_path = os.path.join(tmp_path, f"{prefix}_{i:03d}.mp4")
|
| 151 |
+
else:
|
| 152 |
+
now_save_path = os.path.join(save_path, f"{i:03d}.mp4")
|
| 153 |
+
tmp_save_path = os.path.join(tmp_path, f"{i:03d}.mp4")
|
| 154 |
+
with imageio.get_writer(tmp_save_path, fps=fps) as writer:
|
| 155 |
+
for frame in gif_frames:
|
| 156 |
+
writer.append_data(frame)
|
| 157 |
+
subprocess.run([f"cp {tmp_save_path} {now_save_path}"], check=True, shell=True)
|
| 158 |
+
print(f'save res video to : {now_save_path}')
|
| 159 |
+
if audio is not None or audio_path is not None:
|
| 160 |
+
if audio is not None:
|
| 161 |
+
audio_path = os.path.join(tmp_path, f"{i:06d}.mp3")
|
| 162 |
+
save_wav(audio[i], audio_path)
|
| 163 |
+
# cmd = f'/usr/bin/ffmpeg -i {tmp_save_path} -i {audio_path} -v quiet -c:v copy -c:a libmp3lame -strict experimental {tmp_save_path[:-4]}_wav.mp4 -y'
|
| 164 |
+
cmd = f'/usr/bin/ffmpeg -i {tmp_save_path} -i {audio_path} -v quiet -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac {tmp_save_path[:-4]}_wav.mp4 -y'
|
| 165 |
+
subprocess.check_call(cmd, stdout=None, stdin=subprocess.PIPE, shell=True)
|
| 166 |
+
subprocess.run([f"cp {tmp_save_path[:-4]}_wav.mp4 {now_save_path[:-4]}_wav.mp4"], check=True, shell=True)
|
| 167 |
+
os.remove(now_save_path)
|
| 168 |
+
if prompt is not None and prompt_path is not None:
|
| 169 |
+
with open(prompt_path, "w") as f:
|
| 170 |
+
f.write(prompt)
|
| 171 |
+
out_videos.append(now_save_path)
|
| 172 |
+
return out_videos
|
| 173 |
+
|
| 174 |
+
def is_zero_stage_3(trainer):
|
| 175 |
+
strategy = getattr(trainer, "strategy", None)
|
| 176 |
+
if strategy and hasattr(strategy, "model"):
|
| 177 |
+
ds_engine = strategy.model
|
| 178 |
+
stage = ds_engine.config.get("zero_optimization", {}).get("stage", 0)
|
| 179 |
+
return stage == 3
|
| 180 |
+
return False
|
| 181 |
+
|
| 182 |
+
def hash_state_dict_keys(state_dict, with_shape=True):
|
| 183 |
+
keys_str = convert_state_dict_keys_to_single_str(state_dict, with_shape=with_shape)
|
| 184 |
+
keys_str = keys_str.encode(encoding="UTF-8")
|
| 185 |
+
return hashlib.md5(keys_str).hexdigest()
|
| 186 |
+
|
| 187 |
+
def split_state_dict_with_prefix(state_dict):
|
| 188 |
+
keys = sorted([key for key in state_dict if isinstance(key, str)])
|
| 189 |
+
prefix_dict = {}
|
| 190 |
+
for key in keys:
|
| 191 |
+
prefix = key if "." not in key else key.split(".")[0]
|
| 192 |
+
if prefix not in prefix_dict:
|
| 193 |
+
prefix_dict[prefix] = []
|
| 194 |
+
prefix_dict[prefix].append(key)
|
| 195 |
+
state_dicts = []
|
| 196 |
+
for prefix, keys in prefix_dict.items():
|
| 197 |
+
sub_state_dict = {key: state_dict[key] for key in keys}
|
| 198 |
+
state_dicts.append(sub_state_dict)
|
| 199 |
+
return state_dicts
|
| 200 |
+
|
| 201 |
+
def hash_state_dict_keys(state_dict, with_shape=True):
|
| 202 |
+
keys_str = convert_state_dict_keys_to_single_str(state_dict, with_shape=with_shape)
|
| 203 |
+
keys_str = keys_str.encode(encoding="UTF-8")
|
| 204 |
+
return hashlib.md5(keys_str).hexdigest()
|
| 205 |
+
|
| 206 |
+
def split_state_dict_with_prefix(state_dict):
|
| 207 |
+
keys = sorted([key for key in state_dict if isinstance(key, str)])
|
| 208 |
+
prefix_dict = {}
|
| 209 |
+
for key in keys:
|
| 210 |
+
prefix = key if "." not in key else key.split(".")[0]
|
| 211 |
+
if prefix not in prefix_dict:
|
| 212 |
+
prefix_dict[prefix] = []
|
| 213 |
+
prefix_dict[prefix].append(key)
|
| 214 |
+
state_dicts = []
|
| 215 |
+
for prefix, keys in prefix_dict.items():
|
| 216 |
+
sub_state_dict = {key: state_dict[key] for key in keys}
|
| 217 |
+
state_dicts.append(sub_state_dict)
|
| 218 |
+
return state_dicts
|
| 219 |
+
|
| 220 |
+
def search_for_files(folder, extensions):
|
| 221 |
+
files = []
|
| 222 |
+
if os.path.isdir(folder):
|
| 223 |
+
for file in sorted(os.listdir(folder)):
|
| 224 |
+
files += search_for_files(os.path.join(folder, file), extensions)
|
| 225 |
+
elif os.path.isfile(folder):
|
| 226 |
+
for extension in extensions:
|
| 227 |
+
if folder.endswith(extension):
|
| 228 |
+
files.append(folder)
|
| 229 |
+
break
|
| 230 |
+
return files
|
| 231 |
+
|
| 232 |
+
def convert_state_dict_keys_to_single_str(state_dict, with_shape=True):
|
| 233 |
+
keys = []
|
| 234 |
+
for key, value in state_dict.items():
|
| 235 |
+
if isinstance(key, str):
|
| 236 |
+
if isinstance(value, torch.Tensor):
|
| 237 |
+
if with_shape:
|
| 238 |
+
shape = "_".join(map(str, list(value.shape)))
|
| 239 |
+
keys.append(key + ":" + shape)
|
| 240 |
+
keys.append(key)
|
| 241 |
+
elif isinstance(value, dict):
|
| 242 |
+
keys.append(key + "|" + convert_state_dict_keys_to_single_str(value, with_shape=with_shape))
|
| 243 |
+
keys.sort()
|
| 244 |
+
keys_str = ",".join(keys)
|
| 245 |
+
return keys_str
|
OmniAvatar/utils/latentsync/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LatentSync face detection, affine alignment, and compositing utilities.
|
| 3 |
+
|
| 4 |
+
Provides InsightFace-based face detection, Procrustes affine alignment to 512x512,
|
| 5 |
+
and inverse affine compositing with soft blending for video-to-video lip sync.
|
| 6 |
+
|
| 7 |
+
Adapted from LatentSync.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from .face_detector import FaceDetector
|
| 11 |
+
from .affine_transform import AlignRestore
|
| 12 |
+
from .image_processor import ImageProcessor, load_fixed_mask
|
| 13 |
+
|
| 14 |
+
__all__ = [
|
| 15 |
+
"FaceDetector",
|
| 16 |
+
"AlignRestore",
|
| 17 |
+
"ImageProcessor",
|
| 18 |
+
"load_fixed_mask",
|
| 19 |
+
]
|
OmniAvatar/utils/latentsync/affine_transform.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Adapted from https://github.com/guanjz20/StyleSync/blob/main/utils.py
|
| 2 |
+
# Adapted from LatentSync
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import cv2
|
| 6 |
+
import torch
|
| 7 |
+
from einops import rearrange
|
| 8 |
+
import kornia
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class AlignRestore(object):
|
| 12 |
+
def __init__(self, align_points=3, resolution=256, device="cpu", dtype=torch.float16):
|
| 13 |
+
if align_points == 3:
|
| 14 |
+
self.upscale_factor = 1
|
| 15 |
+
ratio = resolution / 256 * 2.8
|
| 16 |
+
self.crop_ratio = (ratio, ratio)
|
| 17 |
+
self.face_template = np.array([[19 - 2, 30 - 10], [56 + 2, 30 - 10], [37.5, 45 - 5]])
|
| 18 |
+
self.face_template = self.face_template * ratio
|
| 19 |
+
self.face_size = (int(75 * self.crop_ratio[0]), int(100 * self.crop_ratio[1]))
|
| 20 |
+
self.p_bias = None
|
| 21 |
+
self.device = device
|
| 22 |
+
self.dtype = dtype
|
| 23 |
+
self.fill_value = torch.tensor([127, 127, 127], device=device, dtype=dtype)
|
| 24 |
+
self.mask = torch.ones((1, 1, self.face_size[1], self.face_size[0]), device=device, dtype=dtype)
|
| 25 |
+
|
| 26 |
+
def align_warp_face(self, img, landmarks3, smooth=True):
|
| 27 |
+
affine_matrix, self.p_bias = self.transformation_from_points(
|
| 28 |
+
landmarks3, self.face_template, smooth, self.p_bias
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
img = rearrange(torch.from_numpy(img).to(device=self.device, dtype=self.dtype), "h w c -> c h w").unsqueeze(0)
|
| 32 |
+
affine_matrix = torch.from_numpy(affine_matrix).to(device=self.device, dtype=self.dtype).unsqueeze(0)
|
| 33 |
+
|
| 34 |
+
cropped_face = kornia.geometry.transform.warp_affine(
|
| 35 |
+
img,
|
| 36 |
+
affine_matrix,
|
| 37 |
+
(self.face_size[1], self.face_size[0]),
|
| 38 |
+
mode="bilinear",
|
| 39 |
+
padding_mode="fill",
|
| 40 |
+
fill_value=self.fill_value,
|
| 41 |
+
)
|
| 42 |
+
cropped_face = rearrange(cropped_face.squeeze(0), "c h w -> h w c").cpu().numpy().astype(np.uint8)
|
| 43 |
+
return cropped_face, affine_matrix
|
| 44 |
+
|
| 45 |
+
def restore_img(self, input_img, face, affine_matrix):
|
| 46 |
+
"""Inverse-warp composited face back onto original frame with soft blending.
|
| 47 |
+
|
| 48 |
+
Uses float32 for all intermediate computation to avoid color shifts from
|
| 49 |
+
float16 rounding (float16 cannot exactly represent all uint8 values 0-255).
|
| 50 |
+
"""
|
| 51 |
+
h, w, _ = input_img.shape
|
| 52 |
+
# Use float32 for compositing precision regardless of self.dtype
|
| 53 |
+
work_dtype = torch.float32
|
| 54 |
+
|
| 55 |
+
if isinstance(affine_matrix, np.ndarray):
|
| 56 |
+
affine_matrix = torch.from_numpy(affine_matrix).to(device=self.device, dtype=work_dtype).unsqueeze(0)
|
| 57 |
+
else:
|
| 58 |
+
affine_matrix = affine_matrix.to(dtype=work_dtype)
|
| 59 |
+
|
| 60 |
+
inv_affine_matrix = kornia.geometry.transform.invert_affine_transform(affine_matrix)
|
| 61 |
+
face = face.to(device=self.device, dtype=work_dtype).unsqueeze(0)
|
| 62 |
+
fill_value = self.fill_value.to(dtype=work_dtype)
|
| 63 |
+
|
| 64 |
+
inv_face = kornia.geometry.transform.warp_affine(
|
| 65 |
+
face, inv_affine_matrix, (h, w), mode="bilinear", padding_mode="fill", fill_value=fill_value
|
| 66 |
+
).squeeze(0)
|
| 67 |
+
inv_face = (inv_face / 2 + 0.5).clamp(0, 1) * 255
|
| 68 |
+
|
| 69 |
+
input_img = rearrange(torch.from_numpy(input_img).to(device=self.device, dtype=work_dtype), "h w c -> c h w")
|
| 70 |
+
mask_f32 = self.mask.to(dtype=work_dtype)
|
| 71 |
+
inv_mask = kornia.geometry.transform.warp_affine(
|
| 72 |
+
mask_f32, inv_affine_matrix, (h, w), padding_mode="zeros"
|
| 73 |
+
) # (1, 1, h_up, w_up)
|
| 74 |
+
|
| 75 |
+
erosion_kernel = torch.ones(
|
| 76 |
+
(int(2 * self.upscale_factor), int(2 * self.upscale_factor)),
|
| 77 |
+
device=self.device, dtype=work_dtype,
|
| 78 |
+
)
|
| 79 |
+
inv_mask_erosion = kornia.morphology.erosion(inv_mask, erosion_kernel)
|
| 80 |
+
|
| 81 |
+
inv_mask_erosion_t = inv_mask_erosion.squeeze(0).expand_as(inv_face)
|
| 82 |
+
pasted_face = inv_mask_erosion_t * inv_face
|
| 83 |
+
total_face_area = torch.sum(inv_mask_erosion)
|
| 84 |
+
w_edge = int(total_face_area**0.5) // 20
|
| 85 |
+
erosion_radius = w_edge * 2
|
| 86 |
+
|
| 87 |
+
# Run on CPU to avoid consuming a large amount of GPU memory.
|
| 88 |
+
inv_mask_erosion = inv_mask_erosion.squeeze().cpu().numpy().astype(np.float32)
|
| 89 |
+
inv_mask_center = cv2.erode(inv_mask_erosion, np.ones((erosion_radius, erosion_radius), np.uint8))
|
| 90 |
+
inv_mask_center = torch.from_numpy(inv_mask_center).to(device=self.device, dtype=work_dtype)[None, None, ...]
|
| 91 |
+
|
| 92 |
+
blur_size = w_edge * 2 + 1
|
| 93 |
+
sigma = 0.3 * ((blur_size - 1) * 0.5 - 1) + 0.8
|
| 94 |
+
inv_soft_mask = kornia.filters.gaussian_blur2d(
|
| 95 |
+
inv_mask_center, (blur_size, blur_size), (sigma, sigma)
|
| 96 |
+
).squeeze(0)
|
| 97 |
+
inv_soft_mask_3d = inv_soft_mask.expand_as(inv_face)
|
| 98 |
+
img_back = inv_soft_mask_3d * pasted_face + (1 - inv_soft_mask_3d) * input_img
|
| 99 |
+
|
| 100 |
+
img_back = rearrange(img_back, "c h w -> h w c").contiguous().to(dtype=torch.uint8)
|
| 101 |
+
img_back = img_back.cpu().numpy()
|
| 102 |
+
return img_back
|
| 103 |
+
|
| 104 |
+
def transformation_from_points(self, points1: torch.Tensor, points0: torch.Tensor, smooth=True, p_bias=None):
|
| 105 |
+
if isinstance(points0, np.ndarray):
|
| 106 |
+
points2 = torch.tensor(points0, device=self.device, dtype=torch.float32)
|
| 107 |
+
else:
|
| 108 |
+
points2 = points0.clone()
|
| 109 |
+
|
| 110 |
+
if isinstance(points1, np.ndarray):
|
| 111 |
+
points1_tensor = torch.tensor(points1, device=self.device, dtype=torch.float32)
|
| 112 |
+
else:
|
| 113 |
+
points1_tensor = points1.clone()
|
| 114 |
+
|
| 115 |
+
c1 = torch.mean(points1_tensor, dim=0)
|
| 116 |
+
c2 = torch.mean(points2, dim=0)
|
| 117 |
+
|
| 118 |
+
points1_centered = points1_tensor - c1
|
| 119 |
+
points2_centered = points2 - c2
|
| 120 |
+
|
| 121 |
+
s1 = torch.std(points1_centered)
|
| 122 |
+
s2 = torch.std(points2_centered)
|
| 123 |
+
|
| 124 |
+
points1_normalized = points1_centered / s1
|
| 125 |
+
points2_normalized = points2_centered / s2
|
| 126 |
+
|
| 127 |
+
covariance = torch.matmul(points1_normalized.T, points2_normalized)
|
| 128 |
+
U, S, V = torch.svd(covariance.float())
|
| 129 |
+
|
| 130 |
+
R = torch.matmul(V, U.T)
|
| 131 |
+
|
| 132 |
+
det = torch.det(R.float())
|
| 133 |
+
if det < 0:
|
| 134 |
+
V[:, -1] = -V[:, -1]
|
| 135 |
+
R = torch.matmul(V, U.T)
|
| 136 |
+
|
| 137 |
+
sR = (s2 / s1) * R
|
| 138 |
+
T = c2.reshape(2, 1) - (s2 / s1) * torch.matmul(R, c1.reshape(2, 1))
|
| 139 |
+
|
| 140 |
+
M = torch.cat((sR, T), dim=1)
|
| 141 |
+
|
| 142 |
+
if smooth:
|
| 143 |
+
bias = points2_normalized[2] - points1_normalized[2]
|
| 144 |
+
if p_bias is None:
|
| 145 |
+
p_bias = bias
|
| 146 |
+
else:
|
| 147 |
+
bias = p_bias * 0.2 + bias * 0.8
|
| 148 |
+
p_bias = bias
|
| 149 |
+
M[:, 2] = M[:, 2] + bias
|
| 150 |
+
|
| 151 |
+
return M.cpu().numpy(), p_bias
|
OmniAvatar/utils/latentsync/face_detector.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Adapted from LatentSync
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
# Disable ONNX Runtime thread affinity to suppress pthread warnings
|
| 6 |
+
# MUST be set before importing insightface/onnxruntime
|
| 7 |
+
os.environ['ORT_DISABLE_THREAD_AFFINITY'] = '1'
|
| 8 |
+
|
| 9 |
+
from insightface.app import FaceAnalysis
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
INSIGHTFACE_DETECT_SIZE = 512
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class FaceDetector:
|
| 17 |
+
def __init__(self, device="cuda", insightface_root="checkpoints/auxiliary"):
|
| 18 |
+
self.app = FaceAnalysis(
|
| 19 |
+
allowed_modules=["detection", "landmark_2d_106"],
|
| 20 |
+
root=insightface_root,
|
| 21 |
+
providers=["CUDAExecutionProvider"],
|
| 22 |
+
)
|
| 23 |
+
self.app.prepare(ctx_id=cuda_to_int(device), det_size=(INSIGHTFACE_DETECT_SIZE, INSIGHTFACE_DETECT_SIZE))
|
| 24 |
+
|
| 25 |
+
def __call__(self, frame, threshold=0.5):
|
| 26 |
+
f_h, f_w, _ = frame.shape
|
| 27 |
+
|
| 28 |
+
faces = self.app.get(frame)
|
| 29 |
+
|
| 30 |
+
get_face_store = None
|
| 31 |
+
max_size = 0
|
| 32 |
+
|
| 33 |
+
if len(faces) == 0:
|
| 34 |
+
return None, None
|
| 35 |
+
else:
|
| 36 |
+
for face in faces:
|
| 37 |
+
bbox = face.bbox.astype(np.int_).tolist()
|
| 38 |
+
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
| 39 |
+
if w < 50 or h < 80:
|
| 40 |
+
continue
|
| 41 |
+
if w / h > 1.5 or w / h < 0.2:
|
| 42 |
+
continue
|
| 43 |
+
if face.det_score < threshold:
|
| 44 |
+
continue
|
| 45 |
+
size_now = w * h
|
| 46 |
+
|
| 47 |
+
if size_now > max_size:
|
| 48 |
+
max_size = size_now
|
| 49 |
+
get_face_store = face
|
| 50 |
+
|
| 51 |
+
if get_face_store is None:
|
| 52 |
+
return None, None
|
| 53 |
+
else:
|
| 54 |
+
face = get_face_store
|
| 55 |
+
lmk = np.round(face.landmark_2d_106).astype(np.int_)
|
| 56 |
+
|
| 57 |
+
halk_face_coord = np.mean([lmk[74], lmk[73]], axis=0)
|
| 58 |
+
|
| 59 |
+
sub_lmk = lmk[LMK_ADAPT_ORIGIN_ORDER]
|
| 60 |
+
halk_face_dist = np.max(sub_lmk[:, 1]) - halk_face_coord[1]
|
| 61 |
+
upper_bond = halk_face_coord[1] - halk_face_dist
|
| 62 |
+
|
| 63 |
+
x1, y1, x2, y2 = (np.min(sub_lmk[:, 0]), int(upper_bond), np.max(sub_lmk[:, 0]), np.max(sub_lmk[:, 1]))
|
| 64 |
+
|
| 65 |
+
if y2 - y1 <= 0 or x2 - x1 <= 0 or x1 < 0:
|
| 66 |
+
x1, y1, x2, y2 = face.bbox.astype(np.int_).tolist()
|
| 67 |
+
|
| 68 |
+
y2 += int((x2 - x1) * 0.1)
|
| 69 |
+
x1 -= int((x2 - x1) * 0.05)
|
| 70 |
+
x2 += int((x2 - x1) * 0.05)
|
| 71 |
+
|
| 72 |
+
x1 = max(0, x1)
|
| 73 |
+
y1 = max(0, y1)
|
| 74 |
+
x2 = min(f_w, x2)
|
| 75 |
+
y2 = min(f_h, y2)
|
| 76 |
+
|
| 77 |
+
return (x1, y1, x2, y2), lmk
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def cuda_to_int(cuda_str: str) -> int:
|
| 81 |
+
"""Convert the string with format "cuda:X" to integer X."""
|
| 82 |
+
if cuda_str == "cuda":
|
| 83 |
+
return 0
|
| 84 |
+
device = torch.device(cuda_str)
|
| 85 |
+
if device.type != "cuda":
|
| 86 |
+
raise ValueError(f"Device type must be 'cuda', got: {device.type}")
|
| 87 |
+
return device.index
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
LMK_ADAPT_ORIGIN_ORDER = [
|
| 91 |
+
1, 10, 12, 14, 16, 3, 5, 7, 0, 23, 21, 19, 32, 30, 28, 26,
|
| 92 |
+
17, 43, 48, 49, 51, 50, 102, 103, 104, 105, 101, 73, 74, 86,
|
| 93 |
+
]
|
OmniAvatar/utils/latentsync/image_processor.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Adapted from LatentSync
|
| 2 |
+
# Copyright (c) 2024 Bytedance Ltd. and/or its affiliates
|
| 3 |
+
# Licensed under the Apache License, Version 2.0
|
| 4 |
+
|
| 5 |
+
from torchvision import transforms
|
| 6 |
+
import cv2
|
| 7 |
+
from einops import rearrange
|
| 8 |
+
import torch
|
| 9 |
+
import numpy as np
|
| 10 |
+
from typing import Union
|
| 11 |
+
import os
|
| 12 |
+
from .affine_transform import AlignRestore
|
| 13 |
+
from .face_detector import FaceDetector
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def load_fixed_mask(resolution: int, mask_image_path=None) -> torch.Tensor:
|
| 17 |
+
"""Load the LatentSync mouth-shaped mask from PNG.
|
| 18 |
+
|
| 19 |
+
Returns [3, resolution, resolution] tensor. Values in [0, 1].
|
| 20 |
+
Semantics: 1.0 = keep (upper face), 0.0 = mask out (mouth region).
|
| 21 |
+
"""
|
| 22 |
+
if mask_image_path is None:
|
| 23 |
+
# Default to mask.png in the same directory as this module
|
| 24 |
+
mask_image_path = os.path.join(os.path.dirname(__file__), "mask.png")
|
| 25 |
+
mask_image = cv2.imread(mask_image_path)
|
| 26 |
+
mask_image = cv2.cvtColor(mask_image, cv2.COLOR_BGR2RGB)
|
| 27 |
+
mask_image = cv2.resize(mask_image, (resolution, resolution), interpolation=cv2.INTER_LANCZOS4) / 255.0
|
| 28 |
+
mask_image = rearrange(torch.from_numpy(mask_image), "h w c -> c h w")
|
| 29 |
+
return mask_image
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class ImageProcessor:
|
| 33 |
+
def __init__(self, resolution: int = 512, device: str = "cpu",
|
| 34 |
+
mask_image=None, insightface_root="checkpoints/auxiliary"):
|
| 35 |
+
self.resolution = resolution
|
| 36 |
+
self.resize = transforms.Resize(
|
| 37 |
+
(resolution, resolution), interpolation=transforms.InterpolationMode.BICUBIC, antialias=True
|
| 38 |
+
)
|
| 39 |
+
self.normalize = transforms.Normalize([0.5], [0.5], inplace=True)
|
| 40 |
+
|
| 41 |
+
self.restorer = AlignRestore(resolution=resolution, device=device)
|
| 42 |
+
|
| 43 |
+
if mask_image is None:
|
| 44 |
+
self.mask_image = load_fixed_mask(resolution)
|
| 45 |
+
else:
|
| 46 |
+
self.mask_image = mask_image
|
| 47 |
+
|
| 48 |
+
if device == "cpu":
|
| 49 |
+
self.face_detector = None
|
| 50 |
+
else:
|
| 51 |
+
self.face_detector = FaceDetector(device=device, insightface_root=insightface_root)
|
| 52 |
+
|
| 53 |
+
def affine_transform(self, image: np.ndarray):
|
| 54 |
+
"""Detect face and align to resolution x resolution via affine transform.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
image: np.ndarray [H, W, 3] uint8 RGB frame.
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
face: torch.Tensor [C, resolution, resolution] uint8
|
| 61 |
+
box: list [x1, y1, x2, y2] in aligned coordinate space
|
| 62 |
+
affine_matrix: torch.Tensor [1, 2, 3] affine transformation matrix
|
| 63 |
+
"""
|
| 64 |
+
if self.face_detector is None:
|
| 65 |
+
raise NotImplementedError("Using the CPU for face detection is not supported")
|
| 66 |
+
bbox, landmark_2d_106 = self.face_detector(image)
|
| 67 |
+
if bbox is None:
|
| 68 |
+
raise RuntimeError("Face not detected")
|
| 69 |
+
|
| 70 |
+
pt_left_eye = np.mean(landmark_2d_106[[43, 48, 49, 51, 50]], axis=0) # left eyebrow center
|
| 71 |
+
pt_right_eye = np.mean(landmark_2d_106[101:106], axis=0) # right eyebrow center
|
| 72 |
+
pt_nose = np.mean(landmark_2d_106[[74, 77, 83, 86]], axis=0) # nose center
|
| 73 |
+
|
| 74 |
+
landmarks3 = np.round([pt_left_eye, pt_right_eye, pt_nose])
|
| 75 |
+
|
| 76 |
+
face, affine_matrix = self.restorer.align_warp_face(image.copy(), landmarks3=landmarks3, smooth=True)
|
| 77 |
+
box = [0, 0, face.shape[1], face.shape[0]] # x1, y1, x2, y2
|
| 78 |
+
face = cv2.resize(face, (self.resolution, self.resolution), interpolation=cv2.INTER_LANCZOS4)
|
| 79 |
+
face = rearrange(torch.from_numpy(face), "h w c -> c h w")
|
| 80 |
+
return face, box, affine_matrix
|
| 81 |
+
|
| 82 |
+
def preprocess_fixed_mask_image(self, image: torch.Tensor, affine_transform=False):
|
| 83 |
+
if affine_transform:
|
| 84 |
+
image, _, _ = self.affine_transform(image)
|
| 85 |
+
else:
|
| 86 |
+
image = self.resize(image)
|
| 87 |
+
pixel_values = self.normalize(image / 255.0)
|
| 88 |
+
masked_pixel_values = pixel_values * self.mask_image
|
| 89 |
+
return pixel_values, masked_pixel_values, self.mask_image[0:1]
|
| 90 |
+
|
| 91 |
+
def prepare_masks_and_masked_images(self, images: Union[torch.Tensor, np.ndarray], affine_transform=False):
|
| 92 |
+
if isinstance(images, np.ndarray):
|
| 93 |
+
images = torch.from_numpy(images)
|
| 94 |
+
if images.shape[3] == 3:
|
| 95 |
+
images = rearrange(images, "f h w c -> f c h w")
|
| 96 |
+
|
| 97 |
+
results = [self.preprocess_fixed_mask_image(image, affine_transform=affine_transform) for image in images]
|
| 98 |
+
|
| 99 |
+
pixel_values_list, masked_pixel_values_list, masks_list = list(zip(*results))
|
| 100 |
+
return torch.stack(pixel_values_list), torch.stack(masked_pixel_values_list), torch.stack(masks_list)
|
| 101 |
+
|
| 102 |
+
def process_images(self, images: Union[torch.Tensor, np.ndarray]):
|
| 103 |
+
if isinstance(images, np.ndarray):
|
| 104 |
+
images = torch.from_numpy(images)
|
| 105 |
+
if images.shape[3] == 3:
|
| 106 |
+
images = rearrange(images, "f h w c -> f c h w")
|
| 107 |
+
images = self.resize(images)
|
| 108 |
+
pixel_values = self.normalize(images / 255.0)
|
| 109 |
+
return pixel_values
|
README.md
CHANGED
|
@@ -1,13 +1,33 @@
|
|
| 1 |
---
|
| 2 |
title: Lip Forcing
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.20.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Lip Forcing
|
| 3 |
+
emoji: 🗣️
|
| 4 |
+
colorFrom: indigo
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
|
|
|
|
|
|
| 7 |
app_file: app.py
|
| 8 |
pinned: false
|
| 9 |
+
python_version: "3.10"
|
| 10 |
+
short_description: Few-step autoregressive diffusion for real-time lip sync
|
| 11 |
+
startup_duration_timeout: 60m
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Lip Forcing
|
| 15 |
+
|
| 16 |
+
Gradio demo for **Lip Forcing: Few-Step Autoregressive Diffusion for Real-time
|
| 17 |
+
Lip Synchronization** (KAIST AI · AIPARK), running the released self-contained
|
| 18 |
+
**14B student** checkpoint.
|
| 19 |
+
|
| 20 |
+
Given a talking-head reference video and a driving audio clip, the model detects
|
| 21 |
+
and aligns the face to 512×512, then regenerates the mouth region to match the
|
| 22 |
+
audio using a causal 2-step autoregressive diffusion student, and composites the
|
| 23 |
+
result back into the original frames.
|
| 24 |
+
|
| 25 |
+
- Paper: https://arxiv.org/abs/2606.11180
|
| 26 |
+
- Project page: https://cvlab-kaist.github.io/LipForcing/
|
| 27 |
+
- Code: https://github.com/cvlab-kaist/LipForcing
|
| 28 |
+
- Weights: https://huggingface.co/JinhyukJang/lipforcing
|
| 29 |
+
|
| 30 |
+
This demo reproduces the official streaming inference pipeline
|
| 31 |
+
(`scripts/inference/inference_streaming.py`) 1:1 on ZeroGPU. Driving audio is
|
| 32 |
+
capped to the first few seconds per request to keep a single call within the GPU
|
| 33 |
+
time budget.
|
app.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lip Forcing — few-step autoregressive diffusion for real-time lip synchronization.
|
| 2 |
+
|
| 3 |
+
ZeroGPU Gradio demo for the released 14B student
|
| 4 |
+
(https://huggingface.co/JinhyukJang/lipforcing). Given a talking-head reference
|
| 5 |
+
video and a driving audio clip, it re-synchronizes the mouth to the audio using
|
| 6 |
+
the streaming per-chunk AR pipeline from the official repo
|
| 7 |
+
(scripts/inference/inference_streaming.py), reproduced 1:1 here.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
# Allocator: the streaming AR loop has transient spikes (VAE encode/decode of
|
| 13 |
+
# 512x512 chunks + KV cache). expandable segments avoids fragmentation OOMs.
|
| 14 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 15 |
+
os.environ.setdefault("ORT_DISABLE_THREAD_AFFINITY", "1")
|
| 16 |
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
| 17 |
+
|
| 18 |
+
import spaces # noqa: E402 — must precede torch / CUDA-touching imports
|
| 19 |
+
|
| 20 |
+
import sys
|
| 21 |
+
import types
|
| 22 |
+
import tempfile
|
| 23 |
+
import traceback
|
| 24 |
+
|
| 25 |
+
import numpy as np
|
| 26 |
+
import torch
|
| 27 |
+
import gradio as gr
|
| 28 |
+
from PIL import Image
|
| 29 |
+
from huggingface_hub import hf_hub_download, snapshot_download
|
| 30 |
+
|
| 31 |
+
# The inference scripts import their helpers as top-level modules
|
| 32 |
+
# (`from _common import ...`), so make scripts/inference importable that way.
|
| 33 |
+
REPO_ROOT = os.path.dirname(os.path.abspath(__file__))
|
| 34 |
+
sys.path.insert(0, REPO_ROOT)
|
| 35 |
+
sys.path.insert(0, os.path.join(REPO_ROOT, "scripts", "inference"))
|
| 36 |
+
|
| 37 |
+
# ---------------------------------------------------------------------------
|
| 38 |
+
# Weights (downloaded once at startup into the HF cache)
|
| 39 |
+
# ---------------------------------------------------------------------------
|
| 40 |
+
print("Downloading weights ...", flush=True)
|
| 41 |
+
|
| 42 |
+
CKPT_PATH = hf_hub_download("JinhyukJang/lipforcing", "lipforcing_14b.pth")
|
| 43 |
+
|
| 44 |
+
WAN_REPO = "Wan-AI/Wan2.1-T2V-14B"
|
| 45 |
+
VAE_PATH = hf_hub_download(WAN_REPO, "Wan2.1_VAE.pth")
|
| 46 |
+
T5_PATH = hf_hub_download(WAN_REPO, "models_t5_umt5-xxl-enc-bf16.pth")
|
| 47 |
+
# UMT5 tokenizer (lives under google/umt5-xxl/ inside the Wan repo)
|
| 48 |
+
for _f in (
|
| 49 |
+
"google/umt5-xxl/special_tokens_map.json",
|
| 50 |
+
"google/umt5-xxl/spiece.model",
|
| 51 |
+
"google/umt5-xxl/tokenizer.json",
|
| 52 |
+
"google/umt5-xxl/tokenizer_config.json",
|
| 53 |
+
):
|
| 54 |
+
hf_hub_download(WAN_REPO, _f)
|
| 55 |
+
# T5_PATH's parent dir now also holds google/umt5-xxl/* (same snapshot dir).
|
| 56 |
+
|
| 57 |
+
WAV2VEC_DIR = snapshot_download("facebook/wav2vec2-base-960h")
|
| 58 |
+
|
| 59 |
+
# TAEW tiny streaming decoder + LatentSync mouth mask.
|
| 60 |
+
# taew2_1.pth lives in the taehv GitHub repo; mask.png in the LatentSync repo.
|
| 61 |
+
import urllib.request # noqa: E402
|
| 62 |
+
_cache = os.path.join(tempfile.gettempdir(), "lipforcing_assets")
|
| 63 |
+
os.makedirs(_cache, exist_ok=True)
|
| 64 |
+
|
| 65 |
+
TAEHV_CKPT = os.path.join(_cache, "taew2_1.pth")
|
| 66 |
+
if not os.path.exists(TAEHV_CKPT):
|
| 67 |
+
urllib.request.urlretrieve(
|
| 68 |
+
"https://raw.githubusercontent.com/madebyollin/taehv/main/taew2_1.pth",
|
| 69 |
+
TAEHV_CKPT,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
MASK_PATH = os.path.join(_cache, "mask.png")
|
| 73 |
+
if not os.path.exists(MASK_PATH):
|
| 74 |
+
urllib.request.urlretrieve(
|
| 75 |
+
"https://raw.githubusercontent.com/bytedance/LatentSync/main/latentsync/utils/mask.png",
|
| 76 |
+
MASK_PATH,
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
print("Weights downloaded.", flush=True)
|
| 80 |
+
|
| 81 |
+
DTYPE = torch.bfloat16
|
| 82 |
+
DEVICE = "cuda"
|
| 83 |
+
|
| 84 |
+
# ---------------------------------------------------------------------------
|
| 85 |
+
# Args shim — the loaders/helpers read attributes off an argparse-like object.
|
| 86 |
+
# We build one with the released 14B student's default (2-step t769) schedule.
|
| 87 |
+
# ---------------------------------------------------------------------------
|
| 88 |
+
def _make_args():
|
| 89 |
+
a = types.SimpleNamespace()
|
| 90 |
+
a.ckpt_path = CKPT_PATH
|
| 91 |
+
a.vae_path = VAE_PATH
|
| 92 |
+
a.wav2vec_path = WAV2VEC_DIR
|
| 93 |
+
a.mask_path = MASK_PATH
|
| 94 |
+
a.taehv_ckpt = TAEHV_CKPT
|
| 95 |
+
a.base_model_paths = None
|
| 96 |
+
a.omniavatar_ckpt_path = None
|
| 97 |
+
a.model_size = "14B"
|
| 98 |
+
a.merge_lora_post_load = True
|
| 99 |
+
a.text_embeds_path = None
|
| 100 |
+
a.text_encoder_path = None # text encoded once at startup (below)
|
| 101 |
+
a.prompt = "a person talking"
|
| 102 |
+
a.streaming_decoder = "streaming_taehv"
|
| 103 |
+
a.t_list = [0.999, 0.769, 0.0] # released 14B 2-step schedule
|
| 104 |
+
a.chunk_size = 3
|
| 105 |
+
a.num_latent_frames = None
|
| 106 |
+
a.min_latent_frames = 0
|
| 107 |
+
a.context_noise = 0.0
|
| 108 |
+
a.seed = 42
|
| 109 |
+
a.fps = 25.0
|
| 110 |
+
a.dtype = "bf16"
|
| 111 |
+
a.device = DEVICE
|
| 112 |
+
a.local_attn_size = 7
|
| 113 |
+
a.sink_size = 1
|
| 114 |
+
a.use_dynamic_rope = True
|
| 115 |
+
a.skip_preprocessing = False
|
| 116 |
+
a.face_cache_dir = None
|
| 117 |
+
a.composite_full_face = False
|
| 118 |
+
a.streamwise_encode = True
|
| 119 |
+
a.defer_composite = False
|
| 120 |
+
a.compile = False
|
| 121 |
+
a.input_dir = None
|
| 122 |
+
a.output_dir = None
|
| 123 |
+
a.video_path = None
|
| 124 |
+
a.audio_path = None
|
| 125 |
+
a.output_path = None
|
| 126 |
+
return a
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
ARGS = _make_args()
|
| 130 |
+
|
| 131 |
+
# ---------------------------------------------------------------------------
|
| 132 |
+
# Text embedding: encode the default prompt ONCE on CPU, then free the 11 GB
|
| 133 |
+
# UMT5-XXL encoder. This keeps the encoder off the GPU so peak VRAM stays low
|
| 134 |
+
# (~37 GB), per the model card's "48 GB cards work with precomputed embeddings".
|
| 135 |
+
# ---------------------------------------------------------------------------
|
| 136 |
+
def _precompute_text_embeds(prompt: str) -> torch.Tensor:
|
| 137 |
+
from OmniAvatar.models.wan_video_text_encoder import WanTextEncoder
|
| 138 |
+
from OmniAvatar.prompters.wan_prompter import WanPrompter
|
| 139 |
+
from lipforcing import preprocess as pp
|
| 140 |
+
|
| 141 |
+
print(f"Encoding text prompt on CPU: {prompt!r} ...", flush=True)
|
| 142 |
+
text_encoder = WanTextEncoder()
|
| 143 |
+
te_state = torch.load(T5_PATH, map_location="cpu", weights_only=False)
|
| 144 |
+
converter = WanTextEncoder.state_dict_converter()
|
| 145 |
+
te_state = converter.from_civitai(te_state)
|
| 146 |
+
text_encoder.load_state_dict(te_state, strict=True)
|
| 147 |
+
text_encoder = text_encoder.to("cpu").eval()
|
| 148 |
+
|
| 149 |
+
tokenizer_path = pp._resolve_tokenizer_path(T5_PATH)
|
| 150 |
+
prompter = WanPrompter(tokenizer_path=tokenizer_path, text_len=512)
|
| 151 |
+
prompter.fetch_models(text_encoder=text_encoder)
|
| 152 |
+
with torch.no_grad():
|
| 153 |
+
emb = prompter.encode_prompt(prompt, positive=True, device="cpu")
|
| 154 |
+
if emb.dim() == 2:
|
| 155 |
+
emb = emb.unsqueeze(0)
|
| 156 |
+
emb = emb.to(dtype=DTYPE).contiguous()
|
| 157 |
+
del text_encoder, prompter, te_state
|
| 158 |
+
import gc
|
| 159 |
+
gc.collect()
|
| 160 |
+
print(f"Text embeds: {tuple(emb.shape)}", flush=True)
|
| 161 |
+
return emb
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
TEXT_EMBEDS_CPU = _precompute_text_embeds(ARGS.prompt)
|
| 165 |
+
|
| 166 |
+
# ---------------------------------------------------------------------------
|
| 167 |
+
# Models — loaded at module scope, .to("cuda") intercepted by ZeroGPU.
|
| 168 |
+
# ---------------------------------------------------------------------------
|
| 169 |
+
print("Loading diffusion model (14B student) ...", flush=True)
|
| 170 |
+
from _loader import load_diffusion_model # noqa: E402
|
| 171 |
+
from _common import ( # noqa: E402
|
| 172 |
+
TAEHVDecoderWrapper, load_vae, load_wav2vec,
|
| 173 |
+
resolve_audio, compute_generation_length,
|
| 174 |
+
load_image_processor, preprocess_with_latentsync,
|
| 175 |
+
build_condition_streamwise,
|
| 176 |
+
)
|
| 177 |
+
from inference_streaming import run_streaming_pipeline # noqa: E402
|
| 178 |
+
|
| 179 |
+
MODEL = load_diffusion_model(ARGS, DEVICE, DTYPE)
|
| 180 |
+
|
| 181 |
+
print("Loading Wan VAE ...", flush=True)
|
| 182 |
+
VAE = load_vae(ARGS.vae_path, DEVICE)
|
| 183 |
+
|
| 184 |
+
print("Loading TAEHV decoder ...", flush=True)
|
| 185 |
+
DECODER_VAE = TAEHVDecoderWrapper(ARGS.taehv_ckpt, DEVICE)
|
| 186 |
+
|
| 187 |
+
print("Loading Wav2Vec2 ...", flush=True)
|
| 188 |
+
WAV2VEC_MODEL, WAV2VEC_EXTRACTOR = load_wav2vec(ARGS.wav2vec_path, DEVICE)
|
| 189 |
+
|
| 190 |
+
# LatentSync face detector / aligner uses insightface + onnxruntime; those need
|
| 191 |
+
# a live GPU context, so it is initialized lazily inside the GPU call.
|
| 192 |
+
IMAGE_PROCESSOR = None
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def _get_image_processor():
|
| 196 |
+
global IMAGE_PROCESSOR
|
| 197 |
+
if IMAGE_PROCESSOR is None:
|
| 198 |
+
IMAGE_PROCESSOR = load_image_processor(ARGS.mask_path, DEVICE)
|
| 199 |
+
return IMAGE_PROCESSOR
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# ---------------------------------------------------------------------------
|
| 203 |
+
# Inference
|
| 204 |
+
# ---------------------------------------------------------------------------
|
| 205 |
+
MAX_SECONDS = 8.0 # cap driving audio so a single call stays within GPU budget
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def _estimate_duration(video_path, audio_path, *a, **k):
|
| 209 |
+
# 14B student: streaming AR + face detect/composite. Budget generously per
|
| 210 |
+
# second of (capped) audio, plus fixed preprocessing/warmup overhead.
|
| 211 |
+
return 300
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
@spaces.GPU(duration=_estimate_duration)
|
| 215 |
+
def lip_sync(video_path: str, audio_path: str,
|
| 216 |
+
seed: int = 42) -> str:
|
| 217 |
+
"""Lip-sync a talking-head video to a driving audio clip.
|
| 218 |
+
|
| 219 |
+
Args:
|
| 220 |
+
video_path: reference talking-head video (any resolution; a single
|
| 221 |
+
clear front-facing face is detected, aligned to 512x512, and the
|
| 222 |
+
mouth region is regenerated to match the audio).
|
| 223 |
+
audio_path: driving speech audio; the output length follows the audio
|
| 224 |
+
(capped to keep a single request within the GPU budget).
|
| 225 |
+
seed: RNG seed for reproducibility.
|
| 226 |
+
|
| 227 |
+
Returns:
|
| 228 |
+
Path to the generated lip-synced mp4 (muxed with the driving audio).
|
| 229 |
+
"""
|
| 230 |
+
if not video_path:
|
| 231 |
+
raise gr.Error("Please provide a reference talking-head video.")
|
| 232 |
+
if not audio_path:
|
| 233 |
+
raise gr.Error("Please provide a driving audio clip.")
|
| 234 |
+
|
| 235 |
+
import imageio_ffmpeg
|
| 236 |
+
import subprocess
|
| 237 |
+
|
| 238 |
+
args = _make_args()
|
| 239 |
+
args.seed = int(seed)
|
| 240 |
+
args.video_path = video_path
|
| 241 |
+
args.audio_path = audio_path
|
| 242 |
+
|
| 243 |
+
torch.manual_seed(args.seed)
|
| 244 |
+
torch.cuda.manual_seed_all(args.seed)
|
| 245 |
+
|
| 246 |
+
image_processor = _get_image_processor()
|
| 247 |
+
|
| 248 |
+
# Cap audio length so runtime stays bounded.
|
| 249 |
+
ff = imageio_ffmpeg.get_ffmpeg_exe()
|
| 250 |
+
capped_audio = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
|
| 251 |
+
subprocess.run(
|
| 252 |
+
[ff, "-y", "-loglevel", "error", "-nostdin", "-i", audio_path,
|
| 253 |
+
"-t", str(MAX_SECONDS), "-ar", "16000", "-ac", "1", capped_audio],
|
| 254 |
+
check=True,
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
|
| 258 |
+
|
| 259 |
+
tmp_audio = None
|
| 260 |
+
try:
|
| 261 |
+
used_audio, tmp_audio = resolve_audio(audio_path=capped_audio)
|
| 262 |
+
|
| 263 |
+
num_latent_frames, num_video_frames = compute_generation_length(
|
| 264 |
+
used_audio, args.num_latent_frames, args.chunk_size, args.fps,
|
| 265 |
+
min_latent_frames=args.min_latent_frames,
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
print("Face detection + 512x512 alignment ...", flush=True)
|
| 269 |
+
meta = preprocess_with_latentsync(
|
| 270 |
+
args.video_path, image_processor, args.face_cache_dir,
|
| 271 |
+
num_frames=num_video_frames,
|
| 272 |
+
)
|
| 273 |
+
if meta is None:
|
| 274 |
+
raise gr.Error(
|
| 275 |
+
"Face detection failed — please provide a video with a single, "
|
| 276 |
+
"clear, front-facing talking head."
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
aligned_faces = meta["aligned_faces"]
|
| 280 |
+
ref_frames_np = np.stack([
|
| 281 |
+
f.permute(1, 2, 0).numpy() if isinstance(f, torch.Tensor) else f
|
| 282 |
+
for f in aligned_faces[:num_video_frames]
|
| 283 |
+
], axis=0)
|
| 284 |
+
|
| 285 |
+
text_embeds = TEXT_EMBEDS_CPU.to(device=DEVICE, dtype=DTYPE)
|
| 286 |
+
|
| 287 |
+
condition, video_tensor, masked_video_tensor = build_condition_streamwise(
|
| 288 |
+
VAE, WAV2VEC_MODEL, WAV2VEC_EXTRACTOR,
|
| 289 |
+
ref_frames_np, used_audio, text_embeds, args.mask_path,
|
| 290 |
+
num_video_frames, num_latent_frames, DEVICE, DTYPE,
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
print("Running streaming pipeline ...", flush=True)
|
| 294 |
+
run_streaming_pipeline(
|
| 295 |
+
MODEL, DECODER_VAE, VAE, condition,
|
| 296 |
+
num_latent_frames, num_video_frames,
|
| 297 |
+
args, meta, image_processor,
|
| 298 |
+
used_audio, out_path, DEVICE, DTYPE,
|
| 299 |
+
video_tensor=video_tensor,
|
| 300 |
+
masked_video_tensor=masked_video_tensor,
|
| 301 |
+
)
|
| 302 |
+
except gr.Error:
|
| 303 |
+
raise
|
| 304 |
+
except Exception as e:
|
| 305 |
+
traceback.print_exc()
|
| 306 |
+
raise gr.Error(f"Inference failed: {e}")
|
| 307 |
+
finally:
|
| 308 |
+
MODEL.clear_caches()
|
| 309 |
+
torch.cuda.empty_cache()
|
| 310 |
+
if tmp_audio and os.path.exists(tmp_audio):
|
| 311 |
+
os.remove(tmp_audio)
|
| 312 |
+
if os.path.exists(capped_audio):
|
| 313 |
+
os.remove(capped_audio)
|
| 314 |
+
|
| 315 |
+
return out_path
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
# ---------------------------------------------------------------------------
|
| 319 |
+
# UI
|
| 320 |
+
# ---------------------------------------------------------------------------
|
| 321 |
+
CSS = """
|
| 322 |
+
#col-container { max-width: 1100px; margin: 0 auto; }
|
| 323 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 324 |
+
"""
|
| 325 |
+
|
| 326 |
+
DESCRIPTION = """
|
| 327 |
+
# Lip Forcing 🗣️
|
| 328 |
+
**Few-Step Autoregressive Diffusion for Real-time Lip Synchronization** ·
|
| 329 |
+
14B student ·
|
| 330 |
+
[Paper](https://arxiv.org/abs/2606.11180) ·
|
| 331 |
+
[Project](https://cvlab-kaist.github.io/LipForcing/) ·
|
| 332 |
+
[Code](https://github.com/cvlab-kaist/LipForcing) ·
|
| 333 |
+
[Weights](https://huggingface.co/JinhyukJang/lipforcing)
|
| 334 |
+
|
| 335 |
+
Give it a **talking-head video** and a **driving audio** clip — it detects and aligns
|
| 336 |
+
the face, then regenerates the mouth to match the audio with a 2-step causal diffusion
|
| 337 |
+
student. Audio is capped to the first few seconds per run.
|
| 338 |
+
"""
|
| 339 |
+
|
| 340 |
+
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
|
| 341 |
+
with gr.Column(elem_id="col-container"):
|
| 342 |
+
gr.Markdown(DESCRIPTION)
|
| 343 |
+
with gr.Row():
|
| 344 |
+
with gr.Column():
|
| 345 |
+
video_in = gr.Video(label="Reference talking-head video", height=340)
|
| 346 |
+
audio_in = gr.Audio(label="Driving audio", type="filepath")
|
| 347 |
+
run_btn = gr.Button("Lip-sync", variant="primary")
|
| 348 |
+
with gr.Column():
|
| 349 |
+
video_out = gr.Video(label="Lip-synced result", height=340)
|
| 350 |
+
with gr.Accordion("Advanced settings", open=False):
|
| 351 |
+
seed = gr.Number(label="Seed", value=42, precision=0)
|
| 352 |
+
|
| 353 |
+
run_btn.click(
|
| 354 |
+
fn=lip_sync,
|
| 355 |
+
inputs=[video_in, audio_in, seed],
|
| 356 |
+
outputs=video_out,
|
| 357 |
+
api_name="lip_sync",
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
gr.Examples(
|
| 361 |
+
examples=[
|
| 362 |
+
["examples/example1_video.mp4", "examples/example1_audio.wav"],
|
| 363 |
+
["examples/example2_video.mp4", "examples/example2_audio.wav"],
|
| 364 |
+
],
|
| 365 |
+
inputs=[video_in, audio_in],
|
| 366 |
+
outputs=video_out,
|
| 367 |
+
fn=lip_sync,
|
| 368 |
+
cache_examples=True,
|
| 369 |
+
cache_mode="lazy",
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
if __name__ == "__main__":
|
| 373 |
+
demo.launch(mcp_server=True)
|
examples/example1_audio.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fe2f4abfdfa42bb474880cd265071d10c5c743f2dbd4f48abbc06a361ede731f
|
| 3 |
+
size 128078
|
examples/example1_video.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9bbdab9fe0231c662b44897146e52a4a604db27f3f55e81a8c1e3a2a664989a2
|
| 3 |
+
size 896877
|
examples/example2_audio.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dfb6202c7411ac57d16084ac2722c8fb88e1976eef544f169eb6e6e9c5286aec
|
| 3 |
+
size 128078
|
examples/example2_video.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:13ac3d03a4f9f2a81f2d337f6f07283f58f5f19ecb8901dc786a5575f36fd97a
|
| 3 |
+
size 634917
|
lipforcing/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
lipforcing/callbacks/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
lipforcing/callbacks/callback.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
from typing import Callable, Any, TYPE_CHECKING
|
| 6 |
+
import torch
|
| 7 |
+
from torch.utils.data import DataLoader
|
| 8 |
+
|
| 9 |
+
from lipforcing.utils import instantiate
|
| 10 |
+
import lipforcing.utils.logging_utils as logger
|
| 11 |
+
|
| 12 |
+
if TYPE_CHECKING:
|
| 13 |
+
from lipforcing.configs.config import BaseConfig
|
| 14 |
+
from lipforcing.trainer import Trainer
|
| 15 |
+
from lipforcing.methods import FastGenModel
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class CallbackDict:
|
| 19 |
+
def __init__(self, config: BaseConfig, trainer: Trainer):
|
| 20 |
+
self._callbacks = {}
|
| 21 |
+
callback_configs = config.trainer.callbacks
|
| 22 |
+
if callback_configs:
|
| 23 |
+
if isinstance(callback_configs, list):
|
| 24 |
+
logger.warning(msg="The 'config.trainer.callbacks' parameter should be a dict instead of a list. ")
|
| 25 |
+
callback_configs = {f"callback_{k}": v for k, v in enumerate(callback_configs)}
|
| 26 |
+
for callback_name, current_callback_cfg in callback_configs.items():
|
| 27 |
+
if "_target_" not in current_callback_cfg:
|
| 28 |
+
logger.critical(
|
| 29 |
+
f"Callback {callback_name} is missing the '_target_' field. \n Skip {current_callback_cfg}"
|
| 30 |
+
)
|
| 31 |
+
continue
|
| 32 |
+
logger.critical(f"Instantiating callback {callback_name}: {current_callback_cfg}")
|
| 33 |
+
_callback = instantiate(current_callback_cfg)
|
| 34 |
+
assert isinstance(_callback, Callback), f"{current_callback_cfg} is not a valid callback."
|
| 35 |
+
_callback.config = config
|
| 36 |
+
_callback.trainer = trainer
|
| 37 |
+
_callback.on_app_begin()
|
| 38 |
+
self._callbacks[callback_name] = _callback
|
| 39 |
+
|
| 40 |
+
def __getattr__(self, method_name: str) -> Callable:
|
| 41 |
+
def load_state_dict(state_dict: dict[str, Any]) -> None:
|
| 42 |
+
for name, callback in self._callbacks.items():
|
| 43 |
+
if name in state_dict:
|
| 44 |
+
callback.load_state_dict(state_dict[name])
|
| 45 |
+
else:
|
| 46 |
+
logger.warning(f"Callback {name} not found in checkpoint.")
|
| 47 |
+
|
| 48 |
+
def state_dict() -> dict[str, Any]:
|
| 49 |
+
return {name: self._callbacks[name].state_dict() for name in self._callbacks}
|
| 50 |
+
|
| 51 |
+
def callbacks_wrapper(*args, **kwargs):
|
| 52 |
+
for callback in self._callbacks.values():
|
| 53 |
+
assert hasattr(callback, method_name)
|
| 54 |
+
method = getattr(callback, method_name)
|
| 55 |
+
assert callable(method), f"{method_name} is not callable."
|
| 56 |
+
method(*args, **kwargs)
|
| 57 |
+
|
| 58 |
+
if method_name == "state_dict":
|
| 59 |
+
return state_dict
|
| 60 |
+
if method_name == "load_state_dict":
|
| 61 |
+
return load_state_dict
|
| 62 |
+
return callbacks_wrapper
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class Callback:
|
| 66 |
+
config: "BaseConfig"
|
| 67 |
+
trainer: "Trainer"
|
| 68 |
+
|
| 69 |
+
def on_app_begin(self) -> None:
|
| 70 |
+
pass
|
| 71 |
+
|
| 72 |
+
def on_model_init_start(self, model: FastGenModel) -> None:
|
| 73 |
+
pass
|
| 74 |
+
|
| 75 |
+
def on_model_init_end(self, model: FastGenModel | torch.nn.parallel.DistributedDataParallel) -> None:
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
def on_optimizer_init_start(self, model: FastGenModel) -> None:
|
| 79 |
+
pass
|
| 80 |
+
|
| 81 |
+
def on_optimizer_init_end(self, model: FastGenModel) -> None:
|
| 82 |
+
pass
|
| 83 |
+
|
| 84 |
+
def on_load_checkpoint_start(self, model: FastGenModel) -> None:
|
| 85 |
+
pass
|
| 86 |
+
|
| 87 |
+
def on_load_checkpoint_end(self, model: FastGenModel, iteration: int = 0) -> None:
|
| 88 |
+
pass
|
| 89 |
+
|
| 90 |
+
def on_dataloader_init_start(self, model: FastGenModel, iteration: int = 0) -> None:
|
| 91 |
+
pass
|
| 92 |
+
|
| 93 |
+
def on_dataloader_init_end(
|
| 94 |
+
self, model: FastGenModel, dataloader_train: DataLoader, dataloader_val: DataLoader, iteration: int = 0
|
| 95 |
+
) -> None:
|
| 96 |
+
pass
|
| 97 |
+
|
| 98 |
+
def on_train_begin(self, model: FastGenModel, iteration: int = 0) -> None:
|
| 99 |
+
pass
|
| 100 |
+
|
| 101 |
+
def on_training_step_begin(
|
| 102 |
+
self,
|
| 103 |
+
model: FastGenModel,
|
| 104 |
+
iteration: int = 0,
|
| 105 |
+
) -> None:
|
| 106 |
+
pass
|
| 107 |
+
|
| 108 |
+
def on_training_accum_step_begin(
|
| 109 |
+
self,
|
| 110 |
+
model: FastGenModel,
|
| 111 |
+
data_batch: dict[str, torch.Tensor],
|
| 112 |
+
iteration: int = 0,
|
| 113 |
+
accum_iter: int = 0,
|
| 114 |
+
) -> None:
|
| 115 |
+
pass
|
| 116 |
+
|
| 117 |
+
def on_backward_begin(
|
| 118 |
+
self,
|
| 119 |
+
model: FastGenModel,
|
| 120 |
+
data_batch: dict[str, torch.Tensor],
|
| 121 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 122 |
+
loss_dict: dict[str, torch.Tensor],
|
| 123 |
+
iteration: int = 0,
|
| 124 |
+
accum_iter: int = 0,
|
| 125 |
+
) -> None:
|
| 126 |
+
pass
|
| 127 |
+
|
| 128 |
+
def on_training_step_end(
|
| 129 |
+
self,
|
| 130 |
+
model: FastGenModel,
|
| 131 |
+
data_batch: dict[str, torch.Tensor],
|
| 132 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 133 |
+
loss_dict: dict[str, torch.Tensor],
|
| 134 |
+
iteration: int = 0,
|
| 135 |
+
) -> None:
|
| 136 |
+
pass
|
| 137 |
+
|
| 138 |
+
def on_optimizer_step_begin(self, model: FastGenModel, iteration: int = 0) -> None:
|
| 139 |
+
pass
|
| 140 |
+
|
| 141 |
+
def on_train_end(self, model: FastGenModel, iteration: int = 0) -> None:
|
| 142 |
+
pass
|
| 143 |
+
|
| 144 |
+
def on_validation_begin(self, model: FastGenModel, iteration: int = 0, idx: int = 0) -> None:
|
| 145 |
+
pass
|
| 146 |
+
|
| 147 |
+
def on_validation_step_begin(
|
| 148 |
+
self, model: FastGenModel, data_batch: dict[str, torch.Tensor], step: int = 0, iteration: int = 0, idx: int = 0
|
| 149 |
+
) -> None:
|
| 150 |
+
pass
|
| 151 |
+
|
| 152 |
+
def on_validation_step_end(
|
| 153 |
+
self,
|
| 154 |
+
model: FastGenModel,
|
| 155 |
+
data_batch: dict[str, torch.Tensor],
|
| 156 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 157 |
+
loss_dict: dict[str, torch.Tensor],
|
| 158 |
+
step: int = 0,
|
| 159 |
+
iteration: int = 0,
|
| 160 |
+
idx: int = 0,
|
| 161 |
+
) -> None:
|
| 162 |
+
pass
|
| 163 |
+
|
| 164 |
+
def on_validation_end(self, model: FastGenModel, iteration: int = 0, idx: int = 0) -> None:
|
| 165 |
+
pass
|
| 166 |
+
|
| 167 |
+
def on_save_checkpoint_start(self, model: FastGenModel, iteration: int = 0) -> None:
|
| 168 |
+
pass
|
| 169 |
+
|
| 170 |
+
def on_save_checkpoint_success(self, model: FastGenModel, iteration: int = 0, path: str = None) -> None:
|
| 171 |
+
pass
|
| 172 |
+
|
| 173 |
+
def on_save_checkpoint_end(self, model: FastGenModel, iteration: int = 0) -> None:
|
| 174 |
+
pass
|
| 175 |
+
|
| 176 |
+
def on_app_end(self, model: FastGenModel, iteration: int = 0) -> None:
|
| 177 |
+
pass
|
| 178 |
+
|
| 179 |
+
def state_dict(self) -> dict[str, Any]:
|
| 180 |
+
return {}
|
| 181 |
+
|
| 182 |
+
def load_state_dict(self, state_dict: dict[str, Any]) -> None:
|
| 183 |
+
pass
|
lipforcing/callbacks/ct_schedule.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
from typing import Callable, TYPE_CHECKING
|
| 6 |
+
import wandb
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from lipforcing.callbacks.callback import Callback
|
| 11 |
+
import lipforcing.utils.logging_utils as logger
|
| 12 |
+
from lipforcing.utils.basic_utils import get_batch_size_total
|
| 13 |
+
from lipforcing.utils.distributed import is_rank0
|
| 14 |
+
|
| 15 |
+
if TYPE_CHECKING:
|
| 16 |
+
from lipforcing.methods import FastGenModel
|
| 17 |
+
from lipforcing.configs.config import BaseConfig
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class CTScheduleCallback(Callback):
|
| 21 |
+
config: "BaseConfig"
|
| 22 |
+
|
| 23 |
+
def __init__(
|
| 24 |
+
self,
|
| 25 |
+
q: float = 2.0,
|
| 26 |
+
ratio_limit: float = 0.999,
|
| 27 |
+
kimg_per_stage: int = 12500,
|
| 28 |
+
batch_size: int = 1,
|
| 29 |
+
):
|
| 30 |
+
self.q = q
|
| 31 |
+
self.ratio_limit = ratio_limit
|
| 32 |
+
self.kimg_per_stage = kimg_per_stage
|
| 33 |
+
self.batch_size = batch_size
|
| 34 |
+
|
| 35 |
+
self.stage = 0
|
| 36 |
+
self.ratio = 0.0
|
| 37 |
+
|
| 38 |
+
def _get_cur_stage(self, model, iteration):
|
| 39 |
+
# Start from the saved iteration of the first-stage model in TCM
|
| 40 |
+
if hasattr(model, "resume_iter"):
|
| 41 |
+
assert isinstance(model.resume_iter, int)
|
| 42 |
+
iteration = iteration + model.resume_iter
|
| 43 |
+
|
| 44 |
+
batch_size = self.batch_size
|
| 45 |
+
if hasattr(self, "config"):
|
| 46 |
+
# override the batch_size using self.config
|
| 47 |
+
batch_size = get_batch_size_total(self.config)
|
| 48 |
+
|
| 49 |
+
cur_nimg = iteration * batch_size
|
| 50 |
+
stage = cur_nimg // (self.kimg_per_stage * 1000)
|
| 51 |
+
return stage, cur_nimg
|
| 52 |
+
|
| 53 |
+
def _update_schedule(self, stage):
|
| 54 |
+
self.stage = stage
|
| 55 |
+
self.ratio = 1 - 1 / self.q ** (stage + 1)
|
| 56 |
+
if self.ratio > self.ratio_limit:
|
| 57 |
+
logger.info(f"Clipping ratio from {self.ratio} -> {self.ratio_limit}")
|
| 58 |
+
self.ratio = self.ratio_limit
|
| 59 |
+
|
| 60 |
+
def on_train_begin(self, model: FastGenModel, iteration: int = 0) -> None:
|
| 61 |
+
stage, _ = self._get_cur_stage(model, iteration)
|
| 62 |
+
self._update_schedule(stage)
|
| 63 |
+
setattr(model, "ratio", self.ratio)
|
| 64 |
+
|
| 65 |
+
def on_training_step_end(
|
| 66 |
+
self,
|
| 67 |
+
model: FastGenModel,
|
| 68 |
+
data_batch: dict[str, torch.Tensor],
|
| 69 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 70 |
+
loss_dict: dict[str, torch.Tensor],
|
| 71 |
+
iteration: int = 0,
|
| 72 |
+
) -> None:
|
| 73 |
+
del data_batch, output_batch, loss_dict
|
| 74 |
+
new_stage, cur_nimg = self._get_cur_stage(model, iteration)
|
| 75 |
+
if new_stage > self.stage:
|
| 76 |
+
self._update_schedule(new_stage)
|
| 77 |
+
setattr(model, "ratio", self.ratio)
|
| 78 |
+
|
| 79 |
+
if hasattr(self, "config"):
|
| 80 |
+
# only wandb log when config exists
|
| 81 |
+
if iteration % self.config.trainer.logging_iter == 0 and is_rank0():
|
| 82 |
+
if wandb.run:
|
| 83 |
+
wandb.log({"ct_schedule/kimg": cur_nimg / 1e3, "ct_schedule/ratio": self.ratio}, step=iteration)
|
lipforcing/callbacks/ema.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from typing import Callable, TYPE_CHECKING, Optional
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import wandb
|
| 10 |
+
|
| 11 |
+
from lipforcing.callbacks.callback import Callback
|
| 12 |
+
from lipforcing.utils.basic_utils import get_batch_size_total
|
| 13 |
+
from lipforcing.utils.distributed import synchronize, is_rank0
|
| 14 |
+
import lipforcing.utils.logging_utils as logger
|
| 15 |
+
|
| 16 |
+
if TYPE_CHECKING:
|
| 17 |
+
from lipforcing.methods import FastGenModel
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class EMACallback(Callback):
|
| 21 |
+
def __init__(
|
| 22 |
+
self,
|
| 23 |
+
type: str = "constant",
|
| 24 |
+
# params for type=constant
|
| 25 |
+
beta: float = 0.9999,
|
| 26 |
+
# params for type=power
|
| 27 |
+
gamma: float = 16.97,
|
| 28 |
+
# params for type=halflife
|
| 29 |
+
ema_halflife_kimg: float = 500,
|
| 30 |
+
ema_rampup_ratio: Optional[float] = 0.05,
|
| 31 |
+
start_iter: int = 0,
|
| 32 |
+
ema_name: str = "ema",
|
| 33 |
+
batch_size: int = 1, # overwritten by self.config if it exists
|
| 34 |
+
fsdp: bool = False, # overwritten by self.config if it exists
|
| 35 |
+
):
|
| 36 |
+
self.type = type
|
| 37 |
+
self.beta = beta
|
| 38 |
+
self.gamma = gamma
|
| 39 |
+
self.ema_halflife_kimg = ema_halflife_kimg
|
| 40 |
+
self.ema_rampup_ratio = ema_rampup_ratio
|
| 41 |
+
self.start_iter = start_iter
|
| 42 |
+
self.ema_name = ema_name
|
| 43 |
+
self.batch_size = batch_size
|
| 44 |
+
self._is_fsdp = fsdp
|
| 45 |
+
self._enabled = True
|
| 46 |
+
|
| 47 |
+
def on_app_begin(self) -> None:
|
| 48 |
+
if hasattr(self, "config"):
|
| 49 |
+
# override using config
|
| 50 |
+
self._is_fsdp = self.config.trainer.fsdp
|
| 51 |
+
self.batch_size = get_batch_size_total(self.config)
|
| 52 |
+
|
| 53 |
+
def on_model_init_end(
|
| 54 |
+
self, model: FastGenModel | torch.nn.parallel.DistributedDataParallel, iteration: int = 0
|
| 55 |
+
) -> None:
|
| 56 |
+
# Unwrap DDP if needed to access the original model's attributes
|
| 57 |
+
if hasattr(model, "module"):
|
| 58 |
+
model = model.module
|
| 59 |
+
|
| 60 |
+
# check ema initialization
|
| 61 |
+
ema = getattr(model, self.ema_name, None)
|
| 62 |
+
if ema is None:
|
| 63 |
+
self._enabled = False
|
| 64 |
+
logger.info(f"EMA {self.ema_name} is not enabled, skipping callback.")
|
| 65 |
+
return
|
| 66 |
+
|
| 67 |
+
assert ema.training is False, f"EMA {self.ema_name} should be in eval mode"
|
| 68 |
+
for name, p_net in ema.named_parameters():
|
| 69 |
+
assert not p_net.requires_grad, f"EMA parameter {name} should not require gradients"
|
| 70 |
+
|
| 71 |
+
def _total_iteration(self, model: FastGenModel, iteration: int) -> int:
|
| 72 |
+
if hasattr(model, "resume_iter"):
|
| 73 |
+
assert isinstance(model.resume_iter, int)
|
| 74 |
+
iteration = iteration + model.resume_iter
|
| 75 |
+
return iteration
|
| 76 |
+
|
| 77 |
+
def _power_function_beta(self, iteration):
|
| 78 |
+
beta = (1 - 1 / iteration) ** (self.gamma + 1)
|
| 79 |
+
return beta
|
| 80 |
+
|
| 81 |
+
def _get_cur_nimg(self, iteration):
|
| 82 |
+
cur_nimg = iteration * self.batch_size
|
| 83 |
+
return self.batch_size, cur_nimg
|
| 84 |
+
|
| 85 |
+
def _halflife_beta(self, iteration):
|
| 86 |
+
ema_halflife_nimg = self.ema_halflife_kimg * 1000
|
| 87 |
+
batch_size, cur_nimg = self._get_cur_nimg(iteration)
|
| 88 |
+
if self.ema_rampup_ratio is not None:
|
| 89 |
+
ema_halflife_nimg = min(ema_halflife_nimg, cur_nimg * self.ema_rampup_ratio)
|
| 90 |
+
ema_beta = 0.5 ** (batch_size / max(ema_halflife_nimg, 1e-8))
|
| 91 |
+
return ema_beta
|
| 92 |
+
|
| 93 |
+
def on_training_step_end(
|
| 94 |
+
self,
|
| 95 |
+
model: FastGenModel,
|
| 96 |
+
data_batch: dict[str, torch.Tensor],
|
| 97 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 98 |
+
loss_dict: dict[str, torch.Tensor],
|
| 99 |
+
iteration: int = 0,
|
| 100 |
+
) -> None:
|
| 101 |
+
del data_batch, output_batch, loss_dict
|
| 102 |
+
|
| 103 |
+
# Check if EMA is enabled
|
| 104 |
+
if not self._enabled:
|
| 105 |
+
return
|
| 106 |
+
|
| 107 |
+
# Get total iteration and skip if before start_iter
|
| 108 |
+
total_iteration = self._total_iteration(model, iteration)
|
| 109 |
+
if total_iteration < self.start_iter:
|
| 110 |
+
return
|
| 111 |
+
elif total_iteration == self.start_iter:
|
| 112 |
+
logger.info(f"Starting to update {self.ema_name} at iteration {total_iteration}.")
|
| 113 |
+
|
| 114 |
+
if self.type == "constant":
|
| 115 |
+
beta = self.beta
|
| 116 |
+
elif self.type == "power":
|
| 117 |
+
beta = self._power_function_beta(total_iteration)
|
| 118 |
+
elif self.type == "halflife":
|
| 119 |
+
beta = self._halflife_beta(total_iteration)
|
| 120 |
+
else:
|
| 121 |
+
raise ValueError(f"Invalid {self.ema_name} type: {self.type}")
|
| 122 |
+
|
| 123 |
+
with torch.no_grad():
|
| 124 |
+
ema = getattr(model, self.ema_name)
|
| 125 |
+
ema_state_dict = ema.state_dict()
|
| 126 |
+
|
| 127 |
+
for name, p_net in model.net.named_parameters():
|
| 128 |
+
if self._is_fsdp and hasattr(p_net, "full_tensor"):
|
| 129 |
+
# Gather the full tensor from all ranks if using FSDP with DTensor
|
| 130 |
+
# When CPU offloading is enabled, we need to move to CUDA first because
|
| 131 |
+
# full_tensor() performs an all_gather which requires a CUDA backend
|
| 132 |
+
if p_net.device.type == "cpu":
|
| 133 |
+
# Move local shard to CUDA, gather, then the result stays on CUDA
|
| 134 |
+
# which is fine since we'll copy to EMA (which handles device placement)
|
| 135 |
+
full_tensor = p_net.to("cuda").full_tensor()
|
| 136 |
+
else:
|
| 137 |
+
full_tensor = p_net.full_tensor()
|
| 138 |
+
else:
|
| 139 |
+
full_tensor = p_net
|
| 140 |
+
# Strip checkpoint wrapper prefix if present (EMA doesn't have checkpointing)
|
| 141 |
+
ema_name = name.replace("_checkpoint_wrapped_module.", "")
|
| 142 |
+
# Cast to EMA dtype and device for lerp_ compatibility
|
| 143 |
+
if ema_name in ema_state_dict:
|
| 144 |
+
ema_param = ema_state_dict[ema_name]
|
| 145 |
+
if total_iteration == self.start_iter:
|
| 146 |
+
# re-initialize EMA parameter
|
| 147 |
+
ema_param.copy_(full_tensor.to(device=ema_param.device, dtype=ema_param.dtype))
|
| 148 |
+
else:
|
| 149 |
+
# interpolate EMA parameter
|
| 150 |
+
ema_param.lerp_(full_tensor.to(device=ema_param.device, dtype=ema_param.dtype), 1.0 - beta)
|
| 151 |
+
elif iteration == 1:
|
| 152 |
+
# only warn on first iteration if parameter is not found
|
| 153 |
+
logger.warning(f"EMA parameter {ema_name} not found in EMA state dict, skipping update.")
|
| 154 |
+
|
| 155 |
+
# FSDP2 doesn't shard buffers, so we can just copy them
|
| 156 |
+
for name, p_net in model.net.named_buffers():
|
| 157 |
+
if name in ema_state_dict:
|
| 158 |
+
ema_param = ema_state_dict[name]
|
| 159 |
+
ema_param.copy_(p_net.to(device=ema_param.device, dtype=ema_param.dtype))
|
| 160 |
+
elif iteration == 1:
|
| 161 |
+
# only warn on first iteration if buffer is not found
|
| 162 |
+
logger.warning(f"EMA buffer {name} not found in EMA state dict, skipping update.")
|
| 163 |
+
|
| 164 |
+
if hasattr(self, "config"):
|
| 165 |
+
# only wandb log when config exists
|
| 166 |
+
if iteration % self.config.trainer.logging_iter == 0 and is_rank0():
|
| 167 |
+
if wandb.run:
|
| 168 |
+
wandb.log({f"ema/{self.ema_name}_beta": beta}, step=iteration)
|
| 169 |
+
synchronize()
|
lipforcing/callbacks/forced_weight_norm.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from typing import TYPE_CHECKING
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
from lipforcing.callbacks.callback import Callback
|
| 10 |
+
import lipforcing.utils.logging_utils as logger
|
| 11 |
+
|
| 12 |
+
if TYPE_CHECKING:
|
| 13 |
+
from lipforcing.methods import FastGenModel
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ForcedWeightNormCallback(Callback):
|
| 17 |
+
def on_training_accum_step_begin(
|
| 18 |
+
self,
|
| 19 |
+
model: FastGenModel,
|
| 20 |
+
*args,
|
| 21 |
+
**kwargs,
|
| 22 |
+
) -> None:
|
| 23 |
+
if hasattr(model.net, "forced_weight_normalization"):
|
| 24 |
+
model.net.forced_weight_normalization()
|
| 25 |
+
else:
|
| 26 |
+
logger.warning(
|
| 27 |
+
"Enabled ForcedWeightNormCallback but model.net does not have the forced_weight_normalization method."
|
| 28 |
+
)
|
lipforcing/callbacks/gpu_mem_profiler.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import os
|
| 8 |
+
from lipforcing.utils import logging_utils as logger
|
| 9 |
+
from lipforcing.callbacks.callback import Callback
|
| 10 |
+
import atexit
|
| 11 |
+
import pickle
|
| 12 |
+
from typing import Callable, Optional, TYPE_CHECKING
|
| 13 |
+
import base64
|
| 14 |
+
import json
|
| 15 |
+
|
| 16 |
+
if TYPE_CHECKING:
|
| 17 |
+
from lipforcing.methods import FastGenModel
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def create_dump(dump_path):
|
| 21 |
+
logger.critical(f"Creating {dump_path}")
|
| 22 |
+
if not dump_path.endswith("html"):
|
| 23 |
+
print(f"[{__file__}] create_dump produces an HTML file but was called with {dump_path=}")
|
| 24 |
+
torch.cuda.memory._dump_snapshot(dump_path + ".pickle")
|
| 25 |
+
with open(dump_path + ".pickle", "rb") as f:
|
| 26 |
+
data = pickle.load(f)
|
| 27 |
+
_memory_viz_template = r"""
|
| 28 |
+
<!DOCTYPE html>
|
| 29 |
+
<html>
|
| 30 |
+
<head>
|
| 31 |
+
</head>
|
| 32 |
+
<body>
|
| 33 |
+
<script type="module">
|
| 34 |
+
import {add_local_files} from "https://cdn.jsdelivr.net/gh/pytorch/pytorch@main/torch/utils/viz/MemoryViz.js"
|
| 35 |
+
const local_files = $SNAPSHOT
|
| 36 |
+
add_local_files(local_files, $VIZ_KIND)
|
| 37 |
+
</script>
|
| 38 |
+
</body>
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
# find which GPU was active
|
| 42 |
+
idx_device = -1
|
| 43 |
+
for i in range(8):
|
| 44 |
+
if data["device_traces"][i]:
|
| 45 |
+
idx_device = i
|
| 46 |
+
break
|
| 47 |
+
|
| 48 |
+
traces = data["device_traces"][idx_device] # create an aliasing variable for convenience
|
| 49 |
+
traces = [
|
| 50 |
+
d for d in traces if d["action"] == "alloc" or d["action"] == "free_completed"
|
| 51 |
+
] # only the `alloc` and `free_completed` events matter for our visualization
|
| 52 |
+
|
| 53 |
+
for d in traces:
|
| 54 |
+
d["fastgen_frames"] = [
|
| 55 |
+
f for f in d["frames"] if "lipforcing" in f["filename"]
|
| 56 |
+
] # get the callstack frames from lipforcing code (e.g. ignore frames in pytorch/other libraries)
|
| 57 |
+
if not d["fastgen_frames"]:
|
| 58 |
+
d["fastgen_frames"] = d["frames"]
|
| 59 |
+
|
| 60 |
+
# run through the trace and find allocations that were allocated but never freed
|
| 61 |
+
set_alloced_addrs: dict = {}
|
| 62 |
+
for d in traces:
|
| 63 |
+
if d["action"] == "alloc":
|
| 64 |
+
set_alloced_addrs[d["addr"]] = d
|
| 65 |
+
elif d["action"] == "free_completed":
|
| 66 |
+
if d["addr"] in set_alloced_addrs:
|
| 67 |
+
del set_alloced_addrs[d["addr"]]
|
| 68 |
+
else:
|
| 69 |
+
raise NotImplementedError(f"{d['action']}")
|
| 70 |
+
|
| 71 |
+
never_freed_traces = list(set_alloced_addrs.values())
|
| 72 |
+
KB = 1 << 10
|
| 73 |
+
never_freed_traces = [t for t in never_freed_traces if t["size"] > KB] # get rid of allocations below 1 KB
|
| 74 |
+
|
| 75 |
+
# now proceed through the trace (guarenteed to be all `alloc` events as we removed all free events).
|
| 76 |
+
# for each pair of alloc events, merge them iff they share a common lipforcing ancestor.
|
| 77 |
+
# Merging events is useful as it both speeds up the visualization rendering and also makes it more understandable.
|
| 78 |
+
i = 0
|
| 79 |
+
while i < len(never_freed_traces) - 1:
|
| 80 |
+
curr_frames = never_freed_traces[i]["fastgen_frames"]
|
| 81 |
+
next_frames = never_freed_traces[i + 1]["fastgen_frames"]
|
| 82 |
+
if (
|
| 83 |
+
curr_frames and next_frames and curr_frames[0] == next_frames[0]
|
| 84 |
+
): # note: compares only the innermost frame, not the full callstack
|
| 85 |
+
# same ancestor, delete next event and add its size to current event
|
| 86 |
+
never_freed_traces[i]["size"] += never_freed_traces[i + 1]["size"]
|
| 87 |
+
never_freed_traces.pop(i + 1)
|
| 88 |
+
else:
|
| 89 |
+
i += 1 # different ancestor, do not combine and move on
|
| 90 |
+
|
| 91 |
+
data["device_traces"][idx_device] = never_freed_traces # update the trace to only be the merged-alloc events
|
| 92 |
+
data["segments"] = [] # shrink the trace, unused in memory timeline
|
| 93 |
+
data["external_annotations"] = [] # shrink the trace, unused in memory timeline
|
| 94 |
+
buffer = pickle.dumps(data)
|
| 95 |
+
buffer += b"\x00" * (3 - len(buffer) % 3)
|
| 96 |
+
encoded_buffer = base64.b64encode(buffer).decode("utf-8")
|
| 97 |
+
json_format = json.dumps([{"name": "snapshot.pickle", "base64": encoded_buffer}])
|
| 98 |
+
html_src = _memory_viz_template.replace("$VIZ_KIND", repr("Active Memory Timeline")).replace(
|
| 99 |
+
"$SNAPSHOT", json_format
|
| 100 |
+
)
|
| 101 |
+
with open(dump_path, "w") as f:
|
| 102 |
+
f.write(html_src)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class MemTrackerCallback(Callback):
|
| 106 |
+
def __init__(self, save_every_n_iters: Optional[int] = None, deactivate_after_n_iters: int = 100):
|
| 107 |
+
def close_and_save():
|
| 108 |
+
create_dump(
|
| 109 |
+
f"{os.environ.get('LIPFORCING_OUTPUT_ROOT', 'LIPFORCING_OUTPUT')}/crash_rank{os.environ.get('RANK', '0')}.html"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
self.deactivate_after_n_iters = deactivate_after_n_iters # Deactivate eventually to prevent leaking host memory
|
| 113 |
+
self.save_every_n_iters = save_every_n_iters
|
| 114 |
+
self.atexit_fn = close_and_save
|
| 115 |
+
atexit.register(self.atexit_fn)
|
| 116 |
+
|
| 117 |
+
def on_app_begin(self):
|
| 118 |
+
logger.info("[MemTrackerCallback] Tracking peak memory usage")
|
| 119 |
+
torch.cuda.memory._record_memory_history(stacks="python")
|
| 120 |
+
|
| 121 |
+
def on_training_step_end(
|
| 122 |
+
self,
|
| 123 |
+
model: FastGenModel,
|
| 124 |
+
data_batch: dict[str, torch.Tensor],
|
| 125 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 126 |
+
loss_dict: dict[str, torch.Tensor],
|
| 127 |
+
iteration: int = 0,
|
| 128 |
+
) -> None:
|
| 129 |
+
if iteration > self.deactivate_after_n_iters:
|
| 130 |
+
torch.cuda.memory._record_memory_history(enabled=None) # frees pytorch tracking datastructures
|
| 131 |
+
if self.save_every_n_iters is not None and (iteration % self.save_every_n_iters) == 0:
|
| 132 |
+
create_dump(
|
| 133 |
+
f"{os.environ.get('LIPFORCING_OUTPUT_ROOT', 'LIPFORCING_OUTPUT')}/step{iteration}_rank{os.environ.get('RANK', '0')}.html"
|
| 134 |
+
)
|
lipforcing/callbacks/gpu_stats.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from typing import TYPE_CHECKING, Callable, Any, Dict, List
|
| 8 |
+
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import psutil
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
from lipforcing.callbacks.callback import Callback
|
| 14 |
+
from lipforcing.utils.distributed import world_size, is_rank0, synchronize
|
| 15 |
+
import lipforcing.utils.logging_utils as logger
|
| 16 |
+
|
| 17 |
+
if TYPE_CHECKING:
|
| 18 |
+
from lipforcing.methods import FastGenModel
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def log_prof_data(data_list: List[Dict[str, Any]]):
|
| 22 |
+
# Create a table to log data with rank information
|
| 23 |
+
metrics = list(data_list[0].keys())
|
| 24 |
+
|
| 25 |
+
# Initialize dictionaries to store min and max values for each metric
|
| 26 |
+
min_values = {key: float("inf") for key in metrics}
|
| 27 |
+
max_values = {key: float("-inf") for key in metrics}
|
| 28 |
+
sum_values = {key: 0.0 for key in metrics}
|
| 29 |
+
|
| 30 |
+
count = 0
|
| 31 |
+
|
| 32 |
+
for _rank, prof_data in enumerate(data_list):
|
| 33 |
+
count += 1
|
| 34 |
+
|
| 35 |
+
# Update min, max, and sum values
|
| 36 |
+
for key in metrics:
|
| 37 |
+
min_values[key] = min(min_values[key], prof_data[key])
|
| 38 |
+
max_values[key] = max(max_values[key], prof_data[key])
|
| 39 |
+
sum_values[key] += prof_data[key]
|
| 40 |
+
|
| 41 |
+
# Calculate average values
|
| 42 |
+
avg_values = {key: sum_values[key] / count for key in metrics}
|
| 43 |
+
summary_df = pd.DataFrame({"Avg": avg_values, "Max": max_values, "Min": min_values})
|
| 44 |
+
|
| 45 |
+
logger.info(f"GPU stats:\n{summary_df.to_string()}")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class GPUStatsCallback(Callback):
|
| 49 |
+
def __init__(self, every_n: int = 100):
|
| 50 |
+
self.every_n = every_n
|
| 51 |
+
|
| 52 |
+
def on_train_begin(self, model: FastGenModel, iteration: int = 0):
|
| 53 |
+
torch.cuda.reset_peak_memory_stats()
|
| 54 |
+
if hasattr(self, "config"):
|
| 55 |
+
# overwritten by logging_iter if self.config exists
|
| 56 |
+
self.every_n = self.config.trainer.logging_iter
|
| 57 |
+
logger.info(f"every_n to measure gpus stats: {self.every_n}")
|
| 58 |
+
|
| 59 |
+
def on_training_step_end(
|
| 60 |
+
self,
|
| 61 |
+
model: FastGenModel,
|
| 62 |
+
data_batch: dict[str, torch.Tensor],
|
| 63 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 64 |
+
loss_dict: dict[str, torch.Tensor],
|
| 65 |
+
iteration: int = 0,
|
| 66 |
+
) -> None:
|
| 67 |
+
del data_batch, output_batch, loss_dict
|
| 68 |
+
if iteration % self.every_n == 0:
|
| 69 |
+
cur_process = psutil.Process(os.getpid())
|
| 70 |
+
cpu_memory_usage = sum(p.memory_info().rss for p in [cur_process] + cur_process.children(recursive=True))
|
| 71 |
+
cpu_mem_gb = cpu_memory_usage / (1024**3)
|
| 72 |
+
|
| 73 |
+
peak_gpu_mem_gb = torch.cuda.max_memory_allocated() / (1024**3)
|
| 74 |
+
peak_gpu_mem_reserved_gb = torch.cuda.max_memory_reserved() / (1024**3)
|
| 75 |
+
util = torch.cuda.utilization()
|
| 76 |
+
|
| 77 |
+
prof_data = {
|
| 78 |
+
"cpu_mem_gb": float(cpu_mem_gb),
|
| 79 |
+
"peak_gpu_mem_gb": float(peak_gpu_mem_gb),
|
| 80 |
+
"peak_gpu_mem_reserved_gb": float(peak_gpu_mem_reserved_gb),
|
| 81 |
+
"util": float(util),
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
synchronize()
|
| 85 |
+
data_list = [prof_data] * world_size()
|
| 86 |
+
# this is blocking by default
|
| 87 |
+
if world_size() > 1:
|
| 88 |
+
torch.distributed.all_gather_object(data_list, prof_data)
|
| 89 |
+
|
| 90 |
+
if is_rank0():
|
| 91 |
+
log_prof_data(data_list)
|
| 92 |
+
synchronize()
|
lipforcing/callbacks/grad_clip.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from contextlib import contextmanager
|
| 7 |
+
from typing import TYPE_CHECKING, Optional
|
| 8 |
+
import wandb
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch.distributed.tensor import DTensor
|
| 12 |
+
|
| 13 |
+
from lipforcing.callbacks.callback import Callback
|
| 14 |
+
from lipforcing.utils.distributed import is_rank0, world_size
|
| 15 |
+
import lipforcing.utils.logging_utils as logger
|
| 16 |
+
|
| 17 |
+
if TYPE_CHECKING:
|
| 18 |
+
from lipforcing.methods import FastGenModel
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@contextmanager
|
| 22 |
+
def cast_gradients_dtype(model, dtype=torch.float32, enabled=True):
|
| 23 |
+
if enabled:
|
| 24 |
+
try:
|
| 25 |
+
# Cast gradients to the desired dtype
|
| 26 |
+
for param in model.parameters():
|
| 27 |
+
if param.grad is not None and param.grad.dtype != dtype:
|
| 28 |
+
param.grad.data = param.grad.data.to(dtype)
|
| 29 |
+
yield
|
| 30 |
+
finally:
|
| 31 |
+
# Restore original gradient dtypes
|
| 32 |
+
for param in model.parameters():
|
| 33 |
+
if param.grad is not None and param.grad.dtype != param.dtype:
|
| 34 |
+
param.grad.data = param.grad.data.to(param.dtype)
|
| 35 |
+
else:
|
| 36 |
+
yield
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def clip_grad_norm_fsdp(
|
| 40 |
+
parameters,
|
| 41 |
+
max_norm: float,
|
| 42 |
+
norm_type: float = 2.0,
|
| 43 |
+
device: Optional[torch.device] = None,
|
| 44 |
+
) -> torch.Tensor:
|
| 45 |
+
"""
|
| 46 |
+
Clip gradients for FSDP2 models with CPU offloading.
|
| 47 |
+
|
| 48 |
+
The standard torch.nn.utils.clip_grad_norm_ fails with FSDP2 CPU offloading because
|
| 49 |
+
DTensor operations (like division) trigger all_reduce on CPU, which has no backend.
|
| 50 |
+
|
| 51 |
+
This implementation:
|
| 52 |
+
1. Extracts local tensors from DTensors
|
| 53 |
+
2. Computes local norms on native device (CPU or GPU)
|
| 54 |
+
3. All-reduces the scalar norm to get global norm
|
| 55 |
+
4. Clips gradients in-place using the global norm
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
parameters: Iterable of parameters with gradients
|
| 59 |
+
max_norm: Maximum norm value
|
| 60 |
+
norm_type: Type of norm (default: L2)
|
| 61 |
+
device: Device for all-reduce tensor. If None, inferred from gradients or defaults to cuda.
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
Total gradient norm (global across all ranks) as a regular tensor
|
| 65 |
+
"""
|
| 66 |
+
if isinstance(parameters, torch.Tensor):
|
| 67 |
+
parameters = [parameters]
|
| 68 |
+
parameters = list(p for p in parameters if p.grad is not None)
|
| 69 |
+
|
| 70 |
+
if len(parameters) == 0:
|
| 71 |
+
return torch.tensor(0.0)
|
| 72 |
+
|
| 73 |
+
# Compute per-parameter norms on their native device (CPU or GPU)
|
| 74 |
+
# We compute norm^norm_type to allow proper aggregation across ranks
|
| 75 |
+
local_norm_sum = 0.0
|
| 76 |
+
inferred_device = None
|
| 77 |
+
for p in parameters:
|
| 78 |
+
if isinstance(p.grad, DTensor):
|
| 79 |
+
grad = p.grad._local_tensor
|
| 80 |
+
else:
|
| 81 |
+
grad = p.grad
|
| 82 |
+
|
| 83 |
+
# Infer CUDA device from gradients (use first CUDA device found)
|
| 84 |
+
if inferred_device is None and grad.device.type == "cuda":
|
| 85 |
+
inferred_device = grad.device
|
| 86 |
+
|
| 87 |
+
# Compute norm on the gradient's native device, accumulate as Python float
|
| 88 |
+
local_norm_sum += torch.norm(grad.detach().float(), norm_type).item() ** norm_type
|
| 89 |
+
|
| 90 |
+
# Use provided device, or inferred device, or fall back to current CUDA device
|
| 91 |
+
if device is None:
|
| 92 |
+
device = inferred_device if inferred_device is not None else torch.device("cuda")
|
| 93 |
+
local_norm_sum = torch.tensor(local_norm_sum, device=device)
|
| 94 |
+
|
| 95 |
+
# All-reduce to get global norm across all ranks
|
| 96 |
+
if world_size() > 1:
|
| 97 |
+
torch.distributed.all_reduce(local_norm_sum, op=torch.distributed.ReduceOp.SUM)
|
| 98 |
+
|
| 99 |
+
# Compute final global norm
|
| 100 |
+
total_norm = local_norm_sum ** (1.0 / norm_type)
|
| 101 |
+
|
| 102 |
+
# Compute clip coefficient (regular tensor division, no DTensor ops)
|
| 103 |
+
clip_coef = max_norm / (total_norm + 1e-6)
|
| 104 |
+
clip_coef_clamped = torch.clamp(clip_coef, max=1.0)
|
| 105 |
+
|
| 106 |
+
# Apply clipping to gradients
|
| 107 |
+
for p in parameters:
|
| 108 |
+
if isinstance(p.grad, DTensor):
|
| 109 |
+
# For DTensor, scale the local tensor directly
|
| 110 |
+
local_grad = p.grad._local_tensor
|
| 111 |
+
local_grad.mul_(clip_coef_clamped.to(local_grad.device))
|
| 112 |
+
else:
|
| 113 |
+
p.grad.detach().mul_(clip_coef_clamped.to(p.grad.device))
|
| 114 |
+
|
| 115 |
+
return total_norm
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class GradClipCallback(Callback):
|
| 119 |
+
def __init__(
|
| 120 |
+
self,
|
| 121 |
+
grad_norm: float | None = 1.0,
|
| 122 |
+
model_key: str = "net",
|
| 123 |
+
posinf: float | None = None,
|
| 124 |
+
neginf: float | None = None,
|
| 125 |
+
precision_grad_clip: Optional[torch.dtype] = None,
|
| 126 |
+
) -> None:
|
| 127 |
+
self.grad_norm = grad_norm
|
| 128 |
+
self.model_key = model_key
|
| 129 |
+
self.posinf = posinf
|
| 130 |
+
self.neginf = neginf
|
| 131 |
+
self.precision_grad_clip = precision_grad_clip
|
| 132 |
+
|
| 133 |
+
def nan_to_num(self, module: torch.nn.Module) -> tuple[int, torch.dtype | None]:
|
| 134 |
+
grad_dtype = None
|
| 135 |
+
non_finite_grads_count = 0
|
| 136 |
+
|
| 137 |
+
for name, param in module.named_parameters():
|
| 138 |
+
if param.grad is not None:
|
| 139 |
+
grad_dtype = param.grad.dtype
|
| 140 |
+
|
| 141 |
+
# Extract local tensor for DTensor (avoids triggering distributed ops on CPU)
|
| 142 |
+
if isinstance(param.grad, DTensor):
|
| 143 |
+
grad = param.grad._local_tensor
|
| 144 |
+
else:
|
| 145 |
+
grad = param.grad
|
| 146 |
+
|
| 147 |
+
non_finite_grads = grad.numel() - grad.isfinite().sum().item()
|
| 148 |
+
if non_finite_grads:
|
| 149 |
+
non_finite_grads_count += non_finite_grads
|
| 150 |
+
logger.debug(
|
| 151 |
+
f"Gradient of {name} (dtype {grad_dtype}) is not finite: "
|
| 152 |
+
f"Setting {grad.isnan().sum().item()} NaNs to 0 and {grad.isinf().sum().item()} Infs "
|
| 153 |
+
f"to {self.posinf} or {self.neginf}."
|
| 154 |
+
)
|
| 155 |
+
torch.nan_to_num(grad, nan=0.0, posinf=self.posinf, neginf=self.neginf, out=grad)
|
| 156 |
+
|
| 157 |
+
return non_finite_grads_count, grad_dtype
|
| 158 |
+
|
| 159 |
+
def on_optimizer_step_begin(self, model: FastGenModel, iteration: int = 0) -> None:
|
| 160 |
+
# unscale the optimizer related to the `model_key`
|
| 161 |
+
assert (
|
| 162 |
+
self.model_key in model.optimizer_dict.keys()
|
| 163 |
+
), f"Keys in optimizer_dict: {list(model.optimizer_dict.keys())}."
|
| 164 |
+
optimizer = model.optimizer_dict[self.model_key]
|
| 165 |
+
# Only unscale if grad_scaler should be used (checks enabled + float32 grads)
|
| 166 |
+
if model.should_use_grad_scaler(optimizer):
|
| 167 |
+
model.grad_scaler.unscale_(optimizer)
|
| 168 |
+
|
| 169 |
+
# Save model device before selecting subnet (subnet may not have .device)
|
| 170 |
+
model_device = model.device
|
| 171 |
+
|
| 172 |
+
# select subnet if specified (by default, we only perform gradient clips on model.net)
|
| 173 |
+
subnets = self.model_key.split(".")
|
| 174 |
+
for subnet in subnets:
|
| 175 |
+
model = getattr(model, subnet)
|
| 176 |
+
|
| 177 |
+
# set nan to num for each parameter
|
| 178 |
+
non_finite_grads_count, grad_dtype = self.nan_to_num(model)
|
| 179 |
+
logger.debug(f"Gradient dtype of {self.model_key}: {grad_dtype}")
|
| 180 |
+
if non_finite_grads_count > 0:
|
| 181 |
+
logger.info(
|
| 182 |
+
f"Number of parameters with non-finite gradients (of dtype {grad_dtype}): {non_finite_grads_count}"
|
| 183 |
+
)
|
| 184 |
+
log_dict = {f"optimizer/non_finite_grads_count (model_key {self.model_key})": non_finite_grads_count}
|
| 185 |
+
|
| 186 |
+
if self.grad_norm is not None:
|
| 187 |
+
# Cast all gradients to precision_grad_clip for numerical stability during clipping
|
| 188 |
+
cast_grads = (
|
| 189 |
+
self.precision_grad_clip is not None
|
| 190 |
+
and grad_dtype is not None
|
| 191 |
+
and grad_dtype != self.precision_grad_clip
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
# log value at first iteration
|
| 195 |
+
if iteration == 1 and cast_grads:
|
| 196 |
+
logger.info(f"Casting gradients from {grad_dtype} to {self.precision_grad_clip} before clipping.")
|
| 197 |
+
|
| 198 |
+
# Check if CPU offloading is enabled by looking for DTensor grads on CPU
|
| 199 |
+
# CPU offloading = DTensor local tensors are on CPU
|
| 200 |
+
# Check if any gradients are DTensors (FSDP2 sharded params).
|
| 201 |
+
# Standard clip_grad_norm_ with foreach=True can't mix DTensor and regular Tensor.
|
| 202 |
+
has_dtensor_grads = any(
|
| 203 |
+
isinstance(p.grad, DTensor) for p in model.parameters() if p.grad is not None
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
with cast_gradients_dtype(model, dtype=self.precision_grad_clip, enabled=cast_grads):
|
| 207 |
+
if has_dtensor_grads:
|
| 208 |
+
# Use custom clipping that handles DTensor/Tensor mix
|
| 209 |
+
total_norm = clip_grad_norm_fsdp(model.parameters(), self.grad_norm, device=model_device)
|
| 210 |
+
else:
|
| 211 |
+
# Standard clipping for non-FSDP
|
| 212 |
+
total_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), self.grad_norm, foreach=True)
|
| 213 |
+
|
| 214 |
+
log_dict[f"optimizer/grad_norm (model_key {self.model_key})"] = total_norm.item()
|
| 215 |
+
|
| 216 |
+
if hasattr(self, "config"):
|
| 217 |
+
# only wandb log when config exists
|
| 218 |
+
if iteration % self.config.trainer.logging_iter == 0 and is_rank0() and wandb.run:
|
| 219 |
+
wandb.log(log_dict, step=iteration)
|
lipforcing/callbacks/param_count.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
from typing import TYPE_CHECKING
|
| 6 |
+
|
| 7 |
+
from lipforcing.callbacks.callback import Callback
|
| 8 |
+
from lipforcing.utils.distributed import world_size
|
| 9 |
+
import lipforcing.utils.logging_utils as logger
|
| 10 |
+
import torch
|
| 11 |
+
import wandb
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
from torch.distributed.tensor import DTensor
|
| 15 |
+
except ImportError:
|
| 16 |
+
DTensor = None
|
| 17 |
+
|
| 18 |
+
if TYPE_CHECKING:
|
| 19 |
+
from lipforcing.methods import FastGenModel
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _get_local_numel(param: torch.Tensor) -> int:
|
| 23 |
+
"""Get the local (sharded) number of elements for a parameter.
|
| 24 |
+
|
| 25 |
+
For DTensor (FSDP2), returns the local shard size.
|
| 26 |
+
For regular tensors, returns the full size.
|
| 27 |
+
"""
|
| 28 |
+
if DTensor is not None and isinstance(param, DTensor):
|
| 29 |
+
return param._local_tensor.numel()
|
| 30 |
+
return param.numel()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class ParamCountCallback(Callback):
|
| 34 |
+
def on_train_begin(self, model: FastGenModel, **kwargs) -> None:
|
| 35 |
+
# get modules
|
| 36 |
+
modules = {"model": model, **model.model_dict}
|
| 37 |
+
|
| 38 |
+
# iterate over modules
|
| 39 |
+
output = {}
|
| 40 |
+
for name, module in modules.items():
|
| 41 |
+
# Logical (full model) param counts
|
| 42 |
+
trainable_params = sum(p.numel() for p in module.parameters() if p.requires_grad)
|
| 43 |
+
total_params = sum(p.numel() for p in module.parameters())
|
| 44 |
+
|
| 45 |
+
# Local (sharded) param counts - what's actually in memory on this rank
|
| 46 |
+
local_trainable_params = sum(_get_local_numel(p) for p in module.parameters() if p.requires_grad)
|
| 47 |
+
local_total_params = sum(_get_local_numel(p) for p in module.parameters())
|
| 48 |
+
|
| 49 |
+
# check if parameter counts are different across ranks
|
| 50 |
+
if world_size() > 1:
|
| 51 |
+
trainable_params = self.gather_param_counts(trainable_params)
|
| 52 |
+
total_params = self.gather_param_counts(total_params)
|
| 53 |
+
local_trainable_params = self.gather_param_counts(local_trainable_params)
|
| 54 |
+
local_total_params = self.gather_param_counts(local_total_params)
|
| 55 |
+
if len(set(total_params)) == 1 and len(set(trainable_params)) == 1:
|
| 56 |
+
trainable_params = trainable_params[0]
|
| 57 |
+
total_params = total_params[0]
|
| 58 |
+
if len(set(local_total_params)) == 1 and len(set(local_trainable_params)) == 1:
|
| 59 |
+
local_trainable_params = local_trainable_params[0]
|
| 60 |
+
local_total_params = local_total_params[0]
|
| 61 |
+
|
| 62 |
+
# logging
|
| 63 |
+
module_name = module.__class__.__name__
|
| 64 |
+
output.update(
|
| 65 |
+
{
|
| 66 |
+
f"{name}/trainable_params": trainable_params,
|
| 67 |
+
f"{name}/total_params": total_params,
|
| 68 |
+
f"{name}/local_trainable_params": local_trainable_params,
|
| 69 |
+
f"{name}/local_total_params": local_total_params,
|
| 70 |
+
}
|
| 71 |
+
)
|
| 72 |
+
if isinstance(trainable_params, list):
|
| 73 |
+
logger.warning(f"Parameter counts differ across ranks for {module_name}.")
|
| 74 |
+
for rank, (p_train, p) in enumerate(zip(trainable_params, total_params)):
|
| 75 |
+
logger.info(
|
| 76 |
+
f"{name} ({module_name}) has {p_train * 1.e-6:.2f} M trainable and {p * 1.e-6:.2f} M total params on rank {rank}."
|
| 77 |
+
)
|
| 78 |
+
else:
|
| 79 |
+
logger.info(
|
| 80 |
+
f"{name} ({module_name}) has {trainable_params * 1.e-6:.2f} M trainable and {total_params * 1.e-6:.2f} M total params (logical)."
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# Report local/sharded counts
|
| 84 |
+
if isinstance(local_trainable_params, list):
|
| 85 |
+
for rank, (p_train, p) in enumerate(zip(local_trainable_params, local_total_params)):
|
| 86 |
+
logger.info(
|
| 87 |
+
f"{name} ({module_name}) has {p_train * 1.e-6:.2f} M trainable and {p * 1.e-6:.2f} M total params LOCAL on rank {rank}."
|
| 88 |
+
)
|
| 89 |
+
else:
|
| 90 |
+
is_sharded = local_total_params < total_params if not isinstance(total_params, list) else True
|
| 91 |
+
if is_sharded:
|
| 92 |
+
logger.info(
|
| 93 |
+
f"{name} ({module_name}) has {local_trainable_params * 1.e-6:.2f} M trainable and {local_total_params * 1.e-6:.2f} M total params LOCAL per rank (sharding ratio: {world_size()}x)."
|
| 94 |
+
)
|
| 95 |
+
else:
|
| 96 |
+
logger.info(f"{name} ({module_name}) is NOT sharded (local == logical params).")
|
| 97 |
+
|
| 98 |
+
if wandb.run:
|
| 99 |
+
wandb.run.summary.update(output)
|
| 100 |
+
|
| 101 |
+
def gather_param_counts(self, param_count):
|
| 102 |
+
"""
|
| 103 |
+
Gather parameter counts across all ranks.
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
param_count: Parameter count to gather.
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
List of parameter counts across all ranks.
|
| 110 |
+
"""
|
| 111 |
+
param_count = torch.tensor(
|
| 112 |
+
[param_count], dtype=torch.long, device="cuda" if torch.cuda.is_available() else "cpu"
|
| 113 |
+
)
|
| 114 |
+
param_count_list = [torch.zeros_like(param_count) for _ in range(world_size())]
|
| 115 |
+
torch.distributed.all_gather(param_count_list, param_count)
|
| 116 |
+
return [p.item() for p in param_count_list]
|
lipforcing/callbacks/stdout_logger.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""Simple stdout loss logger callback for testing without wandb."""
|
| 5 |
+
|
| 6 |
+
from typing import Callable
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from lipforcing.callbacks.callback import Callback
|
| 10 |
+
from lipforcing.methods.model import FastGenModel
|
| 11 |
+
import lipforcing.utils.logging_utils as logger
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class StdoutLoggerCallback(Callback):
|
| 15 |
+
"""Prints loss values to stdout at every logging_iter."""
|
| 16 |
+
|
| 17 |
+
def on_training_step_end(
|
| 18 |
+
self,
|
| 19 |
+
model: FastGenModel,
|
| 20 |
+
data_batch: dict[str, torch.Tensor],
|
| 21 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 22 |
+
loss_dict: dict[str, torch.Tensor],
|
| 23 |
+
iteration: int = 0,
|
| 24 |
+
) -> None:
|
| 25 |
+
logging_iter = getattr(self.config.trainer, "logging_iter", 1) if self.config else 1
|
| 26 |
+
if iteration % logging_iter == 0:
|
| 27 |
+
parts = [f"iter {iteration:5d}"]
|
| 28 |
+
for k, v in sorted(loss_dict.items()):
|
| 29 |
+
if isinstance(v, torch.Tensor):
|
| 30 |
+
parts.append(f"{k}={v.item():.6f}")
|
| 31 |
+
elif isinstance(v, (int, float)):
|
| 32 |
+
parts.append(f"{k}={v:.6f}")
|
| 33 |
+
logger.info(" | ".join(parts))
|
lipforcing/callbacks/train_profiler.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import time
|
| 7 |
+
from typing import TYPE_CHECKING, Callable
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
import wandb
|
| 11 |
+
|
| 12 |
+
from lipforcing.callbacks.callback import Callback
|
| 13 |
+
from lipforcing.utils.distributed import is_rank0
|
| 14 |
+
import lipforcing.utils.logging_utils as logger
|
| 15 |
+
|
| 16 |
+
if TYPE_CHECKING:
|
| 17 |
+
from lipforcing.methods import FastGenModel
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class TrainProfilerCallback(Callback):
|
| 21 |
+
"""Callback for profiling training speed and detailed timing breakdowns.
|
| 22 |
+
|
| 23 |
+
Tracks:
|
| 24 |
+
- iter_time: seconds per iteration (wall clock time)
|
| 25 |
+
- data_load_time: time spent loading data
|
| 26 |
+
- avg_forward_time: average forward pass time across accumulation steps
|
| 27 |
+
- backward_time: time spent in backward pass
|
| 28 |
+
- optim_step_time: time spent in optimizer step
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
def __init__(self, every_n: int = 100, detailed: bool = True):
|
| 32 |
+
"""Initialize the profiler callback.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
every_n: Log metrics every N iterations
|
| 36 |
+
detailed: If True, log detailed timing breakdown. If False, only log iter_time.
|
| 37 |
+
"""
|
| 38 |
+
# For iter_time tracking
|
| 39 |
+
self.last_log_time = None
|
| 40 |
+
|
| 41 |
+
# For detailed profiling
|
| 42 |
+
self.detailed = detailed
|
| 43 |
+
self.train_step_begin_time = None
|
| 44 |
+
self.accum_begin_times = None
|
| 45 |
+
self.backward_begin_times = None
|
| 46 |
+
self.optimizer_step_begin = None
|
| 47 |
+
self.step_end_time = None
|
| 48 |
+
self.every_n = every_n
|
| 49 |
+
|
| 50 |
+
def on_train_begin(self, model: FastGenModel, iteration: int = 0) -> None:
|
| 51 |
+
if hasattr(self, "config"):
|
| 52 |
+
# overwritten by logging_iter if self.config exists
|
| 53 |
+
self.every_n = self.config.trainer.logging_iter
|
| 54 |
+
logger.info(f"every_n to profile trainer: {self.every_n}")
|
| 55 |
+
|
| 56 |
+
def on_training_step_begin(
|
| 57 |
+
self,
|
| 58 |
+
model: FastGenModel,
|
| 59 |
+
iteration: int = 0,
|
| 60 |
+
):
|
| 61 |
+
if self.detailed:
|
| 62 |
+
self.train_step_begin_time = time.perf_counter()
|
| 63 |
+
self.accum_begin_times = []
|
| 64 |
+
self.backward_begin_times = []
|
| 65 |
+
|
| 66 |
+
def on_training_accum_step_begin(
|
| 67 |
+
self, model: FastGenModel, data_batch: dict[str, torch.Tensor], iteration: int = 0, accum_iter: int = 0
|
| 68 |
+
):
|
| 69 |
+
if self.detailed:
|
| 70 |
+
self.accum_begin_times.append(time.perf_counter())
|
| 71 |
+
|
| 72 |
+
def on_backward_begin(
|
| 73 |
+
self,
|
| 74 |
+
model: FastGenModel,
|
| 75 |
+
data_batch: dict[str, torch.Tensor],
|
| 76 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 77 |
+
loss_dict: dict[str, torch.Tensor],
|
| 78 |
+
iteration: int = 0,
|
| 79 |
+
accum_iter: int = 0,
|
| 80 |
+
):
|
| 81 |
+
if self.detailed:
|
| 82 |
+
self.backward_begin_times.append(time.perf_counter())
|
| 83 |
+
|
| 84 |
+
def on_optimizer_step_begin(self, model: FastGenModel, iteration: int = 0):
|
| 85 |
+
if self.detailed:
|
| 86 |
+
self.optimizer_step_begin = time.perf_counter()
|
| 87 |
+
|
| 88 |
+
def on_training_step_end(
|
| 89 |
+
self,
|
| 90 |
+
model: FastGenModel,
|
| 91 |
+
data_batch: dict[str, torch.Tensor],
|
| 92 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 93 |
+
loss_dict: dict[str, torch.Tensor],
|
| 94 |
+
iteration: int = 0,
|
| 95 |
+
) -> None:
|
| 96 |
+
del data_batch, output_batch, loss_dict
|
| 97 |
+
|
| 98 |
+
if self.detailed:
|
| 99 |
+
self.step_end_time = time.perf_counter()
|
| 100 |
+
|
| 101 |
+
if hasattr(self, "config"):
|
| 102 |
+
# only wandb log when config exists
|
| 103 |
+
if iteration % self.every_n == 0 and is_rank0():
|
| 104 |
+
metrics = {}
|
| 105 |
+
|
| 106 |
+
# Calculate iter_time (wall clock time per iteration)
|
| 107 |
+
cur_time = time.time()
|
| 108 |
+
if self.last_log_time is not None:
|
| 109 |
+
iter_time = (cur_time - self.last_log_time) / self.every_n
|
| 110 |
+
logger.info(f"{iteration} : avg iteration time {iter_time:.2f} seconds")
|
| 111 |
+
metrics["profiler/avg_iteration_time"] = iter_time
|
| 112 |
+
self.last_log_time = cur_time
|
| 113 |
+
|
| 114 |
+
# Calculate detailed timing breakdown
|
| 115 |
+
if self.detailed and self.accum_begin_times and self.backward_begin_times:
|
| 116 |
+
data_load_time = self.accum_begin_times[0] - self.train_step_begin_time
|
| 117 |
+
forward_time = sum(
|
| 118 |
+
[b - a for (b, a) in zip(self.backward_begin_times, self.accum_begin_times)]
|
| 119 |
+
) / len(self.accum_begin_times)
|
| 120 |
+
backward_time = self.optimizer_step_begin - self.backward_begin_times[-1]
|
| 121 |
+
optim_step_time = self.step_end_time - self.optimizer_step_begin
|
| 122 |
+
|
| 123 |
+
logger.info(f"{iteration} : data loading time {data_load_time:.2f}")
|
| 124 |
+
logger.info(f"{iteration} : avg forward pass time {forward_time:.2f}")
|
| 125 |
+
logger.info(f"{iteration} : backward pass time {backward_time:.2f}")
|
| 126 |
+
logger.info(f"{iteration} : optimizer step time {optim_step_time:.2f}")
|
| 127 |
+
|
| 128 |
+
metrics.update(
|
| 129 |
+
{
|
| 130 |
+
"profiler/data_loading_time": data_load_time,
|
| 131 |
+
"profiler/avg_forward_pass_time": forward_time,
|
| 132 |
+
"profiler/backward_pass_time": backward_time,
|
| 133 |
+
"profiler/optimizer_step_time": optim_step_time,
|
| 134 |
+
}
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
if wandb.run and metrics:
|
| 138 |
+
wandb.log(metrics, step=iteration)
|
lipforcing/callbacks/wandb.py
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
import os
|
| 6 |
+
import subprocess
|
| 7 |
+
import tempfile
|
| 8 |
+
from dataclasses import dataclass, field
|
| 9 |
+
import time
|
| 10 |
+
from typing import Optional, Dict, Callable, TYPE_CHECKING
|
| 11 |
+
import gc
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
import torchvision
|
| 18 |
+
from torchvision.transforms import functional as tv_F
|
| 19 |
+
|
| 20 |
+
import wandb
|
| 21 |
+
import wandb.util
|
| 22 |
+
|
| 23 |
+
from lipforcing.callbacks.callback import Callback
|
| 24 |
+
from lipforcing.configs.config_utils import serialize_config
|
| 25 |
+
from lipforcing.utils import basic_utils
|
| 26 |
+
|
| 27 |
+
from lipforcing.utils.distributed import rank0_only, synchronize, world_size
|
| 28 |
+
from lipforcing.utils import logging_utils as logger
|
| 29 |
+
|
| 30 |
+
if TYPE_CHECKING:
|
| 31 |
+
from lipforcing.configs.config import BaseConfig
|
| 32 |
+
from lipforcing.methods import FastGenModel
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def tensor_to_wandb_video_with_audio(
|
| 36 |
+
video_tensor: torch.Tensor,
|
| 37 |
+
audio_path: str,
|
| 38 |
+
fps: int = 25,
|
| 39 |
+
vid_format: str = "mp4",
|
| 40 |
+
caption: str | None = None,
|
| 41 |
+
) -> wandb.Video:
|
| 42 |
+
"""Convert a [B, T, C, H, W] uint8 video tensor + audio file to wandb.Video with audio.
|
| 43 |
+
|
| 44 |
+
Takes the first sample in the batch. Writes video to a temp file, muxes audio
|
| 45 |
+
with ffmpeg, and returns a wandb.Video from the muxed output.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
video_tensor: uint8 tensor of shape [B, T, C, H, W] (already in 0-255 range).
|
| 49 |
+
audio_path: Path to the audio .wav file.
|
| 50 |
+
fps: Video frame rate (default 25 for OmniAvatar).
|
| 51 |
+
vid_format: Video format (default "mp4").
|
| 52 |
+
caption: Optional caption for wandb.Video.
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
wandb.Video with muxed audio, or silent video if muxing fails.
|
| 56 |
+
"""
|
| 57 |
+
try:
|
| 58 |
+
# Take first sample: [T, C, H, W]
|
| 59 |
+
vid = video_tensor[0] if video_tensor.dim() == 5 else video_tensor
|
| 60 |
+
# Convert to [T, H, W, C] uint8 on CPU
|
| 61 |
+
vid = vid.permute(0, 2, 3, 1).cpu()
|
| 62 |
+
|
| 63 |
+
tmpdir = tempfile.mkdtemp()
|
| 64 |
+
silent_path = os.path.join(tmpdir, f"silent.{vid_format}")
|
| 65 |
+
muxed_path = os.path.join(tmpdir, f"muxed.{vid_format}")
|
| 66 |
+
|
| 67 |
+
# Write silent video — try torchvision.io first, fall back to raw ffmpeg pipe
|
| 68 |
+
T, H, W, C = vid.shape
|
| 69 |
+
try:
|
| 70 |
+
torchvision.io.write_video(silent_path, vid, fps=fps, video_codec="libx264")
|
| 71 |
+
except Exception:
|
| 72 |
+
# Fallback: pipe raw frames to ffmpeg
|
| 73 |
+
write_cmd = [
|
| 74 |
+
"ffmpeg", "-y",
|
| 75 |
+
"-f", "rawvideo", "-pix_fmt", "rgb24",
|
| 76 |
+
"-s", f"{W}x{H}", "-r", str(fps),
|
| 77 |
+
"-i", "pipe:0",
|
| 78 |
+
"-c:v", "libx264", "-pix_fmt", "yuv420p",
|
| 79 |
+
"-loglevel", "error",
|
| 80 |
+
silent_path,
|
| 81 |
+
]
|
| 82 |
+
proc = subprocess.run(write_cmd, input=vid.numpy().tobytes(), capture_output=True, timeout=60)
|
| 83 |
+
if proc.returncode != 0:
|
| 84 |
+
raise RuntimeError(f"ffmpeg raw write failed: {proc.stderr.decode()}")
|
| 85 |
+
|
| 86 |
+
# Mux audio with ffmpeg
|
| 87 |
+
cmd = [
|
| 88 |
+
"ffmpeg", "-y",
|
| 89 |
+
"-i", silent_path,
|
| 90 |
+
"-i", audio_path,
|
| 91 |
+
"-c:v", "copy",
|
| 92 |
+
"-c:a", "aac",
|
| 93 |
+
"-shortest",
|
| 94 |
+
"-loglevel", "error",
|
| 95 |
+
muxed_path,
|
| 96 |
+
]
|
| 97 |
+
result = subprocess.run(cmd, capture_output=True, timeout=30)
|
| 98 |
+
|
| 99 |
+
if result.returncode == 0 and os.path.exists(muxed_path):
|
| 100 |
+
return wandb.Video(muxed_path, fps=fps, format=vid_format, caption=caption)
|
| 101 |
+
else:
|
| 102 |
+
logger.warning(f"ffmpeg muxing failed (rc={result.returncode}): {result.stderr.decode()}")
|
| 103 |
+
return wandb.Video(video_tensor[:1].cpu().numpy(), fps=fps, format=vid_format, caption=caption)
|
| 104 |
+
|
| 105 |
+
except Exception as e:
|
| 106 |
+
logger.warning(f"Audio muxing failed, falling back to silent video: {e}")
|
| 107 |
+
return wandb.Video(video_tensor[:1].cpu().numpy(), fps=fps, format=vid_format, caption=caption)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def to_wandb(
|
| 111 |
+
tensor: torch.Tensor,
|
| 112 |
+
rgb_range: float = 255.0,
|
| 113 |
+
normalized: bool = False,
|
| 114 |
+
max_plot_img: int = 16,
|
| 115 |
+
max_plot_vid: int = 2,
|
| 116 |
+
fps: int = 16,
|
| 117 |
+
channel_before_time: bool = True,
|
| 118 |
+
caption: str | None = None,
|
| 119 |
+
vid_format: str = "mp4",
|
| 120 |
+
) -> wandb.Image | wandb.Video:
|
| 121 |
+
"""
|
| 122 |
+
Convert a tensor to a wandb.Image or wandb.Video.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
tensor (torch.Tensor): Input tensor of shape [B,C,H,W], [B,T,C,H,W], or [B,T,C,H,W,D].
|
| 126 |
+
rgb_range (float, optional): Output target RGB range (can almost definitely be kept as 255).
|
| 127 |
+
Defaults to 255.0.
|
| 128 |
+
normalized (bool, optional): Whether the tensor is normalized to [0,1]. Defaults to False which assumes [-1,1] range.
|
| 129 |
+
max_plot_img (int, optional): Max number of images to plot. Defaults to 16.
|
| 130 |
+
max_plot_vid (int, optional): Max number of videos to plot. Defaults to 2.
|
| 131 |
+
fps (int, optional): Frames per second. Defaults to 8.
|
| 132 |
+
channel_before_time (bool, optional): Whether the tensor is in the format [B,C,T,..]. Set False if the [B,T,C,..] format is used.
|
| 133 |
+
caption (str, optional): Caption for the image or video. Defaults to None.
|
| 134 |
+
vid_format (str, optional): Format of the video file. Defaults to "mp4".
|
| 135 |
+
|
| 136 |
+
Returns:
|
| 137 |
+
wandb.Image | wandb.Video: Format a tensor for logging to W&B.
|
| 138 |
+
"""
|
| 139 |
+
|
| 140 |
+
if tensor.ndim == 5:
|
| 141 |
+
max_plot = max_plot_vid
|
| 142 |
+
if channel_before_time:
|
| 143 |
+
tensor = tensor.permute(0, 2, 1, 3, 4)
|
| 144 |
+
elif tensor.ndim == 4:
|
| 145 |
+
max_plot = max_plot_img
|
| 146 |
+
else:
|
| 147 |
+
raise ValueError(f"Tensor must be 4 or 5 dimensional, but got {tensor.ndim} dimensions")
|
| 148 |
+
|
| 149 |
+
# slice and adjust range
|
| 150 |
+
if normalized:
|
| 151 |
+
factor = rgb_range
|
| 152 |
+
offset = 0.0
|
| 153 |
+
else:
|
| 154 |
+
factor = rgb_range / 2.0
|
| 155 |
+
offset = rgb_range / 2.0
|
| 156 |
+
tensor = tensor[:max_plot].mul(factor).add(offset).clip_(0, rgb_range).to(torch.uint8)
|
| 157 |
+
|
| 158 |
+
# convert to wandb.Image or wandb.Video
|
| 159 |
+
assert tensor.shape[-3] == 3, "Make sure that the data is in ..., C, H, W format"
|
| 160 |
+
if tensor.ndim == 5:
|
| 161 |
+
return wandb.Video(tensor.cpu().numpy(), fps=fps, format=vid_format, caption=caption)
|
| 162 |
+
else:
|
| 163 |
+
image_grid = torchvision.utils.make_grid(tensor, nrow=4, pad_value=1)
|
| 164 |
+
image_grid = tv_F.to_pil_image(image_grid)
|
| 165 |
+
return wandb.Image(image_grid, caption=caption)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _to_wandb_with_audio(
|
| 169 |
+
tensor: torch.Tensor,
|
| 170 |
+
audio_path: str,
|
| 171 |
+
fps: int = 25,
|
| 172 |
+
rgb_range: float = 255.0,
|
| 173 |
+
normalized: bool = False,
|
| 174 |
+
vid_format: str = "mp4",
|
| 175 |
+
caption: str | None = None,
|
| 176 |
+
channel_before_time: bool = True,
|
| 177 |
+
) -> wandb.Video:
|
| 178 |
+
"""Convert a video tensor to wandb.Video with audio muxed in.
|
| 179 |
+
|
| 180 |
+
Handles the same normalization as to_wandb, then delegates to
|
| 181 |
+
tensor_to_wandb_video_with_audio for ffmpeg muxing.
|
| 182 |
+
|
| 183 |
+
Args:
|
| 184 |
+
tensor: [B, C, T, H, W] or [B, T, C, H, W] video tensor in [-1,1] or [0,1] range.
|
| 185 |
+
audio_path: Path to audio .wav file.
|
| 186 |
+
fps: Frame rate for the output video.
|
| 187 |
+
rgb_range: Target RGB range (255).
|
| 188 |
+
normalized: Whether tensor is in [0,1] (True) or [-1,1] (False).
|
| 189 |
+
vid_format: Video file format.
|
| 190 |
+
caption: Optional caption.
|
| 191 |
+
channel_before_time: Whether tensor is [B,C,T,H,W] (True) or [B,T,C,H,W] (False).
|
| 192 |
+
|
| 193 |
+
Returns:
|
| 194 |
+
wandb.Video with audio.
|
| 195 |
+
"""
|
| 196 |
+
if channel_before_time:
|
| 197 |
+
tensor = tensor.permute(0, 2, 1, 3, 4) # [B,C,T,H,W] -> [B,T,C,H,W]
|
| 198 |
+
|
| 199 |
+
# Normalize to uint8
|
| 200 |
+
if normalized:
|
| 201 |
+
factor = rgb_range
|
| 202 |
+
offset = 0.0
|
| 203 |
+
else:
|
| 204 |
+
factor = rgb_range / 2.0
|
| 205 |
+
offset = rgb_range / 2.0
|
| 206 |
+
tensor = tensor[:1].mul(factor).add(offset).clip_(0, rgb_range).to(torch.uint8)
|
| 207 |
+
|
| 208 |
+
return tensor_to_wandb_video_with_audio(
|
| 209 |
+
tensor, audio_path, fps=fps, vid_format=vid_format, caption=caption,
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def _load_audio_waveform(audio_path: str, target_sr: int = 16000, num_frames: int = 81, fps: float = 25.0) -> Optional[torch.Tensor]:
|
| 214 |
+
"""Load audio waveform from .wav for SyncCScorer. Returns [L] float32 tensor or None."""
|
| 215 |
+
if not audio_path or not os.path.isfile(audio_path):
|
| 216 |
+
return None
|
| 217 |
+
try:
|
| 218 |
+
import scipy.io.wavfile as wavfile
|
| 219 |
+
from scipy import signal
|
| 220 |
+
sr, wav = wavfile.read(audio_path)
|
| 221 |
+
if wav.dtype == np.int16:
|
| 222 |
+
wav = wav.astype(np.float32) / 32768.0
|
| 223 |
+
elif wav.dtype != np.float32:
|
| 224 |
+
wav = wav.astype(np.float32)
|
| 225 |
+
wav = torch.from_numpy(wav)
|
| 226 |
+
if wav.ndim == 2:
|
| 227 |
+
wav = wav.mean(dim=1)
|
| 228 |
+
if sr != target_sr:
|
| 229 |
+
num_samples_new = int(len(wav) * target_sr / sr)
|
| 230 |
+
wav = torch.from_numpy(signal.resample(wav.numpy(), num_samples_new))
|
| 231 |
+
target_length = int(num_frames / fps * target_sr)
|
| 232 |
+
if wav.shape[0] < target_length:
|
| 233 |
+
wav = torch.nn.functional.pad(wav, (0, target_length - wav.shape[0]))
|
| 234 |
+
else:
|
| 235 |
+
wav = wav[:target_length]
|
| 236 |
+
return wav.to(torch.float32)
|
| 237 |
+
except Exception as e:
|
| 238 |
+
logger.warning(f"[SyncEval] Failed to load audio from {audio_path}: {e}")
|
| 239 |
+
return None
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
@rank0_only
|
| 243 |
+
def init_wandb(config: BaseConfig):
|
| 244 |
+
# wandb login
|
| 245 |
+
wandb_credential = config.log_config.wandb_credential
|
| 246 |
+
if os.path.isfile(wandb_credential):
|
| 247 |
+
os.environ["WANDB_API_KEY"] = open(wandb_credential, encoding="utf-8").read().strip("\n")
|
| 248 |
+
logger.info(f"Loading WANDB_API_KEY from {wandb_credential}")
|
| 249 |
+
|
| 250 |
+
wandb_config = config.log_config
|
| 251 |
+
|
| 252 |
+
# Resume with or generate a wandb id
|
| 253 |
+
logger.info(f"wandb_config.save_path: {wandb_config.save_path}")
|
| 254 |
+
os.makedirs(wandb_config.save_path, exist_ok=True)
|
| 255 |
+
wandb_id_path = f"{wandb_config.save_path}/wandb_id.txt"
|
| 256 |
+
resuming = getattr(config.trainer, "resume", True)
|
| 257 |
+
if os.path.isfile(wandb_id_path) and resuming:
|
| 258 |
+
wandb_id = open(wandb_id_path, encoding="utf-8").read().strip()
|
| 259 |
+
logger.info(f"Resuming with an existing wandb id: {wandb_id}")
|
| 260 |
+
else:
|
| 261 |
+
wandb_id = wandb.util.generate_id()
|
| 262 |
+
with open(wandb_id_path, "w", encoding="utf-8") as f:
|
| 263 |
+
f.write(f"{wandb_id}\n")
|
| 264 |
+
logger.info(f"Generating a wandb id: {wandb_id}")
|
| 265 |
+
|
| 266 |
+
# Get config as plain dict
|
| 267 |
+
config_resolved = serialize_config(config, return_type="dict")
|
| 268 |
+
|
| 269 |
+
# Initialize the wandb library.
|
| 270 |
+
wandb.init(
|
| 271 |
+
id=wandb_id,
|
| 272 |
+
project=wandb_config.project,
|
| 273 |
+
group=wandb_config.group,
|
| 274 |
+
name=wandb_config.name,
|
| 275 |
+
entity=getattr(wandb_config, "wandb_entity", None),
|
| 276 |
+
config=config_resolved,
|
| 277 |
+
dir=wandb_config.save_path,
|
| 278 |
+
resume="allow",
|
| 279 |
+
mode=wandb_config.wandb_mode,
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
# Save a copy of code to a wandb Artifact (this can be slow)
|
| 283 |
+
# Make code upload optional to avoid distributed training delays
|
| 284 |
+
upload_code = basic_utils.str2bool(os.getenv("WANDB_UPLOAD_CODE", "false"))
|
| 285 |
+
if upload_code:
|
| 286 |
+
logger.info("Uploading code to wandb (this may take a few minutes)...")
|
| 287 |
+
wandb.run.log_code(".")
|
| 288 |
+
logger.info("Code upload to wandb completed")
|
| 289 |
+
else:
|
| 290 |
+
logger.info("Wandb code upload disabled (set WANDB_UPLOAD_CODE=true to enable)")
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
@dataclass
|
| 294 |
+
class _LossDictRecord:
|
| 295 |
+
loss_dict: dict = field(default_factory=dict)
|
| 296 |
+
iter_count_dict: dict = field(default_factory=dict)
|
| 297 |
+
|
| 298 |
+
def add(self, loss_dict: Optional[Dict[str, torch.Tensor]]) -> None:
|
| 299 |
+
if loss_dict is not None:
|
| 300 |
+
for loss_name, loss_val in loss_dict.items():
|
| 301 |
+
scalar = loss_val.float().item() if torch.is_tensor(loss_val) else float(loss_val)
|
| 302 |
+
self.loss_dict[loss_name] = self.loss_dict.get(loss_name, 0.0) + scalar
|
| 303 |
+
self.iter_count_dict[loss_name] = self.iter_count_dict.get(loss_name, 0) + 1
|
| 304 |
+
|
| 305 |
+
def reset(self) -> None:
|
| 306 |
+
self.loss_dict = {}
|
| 307 |
+
self.iter_count_dict = {}
|
| 308 |
+
|
| 309 |
+
def gather_dict(self, dictionary: Dict[str, float | int]) -> Dict[str, float | int]:
|
| 310 |
+
n_ranks = world_size()
|
| 311 |
+
if n_ranks > 1:
|
| 312 |
+
dict_list = [None for _ in range(n_ranks)]
|
| 313 |
+
torch.distributed.all_gather_object(dict_list, dictionary)
|
| 314 |
+
# from list of dicts to dict of summed values
|
| 315 |
+
dictionary = {}
|
| 316 |
+
for d in dict_list:
|
| 317 |
+
for key, value in d.items():
|
| 318 |
+
dictionary[key] = dictionary.get(key, 0.0) + value
|
| 319 |
+
return dictionary
|
| 320 |
+
|
| 321 |
+
def get_stat(self) -> Dict[str, float]:
|
| 322 |
+
# number of ranks that logged this loss
|
| 323 |
+
rank_dict = self.gather_dict({k: 1 for k in self.loss_dict.keys()})
|
| 324 |
+
# number of times this loss was computed
|
| 325 |
+
count_dict = self.gather_dict(self.iter_count_dict)
|
| 326 |
+
# sum of all losses
|
| 327 |
+
loss_dict = self.gather_dict(self.loss_dict)
|
| 328 |
+
|
| 329 |
+
avg_loss_dict = {}
|
| 330 |
+
for loss_name, loss_val in loss_dict.items():
|
| 331 |
+
count = count_dict.get(loss_name, 0)
|
| 332 |
+
ranks = rank_dict.get(loss_name, 1)
|
| 333 |
+
iter_count = count / ranks
|
| 334 |
+
avg_loss = (loss_val / count) * (ranks / world_size()) if count > 0 else 0.0
|
| 335 |
+
logger.info(f"avg_{loss_name}: {avg_loss:.4f}".ljust(30) + f"iter count: {iter_count}")
|
| 336 |
+
avg_loss_dict[loss_name] = avg_loss
|
| 337 |
+
self.reset()
|
| 338 |
+
return avg_loss_dict
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
class WandbCallback(Callback):
|
| 342 |
+
"""
|
| 343 |
+
The callback gets precision for data from model
|
| 344 |
+
"""
|
| 345 |
+
|
| 346 |
+
def __init__(
|
| 347 |
+
self,
|
| 348 |
+
*args,
|
| 349 |
+
validation_logging_step: int = 1,
|
| 350 |
+
sample_logging_iter: Optional[int] = None,
|
| 351 |
+
vid_format: str = "mp4",
|
| 352 |
+
fps: int = 16,
|
| 353 |
+
syncnet_checkpoint_path: Optional[str] = None,
|
| 354 |
+
syncnet_vshift: int = 15,
|
| 355 |
+
syncnet_audio_sr: int = 16000,
|
| 356 |
+
**kwargs,
|
| 357 |
+
):
|
| 358 |
+
super().__init__(*args, **kwargs)
|
| 359 |
+
|
| 360 |
+
self.validation_logging_step = validation_logging_step
|
| 361 |
+
self.sample_logging_iter = sample_logging_iter
|
| 362 |
+
self.val_sample_map = None
|
| 363 |
+
self._val_gen_videos: list[torch.Tensor] = []
|
| 364 |
+
self._val_gt_videos: list[torch.Tensor] = []
|
| 365 |
+
self._val_audio_paths: list[str | None] = []
|
| 366 |
+
self.vid_format = vid_format
|
| 367 |
+
self.fps = fps
|
| 368 |
+
self.syncnet_checkpoint_path = syncnet_checkpoint_path
|
| 369 |
+
self.syncnet_vshift = syncnet_vshift
|
| 370 |
+
self.syncnet_audio_sr = syncnet_audio_sr
|
| 371 |
+
self._syncnet_scorer = None
|
| 372 |
+
self._val_sync_c_scores: list[float] = []
|
| 373 |
+
self.loss_dict_record = _LossDictRecord()
|
| 374 |
+
self.val_loss_dict_record = _LossDictRecord()
|
| 375 |
+
|
| 376 |
+
def on_app_begin(self) -> None:
|
| 377 |
+
assert hasattr(self, "config"), "Missing config in WandbCallback."
|
| 378 |
+
init_wandb(self.config)
|
| 379 |
+
self.offload_module_in_decoding = self.config.trainer.offload_module_in_decoding
|
| 380 |
+
# disable offloading if using FSDP
|
| 381 |
+
if self.config.trainer.fsdp:
|
| 382 |
+
self.offload_module_in_decoding = False
|
| 383 |
+
if self.sample_logging_iter is None:
|
| 384 |
+
self.sample_logging_iter = self.config.trainer.logging_iter
|
| 385 |
+
synchronize()
|
| 386 |
+
|
| 387 |
+
def _get_syncnet_scorer(self):
|
| 388 |
+
if self._syncnet_scorer is not None:
|
| 389 |
+
return self._syncnet_scorer
|
| 390 |
+
if not self.syncnet_checkpoint_path:
|
| 391 |
+
return None
|
| 392 |
+
try:
|
| 393 |
+
from lipforcing.methods.reward.sync_c_scorer import SyncCScorer
|
| 394 |
+
self._syncnet_scorer = SyncCScorer(
|
| 395 |
+
checkpoint_path=self.syncnet_checkpoint_path,
|
| 396 |
+
input_fps=25.0,
|
| 397 |
+
audio_sample_rate=self.syncnet_audio_sr,
|
| 398 |
+
vshift=self.syncnet_vshift,
|
| 399 |
+
device="cuda" if torch.cuda.is_available() else "cpu",
|
| 400 |
+
dtype=torch.float32,
|
| 401 |
+
)
|
| 402 |
+
logger.info(f"[WandbCallback] Loaded SyncCScorer from {self.syncnet_checkpoint_path}")
|
| 403 |
+
except Exception as e:
|
| 404 |
+
logger.warning(f"[WandbCallback] Failed to load SyncCScorer: {e}")
|
| 405 |
+
self.syncnet_checkpoint_path = None
|
| 406 |
+
return self._syncnet_scorer
|
| 407 |
+
|
| 408 |
+
def on_dataloader_init_end(
|
| 409 |
+
self, model: FastGenModel, dataloader_train, dataloader_val, iteration: int = 0
|
| 410 |
+
) -> None:
|
| 411 |
+
"""Upload GT validation videos at the start so they're always available for comparison.
|
| 412 |
+
|
| 413 |
+
Not decorated with @rank0_only — all ranks must enter this method to stay
|
| 414 |
+
synchronized (synchronize() calls dist.barrier). Only rank 0 does the actual
|
| 415 |
+
VAE decode and wandb upload.
|
| 416 |
+
"""
|
| 417 |
+
if dataloader_val is None:
|
| 418 |
+
return
|
| 419 |
+
# Skip GT upload if SKIP_GT_VAL_UPLOAD env var is set (avoids NCCL timeout)
|
| 420 |
+
if os.environ.get("SKIP_GT_VAL_UPLOAD", "0") == "1":
|
| 421 |
+
if wandb.run:
|
| 422 |
+
logger.info("SKIP_GT_VAL_UPLOAD=1 — skipping GT val video upload")
|
| 423 |
+
synchronize()
|
| 424 |
+
return
|
| 425 |
+
if iteration > 0:
|
| 426 |
+
if wandb.run:
|
| 427 |
+
logger.info("Resuming from checkpoint — skipping GT val video upload (already logged)")
|
| 428 |
+
synchronize()
|
| 429 |
+
return
|
| 430 |
+
if not hasattr(model.net, "vae"):
|
| 431 |
+
if wandb.run:
|
| 432 |
+
logger.info("No VAE loaded — skipping GT val video upload")
|
| 433 |
+
synchronize()
|
| 434 |
+
return
|
| 435 |
+
|
| 436 |
+
# Only rank 0 decodes and uploads; other ranks wait at the barrier below
|
| 437 |
+
if wandb.run:
|
| 438 |
+
logger.info("Uploading GT validation videos to wandb...")
|
| 439 |
+
device = model.device
|
| 440 |
+
try:
|
| 441 |
+
gt_videos = []
|
| 442 |
+
audio_paths = []
|
| 443 |
+
with torch.no_grad(), basic_utils.inference_mode(
|
| 444 |
+
precision_amp=model.precision_amp_enc, device_type=device.type
|
| 445 |
+
):
|
| 446 |
+
for step, data in enumerate(dataloader_val):
|
| 447 |
+
real = data["real"].to(device) # [1, 16, 21, 64, 64]
|
| 448 |
+
decoded = model.net.vae.decode(real[:1]) # [1, C, T, H, W]
|
| 449 |
+
gt_videos.append(self._to_uint8_video(decoded))
|
| 450 |
+
ap = None
|
| 451 |
+
if "audio_path" in data:
|
| 452 |
+
raw = data["audio_path"]
|
| 453 |
+
if isinstance(raw, (list, tuple)) and len(raw) > 0 and raw[0]:
|
| 454 |
+
ap = raw[0] if os.path.isfile(raw[0]) else None
|
| 455 |
+
audio_paths.append(ap)
|
| 456 |
+
gt_list = []
|
| 457 |
+
for v, ap in zip(gt_videos, audio_paths):
|
| 458 |
+
if ap:
|
| 459 |
+
gt_list.append(tensor_to_wandb_video_with_audio(v, ap, fps=self.fps))
|
| 460 |
+
else:
|
| 461 |
+
gt_list.append(wandb.Video(v[0].numpy(), fps=self.fps, format="mp4"))
|
| 462 |
+
wandb.log({"val_gt/videos": gt_list}, step=0)
|
| 463 |
+
logger.info(f"Uploaded {len(gt_videos)} GT validation videos to wandb")
|
| 464 |
+
except Exception as e:
|
| 465 |
+
logger.warning(f"Failed to upload GT val videos: {e}")
|
| 466 |
+
synchronize()
|
| 467 |
+
|
| 468 |
+
@rank0_only
|
| 469 |
+
def on_optimizer_step_begin(self, model: FastGenModel, iteration: int = 0) -> None:
|
| 470 |
+
assert hasattr(self, "config"), "Missing config in WandbCallback."
|
| 471 |
+
if iteration % self.config.trainer.logging_iter == 0:
|
| 472 |
+
for name, scheduler in model.scheduler_dict.items():
|
| 473 |
+
wandb.log({f"optimizer/lr_{name}": scheduler.get_last_lr()[0]}, step=iteration)
|
| 474 |
+
|
| 475 |
+
def get_sample_map(
|
| 476 |
+
self, model: FastGenModel, data_batch: dict[str, torch.Tensor], output_batch: dict[str, torch.Tensor | Callable]
|
| 477 |
+
) -> dict[str, wandb.Image | wandb.Video]:
|
| 478 |
+
# Collect generated and real data and create copies to avoid modifying the original dicts
|
| 479 |
+
sample_map = {}
|
| 480 |
+
gen_rand = output_batch["gen_rand"]
|
| 481 |
+
if isinstance(gen_rand, Callable):
|
| 482 |
+
synchronize()
|
| 483 |
+
gen_rand = gen_rand()
|
| 484 |
+
synchronize()
|
| 485 |
+
|
| 486 |
+
# Avoid modifying the original dicts
|
| 487 |
+
data_batch = data_batch.copy()
|
| 488 |
+
output_batch = output_batch.copy()
|
| 489 |
+
|
| 490 |
+
# Decide whether we want to visualize multistep teacher generation
|
| 491 |
+
if self.config.trainer.visualize_teacher:
|
| 492 |
+
assert "input_rand" in output_batch, "We need to know the noise to visualize teacher generation"
|
| 493 |
+
teacher_output = model.sample(
|
| 494 |
+
model.teacher,
|
| 495 |
+
output_batch["input_rand"][0:1],
|
| 496 |
+
data_batch["condition"][0:1], # e.g. text condition encoded by the text encoder
|
| 497 |
+
data_batch["neg_condition"][0:1], # e.g. negative text condition encoded by the text encoder
|
| 498 |
+
)
|
| 499 |
+
output_batch["gen_teacher"] = teacher_output
|
| 500 |
+
|
| 501 |
+
# Decode to pixel if it's in latent space
|
| 502 |
+
if hasattr(model.net, "init_preprocessors"):
|
| 503 |
+
torch.cuda.empty_cache()
|
| 504 |
+
device_nets = model.device
|
| 505 |
+
|
| 506 |
+
has_vae = hasattr(model.net, "vae")
|
| 507 |
+
if not has_vae:
|
| 508 |
+
model.net.init_vae()
|
| 509 |
+
model.net.vae.to(device=device_nets, dtype=model.precision)
|
| 510 |
+
|
| 511 |
+
if self.offload_module_in_decoding:
|
| 512 |
+
# offload the unneeded models to CPU (enable it if hitting OOM here)
|
| 513 |
+
logger.info(
|
| 514 |
+
f"GPU Memory BEFORE moving nets to CPU: {torch.cuda.memory_allocated(device_nets) / 1024 ** 2:.2f} MB"
|
| 515 |
+
)
|
| 516 |
+
if hasattr(model, "fake_score"):
|
| 517 |
+
model.fake_score = model.fake_score.to("cpu")
|
| 518 |
+
if hasattr(model, "teacher"):
|
| 519 |
+
model.teacher = model.teacher.to("cpu")
|
| 520 |
+
logger.info(
|
| 521 |
+
f"GPU Memory AFTER moving nets to CPU: {torch.cuda.memory_allocated(device_nets) / 1024 ** 2:.2f} MB"
|
| 522 |
+
)
|
| 523 |
+
synchronize()
|
| 524 |
+
|
| 525 |
+
with basic_utils.inference_mode(precision_amp=model.precision_amp_enc, device_type=device_nets.type):
|
| 526 |
+
if "real" in data_batch:
|
| 527 |
+
# only generate one sample for video
|
| 528 |
+
limit = 1 if len(data_batch["real"].shape) == 5 else len(data_batch["real"])
|
| 529 |
+
data_batch["real"] = model.net.vae.decode(data_batch["real"][:limit])
|
| 530 |
+
if isinstance(gen_rand, dict):
|
| 531 |
+
for k in gen_rand:
|
| 532 |
+
limit = 1 if len(gen_rand[k].shape) == 5 else len(gen_rand[k])
|
| 533 |
+
gen_rand[k] = model.net.vae.decode(gen_rand[k][:limit])
|
| 534 |
+
else:
|
| 535 |
+
limit = 1 if len(gen_rand.shape) == 5 else len(gen_rand)
|
| 536 |
+
gen_rand = model.net.vae.decode(gen_rand[:limit])
|
| 537 |
+
|
| 538 |
+
if "gen_teacher" in output_batch:
|
| 539 |
+
output_batch["gen_teacher"] = model.net.vae.decode(output_batch["gen_teacher"][:limit])
|
| 540 |
+
if logger.LOG_LEVEL == "DEBUG" and "gen_rand_train" in output_batch:
|
| 541 |
+
output_batch["gen_rand_train"] = model.net.vae.decode(output_batch["gen_rand_train"][:limit])
|
| 542 |
+
|
| 543 |
+
if not has_vae:
|
| 544 |
+
del model.net.vae
|
| 545 |
+
|
| 546 |
+
if self.offload_module_in_decoding:
|
| 547 |
+
# move back fake_score to gpu
|
| 548 |
+
if hasattr(model, "fake_score"):
|
| 549 |
+
model.fake_score = model.fake_score.to(device_nets)
|
| 550 |
+
if hasattr(model, "teacher"):
|
| 551 |
+
model.teacher = model.teacher.to(device_nets)
|
| 552 |
+
logger.info(
|
| 553 |
+
f"GPU Memory AFTER moving nets back to GPU: {torch.cuda.memory_allocated(device_nets) / 1024 ** 2:.2f} MB"
|
| 554 |
+
)
|
| 555 |
+
synchronize()
|
| 556 |
+
|
| 557 |
+
if wandb.run:
|
| 558 |
+
if (
|
| 559 |
+
"condition_raw" in data_batch
|
| 560 |
+
and isinstance(data_batch["condition_raw"], (list, tuple))
|
| 561 |
+
and isinstance(data_batch["condition_raw"][0], str)
|
| 562 |
+
):
|
| 563 |
+
caption = "\n".join(data_batch["condition_raw"][: len(gen_rand)])
|
| 564 |
+
else:
|
| 565 |
+
caption = None
|
| 566 |
+
|
| 567 |
+
# Check for audio path (from OmniAvatar dataloader) for audio-muxed video logging.
|
| 568 |
+
# The dataloader sets audio_path="" when audio.wav doesn't exist.
|
| 569 |
+
audio_path = None
|
| 570 |
+
if "audio_path" in data_batch:
|
| 571 |
+
ap = data_batch["audio_path"]
|
| 572 |
+
# default_collate turns strings into a list
|
| 573 |
+
if isinstance(ap, (list, tuple)) and len(ap) > 0:
|
| 574 |
+
audio_path = ap[0] if ap[0] else None
|
| 575 |
+
elif isinstance(ap, str):
|
| 576 |
+
audio_path = ap if ap else None
|
| 577 |
+
# Verify the file actually exists
|
| 578 |
+
if audio_path and not os.path.isfile(audio_path):
|
| 579 |
+
logger.warning(f"audio_path does not exist, logging silent video: {audio_path}")
|
| 580 |
+
audio_path = None
|
| 581 |
+
|
| 582 |
+
if isinstance(gen_rand, dict):
|
| 583 |
+
for k in gen_rand:
|
| 584 |
+
sample_map[f"student/generation/{k}"] = to_wandb(
|
| 585 |
+
gen_rand[k], caption=caption, vid_format=self.vid_format
|
| 586 |
+
)
|
| 587 |
+
else:
|
| 588 |
+
if audio_path and gen_rand.ndim == 5:
|
| 589 |
+
sample_map["student/generation"] = _to_wandb_with_audio(
|
| 590 |
+
gen_rand, audio_path, fps=self.fps, vid_format=self.vid_format, caption=caption,
|
| 591 |
+
)
|
| 592 |
+
else:
|
| 593 |
+
sample_map["student/generation"] = to_wandb(gen_rand, caption=caption, vid_format=self.vid_format, fps=self.fps)
|
| 594 |
+
if "real" in data_batch:
|
| 595 |
+
if audio_path and data_batch["real"].ndim == 5:
|
| 596 |
+
sample_map["data/real"] = _to_wandb_with_audio(
|
| 597 |
+
data_batch["real"], audio_path, fps=self.fps, vid_format=self.vid_format, caption=caption,
|
| 598 |
+
)
|
| 599 |
+
else:
|
| 600 |
+
sample_map["data/real"] = to_wandb(data_batch["real"], caption=caption, vid_format=self.vid_format, fps=self.fps)
|
| 601 |
+
if "gen_teacher" in output_batch:
|
| 602 |
+
sample_map["teacher/generation"] = to_wandb(
|
| 603 |
+
output_batch["gen_teacher"], caption=caption, vid_format=self.vid_format
|
| 604 |
+
)
|
| 605 |
+
if logger.LOG_LEVEL == "DEBUG" and "gen_rand_train" in output_batch:
|
| 606 |
+
sample_map["student/generation_train"] = to_wandb(
|
| 607 |
+
output_batch["gen_rand_train"], caption=caption, vid_format=self.vid_format
|
| 608 |
+
)
|
| 609 |
+
|
| 610 |
+
return sample_map
|
| 611 |
+
|
| 612 |
+
def log_sample_map(
|
| 613 |
+
self,
|
| 614 |
+
model: FastGenModel,
|
| 615 |
+
data_batch: dict[str, torch.Tensor],
|
| 616 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 617 |
+
suffix: str = "",
|
| 618 |
+
iteration: int = 0,
|
| 619 |
+
group: str = "train",
|
| 620 |
+
) -> None:
|
| 621 |
+
sample_map = self.get_sample_map(model, data_batch, output_batch)
|
| 622 |
+
sample_map = {f"{group}_media/{k}{suffix}": v for k, v in sample_map.items()}
|
| 623 |
+
if wandb.run:
|
| 624 |
+
wandb.log(sample_map, step=iteration)
|
| 625 |
+
synchronize()
|
| 626 |
+
gc.collect()
|
| 627 |
+
torch.cuda.empty_cache()
|
| 628 |
+
|
| 629 |
+
def log_stats(self, loss_dict_record: _LossDictRecord, iteration: int = 0, group: str = "train") -> None:
|
| 630 |
+
logger.info(f"logging {group} stats at iteration {iteration}" + "-" * 20)
|
| 631 |
+
# Collect distributed statistics
|
| 632 |
+
avg_loss_dict = loss_dict_record.get_stat()
|
| 633 |
+
stats = {f"{group}/{name}": val for name, val in avg_loss_dict.items()}
|
| 634 |
+
base_info = {"optimizer/iteration": iteration}
|
| 635 |
+
|
| 636 |
+
# log stats and base info
|
| 637 |
+
if wandb.run:
|
| 638 |
+
wandb.log(stats, step=iteration)
|
| 639 |
+
wandb.log(base_info, step=iteration)
|
| 640 |
+
|
| 641 |
+
def on_training_step_end(
|
| 642 |
+
self,
|
| 643 |
+
model: FastGenModel,
|
| 644 |
+
data_batch: dict[str, torch.Tensor],
|
| 645 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 646 |
+
loss_dict: dict[str, torch.Tensor],
|
| 647 |
+
iteration: int = 0,
|
| 648 |
+
) -> None:
|
| 649 |
+
self.loss_dict_record.add(loss_dict)
|
| 650 |
+
time_start = time.perf_counter()
|
| 651 |
+
logged = False
|
| 652 |
+
if iteration % self.config.trainer.logging_iter == 0 or iteration == 1:
|
| 653 |
+
self.log_stats(self.loss_dict_record, iteration=iteration, group="train")
|
| 654 |
+
logged = True
|
| 655 |
+
skip_early_sample = os.environ.get("SKIP_EARLY_SAMPLE_LOG", "0") == "1"
|
| 656 |
+
if iteration % self.sample_logging_iter == 0 or (iteration == 1 and not skip_early_sample):
|
| 657 |
+
self.log_sample_map(model, data_batch, output_batch, iteration=iteration, group="train")
|
| 658 |
+
logged = True
|
| 659 |
+
if logged:
|
| 660 |
+
time_taken = time.perf_counter() - time_start
|
| 661 |
+
logger.info(f"WandB logging complete after {time_taken:.2f} seconds")
|
| 662 |
+
|
| 663 |
+
@staticmethod
|
| 664 |
+
def _to_uint8_video(tensor: torch.Tensor, normalized: bool = False) -> torch.Tensor:
|
| 665 |
+
"""Convert [B, C, T, H, W] float video to [B, T, C, H, W] uint8 on CPU."""
|
| 666 |
+
t = tensor.permute(0, 2, 1, 3, 4) # [B, C, T, H, W] -> [B, T, C, H, W]
|
| 667 |
+
if normalized:
|
| 668 |
+
t = t.mul(255.0)
|
| 669 |
+
else:
|
| 670 |
+
t = t.mul(127.5).add(127.5)
|
| 671 |
+
return t.clamp(0, 255).to(torch.uint8).cpu()
|
| 672 |
+
|
| 673 |
+
def on_validation_step_end(
|
| 674 |
+
self,
|
| 675 |
+
model: FastGenModel,
|
| 676 |
+
data_batch: dict[str, torch.Tensor],
|
| 677 |
+
output_batch: dict[str, torch.Tensor | Callable],
|
| 678 |
+
loss_dict: dict[str, torch.Tensor],
|
| 679 |
+
step: int = 0,
|
| 680 |
+
iteration: int = 0,
|
| 681 |
+
idx: int = 0,
|
| 682 |
+
) -> None:
|
| 683 |
+
self.val_loss_dict_record.add(loss_dict)
|
| 684 |
+
|
| 685 |
+
if step % self.validation_logging_step == 0:
|
| 686 |
+
has_vae = hasattr(model.net, "vae")
|
| 687 |
+
if not has_vae:
|
| 688 |
+
return
|
| 689 |
+
|
| 690 |
+
# AR-generate the video
|
| 691 |
+
gen_rand = output_batch.get("gen_rand")
|
| 692 |
+
if gen_rand is not None and isinstance(gen_rand, Callable):
|
| 693 |
+
synchronize()
|
| 694 |
+
gen_rand = gen_rand()
|
| 695 |
+
synchronize()
|
| 696 |
+
|
| 697 |
+
if gen_rand is None:
|
| 698 |
+
return
|
| 699 |
+
|
| 700 |
+
# VAE decode + video collection — rank 0 only to avoid FSDP deadlock.
|
| 701 |
+
# Other ranks wait at synchronize() below.
|
| 702 |
+
_rank = torch.distributed.get_rank() if torch.distributed.is_initialized() else 0
|
| 703 |
+
if _rank == 0:
|
| 704 |
+
device = model.device
|
| 705 |
+
with torch.no_grad(), basic_utils.inference_mode(
|
| 706 |
+
precision_amp=model.precision_amp_enc, device_type=device.type
|
| 707 |
+
):
|
| 708 |
+
gen_decoded = model.net.vae.decode(gen_rand[:1])
|
| 709 |
+
gt_decoded = model.net.vae.decode(data_batch["real"][:1].to(device))
|
| 710 |
+
|
| 711 |
+
self._val_gen_videos.append(self._to_uint8_video(gen_decoded))
|
| 712 |
+
self._val_gt_videos.append(self._to_uint8_video(gt_decoded))
|
| 713 |
+
|
| 714 |
+
# Extract audio path for muxing
|
| 715 |
+
audio_path = None
|
| 716 |
+
if "audio_path" in data_batch:
|
| 717 |
+
ap = data_batch["audio_path"]
|
| 718 |
+
if isinstance(ap, (list, tuple)) and len(ap) > 0 and ap[0]:
|
| 719 |
+
audio_path = ap[0] if os.path.isfile(ap[0]) else None
|
| 720 |
+
self._val_audio_paths.append(audio_path)
|
| 721 |
+
|
| 722 |
+
# SyncNet-v2 evaluation on generated video
|
| 723 |
+
scorer = self._get_syncnet_scorer()
|
| 724 |
+
if scorer is not None and audio_path is not None:
|
| 725 |
+
try:
|
| 726 |
+
audio_wav = _load_audio_waveform(
|
| 727 |
+
audio_path, target_sr=self.syncnet_audio_sr,
|
| 728 |
+
)
|
| 729 |
+
if audio_wav is not None:
|
| 730 |
+
pixels = gen_decoded.clamp(-1.0, 1.0)
|
| 731 |
+
u8 = ((pixels + 1.0) * 127.5).to(torch.uint8)
|
| 732 |
+
face_frames = u8[0].permute(1, 0, 2, 3).contiguous() # [T, 3, H, W]
|
| 733 |
+
sync_c = scorer._score_single(face_frames, audio_wav)
|
| 734 |
+
self._val_sync_c_scores.append(sync_c.item())
|
| 735 |
+
logger.info(f"[SyncEval] val sample {step}: sync_c={sync_c.item():.3f}")
|
| 736 |
+
except Exception as e:
|
| 737 |
+
logger.warning(f"[SyncEval] Failed on val sample {step}: {e}")
|
| 738 |
+
|
| 739 |
+
synchronize()
|
| 740 |
+
gc.collect()
|
| 741 |
+
torch.cuda.empty_cache()
|
| 742 |
+
|
| 743 |
+
def on_validation_end(self, model: FastGenModel, iteration: int = 0, idx: int = 0) -> None:
|
| 744 |
+
self.log_stats(self.val_loss_dict_record, iteration=iteration, group=f"val{idx}")
|
| 745 |
+
if wandb.run and self._val_gen_videos:
|
| 746 |
+
gen_list = []
|
| 747 |
+
gt_list = []
|
| 748 |
+
for i, (gen_v, gt_v) in enumerate(zip(self._val_gen_videos, self._val_gt_videos)):
|
| 749 |
+
ap = self._val_audio_paths[i] if i < len(self._val_audio_paths) else None
|
| 750 |
+
caption = None
|
| 751 |
+
if i < len(self._val_sync_c_scores):
|
| 752 |
+
caption = f"sync_c={self._val_sync_c_scores[i]:.3f}"
|
| 753 |
+
if ap:
|
| 754 |
+
gen_list.append(tensor_to_wandb_video_with_audio(gen_v, ap, fps=self.fps, caption=caption))
|
| 755 |
+
gt_list.append(tensor_to_wandb_video_with_audio(gt_v, ap, fps=self.fps))
|
| 756 |
+
else:
|
| 757 |
+
gen_list.append(wandb.Video(gen_v[0].numpy(), fps=self.fps, format="mp4", caption=caption))
|
| 758 |
+
gt_list.append(wandb.Video(gt_v[0].numpy(), fps=self.fps, format="mp4"))
|
| 759 |
+
wandb.log({
|
| 760 |
+
f"val{idx}/generated": gen_list,
|
| 761 |
+
f"val{idx}/reconstructed": gt_list,
|
| 762 |
+
}, step=iteration)
|
| 763 |
+
logger.info(f"Logged {len(self._val_gen_videos)} val videos at iteration {iteration}")
|
| 764 |
+
if self._val_sync_c_scores:
|
| 765 |
+
mean_sync_c = sum(self._val_sync_c_scores) / len(self._val_sync_c_scores)
|
| 766 |
+
wandb.log({f"val{idx}/sync_c_mean": mean_sync_c}, step=iteration)
|
| 767 |
+
for i, sc in enumerate(self._val_sync_c_scores):
|
| 768 |
+
wandb.log({f"val{idx}/sync_c_sample_{i}": sc}, step=iteration)
|
| 769 |
+
logger.info(f"[SyncEval] val{idx} mean sync_c: {mean_sync_c:.3f} (n={len(self._val_sync_c_scores)})")
|
| 770 |
+
self._val_gen_videos = []
|
| 771 |
+
self._val_gt_videos = []
|
| 772 |
+
self._val_audio_paths = []
|
| 773 |
+
self._val_sync_c_scores = []
|
lipforcing/configs/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
lipforcing/configs/callbacks.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from lipforcing.utils import LazyCall as L
|
| 5 |
+
|
| 6 |
+
from lipforcing.callbacks.ct_schedule import CTScheduleCallback
|
| 7 |
+
from lipforcing.callbacks.grad_clip import GradClipCallback
|
| 8 |
+
from lipforcing.callbacks.param_count import ParamCountCallback
|
| 9 |
+
from lipforcing.callbacks.wandb import WandbCallback
|
| 10 |
+
from lipforcing.callbacks.ema import EMACallback
|
| 11 |
+
from lipforcing.callbacks.train_profiler import TrainProfilerCallback
|
| 12 |
+
from lipforcing.callbacks.gpu_stats import GPUStatsCallback
|
| 13 |
+
from lipforcing.callbacks.forced_weight_norm import ForcedWeightNormCallback
|
| 14 |
+
from lipforcing.callbacks.gpu_mem_profiler import MemTrackerCallback
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
CTSchedule_CALLBACK = dict(
|
| 18 |
+
ct_schedule=L(CTScheduleCallback)(q=2.0, ratio_limit=0.999, kimg_per_stage=12500),
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
EMA_CALLBACK = dict(
|
| 22 |
+
ema=L(EMACallback)(
|
| 23 |
+
type="constant", beta=0.9999, gamma=16.97, ema_halflife_kimg=500, ema_rampup_ratio=0.05, start_iter=0
|
| 24 |
+
),
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
EMA_CONST_CALLBACKS = dict(
|
| 28 |
+
ema_9999=L(EMACallback)(type="constant", beta=0.9999, ema_name="ema_9999"),
|
| 29 |
+
ema_99995=L(EMACallback)(type="constant", beta=0.99995, ema_name="ema_99995"),
|
| 30 |
+
ema_9996=L(EMACallback)(type="constant", beta=0.9996, ema_name="ema_9996"),
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
EMA_POWER_CALLBACKS = dict(
|
| 34 |
+
ema_1=L(EMACallback)(type="power", gamma=96.99, ema_name="ema_1"),
|
| 35 |
+
ema_5=L(EMACallback)(type="power", gamma=16.97, ema_name="ema_5"),
|
| 36 |
+
ema_10=L(EMACallback)(type="power", gamma=6.94, ema_name="ema_10"),
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
ForcedWeightNorm_CALLBACK = dict(
|
| 40 |
+
forced_weight_norm=L(ForcedWeightNormCallback)(),
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
GradClip_CALLBACK = dict(
|
| 44 |
+
grad_clip=L(GradClipCallback)(grad_norm=10.0, model_key="net"),
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
GPUStats_CALLBACK = dict(
|
| 48 |
+
gpu_stats=L(GPUStatsCallback)(every_n=100),
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
ParamCount_CALLBACK = dict(
|
| 52 |
+
param_count=L(ParamCountCallback)(),
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
TrainProfiler_CALLBACK = dict(
|
| 56 |
+
train_profiler=L(TrainProfilerCallback)(every_n=100),
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
WANDB_CALLBACK = dict(
|
| 60 |
+
wandb=L(WandbCallback)(sample_logging_iter=None),
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
MemTracker_CALLBACK = dict(
|
| 64 |
+
mem_tracker=L(MemTrackerCallback)(),
|
| 65 |
+
)
|
lipforcing/configs/config.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
from typing import Any, List, Optional, Dict
|
| 6 |
+
|
| 7 |
+
import copy
|
| 8 |
+
import attrs
|
| 9 |
+
from omegaconf import DictConfig
|
| 10 |
+
|
| 11 |
+
from lipforcing.utils import LazyCall as L
|
| 12 |
+
from lipforcing.configs.callbacks import WANDB_CALLBACK
|
| 13 |
+
from lipforcing.configs.opt import BaseOptimizerConfig, BaseSchedulerConfig
|
| 14 |
+
from lipforcing.methods import FastGenModel
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@attrs.define(slots=False)
|
| 18 |
+
class CuDNNConfig:
|
| 19 |
+
# If set to True, cudnn will use deterministic cudnn functions for better reproducibility.
|
| 20 |
+
deterministic: bool = False
|
| 21 |
+
# If set to True, cudnn will benchmark several algorithms and pick the fastest one.
|
| 22 |
+
benchmark: bool = True
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@attrs.define(slots=False)
|
| 26 |
+
class LogConfig:
|
| 27 |
+
# Project name
|
| 28 |
+
project: str = "LipForcing"
|
| 29 |
+
# Experiment name
|
| 30 |
+
group: str = "default"
|
| 31 |
+
# Run/job name
|
| 32 |
+
name: str = "debug"
|
| 33 |
+
# W&B mode, can be "online" or "disabled".
|
| 34 |
+
wandb_mode: str = "online"
|
| 35 |
+
# W&B entity (team or username)
|
| 36 |
+
wandb_entity: Optional[str] = None
|
| 37 |
+
# Wandb credential path
|
| 38 |
+
wandb_credential: str = "./credentials/wandb_api.txt"
|
| 39 |
+
|
| 40 |
+
# save path
|
| 41 |
+
@property
|
| 42 |
+
def save_path(self) -> str:
|
| 43 |
+
return os.path.join(
|
| 44 |
+
os.environ.get("LIPFORCING_OUTPUT_ROOT", "outputs"), f"{self.project}/{self.group}/{self.name}"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@attrs.define(slots=False)
|
| 49 |
+
class EvalConfig:
|
| 50 |
+
# Number of samples to generate
|
| 51 |
+
num_samples: int = 50000
|
| 52 |
+
# Save a small batch of images
|
| 53 |
+
save_images: bool = False
|
| 54 |
+
# Minimum checkpoint to evaluate
|
| 55 |
+
min_ckpt: int = 0
|
| 56 |
+
# Maximum checkpoint to evaluate
|
| 57 |
+
max_ckpt: int = 100000000
|
| 58 |
+
# Directory to save samples
|
| 59 |
+
samples_dir: str = "samples"
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@attrs.define(slots=False)
|
| 63 |
+
class BaseCheckpointerConfig:
|
| 64 |
+
save_dir: str = "checkpoints"
|
| 65 |
+
use_s3: bool = False
|
| 66 |
+
s3_container: str = "s3://checkpoints/lipforcing"
|
| 67 |
+
s3_credential: str = "./credentials/s3.json"
|
| 68 |
+
|
| 69 |
+
# path to pretrained model (from previous stages),
|
| 70 |
+
# it's used by loading fsdp/ddp trained ckpt to an fsdp/ddp pipeline
|
| 71 |
+
pretrained_ckpt_path: str = ""
|
| 72 |
+
# submodule names of model and keys of a pretrained checkpoint of the form {"model": {"submodule_key": ...}, ...}
|
| 73 |
+
pretrained_ckpt_key_map: Dict[str, str] = {"net": "net"}
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@attrs.define(slots=False)
|
| 77 |
+
class SampleTConfig:
|
| 78 |
+
"""Config for sampling t from a time distribution."""
|
| 79 |
+
|
| 80 |
+
# time distribution (currently supporting: uniform, lognormal, polynomial, logitnormal, shift, and log_t)
|
| 81 |
+
time_dist_type: str = "uniform"
|
| 82 |
+
# mu in lognormal, logitnormal, and log_t distributions
|
| 83 |
+
train_p_mean: float = -1.1
|
| 84 |
+
# sigma in lognormal, logitnormal, and log_t distributions
|
| 85 |
+
train_p_std: float = 2.0
|
| 86 |
+
# shift value in shifted sampling (t_shifted = t * shift / (t * (shift - 1) + 1))
|
| 87 |
+
shift: float = 5.0
|
| 88 |
+
# lowest value in truncated range
|
| 89 |
+
min_t: float = 0.002
|
| 90 |
+
# highest value in truncated range
|
| 91 |
+
max_t: float = 80.0
|
| 92 |
+
# If provided, it is in the form [t_max, ..., 0] where len(t_list) needs to equal student_sample_steps + 1
|
| 93 |
+
t_list: Optional[List[float]] = None
|
| 94 |
+
# degree of freedom in log-transformed student-t distribution
|
| 95 |
+
log_t_df: float = 0.01
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@attrs.define(slots=False)
|
| 99 |
+
class SyncWindowCFGConfig:
|
| 100 |
+
"""Sync-Window DMD (SW-DMD) — timestep-gated classifier-free guidance.
|
| 101 |
+
|
| 102 |
+
This is the paper's Sync-Window DMD (SW-DMD; Sec. 4.3, Eq. 6): the teacher
|
| 103 |
+
CFG scale is gated by the DMD re-noising timestep. When enabled, CFG uses the
|
| 104 |
+
configured guidance_scale only when t is inside the sync window [t_lo, t_hi]
|
| 105 |
+
(the shifted-timestep band corresponding to teacher ODE steps j in [20, 40]);
|
| 106 |
+
outside the window the effective scale is 1.0 (no-CFG, fidelity-preserving).
|
| 107 |
+
Both teacher forward passes always run for FSDP consistency.
|
| 108 |
+
|
| 109 |
+
When ``reverse=True``, the window is inverted: CFG is OFF inside [t_lo, t_hi]
|
| 110 |
+
and ON outside it (the reverse-window ablation).
|
| 111 |
+
"""
|
| 112 |
+
enabled: bool = False
|
| 113 |
+
t_lo: float = 0.0
|
| 114 |
+
t_hi: float = 1.0
|
| 115 |
+
reverse: bool = False
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
@attrs.define(slots=False)
|
| 119 |
+
class BaseModelConfig:
|
| 120 |
+
# Use factory functions to ensure each instance gets its own copy
|
| 121 |
+
net: Optional[dict] = None # set per-experiment (OmniAvatar configs assign the causal/bidirectional net)
|
| 122 |
+
teacher: Optional[dict] = None # Usually not used, only used when teacher is different from net (i.e. Causvid)
|
| 123 |
+
fake_score_net: Optional[dict] = None # When critic architecture differs from teacher (e.g. 1.3B fake_score with 14B teacher)
|
| 124 |
+
|
| 125 |
+
# guidance scale for classifier-free guidance in teacher diffusion model. None means no guidance.
|
| 126 |
+
guidance_scale: Optional[float] = None
|
| 127 |
+
# Sync-Window DMD (SW-DMD): timestep-gated CFG (applies when guidance_scale is not None)
|
| 128 |
+
sync_window_cfg: SyncWindowCFGConfig = attrs.field(factory=SyncWindowCFGConfig)
|
| 129 |
+
|
| 130 |
+
# enable skip layer guidance (currently only wan network has the skip_layers option in cfg)
|
| 131 |
+
skip_layers: List[int] | None = None
|
| 132 |
+
|
| 133 |
+
# optimizer and scheduler for the main net (i.e., one-step generator in DMD)
|
| 134 |
+
net_optimizer: dict = attrs.field(factory=lambda: copy.deepcopy(BaseOptimizerConfig))
|
| 135 |
+
net_scheduler: dict = attrs.field(factory=lambda: copy.deepcopy(BaseSchedulerConfig))
|
| 136 |
+
|
| 137 |
+
# sampling t from a given distribution
|
| 138 |
+
sample_t_cfg: SampleTConfig = attrs.field(factory=SampleTConfig)
|
| 139 |
+
|
| 140 |
+
# shape of the input to the model (defaults to CIFAR-10)
|
| 141 |
+
input_shape: List[int] = [3, 32, 32]
|
| 142 |
+
# device ("cuda" or "cpu")
|
| 143 |
+
device: str = "cuda"
|
| 144 |
+
|
| 145 |
+
# enable gradient scaler
|
| 146 |
+
grad_scaler_enabled: bool = False
|
| 147 |
+
grad_scaler_init_scale: float = 65536.0
|
| 148 |
+
grad_scaler_growth_interval: int = 2000
|
| 149 |
+
|
| 150 |
+
# path to the pretrained teacher model ckpt
|
| 151 |
+
pretrained_model_path: str = ""
|
| 152 |
+
# path to the pretrained student net ckpt (if different from the teacher)
|
| 153 |
+
pretrained_student_net_path: str = ""
|
| 154 |
+
# initialize student from the above checkpoints (can be turned off to only load weights to the teacher)
|
| 155 |
+
load_student_weights: bool = True
|
| 156 |
+
|
| 157 |
+
# enable preprocessors in the model
|
| 158 |
+
enable_preprocessors: bool = True
|
| 159 |
+
|
| 160 |
+
# EMA for the main net (requires EMACallback)
|
| 161 |
+
use_ema: Any = False
|
| 162 |
+
|
| 163 |
+
# multistep generation if larger than 1 (default: single-step generation)
|
| 164 |
+
student_sample_steps: int = 1
|
| 165 |
+
# sampling type in multistep generation ('sde', 'ode')
|
| 166 |
+
student_sample_type: str = "sde"
|
| 167 |
+
|
| 168 |
+
# Enable memory-efficient model loading with meta device:
|
| 169 |
+
# - Rank 0 loads pretrained weights normally
|
| 170 |
+
# - Other ranks use torch.device("meta") for ZERO memory allocation (just metadata)
|
| 171 |
+
# - FSDP materializes meta tensors and broadcasts weights from rank 0
|
| 172 |
+
# This dramatically speeds up initialization for large models (14B+):
|
| 173 |
+
# - Reduces RAM from N*model_size to 1*model_size
|
| 174 |
+
# - Eliminates disk I/O contention (N parallel reads -> 1 read)
|
| 175 |
+
# - Expected speedup: 30+ min -> <1 min for 14B models on 8 GPUs
|
| 176 |
+
fsdp_meta_init: bool = False
|
| 177 |
+
|
| 178 |
+
# whether to add the teacher model to the fsdp_dict
|
| 179 |
+
add_teacher_to_fsdp_dict: bool = True
|
| 180 |
+
|
| 181 |
+
# whether to find unused parameters in ddp
|
| 182 |
+
# - can be turned off for improved performance
|
| 183 |
+
# - however, it is required if the model has a discriminator or the net initializes unused modules (e.g., for logvar predictions)
|
| 184 |
+
ddp_find_unused_parameters: bool = True
|
| 185 |
+
|
| 186 |
+
# precision variables (choose from "float64", "float32", "bfloat16", or "float16")
|
| 187 |
+
# (precision of the time steps is handled in the noise scheduler, defaulting to float64 for numerical stability)
|
| 188 |
+
|
| 189 |
+
# precision for model/optimizer states and data - recommended to be float32 if precision_amp is not None
|
| 190 |
+
precision: str = "float32"
|
| 191 |
+
# AMP during training - if None or equal to precision, AMP is disabled during training.
|
| 192 |
+
precision_amp: str | None = None
|
| 193 |
+
# AMP during inference - if None or equal to precision, AMP is disabled during inference.
|
| 194 |
+
precision_amp_infer: str | None = None
|
| 195 |
+
# AMP during en-/decoding (e.g., for VAEs or text encoders) - if None or equal to precision, AMP is disabled during en-/decoding.
|
| 196 |
+
precision_amp_enc: str | None = None
|
| 197 |
+
# FSDP2 precision for parameter storage and gradient reduction.
|
| 198 |
+
# If None, defaults to `precision`. Useful for storing params/grads in float32 while computing in bfloat16.
|
| 199 |
+
precision_fsdp: str | None = None
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
@attrs.define(slots=False)
|
| 203 |
+
class BaseTrainerConfig:
|
| 204 |
+
cudnn: CuDNNConfig = attrs.field(factory=CuDNNConfig)
|
| 205 |
+
checkpointer: BaseCheckpointerConfig = attrs.field(factory=BaseCheckpointerConfig)
|
| 206 |
+
|
| 207 |
+
# Callbacks configs.
|
| 208 |
+
callbacks: dict = DictConfig(WANDB_CALLBACK)
|
| 209 |
+
|
| 210 |
+
# save checkpoint frequency
|
| 211 |
+
save_ckpt_iter: int = 5000
|
| 212 |
+
# test on validation set frequency
|
| 213 |
+
validation_iter: int = 1000
|
| 214 |
+
# skip validation before first training step (avoids compilation hang with FlexAttention)
|
| 215 |
+
skip_initial_validation: bool = False
|
| 216 |
+
# logging frequency
|
| 217 |
+
logging_iter: int = 1000
|
| 218 |
+
# maximum training iteration
|
| 219 |
+
max_iter: int = 1000000
|
| 220 |
+
# whether to visualize multistep teacher generation
|
| 221 |
+
visualize_teacher: bool = False
|
| 222 |
+
|
| 223 |
+
# Set the random seed.
|
| 224 |
+
seed: int = 0
|
| 225 |
+
# Validation seed
|
| 226 |
+
val_seed: int | None = None
|
| 227 |
+
# Resume
|
| 228 |
+
resume: bool = True
|
| 229 |
+
|
| 230 |
+
# DDP Parallelism
|
| 231 |
+
ddp: bool = False
|
| 232 |
+
# FSDP Parallelism
|
| 233 |
+
fsdp: bool = False
|
| 234 |
+
# Enable TensorFloat32 (convolution and matmul)
|
| 235 |
+
tf32_enabled: bool = True
|
| 236 |
+
|
| 237 |
+
# Number of gradient accumulation rounds
|
| 238 |
+
grad_accum_rounds: int = 1
|
| 239 |
+
|
| 240 |
+
# Global batch size (if not None, overrides grad_accum_rounds to match the specified batch size)
|
| 241 |
+
batch_size_global: int | None = None
|
| 242 |
+
|
| 243 |
+
# offload other modules to cpu during latent decoding
|
| 244 |
+
offload_module_in_decoding: bool = False
|
| 245 |
+
|
| 246 |
+
# apply cpu offloading in fsdp
|
| 247 |
+
fsdp_cpu_offload: bool = False
|
| 248 |
+
# Fallback minimum number of parameters for FSDP wrapping
|
| 249 |
+
# (10M wraps large models into fairly small shards)
|
| 250 |
+
# The FastGenNetwork should provide a fully_shard method that can be used to shard the network.
|
| 251 |
+
# If we need to shard a different module, we fall back to an auto-sharding policy based on this value.
|
| 252 |
+
fsdp_min_num_params: int = 10_000_000
|
| 253 |
+
# Sharding group size for FSDP. If None, fully shard across all ranks.
|
| 254 |
+
# If set, creates a 2D mesh with (replicate, shard) dimensions.
|
| 255 |
+
fsdp_sharding_group_size: Optional[int] = None
|
| 256 |
+
|
| 257 |
+
# global variables
|
| 258 |
+
global_vars: Optional[dict] = None
|
| 259 |
+
global_vars_val: List[dict | None] = [None]
|
| 260 |
+
|
| 261 |
+
# augment config
|
| 262 |
+
augment_pipe: Optional[DictConfig] = None
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
@attrs.define(slots=False)
|
| 266 |
+
class BaseConfig:
|
| 267 |
+
# Log config.
|
| 268 |
+
log_config: LogConfig = attrs.field(factory=LogConfig)
|
| 269 |
+
|
| 270 |
+
# Trainer configs.
|
| 271 |
+
trainer: BaseTrainerConfig = attrs.field(factory=BaseTrainerConfig)
|
| 272 |
+
|
| 273 |
+
# Model configs.
|
| 274 |
+
model: BaseModelConfig = attrs.field(factory=BaseModelConfig)
|
| 275 |
+
model_class: DictConfig = L(FastGenModel)(config=None)
|
| 276 |
+
|
| 277 |
+
# Data configs.
|
| 278 |
+
dataloader_train: dict = attrs.field(factory=lambda: DictConfig({"batch_size": 1})) # placeholder; experiments assign the real loader
|
| 279 |
+
dataloader_val: Any = None
|
| 280 |
+
|
| 281 |
+
# Eval configs.
|
| 282 |
+
eval: EvalConfig = attrs.field(factory=EvalConfig)
|
lipforcing/configs/config_utils.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
from typing import Any, Optional, List, Dict
|
| 6 |
+
import inspect
|
| 7 |
+
from copy import deepcopy
|
| 8 |
+
|
| 9 |
+
import attrs
|
| 10 |
+
import yaml
|
| 11 |
+
from omegaconf import DictConfig, OmegaConf, ListConfig
|
| 12 |
+
from hydra import compose, initialize
|
| 13 |
+
from hydra.core.config_store import ConfigStore
|
| 14 |
+
|
| 15 |
+
import importlib
|
| 16 |
+
from dataclasses import fields as dataclass_fields
|
| 17 |
+
import attr
|
| 18 |
+
from dataclasses import is_dataclass
|
| 19 |
+
import lipforcing.utils.logging_utils as logger
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def import_config_from_python_file(config_file: str) -> Any:
|
| 23 |
+
"""
|
| 24 |
+
Import a config from a python file.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
config_file (str): The path to the python file.
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
Any: The config object.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
if not config_file.endswith(".py"):
|
| 34 |
+
raise ValueError("Config file must be a Python file with a .py extension. " f"Received: {config_file}")
|
| 35 |
+
|
| 36 |
+
if not os.path.isfile(config_file):
|
| 37 |
+
raise FileNotFoundError(f"Config file ({config_file}) not found.")
|
| 38 |
+
|
| 39 |
+
# Convert to importable module format.
|
| 40 |
+
config_module = config_file.replace("/", ".").replace(".py", "")
|
| 41 |
+
|
| 42 |
+
# Import the module
|
| 43 |
+
try:
|
| 44 |
+
config = importlib.import_module(config_module)
|
| 45 |
+
except ImportError as e:
|
| 46 |
+
logger.error(f"Failed to import config from python file: {e}")
|
| 47 |
+
raise e
|
| 48 |
+
|
| 49 |
+
return config.create_config()
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def config_from_dict(ref_instance: Any, kwargs: Any) -> Any:
|
| 53 |
+
"""
|
| 54 |
+
Construct an instance of the same type as ref_instance using the provided dictionary or data or unstructured data
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
ref_instance: The reference instance to determine the type and fields when needed
|
| 58 |
+
kwargs: A dictionary of keyword arguments to use for constructing the new instance or primitive data or unstructured data
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
Any: A new instance of the same type as ref_instance constructed using the provided kwargs or the primitive data or unstructured data
|
| 62 |
+
|
| 63 |
+
Raises:
|
| 64 |
+
AssertionError: If the fields do not match or if extra keys are found.
|
| 65 |
+
Exception: If there is an error constructing the new instance.
|
| 66 |
+
"""
|
| 67 |
+
is_type = is_attrs_or_dataclass(ref_instance)
|
| 68 |
+
if not is_type:
|
| 69 |
+
return kwargs
|
| 70 |
+
else:
|
| 71 |
+
ref_fields = set(get_fields(ref_instance))
|
| 72 |
+
assert isinstance(kwargs, dict) or isinstance(kwargs, DictConfig), "kwargs must be a dictionary or a DictConfig"
|
| 73 |
+
keys = set(kwargs.keys())
|
| 74 |
+
|
| 75 |
+
# ref_fields must equal to or include all keys
|
| 76 |
+
extra_keys = keys - ref_fields
|
| 77 |
+
assert (
|
| 78 |
+
ref_fields == keys or keys.issubset(ref_fields)
|
| 79 |
+
), f"Fields mismatch: {ref_fields} != {keys}. Extra keys found: {extra_keys} \n \t when constructing {type(ref_instance)} with {keys}"
|
| 80 |
+
|
| 81 |
+
resolved_kwargs: Dict[str, Any] = {}
|
| 82 |
+
for f in keys:
|
| 83 |
+
resolved_kwargs[f] = config_from_dict(getattr(ref_instance, f), kwargs[f])
|
| 84 |
+
try:
|
| 85 |
+
new_instance = type(ref_instance)(**resolved_kwargs)
|
| 86 |
+
except Exception as e:
|
| 87 |
+
logger.error(f"Error when constructing {type(ref_instance)} with {resolved_kwargs}")
|
| 88 |
+
logger.error(e)
|
| 89 |
+
raise e
|
| 90 |
+
return new_instance
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def override_config_with_opts(config: Any, opts: Optional[List[str]] = None) -> Any:
|
| 94 |
+
"""
|
| 95 |
+
Override the config with the opts.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
config (Any): The config object.
|
| 99 |
+
opts (Dict[str, Any]): Dict for the overrides.
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
Any: The config object.
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
# Convert Config object to a DictConfig object for Hydra
|
| 106 |
+
|
| 107 |
+
if not isinstance(config, DictConfig):
|
| 108 |
+
config_dict = attrs.asdict(config)
|
| 109 |
+
config_dict = DictConfig(content=config_dict, flags={"allow_objects": True})
|
| 110 |
+
else:
|
| 111 |
+
config_dict = config
|
| 112 |
+
|
| 113 |
+
# Use Hydra to handle overrides
|
| 114 |
+
cs = ConfigStore.instance()
|
| 115 |
+
cs.store(name="config", node=config_dict)
|
| 116 |
+
|
| 117 |
+
if opts is None:
|
| 118 |
+
opts = []
|
| 119 |
+
|
| 120 |
+
if len(opts) > 0 and opts[0] != "-":
|
| 121 |
+
raise ValueError(f"opts must start with '-' to separate from other arguments. Got: {opts}")
|
| 122 |
+
opts = opts[1:]
|
| 123 |
+
with initialize(version_base=None):
|
| 124 |
+
try:
|
| 125 |
+
cfg = compose(config_name="config", overrides=opts)
|
| 126 |
+
except Exception as e:
|
| 127 |
+
raise ValueError(f"Failed to compose config with opts: {e}")
|
| 128 |
+
|
| 129 |
+
OmegaConf.resolve(cfg)
|
| 130 |
+
|
| 131 |
+
config = config_from_dict(config, cfg)
|
| 132 |
+
|
| 133 |
+
return config
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def is_attrs_or_dataclass(obj) -> bool:
|
| 137 |
+
"""
|
| 138 |
+
Check if the object is an instance of an attrs class or a dataclass.
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
obj: The object to check.
|
| 142 |
+
|
| 143 |
+
Returns:
|
| 144 |
+
bool: True if the object is an instance of an attrs class or a dataclass, False otherwise.
|
| 145 |
+
"""
|
| 146 |
+
return is_dataclass(obj) or attr.has(type(obj))
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def get_fields(obj):
|
| 150 |
+
"""
|
| 151 |
+
Get the fields of an attrs class or a dataclass.
|
| 152 |
+
|
| 153 |
+
Args:
|
| 154 |
+
obj: The object to get fields from. Must be an instance of an attrs class or a dataclass.
|
| 155 |
+
|
| 156 |
+
Returns:
|
| 157 |
+
list: A list of field names.
|
| 158 |
+
|
| 159 |
+
Raises:
|
| 160 |
+
ValueError: If the object is neither an attrs class nor a dataclass.
|
| 161 |
+
"""
|
| 162 |
+
if is_dataclass(obj):
|
| 163 |
+
return [field.name for field in dataclass_fields(obj)]
|
| 164 |
+
elif attr.has(type(obj)):
|
| 165 |
+
return [field.name for field in attr.fields(type(obj))]
|
| 166 |
+
else:
|
| 167 |
+
raise ValueError("The object is neither an attrs class nor a dataclass.")
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def serialize_config(
|
| 171 |
+
config: Any,
|
| 172 |
+
return_type: str = "dict",
|
| 173 |
+
path: str | bytes | None = None,
|
| 174 |
+
filename: str = "config.yaml",
|
| 175 |
+
include_defaults: bool = False,
|
| 176 |
+
) -> Dict[str, Any] | str:
|
| 177 |
+
"""
|
| 178 |
+
Serialize a config (BaseConfig or DictConfig) to various formats.
|
| 179 |
+
|
| 180 |
+
Args:
|
| 181 |
+
config: The config to serialize (BaseConfig attrs object or DictConfig).
|
| 182 |
+
return_type: Output format - "dict" (plain dict), "yaml" (YAML string), or "file" (save to file).
|
| 183 |
+
path: Directory path to save the file. Required if return_type is "file".
|
| 184 |
+
filename: Name of the file to save. Only used if return_type is "file".
|
| 185 |
+
include_defaults: If True, add default parameter values from _target_ classes.
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
Dict[str, Any] if return_type is "dict"
|
| 189 |
+
str (YAML) if return_type is "yaml" or "file"
|
| 190 |
+
|
| 191 |
+
Raises:
|
| 192 |
+
ValueError: If return_type is "file" but path is not provided.
|
| 193 |
+
"""
|
| 194 |
+
if return_type == "file" and path is None:
|
| 195 |
+
raise ValueError("path must be provided when return_type is 'file'")
|
| 196 |
+
|
| 197 |
+
# Deep copy to avoid modifying original
|
| 198 |
+
config = deepcopy(config)
|
| 199 |
+
|
| 200 |
+
# Normalize to DictConfig with object support
|
| 201 |
+
if not isinstance(config, DictConfig):
|
| 202 |
+
config_dict = attrs.asdict(config)
|
| 203 |
+
config_omegaconf = DictConfig(content=config_dict, flags={"allow_objects": True})
|
| 204 |
+
else:
|
| 205 |
+
config_omegaconf = config
|
| 206 |
+
|
| 207 |
+
def is_serializable(item) -> bool:
|
| 208 |
+
try:
|
| 209 |
+
OmegaConf.to_yaml(item)
|
| 210 |
+
return True
|
| 211 |
+
except Exception:
|
| 212 |
+
return False
|
| 213 |
+
|
| 214 |
+
def get_default_params(cls_or_func):
|
| 215 |
+
if callable(cls_or_func):
|
| 216 |
+
signature = inspect.signature(cls_or_func)
|
| 217 |
+
else:
|
| 218 |
+
signature = inspect.signature(cls_or_func.__init__)
|
| 219 |
+
params = signature.parameters
|
| 220 |
+
return {name: param.default for name, param in params.items() if param.default is not inspect.Parameter.empty}
|
| 221 |
+
|
| 222 |
+
def process_config(conf):
|
| 223 |
+
if isinstance(conf, DictConfig):
|
| 224 |
+
for key, value in conf.items():
|
| 225 |
+
if isinstance(value, (DictConfig, ListConfig)):
|
| 226 |
+
# Optionally add default params from _target_ classes
|
| 227 |
+
if include_defaults:
|
| 228 |
+
try:
|
| 229 |
+
if "_target_" in value:
|
| 230 |
+
default_params = get_default_params(value["_target_"])
|
| 231 |
+
for default_key, default_v in default_params.items():
|
| 232 |
+
if default_key not in value:
|
| 233 |
+
value[default_key] = default_v
|
| 234 |
+
except Exception as e:
|
| 235 |
+
logger.error(f"Failed to add default argument values: {e}")
|
| 236 |
+
process_config(value)
|
| 237 |
+
else:
|
| 238 |
+
if not is_serializable(value) and value is not None:
|
| 239 |
+
conf[key] = str(value)
|
| 240 |
+
elif isinstance(conf, ListConfig):
|
| 241 |
+
for i, item in enumerate(conf):
|
| 242 |
+
if isinstance(item, (DictConfig, ListConfig)):
|
| 243 |
+
process_config(item)
|
| 244 |
+
else:
|
| 245 |
+
if not is_serializable(item) and item is not None:
|
| 246 |
+
conf[i] = str(item)
|
| 247 |
+
else:
|
| 248 |
+
raise NotImplementedError("Input config must be a DictConfig or ListConfig.")
|
| 249 |
+
return conf
|
| 250 |
+
|
| 251 |
+
config_omegaconf = process_config(config_omegaconf)
|
| 252 |
+
result_dict: Dict[str, Any] = OmegaConf.to_container(config_omegaconf, resolve=True) # type: ignore
|
| 253 |
+
|
| 254 |
+
if return_type == "dict":
|
| 255 |
+
return result_dict
|
| 256 |
+
|
| 257 |
+
# For yaml and file, convert to YAML string
|
| 258 |
+
yaml_str = yaml.dump(result_dict, default_flow_style=False, sort_keys=True)
|
| 259 |
+
|
| 260 |
+
if return_type == "file":
|
| 261 |
+
os.makedirs(path, exist_ok=True) # type: ignore
|
| 262 |
+
with open(f"{path}/{filename}", "w") as f:
|
| 263 |
+
f.write(yaml_str)
|
| 264 |
+
logger.info(f"Config is saved at {path}/{filename}")
|
| 265 |
+
|
| 266 |
+
return yaml_str
|
lipforcing/configs/discriminator.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from omegaconf import DictConfig
|
| 5 |
+
|
| 6 |
+
from lipforcing.utils import LazyCall as L
|
| 7 |
+
from lipforcing.networks.discriminators import Discriminator_VideoDiT
|
| 8 |
+
|
| 9 |
+
# 1.3B patchify: spatial-2, temporal-1; inner_dim=1536; layer=30
|
| 10 |
+
Discriminator_Wan_1_3B_Config: DictConfig = L(Discriminator_VideoDiT)(
|
| 11 |
+
feature_indices=None,
|
| 12 |
+
num_blocks=30,
|
| 13 |
+
disc_type="dit_simple_conv3d",
|
| 14 |
+
inner_dim=1536 // 4,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# 14B patchify: spatial-2, temporal-1; inner_dim=5120; layer=40
|
| 18 |
+
Discriminator_Wan_14B_Config: DictConfig = L(Discriminator_VideoDiT)(
|
| 19 |
+
feature_indices=None,
|
| 20 |
+
num_blocks=40,
|
| 21 |
+
disc_type="dit_simple_conv3d",
|
| 22 |
+
inner_dim=5120 // 4,
|
| 23 |
+
)
|
lipforcing/configs/experiments/OmniAvatar/__init__.py
ADDED
|
File without changes
|
lipforcing/configs/experiments/OmniAvatar/config_df.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""DF (shift=5) for the 14B causal student — LoRA + selective unfreeze + t769 schedule.
|
| 5 |
+
|
| 6 |
+
Flattened, self-contained Diffusion-Forcing config for the 14B causal
|
| 7 |
+
student (shift=5, LoRA + selective unfreeze, t769 schedule).
|
| 8 |
+
|
| 9 |
+
Effect: the DF student is only ever trained at the noise levels used at
|
| 10 |
+
inference (input on step 1 = t=0.999, input on step 2 = t=0.769),
|
| 11 |
+
avoiding a train/test schedule mismatch.
|
| 12 |
+
|
| 13 |
+
Combines:
|
| 14 |
+
|
| 15 |
+
1) 14B LoRA + selective unfreeze student:
|
| 16 |
+
- model_size="14B" student
|
| 17 |
+
- pretrained 14B student checkpoint (set via OMNIAVATAR_STUDENT_CKPT_14B)
|
| 18 |
+
- merge_lora=False (PEFT injects LoRA on transformer blocks)
|
| 19 |
+
- unfreeze_modules on the audio path + patch embedding
|
| 20 |
+
- lora_rank=128, lora_alpha=64
|
| 21 |
+
- FSDP + bf16 fwd / fp32 master+optim
|
| 22 |
+
|
| 23 |
+
2) t769 schedule narrowing:
|
| 24 |
+
- sample_t_cfg.t_list = [0.999, 0.769, 0.0]
|
| 25 |
+
- student_sample_steps = 2
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
import os
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
from lipforcing.utils import LazyCall as L
|
| 32 |
+
|
| 33 |
+
# Methods-level config (defines the attrs Config/ModelConfig classes and the
|
| 34 |
+
# default create_config()). We keep importing this — it is NOT an experiment
|
| 35 |
+
# chain file.
|
| 36 |
+
import lipforcing.configs.methods.config_omniavatar_df as config_df_default
|
| 37 |
+
|
| 38 |
+
from lipforcing.networks.OmniAvatar.network_causal import CausalOmniAvatarWan
|
| 39 |
+
from lipforcing.datasets.omniavatar_dataloader import OmniAvatarDataLoader, create_omniavatar_dataloader
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# ---- Paths (override via env vars) ----
|
| 43 |
+
OMNIAVATAR_ROOT = os.getenv("OMNIAVATAR_ROOT", "./OmniAvatar")
|
| 44 |
+
DATA_ROOT = os.getenv("OMNIAVATAR_DATA_ROOT", "./data/v2v_training_data")
|
| 45 |
+
# Pretrained 1.3B student checkpoint (used by the base network specs below).
|
| 46 |
+
# The 14B release loads STUDENT_CKPT_14B instead; this is kept so the 1.3B
|
| 47 |
+
# student can be enabled later without restructuring the config.
|
| 48 |
+
STUDENT_CKPT_1_3B = os.getenv(
|
| 49 |
+
"OMNIAVATAR_STUDENT_CKPT_1_3B",
|
| 50 |
+
"/path/to/omniavatar_1.3b.pt",
|
| 51 |
+
)
|
| 52 |
+
DATA_LIST = os.getenv("OMNIAVATAR_DATA_LIST", f"{DATA_ROOT}/train_list.txt")
|
| 53 |
+
VAL_LIST = os.getenv("OMNIAVATAR_VAL_LIST", f"{DATA_ROOT}/val_list.txt")
|
| 54 |
+
MASK_PATH = os.getenv(
|
| 55 |
+
"MASK_PATH",
|
| 56 |
+
"/path/to/mask.png",
|
| 57 |
+
)
|
| 58 |
+
VAE_PATH = os.getenv(
|
| 59 |
+
"OMNIAVATAR_VAE_PATH",
|
| 60 |
+
os.path.join(OMNIAVATAR_ROOT, "pretrained_models/Wan2.1-T2V-1.3B/Wan2.1_VAE.pth"),
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# Pretrained 14B student checkpoint. Override via OMNIAVATAR_STUDENT_CKPT_14B
|
| 64 |
+
# to use a different checkpoint.
|
| 65 |
+
STUDENT_CKPT_14B = os.getenv(
|
| 66 |
+
"OMNIAVATAR_STUDENT_CKPT_14B",
|
| 67 |
+
"/path/to/omniavatar_14b_init.pt",
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Directory holding the base Wan2.1-T2V-14B diffusion safetensors shards.
|
| 71 |
+
WAN_BASE_DIR = os.getenv(
|
| 72 |
+
"OMNIAVATAR_WAN_BASE", f"{OMNIAVATAR_ROOT}/pretrained_models/Wan2.1-T2V-14B"
|
| 73 |
+
)
|
| 74 |
+
WAN_14B_BASE = ",".join(
|
| 75 |
+
f"{WAN_BASE_DIR}/diffusion_pytorch_model-{i:05d}-of-00006.safetensors"
|
| 76 |
+
for i in range(1, 7)
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# Submodules to keep fully trainable alongside LoRA on the transformer blocks.
|
| 80 |
+
# Paths are dotted, relative to the CausalOmniAvatarWan instance (so they
|
| 81 |
+
# include the "_core." prefix where the actual modules live).
|
| 82 |
+
DEFAULT_UNFREEZE_MODULES = [
|
| 83 |
+
"_core.audio_proj",
|
| 84 |
+
"_core.audio_cond_projs",
|
| 85 |
+
"_core.patch_embedding",
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# ---- Student network config (1.3B base — overridden to 14B below) ----
|
| 90 |
+
CausalOmniAvatar_V2V_1_3B_Config: dict = L(CausalOmniAvatarWan)(
|
| 91 |
+
model_size="1.3B",
|
| 92 |
+
in_dim=65,
|
| 93 |
+
mode="v2v",
|
| 94 |
+
use_audio=True,
|
| 95 |
+
audio_hidden_size=32,
|
| 96 |
+
chunk_size=3,
|
| 97 |
+
total_num_frames=21,
|
| 98 |
+
base_model_paths=f"{OMNIAVATAR_ROOT}/pretrained_models/Wan2.1-T2V-1.3B/diffusion_pytorch_model.safetensors",
|
| 99 |
+
omniavatar_ckpt_path=STUDENT_CKPT_1_3B,
|
| 100 |
+
net_pred_type="flow",
|
| 101 |
+
schedule_type="rf",
|
| 102 |
+
use_dynamic_rope=False,
|
| 103 |
+
stochastic_attn_configs=[
|
| 104 |
+
{"local_attn_size": 7, "sink_size": 1, "weight": 0.2}, # sink=1, window=6
|
| 105 |
+
{"local_attn_size": 10, "sink_size": 1, "weight": 0.2}, # sink=1, window=9
|
| 106 |
+
{"local_attn_size": 13, "sink_size": 1, "weight": 0.2}, # sink=1, window=12
|
| 107 |
+
{"local_attn_size": 9, "sink_size": 3, "weight": 0.2}, # sink=3, window=6
|
| 108 |
+
{"local_attn_size": 12, "sink_size": 3, "weight": 0.2}, # sink=3, window=9
|
| 109 |
+
],
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def create_config():
|
| 114 |
+
# ================================================================== #
|
| 115 |
+
# Base: methods-level OmniAvatar Diffusion Forcing config #
|
| 116 |
+
# ================================================================== #
|
| 117 |
+
config = config_df_default.create_config()
|
| 118 |
+
|
| 119 |
+
# ================================================================== #
|
| 120 |
+
# DF (shift=5) overrides: LR, precision #
|
| 121 |
+
# ================================================================== #
|
| 122 |
+
# Learning rate
|
| 123 |
+
config.model.net_optimizer.lr = 1e-5
|
| 124 |
+
|
| 125 |
+
# Precision
|
| 126 |
+
config.model.precision = "bfloat16"
|
| 127 |
+
config.model.precision_fsdp = "float32"
|
| 128 |
+
|
| 129 |
+
# Input shape: 512x512 @ 81 frames -> latent [16, 21, 64, 64]
|
| 130 |
+
config.model.input_shape = [16, 21, 64, 64]
|
| 131 |
+
|
| 132 |
+
# Student network
|
| 133 |
+
config.model.net = CausalOmniAvatar_V2V_1_3B_Config
|
| 134 |
+
config.model.net.total_num_frames = config.model.input_shape[1]
|
| 135 |
+
|
| 136 |
+
# Timestep schedule — shift=5.0 matches OmniAvatar's default scheduler
|
| 137 |
+
config.model.sample_t_cfg.time_dist_type = "shifted"
|
| 138 |
+
config.model.sample_t_cfg.shift = 5.0
|
| 139 |
+
config.model.sample_t_cfg.min_t = 0.001
|
| 140 |
+
config.model.sample_t_cfg.max_t = 0.999
|
| 141 |
+
config.model.sample_t_cfg.t_list = [0.999, 0.937, 0.833, 0.624, 0.0]
|
| 142 |
+
|
| 143 |
+
# Diffusion forcing settings
|
| 144 |
+
config.model.student_sample_steps = 4
|
| 145 |
+
|
| 146 |
+
# Dataloader
|
| 147 |
+
config.dataloader_train = L(OmniAvatarDataLoader)(
|
| 148 |
+
data_list_path=DATA_LIST,
|
| 149 |
+
latentsync_mask_path=MASK_PATH,
|
| 150 |
+
batch_size=2,
|
| 151 |
+
num_workers=4,
|
| 152 |
+
neg_text_emb_path=os.getenv("NEG_TEXT_EMB_PATH", None),
|
| 153 |
+
use_ref_sequence=True,
|
| 154 |
+
load_ode_path=False,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
# Validation dataloader — 10 fixed samples, finite iterator, batch_size=1
|
| 158 |
+
config.dataloader_val = L(create_omniavatar_dataloader)(
|
| 159 |
+
data_list_path=VAL_LIST,
|
| 160 |
+
latentsync_mask_path=MASK_PATH,
|
| 161 |
+
batch_size=1,
|
| 162 |
+
num_workers=2,
|
| 163 |
+
neg_text_emb_path=os.getenv("NEG_TEXT_EMB_PATH", None),
|
| 164 |
+
use_ref_sequence=True,
|
| 165 |
+
load_ode_path=False,
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
# VAE for visual logging (decodes latents to video for wandb)
|
| 169 |
+
config.model.vae_path = VAE_PATH
|
| 170 |
+
|
| 171 |
+
# ---- Optional: on-the-fly preprocessing (opt-in via env) ----
|
| 172 |
+
# When OMNIAVATAR_ON_THE_FLY=1, samples lacking precomputed .pt are encoded
|
| 173 |
+
# from raw (sub_clip.mp4 + audio.wav + prompt.txt) by frozen encoders in the
|
| 174 |
+
# MAIN training process; encoded tensors are optionally cached for reuse. The
|
| 175 |
+
# precompute fast path is unchanged, and with the env unset this block is a
|
| 176 |
+
# no-op so the precompute-based data path is used unchanged.
|
| 177 |
+
if os.getenv("OMNIAVATAR_ON_THE_FLY", "0") == "1":
|
| 178 |
+
config.dataloader_train.on_the_fly = True
|
| 179 |
+
config.dataloader_train.cache_encoded = os.getenv("OMNIAVATAR_CACHE_ENCODED", "1") == "1"
|
| 180 |
+
config.dataloader_train.cache_dir = os.getenv("OMNIAVATAR_CACHE_DIR", None)
|
| 181 |
+
config.dataloader_train.vae_path = VAE_PATH
|
| 182 |
+
config.dataloader_train.wav2vec_path = os.getenv("OMNIAVATAR_WAV2VEC_PATH", None)
|
| 183 |
+
config.dataloader_train.text_encoder_path = os.getenv("OMNIAVATAR_TEXT_ENCODER_PATH", None)
|
| 184 |
+
|
| 185 |
+
# Training (5K steps — matches the released DF run and paper App. D.1)
|
| 186 |
+
config.trainer.max_iter = 5000
|
| 187 |
+
config.trainer.logging_iter = 1
|
| 188 |
+
config.trainer.save_ckpt_iter = 500
|
| 189 |
+
config.trainer.validation_iter = 500
|
| 190 |
+
config.trainer.skip_initial_validation = True
|
| 191 |
+
config.trainer.callbacks.wandb.sample_logging_iter = 500
|
| 192 |
+
|
| 193 |
+
config.log_config.group = "stage1_df"
|
| 194 |
+
config.log_config.name = "df_14b_lora_t769"
|
| 195 |
+
|
| 196 |
+
# ================================================================== #
|
| 197 |
+
# Switch student to 14B + FSDP #
|
| 198 |
+
# ================================================================== #
|
| 199 |
+
# ---- Switch student to 14B (omit this block for the 1.3B base student) ----
|
| 200 |
+
config.model.net.model_size = "14B"
|
| 201 |
+
config.model.net.base_model_paths = WAN_14B_BASE
|
| 202 |
+
config.model.net.omniavatar_ckpt_path = STUDENT_CKPT_14B
|
| 203 |
+
# The 14B teacher uses merge_lora=True to fuse the adapter into the base
|
| 204 |
+
# before training. Mirror it for the 14B student so DF starts from the
|
| 205 |
+
# fused state, not a base+LoRA stacked state.
|
| 206 |
+
config.model.net.merge_lora = True
|
| 207 |
+
|
| 208 |
+
# ---- DDP -> FSDP ----
|
| 209 |
+
config.trainer.ddp = False
|
| 210 |
+
config.trainer.fsdp = True
|
| 211 |
+
config.trainer.fsdp_min_num_params = int(1e8)
|
| 212 |
+
config.trainer.fsdp_cpu_offload = False
|
| 213 |
+
config.trainer.fsdp_sharding_group_size = None # default = world_size
|
| 214 |
+
# Mixed-precision FSDP: bf16 fwd/bwd, fp32 master+optim.
|
| 215 |
+
config.model.precision = "bfloat16"
|
| 216 |
+
config.model.precision_fsdp = "float32"
|
| 217 |
+
# Meta-init disabled (RoPE Python-attr issue at 14B DF).
|
| 218 |
+
config.model.fsdp_meta_init = False
|
| 219 |
+
|
| 220 |
+
# ---- Effective batch 16 = 2/GPU * 4 GPUs * grad_accum 2 (matches train_stage1_df.sh) ----
|
| 221 |
+
config.trainer.grad_accum_rounds = 2
|
| 222 |
+
|
| 223 |
+
# ================================================================== #
|
| 224 |
+
# Full fine-tune -> LoRA + selective unfreeze #
|
| 225 |
+
# ================================================================== #
|
| 226 |
+
# ---- Switch from full FT to LoRA + selective unfreeze ----
|
| 227 |
+
config.model.net.merge_lora = False
|
| 228 |
+
config.model.net.unfreeze_modules = DEFAULT_UNFREEZE_MODULES
|
| 229 |
+
|
| 230 |
+
# LoRA hyperparameters. Match the V2V adapter we're loading from
|
| 231 |
+
# (rank=128, alpha=64).
|
| 232 |
+
config.model.net.lora_rank = 128
|
| 233 |
+
config.model.net.lora_alpha = 64
|
| 234 |
+
|
| 235 |
+
# ================================================================== #
|
| 236 |
+
# t769 2-step schedule #
|
| 237 |
+
# ================================================================== #
|
| 238 |
+
# 2-step schedule overrides. With student_sample_steps=2, the student
|
| 239 |
+
# is trained on the two intervals (0.999 -> 0.769) and (0.769 -> 0.0)
|
| 240 |
+
# — the exact intervals SF t769 inference uses.
|
| 241 |
+
config.model.sample_t_cfg.t_list = [0.999, 0.769, 0.0]
|
| 242 |
+
config.model.student_sample_steps = 2
|
| 243 |
+
|
| 244 |
+
return config
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
config = create_config()
|
lipforcing/configs/experiments/OmniAvatar/config_sf.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""SF (Re-DMD beta=2 + TAEW) for the 14B causal student + 14B fake_score,
|
| 5 |
+
LoRA + selective unfreeze, t769 schedule, matched critic LR 2e-6.
|
| 6 |
+
|
| 7 |
+
Self-contained Self-Forcing experiment config for the 14B causal student +
|
| 8 |
+
14B critic (Re-DMD, beta=2, TAEW decoder, t769 2-step schedule).
|
| 9 |
+
|
| 10 |
+
Combines:
|
| 11 |
+
- 14B student (causal) + 14B fake_score (bidirectional), both LoRA +
|
| 12 |
+
selective unfreeze, initialized from a pretrained 14B audio adapter checkpoint.
|
| 13 |
+
- 14B teacher = OmniAvatar-LS (the lip-sync-finetuned OmniAvatar teacher,
|
| 14 |
+
paper App. B.2; bidirectional, frozen, merge_lora=True).
|
| 15 |
+
- Sliding-window attention: sink=1 + window=7, dynamic RoPE.
|
| 16 |
+
- Re-DMD reward path (SyncNet-v2 sync-C), beta=2, TAEW decoder.
|
| 17 |
+
- t769 2-step schedule: t_list=[0.999, 0.769, 0.0], student_sample_steps=2.
|
| 18 |
+
- Effective batch 16 = 1/GPU * 4 GPUs * grad_accum=4; FSDP bf16/fp32.
|
| 19 |
+
- Critic (fake_score) LR matched to the student at 2e-6 (paper D.1).
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import os
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
from lipforcing.utils import LazyCall as L
|
| 26 |
+
|
| 27 |
+
# Methods-level configs (define the attrs Config/ModelConfig classes, network
|
| 28 |
+
# classes, RewardConfig, and the Re-DMD model class). These are NOT experiment
|
| 29 |
+
# chain files and are kept.
|
| 30 |
+
import lipforcing.configs.methods.config_omniavatar_sf as config_sf_default
|
| 31 |
+
from lipforcing.configs.methods.config_omniavatar_sf import RewardConfig
|
| 32 |
+
|
| 33 |
+
from lipforcing.networks.OmniAvatar.network import OmniAvatarWan
|
| 34 |
+
from lipforcing.networks.OmniAvatar.network_causal import CausalOmniAvatarWan
|
| 35 |
+
from lipforcing.datasets.omniavatar_dataloader import OmniAvatarDataLoader, create_omniavatar_dataloader
|
| 36 |
+
from lipforcing.methods.omniavatar_self_forcing_re_dmd import OmniAvatarSelfForcingReDMD
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# ---- Paths (override via CLI or env) ----
|
| 40 |
+
OMNIAVATAR_ROOT = os.getenv("OMNIAVATAR_ROOT", "./OmniAvatar")
|
| 41 |
+
DATA_ROOT = os.getenv("OMNIAVATAR_DATA_ROOT", "./data/v2v_training_data")
|
| 42 |
+
TEACHER_CKPT = os.getenv(
|
| 43 |
+
"OMNIAVATAR_TEACHER_CKPT",
|
| 44 |
+
"/path/to/omniavatar_teacher.pt",
|
| 45 |
+
)
|
| 46 |
+
# Pretrained 1.3B student checkpoint (used by the base network specs below).
|
| 47 |
+
# The 14B release loads STUDENT_CKPT_14B instead; this is kept so the 1.3B
|
| 48 |
+
# student can be enabled later without restructuring the config.
|
| 49 |
+
STUDENT_CKPT_1_3B = os.getenv(
|
| 50 |
+
"OMNIAVATAR_STUDENT_CKPT_1_3B",
|
| 51 |
+
"/path/to/omniavatar_1.3b.pt",
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# Initial 14B checkpoint (same as the teacher): provides initial LoRA values
|
| 55 |
+
# plus audio/patch-embedding weights for both the student and the fake_score
|
| 56 |
+
# before training.
|
| 57 |
+
STUDENT_CKPT_14B = os.getenv(
|
| 58 |
+
"OMNIAVATAR_STUDENT_CKPT_14B",
|
| 59 |
+
"/path/to/omniavatar_14b_init.pt",
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
# Directory holding the base Wan2.1-T2V-14B diffusion safetensors shards.
|
| 63 |
+
WAN_BASE_DIR = os.getenv(
|
| 64 |
+
"OMNIAVATAR_WAN_BASE", f"{OMNIAVATAR_ROOT}/pretrained_models/Wan2.1-T2V-14B"
|
| 65 |
+
)
|
| 66 |
+
WAN_14B_BASE = ",".join(
|
| 67 |
+
f"{WAN_BASE_DIR}/diffusion_pytorch_model-{i:05d}-of-00006.safetensors"
|
| 68 |
+
for i in range(1, 7)
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
# Submodule paths (relative to each network) to keep fully trainable
|
| 72 |
+
# alongside the LoRA A/B matrices on the transformer blocks. Different
|
| 73 |
+
# prefix between the causal and bidirectional classes:
|
| 74 |
+
# - CausalOmniAvatarWan (student): WanModel lives at self._core
|
| 75 |
+
# - OmniAvatarWan (fake_score): WanModel lives at self.model
|
| 76 |
+
STUDENT_UNFREEZE = [
|
| 77 |
+
"_core.audio_proj",
|
| 78 |
+
"_core.audio_cond_projs",
|
| 79 |
+
"_core.patch_embedding",
|
| 80 |
+
]
|
| 81 |
+
FAKE_SCORE_UNFREEZE = [
|
| 82 |
+
"model.audio_proj",
|
| 83 |
+
"model.audio_cond_projs",
|
| 84 |
+
"model.patch_embedding",
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
# Reward checkpoints: SyncNet scorer + TAEW decoder for the reward-path pixel decode.
|
| 88 |
+
SYNCNET_CKPT = os.getenv("SYNCNET_CKPT", "/path/to/syncnet_v2.model")
|
| 89 |
+
TAEW_CKPT = os.getenv("TAEW_CKPT", "/path/to/taew2_1.pth")
|
| 90 |
+
|
| 91 |
+
# Critic (fake_score) LR: matched to the student LR (paper D.1).
|
| 92 |
+
CRITIC_LR = 2e-6
|
| 93 |
+
RUN_NAME = "sf_14b_lora_t769"
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# ---- Network configs ----
|
| 97 |
+
OmniAvatar_V2V_14B_Teacher: dict = L(OmniAvatarWan)(
|
| 98 |
+
model_size="14B",
|
| 99 |
+
in_dim=65,
|
| 100 |
+
mode="v2v",
|
| 101 |
+
use_audio=True,
|
| 102 |
+
audio_hidden_size=32,
|
| 103 |
+
base_model_paths=WAN_14B_BASE,
|
| 104 |
+
omniavatar_ckpt_path=TEACHER_CKPT,
|
| 105 |
+
merge_lora=True,
|
| 106 |
+
net_pred_type="flow",
|
| 107 |
+
schedule_type="rf",
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
OmniAvatar_V2V_1_3B_FakeScore: dict = L(OmniAvatarWan)(
|
| 111 |
+
model_size="1.3B",
|
| 112 |
+
in_dim=65,
|
| 113 |
+
mode="v2v",
|
| 114 |
+
use_audio=True,
|
| 115 |
+
audio_hidden_size=32,
|
| 116 |
+
base_model_paths=f"{OMNIAVATAR_ROOT}/pretrained_models/Wan2.1-T2V-1.3B/diffusion_pytorch_model.safetensors",
|
| 117 |
+
omniavatar_ckpt_path=STUDENT_CKPT_1_3B,
|
| 118 |
+
merge_lora=False, # Fake score is trainable, keep LoRA separate
|
| 119 |
+
net_pred_type="flow",
|
| 120 |
+
schedule_type="rf",
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
CausalOmniAvatar_V2V_1_3B_Student: dict = L(CausalOmniAvatarWan)(
|
| 124 |
+
model_size="1.3B",
|
| 125 |
+
in_dim=65,
|
| 126 |
+
mode="v2v",
|
| 127 |
+
use_audio=True,
|
| 128 |
+
audio_hidden_size=32,
|
| 129 |
+
chunk_size=3,
|
| 130 |
+
total_num_frames=21,
|
| 131 |
+
base_model_paths=f"{OMNIAVATAR_ROOT}/pretrained_models/Wan2.1-T2V-1.3B/diffusion_pytorch_model.safetensors",
|
| 132 |
+
omniavatar_ckpt_path=STUDENT_CKPT_1_3B,
|
| 133 |
+
net_pred_type="flow",
|
| 134 |
+
schedule_type="rf",
|
| 135 |
+
# Sliding window attention for AR rollout.
|
| 136 |
+
# Defaults: full causal (no window constraint during SF).
|
| 137 |
+
local_attn_size=-1,
|
| 138 |
+
sink_size=0,
|
| 139 |
+
use_dynamic_rope=True,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def create_config():
|
| 144 |
+
# ================================================================== #
|
| 145 |
+
# Base: methods-level OmniAvatar Self-Forcing config #
|
| 146 |
+
# ================================================================== #
|
| 147 |
+
config = config_sf_default.create_config()
|
| 148 |
+
|
| 149 |
+
# ================================================================== #
|
| 150 |
+
# Self-Forcing overrides: LR, optimizer, FSDP #
|
| 151 |
+
# ================================================================== #
|
| 152 |
+
# Learning rates and optimizer (Adam beta1=0.0, as used by Self-Forcing)
|
| 153 |
+
config.model.net_optimizer.lr = 2e-6
|
| 154 |
+
config.model.net_optimizer.betas = (0.0, 0.999)
|
| 155 |
+
config.model.fake_score_optimizer.lr = 2e-6
|
| 156 |
+
config.model.fake_score_optimizer.betas = (0.0, 0.999)
|
| 157 |
+
|
| 158 |
+
# Multi-GPU: FSDP required (DDP OOMs on student update at ~79GB/GPU)
|
| 159 |
+
config.trainer.fsdp = True
|
| 160 |
+
|
| 161 |
+
# Precision
|
| 162 |
+
config.model.precision = "bfloat16"
|
| 163 |
+
config.model.precision_fsdp = "float32"
|
| 164 |
+
|
| 165 |
+
# Input shape: 512x512 @ 81 frames -> latent [16, 21, 64, 64]
|
| 166 |
+
config.model.input_shape = [16, 21, 64, 64]
|
| 167 |
+
config.model.fake_score_pred_type = "x0"
|
| 168 |
+
config.model.guidance_scale = 4.5
|
| 169 |
+
# Sync-Window DMD (SW-DMD; paper Sec. 4.3 / Eq. 6): teacher CFG is gated to the
|
| 170 |
+
# sync window. t_lo/t_hi = the shifted-timestep band [0.556, 0.882] = teacher ODE
|
| 171 |
+
# steps j in [20, 40] (the sync-favoring band from the trajectory analysis).
|
| 172 |
+
config.model.sync_window_cfg.enabled = False # base default; enabled in the sliding-window block below
|
| 173 |
+
config.model.sync_window_cfg.t_lo = 0.556
|
| 174 |
+
config.model.sync_window_cfg.t_hi = 0.882
|
| 175 |
+
|
| 176 |
+
# Networks (base specs): 14B teacher + 1.3B student + 1.3B fake_score.
|
| 177 |
+
# net and fake_score are switched to 14B in the 14B block below.
|
| 178 |
+
config.model.net = CausalOmniAvatar_V2V_1_3B_Student
|
| 179 |
+
config.model.net.total_num_frames = config.model.input_shape[1]
|
| 180 |
+
config.model.teacher = OmniAvatar_V2V_14B_Teacher # OmniAvatar-LS (paper App. B.2)
|
| 181 |
+
config.model.fake_score_net = OmniAvatar_V2V_1_3B_FakeScore
|
| 182 |
+
|
| 183 |
+
# GAN disabled by default.
|
| 184 |
+
config.model.gan_loss_weight_gen = 0
|
| 185 |
+
config.model.student_update_freq = 5 # 1:5 ratio
|
| 186 |
+
|
| 187 |
+
# Student weights: do NOT copy 14B teacher weights onto 1.3B student.
|
| 188 |
+
config.model.load_student_weights = False
|
| 189 |
+
# Load the DF-initialized student from the Stage-1 checkpoint. This DF init
|
| 190 |
+
# must match the sliding-window (sink=1, window=7) architecture set below.
|
| 191 |
+
config.trainer.checkpointer.pretrained_ckpt_path = os.getenv(
|
| 192 |
+
"OMNIAVATAR_DF_CKPT",
|
| 193 |
+
"/path/to/df_init_checkpoint.pth",
|
| 194 |
+
)
|
| 195 |
+
config.trainer.checkpointer.pretrained_ckpt_key_map = {"net": "net"}
|
| 196 |
+
|
| 197 |
+
# Timestep schedule — shift=5.0 matches OmniAvatar's inference scheduler
|
| 198 |
+
config.model.sample_t_cfg.time_dist_type = "shifted"
|
| 199 |
+
config.model.sample_t_cfg.shift = 5.0
|
| 200 |
+
config.model.sample_t_cfg.min_t = 0.001
|
| 201 |
+
config.model.sample_t_cfg.max_t = 0.999
|
| 202 |
+
config.model.sample_t_cfg.t_list = [0.999, 0.937, 0.833, 0.624, 0.0]
|
| 203 |
+
|
| 204 |
+
# Self-Forcing specific
|
| 205 |
+
config.model.enable_gradient_in_rollout = True
|
| 206 |
+
config.model.start_gradient_frame = 0
|
| 207 |
+
config.model.same_step_across_blocks = True
|
| 208 |
+
config.model.context_noise = 0.0
|
| 209 |
+
|
| 210 |
+
# Dataloader (OmniAvatarDataLoader provides infinite iteration)
|
| 211 |
+
config.dataloader_train = L(OmniAvatarDataLoader)(
|
| 212 |
+
data_list_path=os.getenv("OMNIAVATAR_DATA_LIST", f"{DATA_ROOT}/train_list.txt"),
|
| 213 |
+
latentsync_mask_path=os.getenv(
|
| 214 |
+
"MASK_PATH",
|
| 215 |
+
"/path/to/mask.png",
|
| 216 |
+
),
|
| 217 |
+
batch_size=8,
|
| 218 |
+
num_workers=2,
|
| 219 |
+
neg_text_emb_path=os.getenv("NEG_TEXT_EMB_PATH", None),
|
| 220 |
+
use_ref_sequence=True,
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
# Validation dataloader — 10 fixed samples, finite iterator, batch_size=1
|
| 224 |
+
VAL_LIST = os.getenv("OMNIAVATAR_VAL_LIST", f"{DATA_ROOT}/val_list.txt")
|
| 225 |
+
VAE_PATH = os.getenv(
|
| 226 |
+
"OMNIAVATAR_VAE_PATH",
|
| 227 |
+
os.path.join(OMNIAVATAR_ROOT, "pretrained_models/Wan2.1-T2V-1.3B/Wan2.1_VAE.pth"),
|
| 228 |
+
)
|
| 229 |
+
config.dataloader_val = L(create_omniavatar_dataloader)(
|
| 230 |
+
data_list_path=VAL_LIST,
|
| 231 |
+
latentsync_mask_path=os.getenv(
|
| 232 |
+
"MASK_PATH",
|
| 233 |
+
"/path/to/mask.png",
|
| 234 |
+
),
|
| 235 |
+
batch_size=1,
|
| 236 |
+
num_workers=2,
|
| 237 |
+
neg_text_emb_path=os.getenv("NEG_TEXT_EMB_PATH", None),
|
| 238 |
+
use_ref_sequence=True,
|
| 239 |
+
load_ode_path=False,
|
| 240 |
+
)
|
| 241 |
+
config.model.vae_path = VAE_PATH
|
| 242 |
+
|
| 243 |
+
# ---- Optional: on-the-fly preprocessing (opt-in via env) ----
|
| 244 |
+
# When OMNIAVATAR_ON_THE_FLY=1, samples lacking precomputed .pt are encoded
|
| 245 |
+
# from raw (sub_clip.mp4 + audio.wav + prompt.txt) by frozen encoders in the
|
| 246 |
+
# MAIN training process; encoded tensors are optionally cached for reuse. The
|
| 247 |
+
# precompute fast path is used by default, and with the env unset this block
|
| 248 |
+
# is a no-op.
|
| 249 |
+
if os.getenv("OMNIAVATAR_ON_THE_FLY", "0") == "1":
|
| 250 |
+
config.dataloader_train.on_the_fly = True
|
| 251 |
+
config.dataloader_train.cache_encoded = os.getenv("OMNIAVATAR_CACHE_ENCODED", "1") == "1"
|
| 252 |
+
config.dataloader_train.cache_dir = os.getenv("OMNIAVATAR_CACHE_DIR", None)
|
| 253 |
+
config.dataloader_train.vae_path = VAE_PATH
|
| 254 |
+
config.dataloader_train.wav2vec_path = os.getenv("OMNIAVATAR_WAV2VEC_PATH", None)
|
| 255 |
+
config.dataloader_train.text_encoder_path = os.getenv("OMNIAVATAR_TEXT_ENCODER_PATH", None)
|
| 256 |
+
|
| 257 |
+
# Training — 1.3B base: bs=8, grad_accum=2 -> eff batch 64 (14B override below = eff 16)
|
| 258 |
+
config.trainer.grad_accum_rounds = 2
|
| 259 |
+
config.trainer.max_iter = 600 # released 14B checkpoint = step 600 (paper App. D.1)
|
| 260 |
+
config.trainer.logging_iter = 1
|
| 261 |
+
config.trainer.save_ckpt_iter = 100
|
| 262 |
+
config.trainer.validation_iter = 100
|
| 263 |
+
config.trainer.skip_initial_validation = True
|
| 264 |
+
|
| 265 |
+
# Wandb sample logging (video generation) every 100 steps
|
| 266 |
+
config.trainer.callbacks.wandb.sample_logging_iter = 100
|
| 267 |
+
config.trainer.callbacks.wandb.fps = 25 # OmniAvatar is 25 fps
|
| 268 |
+
config.trainer.callbacks.wandb.syncnet_checkpoint_path = SYNCNET_CKPT
|
| 269 |
+
|
| 270 |
+
config.log_config.group = "stage2_sf"
|
| 271 |
+
config.log_config.wandb_entity = ""
|
| 272 |
+
|
| 273 |
+
# ================================================================== #
|
| 274 |
+
# Sliding-window attention (sink=1, window=7) #
|
| 275 |
+
# ================================================================== #
|
| 276 |
+
# Sliding window: 1 sink + 6 rolling = 7 total visible frames
|
| 277 |
+
config.model.net.local_attn_size = 7
|
| 278 |
+
config.model.net.sink_size = 1
|
| 279 |
+
config.model.net.use_dynamic_rope = True
|
| 280 |
+
|
| 281 |
+
# 2-step distillation: t_list[0] and t_list[2] from the 4-step schedule
|
| 282 |
+
config.model.sample_t_cfg.t_list = [0.999, 0.833, 0.0]
|
| 283 |
+
config.model.student_sample_steps = 2
|
| 284 |
+
|
| 285 |
+
# Enable Sync-Window DMD (SW-DMD) for this experiment
|
| 286 |
+
config.model.sync_window_cfg.enabled = True
|
| 287 |
+
|
| 288 |
+
# ================================================================== #
|
| 289 |
+
# Re-DMD reward weighting (sync-C, beta=2) #
|
| 290 |
+
# ================================================================== #
|
| 291 |
+
# Switch model class to the Re-DMD variant.
|
| 292 |
+
config.model_class._target_ = OmniAvatarSelfForcingReDMD
|
| 293 |
+
|
| 294 |
+
# Reward sub-config (RewardConfig attrs class survives OmegaConf serialization)
|
| 295 |
+
config.model.reward = RewardConfig(
|
| 296 |
+
enabled=True,
|
| 297 |
+
checkpoint_path=SYNCNET_CKPT,
|
| 298 |
+
input_fps=25.0,
|
| 299 |
+
audio_sample_rate=16000,
|
| 300 |
+
vshift=15,
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
# Top-level reward knobs (read by _apply_reward_weighting)
|
| 304 |
+
config.model.reward_beta = 2
|
| 305 |
+
config.model.center_reward = False
|
| 306 |
+
config.model.clamp_reward = None
|
| 307 |
+
|
| 308 |
+
# VAE path (required for reward decode in _decode_gen_to_pixels)
|
| 309 |
+
assert getattr(config.model, "vae_path", "") != "", (
|
| 310 |
+
"config.model.vae_path must be set for Re-DMD — the reward path VAE-decodes "
|
| 311 |
+
"the generator output to pixels. Either set OMNIAVATAR_VAE_PATH env or "
|
| 312 |
+
"ensure the default OmniAvatar install path is present."
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
# Data: raw waveform loading (required for audio_waveform in batch)
|
| 316 |
+
config.dataloader_train.load_raw_audio = True
|
| 317 |
+
config.dataloader_train.raw_audio_sample_rate = 16000
|
| 318 |
+
config.dataloader_train.raw_audio_num_frames = 81
|
| 319 |
+
config.dataloader_train.raw_audio_fps = 25.0
|
| 320 |
+
|
| 321 |
+
# ================================================================== #
|
| 322 |
+
# TAEW decoder for reward-path pixel decode #
|
| 323 |
+
# ================================================================== #
|
| 324 |
+
config.model.reward.decoder_kind = "taew"
|
| 325 |
+
config.model.reward.taew_checkpoint_path = TAEW_CKPT
|
| 326 |
+
|
| 327 |
+
# ================================================================== #
|
| 328 |
+
# 14B student + critic: LoRA, t769 schedule #
|
| 329 |
+
# ================================================================== #
|
| 330 |
+
# ---- Student: 1.3B causal -> 14B causal LoRA (omit to keep the 1.3B student) ----
|
| 331 |
+
config.model.net.model_size = "14B"
|
| 332 |
+
config.model.net.base_model_paths = WAN_14B_BASE
|
| 333 |
+
config.model.net.omniavatar_ckpt_path = STUDENT_CKPT_14B
|
| 334 |
+
config.model.net.merge_lora = False
|
| 335 |
+
config.model.net.unfreeze_modules = STUDENT_UNFREEZE
|
| 336 |
+
config.model.net.lora_rank = 128
|
| 337 |
+
config.model.net.lora_alpha = 64
|
| 338 |
+
|
| 339 |
+
# ---- Fake_score: 1.3B bidirectional -> 14B bidirectional LoRA ----
|
| 340 |
+
config.model.fake_score_net.model_size = "14B"
|
| 341 |
+
config.model.fake_score_net.base_model_paths = WAN_14B_BASE
|
| 342 |
+
config.model.fake_score_net.omniavatar_ckpt_path = STUDENT_CKPT_14B
|
| 343 |
+
config.model.fake_score_net.merge_lora = False
|
| 344 |
+
config.model.fake_score_net.unfreeze_modules = FAKE_SCORE_UNFREEZE
|
| 345 |
+
config.model.fake_score_net.lora_rank = 128
|
| 346 |
+
config.model.fake_score_net.lora_alpha = 64
|
| 347 |
+
|
| 348 |
+
# ---- Teacher: stays 14B + merge_lora=True (frozen, full state) ----
|
| 349 |
+
# No changes.
|
| 350 |
+
|
| 351 |
+
# ---- Schedule: t769 ----
|
| 352 |
+
config.model.sample_t_cfg.t_list = [0.999, 0.769, 0.0]
|
| 353 |
+
config.model.student_sample_steps = 2
|
| 354 |
+
|
| 355 |
+
# ---- Effective batch 16 = 1/GPU * 4 GPUs * grad_accum=4 ----
|
| 356 |
+
config.dataloader_train.batch_size = 1
|
| 357 |
+
config.trainer.grad_accum_rounds = 4
|
| 358 |
+
|
| 359 |
+
# ---- FSDP knobs (mirror 14B DF LoRA setup) ----
|
| 360 |
+
config.trainer.ddp = False
|
| 361 |
+
config.trainer.fsdp = True
|
| 362 |
+
config.trainer.fsdp_min_num_params = int(1e8)
|
| 363 |
+
config.trainer.fsdp_cpu_offload = False
|
| 364 |
+
config.trainer.fsdp_sharding_group_size = None
|
| 365 |
+
config.model.precision = "bfloat16"
|
| 366 |
+
config.model.precision_fsdp = "float32"
|
| 367 |
+
# Meta-init enabled (3-network 14B run); RoPE buffers are re-materialized via
|
| 368 |
+
# reset_parameters() on both network classes.
|
| 369 |
+
config.model.fsdp_meta_init = True
|
| 370 |
+
|
| 371 |
+
# ================================================================== #
|
| 372 |
+
# Critic LR = student LR (matched, 2e-6) #
|
| 373 |
+
# ================================================================== #
|
| 374 |
+
# Keep the student optimizer unchanged; reduce only the critic.
|
| 375 |
+
config.model.fake_score_optimizer.lr = CRITIC_LR
|
| 376 |
+
|
| 377 |
+
config.log_config.name = RUN_NAME
|
| 378 |
+
|
| 379 |
+
assert abs(config.model.net_optimizer.lr - 2e-6) < 1e-12, (
|
| 380 |
+
f"Expected student LR to stay at 2e-6, got {config.model.net_optimizer.lr}"
|
| 381 |
+
)
|
| 382 |
+
assert abs(config.model.fake_score_optimizer.lr - CRITIC_LR) < 1e-12, (
|
| 383 |
+
f"Expected critic LR {CRITIC_LR}, got {config.model.fake_score_optimizer.lr}"
|
| 384 |
+
)
|
| 385 |
+
assert config.model.sample_t_cfg.t_list == [0.999, 0.769, 0.0], (
|
| 386 |
+
f"Expected t769 schedule, got {config.model.sample_t_cfg.t_list}"
|
| 387 |
+
)
|
| 388 |
+
assert config.model.student_sample_steps == 2, (
|
| 389 |
+
f"Expected 2 student sample steps, got {config.model.student_sample_steps}"
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
return config
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
config = create_config()
|
lipforcing/configs/methods/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
lipforcing/configs/methods/config_dmd2.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
import copy
|
| 5 |
+
import attrs
|
| 6 |
+
from omegaconf import DictConfig
|
| 7 |
+
|
| 8 |
+
from lipforcing.utils import LazyCall as L
|
| 9 |
+
from lipforcing.configs.config import (
|
| 10 |
+
BaseModelConfig,
|
| 11 |
+
BaseConfig,
|
| 12 |
+
)
|
| 13 |
+
from lipforcing.configs.opt import BaseOptimizerConfig, BaseSchedulerConfig
|
| 14 |
+
from lipforcing.methods import DMD2Model
|
| 15 |
+
from lipforcing.configs.callbacks import (
|
| 16 |
+
WANDB_CALLBACK,
|
| 17 |
+
GradClip_CALLBACK,
|
| 18 |
+
GPUStats_CALLBACK,
|
| 19 |
+
TrainProfiler_CALLBACK,
|
| 20 |
+
ParamCount_CALLBACK,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@attrs.define(slots=False)
|
| 25 |
+
class ModelConfig(BaseModelConfig):
|
| 26 |
+
# optimizer and scheduler for the fake score net
|
| 27 |
+
fake_score_optimizer: DictConfig = attrs.field(factory=lambda: copy.deepcopy(BaseOptimizerConfig))
|
| 28 |
+
fake_score_scheduler: DictConfig = attrs.field(factory=lambda: copy.deepcopy(BaseSchedulerConfig))
|
| 29 |
+
|
| 30 |
+
discriminator: DictConfig = None # opt-in GAN (set e.g. Discriminator_Wan_14B_Config to enable)
|
| 31 |
+
# optimizer and scheduler for the discriminator
|
| 32 |
+
discriminator_optimizer: DictConfig = attrs.field(factory=lambda: copy.deepcopy(BaseOptimizerConfig))
|
| 33 |
+
discriminator_scheduler: DictConfig = attrs.field(factory=lambda: copy.deepcopy(BaseSchedulerConfig))
|
| 34 |
+
|
| 35 |
+
# student update frequency
|
| 36 |
+
student_update_freq: int = 5
|
| 37 |
+
|
| 38 |
+
# weight for the GAN loss in the student update phase
|
| 39 |
+
gan_loss_weight_gen: float = 0.001
|
| 40 |
+
|
| 41 |
+
# use the same t and noise to perturb the real data and fake data
|
| 42 |
+
gan_use_same_t_noise: bool = False
|
| 43 |
+
|
| 44 |
+
# perform dsm loss in a space specified by fake_score_pred_type (None means use original net_pred_type)
|
| 45 |
+
fake_score_pred_type: str | None = None
|
| 46 |
+
|
| 47 |
+
# R1 regularization weight (0 means no R1 reg, recommended value when using R1 reg: 100-1000)
|
| 48 |
+
gan_r1_reg_weight: float = 0.0
|
| 49 |
+
# R1 regularization noise scale (it only takes effect when gan_r1_reg_weight > 0)
|
| 50 |
+
gan_r1_reg_alpha: float = 0.1
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@attrs.define(slots=False)
|
| 54 |
+
class Config(BaseConfig):
|
| 55 |
+
model: ModelConfig = attrs.field(factory=ModelConfig)
|
| 56 |
+
model_class: DictConfig = L(DMD2Model)(
|
| 57 |
+
config=None,
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def create_config():
|
| 62 |
+
config = Config()
|
| 63 |
+
config.trainer.callbacks = DictConfig(
|
| 64 |
+
{
|
| 65 |
+
**GradClip_CALLBACK,
|
| 66 |
+
**GPUStats_CALLBACK,
|
| 67 |
+
**TrainProfiler_CALLBACK,
|
| 68 |
+
**ParamCount_CALLBACK,
|
| 69 |
+
**WANDB_CALLBACK,
|
| 70 |
+
}
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
config.dataloader_train.batch_size = 256
|
| 74 |
+
config.model.discriminator_scheduler.warm_up_steps = [0]
|
| 75 |
+
config.model.fake_score_scheduler.warm_up_steps = [0]
|
| 76 |
+
config.model.net_scheduler.warm_up_steps = [0]
|
| 77 |
+
config.model.sample_t_cfg.time_dist_type = "polynomial"
|
| 78 |
+
|
| 79 |
+
return config
|
lipforcing/configs/methods/config_omniavatar_df.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""Method config for OmniAvatar Diffusion Forcing (Stage 1 alternative to ODE KD)."""
|
| 5 |
+
|
| 6 |
+
import attrs
|
| 7 |
+
from omegaconf import DictConfig
|
| 8 |
+
|
| 9 |
+
from lipforcing.utils import LazyCall as L
|
| 10 |
+
from lipforcing.configs.config import BaseConfig, BaseModelConfig
|
| 11 |
+
from lipforcing.methods.omniavatar_diffusion_forcing import OmniAvatarDiffusionForcingModel
|
| 12 |
+
from lipforcing.callbacks.wandb import WandbCallback
|
| 13 |
+
from lipforcing.configs.callbacks import (
|
| 14 |
+
GradClip_CALLBACK,
|
| 15 |
+
ParamCount_CALLBACK,
|
| 16 |
+
TrainProfiler_CALLBACK,
|
| 17 |
+
GPUStats_CALLBACK,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@attrs.define(slots=False)
|
| 22 |
+
class ModelConfig(BaseModelConfig):
|
| 23 |
+
context_noise: float = 0.0
|
| 24 |
+
vae_path: str = "" # Path to WanVAE for visual logging (optional)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@attrs.define(slots=False)
|
| 28 |
+
class Config(BaseConfig):
|
| 29 |
+
model: ModelConfig = attrs.field(factory=ModelConfig)
|
| 30 |
+
model_class: DictConfig = L(OmniAvatarDiffusionForcingModel)(
|
| 31 |
+
config=None,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def create_config():
|
| 36 |
+
config = Config()
|
| 37 |
+
# OmniAvatar uses 25fps video
|
| 38 |
+
OMNIAVATAR_WANDB = dict(wandb=L(WandbCallback)(sample_logging_iter=None, fps=25))
|
| 39 |
+
config.trainer.callbacks = DictConfig(
|
| 40 |
+
{
|
| 41 |
+
**GradClip_CALLBACK,
|
| 42 |
+
**GPUStats_CALLBACK,
|
| 43 |
+
**TrainProfiler_CALLBACK,
|
| 44 |
+
**ParamCount_CALLBACK,
|
| 45 |
+
**OMNIAVATAR_WANDB,
|
| 46 |
+
}
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
config.dataloader_train.batch_size = 1
|
| 50 |
+
config.model.student_sample_steps = 4
|
| 51 |
+
config.model.net_scheduler.warm_up_steps = [0]
|
| 52 |
+
|
| 53 |
+
return config
|
lipforcing/configs/methods/config_omniavatar_sf.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
"""Method config for OmniAvatar Self-Forcing distillation."""
|
| 5 |
+
|
| 6 |
+
import attrs
|
| 7 |
+
from omegaconf import DictConfig
|
| 8 |
+
|
| 9 |
+
from lipforcing.utils import LazyCall as L
|
| 10 |
+
from typing import Optional
|
| 11 |
+
|
| 12 |
+
from lipforcing.configs.methods.config_self_forcing import (
|
| 13 |
+
Config as SFConfig,
|
| 14 |
+
ModelConfig as SFModelConfig,
|
| 15 |
+
)
|
| 16 |
+
from lipforcing.methods.omniavatar_self_forcing import OmniAvatarSelfForcingModel
|
| 17 |
+
from lipforcing.configs.callbacks import (
|
| 18 |
+
WANDB_CALLBACK,
|
| 19 |
+
GradClip_CALLBACK,
|
| 20 |
+
ParamCount_CALLBACK,
|
| 21 |
+
TrainProfiler_CALLBACK,
|
| 22 |
+
GPUStats_CALLBACK,
|
| 23 |
+
EMA_CALLBACK,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@attrs.define(slots=False)
|
| 28 |
+
class RewardConfig:
|
| 29 |
+
"""Config for the Re-DMD reward scorer (SyncNet-v2 sync-C)."""
|
| 30 |
+
enabled: bool = True
|
| 31 |
+
checkpoint_path: str = ""
|
| 32 |
+
input_fps: float = 25.0
|
| 33 |
+
audio_sample_rate: int = 16000
|
| 34 |
+
vshift: int = 15
|
| 35 |
+
# Opt-in TAEW decoder. Default "vae" preserves WanVideoVAE.decode behavior.
|
| 36 |
+
# When "taew", the Re-DMD model loads a TAEHVDecoderWrapper from
|
| 37 |
+
# taew_checkpoint_path and uses it in place of self.net.vae for the
|
| 38 |
+
# reward-path pixel decode.
|
| 39 |
+
decoder_kind: str = "vae"
|
| 40 |
+
taew_checkpoint_path: str = ""
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@attrs.define(slots=False)
|
| 44 |
+
class OmniAvatarModelConfig(SFModelConfig):
|
| 45 |
+
# Separate fake_score config (allows 1.3B fake_score with 14B teacher)
|
| 46 |
+
fake_score: Optional[DictConfig] = None
|
| 47 |
+
|
| 48 |
+
# Wan VAE path for decoding validation videos in the wandb callback.
|
| 49 |
+
# Logging-only; declared here so it survives config serialization.
|
| 50 |
+
vae_path: str = ""
|
| 51 |
+
|
| 52 |
+
# Re-DMD reward config. None = reward disabled (vanilla SF).
|
| 53 |
+
reward: Optional[RewardConfig] = None
|
| 54 |
+
reward_beta: float = 0.25
|
| 55 |
+
center_reward: bool = False
|
| 56 |
+
clamp_reward: Optional[list] = None
|
| 57 |
+
|
| 58 |
+
# How to combine per-sample reward weights with per-sample VSD losses.
|
| 59 |
+
# - "per_sample" (default): mean_i(exp(beta*r_i) * L_i). Per-sample
|
| 60 |
+
# coupling without normalization. Preserves reward magnitude as
|
| 61 |
+
# a loss-scale signal.
|
| 62 |
+
# - "self_normalized": sum_i(exp(beta*r_i) * L_i) / sum_j(exp(beta*r_j)).
|
| 63 |
+
# Self-normalized importance sampling (paper's Z(c) partition).
|
| 64 |
+
# Shift-invariant; loss bounded in [min(L), max(L)]. Better for
|
| 65 |
+
# additive combination with GAN/other aux losses.
|
| 66 |
+
# - "legacy_batch_mean": mean_i(exp(beta*r_i)) * mean_i(L_i). Batch-mean
|
| 67 |
+
# form that decouples reward from loss at batch > 1; not recommended.
|
| 68 |
+
reward_weighting_mode: str = "per_sample"
|
| 69 |
+
|
| 70 |
+
# Diagnostic video dump at every generator step (rank 0 only).
|
| 71 |
+
save_reward_debug_video: bool = False
|
| 72 |
+
reward_debug_dir: str = "logs/redmd_debug"
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@attrs.define(slots=False)
|
| 76 |
+
class Config(SFConfig):
|
| 77 |
+
model: OmniAvatarModelConfig = attrs.field(factory=OmniAvatarModelConfig)
|
| 78 |
+
model_class: DictConfig = L(OmniAvatarSelfForcingModel)(
|
| 79 |
+
config=None,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def create_config():
|
| 84 |
+
config = Config()
|
| 85 |
+
config.trainer.callbacks = DictConfig(
|
| 86 |
+
{
|
| 87 |
+
**GradClip_CALLBACK,
|
| 88 |
+
**GPUStats_CALLBACK,
|
| 89 |
+
**TrainProfiler_CALLBACK,
|
| 90 |
+
**ParamCount_CALLBACK,
|
| 91 |
+
**EMA_CALLBACK,
|
| 92 |
+
**WANDB_CALLBACK,
|
| 93 |
+
}
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
config.dataloader_train.batch_size = 1
|
| 97 |
+
config.model.student_sample_steps = 4
|
| 98 |
+
config.model.discriminator_scheduler.warm_up_steps = [0]
|
| 99 |
+
config.model.fake_score_scheduler.warm_up_steps = [0]
|
| 100 |
+
config.model.net_scheduler.warm_up_steps = [0]
|
| 101 |
+
|
| 102 |
+
return config
|
lipforcing/configs/methods/config_self_forcing.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
import attrs
|
| 5 |
+
from omegaconf import DictConfig
|
| 6 |
+
|
| 7 |
+
from lipforcing.utils import LazyCall as L
|
| 8 |
+
from lipforcing.configs.methods.config_dmd2 import (
|
| 9 |
+
Config as DMD2Config,
|
| 10 |
+
ModelConfig as DMD2ModelConfig,
|
| 11 |
+
)
|
| 12 |
+
from lipforcing.methods import SelfForcingModel
|
| 13 |
+
from lipforcing.configs.callbacks import (
|
| 14 |
+
WANDB_CALLBACK,
|
| 15 |
+
GradClip_CALLBACK,
|
| 16 |
+
ParamCount_CALLBACK,
|
| 17 |
+
TrainProfiler_CALLBACK,
|
| 18 |
+
GPUStats_CALLBACK,
|
| 19 |
+
EMA_CALLBACK,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@attrs.define(slots=False)
|
| 24 |
+
class ModelConfig(DMD2ModelConfig):
|
| 25 |
+
enable_gradient_in_rollout: bool = True
|
| 26 |
+
start_gradient_frame: int = 0
|
| 27 |
+
same_step_across_blocks: bool = True
|
| 28 |
+
last_step_only: bool = False
|
| 29 |
+
context_noise: float = 0.0
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@attrs.define(slots=False)
|
| 33 |
+
class Config(DMD2Config):
|
| 34 |
+
model: ModelConfig = attrs.field(factory=ModelConfig)
|
| 35 |
+
model_class: DictConfig = L(SelfForcingModel)(
|
| 36 |
+
config=None,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def create_config():
|
| 41 |
+
config = Config()
|
| 42 |
+
config.trainer.callbacks = DictConfig(
|
| 43 |
+
{
|
| 44 |
+
**GradClip_CALLBACK,
|
| 45 |
+
**GPUStats_CALLBACK,
|
| 46 |
+
**TrainProfiler_CALLBACK,
|
| 47 |
+
**ParamCount_CALLBACK,
|
| 48 |
+
**EMA_CALLBACK,
|
| 49 |
+
**WANDB_CALLBACK,
|
| 50 |
+
}
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
config.dataloader_train.batch_size = 256
|
| 54 |
+
config.model.student_sample_steps = 4
|
| 55 |
+
config.model.discriminator_scheduler.warm_up_steps = [0]
|
| 56 |
+
config.model.fake_score_scheduler.warm_up_steps = [0]
|
| 57 |
+
config.model.net_scheduler.warm_up_steps = [0]
|
| 58 |
+
|
| 59 |
+
return config
|