Cccccz commited on
Commit
ae8ade0
·
verified ·
1 Parent(s): 4eeac98

Upload code and configuration only

Browse files

Source code, configs, dependency metadata, README and license. Excludes datasets, model weights, generated outputs and runtime artifacts.

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +9 -0
  2. LICENSE +201 -0
  3. README.md +460 -0
  4. configs/ar_diffusion_tf_chunkwise.yaml +46 -0
  5. configs/ar_diffusion_tf_framewise.yaml +46 -0
  6. configs/causal_cd_chunkwise.yaml +57 -0
  7. configs/causal_cd_framewise.yaml +57 -0
  8. configs/causal_forcing_dmd_chunkwise.yaml +51 -0
  9. configs/causal_forcing_dmd_framewise.yaml +51 -0
  10. configs/causal_forcing_dmd_framewise_1step.yaml +53 -0
  11. configs/causal_forcing_dmd_framewise_2step.yaml +54 -0
  12. configs/causal_ode_chunkwise.yaml +31 -0
  13. configs/causal_ode_framewise.yaml +31 -0
  14. configs/default_config.yaml +20 -0
  15. demo.py +631 -0
  16. demo_utils/constant.py +41 -0
  17. demo_utils/memory.py +135 -0
  18. demo_utils/taehv.py +313 -0
  19. demo_utils/utils.py +616 -0
  20. demo_utils/vae.py +390 -0
  21. demo_utils/vae_block3.py +291 -0
  22. demo_utils/vae_torch2trt.py +308 -0
  23. get_causal_ode_data_chunkwise.py +140 -0
  24. get_causal_ode_data_framewise.py +140 -0
  25. get_causal_ode_data_kv_optimized.py +188 -0
  26. inference.py +218 -0
  27. long_video/LICENSE +81 -0
  28. long_video/README.md +40 -0
  29. long_video/app.py +167 -0
  30. long_video/configs/default_config.yaml +20 -0
  31. long_video/configs/rolling_forcing_dmd.yaml +47 -0
  32. long_video/inference.py +197 -0
  33. long_video/model/__init__.py +14 -0
  34. long_video/model/base.py +230 -0
  35. long_video/model/causvid.py +391 -0
  36. long_video/model/diffusion.py +125 -0
  37. long_video/model/dmd.py +332 -0
  38. long_video/model/gan.py +295 -0
  39. long_video/model/ode_regression.py +136 -0
  40. long_video/model/sid.py +283 -0
  41. long_video/pipeline/__init__.py +13 -0
  42. long_video/pipeline/bidirectional_diffusion_inference.py +110 -0
  43. long_video/pipeline/bidirectional_inference.py +71 -0
  44. long_video/pipeline/causal_diffusion_inference.py +342 -0
  45. long_video/pipeline/rolling_forcing_inference.py +372 -0
  46. long_video/pipeline/rolling_forcing_training.py +464 -0
  47. long_video/requirements.txt +44 -0
  48. long_video/train.py +45 -0
  49. long_video/trainer/__init__.py +11 -0
  50. long_video/trainer/diffusion.py +265 -0
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ *.egg-info
3
+
4
+ wan_models
5
+ checkpoints
6
+ output
7
+ dataset
8
+ prompts/vidprom_filtered_extended.txt
9
+ logs
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,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+
3
+ ## Causal Forcing & Causal Forcing++
4
+ ### Autoregressive Diffusion Distillation Done Right for High-Quality Real-Time Interactive Video Generation
5
+
6
+ <p align="center">
7
+ <p align="center">
8
+ <div>
9
+ <a href="https://zhuhz22.github.io/" target="_blank">Hongzhou Zhu*</a><sup></sup>,
10
+ <a href="https://gracezhao1997.github.io/" target="_blank">Min Zhao*</a><sup></sup> ,
11
+ <a href="https://guandehe.github.io/" target="_blank">Guande He</a><sup></sup>,
12
+ <a href="https://scholar.google.com/citations?user=dxN1_X0AAAAJ&hl=en" target="_blank">Hang Su</a><sup></sup>,
13
+ <a href="https://zhenxuan00.github.io/" target="_blank">Chongxuan Li</a><sup></sup> ,
14
+ <a href="https://ml.cs.tsinghua.edu.cn/~jun/index.shtml" target="_blank">Jun Zhu</a><sup></sup>
15
+ </div>
16
+ <div>
17
+ <sup></sup>Tsinghua University & Shengshu & UT Austin & RUC
18
+ </div>
19
+
20
+
21
+ </div>
22
+ </p>
23
+ <h3 align="center"><a href="https://arxiv.org/abs/2602.02214">Causal Forcing</a> | <a href="https://arxiv.org/abs/2605.15141">Causal Forcing++</a> | <a href="https://thu-ml.github.io/CausalForcing.github.io">Website</a> | <a href="https://huggingface.co/zhuhz22/Causal-Forcing/tree/main">Models</a> | <a href="https://my.feishu.cn/wiki/AjBSwcjpqiN0ECkodIWcGDcMn4e?from=from_copylink">Document</a> </h3>
24
+ </p>
25
+
26
+
27
+
28
+ -----
29
+ The Causal Forcing series uses **Causal ODE** or **Causal Consistency Distillation** to drive asymmetric DMD as a theoretically correct initialization for real-time interactive video generation. **Refer to [this extension for long video generation](#minute-level-long-video-generation).**
30
+
31
+ [Causal Forcing](https://arxiv.org/abs/2602.02214) significantly outperforms Self Forcing in **both visual quality and motion dynamics**, while keeping **the same training budget and inference efficiency**. We support both chunk-wise and **frame-wise** models, with the latter natively unifying T2V and **I2V**.
32
+
33
+ We further propose [**Causal Forcing++**](https://arxiv.org/abs/2605.15141)[ technical report ], replacing ODE with **causal Consistency Distillation** to eliminate ODE data curation and improve performance, releasing the first **1-step/2-step frame-wise** models.
34
+
35
+ -----
36
+ <img width="2090" height="850" alt="overview" src="assets/pipeline.png" />
37
+
38
+ ## Table of Contents
39
+
40
+ - [Quick Start](#quick-start)
41
+ - [Installation](#installation)
42
+ - [Inference: 1/2/4-step T2V & I2V](#cli-inference)
43
+ - [Long Video](#minute-level-long-video-generation)
44
+ - [Training Pipeline](#training) (Causal Foricng / Causal Forcing++)
45
+ - [Stage 1: AR Diffusion](#training)
46
+ - [Stage 2: Causal ODE (Causal Forcing)](#training) / [🔥Causal CD (Causal Forcing++)](#-stage-2-option-b-causal-consistency-distillation-initialization-causal-forcing)
47
+ - [Stage 3: Asymmetric DMD](#stage-3-dmd)
48
+
49
+
50
+ | Models | Checkpoints | Description|
51
+ |--------------------|---------------------------------------------------------------------------------------------------------------------------------------------|-------|
52
+ | Chunk-wise 4-step | 🤗 [Huggingface](https://huggingface.co/zhuhz22/Causal-Forcing/blob/main/chunkwise/causal_forcing.pt) | SOTA AR model on Wan1.3B outperforming Self Forcing|
53
+ | Frame-wise 4-step | 🤗 [Huggingface](https://huggingface.co/zhuhz22/Causal-Forcing/blob/main/framewise/causal_forcing.pt) | Rich dynamics and high quality |
54
+ | Frame-wise 2-step🔥 | 🤗 [Huggingface](https://huggingface.co/zhuhz22/Causal-Forcing/blob/main/causal-forcing%2B%2B/framewise-2step.pt) | The first frame-wise 2-step model **even better than 4-step**|
55
+ | Frame-wise 1-step | 🤗 [Huggingface](https://huggingface.co/zhuhz22/Causal-Forcing/blob/main/causal-forcing%2B%2B/framewise-1step.pt) | Extremely low latency |
56
+ | Long Video Generator 🔥 | 🤗 [Huggingface](https://huggingface.co/zhuhz22/Causal-Forcing/blob/main/chunkwise/longvideo.pt) | Minute-long AR video generator |
57
+ | HY1.5-TI2V (8B) 🔥 | 🤗 [Huggingface](https://huggingface.co/MIN-Lab/minWM) | Refer to [this repo](https://github.com/shengshu-ai/minWM). |
58
+ | Action-conditioned WM | 🤗 [Huggingface](https://huggingface.co/MIN-Lab/minWM) | Refer to [this repo](https://github.com/shengshu-ai/minWM). |
59
+
60
+ -----
61
+
62
+ https://github.com/user-attachments/assets/310f0cfa-e1bb-496d-8941-87f77b3271c0
63
+
64
+ ## 🔥 News
65
+ - **2026.7.23**: [Self Gradient Forcing](https://github.com/zhuang2002/Self_Gradient_Forcing) is built on Causal Forcing initialization.
66
+ - **2026.7.20**: Happy to see that the recent SOTA video world models [DreamX-World 1.0](https://arxiv.org/pdf/2606.16993) and [Matrix-Game 3.5](https://matrix-game-v3-5.github.io/paper/Matrix-Game-3.5.pdf) are built on Causal Forcing!
67
+ - **2026.5.17**: We release Causal Forcing for the HY1.5-TI2V-8B model! Refer to [this repo](https://github.com/shengshu-ai/minWM) for the details. This model explicitly supports I2V.
68
+ - **2026.5.15**: We release [Causal Forcing++](https://arxiv.org/abs/2605.15141), supporting Casual Consistency Distillation for few-step initialization, and open-source **the first frame-wise 2-step AR model** comparable to chunk-wise 4-step models!
69
+ - **2026.5.10**: Thanks to @[AshadowZ](https://github.com/AshadowZ), now our chunk-wise ODE data curation is **3x faster**!
70
+ - **2026.4.16**: **Optimize the Stage 2 🔥consistency distillation🔥 infrastructure for 3× faster training, let's try it now!** We have also released the ckpt.
71
+ - **2026.3.15** : [Rolling Sink](https://github.com/haodong2000/RollingSink), [Infinity-RoPE](https://github.com/yesiltepe-hidir/infinity-rope) and [Deep Forcing](https://cvlab-kaist.github.io/DeepForcing/) adopt Causal Forcing as one of the base models!
72
+ - **2026.2.28** : Add [FAQ section](#faq--blog) regarding hot topics, specifically which is the better Initialization between AR diffusion and causal ODE distillation.
73
+ - **2026.2.11** : We now support **I2V** generation! Feel free to try it [here](#new-i2v)!
74
+ - **2026.2.7** : Causal Forcing now supports [Rolling Forcing](https://github.com/TencentARC/RollingForcing), enabling minute-level long video generation!
75
+ - **2026.2.5** : Release causal consistency distillation (Preview) as substitute for ODE distillation, **free of generating ODE paired data**!
76
+ - **2026.2.2** : The [paper](https://arxiv.org/abs/2602.02214), [project page](https://thu-ml.github.io/CausalForcing.github.io/), and code are released.
77
+
78
+
79
+ ## Quick Start
80
+
81
+ > The inference environment is identical to Self Forcing.
82
+
83
+ **NOTE**: Similar to CausVid/Self Forcing, Causal Forcing does not natively support videos longer than 81 frames. As a base training method, it is orthogonal to techniques like Longlive/Rolling Forcing. To use Causal Forcing as a long video baseline, see [this extension](#minute-level-long-video-generation). **Directly using the 5-second trained Causal Forcing model as a baseline for long video generation is extremely unfair**.
84
+
85
+
86
+ ### Installation
87
+ ```bash
88
+ conda create -n causal_forcing python=3.10 -y
89
+ conda activate causal_forcing
90
+ pip install -r requirements.txt
91
+ pip install git+https://github.com/openai/CLIP.git
92
+ pip install flash-attn --no-build-isolation
93
+ python setup.py develop
94
+ ```
95
+ ### Download Checkpoints
96
+ ```bash
97
+ hf download Wan-AI/Wan2.1-T2V-1.3B --local-dir wan_models/Wan2.1-T2V-1.3B
98
+ hf download Wan-AI/Wan2.1-T2V-14B --local-dir wan_models/Wan2.1-T2V-14B
99
+ # Causal Forcing
100
+ hf download zhuhz22/Causal-Forcing chunkwise/causal_forcing.pt --local-dir checkpoints
101
+ hf download zhuhz22/Causal-Forcing framewise/causal_forcing.pt --local-dir checkpoints
102
+ # Causal Forcing++
103
+ hf download zhuhz22/Causal-Forcing causal-forcing++/framewise-2step.pt --local-dir checkpoints
104
+ hf download zhuhz22/Causal-Forcing causal-forcing++/framewise-1step.pt --local-dir checkpoints
105
+ ```
106
+
107
+ ### CLI Inference
108
+
109
+ #### T2V
110
+ Chunk-wise model:
111
+ ```bash
112
+ python inference.py \
113
+ --config_path configs/causal_forcing_dmd_chunkwise.yaml \
114
+ --output_folder output/chunkwise \
115
+ --checkpoint_path checkpoints/chunkwise/causal_forcing.pt \
116
+ --data_path prompts/demos.txt
117
+ ```
118
+
119
+ Frame-wise model:
120
+
121
+ ```bash
122
+ # =============== Causal Forcing++ ================
123
+ # 2-step Causal Forcing++
124
+ python inference.py \
125
+ --config_path configs/causal_forcing_dmd_framewise_2step.yaml \
126
+ --output_folder output/framewise_2step_cf++ \
127
+ --checkpoint_path checkpoints/causal-forcing++/framewise-2step.pt \
128
+ --data_path prompts/demos.txt \
129
+ --use_ema
130
+
131
+ # 1-step Causal Forcing++
132
+ python inference.py \
133
+ --config_path configs/causal_forcing_dmd_framewise_1step.yaml \
134
+ --output_folder output/framewise_1step_cf++ \
135
+ --checkpoint_path checkpoints/causal-forcing++/framewise-1step.pt \
136
+ --data_path prompts/demos.txt \
137
+ --use_ema
138
+
139
+ # =============== Causal Forcing ================
140
+ # 4-step Causal Forcing
141
+ python inference.py \
142
+ --config_path configs/causal_forcing_dmd_framewise.yaml \
143
+ --output_folder output/framewise \
144
+ --checkpoint_path checkpoints/framewise/causal_forcing.pt \
145
+ --data_path prompts/demos.txt \
146
+ --use_ema
147
+ ```
148
+
149
+ #### I2V
150
+ > Our frame-wise setting natively supports I2V. You simply need to set the first latent initial frame as your conditional image.
151
+
152
+ ```bash
153
+ python inference.py \
154
+ --config_path configs/causal_forcing_dmd_framewise.yaml \
155
+ --output_folder output/framewise \
156
+ --checkpoint_path checkpoints/framewise/causal_forcing.pt \
157
+ --data_path prompts/i2v \
158
+ --i2v \
159
+ --use_ema
160
+ ```
161
+
162
+
163
+ ### Minute-level Long Video Generation
164
+ Built on [Rolling Forcing](https://github.com/TencentARC/RollingForcing), we implemented minute-level long video generation. See [here](./long_video) for the detail.
165
+
166
+ [Infinity-RoPE](https://github.com/yesiltepe-hidir/infinity-rope), [Deep Forcing](https://cvlab-kaist.github.io/DeepForcing/) and [Rolling Sink](https://github.com/haodong2000/RollingSink) also adopt Causal Forcing as one of their base models, enabling interactive (prompt-switchable) long video generation at the minute scale. You can also try them out at their repos.
167
+
168
+ ## Training
169
+
170
+ <details>
171
+ <summary> Stage 1: Autoregressive Diffusion Training (Can skip by using our pretrained checkpoints. Click to expand.)</summary>
172
+
173
+ First download the dataset (we provide a 6K toy dataset here):
174
+ ```bash
175
+ hf download zhuhz22/Causal-Forcing-data --local-dir dataset
176
+ python utils/merge_and_get_clean.py
177
+ ```
178
+ > If the download gets stuck, Ctrl^C and then resume it.
179
+
180
+
181
+ > For training on your own dataset, refer to [this issue](https://github.com/thu-ml/Causal-Forcing/issues/8).
182
+
183
+
184
+ Then train the AR-diffusion model:
185
+ - Framewise:
186
+ ```bash
187
+ torchrun --nnodes=8 --nproc_per_node=8 --rdzv_id=5235 \
188
+ --rdzv_backend=c10d \
189
+ --rdzv_endpoint $MASTER_ADDR \
190
+ train.py \
191
+ --config_path configs/ar_diffusion_tf_framewise.yaml \
192
+ --logdir logs/ar_diffusion_framewise
193
+ ```
194
+
195
+ - Chunkwise:
196
+ ```bash
197
+ torchrun --nnodes=8 --nproc_per_node=8 --rdzv_id=5235 \
198
+ --rdzv_backend=c10d \
199
+ --rdzv_endpoint $MASTER_ADDR \
200
+ train.py \
201
+ --config_path configs/ar_diffusion_tf_chunkwise.yaml \
202
+ --logdir logs/ar_diffusion_chunkwise
203
+ ```
204
+
205
+ > We recommend training no less than 2K steps, and more steps (e.g., 5~10K) will lead to better performance.
206
+
207
+ Inference to test training results:
208
+ ```bash
209
+ python inference.py \
210
+ --config_path configs/ar_diffusion_tf_{framewise OR chunkwise}.yaml \
211
+ --output_folder output/{framewise OR chunkwise}_ar_diffusion \
212
+ --checkpoint_path checkpoints/{framewise OR chunkwise}/ar_diffusion.pt \
213
+ --data_path prompts/demos.txt
214
+ ```
215
+ </details>
216
+
217
+
218
+ <details>
219
+ <summary> Stage 2 (Option a): Causal ODE Initialization (Can skip by using our pretrained checkpoints. Click to expand.)</summary>
220
+
221
+ 🔥 Thanks to @[AshadowZ](https://github.com/AshadowZ), now our chunk-wise ODE data curation is **3x faster**!
222
+
223
+ You can also use `bf16` to accelerate generation.
224
+
225
+ If you have skipped Stage 1, you need to download the pretrained models:
226
+ ```bash
227
+ hf download zhuhz22/Causal-Forcing framewise/ar_diffusion.pt --local-dir checkpoints
228
+ hf download zhuhz22/Causal-Forcing chunkwise/ar_diffusion.pt --local-dir checkpoints
229
+ ```
230
+
231
+ In this stage, first generate ODE paired data:
232
+ ```bash
233
+ # for the frame-wise model
234
+ torchrun --nproc_per_node=8 \
235
+ get_causal_ode_data_framewise.py \
236
+ --generator_ckpt checkpoints/framewise/ar_diffusion.pt \
237
+ --rawdata_path dataset/clean_data \
238
+ --output_folder dataset/ODE6KCausal_framewise_latents
239
+
240
+ python utils/create_lmdb_iterative.py \
241
+ --data_path dataset/ODE6KCausal_framewise_latents \
242
+ --lmdb_path dataset/ODE6KCausal_framewise
243
+
244
+ # for the chunk-wise model
245
+ torchrun --nproc_per_node=8 \
246
+ get_causal_ode_data_chunkwise.py \
247
+ --generator_ckpt checkpoints/chunkwise/ar_diffusion.pt \
248
+ --rawdata_path dataset/clean_data \
249
+ --output_folder dataset/ODE6KCausal_chunkwise_latents
250
+
251
+ python utils/create_lmdb_iterative.py \
252
+ --data_path dataset/ODE6KCausal_chunkwise_latents \
253
+ --lmdb_path dataset/ODE6KCausal_chunkwise
254
+
255
+ #🔥NEW: Or you can use the optimized code (3x speedup) by @AshadowZ
256
+ torchrun --nproc_per_node=8 \
257
+ get_causal_ode_data_kv.py \
258
+ --generator_ckpt checkpoints/{chunkwise,framewise}/ar_diffusion.pt \
259
+ --rawdata_path /mnt/vepfs/base2/zhaomin/group_0001_lmdb \
260
+ --output_folder dataset/ODE6KCausal_{chunkwise,framewise}_latents \
261
+ --num_frames_per_chunk 3/1 # 3 for chunkwise, 1 for framewise
262
+ --generation_mode blockwise_kv
263
+ ```
264
+
265
+ Or you can also directly download our prepared dataset (~300G):
266
+ ```bash
267
+ hf download zhuhz22/Causal-Forcing-data --local-dir dataset
268
+ python utils/merge_lmdb.py
269
+ ```
270
+ > If the download gets stuck, Ctrl^C and then resume it.
271
+
272
+
273
+ And then train ODE initialization models:
274
+ - Frame-wise:
275
+ ```bash
276
+ torchrun --nnodes=8 --nproc_per_node=8 --rdzv_id=5235 \
277
+ --rdzv_backend=c10d \
278
+ --rdzv_endpoint $MASTER_ADDR \
279
+ train.py \
280
+ --config_path configs/causal_ode_framewise.yaml \
281
+ --logdir logs/causal_ode_framewise
282
+ ```
283
+ - Chunk-wise:
284
+ ```bash
285
+ torchrun --nnodes=8 --nproc_per_node=8 --rdzv_id=5235 \
286
+ --rdzv_backend=c10d \
287
+ --rdzv_endpoint $MASTER_ADDR \
288
+ train.py \
289
+ --config_path configs/causal_ode_chunkwise.yaml \
290
+ --logdir logs/causal_ode_chunkwise
291
+ ```
292
+
293
+ > We recommend training no less than 1K steps, and more steps (e.g., 5~10K) will lead to better performance.
294
+
295
+ Inference to test training results:
296
+
297
+ The same as [here](#cli-inference).
298
+ </details>
299
+
300
+
301
+
302
+ ### 🔥 Stage 2 (Option b): Causal Consistency Distillation Initialization ([Causal Forcing++](https://arxiv.org/abs/2605.15141))
303
+ Since creating ODE-paired data is very time-consuming, we also provide an alternative here that achieves the same effect (or better) as ODE distillation while requiring only ground-truth data, **free of generating ODE data!**
304
+
305
+ > Thanks to [@chijw's effort](https://github.com/thu-ml/Causal-Forcing/pull/20), now the EMA mechanism is more efficient!
306
+
307
+ If you have skipped Stage 1, you need to download the pretrained multi-step AR diffusion models:
308
+ ```bash
309
+ hf download zhuhz22/Causal-Forcing framewise/ar_diffusion.pt --local-dir checkpoints
310
+ hf download zhuhz22/Causal-Forcing chunkwise/ar_diffusion.pt --local-dir checkpoints
311
+ ```
312
+
313
+ - Frame-wise:
314
+ ```bash
315
+ torchrun --nnodes=8 --nproc_per_node=8 --rdzv_id=5235 \
316
+ --rdzv_backend=c10d \
317
+ --rdzv_endpoint $MASTER_ADDR \
318
+ train.py \
319
+ --config_path configs/causal_cd_framewise.yaml \
320
+ --logdir logs/causal_cd_framewise
321
+ ```
322
+ - Chunk-wise:
323
+ ```bash
324
+ torchrun --nnodes=8 --nproc_per_node=8 --rdzv_id=5235 \
325
+ --rdzv_backend=c10d \
326
+ --rdzv_endpoint $MASTER_ADDR \
327
+ train.py \
328
+ --config_path configs/causal_cd_chunkwise.yaml \
329
+ --logdir logs/causal_cd_chunkwise
330
+ ```
331
+
332
+ > We recommend training no less than 3K steps, and more steps (e.g., 5~10K) will lead to better performance.
333
+
334
+ <details>
335
+ <summary>
336
+ You can also download the checkpoints directly (click to expand) :
337
+ </summary>
338
+
339
+ ```bash
340
+ hf download zhuhz22/Causal-Forcing framewise/causal_cd.pt --local-dir checkpoints
341
+ hf download zhuhz22/Causal-Forcing chunkwise/causal_cd.pt --local-dir checkpoints
342
+ ```
343
+ </details>
344
+
345
+ Inference to test training results: the same as [here](#cli-inference).
346
+
347
+
348
+
349
+
350
+ ### Stage 3: DMD
351
+
352
+ First download the dataset:
353
+ ```bash
354
+ hf download gdhe17/Self-Forcing vidprom_filtered_extended.txt --local-dir prompts
355
+ ```
356
+
357
+ <details>
358
+
359
+ <summary>
360
+ If you have skipped Stage 2, you need to download our pretrained checkpoints (click to expand):
361
+ </summary>
362
+
363
+ ```bash
364
+ hf download zhuhz22/Causal-Forcing framewise/causal_ode.pt --local-dir checkpoints
365
+ hf download zhuhz22/Causal-Forcing chunkwise/causal_ode.pt --local-dir checkpoints
366
+
367
+ # 🔥 Or you can also use the improved version (Causal Forcing++): causal CD instead of causal ODE
368
+ hf download zhuhz22/Causal-Forcing framewise/causal_cd.pt --local-dir checkpoints
369
+ hf download zhuhz22/Causal-Forcing chunkwise/causal_cd.pt --local-dir checkpoints
370
+ ```
371
+ </details>
372
+
373
+ And then train DMD models:
374
+
375
+ - Frame-wise model:
376
+ ```bash
377
+ # =============== Causal Forcing ================
378
+ torchrun --nnodes=8 --nproc_per_node=8 --rdzv_id=5235 \
379
+ --rdzv_backend=c10d \
380
+ --rdzv_endpoint $MASTER_ADDR \
381
+ train.py \
382
+ --config_path configs/causal_forcing_dmd_framewise.yaml \
383
+ --logdir logs/causal_forcing_dmd_framewise
384
+ # =============== Causal Forcing++ ================
385
+ # 2-step Causal Forcing++
386
+ torchrun --nnodes=8 --nproc_per_node=8 --rdzv_id=5235 \
387
+ --rdzv_backend=c10d \
388
+ --rdzv_endpoint $MASTER_ADDR \
389
+ train.py \
390
+ --config_path configs/causal_forcing_dmd_framewise_2step.yaml \
391
+ --logdir logs/causal_forcing_dmd_framewise
392
+
393
+ # 1-step Causal Forcing++
394
+ torchrun --nnodes=8 --nproc_per_node=8 --rdzv_id=5235 \
395
+ --rdzv_backend=c10d \
396
+ --rdzv_endpoint $MASTER_ADDR \
397
+ train.py \
398
+ --config_path configs/causal_forcing_dmd_framewise_1step.yaml \
399
+ --logdir logs/causal_forcing_dmd_framewise
400
+ ```
401
+
402
+ - Chunk-wise model:
403
+ ```bash
404
+ torchrun --nnodes=8 --nproc_per_node=8 --rdzv_id=5235 \
405
+ --rdzv_backend=c10d \
406
+ --rdzv_endpoint $MASTER_ADDR \
407
+ train.py \
408
+ --config_path configs/causal_forcing_dmd_chunkwise.yaml \
409
+ --logdir logs/causal_forcing_dmd_chunkwise
410
+ ```
411
+ > For bs64, we recommend training for no more than 1K steps; otherwise the motion dynamics may degrade. If bs is smaller (e.g., 8), more training steps is preferable.
412
+
413
+ Such models are the final models used to generate videos.
414
+ ## FAQ & Blog
415
+ See the [FAQ](https://my.feishu.cn/wiki/AjBSwcjpqiN0ECkodIWcGDcMn4e) and the [blog](https://zhuanlan.zhihu.com/p/2002114039493461457). (currently in Chinese)
416
+
417
+ <details>
418
+ <summary> Typical Questions (click to expand)
419
+ </summary>
420
+
421
+ **Why using bidirectional teacher in the DMD stage ?**
422
+ - Q: In the DMD stage, do you still use a bidirectional teacher? Why not an AR teacher?
423
+ - A: Yes. DMD only requires the student to match the teacher’s final distribution, not the generation trajectory, so a bidirectional teacher is fine. Also, bidirectional diffusion models are typically stronger than AR diffusion, so they make a better teacher.
424
+
425
+ - Q: Then why must the ODE (or Consistency Distillation) stage use an AR teacher?
426
+ - A: Because ODE/CD requires the student and teacher to follow the same trajectory, so their structures must be matched; an AR student cannot be trajectory-aligned with a bidirectional teacher.
427
+
428
+ 🔥🔥 **ODE initialization or multi-step AR diffusion initialization ?**
429
+ - Q: Which is better as initialization: a “proper” ODE initialization or directly using multi-step AR diffusion?
430
+ - A: We compared this in the Appendix C2. Overall, proper ODE init is better: multi-step AR diffusion init + DMD occasionally yields grid-like or waxy/greasy results. A key reason is that DMD is inherently few-step, so the right comparison is under few-step; in that regime, a few-step diffusion teacher is much weaker than an ODE-distilled teacher. Without ODE distillation, DMD must both close the step gap and handle an added conditioning gap from self-rollout: early few-step errors corrupt the history and get amplified across frames (large exposure bias), which increases DMD pressure. It can still converge, but typically with worse quality than ODE initialization. Also, with ODE init, DMD can be trained very few steps (e.g., ~100), reducing the risk of dynamics degradation from long DMD training.
431
+
432
+ **Can frame-level non-injectivity appears in the actual training dataset ?**
433
+ - Q: Regarding the “one-to-many” analysis in the ODE stage: since a single frame’s latent has very high dimensionality, isn’t the probability of being exactly identical extremely small?
434
+ - A: Yes, but the key point here is not whether the dataset literally contains identical samples; it’s whether there exists a well-defined function in the mathematical sense. Our vision modalities live in a continuous space—even in 1D, getting two samples to be exactly identical is extremely unlikely. However, the theoretical existence of exact collisions is enough to break the function property and make it ill-defined.
435
+
436
+ </details>
437
+
438
+ ## Acknowledgements
439
+ - This codebase is built on top of the open-source implementation of [CausVid](https://github.com/tianweiy/CausVid), [Self Forcing](https://github.com/guandeh17/Self-Forcing), [Rolling Forcing](https://github.com/TencentARC/RollingForcing) and the [Wan2.1](https://github.com/Wan-Video/Wan2.1) repo.
440
+ - Thanks to @[chijw](https://github.com/chijw) for improving the EMA mechanism.
441
+ - Thanks to @[AshadowZ](https://github.com/AshadowZ) for improving the causal ODE data curation efficiency.
442
+ - Causal Forcing++ with 1/2 steps applies the first-frame 4-step technique proposed by [ASD](https://github.com/BigAandSmallq/SAD). We thank ASD for its contribution.
443
+
444
+ ## References
445
+ If you find the method useful, please cite
446
+ ```
447
+ @article{zhu2026causal,
448
+ title={Causal Forcing: Autoregressive Diffusion Distillation Done Right for High-Quality Real-Time Interactive Video Generation},
449
+ author={Zhu, Hongzhou and Zhao, Min and He, Guande and Su, Hang and Li, Chongxuan and Zhu, Jun},
450
+ journal={arXiv preprint arXiv:2602.02214},
451
+ year={2026}
452
+ }
453
+
454
+ @article{zhao2026causal,
455
+ title={Causal Forcing++: Scalable Few-Step Autoregressive Diffusion Distillation for Real-Time Interactive Video Generation},
456
+ author={Zhao, Min and Zhu, Hongzhou and Zheng, Kaiwen and Zhou, Zihan and Yan, Bokai and Li, Xinyuan and Yang, Xiao and Li, Chongxuan and Zhu, Jun},
457
+ journal={arXiv preprint arXiv:2605.15141},
458
+ year={2026}
459
+ }
460
+ ```
configs/ar_diffusion_tf_chunkwise.yaml ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator_fsdp_wrap_strategy: size
2
+ real_score_fsdp_wrap_strategy: size
3
+ fake_score_fsdp_wrap_strategy: size
4
+ real_name: Wan2.1-T2V-1.3B
5
+ text_encoder_fsdp_wrap_strategy: size
6
+ warp_denoising_step: true
7
+ ts_schedule: false
8
+ num_train_timestep: 1000
9
+ timestep_shift: 5.0
10
+ guidance_scale: 3.0
11
+ denoising_loss_type: flow
12
+ mixed_precision: true
13
+ seed: 0
14
+ wandb_host: https://api.wandb.ai
15
+ wandb_key: {your key}
16
+ wandb_entity: {your entity}
17
+ wandb_project: {your project}
18
+ sharding_strategy: hybrid_full
19
+ lr: 2.0e-06
20
+ lr_critic: 4.0e-07
21
+ beta1: 0.0
22
+ beta2: 0.999
23
+ beta1_critic: 0.0
24
+ beta2_critic: 0.999
25
+ batch_size: 1
26
+ ema_weight: 0.99
27
+ ema_start_step: 2000000000
28
+ total_batch_size: 8
29
+ log_iters: 1000
30
+ negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
31
+ dfake_gen_update_ratio: 5
32
+ image_or_video_shape:
33
+ - 1
34
+ - 21
35
+ - 16
36
+ - 60
37
+ - 104
38
+ distribution_loss: dmd
39
+ trainer: diffusion
40
+ gradient_checkpointing: true
41
+ num_frame_per_block: 3
42
+ load_raw_video: false
43
+ model_kwargs:
44
+ timestep_shift: 5.0
45
+ data_path: dataset/clean_data
46
+ teacher_forcing: true # TF/DF
configs/ar_diffusion_tf_framewise.yaml ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator_fsdp_wrap_strategy: size
2
+ real_score_fsdp_wrap_strategy: size
3
+ fake_score_fsdp_wrap_strategy: size
4
+ real_name: Wan2.1-T2V-1.3B
5
+ text_encoder_fsdp_wrap_strategy: size
6
+ warp_denoising_step: true
7
+ ts_schedule: false
8
+ num_train_timestep: 1000
9
+ timestep_shift: 5.0
10
+ guidance_scale: 3.0
11
+ denoising_loss_type: flow
12
+ mixed_precision: true
13
+ seed: 0
14
+ wandb_host: https://api.wandb.ai
15
+ wandb_key: {your key}
16
+ wandb_entity: {your entity}
17
+ wandb_project: {your project}
18
+ sharding_strategy: hybrid_full
19
+ lr: 2.0e-06
20
+ lr_critic: 4.0e-07
21
+ beta1: 0.0
22
+ beta2: 0.999
23
+ beta1_critic: 0.0
24
+ beta2_critic: 0.999
25
+ batch_size: 1
26
+ ema_weight: 0.99
27
+ ema_start_step: 2000000000
28
+ total_batch_size: 8
29
+ log_iters: 1000
30
+ negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
31
+ dfake_gen_update_ratio: 5
32
+ image_or_video_shape:
33
+ - 1
34
+ - 21
35
+ - 16
36
+ - 60
37
+ - 104
38
+ distribution_loss: dmd
39
+ trainer: diffusion
40
+ gradient_checkpointing: true
41
+ num_frame_per_block: 1
42
+ load_raw_video: false
43
+ model_kwargs:
44
+ timestep_shift: 5.0
45
+ data_path: dataset/clean_data
46
+ teacher_forcing: true # TF/DF
configs/causal_cd_chunkwise.yaml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator_ckpt: checkpoints/chunkwise/ar_diffusion.pt
2
+ generator_fsdp_wrap_strategy: size
3
+ real_score_fsdp_wrap_strategy: size
4
+ fake_score_fsdp_wrap_strategy: size
5
+ real_name: Wan2.1-T2V-1.3B
6
+ text_encoder_fsdp_wrap_strategy: size
7
+ denoising_step_list:
8
+ - 1000
9
+ - 750
10
+ - 500
11
+ - 250
12
+ warp_denoising_step: true # need to remove - 0 in denoising_step_list if warp_denoising_step is true
13
+ ts_schedule: false
14
+ num_train_timestep: 1000
15
+ timestep_shift: 5.0
16
+ guidance_scale: 3.0
17
+ denoising_loss_type: flow
18
+ mixed_precision: true
19
+ seed: 0
20
+ wandb_host: https://api.wandb.ai
21
+ wandb_key: {your key}
22
+ wandb_entity: {your entity}
23
+ wandb_project: {your project}
24
+ sharding_strategy: hybrid_full
25
+ lr: 2.0e-06
26
+ lr_critic: 4.0e-07
27
+ beta1: 0.0
28
+ beta2: 0.999
29
+ beta1_critic: 0.0
30
+ beta2_critic: 0.999
31
+ batch_size: 1
32
+ ema_weight: 0.99
33
+ ema_start_step: 200
34
+ total_batch_size: 8
35
+ log_iters: 1000
36
+ negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
37
+ dfake_gen_update_ratio: 5
38
+ image_or_video_shape:
39
+ - 1
40
+ - 21
41
+ - 16
42
+ - 60
43
+ - 104
44
+ distribution_loss: dmd
45
+ trainer: consistency_distillation
46
+ gradient_checkpointing: true
47
+ num_frame_per_block: 3
48
+ load_raw_video: false
49
+ model_kwargs:
50
+ timestep_shift: 5.0
51
+ data_path: dataset/clean_data
52
+ grad_accum: 1
53
+ spatial_sf: true
54
+ teacher_forcing: True
55
+ causal: True
56
+ is_causal: True
57
+ discrete_cd_N: 48
configs/causal_cd_framewise.yaml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator_ckpt: checkpoints/framewise/ar_diffusion.pt
2
+ generator_fsdp_wrap_strategy: size
3
+ real_score_fsdp_wrap_strategy: size
4
+ fake_score_fsdp_wrap_strategy: size
5
+ real_name: Wan2.1-T2V-1.3B
6
+ text_encoder_fsdp_wrap_strategy: size
7
+ denoising_step_list:
8
+ - 1000
9
+ - 750
10
+ - 500
11
+ - 250
12
+ warp_denoising_step: true # need to remove - 0 in denoising_step_list if warp_denoising_step is true
13
+ ts_schedule: false
14
+ num_train_timestep: 1000
15
+ timestep_shift: 5.0
16
+ guidance_scale: 3.0
17
+ denoising_loss_type: flow
18
+ mixed_precision: true
19
+ seed: 0
20
+ wandb_host: https://api.wandb.ai
21
+ wandb_key: {your key}
22
+ wandb_entity: {your entity}
23
+ wandb_project: {your project}
24
+ sharding_strategy: hybrid_full
25
+ lr: 2.0e-06
26
+ lr_critic: 4.0e-07
27
+ beta1: 0.0
28
+ beta2: 0.999
29
+ beta1_critic: 0.0
30
+ beta2_critic: 0.999
31
+ batch_size: 1
32
+ ema_weight: 0.99
33
+ ema_start_step: 200
34
+ total_batch_size: 8
35
+ log_iters: 1000
36
+ negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
37
+ dfake_gen_update_ratio: 5
38
+ image_or_video_shape:
39
+ - 1
40
+ - 21
41
+ - 16
42
+ - 60
43
+ - 104
44
+ distribution_loss: dmd
45
+ trainer: consistency_distillation
46
+ gradient_checkpointing: true
47
+ num_frame_per_block: 1
48
+ load_raw_video: false
49
+ model_kwargs:
50
+ timestep_shift: 5.0
51
+ data_path: dataset/clean_data
52
+ grad_accum: 1
53
+ spatial_sf: true
54
+ teacher_forcing: True
55
+ causal: True
56
+ is_causal: True
57
+ discrete_cd_N: 48
configs/causal_forcing_dmd_chunkwise.yaml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator_ckpt: checkpoints/chunkwise/causal_ode.pt # 🔥 or checkpoints/chunkwise/causal_cd.pt
2
+ generator_fsdp_wrap_strategy: size
3
+ real_score_fsdp_wrap_strategy: size
4
+ fake_score_fsdp_wrap_strategy: size
5
+ real_name: Wan2.1-T2V-14B
6
+ text_encoder_fsdp_wrap_strategy: size
7
+ denoising_step_list:
8
+ - 1000
9
+ - 750
10
+ - 500
11
+ - 250
12
+ warp_denoising_step: true # need to remove - 0 in denoising_step_list if warp_denoising_step is true
13
+ ts_schedule: false
14
+ num_train_timestep: 1000
15
+ timestep_shift: 5.0
16
+ guidance_scale: 3.0
17
+ denoising_loss_type: flow
18
+ mixed_precision: true
19
+ seed: 0
20
+ wandb_host: https://api.wandb.ai
21
+ wandb_key: {your key}
22
+ wandb_entity: {your entity}
23
+ wandb_project: {your project}
24
+ sharding_strategy: hybrid_full
25
+ lr: 2.0e-06
26
+ lr_critic: 4.0e-07
27
+ beta1: 0.0
28
+ beta2: 0.999
29
+ beta1_critic: 0.0
30
+ beta2_critic: 0.999
31
+ batch_size: 1
32
+ ema_weight: 0.99
33
+ ema_start_step: 200
34
+ total_batch_size: 8
35
+ log_iters: 250
36
+ negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
37
+ dfake_gen_update_ratio: 5
38
+ image_or_video_shape:
39
+ - 1
40
+ - 21
41
+ - 16
42
+ - 60
43
+ - 104
44
+ distribution_loss: dmd
45
+ trainer: score_distillation
46
+ gradient_checkpointing: true
47
+ num_frame_per_block: 3
48
+ load_raw_video: false
49
+ model_kwargs:
50
+ timestep_shift: 5.0
51
+ data_path: prompts/vidprom_filtered_extended.txt
configs/causal_forcing_dmd_framewise.yaml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator_ckpt: checkpoints/framewise/causal_ode.pt # 🔥 or checkpoints/framewise/causal_cd.pt
2
+ generator_fsdp_wrap_strategy: size
3
+ real_score_fsdp_wrap_strategy: size
4
+ fake_score_fsdp_wrap_strategy: size
5
+ real_name: Wan2.1-T2V-14B
6
+ text_encoder_fsdp_wrap_strategy: size
7
+ denoising_step_list:
8
+ - 1000
9
+ - 750
10
+ - 500
11
+ - 250
12
+ warp_denoising_step: true # need to remove - 0 in denoising_step_list if warp_denoising_step is true
13
+ ts_schedule: false
14
+ num_train_timestep: 1000
15
+ timestep_shift: 5.0
16
+ guidance_scale: 3.0
17
+ denoising_loss_type: flow
18
+ mixed_precision: true
19
+ seed: 0
20
+ wandb_host: https://api.wandb.ai
21
+ wandb_key: {your key}
22
+ wandb_entity: {your entity}
23
+ wandb_project: {your project}
24
+ sharding_strategy: hybrid_full
25
+ lr: 2.0e-06
26
+ lr_critic: 4.0e-07
27
+ beta1: 0.0
28
+ beta2: 0.999
29
+ beta1_critic: 0.0
30
+ beta2_critic: 0.999
31
+ batch_size: 1
32
+ ema_weight: 0.99
33
+ ema_start_step: 200
34
+ total_batch_size: 8
35
+ log_iters: 250
36
+ negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
37
+ dfake_gen_update_ratio: 5
38
+ image_or_video_shape:
39
+ - 1
40
+ - 21
41
+ - 16
42
+ - 60
43
+ - 104
44
+ distribution_loss: dmd
45
+ trainer: score_distillation
46
+ gradient_checkpointing: true
47
+ num_frame_per_block: 1
48
+ load_raw_video: false
49
+ model_kwargs:
50
+ timestep_shift: 5.0
51
+ data_path: prompts/vidprom_filtered_extended.txt
configs/causal_forcing_dmd_framewise_1step.yaml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator_ckpt: checkpoints/framewise/causal_ode.pt # 🔥 or checkpoints/framewise/causal_cd.pt
2
+ generator_fsdp_wrap_strategy: size
3
+ real_score_fsdp_wrap_strategy: size
4
+ fake_score_fsdp_wrap_strategy: size
5
+ real_name: Wan2.1-T2V-14B
6
+ text_encoder_fsdp_wrap_strategy: size
7
+ denoising_step_list:
8
+ - 1000
9
+ denoising_step_list_first_chunk: # Causal Forcing++ with 1/2 steps applies the first-frame 4-step technique proposed by [ASD](https://github.com/BigAandSmallq/SAD). We thank ASD for its contribution.
10
+ - 1000
11
+ - 750
12
+ - 500
13
+ - 250
14
+ warp_denoising_step: true # need to remove - 0 in denoising_step_list if warp_denoising_step is true
15
+ ts_schedule: false
16
+ num_train_timestep: 1000
17
+ timestep_shift: 5.0
18
+ guidance_scale: 3.0
19
+ denoising_loss_type: flow
20
+ mixed_precision: true
21
+ seed: 0
22
+ wandb_host: https://api.wandb.ai
23
+ wandb_key: {your key}
24
+ wandb_entity: {your entity}
25
+ wandb_project: {your project}
26
+ sharding_strategy: hybrid_full
27
+ lr: 2.0e-06
28
+ lr_critic: 4.0e-07
29
+ beta1: 0.0
30
+ beta2: 0.999
31
+ beta1_critic: 0.0
32
+ beta2_critic: 0.999
33
+ batch_size: 1
34
+ ema_weight: 0.99
35
+ ema_start_step: 200
36
+ total_batch_size: 8
37
+ log_iters: 250
38
+ negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
39
+ dfake_gen_update_ratio: 5
40
+ image_or_video_shape:
41
+ - 1
42
+ - 21
43
+ - 16
44
+ - 60
45
+ - 104
46
+ distribution_loss: dmd
47
+ trainer: score_distillation
48
+ gradient_checkpointing: true
49
+ num_frame_per_block: 1
50
+ load_raw_video: false
51
+ model_kwargs:
52
+ timestep_shift: 5.0
53
+ data_path: prompts/vidprom_filtered_extended.txt
configs/causal_forcing_dmd_framewise_2step.yaml ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator_ckpt: checkpoints/framewise/causal_ode.pt # 🔥 or checkpoints/framewise/causal_cd.pt
2
+ generator_fsdp_wrap_strategy: size
3
+ real_score_fsdp_wrap_strategy: size
4
+ fake_score_fsdp_wrap_strategy: size
5
+ real_name: Wan2.1-T2V-14B
6
+ text_encoder_fsdp_wrap_strategy: size
7
+ denoising_step_list:
8
+ - 1000
9
+ - 500
10
+ denoising_step_list_first_chunk: # Causal Forcing++ with 1/2 steps applies the first-frame 4-step technique proposed by [ASD](https://github.com/BigAandSmallq/SAD). We thank ASD for its contribution.
11
+ - 1000
12
+ - 750
13
+ - 500
14
+ - 250
15
+ warp_denoising_step: true # need to remove - 0 in denoising_step_list if warp_denoising_step is true
16
+ ts_schedule: false
17
+ num_train_timestep: 1000
18
+ timestep_shift: 5.0
19
+ guidance_scale: 3.0
20
+ denoising_loss_type: flow
21
+ mixed_precision: true
22
+ seed: 0
23
+ wandb_host: https://api.wandb.ai
24
+ wandb_key: {your key}
25
+ wandb_entity: {your entity}
26
+ wandb_project: {your project}
27
+ sharding_strategy: hybrid_full
28
+ lr: 2.0e-06
29
+ lr_critic: 4.0e-07
30
+ beta1: 0.0
31
+ beta2: 0.999
32
+ beta1_critic: 0.0
33
+ beta2_critic: 0.999
34
+ batch_size: 1
35
+ ema_weight: 0.99
36
+ ema_start_step: 200
37
+ total_batch_size: 8
38
+ log_iters: 250
39
+ negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
40
+ dfake_gen_update_ratio: 5
41
+ image_or_video_shape:
42
+ - 1
43
+ - 21
44
+ - 16
45
+ - 60
46
+ - 104
47
+ distribution_loss: dmd
48
+ trainer: score_distillation
49
+ gradient_checkpointing: true
50
+ num_frame_per_block: 1
51
+ load_raw_video: false
52
+ model_kwargs:
53
+ timestep_shift: 5.0
54
+ data_path: prompts/vidprom_filtered_extended.txt
configs/causal_ode_chunkwise.yaml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator_ckpt: checkpoints/chunkwise/ar_diffusion.pt
2
+ generator_grad:
3
+ model: true
4
+ denoising_step_list:
5
+ - 1000
6
+ - 750
7
+ - 500
8
+ - 250
9
+ warp_denoising_step: true
10
+ generator_task: causal_video
11
+ generator_fsdp_wrap_strategy: size
12
+ text_encoder_fsdp_wrap_strategy: size
13
+ mixed_precision: true
14
+ seed: 0
15
+ wandb_host: https://api.wandb.ai
16
+ wandb_key: {your key}
17
+ wandb_entity: {your entity}
18
+ wandb_project: {your project}
19
+ wandb_name: ode
20
+ sharding_strategy: hybrid_full
21
+ lr: 2.0e-06
22
+ beta1: 0.9
23
+ beta2: 0.999
24
+ data_path: dataset/ODE6KCausal_chunkwise
25
+ batch_size: 1
26
+ log_iters: 1000
27
+ trainer: ode
28
+ gradient_checkpointing: true
29
+ num_frame_per_block: 3
30
+ model_kwargs:
31
+ timestep_shift: 5.0
configs/causal_ode_framewise.yaml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator_ckpt: checkpoints/framewise/ar_diffusion.pt
2
+ generator_grad:
3
+ model: true
4
+ denoising_step_list:
5
+ - 1000
6
+ - 750
7
+ - 500
8
+ - 250
9
+ warp_denoising_step: true
10
+ generator_task: causal_video
11
+ generator_fsdp_wrap_strategy: size
12
+ text_encoder_fsdp_wrap_strategy: size
13
+ mixed_precision: true
14
+ seed: 0
15
+ wandb_host: https://api.wandb.ai
16
+ wandb_key: {your key}
17
+ wandb_entity: {your entity}
18
+ wandb_project: {your project}
19
+ wandb_name: ode
20
+ sharding_strategy: hybrid_full
21
+ lr: 2.0e-06
22
+ beta1: 0.9
23
+ beta2: 0.999
24
+ data_path: dataset/ODE6KCausal_framewise
25
+ batch_size: 1
26
+ log_iters: 1000
27
+ trainer: ode
28
+ gradient_checkpointing: true
29
+ num_frame_per_block: 1
30
+ model_kwargs:
31
+ timestep_shift: 5.0
configs/default_config.yaml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ independent_first_frame: false
2
+ warp_denoising_step: false
3
+ weight_decay: 0.01
4
+ same_step_across_blocks: true
5
+ discriminator_lr_multiplier: 1.0
6
+ last_step_only: false
7
+ i2v: false
8
+ num_training_frames: 21
9
+ gc_interval: 100
10
+ context_noise: 0
11
+ causal: true
12
+
13
+ ckpt_step: 0
14
+ prompt_name: MovieGenVideoBench
15
+ prompt_path: prompts/MovieGenVideoBench.txt
16
+ eval_first_n: 64
17
+ num_samples: 1
18
+ height: 480
19
+ width: 832
20
+ num_frames: 81
demo.py ADDED
@@ -0,0 +1,631 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Demo for Causal-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/framewise/causal_forcing.pt')
36
+ parser.add_argument("--config_path", type=str, default='.configs/causal_forcing_dmd_framewise.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)
demo_utils/constant.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import torch
3
+
4
+
5
+ ZERO_VAE_CACHE = [
6
+ torch.zeros(1, 16, 2, 60, 104),
7
+ torch.zeros(1, 384, 2, 60, 104),
8
+ torch.zeros(1, 384, 2, 60, 104),
9
+ torch.zeros(1, 384, 2, 60, 104),
10
+ torch.zeros(1, 384, 2, 60, 104),
11
+ torch.zeros(1, 384, 2, 60, 104),
12
+ torch.zeros(1, 384, 2, 60, 104),
13
+ torch.zeros(1, 384, 2, 60, 104),
14
+ torch.zeros(1, 384, 2, 60, 104),
15
+ torch.zeros(1, 384, 2, 60, 104),
16
+ torch.zeros(1, 384, 2, 60, 104),
17
+ torch.zeros(1, 384, 2, 60, 104),
18
+ torch.zeros(1, 192, 2, 120, 208),
19
+ torch.zeros(1, 384, 2, 120, 208),
20
+ torch.zeros(1, 384, 2, 120, 208),
21
+ torch.zeros(1, 384, 2, 120, 208),
22
+ torch.zeros(1, 384, 2, 120, 208),
23
+ torch.zeros(1, 384, 2, 120, 208),
24
+ torch.zeros(1, 384, 2, 120, 208),
25
+ torch.zeros(1, 192, 2, 240, 416),
26
+ torch.zeros(1, 192, 2, 240, 416),
27
+ torch.zeros(1, 192, 2, 240, 416),
28
+ torch.zeros(1, 192, 2, 240, 416),
29
+ torch.zeros(1, 192, 2, 240, 416),
30
+ torch.zeros(1, 192, 2, 240, 416),
31
+ torch.zeros(1, 96, 2, 480, 832),
32
+ torch.zeros(1, 96, 2, 480, 832),
33
+ torch.zeros(1, 96, 2, 480, 832),
34
+ torch.zeros(1, 96, 2, 480, 832),
35
+ torch.zeros(1, 96, 2, 480, 832),
36
+ torch.zeros(1, 96, 2, 480, 832),
37
+ torch.zeros(1, 96, 2, 480, 832)
38
+ ]
39
+
40
+ feat_names = [f"vae_cache_{i}" for i in range(len(ZERO_VAE_CACHE))]
41
+ ALL_INPUTS_NAMES = ["z", "use_cache"] + feat_names
demo_utils/memory.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copied from https://github.com/lllyasviel/FramePack/tree/main/demo_utils
2
+ # Apache-2.0 License
3
+ # By lllyasviel
4
+
5
+ import torch
6
+
7
+
8
+ cpu = torch.device('cpu')
9
+ gpu = torch.device(f'cuda:{torch.cuda.current_device()}')
10
+ gpu_complete_modules = []
11
+
12
+
13
+ class DynamicSwapInstaller:
14
+ @staticmethod
15
+ def _install_module(module: torch.nn.Module, **kwargs):
16
+ original_class = module.__class__
17
+ module.__dict__['forge_backup_original_class'] = original_class
18
+
19
+ def hacked_get_attr(self, name: str):
20
+ if '_parameters' in self.__dict__:
21
+ _parameters = self.__dict__['_parameters']
22
+ if name in _parameters:
23
+ p = _parameters[name]
24
+ if p is None:
25
+ return None
26
+ if p.__class__ == torch.nn.Parameter:
27
+ return torch.nn.Parameter(p.to(**kwargs), requires_grad=p.requires_grad)
28
+ else:
29
+ return p.to(**kwargs)
30
+ if '_buffers' in self.__dict__:
31
+ _buffers = self.__dict__['_buffers']
32
+ if name in _buffers:
33
+ return _buffers[name].to(**kwargs)
34
+ return super(original_class, self).__getattr__(name)
35
+
36
+ module.__class__ = type('DynamicSwap_' + original_class.__name__, (original_class,), {
37
+ '__getattr__': hacked_get_attr,
38
+ })
39
+
40
+ return
41
+
42
+ @staticmethod
43
+ def _uninstall_module(module: torch.nn.Module):
44
+ if 'forge_backup_original_class' in module.__dict__:
45
+ module.__class__ = module.__dict__.pop('forge_backup_original_class')
46
+ return
47
+
48
+ @staticmethod
49
+ def install_model(model: torch.nn.Module, **kwargs):
50
+ for m in model.modules():
51
+ DynamicSwapInstaller._install_module(m, **kwargs)
52
+ return
53
+
54
+ @staticmethod
55
+ def uninstall_model(model: torch.nn.Module):
56
+ for m in model.modules():
57
+ DynamicSwapInstaller._uninstall_module(m)
58
+ return
59
+
60
+
61
+ def fake_diffusers_current_device(model: torch.nn.Module, target_device: torch.device):
62
+ if hasattr(model, 'scale_shift_table'):
63
+ model.scale_shift_table.data = model.scale_shift_table.data.to(target_device)
64
+ return
65
+
66
+ for k, p in model.named_modules():
67
+ if hasattr(p, 'weight'):
68
+ p.to(target_device)
69
+ return
70
+
71
+
72
+ def get_cuda_free_memory_gb(device=None):
73
+ if device is None:
74
+ device = gpu
75
+
76
+ memory_stats = torch.cuda.memory_stats(device)
77
+ bytes_active = memory_stats['active_bytes.all.current']
78
+ bytes_reserved = memory_stats['reserved_bytes.all.current']
79
+ bytes_free_cuda, _ = torch.cuda.mem_get_info(device)
80
+ bytes_inactive_reserved = bytes_reserved - bytes_active
81
+ bytes_total_available = bytes_free_cuda + bytes_inactive_reserved
82
+ return bytes_total_available / (1024 ** 3)
83
+
84
+
85
+ def move_model_to_device_with_memory_preservation(model, target_device, preserved_memory_gb=0):
86
+ print(f'Moving {model.__class__.__name__} to {target_device} with preserved memory: {preserved_memory_gb} GB')
87
+
88
+ for m in model.modules():
89
+ if get_cuda_free_memory_gb(target_device) <= preserved_memory_gb:
90
+ torch.cuda.empty_cache()
91
+ return
92
+
93
+ if hasattr(m, 'weight'):
94
+ m.to(device=target_device)
95
+
96
+ model.to(device=target_device)
97
+ torch.cuda.empty_cache()
98
+ return
99
+
100
+
101
+ def offload_model_from_device_for_memory_preservation(model, target_device, preserved_memory_gb=0):
102
+ print(f'Offloading {model.__class__.__name__} from {target_device} to preserve memory: {preserved_memory_gb} GB')
103
+
104
+ for m in model.modules():
105
+ if get_cuda_free_memory_gb(target_device) >= preserved_memory_gb:
106
+ torch.cuda.empty_cache()
107
+ return
108
+
109
+ if hasattr(m, 'weight'):
110
+ m.to(device=cpu)
111
+
112
+ model.to(device=cpu)
113
+ torch.cuda.empty_cache()
114
+ return
115
+
116
+
117
+ def unload_complete_models(*args):
118
+ for m in gpu_complete_modules + list(args):
119
+ m.to(device=cpu)
120
+ print(f'Unloaded {m.__class__.__name__} as complete.')
121
+
122
+ gpu_complete_modules.clear()
123
+ torch.cuda.empty_cache()
124
+ return
125
+
126
+
127
+ def load_model_as_complete(model, target_device, unload=True):
128
+ if unload:
129
+ unload_complete_models()
130
+
131
+ model.to(device=target_device)
132
+ print(f'Loaded {model.__class__.__name__} to {target_device} as complete.')
133
+
134
+ gpu_complete_modules.append(model)
135
+ return
demo_utils/taehv.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tiny AutoEncoder for Hunyuan Video
4
+ (DNN for encoding / decoding videos to Hunyuan Video's latent space)
5
+ """
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.nn.functional as F
9
+ from tqdm.auto import tqdm
10
+ from collections import namedtuple
11
+
12
+ DecoderResult = namedtuple("DecoderResult", ("frame", "memory"))
13
+ TWorkItem = namedtuple("TWorkItem", ("input_tensor", "block_index"))
14
+
15
+
16
+ def conv(n_in, n_out, **kwargs):
17
+ return nn.Conv2d(n_in, n_out, 3, padding=1, **kwargs)
18
+
19
+
20
+ class Clamp(nn.Module):
21
+ def forward(self, x):
22
+ return torch.tanh(x / 3) * 3
23
+
24
+
25
+ class MemBlock(nn.Module):
26
+ def __init__(self, n_in, n_out):
27
+ super().__init__()
28
+ self.conv = nn.Sequential(conv(n_in * 2, n_out), nn.ReLU(inplace=True),
29
+ conv(n_out, n_out), nn.ReLU(inplace=True), conv(n_out, n_out))
30
+ self.skip = nn.Conv2d(n_in, n_out, 1, bias=False) if n_in != n_out else nn.Identity()
31
+ self.act = nn.ReLU(inplace=True)
32
+
33
+ def forward(self, x, past):
34
+ return self.act(self.conv(torch.cat([x, past], 1)) + self.skip(x))
35
+
36
+
37
+ class TPool(nn.Module):
38
+ def __init__(self, n_f, stride):
39
+ super().__init__()
40
+ self.stride = stride
41
+ self.conv = nn.Conv2d(n_f * stride, n_f, 1, bias=False)
42
+
43
+ def forward(self, x):
44
+ _NT, C, H, W = x.shape
45
+ return self.conv(x.reshape(-1, self.stride * C, H, W))
46
+
47
+
48
+ class TGrow(nn.Module):
49
+ def __init__(self, n_f, stride):
50
+ super().__init__()
51
+ self.stride = stride
52
+ self.conv = nn.Conv2d(n_f, n_f * stride, 1, bias=False)
53
+
54
+ def forward(self, x):
55
+ _NT, C, H, W = x.shape
56
+ x = self.conv(x)
57
+ return x.reshape(-1, C, H, W)
58
+
59
+
60
+ def apply_model_with_memblocks(model, x, parallel, show_progress_bar):
61
+ """
62
+ Apply a sequential model with memblocks to the given input.
63
+ Args:
64
+ - model: nn.Sequential of blocks to apply
65
+ - x: input data, of dimensions NTCHW
66
+ - parallel: if True, parallelize over timesteps (fast but uses O(T) memory)
67
+ if False, each timestep will be processed sequentially (slow but uses O(1) memory)
68
+ - show_progress_bar: if True, enables tqdm progressbar display
69
+
70
+ Returns NTCHW tensor of output data.
71
+ """
72
+ assert x.ndim == 5, f"TAEHV operates on NTCHW tensors, but got {x.ndim}-dim tensor"
73
+ N, T, C, H, W = x.shape
74
+ if parallel:
75
+ x = x.reshape(N * T, C, H, W)
76
+ # parallel over input timesteps, iterate over blocks
77
+ for b in tqdm(model, disable=not show_progress_bar):
78
+ if isinstance(b, MemBlock):
79
+ NT, C, H, W = x.shape
80
+ T = NT // N
81
+ _x = x.reshape(N, T, C, H, W)
82
+ mem = F.pad(_x, (0, 0, 0, 0, 0, 0, 1, 0), value=0)[:, :T].reshape(x.shape)
83
+ x = b(x, mem)
84
+ else:
85
+ x = b(x)
86
+ NT, C, H, W = x.shape
87
+ T = NT // N
88
+ x = x.view(N, T, C, H, W)
89
+ else:
90
+ # TODO(oboerbohan): at least on macos this still gradually uses more memory during decode...
91
+ # need to fix :(
92
+ out = []
93
+ # iterate over input timesteps and also iterate over blocks.
94
+ # because of the cursed TPool/TGrow blocks, this is not a nested loop,
95
+ # it's actually a ***graph traversal*** problem! so let's make a queue
96
+ work_queue = [TWorkItem(xt, 0) for t, xt in enumerate(x.reshape(N, T * C, H, W).chunk(T, dim=1))]
97
+ # in addition to manually managing our queue, we also need to manually manage our progressbar.
98
+ # we'll update it for every source node that we consume.
99
+ progress_bar = tqdm(range(T), disable=not show_progress_bar)
100
+ # we'll also need a separate addressable memory per node as well
101
+ mem = [None] * len(model)
102
+ while work_queue:
103
+ xt, i = work_queue.pop(0)
104
+ if i == 0:
105
+ # new source node consumed
106
+ progress_bar.update(1)
107
+ if i == len(model):
108
+ # reached end of the graph, append result to output list
109
+ out.append(xt)
110
+ else:
111
+ # fetch the block to process
112
+ b = model[i]
113
+ if isinstance(b, MemBlock):
114
+ # mem blocks are simple since we're visiting the graph in causal order
115
+ if mem[i] is None:
116
+ xt_new = b(xt, xt * 0)
117
+ mem[i] = xt
118
+ else:
119
+ xt_new = b(xt, mem[i])
120
+ mem[i].copy_(xt) # inplace might reduce mysterious pytorch memory allocations? doesn't help though
121
+ # add successor to work queue
122
+ work_queue.insert(0, TWorkItem(xt_new, i + 1))
123
+ elif isinstance(b, TPool):
124
+ # pool blocks are miserable
125
+ if mem[i] is None:
126
+ mem[i] = [] # pool memory is itself a queue of inputs to pool
127
+ mem[i].append(xt)
128
+ if len(mem[i]) > b.stride:
129
+ # pool mem is in invalid state, we should have pooled before this
130
+ raise ValueError("???")
131
+ elif len(mem[i]) < b.stride:
132
+ # pool mem is not yet full, go back to processing the work queue
133
+ pass
134
+ else:
135
+ # pool mem is ready, run the pool block
136
+ N, C, H, W = xt.shape
137
+ xt = b(torch.cat(mem[i], 1).view(N * b.stride, C, H, W))
138
+ # reset the pool mem
139
+ mem[i] = []
140
+ # add successor to work queue
141
+ work_queue.insert(0, TWorkItem(xt, i + 1))
142
+ elif isinstance(b, TGrow):
143
+ xt = b(xt)
144
+ NT, C, H, W = xt.shape
145
+ # each tgrow has multiple successor nodes
146
+ for xt_next in reversed(xt.view(N, b.stride * C, H, W).chunk(b.stride, 1)):
147
+ # add successor to work queue
148
+ work_queue.insert(0, TWorkItem(xt_next, i + 1))
149
+ else:
150
+ # normal block with no funny business
151
+ xt = b(xt)
152
+ # add successor to work queue
153
+ work_queue.insert(0, TWorkItem(xt, i + 1))
154
+ progress_bar.close()
155
+ x = torch.stack(out, 1)
156
+ return x
157
+
158
+
159
+ class TAEHV(nn.Module):
160
+ latent_channels = 16
161
+ image_channels = 3
162
+
163
+ def __init__(self, checkpoint_path="taehv.pth", decoder_time_upscale=(True, True), decoder_space_upscale=(True, True, True)):
164
+ """Initialize pretrained TAEHV from the given checkpoint.
165
+
166
+ Arg:
167
+ checkpoint_path: path to weight file to load. taehv.pth for Hunyuan, taew2_1.pth for Wan 2.1.
168
+ decoder_time_upscale: whether temporal upsampling is enabled for each block. upsampling can be disabled for a cheaper preview.
169
+ decoder_space_upscale: whether spatial upsampling is enabled for each block. upsampling can be disabled for a cheaper preview.
170
+ """
171
+ super().__init__()
172
+ self.encoder = nn.Sequential(
173
+ conv(TAEHV.image_channels, 64), nn.ReLU(inplace=True),
174
+ TPool(64, 2), conv(64, 64, stride=2, bias=False), MemBlock(64, 64), MemBlock(64, 64), MemBlock(64, 64),
175
+ TPool(64, 2), conv(64, 64, stride=2, bias=False), MemBlock(64, 64), MemBlock(64, 64), MemBlock(64, 64),
176
+ TPool(64, 1), conv(64, 64, stride=2, bias=False), MemBlock(64, 64), MemBlock(64, 64), MemBlock(64, 64),
177
+ conv(64, TAEHV.latent_channels),
178
+ )
179
+ n_f = [256, 128, 64, 64]
180
+ self.frames_to_trim = 2**sum(decoder_time_upscale) - 1
181
+ self.decoder = nn.Sequential(
182
+ Clamp(), conv(TAEHV.latent_channels, n_f[0]), nn.ReLU(inplace=True),
183
+ MemBlock(n_f[0], n_f[0]), MemBlock(n_f[0], n_f[0]), MemBlock(n_f[0], n_f[0]), nn.Upsample(
184
+ scale_factor=2 if decoder_space_upscale[0] else 1), TGrow(n_f[0], 1), conv(n_f[0], n_f[1], bias=False),
185
+ MemBlock(n_f[1], n_f[1]), MemBlock(n_f[1], n_f[1]), MemBlock(n_f[1], n_f[1]), nn.Upsample(
186
+ scale_factor=2 if decoder_space_upscale[1] else 1), TGrow(n_f[1], 2 if decoder_time_upscale[0] else 1), conv(n_f[1], n_f[2], bias=False),
187
+ MemBlock(n_f[2], n_f[2]), MemBlock(n_f[2], n_f[2]), MemBlock(n_f[2], n_f[2]), nn.Upsample(
188
+ scale_factor=2 if decoder_space_upscale[2] else 1), TGrow(n_f[2], 2 if decoder_time_upscale[1] else 1), conv(n_f[2], n_f[3], bias=False),
189
+ nn.ReLU(inplace=True), conv(n_f[3], TAEHV.image_channels),
190
+ )
191
+ if checkpoint_path is not None:
192
+ self.load_state_dict(self.patch_tgrow_layers(torch.load(
193
+ checkpoint_path, map_location="cpu", weights_only=True)))
194
+
195
+ def patch_tgrow_layers(self, sd):
196
+ """Patch TGrow layers to use a smaller kernel if needed.
197
+
198
+ Args:
199
+ sd: state dict to patch
200
+ """
201
+ new_sd = self.state_dict()
202
+ for i, layer in enumerate(self.decoder):
203
+ if isinstance(layer, TGrow):
204
+ key = f"decoder.{i}.conv.weight"
205
+ if sd[key].shape[0] > new_sd[key].shape[0]:
206
+ # take the last-timestep output channels
207
+ sd[key] = sd[key][-new_sd[key].shape[0]:]
208
+ return sd
209
+
210
+ def encode_video(self, x, parallel=True, show_progress_bar=True):
211
+ """Encode a sequence of frames.
212
+
213
+ Args:
214
+ x: input NTCHW RGB (C=3) tensor with values in [0, 1].
215
+ parallel: if True, all frames will be processed at once.
216
+ (this is faster but may require more memory).
217
+ if False, frames will be processed sequentially.
218
+ Returns NTCHW latent tensor with ~Gaussian values.
219
+ """
220
+ return apply_model_with_memblocks(self.encoder, x, parallel, show_progress_bar)
221
+
222
+ def decode_video(self, x, parallel=True, show_progress_bar=False):
223
+ """Decode a sequence of frames.
224
+
225
+ Args:
226
+ x: input NTCHW latent (C=12) tensor with ~Gaussian values.
227
+ parallel: if True, all frames will be processed at once.
228
+ (this is faster but may require more memory).
229
+ if False, frames will be processed sequentially.
230
+ Returns NTCHW RGB tensor with ~[0, 1] values.
231
+ """
232
+ x = apply_model_with_memblocks(self.decoder, x, parallel, show_progress_bar)
233
+ # return x[:, self.frames_to_trim:]
234
+ return x
235
+
236
+ def forward(self, x):
237
+ return self.c(x)
238
+
239
+
240
+ @torch.no_grad()
241
+ def main():
242
+ """Run TAEHV roundtrip reconstruction on the given video paths."""
243
+ import os
244
+ import sys
245
+ import cv2 # no highly esteemed deed is commemorated here
246
+
247
+ class VideoTensorReader:
248
+ def __init__(self, video_file_path):
249
+ self.cap = cv2.VideoCapture(video_file_path)
250
+ assert self.cap.isOpened(), f"Could not load {video_file_path}"
251
+ self.fps = self.cap.get(cv2.CAP_PROP_FPS)
252
+
253
+ def __iter__(self):
254
+ return self
255
+
256
+ def __next__(self):
257
+ ret, frame = self.cap.read()
258
+ if not ret:
259
+ self.cap.release()
260
+ raise StopIteration # End of video or error
261
+ return torch.from_numpy(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)).permute(2, 0, 1) # BGR HWC -> RGB CHW
262
+
263
+ class VideoTensorWriter:
264
+ def __init__(self, video_file_path, width_height, fps=30):
265
+ self.writer = cv2.VideoWriter(video_file_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, width_height)
266
+ assert self.writer.isOpened(), f"Could not create writer for {video_file_path}"
267
+
268
+ def write(self, frame_tensor):
269
+ assert frame_tensor.ndim == 3 and frame_tensor.shape[0] == 3, f"{frame_tensor.shape}??"
270
+ self.writer.write(cv2.cvtColor(frame_tensor.permute(1, 2, 0).numpy(),
271
+ cv2.COLOR_RGB2BGR)) # RGB CHW -> BGR HWC
272
+
273
+ def __del__(self):
274
+ if hasattr(self, 'writer'):
275
+ self.writer.release()
276
+
277
+ dev = torch.device("cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu")
278
+ dtype = torch.float16
279
+ checkpoint_path = os.getenv("TAEHV_CHECKPOINT_PATH", "taehv.pth")
280
+ checkpoint_name = os.path.splitext(os.path.basename(checkpoint_path))[0]
281
+ print(
282
+ f"Using device \033[31m{dev}\033[0m, dtype \033[32m{dtype}\033[0m, checkpoint \033[34m{checkpoint_name}\033[0m ({checkpoint_path})")
283
+ taehv = TAEHV(checkpoint_path=checkpoint_path).to(dev, dtype)
284
+ for video_path in sys.argv[1:]:
285
+ print(f"Processing {video_path}...")
286
+ video_in = VideoTensorReader(video_path)
287
+ video = torch.stack(list(video_in), 0)[None]
288
+ vid_dev = video.to(dev, dtype).div_(255.0)
289
+ # convert to device tensor
290
+ if video.numel() < 100_000_000:
291
+ print(f" {video_path} seems small enough, will process all frames in parallel")
292
+ # convert to device tensor
293
+ vid_enc = taehv.encode_video(vid_dev)
294
+ print(f" Encoded {video_path} -> {vid_enc.shape}. Decoding...")
295
+ vid_dec = taehv.decode_video(vid_enc)
296
+ print(f" Decoded {video_path} -> {vid_dec.shape}")
297
+ else:
298
+ print(f" {video_path} seems large, will process each frame sequentially")
299
+ # convert to device tensor
300
+ vid_enc = taehv.encode_video(vid_dev, parallel=False)
301
+ print(f" Encoded {video_path} -> {vid_enc.shape}. Decoding...")
302
+ vid_dec = taehv.decode_video(vid_enc, parallel=False)
303
+ print(f" Decoded {video_path} -> {vid_dec.shape}")
304
+ video_out_path = video_path + f".reconstructed_by_{checkpoint_name}.mp4"
305
+ video_out = VideoTensorWriter(
306
+ video_out_path, (vid_dec.shape[-1], vid_dec.shape[-2]), fps=int(round(video_in.fps)))
307
+ for frame in vid_dec.clamp_(0, 1).mul_(255).round_().byte().cpu()[0]:
308
+ video_out.write(frame)
309
+ print(f" Saved to {video_out_path}")
310
+
311
+
312
+ if __name__ == "__main__":
313
+ main()
demo_utils/utils.py ADDED
@@ -0,0 +1,616 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copied from https://github.com/lllyasviel/FramePack/tree/main/demo_utils
2
+ # Apache-2.0 License
3
+ # By lllyasviel
4
+
5
+ import os
6
+ import cv2
7
+ import json
8
+ import random
9
+ import glob
10
+ import torch
11
+ import einops
12
+ import numpy as np
13
+ import datetime
14
+ import torchvision
15
+
16
+ from PIL import Image
17
+
18
+
19
+ def min_resize(x, m):
20
+ if x.shape[0] < x.shape[1]:
21
+ s0 = m
22
+ s1 = int(float(m) / float(x.shape[0]) * float(x.shape[1]))
23
+ else:
24
+ s0 = int(float(m) / float(x.shape[1]) * float(x.shape[0]))
25
+ s1 = m
26
+ new_max = max(s1, s0)
27
+ raw_max = max(x.shape[0], x.shape[1])
28
+ if new_max < raw_max:
29
+ interpolation = cv2.INTER_AREA
30
+ else:
31
+ interpolation = cv2.INTER_LANCZOS4
32
+ y = cv2.resize(x, (s1, s0), interpolation=interpolation)
33
+ return y
34
+
35
+
36
+ def d_resize(x, y):
37
+ H, W, C = y.shape
38
+ new_min = min(H, W)
39
+ raw_min = min(x.shape[0], x.shape[1])
40
+ if new_min < raw_min:
41
+ interpolation = cv2.INTER_AREA
42
+ else:
43
+ interpolation = cv2.INTER_LANCZOS4
44
+ y = cv2.resize(x, (W, H), interpolation=interpolation)
45
+ return y
46
+
47
+
48
+ def resize_and_center_crop(image, target_width, target_height):
49
+ if target_height == image.shape[0] and target_width == image.shape[1]:
50
+ return image
51
+
52
+ pil_image = Image.fromarray(image)
53
+ original_width, original_height = pil_image.size
54
+ scale_factor = max(target_width / original_width, target_height / original_height)
55
+ resized_width = int(round(original_width * scale_factor))
56
+ resized_height = int(round(original_height * scale_factor))
57
+ resized_image = pil_image.resize((resized_width, resized_height), Image.LANCZOS)
58
+ left = (resized_width - target_width) / 2
59
+ top = (resized_height - target_height) / 2
60
+ right = (resized_width + target_width) / 2
61
+ bottom = (resized_height + target_height) / 2
62
+ cropped_image = resized_image.crop((left, top, right, bottom))
63
+ return np.array(cropped_image)
64
+
65
+
66
+ def resize_and_center_crop_pytorch(image, target_width, target_height):
67
+ B, C, H, W = image.shape
68
+
69
+ if H == target_height and W == target_width:
70
+ return image
71
+
72
+ scale_factor = max(target_width / W, target_height / H)
73
+ resized_width = int(round(W * scale_factor))
74
+ resized_height = int(round(H * scale_factor))
75
+
76
+ resized = torch.nn.functional.interpolate(image, size=(resized_height, resized_width), mode='bilinear', align_corners=False)
77
+
78
+ top = (resized_height - target_height) // 2
79
+ left = (resized_width - target_width) // 2
80
+ cropped = resized[:, :, top:top + target_height, left:left + target_width]
81
+
82
+ return cropped
83
+
84
+
85
+ def resize_without_crop(image, target_width, target_height):
86
+ if target_height == image.shape[0] and target_width == image.shape[1]:
87
+ return image
88
+
89
+ pil_image = Image.fromarray(image)
90
+ resized_image = pil_image.resize((target_width, target_height), Image.LANCZOS)
91
+ return np.array(resized_image)
92
+
93
+
94
+ def just_crop(image, w, h):
95
+ if h == image.shape[0] and w == image.shape[1]:
96
+ return image
97
+
98
+ original_height, original_width = image.shape[:2]
99
+ k = min(original_height / h, original_width / w)
100
+ new_width = int(round(w * k))
101
+ new_height = int(round(h * k))
102
+ x_start = (original_width - new_width) // 2
103
+ y_start = (original_height - new_height) // 2
104
+ cropped_image = image[y_start:y_start + new_height, x_start:x_start + new_width]
105
+ return cropped_image
106
+
107
+
108
+ def write_to_json(data, file_path):
109
+ temp_file_path = file_path + ".tmp"
110
+ with open(temp_file_path, 'wt', encoding='utf-8') as temp_file:
111
+ json.dump(data, temp_file, indent=4)
112
+ os.replace(temp_file_path, file_path)
113
+ return
114
+
115
+
116
+ def read_from_json(file_path):
117
+ with open(file_path, 'rt', encoding='utf-8') as file:
118
+ data = json.load(file)
119
+ return data
120
+
121
+
122
+ def get_active_parameters(m):
123
+ return {k: v for k, v in m.named_parameters() if v.requires_grad}
124
+
125
+
126
+ def cast_training_params(m, dtype=torch.float32):
127
+ result = {}
128
+ for n, param in m.named_parameters():
129
+ if param.requires_grad:
130
+ param.data = param.to(dtype)
131
+ result[n] = param
132
+ return result
133
+
134
+
135
+ def separate_lora_AB(parameters, B_patterns=None):
136
+ parameters_normal = {}
137
+ parameters_B = {}
138
+
139
+ if B_patterns is None:
140
+ B_patterns = ['.lora_B.', '__zero__']
141
+
142
+ for k, v in parameters.items():
143
+ if any(B_pattern in k for B_pattern in B_patterns):
144
+ parameters_B[k] = v
145
+ else:
146
+ parameters_normal[k] = v
147
+
148
+ return parameters_normal, parameters_B
149
+
150
+
151
+ def set_attr_recursive(obj, attr, value):
152
+ attrs = attr.split(".")
153
+ for name in attrs[:-1]:
154
+ obj = getattr(obj, name)
155
+ setattr(obj, attrs[-1], value)
156
+ return
157
+
158
+
159
+ def print_tensor_list_size(tensors):
160
+ total_size = 0
161
+ total_elements = 0
162
+
163
+ if isinstance(tensors, dict):
164
+ tensors = tensors.values()
165
+
166
+ for tensor in tensors:
167
+ total_size += tensor.nelement() * tensor.element_size()
168
+ total_elements += tensor.nelement()
169
+
170
+ total_size_MB = total_size / (1024 ** 2)
171
+ total_elements_B = total_elements / 1e9
172
+
173
+ print(f"Total number of tensors: {len(tensors)}")
174
+ print(f"Total size of tensors: {total_size_MB:.2f} MB")
175
+ print(f"Total number of parameters: {total_elements_B:.3f} billion")
176
+ return
177
+
178
+
179
+ @torch.no_grad()
180
+ def batch_mixture(a, b=None, probability_a=0.5, mask_a=None):
181
+ batch_size = a.size(0)
182
+
183
+ if b is None:
184
+ b = torch.zeros_like(a)
185
+
186
+ if mask_a is None:
187
+ mask_a = torch.rand(batch_size) < probability_a
188
+
189
+ mask_a = mask_a.to(a.device)
190
+ mask_a = mask_a.reshape((batch_size,) + (1,) * (a.dim() - 1))
191
+ result = torch.where(mask_a, a, b)
192
+ return result
193
+
194
+
195
+ @torch.no_grad()
196
+ def zero_module(module):
197
+ for p in module.parameters():
198
+ p.detach().zero_()
199
+ return module
200
+
201
+
202
+ @torch.no_grad()
203
+ def supress_lower_channels(m, k, alpha=0.01):
204
+ data = m.weight.data.clone()
205
+
206
+ assert int(data.shape[1]) >= k
207
+
208
+ data[:, :k] = data[:, :k] * alpha
209
+ m.weight.data = data.contiguous().clone()
210
+ return m
211
+
212
+
213
+ def freeze_module(m):
214
+ if not hasattr(m, '_forward_inside_frozen_module'):
215
+ m._forward_inside_frozen_module = m.forward
216
+ m.requires_grad_(False)
217
+ m.forward = torch.no_grad()(m.forward)
218
+ return m
219
+
220
+
221
+ def get_latest_safetensors(folder_path):
222
+ safetensors_files = glob.glob(os.path.join(folder_path, '*.safetensors'))
223
+
224
+ if not safetensors_files:
225
+ raise ValueError('No file to resume!')
226
+
227
+ latest_file = max(safetensors_files, key=os.path.getmtime)
228
+ latest_file = os.path.abspath(os.path.realpath(latest_file))
229
+ return latest_file
230
+
231
+
232
+ def generate_random_prompt_from_tags(tags_str, min_length=3, max_length=32):
233
+ tags = tags_str.split(', ')
234
+ tags = random.sample(tags, k=min(random.randint(min_length, max_length), len(tags)))
235
+ prompt = ', '.join(tags)
236
+ return prompt
237
+
238
+
239
+ def interpolate_numbers(a, b, n, round_to_int=False, gamma=1.0):
240
+ numbers = a + (b - a) * (np.linspace(0, 1, n) ** gamma)
241
+ if round_to_int:
242
+ numbers = np.round(numbers).astype(int)
243
+ return numbers.tolist()
244
+
245
+
246
+ def uniform_random_by_intervals(inclusive, exclusive, n, round_to_int=False):
247
+ edges = np.linspace(0, 1, n + 1)
248
+ points = np.random.uniform(edges[:-1], edges[1:])
249
+ numbers = inclusive + (exclusive - inclusive) * points
250
+ if round_to_int:
251
+ numbers = np.round(numbers).astype(int)
252
+ return numbers.tolist()
253
+
254
+
255
+ def soft_append_bcthw(history, current, overlap=0):
256
+ if overlap <= 0:
257
+ return torch.cat([history, current], dim=2)
258
+
259
+ assert history.shape[2] >= overlap, f"History length ({history.shape[2]}) must be >= overlap ({overlap})"
260
+ assert current.shape[2] >= overlap, f"Current length ({current.shape[2]}) must be >= overlap ({overlap})"
261
+
262
+ weights = torch.linspace(1, 0, overlap, dtype=history.dtype, device=history.device).view(1, 1, -1, 1, 1)
263
+ blended = weights * history[:, :, -overlap:] + (1 - weights) * current[:, :, :overlap]
264
+ output = torch.cat([history[:, :, :-overlap], blended, current[:, :, overlap:]], dim=2)
265
+
266
+ return output.to(history)
267
+
268
+
269
+ def save_bcthw_as_mp4(x, output_filename, fps=10, crf=0):
270
+ b, c, t, h, w = x.shape
271
+
272
+ per_row = b
273
+ for p in [6, 5, 4, 3, 2]:
274
+ if b % p == 0:
275
+ per_row = p
276
+ break
277
+
278
+ os.makedirs(os.path.dirname(os.path.abspath(os.path.realpath(output_filename))), exist_ok=True)
279
+ x = torch.clamp(x.float(), -1., 1.) * 127.5 + 127.5
280
+ x = x.detach().cpu().to(torch.uint8)
281
+ x = einops.rearrange(x, '(m n) c t h w -> t (m h) (n w) c', n=per_row)
282
+ torchvision.io.write_video(output_filename, x, fps=fps, video_codec='libx264', options={'crf': str(int(crf))})
283
+ return x
284
+
285
+
286
+ def save_bcthw_as_png(x, output_filename):
287
+ os.makedirs(os.path.dirname(os.path.abspath(os.path.realpath(output_filename))), exist_ok=True)
288
+ x = torch.clamp(x.float(), -1., 1.) * 127.5 + 127.5
289
+ x = x.detach().cpu().to(torch.uint8)
290
+ x = einops.rearrange(x, 'b c t h w -> c (b h) (t w)')
291
+ torchvision.io.write_png(x, output_filename)
292
+ return output_filename
293
+
294
+
295
+ def save_bchw_as_png(x, output_filename):
296
+ os.makedirs(os.path.dirname(os.path.abspath(os.path.realpath(output_filename))), exist_ok=True)
297
+ x = torch.clamp(x.float(), -1., 1.) * 127.5 + 127.5
298
+ x = x.detach().cpu().to(torch.uint8)
299
+ x = einops.rearrange(x, 'b c h w -> c h (b w)')
300
+ torchvision.io.write_png(x, output_filename)
301
+ return output_filename
302
+
303
+
304
+ def add_tensors_with_padding(tensor1, tensor2):
305
+ if tensor1.shape == tensor2.shape:
306
+ return tensor1 + tensor2
307
+
308
+ shape1 = tensor1.shape
309
+ shape2 = tensor2.shape
310
+
311
+ new_shape = tuple(max(s1, s2) for s1, s2 in zip(shape1, shape2))
312
+
313
+ padded_tensor1 = torch.zeros(new_shape)
314
+ padded_tensor2 = torch.zeros(new_shape)
315
+
316
+ padded_tensor1[tuple(slice(0, s) for s in shape1)] = tensor1
317
+ padded_tensor2[tuple(slice(0, s) for s in shape2)] = tensor2
318
+
319
+ result = padded_tensor1 + padded_tensor2
320
+ return result
321
+
322
+
323
+ def print_free_mem():
324
+ torch.cuda.empty_cache()
325
+ free_mem, total_mem = torch.cuda.mem_get_info(0)
326
+ free_mem_mb = free_mem / (1024 ** 2)
327
+ total_mem_mb = total_mem / (1024 ** 2)
328
+ print(f"Free memory: {free_mem_mb:.2f} MB")
329
+ print(f"Total memory: {total_mem_mb:.2f} MB")
330
+ return
331
+
332
+
333
+ def print_gpu_parameters(device, state_dict, log_count=1):
334
+ summary = {"device": device, "keys_count": len(state_dict)}
335
+
336
+ logged_params = {}
337
+ for i, (key, tensor) in enumerate(state_dict.items()):
338
+ if i >= log_count:
339
+ break
340
+ logged_params[key] = tensor.flatten()[:3].tolist()
341
+
342
+ summary["params"] = logged_params
343
+
344
+ print(str(summary))
345
+ return
346
+
347
+
348
+ def visualize_txt_as_img(width, height, text, font_path='font/DejaVuSans.ttf', size=18):
349
+ from PIL import Image, ImageDraw, ImageFont
350
+
351
+ txt = Image.new("RGB", (width, height), color="white")
352
+ draw = ImageDraw.Draw(txt)
353
+ font = ImageFont.truetype(font_path, size=size)
354
+
355
+ if text == '':
356
+ return np.array(txt)
357
+
358
+ # Split text into lines that fit within the image width
359
+ lines = []
360
+ words = text.split()
361
+ current_line = words[0]
362
+
363
+ for word in words[1:]:
364
+ line_with_word = f"{current_line} {word}"
365
+ if draw.textbbox((0, 0), line_with_word, font=font)[2] <= width:
366
+ current_line = line_with_word
367
+ else:
368
+ lines.append(current_line)
369
+ current_line = word
370
+
371
+ lines.append(current_line)
372
+
373
+ # Draw the text line by line
374
+ y = 0
375
+ line_height = draw.textbbox((0, 0), "A", font=font)[3]
376
+
377
+ for line in lines:
378
+ if y + line_height > height:
379
+ break # stop drawing if the next line will be outside the image
380
+ draw.text((0, y), line, fill="black", font=font)
381
+ y += line_height
382
+
383
+ return np.array(txt)
384
+
385
+
386
+ def blue_mark(x):
387
+ x = x.copy()
388
+ c = x[:, :, 2]
389
+ b = cv2.blur(c, (9, 9))
390
+ x[:, :, 2] = ((c - b) * 16.0 + b).clip(-1, 1)
391
+ return x
392
+
393
+
394
+ def green_mark(x):
395
+ x = x.copy()
396
+ x[:, :, 2] = -1
397
+ x[:, :, 0] = -1
398
+ return x
399
+
400
+
401
+ def frame_mark(x):
402
+ x = x.copy()
403
+ x[:64] = -1
404
+ x[-64:] = -1
405
+ x[:, :8] = 1
406
+ x[:, -8:] = 1
407
+ return x
408
+
409
+
410
+ @torch.inference_mode()
411
+ def pytorch2numpy(imgs):
412
+ results = []
413
+ for x in imgs:
414
+ y = x.movedim(0, -1)
415
+ y = y * 127.5 + 127.5
416
+ y = y.detach().float().cpu().numpy().clip(0, 255).astype(np.uint8)
417
+ results.append(y)
418
+ return results
419
+
420
+
421
+ @torch.inference_mode()
422
+ def numpy2pytorch(imgs):
423
+ h = torch.from_numpy(np.stack(imgs, axis=0)).float() / 127.5 - 1.0
424
+ h = h.movedim(-1, 1)
425
+ return h
426
+
427
+
428
+ @torch.no_grad()
429
+ def duplicate_prefix_to_suffix(x, count, zero_out=False):
430
+ if zero_out:
431
+ return torch.cat([x, torch.zeros_like(x[:count])], dim=0)
432
+ else:
433
+ return torch.cat([x, x[:count]], dim=0)
434
+
435
+
436
+ def weighted_mse(a, b, weight):
437
+ return torch.mean(weight.float() * (a.float() - b.float()) ** 2)
438
+
439
+
440
+ def clamped_linear_interpolation(x, x_min, y_min, x_max, y_max, sigma=1.0):
441
+ x = (x - x_min) / (x_max - x_min)
442
+ x = max(0.0, min(x, 1.0))
443
+ x = x ** sigma
444
+ return y_min + x * (y_max - y_min)
445
+
446
+
447
+ def expand_to_dims(x, target_dims):
448
+ return x.view(*x.shape, *([1] * max(0, target_dims - x.dim())))
449
+
450
+
451
+ def repeat_to_batch_size(tensor: torch.Tensor, batch_size: int):
452
+ if tensor is None:
453
+ return None
454
+
455
+ first_dim = tensor.shape[0]
456
+
457
+ if first_dim == batch_size:
458
+ return tensor
459
+
460
+ if batch_size % first_dim != 0:
461
+ raise ValueError(f"Cannot evenly repeat first dim {first_dim} to match batch_size {batch_size}.")
462
+
463
+ repeat_times = batch_size // first_dim
464
+
465
+ return tensor.repeat(repeat_times, *[1] * (tensor.dim() - 1))
466
+
467
+
468
+ def dim5(x):
469
+ return expand_to_dims(x, 5)
470
+
471
+
472
+ def dim4(x):
473
+ return expand_to_dims(x, 4)
474
+
475
+
476
+ def dim3(x):
477
+ return expand_to_dims(x, 3)
478
+
479
+
480
+ def crop_or_pad_yield_mask(x, length):
481
+ B, F, C = x.shape
482
+ device = x.device
483
+ dtype = x.dtype
484
+
485
+ if F < length:
486
+ y = torch.zeros((B, length, C), dtype=dtype, device=device)
487
+ mask = torch.zeros((B, length), dtype=torch.bool, device=device)
488
+ y[:, :F, :] = x
489
+ mask[:, :F] = True
490
+ return y, mask
491
+
492
+ return x[:, :length, :], torch.ones((B, length), dtype=torch.bool, device=device)
493
+
494
+
495
+ def extend_dim(x, dim, minimal_length, zero_pad=False):
496
+ original_length = int(x.shape[dim])
497
+
498
+ if original_length >= minimal_length:
499
+ return x
500
+
501
+ if zero_pad:
502
+ padding_shape = list(x.shape)
503
+ padding_shape[dim] = minimal_length - original_length
504
+ padding = torch.zeros(padding_shape, dtype=x.dtype, device=x.device)
505
+ else:
506
+ idx = (slice(None),) * dim + (slice(-1, None),) + (slice(None),) * (len(x.shape) - dim - 1)
507
+ last_element = x[idx]
508
+ padding = last_element.repeat_interleave(minimal_length - original_length, dim=dim)
509
+
510
+ return torch.cat([x, padding], dim=dim)
511
+
512
+
513
+ def lazy_positional_encoding(t, repeats=None):
514
+ if not isinstance(t, list):
515
+ t = [t]
516
+
517
+ from diffusers.models.embeddings import get_timestep_embedding
518
+
519
+ te = torch.tensor(t)
520
+ te = get_timestep_embedding(timesteps=te, embedding_dim=256, flip_sin_to_cos=True, downscale_freq_shift=0.0, scale=1.0)
521
+
522
+ if repeats is None:
523
+ return te
524
+
525
+ te = te[:, None, :].expand(-1, repeats, -1)
526
+
527
+ return te
528
+
529
+
530
+ def state_dict_offset_merge(A, B, C=None):
531
+ result = {}
532
+ keys = A.keys()
533
+
534
+ for key in keys:
535
+ A_value = A[key]
536
+ B_value = B[key].to(A_value)
537
+
538
+ if C is None:
539
+ result[key] = A_value + B_value
540
+ else:
541
+ C_value = C[key].to(A_value)
542
+ result[key] = A_value + B_value - C_value
543
+
544
+ return result
545
+
546
+
547
+ def state_dict_weighted_merge(state_dicts, weights):
548
+ if len(state_dicts) != len(weights):
549
+ raise ValueError("Number of state dictionaries must match number of weights")
550
+
551
+ if not state_dicts:
552
+ return {}
553
+
554
+ total_weight = sum(weights)
555
+
556
+ if total_weight == 0:
557
+ raise ValueError("Sum of weights cannot be zero")
558
+
559
+ normalized_weights = [w / total_weight for w in weights]
560
+
561
+ keys = state_dicts[0].keys()
562
+ result = {}
563
+
564
+ for key in keys:
565
+ result[key] = state_dicts[0][key] * normalized_weights[0]
566
+
567
+ for i in range(1, len(state_dicts)):
568
+ state_dict_value = state_dicts[i][key].to(result[key])
569
+ result[key] += state_dict_value * normalized_weights[i]
570
+
571
+ return result
572
+
573
+
574
+ def group_files_by_folder(all_files):
575
+ grouped_files = {}
576
+
577
+ for file in all_files:
578
+ folder_name = os.path.basename(os.path.dirname(file))
579
+ if folder_name not in grouped_files:
580
+ grouped_files[folder_name] = []
581
+ grouped_files[folder_name].append(file)
582
+
583
+ list_of_lists = list(grouped_files.values())
584
+ return list_of_lists
585
+
586
+
587
+ def generate_timestamp():
588
+ now = datetime.datetime.now()
589
+ timestamp = now.strftime('%y%m%d_%H%M%S')
590
+ milliseconds = f"{int(now.microsecond / 1000):03d}"
591
+ random_number = random.randint(0, 9999)
592
+ return f"{timestamp}_{milliseconds}_{random_number}"
593
+
594
+
595
+ def write_PIL_image_with_png_info(image, metadata, path):
596
+ from PIL.PngImagePlugin import PngInfo
597
+
598
+ png_info = PngInfo()
599
+ for key, value in metadata.items():
600
+ png_info.add_text(key, value)
601
+
602
+ image.save(path, "PNG", pnginfo=png_info)
603
+ return image
604
+
605
+
606
+ def torch_safe_save(content, path):
607
+ torch.save(content, path + '_tmp')
608
+ os.replace(path + '_tmp', path)
609
+ return path
610
+
611
+
612
+ def move_optimizer_to_device(optimizer, device):
613
+ for state in optimizer.state.values():
614
+ for k, v in state.items():
615
+ if isinstance(v, torch.Tensor):
616
+ state[k] = v.to(device)
demo_utils/vae.py ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from einops import rearrange
3
+ import tensorrt as trt
4
+ import torch
5
+ import torch.nn as nn
6
+
7
+ from demo_utils.constant import ALL_INPUTS_NAMES, ZERO_VAE_CACHE
8
+ from wan.modules.vae import AttentionBlock, CausalConv3d, RMS_norm, Upsample
9
+
10
+ CACHE_T = 2
11
+
12
+
13
+ class ResidualBlock(nn.Module):
14
+
15
+ def __init__(self, in_dim, out_dim, dropout=0.0):
16
+ super().__init__()
17
+ self.in_dim = in_dim
18
+ self.out_dim = out_dim
19
+
20
+ # layers
21
+ self.residual = nn.Sequential(
22
+ RMS_norm(in_dim, images=False), nn.SiLU(),
23
+ CausalConv3d(in_dim, out_dim, 3, padding=1),
24
+ RMS_norm(out_dim, images=False), nn.SiLU(), nn.Dropout(dropout),
25
+ CausalConv3d(out_dim, out_dim, 3, padding=1))
26
+ self.shortcut = CausalConv3d(in_dim, out_dim, 1) \
27
+ if in_dim != out_dim else nn.Identity()
28
+
29
+ def forward(self, x, feat_cache_1, feat_cache_2):
30
+ h = self.shortcut(x)
31
+ feat_cache = feat_cache_1
32
+ out_feat_cache = []
33
+ for layer in self.residual:
34
+ if isinstance(layer, CausalConv3d):
35
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
36
+ if cache_x.shape[2] < 2 and feat_cache is not None:
37
+ # cache last frame of last two chunk
38
+ cache_x = torch.cat([
39
+ feat_cache[:, :, -1, :, :].unsqueeze(2).to(
40
+ cache_x.device), cache_x
41
+ ],
42
+ dim=2)
43
+ x = layer(x, feat_cache)
44
+ out_feat_cache.append(cache_x)
45
+ feat_cache = feat_cache_2
46
+ else:
47
+ x = layer(x)
48
+ return x + h, *out_feat_cache
49
+
50
+
51
+ class Resample(nn.Module):
52
+
53
+ def __init__(self, dim, mode):
54
+ assert mode in ('none', 'upsample2d', 'upsample3d')
55
+ super().__init__()
56
+ self.dim = dim
57
+ self.mode = mode
58
+
59
+ # layers
60
+ if mode == 'upsample2d':
61
+ self.resample = nn.Sequential(
62
+ Upsample(scale_factor=(2., 2.), mode='nearest'),
63
+ nn.Conv2d(dim, dim // 2, 3, padding=1))
64
+ elif mode == 'upsample3d':
65
+ self.resample = nn.Sequential(
66
+ Upsample(scale_factor=(2., 2.), mode='nearest'),
67
+ nn.Conv2d(dim, dim // 2, 3, padding=1))
68
+ self.time_conv = CausalConv3d(
69
+ dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
70
+ else:
71
+ self.resample = nn.Identity()
72
+
73
+ def forward(self, x, is_first_frame, feat_cache):
74
+ if self.mode == 'upsample3d':
75
+ b, c, t, h, w = x.size()
76
+ # x, out_feat_cache = torch.cond(
77
+ # is_first_frame,
78
+ # lambda: (torch.cat([torch.zeros_like(x), x], dim=2), feat_cache.clone()),
79
+ # lambda: self.temporal_conv(x, feat_cache),
80
+ # )
81
+ # x, out_feat_cache = torch.cond(
82
+ # is_first_frame,
83
+ # lambda: (torch.cat([torch.zeros_like(x), x], dim=2), feat_cache.clone()),
84
+ # lambda: self.temporal_conv(x, feat_cache),
85
+ # )
86
+ x, out_feat_cache = self.temporal_conv(x, is_first_frame, feat_cache)
87
+ out_feat_cache = torch.cond(
88
+ is_first_frame,
89
+ lambda: feat_cache.clone().contiguous(),
90
+ lambda: out_feat_cache.clone().contiguous(),
91
+ )
92
+ # if is_first_frame:
93
+ # x = torch.cat([torch.zeros_like(x), x], dim=2)
94
+ # out_feat_cache = feat_cache.clone()
95
+ # else:
96
+ # x, out_feat_cache = self.temporal_conv(x, feat_cache)
97
+ else:
98
+ out_feat_cache = None
99
+ t = x.shape[2]
100
+ x = rearrange(x, 'b c t h w -> (b t) c h w')
101
+ x = self.resample(x)
102
+ x = rearrange(x, '(b t) c h w -> b c t h w', t=t)
103
+ return x, out_feat_cache
104
+
105
+ def temporal_conv(self, x, is_first_frame, feat_cache):
106
+ b, c, t, h, w = x.size()
107
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
108
+ if cache_x.shape[2] < 2 and feat_cache is not None:
109
+ cache_x = torch.cat([
110
+ torch.zeros_like(cache_x),
111
+ cache_x
112
+ ], dim=2)
113
+ x = torch.cond(
114
+ is_first_frame,
115
+ lambda: torch.cat([torch.zeros_like(x), x], dim=1).contiguous(),
116
+ lambda: self.time_conv(x, feat_cache).contiguous(),
117
+ )
118
+ # x = self.time_conv(x, feat_cache)
119
+ out_feat_cache = cache_x
120
+
121
+ x = x.reshape(b, 2, c, t, h, w)
122
+ x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]),
123
+ 3)
124
+ x = x.reshape(b, c, t * 2, h, w)
125
+ return x.contiguous(), out_feat_cache.contiguous()
126
+
127
+ def init_weight(self, conv):
128
+ conv_weight = conv.weight
129
+ nn.init.zeros_(conv_weight)
130
+ c1, c2, t, h, w = conv_weight.size()
131
+ one_matrix = torch.eye(c1, c2)
132
+ init_matrix = one_matrix
133
+ nn.init.zeros_(conv_weight)
134
+ # conv_weight.data[:,:,-1,1,1] = init_matrix * 0.5
135
+ conv_weight.data[:, :, 1, 0, 0] = init_matrix # * 0.5
136
+ conv.weight.data.copy_(conv_weight)
137
+ nn.init.zeros_(conv.bias.data)
138
+
139
+ def init_weight2(self, conv):
140
+ conv_weight = conv.weight.data
141
+ nn.init.zeros_(conv_weight)
142
+ c1, c2, t, h, w = conv_weight.size()
143
+ init_matrix = torch.eye(c1 // 2, c2)
144
+ # init_matrix = repeat(init_matrix, 'o ... -> (o 2) ...').permute(1,0,2).contiguous().reshape(c1,c2)
145
+ conv_weight[:c1 // 2, :, -1, 0, 0] = init_matrix
146
+ conv_weight[c1 // 2:, :, -1, 0, 0] = init_matrix
147
+ conv.weight.data.copy_(conv_weight)
148
+ nn.init.zeros_(conv.bias.data)
149
+
150
+
151
+ class VAEDecoderWrapperSingle(nn.Module):
152
+ def __init__(self):
153
+ super().__init__()
154
+ self.decoder = VAEDecoder3d()
155
+ mean = [
156
+ -0.7571, -0.7089, -0.9113, 0.1075, -0.1745, 0.9653, -0.1517, 1.5508,
157
+ 0.4134, -0.0715, 0.5517, -0.3632, -0.1922, -0.9497, 0.2503, -0.2921
158
+ ]
159
+ std = [
160
+ 2.8184, 1.4541, 2.3275, 2.6558, 1.2196, 1.7708, 2.6052, 2.0743,
161
+ 3.2687, 2.1526, 2.8652, 1.5579, 1.6382, 1.1253, 2.8251, 1.9160
162
+ ]
163
+ self.mean = torch.tensor(mean, dtype=torch.float32)
164
+ self.std = torch.tensor(std, dtype=torch.float32)
165
+ self.z_dim = 16
166
+ self.conv2 = CausalConv3d(self.z_dim, self.z_dim, 1)
167
+
168
+ def forward(
169
+ self,
170
+ z: torch.Tensor,
171
+ is_first_frame: torch.Tensor,
172
+ *feat_cache: List[torch.Tensor]
173
+ ):
174
+ # from [batch_size, num_frames, num_channels, height, width]
175
+ # to [batch_size, num_channels, num_frames, height, width]
176
+ z = z.permute(0, 2, 1, 3, 4)
177
+ assert z.shape[2] == 1
178
+ feat_cache = list(feat_cache)
179
+ is_first_frame = is_first_frame.bool()
180
+
181
+ device, dtype = z.device, z.dtype
182
+ scale = [self.mean.to(device=device, dtype=dtype),
183
+ 1.0 / self.std.to(device=device, dtype=dtype)]
184
+
185
+ if isinstance(scale[0], torch.Tensor):
186
+ z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
187
+ 1, self.z_dim, 1, 1, 1)
188
+ else:
189
+ z = z / scale[1] + scale[0]
190
+ x = self.conv2(z)
191
+ out, feat_cache = self.decoder(x, is_first_frame, feat_cache=feat_cache)
192
+ out = out.clamp_(-1, 1)
193
+ # from [batch_size, num_channels, num_frames, height, width]
194
+ # to [batch_size, num_frames, num_channels, height, width]
195
+ out = out.permute(0, 2, 1, 3, 4)
196
+ return out, feat_cache
197
+
198
+
199
+ class VAEDecoder3d(nn.Module):
200
+ def __init__(self,
201
+ dim=96,
202
+ z_dim=16,
203
+ dim_mult=[1, 2, 4, 4],
204
+ num_res_blocks=2,
205
+ attn_scales=[],
206
+ temperal_upsample=[True, True, False],
207
+ dropout=0.0):
208
+ super().__init__()
209
+ self.dim = dim
210
+ self.z_dim = z_dim
211
+ self.dim_mult = dim_mult
212
+ self.num_res_blocks = num_res_blocks
213
+ self.attn_scales = attn_scales
214
+ self.temperal_upsample = temperal_upsample
215
+ self.cache_t = 2
216
+ self.decoder_conv_num = 32
217
+
218
+ # dimensions
219
+ dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
220
+ scale = 1.0 / 2**(len(dim_mult) - 2)
221
+
222
+ # init block
223
+ self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
224
+
225
+ # middle blocks
226
+ self.middle = nn.Sequential(
227
+ ResidualBlock(dims[0], dims[0], dropout), AttentionBlock(dims[0]),
228
+ ResidualBlock(dims[0], dims[0], dropout))
229
+
230
+ # upsample blocks
231
+ upsamples = []
232
+ for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
233
+ # residual (+attention) blocks
234
+ if i == 1 or i == 2 or i == 3:
235
+ in_dim = in_dim // 2
236
+ for _ in range(num_res_blocks + 1):
237
+ upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
238
+ if scale in attn_scales:
239
+ upsamples.append(AttentionBlock(out_dim))
240
+ in_dim = out_dim
241
+
242
+ # upsample block
243
+ if i != len(dim_mult) - 1:
244
+ mode = 'upsample3d' if temperal_upsample[i] else 'upsample2d'
245
+ upsamples.append(Resample(out_dim, mode=mode))
246
+ scale *= 2.0
247
+ self.upsamples = nn.Sequential(*upsamples)
248
+
249
+ # output blocks
250
+ self.head = nn.Sequential(
251
+ RMS_norm(out_dim, images=False), nn.SiLU(),
252
+ CausalConv3d(out_dim, 3, 3, padding=1))
253
+
254
+ def forward(
255
+ self,
256
+ x: torch.Tensor,
257
+ is_first_frame: torch.Tensor,
258
+ feat_cache: List[torch.Tensor]
259
+ ):
260
+ idx = 0
261
+ out_feat_cache = []
262
+
263
+ # conv1
264
+ cache_x = x[:, :, -self.cache_t:, :, :].clone()
265
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
266
+ # cache last frame of last two chunk
267
+ cache_x = torch.cat([
268
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
269
+ cache_x.device), cache_x
270
+ ],
271
+ dim=2)
272
+ x = self.conv1(x, feat_cache[idx])
273
+ out_feat_cache.append(cache_x)
274
+ idx += 1
275
+
276
+ # middle
277
+ for layer in self.middle:
278
+ if isinstance(layer, ResidualBlock) and feat_cache is not None:
279
+ x, out_feat_cache_1, out_feat_cache_2 = layer(x, feat_cache[idx], feat_cache[idx + 1])
280
+ idx += 2
281
+ out_feat_cache.append(out_feat_cache_1)
282
+ out_feat_cache.append(out_feat_cache_2)
283
+ else:
284
+ x = layer(x)
285
+
286
+ # upsamples
287
+ for layer in self.upsamples:
288
+ if isinstance(layer, Resample):
289
+ x, cache_x = layer(x, is_first_frame, feat_cache[idx])
290
+ if cache_x is not None:
291
+ out_feat_cache.append(cache_x)
292
+ idx += 1
293
+ else:
294
+ x, out_feat_cache_1, out_feat_cache_2 = layer(x, feat_cache[idx], feat_cache[idx + 1])
295
+ idx += 2
296
+ out_feat_cache.append(out_feat_cache_1)
297
+ out_feat_cache.append(out_feat_cache_2)
298
+
299
+ # head
300
+ for layer in self.head:
301
+ if isinstance(layer, CausalConv3d) and feat_cache is not None:
302
+ cache_x = x[:, :, -self.cache_t:, :, :].clone()
303
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
304
+ # cache last frame of last two chunk
305
+ cache_x = torch.cat([
306
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
307
+ cache_x.device), cache_x
308
+ ],
309
+ dim=2)
310
+ x = layer(x, feat_cache[idx])
311
+ out_feat_cache.append(cache_x)
312
+ idx += 1
313
+ else:
314
+ x = layer(x)
315
+ return x, out_feat_cache
316
+
317
+
318
+ class VAETRTWrapper():
319
+ def __init__(self):
320
+ TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
321
+ with open("checkpoints/vae_decoder_int8.trt", "rb") as f, trt.Runtime(TRT_LOGGER) as rt:
322
+ self.engine: trt.ICudaEngine = rt.deserialize_cuda_engine(f.read())
323
+
324
+ self.context: trt.IExecutionContext = self.engine.create_execution_context()
325
+ self.stream = torch.cuda.current_stream().cuda_stream
326
+
327
+ # ──────────────────────────────
328
+ # 2️⃣ Feed the engine with tensors
329
+ # (name-based API in TRT ≥10)
330
+ # ──────────────────────────────
331
+ self.dtype_map = {
332
+ trt.float32: torch.float32,
333
+ trt.float16: torch.float16,
334
+ trt.int8: torch.int8,
335
+ trt.int32: torch.int32,
336
+ }
337
+ test_input = torch.zeros(1, 16, 1, 60, 104).cuda().half()
338
+ is_first_frame = torch.tensor(1.0).cuda().half()
339
+ test_cache_inputs = [c.cuda().half() for c in ZERO_VAE_CACHE]
340
+ test_inputs = [test_input, is_first_frame] + test_cache_inputs
341
+
342
+ # keep references so buffers stay alive
343
+ self.device_buffers, self.outputs = {}, []
344
+
345
+ # ---- inputs ----
346
+ for i, name in enumerate(ALL_INPUTS_NAMES):
347
+ tensor, scale = test_inputs[i], 1 / 127
348
+ tensor = self.quantize_if_needed(tensor, self.engine.get_tensor_dtype(name), scale)
349
+
350
+ # dynamic shapes
351
+ if -1 in self.engine.get_tensor_shape(name):
352
+ # new API :contentReference[oaicite:0]{index=0}
353
+ self.context.set_input_shape(name, tuple(tensor.shape))
354
+
355
+ # replaces bindings[] :contentReference[oaicite:1]{index=1}
356
+ self.context.set_tensor_address(name, int(tensor.data_ptr()))
357
+ self.device_buffers[name] = tensor # keep pointer alive
358
+
359
+ # ---- (after all input shapes are known) infer output shapes ----
360
+ # propagates shapes :contentReference[oaicite:2]{index=2}
361
+ self.context.infer_shapes()
362
+
363
+ for i in range(self.engine.num_io_tensors):
364
+ name = self.engine.get_tensor_name(i)
365
+ # replaces binding_is_input :contentReference[oaicite:3]{index=3}
366
+ if self.engine.get_tensor_mode(name) == trt.TensorIOMode.OUTPUT:
367
+ shape = tuple(self.context.get_tensor_shape(name))
368
+ dtype = self.dtype_map[self.engine.get_tensor_dtype(name)]
369
+ out = torch.empty(shape, dtype=dtype, device="cuda").contiguous()
370
+
371
+ self.context.set_tensor_address(name, int(out.data_ptr()))
372
+ self.outputs.append(out)
373
+ self.device_buffers[name] = out
374
+
375
+ # helper to quant-convert on the fly
376
+ def quantize_if_needed(self, t, expected_dtype, scale):
377
+ if expected_dtype == trt.int8 and t.dtype != torch.int8:
378
+ t = torch.clamp((t / scale).round(), -128, 127).to(torch.int8).contiguous()
379
+ return t # keep pointer alive
380
+
381
+ def forward(self, *test_inputs):
382
+ for i, name in enumerate(ALL_INPUTS_NAMES):
383
+ tensor, scale = test_inputs[i], 1 / 127
384
+ tensor = self.quantize_if_needed(tensor, self.engine.get_tensor_dtype(name), scale)
385
+ self.context.set_tensor_address(name, int(tensor.data_ptr()))
386
+ self.device_buffers[name] = tensor
387
+
388
+ self.context.execute_async_v3(stream_handle=self.stream)
389
+ torch.cuda.current_stream().synchronize()
390
+ return self.outputs
demo_utils/vae_block3.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from einops import rearrange
3
+ import torch
4
+ import torch.nn as nn
5
+
6
+ from wan.modules.vae import AttentionBlock, CausalConv3d, RMS_norm, ResidualBlock, Upsample
7
+
8
+
9
+ class Resample(nn.Module):
10
+
11
+ def __init__(self, dim, mode):
12
+ assert mode in ('none', 'upsample2d', 'upsample3d', 'downsample2d',
13
+ 'downsample3d')
14
+ super().__init__()
15
+ self.dim = dim
16
+ self.mode = mode
17
+ self.cache_t = 2
18
+
19
+ # layers
20
+ if mode == 'upsample2d':
21
+ self.resample = nn.Sequential(
22
+ Upsample(scale_factor=(2., 2.), mode='nearest'),
23
+ nn.Conv2d(dim, dim // 2, 3, padding=1))
24
+ elif mode == 'upsample3d':
25
+ self.resample = nn.Sequential(
26
+ Upsample(scale_factor=(2., 2.), mode='nearest'),
27
+ nn.Conv2d(dim, dim // 2, 3, padding=1))
28
+ self.time_conv = CausalConv3d(
29
+ dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
30
+
31
+ elif mode == 'downsample2d':
32
+ self.resample = nn.Sequential(
33
+ nn.ZeroPad2d((0, 1, 0, 1)),
34
+ nn.Conv2d(dim, dim, 3, stride=(2, 2)))
35
+ elif mode == 'downsample3d':
36
+ self.resample = nn.Sequential(
37
+ nn.ZeroPad2d((0, 1, 0, 1)),
38
+ nn.Conv2d(dim, dim, 3, stride=(2, 2)))
39
+ self.time_conv = CausalConv3d(
40
+ dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0))
41
+
42
+ else:
43
+ self.resample = nn.Identity()
44
+
45
+ def forward(self, x, feat_cache=None, feat_idx=[0]):
46
+ b, c, t, h, w = x.size()
47
+ if self.mode == 'upsample3d':
48
+ if feat_cache is not None:
49
+ idx = feat_idx[0]
50
+ if feat_cache[idx] is None:
51
+ feat_cache[idx] = 'Rep'
52
+ feat_idx[0] += 1
53
+ else:
54
+
55
+ cache_x = x[:, :, -self.cache_t:, :, :].clone()
56
+ if cache_x.shape[2] < 2 and feat_cache[
57
+ idx] is not None and feat_cache[idx] != 'Rep':
58
+ # cache last frame of last two chunk
59
+ cache_x = torch.cat([
60
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
61
+ cache_x.device), cache_x
62
+ ],
63
+ dim=2)
64
+ if cache_x.shape[2] < 2 and feat_cache[
65
+ idx] is not None and feat_cache[idx] == 'Rep':
66
+ cache_x = torch.cat([
67
+ torch.zeros_like(cache_x).to(cache_x.device),
68
+ cache_x
69
+ ],
70
+ dim=2)
71
+ if feat_cache[idx] == 'Rep':
72
+ x = self.time_conv(x)
73
+ else:
74
+ x = self.time_conv(x, feat_cache[idx])
75
+ feat_cache[idx] = cache_x
76
+ feat_idx[0] += 1
77
+
78
+ x = x.reshape(b, 2, c, t, h, w)
79
+ x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]),
80
+ 3)
81
+ x = x.reshape(b, c, t * 2, h, w)
82
+ t = x.shape[2]
83
+ x = rearrange(x, 'b c t h w -> (b t) c h w')
84
+ x = self.resample(x)
85
+ x = rearrange(x, '(b t) c h w -> b c t h w', t=t)
86
+
87
+ if self.mode == 'downsample3d':
88
+ if feat_cache is not None:
89
+ idx = feat_idx[0]
90
+ if feat_cache[idx] is None:
91
+ feat_cache[idx] = x.clone()
92
+ feat_idx[0] += 1
93
+ else:
94
+
95
+ cache_x = x[:, :, -1:, :, :].clone()
96
+ # if cache_x.shape[2] < 2 and feat_cache[idx] is not None and feat_cache[idx]!='Rep':
97
+ # # cache last frame of last two chunk
98
+ # cache_x = torch.cat([feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device), cache_x], dim=2)
99
+
100
+ x = self.time_conv(
101
+ torch.cat([feat_cache[idx][:, :, -1:, :, :], x], 2))
102
+ feat_cache[idx] = cache_x
103
+ feat_idx[0] += 1
104
+ return x
105
+
106
+ def init_weight(self, conv):
107
+ conv_weight = conv.weight
108
+ nn.init.zeros_(conv_weight)
109
+ c1, c2, t, h, w = conv_weight.size()
110
+ one_matrix = torch.eye(c1, c2)
111
+ init_matrix = one_matrix
112
+ nn.init.zeros_(conv_weight)
113
+ # conv_weight.data[:,:,-1,1,1] = init_matrix * 0.5
114
+ conv_weight.data[:, :, 1, 0, 0] = init_matrix # * 0.5
115
+ conv.weight.data.copy_(conv_weight)
116
+ nn.init.zeros_(conv.bias.data)
117
+
118
+ def init_weight2(self, conv):
119
+ conv_weight = conv.weight.data
120
+ nn.init.zeros_(conv_weight)
121
+ c1, c2, t, h, w = conv_weight.size()
122
+ init_matrix = torch.eye(c1 // 2, c2)
123
+ # init_matrix = repeat(init_matrix, 'o ... -> (o 2) ...').permute(1,0,2).contiguous().reshape(c1,c2)
124
+ conv_weight[:c1 // 2, :, -1, 0, 0] = init_matrix
125
+ conv_weight[c1 // 2:, :, -1, 0, 0] = init_matrix
126
+ conv.weight.data.copy_(conv_weight)
127
+ nn.init.zeros_(conv.bias.data)
128
+
129
+
130
+ class VAEDecoderWrapper(nn.Module):
131
+ def __init__(self):
132
+ super().__init__()
133
+ self.decoder = VAEDecoder3d()
134
+ mean = [
135
+ -0.7571, -0.7089, -0.9113, 0.1075, -0.1745, 0.9653, -0.1517, 1.5508,
136
+ 0.4134, -0.0715, 0.5517, -0.3632, -0.1922, -0.9497, 0.2503, -0.2921
137
+ ]
138
+ std = [
139
+ 2.8184, 1.4541, 2.3275, 2.6558, 1.2196, 1.7708, 2.6052, 2.0743,
140
+ 3.2687, 2.1526, 2.8652, 1.5579, 1.6382, 1.1253, 2.8251, 1.9160
141
+ ]
142
+ self.mean = torch.tensor(mean, dtype=torch.float32)
143
+ self.std = torch.tensor(std, dtype=torch.float32)
144
+ self.z_dim = 16
145
+ self.conv2 = CausalConv3d(self.z_dim, self.z_dim, 1)
146
+
147
+ def forward(
148
+ self,
149
+ z: torch.Tensor,
150
+ *feat_cache: List[torch.Tensor]
151
+ ):
152
+ # from [batch_size, num_frames, num_channels, height, width]
153
+ # to [batch_size, num_channels, num_frames, height, width]
154
+ z = z.permute(0, 2, 1, 3, 4)
155
+ feat_cache = list(feat_cache)
156
+ print("Length of feat_cache: ", len(feat_cache))
157
+
158
+ device, dtype = z.device, z.dtype
159
+ scale = [self.mean.to(device=device, dtype=dtype),
160
+ 1.0 / self.std.to(device=device, dtype=dtype)]
161
+
162
+ if isinstance(scale[0], torch.Tensor):
163
+ z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
164
+ 1, self.z_dim, 1, 1, 1)
165
+ else:
166
+ z = z / scale[1] + scale[0]
167
+ iter_ = z.shape[2]
168
+ x = self.conv2(z)
169
+ for i in range(iter_):
170
+ if i == 0:
171
+ out, feat_cache = self.decoder(
172
+ x[:, :, i:i + 1, :, :],
173
+ feat_cache=feat_cache)
174
+ else:
175
+ out_, feat_cache = self.decoder(
176
+ x[:, :, i:i + 1, :, :],
177
+ feat_cache=feat_cache)
178
+ out = torch.cat([out, out_], 2)
179
+
180
+ out = out.float().clamp_(-1, 1)
181
+ # from [batch_size, num_channels, num_frames, height, width]
182
+ # to [batch_size, num_frames, num_channels, height, width]
183
+ out = out.permute(0, 2, 1, 3, 4)
184
+ return out, feat_cache
185
+
186
+
187
+ class VAEDecoder3d(nn.Module):
188
+ def __init__(self,
189
+ dim=96,
190
+ z_dim=16,
191
+ dim_mult=[1, 2, 4, 4],
192
+ num_res_blocks=2,
193
+ attn_scales=[],
194
+ temperal_upsample=[True, True, False],
195
+ dropout=0.0):
196
+ super().__init__()
197
+ self.dim = dim
198
+ self.z_dim = z_dim
199
+ self.dim_mult = dim_mult
200
+ self.num_res_blocks = num_res_blocks
201
+ self.attn_scales = attn_scales
202
+ self.temperal_upsample = temperal_upsample
203
+ self.cache_t = 2
204
+ self.decoder_conv_num = 32
205
+
206
+ # dimensions
207
+ dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
208
+ scale = 1.0 / 2**(len(dim_mult) - 2)
209
+
210
+ # init block
211
+ self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
212
+
213
+ # middle blocks
214
+ self.middle = nn.Sequential(
215
+ ResidualBlock(dims[0], dims[0], dropout), AttentionBlock(dims[0]),
216
+ ResidualBlock(dims[0], dims[0], dropout))
217
+
218
+ # upsample blocks
219
+ upsamples = []
220
+ for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
221
+ # residual (+attention) blocks
222
+ if i == 1 or i == 2 or i == 3:
223
+ in_dim = in_dim // 2
224
+ for _ in range(num_res_blocks + 1):
225
+ upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
226
+ if scale in attn_scales:
227
+ upsamples.append(AttentionBlock(out_dim))
228
+ in_dim = out_dim
229
+
230
+ # upsample block
231
+ if i != len(dim_mult) - 1:
232
+ mode = 'upsample3d' if temperal_upsample[i] else 'upsample2d'
233
+ upsamples.append(Resample(out_dim, mode=mode))
234
+ scale *= 2.0
235
+ self.upsamples = nn.Sequential(*upsamples)
236
+
237
+ # output blocks
238
+ self.head = nn.Sequential(
239
+ RMS_norm(out_dim, images=False), nn.SiLU(),
240
+ CausalConv3d(out_dim, 3, 3, padding=1))
241
+
242
+ def forward(
243
+ self,
244
+ x: torch.Tensor,
245
+ feat_cache: List[torch.Tensor]
246
+ ):
247
+ feat_idx = [0]
248
+
249
+ # conv1
250
+ idx = feat_idx[0]
251
+ cache_x = x[:, :, -self.cache_t:, :, :].clone()
252
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
253
+ # cache last frame of last two chunk
254
+ cache_x = torch.cat([
255
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
256
+ cache_x.device), cache_x
257
+ ],
258
+ dim=2)
259
+ x = self.conv1(x, feat_cache[idx])
260
+ feat_cache[idx] = cache_x
261
+ feat_idx[0] += 1
262
+
263
+ # middle
264
+ for layer in self.middle:
265
+ if isinstance(layer, ResidualBlock) and feat_cache is not None:
266
+ x = layer(x, feat_cache, feat_idx)
267
+ else:
268
+ x = layer(x)
269
+
270
+ # upsamples
271
+ for layer in self.upsamples:
272
+ x = layer(x, feat_cache, feat_idx)
273
+
274
+ # head
275
+ for layer in self.head:
276
+ if isinstance(layer, CausalConv3d) and feat_cache is not None:
277
+ idx = feat_idx[0]
278
+ cache_x = x[:, :, -self.cache_t:, :, :].clone()
279
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
280
+ # cache last frame of last two chunk
281
+ cache_x = torch.cat([
282
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
283
+ cache_x.device), cache_x
284
+ ],
285
+ dim=2)
286
+ x = layer(x, feat_cache[idx])
287
+ feat_cache[idx] = cache_x
288
+ feat_idx[0] += 1
289
+ else:
290
+ x = layer(x)
291
+ return x, feat_cache
demo_utils/vae_torch2trt.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---- INT8 (optional) ----
2
+ from demo_utils.vae import (
3
+ VAEDecoderWrapperSingle, # main nn.Module
4
+ ZERO_VAE_CACHE # helper constants shipped with your code base
5
+ )
6
+ import pycuda.driver as cuda # ← add
7
+ import pycuda.autoinit # noqa
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import torch
13
+ import tensorrt as trt
14
+
15
+ from utils.dataset import ShardingLMDBDataset
16
+
17
+ data_path = "/mnt/localssd/wanx_14B_shift-3.0_cfg-5.0_lmdb_oneshard"
18
+ dataset = ShardingLMDBDataset(data_path, max_pair=int(1e8))
19
+ dataloader = torch.utils.data.DataLoader(
20
+ dataset,
21
+ batch_size=1,
22
+ num_workers=0
23
+ )
24
+
25
+ # ─────────────────────────────────────────────────────────
26
+ # 1️⃣ Bring the PyTorch model into scope
27
+ # (all code you pasted lives in `vae_decoder.py`)
28
+ # ─────────────────────────────────────────────────────────
29
+
30
+ # --- dummy tensors (exact shapes you posted) ---
31
+ dummy_input = torch.randn(1, 1, 16, 60, 104).half().cuda()
32
+ is_first_frame = torch.tensor([1.0], device="cuda", dtype=torch.float16)
33
+ dummy_cache_input = [
34
+ torch.randn(*s.shape).half().cuda() if isinstance(s, torch.Tensor) else s
35
+ for s in ZERO_VAE_CACHE # keep exactly the same ordering
36
+ ]
37
+ inputs = [dummy_input, is_first_frame, *dummy_cache_input]
38
+
39
+ # ─────────────────────────────────────────────────────────
40
+ # 2️⃣ Export → ONNX
41
+ # ─────────────────────────────────────────────────────────
42
+ model = VAEDecoderWrapperSingle().half().cuda().eval()
43
+
44
+ vae_state_dict = torch.load('wan_models/Wan2.1-T2V-1.3B/Wan2.1_VAE.pth', map_location="cpu")
45
+ decoder_state_dict = {}
46
+ for key, value in vae_state_dict.items():
47
+ if 'decoder.' in key or 'conv2' in key:
48
+ decoder_state_dict[key] = value
49
+ model.load_state_dict(decoder_state_dict)
50
+ model = model.half().cuda().eval() # only batch dim dynamic
51
+
52
+ onnx_path = Path("vae_decoder.onnx")
53
+ feat_names = [f"vae_cache_{i}" for i in range(len(dummy_cache_input))]
54
+ all_inputs_names = ["z", "use_cache"] + feat_names
55
+
56
+ with torch.inference_mode():
57
+ torch.onnx.export(
58
+ model,
59
+ tuple(inputs), # must be a tuple
60
+ onnx_path.as_posix(),
61
+ input_names=all_inputs_names,
62
+ output_names=["rgb_out", "cache_out"],
63
+ opset_version=17,
64
+ do_constant_folding=True,
65
+ dynamo=True
66
+ )
67
+ print(f"✅ ONNX graph saved to {onnx_path.resolve()}")
68
+
69
+ # (Optional) quick sanity-check with ONNX-Runtime
70
+ try:
71
+ import onnxruntime as ort
72
+ sess = ort.InferenceSession(onnx_path.as_posix(),
73
+ providers=["CUDAExecutionProvider"])
74
+ ort_inputs = {n: t.cpu().numpy() for n, t in zip(all_inputs_names, inputs)}
75
+ _ = sess.run(None, ort_inputs)
76
+ print("✅ ONNX graph is executable")
77
+ except Exception as e:
78
+ print("⚠️ ONNX check failed:", e)
79
+
80
+ # ─────────────────────────────────────────────────────────
81
+ # 3️⃣ Build the TensorRT engine
82
+ # ─────────────────────────────────────────────────────────
83
+ TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
84
+ builder = trt.Builder(TRT_LOGGER)
85
+ network = builder.create_network(
86
+ 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
87
+ parser = trt.OnnxParser(network, TRT_LOGGER)
88
+
89
+ with open(onnx_path, "rb") as f:
90
+ if not parser.parse(f.read()):
91
+ for i in range(parser.num_errors):
92
+ print(parser.get_error(i))
93
+ sys.exit("❌ ONNX → TRT parsing failed")
94
+
95
+ config = builder.create_builder_config()
96
+
97
+
98
+ def set_workspace(config, bytes_):
99
+ """Version-agnostic workspace limit."""
100
+ if hasattr(config, "max_workspace_size"): # TRT 8 / 9
101
+ config.max_workspace_size = bytes_
102
+ else: # TRT 10+
103
+ config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, bytes_)
104
+
105
+
106
+ # …
107
+ config = builder.create_builder_config()
108
+ set_workspace(config, 4 << 30) # 4 GB
109
+ # 4 GB
110
+
111
+ if builder.platform_has_fast_fp16:
112
+ config.set_flag(trt.BuilderFlag.FP16)
113
+
114
+ # ---- INT8 (optional) ----
115
+ # provide a calibrator if you need an INT8 engine; comment this
116
+ # block if you only care about FP16.
117
+ # ─────────────────────────────────────────────────────────
118
+ # helper: version-agnostic workspace limit
119
+ # ─────────────────────────────────────────────────────────
120
+
121
+
122
+ def set_workspace(config: trt.IBuilderConfig, bytes_: int = 4 << 30):
123
+ """
124
+ TRT < 10.x → config.max_workspace_size
125
+ TRT ≥ 10.x → config.set_memory_pool_limit(...)
126
+ """
127
+ if hasattr(config, "max_workspace_size"): # TRT 8 / 9
128
+ config.max_workspace_size = bytes_
129
+ else: # TRT 10+
130
+ config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE,
131
+ bytes_)
132
+
133
+ # ─────────────────────────────────────────────────────────
134
+ # (optional) INT-8 calibrator
135
+ # ─────────────────────────────────────────────────────────
136
+ # ‼ Only keep this block if you really need INT-8 ‼ # gracefully skip if PyCUDA not present
137
+
138
+
139
+ class VAECalibrator(trt.IInt8EntropyCalibrator2):
140
+ def __init__(self, loader, cache="calibration.cache", max_batches=10):
141
+ super().__init__()
142
+ self.loader = iter(loader)
143
+ self.batch_size = loader.batch_size or 1
144
+ self.max_batches = max_batches
145
+ self.count = 0
146
+ self.cache_file = cache
147
+ self.stream = cuda.Stream()
148
+ self.dev_ptrs = {}
149
+
150
+ # --- TRT 10 needs BOTH spellings ---
151
+ def get_batch_size(self):
152
+ return self.batch_size
153
+
154
+ def getBatchSize(self):
155
+ return self.batch_size
156
+
157
+ def get_batch(self, names):
158
+ if self.count >= self.max_batches:
159
+ return None
160
+
161
+ # Randomly sample a number from 1 to 10
162
+ import random
163
+ vae_idx = random.randint(0, 10)
164
+ data = next(self.loader)
165
+
166
+ latent = data['ode_latent'][0][:, :1]
167
+ is_first_frame = torch.tensor([1.0], device="cuda", dtype=torch.float16)
168
+ feat_cache = ZERO_VAE_CACHE
169
+ for i in range(vae_idx):
170
+ inputs = [latent, is_first_frame, *feat_cache]
171
+ with torch.inference_mode():
172
+ outputs = model(*inputs)
173
+ latent = data['ode_latent'][0][:, i + 1:i + 2]
174
+ is_first_frame = torch.tensor([0.0], device="cuda", dtype=torch.float16)
175
+ feat_cache = outputs[1:]
176
+
177
+ # -------- ensure context is current --------
178
+ z_np = latent.cpu().numpy().astype('float32')
179
+
180
+ ptrs = [] # list[int] – one entry per name
181
+ for name in names: # <-- match TRT's binding order
182
+ if name == "z":
183
+ arr = z_np
184
+ elif name == "use_cache":
185
+ arr = is_first_frame.cpu().numpy().astype('float32')
186
+ else:
187
+ idx = int(name.split('_')[-1]) # "vae_cache_17" -> 17
188
+ arr = feat_cache[idx].cpu().numpy().astype('float32')
189
+
190
+ if name not in self.dev_ptrs:
191
+ self.dev_ptrs[name] = cuda.mem_alloc(arr.nbytes)
192
+
193
+ cuda.memcpy_htod_async(self.dev_ptrs[name], arr, self.stream)
194
+ ptrs.append(int(self.dev_ptrs[name])) # ***int() is required***
195
+
196
+ self.stream.synchronize()
197
+ self.count += 1
198
+ print(f"Calibration batch {self.count}/{self.max_batches}")
199
+ return ptrs
200
+
201
+ # --- calibration-cache helpers (both spellings) ---
202
+ def read_calibration_cache(self):
203
+ try:
204
+ with open(self.cache_file, "rb") as f:
205
+ return f.read()
206
+ except FileNotFoundError:
207
+ return None
208
+
209
+ def readCalibrationCache(self):
210
+ return self.read_calibration_cache()
211
+
212
+ def write_calibration_cache(self, cache):
213
+ with open(self.cache_file, "wb") as f:
214
+ f.write(cache)
215
+
216
+ def writeCalibrationCache(self, cache):
217
+ self.write_calibration_cache(cache)
218
+
219
+
220
+ # ─────────────────────────────────────────────────────────
221
+ # Builder-config + optimisation profile
222
+ # ─────────────────────────────────────────────────────────
223
+ config = builder.create_builder_config()
224
+ set_workspace(config, 4 << 30) # 4 GB
225
+
226
+ # ► enable FP16 if possible
227
+ if builder.platform_has_fast_fp16:
228
+ config.set_flag(trt.BuilderFlag.FP16)
229
+
230
+ # ► enable INT-8 (delete this block if you don’t need it)
231
+ if cuda is not None:
232
+ config.set_flag(trt.BuilderFlag.INT8)
233
+ # supply any representative batch you like – here we reuse the latent z
234
+ calib = VAECalibrator(dataloader)
235
+ # TRT-10 renamed the setter:
236
+ if hasattr(config, "set_int8_calibrator"): # TRT 10+
237
+ config.set_int8_calibrator(calib)
238
+ else: # TRT ≤ 9
239
+ config.int8_calibrator = calib
240
+
241
+ # ---- optimisation profile ----
242
+ profile = builder.create_optimization_profile()
243
+ profile.set_shape(all_inputs_names[0], # latent z
244
+ min=(1, 1, 16, 60, 104),
245
+ opt=(1, 1, 16, 60, 104),
246
+ max=(1, 1, 16, 60, 104))
247
+ profile.set_shape("use_cache", # scalar flag
248
+ min=(1,), opt=(1,), max=(1,))
249
+ for name, tensor in zip(all_inputs_names[2:], dummy_cache_input):
250
+ profile.set_shape(name, tensor.shape, tensor.shape, tensor.shape)
251
+
252
+ config.add_optimization_profile(profile)
253
+
254
+ # ─────────────────────────────────────────────────────────
255
+ # Build the engine (API changed in TRT-10)
256
+ # ─────────────────────────────────────────────────────────
257
+ print("⚙️ Building engine … (can take a minute)")
258
+
259
+ if hasattr(builder, "build_serialized_network"): # TRT 10+
260
+ serialized_engine = builder.build_serialized_network(network, config)
261
+ assert serialized_engine is not None, "build_serialized_network() failed"
262
+ plan_path = Path("checkpoints/vae_decoder_int8.trt")
263
+ plan_path.write_bytes(serialized_engine)
264
+ engine_bytes = serialized_engine # keep for smoke-test
265
+ else: # TRT ≤ 9
266
+ engine = builder.build_engine(network, config)
267
+ assert engine is not None, "build_engine() returned None"
268
+ plan_path = Path("checkpoints/vae_decoder_int8.trt")
269
+ plan_path.write_bytes(engine.serialize())
270
+ engine_bytes = engine.serialize()
271
+
272
+ print(f"✅ TensorRT engine written to {plan_path.resolve()}")
273
+
274
+ # ─────────────────────────────────────────────────────────
275
+ # 4️⃣ Quick smoke-test with the brand-new engine
276
+ # ─────────────────────────────────────────────────────────
277
+ with trt.Runtime(TRT_LOGGER) as rt:
278
+ engine = rt.deserialize_cuda_engine(engine_bytes)
279
+ context = engine.create_execution_context()
280
+ stream = torch.cuda.current_stream().cuda_stream
281
+
282
+ # pre-allocate device buffers once
283
+ device_buffers, outputs = {}, []
284
+ dtype_map = {trt.float32: torch.float32,
285
+ trt.float16: torch.float16,
286
+ trt.int8: torch.int8,
287
+ trt.int32: torch.int32}
288
+
289
+ for name, tensor in zip(all_inputs_names, inputs):
290
+ if -1 in engine.get_tensor_shape(name): # dynamic input
291
+ context.set_input_shape(name, tensor.shape)
292
+ context.set_tensor_address(name, int(tensor.data_ptr()))
293
+ device_buffers[name] = tensor
294
+
295
+ context.infer_shapes() # propagate ⇢ outputs
296
+ for i in range(engine.num_io_tensors):
297
+ name = engine.get_tensor_name(i)
298
+ if engine.get_tensor_mode(name) == trt.TensorIOMode.OUTPUT:
299
+ shape = tuple(context.get_tensor_shape(name))
300
+ dtype = dtype_map[engine.get_tensor_dtype(name)]
301
+ out = torch.empty(shape, dtype=dtype, device="cuda")
302
+ context.set_tensor_address(name, int(out.data_ptr()))
303
+ outputs.append(out)
304
+ print(f"output {name} shape: {shape}")
305
+
306
+ context.execute_async_v3(stream_handle=stream)
307
+ torch.cuda.current_stream().synchronize()
308
+ print("✅ TRT execution OK – first output shape:", outputs[0].shape)
get_causal_ode_data_chunkwise.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
2
+ from utils.scheduler import FlowMatchScheduler
3
+ from utils.distributed import launch_distributed_job
4
+
5
+ import torch.distributed as dist
6
+ from tqdm import tqdm
7
+ import argparse
8
+ import torch
9
+ import math
10
+ import os
11
+ from utils.dataset import LatentLMDBDataset
12
+
13
+ def init_model(device):
14
+ model = WanDiffusionWrapper(is_causal=True).to(device).to(torch.float32)
15
+ model.model.num_frame_per_block = 3 # !!
16
+ encoder = WanTextEncoder().to(device).to(torch.float32)
17
+
18
+
19
+ scheduler = FlowMatchScheduler(shift=5.0, sigma_min=0.0, extra_one_step=True)
20
+ scheduler.set_timesteps(num_inference_steps=48, denoising_strength=1.0)
21
+ scheduler.sigmas = scheduler.sigmas.to(device)
22
+
23
+ sample_neg_prompt = '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
24
+
25
+ unconditional_dict = encoder(
26
+ text_prompts=[sample_neg_prompt]
27
+ )
28
+
29
+ return model, encoder, scheduler, unconditional_dict
30
+
31
+
32
+ def main():
33
+ parser = argparse.ArgumentParser()
34
+ parser.add_argument("--local_rank", type=int, default=-1)
35
+ parser.add_argument("--output_folder", type=str)
36
+ parser.add_argument("--rawdata_path", type=str)
37
+ parser.add_argument("--generator_ckpt", type=str)
38
+ parser.add_argument("--guidance_scale", type=float, default=6.0)
39
+
40
+
41
+ args = parser.parse_args()
42
+
43
+ launch_distributed_job()
44
+ global_rank = dist.get_rank()
45
+
46
+ device = torch.cuda.current_device()
47
+
48
+ torch.set_grad_enabled(False)
49
+ torch.backends.cuda.matmul.allow_tf32 = True
50
+ torch.backends.cudnn.allow_tf32 = True
51
+
52
+ model, encoder, scheduler, unconditional_dict = init_model(device=device)
53
+ state_dict = torch.load(args.generator_ckpt, map_location="cpu")
54
+
55
+ gen_sd = state_dict["generator"]
56
+ fixed = {}
57
+ for k, v in gen_sd.items():
58
+ if k.startswith("model._fsdp_wrapped_module."):
59
+ k = k.replace("model._fsdp_wrapped_module.", "", 1)
60
+ if k.startswith("model."):
61
+ k = k.replace("model.", "", 1)
62
+ fixed[k] = v
63
+ state_dict = fixed
64
+ model.model.load_state_dict(
65
+ state_dict, strict=True
66
+ )
67
+
68
+
69
+
70
+ dataset = LatentLMDBDataset(args.rawdata_path)
71
+
72
+ if global_rank == 0:
73
+ os.makedirs(args.output_folder, exist_ok=True)
74
+
75
+ total_steps = int(math.ceil(len(dataset) / dist.get_world_size()))
76
+ for index in tqdm(
77
+ range(total_steps), disable=(dist.get_rank() != 0),
78
+ ):
79
+ prompt_index = index * dist.get_world_size() + dist.get_rank()
80
+ if prompt_index >= len(dataset):
81
+ continue
82
+ sample = dataset[prompt_index]
83
+ prompt = sample["prompts"]
84
+
85
+ clean_latent = sample["clean_latent"].to(device).unsqueeze(0)
86
+
87
+
88
+
89
+ conditional_dict = encoder(
90
+ text_prompts=prompt
91
+ )
92
+
93
+ latents = torch.randn(
94
+ [1, 21, 16, 60, 104], dtype=torch.float32, device=device
95
+ )
96
+
97
+ noisy_input = []
98
+
99
+ for progress_id, t in enumerate(tqdm(scheduler.timesteps, disable=(dist.get_rank() != 0))):
100
+ timestep = t * \
101
+ torch.ones([1, 21], device=device, dtype=torch.float32)
102
+ noisy_input.append(latents)
103
+ f_cond, x0_pred_cond = model(
104
+ latents, conditional_dict, timestep, clean_x = clean_latent
105
+ )
106
+
107
+ f_uncond, x0_pred_uncond = model(
108
+ latents, unconditional_dict, timestep, clean_x = clean_latent
109
+ )
110
+
111
+ flow_pred = f_uncond + args.guidance_scale * (
112
+ f_cond - f_uncond
113
+ )
114
+
115
+
116
+ latents = scheduler.step(
117
+ flow_pred.flatten(0, 1),
118
+ timestep.flatten(0, 1),
119
+ latents.flatten(0, 1)
120
+ ).unflatten(dim=0, sizes=flow_pred.shape[:2])
121
+
122
+ noisy_input.append(latents)
123
+ noisy_input.append(clean_latent)
124
+
125
+ noisy_inputs = torch.stack(noisy_input, dim=1)
126
+
127
+ noisy_inputs = noisy_inputs[:, [0, 12, 24, 36, -2, -1]]
128
+
129
+ stored_data = noisy_inputs
130
+
131
+ torch.save(
132
+ {prompt: stored_data.cpu().detach()},
133
+ os.path.join(args.output_folder, f"{prompt_index:05d}.pt")
134
+ )
135
+
136
+ dist.barrier()
137
+
138
+
139
+ if __name__ == "__main__":
140
+ main()
get_causal_ode_data_framewise.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
2
+ from utils.scheduler import FlowMatchScheduler
3
+ from utils.distributed import launch_distributed_job
4
+
5
+ import torch.distributed as dist
6
+ from tqdm import tqdm
7
+ import argparse
8
+ import torch
9
+ import math
10
+ import os
11
+ from utils.dataset import LatentLMDBDataset
12
+
13
+ def init_model(device):
14
+ model = WanDiffusionWrapper(is_causal=True).to(device).to(torch.float32)
15
+ model.model.num_frame_per_block = 1 # !!
16
+ encoder = WanTextEncoder().to(device).to(torch.float32)
17
+
18
+
19
+ scheduler = FlowMatchScheduler(shift=5.0, sigma_min=0.0, extra_one_step=True)
20
+ scheduler.set_timesteps(num_inference_steps=48, denoising_strength=1.0)
21
+ scheduler.sigmas = scheduler.sigmas.to(device)
22
+
23
+ sample_neg_prompt = '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
24
+
25
+ unconditional_dict = encoder(
26
+ text_prompts=[sample_neg_prompt]
27
+ )
28
+
29
+ return model, encoder, scheduler, unconditional_dict
30
+
31
+
32
+ def main():
33
+ parser = argparse.ArgumentParser()
34
+ parser.add_argument("--local_rank", type=int, default=-1)
35
+ parser.add_argument("--output_folder", type=str)
36
+ parser.add_argument("--rawdata_path", type=str)
37
+ parser.add_argument("--generator_ckpt", type=str)
38
+ parser.add_argument("--guidance_scale", type=float, default=6.0)
39
+
40
+
41
+ args = parser.parse_args()
42
+
43
+ launch_distributed_job()
44
+ global_rank = dist.get_rank()
45
+
46
+ device = torch.cuda.current_device()
47
+
48
+ torch.set_grad_enabled(False)
49
+ torch.backends.cuda.matmul.allow_tf32 = True
50
+ torch.backends.cudnn.allow_tf32 = True
51
+
52
+ model, encoder, scheduler, unconditional_dict = init_model(device=device)
53
+ state_dict = torch.load(args.generator_ckpt, map_location="cpu")
54
+
55
+ gen_sd = state_dict["generator"]
56
+ fixed = {}
57
+ for k, v in gen_sd.items():
58
+ if k.startswith("model._fsdp_wrapped_module."):
59
+ k = k.replace("model._fsdp_wrapped_module.", "", 1)
60
+ if k.startswith("model."):
61
+ k = k.replace("model.", "", 1)
62
+ fixed[k] = v
63
+ state_dict = fixed
64
+ model.model.load_state_dict(
65
+ state_dict, strict=True
66
+ )
67
+
68
+
69
+
70
+ dataset = LatentLMDBDataset(args.rawdata_path)
71
+
72
+ if global_rank == 0:
73
+ os.makedirs(args.output_folder, exist_ok=True)
74
+
75
+ total_steps = int(math.ceil(len(dataset) / dist.get_world_size()))
76
+ for index in tqdm(
77
+ range(total_steps), disable=(dist.get_rank() != 0),
78
+ ):
79
+ prompt_index = index * dist.get_world_size() + dist.get_rank()
80
+ if prompt_index >= len(dataset):
81
+ continue
82
+ sample = dataset[prompt_index]
83
+ prompt = sample["prompts"]
84
+
85
+ clean_latent = sample["clean_latent"].to(device).unsqueeze(0)
86
+
87
+
88
+
89
+ conditional_dict = encoder(
90
+ text_prompts=prompt
91
+ )
92
+
93
+ latents = torch.randn(
94
+ [1, 21, 16, 60, 104], dtype=torch.float32, device=device
95
+ )
96
+
97
+ noisy_input = []
98
+
99
+ for progress_id, t in enumerate(tqdm(scheduler.timesteps, disable=(dist.get_rank() != 0))):
100
+ timestep = t * \
101
+ torch.ones([1, 21], device=device, dtype=torch.float32)
102
+ noisy_input.append(latents)
103
+ f_cond, x0_pred_cond = model(
104
+ latents, conditional_dict, timestep, clean_x = clean_latent
105
+ )
106
+
107
+ f_uncond, x0_pred_uncond = model(
108
+ latents, unconditional_dict, timestep, clean_x = clean_latent
109
+ )
110
+
111
+ flow_pred = f_uncond + args.guidance_scale * (
112
+ f_cond - f_uncond
113
+ )
114
+
115
+
116
+ latents = scheduler.step(
117
+ flow_pred.flatten(0, 1),
118
+ timestep.flatten(0, 1),
119
+ latents.flatten(0, 1)
120
+ ).unflatten(dim=0, sizes=flow_pred.shape[:2])
121
+
122
+ noisy_input.append(latents)
123
+ noisy_input.append(clean_latent)
124
+
125
+ noisy_inputs = torch.stack(noisy_input, dim=1)
126
+
127
+ noisy_inputs = noisy_inputs[:, [0, 12, 24, 36, -2, -1]]
128
+
129
+ stored_data = noisy_inputs
130
+
131
+ torch.save(
132
+ {prompt: stored_data.cpu().detach()},
133
+ os.path.join(args.output_folder, f"{prompt_index:05d}.pt")
134
+ )
135
+
136
+ dist.barrier()
137
+
138
+
139
+ if __name__ == "__main__":
140
+ main()
get_causal_ode_data_kv_optimized.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import math
3
+ import os
4
+
5
+ import torch
6
+ import torch.distributed as dist
7
+ from tqdm import tqdm
8
+
9
+ from utils.dataset import LatentLMDBDataset
10
+ from utils.distributed import launch_distributed_job
11
+ from utils.ode_generation import (
12
+ CausalODETrajectoryGenerator,
13
+ merge_cfg_prompt_embeds,
14
+ )
15
+ from utils.scheduler import FlowMatchScheduler
16
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder
17
+
18
+
19
+ DEFAULT_NEGATIVE_PROMPT = (
20
+ "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,"
21
+ "最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,"
22
+ "画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,"
23
+ "杂乱的背景,三条腿,背景人很多,倒着走"
24
+ )
25
+ DEFAULT_NUM_INFERENCE_STEPS = 48
26
+ DEFAULT_SCHEDULER_SHIFT = 5.0
27
+ DEFAULT_TARGET_NUM_FRAMES = 21
28
+ DEFAULT_TRAJECTORY_INDICES = [0, 12, 24, 36, -2, -1]
29
+
30
+
31
+ def normalize_generator_state_dict(state_dict: dict) -> dict:
32
+ if "generator" in state_dict:
33
+ state_dict = state_dict["generator"]
34
+ elif "generator_ema" in state_dict:
35
+ state_dict = state_dict["generator_ema"]
36
+
37
+ fixed = {}
38
+ for k, v in state_dict.items():
39
+ if k.startswith("model._fsdp_wrapped_module."):
40
+ k = k.replace("model._fsdp_wrapped_module.", "", 1)
41
+ if k.startswith("model."):
42
+ k = k.replace("model.", "", 1)
43
+ fixed[k] = v
44
+ return fixed
45
+
46
+
47
+ def init_model(
48
+ device,
49
+ num_frame_per_block: int,
50
+ scheduler_shift: float,
51
+ num_inference_steps: int,
52
+ negative_prompt: str,
53
+ ):
54
+ model = WanDiffusionWrapper(is_causal=True).to(device).to(torch.float32)
55
+ model.model.num_frame_per_block = num_frame_per_block
56
+ encoder = WanTextEncoder().to(device).to(torch.float32)
57
+
58
+ scheduler = FlowMatchScheduler(
59
+ shift=scheduler_shift,
60
+ sigma_min=0.0,
61
+ extra_one_step=True,
62
+ )
63
+ scheduler.set_timesteps(
64
+ num_inference_steps=num_inference_steps,
65
+ denoising_strength=1.0,
66
+ )
67
+ scheduler.sigmas = scheduler.sigmas.to(device)
68
+
69
+ unconditional_dict = encoder(text_prompts=[negative_prompt])
70
+ return model, encoder, scheduler, unconditional_dict
71
+
72
+
73
+ def prepare_clean_latent(
74
+ sample: dict,
75
+ target_num_frames: int | None,
76
+ device,
77
+ ) -> torch.Tensor:
78
+ clean_latent = sample["clean_latent"].to(device).unsqueeze(0)
79
+ if target_num_frames is None:
80
+ return clean_latent
81
+
82
+ if clean_latent.shape[1] < target_num_frames:
83
+ raise ValueError(
84
+ "clean_latent has fewer frames than requested: "
85
+ f"{clean_latent.shape[1]} < {target_num_frames}"
86
+ )
87
+ if clean_latent.shape[1] != target_num_frames:
88
+ clean_latent = clean_latent[:, :target_num_frames, ...]
89
+ return clean_latent.contiguous()
90
+
91
+
92
+ def main():
93
+ parser = argparse.ArgumentParser()
94
+ parser.add_argument("--local_rank", type=int, default=-1)
95
+ parser.add_argument("--output_folder", type=str, required=True)
96
+ parser.add_argument("--rawdata_path", type=str, required=True)
97
+ parser.add_argument("--generator_ckpt", type=str, required=True)
98
+ parser.add_argument("--num_frames_per_chunk", type=int, required=True)
99
+ parser.add_argument("--guidance_scale", type=float, default=6.0)
100
+ parser.add_argument(
101
+ "--generation_mode",
102
+ type=str,
103
+ default="full",
104
+ choices=["full", "blockwise_kv"],
105
+ )
106
+
107
+ args = parser.parse_args()
108
+
109
+ launch_distributed_job()
110
+ global_rank = dist.get_rank()
111
+
112
+ device = torch.cuda.current_device()
113
+
114
+ torch.set_grad_enabled(False)
115
+ torch.backends.cuda.matmul.allow_tf32 = True
116
+ torch.backends.cudnn.allow_tf32 = True
117
+
118
+ model, encoder, scheduler, unconditional_dict = init_model(
119
+ device=device,
120
+ num_frame_per_block=args.num_frame_per_block,
121
+ scheduler_shift=DEFAULT_SCHEDULER_SHIFT,
122
+ num_inference_steps=DEFAULT_NUM_INFERENCE_STEPS,
123
+ negative_prompt=DEFAULT_NEGATIVE_PROMPT,
124
+ )
125
+ state_dict = torch.load(args.generator_ckpt, map_location="cpu")
126
+ model.model.load_state_dict(
127
+ normalize_generator_state_dict(state_dict),
128
+ strict=True,
129
+ )
130
+
131
+ dataset = LatentLMDBDataset(
132
+ args.rawdata_path,
133
+ max_pair=int(1e8),
134
+ )
135
+
136
+ if global_rank == 0:
137
+ os.makedirs(args.output_folder, exist_ok=True)
138
+
139
+ trajectory_generator = CausalODETrajectoryGenerator(
140
+ model=model,
141
+ scheduler=scheduler,
142
+ num_frame_per_block=args.num_frame_per_block,
143
+ num_inference_steps=DEFAULT_NUM_INFERENCE_STEPS,
144
+ guidance_scale=args.guidance_scale,
145
+ )
146
+
147
+ total_steps = int(math.ceil(len(dataset) / dist.get_world_size()))
148
+ for index in tqdm(
149
+ range(total_steps), disable=(global_rank != 0),
150
+ ):
151
+ prompt_index = index * dist.get_world_size() + global_rank
152
+ if prompt_index >= len(dataset):
153
+ continue
154
+
155
+ output_path = os.path.join(args.output_folder, f"{prompt_index:05d}.pt")
156
+
157
+ sample = dataset[prompt_index]
158
+ prompt = sample["prompts"]
159
+ clean_latent = prepare_clean_latent(
160
+ sample=sample,
161
+ target_num_frames=DEFAULT_TARGET_NUM_FRAMES,
162
+ device=device,
163
+ )
164
+
165
+ conditional_dict = encoder(text_prompts=[prompt])
166
+ paired_conditional_dict = merge_cfg_prompt_embeds(
167
+ conditional_dict=conditional_dict,
168
+ unconditional_dict=unconditional_dict,
169
+ )
170
+ initial_noise = torch.randn_like(clean_latent, dtype=torch.float32)
171
+ stored_data = trajectory_generator.generate(
172
+ clean_latent=clean_latent,
173
+ paired_conditional_dict=paired_conditional_dict,
174
+ trajectory_indices=DEFAULT_TRAJECTORY_INDICES,
175
+ generation_mode=args.generation_mode,
176
+ initial_noise=initial_noise,
177
+ )
178
+
179
+ torch.save(
180
+ {prompt: stored_data.cpu().detach()},
181
+ output_path,
182
+ )
183
+
184
+ dist.barrier()
185
+
186
+
187
+ if __name__ == "__main__":
188
+ main()
inference.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import argparse
3
+ import torch
4
+ import os
5
+ from omegaconf import OmegaConf
6
+ from tqdm import tqdm
7
+ from torchvision import transforms
8
+ from torchvision.io import write_video
9
+ from einops import rearrange
10
+ import torch.distributed as dist
11
+ from torch.utils.data import DataLoader, SequentialSampler
12
+ from torch.utils.data.distributed import DistributedSampler
13
+ import json
14
+
15
+ from pipeline import (
16
+ CausalDiffusionInferencePipeline,
17
+ CausalInferencePipeline,
18
+ )
19
+ from utils.dataset import TextDataset, TextImagePairDataset
20
+ from utils.misc import set_seed
21
+
22
+ from demo_utils.memory import gpu, get_cuda_free_memory_gb, DynamicSwapInstaller
23
+
24
+ parser = argparse.ArgumentParser()
25
+ parser.add_argument("--config_path", type=str, help="Path to the config file")
26
+ parser.add_argument("--checkpoint_path", type=str, help="Path to the checkpoint folder")
27
+ parser.add_argument("--data_path", type=str, help="Path to the dataset")
28
+ parser.add_argument("--output_folder", type=str, help="Output folder")
29
+ parser.add_argument("--num_output_frames", type=int, default=21, help="Number of overlap frames between sliding windows")
30
+ parser.add_argument("--use_ema", action="store_true", help="Whether to use EMA parameters")
31
+ parser.add_argument("--seed", type=int, default=0, help="Random seed")
32
+ parser.add_argument("--i2v", action="store_true", help="Whether to perform I2V (or T2V by default)")
33
+ parser.add_argument("--report_timing", action="store_true",
34
+ help="Only tested on A800, for the Causal Forcing++ latency. Not make claims for other hardware like H100. For the result on H100, refer to the reported results in the Self Forcing paper.")
35
+ args = parser.parse_args()
36
+
37
+ # Initialize distributed inference
38
+ if "LOCAL_RANK" in os.environ:
39
+ dist.init_process_group(backend='nccl')
40
+ local_rank = int(os.environ["LOCAL_RANK"])
41
+ torch.cuda.set_device(local_rank)
42
+ device = torch.device(f"cuda:{local_rank}")
43
+ world_size = dist.get_world_size()
44
+
45
+ else:
46
+ device = torch.device("cuda")
47
+ local_rank = 0
48
+ world_size = 1
49
+
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
+ key = 'generator_ema' if args.use_ema else 'generator'
72
+ gen_sd = state_dict[key]
73
+
74
+ try:
75
+ pipeline.generator.load_state_dict(gen_sd)
76
+ except RuntimeError:
77
+ fixed = {}
78
+ for k, v in gen_sd.items():
79
+ if k.startswith("model._fsdp_wrapped_module."):
80
+ k = k.replace("model._fsdp_wrapped_module.", "model.", 1)
81
+ fixed[k] = v
82
+ pipeline.generator.load_state_dict(fixed, strict=False)
83
+
84
+ pipeline = pipeline.to(dtype=torch.bfloat16)
85
+ if low_memory:
86
+ DynamicSwapInstaller.install_model(pipeline.text_encoder, device=gpu)
87
+ else:
88
+ pipeline.text_encoder.to(device=gpu)
89
+ pipeline.generator.to(device=gpu)
90
+ pipeline.vae.to(device=gpu)
91
+
92
+
93
+ # Create dataset
94
+ if args.i2v:
95
+ assert not dist.is_initialized(), "I2V does not support distributed inference yet"
96
+ transform = transforms.Compose([
97
+ transforms.Resize((480, 832)),
98
+ transforms.ToTensor(),
99
+ transforms.Normalize([0.5], [0.5])
100
+ ])
101
+ dataset = TextImagePairDataset(args.data_path, transform=transform)
102
+ else:
103
+ dataset = TextDataset(prompt_path=args.data_path)
104
+ num_prompts = len(dataset)
105
+ print(f"Number of prompts: {num_prompts}")
106
+
107
+ if args.report_timing and num_prompts < 2:
108
+ print(f"[WARN] --report_timing requires at least 2 prompts "
109
+ f"(got {num_prompts}); timing disabled.")
110
+ args.report_timing = False
111
+
112
+ if dist.is_initialized():
113
+ sampler = DistributedSampler(dataset, shuffle=False, drop_last=True)
114
+ else:
115
+ sampler = SequentialSampler(dataset)
116
+ dataloader = DataLoader(dataset, batch_size=1, sampler=sampler, num_workers=0, drop_last=False)
117
+
118
+ # Create output directory (only on main process to avoid race conditions)
119
+ if local_rank == 0:
120
+ os.makedirs(args.output_folder, exist_ok=True)
121
+
122
+ if dist.is_initialized():
123
+ dist.barrier()
124
+
125
+ def encode(self, videos: torch.Tensor) -> torch.Tensor:
126
+ device, dtype = videos[0].device, videos[0].dtype
127
+ scale = [self.mean.to(device=device, dtype=dtype),
128
+ 1.0 / self.std.to(device=device, dtype=dtype)]
129
+ output = [
130
+ self.model.encode(u.unsqueeze(0), scale).float().squeeze(0)
131
+ for u in videos
132
+ ]
133
+
134
+ output = torch.stack(output, dim=0)
135
+ return output
136
+
137
+
138
+ for i, batch_data in tqdm(enumerate(dataloader), disable=(local_rank != 0)):
139
+ idx = batch_data['idx'].item()
140
+
141
+ if isinstance(batch_data, dict):
142
+ batch = batch_data
143
+ elif isinstance(batch_data, list):
144
+ batch = batch_data[0] # First (and only) item in the batch
145
+
146
+ all_video = []
147
+ num_generated_frames = 0 # Number of generated (latent) frames
148
+
149
+
150
+ if args.i2v:
151
+ assert config.num_frame_per_block == 1, "Current I2V only supports the frame-wise model."
152
+ # For image-to-video, batch contains image and caption
153
+ prompt = batch['prompts'][0] # Get caption from batch
154
+ output_path = os.path.join(args.output_folder, f'{prompt[:100]}.mp4')
155
+ if os.path.exists(output_path):
156
+ print('Video has been generated. Pass!')
157
+ continue
158
+ # Process the image
159
+ image = batch['image'].squeeze(0).unsqueeze(0).unsqueeze(2).to(device=device, dtype=torch.bfloat16)
160
+
161
+ # Encode the input image as the first latent
162
+ initial_latent = pipeline.vae.encode_to_latent(image).to(device=device, dtype=torch.bfloat16)
163
+ prompts = [prompt]
164
+ sampled_noise = torch.randn(
165
+ [1, args.num_output_frames - 1, 16, 60, 104], device=device, dtype=torch.bfloat16
166
+ )
167
+ else:
168
+ # For text-to-video, batch is just the text prompt
169
+ prompt = batch['prompts'][0]
170
+ output_path = os.path.join(args.output_folder, f'{prompt[:100]}.mp4')
171
+ if os.path.exists(output_path):
172
+ print('Video has been generated. Pass!')
173
+ continue
174
+ extended_prompt = batch['extended_prompts'][0] if 'extended_prompts' in batch else None
175
+ if extended_prompt is not None:
176
+ prompts = [extended_prompt]
177
+ else:
178
+ prompts = [prompt]
179
+
180
+ initial_latent = None
181
+ sampled_noise = torch.randn(
182
+ [1, args.num_output_frames, 16, 60, 104], device=device, dtype=torch.bfloat16
183
+ )
184
+
185
+ sample_report_timing = args.report_timing and i >= 1
186
+ video, latents = pipeline.inference(
187
+ noise=sampled_noise,
188
+ text_prompts=prompts,
189
+ return_latents=True,
190
+ initial_latent=initial_latent,
191
+ report_timing=sample_report_timing,
192
+ )
193
+ if sample_report_timing:
194
+ latency = pipeline.first_chunk_time
195
+ elapsed = pipeline.last_generation_time
196
+ num_pixel_frames = video.shape[1]
197
+ fps = num_pixel_frames / elapsed if elapsed > 0 else float('inf')
198
+ print(f"[Sample {i}] {num_pixel_frames} frames, "
199
+ f"latency ↓ {latency:.2f}s, FPS ↑ {fps:.2f}")
200
+ # Only tested on A800, for the Causal Forcing++ paper latency & throughput.
201
+ # Not make claims for other hardware like H100.
202
+ # For the result on H100, refer to the reported results in the Self Forcing paper.
203
+ # We do not guarantee that our FPS/latency measurement protocol is identical to that used in the Self Forcing paper.
204
+ current_video = rearrange(video, 'b t c h w -> b t h w c').cpu()
205
+ all_video.append(current_video)
206
+ num_generated_frames += latents.shape[1]
207
+
208
+ # Final output video
209
+ clean_latent = latents[0].cpu()
210
+ video = 255.0 * torch.cat(all_video, dim=1)
211
+
212
+ # Clear VAE cache
213
+ pipeline.vae.model.clear_cache()
214
+
215
+ output_path = os.path.join(args.output_folder, f'{prompt[:100]}.mp4')
216
+ write_video(output_path, video[0], fps=16)
217
+
218
+
long_video/LICENSE ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Tencent is pleased to support the community by making RollingForcing available.
2
+
3
+ Copyright (C) 2025 Tencent. All rights reserved.
4
+
5
+ The open-source software and/or models included in this distribution may have been modified by Tencent (“Tencent Modifications”). All Tencent Modifications are Copyright (C) Tencent.
6
+
7
+ RollingForcing is licensed under the License Terms of RollingForcing, except for the third-party components listed below, which remain licensed under their respective original terms. RollingForcing does not impose any additional restrictions beyond those specified in the original licenses of these third-party components. Users are required to comply with all applicable terms and conditions of the original licenses and to ensure that the use of these third-party components conforms to all relevant laws and regulations.
8
+
9
+ For the avoidance of doubt, RollingForcing refers solely to training code, inference code, parameters, and weights made publicly available by Tencent in accordance with the License Terms of RollingForcing.
10
+
11
+ Terms of the License Terms of RollingForcing:
12
+ --------------------------------------------------------------------
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and /or sublicense copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
14
+
15
+ - You agree to use RollingForcing only for academic purposes, and refrain from using it for any commercial or production purposes under any circumstances.
16
+
17
+ - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+
21
+
22
+
23
+ Dependencies and Licenses:
24
+
25
+ This open-source project, RollingForcing, builds upon the following open-source models and/or software components, each of which remains licensed under its original license. Certain models or software may include modifications made by Tencent (“Tencent Modifications”), which are Copyright (C) Tencent.
26
+
27
+ In case you believe there have been errors in the attribution below, you may submit the concerns to us for review and correction.
28
+
29
+ Open Source Model Licensed under the Apache-2.0:
30
+ --------------------------------------------------------------------
31
+ 1. Wan-AI/Wan2.1-T2V-1.3B
32
+ Copyright (c) 2025 Wan Team
33
+
34
+ Terms of the Apache-2.0:
35
+ --------------------------------------------------------------------
36
+ Apache License
37
+ Version 2.0, January 2004
38
+ http://www.apache.org/licenses/
39
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
40
+
41
+ Definitions.
42
+
43
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
44
+
45
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
46
+
47
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
48
+
49
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
50
+
51
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
52
+
53
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
54
+
55
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
56
+
57
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
58
+
59
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
60
+
61
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
62
+
63
+ Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
64
+
65
+ Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
66
+
67
+ Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
68
+
69
+ (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
70
+
71
+ Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
72
+
73
+ Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
74
+
75
+ Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
76
+
77
+ Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
78
+
79
+ Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
80
+
81
+ END OF TERMS AND CONDITIONS
long_video/README.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Builing on [Rolling Forcing](https://github.com/TencentARC/RollingForcing), we implemented minute-level long video generation.
2
+
3
+ ### Installation
4
+
5
+ ```bash
6
+ conda activate causal_forcing
7
+ pip install tensorboard opencv-python packaging
8
+ ```
9
+
10
+ ### CLI inference
11
+ ```
12
+ hf download Wan-AI/Wan2.1-T2V-1.3B --local-dir wan_models/Wan2.1-T2V-1.3B
13
+ hf download zhuhz22/Causal-Forcing chunkwise/longvideo.pt --local-dir ../checkpoints
14
+
15
+ python inference.py \
16
+ --config_path configs/rolling_forcing_dmd.yaml \
17
+ --output_folder videos/rolling_forcing_dmd \
18
+ --checkpoint_path ../checkpoints/chunkwise/longvideo.pt \
19
+ --data_path prompts/example_prompts.txt \
20
+ --num_output_frames 252 \
21
+ --use_ema
22
+ ```
23
+
24
+ ### Training
25
+ ```
26
+ hf download Wan-AI/Wan2.1-T2V-1.3B --local-dir wan_models/Wan2.1-T2V-1.3B
27
+ hf download Wan-AI/Wan2.1-T2V-14B --local-dir wan_models/Wan2.1-T2V-14B
28
+
29
+ torchrun --nproc_per_node=8 \
30
+ --rdzv_backend=c10d \
31
+ --rdzv_endpoint 127.0.0.1:29500 \
32
+ train.py \
33
+ -- \
34
+ --config_path configs/rolling_forcing_dmd.yaml \
35
+ --logdir logs/rolling_forcing_dmd
36
+ ```
37
+ > We recommend training for 3000 steps.
38
+
39
+ ### Acknowledge
40
+ We adopt [Rolling Forcing](https://github.com/TencentARC/RollingForcing) as our long video generation framework and only change the ODE initialization part.
long_video/app.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import argparse
3
+ import time
4
+ from typing import Optional
5
+
6
+ import torch
7
+ from torchvision.io import write_video
8
+ from omegaconf import OmegaConf
9
+ from einops import rearrange
10
+ import gradio as gr
11
+
12
+ from pipeline import CausalDiffusionInferencePipeline, CausalInferencePipeline
13
+
14
+
15
+ # -----------------------------
16
+ # Globals (loaded once per process)
17
+ # -----------------------------
18
+ _PIPELINE: Optional[torch.nn.Module] = None
19
+ _DEVICE: Optional[torch.device] = None
20
+
21
+
22
+ def _ensure_gpu():
23
+ if not torch.cuda.is_available():
24
+ raise gr.Error("CUDA GPU is required to run this demo. Please run on a machine with an NVIDIA GPU.")
25
+ # Bind to GPU:0 by default
26
+ torch.cuda.set_device(0)
27
+
28
+
29
+ def _load_pipeline(config_path: str, checkpoint_path: Optional[str], use_ema: bool) -> torch.nn.Module:
30
+ global _PIPELINE, _DEVICE
31
+ if _PIPELINE is not None:
32
+ return _PIPELINE
33
+
34
+ _ensure_gpu()
35
+ _DEVICE = torch.device("cuda:0")
36
+
37
+ # Load and merge configs
38
+ config = OmegaConf.load(config_path)
39
+ default_config = OmegaConf.load("configs/default_config.yaml")
40
+ config = OmegaConf.merge(default_config, config)
41
+
42
+ # Choose pipeline type based on config
43
+ pipeline = CausalInferencePipeline(config, device=_DEVICE)
44
+
45
+
46
+ # Load checkpoint if provided
47
+ if checkpoint_path and os.path.exists(checkpoint_path):
48
+ state_dict = torch.load(checkpoint_path, map_location="cpu")
49
+ if use_ema and 'generator_ema' in state_dict:
50
+ state_dict_to_load = state_dict['generator_ema']
51
+ # Remove possible FSDP prefix
52
+ from collections import OrderedDict
53
+ new_state_dict = OrderedDict()
54
+ for k, v in state_dict_to_load.items():
55
+ new_state_dict[k.replace("_fsdp_wrapped_module.", "")] = v
56
+ state_dict_to_load = new_state_dict
57
+ else:
58
+ state_dict_to_load = state_dict.get('generator', state_dict)
59
+ pipeline.generator.load_state_dict(state_dict_to_load, strict=False)
60
+
61
+ # The codebase assumes bfloat16 on GPU
62
+ pipeline = pipeline.to(device=_DEVICE, dtype=torch.bfloat16)
63
+ pipeline.eval()
64
+
65
+ # Quick sanity path check for Wan models to give friendly errors
66
+ wan_dir = os.path.join('wan_models', 'Wan2.1-T2V-1.3B')
67
+ if not os.path.isdir(wan_dir):
68
+ raise gr.Error(
69
+ "Wan2.1-T2V-1.3B not found at 'wan_models/Wan2.1-T2V-1.3B'.\n"
70
+ "Please download it first, e.g.:\n"
71
+ "huggingface-cli download Wan-AI/Wan2.1-T2V-1.3B --local-dir-use-symlinks False --local-dir wan_models/Wan2.1-T2V-1.3B"
72
+ )
73
+
74
+ _PIPELINE = pipeline
75
+ return _PIPELINE
76
+
77
+
78
+ def build_predict(config_path: str, checkpoint_path: Optional[str], output_dir: str, use_ema: bool):
79
+ os.makedirs(output_dir, exist_ok=True)
80
+
81
+ def predict(prompt: str, num_frames: int) -> str:
82
+ if not prompt or not prompt.strip():
83
+ raise gr.Error("Please enter a non-empty text prompt.")
84
+
85
+ num_frames = int(num_frames)
86
+ if num_frames % 3 != 0 or not (21 <= num_frames <= 252):
87
+ raise gr.Error("Number of frames must be a multiple of 3 between 21 and 252.")
88
+
89
+ pipeline = _load_pipeline(config_path, checkpoint_path, use_ema)
90
+
91
+ # Prepare inputs
92
+ prompts = [prompt.strip()]
93
+ noise = torch.randn([1, num_frames, 16, 60, 104], device=_DEVICE, dtype=torch.bfloat16)
94
+
95
+ torch.set_grad_enabled(False)
96
+ with torch.inference_mode(), torch.cuda.amp.autocast(dtype=torch.bfloat16):
97
+ video = pipeline.inference_rolling_forcing(
98
+ noise=noise,
99
+ text_prompts=prompts,
100
+ return_latents=False,
101
+ initial_latent=None,
102
+ )
103
+
104
+ # video: [B=1, T, C, H, W] in [0,1]
105
+ video = rearrange(video, 'b t c h w -> b t h w c')[0]
106
+ video_uint8 = (video * 255.0).clamp(0, 255).to(torch.uint8).cpu()
107
+
108
+ # Save to a unique filepath
109
+ safe_stub = prompt[:60].replace(' ', '_').replace('/', '_')
110
+ ts = int(time.time())
111
+ filepath = os.path.join(output_dir, f"{safe_stub or 'video'}_{ts}.mp4")
112
+ write_video(filepath, video_uint8, fps=16)
113
+ print(f"Saved generated video to {filepath}")
114
+
115
+ return filepath
116
+
117
+ return predict
118
+
119
+
120
+ def main():
121
+ parser = argparse.ArgumentParser()
122
+ parser.add_argument('--config_path', type=str, default='configs/rolling_forcing_dmd.yaml',
123
+ help='Path to the model config')
124
+ parser.add_argument('--checkpoint_path', type=str, default='checkpoints/rolling_forcing_dmd.pt',
125
+ help='Path to rolling forcing checkpoint (.pt). If missing, will run with base weights only if available.')
126
+ parser.add_argument('--output_dir', type=str, default='videos/gradio', help='Where to save generated videos')
127
+ parser.add_argument('--no_ema', action='store_true', help='Disable EMA weights when loading checkpoint')
128
+ parser.add_argument('--server_name', type=str, default='0.0.0.0', help='Gradio server host')
129
+ parser.add_argument('--server_port', type=int, default=7860, help='Gradio server port')
130
+ args = parser.parse_args()
131
+
132
+ predict = build_predict(
133
+ config_path=args.config_path,
134
+ checkpoint_path=args.checkpoint_path,
135
+ output_dir=args.output_dir,
136
+ use_ema=not args.no_ema,
137
+ )
138
+
139
+ demo = gr.Interface(
140
+ fn=predict,
141
+ inputs=[
142
+ gr.Textbox(label="Text Prompt", lines=2, placeholder="A cinematic shot of a girl dancing in the sunset."),
143
+ gr.Slider(label="Number of Latent Frames", minimum=21, maximum=252, step=3, value=21),
144
+ ],
145
+ outputs=gr.Video(label="Generated Video", format="mp4"),
146
+ title="Rolling Forcing: Autoregressive Long Video Diffusion in Real Time",
147
+ description=(
148
+ "Enter a prompt and generate a video using the Rolling Forcing pipeline.\n"
149
+ "**Note:** although Rolling Forcing generates videos autoregressivelty, current Gradio demo does not support streaming outputs, so the entire video will be generated before it is displayed.\n"
150
+ "\n"
151
+ "If you find this demo useful, please consider giving it a ⭐ star on [GitHub](https://github.com/TencentARC/RollingForcing)--your support is crucial for sustaining this open-source project. "
152
+ "You can also dive deeper by reading the [paper](https://arxiv.org/abs/2509.25161) or exploring the [project page](https://kunhao-liu.github.io/Rolling_Forcing_Webpage) for more details."
153
+ ),
154
+ allow_flagging='never',
155
+ )
156
+
157
+ try:
158
+ # Gradio <= 3.x
159
+ demo.queue(concurrency_count=1, max_size=2)
160
+ except TypeError:
161
+ # Gradio >= 4.x
162
+ demo.queue(max_size=2)
163
+ demo.launch(server_name=args.server_name, server_port=args.server_port, show_error=True)
164
+
165
+
166
+ if __name__ == "__main__":
167
+ main()
long_video/configs/default_config.yaml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ independent_first_frame: false
2
+ warp_denoising_step: false
3
+ weight_decay: 0.01
4
+ same_step_across_blocks: true
5
+ discriminator_lr_multiplier: 1.0
6
+ last_step_only: false
7
+ i2v: false
8
+ num_training_frames: 27
9
+ gc_interval: 100
10
+ context_noise: 0
11
+ causal: true
12
+
13
+ ckpt_step: 0
14
+ prompt_name: MovieGenVideoBench
15
+ prompt_path: prompts/MovieGenVideoBench.txt
16
+ eval_first_n: 64
17
+ num_samples: 1
18
+ height: 480
19
+ width: 832
20
+ num_frames: 81
long_video/configs/rolling_forcing_dmd.yaml ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator_ckpt: ../checkpoints/chunkwise/causal_ode.pt
2
+ generator_fsdp_wrap_strategy: size
3
+ real_score_fsdp_wrap_strategy: size
4
+ fake_score_fsdp_wrap_strategy: size
5
+ real_name: Wan2.1-T2V-14B
6
+ text_encoder_fsdp_wrap_strategy: size
7
+ denoising_step_list:
8
+ - 1000
9
+ - 750
10
+ - 500
11
+ - 250
12
+ warp_denoising_step: true # need to remove - 0 in denoising_step_list if warp_denoising_step is true
13
+ ts_schedule: false
14
+ num_train_timestep: 1000
15
+ timestep_shift: 5.0
16
+ guidance_scale: 3.0
17
+ denoising_loss_type: flow
18
+ mixed_precision: true
19
+ seed: 0
20
+ sharding_strategy: hybrid_full
21
+ lr: 1.5e-06
22
+ lr_critic: 4.0e-07
23
+ beta1: 0.0
24
+ beta2: 0.999
25
+ beta1_critic: 0.0
26
+ beta2_critic: 0.999
27
+ data_path: ../prompts/vidprom_filtered_extended.txt
28
+ batch_size: 1
29
+ ema_weight: 0.99
30
+ ema_start_step: 200
31
+ total_batch_size: 64
32
+ log_iters: 1000
33
+ negative_prompt: '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
34
+ dfake_gen_update_ratio: 5
35
+ image_or_video_shape:
36
+ - 1
37
+ - 21
38
+ - 16
39
+ - 60
40
+ - 104
41
+ distribution_loss: dmd
42
+ trainer: score_distillation
43
+ gradient_checkpointing: true
44
+ num_frame_per_block: 3
45
+ load_raw_video: false
46
+ model_kwargs:
47
+ timestep_shift: 5.0
long_video/inference.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import torch
3
+ import os
4
+ from omegaconf import OmegaConf
5
+ from collections import OrderedDict
6
+ from tqdm import tqdm
7
+ from torchvision import transforms
8
+ from torchvision.io import write_video
9
+ from einops import rearrange
10
+ import torch.distributed as dist
11
+ import imageio
12
+ from torch.utils.data import DataLoader, SequentialSampler
13
+ from torch.utils.data.distributed import DistributedSampler
14
+
15
+ from pipeline import (
16
+ CausalDiffusionInferencePipeline,
17
+ CausalInferencePipeline
18
+ )
19
+ from utils.dataset import TextDataset, TextImagePairDataset
20
+ from utils.misc import set_seed
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
+ torch.set_grad_enabled(False)
53
+
54
+ config = OmegaConf.load(args.config_path)
55
+ default_config = OmegaConf.load("configs/default_config.yaml")
56
+ config = OmegaConf.merge(default_config, config)
57
+
58
+ # Initialize pipeline
59
+ if hasattr(config, 'denoising_step_list'):
60
+ # Few-step inference
61
+ pipeline = CausalInferencePipeline(config, device=device)
62
+ else:
63
+ # Multi-step diffusion inference
64
+ pipeline = CausalDiffusionInferencePipeline(config, device=device)
65
+
66
+ if args.checkpoint_path:
67
+ state_dict = torch.load(args.checkpoint_path, map_location="cpu")
68
+ if args.use_ema:
69
+ state_dict_to_load = state_dict['generator_ema']
70
+ def remove_fsdp_prefix(state_dict):
71
+ new_state_dict = OrderedDict()
72
+ for key, value in state_dict.items():
73
+ if "_fsdp_wrapped_module." in key:
74
+ new_key = key.replace("_fsdp_wrapped_module.", "")
75
+ new_state_dict[new_key] = value
76
+ else:
77
+ new_state_dict[key] = value
78
+ return new_state_dict
79
+ state_dict_to_load = remove_fsdp_prefix(state_dict_to_load)
80
+ else:
81
+ state_dict_to_load = state_dict['generator']
82
+ pipeline.generator.load_state_dict(state_dict_to_load)
83
+
84
+ pipeline = pipeline.to(device=device, dtype=torch.bfloat16)
85
+
86
+ # Create dataset
87
+ if args.i2v:
88
+ assert not dist.is_initialized(), "I2V does not support distributed inference yet"
89
+ transform = transforms.Compose([
90
+ transforms.Resize((480, 832)),
91
+ transforms.ToTensor(),
92
+ transforms.Normalize([0.5], [0.5])
93
+ ])
94
+ dataset = TextImagePairDataset(args.data_path, transform=transform)
95
+ else:
96
+ dataset = TextDataset(prompt_path=args.data_path, extended_prompt_path=args.extended_prompt_path)
97
+ num_prompts = len(dataset)
98
+ print(f"Number of prompts: {num_prompts}")
99
+
100
+ if dist.is_initialized():
101
+ sampler = DistributedSampler(dataset, shuffle=False, drop_last=True)
102
+ else:
103
+ sampler = SequentialSampler(dataset)
104
+ dataloader = DataLoader(dataset, batch_size=1, sampler=sampler, num_workers=0, drop_last=False)
105
+
106
+ # Create output directory (only on main process to avoid race conditions)
107
+ if local_rank == 0:
108
+ os.makedirs(args.output_folder, exist_ok=True)
109
+
110
+ if dist.is_initialized():
111
+ dist.barrier()
112
+
113
+
114
+ def encode(self, videos: torch.Tensor) -> torch.Tensor:
115
+ device, dtype = videos[0].device, videos[0].dtype
116
+ scale = [self.mean.to(device=device, dtype=dtype),
117
+ 1.0 / self.std.to(device=device, dtype=dtype)]
118
+ output = [
119
+ self.model.encode(u.unsqueeze(0), scale).float().squeeze(0)
120
+ for u in videos
121
+ ]
122
+
123
+ output = torch.stack(output, dim=0)
124
+ return output
125
+
126
+
127
+ for i, batch_data in tqdm(enumerate(dataloader), disable=(local_rank != 0)):
128
+ idx = batch_data['idx'].item()
129
+
130
+ # For DataLoader batch_size=1, the batch_data is already a single item, but in a batch container
131
+ # Unpack the batch data for convenience
132
+ if isinstance(batch_data, dict):
133
+ batch = batch_data
134
+ elif isinstance(batch_data, list):
135
+ batch = batch_data[0] # First (and only) item in the batch
136
+
137
+ all_video = []
138
+ num_generated_frames = 0 # Number of generated (latent) frames
139
+
140
+ if args.i2v:
141
+ # For image-to-video, batch contains image and caption
142
+ prompt = batch['prompts'][0] # Get caption from batch
143
+ prompts = [prompt] * args.num_samples
144
+
145
+ # Process the image
146
+ image = batch['image'].squeeze(0).unsqueeze(0).unsqueeze(2).to(device=device, dtype=torch.bfloat16)
147
+
148
+ # Encode the input image as the first latent
149
+ initial_latent = pipeline.vae.encode_to_latent(image).to(device=device, dtype=torch.bfloat16)
150
+ initial_latent = initial_latent.repeat(args.num_samples, 1, 1, 1, 1)
151
+
152
+ sampled_noise = torch.randn(
153
+ [args.num_samples, args.num_output_frames - 1, 16, 60, 104], device=device, dtype=torch.bfloat16
154
+ )
155
+ else:
156
+ # For text-to-video, batch is just the text prompt
157
+ prompt = batch['prompts'][0]
158
+ extended_prompt = batch['extended_prompts'][0] if 'extended_prompts' in batch else None
159
+ if extended_prompt is not None:
160
+ prompts = [extended_prompt] * args.num_samples
161
+ else:
162
+ prompts = [prompt] * args.num_samples
163
+ initial_latent = None
164
+
165
+ sampled_noise = torch.randn(
166
+ [args.num_samples, args.num_output_frames, 16, 60, 104], device=device, dtype=torch.bfloat16
167
+ )
168
+
169
+ # Generate 81 frames
170
+ video, latents = pipeline.inference_rolling_forcing(
171
+ noise=sampled_noise,
172
+ text_prompts=prompts,
173
+ return_latents=True,
174
+ initial_latent=initial_latent,
175
+ )
176
+ current_video = rearrange(video, 'b t c h w -> b t h w c').cpu()
177
+ all_video.append(current_video)
178
+ num_generated_frames += latents.shape[1]
179
+
180
+ # Final output video
181
+ video = 255.0 * torch.cat(all_video, dim=1)
182
+
183
+ # Clear VAE cache
184
+ pipeline.vae.model.clear_cache()
185
+
186
+ # Save the video if the current prompt is not a dummy prompt
187
+ if idx < num_prompts:
188
+ model = "regular" if not args.use_ema else "ema"
189
+ for seed_idx in range(args.num_samples):
190
+ # All processes save their videos
191
+ if args.save_with_index:
192
+ output_path = os.path.join(args.output_folder, f'{idx}-{seed_idx}_{model}.mp4')
193
+ else:
194
+ output_path = os.path.join(args.output_folder, f'{prompt[:100]}-{seed_idx}.mp4')
195
+ write_video(output_path, video[seed_idx], fps=16)
196
+ # imageio.mimwrite(output_path, video[seed_idx], fps=16, quality=8, output_params=["-loglevel", "error"])
197
+
long_video/model/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .diffusion import CausalDiffusion
2
+ from .causvid import CausVid
3
+ from .dmd import DMD
4
+ from .gan import GAN
5
+ from .sid import SiD
6
+ from .ode_regression import ODERegression
7
+ __all__ = [
8
+ "CausalDiffusion",
9
+ "CausVid",
10
+ "DMD",
11
+ "GAN",
12
+ "SiD",
13
+ "ODERegression"
14
+ ]
long_video/model/base.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple
2
+ from einops import rearrange
3
+ from torch import nn
4
+ import torch.distributed as dist
5
+ import torch
6
+
7
+ from pipeline import RollingForcingTrainingPipeline
8
+ from utils.loss import get_denoising_loss
9
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
10
+
11
+
12
+ class BaseModel(nn.Module):
13
+ def __init__(self, args, device):
14
+ super().__init__()
15
+ self._initialize_models(args, device)
16
+
17
+ self.device = device
18
+ self.args = args
19
+ self.dtype = torch.bfloat16 if args.mixed_precision else torch.float32
20
+ if hasattr(args, "denoising_step_list"):
21
+ self.denoising_step_list = torch.tensor(args.denoising_step_list, dtype=torch.long)
22
+ if args.warp_denoising_step:
23
+ timesteps = torch.cat((self.scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32)))
24
+ self.denoising_step_list = timesteps[1000 - self.denoising_step_list]
25
+
26
+ def _initialize_models(self, args, device):
27
+ self.real_model_name = getattr(args, "real_name", "Wan2.1-T2V-1.3B")
28
+ self.fake_model_name = getattr(args, "fake_name", "Wan2.1-T2V-1.3B")
29
+
30
+ self.generator = WanDiffusionWrapper(**getattr(args, "model_kwargs", {}), is_causal=True)
31
+ self.generator.model.requires_grad_(True)
32
+
33
+ self.real_score = WanDiffusionWrapper(model_name=self.real_model_name, is_causal=False)
34
+ self.real_score.model.requires_grad_(False)
35
+
36
+ self.fake_score = WanDiffusionWrapper(model_name=self.fake_model_name, is_causal=False)
37
+ self.fake_score.model.requires_grad_(True)
38
+
39
+ self.text_encoder = WanTextEncoder()
40
+ self.text_encoder.requires_grad_(False)
41
+
42
+ self.vae = WanVAEWrapper()
43
+ self.vae.requires_grad_(False)
44
+
45
+ self.scheduler = self.generator.get_scheduler()
46
+ self.scheduler.timesteps = self.scheduler.timesteps.to(device)
47
+
48
+ def _get_timestep(
49
+ self,
50
+ min_timestep: int,
51
+ max_timestep: int,
52
+ batch_size: int,
53
+ num_frame: int,
54
+ num_frame_per_block: int,
55
+ uniform_timestep: bool = False
56
+ ) -> torch.Tensor:
57
+ """
58
+ Randomly generate a timestep tensor based on the generator's task type. It uniformly samples a timestep
59
+ from the range [min_timestep, max_timestep], and returns a tensor of shape [batch_size, num_frame].
60
+ - If uniform_timestep, it will use the same timestep for all frames.
61
+ - If not uniform_timestep, it will use a different timestep for each block.
62
+ """
63
+ if uniform_timestep:
64
+ timestep = torch.randint(
65
+ min_timestep,
66
+ max_timestep,
67
+ [batch_size, 1],
68
+ device=self.device,
69
+ dtype=torch.long
70
+ ).repeat(1, num_frame)
71
+ return timestep
72
+ else:
73
+ timestep = torch.randint(
74
+ min_timestep,
75
+ max_timestep,
76
+ [batch_size, num_frame],
77
+ device=self.device,
78
+ dtype=torch.long
79
+ )
80
+ # make the noise level the same within every block
81
+ if self.independent_first_frame:
82
+ # the first frame is always kept the same
83
+ timestep_from_second = timestep[:, 1:]
84
+ timestep_from_second = timestep_from_second.reshape(
85
+ timestep_from_second.shape[0], -1, num_frame_per_block)
86
+ timestep_from_second[:, :, 1:] = timestep_from_second[:, :, 0:1]
87
+ timestep_from_second = timestep_from_second.reshape(
88
+ timestep_from_second.shape[0], -1)
89
+ timestep = torch.cat([timestep[:, 0:1], timestep_from_second], dim=1)
90
+ else:
91
+ timestep = timestep.reshape(
92
+ timestep.shape[0], -1, num_frame_per_block)
93
+ timestep[:, :, 1:] = timestep[:, :, 0:1]
94
+ timestep = timestep.reshape(timestep.shape[0], -1)
95
+ return timestep
96
+
97
+
98
+ class RollingForcingModel(BaseModel):
99
+ def __init__(self, args, device):
100
+ super().__init__(args, device)
101
+ self.denoising_loss_func = get_denoising_loss(args.denoising_loss_type)()
102
+
103
+ def _run_generator(
104
+ self,
105
+ image_or_video_shape,
106
+ conditional_dict: dict,
107
+ initial_latent: torch.tensor = None
108
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
109
+ """
110
+ Optionally simulate the generator's input from noise using backward simulation
111
+ and then run the generator for one-step.
112
+ Input:
113
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
114
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
115
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
116
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
117
+ - initial_latent: a tensor containing the initial latents [B, F, C, H, W].
118
+ Output:
119
+ - pred_image: a tensor with shape [B, F, C, H, W].
120
+ - denoised_timestep: an integer
121
+ """
122
+ # Step 1: Sample noise and backward simulate the generator's input
123
+ assert getattr(self.args, "backward_simulation", True), "Backward simulation needs to be enabled"
124
+ if initial_latent is not None:
125
+ conditional_dict["initial_latent"] = initial_latent
126
+ if self.args.i2v:
127
+ noise_shape = [image_or_video_shape[0], image_or_video_shape[1] - 1, *image_or_video_shape[2:]]
128
+ else:
129
+ noise_shape = image_or_video_shape.copy()
130
+
131
+ # During training, the number of generated frames should be uniformly sampled from
132
+ # [21, self.num_training_frames], but still being a multiple of self.num_frame_per_block
133
+ min_num_frames = 20 if self.args.independent_first_frame else 21
134
+ max_num_frames = self.num_training_frames - 1 if self.args.independent_first_frame else self.num_training_frames
135
+ assert max_num_frames % self.num_frame_per_block == 0
136
+ assert min_num_frames % self.num_frame_per_block == 0
137
+ max_num_blocks = max_num_frames // self.num_frame_per_block
138
+ min_num_blocks = min_num_frames // self.num_frame_per_block
139
+ num_generated_blocks = torch.randint(min_num_blocks, max_num_blocks + 1, (1,), device=self.device)
140
+ dist.broadcast(num_generated_blocks, src=0)
141
+ num_generated_blocks = num_generated_blocks.item()
142
+ num_generated_frames = num_generated_blocks * self.num_frame_per_block
143
+ if self.args.independent_first_frame and initial_latent is None:
144
+ num_generated_frames += 1
145
+ min_num_frames += 1
146
+ # Sync num_generated_frames across all processes
147
+ noise_shape[1] = num_generated_frames
148
+
149
+ pred_image_or_video, denoised_timestep_from, denoised_timestep_to = self._consistency_backward_simulation(
150
+ noise=torch.randn(noise_shape,
151
+ device=self.device, dtype=self.dtype),
152
+ **conditional_dict,
153
+ )
154
+ # Slice last 21 frames
155
+ if pred_image_or_video.shape[1] > 21:
156
+ with torch.no_grad():
157
+ # Reencode to get image latent
158
+ latent_to_decode = pred_image_or_video[:, :-20, ...]
159
+ # Deccode to video
160
+ pixels = self.vae.decode_to_pixel(latent_to_decode)
161
+ frame = pixels[:, -1:, ...].to(self.dtype)
162
+ frame = rearrange(frame, "b t c h w -> b c t h w")
163
+ # Encode frame to get image latent
164
+ image_latent = self.vae.encode_to_latent(frame).to(self.dtype)
165
+ pred_image_or_video_last_21 = torch.cat([image_latent, pred_image_or_video[:, -20:, ...]], dim=1)
166
+ else:
167
+ pred_image_or_video_last_21 = pred_image_or_video
168
+
169
+ if num_generated_frames != min_num_frames:
170
+ # Currently, we do not use gradient for the first chunk, since it contains image latents
171
+ gradient_mask = torch.ones_like(pred_image_or_video_last_21, dtype=torch.bool)
172
+ if self.args.independent_first_frame:
173
+ gradient_mask[:, :1] = False
174
+ else:
175
+ gradient_mask[:, :self.num_frame_per_block] = False
176
+ else:
177
+ gradient_mask = None
178
+
179
+ pred_image_or_video_last_21 = pred_image_or_video_last_21.to(self.dtype)
180
+ return pred_image_or_video_last_21, gradient_mask, denoised_timestep_from, denoised_timestep_to
181
+
182
+ def _consistency_backward_simulation(
183
+ self,
184
+ noise: torch.Tensor,
185
+ **conditional_dict: dict
186
+ ) -> torch.Tensor:
187
+ """
188
+ Simulate the generator's input from noise to avoid training/inference mismatch.
189
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
190
+ Here we use the consistency sampler (https://arxiv.org/abs/2303.01469)
191
+ Input:
192
+ - noise: a tensor sampled from N(0, 1) with shape [B, F, C, H, W] where the number of frame is 1 for images.
193
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
194
+ Output:
195
+ - output: a tensor with shape [B, T, F, C, H, W].
196
+ T is the total number of timesteps. output[0] is a pure noise and output[i] and i>0
197
+ represents the x0 prediction at each timestep.
198
+ """
199
+ if self.inference_pipeline is None:
200
+ self._initialize_inference_pipeline()
201
+
202
+ infer_w_rolling = torch.rand(1, device=self.device) > 0.5
203
+ dist.broadcast(infer_w_rolling, src=0)
204
+
205
+ if infer_w_rolling:
206
+ return self.inference_pipeline.inference_with_rolling_forcing(
207
+ noise=noise, **conditional_dict
208
+ )
209
+ else:
210
+ return self.inference_pipeline.inference_with_self_forcing(
211
+ noise=noise, **conditional_dict
212
+ )
213
+
214
+ def _initialize_inference_pipeline(self):
215
+ """
216
+ Lazy initialize the inference pipeline during the first backward simulation run.
217
+ Here we encapsulate the inference code with a model-dependent outside function.
218
+ We pass our FSDP-wrapped modules into the pipeline to save memory.
219
+ """
220
+ self.inference_pipeline = RollingForcingTrainingPipeline(
221
+ denoising_step_list=self.denoising_step_list,
222
+ scheduler=self.scheduler,
223
+ generator=self.generator,
224
+ num_frame_per_block=self.num_frame_per_block,
225
+ independent_first_frame=self.args.independent_first_frame,
226
+ same_step_across_blocks=self.args.same_step_across_blocks,
227
+ last_step_only=self.args.last_step_only,
228
+ num_max_frames=self.num_training_frames,
229
+ context_noise=self.args.context_noise
230
+ )
long_video/model/causvid.py ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch.nn.functional as F
2
+ from typing import Tuple
3
+ import torch
4
+
5
+ from model.base import BaseModel
6
+
7
+
8
+ class CausVid(BaseModel):
9
+ def __init__(self, args, device):
10
+ """
11
+ Initialize the DMD (Distribution Matching Distillation) module.
12
+ This class is self-contained and compute generator and fake score losses
13
+ in the forward pass.
14
+ """
15
+ super().__init__(args, device)
16
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
17
+ self.num_training_frames = getattr(args, "num_training_frames", 21)
18
+
19
+ if self.num_frame_per_block > 1:
20
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
21
+
22
+ self.independent_first_frame = getattr(args, "independent_first_frame", False)
23
+ if self.independent_first_frame:
24
+ self.generator.model.independent_first_frame = True
25
+ if args.gradient_checkpointing:
26
+ self.generator.enable_gradient_checkpointing()
27
+ self.fake_score.enable_gradient_checkpointing()
28
+
29
+ # Step 2: Initialize all dmd hyperparameters
30
+ self.num_train_timestep = args.num_train_timestep
31
+ self.min_step = int(0.02 * self.num_train_timestep)
32
+ self.max_step = int(0.98 * self.num_train_timestep)
33
+ if hasattr(args, "real_guidance_scale"):
34
+ self.real_guidance_scale = args.real_guidance_scale
35
+ self.fake_guidance_scale = args.fake_guidance_scale
36
+ else:
37
+ self.real_guidance_scale = args.guidance_scale
38
+ self.fake_guidance_scale = 0.0
39
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
40
+ self.teacher_forcing = getattr(args, "teacher_forcing", False)
41
+
42
+ if getattr(self.scheduler, "alphas_cumprod", None) is not None:
43
+ self.scheduler.alphas_cumprod = self.scheduler.alphas_cumprod.to(device)
44
+ else:
45
+ self.scheduler.alphas_cumprod = None
46
+
47
+ def _compute_kl_grad(
48
+ self, noisy_image_or_video: torch.Tensor,
49
+ estimated_clean_image_or_video: torch.Tensor,
50
+ timestep: torch.Tensor,
51
+ conditional_dict: dict, unconditional_dict: dict,
52
+ normalization: bool = True
53
+ ) -> Tuple[torch.Tensor, dict]:
54
+ """
55
+ Compute the KL grad (eq 7 in https://arxiv.org/abs/2311.18828).
56
+ Input:
57
+ - noisy_image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
58
+ - estimated_clean_image_or_video: a tensor with shape [B, F, C, H, W] representing the estimated clean image or video.
59
+ - timestep: a tensor with shape [B, F] containing the randomly generated timestep.
60
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
61
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
62
+ - normalization: a boolean indicating whether to normalize the gradient.
63
+ Output:
64
+ - kl_grad: a tensor representing the KL grad.
65
+ - kl_log_dict: a dictionary containing the intermediate tensors for logging.
66
+ """
67
+ # Step 1: Compute the fake score
68
+ _, pred_fake_image_cond = self.fake_score(
69
+ noisy_image_or_video=noisy_image_or_video,
70
+ conditional_dict=conditional_dict,
71
+ timestep=timestep
72
+ )
73
+
74
+ if self.fake_guidance_scale != 0.0:
75
+ _, pred_fake_image_uncond = self.fake_score(
76
+ noisy_image_or_video=noisy_image_or_video,
77
+ conditional_dict=unconditional_dict,
78
+ timestep=timestep
79
+ )
80
+ pred_fake_image = pred_fake_image_cond + (
81
+ pred_fake_image_cond - pred_fake_image_uncond
82
+ ) * self.fake_guidance_scale
83
+ else:
84
+ pred_fake_image = pred_fake_image_cond
85
+
86
+ # Step 2: Compute the real score
87
+ # We compute the conditional and unconditional prediction
88
+ # and add them together to achieve cfg (https://arxiv.org/abs/2207.12598)
89
+ _, pred_real_image_cond = self.real_score(
90
+ noisy_image_or_video=noisy_image_or_video,
91
+ conditional_dict=conditional_dict,
92
+ timestep=timestep
93
+ )
94
+
95
+ _, pred_real_image_uncond = self.real_score(
96
+ noisy_image_or_video=noisy_image_or_video,
97
+ conditional_dict=unconditional_dict,
98
+ timestep=timestep
99
+ )
100
+
101
+ pred_real_image = pred_real_image_cond + (
102
+ pred_real_image_cond - pred_real_image_uncond
103
+ ) * self.real_guidance_scale
104
+
105
+ # Step 3: Compute the DMD gradient (DMD paper eq. 7).
106
+ grad = (pred_fake_image - pred_real_image)
107
+
108
+ # TODO: Change the normalizer for causal teacher
109
+ if normalization:
110
+ # Step 4: Gradient normalization (DMD paper eq. 8).
111
+ p_real = (estimated_clean_image_or_video - pred_real_image)
112
+ normalizer = torch.abs(p_real).mean(dim=[1, 2, 3, 4], keepdim=True)
113
+ grad = grad / normalizer
114
+ grad = torch.nan_to_num(grad)
115
+
116
+ return grad, {
117
+ "dmdtrain_gradient_norm": torch.mean(torch.abs(grad)).detach(),
118
+ "timestep": timestep.detach()
119
+ }
120
+
121
+ def compute_distribution_matching_loss(
122
+ self,
123
+ image_or_video: torch.Tensor,
124
+ conditional_dict: dict,
125
+ unconditional_dict: dict,
126
+ gradient_mask: torch.Tensor = None,
127
+ ) -> Tuple[torch.Tensor, dict]:
128
+ """
129
+ Compute the DMD loss (eq 7 in https://arxiv.org/abs/2311.18828).
130
+ Input:
131
+ - image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
132
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
133
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
134
+ - gradient_mask: a boolean tensor with the same shape as image_or_video indicating which pixels to compute loss .
135
+ Output:
136
+ - dmd_loss: a scalar tensor representing the DMD loss.
137
+ - dmd_log_dict: a dictionary containing the intermediate tensors for logging.
138
+ """
139
+ original_latent = image_or_video
140
+
141
+ batch_size, num_frame = image_or_video.shape[:2]
142
+
143
+ with torch.no_grad():
144
+ # Step 1: Randomly sample timestep based on the given schedule and corresponding noise
145
+ timestep = self._get_timestep(
146
+ 0,
147
+ self.num_train_timestep,
148
+ batch_size,
149
+ num_frame,
150
+ self.num_frame_per_block,
151
+ uniform_timestep=True
152
+ )
153
+
154
+ if self.timestep_shift > 1:
155
+ timestep = self.timestep_shift * \
156
+ (timestep / 1000) / \
157
+ (1 + (self.timestep_shift - 1) * (timestep / 1000)) * 1000
158
+ timestep = timestep.clamp(self.min_step, self.max_step)
159
+
160
+ noise = torch.randn_like(image_or_video)
161
+ noisy_latent = self.scheduler.add_noise(
162
+ image_or_video.flatten(0, 1),
163
+ noise.flatten(0, 1),
164
+ timestep.flatten(0, 1)
165
+ ).detach().unflatten(0, (batch_size, num_frame))
166
+
167
+ # Step 2: Compute the KL grad
168
+ grad, dmd_log_dict = self._compute_kl_grad(
169
+ noisy_image_or_video=noisy_latent,
170
+ estimated_clean_image_or_video=original_latent,
171
+ timestep=timestep,
172
+ conditional_dict=conditional_dict,
173
+ unconditional_dict=unconditional_dict
174
+ )
175
+
176
+ if gradient_mask is not None:
177
+ dmd_loss = 0.5 * F.mse_loss(original_latent.double(
178
+ )[gradient_mask], (original_latent.double() - grad.double()).detach()[gradient_mask], reduction="mean")
179
+ else:
180
+ dmd_loss = 0.5 * F.mse_loss(original_latent.double(
181
+ ), (original_latent.double() - grad.double()).detach(), reduction="mean")
182
+ return dmd_loss, dmd_log_dict
183
+
184
+ def _run_generator(
185
+ self,
186
+ image_or_video_shape,
187
+ conditional_dict: dict,
188
+ clean_latent: torch.tensor
189
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
190
+ """
191
+ Optionally simulate the generator's input from noise using backward simulation
192
+ and then run the generator for one-step.
193
+ Input:
194
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
195
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
196
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
197
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
198
+ - initial_latent: a tensor containing the initial latents [B, F, C, H, W].
199
+ Output:
200
+ - pred_image: a tensor with shape [B, F, C, H, W].
201
+ """
202
+ simulated_noisy_input = []
203
+ for timestep in self.denoising_step_list:
204
+ noise = torch.randn(
205
+ image_or_video_shape, device=self.device, dtype=self.dtype)
206
+
207
+ noisy_timestep = timestep * torch.ones(
208
+ image_or_video_shape[:2], device=self.device, dtype=torch.long)
209
+
210
+ if timestep != 0:
211
+ noisy_image = self.scheduler.add_noise(
212
+ clean_latent.flatten(0, 1),
213
+ noise.flatten(0, 1),
214
+ noisy_timestep.flatten(0, 1)
215
+ ).unflatten(0, image_or_video_shape[:2])
216
+ else:
217
+ noisy_image = clean_latent
218
+
219
+ simulated_noisy_input.append(noisy_image)
220
+
221
+ simulated_noisy_input = torch.stack(simulated_noisy_input, dim=1)
222
+
223
+ # Step 2: Randomly sample a timestep and pick the corresponding input
224
+ index = self._get_timestep(
225
+ 0,
226
+ len(self.denoising_step_list),
227
+ image_or_video_shape[0],
228
+ image_or_video_shape[1],
229
+ self.num_frame_per_block,
230
+ uniform_timestep=False
231
+ )
232
+
233
+ # select the corresponding timestep's noisy input from the stacked tensor [B, T, F, C, H, W]
234
+ noisy_input = torch.gather(
235
+ simulated_noisy_input, dim=1,
236
+ index=index.reshape(index.shape[0], 1, index.shape[1], 1, 1, 1).expand(
237
+ -1, -1, -1, *image_or_video_shape[2:]).to(self.device)
238
+ ).squeeze(1)
239
+
240
+ timestep = self.denoising_step_list[index].to(self.device)
241
+
242
+ _, pred_image_or_video = self.generator(
243
+ noisy_image_or_video=noisy_input,
244
+ conditional_dict=conditional_dict,
245
+ timestep=timestep,
246
+ clean_x=clean_latent if self.teacher_forcing else None,
247
+ )
248
+
249
+ gradient_mask = None # timestep != 0
250
+
251
+ pred_image_or_video = pred_image_or_video.type_as(noisy_input)
252
+
253
+ return pred_image_or_video, gradient_mask
254
+
255
+ def generator_loss(
256
+ self,
257
+ image_or_video_shape,
258
+ conditional_dict: dict,
259
+ unconditional_dict: dict,
260
+ clean_latent: torch.Tensor,
261
+ initial_latent: torch.Tensor = None
262
+ ) -> Tuple[torch.Tensor, dict]:
263
+ """
264
+ Generate image/videos from noise and compute the DMD loss.
265
+ The noisy input to the generator is backward simulated.
266
+ This removes the need of any datasets during distillation.
267
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
268
+ Input:
269
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
270
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
271
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
272
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
273
+ Output:
274
+ - loss: a scalar tensor representing the generator loss.
275
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
276
+ """
277
+ # Step 1: Run generator on backward simulated noisy input
278
+ pred_image, gradient_mask = self._run_generator(
279
+ image_or_video_shape=image_or_video_shape,
280
+ conditional_dict=conditional_dict,
281
+ clean_latent=clean_latent
282
+ )
283
+
284
+ # Step 2: Compute the DMD loss
285
+ dmd_loss, dmd_log_dict = self.compute_distribution_matching_loss(
286
+ image_or_video=pred_image,
287
+ conditional_dict=conditional_dict,
288
+ unconditional_dict=unconditional_dict,
289
+ gradient_mask=gradient_mask
290
+ )
291
+
292
+ # Step 3: TODO: Implement the GAN loss
293
+
294
+ return dmd_loss, dmd_log_dict
295
+
296
+ def critic_loss(
297
+ self,
298
+ image_or_video_shape,
299
+ conditional_dict: dict,
300
+ unconditional_dict: dict,
301
+ clean_latent: torch.Tensor,
302
+ initial_latent: torch.Tensor = None
303
+ ) -> Tuple[torch.Tensor, dict]:
304
+ """
305
+ Generate image/videos from noise and train the critic with generated samples.
306
+ The noisy input to the generator is backward simulated.
307
+ This removes the need of any datasets during distillation.
308
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
309
+ Input:
310
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
311
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
312
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
313
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
314
+ Output:
315
+ - loss: a scalar tensor representing the generator loss.
316
+ - critic_log_dict: a dictionary containing the intermediate tensors for logging.
317
+ """
318
+
319
+ # Step 1: Run generator on backward simulated noisy input
320
+ with torch.no_grad():
321
+ generated_image, _ = self._run_generator(
322
+ image_or_video_shape=image_or_video_shape,
323
+ conditional_dict=conditional_dict,
324
+ clean_latent=clean_latent
325
+ )
326
+
327
+ # Step 2: Compute the fake prediction
328
+ critic_timestep = self._get_timestep(
329
+ 0,
330
+ self.num_train_timestep,
331
+ image_or_video_shape[0],
332
+ image_or_video_shape[1],
333
+ self.num_frame_per_block,
334
+ uniform_timestep=True
335
+ )
336
+
337
+ if self.timestep_shift > 1:
338
+ critic_timestep = self.timestep_shift * \
339
+ (critic_timestep / 1000) / (1 + (self.timestep_shift - 1) * (critic_timestep / 1000)) * 1000
340
+
341
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
342
+
343
+ critic_noise = torch.randn_like(generated_image)
344
+ noisy_generated_image = self.scheduler.add_noise(
345
+ generated_image.flatten(0, 1),
346
+ critic_noise.flatten(0, 1),
347
+ critic_timestep.flatten(0, 1)
348
+ ).unflatten(0, image_or_video_shape[:2])
349
+
350
+ _, pred_fake_image = self.fake_score(
351
+ noisy_image_or_video=noisy_generated_image,
352
+ conditional_dict=conditional_dict,
353
+ timestep=critic_timestep
354
+ )
355
+
356
+ # Step 3: Compute the denoising loss for the fake critic
357
+ if self.args.denoising_loss_type == "flow":
358
+ from utils.wan_wrapper import WanDiffusionWrapper
359
+ flow_pred = WanDiffusionWrapper._convert_x0_to_flow_pred(
360
+ scheduler=self.scheduler,
361
+ x0_pred=pred_fake_image.flatten(0, 1),
362
+ xt=noisy_generated_image.flatten(0, 1),
363
+ timestep=critic_timestep.flatten(0, 1)
364
+ )
365
+ pred_fake_noise = None
366
+ else:
367
+ flow_pred = None
368
+ pred_fake_noise = self.scheduler.convert_x0_to_noise(
369
+ x0=pred_fake_image.flatten(0, 1),
370
+ xt=noisy_generated_image.flatten(0, 1),
371
+ timestep=critic_timestep.flatten(0, 1)
372
+ ).unflatten(0, image_or_video_shape[:2])
373
+
374
+ denoising_loss = self.denoising_loss_func(
375
+ x=generated_image.flatten(0, 1),
376
+ x_pred=pred_fake_image.flatten(0, 1),
377
+ noise=critic_noise.flatten(0, 1),
378
+ noise_pred=pred_fake_noise,
379
+ alphas_cumprod=self.scheduler.alphas_cumprod,
380
+ timestep=critic_timestep.flatten(0, 1),
381
+ flow_pred=flow_pred
382
+ )
383
+
384
+ # Step 4: TODO: Compute the GAN loss
385
+
386
+ # Step 5: Debugging Log
387
+ critic_log_dict = {
388
+ "critic_timestep": critic_timestep.detach()
389
+ }
390
+
391
+ return denoising_loss, critic_log_dict
long_video/model/diffusion.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple
2
+ import torch
3
+
4
+ from model.base import BaseModel
5
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
6
+
7
+
8
+ class CausalDiffusion(BaseModel):
9
+ def __init__(self, args, device):
10
+ """
11
+ Initialize the Diffusion loss module.
12
+ """
13
+ super().__init__(args, device)
14
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
15
+ if self.num_frame_per_block > 1:
16
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
17
+ self.independent_first_frame = getattr(args, "independent_first_frame", False)
18
+ if self.independent_first_frame:
19
+ self.generator.model.independent_first_frame = True
20
+
21
+ if args.gradient_checkpointing:
22
+ self.generator.enable_gradient_checkpointing()
23
+
24
+ # Step 2: Initialize all hyperparameters
25
+ self.num_train_timestep = args.num_train_timestep
26
+ self.min_step = int(0.02 * self.num_train_timestep)
27
+ self.max_step = int(0.98 * self.num_train_timestep)
28
+ self.guidance_scale = args.guidance_scale
29
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
30
+ self.teacher_forcing = getattr(args, "teacher_forcing", False)
31
+ # Noise augmentation in teacher forcing, we add small noise to clean context latents
32
+ self.noise_augmentation_max_timestep = getattr(args, "noise_augmentation_max_timestep", 0)
33
+
34
+ def _initialize_models(self, args):
35
+ self.generator = WanDiffusionWrapper(**getattr(args, "model_kwargs", {}), is_causal=True)
36
+ self.generator.model.requires_grad_(True)
37
+
38
+ self.text_encoder = WanTextEncoder()
39
+ self.text_encoder.requires_grad_(False)
40
+
41
+ self.vae = WanVAEWrapper()
42
+ self.vae.requires_grad_(False)
43
+
44
+ def generator_loss(
45
+ self,
46
+ image_or_video_shape,
47
+ conditional_dict: dict,
48
+ unconditional_dict: dict,
49
+ clean_latent: torch.Tensor,
50
+ initial_latent: torch.Tensor = None
51
+ ) -> Tuple[torch.Tensor, dict]:
52
+ """
53
+ Generate image/videos from noise and compute the DMD loss.
54
+ The noisy input to the generator is backward simulated.
55
+ This removes the need of any datasets during distillation.
56
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
57
+ Input:
58
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
59
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
60
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
61
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
62
+ Output:
63
+ - loss: a scalar tensor representing the generator loss.
64
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
65
+ """
66
+ noise = torch.randn_like(clean_latent)
67
+ batch_size, num_frame = image_or_video_shape[:2]
68
+
69
+ # Step 2: Randomly sample a timestep and add noise to denoiser inputs
70
+ index = self._get_timestep(
71
+ 0,
72
+ self.scheduler.num_train_timesteps,
73
+ image_or_video_shape[0],
74
+ image_or_video_shape[1],
75
+ self.num_frame_per_block,
76
+ uniform_timestep=False
77
+ )
78
+ timestep = self.scheduler.timesteps[index].to(dtype=self.dtype, device=self.device)
79
+ noisy_latents = self.scheduler.add_noise(
80
+ clean_latent.flatten(0, 1),
81
+ noise.flatten(0, 1),
82
+ timestep.flatten(0, 1)
83
+ ).unflatten(0, (batch_size, num_frame))
84
+ training_target = self.scheduler.training_target(clean_latent, noise, timestep)
85
+
86
+ # Step 3: Noise augmentation, also add small noise to clean context latents
87
+ if self.noise_augmentation_max_timestep > 0:
88
+ index_clean_aug = self._get_timestep(
89
+ 0,
90
+ self.noise_augmentation_max_timestep,
91
+ image_or_video_shape[0],
92
+ image_or_video_shape[1],
93
+ self.num_frame_per_block,
94
+ uniform_timestep=False
95
+ )
96
+ timestep_clean_aug = self.scheduler.timesteps[index_clean_aug].to(dtype=self.dtype, device=self.device)
97
+ clean_latent_aug = self.scheduler.add_noise(
98
+ clean_latent.flatten(0, 1),
99
+ noise.flatten(0, 1),
100
+ timestep_clean_aug.flatten(0, 1)
101
+ ).unflatten(0, (batch_size, num_frame))
102
+ else:
103
+ clean_latent_aug = clean_latent
104
+ timestep_clean_aug = None
105
+
106
+ # Compute loss
107
+ flow_pred, x0_pred = self.generator(
108
+ noisy_image_or_video=noisy_latents,
109
+ conditional_dict=conditional_dict,
110
+ timestep=timestep,
111
+ clean_x=clean_latent_aug if self.teacher_forcing else None,
112
+ aug_t=timestep_clean_aug if self.teacher_forcing else None
113
+ )
114
+ # loss = torch.nn.functional.mse_loss(flow_pred.float(), training_target.float())
115
+ loss = torch.nn.functional.mse_loss(
116
+ flow_pred.float(), training_target.float(), reduction='none'
117
+ ).mean(dim=(2, 3, 4))
118
+ loss = loss * self.scheduler.training_weight(timestep).unflatten(0, (batch_size, num_frame))
119
+ loss = loss.mean()
120
+
121
+ log_dict = {
122
+ "x0": clean_latent.detach(),
123
+ "x0_pred": x0_pred.detach()
124
+ }
125
+ return loss, log_dict
long_video/model/dmd.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pipeline import RollingForcingTrainingPipeline
2
+ import torch.nn.functional as F
3
+ from typing import Optional, Tuple
4
+ import torch
5
+
6
+ from model.base import RollingForcingModel
7
+
8
+
9
+ class DMD(RollingForcingModel):
10
+ def __init__(self, args, device):
11
+ """
12
+ Initialize the DMD (Distribution Matching Distillation) module.
13
+ This class is self-contained and compute generator and fake score losses
14
+ in the forward pass.
15
+ """
16
+ super().__init__(args, device)
17
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
18
+ self.same_step_across_blocks = getattr(args, "same_step_across_blocks", True)
19
+ self.num_training_frames = getattr(args, "num_training_frames", 21)
20
+
21
+ if self.num_frame_per_block > 1:
22
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
23
+
24
+ self.independent_first_frame = getattr(args, "independent_first_frame", False)
25
+ if self.independent_first_frame:
26
+ self.generator.model.independent_first_frame = True
27
+ if args.gradient_checkpointing:
28
+ self.generator.enable_gradient_checkpointing()
29
+ self.fake_score.enable_gradient_checkpointing()
30
+
31
+ # this will be init later with fsdp-wrapped modules
32
+ self.inference_pipeline: RollingForcingTrainingPipeline = None
33
+
34
+ # Step 2: Initialize all dmd hyperparameters
35
+ self.num_train_timestep = args.num_train_timestep
36
+ self.min_step = int(0.02 * self.num_train_timestep)
37
+ self.max_step = int(0.98 * self.num_train_timestep)
38
+ if hasattr(args, "real_guidance_scale"):
39
+ self.real_guidance_scale = args.real_guidance_scale
40
+ self.fake_guidance_scale = args.fake_guidance_scale
41
+ else:
42
+ self.real_guidance_scale = args.guidance_scale
43
+ self.fake_guidance_scale = 0.0
44
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
45
+ self.ts_schedule = getattr(args, "ts_schedule", True)
46
+ self.ts_schedule_max = getattr(args, "ts_schedule_max", False)
47
+ self.min_score_timestep = getattr(args, "min_score_timestep", 0)
48
+
49
+ if getattr(self.scheduler, "alphas_cumprod", None) is not None:
50
+ self.scheduler.alphas_cumprod = self.scheduler.alphas_cumprod.to(device)
51
+ else:
52
+ self.scheduler.alphas_cumprod = None
53
+
54
+ def _compute_kl_grad(
55
+ self, noisy_image_or_video: torch.Tensor,
56
+ estimated_clean_image_or_video: torch.Tensor,
57
+ timestep: torch.Tensor,
58
+ conditional_dict: dict, unconditional_dict: dict,
59
+ normalization: bool = True
60
+ ) -> Tuple[torch.Tensor, dict]:
61
+ """
62
+ Compute the KL grad (eq 7 in https://arxiv.org/abs/2311.18828).
63
+ Input:
64
+ - noisy_image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
65
+ - estimated_clean_image_or_video: a tensor with shape [B, F, C, H, W] representing the estimated clean image or video.
66
+ - timestep: a tensor with shape [B, F] containing the randomly generated timestep.
67
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
68
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
69
+ - normalization: a boolean indicating whether to normalize the gradient.
70
+ Output:
71
+ - kl_grad: a tensor representing the KL grad.
72
+ - kl_log_dict: a dictionary containing the intermediate tensors for logging.
73
+ """
74
+ # Step 1: Compute the fake score
75
+ _, pred_fake_image_cond = self.fake_score(
76
+ noisy_image_or_video=noisy_image_or_video,
77
+ conditional_dict=conditional_dict,
78
+ timestep=timestep
79
+ )
80
+
81
+ if self.fake_guidance_scale != 0.0:
82
+ _, pred_fake_image_uncond = self.fake_score(
83
+ noisy_image_or_video=noisy_image_or_video,
84
+ conditional_dict=unconditional_dict,
85
+ timestep=timestep
86
+ )
87
+ pred_fake_image = pred_fake_image_cond + (
88
+ pred_fake_image_cond - pred_fake_image_uncond
89
+ ) * self.fake_guidance_scale
90
+ else:
91
+ pred_fake_image = pred_fake_image_cond
92
+
93
+ # Step 2: Compute the real score
94
+ # We compute the conditional and unconditional prediction
95
+ # and add them together to achieve cfg (https://arxiv.org/abs/2207.12598)
96
+ _, pred_real_image_cond = self.real_score(
97
+ noisy_image_or_video=noisy_image_or_video,
98
+ conditional_dict=conditional_dict,
99
+ timestep=timestep
100
+ )
101
+
102
+ _, pred_real_image_uncond = self.real_score(
103
+ noisy_image_or_video=noisy_image_or_video,
104
+ conditional_dict=unconditional_dict,
105
+ timestep=timestep
106
+ )
107
+
108
+ pred_real_image = pred_real_image_cond + (
109
+ pred_real_image_cond - pred_real_image_uncond
110
+ ) * self.real_guidance_scale
111
+
112
+ # Step 3: Compute the DMD gradient (DMD paper eq. 7).
113
+ grad = (pred_fake_image - pred_real_image)
114
+
115
+ # TODO: Change the normalizer for causal teacher
116
+ if normalization:
117
+ # Step 4: Gradient normalization (DMD paper eq. 8).
118
+ p_real = (estimated_clean_image_or_video - pred_real_image)
119
+ normalizer = torch.abs(p_real).mean(dim=[1, 2, 3, 4], keepdim=True)
120
+ grad = grad / normalizer
121
+ grad = torch.nan_to_num(grad)
122
+
123
+ return grad, {
124
+ "dmdtrain_gradient_norm": torch.mean(torch.abs(grad)).detach(),
125
+ "timestep": timestep.detach()
126
+ }
127
+
128
+ def compute_distribution_matching_loss(
129
+ self,
130
+ image_or_video: torch.Tensor,
131
+ conditional_dict: dict,
132
+ unconditional_dict: dict,
133
+ gradient_mask: Optional[torch.Tensor] = None,
134
+ denoised_timestep_from: int = 0,
135
+ denoised_timestep_to: int = 0
136
+ ) -> Tuple[torch.Tensor, dict]:
137
+ """
138
+ Compute the DMD loss (eq 7 in https://arxiv.org/abs/2311.18828).
139
+ Input:
140
+ - image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
141
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
142
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
143
+ - gradient_mask: a boolean tensor with the same shape as image_or_video indicating which pixels to compute loss .
144
+ Output:
145
+ - dmd_loss: a scalar tensor representing the DMD loss.
146
+ - dmd_log_dict: a dictionary containing the intermediate tensors for logging.
147
+ """
148
+ original_latent = image_or_video
149
+
150
+ batch_size, num_frame = image_or_video.shape[:2]
151
+
152
+ with torch.no_grad():
153
+ # Step 1: Randomly sample timestep based on the given schedule and corresponding noise
154
+ min_timestep = denoised_timestep_to if self.ts_schedule and denoised_timestep_to is not None else self.min_score_timestep
155
+ max_timestep = denoised_timestep_from if self.ts_schedule_max and denoised_timestep_from is not None else self.num_train_timestep
156
+ timestep = self._get_timestep(
157
+ min_timestep,
158
+ max_timestep,
159
+ batch_size,
160
+ num_frame,
161
+ self.num_frame_per_block,
162
+ uniform_timestep=True
163
+ )
164
+
165
+ # TODO:should we change it to `timestep = self.scheduler.timesteps[timestep]`?
166
+ if self.timestep_shift > 1:
167
+ timestep = self.timestep_shift * \
168
+ (timestep / 1000) / \
169
+ (1 + (self.timestep_shift - 1) * (timestep / 1000)) * 1000
170
+ timestep = timestep.clamp(self.min_step, self.max_step)
171
+
172
+ noise = torch.randn_like(image_or_video)
173
+ noisy_latent = self.scheduler.add_noise(
174
+ image_or_video.flatten(0, 1),
175
+ noise.flatten(0, 1),
176
+ timestep.flatten(0, 1)
177
+ ).detach().unflatten(0, (batch_size, num_frame))
178
+
179
+ # Step 2: Compute the KL grad
180
+ grad, dmd_log_dict = self._compute_kl_grad(
181
+ noisy_image_or_video=noisy_latent,
182
+ estimated_clean_image_or_video=original_latent,
183
+ timestep=timestep,
184
+ conditional_dict=conditional_dict,
185
+ unconditional_dict=unconditional_dict
186
+ )
187
+
188
+ if gradient_mask is not None:
189
+ dmd_loss = 0.5 * F.mse_loss(original_latent.double(
190
+ )[gradient_mask], (original_latent.double() - grad.double()).detach()[gradient_mask], reduction="mean")
191
+ else:
192
+ dmd_loss = 0.5 * F.mse_loss(original_latent.double(
193
+ ), (original_latent.double() - grad.double()).detach(), reduction="mean")
194
+ return dmd_loss, dmd_log_dict
195
+
196
+ def generator_loss(
197
+ self,
198
+ image_or_video_shape,
199
+ conditional_dict: dict,
200
+ unconditional_dict: dict,
201
+ clean_latent: torch.Tensor,
202
+ initial_latent: torch.Tensor = None
203
+ ) -> Tuple[torch.Tensor, dict]:
204
+ """
205
+ Generate image/videos from noise and compute the DMD loss.
206
+ The noisy input to the generator is backward simulated.
207
+ This removes the need of any datasets during distillation.
208
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
209
+ Input:
210
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
211
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
212
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
213
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
214
+ Output:
215
+ - loss: a scalar tensor representing the generator loss.
216
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
217
+ """
218
+ # Step 1: Unroll generator to obtain fake videos
219
+ pred_image, gradient_mask, denoised_timestep_from, denoised_timestep_to = self._run_generator(
220
+ image_or_video_shape=image_or_video_shape,
221
+ conditional_dict=conditional_dict,
222
+ initial_latent=initial_latent
223
+ )
224
+
225
+ # Step 2: Compute the DMD loss
226
+ dmd_loss, dmd_log_dict = self.compute_distribution_matching_loss(
227
+ image_or_video=pred_image,
228
+ conditional_dict=conditional_dict,
229
+ unconditional_dict=unconditional_dict,
230
+ gradient_mask=gradient_mask,
231
+ denoised_timestep_from=denoised_timestep_from,
232
+ denoised_timestep_to=denoised_timestep_to
233
+ )
234
+
235
+ return dmd_loss, dmd_log_dict
236
+
237
+ def critic_loss(
238
+ self,
239
+ image_or_video_shape,
240
+ conditional_dict: dict,
241
+ unconditional_dict: dict,
242
+ clean_latent: torch.Tensor,
243
+ initial_latent: torch.Tensor = None
244
+ ) -> Tuple[torch.Tensor, dict]:
245
+ """
246
+ Generate image/videos from noise and train the critic with generated samples.
247
+ The noisy input to the generator is backward simulated.
248
+ This removes the need of any datasets during distillation.
249
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
250
+ Input:
251
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
252
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
253
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
254
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
255
+ Output:
256
+ - loss: a scalar tensor representing the generator loss.
257
+ - critic_log_dict: a dictionary containing the intermediate tensors for logging.
258
+ """
259
+
260
+ # Step 1: Run generator on backward simulated noisy input
261
+ with torch.no_grad():
262
+ generated_image, _, denoised_timestep_from, denoised_timestep_to = self._run_generator(
263
+ image_or_video_shape=image_or_video_shape,
264
+ conditional_dict=conditional_dict,
265
+ initial_latent=initial_latent
266
+ )
267
+
268
+ # Step 2: Compute the fake prediction
269
+ min_timestep = denoised_timestep_to if self.ts_schedule and denoised_timestep_to is not None else self.min_score_timestep
270
+ max_timestep = denoised_timestep_from if self.ts_schedule_max and denoised_timestep_from is not None else self.num_train_timestep
271
+ critic_timestep = self._get_timestep(
272
+ min_timestep,
273
+ max_timestep,
274
+ image_or_video_shape[0],
275
+ image_or_video_shape[1],
276
+ self.num_frame_per_block,
277
+ uniform_timestep=True
278
+ )
279
+
280
+ if self.timestep_shift > 1:
281
+ critic_timestep = self.timestep_shift * \
282
+ (critic_timestep / 1000) / (1 + (self.timestep_shift - 1) * (critic_timestep / 1000)) * 1000
283
+
284
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
285
+
286
+ critic_noise = torch.randn_like(generated_image)
287
+ noisy_generated_image = self.scheduler.add_noise(
288
+ generated_image.flatten(0, 1),
289
+ critic_noise.flatten(0, 1),
290
+ critic_timestep.flatten(0, 1)
291
+ ).unflatten(0, image_or_video_shape[:2])
292
+
293
+ _, pred_fake_image = self.fake_score(
294
+ noisy_image_or_video=noisy_generated_image,
295
+ conditional_dict=conditional_dict,
296
+ timestep=critic_timestep
297
+ )
298
+
299
+ # Step 3: Compute the denoising loss for the fake critic
300
+ if self.args.denoising_loss_type == "flow":
301
+ from utils.wan_wrapper import WanDiffusionWrapper
302
+ flow_pred = WanDiffusionWrapper._convert_x0_to_flow_pred(
303
+ scheduler=self.scheduler,
304
+ x0_pred=pred_fake_image.flatten(0, 1),
305
+ xt=noisy_generated_image.flatten(0, 1),
306
+ timestep=critic_timestep.flatten(0, 1)
307
+ )
308
+ pred_fake_noise = None
309
+ else:
310
+ flow_pred = None
311
+ pred_fake_noise = self.scheduler.convert_x0_to_noise(
312
+ x0=pred_fake_image.flatten(0, 1),
313
+ xt=noisy_generated_image.flatten(0, 1),
314
+ timestep=critic_timestep.flatten(0, 1)
315
+ ).unflatten(0, image_or_video_shape[:2])
316
+
317
+ denoising_loss = self.denoising_loss_func(
318
+ x=generated_image.flatten(0, 1),
319
+ x_pred=pred_fake_image.flatten(0, 1),
320
+ noise=critic_noise.flatten(0, 1),
321
+ noise_pred=pred_fake_noise,
322
+ alphas_cumprod=self.scheduler.alphas_cumprod,
323
+ timestep=critic_timestep.flatten(0, 1),
324
+ flow_pred=flow_pred
325
+ )
326
+
327
+ # Step 5: Debugging Log
328
+ critic_log_dict = {
329
+ "critic_timestep": critic_timestep.detach()
330
+ }
331
+
332
+ return denoising_loss, critic_log_dict
long_video/model/gan.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ from pipeline import RollingForcingTrainingPipeline
3
+ import torch.nn.functional as F
4
+ from typing import Tuple
5
+ import torch
6
+
7
+ from model.base import RollingForcingModel
8
+
9
+
10
+ class GAN(RollingForcingModel):
11
+ def __init__(self, args, device):
12
+ """
13
+ Initialize the GAN module.
14
+ This class is self-contained and compute generator and fake score losses
15
+ in the forward pass.
16
+ """
17
+ super().__init__(args, device)
18
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
19
+ self.same_step_across_blocks = getattr(args, "same_step_across_blocks", True)
20
+ self.concat_time_embeddings = getattr(args, "concat_time_embeddings", False)
21
+ self.num_class = args.num_class
22
+ self.relativistic_discriminator = getattr(args, "relativistic_discriminator", False)
23
+
24
+ if self.num_frame_per_block > 1:
25
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
26
+
27
+ self.fake_score.adding_cls_branch(
28
+ atten_dim=1536, num_class=args.num_class, time_embed_dim=1536 if self.concat_time_embeddings else 0)
29
+ self.fake_score.model.requires_grad_(True)
30
+
31
+ self.independent_first_frame = getattr(args, "independent_first_frame", False)
32
+ if self.independent_first_frame:
33
+ self.generator.model.independent_first_frame = True
34
+ if args.gradient_checkpointing:
35
+ self.generator.enable_gradient_checkpointing()
36
+ self.fake_score.enable_gradient_checkpointing()
37
+
38
+ # this will be init later with fsdp-wrapped modules
39
+ self.inference_pipeline: RollingForcingTrainingPipeline = None
40
+
41
+ # Step 2: Initialize all dmd hyperparameters
42
+ self.num_train_timestep = args.num_train_timestep
43
+ self.min_step = int(0.02 * self.num_train_timestep)
44
+ self.max_step = int(0.98 * self.num_train_timestep)
45
+ if hasattr(args, "real_guidance_scale"):
46
+ self.real_guidance_scale = args.real_guidance_scale
47
+ self.fake_guidance_scale = args.fake_guidance_scale
48
+ else:
49
+ self.real_guidance_scale = args.guidance_scale
50
+ self.fake_guidance_scale = 0.0
51
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
52
+ self.critic_timestep_shift = getattr(args, "critic_timestep_shift", self.timestep_shift)
53
+ self.ts_schedule = getattr(args, "ts_schedule", True)
54
+ self.ts_schedule_max = getattr(args, "ts_schedule_max", False)
55
+ self.min_score_timestep = getattr(args, "min_score_timestep", 0)
56
+
57
+ self.gan_g_weight = getattr(args, "gan_g_weight", 1e-2)
58
+ self.gan_d_weight = getattr(args, "gan_d_weight", 1e-2)
59
+ self.r1_weight = getattr(args, "r1_weight", 0.0)
60
+ self.r2_weight = getattr(args, "r2_weight", 0.0)
61
+ self.r1_sigma = getattr(args, "r1_sigma", 0.01)
62
+ self.r2_sigma = getattr(args, "r2_sigma", 0.01)
63
+
64
+ if getattr(self.scheduler, "alphas_cumprod", None) is not None:
65
+ self.scheduler.alphas_cumprod = self.scheduler.alphas_cumprod.to(device)
66
+ else:
67
+ self.scheduler.alphas_cumprod = None
68
+
69
+ def _run_cls_pred_branch(self,
70
+ noisy_image_or_video: torch.Tensor,
71
+ conditional_dict: dict,
72
+ timestep: torch.Tensor) -> torch.Tensor:
73
+ """
74
+ Run the classifier prediction branch on the generated image or video.
75
+ Input:
76
+ - image_or_video: a tensor with shape [B, F, C, H, W].
77
+ Output:
78
+ - cls_pred: a tensor with shape [B, 1, 1, 1, 1] representing the feature map for classification.
79
+ """
80
+ _, _, noisy_logit = self.fake_score(
81
+ noisy_image_or_video=noisy_image_or_video,
82
+ conditional_dict=conditional_dict,
83
+ timestep=timestep,
84
+ classify_mode=True,
85
+ concat_time_embeddings=self.concat_time_embeddings
86
+ )
87
+
88
+ return noisy_logit
89
+
90
+ def generator_loss(
91
+ self,
92
+ image_or_video_shape,
93
+ conditional_dict: dict,
94
+ unconditional_dict: dict,
95
+ clean_latent: torch.Tensor,
96
+ initial_latent: torch.Tensor = None
97
+ ) -> Tuple[torch.Tensor, dict]:
98
+ """
99
+ Generate image/videos from noise and compute the DMD loss.
100
+ The noisy input to the generator is backward simulated.
101
+ This removes the need of any datasets during distillation.
102
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
103
+ Input:
104
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
105
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
106
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
107
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
108
+ Output:
109
+ - loss: a scalar tensor representing the generator loss.
110
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
111
+ """
112
+ # Step 1: Unroll generator to obtain fake videos
113
+ pred_image, gradient_mask, denoised_timestep_from, denoised_timestep_to = self._run_generator(
114
+ image_or_video_shape=image_or_video_shape,
115
+ conditional_dict=conditional_dict,
116
+ initial_latent=initial_latent
117
+ )
118
+
119
+ # Step 2: Get timestep and add noise to generated/real latents
120
+ min_timestep = denoised_timestep_to if self.ts_schedule and denoised_timestep_to is not None else self.min_score_timestep
121
+ max_timestep = denoised_timestep_from if self.ts_schedule_max and denoised_timestep_from is not None else self.num_train_timestep
122
+ critic_timestep = self._get_timestep(
123
+ min_timestep,
124
+ max_timestep,
125
+ image_or_video_shape[0],
126
+ image_or_video_shape[1],
127
+ self.num_frame_per_block,
128
+ uniform_timestep=True
129
+ )
130
+
131
+ if self.critic_timestep_shift > 1:
132
+ critic_timestep = self.critic_timestep_shift * \
133
+ (critic_timestep / 1000) / (1 + (self.critic_timestep_shift - 1) * (critic_timestep / 1000)) * 1000
134
+
135
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
136
+
137
+ critic_noise = torch.randn_like(pred_image)
138
+ noisy_fake_latent = self.scheduler.add_noise(
139
+ pred_image.flatten(0, 1),
140
+ critic_noise.flatten(0, 1),
141
+ critic_timestep.flatten(0, 1)
142
+ ).unflatten(0, image_or_video_shape[:2])
143
+
144
+ # Step 4: Compute the real GAN discriminator loss
145
+ real_image_or_video = clean_latent.clone()
146
+ critic_noise = torch.randn_like(real_image_or_video)
147
+ noisy_real_latent = self.scheduler.add_noise(
148
+ real_image_or_video.flatten(0, 1),
149
+ critic_noise.flatten(0, 1),
150
+ critic_timestep.flatten(0, 1)
151
+ ).unflatten(0, image_or_video_shape[:2])
152
+
153
+ conditional_dict["prompt_embeds"] = torch.concatenate(
154
+ (conditional_dict["prompt_embeds"], conditional_dict["prompt_embeds"]), dim=0)
155
+ critic_timestep = torch.concatenate((critic_timestep, critic_timestep), dim=0)
156
+ noisy_latent = torch.concatenate((noisy_fake_latent, noisy_real_latent), dim=0)
157
+ _, _, noisy_logit = self.fake_score(
158
+ noisy_image_or_video=noisy_latent,
159
+ conditional_dict=conditional_dict,
160
+ timestep=critic_timestep,
161
+ classify_mode=True,
162
+ concat_time_embeddings=self.concat_time_embeddings
163
+ )
164
+ noisy_fake_logit, noisy_real_logit = noisy_logit.chunk(2, dim=0)
165
+
166
+ if not self.relativistic_discriminator:
167
+ gan_G_loss = F.softplus(-noisy_fake_logit.float()).mean() * self.gan_g_weight
168
+ else:
169
+ relative_fake_logit = noisy_fake_logit - noisy_real_logit
170
+ gan_G_loss = F.softplus(-relative_fake_logit.float()).mean() * self.gan_g_weight
171
+
172
+ return gan_G_loss
173
+
174
+ def critic_loss(
175
+ self,
176
+ image_or_video_shape,
177
+ conditional_dict: dict,
178
+ unconditional_dict: dict,
179
+ clean_latent: torch.Tensor,
180
+ real_image_or_video: torch.Tensor,
181
+ initial_latent: torch.Tensor = None
182
+ ) -> Tuple[torch.Tensor, dict]:
183
+ """
184
+ Generate image/videos from noise and train the critic with generated samples.
185
+ The noisy input to the generator is backward simulated.
186
+ This removes the need of any datasets during distillation.
187
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
188
+ Input:
189
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
190
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
191
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
192
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
193
+ Output:
194
+ - loss: a scalar tensor representing the generator loss.
195
+ - critic_log_dict: a dictionary containing the intermediate tensors for logging.
196
+ """
197
+
198
+ # Step 1: Run generator on backward simulated noisy input
199
+ with torch.no_grad():
200
+ generated_image, _, denoised_timestep_from, denoised_timestep_to, num_sim_steps = self._run_generator(
201
+ image_or_video_shape=image_or_video_shape,
202
+ conditional_dict=conditional_dict,
203
+ initial_latent=initial_latent
204
+ )
205
+
206
+ # Step 2: Get timestep and add noise to generated/real latents
207
+ min_timestep = denoised_timestep_to if self.ts_schedule and denoised_timestep_to is not None else self.min_score_timestep
208
+ max_timestep = denoised_timestep_from if self.ts_schedule_max and denoised_timestep_from is not None else self.num_train_timestep
209
+ critic_timestep = self._get_timestep(
210
+ min_timestep,
211
+ max_timestep,
212
+ image_or_video_shape[0],
213
+ image_or_video_shape[1],
214
+ self.num_frame_per_block,
215
+ uniform_timestep=True
216
+ )
217
+
218
+ if self.critic_timestep_shift > 1:
219
+ critic_timestep = self.critic_timestep_shift * \
220
+ (critic_timestep / 1000) / (1 + (self.critic_timestep_shift - 1) * (critic_timestep / 1000)) * 1000
221
+
222
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
223
+
224
+ critic_noise = torch.randn_like(generated_image)
225
+ noisy_fake_latent = self.scheduler.add_noise(
226
+ generated_image.flatten(0, 1),
227
+ critic_noise.flatten(0, 1),
228
+ critic_timestep.flatten(0, 1)
229
+ ).unflatten(0, image_or_video_shape[:2])
230
+
231
+ # Step 4: Compute the real GAN discriminator loss
232
+ noisy_real_latent = self.scheduler.add_noise(
233
+ real_image_or_video.flatten(0, 1),
234
+ critic_noise.flatten(0, 1),
235
+ critic_timestep.flatten(0, 1)
236
+ ).unflatten(0, image_or_video_shape[:2])
237
+
238
+ conditional_dict_cloned = copy.deepcopy(conditional_dict)
239
+ conditional_dict_cloned["prompt_embeds"] = torch.concatenate(
240
+ (conditional_dict_cloned["prompt_embeds"], conditional_dict_cloned["prompt_embeds"]), dim=0)
241
+ _, _, noisy_logit = self.fake_score(
242
+ noisy_image_or_video=torch.concatenate((noisy_fake_latent, noisy_real_latent), dim=0),
243
+ conditional_dict=conditional_dict_cloned,
244
+ timestep=torch.concatenate((critic_timestep, critic_timestep), dim=0),
245
+ classify_mode=True,
246
+ concat_time_embeddings=self.concat_time_embeddings
247
+ )
248
+ noisy_fake_logit, noisy_real_logit = noisy_logit.chunk(2, dim=0)
249
+
250
+ if not self.relativistic_discriminator:
251
+ gan_D_loss = F.softplus(-noisy_real_logit.float()).mean() + F.softplus(noisy_fake_logit.float()).mean()
252
+ else:
253
+ relative_real_logit = noisy_real_logit - noisy_fake_logit
254
+ gan_D_loss = F.softplus(-relative_real_logit.float()).mean()
255
+ gan_D_loss = gan_D_loss * self.gan_d_weight
256
+
257
+ # R1 regularization
258
+ if self.r1_weight > 0.:
259
+ noisy_real_latent_perturbed = noisy_real_latent.clone()
260
+ epison_real = self.r1_sigma * torch.randn_like(noisy_real_latent_perturbed)
261
+ noisy_real_latent_perturbed = noisy_real_latent_perturbed + epison_real
262
+ noisy_real_logit_perturbed = self._run_cls_pred_branch(
263
+ noisy_image_or_video=noisy_real_latent_perturbed,
264
+ conditional_dict=conditional_dict,
265
+ timestep=critic_timestep
266
+ )
267
+
268
+ r1_grad = (noisy_real_logit_perturbed - noisy_real_logit) / self.r1_sigma
269
+ r1_loss = self.r1_weight * torch.mean((r1_grad)**2)
270
+ else:
271
+ r1_loss = torch.zeros_like(gan_D_loss)
272
+
273
+ # R2 regularization
274
+ if self.r2_weight > 0.:
275
+ noisy_fake_latent_perturbed = noisy_fake_latent.clone()
276
+ epison_generated = self.r2_sigma * torch.randn_like(noisy_fake_latent_perturbed)
277
+ noisy_fake_latent_perturbed = noisy_fake_latent_perturbed + epison_generated
278
+ noisy_fake_logit_perturbed = self._run_cls_pred_branch(
279
+ noisy_image_or_video=noisy_fake_latent_perturbed,
280
+ conditional_dict=conditional_dict,
281
+ timestep=critic_timestep
282
+ )
283
+
284
+ r2_grad = (noisy_fake_logit_perturbed - noisy_fake_logit) / self.r2_sigma
285
+ r2_loss = self.r2_weight * torch.mean((r2_grad)**2)
286
+ else:
287
+ r2_loss = torch.zeros_like(r2_loss)
288
+
289
+ critic_log_dict = {
290
+ "critic_timestep": critic_timestep.detach(),
291
+ 'noisy_real_logit': noisy_real_logit.detach(),
292
+ 'noisy_fake_logit': noisy_fake_logit.detach(),
293
+ }
294
+
295
+ return (gan_D_loss, r1_loss, r2_loss), critic_log_dict
long_video/model/ode_regression.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch.nn.functional as F
2
+ from typing import Tuple
3
+ import torch
4
+
5
+ from model.base import BaseModel
6
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
7
+
8
+
9
+ class ODERegression(BaseModel):
10
+ def __init__(self, args, device):
11
+ """
12
+ Initialize the ODERegression module.
13
+ This class is self-contained and compute generator losses
14
+ in the forward pass given precomputed ode solution pairs.
15
+ This class supports the ode regression loss for both causal and bidirectional models.
16
+ See Sec 4.3 of CausVid https://arxiv.org/abs/2412.07772 for details
17
+ """
18
+ super().__init__(args, device)
19
+ # Step 1: Initialize all models
20
+ self.generator = WanDiffusionWrapper(**getattr(args, "model_kwargs", {}), is_causal=True)
21
+ self.generator.model.requires_grad_(True)
22
+ if getattr(args, "generator_ckpt", False):
23
+ print(f"Loading pretrained generator from {args.generator_ckpt}")
24
+ state_dict = torch.load(args.generator_ckpt, map_location="cpu")[
25
+ 'generator']
26
+ self.generator.load_state_dict(
27
+ state_dict, strict=True
28
+ )
29
+
30
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
31
+
32
+ if self.num_frame_per_block > 1:
33
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
34
+
35
+ self.independent_first_frame = getattr(args, "independent_first_frame", False)
36
+ if self.independent_first_frame:
37
+ self.generator.model.independent_first_frame = True
38
+ if args.gradient_checkpointing:
39
+ self.generator.enable_gradient_checkpointing()
40
+
41
+ # Step 2: Initialize all hyperparameters
42
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
43
+
44
+
45
+ def _initialize_models(self, args, device):
46
+ self.generator = WanDiffusionWrapper(**getattr(args, "model_kwargs", {}), is_causal=True)
47
+ self.generator.model.requires_grad_(True)
48
+
49
+ self.text_encoder = WanTextEncoder()
50
+ self.text_encoder.requires_grad_(False)
51
+
52
+ self.vae = WanVAEWrapper()
53
+ self.vae.requires_grad_(False)
54
+
55
+ self.scheduler = self.generator.get_scheduler()
56
+ self.scheduler.timesteps = self.scheduler.timesteps.to(device)
57
+
58
+ @torch.no_grad()
59
+ def _prepare_generator_input(self, ode_latent: torch.Tensor, tf=False, causal = True) -> Tuple[torch.Tensor, torch.Tensor]:
60
+ """
61
+ Given a tensor containing the whole ODE sampling trajectories,
62
+ randomly choose an intermediate timestep and return the latent as well as the corresponding timestep.
63
+ Input:
64
+ - ode_latent: a tensor containing the whole ODE sampling trajectories [batch_size, num_denoising_steps, num_frames, num_channels, height, width].
65
+ Output:
66
+ - noisy_input: a tensor containing the selected latent [batch_size, num_frames, num_channels, height, width].
67
+ - timestep: a tensor containing the corresponding timestep [batch_size].
68
+ """
69
+ batch_size, num_denoising_steps, num_frames, num_channels, height, width = ode_latent.shape
70
+
71
+ # Step 1: Randomly choose a timestep for each frame
72
+ uniform_timestep = True
73
+ index = self._get_timestep(
74
+ 0,
75
+ len(self.denoising_step_list),
76
+ batch_size,
77
+ num_frames,
78
+ self.num_frame_per_block,
79
+ uniform_timestep=uniform_timestep
80
+ )
81
+
82
+ if self.args.i2v:
83
+ index[:, 0] = len(self.denoising_step_list) - 1
84
+
85
+ noisy_input = torch.gather(
86
+ ode_latent, dim=1,
87
+ index=index.reshape(batch_size, 1, num_frames, 1, 1, 1).expand(
88
+ -1, -1, -1, num_channels, height, width).to(self.device)
89
+ ).squeeze(1)
90
+
91
+ timestep = self.denoising_step_list[index].to(self.device)
92
+
93
+ return noisy_input, timestep
94
+
95
+ def generator_loss(self, ode_latent: torch.Tensor, conditional_dict: dict) -> Tuple[torch.Tensor, dict]:
96
+ """
97
+ Generate image/videos from noisy latents and compute the ODE regression loss.
98
+ Input:
99
+ - ode_latent: a tensor containing the ODE latents [batch_size, num_denoising_steps, num_frames, num_channels, height, width].
100
+ They are ordered from most noisy to clean latents.
101
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
102
+ Output:
103
+ - loss: a scalar tensor representing the generator loss.
104
+ - log_dict: a dictionary containing additional information for loss timestep breakdown.
105
+ """
106
+ # Step 1: Run generator on noisy latents
107
+ clean_latent = ode_latent[:, -1]
108
+ target_latent = ode_latent[:, -2]
109
+ ode_latent_valid = ode_latent[:, :-1]
110
+
111
+
112
+ noisy_input, timestep = self._prepare_generator_input(
113
+ ode_latent=ode_latent_valid, tf=True, causal = True)
114
+
115
+
116
+ _, pred_image_or_video = self.generator(
117
+ noisy_image_or_video=noisy_input,
118
+ conditional_dict=conditional_dict,
119
+ timestep=timestep,
120
+ clean_x = clean_latent
121
+ )
122
+
123
+ # Step 2: Compute the regression loss
124
+ mask = timestep != 0
125
+
126
+ loss = F.mse_loss(
127
+ pred_image_or_video[mask], target_latent[mask], reduction="mean")
128
+
129
+ log_dict = {
130
+ "unnormalized_loss": F.mse_loss(pred_image_or_video, target_latent, reduction='none').mean(dim=[1, 2, 3, 4]).detach(),
131
+ "timestep": timestep.float().mean(dim=1).detach(),
132
+ "input": noisy_input.detach(),
133
+ "output": pred_image_or_video.detach(),
134
+ }
135
+
136
+ return loss, log_dict
long_video/model/sid.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pipeline import RollingForcingTrainingPipeline
2
+ from typing import Optional, Tuple
3
+ import torch
4
+
5
+ from model.base import RollingForcingModel
6
+
7
+
8
+ class SiD(RollingForcingModel):
9
+ def __init__(self, args, device):
10
+ """
11
+ Initialize the DMD (Distribution Matching Distillation) module.
12
+ This class is self-contained and compute generator and fake score losses
13
+ in the forward pass.
14
+ """
15
+ super().__init__(args, device)
16
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
17
+
18
+ if self.num_frame_per_block > 1:
19
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
20
+
21
+ if args.gradient_checkpointing:
22
+ self.generator.enable_gradient_checkpointing()
23
+ self.fake_score.enable_gradient_checkpointing()
24
+ self.real_score.enable_gradient_checkpointing()
25
+
26
+ # this will be init later with fsdp-wrapped modules
27
+ self.inference_pipeline: RollingForcingTrainingPipeline = None
28
+
29
+ # Step 2: Initialize all dmd hyperparameters
30
+ self.num_train_timestep = args.num_train_timestep
31
+ self.min_step = int(0.02 * self.num_train_timestep)
32
+ self.max_step = int(0.98 * self.num_train_timestep)
33
+ if hasattr(args, "real_guidance_scale"):
34
+ self.real_guidance_scale = args.real_guidance_scale
35
+ else:
36
+ self.real_guidance_scale = args.guidance_scale
37
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
38
+ self.sid_alpha = getattr(args, "sid_alpha", 1.0)
39
+ self.ts_schedule = getattr(args, "ts_schedule", True)
40
+ self.ts_schedule_max = getattr(args, "ts_schedule_max", False)
41
+
42
+ if getattr(self.scheduler, "alphas_cumprod", None) is not None:
43
+ self.scheduler.alphas_cumprod = self.scheduler.alphas_cumprod.to(device)
44
+ else:
45
+ self.scheduler.alphas_cumprod = None
46
+
47
+ def compute_distribution_matching_loss(
48
+ self,
49
+ image_or_video: torch.Tensor,
50
+ conditional_dict: dict,
51
+ unconditional_dict: dict,
52
+ gradient_mask: Optional[torch.Tensor] = None,
53
+ denoised_timestep_from: int = 0,
54
+ denoised_timestep_to: int = 0
55
+ ) -> Tuple[torch.Tensor, dict]:
56
+ """
57
+ Compute the DMD loss (eq 7 in https://arxiv.org/abs/2311.18828).
58
+ Input:
59
+ - image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
60
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
61
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
62
+ - gradient_mask: a boolean tensor with the same shape as image_or_video indicating which pixels to compute loss .
63
+ Output:
64
+ - dmd_loss: a scalar tensor representing the DMD loss.
65
+ - dmd_log_dict: a dictionary containing the intermediate tensors for logging.
66
+ """
67
+ original_latent = image_or_video
68
+
69
+ batch_size, num_frame = image_or_video.shape[:2]
70
+
71
+ # Step 1: Randomly sample timestep based on the given schedule and corresponding noise
72
+ min_timestep = denoised_timestep_to if self.ts_schedule and denoised_timestep_to is not None else self.min_score_timestep
73
+ max_timestep = denoised_timestep_from if self.ts_schedule_max and denoised_timestep_from is not None else self.num_train_timestep
74
+ timestep = self._get_timestep(
75
+ min_timestep,
76
+ max_timestep,
77
+ batch_size,
78
+ num_frame,
79
+ self.num_frame_per_block,
80
+ uniform_timestep=True
81
+ )
82
+
83
+ if self.timestep_shift > 1:
84
+ timestep = self.timestep_shift * \
85
+ (timestep / 1000) / \
86
+ (1 + (self.timestep_shift - 1) * (timestep / 1000)) * 1000
87
+ timestep = timestep.clamp(self.min_step, self.max_step)
88
+
89
+ noise = torch.randn_like(image_or_video)
90
+ noisy_latent = self.scheduler.add_noise(
91
+ image_or_video.flatten(0, 1),
92
+ noise.flatten(0, 1),
93
+ timestep.flatten(0, 1)
94
+ ).unflatten(0, (batch_size, num_frame))
95
+
96
+ # Step 2: SiD (May be wrap it?)
97
+ noisy_image_or_video = noisy_latent
98
+ # Step 2.1: Compute the fake score
99
+ _, pred_fake_image = self.fake_score(
100
+ noisy_image_or_video=noisy_image_or_video,
101
+ conditional_dict=conditional_dict,
102
+ timestep=timestep
103
+ )
104
+ # Step 2.2: Compute the real score
105
+ # We compute the conditional and unconditional prediction
106
+ # and add them together to achieve cfg (https://arxiv.org/abs/2207.12598)
107
+ # NOTE: This step may cause OOM issue, which can be addressed by the CFG-free technique
108
+
109
+ _, pred_real_image_cond = self.real_score(
110
+ noisy_image_or_video=noisy_image_or_video,
111
+ conditional_dict=conditional_dict,
112
+ timestep=timestep
113
+ )
114
+
115
+ _, pred_real_image_uncond = self.real_score(
116
+ noisy_image_or_video=noisy_image_or_video,
117
+ conditional_dict=unconditional_dict,
118
+ timestep=timestep
119
+ )
120
+
121
+ pred_real_image = pred_real_image_cond + (
122
+ pred_real_image_cond - pred_real_image_uncond
123
+ ) * self.real_guidance_scale
124
+
125
+ # Step 2.3: SiD Loss
126
+ # TODO: Add alpha
127
+ # TODO: Double?
128
+ sid_loss = (pred_real_image.double() - pred_fake_image.double()) * ((pred_real_image.double() - original_latent.double()) - self.sid_alpha * (pred_real_image.double() - pred_fake_image.double()))
129
+
130
+ # Step 2.4: Loss normalizer
131
+ with torch.no_grad():
132
+ p_real = (original_latent - pred_real_image)
133
+ normalizer = torch.abs(p_real).mean(dim=[1, 2, 3, 4], keepdim=True)
134
+ sid_loss = sid_loss / normalizer
135
+
136
+ sid_loss = torch.nan_to_num(sid_loss)
137
+ num_frame = sid_loss.shape[1]
138
+ sid_loss = sid_loss.mean()
139
+
140
+ sid_log_dict = {
141
+ "dmdtrain_gradient_norm": torch.zeros_like(sid_loss),
142
+ "timestep": timestep.detach()
143
+ }
144
+
145
+ return sid_loss, sid_log_dict
146
+
147
+ def generator_loss(
148
+ self,
149
+ image_or_video_shape,
150
+ conditional_dict: dict,
151
+ unconditional_dict: dict,
152
+ clean_latent: torch.Tensor,
153
+ initial_latent: torch.Tensor = None
154
+ ) -> Tuple[torch.Tensor, dict]:
155
+ """
156
+ Generate image/videos from noise and compute the DMD loss.
157
+ The noisy input to the generator is backward simulated.
158
+ This removes the need of any datasets during distillation.
159
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
160
+ Input:
161
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
162
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
163
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
164
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
165
+ Output:
166
+ - loss: a scalar tensor representing the generator loss.
167
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
168
+ """
169
+ # Step 1: Unroll generator to obtain fake videos
170
+ pred_image, gradient_mask, denoised_timestep_from, denoised_timestep_to = self._run_generator(
171
+ image_or_video_shape=image_or_video_shape,
172
+ conditional_dict=conditional_dict,
173
+ initial_latent=initial_latent
174
+ )
175
+
176
+ # Step 2: Compute the DMD loss
177
+ dmd_loss, dmd_log_dict = self.compute_distribution_matching_loss(
178
+ image_or_video=pred_image,
179
+ conditional_dict=conditional_dict,
180
+ unconditional_dict=unconditional_dict,
181
+ gradient_mask=gradient_mask,
182
+ denoised_timestep_from=denoised_timestep_from,
183
+ denoised_timestep_to=denoised_timestep_to
184
+ )
185
+
186
+ return dmd_loss, dmd_log_dict
187
+
188
+ def critic_loss(
189
+ self,
190
+ image_or_video_shape,
191
+ conditional_dict: dict,
192
+ unconditional_dict: dict,
193
+ clean_latent: torch.Tensor,
194
+ initial_latent: torch.Tensor = None
195
+ ) -> Tuple[torch.Tensor, dict]:
196
+ """
197
+ Generate image/videos from noise and train the critic with generated samples.
198
+ The noisy input to the generator is backward simulated.
199
+ This removes the need of any datasets during distillation.
200
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
201
+ Input:
202
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
203
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
204
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
205
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
206
+ Output:
207
+ - loss: a scalar tensor representing the generator loss.
208
+ - critic_log_dict: a dictionary containing the intermediate tensors for logging.
209
+ """
210
+
211
+ # Step 1: Run generator on backward simulated noisy input
212
+ with torch.no_grad():
213
+ generated_image, _, denoised_timestep_from, denoised_timestep_to = self._run_generator(
214
+ image_or_video_shape=image_or_video_shape,
215
+ conditional_dict=conditional_dict,
216
+ initial_latent=initial_latent
217
+ )
218
+
219
+ # Step 2: Compute the fake prediction
220
+ min_timestep = denoised_timestep_to if self.ts_schedule and denoised_timestep_to is not None else self.min_score_timestep
221
+ max_timestep = denoised_timestep_from if self.ts_schedule_max and denoised_timestep_from is not None else self.num_train_timestep
222
+ critic_timestep = self._get_timestep(
223
+ min_timestep,
224
+ max_timestep,
225
+ image_or_video_shape[0],
226
+ image_or_video_shape[1],
227
+ self.num_frame_per_block,
228
+ uniform_timestep=True
229
+ )
230
+
231
+ if self.timestep_shift > 1:
232
+ critic_timestep = self.timestep_shift * \
233
+ (critic_timestep / 1000) / (1 + (self.timestep_shift - 1) * (critic_timestep / 1000)) * 1000
234
+
235
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
236
+
237
+ critic_noise = torch.randn_like(generated_image)
238
+ noisy_generated_image = self.scheduler.add_noise(
239
+ generated_image.flatten(0, 1),
240
+ critic_noise.flatten(0, 1),
241
+ critic_timestep.flatten(0, 1)
242
+ ).unflatten(0, image_or_video_shape[:2])
243
+
244
+ _, pred_fake_image = self.fake_score(
245
+ noisy_image_or_video=noisy_generated_image,
246
+ conditional_dict=conditional_dict,
247
+ timestep=critic_timestep
248
+ )
249
+
250
+ # Step 3: Compute the denoising loss for the fake critic
251
+ if self.args.denoising_loss_type == "flow":
252
+ from utils.wan_wrapper import WanDiffusionWrapper
253
+ flow_pred = WanDiffusionWrapper._convert_x0_to_flow_pred(
254
+ scheduler=self.scheduler,
255
+ x0_pred=pred_fake_image.flatten(0, 1),
256
+ xt=noisy_generated_image.flatten(0, 1),
257
+ timestep=critic_timestep.flatten(0, 1)
258
+ )
259
+ pred_fake_noise = None
260
+ else:
261
+ flow_pred = None
262
+ pred_fake_noise = self.scheduler.convert_x0_to_noise(
263
+ x0=pred_fake_image.flatten(0, 1),
264
+ xt=noisy_generated_image.flatten(0, 1),
265
+ timestep=critic_timestep.flatten(0, 1)
266
+ ).unflatten(0, image_or_video_shape[:2])
267
+
268
+ denoising_loss = self.denoising_loss_func(
269
+ x=generated_image.flatten(0, 1),
270
+ x_pred=pred_fake_image.flatten(0, 1),
271
+ noise=critic_noise.flatten(0, 1),
272
+ noise_pred=pred_fake_noise,
273
+ alphas_cumprod=self.scheduler.alphas_cumprod,
274
+ timestep=critic_timestep.flatten(0, 1),
275
+ flow_pred=flow_pred
276
+ )
277
+
278
+ # Step 5: Debugging Log
279
+ critic_log_dict = {
280
+ "critic_timestep": critic_timestep.detach()
281
+ }
282
+
283
+ return denoising_loss, critic_log_dict
long_video/pipeline/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .bidirectional_diffusion_inference import BidirectionalDiffusionInferencePipeline
2
+ from .bidirectional_inference import BidirectionalInferencePipeline
3
+ from .causal_diffusion_inference import CausalDiffusionInferencePipeline
4
+ from .rolling_forcing_inference import CausalInferencePipeline
5
+ from .rolling_forcing_training import RollingForcingTrainingPipeline
6
+
7
+ __all__ = [
8
+ "BidirectionalDiffusionInferencePipeline",
9
+ "BidirectionalInferencePipeline",
10
+ "CausalDiffusionInferencePipeline",
11
+ "CausalInferencePipeline",
12
+ "RollingForcingTrainingPipeline"
13
+ ]
long_video/pipeline/bidirectional_diffusion_inference.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tqdm import tqdm
2
+ from typing import List
3
+ import torch
4
+
5
+ from wan.utils.fm_solvers import FlowDPMSolverMultistepScheduler, get_sampling_sigmas, retrieve_timesteps
6
+ from wan.utils.fm_solvers_unipc import FlowUniPCMultistepScheduler
7
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
8
+
9
+
10
+ class BidirectionalDiffusionInferencePipeline(torch.nn.Module):
11
+ def __init__(
12
+ self,
13
+ args,
14
+ device,
15
+ generator=None,
16
+ text_encoder=None,
17
+ vae=None
18
+ ):
19
+ super().__init__()
20
+ # Step 1: Initialize all models
21
+ self.generator = WanDiffusionWrapper(
22
+ **getattr(args, "model_kwargs", {}), is_causal=False) if generator is None else generator
23
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
24
+ self.vae = WanVAEWrapper() if vae is None else vae
25
+
26
+ # Step 2: Initialize scheduler
27
+ self.num_train_timesteps = args.num_train_timestep
28
+ self.sampling_steps = 50
29
+ self.sample_solver = 'unipc'
30
+ self.shift = 8.0
31
+
32
+ self.args = args
33
+
34
+ def inference(
35
+ self,
36
+ noise: torch.Tensor,
37
+ text_prompts: List[str],
38
+ return_latents=False
39
+ ) -> torch.Tensor:
40
+ """
41
+ Perform inference on the given noise and text prompts.
42
+ Inputs:
43
+ noise (torch.Tensor): The input noise tensor of shape
44
+ (batch_size, num_frames, num_channels, height, width).
45
+ text_prompts (List[str]): The list of text prompts.
46
+ Outputs:
47
+ video (torch.Tensor): The generated video tensor of shape
48
+ (batch_size, num_frames, num_channels, height, width). It is normalized to be in the range [0, 1].
49
+ """
50
+
51
+ conditional_dict = self.text_encoder(
52
+ text_prompts=text_prompts
53
+ )
54
+ unconditional_dict = self.text_encoder(
55
+ text_prompts=[self.args.negative_prompt] * len(text_prompts)
56
+ )
57
+
58
+ latents = noise
59
+
60
+ sample_scheduler = self._initialize_sample_scheduler(noise)
61
+ for _, t in enumerate(tqdm(sample_scheduler.timesteps)):
62
+ latent_model_input = latents
63
+ timestep = t * torch.ones([latents.shape[0], 21], device=noise.device, dtype=torch.float32)
64
+
65
+ flow_pred_cond, _ = self.generator(latent_model_input, conditional_dict, timestep)
66
+ flow_pred_uncond, _ = self.generator(latent_model_input, unconditional_dict, timestep)
67
+
68
+ flow_pred = flow_pred_uncond + self.args.guidance_scale * (
69
+ flow_pred_cond - flow_pred_uncond)
70
+
71
+ temp_x0 = sample_scheduler.step(
72
+ flow_pred.unsqueeze(0),
73
+ t,
74
+ latents.unsqueeze(0),
75
+ return_dict=False)[0]
76
+ latents = temp_x0.squeeze(0)
77
+
78
+ x0 = latents
79
+ video = self.vae.decode_to_pixel(x0)
80
+ video = (video * 0.5 + 0.5).clamp(0, 1)
81
+
82
+ del sample_scheduler
83
+
84
+ if return_latents:
85
+ return video, latents
86
+ else:
87
+ return video
88
+
89
+ def _initialize_sample_scheduler(self, noise):
90
+ if self.sample_solver == 'unipc':
91
+ sample_scheduler = FlowUniPCMultistepScheduler(
92
+ num_train_timesteps=self.num_train_timesteps,
93
+ shift=1,
94
+ use_dynamic_shifting=False)
95
+ sample_scheduler.set_timesteps(
96
+ self.sampling_steps, device=noise.device, shift=self.shift)
97
+ self.timesteps = sample_scheduler.timesteps
98
+ elif self.sample_solver == 'dpm++':
99
+ sample_scheduler = FlowDPMSolverMultistepScheduler(
100
+ num_train_timesteps=self.num_train_timesteps,
101
+ shift=1,
102
+ use_dynamic_shifting=False)
103
+ sampling_sigmas = get_sampling_sigmas(self.sampling_steps, self.shift)
104
+ self.timesteps, _ = retrieve_timesteps(
105
+ sample_scheduler,
106
+ device=noise.device,
107
+ sigmas=sampling_sigmas)
108
+ else:
109
+ raise NotImplementedError("Unsupported solver.")
110
+ return sample_scheduler
long_video/pipeline/bidirectional_inference.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ import torch
3
+
4
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
5
+
6
+
7
+ class BidirectionalInferencePipeline(torch.nn.Module):
8
+ def __init__(
9
+ self,
10
+ args,
11
+ device,
12
+ generator=None,
13
+ text_encoder=None,
14
+ vae=None
15
+ ):
16
+ super().__init__()
17
+ # Step 1: Initialize all models
18
+ self.generator = WanDiffusionWrapper(
19
+ **getattr(args, "model_kwargs", {}), is_causal=False) if generator is None else generator
20
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
21
+ self.vae = WanVAEWrapper() if vae is None else vae
22
+
23
+ # Step 2: Initialize all bidirectional wan hyperparmeters
24
+ self.scheduler = self.generator.get_scheduler()
25
+ self.denoising_step_list = torch.tensor(
26
+ args.denoising_step_list, dtype=torch.long, device=device)
27
+ if self.denoising_step_list[-1] == 0:
28
+ self.denoising_step_list = self.denoising_step_list[:-1] # remove the zero timestep for inference
29
+ if args.warp_denoising_step:
30
+ timesteps = torch.cat((self.scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32)))
31
+ self.denoising_step_list = timesteps[1000 - self.denoising_step_list]
32
+
33
+ def inference(self, noise: torch.Tensor, text_prompts: List[str]) -> torch.Tensor:
34
+ """
35
+ Perform inference on the given noise and text prompts.
36
+ Inputs:
37
+ noise (torch.Tensor): The input noise tensor of shape
38
+ (batch_size, num_frames, num_channels, height, width).
39
+ text_prompts (List[str]): The list of text prompts.
40
+ Outputs:
41
+ video (torch.Tensor): The generated video tensor of shape
42
+ (batch_size, num_frames, num_channels, height, width). It is normalized to be in the range [0, 1].
43
+ """
44
+ conditional_dict = self.text_encoder(
45
+ text_prompts=text_prompts
46
+ )
47
+
48
+ # initial point
49
+ noisy_image_or_video = noise
50
+
51
+ # use the last n-1 timesteps to simulate the generator's input
52
+ for index, current_timestep in enumerate(self.denoising_step_list[:-1]):
53
+ _, pred_image_or_video = self.generator(
54
+ noisy_image_or_video=noisy_image_or_video,
55
+ conditional_dict=conditional_dict,
56
+ timestep=torch.ones(
57
+ noise.shape[:2], dtype=torch.long, device=noise.device) * current_timestep
58
+ ) # [B, F, C, H, W]
59
+
60
+ next_timestep = self.denoising_step_list[index + 1] * torch.ones(
61
+ noise.shape[:2], dtype=torch.long, device=noise.device)
62
+
63
+ noisy_image_or_video = self.scheduler.add_noise(
64
+ pred_image_or_video.flatten(0, 1),
65
+ torch.randn_like(pred_image_or_video.flatten(0, 1)),
66
+ next_timestep.flatten(0, 1)
67
+ ).unflatten(0, noise.shape[:2])
68
+
69
+ video = self.vae.decode_to_pixel(pred_image_or_video)
70
+ video = (video * 0.5 + 0.5).clamp(0, 1)
71
+ return video
long_video/pipeline/causal_diffusion_inference.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tqdm import tqdm
2
+ from typing import List, Optional
3
+ import torch
4
+
5
+ from wan.utils.fm_solvers import FlowDPMSolverMultistepScheduler, get_sampling_sigmas, retrieve_timesteps
6
+ from wan.utils.fm_solvers_unipc import FlowUniPCMultistepScheduler
7
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
8
+
9
+
10
+ class CausalDiffusionInferencePipeline(torch.nn.Module):
11
+ def __init__(
12
+ self,
13
+ args,
14
+ device,
15
+ generator=None,
16
+ text_encoder=None,
17
+ vae=None
18
+ ):
19
+ super().__init__()
20
+ # Step 1: Initialize all models
21
+ self.generator = WanDiffusionWrapper(
22
+ **getattr(args, "model_kwargs", {}), is_causal=True) if generator is None else generator
23
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
24
+ self.vae = WanVAEWrapper() if vae is None else vae
25
+
26
+ # Step 2: Initialize scheduler
27
+ self.num_train_timesteps = args.num_train_timestep
28
+ self.sampling_steps = 50
29
+ self.sample_solver = 'unipc'
30
+ self.shift = args.timestep_shift
31
+
32
+ self.num_transformer_blocks = 30
33
+ self.frame_seq_length = 1560
34
+
35
+ self.kv_cache_pos = None
36
+ self.kv_cache_neg = None
37
+ self.crossattn_cache_pos = None
38
+ self.crossattn_cache_neg = None
39
+ self.args = args
40
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
41
+ self.independent_first_frame = args.independent_first_frame
42
+ self.local_attn_size = self.generator.model.local_attn_size
43
+
44
+ print(f"KV inference with {self.num_frame_per_block} frames per block")
45
+
46
+ if self.num_frame_per_block > 1:
47
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
48
+
49
+ def inference(
50
+ self,
51
+ noise: torch.Tensor,
52
+ text_prompts: List[str],
53
+ initial_latent: Optional[torch.Tensor] = None,
54
+ return_latents: bool = False,
55
+ start_frame_index: Optional[int] = 0
56
+ ) -> torch.Tensor:
57
+ """
58
+ Perform inference on the given noise and text prompts.
59
+ Inputs:
60
+ noise (torch.Tensor): The input noise tensor of shape
61
+ (batch_size, num_output_frames, num_channels, height, width).
62
+ text_prompts (List[str]): The list of text prompts.
63
+ initial_latent (torch.Tensor): The initial latent tensor of shape
64
+ (batch_size, num_input_frames, num_channels, height, width).
65
+ If num_input_frames is 1, perform image to video.
66
+ If num_input_frames is greater than 1, perform video extension.
67
+ return_latents (bool): Whether to return the latents.
68
+ start_frame_index (int): In long video generation, where does the current window start?
69
+ Outputs:
70
+ video (torch.Tensor): The generated video tensor of shape
71
+ (batch_size, num_frames, num_channels, height, width). It is normalized to be in the range [0, 1].
72
+ """
73
+ batch_size, num_frames, num_channels, height, width = noise.shape
74
+ if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
75
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
76
+ # noise should still be a multiple of num_frame_per_block
77
+ assert num_frames % self.num_frame_per_block == 0
78
+ num_blocks = num_frames // self.num_frame_per_block
79
+ elif self.independent_first_frame and initial_latent is None:
80
+ # Using a [1, 4, 4, 4, 4, 4] model to generate a video without image conditioning
81
+ assert (num_frames - 1) % self.num_frame_per_block == 0
82
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
83
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
84
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
85
+ conditional_dict = self.text_encoder(
86
+ text_prompts=text_prompts
87
+ )
88
+ unconditional_dict = self.text_encoder(
89
+ text_prompts=[self.args.negative_prompt] * len(text_prompts)
90
+ )
91
+
92
+ output = torch.zeros(
93
+ [batch_size, num_output_frames, num_channels, height, width],
94
+ device=noise.device,
95
+ dtype=noise.dtype
96
+ )
97
+
98
+ # Step 1: Initialize KV cache to all zeros
99
+ if self.kv_cache_pos is None:
100
+ self._initialize_kv_cache(
101
+ batch_size=batch_size,
102
+ dtype=noise.dtype,
103
+ device=noise.device
104
+ )
105
+ self._initialize_crossattn_cache(
106
+ batch_size=batch_size,
107
+ dtype=noise.dtype,
108
+ device=noise.device
109
+ )
110
+ else:
111
+ # reset cross attn cache
112
+ for block_index in range(self.num_transformer_blocks):
113
+ self.crossattn_cache_pos[block_index]["is_init"] = False
114
+ self.crossattn_cache_neg[block_index]["is_init"] = False
115
+ # reset kv cache
116
+ for block_index in range(len(self.kv_cache_pos)):
117
+ self.kv_cache_pos[block_index]["global_end_index"] = torch.tensor(
118
+ [0], dtype=torch.long, device=noise.device)
119
+ self.kv_cache_pos[block_index]["local_end_index"] = torch.tensor(
120
+ [0], dtype=torch.long, device=noise.device)
121
+ self.kv_cache_neg[block_index]["global_end_index"] = torch.tensor(
122
+ [0], dtype=torch.long, device=noise.device)
123
+ self.kv_cache_neg[block_index]["local_end_index"] = torch.tensor(
124
+ [0], dtype=torch.long, device=noise.device)
125
+
126
+ # Step 2: Cache context feature
127
+ current_start_frame = start_frame_index
128
+ cache_start_frame = 0
129
+ if initial_latent is not None:
130
+ timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
131
+ if self.independent_first_frame:
132
+ # Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
133
+ assert (num_input_frames - 1) % self.num_frame_per_block == 0
134
+ num_input_blocks = (num_input_frames - 1) // self.num_frame_per_block
135
+ output[:, :1] = initial_latent[:, :1]
136
+ self.generator(
137
+ noisy_image_or_video=initial_latent[:, :1],
138
+ conditional_dict=conditional_dict,
139
+ timestep=timestep * 0,
140
+ kv_cache=self.kv_cache_pos,
141
+ crossattn_cache=self.crossattn_cache_pos,
142
+ current_start=current_start_frame * self.frame_seq_length,
143
+ cache_start=cache_start_frame * self.frame_seq_length
144
+ )
145
+ self.generator(
146
+ noisy_image_or_video=initial_latent[:, :1],
147
+ conditional_dict=unconditional_dict,
148
+ timestep=timestep * 0,
149
+ kv_cache=self.kv_cache_neg,
150
+ crossattn_cache=self.crossattn_cache_neg,
151
+ current_start=current_start_frame * self.frame_seq_length,
152
+ cache_start=cache_start_frame * self.frame_seq_length
153
+ )
154
+ current_start_frame += 1
155
+ cache_start_frame += 1
156
+ else:
157
+ # Assume num_input_frames is self.num_frame_per_block * num_input_blocks
158
+ assert num_input_frames % self.num_frame_per_block == 0
159
+ num_input_blocks = num_input_frames // self.num_frame_per_block
160
+
161
+ for block_index in range(num_input_blocks):
162
+ current_ref_latents = \
163
+ initial_latent[:, cache_start_frame:cache_start_frame + self.num_frame_per_block]
164
+ output[:, cache_start_frame:cache_start_frame + self.num_frame_per_block] = current_ref_latents
165
+ self.generator(
166
+ noisy_image_or_video=current_ref_latents,
167
+ conditional_dict=conditional_dict,
168
+ timestep=timestep * 0,
169
+ kv_cache=self.kv_cache_pos,
170
+ crossattn_cache=self.crossattn_cache_pos,
171
+ current_start=current_start_frame * self.frame_seq_length,
172
+ cache_start=cache_start_frame * self.frame_seq_length
173
+ )
174
+ self.generator(
175
+ noisy_image_or_video=current_ref_latents,
176
+ conditional_dict=unconditional_dict,
177
+ timestep=timestep * 0,
178
+ kv_cache=self.kv_cache_neg,
179
+ crossattn_cache=self.crossattn_cache_neg,
180
+ current_start=current_start_frame * self.frame_seq_length,
181
+ cache_start=cache_start_frame * self.frame_seq_length
182
+ )
183
+ current_start_frame += self.num_frame_per_block
184
+ cache_start_frame += self.num_frame_per_block
185
+
186
+ # Step 3: Temporal denoising loop
187
+ all_num_frames = [self.num_frame_per_block] * num_blocks
188
+ if self.independent_first_frame and initial_latent is None:
189
+ all_num_frames = [1] + all_num_frames
190
+ for current_num_frames in all_num_frames:
191
+ noisy_input = noise[
192
+ :, cache_start_frame - num_input_frames:cache_start_frame + current_num_frames - num_input_frames]
193
+ latents = noisy_input
194
+
195
+ # Step 3.1: Spatial denoising loop
196
+ sample_scheduler = self._initialize_sample_scheduler(noise)
197
+ for _, t in enumerate(tqdm(sample_scheduler.timesteps)):
198
+ latent_model_input = latents
199
+ timestep = t * torch.ones(
200
+ [batch_size, current_num_frames], device=noise.device, dtype=torch.float32
201
+ )
202
+
203
+ flow_pred_cond, _ = self.generator(
204
+ noisy_image_or_video=latent_model_input,
205
+ conditional_dict=conditional_dict,
206
+ timestep=timestep,
207
+ kv_cache=self.kv_cache_pos,
208
+ crossattn_cache=self.crossattn_cache_pos,
209
+ current_start=current_start_frame * self.frame_seq_length,
210
+ cache_start=cache_start_frame * self.frame_seq_length
211
+ )
212
+ flow_pred_uncond, _ = self.generator(
213
+ noisy_image_or_video=latent_model_input,
214
+ conditional_dict=unconditional_dict,
215
+ timestep=timestep,
216
+ kv_cache=self.kv_cache_neg,
217
+ crossattn_cache=self.crossattn_cache_neg,
218
+ current_start=current_start_frame * self.frame_seq_length,
219
+ cache_start=cache_start_frame * self.frame_seq_length
220
+ )
221
+
222
+ flow_pred = flow_pred_uncond + self.args.guidance_scale * (
223
+ flow_pred_cond - flow_pred_uncond)
224
+
225
+ temp_x0 = sample_scheduler.step(
226
+ flow_pred,
227
+ t,
228
+ latents,
229
+ return_dict=False)[0]
230
+ latents = temp_x0
231
+ print(f"kv_cache['local_end_index']: {self.kv_cache_pos[0]['local_end_index']}")
232
+ print(f"kv_cache['global_end_index']: {self.kv_cache_pos[0]['global_end_index']}")
233
+
234
+ # Step 3.2: record the model's output
235
+ output[:, cache_start_frame:cache_start_frame + current_num_frames] = latents
236
+
237
+ # Step 3.3: rerun with timestep zero to update KV cache using clean context
238
+ self.generator(
239
+ noisy_image_or_video=latents,
240
+ conditional_dict=conditional_dict,
241
+ timestep=timestep * 0,
242
+ kv_cache=self.kv_cache_pos,
243
+ crossattn_cache=self.crossattn_cache_pos,
244
+ current_start=current_start_frame * self.frame_seq_length,
245
+ cache_start=cache_start_frame * self.frame_seq_length
246
+ )
247
+ self.generator(
248
+ noisy_image_or_video=latents,
249
+ conditional_dict=unconditional_dict,
250
+ timestep=timestep * 0,
251
+ kv_cache=self.kv_cache_neg,
252
+ crossattn_cache=self.crossattn_cache_neg,
253
+ current_start=current_start_frame * self.frame_seq_length,
254
+ cache_start=cache_start_frame * self.frame_seq_length
255
+ )
256
+
257
+ # Step 3.4: update the start and end frame indices
258
+ current_start_frame += current_num_frames
259
+ cache_start_frame += current_num_frames
260
+
261
+ # Step 4: Decode the output
262
+ video = self.vae.decode_to_pixel(output)
263
+ video = (video * 0.5 + 0.5).clamp(0, 1)
264
+
265
+ if return_latents:
266
+ return video, output
267
+ else:
268
+ return video
269
+
270
+ def _initialize_kv_cache(self, batch_size, dtype, device):
271
+ """
272
+ Initialize a Per-GPU KV cache for the Wan model.
273
+ """
274
+ kv_cache_pos = []
275
+ kv_cache_neg = []
276
+ if self.local_attn_size != -1:
277
+ # Use the local attention size to compute the KV cache size
278
+ kv_cache_size = self.local_attn_size * self.frame_seq_length
279
+ else:
280
+ # Use the default KV cache size
281
+ kv_cache_size = 32760
282
+
283
+ for _ in range(self.num_transformer_blocks):
284
+ kv_cache_pos.append({
285
+ "k": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
286
+ "v": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
287
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
288
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
289
+ })
290
+ kv_cache_neg.append({
291
+ "k": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
292
+ "v": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
293
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
294
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
295
+ })
296
+
297
+ self.kv_cache_pos = kv_cache_pos # always store the clean cache
298
+ self.kv_cache_neg = kv_cache_neg # always store the clean cache
299
+
300
+ def _initialize_crossattn_cache(self, batch_size, dtype, device):
301
+ """
302
+ Initialize a Per-GPU cross-attention cache for the Wan model.
303
+ """
304
+ crossattn_cache_pos = []
305
+ crossattn_cache_neg = []
306
+ for _ in range(self.num_transformer_blocks):
307
+ crossattn_cache_pos.append({
308
+ "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
309
+ "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
310
+ "is_init": False
311
+ })
312
+ crossattn_cache_neg.append({
313
+ "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
314
+ "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
315
+ "is_init": False
316
+ })
317
+
318
+ self.crossattn_cache_pos = crossattn_cache_pos # always store the clean cache
319
+ self.crossattn_cache_neg = crossattn_cache_neg # always store the clean cache
320
+
321
+ def _initialize_sample_scheduler(self, noise):
322
+ if self.sample_solver == 'unipc':
323
+ sample_scheduler = FlowUniPCMultistepScheduler(
324
+ num_train_timesteps=self.num_train_timesteps,
325
+ shift=1,
326
+ use_dynamic_shifting=False)
327
+ sample_scheduler.set_timesteps(
328
+ self.sampling_steps, device=noise.device, shift=self.shift)
329
+ self.timesteps = sample_scheduler.timesteps
330
+ elif self.sample_solver == 'dpm++':
331
+ sample_scheduler = FlowDPMSolverMultistepScheduler(
332
+ num_train_timesteps=self.num_train_timesteps,
333
+ shift=1,
334
+ use_dynamic_shifting=False)
335
+ sampling_sigmas = get_sampling_sigmas(self.sampling_steps, self.shift)
336
+ self.timesteps, _ = retrieve_timesteps(
337
+ sample_scheduler,
338
+ device=noise.device,
339
+ sigmas=sampling_sigmas)
340
+ else:
341
+ raise NotImplementedError("Unsupported solver.")
342
+ return sample_scheduler
long_video/pipeline/rolling_forcing_inference.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+ import torch
3
+
4
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
5
+
6
+
7
+ class CausalInferencePipeline(torch.nn.Module):
8
+ def __init__(
9
+ self,
10
+ args,
11
+ device,
12
+ generator=None,
13
+ text_encoder=None,
14
+ vae=None
15
+ ):
16
+ super().__init__()
17
+ # Step 1: Initialize all models
18
+ self.generator = WanDiffusionWrapper(
19
+ **getattr(args, "model_kwargs", {}), is_causal=True) if generator is None else generator
20
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
21
+ self.vae = WanVAEWrapper() if vae is None else vae
22
+
23
+ # Step 2: Initialize all causal hyperparmeters
24
+ self.scheduler = self.generator.get_scheduler()
25
+ self.denoising_step_list = torch.tensor(
26
+ args.denoising_step_list, dtype=torch.long)
27
+ if args.warp_denoising_step:
28
+ timesteps = torch.cat((self.scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32)))
29
+ self.denoising_step_list = timesteps[1000 - self.denoising_step_list]
30
+
31
+ self.num_transformer_blocks = 30
32
+ self.frame_seq_length = 1560
33
+
34
+ self.kv_cache_clean = None
35
+ self.args = args
36
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
37
+ self.independent_first_frame = args.independent_first_frame
38
+ self.local_attn_size = self.generator.model.local_attn_size
39
+
40
+ print(f"KV inference with {self.num_frame_per_block} frames per block")
41
+
42
+ if self.num_frame_per_block > 1:
43
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
44
+
45
+ def inference_rolling_forcing(
46
+ self,
47
+ noise: torch.Tensor,
48
+ text_prompts: List[str],
49
+ initial_latent: Optional[torch.Tensor] = None,
50
+ return_latents: bool = False,
51
+ profile: bool = False
52
+ ) -> torch.Tensor:
53
+ """
54
+ Perform inference on the given noise and text prompts.
55
+ Inputs:
56
+ noise (torch.Tensor): The input noise tensor of shape
57
+ (batch_size, num_output_frames, num_channels, height, width).
58
+ text_prompts (List[str]): The list of text prompts.
59
+ initial_latent (torch.Tensor): The initial latent tensor of shape
60
+ (batch_size, num_input_frames, num_channels, height, width).
61
+ If num_input_frames is 1, perform image to video.
62
+ If num_input_frames is greater than 1, perform video extension.
63
+ return_latents (bool): Whether to return the latents.
64
+ Outputs:
65
+ video (torch.Tensor): The generated video tensor of shape
66
+ (batch_size, num_output_frames, num_channels, height, width).
67
+ It is normalized to be in the range [0, 1].
68
+ """
69
+ batch_size, num_frames, num_channels, height, width = noise.shape
70
+ if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
71
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
72
+ # noise should still be a multiple of num_frame_per_block
73
+ assert num_frames % self.num_frame_per_block == 0
74
+ num_blocks = num_frames // self.num_frame_per_block
75
+ else:
76
+ # Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
77
+ assert (num_frames - 1) % self.num_frame_per_block == 0
78
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
79
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
80
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
81
+ conditional_dict = self.text_encoder(
82
+ text_prompts=text_prompts
83
+ )
84
+
85
+ output = torch.zeros(
86
+ [batch_size, num_output_frames, num_channels, height, width],
87
+ device=noise.device,
88
+ dtype=noise.dtype
89
+ )
90
+
91
+ # Set up profiling if requested
92
+ if profile:
93
+ init_start = torch.cuda.Event(enable_timing=True)
94
+ init_end = torch.cuda.Event(enable_timing=True)
95
+ diffusion_start = torch.cuda.Event(enable_timing=True)
96
+ diffusion_end = torch.cuda.Event(enable_timing=True)
97
+ vae_start = torch.cuda.Event(enable_timing=True)
98
+ vae_end = torch.cuda.Event(enable_timing=True)
99
+ block_times = []
100
+ block_start = torch.cuda.Event(enable_timing=True)
101
+ block_end = torch.cuda.Event(enable_timing=True)
102
+ init_start.record()
103
+
104
+ # Step 1: Initialize KV cache to all zeros
105
+ if self.kv_cache_clean is None:
106
+ self._initialize_kv_cache(
107
+ batch_size=batch_size,
108
+ dtype=noise.dtype,
109
+ device=noise.device
110
+ )
111
+ self._initialize_crossattn_cache(
112
+ batch_size=batch_size,
113
+ dtype=noise.dtype,
114
+ device=noise.device
115
+ )
116
+ else:
117
+ # reset cross attn cache
118
+ for block_index in range(self.num_transformer_blocks):
119
+ self.crossattn_cache[block_index]["is_init"] = False
120
+ # reset kv cache
121
+ for block_index in range(len(self.kv_cache_clean)):
122
+ self.kv_cache_clean[block_index]["global_end_index"] = torch.tensor(
123
+ [0], dtype=torch.long, device=noise.device)
124
+ self.kv_cache_clean[block_index]["local_end_index"] = torch.tensor(
125
+ [0], dtype=torch.long, device=noise.device)
126
+
127
+ # Step 2: Cache context feature
128
+ if initial_latent is not None:
129
+ timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
130
+ if self.independent_first_frame:
131
+ # Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
132
+ assert (num_input_frames - 1) % self.num_frame_per_block == 0
133
+ num_input_blocks = (num_input_frames - 1) // self.num_frame_per_block
134
+ output[:, :1] = initial_latent[:, :1]
135
+ self.generator(
136
+ noisy_image_or_video=initial_latent[:, :1],
137
+ conditional_dict=conditional_dict,
138
+ timestep=timestep * 0,
139
+ kv_cache=self.kv_cache_clean,
140
+ crossattn_cache=self.crossattn_cache,
141
+ current_start=current_start_frame * self.frame_seq_length,
142
+ )
143
+ current_start_frame += 1
144
+ else:
145
+ # Assume num_input_frames is self.num_frame_per_block * num_input_blocks
146
+ assert num_input_frames % self.num_frame_per_block == 0
147
+ num_input_blocks = num_input_frames // self.num_frame_per_block
148
+
149
+ for _ in range(num_input_blocks):
150
+ current_ref_latents = \
151
+ initial_latent[:, current_start_frame:current_start_frame + self.num_frame_per_block]
152
+ output[:, current_start_frame:current_start_frame + self.num_frame_per_block] = current_ref_latents
153
+ self.generator(
154
+ noisy_image_or_video=current_ref_latents,
155
+ conditional_dict=conditional_dict,
156
+ timestep=timestep * 0,
157
+ kv_cache=self.kv_cache_clean,
158
+ crossattn_cache=self.crossattn_cache,
159
+ current_start=current_start_frame * self.frame_seq_length,
160
+ )
161
+ current_start_frame += self.num_frame_per_block
162
+
163
+ if profile:
164
+ init_end.record()
165
+ torch.cuda.synchronize()
166
+ diffusion_start.record()
167
+
168
+ # implementing rolling forcing
169
+ # construct the rolling forcing windows
170
+ num_denoising_steps = len(self.denoising_step_list)
171
+ rolling_window_length_blocks = num_denoising_steps
172
+ window_start_blocks = []
173
+ window_end_blocks = []
174
+ window_num = num_blocks + rolling_window_length_blocks - 1
175
+
176
+ for window_index in range(window_num):
177
+ start_block = max(0, window_index - rolling_window_length_blocks + 1)
178
+ end_block = min(num_blocks - 1, window_index)
179
+ window_start_blocks.append(start_block)
180
+ window_end_blocks.append(end_block)
181
+
182
+ # init noisy cache
183
+ noisy_cache = torch.zeros(
184
+ [batch_size, num_output_frames, num_channels, height, width],
185
+ device=noise.device,
186
+ dtype=noise.dtype
187
+ )
188
+
189
+ # init denosing timestep, same accross windows
190
+ shared_timestep = torch.ones(
191
+ [batch_size, rolling_window_length_blocks * self.num_frame_per_block],
192
+ device=noise.device,
193
+ dtype=torch.float32)
194
+
195
+ for index, current_timestep in enumerate(reversed(self.denoising_step_list)): # from clean to noisy
196
+ shared_timestep[:, index * self.num_frame_per_block:(index + 1) * self.num_frame_per_block] *= current_timestep
197
+
198
+
199
+ # Denoising loop with rolling forcing
200
+ for window_index in range(window_num):
201
+
202
+ if profile:
203
+ block_start.record()
204
+
205
+ print('window_index:', window_index)
206
+ start_block = window_start_blocks[window_index]
207
+ end_block = window_end_blocks[window_index] # include
208
+ print(f"start_block: {start_block}, end_block: {end_block}")
209
+
210
+ current_start_frame = start_block * self.num_frame_per_block
211
+ current_end_frame = (end_block + 1) * self.num_frame_per_block # not include
212
+ current_num_frames = current_end_frame - current_start_frame
213
+
214
+ # noisy_input: new noise and previous denoised noisy frames, only last block is pure noise
215
+ if current_num_frames == rolling_window_length_blocks * self.num_frame_per_block or current_start_frame == 0:
216
+ noisy_input = torch.cat([
217
+ noisy_cache[:, current_start_frame : current_end_frame - self.num_frame_per_block],
218
+ noise[:, current_end_frame - self.num_frame_per_block : current_end_frame ]
219
+ ], dim=1)
220
+ else: # at the end of the video
221
+ noisy_input = noisy_cache[:, current_start_frame:current_end_frame]
222
+
223
+ # init denosing timestep
224
+ if current_num_frames == rolling_window_length_blocks * self.num_frame_per_block:
225
+ current_timestep = shared_timestep
226
+ elif current_start_frame == 0:
227
+ current_timestep = shared_timestep[:,-current_num_frames:]
228
+ elif current_end_frame == num_frames:
229
+ current_timestep = shared_timestep[:,:current_num_frames]
230
+ else:
231
+ raise ValueError("current_num_frames should be equal to rolling_window_length_blocks * self.num_frame_per_block, or the first or last window.")
232
+
233
+
234
+ # calling DiT
235
+ _, denoised_pred = self.generator(
236
+ noisy_image_or_video=noisy_input,
237
+ conditional_dict=conditional_dict,
238
+ timestep=current_timestep,
239
+ kv_cache=self.kv_cache_clean,
240
+ crossattn_cache=self.crossattn_cache,
241
+ current_start=current_start_frame * self.frame_seq_length
242
+ )
243
+
244
+ output[:, current_start_frame:current_end_frame] = denoised_pred
245
+
246
+
247
+ # update noisy_cache, which is detached from the computation graph
248
+ with torch.no_grad():
249
+ for block_idx in range(start_block, end_block + 1):
250
+
251
+ block_time_step = current_timestep[:,
252
+ (block_idx - start_block)*self.num_frame_per_block :
253
+ (block_idx - start_block+1)*self.num_frame_per_block].mean().item()
254
+ matches = torch.abs(self.denoising_step_list - block_time_step) < 1e-4
255
+ block_timestep_index = torch.nonzero(matches, as_tuple=True)[0]
256
+
257
+ if block_timestep_index == len(self.denoising_step_list) - 1:
258
+ continue
259
+
260
+ next_timestep = self.denoising_step_list[block_timestep_index + 1].to(noise.device)
261
+
262
+ noisy_cache[:, block_idx * self.num_frame_per_block:
263
+ (block_idx+1) * self.num_frame_per_block] = \
264
+ self.scheduler.add_noise(
265
+ denoised_pred.flatten(0, 1),
266
+ torch.randn_like(denoised_pred.flatten(0, 1)),
267
+ next_timestep * torch.ones(
268
+ [batch_size * current_num_frames], device=noise.device, dtype=torch.long)
269
+ ).unflatten(0, denoised_pred.shape[:2])[:, (block_idx - start_block)*self.num_frame_per_block:
270
+ (block_idx - start_block+1)*self.num_frame_per_block]
271
+
272
+
273
+ # rerun with timestep zero to update the clean cache, which is also detached from the computation graph
274
+ with torch.no_grad():
275
+ context_timestep = torch.ones_like(current_timestep) * self.args.context_noise
276
+ # # add context noise
277
+ # denoised_pred = self.scheduler.add_noise(
278
+ # denoised_pred.flatten(0, 1),
279
+ # torch.randn_like(denoised_pred.flatten(0, 1)),
280
+ # context_timestep * torch.ones(
281
+ # [batch_size * current_num_frames], device=noise.device, dtype=torch.long)
282
+ # ).unflatten(0, denoised_pred.shape[:2])
283
+
284
+ # only cache the first block
285
+ denoised_pred = denoised_pred[:,:self.num_frame_per_block]
286
+ context_timestep = context_timestep[:,:self.num_frame_per_block]
287
+ self.generator(
288
+ noisy_image_or_video=denoised_pred,
289
+ conditional_dict=conditional_dict,
290
+ timestep=context_timestep,
291
+ kv_cache=self.kv_cache_clean,
292
+ crossattn_cache=self.crossattn_cache,
293
+ current_start=current_start_frame * self.frame_seq_length,
294
+ updating_cache=True,
295
+ )
296
+
297
+ if profile:
298
+ block_end.record()
299
+ torch.cuda.synchronize()
300
+ block_time = block_start.elapsed_time(block_end)
301
+ block_times.append(block_time)
302
+
303
+
304
+ if profile:
305
+ # End diffusion timing and synchronize CUDA
306
+ diffusion_end.record()
307
+ torch.cuda.synchronize()
308
+ diffusion_time = diffusion_start.elapsed_time(diffusion_end)
309
+ init_time = init_start.elapsed_time(init_end)
310
+ vae_start.record()
311
+
312
+ # Step 4: Decode the output
313
+ video = self.vae.decode_to_pixel(output, use_cache=False)
314
+ video = (video * 0.5 + 0.5).clamp(0, 1)
315
+
316
+ if profile:
317
+ # End VAE timing and synchronize CUDA
318
+ vae_end.record()
319
+ torch.cuda.synchronize()
320
+ vae_time = vae_start.elapsed_time(vae_end)
321
+ total_time = init_time + diffusion_time + vae_time
322
+
323
+ print("Profiling results:")
324
+ print(f" - Initialization/caching time: {init_time:.2f} ms ({100 * init_time / total_time:.2f}%)")
325
+ print(f" - Diffusion generation time: {diffusion_time:.2f} ms ({100 * diffusion_time / total_time:.2f}%)")
326
+ for i, block_time in enumerate(block_times):
327
+ print(f" - Block {i} generation time: {block_time:.2f} ms ({100 * block_time / diffusion_time:.2f}% of diffusion)")
328
+ print(f" - VAE decoding time: {vae_time:.2f} ms ({100 * vae_time / total_time:.2f}%)")
329
+ print(f" - Total time: {total_time:.2f} ms")
330
+
331
+ if return_latents:
332
+ return video, output
333
+ else:
334
+ return video
335
+
336
+
337
+
338
+ def _initialize_kv_cache(self, batch_size, dtype, device):
339
+ """
340
+ Initialize a Per-GPU KV cache for the Wan model.
341
+ """
342
+ kv_cache_clean = []
343
+ # if self.local_attn_size != -1:
344
+ # # Use the local attention size to compute the KV cache size
345
+ # kv_cache_size = self.local_attn_size * self.frame_seq_length
346
+ # else:
347
+ # # Use the default KV cache size
348
+ kv_cache_size = 1560 * 24
349
+
350
+ for _ in range(self.num_transformer_blocks):
351
+ kv_cache_clean.append({
352
+ "k": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
353
+ "v": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
354
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
355
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
356
+ })
357
+
358
+ self.kv_cache_clean = kv_cache_clean # always store the clean cache
359
+
360
+ def _initialize_crossattn_cache(self, batch_size, dtype, device):
361
+ """
362
+ Initialize a Per-GPU cross-attention cache for the Wan model.
363
+ """
364
+ crossattn_cache = []
365
+
366
+ for _ in range(self.num_transformer_blocks):
367
+ crossattn_cache.append({
368
+ "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
369
+ "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
370
+ "is_init": False
371
+ })
372
+ self.crossattn_cache = crossattn_cache
long_video/pipeline/rolling_forcing_training.py ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.wan_wrapper import WanDiffusionWrapper
2
+ from utils.scheduler import SchedulerInterface
3
+ from typing import List, Optional
4
+ import torch
5
+ import torch.distributed as dist
6
+
7
+
8
+ class RollingForcingTrainingPipeline:
9
+ def __init__(self,
10
+ denoising_step_list: List[int],
11
+ scheduler: SchedulerInterface,
12
+ generator: WanDiffusionWrapper,
13
+ num_frame_per_block=3,
14
+ independent_first_frame: bool = False,
15
+ same_step_across_blocks: bool = False,
16
+ last_step_only: bool = False,
17
+ num_max_frames: int = 21,
18
+ context_noise: int = 0,
19
+ **kwargs):
20
+ super().__init__()
21
+ self.scheduler = scheduler
22
+ self.generator = generator
23
+ self.denoising_step_list = denoising_step_list
24
+ if self.denoising_step_list[-1] == 0:
25
+ self.denoising_step_list = self.denoising_step_list[:-1] # remove the zero timestep for inference
26
+
27
+ # Wan specific hyperparameters
28
+ self.num_transformer_blocks = 30
29
+ self.frame_seq_length = 1560
30
+ self.num_frame_per_block = num_frame_per_block
31
+ self.context_noise = context_noise
32
+ self.i2v = False
33
+
34
+ self.kv_cache_clean = None
35
+ self.kv_cache2 = None
36
+ self.independent_first_frame = independent_first_frame
37
+ self.same_step_across_blocks = same_step_across_blocks
38
+ self.last_step_only = last_step_only
39
+ self.kv_cache_size = num_max_frames * self.frame_seq_length
40
+
41
+ def generate_and_sync_list(self, num_blocks, num_denoising_steps, device):
42
+ rank = dist.get_rank() if dist.is_initialized() else 0
43
+
44
+ if rank == 0:
45
+ # Generate random indices
46
+ indices = torch.randint(
47
+ low=0,
48
+ high=num_denoising_steps,
49
+ size=(num_blocks,),
50
+ device=device
51
+ )
52
+ if self.last_step_only:
53
+ indices = torch.ones_like(indices) * (num_denoising_steps - 1)
54
+ else:
55
+ indices = torch.empty(num_blocks, dtype=torch.long, device=device)
56
+
57
+ dist.broadcast(indices, src=0) # Broadcast the random indices to all ranks
58
+ return indices.tolist()
59
+
60
+ def generate_list(self, num_blocks, num_denoising_steps, device):
61
+
62
+ # Generate random indices
63
+ indices = torch.randint(
64
+ low=0,
65
+ high=num_denoising_steps,
66
+ size=(num_blocks,),
67
+ device=device
68
+ )
69
+ if self.last_step_only:
70
+ indices = torch.ones_like(indices) * (num_denoising_steps - 1)
71
+
72
+ return indices.tolist()
73
+
74
+
75
+ def inference_with_rolling_forcing(
76
+ self,
77
+ noise: torch.Tensor,
78
+ initial_latent: Optional[torch.Tensor] = None,
79
+ return_sim_step: bool = False,
80
+ **conditional_dict
81
+ ) -> torch.Tensor:
82
+ batch_size, num_frames, num_channels, height, width = noise.shape
83
+ if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
84
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
85
+ # noise should still be a multiple of num_frame_per_block
86
+ assert num_frames % self.num_frame_per_block == 0
87
+ num_blocks = num_frames // self.num_frame_per_block
88
+ else:
89
+ # Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
90
+ assert (num_frames - 1) % self.num_frame_per_block == 0
91
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
92
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
93
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
94
+ output = torch.zeros(
95
+ [batch_size, num_output_frames, num_channels, height, width],
96
+ device=noise.device,
97
+ dtype=noise.dtype
98
+ )
99
+
100
+ # Step 1: Initialize KV cache to all zeros
101
+ self._initialize_kv_cache(
102
+ batch_size=batch_size, dtype=noise.dtype, device=noise.device
103
+ )
104
+ self._initialize_crossattn_cache(
105
+ batch_size=batch_size, dtype=noise.dtype, device=noise.device
106
+ )
107
+
108
+ # implementing rolling forcing
109
+ # construct the rolling forcing windows
110
+ num_denoising_steps = len(self.denoising_step_list)
111
+ rolling_window_length_blocks = num_denoising_steps
112
+ window_start_blocks = []
113
+ window_end_blocks = []
114
+ window_num = num_blocks + rolling_window_length_blocks - 1
115
+
116
+ for window_index in range(window_num):
117
+ start_block = max(0, window_index - rolling_window_length_blocks + 1)
118
+ end_block = min(num_blocks - 1, window_index)
119
+ window_start_blocks.append(start_block)
120
+ window_end_blocks.append(end_block)
121
+
122
+ # exit_flag indicates the window at which the model will backpropagate gradients.
123
+ exit_flag = torch.randint(high=rolling_window_length_blocks, device=noise.device, size=())
124
+ start_gradient_frame_index = num_output_frames - 21
125
+
126
+ # init noisy cache
127
+ noisy_cache = torch.zeros(
128
+ [batch_size, num_output_frames, num_channels, height, width],
129
+ device=noise.device,
130
+ dtype=noise.dtype
131
+ )
132
+
133
+ # init denosing timestep, same accross windows
134
+ shared_timestep = torch.ones(
135
+ [batch_size, rolling_window_length_blocks * self.num_frame_per_block],
136
+ device=noise.device,
137
+ dtype=torch.float32)
138
+
139
+ for index, current_timestep in enumerate(reversed(self.denoising_step_list)): # from clean to noisy
140
+ shared_timestep[:, index * self.num_frame_per_block:(index + 1) * self.num_frame_per_block] *= current_timestep
141
+
142
+
143
+ # Denoising loop with rolling forcing
144
+ for window_index in range(window_num):
145
+ start_block = window_start_blocks[window_index]
146
+ end_block = window_end_blocks[window_index] # include
147
+
148
+ current_start_frame = start_block * self.num_frame_per_block
149
+ current_end_frame = (end_block + 1) * self.num_frame_per_block # not include
150
+ current_num_frames = current_end_frame - current_start_frame
151
+
152
+ # noisy_input: new noise and previous denoised noisy frames, only last block is pure noise
153
+ if current_num_frames == rolling_window_length_blocks * self.num_frame_per_block or current_start_frame == 0:
154
+ noisy_input = torch.cat([
155
+ noisy_cache[:, current_start_frame : current_end_frame - self.num_frame_per_block],
156
+ noise[:, current_end_frame - self.num_frame_per_block : current_end_frame ]
157
+ ], dim=1)
158
+ else: # at the end of the video
159
+ noisy_input = noisy_cache[:, current_start_frame:current_end_frame].clone()
160
+
161
+ # init denosing timestep
162
+ if current_num_frames == rolling_window_length_blocks * self.num_frame_per_block:
163
+ current_timestep = shared_timestep
164
+ elif current_start_frame == 0:
165
+ current_timestep = shared_timestep[:,-current_num_frames:]
166
+ elif current_end_frame == num_frames:
167
+ current_timestep = shared_timestep[:,:current_num_frames]
168
+ else:
169
+ raise ValueError("current_num_frames should be equal to rolling_window_length_blocks * self.num_frame_per_block, or the first or last window.")
170
+
171
+ require_grad = window_index % rolling_window_length_blocks == exit_flag
172
+ if current_end_frame <= start_gradient_frame_index:
173
+ require_grad = False
174
+
175
+ # calling DiT
176
+ if not require_grad:
177
+ with torch.no_grad():
178
+ _, denoised_pred = self.generator(
179
+ noisy_image_or_video=noisy_input,
180
+ conditional_dict=conditional_dict,
181
+ timestep=current_timestep,
182
+ kv_cache=self.kv_cache_clean,
183
+ crossattn_cache=self.crossattn_cache,
184
+ current_start=current_start_frame * self.frame_seq_length
185
+ )
186
+ else:
187
+ _, denoised_pred = self.generator(
188
+ noisy_image_or_video=noisy_input,
189
+ conditional_dict=conditional_dict,
190
+ timestep=current_timestep,
191
+ kv_cache=self.kv_cache_clean,
192
+ crossattn_cache=self.crossattn_cache,
193
+ current_start=current_start_frame * self.frame_seq_length
194
+ )
195
+ output[:, current_start_frame:current_end_frame] = denoised_pred
196
+
197
+
198
+ # update noisy_cache, which is detached from the computation graph
199
+ with torch.no_grad():
200
+ for block_idx in range(start_block, end_block + 1):
201
+
202
+ block_time_step = current_timestep[:,
203
+ (block_idx - start_block)*self.num_frame_per_block :
204
+ (block_idx - start_block+1)*self.num_frame_per_block].mean().item()
205
+ matches = torch.abs(self.denoising_step_list - block_time_step) < 1e-4
206
+ block_timestep_index = torch.nonzero(matches, as_tuple=True)[0]
207
+
208
+ if block_timestep_index == len(self.denoising_step_list) - 1:
209
+ continue
210
+
211
+ next_timestep = self.denoising_step_list[block_timestep_index + 1].to(noise.device)
212
+
213
+ noisy_cache[:, block_idx * self.num_frame_per_block:
214
+ (block_idx+1) * self.num_frame_per_block] = \
215
+ self.scheduler.add_noise(
216
+ denoised_pred.flatten(0, 1),
217
+ torch.randn_like(denoised_pred.flatten(0, 1)),
218
+ next_timestep * torch.ones(
219
+ [batch_size * current_num_frames], device=noise.device, dtype=torch.long)
220
+ ).unflatten(0, denoised_pred.shape[:2])[:, (block_idx - start_block)*self.num_frame_per_block:
221
+ (block_idx - start_block+1)*self.num_frame_per_block]
222
+
223
+
224
+ # rerun with timestep zero to update the clean cache, which is also detached from the computation graph
225
+ with torch.no_grad():
226
+ context_timestep = torch.ones_like(current_timestep) * self.context_noise
227
+ # # add context noise
228
+ # denoised_pred = self.scheduler.add_noise(
229
+ # denoised_pred.flatten(0, 1),
230
+ # torch.randn_like(denoised_pred.flatten(0, 1)),
231
+ # context_timestep * torch.ones(
232
+ # [batch_size * current_num_frames], device=noise.device, dtype=torch.long)
233
+ # ).unflatten(0, denoised_pred.shape[:2])
234
+
235
+ # only cache the first block
236
+ denoised_pred = denoised_pred[:,:self.num_frame_per_block]
237
+ context_timestep = context_timestep[:,:self.num_frame_per_block]
238
+ self.generator(
239
+ noisy_image_or_video=denoised_pred,
240
+ conditional_dict=conditional_dict,
241
+ timestep=context_timestep,
242
+ kv_cache=self.kv_cache_clean,
243
+ crossattn_cache=self.crossattn_cache,
244
+ current_start=current_start_frame * self.frame_seq_length,
245
+ updating_cache=True,
246
+ )
247
+
248
+ # Step 3.5: Return the denoised timestep
249
+ # can ignore since not used
250
+ denoised_timestep_from, denoised_timestep_to = None, None
251
+
252
+ return output, denoised_timestep_from, denoised_timestep_to
253
+
254
+
255
+
256
+ def inference_with_self_forcing(
257
+ self,
258
+ noise: torch.Tensor,
259
+ initial_latent: Optional[torch.Tensor] = None,
260
+ return_sim_step: bool = False,
261
+ **conditional_dict
262
+ ) -> torch.Tensor:
263
+ batch_size, num_frames, num_channels, height, width = noise.shape
264
+ if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
265
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
266
+ # noise should still be a multiple of num_frame_per_block
267
+ assert num_frames % self.num_frame_per_block == 0
268
+ num_blocks = num_frames // self.num_frame_per_block
269
+ else:
270
+ # Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
271
+ assert (num_frames - 1) % self.num_frame_per_block == 0
272
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
273
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
274
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
275
+ output = torch.zeros(
276
+ [batch_size, num_output_frames, num_channels, height, width],
277
+ device=noise.device,
278
+ dtype=noise.dtype
279
+ )
280
+
281
+ # Step 1: Initialize KV cache to all zeros
282
+ self._initialize_kv_cache(
283
+ batch_size=batch_size, dtype=noise.dtype, device=noise.device
284
+ )
285
+ self._initialize_crossattn_cache(
286
+ batch_size=batch_size, dtype=noise.dtype, device=noise.device
287
+ )
288
+ # if self.kv_cache_clean is None:
289
+ # self._initialize_kv_cache(
290
+ # batch_size=batch_size,
291
+ # dtype=noise.dtype,
292
+ # device=noise.device,
293
+ # )
294
+ # self._initialize_crossattn_cache(
295
+ # batch_size=batch_size,
296
+ # dtype=noise.dtype,
297
+ # device=noise.device
298
+ # )
299
+ # else:
300
+ # # reset cross attn cache
301
+ # for block_index in range(self.num_transformer_blocks):
302
+ # self.crossattn_cache[block_index]["is_init"] = False
303
+ # # reset kv cache
304
+ # for block_index in range(len(self.kv_cache_clean)):
305
+ # self.kv_cache_clean[block_index]["global_end_index"] = torch.tensor(
306
+ # [0], dtype=torch.long, device=noise.device)
307
+ # self.kv_cache_clean[block_index]["local_end_index"] = torch.tensor(
308
+ # [0], dtype=torch.long, device=noise.device)
309
+
310
+ # Step 2: Cache context feature
311
+ current_start_frame = 0
312
+ if initial_latent is not None:
313
+ timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
314
+ # Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
315
+ output[:, :1] = initial_latent
316
+ with torch.no_grad():
317
+ self.generator(
318
+ noisy_image_or_video=initial_latent,
319
+ conditional_dict=conditional_dict,
320
+ timestep=timestep * 0,
321
+ kv_cache=self.kv_cache_clean,
322
+ crossattn_cache=self.crossattn_cache,
323
+ current_start=current_start_frame * self.frame_seq_length
324
+ )
325
+ current_start_frame += 1
326
+
327
+ # Step 3: Temporal denoising loop
328
+ all_num_frames = [self.num_frame_per_block] * num_blocks
329
+ if self.independent_first_frame and initial_latent is None:
330
+ all_num_frames = [1] + all_num_frames
331
+ num_denoising_steps = len(self.denoising_step_list)
332
+ exit_flags = self.generate_and_sync_list(len(all_num_frames), num_denoising_steps, device=noise.device)
333
+ start_gradient_frame_index = num_output_frames - 21
334
+
335
+ # for block_index in range(num_blocks):
336
+ for block_index, current_num_frames in enumerate(all_num_frames):
337
+ noisy_input = noise[
338
+ :, current_start_frame - num_input_frames:current_start_frame + current_num_frames - num_input_frames]
339
+
340
+ # Step 3.1: Spatial denoising loop
341
+ for index, current_timestep in enumerate(self.denoising_step_list):
342
+ if self.same_step_across_blocks:
343
+ exit_flag = (index == exit_flags[0])
344
+ else:
345
+ exit_flag = (index == exit_flags[block_index]) # Only backprop at the randomly selected timestep (consistent across all ranks)
346
+ timestep = torch.ones(
347
+ [batch_size, current_num_frames],
348
+ device=noise.device,
349
+ dtype=torch.int64) * current_timestep
350
+
351
+ if not exit_flag:
352
+ with torch.no_grad():
353
+ _, denoised_pred = self.generator(
354
+ noisy_image_or_video=noisy_input,
355
+ conditional_dict=conditional_dict,
356
+ timestep=timestep,
357
+ kv_cache=self.kv_cache_clean,
358
+ crossattn_cache=self.crossattn_cache,
359
+ current_start=current_start_frame * self.frame_seq_length
360
+ )
361
+ next_timestep = self.denoising_step_list[index + 1]
362
+ noisy_input = self.scheduler.add_noise(
363
+ denoised_pred.flatten(0, 1),
364
+ torch.randn_like(denoised_pred.flatten(0, 1)),
365
+ next_timestep * torch.ones(
366
+ [batch_size * current_num_frames], device=noise.device, dtype=torch.long)
367
+ ).unflatten(0, denoised_pred.shape[:2])
368
+ else:
369
+ # for getting real output
370
+ # with torch.set_grad_enabled(current_start_frame >= start_gradient_frame_index):
371
+ if current_start_frame < start_gradient_frame_index:
372
+ with torch.no_grad():
373
+ _, denoised_pred = self.generator(
374
+ noisy_image_or_video=noisy_input,
375
+ conditional_dict=conditional_dict,
376
+ timestep=timestep,
377
+ kv_cache=self.kv_cache_clean,
378
+ crossattn_cache=self.crossattn_cache,
379
+ current_start=current_start_frame * self.frame_seq_length
380
+ )
381
+ else:
382
+ _, denoised_pred = self.generator(
383
+ noisy_image_or_video=noisy_input,
384
+ conditional_dict=conditional_dict,
385
+ timestep=timestep,
386
+ kv_cache=self.kv_cache_clean,
387
+ crossattn_cache=self.crossattn_cache,
388
+ current_start=current_start_frame * self.frame_seq_length
389
+ )
390
+ break
391
+
392
+ # Step 3.2: record the model's output
393
+ output[:, current_start_frame:current_start_frame + current_num_frames] = denoised_pred
394
+
395
+ # Step 3.3: rerun with timestep zero to update the cache
396
+ context_timestep = torch.ones_like(timestep) * self.context_noise
397
+ # add context noise
398
+ denoised_pred = self.scheduler.add_noise(
399
+ denoised_pred.flatten(0, 1),
400
+ torch.randn_like(denoised_pred.flatten(0, 1)),
401
+ context_timestep * torch.ones(
402
+ [batch_size * current_num_frames], device=noise.device, dtype=torch.long)
403
+ ).unflatten(0, denoised_pred.shape[:2])
404
+ with torch.no_grad():
405
+ self.generator(
406
+ noisy_image_or_video=denoised_pred,
407
+ conditional_dict=conditional_dict,
408
+ timestep=context_timestep,
409
+ kv_cache=self.kv_cache_clean,
410
+ crossattn_cache=self.crossattn_cache,
411
+ current_start=current_start_frame * self.frame_seq_length,
412
+ updating_cache=True,
413
+ )
414
+
415
+ # Step 3.4: update the start and end frame indices
416
+ current_start_frame += current_num_frames
417
+
418
+ # Step 3.5: Return the denoised timestep
419
+ if not self.same_step_across_blocks:
420
+ denoised_timestep_from, denoised_timestep_to = None, None
421
+ elif exit_flags[0] == len(self.denoising_step_list) - 1:
422
+ denoised_timestep_to = 0
423
+ denoised_timestep_from = 1000 - torch.argmin(
424
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
425
+ else:
426
+ denoised_timestep_to = 1000 - torch.argmin(
427
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0] + 1].cuda()).abs(), dim=0).item()
428
+ denoised_timestep_from = 1000 - torch.argmin(
429
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
430
+
431
+ if return_sim_step:
432
+ return output, denoised_timestep_from, denoised_timestep_to, exit_flags[0] + 1
433
+
434
+ return output, denoised_timestep_from, denoised_timestep_to
435
+
436
+ def _initialize_kv_cache(self, batch_size, dtype, device):
437
+ """
438
+ Initialize a Per-GPU KV cache for the Wan model.
439
+ """
440
+ kv_cache_clean = []
441
+
442
+ for _ in range(self.num_transformer_blocks):
443
+ kv_cache_clean.append({
444
+ "k": torch.zeros([batch_size, self.kv_cache_size, 12, 128], dtype=dtype, device=device),
445
+ "v": torch.zeros([batch_size, self.kv_cache_size, 12, 128], dtype=dtype, device=device),
446
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
447
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
448
+ })
449
+
450
+ self.kv_cache_clean = kv_cache_clean # always store the clean cache
451
+
452
+ def _initialize_crossattn_cache(self, batch_size, dtype, device):
453
+ """
454
+ Initialize a Per-GPU cross-attention cache for the Wan model.
455
+ """
456
+ crossattn_cache = []
457
+
458
+ for _ in range(self.num_transformer_blocks):
459
+ crossattn_cache.append({
460
+ "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
461
+ "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
462
+ "is_init": False
463
+ })
464
+ self.crossattn_cache = crossattn_cache
long_video/requirements.txt ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.5.1
2
+ torchvision==0.20.1
3
+ torchaudio==2.5.1
4
+ opencv-python>=4.9.0.80
5
+ diffusers==0.31.0
6
+ transformers>=4.49.0
7
+ tokenizers>=0.20.3
8
+ accelerate>=1.1.1
9
+ tqdm
10
+ imageio
11
+ easydict
12
+ ftfy
13
+ dashscope
14
+ imageio-ffmpeg
15
+ numpy==1.24.4
16
+ wandb
17
+ omegaconf
18
+ einops
19
+ av==13.1.0
20
+ opencv-python
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
41
+ tensorboard
42
+ ninja
43
+ packaging
44
+ gradio>=4.44.0
long_video/train.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ from omegaconf import OmegaConf
4
+
5
+ from trainer import DiffusionTrainer, GANTrainer, ODETrainer, ScoreDistillationTrainer
6
+
7
+
8
+ def main():
9
+ parser = argparse.ArgumentParser()
10
+ parser.add_argument("--config_path", type=str, required=True)
11
+ parser.add_argument("--no_save", action="store_true")
12
+ parser.add_argument("--no_visualize", action="store_true")
13
+ parser.add_argument("--logdir", type=str, default="", help="Path to the directory to save logs")
14
+ parser.add_argument("--wandb-save-dir", type=str, default="", help="Path to the directory to save wandb logs")
15
+ parser.add_argument("--disable-wandb", default=False, action="store_true")
16
+
17
+ args = parser.parse_args()
18
+
19
+ config = OmegaConf.load(args.config_path)
20
+ default_config = OmegaConf.load("configs/default_config.yaml")
21
+ config = OmegaConf.merge(default_config, config)
22
+ config.no_save = args.no_save
23
+ config.no_visualize = args.no_visualize
24
+
25
+ # get the filename of config_path
26
+ config_name = os.path.basename(args.config_path).split(".")[0]
27
+ config.config_name = config_name
28
+ config.logdir = args.logdir
29
+ config.wandb_save_dir = args.wandb_save_dir
30
+ config.disable_wandb = args.disable_wandb
31
+
32
+ if config.trainer == "diffusion":
33
+ trainer = DiffusionTrainer(config)
34
+ elif config.trainer == "gan":
35
+ trainer = GANTrainer(config)
36
+ elif config.trainer == "ode":
37
+ trainer = ODETrainer(config)
38
+ elif config.trainer == "score_distillation":
39
+ trainer = ScoreDistillationTrainer(config)
40
+ trainer.train()
41
+
42
+
43
+
44
+ if __name__ == "__main__":
45
+ main()
long_video/trainer/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .diffusion import Trainer as DiffusionTrainer
2
+ from .gan import Trainer as GANTrainer
3
+ from .ode import Trainer as ODETrainer
4
+ from .distillation import Trainer as ScoreDistillationTrainer
5
+
6
+ __all__ = [
7
+ "DiffusionTrainer",
8
+ "GANTrainer",
9
+ "ODETrainer",
10
+ "ScoreDistillationTrainer"
11
+ ]
long_video/trainer/diffusion.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gc
2
+ import logging
3
+
4
+ from model import CausalDiffusion
5
+ from utils.dataset import ShardingLMDBDataset, cycle
6
+ from utils.misc import set_seed
7
+ import torch.distributed as dist
8
+ from omegaconf import OmegaConf
9
+ import torch
10
+ import wandb
11
+ import time
12
+ import os
13
+
14
+ from utils.distributed import EMA_FSDP, barrier, fsdp_wrap, fsdp_state_dict, launch_distributed_job
15
+
16
+
17
+ class Trainer:
18
+ def __init__(self, config):
19
+ self.config = config
20
+ self.step = 0
21
+
22
+ # Step 1: Initialize the distributed training environment (rank, seed, dtype, logging etc.)
23
+ torch.backends.cuda.matmul.allow_tf32 = True
24
+ torch.backends.cudnn.allow_tf32 = True
25
+
26
+ launch_distributed_job()
27
+ global_rank = dist.get_rank()
28
+
29
+ self.dtype = torch.bfloat16 if config.mixed_precision else torch.float32
30
+ self.device = torch.cuda.current_device()
31
+ self.is_main_process = global_rank == 0
32
+ self.causal = config.causal
33
+ self.disable_wandb = config.disable_wandb
34
+
35
+ # use a random seed for the training
36
+ if config.seed == 0:
37
+ random_seed = torch.randint(0, 10000000, (1,), device=self.device)
38
+ dist.broadcast(random_seed, src=0)
39
+ config.seed = random_seed.item()
40
+
41
+ set_seed(config.seed + global_rank)
42
+
43
+ if self.is_main_process and not self.disable_wandb:
44
+ wandb.login(host=config.wandb_host, key=config.wandb_key)
45
+ wandb.init(
46
+ config=OmegaConf.to_container(config, resolve=True),
47
+ name=config.config_name,
48
+ mode="online",
49
+ entity=config.wandb_entity,
50
+ project=config.wandb_project,
51
+ dir=config.wandb_save_dir
52
+ )
53
+
54
+ self.output_path = config.logdir
55
+
56
+ # Step 2: Initialize the model and optimizer
57
+ self.model = CausalDiffusion(config, device=self.device)
58
+ self.model.generator = fsdp_wrap(
59
+ self.model.generator,
60
+ sharding_strategy=config.sharding_strategy,
61
+ mixed_precision=config.mixed_precision,
62
+ wrap_strategy=config.generator_fsdp_wrap_strategy
63
+ )
64
+
65
+ self.model.text_encoder = fsdp_wrap(
66
+ self.model.text_encoder,
67
+ sharding_strategy=config.sharding_strategy,
68
+ mixed_precision=config.mixed_precision,
69
+ wrap_strategy=config.text_encoder_fsdp_wrap_strategy
70
+ )
71
+
72
+ if not config.no_visualize or config.load_raw_video:
73
+ self.model.vae = self.model.vae.to(
74
+ device=self.device, dtype=torch.bfloat16 if config.mixed_precision else torch.float32)
75
+
76
+ self.generator_optimizer = torch.optim.AdamW(
77
+ [param for param in self.model.generator.parameters()
78
+ if param.requires_grad],
79
+ lr=config.lr,
80
+ betas=(config.beta1, config.beta2),
81
+ weight_decay=config.weight_decay
82
+ )
83
+
84
+ # Step 3: Initialize the dataloader
85
+ dataset = ShardingLMDBDataset(config.data_path, max_pair=int(1e8))
86
+ sampler = torch.utils.data.distributed.DistributedSampler(
87
+ dataset, shuffle=True, drop_last=True)
88
+ dataloader = torch.utils.data.DataLoader(
89
+ dataset,
90
+ batch_size=config.batch_size,
91
+ sampler=sampler,
92
+ num_workers=8)
93
+
94
+ if dist.get_rank() == 0:
95
+ print("DATASET SIZE %d" % len(dataset))
96
+ self.dataloader = cycle(dataloader)
97
+
98
+ ##############################################################################################################
99
+ # 6. Set up EMA parameter containers
100
+ rename_param = (
101
+ lambda name: name.replace("_fsdp_wrapped_module.", "")
102
+ .replace("_checkpoint_wrapped_module.", "")
103
+ .replace("_orig_mod.", "")
104
+ )
105
+ self.name_to_trainable_params = {}
106
+ for n, p in self.model.generator.named_parameters():
107
+ if not p.requires_grad:
108
+ continue
109
+
110
+ renamed_n = rename_param(n)
111
+ self.name_to_trainable_params[renamed_n] = p
112
+ ema_weight = config.ema_weight
113
+ self.generator_ema = None
114
+ if (ema_weight is not None) and (ema_weight > 0.0):
115
+ print(f"Setting up EMA with weight {ema_weight}")
116
+ self.generator_ema = EMA_FSDP(self.model.generator, decay=ema_weight)
117
+
118
+ ##############################################################################################################
119
+ # 7. (If resuming) Load the model and optimizer, lr_scheduler, ema's statedicts
120
+ if getattr(config, "generator_ckpt", False):
121
+ print(f"Loading pretrained generator from {config.generator_ckpt}")
122
+ state_dict = torch.load(config.generator_ckpt, map_location="cpu")
123
+ if "generator" in state_dict:
124
+ state_dict = state_dict["generator"]
125
+ elif "model" in state_dict:
126
+ state_dict = state_dict["model"]
127
+ self.model.generator.load_state_dict(
128
+ state_dict, strict=True
129
+ )
130
+
131
+ ##############################################################################################################
132
+
133
+ # Let's delete EMA params for early steps to save some computes at training and inference
134
+ if self.step < config.ema_start_step:
135
+ self.generator_ema = None
136
+
137
+ self.max_grad_norm = 10.0
138
+ self.previous_time = None
139
+
140
+ def save(self):
141
+ print("Start gathering distributed model states...")
142
+ generator_state_dict = fsdp_state_dict(
143
+ self.model.generator)
144
+
145
+ if self.config.ema_start_step < self.step:
146
+ state_dict = {
147
+ "generator": generator_state_dict,
148
+ "generator_ema": self.generator_ema.state_dict(),
149
+ }
150
+ else:
151
+ state_dict = {
152
+ "generator": generator_state_dict,
153
+ }
154
+
155
+ if self.is_main_process:
156
+ os.makedirs(os.path.join(self.output_path,
157
+ f"checkpoint_model_{self.step:06d}"), exist_ok=True)
158
+ torch.save(state_dict, os.path.join(self.output_path,
159
+ f"checkpoint_model_{self.step:06d}", "model.pt"))
160
+ print("Model saved to", os.path.join(self.output_path,
161
+ f"checkpoint_model_{self.step:06d}", "model.pt"))
162
+
163
+ def train_one_step(self, batch):
164
+ self.log_iters = 1
165
+
166
+ if self.step % 20 == 0:
167
+ torch.cuda.empty_cache()
168
+
169
+ # Step 1: Get the next batch of text prompts
170
+ text_prompts = batch["prompts"]
171
+ if not self.config.load_raw_video: # precomputed latent
172
+ clean_latent = batch["ode_latent"][:, -1].to(
173
+ device=self.device, dtype=self.dtype)
174
+ else: # encode raw video to latent
175
+ frames = batch["frames"].to(
176
+ device=self.device, dtype=self.dtype)
177
+ with torch.no_grad():
178
+ clean_latent = self.model.vae.encode_to_latent(
179
+ frames).to(device=self.device, dtype=self.dtype)
180
+ image_latent = clean_latent[:, 0:1, ]
181
+
182
+ batch_size = len(text_prompts)
183
+ image_or_video_shape = list(self.config.image_or_video_shape)
184
+ image_or_video_shape[0] = batch_size
185
+
186
+ # Step 2: Extract the conditional infos
187
+ with torch.no_grad():
188
+ conditional_dict = self.model.text_encoder(
189
+ text_prompts=text_prompts)
190
+
191
+ if not getattr(self, "unconditional_dict", None):
192
+ unconditional_dict = self.model.text_encoder(
193
+ text_prompts=[self.config.negative_prompt] * batch_size)
194
+ unconditional_dict = {k: v.detach()
195
+ for k, v in unconditional_dict.items()}
196
+ self.unconditional_dict = unconditional_dict # cache the unconditional_dict
197
+ else:
198
+ unconditional_dict = self.unconditional_dict
199
+
200
+ # Step 3: Train the generator
201
+ generator_loss, log_dict = self.model.generator_loss(
202
+ image_or_video_shape=image_or_video_shape,
203
+ conditional_dict=conditional_dict,
204
+ unconditional_dict=unconditional_dict,
205
+ clean_latent=clean_latent,
206
+ initial_latent=image_latent
207
+ )
208
+ self.generator_optimizer.zero_grad()
209
+ generator_loss.backward()
210
+ generator_grad_norm = self.model.generator.clip_grad_norm_(
211
+ self.max_grad_norm)
212
+ self.generator_optimizer.step()
213
+
214
+ # Increment the step since we finished gradient update
215
+ self.step += 1
216
+
217
+ wandb_loss_dict = {
218
+ "generator_loss": generator_loss.item(),
219
+ "generator_grad_norm": generator_grad_norm.item(),
220
+ }
221
+
222
+ # Step 4: Logging
223
+ if self.is_main_process:
224
+ if not self.disable_wandb:
225
+ wandb.log(wandb_loss_dict, step=self.step)
226
+
227
+ if self.step % self.config.gc_interval == 0:
228
+ if dist.get_rank() == 0:
229
+ logging.info("DistGarbageCollector: Running GC.")
230
+ gc.collect()
231
+
232
+ # Step 5. Create EMA params
233
+ # TODO: Implement EMA
234
+
235
+ def generate_video(self, pipeline, prompts, image=None):
236
+ batch_size = len(prompts)
237
+ sampled_noise = torch.randn(
238
+ [batch_size, 21, 16, 60, 104], device="cuda", dtype=self.dtype
239
+ )
240
+ video, _ = pipeline.inference(
241
+ noise=sampled_noise,
242
+ text_prompts=prompts,
243
+ return_latents=True
244
+ )
245
+ current_video = video.permute(0, 1, 3, 4, 2).cpu().numpy() * 255.0
246
+ return current_video
247
+
248
+ def train(self):
249
+ while True:
250
+ batch = next(self.dataloader)
251
+ self.train_one_step(batch)
252
+ if (not self.config.no_save) and self.step % self.config.log_iters == 0:
253
+ torch.cuda.empty_cache()
254
+ self.save()
255
+ torch.cuda.empty_cache()
256
+
257
+ barrier()
258
+ if self.is_main_process:
259
+ current_time = time.time()
260
+ if self.previous_time is None:
261
+ self.previous_time = current_time
262
+ else:
263
+ if not self.disable_wandb:
264
+ wandb.log({"per iteration time": current_time - self.previous_time}, step=self.step)
265
+ self.previous_time = current_time