Upload code and configuration only
Browse filesSource 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
- .gitignore +9 -0
- LICENSE +201 -0
- README.md +460 -0
- configs/ar_diffusion_tf_chunkwise.yaml +46 -0
- configs/ar_diffusion_tf_framewise.yaml +46 -0
- configs/causal_cd_chunkwise.yaml +57 -0
- configs/causal_cd_framewise.yaml +57 -0
- configs/causal_forcing_dmd_chunkwise.yaml +51 -0
- configs/causal_forcing_dmd_framewise.yaml +51 -0
- configs/causal_forcing_dmd_framewise_1step.yaml +53 -0
- configs/causal_forcing_dmd_framewise_2step.yaml +54 -0
- configs/causal_ode_chunkwise.yaml +31 -0
- configs/causal_ode_framewise.yaml +31 -0
- configs/default_config.yaml +20 -0
- demo.py +631 -0
- demo_utils/constant.py +41 -0
- demo_utils/memory.py +135 -0
- demo_utils/taehv.py +313 -0
- demo_utils/utils.py +616 -0
- demo_utils/vae.py +390 -0
- demo_utils/vae_block3.py +291 -0
- demo_utils/vae_torch2trt.py +308 -0
- get_causal_ode_data_chunkwise.py +140 -0
- get_causal_ode_data_framewise.py +140 -0
- get_causal_ode_data_kv_optimized.py +188 -0
- inference.py +218 -0
- long_video/LICENSE +81 -0
- long_video/README.md +40 -0
- long_video/app.py +167 -0
- long_video/configs/default_config.yaml +20 -0
- long_video/configs/rolling_forcing_dmd.yaml +47 -0
- long_video/inference.py +197 -0
- long_video/model/__init__.py +14 -0
- long_video/model/base.py +230 -0
- long_video/model/causvid.py +391 -0
- long_video/model/diffusion.py +125 -0
- long_video/model/dmd.py +332 -0
- long_video/model/gan.py +295 -0
- long_video/model/ode_regression.py +136 -0
- long_video/model/sid.py +283 -0
- long_video/pipeline/__init__.py +13 -0
- long_video/pipeline/bidirectional_diffusion_inference.py +110 -0
- long_video/pipeline/bidirectional_inference.py +71 -0
- long_video/pipeline/causal_diffusion_inference.py +342 -0
- long_video/pipeline/rolling_forcing_inference.py +372 -0
- long_video/pipeline/rolling_forcing_training.py +464 -0
- long_video/requirements.txt +44 -0
- long_video/train.py +45 -0
- long_video/trainer/__init__.py +11 -0
- 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
|