Cccccz commited on
Commit
4e8e1be
·
verified ·
1 Parent(s): fe3d7e9

Upload root source files

Browse files
Files changed (8) hide show
  1. .gitignore +5 -0
  2. LICENSE +201 -0
  3. README.md +109 -0
  4. demo.py +631 -0
  5. inference.py +192 -0
  6. requirements.txt +40 -0
  7. setup.py +6 -0
  8. train.py +47 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__
2
+ *.egg-info
3
+
4
+ wan_models
5
+ checkpoints
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="center">
2
+ <h1 align="center">Self Forcing</h1>
3
+ <h3 align="center">Bridging the Train-Test Gap in Autoregressive Video Diffusion</h3>
4
+ </p>
5
+ <p align="center">
6
+ <p align="center">
7
+ <a href="https://www.xunhuang.me/">Xun Huang</a><sup>1</sup>
8
+ ·
9
+ <a href="https://zhengqili.github.io/">Zhengqi Li</a><sup>1</sup>
10
+ ·
11
+ <a href="https://guandehe.github.io/">Guande He</a><sup>2</sup>
12
+ ·
13
+ <a href="https://mingyuanzhou.github.io/">Mingyuan Zhou</a><sup>2</sup>
14
+ ·
15
+ <a href="https://research.adobe.com/person/eli-shechtman/">Eli Shechtman</a><sup>1</sup><br>
16
+ <sup>1</sup>Adobe Research <sup>2</sup>UT Austin
17
+ </p>
18
+ <h3 align="center"><a href="https://arxiv.org/abs/2506.08009">Paper</a> | <a href="https://self-forcing.github.io">Website</a> | <a href="https://huggingface.co/gdhe17/Self-Forcing/tree/main">Models (HuggingFace)</a></h3>
19
+ </p>
20
+
21
+ ---
22
+
23
+ Self Forcing trains autoregressive video diffusion models by **simulating the inference process during training**, performing autoregressive rollout with KV caching. It resolves the train-test distribution mismatch and enables **real-time, streaming video generation on a single RTX 4090** while matching the quality of state-of-the-art diffusion models.
24
+
25
+ ---
26
+
27
+
28
+ https://github.com/user-attachments/assets/7548c2db-fe03-4ba8-8dd3-52d2c6160739
29
+
30
+
31
+ ## Requirements
32
+ We tested this repo on the following setup:
33
+ * Nvidia GPU with at least 24 GB memory (RTX 4090, A100, and H100 are tested).
34
+ * Linux operating system.
35
+ * 64 GB RAM.
36
+
37
+ Other hardware setup could also work but hasn't been tested.
38
+
39
+ ## Installation
40
+ Create a conda environment and install dependencies:
41
+ ```
42
+ conda create -n self_forcing python=3.10 -y
43
+ conda activate self_forcing
44
+ pip install -r requirements.txt
45
+ pip install flash-attn --no-build-isolation
46
+ python setup.py develop
47
+ ```
48
+
49
+ ## Quick Start
50
+ ### Download checkpoints
51
+ ```
52
+ huggingface-cli download Wan-AI/Wan2.1-T2V-1.3B --local-dir-use-symlinks False --local-dir wan_models/Wan2.1-T2V-1.3B
53
+ huggingface-cli download gdhe17/Self-Forcing checkpoints/self_forcing_dmd.pt --local-dir .
54
+ ```
55
+
56
+ ### GUI demo
57
+ ```
58
+ python demo.py
59
+ ```
60
+ Note:
61
+ * **Our model works better with long, detailed prompts** since it's trained with such prompts. We will integrate prompt extension into the codebase (similar to [Wan2.1](https://github.com/Wan-Video/Wan2.1/tree/main?tab=readme-ov-file#2-using-prompt-extention)) in the future. For now, it is recommended to use third-party LLMs (such as GPT-4o) to extend your prompt before providing to the model.
62
+ * You may want to adjust FPS so it plays smoothly on your device.
63
+ * The speed can be improved by enabling `torch.compile`, [TAEHV-VAE](https://github.com/madebyollin/taehv/), or using FP8 Linear layers, although the latter two options may sacrifice quality. It is recommended to use `torch.compile` if possible and enable TAEHV-VAE if further speedup is needed.
64
+
65
+ ### CLI Inference
66
+ Example inference script using the chunk-wise autoregressive checkpoint trained with DMD:
67
+ ```
68
+ python inference.py \
69
+ --config_path configs/self_forcing_dmd.yaml \
70
+ --output_folder videos/self_forcing_dmd \
71
+ --checkpoint_path checkpoints/self_forcing_dmd.pt \
72
+ --data_path prompts/MovieGenVideoBench_extended.txt \
73
+ --use_ema
74
+ ```
75
+ Other config files and corresponding checkpoints can be found in [configs](configs) folder and our [huggingface repo](https://huggingface.co/gdhe17/Self-Forcing/tree/main/checkpoints).
76
+
77
+ ## Training
78
+ ### Download text prompts and ODE initialized checkpoint
79
+ ```
80
+ huggingface-cli download gdhe17/Self-Forcing checkpoints/ode_init.pt --local-dir .
81
+ huggingface-cli download gdhe17/Self-Forcing vidprom_filtered_extended.txt --local-dir prompts
82
+ ```
83
+ Note: Our training algorithm (except for the GAN version) is data-free (**no video data is needed**). For now, we directly provide the ODE initialization checkpoint and will add more instructions on how to perform ODE initialization in the future (which is identical to the process described in the [CausVid](https://github.com/tianweiy/CausVid) repo).
84
+
85
+ ### Self Forcing Training with DMD
86
+ ```
87
+ torchrun --nnodes=8 --nproc_per_node=8 --rdzv_id=5235 \
88
+ --rdzv_backend=c10d \
89
+ --rdzv_endpoint $MASTER_ADDR \
90
+ train.py \
91
+ --config_path configs/self_forcing_dmd.yaml \
92
+ --logdir logs/self_forcing_dmd \
93
+ --disable-wandb
94
+ ```
95
+ Our training run uses 600 iterations and completes in under 2 hours using 64 H100 GPUs. By implementing gradient accumulation, it should be possible to reproduce the results in less than 16 hours using 8 H100 GPUs.
96
+
97
+ ## Acknowledgements
98
+ This codebase is built on top of the open-source implementation of [CausVid](https://github.com/tianweiy/CausVid) by [Tianwei Yin](https://tianweiy.github.io/) and the [Wan2.1](https://github.com/Wan-Video/Wan2.1) repo.
99
+
100
+ ## Citation
101
+ If you find this codebase useful for your research, please kindly cite our paper:
102
+ ```
103
+ @article{huang2025selfforcing,
104
+ title={Self Forcing: Bridging the Train-Test Gap in Autoregressive Video Diffusion},
105
+ author={Huang, Xun and Li, Zhengqi and He, Guande and Zhou, Mingyuan and Shechtman, Eli},
106
+ journal={arXiv preprint arXiv:2506.08009},
107
+ year={2025}
108
+ }
109
+ ```
demo.py ADDED
@@ -0,0 +1,631 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Demo for Self-Forcing.
3
+ """
4
+
5
+ import os
6
+ import re
7
+ import random
8
+ import time
9
+ import base64
10
+ import argparse
11
+ import hashlib
12
+ import subprocess
13
+ import urllib.request
14
+ from io import BytesIO
15
+ from PIL import Image
16
+ import numpy as np
17
+ import torch
18
+ from omegaconf import OmegaConf
19
+ from flask import Flask, render_template, jsonify
20
+ from flask_socketio import SocketIO, emit
21
+ import queue
22
+ from threading import Thread, Event
23
+
24
+ from pipeline import CausalInferencePipeline
25
+ from demo_utils.constant import ZERO_VAE_CACHE
26
+ from demo_utils.vae_block3 import VAEDecoderWrapper
27
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder
28
+ from demo_utils.utils import generate_timestamp
29
+ from demo_utils.memory import gpu, get_cuda_free_memory_gb, DynamicSwapInstaller, move_model_to_device_with_memory_preservation
30
+
31
+ # Parse arguments
32
+ parser = argparse.ArgumentParser()
33
+ parser.add_argument('--port', type=int, default=5001)
34
+ parser.add_argument('--host', type=str, default='0.0.0.0')
35
+ parser.add_argument("--checkpoint_path", type=str, default='./checkpoints/self_forcing_dmd.pt')
36
+ parser.add_argument("--config_path", type=str, default='./configs/self_forcing_dmd.yaml')
37
+ parser.add_argument('--trt', action='store_true')
38
+ args = parser.parse_args()
39
+
40
+ print(f'Free VRAM {get_cuda_free_memory_gb(gpu)} GB')
41
+ low_memory = get_cuda_free_memory_gb(gpu) < 40
42
+
43
+ # Load models
44
+ config = OmegaConf.load(args.config_path)
45
+ default_config = OmegaConf.load("configs/default_config.yaml")
46
+ config = OmegaConf.merge(default_config, config)
47
+
48
+ text_encoder = WanTextEncoder()
49
+
50
+ # Global variables for dynamic model switching
51
+ current_vae_decoder = None
52
+ current_use_taehv = False
53
+ fp8_applied = False
54
+ torch_compile_applied = False
55
+ global frame_number
56
+ frame_number = 0
57
+ anim_name = ""
58
+ frame_rate = 6
59
+
60
+ def initialize_vae_decoder(use_taehv=False, use_trt=False):
61
+ """Initialize VAE decoder based on the selected option"""
62
+ global current_vae_decoder, current_use_taehv
63
+
64
+ if use_trt:
65
+ from demo_utils.vae import VAETRTWrapper
66
+ current_vae_decoder = VAETRTWrapper()
67
+ return current_vae_decoder
68
+
69
+ if use_taehv:
70
+ from demo_utils.taehv import TAEHV
71
+ # Check if taew2_1.pth exists in checkpoints folder, download if missing
72
+ taehv_checkpoint_path = "checkpoints/taew2_1.pth"
73
+ if not os.path.exists(taehv_checkpoint_path):
74
+ print(f"taew2_1.pth not found in checkpoints folder {taehv_checkpoint_path}. Downloading...")
75
+ os.makedirs("checkpoints", exist_ok=True)
76
+ download_url = "https://github.com/madebyollin/taehv/raw/main/taew2_1.pth"
77
+ try:
78
+ urllib.request.urlretrieve(download_url, taehv_checkpoint_path)
79
+ print(f"Successfully downloaded taew2_1.pth to {taehv_checkpoint_path}")
80
+ except Exception as e:
81
+ print(f"Failed to download taew2_1.pth: {e}")
82
+ raise
83
+
84
+ class DotDict(dict):
85
+ __getattr__ = dict.__getitem__
86
+ __setattr__ = dict.__setitem__
87
+
88
+ class TAEHVDiffusersWrapper(torch.nn.Module):
89
+ def __init__(self):
90
+ super().__init__()
91
+ self.dtype = torch.float16
92
+ self.taehv = TAEHV(checkpoint_path=taehv_checkpoint_path).to(self.dtype)
93
+ self.config = DotDict(scaling_factor=1.0)
94
+
95
+ def decode(self, latents, return_dict=None):
96
+ # n, c, t, h, w = latents.shape
97
+ # low-memory, set parallel=True for faster + higher memory
98
+ return self.taehv.decode_video(latents, parallel=False).mul_(2).sub_(1)
99
+
100
+ current_vae_decoder = TAEHVDiffusersWrapper()
101
+ else:
102
+ current_vae_decoder = VAEDecoderWrapper()
103
+ vae_state_dict = torch.load('wan_models/Wan2.1-T2V-1.3B/Wan2.1_VAE.pth', map_location="cpu")
104
+ decoder_state_dict = {}
105
+ for key, value in vae_state_dict.items():
106
+ if 'decoder.' in key or 'conv2' in key:
107
+ decoder_state_dict[key] = value
108
+ current_vae_decoder.load_state_dict(decoder_state_dict)
109
+
110
+ current_vae_decoder.eval()
111
+ current_vae_decoder.to(dtype=torch.float16)
112
+ current_vae_decoder.requires_grad_(False)
113
+ current_vae_decoder.to(gpu)
114
+ current_use_taehv = use_taehv
115
+
116
+ print(f"✅ VAE decoder initialized with {'TAEHV' if use_taehv else 'default VAE'}")
117
+ return current_vae_decoder
118
+
119
+
120
+ # Initialize with default VAE
121
+ vae_decoder = initialize_vae_decoder(use_taehv=False, use_trt=args.trt)
122
+
123
+ transformer = WanDiffusionWrapper(is_causal=True)
124
+ state_dict = torch.load(args.checkpoint_path, map_location="cpu")
125
+ transformer.load_state_dict(state_dict['generator_ema'])
126
+
127
+ text_encoder.eval()
128
+ transformer.eval()
129
+
130
+ transformer.to(dtype=torch.float16)
131
+ text_encoder.to(dtype=torch.bfloat16)
132
+
133
+ text_encoder.requires_grad_(False)
134
+ transformer.requires_grad_(False)
135
+
136
+ pipeline = CausalInferencePipeline(
137
+ config,
138
+ device=gpu,
139
+ generator=transformer,
140
+ text_encoder=text_encoder,
141
+ vae=vae_decoder
142
+ )
143
+
144
+ if low_memory:
145
+ DynamicSwapInstaller.install_model(text_encoder, device=gpu)
146
+ else:
147
+ text_encoder.to(gpu)
148
+ transformer.to(gpu)
149
+
150
+ # Flask and SocketIO setup
151
+ app = Flask(__name__)
152
+ app.config['SECRET_KEY'] = 'frontend_buffered_demo'
153
+ socketio = SocketIO(app, cors_allowed_origins="*")
154
+
155
+ generation_active = False
156
+ stop_event = Event()
157
+ frame_send_queue = queue.Queue()
158
+ sender_thread = None
159
+ models_compiled = False
160
+
161
+
162
+ def tensor_to_base64_frame(frame_tensor):
163
+ """Convert a single frame tensor to base64 image string."""
164
+ global frame_number, anim_name
165
+ # Clamp and normalize to 0-255
166
+ frame = torch.clamp(frame_tensor.float(), -1., 1.) * 127.5 + 127.5
167
+ frame = frame.to(torch.uint8).cpu().numpy()
168
+
169
+ # CHW -> HWC
170
+ if len(frame.shape) == 3:
171
+ frame = np.transpose(frame, (1, 2, 0))
172
+
173
+ # Convert to PIL Image
174
+ if frame.shape[2] == 3: # RGB
175
+ image = Image.fromarray(frame, 'RGB')
176
+ else: # Handle other formats
177
+ image = Image.fromarray(frame)
178
+
179
+ # Convert to base64
180
+ buffer = BytesIO()
181
+ image.save(buffer, format='JPEG', quality=100)
182
+ if not os.path.exists("./images/%s" % anim_name):
183
+ os.makedirs("./images/%s" % anim_name)
184
+ frame_number += 1
185
+ image.save("./images/%s/%s_%03d.jpg" % (anim_name, anim_name, frame_number))
186
+ img_str = base64.b64encode(buffer.getvalue()).decode()
187
+ return f"data:image/jpeg;base64,{img_str}"
188
+
189
+
190
+ def frame_sender_worker():
191
+ """Background thread that processes frame send queue non-blocking."""
192
+ global frame_send_queue, generation_active, stop_event
193
+
194
+ print("📡 Frame sender thread started")
195
+
196
+ while True:
197
+ frame_data = None
198
+ try:
199
+ # Get frame data from queue
200
+ frame_data = frame_send_queue.get(timeout=1.0)
201
+
202
+ if frame_data is None: # Shutdown signal
203
+ frame_send_queue.task_done() # Mark shutdown signal as done
204
+ break
205
+
206
+ frame_tensor, frame_index, block_index, job_id = frame_data
207
+
208
+ # Convert tensor to base64
209
+ base64_frame = tensor_to_base64_frame(frame_tensor)
210
+
211
+ # Send via SocketIO
212
+ try:
213
+ socketio.emit('frame_ready', {
214
+ 'data': base64_frame,
215
+ 'frame_index': frame_index,
216
+ 'block_index': block_index,
217
+ 'job_id': job_id
218
+ })
219
+ except Exception as e:
220
+ print(f"⚠️ Failed to send frame {frame_index}: {e}")
221
+
222
+ frame_send_queue.task_done()
223
+
224
+ except queue.Empty:
225
+ # Check if we should continue running
226
+ if not generation_active and frame_send_queue.empty():
227
+ break
228
+ except Exception as e:
229
+ print(f"❌ Frame sender error: {e}")
230
+ # Make sure to mark task as done even if there's an error
231
+ if frame_data is not None:
232
+ try:
233
+ frame_send_queue.task_done()
234
+ except Exception as e:
235
+ print(f"❌ Failed to mark frame task as done: {e}")
236
+ break
237
+
238
+ print("📡 Frame sender thread stopped")
239
+
240
+
241
+ @torch.no_grad()
242
+ def generate_video_stream(prompt, seed, enable_torch_compile=False, enable_fp8=False, use_taehv=False):
243
+ """Generate video and push frames immediately to frontend."""
244
+ global generation_active, stop_event, frame_send_queue, sender_thread, models_compiled, torch_compile_applied, fp8_applied, current_vae_decoder, current_use_taehv, frame_rate, anim_name
245
+
246
+ try:
247
+ generation_active = True
248
+ stop_event.clear()
249
+ job_id = generate_timestamp()
250
+
251
+ # Start frame sender thread if not already running
252
+ if sender_thread is None or not sender_thread.is_alive():
253
+ sender_thread = Thread(target=frame_sender_worker, daemon=True)
254
+ sender_thread.start()
255
+
256
+ # Emit progress updates
257
+ def emit_progress(message, progress):
258
+ try:
259
+ socketio.emit('progress', {
260
+ 'message': message,
261
+ 'progress': progress,
262
+ 'job_id': job_id
263
+ })
264
+ except Exception as e:
265
+ print(f"❌ Failed to emit progress: {e}")
266
+
267
+ emit_progress('Starting generation...', 0)
268
+
269
+ # Handle VAE decoder switching
270
+ if use_taehv != current_use_taehv:
271
+ emit_progress('Switching VAE decoder...', 2)
272
+ print(f"🔄 Switching VAE decoder to {'TAEHV' if use_taehv else 'default VAE'}")
273
+ current_vae_decoder = initialize_vae_decoder(use_taehv=use_taehv)
274
+ # Update pipeline with new VAE decoder
275
+ pipeline.vae = current_vae_decoder
276
+
277
+ # Handle FP8 quantization
278
+ if enable_fp8 and not fp8_applied:
279
+ emit_progress('Applying FP8 quantization...', 3)
280
+ print("🔧 Applying FP8 quantization to transformer")
281
+ from torchao.quantization.quant_api import quantize_, Float8DynamicActivationFloat8WeightConfig, PerTensor
282
+ quantize_(transformer, Float8DynamicActivationFloat8WeightConfig(granularity=PerTensor()))
283
+ fp8_applied = True
284
+
285
+ # Text encoding
286
+ emit_progress('Encoding text prompt...', 8)
287
+ conditional_dict = text_encoder(text_prompts=[prompt])
288
+ for key, value in conditional_dict.items():
289
+ conditional_dict[key] = value.to(dtype=torch.float16)
290
+ if low_memory:
291
+ gpu_memory_preservation = get_cuda_free_memory_gb(gpu) + 5
292
+ move_model_to_device_with_memory_preservation(
293
+ text_encoder,target_device=gpu, preserved_memory_gb=gpu_memory_preservation)
294
+
295
+ # Handle torch.compile if enabled
296
+ torch_compile_applied = enable_torch_compile
297
+ if enable_torch_compile and not models_compiled:
298
+ # Compile transformer and decoder
299
+ transformer.compile(mode="max-autotune-no-cudagraphs")
300
+ if not current_use_taehv and not low_memory and not args.trt:
301
+ current_vae_decoder.compile(mode="max-autotune-no-cudagraphs")
302
+
303
+ # Initialize generation
304
+ emit_progress('Initializing generation...', 12)
305
+
306
+ rnd = torch.Generator(gpu).manual_seed(seed)
307
+ # all_latents = torch.zeros([1, 21, 16, 60, 104], device=gpu, dtype=torch.bfloat16)
308
+
309
+ pipeline._initialize_kv_cache(batch_size=1, dtype=torch.float16, device=gpu)
310
+ pipeline._initialize_crossattn_cache(batch_size=1, dtype=torch.float16, device=gpu)
311
+
312
+ noise = torch.randn([1, 21, 16, 60, 104], device=gpu, dtype=torch.float16, generator=rnd)
313
+
314
+ # Generation parameters
315
+ num_blocks = 7
316
+ current_start_frame = 0
317
+ num_input_frames = 0
318
+ all_num_frames = [pipeline.num_frame_per_block] * num_blocks
319
+ if current_use_taehv:
320
+ vae_cache = None
321
+ else:
322
+ vae_cache = ZERO_VAE_CACHE
323
+ for i in range(len(vae_cache)):
324
+ vae_cache[i] = vae_cache[i].to(device=gpu, dtype=torch.float16)
325
+
326
+ total_frames_sent = 0
327
+ generation_start_time = time.time()
328
+
329
+ emit_progress('Generating frames... (frontend handles timing)', 15)
330
+
331
+ for idx, current_num_frames in enumerate(all_num_frames):
332
+ if not generation_active or stop_event.is_set():
333
+ break
334
+
335
+ progress = int(((idx + 1) / len(all_num_frames)) * 80) + 15
336
+
337
+ # Special message for first block with torch.compile
338
+ if idx == 0 and torch_compile_applied and not models_compiled:
339
+ emit_progress(
340
+ f'Processing block 1/{len(all_num_frames)} - Compiling models (may take 5-10 minutes)...', progress)
341
+ print(f"🔥 Processing block {idx+1}/{len(all_num_frames)}")
342
+ models_compiled = True
343
+ else:
344
+ emit_progress(f'Processing block {idx+1}/{len(all_num_frames)}...', progress)
345
+ print(f"🔄 Processing block {idx+1}/{len(all_num_frames)}")
346
+
347
+ block_start_time = time.time()
348
+
349
+ noisy_input = noise[:, current_start_frame -
350
+ num_input_frames:current_start_frame + current_num_frames - num_input_frames]
351
+
352
+ # Denoising loop
353
+ denoising_start = time.time()
354
+ for index, current_timestep in enumerate(pipeline.denoising_step_list):
355
+ if not generation_active or stop_event.is_set():
356
+ break
357
+
358
+ timestep = torch.ones([1, current_num_frames], device=noise.device,
359
+ dtype=torch.int64) * current_timestep
360
+
361
+ if index < len(pipeline.denoising_step_list) - 1:
362
+ _, denoised_pred = transformer(
363
+ noisy_image_or_video=noisy_input,
364
+ conditional_dict=conditional_dict,
365
+ timestep=timestep,
366
+ kv_cache=pipeline.kv_cache1,
367
+ crossattn_cache=pipeline.crossattn_cache,
368
+ current_start=current_start_frame * pipeline.frame_seq_length
369
+ )
370
+ next_timestep = pipeline.denoising_step_list[index + 1]
371
+ noisy_input = pipeline.scheduler.add_noise(
372
+ denoised_pred.flatten(0, 1),
373
+ torch.randn_like(denoised_pred.flatten(0, 1)),
374
+ next_timestep * torch.ones([1 * current_num_frames], device=noise.device, dtype=torch.long)
375
+ ).unflatten(0, denoised_pred.shape[:2])
376
+ else:
377
+ _, denoised_pred = transformer(
378
+ noisy_image_or_video=noisy_input,
379
+ conditional_dict=conditional_dict,
380
+ timestep=timestep,
381
+ kv_cache=pipeline.kv_cache1,
382
+ crossattn_cache=pipeline.crossattn_cache,
383
+ current_start=current_start_frame * pipeline.frame_seq_length
384
+ )
385
+
386
+ if not generation_active or stop_event.is_set():
387
+ break
388
+
389
+ denoising_time = time.time() - denoising_start
390
+ print(f"⚡ Block {idx+1} denoising completed in {denoising_time:.2f}s")
391
+
392
+ # Record output
393
+ # all_latents[:, current_start_frame:current_start_frame + current_num_frames] = denoised_pred
394
+
395
+ # Update KV cache for next block
396
+ if idx != len(all_num_frames) - 1:
397
+ transformer(
398
+ noisy_image_or_video=denoised_pred,
399
+ conditional_dict=conditional_dict,
400
+ timestep=torch.zeros_like(timestep),
401
+ kv_cache=pipeline.kv_cache1,
402
+ crossattn_cache=pipeline.crossattn_cache,
403
+ current_start=current_start_frame * pipeline.frame_seq_length,
404
+ )
405
+
406
+ # Decode to pixels and send frames immediately
407
+ print(f"🎨 Decoding block {idx+1} to pixels...")
408
+ decode_start = time.time()
409
+ if args.trt:
410
+ all_current_pixels = []
411
+ for i in range(denoised_pred.shape[1]):
412
+ is_first_frame = torch.tensor(1.0).cuda().half() if idx == 0 and i == 0 else \
413
+ torch.tensor(0.0).cuda().half()
414
+ outputs = vae_decoder.forward(denoised_pred[:, i:i + 1, :, :, :].half(), is_first_frame, *vae_cache)
415
+ # outputs = vae_decoder.forward(denoised_pred.float(), *vae_cache)
416
+ current_pixels, vae_cache = outputs[0], outputs[1:]
417
+ print(current_pixels.max(), current_pixels.min())
418
+ all_current_pixels.append(current_pixels.clone())
419
+ pixels = torch.cat(all_current_pixels, dim=1)
420
+ if idx == 0:
421
+ pixels = pixels[:, 3:, :, :, :] # Skip first 3 frames of first block
422
+ else:
423
+ if current_use_taehv:
424
+ if vae_cache is None:
425
+ vae_cache = denoised_pred
426
+ else:
427
+ denoised_pred = torch.cat([vae_cache, denoised_pred], dim=1)
428
+ vae_cache = denoised_pred[:, -3:, :, :, :]
429
+ pixels = current_vae_decoder.decode(denoised_pred)
430
+ print(f"denoised_pred shape: {denoised_pred.shape}")
431
+ print(f"pixels shape: {pixels.shape}")
432
+ if idx == 0:
433
+ pixels = pixels[:, 3:, :, :, :] # Skip first 3 frames of first block
434
+ else:
435
+ pixels = pixels[:, 12:, :, :, :]
436
+
437
+ else:
438
+ pixels, vae_cache = current_vae_decoder(denoised_pred.half(), *vae_cache)
439
+ if idx == 0:
440
+ pixels = pixels[:, 3:, :, :, :] # Skip first 3 frames of first block
441
+
442
+ decode_time = time.time() - decode_start
443
+ print(f"🎨 Block {idx+1} VAE decoding completed in {decode_time:.2f}s")
444
+
445
+ # Queue frames for non-blocking sending
446
+ block_frames = pixels.shape[1]
447
+ print(f"📡 Queueing {block_frames} frames from block {idx+1} for sending...")
448
+ queue_start = time.time()
449
+
450
+ for frame_idx in range(block_frames):
451
+ if not generation_active or stop_event.is_set():
452
+ break
453
+
454
+ frame_tensor = pixels[0, frame_idx].cpu()
455
+
456
+ # Queue frame data in non-blocking way
457
+ frame_send_queue.put((frame_tensor, total_frames_sent, idx, job_id))
458
+ total_frames_sent += 1
459
+
460
+ queue_time = time.time() - queue_start
461
+ block_time = time.time() - block_start_time
462
+ print(f"✅ Block {idx+1} completed in {block_time:.2f}s ({block_frames} frames queued in {queue_time:.3f}s)")
463
+
464
+ current_start_frame += current_num_frames
465
+
466
+ generation_time = time.time() - generation_start_time
467
+ print(f"🎉 Generation completed in {generation_time:.2f}s! {total_frames_sent} frames queued for sending")
468
+
469
+ # Wait for all frames to be sent before completing
470
+ emit_progress('Waiting for all frames to be sent...', 97)
471
+ print("⏳ Waiting for all frames to be sent...")
472
+ frame_send_queue.join() # Wait for all queued frames to be processed
473
+ print("✅ All frames sent successfully!")
474
+
475
+ generate_mp4_from_images("./images","./videos/"+anim_name+".mp4", frame_rate )
476
+ # Final progress update
477
+ emit_progress('Generation complete!', 100)
478
+
479
+ try:
480
+ socketio.emit('generation_complete', {
481
+ 'message': 'Video generation completed!',
482
+ 'total_frames': total_frames_sent,
483
+ 'generation_time': f"{generation_time:.2f}s",
484
+ 'job_id': job_id
485
+ })
486
+ except Exception as e:
487
+ print(f"❌ Failed to emit generation complete: {e}")
488
+
489
+ except Exception as e:
490
+ print(f"❌ Generation failed: {e}")
491
+ try:
492
+ socketio.emit('error', {
493
+ 'message': f'Generation failed: {str(e)}',
494
+ 'job_id': job_id
495
+ })
496
+ except Exception as e:
497
+ print(f"❌ Failed to emit error: {e}")
498
+ finally:
499
+ generation_active = False
500
+ stop_event.set()
501
+
502
+ # Clean up sender thread
503
+ try:
504
+ frame_send_queue.put(None)
505
+ except Exception as e:
506
+ print(f"❌ Failed to put None in frame_send_queue: {e}")
507
+
508
+
509
+ def generate_mp4_from_images(image_directory, output_video_path, fps=24):
510
+ """
511
+ Generate an MP4 video from a directory of images ordered alphabetically.
512
+
513
+ :param image_directory: Path to the directory containing images.
514
+ :param output_video_path: Path where the output MP4 will be saved.
515
+ :param fps: Frames per second for the output video.
516
+ """
517
+ global anim_name
518
+ # Construct the ffmpeg command
519
+ cmd = [
520
+ 'ffmpeg',
521
+ '-framerate', str(fps),
522
+ '-i', os.path.join(image_directory, anim_name+'/'+anim_name+'_%03d.jpg'), # Adjust the pattern if necessary
523
+ '-c:v', 'libx264',
524
+ '-pix_fmt', 'yuv420p',
525
+ output_video_path
526
+ ]
527
+ try:
528
+ subprocess.run(cmd, check=True)
529
+ print(f"Video saved to {output_video_path}")
530
+ except subprocess.CalledProcessError as e:
531
+ print(f"An error occurred: {e}")
532
+
533
+ def calculate_sha256(data):
534
+ # Convert data to bytes if it's not already
535
+ if isinstance(data, str):
536
+ data = data.encode()
537
+ # Calculate SHA-256 hash
538
+ sha256_hash = hashlib.sha256(data).hexdigest()
539
+ return sha256_hash
540
+
541
+ # Socket.IO event handlers
542
+ @socketio.on('connect')
543
+ def handle_connect():
544
+ print('Client connected')
545
+ emit('status', {'message': 'Connected to frontend-buffered demo server'})
546
+
547
+
548
+ @socketio.on('disconnect')
549
+ def handle_disconnect():
550
+ print('Client disconnected')
551
+
552
+
553
+ @socketio.on('start_generation')
554
+ def handle_start_generation(data):
555
+ global generation_active, frame_number, anim_name, frame_rate
556
+
557
+ frame_number = 0
558
+ if generation_active:
559
+ emit('error', {'message': 'Generation already in progress'})
560
+ return
561
+
562
+ prompt = data.get('prompt', '')
563
+
564
+ seed = data.get('seed', -1)
565
+ if seed==-1:
566
+ seed = random.randint(0, 2**32)
567
+
568
+ # Extract words up to the first punctuation or newline
569
+ words_up_to_punctuation = re.split(r'[^\w\s]', prompt)[0].strip() if prompt else ''
570
+ if not words_up_to_punctuation:
571
+ words_up_to_punctuation = re.split(r'[\n\r]', prompt)[0].strip()
572
+
573
+ # Calculate SHA-256 hash of the entire prompt
574
+ sha256_hash = calculate_sha256(prompt)
575
+
576
+ # Create anim_name with the extracted words and first 10 characters of the hash
577
+ anim_name = f"{words_up_to_punctuation[:20]}_{str(seed)}_{sha256_hash[:10]}"
578
+
579
+ generation_active = True
580
+ generation_start_time = time.time()
581
+ enable_torch_compile = data.get('enable_torch_compile', False)
582
+ enable_fp8 = data.get('enable_fp8', False)
583
+ use_taehv = data.get('use_taehv', False)
584
+ frame_rate = data.get('fps', 6)
585
+
586
+ if not prompt:
587
+ emit('error', {'message': 'Prompt is required'})
588
+ return
589
+
590
+ # Start generation in background thread
591
+ socketio.start_background_task(generate_video_stream, prompt, seed,
592
+ enable_torch_compile, enable_fp8, use_taehv)
593
+ emit('status', {'message': 'Generation started - frames will be sent immediately'})
594
+
595
+
596
+ @socketio.on('stop_generation')
597
+ def handle_stop_generation():
598
+ global generation_active, stop_event, frame_send_queue
599
+ generation_active = False
600
+ stop_event.set()
601
+
602
+ # Signal sender thread to stop (will be processed after current frames)
603
+ try:
604
+ frame_send_queue.put(None)
605
+ except Exception as e:
606
+ print(f"❌ Failed to put None in frame_send_queue: {e}")
607
+
608
+ emit('status', {'message': 'Generation stopped'})
609
+
610
+ # Web routes
611
+
612
+
613
+ @app.route('/')
614
+ def index():
615
+ return render_template('demo.html')
616
+
617
+
618
+ @app.route('/api/status')
619
+ def api_status():
620
+ return jsonify({
621
+ 'generation_active': generation_active,
622
+ 'free_vram_gb': get_cuda_free_memory_gb(gpu),
623
+ 'fp8_applied': fp8_applied,
624
+ 'torch_compile_applied': torch_compile_applied,
625
+ 'current_use_taehv': current_use_taehv
626
+ })
627
+
628
+
629
+ if __name__ == '__main__':
630
+ print(f"🚀 Starting demo on http://{args.host}:{args.port}")
631
+ socketio.run(app, host=args.host, port=args.port, debug=False)
inference.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import torch
3
+ import os
4
+ from omegaconf import OmegaConf
5
+ from tqdm import tqdm
6
+ from torchvision import transforms
7
+ from torchvision.io import write_video
8
+ from einops import rearrange
9
+ import torch.distributed as dist
10
+ from torch.utils.data import DataLoader, SequentialSampler
11
+ from torch.utils.data.distributed import DistributedSampler
12
+
13
+ from pipeline import (
14
+ CausalDiffusionInferencePipeline,
15
+ CausalInferencePipeline,
16
+ )
17
+ from utils.dataset import TextDataset, TextImagePairDataset
18
+ from utils.misc import set_seed
19
+
20
+ from demo_utils.memory import gpu, get_cuda_free_memory_gb, DynamicSwapInstaller
21
+
22
+ parser = argparse.ArgumentParser()
23
+ parser.add_argument("--config_path", type=str, help="Path to the config file")
24
+ parser.add_argument("--checkpoint_path", type=str, help="Path to the checkpoint folder")
25
+ parser.add_argument("--data_path", type=str, help="Path to the dataset")
26
+ parser.add_argument("--extended_prompt_path", type=str, help="Path to the extended prompt")
27
+ parser.add_argument("--output_folder", type=str, help="Output folder")
28
+ parser.add_argument("--num_output_frames", type=int, default=21,
29
+ help="Number of overlap frames between sliding windows")
30
+ parser.add_argument("--i2v", action="store_true", help="Whether to perform I2V (or T2V by default)")
31
+ parser.add_argument("--use_ema", action="store_true", help="Whether to use EMA parameters")
32
+ parser.add_argument("--seed", type=int, default=0, help="Random seed")
33
+ parser.add_argument("--num_samples", type=int, default=1, help="Number of samples to generate per prompt")
34
+ parser.add_argument("--save_with_index", action="store_true",
35
+ help="Whether to save the video using the index or prompt as the filename")
36
+ args = parser.parse_args()
37
+
38
+ # Initialize distributed inference
39
+ if "LOCAL_RANK" in os.environ:
40
+ dist.init_process_group(backend='nccl')
41
+ local_rank = int(os.environ["LOCAL_RANK"])
42
+ torch.cuda.set_device(local_rank)
43
+ device = torch.device(f"cuda:{local_rank}")
44
+ world_size = dist.get_world_size()
45
+ set_seed(args.seed + local_rank)
46
+ else:
47
+ device = torch.device("cuda")
48
+ local_rank = 0
49
+ world_size = 1
50
+ set_seed(args.seed)
51
+
52
+ print(f'Free VRAM {get_cuda_free_memory_gb(gpu)} GB')
53
+ low_memory = get_cuda_free_memory_gb(gpu) < 40
54
+
55
+ torch.set_grad_enabled(False)
56
+
57
+ config = OmegaConf.load(args.config_path)
58
+ default_config = OmegaConf.load("configs/default_config.yaml")
59
+ config = OmegaConf.merge(default_config, config)
60
+
61
+ # Initialize pipeline
62
+ if hasattr(config, 'denoising_step_list'):
63
+ # Few-step inference
64
+ pipeline = CausalInferencePipeline(config, device=device)
65
+ else:
66
+ # Multi-step diffusion inference
67
+ pipeline = CausalDiffusionInferencePipeline(config, device=device)
68
+
69
+ if args.checkpoint_path:
70
+ state_dict = torch.load(args.checkpoint_path, map_location="cpu")
71
+ pipeline.generator.load_state_dict(state_dict['generator' if not args.use_ema else 'generator_ema'])
72
+
73
+ pipeline = pipeline.to(dtype=torch.bfloat16)
74
+ if low_memory:
75
+ DynamicSwapInstaller.install_model(pipeline.text_encoder, device=gpu)
76
+ else:
77
+ pipeline.text_encoder.to(device=gpu)
78
+ pipeline.generator.to(device=gpu)
79
+ pipeline.vae.to(device=gpu)
80
+
81
+
82
+ # Create dataset
83
+ if args.i2v:
84
+ assert not dist.is_initialized(), "I2V does not support distributed inference yet"
85
+ transform = transforms.Compose([
86
+ transforms.Resize((480, 832)),
87
+ transforms.ToTensor(),
88
+ transforms.Normalize([0.5], [0.5])
89
+ ])
90
+ dataset = TextImagePairDataset(args.data_path, transform=transform)
91
+ else:
92
+ dataset = TextDataset(prompt_path=args.data_path, extended_prompt_path=args.extended_prompt_path)
93
+ num_prompts = len(dataset)
94
+ print(f"Number of prompts: {num_prompts}")
95
+
96
+ if dist.is_initialized():
97
+ sampler = DistributedSampler(dataset, shuffle=False, drop_last=True)
98
+ else:
99
+ sampler = SequentialSampler(dataset)
100
+ dataloader = DataLoader(dataset, batch_size=1, sampler=sampler, num_workers=0, drop_last=False)
101
+
102
+ # Create output directory (only on main process to avoid race conditions)
103
+ if local_rank == 0:
104
+ os.makedirs(args.output_folder, exist_ok=True)
105
+
106
+ if dist.is_initialized():
107
+ dist.barrier()
108
+
109
+
110
+ def encode(self, videos: torch.Tensor) -> torch.Tensor:
111
+ device, dtype = videos[0].device, videos[0].dtype
112
+ scale = [self.mean.to(device=device, dtype=dtype),
113
+ 1.0 / self.std.to(device=device, dtype=dtype)]
114
+ output = [
115
+ self.model.encode(u.unsqueeze(0), scale).float().squeeze(0)
116
+ for u in videos
117
+ ]
118
+
119
+ output = torch.stack(output, dim=0)
120
+ return output
121
+
122
+
123
+ for i, batch_data in tqdm(enumerate(dataloader), disable=(local_rank != 0)):
124
+ idx = batch_data['idx'].item()
125
+
126
+ # For DataLoader batch_size=1, the batch_data is already a single item, but in a batch container
127
+ # Unpack the batch data for convenience
128
+ if isinstance(batch_data, dict):
129
+ batch = batch_data
130
+ elif isinstance(batch_data, list):
131
+ batch = batch_data[0] # First (and only) item in the batch
132
+
133
+ all_video = []
134
+ num_generated_frames = 0 # Number of generated (latent) frames
135
+
136
+ if args.i2v:
137
+ # For image-to-video, batch contains image and caption
138
+ prompt = batch['prompts'][0] # Get caption from batch
139
+ prompts = [prompt] * args.num_samples
140
+
141
+ # Process the image
142
+ image = batch['image'].squeeze(0).unsqueeze(0).unsqueeze(2).to(device=device, dtype=torch.bfloat16)
143
+
144
+ # Encode the input image as the first latent
145
+ initial_latent = pipeline.vae.encode_to_latent(image).to(device=device, dtype=torch.bfloat16)
146
+ initial_latent = initial_latent.repeat(args.num_samples, 1, 1, 1, 1)
147
+
148
+ sampled_noise = torch.randn(
149
+ [args.num_samples, args.num_output_frames - 1, 16, 60, 104], device=device, dtype=torch.bfloat16
150
+ )
151
+ else:
152
+ # For text-to-video, batch is just the text prompt
153
+ prompt = batch['prompts'][0]
154
+ extended_prompt = batch['extended_prompts'][0] if 'extended_prompts' in batch else None
155
+ if extended_prompt is not None:
156
+ prompts = [extended_prompt] * args.num_samples
157
+ else:
158
+ prompts = [prompt] * args.num_samples
159
+ initial_latent = None
160
+
161
+ sampled_noise = torch.randn(
162
+ [args.num_samples, args.num_output_frames, 16, 60, 104], device=device, dtype=torch.bfloat16
163
+ )
164
+
165
+ # Generate 81 frames
166
+ video, latents = pipeline.inference(
167
+ noise=sampled_noise,
168
+ text_prompts=prompts,
169
+ return_latents=True,
170
+ initial_latent=initial_latent,
171
+ low_memory=low_memory,
172
+ )
173
+ current_video = rearrange(video, 'b t c h w -> b t h w c').cpu()
174
+ all_video.append(current_video)
175
+ num_generated_frames += latents.shape[1]
176
+
177
+ # Final output video
178
+ video = 255.0 * torch.cat(all_video, dim=1)
179
+
180
+ # Clear VAE cache
181
+ pipeline.vae.model.clear_cache()
182
+
183
+ # Save the video if the current prompt is not a dummy prompt
184
+ if idx < num_prompts:
185
+ model = "regular" if not args.use_ema else "ema"
186
+ for seed_idx in range(args.num_samples):
187
+ # All processes save their videos
188
+ if args.save_with_index:
189
+ output_path = os.path.join(args.output_folder, f'{idx}-{seed_idx}_{model}.mp4')
190
+ else:
191
+ output_path = os.path.join(args.output_folder, f'{prompt[:100]}-{seed_idx}.mp4')
192
+ write_video(output_path, video[seed_idx], fps=16)
requirements.txt ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch>=2.4.0
2
+ torchvision>=0.19.0
3
+ opencv-python>=4.9.0.80
4
+ diffusers==0.31.0
5
+ transformers>=4.49.0
6
+ tokenizers>=0.20.3
7
+ accelerate>=1.1.1
8
+ tqdm
9
+ imageio
10
+ easydict
11
+ ftfy
12
+ dashscope
13
+ imageio-ffmpeg
14
+ numpy==1.24.4
15
+ wandb
16
+ omegaconf
17
+ einops
18
+ av==13.1.0
19
+ opencv-python
20
+ git+https://github.com/openai/CLIP.git
21
+ open_clip_torch
22
+ starlette
23
+ pycocotools
24
+ lmdb
25
+ matplotlib
26
+ sentencepiece
27
+ pydantic==2.10.6
28
+ scikit-image
29
+ huggingface_hub[cli]
30
+ dominate
31
+ nvidia-pyindex
32
+ nvidia-tensorrt
33
+ pycuda
34
+ onnx
35
+ onnxruntime
36
+ onnxscript
37
+ onnxconverter_common
38
+ flask
39
+ flask-socketio
40
+ torchao
setup.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from setuptools import setup, find_packages
2
+ setup(
3
+ name="self_forcing",
4
+ version="0.0.1",
5
+ packages=find_packages(),
6
+ )
train.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ from omegaconf import OmegaConf
4
+ import wandb
5
+
6
+ from trainer import DiffusionTrainer, GANTrainer, ODETrainer, ScoreDistillationTrainer
7
+
8
+
9
+ def main():
10
+ parser = argparse.ArgumentParser()
11
+ parser.add_argument("--config_path", type=str, required=True)
12
+ parser.add_argument("--no_save", action="store_true")
13
+ parser.add_argument("--no_visualize", action="store_true")
14
+ parser.add_argument("--logdir", type=str, default="", help="Path to the directory to save logs")
15
+ parser.add_argument("--wandb-save-dir", type=str, default="", help="Path to the directory to save wandb logs")
16
+ parser.add_argument("--disable-wandb", action="store_true")
17
+
18
+ args = parser.parse_args()
19
+
20
+ config = OmegaConf.load(args.config_path)
21
+ default_config = OmegaConf.load("configs/default_config.yaml")
22
+ config = OmegaConf.merge(default_config, config)
23
+ config.no_save = args.no_save
24
+ config.no_visualize = args.no_visualize
25
+
26
+ # get the filename of config_path
27
+ config_name = os.path.basename(args.config_path).split(".")[0]
28
+ config.config_name = config_name
29
+ config.logdir = args.logdir
30
+ config.wandb_save_dir = args.wandb_save_dir
31
+ config.disable_wandb = args.disable_wandb
32
+
33
+ if config.trainer == "diffusion":
34
+ trainer = DiffusionTrainer(config)
35
+ elif config.trainer == "gan":
36
+ trainer = GANTrainer(config)
37
+ elif config.trainer == "ode":
38
+ trainer = ODETrainer(config)
39
+ elif config.trainer == "score_distillation":
40
+ trainer = ScoreDistillationTrainer(config)
41
+ trainer.train()
42
+
43
+ wandb.finish()
44
+
45
+
46
+ if __name__ == "__main__":
47
+ main()