Upload root source files
Browse files- .gitignore +5 -0
- LICENSE +201 -0
- README.md +109 -0
- demo.py +631 -0
- inference.py +192 -0
- requirements.txt +40 -0
- setup.py +6 -0
- 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()
|