multimodalart HF Staff commited on
Commit
9368ee7
·
verified ·
1 Parent(s): ea7df04

Initial Lip Forcing 14B streaming demo

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +4 -0
  2. OmniAvatar/configs/__init__.py +0 -0
  3. OmniAvatar/configs/model_config.py +664 -0
  4. OmniAvatar/models/audio_pack.py +40 -0
  5. OmniAvatar/models/model_manager.py +444 -0
  6. OmniAvatar/models/wan_video_dit.py +611 -0
  7. OmniAvatar/models/wan_video_text_encoder.py +269 -0
  8. OmniAvatar/models/wan_video_vae.py +938 -0
  9. OmniAvatar/models/wav2vec.py +209 -0
  10. OmniAvatar/prompters/__init__.py +1 -0
  11. OmniAvatar/prompters/base_prompter.py +70 -0
  12. OmniAvatar/prompters/wan_prompter.py +109 -0
  13. OmniAvatar/utils/args_config.py +122 -0
  14. OmniAvatar/utils/io_utils.py +245 -0
  15. OmniAvatar/utils/latentsync/__init__.py +19 -0
  16. OmniAvatar/utils/latentsync/affine_transform.py +151 -0
  17. OmniAvatar/utils/latentsync/face_detector.py +93 -0
  18. OmniAvatar/utils/latentsync/image_processor.py +109 -0
  19. README.md +25 -5
  20. app.py +373 -0
  21. examples/example1_audio.wav +3 -0
  22. examples/example1_video.mp4 +3 -0
  23. examples/example2_audio.wav +3 -0
  24. examples/example2_video.mp4 +3 -0
  25. lipforcing/__init__.py +2 -0
  26. lipforcing/callbacks/__init__.py +2 -0
  27. lipforcing/callbacks/callback.py +183 -0
  28. lipforcing/callbacks/ct_schedule.py +83 -0
  29. lipforcing/callbacks/ema.py +169 -0
  30. lipforcing/callbacks/forced_weight_norm.py +28 -0
  31. lipforcing/callbacks/gpu_mem_profiler.py +134 -0
  32. lipforcing/callbacks/gpu_stats.py +92 -0
  33. lipforcing/callbacks/grad_clip.py +219 -0
  34. lipforcing/callbacks/param_count.py +116 -0
  35. lipforcing/callbacks/stdout_logger.py +33 -0
  36. lipforcing/callbacks/train_profiler.py +138 -0
  37. lipforcing/callbacks/wandb.py +773 -0
  38. lipforcing/configs/__init__.py +2 -0
  39. lipforcing/configs/callbacks.py +65 -0
  40. lipforcing/configs/config.py +282 -0
  41. lipforcing/configs/config_utils.py +266 -0
  42. lipforcing/configs/discriminator.py +23 -0
  43. lipforcing/configs/experiments/OmniAvatar/__init__.py +0 -0
  44. lipforcing/configs/experiments/OmniAvatar/config_df.py +247 -0
  45. lipforcing/configs/experiments/OmniAvatar/config_sf.py +395 -0
  46. lipforcing/configs/methods/__init__.py +2 -0
  47. lipforcing/configs/methods/config_dmd2.py +79 -0
  48. lipforcing/configs/methods/config_omniavatar_df.py +53 -0
  49. lipforcing/configs/methods/config_omniavatar_sf.py +102 -0
  50. 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: gray
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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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** &nbsp;·&nbsp;
329
+ 14B student &nbsp;·&nbsp;
330
+ [Paper](https://arxiv.org/abs/2606.11180) &nbsp;·&nbsp;
331
+ [Project](https://cvlab-kaist.github.io/LipForcing/) &nbsp;·&nbsp;
332
+ [Code](https://github.com/cvlab-kaist/LipForcing) &nbsp;·&nbsp;
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