Add isolated Minecraft and RE10K baseline evaluation suite
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +16 -0
- DecMem/.gitignore +25 -0
- DecMem/LICENSE +201 -0
- DecMem/README.md +318 -0
- DecMem/configs/decmem.yaml +74 -0
- DecMem/configs/default_config.yaml +20 -0
- DecMem/configs/dense.yaml +68 -0
- DecMem/configs/dense_short.yaml +67 -0
- DecMem/configs/worldmem_diffusion.yaml +69 -0
- DecMem/demo_utils/constant.py +41 -0
- DecMem/demo_utils/memory.py +135 -0
- DecMem/demo_utils/taehv.py +313 -0
- DecMem/demo_utils/utils.py +616 -0
- DecMem/demo_utils/vae.py +390 -0
- DecMem/demo_utils/vae_block3.py +291 -0
- DecMem/demo_utils/vae_torch2trt.py +308 -0
- DecMem/evaluation/method_manifest.json +91 -0
- DecMem/evaluation/worldmem_adapter.py +188 -0
- DecMem/example/example.csv +3 -0
- DecMem/infer.py +338 -0
- DecMem/model/__init__.py +9 -0
- DecMem/model/base.py +222 -0
- DecMem/model/causvid.py +391 -0
- DecMem/model/diffusion.py +155 -0
- DecMem/model/gan.py +295 -0
- DecMem/pipeline/__init__.py +17 -0
- DecMem/pipeline/bidirectional_diffusion_inference.py +110 -0
- DecMem/pipeline/bidirectional_inference.py +71 -0
- DecMem/pipeline/causal_diffusion_inference.py +707 -0
- DecMem/pipeline/causal_inference.py +331 -0
- DecMem/pipeline/self_forcing_training.py +267 -0
- DecMem/requirements.txt +26 -0
- DecMem/scripts/build_worldmem_csv.py +111 -0
- DecMem/scripts/infer.sh +60 -0
- DecMem/scripts/infer_example.sh +59 -0
- DecMem/scripts/train_multinode.sh +85 -0
- DecMem/slurm_logs/.gitkeep +0 -0
- DecMem/train.py +45 -0
- DecMem/trainer/__init__.py +7 -0
- DecMem/trainer/diffusion.py +432 -0
- DecMem/trainer/distillation.py +380 -0
- DecMem/trainer/gan.py +452 -0
- DecMem/trainer/ode.py +242 -0
- DecMem/utils/action_encoder.py +47 -0
- DecMem/utils/dataset.py +306 -0
- DecMem/utils/distributed.py +158 -0
- DecMem/utils/lmdb.py +72 -0
- DecMem/utils/loss.py +81 -0
- DecMem/utils/misc.py +39 -0
- DecMem/utils/pose_utils.py +203 -0
.gitignore
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
envs/
|
| 2 |
+
cache/
|
| 3 |
+
checkpoints/
|
| 4 |
+
artifacts/
|
| 5 |
+
shared/metric_vendor/
|
| 6 |
+
**/wan_models/
|
| 7 |
+
**/outputs/
|
| 8 |
+
**/build/
|
| 9 |
+
**/dist/
|
| 10 |
+
**/__pycache__/
|
| 11 |
+
**/.pytest_cache/
|
| 12 |
+
**/*.pyc
|
| 13 |
+
**/*.so
|
| 14 |
+
**/*.egg-info/
|
| 15 |
+
**/slurm_logs/*
|
| 16 |
+
!**/slurm_logs/.gitkeep
|
DecMem/.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.egg-info
|
| 3 |
+
|
| 4 |
+
wan_models
|
| 5 |
+
checkpoints
|
| 6 |
+
|
| 7 |
+
test/*
|
| 8 |
+
|
| 9 |
+
videos/*
|
| 10 |
+
|
| 11 |
+
exps
|
| 12 |
+
|
| 13 |
+
.vscode/
|
| 14 |
+
|
| 15 |
+
configs/*_overfit.yaml
|
| 16 |
+
|
| 17 |
+
data
|
| 18 |
+
|
| 19 |
+
video_sparse_attn/build
|
| 20 |
+
|
| 21 |
+
scripts/train_worldmem.sh
|
| 22 |
+
|
| 23 |
+
scripts/train_local.sh
|
| 24 |
+
|
| 25 |
+
scripts/train_worldmem_multinode_torchrun.sh
|
DecMem/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.
|
DecMem/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<h1 align="center">DecMem: Towards Minute-Long Consistent World Generation with Decoupled Memory</h1>
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
<div align="center">
|
| 6 |
+
<p>
|
| 7 |
+
Zhenhao Yang<sup>1</sup>
|
| 8 |
+
Xiaoshi Wu<sup>2</sup>
|
| 9 |
+
Zhengyao Lv<sup>1</sup>
|
| 10 |
+
Xiaoyu Shi<sup>2,†</sup>
|
| 11 |
+
Xintao Wang<sup>2</sup>
|
| 12 |
+
Pengfei Wan<sup>2</sup>
|
| 13 |
+
Kun Gai<sup>2</sup>
|
| 14 |
+
Kwan-Yee K. Wong<sup>1,†</sup>
|
| 15 |
+
</p>
|
| 16 |
+
<p>
|
| 17 |
+
<sup>1</sup>The University of Hong Kong
|
| 18 |
+
<sup>2</sup>Kling Team, Kuaishou Technology<br>
|
| 19 |
+
<sup>†</sup>Corresponding author
|
| 20 |
+
</p>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<p align="center">
|
| 24 |
+
<a href='https://jeffreyyzh.github.io/DecMem-Page/'><img src='https://img.shields.io/badge/Project-Page-Green'></a>
|
| 25 |
+
|
| 26 |
+
<a href="https://arxiv.org/abs/2605.31336"><img src="https://img.shields.io/static/v1?label=Arxiv&message=DecMem&color=red&logo=arxiv"></a>
|
| 27 |
+
|
| 28 |
+
<a href='https://huggingface.co/KlingTeam/DecMem/'><img src='https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Models-orange'></a>
|
| 29 |
+
|
| 30 |
+
<a href='#-citation'><img src='https://img.shields.io/badge/BibTeX-Cite-blue'></a>
|
| 31 |
+
</p>
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
## 📋 Table of Contents
|
| 35 |
+
|
| 36 |
+
- [📋 Table of Contents](#-table-of-contents)
|
| 37 |
+
- [🔥 Updates](#-updates)
|
| 38 |
+
- [📷 Introduction](#-introduction)
|
| 39 |
+
- [⚙️ Code: DecMem + Wan2.1-T2V-1.3B](#️-code-decmem--wan21-t2v-13b)
|
| 40 |
+
- [Requirements](#requirements)
|
| 41 |
+
- [Installation](#installation)
|
| 42 |
+
- [Download Checkpoints](#download-checkpoints)
|
| 43 |
+
- [Quick Start](#quick-start)
|
| 44 |
+
- [Data](#data)
|
| 45 |
+
- [Train](#train)
|
| 46 |
+
- [Inference](#inference)
|
| 47 |
+
- [🤗 Acknowledgement](#-acknowledgement)
|
| 48 |
+
- [🌟 Citation](#-citation)
|
| 49 |
+
|
| 50 |
+
**Note:** This open-source repository is a reference implementation built on Wan2.1-T2V-1.3B. The original model is trained on internal pretrained model so we will not opensource such codebase.
|
| 51 |
+
|
| 52 |
+
## 🔥 Updates
|
| 53 |
+
|
| 54 |
+
- **[2026.06.01]**: Release the [Training and Inference Code](https://github.com/KlingAIResearch/DecMem) and the [Checkpoints](https://huggingface.co/KlingTeam/DecMem).
|
| 55 |
+
- **[2026.06.01]**: Release the [Project Page](https://jeffreyyzh.github.io/DecMem-Page/) and the [Arxiv](https://arxiv.org/abs/2605.31336) version.
|
| 56 |
+
|
| 57 |
+
## 📷 Introduction
|
| 58 |
+
|
| 59 |
+
We systematically reveal the root cause of the limited long-horizon world generation capability of naïve dense-attention designs and propose **DecMem**, a decoupled memory architecture that employs **Sparse Global Memory** for efficient fine-grained access to global history and **Anchored Local Memory** for stable and high-quality extrapolation.
|
| 60 |
+
|
| 61 |
+
**(a) Sparse Global Memory (SGM):** Combines a block-level sparse retrieval module and a context-aware attention module for long-term memory fine-grained retrieval in an end-to-end manner, enabling efficient access to global history with bounded cost.
|
| 62 |
+
|
| 63 |
+
**(b) Anchored Local Memory (ALM):** Keeps short-term transition smooth and supports stable, high-quality extrapolation by anchoring generation on recent local context.
|
| 64 |
+
|
| 65 |
+
**(c) Capability:** DecMem enables **minute-level** controllable long video generation with **high fidelity**, **consistency** and **efficiency**.
|
| 66 |
+
|
| 67 |
+
<div align="center">
|
| 68 |
+
|
| 69 |
+
[](https://www.youtube.com/watch?v=f6jBmFXxgzg)
|
| 70 |
+
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
## ⚙️ Code: DecMem + Wan2.1-T2V-1.3B
|
| 74 |
+
|
| 75 |
+
### Requirements
|
| 76 |
+
|
| 77 |
+
We tested this repo on the following setup:
|
| 78 |
+
|
| 79 |
+
- **GPU:** NVIDIA GPU with at least 24 GB VRAM.
|
| 80 |
+
- **LTM (long-term memory) via Video Sparse Attention requires SM 90a** (**H100 / H200 / H800)**. The block-sparse CUDA kernel is built for `sm_90a` and is not supported on A100 or consumer GPUs.
|
| 81 |
+
- Dense-only training / inference (`use_ltm_attn: false`) can run on A100 and other CUDA GPUs, but the full DecMem model with LTM needs the hardware above.
|
| 82 |
+
- **RAM:** 64 GB recommended.
|
| 83 |
+
- **CUDA:** 12.x toolkit matching the PyTorch build.
|
| 84 |
+
- **Compiler:** **gcc / g++ ≥ 11** (C++20 required by the VSA kernel).
|
| 85 |
+
|
| 86 |
+
### Installation
|
| 87 |
+
|
| 88 |
+
#### 1. Core environment
|
| 89 |
+
|
| 90 |
+
Create a conda environment and install dependencies:
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
git clone https://github.com/KlingAIResearch/DecMem.git
|
| 94 |
+
cd DecMem
|
| 95 |
+
conda create -n decmem python=3.10 -y
|
| 96 |
+
conda activate decmem
|
| 97 |
+
pip install torch==2.5.1 torchvision==0.20.1 --index-url https://download.pytorch.org/whl/cu121
|
| 98 |
+
pip install -r requirements.txt
|
| 99 |
+
pip install flash-attn --no-build-isolation
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
#### 2. Video Sparse Attention (VSA) kernel
|
| 103 |
+
|
| 104 |
+
Our LTM module is implemented with modified VSA kernel so it is required when training or inferring with `use_ltm_attn: true` (e.g. `configs/decmem.yaml`). Install from source:
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
cd video_sparse_attn
|
| 108 |
+
python setup.py install
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
Verify:
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
python -c "
|
| 115 |
+
import torch, vsa
|
| 116 |
+
from vsa.block_sparse_wrapper import block_sparse_attn_SM90
|
| 117 |
+
print('device cap:', torch.cuda.get_device_capability(0))
|
| 118 |
+
print('SM90 op:', block_sparse_attn_SM90)
|
| 119 |
+
"
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
### Checkpoints
|
| 123 |
+
|
| 124 |
+
Download the Wan2.1 backbone (VAE + tokenizer weights used by the pipeline):
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
huggingface-cli download Wan-AI/Wan2.1-T2V-1.3B \
|
| 128 |
+
--local-dir-use-symlinks False \
|
| 129 |
+
--local-dir wan_models/Wan2.1-T2V-1.3B
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
Download DecMem trained checkpoints from HuggingFace:
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
huggingface-cli download KlingTeam/DecMem --local-dir checkpoints
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
Checkpoint layout expected by training / inference scripts:
|
| 139 |
+
|
| 140 |
+
```
|
| 141 |
+
checkpoints/
|
| 142 |
+
└── decmem.pt # released weights
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
### Quick start
|
| 146 |
+
|
| 147 |
+
We provide the example video-pose pairs for quick inference. The inference is Block-by-block causal denoising manner with KV cache.
|
| 148 |
+
|
| 149 |
+
```bash
|
| 150 |
+
bash scripts/infer_example.sh
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
### Data
|
| 154 |
+
|
| 155 |
+
DecMem trains on **WorldMem** gameplay data: paired MP4 videos and NPZ action/pose files indexed by a CSV.
|
| 156 |
+
|
| 157 |
+
#### Download dataset
|
| 158 |
+
|
| 159 |
+
Download the Minecraft dataset from [Hugging Face](https://huggingface.co/datasets/zeqixiao/worldmem_minecraft_dataset) (see the [WorldMem dataset section](https://github.com/xizaoqu/WorldMem#dataset)).
|
| 160 |
+
|
| 161 |
+
Place the dataset in the following directory structure:
|
| 162 |
+
|
| 163 |
+
```
|
| 164 |
+
data/
|
| 165 |
+
├── training/
|
| 166 |
+
├── validation/
|
| 167 |
+
└── test/
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
Each clip is an MP4 file with a matching NPZ file (same basename) in the same directory.
|
| 171 |
+
|
| 172 |
+
#### Build metadata CSV
|
| 173 |
+
|
| 174 |
+
After download, run the helper script to scan MP4/NPZ pairs and generate index files:
|
| 175 |
+
|
| 176 |
+
```bash
|
| 177 |
+
python scripts/build_worldmem_csv.py
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
This writes `train.csv` (from `training/`) and `test.csv` (from `test/`) under `data/worldmem_minecraft_dataset/`. Rows are shuffled with seed `42` by default; use `--seed` to change it.
|
| 181 |
+
|
| 182 |
+
#### Directory layout
|
| 183 |
+
|
| 184 |
+
After download and indexing:
|
| 185 |
+
|
| 186 |
+
```
|
| 187 |
+
data/
|
| 188 |
+
├── train.csv # training index (generated)
|
| 189 |
+
├── test.csv # evaluation index (generated)
|
| 190 |
+
├── training/
|
| 191 |
+
│ └── *.mp4 + *.npz # paired clips
|
| 192 |
+
├── validation/
|
| 193 |
+
│ └── *.mp4 + *.npz
|
| 194 |
+
└── test/
|
| 195 |
+
└── *.mp4 + *.npz
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
#### CSV format
|
| 199 |
+
|
| 200 |
+
Each row has two columns (`utils/dataset.py` → `WorldMemCSVDataset`):
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
| Column | Description |
|
| 204 |
+
| ------------- | ------------------------------ |
|
| 205 |
+
| `video_path` | Path to an MP4 video clip. |
|
| 206 |
+
| `action_path` | Path to the matching NPZ file. |
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
Example (`data/minecraft/train.csv`):
|
| 210 |
+
|
| 211 |
+
```csv
|
| 212 |
+
video_path,action_path
|
| 213 |
+
data/training/000001.mp4,data/training/000001.npz
|
| 214 |
+
data/training/000002.mp4,data/training/000002.npz
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
### Train
|
| 218 |
+
|
| 219 |
+
> **Note:**
|
| 220 |
+
>
|
| 221 |
+
> 1. Update `MASTER_ADDR` (and `NNODES` for multi-node) in the launch scripts before distributed training.
|
| 222 |
+
> 2. Dense training (`use_ltm_attn: false`) can run on A100; LTM training requires H100/H800/H200 with the block-sparse kernel installed.
|
| 223 |
+
> 3. Set `WANDB_KEY` / `wandb_entity` in the config or disable wandb with `--disable-wandb`.
|
| 224 |
+
> 4. We train DecMem in three stages in this opensource version and load the checkpoints in the previous stage for resume training.
|
| 225 |
+
|
| 226 |
+
#### Stage 1: Dense causal diffusion (Short)
|
| 227 |
+
|
| 228 |
+
```bash
|
| 229 |
+
# Edit NNODES, MASTER_ADDR, NPROC_PER_NODE in the script first.
|
| 230 |
+
# On node 0:
|
| 231 |
+
bash scripts/train_worldmem_multinode_manual.sh 0 dense_short
|
| 232 |
+
# On node 1:
|
| 233 |
+
bash scripts/train_worldmem_multinode_manual.sh 1 dense_short
|
| 234 |
+
# On node 2:...
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
#### Stage 2: Dense causal diffusion (Long)
|
| 238 |
+
|
| 239 |
+
Fine-tune from a short-sequence training dense checkpoint ; set `generator_ckpt` in the config:
|
| 240 |
+
|
| 241 |
+
```bash
|
| 242 |
+
# Edit NNODES, MASTER_ADDR, NPROC_PER_NODE in the script first.
|
| 243 |
+
# On node 0:
|
| 244 |
+
bash scripts/train_worldmem_multinode_manual.sh 0 dense
|
| 245 |
+
# On node 1:
|
| 246 |
+
bash scripts/train_worldmem_multinode_manual.sh 1 dense
|
| 247 |
+
# On node 2:...
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
#### Stage 3: Decoupled memory (STM + LTM)
|
| 251 |
+
|
| 252 |
+
Fine-tune from a long-sequence training dense checkpoint; set `generator_ckpt` in the config:
|
| 253 |
+
|
| 254 |
+
```bash
|
| 255 |
+
# Edit NNODES, MASTER_ADDR, NPROC_PER_NODE in the script first.
|
| 256 |
+
# On node 0:
|
| 257 |
+
bash scripts/train_worldmem_multinode_manual.sh 0 decmem
|
| 258 |
+
# On node 1:
|
| 259 |
+
bash scripts/train_worldmem_multinode_manual.sh 1 decmem
|
| 260 |
+
# On node 2:...
|
| 261 |
+
```
|
| 262 |
+
|
| 263 |
+
You can set `RESUME_CKPT` in training scripts to resume from a full training checkpoint (weights + optimizer + step) .
|
| 264 |
+
|
| 265 |
+
### Inference
|
| 266 |
+
|
| 267 |
+
Once you finish the training in each stage, you can load the correponding checkpoints and configs for inference.
|
| 268 |
+
|
| 269 |
+
Common overrides:
|
| 270 |
+
|
| 271 |
+
```bash
|
| 272 |
+
EXP_NAME=decmem \
|
| 273 |
+
CHECKPOINT_PATH=checkpoints/decmem.pt \
|
| 274 |
+
T_LAT=160 \
|
| 275 |
+
NUM_CONDITION_FRAMES=56 \
|
| 276 |
+
GUIDANCE_SCALE=5.0 \
|
| 277 |
+
NUM_INFERENCE_STEPS=50 \
|
| 278 |
+
bash scripts/infer.sh
|
| 279 |
+
|
| 280 |
+
# Multi-GPU inference
|
| 281 |
+
NUM_GPUS=8 bash scripts/infer.sh
|
| 282 |
+
```
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
| Argument | Description |
|
| 286 |
+
| ---------------------- | --------------------------------------------------------------------- |
|
| 287 |
+
| `NUM_CONDITION_FRAMES` | Number of clean prefix **latent** frames (conditioning video prefix). |
|
| 288 |
+
| `T_LAT` | Number of clean prefix **latent** frames (conditioning video prefix). |
|
| 289 |
+
| `GUIDANCE_SCALE` | CFG scale on actions;`≤ 1.0` disables CFG. |
|
| 290 |
+
| `N` | Number of inference samples |
|
| 291 |
+
| `NUM_INFERENCE_STEPS` | Number of denoising step |
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
## 🤗 Acknowledgement
|
| 295 |
+
|
| 296 |
+
This codebase is built on top of:
|
| 297 |
+
|
| 298 |
+
- [WorldMem](https://github.com/xizaoqu/WorldMem) for action-pose data format and pose utilities.
|
| 299 |
+
- [PRoPE](https://arxiv.org/pdf/2507.10496) for camera-relative positional encoding.
|
| 300 |
+
- [Video Sparse Attention](https://github.com/hao-ai-lab/FastVideo) for the LTM sparse attention kernel.
|
| 301 |
+
- [Self Forcing](https://github.com/gdhe17/Self-Forcing) and [CausVid](https://github.com/tianweiy/CausVid) for causal diffusion training and teacher forcing codebase.
|
| 302 |
+
- [Wan2.1](https://github.com/Wan-Video/Wan2.1) as the base video diffusion backbone.
|
| 303 |
+
|
| 304 |
+
## 🌟 Citation
|
| 305 |
+
|
| 306 |
+
Please leave us a star 🌟 and cite our paper if you find our work helpful.
|
| 307 |
+
|
| 308 |
+
```bibtex
|
| 309 |
+
@misc{yang2026decmemminutelongconsistentworld,
|
| 310 |
+
title={DecMem: Towards Minute-Long Consistent World Generation with Decoupled Memory},
|
| 311 |
+
author={Zhenhao Yang and Xiaoshi Wu and Zhengyao Lv and Xiaoyu Shi and Xintao Wang and Pengfei Wan and Kun Gai and Kwan-Yee K. Wong},
|
| 312 |
+
year={2026},
|
| 313 |
+
eprint={2605.31336},
|
| 314 |
+
archivePrefix={arXiv},
|
| 315 |
+
primaryClass={cs.CV},
|
| 316 |
+
url={https://arxiv.org/abs/2605.31336},
|
| 317 |
+
}
|
| 318 |
+
```
|
DecMem/configs/decmem.yaml
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# WorldMem diffusion training config with Long-Term Memory (LTM)
|
| 2 |
+
# Replaces the flex_attention self-attn path with STM (sliding-window dense via
|
| 3 |
+
# flex_attention) + LTM (sparse block attention on H100 SM90), gated by a
|
| 4 |
+
# zero-init mem_proj so initial step output matches the prior dense behaviour.
|
| 5 |
+
|
| 6 |
+
trainer: diffusion
|
| 7 |
+
causal: true
|
| 8 |
+
mixed_precision: true
|
| 9 |
+
seed: 0
|
| 10 |
+
|
| 11 |
+
# Data
|
| 12 |
+
# CSV columns: video_path, action_path (WorldMem format)
|
| 13 |
+
data_path: data/train.csv
|
| 14 |
+
num_frames: 60 # latent frame count; dataset loads (21-1)*4+1=81 video frames
|
| 15 |
+
sample_strategy: random
|
| 16 |
+
load_raw_video: true
|
| 17 |
+
|
| 18 |
+
# CFG: probability of dropping action conditioning (zeros actions + identity viewmats)
|
| 19 |
+
proportion_empty_prompts: 0.1
|
| 20 |
+
|
| 21 |
+
# Model
|
| 22 |
+
image_or_video_shape:
|
| 23 |
+
- 1
|
| 24 |
+
- 60
|
| 25 |
+
- 16
|
| 26 |
+
- 44
|
| 27 |
+
- 80
|
| 28 |
+
|
| 29 |
+
model_kwargs:
|
| 30 |
+
timestep_shift: 5.0
|
| 31 |
+
local_attn_size: -1
|
| 32 |
+
sink_size: 0
|
| 33 |
+
use_action_encoder: true
|
| 34 |
+
action_dim: 25
|
| 35 |
+
latent_channels: 16
|
| 36 |
+
# ── LTM ──
|
| 37 |
+
use_ltm_attn: true
|
| 38 |
+
ltm_tile_size: [1, 8, 8]
|
| 39 |
+
ltm_topk: 80 # top-k key blocks per query block (matches ar_mem default)
|
| 40 |
+
stm_sliding_window: 8 # STM window = 2 * num_frame_per_block (= 2 * 4 frames)
|
| 41 |
+
|
| 42 |
+
# Training hyperparams
|
| 43 |
+
num_train_timestep: 1000
|
| 44 |
+
timestep_shift: 5.0
|
| 45 |
+
guidance_scale: 1.0
|
| 46 |
+
teacher_forcing: true
|
| 47 |
+
noise_augmentation_max_timestep: 0
|
| 48 |
+
generator_ckpt: exps/dense/checkpoint_model_010000/model.pt
|
| 49 |
+
|
| 50 |
+
lr: 2.0e-5
|
| 51 |
+
beta1: 0.9
|
| 52 |
+
beta2: 0.999
|
| 53 |
+
weight_decay: 0.01
|
| 54 |
+
batch_size: 1
|
| 55 |
+
gradient_checkpointing: true
|
| 56 |
+
num_frame_per_block: 4
|
| 57 |
+
sharding_strategy: hybrid_full
|
| 58 |
+
|
| 59 |
+
# Logging / saving
|
| 60 |
+
no_save: false
|
| 61 |
+
no_visualize: true
|
| 62 |
+
log_iters: 500
|
| 63 |
+
# ema_weight: 0.999
|
| 64 |
+
# ema_start_step: 500
|
| 65 |
+
gc_interval: 100
|
| 66 |
+
wandb_host: "https://api.wandb.ai"
|
| 67 |
+
wandb_key: ""
|
| 68 |
+
wandb_entity: ""
|
| 69 |
+
wandb_project: ""
|
| 70 |
+
negative_prompt: ""
|
| 71 |
+
|
| 72 |
+
# FSDP wrap strategy
|
| 73 |
+
generator_fsdp_wrap_strategy: size
|
| 74 |
+
text_encoder_fsdp_wrap_strategy: size
|
DecMem/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
|
DecMem/configs/dense.yaml
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# WorldMem diffusion training config
|
| 2 |
+
# Action + PRoPE conditioning, no text conditioning, CFG on actions only
|
| 3 |
+
|
| 4 |
+
trainer: diffusion
|
| 5 |
+
causal: true
|
| 6 |
+
mixed_precision: true
|
| 7 |
+
seed: 0
|
| 8 |
+
|
| 9 |
+
# Data
|
| 10 |
+
# CSV columns: video_path, action_path (WorldMem format)
|
| 11 |
+
data_path: data/train_wm_gamecraft/v2/worldmem_train_v2_shuffle.csv
|
| 12 |
+
num_frames: 60 # latent frame count; dataset loads (21-1)*4+1=81 video frames
|
| 13 |
+
load_raw_video: true
|
| 14 |
+
sample_strategy: random # overfit: always start from SKIP_FRAMES; use "random" for normal training
|
| 15 |
+
|
| 16 |
+
# CFG: probability of dropping action conditioning (zeros actions + identity viewmats)
|
| 17 |
+
proportion_empty_prompts: 0.1
|
| 18 |
+
|
| 19 |
+
# Model
|
| 20 |
+
# 81 video frames @ 640x352 -> 21 latent frames @ 80x44 (WAN 4x temporal, 8x spatial)
|
| 21 |
+
image_or_video_shape:
|
| 22 |
+
- 1 # batch_size placeholder (overridden at runtime)
|
| 23 |
+
- 60 # latent frames
|
| 24 |
+
- 16 # latent channels
|
| 25 |
+
- 44 # latent height (352/8)
|
| 26 |
+
- 80 # latent width (640/8)
|
| 27 |
+
|
| 28 |
+
model_kwargs:
|
| 29 |
+
timestep_shift: 5.0
|
| 30 |
+
local_attn_size: -1
|
| 31 |
+
sink_size: 0
|
| 32 |
+
use_action_encoder: true
|
| 33 |
+
action_dim: 25
|
| 34 |
+
latent_channels: 16
|
| 35 |
+
|
| 36 |
+
# Training hyperparams
|
| 37 |
+
num_train_timestep: 1000
|
| 38 |
+
timestep_shift: 5.0
|
| 39 |
+
guidance_scale: 1.0
|
| 40 |
+
teacher_forcing: true
|
| 41 |
+
noise_augmentation_max_timestep: 0
|
| 42 |
+
generator_ckpt: exps/dense_short/checkpoint_model_022000/model.pt
|
| 43 |
+
|
| 44 |
+
lr: 2.0e-5
|
| 45 |
+
beta1: 0.9
|
| 46 |
+
beta2: 0.999
|
| 47 |
+
weight_decay: 0.01
|
| 48 |
+
batch_size: 1
|
| 49 |
+
gradient_checkpointing: true
|
| 50 |
+
num_frame_per_block: 4
|
| 51 |
+
sharding_strategy: hybrid_full
|
| 52 |
+
|
| 53 |
+
# Logging / saving
|
| 54 |
+
no_save: false
|
| 55 |
+
no_visualize: true
|
| 56 |
+
log_iters: 500
|
| 57 |
+
# ema_weight: 0.999
|
| 58 |
+
# ema_start_step: 500
|
| 59 |
+
gc_interval: 100
|
| 60 |
+
wandb_host: "https://api.wandb.ai"
|
| 61 |
+
wandb_key:
|
| 62 |
+
wandb_entity: ""
|
| 63 |
+
wandb_project: ""
|
| 64 |
+
negative_prompt: ""
|
| 65 |
+
|
| 66 |
+
# FSDP wrap strategy
|
| 67 |
+
generator_fsdp_wrap_strategy: size
|
| 68 |
+
text_encoder_fsdp_wrap_strategy: size
|
DecMem/configs/dense_short.yaml
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# WorldMem diffusion training config
|
| 2 |
+
# Action + PRoPE conditioning, no text conditioning, CFG on actions only
|
| 3 |
+
|
| 4 |
+
trainer: diffusion
|
| 5 |
+
causal: true
|
| 6 |
+
mixed_precision: true
|
| 7 |
+
seed: 0
|
| 8 |
+
|
| 9 |
+
# Data
|
| 10 |
+
# CSV columns: video_path, action_path (WorldMem format)
|
| 11 |
+
data_path: data/train_wm_gamecraft/v2/worldmem_train_v2_shuffle.csv
|
| 12 |
+
num_frames: 20 # latent frame count; dataset loads (21-1)*4+1=81 video frames
|
| 13 |
+
load_raw_video: true
|
| 14 |
+
sample_strategy: random # overfit: always start from SKIP_FRAMES; use "random" for normal training
|
| 15 |
+
|
| 16 |
+
# CFG: probability of dropping action conditioning (zeros actions + identity viewmats)
|
| 17 |
+
proportion_empty_prompts: 0.1
|
| 18 |
+
|
| 19 |
+
# Model
|
| 20 |
+
# 81 video frames @ 640x352 -> 21 latent frames @ 80x44 (WAN 4x temporal, 8x spatial)
|
| 21 |
+
image_or_video_shape:
|
| 22 |
+
- 1 # batch_size placeholder (overridden at runtime)
|
| 23 |
+
- 20 # latent frames
|
| 24 |
+
- 16 # latent channels
|
| 25 |
+
- 44 # latent height (352/8)
|
| 26 |
+
- 80 # latent width (640/8)
|
| 27 |
+
|
| 28 |
+
model_kwargs:
|
| 29 |
+
timestep_shift: 5.0
|
| 30 |
+
local_attn_size: -1
|
| 31 |
+
sink_size: 0
|
| 32 |
+
use_action_encoder: true
|
| 33 |
+
action_dim: 25
|
| 34 |
+
latent_channels: 16
|
| 35 |
+
|
| 36 |
+
# Training hyperparams
|
| 37 |
+
num_train_timestep: 1000
|
| 38 |
+
timestep_shift: 5.0
|
| 39 |
+
guidance_scale: 1.0
|
| 40 |
+
teacher_forcing: true
|
| 41 |
+
noise_augmentation_max_timestep: 0
|
| 42 |
+
|
| 43 |
+
lr: 1.0e-4
|
| 44 |
+
beta1: 0.9
|
| 45 |
+
beta2: 0.999
|
| 46 |
+
weight_decay: 0.01
|
| 47 |
+
batch_size: 1
|
| 48 |
+
gradient_checkpointing: true
|
| 49 |
+
num_frame_per_block: 4
|
| 50 |
+
sharding_strategy: hybrid_full
|
| 51 |
+
|
| 52 |
+
# Logging / saving
|
| 53 |
+
no_save: false
|
| 54 |
+
no_visualize: true
|
| 55 |
+
log_iters: 500
|
| 56 |
+
# ema_weight: 0.0
|
| 57 |
+
# ema_start_step: 200
|
| 58 |
+
gc_interval: 100
|
| 59 |
+
wandb_host: "https://api.wandb.ai"
|
| 60 |
+
wandb_key:
|
| 61 |
+
wandb_entity: ""
|
| 62 |
+
wandb_project: ""
|
| 63 |
+
negative_prompt: ""
|
| 64 |
+
|
| 65 |
+
# FSDP wrap strategy
|
| 66 |
+
generator_fsdp_wrap_strategy: size
|
| 67 |
+
text_encoder_fsdp_wrap_strategy: size
|
DecMem/configs/worldmem_diffusion.yaml
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# WorldMem diffusion training config
|
| 2 |
+
# Action + PRoPE conditioning, no text conditioning, CFG on actions only
|
| 3 |
+
|
| 4 |
+
trainer: diffusion
|
| 5 |
+
causal: true
|
| 6 |
+
mixed_precision: true
|
| 7 |
+
seed: 0
|
| 8 |
+
|
| 9 |
+
# Data
|
| 10 |
+
# CSV columns: video_path, action_path (WorldMem format)
|
| 11 |
+
data_path: data/train_wm_gamecraft/v2/worldmem_train_v2_shuffle.csv
|
| 12 |
+
num_frames: 28 # latent frame count; dataset loads (21-1)*4+1=81 video frames
|
| 13 |
+
load_raw_video: true
|
| 14 |
+
sample_strategy: first # overfit: always start from SKIP_FRAMES; use "random" for normal training
|
| 15 |
+
|
| 16 |
+
# CFG: probability of dropping action conditioning (zeros actions + identity viewmats)
|
| 17 |
+
proportion_empty_prompts: 0.1
|
| 18 |
+
|
| 19 |
+
# Model
|
| 20 |
+
# 81 video frames @ 640x352 -> 21 latent frames @ 80x44 (WAN 4x temporal, 8x spatial)
|
| 21 |
+
image_or_video_shape:
|
| 22 |
+
- 1 # batch_size placeholder (overridden at runtime)
|
| 23 |
+
- 28 # latent frames
|
| 24 |
+
- 16 # latent channels
|
| 25 |
+
- 44 # latent height (352/8)
|
| 26 |
+
- 80 # latent width (640/8)
|
| 27 |
+
|
| 28 |
+
model_kwargs:
|
| 29 |
+
timestep_shift: 5.0
|
| 30 |
+
local_attn_size: -1
|
| 31 |
+
sink_size: 0
|
| 32 |
+
use_action_encoder: true
|
| 33 |
+
action_dim: 25
|
| 34 |
+
action_hidden_dim: 128
|
| 35 |
+
latent_channels: 16
|
| 36 |
+
|
| 37 |
+
# Training hyperparams
|
| 38 |
+
num_train_timestep: 1000
|
| 39 |
+
timestep_shift: 5.0
|
| 40 |
+
guidance_scale: 1.0
|
| 41 |
+
teacher_forcing: true
|
| 42 |
+
# noise_augmentation_max_timestep: 100
|
| 43 |
+
noise_augmentation_max_timestep: 0
|
| 44 |
+
|
| 45 |
+
lr: 1.0e-4
|
| 46 |
+
beta1: 0.9
|
| 47 |
+
beta2: 0.999
|
| 48 |
+
weight_decay: 0.01
|
| 49 |
+
batch_size: 1
|
| 50 |
+
gradient_checkpointing: true
|
| 51 |
+
num_frame_per_block: 4
|
| 52 |
+
sharding_strategy: hybrid_full
|
| 53 |
+
|
| 54 |
+
# Logging / saving
|
| 55 |
+
no_save: false
|
| 56 |
+
no_visualize: true
|
| 57 |
+
log_iters: 300
|
| 58 |
+
ema_weight: 0.999
|
| 59 |
+
ema_start_step: 200
|
| 60 |
+
gc_interval: 100
|
| 61 |
+
wandb_host: "https://api.wandb.ai"
|
| 62 |
+
wandb_key: WANDB_KEY
|
| 63 |
+
wandb_entity: "zhenhao-yang11-the-university-of-hong-kong"
|
| 64 |
+
wandb_project: worldmem_diffusion
|
| 65 |
+
negative_prompt: ""
|
| 66 |
+
|
| 67 |
+
# FSDP wrap strategy
|
| 68 |
+
generator_fsdp_wrap_strategy: size
|
| 69 |
+
text_encoder_fsdp_wrap_strategy: size
|
DecMem/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
|
DecMem/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
|
DecMem/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()
|
DecMem/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)
|
DecMem/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
|
DecMem/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
|
DecMem/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)
|
DecMem/evaluation/method_manifest.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"method": "DecMem official public Wan2.1-T2V-1.3B reference",
|
| 3 |
+
"upstream": "https://github.com/KlingAIResearch/DecMem",
|
| 4 |
+
"upstream_revision": "f355a1809444a9cc6746ff99d5255614348b20ac",
|
| 5 |
+
"checkpoint": "KlingTeam/DecMem/decmem.pt generator state",
|
| 6 |
+
"checkpoint_tensor_count": 492,
|
| 7 |
+
"checkpoint_parameter_count": 1197639296,
|
| 8 |
+
"checkpoint_weight_policy": "released generator; no EMA substitution",
|
| 9 |
+
"model": {
|
| 10 |
+
"trainable_parameters": 0,
|
| 11 |
+
"optimizer_groups": [],
|
| 12 |
+
"lr_stages": [],
|
| 13 |
+
"lora": "none",
|
| 14 |
+
"action_encoder": "official linear100to16",
|
| 15 |
+
"memory_projection_layers": 30,
|
| 16 |
+
"use_ltm_attn": true,
|
| 17 |
+
"ltm_topk": 80,
|
| 18 |
+
"ltm_tile_size": [
|
| 19 |
+
1,
|
| 20 |
+
8,
|
| 21 |
+
8
|
| 22 |
+
],
|
| 23 |
+
"stm_sliding_window": 8,
|
| 24 |
+
"local_attn_size": -1,
|
| 25 |
+
"block_latents": 4,
|
| 26 |
+
"activation_checkpointing": false
|
| 27 |
+
},
|
| 28 |
+
"sampling": {
|
| 29 |
+
"profile": "scripts/infer_example.sh released example defaults",
|
| 30 |
+
"solver": "UniPC",
|
| 31 |
+
"steps": 20,
|
| 32 |
+
"cfg": 5.0,
|
| 33 |
+
"timestep_shift": 5.0,
|
| 34 |
+
"dtype": "bfloat16",
|
| 35 |
+
"batch_size": 1,
|
| 36 |
+
"cfg_effective_batch": 2
|
| 37 |
+
},
|
| 38 |
+
"protocol": {
|
| 39 |
+
"manifest": "evaluation/manifests/minecraft_test_300.csv",
|
| 40 |
+
"observe_source": [
|
| 41 |
+
100,
|
| 42 |
+
700
|
| 43 |
+
],
|
| 44 |
+
"score_source": [
|
| 45 |
+
700,
|
| 46 |
+
1200
|
| 47 |
+
],
|
| 48 |
+
"stride": 1,
|
| 49 |
+
"unique_observed": 600,
|
| 50 |
+
"left_duplicates": 1,
|
| 51 |
+
"observed_rgb_with_padding": 601,
|
| 52 |
+
"observed_latents": 151,
|
| 53 |
+
"total_latents": 276,
|
| 54 |
+
"generated_latents": 125,
|
| 55 |
+
"generated_rgb": 500,
|
| 56 |
+
"score_rgb": 500,
|
| 57 |
+
"full_history": "allobserved latents warm SGM+ALM; partial3cleanlatentsinfirstgeneratedblock preserved"
|
| 58 |
+
},
|
| 59 |
+
"controls": {
|
| 60 |
+
"actions": "unchangedcanonicalWorldMem25channels; prependonezeroaction",
|
| 61 |
+
"poses": "xyz-firstxyz, negateyaw, wrapanglesmod360; sampled[::4]afterleftduplicate",
|
| 62 |
+
"pose_construction": "float32 world-to-camera then castbfloat16",
|
| 63 |
+
"first_generated_latent_pose_source": 703,
|
| 64 |
+
"last_pose_source": 1199,
|
| 65 |
+
"prope_max_time": 60,
|
| 66 |
+
"prope_sequence_semantics": "preserve native temporal indices, dynamicallyextendcoefficienttable; no60framecap"
|
| 67 |
+
},
|
| 68 |
+
"image": {
|
| 69 |
+
"native_resolution": [
|
| 70 |
+
352,
|
| 71 |
+
640
|
| 72 |
+
],
|
| 73 |
+
"input": "commonRGB01, fullframebilinearresize antialias, then[-1,1]",
|
| 74 |
+
"encode": "observed601framesonly; futureinitiallatentszero",
|
| 75 |
+
"decode": "entire observed+generated1101timelineusingnativecausalVAECPUstreaming;discardfirst601",
|
| 76 |
+
"score_targets": "sharedfixedOasisVAEreconstruction, neverDecMemVAEreconstruction"
|
| 77 |
+
},
|
| 78 |
+
"verification": {
|
| 79 |
+
"syntax": "passed",
|
| 80 |
+
"actual_first_clip_actions": "matchesofficialextract_pose_action_from_npz",
|
| 81 |
+
"actual_first_clip_poses": "matchesofficialnormalizationand[::4]",
|
| 82 |
+
"timeline_600_500": "151context,276total,500generated;firstscored700,last1199",
|
| 83 |
+
"timeline_600_20": "151context,156total,20generated",
|
| 84 |
+
"native_model_gpu_import": "pendingavailableSlurmallocation",
|
| 85 |
+
"strict_checkpoint_model_compatibility": "performed in GPU constructor, pending",
|
| 86 |
+
"vsa_compile": "passed; CUDA12.9 explicit compute90a/sm90a",
|
| 87 |
+
"vsa_binary_import": "passed on CPU; execution still awaits GPU"
|
| 88 |
+
},
|
| 89 |
+
"full_ready": false,
|
| 90 |
+
"blocker": "GPU native VSA import and actualgeneration smoke not completed"
|
| 91 |
+
}
|
DecMem/evaluation/worldmem_adapter.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run the released DecMem memory model on the fixed WorldMem Minecraft split."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import sys
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
from omegaconf import OmegaConf
|
| 13 |
+
|
| 14 |
+
REPO = Path(__file__).resolve().parents[1]
|
| 15 |
+
sys.path.insert(0, str(REPO))
|
| 16 |
+
sys.path.insert(0, str(REPO.parent / "shared"))
|
| 17 |
+
|
| 18 |
+
from protocol import case_seed, load_minecraft_case, save_case, select_cases
|
| 19 |
+
from utils.pose_utils import poses_to_viewmats
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def timeline(history_frames=600, target_frames=500):
|
| 23 |
+
prefix_padding = (1 - history_frames) % 4
|
| 24 |
+
context_latents = (history_frames + prefix_padding - 1) // 4 + 1
|
| 25 |
+
total_latents = math.ceil((context_latents + math.ceil(target_frames / 4)) / 4) * 4
|
| 26 |
+
generated_frames = (total_latents - context_latents) * 4
|
| 27 |
+
return context_latents, prefix_padding, total_latents, generated_frames
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def prepare_controls(case, target_frames):
|
| 31 |
+
history = case["history"]
|
| 32 |
+
n_cond, padding, total_latents, generated_frames = timeline(history, target_frames)
|
| 33 |
+
poses = case["poses"][:history + target_frames].clone().float()
|
| 34 |
+
# Match extract_pose_action_from_npz: translate xyz, negate yaw, then wrap angles.
|
| 35 |
+
poses[:, :3] -= poses[:1, :3].clone()
|
| 36 |
+
poses[:, -1].neg_()
|
| 37 |
+
poses[:, 3:].remainder_(360)
|
| 38 |
+
actions = case["actions"][:history + target_frames].float()
|
| 39 |
+
tail = generated_frames - target_frames
|
| 40 |
+
poses = torch.cat([poses[:1].expand(padding, -1), poses, poses[-1:].expand(tail, -1)])
|
| 41 |
+
actions = torch.cat([actions.new_zeros((padding, 25)), actions, actions.new_zeros((tail, 25))])
|
| 42 |
+
source_indices = case["source_indices"][:history + target_frames]
|
| 43 |
+
source_indices = torch.cat([source_indices[:1].expand(padding), source_indices, source_indices[-1:].expand(tail)])
|
| 44 |
+
return actions, poses[::4], source_indices[::4], (n_cond, padding, total_latents, generated_frames)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class DecMemAdapter:
|
| 48 |
+
def __init__(self, checkpoint, device="cuda"):
|
| 49 |
+
self.checkpoint = str(Path(checkpoint).resolve())
|
| 50 |
+
# Upstream model and VAE loading use paths relative to the DecMem checkout.
|
| 51 |
+
os.chdir(REPO)
|
| 52 |
+
from pipeline import WorldMemCausalDiffusionInferencePipeline
|
| 53 |
+
|
| 54 |
+
self.device = torch.device(device)
|
| 55 |
+
self.dtype = torch.bfloat16
|
| 56 |
+
self.config = OmegaConf.merge(
|
| 57 |
+
OmegaConf.load(REPO / "configs/default_config.yaml"),
|
| 58 |
+
OmegaConf.load(REPO / "configs/decmem.yaml"),
|
| 59 |
+
)
|
| 60 |
+
self.config.use_text_encoder = False
|
| 61 |
+
self.config.image_or_video_shape[1] = timeline()[2]
|
| 62 |
+
self.config.num_frames = self.config.image_or_video_shape[1]
|
| 63 |
+
self.config.prope_max_time = 60
|
| 64 |
+
self.config.rope_scale_pattern = "rope"
|
| 65 |
+
self.pipeline = WorldMemCausalDiffusionInferencePipeline(self.config, device=self.device)
|
| 66 |
+
checkpoint_state = torch.load(self.checkpoint, map_location="cpu", weights_only=True, mmap=True)
|
| 67 |
+
self.pipeline.generator.load_state_dict(checkpoint_state["generator"], strict=True)
|
| 68 |
+
self.pipeline.to(device=self.device, dtype=self.dtype).eval().requires_grad_(False)
|
| 69 |
+
self.parameter_counts = {
|
| 70 |
+
"generator_total": sum(p.numel() for p in self.pipeline.generator.parameters()),
|
| 71 |
+
"generator_trainable": sum(p.numel() for p in self.pipeline.generator.parameters() if p.requires_grad),
|
| 72 |
+
"action_encoder": sum(p.numel() for p in self.pipeline.generator.action_encoder.parameters()),
|
| 73 |
+
"memory_projection": sum(p.numel() for n, p in self.pipeline.generator.named_parameters() if ".mem_proj." in n),
|
| 74 |
+
"vae_total": sum(p.numel() for p in self.pipeline.vae.parameters()),
|
| 75 |
+
"vae_trainable": sum(p.numel() for p in self.pipeline.vae.parameters() if p.requires_grad),
|
| 76 |
+
}
|
| 77 |
+
print(json.dumps({"parameter_counts": self.parameter_counts, "checkpoint_strict_load": "passed"}), flush=True)
|
| 78 |
+
|
| 79 |
+
@torch.inference_mode()
|
| 80 |
+
def generate(self, case, target_frames=500, seed=0):
|
| 81 |
+
torch.manual_seed(seed)
|
| 82 |
+
torch.cuda.manual_seed_all(seed)
|
| 83 |
+
history = case["history"]
|
| 84 |
+
actions, poses, pose_indices, layout = prepare_controls(case, target_frames)
|
| 85 |
+
n_cond, padding, total_latents, generated_frames = layout
|
| 86 |
+
# Future RGB never reaches the VAE; only the complete observed prefix is encoded.
|
| 87 |
+
observed = case["rgb"][:history].to(self.device)
|
| 88 |
+
observed = F.interpolate(observed, (352, 640), mode="bilinear", align_corners=False, antialias=True)
|
| 89 |
+
observed = torch.cat([observed[:1].expand(padding, -1, -1, -1), observed])
|
| 90 |
+
observed = observed.transpose(0, 1).unsqueeze(0).mul(2).sub(1).to(self.dtype)
|
| 91 |
+
context_latent = self.pipeline.vae.encode_to_latent(observed).to(self.device, self.dtype)
|
| 92 |
+
del observed
|
| 93 |
+
|
| 94 |
+
initial_latent = context_latent.new_zeros((1, total_latents, 16, 44, 80))
|
| 95 |
+
initial_latent[:, :n_cond] = context_latent
|
| 96 |
+
del context_latent
|
| 97 |
+
actions = actions.unsqueeze(0).to(self.device, self.dtype)
|
| 98 |
+
viewmats = poses_to_viewmats(poses.unsqueeze(0).to(self.device)).to(self.dtype)
|
| 99 |
+
noise = torch.randn(initial_latent.shape, device=self.device, dtype=self.dtype)
|
| 100 |
+
print(json.dumps({"observed_frames": history, "prefix_padding": padding, "condition_latents": n_cond, "total_latents": total_latents}), flush=True)
|
| 101 |
+
_, output_latents = self.pipeline.inference(
|
| 102 |
+
noise=noise,
|
| 103 |
+
actions=actions,
|
| 104 |
+
viewmats=viewmats,
|
| 105 |
+
initial_latent=initial_latent,
|
| 106 |
+
num_condition_frames=n_cond,
|
| 107 |
+
guidance_scale=5.0,
|
| 108 |
+
sampling_steps=20,
|
| 109 |
+
return_latents=True,
|
| 110 |
+
skip_vae_decode=True,
|
| 111 |
+
)
|
| 112 |
+
# Denoising is finished. Release global KV before decoding the complete causal timeline.
|
| 113 |
+
self.pipeline.kv_cache = None
|
| 114 |
+
self.pipeline.crossattn_cache = None
|
| 115 |
+
torch.cuda.empty_cache()
|
| 116 |
+
video = self.pipeline.vae.decode_to_pixel(output_latents)
|
| 117 |
+
prediction = video[0, history + padding:history + padding + target_frames].float().mul(0.5).add(0.5).clamp(0, 1).cpu()
|
| 118 |
+
metadata = {
|
| 119 |
+
"method": "decmem",
|
| 120 |
+
"implementation": "official public Wan2.1-T2V-1.3B reference",
|
| 121 |
+
"checkpoint": self.checkpoint,
|
| 122 |
+
"weight_source": "released generator state; no EMA substitution",
|
| 123 |
+
"parameter_counts": self.parameter_counts,
|
| 124 |
+
"dtype": "bfloat16",
|
| 125 |
+
"batch_size": 1,
|
| 126 |
+
"cfg_batch_size": 2,
|
| 127 |
+
"sampling": {"solver": "UniPC", "steps": 20, "guidance_scale": 5.0, "timestep_shift": 5.0, "source": "scripts/infer_example.sh"},
|
| 128 |
+
"memory": {"use_ltm_attn": True, "ltm_topk": 80, "ltm_tile_size": [1, 8, 8], "stm_sliding_window": 8, "num_frame_per_block": 4},
|
| 129 |
+
"history_frames": history,
|
| 130 |
+
"history_source_indices": case["source_indices"][:history].tolist(),
|
| 131 |
+
"history_left_padding": padding,
|
| 132 |
+
"condition_latents": n_cond,
|
| 133 |
+
"total_latents": total_latents,
|
| 134 |
+
"generated_frames": generated_frames,
|
| 135 |
+
"scored_frames": target_frames,
|
| 136 |
+
"future_padding": generated_frames - target_frames,
|
| 137 |
+
"action_mapping": "official WorldMem 25 channels, no scale changes; prepend one noop",
|
| 138 |
+
"pose_mapping": "translate xyz relative to source100; negate yaw; angles modulo360; padded source indices[::4]",
|
| 139 |
+
"latent_pose_source_indices": pose_indices.tolist(),
|
| 140 |
+
"pose_matrix_precision": "float32 construction, then bfloat16 model input",
|
| 141 |
+
"prope_max_time": 60,
|
| 142 |
+
"prope_time_semantics": "native absolute latent indices;60 is not a sequence length cap",
|
| 143 |
+
"native_resolution": [352, 640],
|
| 144 |
+
"preprocessing": "full-frame bilinear resize with antialias; no crop",
|
| 145 |
+
"target_contract": "shared fixed Oasis VAE reconstruction; no method VAE target substitution",
|
| 146 |
+
"seed": seed,
|
| 147 |
+
}
|
| 148 |
+
return prediction, metadata
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def main():
|
| 152 |
+
parser = argparse.ArgumentParser()
|
| 153 |
+
parser.add_argument("--manifest", required=True)
|
| 154 |
+
parser.add_argument("--data-root", required=True)
|
| 155 |
+
parser.add_argument("--checkpoint", default=str(REPO.parent / "checkpoints/decmem/decmem.pt"))
|
| 156 |
+
parser.add_argument("--output", required=True)
|
| 157 |
+
parser.add_argument("--limit", type=int)
|
| 158 |
+
parser.add_argument("--max-generated", type=int, default=500)
|
| 159 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 160 |
+
parser.add_argument("--rank", type=int, default=int(os.environ.get("SLURM_PROCID", 0)))
|
| 161 |
+
parser.add_argument("--world-size", type=int, default=int(os.environ.get("SLURM_NTASKS", 1)))
|
| 162 |
+
parser.add_argument("--preflight-only", action="store_true")
|
| 163 |
+
args = parser.parse_args()
|
| 164 |
+
if not 1 <= args.max_generated <= 500:
|
| 165 |
+
parser.error("--max-generated must be between 1 and the fixed 500-frame evaluation horizon")
|
| 166 |
+
torch.set_num_threads(4)
|
| 167 |
+
rows = select_cases(args.manifest, rank=args.rank, world_size=args.world_size, limit=args.limit)
|
| 168 |
+
if args.preflight_only:
|
| 169 |
+
case = load_minecraft_case(rows[0], args.data_root)
|
| 170 |
+
actions, poses, indices, layout = prepare_controls(case, args.max_generated)
|
| 171 |
+
report = {"selected_cases": len(rows), "actions_shape": list(actions.shape), "poses_shape": list(poses.shape), "latent_pose_source_indices": indices.tolist(), "layout": layout, "gpu_model_import": "not attempted"}
|
| 172 |
+
output = Path(args.output)
|
| 173 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 174 |
+
(output / f"preflight_rank{args.rank}.json").write_text(json.dumps(report, indent=2) + "\n")
|
| 175 |
+
print(json.dumps(report), flush=True)
|
| 176 |
+
return
|
| 177 |
+
model = DecMemAdapter(args.checkpoint)
|
| 178 |
+
revision = "f355a1809444a9cc6746ff99d5255614348b20ac"
|
| 179 |
+
for row in rows:
|
| 180 |
+
case = load_minecraft_case(row, args.data_root)
|
| 181 |
+
prediction, metadata = model.generate(case, args.max_generated, case_seed(row, args.seed))
|
| 182 |
+
metadata.update({"git_revision": revision, "arguments": vars(args)})
|
| 183 |
+
directory = save_case(args.output, row, prediction, metadata)
|
| 184 |
+
print(f"Saved {tuple(prediction.shape)} to {directory}", flush=True)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
if __name__ == "__main__":
|
| 188 |
+
main()
|
DecMem/example/example.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
video_path,action_path
|
| 2 |
+
example/000045.mp4,example/000045.npz
|
| 3 |
+
example/000046.mp4,example/000046.npz
|
DecMem/infer.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WorldMem CFG inference — block-by-block causal denoising with KV cache + PRoPE.
|
| 3 |
+
|
| 4 |
+
Delegates to WorldMemCausalDiffusionInferencePipeline, which:
|
| 5 |
+
- Writes any fully-clean prefix block to the KV cache at timestep=0 first.
|
| 6 |
+
- Denoises one block at a time via UniPC, each step a single forward with
|
| 7 |
+
kv_cache + PRoPE recomputed for that block's viewmats.
|
| 8 |
+
- Commits each block's clean K/V to cache before moving to the next, so
|
| 9 |
+
later blocks attend against clean history (matching teacher-forcing
|
| 10 |
+
training distribution).
|
| 11 |
+
- CFG is one batched forward with batch_size = 2*B; the KV cache is also
|
| 12 |
+
allocated with batch_size = 2*B. cond/uncond chunked back after each step.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import gc
|
| 17 |
+
import os
|
| 18 |
+
import sys
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
import torch.distributed as dist
|
| 22 |
+
import numpy as np
|
| 23 |
+
from omegaconf import OmegaConf
|
| 24 |
+
from torchvision.io import write_video
|
| 25 |
+
|
| 26 |
+
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
| 27 |
+
sys.path.insert(0, ".")
|
| 28 |
+
|
| 29 |
+
from pipeline import WorldMemCausalDiffusionInferencePipeline
|
| 30 |
+
from utils.dataset import WorldMemCSVDataset
|
| 31 |
+
from utils.pose_utils import poses_to_viewmats
|
| 32 |
+
from utils.misc import set_seed
|
| 33 |
+
|
| 34 |
+
parser = argparse.ArgumentParser()
|
| 35 |
+
parser.add_argument("--config_path", default="configs/worldmem_diffusion_overfit.yaml")
|
| 36 |
+
parser.add_argument("--output_dir", default="test/res/infer_worldmem_cfg")
|
| 37 |
+
parser.add_argument("--checkpoint_path", default=None,
|
| 38 |
+
help="Optional generator checkpoint (model.pt)")
|
| 39 |
+
parser.add_argument("--data_path", default=None,
|
| 40 |
+
help="Override config.data_path (CSV) for inference test set.")
|
| 41 |
+
parser.add_argument("--num_samples", type=int, default=1,
|
| 42 |
+
help="Number of noise resamples per dataset entry.")
|
| 43 |
+
parser.add_argument("--num_inference_steps", type=int, default=50)
|
| 44 |
+
parser.add_argument("--guidance_scale", type=float, default=3.0,
|
| 45 |
+
help="CFG scale. <=1.0 disables CFG (single forward, B=1 cache).")
|
| 46 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 47 |
+
parser.add_argument("--start_index", type=int, default=0,
|
| 48 |
+
help="First dataset row (env: START_INDEX). "
|
| 49 |
+
"Ignored if --sample_indices is provided.")
|
| 50 |
+
parser.add_argument("--num_entries", type=int, default=None,
|
| 51 |
+
help="Number of consecutive dataset rows from --start_index (env: N). "
|
| 52 |
+
"Default: all rows from start_index to dataset end. "
|
| 53 |
+
"Ignored if --sample_indices is provided.")
|
| 54 |
+
parser.add_argument("--sample_indices", type=str, default=None,
|
| 55 |
+
help="Explicit dataset indices, overrides --start_index/--num_entries. "
|
| 56 |
+
"Accepts comma-separated ints and inclusive ranges, e.g. "
|
| 57 |
+
"'0,3,5' or '0-9,15,20-22', or 'all' for the full dataset.")
|
| 58 |
+
parser.add_argument("--num_condition_frames", type=int, default=1,
|
| 59 |
+
help="Number of clean prefix latent frames (1=first image, "
|
| 60 |
+
"3=first block when num_frame_per_block=3).")
|
| 61 |
+
parser.add_argument("--num_latent_frames", type=int, default=None,
|
| 62 |
+
help="Override config.image_or_video_shape[1] (latent T). "
|
| 63 |
+
"Must be a multiple of num_frame_per_block.")
|
| 64 |
+
parser.add_argument("--tag", type=str, default="",
|
| 65 |
+
help="Filename tag appended to outputs.")
|
| 66 |
+
parser.add_argument("--offload", action="store_true",
|
| 67 |
+
help="Mutually-exclusive GPU residency for generator and VAE: "
|
| 68 |
+
"during denoising the VAE lives on CPU; for VAE encode/decode "
|
| 69 |
+
"the generator lives on CPU. Trades extra H2D/D2H copies for "
|
| 70 |
+
"lower peak VRAM.")
|
| 71 |
+
args = parser.parse_args()
|
| 72 |
+
|
| 73 |
+
# ─── Distributed (optional, via torchrun) ────────────────────────────────────
|
| 74 |
+
if "LOCAL_RANK" in os.environ:
|
| 75 |
+
dist.init_process_group(backend="nccl")
|
| 76 |
+
local_rank = int(os.environ["LOCAL_RANK"])
|
| 77 |
+
world_size = dist.get_world_size()
|
| 78 |
+
torch.cuda.set_device(local_rank)
|
| 79 |
+
device = torch.device(f"cuda:{local_rank}")
|
| 80 |
+
else:
|
| 81 |
+
local_rank = 0
|
| 82 |
+
world_size = 1
|
| 83 |
+
device = torch.device("cuda")
|
| 84 |
+
|
| 85 |
+
set_seed(args.seed + local_rank)
|
| 86 |
+
dtype = torch.bfloat16
|
| 87 |
+
|
| 88 |
+
# ─── Config ──────────────────────────────────────────────────────────────────
|
| 89 |
+
config = OmegaConf.load(args.config_path)
|
| 90 |
+
default_config = OmegaConf.load("configs/default_config.yaml")
|
| 91 |
+
config = OmegaConf.merge(default_config, config)
|
| 92 |
+
config.use_text_encoder = False
|
| 93 |
+
if args.data_path is not None:
|
| 94 |
+
print(f"Overriding config.data_path: {config.data_path} -> {args.data_path}")
|
| 95 |
+
config.data_path = args.data_path
|
| 96 |
+
|
| 97 |
+
if args.num_latent_frames is not None:
|
| 98 |
+
nfb = int(config.num_frame_per_block)
|
| 99 |
+
assert args.num_latent_frames % nfb == 0, (
|
| 100 |
+
f"--num_latent_frames={args.num_latent_frames} must be a multiple of "
|
| 101 |
+
f"num_frame_per_block={nfb}")
|
| 102 |
+
print(f"Overriding latent T: {config.image_or_video_shape[1]} -> {args.num_latent_frames}")
|
| 103 |
+
config.image_or_video_shape[1] = args.num_latent_frames
|
| 104 |
+
config.num_frames = args.num_latent_frames
|
| 105 |
+
|
| 106 |
+
num_latent_frames = config.image_or_video_shape[1]
|
| 107 |
+
latent_C, latent_H, latent_W = config.image_or_video_shape[2:]
|
| 108 |
+
n_cond = int(args.num_condition_frames)
|
| 109 |
+
assert 1 <= n_cond < num_latent_frames, \
|
| 110 |
+
f"num_condition_frames must be in [1, {num_latent_frames - 1}], got {n_cond}"
|
| 111 |
+
|
| 112 |
+
guidance_scale = float(args.guidance_scale)
|
| 113 |
+
use_cfg = guidance_scale > 1.0
|
| 114 |
+
if local_rank == 0:
|
| 115 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 116 |
+
if dist.is_initialized():
|
| 117 |
+
dist.barrier()
|
| 118 |
+
print(f"Config: T={num_latent_frames} latent {latent_H}x{latent_W} "
|
| 119 |
+
f"nfb={config.num_frame_per_block} "
|
| 120 |
+
f"guidance_scale={guidance_scale} (cfg={use_cfg}) "
|
| 121 |
+
f"n_cond={n_cond}")
|
| 122 |
+
|
| 123 |
+
# ─── Pipeline ────────────────────────────────────────────────────────────────
|
| 124 |
+
torch.set_grad_enabled(False)
|
| 125 |
+
pipeline = WorldMemCausalDiffusionInferencePipeline(config, device=device)
|
| 126 |
+
pipeline.generator = pipeline.generator.to(device=device, dtype=dtype)
|
| 127 |
+
pipeline.generator.eval()
|
| 128 |
+
pipeline.vae = pipeline.vae.to(device=device, dtype=dtype)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# ─── Optional CPU/GPU swap for generator/VAE ────────────────────────────────
|
| 132 |
+
# When --offload is set, only one of {generator, VAE} lives on the GPU at any
|
| 133 |
+
# time. The "owner" variable below tracks which model currently holds GPU
|
| 134 |
+
# residency so we can no-op redundant swaps. Idle model is parked on CPU.
|
| 135 |
+
_GPU_OWNER = "both" # "generator" | "vae" | "both" | "none"
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def _swap_to(target: str):
|
| 139 |
+
"""Make ``target`` the sole GPU resident; park the other on CPU.
|
| 140 |
+
|
| 141 |
+
target ∈ {"generator", "vae"}. No-op when --offload is disabled.
|
| 142 |
+
Frees pipeline.kv_cache before parking the generator and runs an
|
| 143 |
+
explicit gc + empty_cache so VRAM is reclaimed before the next load.
|
| 144 |
+
"""
|
| 145 |
+
global _GPU_OWNER
|
| 146 |
+
if not args.offload:
|
| 147 |
+
return
|
| 148 |
+
if _GPU_OWNER == target:
|
| 149 |
+
return
|
| 150 |
+
if target == "vae":
|
| 151 |
+
# Park generator on CPU. Drop transient GPU state (kv_cache) first
|
| 152 |
+
# so the move actually frees memory rather than fragmenting it.
|
| 153 |
+
if pipeline.kv_cache is not None:
|
| 154 |
+
pipeline.kv_cache = None
|
| 155 |
+
if pipeline.crossattn_cache is not None:
|
| 156 |
+
pipeline.crossattn_cache = None
|
| 157 |
+
pipeline.generator.to("cpu")
|
| 158 |
+
gc.collect()
|
| 159 |
+
torch.cuda.empty_cache()
|
| 160 |
+
pipeline.vae.to(device=device, dtype=dtype)
|
| 161 |
+
elif target == "generator":
|
| 162 |
+
pipeline.vae.to("cpu")
|
| 163 |
+
gc.collect()
|
| 164 |
+
torch.cuda.empty_cache()
|
| 165 |
+
pipeline.generator.to(device=device, dtype=dtype)
|
| 166 |
+
else:
|
| 167 |
+
raise ValueError(f"Unknown swap target: {target}")
|
| 168 |
+
_GPU_OWNER = target
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
if args.offload:
|
| 172 |
+
# Initial state: VAE on GPU (first thing we do per-sample is encode).
|
| 173 |
+
pipeline.generator.to("cpu")
|
| 174 |
+
gc.collect()
|
| 175 |
+
torch.cuda.empty_cache()
|
| 176 |
+
_GPU_OWNER = "vae"
|
| 177 |
+
print("Offload enabled: generator/VAE are mutually exclusive on GPU.")
|
| 178 |
+
|
| 179 |
+
if args.checkpoint_path:
|
| 180 |
+
weight_tag = "generator"
|
| 181 |
+
print(f"Loading checkpoint: {args.checkpoint_path} ({weight_tag})")
|
| 182 |
+
ckpt = torch.load(args.checkpoint_path, map_location="cpu", weights_only=False)
|
| 183 |
+
sd = ckpt.get("generator", ckpt.get("model", ckpt))
|
| 184 |
+
sample_key = next(iter(sd.keys()))
|
| 185 |
+
target = pipeline.generator if sample_key.startswith("model.") else pipeline.generator.model
|
| 186 |
+
missing, unexpected = target.load_state_dict(sd, strict=False)
|
| 187 |
+
if missing:
|
| 188 |
+
print(f" missing keys ({len(missing)}): {missing[:5]}{'...' if len(missing) > 5 else ''}")
|
| 189 |
+
if unexpected:
|
| 190 |
+
print(f" unexpected keys ({len(unexpected)}): {unexpected[:5]}{'...' if len(unexpected) > 5 else ''}")
|
| 191 |
+
else:
|
| 192 |
+
print("No checkpoint — using pretrained WAN backbone + random action encoder.")
|
| 193 |
+
|
| 194 |
+
# ─── Dataset ─────────────────────────────────────────────────────────────────
|
| 195 |
+
dataset = WorldMemCSVDataset(
|
| 196 |
+
config.data_path,
|
| 197 |
+
num_latent_frames=num_latent_frames,
|
| 198 |
+
sample_strategy="first",
|
| 199 |
+
)
|
| 200 |
+
print(f"Dataset size: {len(dataset)}")
|
| 201 |
+
|
| 202 |
+
def _parse_sample_indices(spec: str, dataset_size: int) -> list:
|
| 203 |
+
"""Parse '0,3,5' / '0-9,15,20-22' / 'all' into a deduplicated sorted index list."""
|
| 204 |
+
if spec.strip().lower() == "all":
|
| 205 |
+
return list(range(dataset_size))
|
| 206 |
+
out = set()
|
| 207 |
+
for token in spec.split(","):
|
| 208 |
+
token = token.strip()
|
| 209 |
+
if not token:
|
| 210 |
+
continue
|
| 211 |
+
if "-" in token:
|
| 212 |
+
lo, hi = token.split("-", 1)
|
| 213 |
+
lo, hi = int(lo), int(hi)
|
| 214 |
+
assert lo <= hi, f"bad range '{token}': lo > hi"
|
| 215 |
+
out.update(range(lo, hi + 1))
|
| 216 |
+
else:
|
| 217 |
+
out.add(int(token))
|
| 218 |
+
indices = sorted(out)
|
| 219 |
+
for i in indices:
|
| 220 |
+
assert 0 <= i < dataset_size, (
|
| 221 |
+
f"index {i} out of range [0, {dataset_size}) for --sample_indices='{spec}'")
|
| 222 |
+
return indices
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
if args.sample_indices is not None:
|
| 226 |
+
sample_indices = _parse_sample_indices(args.sample_indices, len(dataset))
|
| 227 |
+
assert sample_indices, f"--sample_indices='{args.sample_indices}' parsed to empty list"
|
| 228 |
+
print(f"Using explicit --sample_indices ({len(sample_indices)}): {sample_indices}")
|
| 229 |
+
else:
|
| 230 |
+
assert args.start_index >= 0, f"--start_index must be >= 0, got {args.start_index}"
|
| 231 |
+
if args.num_entries is not None:
|
| 232 |
+
assert args.num_entries > 0, f"--num_entries must be > 0, got {args.num_entries}"
|
| 233 |
+
end_index = args.start_index + args.num_entries
|
| 234 |
+
sample_indices = list(range(args.start_index, min(end_index, len(dataset))))
|
| 235 |
+
if end_index > len(dataset):
|
| 236 |
+
print(f"Warning: start_index={args.start_index} + num_entries={args.num_entries} "
|
| 237 |
+
f"exceeds dataset size {len(dataset)}; using indices "
|
| 238 |
+
f"{args.start_index}..{len(dataset) - 1}")
|
| 239 |
+
else:
|
| 240 |
+
sample_indices = list(range(args.start_index, len(dataset)))
|
| 241 |
+
|
| 242 |
+
if world_size > 1:
|
| 243 |
+
sample_indices = sample_indices[local_rank::world_size]
|
| 244 |
+
print(f"[rank {local_rank}/{world_size}] shard indices: {sample_indices}")
|
| 245 |
+
elif sample_indices:
|
| 246 |
+
print(f"Will run on dataset indices: {sample_indices}")
|
| 247 |
+
|
| 248 |
+
def run_one_sample(ds_idx: int):
|
| 249 |
+
print(f"\n=== Dataset index {ds_idx} ===")
|
| 250 |
+
rng_state = np.random.get_state()
|
| 251 |
+
np.random.seed(args.seed)
|
| 252 |
+
sample = dataset[ds_idx]
|
| 253 |
+
np.random.set_state(rng_state)
|
| 254 |
+
|
| 255 |
+
frames_raw = sample["frames"].unsqueeze(0).to(device=device, dtype=dtype) # (1,3,T_vid,H,W)
|
| 256 |
+
actions = sample["actions"].unsqueeze(0).to(device=device, dtype=dtype) # (1,T_vid,25)
|
| 257 |
+
poses = sample["poses"].unsqueeze(0).to(device=device, dtype=dtype) # (1,T_lat,5)
|
| 258 |
+
|
| 259 |
+
# VAE encode: needs VAE on GPU.
|
| 260 |
+
_swap_to("vae")
|
| 261 |
+
gt_latent = pipeline.vae.encode_to_latent(frames_raw).to(device=device, dtype=dtype)
|
| 262 |
+
viewmats = poses_to_viewmats(poses) # (1,T,4,4)
|
| 263 |
+
|
| 264 |
+
for sample_idx in range(args.num_samples):
|
| 265 |
+
print(f" noise sample {sample_idx + 1}/{args.num_samples} (prefix={n_cond}) ...")
|
| 266 |
+
torch.manual_seed(args.seed + sample_idx)
|
| 267 |
+
|
| 268 |
+
noise = torch.randn(1, num_latent_frames, latent_C, latent_H, latent_W,
|
| 269 |
+
device=device, dtype=dtype)
|
| 270 |
+
# Denoising: needs generator on GPU; VAE parked on CPU.
|
| 271 |
+
_swap_to("generator")
|
| 272 |
+
with torch.no_grad():
|
| 273 |
+
video, latents = pipeline.inference(
|
| 274 |
+
noise=noise,
|
| 275 |
+
actions=actions,
|
| 276 |
+
viewmats=viewmats,
|
| 277 |
+
initial_latent=gt_latent,
|
| 278 |
+
num_condition_frames=n_cond,
|
| 279 |
+
guidance_scale=guidance_scale,
|
| 280 |
+
sampling_steps=args.num_inference_steps,
|
| 281 |
+
return_latents=True,
|
| 282 |
+
skip_vae_decode=args.offload,
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
# Decode: park generator, bring VAE back to GPU.
|
| 286 |
+
# In offload mode, `video` is None — we decode externally now that the
|
| 287 |
+
# VAE owns the GPU. In non-offload mode the decode already happened.
|
| 288 |
+
_swap_to("vae")
|
| 289 |
+
if args.offload:
|
| 290 |
+
video = pipeline.vae.decode_to_pixel(latents.to(dtype=dtype))
|
| 291 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 292 |
+
|
| 293 |
+
gt_video = pipeline.vae.decode_to_pixel(gt_latent.to(dtype=dtype))
|
| 294 |
+
gt_video = (gt_video * 0.5 + 0.5).clamp(0, 1)
|
| 295 |
+
|
| 296 |
+
gen_uint8 = (video[0].permute(0, 2, 3, 1).cpu().float() * 255).byte()
|
| 297 |
+
gt_uint8 = (gt_video[0].permute(0, 2, 3, 1).cpu().float() * 255).byte()
|
| 298 |
+
side_by_side = torch.cat([gt_uint8, gen_uint8], dim=2)
|
| 299 |
+
|
| 300 |
+
tag = f"_{args.tag}" if args.tag else ""
|
| 301 |
+
out_path = os.path.join(
|
| 302 |
+
args.output_dir,
|
| 303 |
+
f"worldmem_s{ds_idx}_n{sample_idx}_cond{n_cond}_cfg{guidance_scale}{tag}.mp4")
|
| 304 |
+
write_video(out_path, side_by_side, fps=10)
|
| 305 |
+
|
| 306 |
+
out_dir = os.path.join(args.output_dir, 'pred')
|
| 307 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 308 |
+
out_gen_path = os.path.join(
|
| 309 |
+
out_dir,
|
| 310 |
+
f"worldmem_s{ds_idx}_n{sample_idx}_cond{n_cond}_cfg{guidance_scale}{tag}.mp4")
|
| 311 |
+
write_video(out_gen_path, gen_uint8, fps=10)
|
| 312 |
+
|
| 313 |
+
out_dir = os.path.join(args.output_dir, 'gt')
|
| 314 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 315 |
+
out_gt_path = os.path.join(
|
| 316 |
+
out_dir,
|
| 317 |
+
f"worldmem_s{ds_idx}_n{sample_idx}_cond{n_cond}_cfg{guidance_scale}{tag}.mp4")
|
| 318 |
+
write_video(out_gt_path, gt_uint8, fps=10)
|
| 319 |
+
|
| 320 |
+
print(f" Saved {out_path} (left=GT | right=generated)")
|
| 321 |
+
|
| 322 |
+
# Release per-sample GPU residents and defrag the allocator before the
|
| 323 |
+
# next iteration re-allocates the KV cache inside pipeline.inference().
|
| 324 |
+
del video, latents, gt_video, gen_uint8, gt_uint8, side_by_side
|
| 325 |
+
gc.collect()
|
| 326 |
+
torch.cuda.empty_cache()
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
if not sample_indices:
|
| 330 |
+
print(f"[rank {local_rank}] no samples in this shard, skipping.")
|
| 331 |
+
else:
|
| 332 |
+
for ds_idx in sample_indices:
|
| 333 |
+
run_one_sample(ds_idx)
|
| 334 |
+
|
| 335 |
+
if dist.is_initialized():
|
| 336 |
+
dist.barrier()
|
| 337 |
+
if local_rank == 0:
|
| 338 |
+
print("\nDone.")
|
DecMem/model/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .diffusion import CausalDiffusion
|
| 2 |
+
from .causvid import CausVid
|
| 3 |
+
from .gan import GAN
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"CausalDiffusion",
|
| 7 |
+
"CausVid",
|
| 8 |
+
"GAN",
|
| 9 |
+
]
|
DecMem/model/base.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 SelfForcingTrainingPipeline
|
| 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 SelfForcingModel(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 |
+
return self.inference_pipeline.inference_with_trajectory(
|
| 203 |
+
noise=noise, **conditional_dict
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
def _initialize_inference_pipeline(self):
|
| 207 |
+
"""
|
| 208 |
+
Lazy initialize the inference pipeline during the first backward simulation run.
|
| 209 |
+
Here we encapsulate the inference code with a model-dependent outside function.
|
| 210 |
+
We pass our FSDP-wrapped modules into the pipeline to save memory.
|
| 211 |
+
"""
|
| 212 |
+
self.inference_pipeline = SelfForcingTrainingPipeline(
|
| 213 |
+
denoising_step_list=self.denoising_step_list,
|
| 214 |
+
scheduler=self.scheduler,
|
| 215 |
+
generator=self.generator,
|
| 216 |
+
num_frame_per_block=self.num_frame_per_block,
|
| 217 |
+
independent_first_frame=self.args.independent_first_frame,
|
| 218 |
+
same_step_across_blocks=self.args.same_step_across_blocks,
|
| 219 |
+
last_step_only=self.args.last_step_only,
|
| 220 |
+
num_max_frames=self.num_training_frames,
|
| 221 |
+
context_noise=self.args.context_noise
|
| 222 |
+
)
|
DecMem/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
|
DecMem/model/diffusion.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, device=None):
|
| 35 |
+
self.generator = WanDiffusionWrapper(**getattr(args, "model_kwargs", {}), is_causal=True)
|
| 36 |
+
self.generator.model.requires_grad_(True)
|
| 37 |
+
|
| 38 |
+
# Text encoder is optional; skip when training without text conditioning (e.g. WorldMem)
|
| 39 |
+
if getattr(args, "use_text_encoder", True):
|
| 40 |
+
self.text_encoder = WanTextEncoder()
|
| 41 |
+
self.text_encoder.requires_grad_(False)
|
| 42 |
+
else:
|
| 43 |
+
self.text_encoder = None
|
| 44 |
+
|
| 45 |
+
self.vae = WanVAEWrapper()
|
| 46 |
+
self.vae.requires_grad_(False)
|
| 47 |
+
|
| 48 |
+
self.scheduler = self.generator.get_scheduler()
|
| 49 |
+
if device is not None:
|
| 50 |
+
self.scheduler.timesteps = self.scheduler.timesteps.to(device)
|
| 51 |
+
|
| 52 |
+
# Set up PRoPE for all attention layers when action encoder is enabled
|
| 53 |
+
model_kwargs = getattr(args, "model_kwargs", {})
|
| 54 |
+
if model_kwargs.get("use_action_encoder", False):
|
| 55 |
+
image_or_video_shape = getattr(args, "image_or_video_shape", [1, 21, 16, 60, 104])
|
| 56 |
+
num_frames = image_or_video_shape[1]
|
| 57 |
+
latent_h = image_or_video_shape[3]
|
| 58 |
+
latent_w = image_or_video_shape[4]
|
| 59 |
+
# patch_size for WAN is (1, 2, 2)
|
| 60 |
+
patches_y = latent_h // 2
|
| 61 |
+
patches_x = latent_w // 2
|
| 62 |
+
self.generator.model.setup_prope(
|
| 63 |
+
patches_x=patches_x,
|
| 64 |
+
patches_y=patches_y,
|
| 65 |
+
image_width=latent_w,
|
| 66 |
+
image_height=latent_h,
|
| 67 |
+
cameras=num_frames,
|
| 68 |
+
prope_max_time=getattr(args, "prope_max_time", 60),
|
| 69 |
+
rope_scale_pattern=getattr(args, "rope_scale_pattern", "rope"),
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
def generator_loss(
|
| 73 |
+
self,
|
| 74 |
+
image_or_video_shape,
|
| 75 |
+
conditional_dict: dict,
|
| 76 |
+
unconditional_dict: dict,
|
| 77 |
+
clean_latent: torch.Tensor,
|
| 78 |
+
initial_latent: torch.Tensor = None
|
| 79 |
+
) -> Tuple[torch.Tensor, dict]:
|
| 80 |
+
"""
|
| 81 |
+
Generate image/videos from noise and compute the DMD loss.
|
| 82 |
+
The noisy input to the generator is backward simulated.
|
| 83 |
+
This removes the need of any datasets during distillation.
|
| 84 |
+
See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
|
| 85 |
+
Input:
|
| 86 |
+
- image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
|
| 87 |
+
- conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
|
| 88 |
+
- unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
|
| 89 |
+
- clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
|
| 90 |
+
Output:
|
| 91 |
+
- loss: a scalar tensor representing the generator loss.
|
| 92 |
+
- generator_log_dict: a dictionary containing the intermediate tensors for logging.
|
| 93 |
+
"""
|
| 94 |
+
noise = torch.randn_like(clean_latent)
|
| 95 |
+
batch_size, num_frame = image_or_video_shape[:2]
|
| 96 |
+
|
| 97 |
+
# Step 2: Randomly sample a timestep and add noise to denoiser inputs
|
| 98 |
+
index = self._get_timestep(
|
| 99 |
+
0,
|
| 100 |
+
self.scheduler.num_train_timesteps,
|
| 101 |
+
image_or_video_shape[0],
|
| 102 |
+
image_or_video_shape[1],
|
| 103 |
+
self.num_frame_per_block,
|
| 104 |
+
uniform_timestep=False
|
| 105 |
+
)
|
| 106 |
+
timestep = self.scheduler.timesteps[index].to(dtype=self.dtype, device=self.device)
|
| 107 |
+
noisy_latents = self.scheduler.add_noise(
|
| 108 |
+
clean_latent.flatten(0, 1),
|
| 109 |
+
noise.flatten(0, 1),
|
| 110 |
+
timestep.flatten(0, 1)
|
| 111 |
+
).unflatten(0, (batch_size, num_frame))
|
| 112 |
+
training_target = self.scheduler.training_target(clean_latent, noise, timestep)
|
| 113 |
+
|
| 114 |
+
# Step 3: Noise augmentation, also add small noise to clean context latents
|
| 115 |
+
if self.noise_augmentation_max_timestep > 0:
|
| 116 |
+
index_clean_aug = self._get_timestep(
|
| 117 |
+
0,
|
| 118 |
+
self.noise_augmentation_max_timestep,
|
| 119 |
+
image_or_video_shape[0],
|
| 120 |
+
image_or_video_shape[1],
|
| 121 |
+
self.num_frame_per_block,
|
| 122 |
+
uniform_timestep=False
|
| 123 |
+
)
|
| 124 |
+
timestep_clean_aug = self.scheduler.timesteps[index_clean_aug].to(dtype=self.dtype, device=self.device)
|
| 125 |
+
clean_latent_aug = self.scheduler.add_noise(
|
| 126 |
+
clean_latent.flatten(0, 1),
|
| 127 |
+
noise.flatten(0, 1),
|
| 128 |
+
timestep_clean_aug.flatten(0, 1)
|
| 129 |
+
).unflatten(0, (batch_size, num_frame))
|
| 130 |
+
else:
|
| 131 |
+
clean_latent_aug = clean_latent
|
| 132 |
+
timestep_clean_aug = None
|
| 133 |
+
|
| 134 |
+
# Compute loss
|
| 135 |
+
flow_pred, x0_pred = self.generator(
|
| 136 |
+
noisy_image_or_video=noisy_latents,
|
| 137 |
+
conditional_dict=conditional_dict,
|
| 138 |
+
timestep=timestep,
|
| 139 |
+
clean_x=clean_latent_aug if self.teacher_forcing else None,
|
| 140 |
+
aug_t=timestep_clean_aug if self.teacher_forcing else None
|
| 141 |
+
)
|
| 142 |
+
# loss = torch.nn.functional.mse_loss(flow_pred.float(), training_target.float())
|
| 143 |
+
loss_unweighted = torch.nn.functional.mse_loss(
|
| 144 |
+
flow_pred.float(), training_target.float(), reduction='none'
|
| 145 |
+
).mean(dim=(2, 3, 4)) # (B, F) unweighted per-frame loss
|
| 146 |
+
weight = self.scheduler.training_weight(timestep).unflatten(0, (batch_size, num_frame))
|
| 147 |
+
loss = (loss_unweighted * weight).mean() # scalar; used for backprop
|
| 148 |
+
|
| 149 |
+
log_dict = {
|
| 150 |
+
"x0": clean_latent.detach(),
|
| 151 |
+
"x0_pred": x0_pred.detach(),
|
| 152 |
+
"loss_per_frame": loss_unweighted.detach(), # (B, F) unweighted
|
| 153 |
+
"loss_unweighted": loss_unweighted.detach().mean(), # scalar unweighted
|
| 154 |
+
}
|
| 155 |
+
return loss, log_dict
|
DecMem/model/gan.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
from pipeline import SelfForcingTrainingPipeline
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from typing import Tuple
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from model.base import SelfForcingModel
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class GAN(SelfForcingModel):
|
| 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: SelfForcingTrainingPipeline = 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
|
DecMem/pipeline/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .bidirectional_diffusion_inference import BidirectionalDiffusionInferencePipeline
|
| 2 |
+
from .bidirectional_inference import BidirectionalInferencePipeline
|
| 3 |
+
from .causal_diffusion_inference import (
|
| 4 |
+
CausalDiffusionInferencePipeline,
|
| 5 |
+
WorldMemCausalDiffusionInferencePipeline,
|
| 6 |
+
)
|
| 7 |
+
from .causal_inference import CausalInferencePipeline
|
| 8 |
+
from .self_forcing_training import SelfForcingTrainingPipeline
|
| 9 |
+
|
| 10 |
+
__all__ = [
|
| 11 |
+
"BidirectionalDiffusionInferencePipeline",
|
| 12 |
+
"BidirectionalInferencePipeline",
|
| 13 |
+
"CausalDiffusionInferencePipeline",
|
| 14 |
+
"CausalInferencePipeline",
|
| 15 |
+
"SelfForcingTrainingPipeline",
|
| 16 |
+
"WorldMemCausalDiffusionInferencePipeline",
|
| 17 |
+
]
|
DecMem/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
|
DecMem/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
|
DecMem/pipeline/causal_diffusion_inference.py
ADDED
|
@@ -0,0 +1,707 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
class WorldMemCausalDiffusionInferencePipeline(torch.nn.Module):
|
| 9 |
+
"""Block-by-block causal inference for WorldMem (action + PRoPE, no text).
|
| 10 |
+
|
| 11 |
+
Key differences vs. CausalDiffusionInferencePipeline:
|
| 12 |
+
* No text encoder; conditioning comes from per-frame actions + viewmats.
|
| 13 |
+
* CFG is batched into a single KV cache with batch_size = 2 * B; cond
|
| 14 |
+
and uncond are stacked along dim 0 and chunked back after each forward.
|
| 15 |
+
* PRoPE is precomputed PER BLOCK (with the block's viewmats slice and
|
| 16 |
+
absolute `time_ids=blk_start`). The cache stores PRoPE-rotated K/V,
|
| 17 |
+
so subsequent blocks attend against features in their correct
|
| 18 |
+
rotated frames (relative-position attention falls out naturally).
|
| 19 |
+
|
| 20 |
+
KV-cache schedule for one inference call:
|
| 21 |
+
1. PREFIX: forward each fully-clean block at timestep=0 to write its
|
| 22 |
+
PRoPE-rotated K/V to the cache.
|
| 23 |
+
2. DENOISE: for each remaining block, run `sampling_steps` of UniPC,
|
| 24 |
+
each step a forward with kv_cache + PRoPE for that block.
|
| 25 |
+
Frames already pinned clean (partial-prefix case) keep
|
| 26 |
+
timestep=0 within the block.
|
| 27 |
+
3. COMMIT: after each block converges, re-forward at timestep=0 with
|
| 28 |
+
the now-clean latents to overwrite that block's cache slot.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
def __init__(self, args, device, generator=None, vae=None):
|
| 32 |
+
super().__init__()
|
| 33 |
+
self.args = args
|
| 34 |
+
self.device = device
|
| 35 |
+
|
| 36 |
+
self.generator = WanDiffusionWrapper(
|
| 37 |
+
**getattr(args, "model_kwargs", {}), is_causal=True
|
| 38 |
+
) if generator is None else generator
|
| 39 |
+
self.vae = WanVAEWrapper() if vae is None else vae
|
| 40 |
+
|
| 41 |
+
self.num_train_timesteps = args.num_train_timestep
|
| 42 |
+
self.sampling_steps = 50
|
| 43 |
+
self.sample_solver = 'unipc'
|
| 44 |
+
self.shift = args.timestep_shift
|
| 45 |
+
|
| 46 |
+
self.num_transformer_blocks = len(self.generator.model.blocks)
|
| 47 |
+
# frame_seq_length = patches_y * patches_x for WAN patch_size=(1,2,2)
|
| 48 |
+
latent_h = args.image_or_video_shape[3]
|
| 49 |
+
latent_w = args.image_or_video_shape[4]
|
| 50 |
+
self.frame_seq_length = (latent_h // 2) * (latent_w // 2)
|
| 51 |
+
|
| 52 |
+
self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
|
| 53 |
+
if self.num_frame_per_block > 1:
|
| 54 |
+
self.generator.model.num_frame_per_block = self.num_frame_per_block
|
| 55 |
+
|
| 56 |
+
self.local_attn_size = self.generator.model.local_attn_size
|
| 57 |
+
|
| 58 |
+
# Setup PRoPE (mirrors CausalDiffusion._initialize_models).
|
| 59 |
+
model_kwargs = getattr(args, "model_kwargs", {})
|
| 60 |
+
if model_kwargs.get("use_action_encoder", False):
|
| 61 |
+
num_frames = args.image_or_video_shape[1]
|
| 62 |
+
patches_y = latent_h // 2
|
| 63 |
+
patches_x = latent_w // 2
|
| 64 |
+
self.generator.model.setup_prope(
|
| 65 |
+
patches_x=patches_x,
|
| 66 |
+
patches_y=patches_y,
|
| 67 |
+
image_width=latent_w,
|
| 68 |
+
image_height=latent_h,
|
| 69 |
+
cameras=num_frames,
|
| 70 |
+
prope_max_time=getattr(args, "prope_max_time", 60),
|
| 71 |
+
rope_scale_pattern=getattr(args, "rope_scale_pattern", "rope"),
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
self.kv_cache = None
|
| 75 |
+
self.crossattn_cache = None
|
| 76 |
+
|
| 77 |
+
# ─── Cache lifecycle ─────────────────────────────────────────────────────
|
| 78 |
+
def _initialize_kv_cache(self, batch_size, dtype, device, num_total_frames):
|
| 79 |
+
self.kv_cache = None
|
| 80 |
+
torch.cuda.empty_cache()
|
| 81 |
+
kv_cache = []
|
| 82 |
+
if self.local_attn_size != -1:
|
| 83 |
+
kv_cache_size = self.local_attn_size * self.frame_seq_length
|
| 84 |
+
else:
|
| 85 |
+
kv_cache_size = num_total_frames * self.frame_seq_length
|
| 86 |
+
|
| 87 |
+
# Extend max_attention_size so attention reads the full cache range.
|
| 88 |
+
if self.local_attn_size == -1:
|
| 89 |
+
for module in self.generator.model.modules():
|
| 90 |
+
if hasattr(module, 'max_attention_size'):
|
| 91 |
+
module.max_attention_size = kv_cache_size
|
| 92 |
+
|
| 93 |
+
head_dim = self.generator.model.dim // self.generator.model.num_heads
|
| 94 |
+
num_heads = self.generator.model.num_heads
|
| 95 |
+
use_ltm = getattr(self.generator.model, "use_ltm_attn", False)
|
| 96 |
+
for _ in range(self.num_transformer_blocks):
|
| 97 |
+
entry = {
|
| 98 |
+
"global_end_index": torch.tensor([0], dtype=torch.long, device=device),
|
| 99 |
+
"local_end_index": torch.tensor([0], dtype=torch.long, device=device),
|
| 100 |
+
}
|
| 101 |
+
if use_ltm:
|
| 102 |
+
# LTM: untiled k/v + incremental tiled/compressed KV (per block).
|
| 103 |
+
# entry["num_frames"] = 0
|
| 104 |
+
entry["k"] = None
|
| 105 |
+
entry["v"] = None
|
| 106 |
+
entry["k_tiled"] = None
|
| 107 |
+
entry["v_tiled"] = None
|
| 108 |
+
entry["k_compress"] = None
|
| 109 |
+
entry["v_compress"] = None
|
| 110 |
+
entry["variable_block_sizes"] = None
|
| 111 |
+
else:
|
| 112 |
+
entry["k"] = torch.zeros(
|
| 113 |
+
[batch_size, kv_cache_size, num_heads, head_dim], dtype=dtype, device=device)
|
| 114 |
+
entry["v"] = torch.zeros(
|
| 115 |
+
[batch_size, kv_cache_size, num_heads, head_dim], dtype=dtype, device=device)
|
| 116 |
+
kv_cache.append(entry)
|
| 117 |
+
self.kv_cache = kv_cache
|
| 118 |
+
|
| 119 |
+
def _initialize_crossattn_cache(self, batch_size, dtype, device):
|
| 120 |
+
# Cross-attention is disabled for WorldMem (no text); allocate stubs
|
| 121 |
+
# so the model's block forward can still index `crossattn_cache[i]`
|
| 122 |
+
# without surprises.
|
| 123 |
+
crossattn_cache = []
|
| 124 |
+
head_dim = self.generator.model.dim // self.generator.model.num_heads
|
| 125 |
+
num_heads = self.generator.model.num_heads
|
| 126 |
+
for _ in range(self.num_transformer_blocks):
|
| 127 |
+
crossattn_cache.append({
|
| 128 |
+
"k": torch.zeros([batch_size, 512, num_heads, head_dim],
|
| 129 |
+
dtype=dtype, device=device),
|
| 130 |
+
"v": torch.zeros([batch_size, 512, num_heads, head_dim],
|
| 131 |
+
dtype=dtype, device=device),
|
| 132 |
+
"is_init": False,
|
| 133 |
+
})
|
| 134 |
+
self.crossattn_cache = crossattn_cache
|
| 135 |
+
|
| 136 |
+
def _init_scheduler(self, device):
|
| 137 |
+
if self.sample_solver == 'unipc':
|
| 138 |
+
scheduler = FlowUniPCMultistepScheduler(
|
| 139 |
+
num_train_timesteps=self.num_train_timesteps,
|
| 140 |
+
shift=1, use_dynamic_shifting=False)
|
| 141 |
+
scheduler.set_timesteps(self.sampling_steps, device=device, shift=self.shift)
|
| 142 |
+
elif self.sample_solver == 'dpm++':
|
| 143 |
+
scheduler = FlowDPMSolverMultistepScheduler(
|
| 144 |
+
num_train_timesteps=self.num_train_timesteps,
|
| 145 |
+
shift=1, use_dynamic_shifting=False)
|
| 146 |
+
sigmas = get_sampling_sigmas(self.sampling_steps, self.shift)
|
| 147 |
+
_, _ = retrieve_timesteps(scheduler, device=device, sigmas=sigmas)
|
| 148 |
+
else:
|
| 149 |
+
raise NotImplementedError(self.sample_solver)
|
| 150 |
+
return scheduler
|
| 151 |
+
|
| 152 |
+
# ─── Inference ───────────────────────────────────────────────────────────
|
| 153 |
+
def _precompute_prope_for_block(self, viewmats_in, blk_start_frame, num_frame_per_block):
|
| 154 |
+
"""Call PRoPE precompute once for this block's viewmats slice.
|
| 155 |
+
|
| 156 |
+
Must be invoked before any `_forward_block(..., blk_start_frame)` call
|
| 157 |
+
that targets the same block. Reused across all denoising steps within
|
| 158 |
+
the block (viewmats don't change per step) and the clean-commit pass.
|
| 159 |
+
"""
|
| 160 |
+
viewmats_blk = viewmats_in[:, blk_start_frame:blk_start_frame + num_frame_per_block]
|
| 161 |
+
self.generator.model.precompute_prope(
|
| 162 |
+
viewmats_blk,
|
| 163 |
+
time_ids=blk_start_frame,
|
| 164 |
+
pre_rope=True,
|
| 165 |
+
)
|
| 166 |
+
return viewmats_blk
|
| 167 |
+
|
| 168 |
+
def _forward_block(self, blk_latents, blk_timestep, viewmats_blk,
|
| 169 |
+
full_action_emb, blk_start_frame, kv_batch, use_cfg,
|
| 170 |
+
write_cache=True):
|
| 171 |
+
"""Run a single forward for one block, assuming PRoPE has already been
|
| 172 |
+
precomputed for this block (via `_precompute_prope_for_block`).
|
| 173 |
+
|
| 174 |
+
blk_latents: (kv_batch, num_frame_per_block, C, H, W)
|
| 175 |
+
blk_timestep: (kv_batch, num_frame_per_block)
|
| 176 |
+
viewmats_blk: (kv_batch, num_frame_per_block, 4, 4)
|
| 177 |
+
full_action_emb: (kv_batch, T_total, C, 1, 1) or None
|
| 178 |
+
write_cache: if False, k/v are read-only (denoising steps).
|
| 179 |
+
True for prefix / clean commit forwards (t=0).
|
| 180 |
+
"""
|
| 181 |
+
num_frame_per_block = blk_latents.shape[1]
|
| 182 |
+
for cache in self.kv_cache:
|
| 183 |
+
cache["write_cache"] = write_cache
|
| 184 |
+
# Build a per-block conditional_dict: action_emb slice + block viewmats.
|
| 185 |
+
# (We omit "actions" so WanDiffusionWrapper takes the "action_emb" path.)
|
| 186 |
+
conditional_dict = {"viewmats": viewmats_blk}
|
| 187 |
+
if full_action_emb is not None:
|
| 188 |
+
conditional_dict["action_emb"] = full_action_emb[
|
| 189 |
+
:, blk_start_frame:blk_start_frame + num_frame_per_block]
|
| 190 |
+
|
| 191 |
+
flow_both, _ = self.generator(
|
| 192 |
+
noisy_image_or_video=blk_latents,
|
| 193 |
+
conditional_dict=conditional_dict,
|
| 194 |
+
timestep=blk_timestep,
|
| 195 |
+
kv_cache=self.kv_cache,
|
| 196 |
+
crossattn_cache=self.crossattn_cache,
|
| 197 |
+
current_start=blk_start_frame * self.frame_seq_length,
|
| 198 |
+
cache_start=blk_start_frame * self.frame_seq_length,
|
| 199 |
+
)
|
| 200 |
+
return flow_both
|
| 201 |
+
|
| 202 |
+
def inference(
|
| 203 |
+
self,
|
| 204 |
+
noise: torch.Tensor, # (B, T, C, H, W) pure noise
|
| 205 |
+
actions: torch.Tensor, # (B, T_vid, action_dim)
|
| 206 |
+
viewmats: torch.Tensor, # (B, T, 4, 4)
|
| 207 |
+
initial_latent: torch.Tensor, # (B, T, C, H, W) — clean latent for frames [:n_cond]
|
| 208 |
+
num_condition_frames: int = 1,
|
| 209 |
+
guidance_scale: float = 3.0,
|
| 210 |
+
sampling_steps: int = 50,
|
| 211 |
+
return_latents: bool = True,
|
| 212 |
+
skip_vae_decode: bool = False,
|
| 213 |
+
):
|
| 214 |
+
"""Block-by-block causal inference for WorldMem."""
|
| 215 |
+
if sampling_steps is not None:
|
| 216 |
+
self.sampling_steps = sampling_steps
|
| 217 |
+
|
| 218 |
+
B, T, C, H, W = noise.shape
|
| 219 |
+
device = noise.device
|
| 220 |
+
dtype = noise.dtype
|
| 221 |
+
num_frame_per_block = self.num_frame_per_block
|
| 222 |
+
n_cond = int(num_condition_frames)
|
| 223 |
+
assert 1 <= n_cond < T
|
| 224 |
+
use_cfg = guidance_scale > 1.0
|
| 225 |
+
kv_batch = 2 * B if use_cfg else B
|
| 226 |
+
|
| 227 |
+
# ─── Build cond / uncond (batched along dim 0) ───────────────────────
|
| 228 |
+
if use_cfg:
|
| 229 |
+
actions_in = torch.cat([actions, torch.zeros_like(actions)], dim=0)
|
| 230 |
+
viewmats_in = torch.cat([viewmats, viewmats], dim=0)
|
| 231 |
+
else:
|
| 232 |
+
actions_in = actions
|
| 233 |
+
viewmats_in = viewmats
|
| 234 |
+
|
| 235 |
+
# Pre-encode the full action embedding once (B, T, C, 1, 1). The block
|
| 236 |
+
# forwards pass a per-block slice via conditional_dict["action_emb"],
|
| 237 |
+
# bypassing the action encoder's "first-frame is special" packing rule
|
| 238 |
+
# which only holds when the slice starts at frame 0.
|
| 239 |
+
if getattr(self.generator, "use_action_encoder", False):
|
| 240 |
+
with torch.no_grad():
|
| 241 |
+
full_action_emb = self.generator.action_encoder(
|
| 242 |
+
actions_in.to(dtype)) # (kv_batch, T, C, 1, 1)
|
| 243 |
+
else:
|
| 244 |
+
full_action_emb = None
|
| 245 |
+
|
| 246 |
+
# ─── Allocate KV/crossattn caches sized for kv_batch ─────────────────
|
| 247 |
+
self._initialize_kv_cache(kv_batch, dtype=dtype, device=device, num_total_frames=T)
|
| 248 |
+
# self._initialize_crossattn_cache(kv_batch, dtype=dtype, device=device)
|
| 249 |
+
|
| 250 |
+
# ─── Output buffer (B-batch, not kv_batch) ───────────────────────────
|
| 251 |
+
output = torch.zeros_like(noise)
|
| 252 |
+
output[:, :n_cond] = initial_latent[:, :n_cond]
|
| 253 |
+
|
| 254 |
+
# ─── Block layout ────────────────────────────────────────────────────
|
| 255 |
+
# Fully-clean blocks at the front are committed once at t=0. The first
|
| 256 |
+
# block that contains any noisy frames enters the denoise loop; if
|
| 257 |
+
# n_cond is not block-aligned, that block's leading `partial_clean`
|
| 258 |
+
# frames stay pinned to t=0 while the rest are denoised.
|
| 259 |
+
cleaned_blocks = n_cond // num_frame_per_block
|
| 260 |
+
partial_clean = n_cond % num_frame_per_block
|
| 261 |
+
num_total_blocks = T // num_frame_per_block
|
| 262 |
+
assert T % num_frame_per_block == 0, f"T={T} must be a multiple of num_frame_per_block={num_frame_per_block}"
|
| 263 |
+
|
| 264 |
+
# Step 1 — commit fully-clean prefix blocks at t=0.
|
| 265 |
+
# PRoPE precomputed once per block (viewmats are block-local).
|
| 266 |
+
for blk_idx in range(cleaned_blocks):
|
| 267 |
+
st = blk_idx * num_frame_per_block
|
| 268 |
+
viewmats_blk = self._precompute_prope_for_block(
|
| 269 |
+
viewmats_in, st, num_frame_per_block)
|
| 270 |
+
blk_lat = initial_latent[:, st:st + num_frame_per_block]
|
| 271 |
+
if use_cfg:
|
| 272 |
+
blk_lat = torch.cat([blk_lat, blk_lat], dim=0)
|
| 273 |
+
t0 = torch.zeros(kv_batch, num_frame_per_block, dtype=torch.float32, device=device)
|
| 274 |
+
self._forward_block(blk_lat, t0, viewmats_blk,
|
| 275 |
+
full_action_emb, st, kv_batch, use_cfg)
|
| 276 |
+
|
| 277 |
+
# Step 2 — denoise remaining blocks one at a time, then commit each.
|
| 278 |
+
# PRoPE is precomputed ONCE per block at the top, reused across all
|
| 279 |
+
# `sampling_steps` denoising forwards AND the clean-commit pass.
|
| 280 |
+
first_noisy_block = cleaned_blocks
|
| 281 |
+
for blk_idx in tqdm(range(first_noisy_block, num_total_blocks), desc="blocks"):
|
| 282 |
+
st = blk_idx * num_frame_per_block
|
| 283 |
+
ed = st + num_frame_per_block
|
| 284 |
+
|
| 285 |
+
viewmats_blk = self._precompute_prope_for_block(
|
| 286 |
+
viewmats_in, st, num_frame_per_block)
|
| 287 |
+
|
| 288 |
+
# Initialise this block's latents.
|
| 289 |
+
blk_lat = noise[:, st:ed].clone()
|
| 290 |
+
local_partial_clean = partial_clean if blk_idx == cleaned_blocks else 0
|
| 291 |
+
if local_partial_clean > 0:
|
| 292 |
+
blk_lat[:, :local_partial_clean] = initial_latent[:, st:st + local_partial_clean]
|
| 293 |
+
|
| 294 |
+
sched = self._init_scheduler(device)
|
| 295 |
+
for step_idx, t in enumerate(sched.timesteps):
|
| 296 |
+
# Per-frame timestep within this block.
|
| 297 |
+
ts = torch.full((B, num_frame_per_block), float(t.item()),
|
| 298 |
+
dtype=torch.float32, device=device)
|
| 299 |
+
if local_partial_clean > 0:
|
| 300 |
+
ts[:, :local_partial_clean] = 0.0
|
| 301 |
+
|
| 302 |
+
if use_cfg:
|
| 303 |
+
blk_lat_in = torch.cat([blk_lat, blk_lat], dim=0)
|
| 304 |
+
ts_in = torch.cat([ts, ts], dim=0)
|
| 305 |
+
else:
|
| 306 |
+
blk_lat_in = blk_lat
|
| 307 |
+
ts_in = ts
|
| 308 |
+
|
| 309 |
+
flow_both = self._forward_block(
|
| 310 |
+
blk_lat_in, ts_in, viewmats_blk,
|
| 311 |
+
full_action_emb, st, kv_batch, use_cfg,
|
| 312 |
+
write_cache=False)
|
| 313 |
+
|
| 314 |
+
if use_cfg:
|
| 315 |
+
flow_cond, flow_uncond = flow_both.chunk(2, dim=0)
|
| 316 |
+
flow_pred = flow_uncond + guidance_scale * (flow_cond - flow_uncond)
|
| 317 |
+
else:
|
| 318 |
+
flow_pred = flow_both
|
| 319 |
+
|
| 320 |
+
# Scheduler step on the noisy portion only.
|
| 321 |
+
noisy_flow = flow_pred[:, local_partial_clean:]
|
| 322 |
+
noisy_lat = blk_lat[:, local_partial_clean:]
|
| 323 |
+
new_noisy = sched.step(noisy_flow, t, noisy_lat, return_dict=False)[0]
|
| 324 |
+
blk_lat = blk_lat.clone()
|
| 325 |
+
blk_lat[:, local_partial_clean:] = new_noisy
|
| 326 |
+
|
| 327 |
+
output[:, st:ed] = blk_lat
|
| 328 |
+
|
| 329 |
+
# Commit clean K/V for this block to cache (overwrites the last
|
| 330 |
+
# denoise step's writes). Reuses the PRoPE precompute from above.
|
| 331 |
+
blk_lat_clean = blk_lat
|
| 332 |
+
if use_cfg:
|
| 333 |
+
blk_lat_clean = torch.cat([blk_lat_clean, blk_lat_clean], dim=0)
|
| 334 |
+
t0 = torch.zeros(kv_batch, num_frame_per_block, dtype=torch.float32, device=device)
|
| 335 |
+
self._forward_block(blk_lat_clean, t0, viewmats_blk,
|
| 336 |
+
full_action_emb, st, kv_batch, use_cfg)
|
| 337 |
+
|
| 338 |
+
# ─── Decode ──────────────────────────────────────────────────────────
|
| 339 |
+
# When `skip_vae_decode=True`, return only the latent so the caller can
|
| 340 |
+
# control where/when the VAE runs (used by the GPU/CPU offload path
|
| 341 |
+
# that swaps generator and VAE in/out of GPU memory).
|
| 342 |
+
if skip_vae_decode:
|
| 343 |
+
if return_latents:
|
| 344 |
+
return None, output
|
| 345 |
+
return None
|
| 346 |
+
|
| 347 |
+
video = self.vae.decode_to_pixel(output.to(dtype=dtype))
|
| 348 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 349 |
+
if return_latents:
|
| 350 |
+
return video, output
|
| 351 |
+
return video
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
class CausalDiffusionInferencePipeline(torch.nn.Module):
|
| 355 |
+
def __init__(
|
| 356 |
+
self,
|
| 357 |
+
args,
|
| 358 |
+
device,
|
| 359 |
+
generator=None,
|
| 360 |
+
text_encoder=None,
|
| 361 |
+
vae=None
|
| 362 |
+
):
|
| 363 |
+
super().__init__()
|
| 364 |
+
# Step 1: Initialize all models
|
| 365 |
+
self.generator = WanDiffusionWrapper(
|
| 366 |
+
**getattr(args, "model_kwargs", {}), is_causal=True) if generator is None else generator
|
| 367 |
+
self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
|
| 368 |
+
self.vae = WanVAEWrapper() if vae is None else vae
|
| 369 |
+
|
| 370 |
+
# Step 2: Initialize scheduler
|
| 371 |
+
self.num_train_timesteps = args.num_train_timestep
|
| 372 |
+
self.sampling_steps = 50
|
| 373 |
+
self.sample_solver = 'unipc'
|
| 374 |
+
self.shift = args.timestep_shift
|
| 375 |
+
|
| 376 |
+
self.num_transformer_blocks = 30
|
| 377 |
+
self.frame_seq_length = 1560
|
| 378 |
+
|
| 379 |
+
self.kv_cache_pos = None
|
| 380 |
+
self.kv_cache_neg = None
|
| 381 |
+
self.crossattn_cache_pos = None
|
| 382 |
+
self.crossattn_cache_neg = None
|
| 383 |
+
self.args = args
|
| 384 |
+
self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
|
| 385 |
+
self.independent_first_frame = args.independent_first_frame
|
| 386 |
+
self.local_attn_size = self.generator.model.local_attn_size
|
| 387 |
+
|
| 388 |
+
print(f"KV inference with {self.num_frame_per_block} frames per block")
|
| 389 |
+
|
| 390 |
+
if self.num_frame_per_block > 1:
|
| 391 |
+
self.generator.model.num_frame_per_block = self.num_frame_per_block
|
| 392 |
+
|
| 393 |
+
def inference(
|
| 394 |
+
self,
|
| 395 |
+
noise: torch.Tensor,
|
| 396 |
+
text_prompts: List[str],
|
| 397 |
+
initial_latent: Optional[torch.Tensor] = None,
|
| 398 |
+
return_latents: bool = False,
|
| 399 |
+
start_frame_index: Optional[int] = 0,
|
| 400 |
+
) -> torch.Tensor:
|
| 401 |
+
"""
|
| 402 |
+
Perform inference on the given noise and text prompts.
|
| 403 |
+
Inputs:
|
| 404 |
+
noise (torch.Tensor): The input noise tensor of shape
|
| 405 |
+
(batch_size, num_output_frames, num_channels, height, width).
|
| 406 |
+
text_prompts (List[str]): The list of text prompts.
|
| 407 |
+
initial_latent (torch.Tensor): The initial latent tensor of shape
|
| 408 |
+
(batch_size, num_input_frames, num_channels, height, width).
|
| 409 |
+
If num_input_frames is 1, perform image to video.
|
| 410 |
+
If num_input_frames is greater than 1, perform video extension.
|
| 411 |
+
return_latents (bool): Whether to return the latents.
|
| 412 |
+
start_frame_index (int): In long video generation, where does the current window start?
|
| 413 |
+
Outputs:
|
| 414 |
+
video (torch.Tensor): The generated video tensor of shape
|
| 415 |
+
(batch_size, num_frames, num_channels, height, width). It is normalized to be in the range [0, 1].
|
| 416 |
+
"""
|
| 417 |
+
batch_size, num_frames, num_channels, height, width = noise.shape
|
| 418 |
+
if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
|
| 419 |
+
# If the first frame is independent and the first frame is provided, then the number of frames in the
|
| 420 |
+
# noise should still be a multiple of num_frame_per_block
|
| 421 |
+
assert num_frames % self.num_frame_per_block == 0
|
| 422 |
+
num_blocks = num_frames // self.num_frame_per_block
|
| 423 |
+
elif self.independent_first_frame and initial_latent is None:
|
| 424 |
+
# Using a [1, 4, 4, 4, 4, 4] model to generate a video without image conditioning
|
| 425 |
+
assert (num_frames - 1) % self.num_frame_per_block == 0
|
| 426 |
+
num_blocks = (num_frames - 1) // self.num_frame_per_block
|
| 427 |
+
num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
|
| 428 |
+
num_output_frames = num_frames + num_input_frames # add the initial latent frames
|
| 429 |
+
conditional_dict = self.text_encoder(
|
| 430 |
+
text_prompts=text_prompts
|
| 431 |
+
)
|
| 432 |
+
unconditional_dict = self.text_encoder(
|
| 433 |
+
text_prompts=[self.args.negative_prompt] * len(text_prompts)
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
output = torch.zeros(
|
| 437 |
+
[batch_size, num_output_frames, num_channels, height, width],
|
| 438 |
+
device=noise.device,
|
| 439 |
+
dtype=noise.dtype
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
# Step 1: Initialize KV cache to all zeros
|
| 443 |
+
required_cache_size = (
|
| 444 |
+
num_output_frames * self.frame_seq_length
|
| 445 |
+
if self.local_attn_size == -1
|
| 446 |
+
else self.local_attn_size * self.frame_seq_length
|
| 447 |
+
)
|
| 448 |
+
need_reinit = (
|
| 449 |
+
self.kv_cache_pos is None
|
| 450 |
+
or self.kv_cache_pos[0]["k"].shape[1] < required_cache_size
|
| 451 |
+
)
|
| 452 |
+
if need_reinit:
|
| 453 |
+
self._initialize_kv_cache(
|
| 454 |
+
batch_size=batch_size,
|
| 455 |
+
dtype=noise.dtype,
|
| 456 |
+
device=noise.device,
|
| 457 |
+
num_total_frames=num_output_frames
|
| 458 |
+
)
|
| 459 |
+
self._initialize_crossattn_cache(
|
| 460 |
+
batch_size=batch_size,
|
| 461 |
+
dtype=noise.dtype,
|
| 462 |
+
device=noise.device
|
| 463 |
+
)
|
| 464 |
+
else:
|
| 465 |
+
# reset cross attn cache
|
| 466 |
+
for block_index in range(self.num_transformer_blocks):
|
| 467 |
+
self.crossattn_cache_pos[block_index]["is_init"] = False
|
| 468 |
+
self.crossattn_cache_neg[block_index]["is_init"] = False
|
| 469 |
+
# reset kv cache
|
| 470 |
+
for block_index in range(len(self.kv_cache_pos)):
|
| 471 |
+
self.kv_cache_pos[block_index]["global_end_index"] = torch.tensor(
|
| 472 |
+
[0], dtype=torch.long, device=noise.device)
|
| 473 |
+
self.kv_cache_pos[block_index]["local_end_index"] = torch.tensor(
|
| 474 |
+
[0], dtype=torch.long, device=noise.device)
|
| 475 |
+
self.kv_cache_neg[block_index]["global_end_index"] = torch.tensor(
|
| 476 |
+
[0], dtype=torch.long, device=noise.device)
|
| 477 |
+
self.kv_cache_neg[block_index]["local_end_index"] = torch.tensor(
|
| 478 |
+
[0], dtype=torch.long, device=noise.device)
|
| 479 |
+
|
| 480 |
+
# Step 2: Cache context feature
|
| 481 |
+
current_start_frame = start_frame_index
|
| 482 |
+
cache_start_frame = 0
|
| 483 |
+
if initial_latent is not None:
|
| 484 |
+
timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
|
| 485 |
+
if self.independent_first_frame:
|
| 486 |
+
# Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
|
| 487 |
+
assert (num_input_frames - 1) % self.num_frame_per_block == 0
|
| 488 |
+
num_input_blocks = (num_input_frames - 1) // self.num_frame_per_block
|
| 489 |
+
output[:, :1] = initial_latent[:, :1]
|
| 490 |
+
self.generator(
|
| 491 |
+
noisy_image_or_video=initial_latent[:, :1],
|
| 492 |
+
conditional_dict=conditional_dict,
|
| 493 |
+
timestep=timestep * 0,
|
| 494 |
+
kv_cache=self.kv_cache_pos,
|
| 495 |
+
crossattn_cache=self.crossattn_cache_pos,
|
| 496 |
+
current_start=current_start_frame * self.frame_seq_length,
|
| 497 |
+
cache_start=cache_start_frame * self.frame_seq_length
|
| 498 |
+
)
|
| 499 |
+
self.generator(
|
| 500 |
+
noisy_image_or_video=initial_latent[:, :1],
|
| 501 |
+
conditional_dict=unconditional_dict,
|
| 502 |
+
timestep=timestep * 0,
|
| 503 |
+
kv_cache=self.kv_cache_neg,
|
| 504 |
+
crossattn_cache=self.crossattn_cache_neg,
|
| 505 |
+
current_start=current_start_frame * self.frame_seq_length,
|
| 506 |
+
cache_start=cache_start_frame * self.frame_seq_length
|
| 507 |
+
)
|
| 508 |
+
current_start_frame += 1
|
| 509 |
+
cache_start_frame += 1
|
| 510 |
+
else:
|
| 511 |
+
# Assume num_input_frames is self.num_frame_per_block * num_input_blocks
|
| 512 |
+
assert num_input_frames % self.num_frame_per_block == 0
|
| 513 |
+
num_input_blocks = num_input_frames // self.num_frame_per_block
|
| 514 |
+
|
| 515 |
+
for block_index in range(num_input_blocks):
|
| 516 |
+
current_ref_latents = \
|
| 517 |
+
initial_latent[:, cache_start_frame:cache_start_frame + self.num_frame_per_block]
|
| 518 |
+
output[:, cache_start_frame:cache_start_frame + self.num_frame_per_block] = current_ref_latents
|
| 519 |
+
self.generator(
|
| 520 |
+
noisy_image_or_video=current_ref_latents,
|
| 521 |
+
conditional_dict=conditional_dict,
|
| 522 |
+
timestep=timestep * 0,
|
| 523 |
+
kv_cache=self.kv_cache_pos,
|
| 524 |
+
crossattn_cache=self.crossattn_cache_pos,
|
| 525 |
+
current_start=current_start_frame * self.frame_seq_length,
|
| 526 |
+
cache_start=cache_start_frame * self.frame_seq_length
|
| 527 |
+
)
|
| 528 |
+
self.generator(
|
| 529 |
+
noisy_image_or_video=current_ref_latents,
|
| 530 |
+
conditional_dict=unconditional_dict,
|
| 531 |
+
timestep=timestep * 0,
|
| 532 |
+
kv_cache=self.kv_cache_neg,
|
| 533 |
+
crossattn_cache=self.crossattn_cache_neg,
|
| 534 |
+
current_start=current_start_frame * self.frame_seq_length,
|
| 535 |
+
cache_start=cache_start_frame * self.frame_seq_length
|
| 536 |
+
)
|
| 537 |
+
current_start_frame += self.num_frame_per_block
|
| 538 |
+
cache_start_frame += self.num_frame_per_block
|
| 539 |
+
|
| 540 |
+
# Step 3: Temporal denoising loop
|
| 541 |
+
all_num_frames = [self.num_frame_per_block] * num_blocks
|
| 542 |
+
if self.independent_first_frame and initial_latent is None:
|
| 543 |
+
all_num_frames = [1] + all_num_frames
|
| 544 |
+
|
| 545 |
+
for current_num_frames in all_num_frames:
|
| 546 |
+
noisy_input = noise[
|
| 547 |
+
:, cache_start_frame - num_input_frames:cache_start_frame + current_num_frames - num_input_frames]
|
| 548 |
+
latents = noisy_input
|
| 549 |
+
|
| 550 |
+
# Step 3.1: Spatial denoising loop
|
| 551 |
+
sample_scheduler = self._initialize_sample_scheduler(noise)
|
| 552 |
+
for step_idx, t in enumerate(tqdm(sample_scheduler.timesteps)):
|
| 553 |
+
latent_model_input = latents
|
| 554 |
+
timestep = t * torch.ones(
|
| 555 |
+
[batch_size, current_num_frames], device=noise.device, dtype=torch.float32
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
flow_pred_cond, _ = self.generator(
|
| 559 |
+
noisy_image_or_video=latent_model_input,
|
| 560 |
+
conditional_dict=conditional_dict,
|
| 561 |
+
timestep=timestep,
|
| 562 |
+
kv_cache=self.kv_cache_pos,
|
| 563 |
+
crossattn_cache=self.crossattn_cache_pos,
|
| 564 |
+
current_start=current_start_frame * self.frame_seq_length,
|
| 565 |
+
cache_start=cache_start_frame * self.frame_seq_length
|
| 566 |
+
)
|
| 567 |
+
|
| 568 |
+
flow_pred_uncond, _ = self.generator(
|
| 569 |
+
noisy_image_or_video=latent_model_input,
|
| 570 |
+
conditional_dict=unconditional_dict,
|
| 571 |
+
timestep=timestep,
|
| 572 |
+
kv_cache=self.kv_cache_neg,
|
| 573 |
+
crossattn_cache=self.crossattn_cache_neg,
|
| 574 |
+
current_start=current_start_frame * self.frame_seq_length,
|
| 575 |
+
cache_start=cache_start_frame * self.frame_seq_length
|
| 576 |
+
)
|
| 577 |
+
|
| 578 |
+
flow_pred = flow_pred_uncond + self.args.guidance_scale * (
|
| 579 |
+
flow_pred_cond - flow_pred_uncond)
|
| 580 |
+
|
| 581 |
+
temp_x0 = sample_scheduler.step(
|
| 582 |
+
flow_pred,
|
| 583 |
+
t,
|
| 584 |
+
latents,
|
| 585 |
+
return_dict=False)[0]
|
| 586 |
+
latents = temp_x0
|
| 587 |
+
print(f"kv_cache['local_end_index']: {self.kv_cache_pos[0]['local_end_index']}")
|
| 588 |
+
print(f"kv_cache['global_end_index']: {self.kv_cache_pos[0]['global_end_index']}")
|
| 589 |
+
|
| 590 |
+
# Step 3.2: record the model's output
|
| 591 |
+
output[:, cache_start_frame:cache_start_frame + current_num_frames] = latents
|
| 592 |
+
|
| 593 |
+
# Step 3.3: rerun with timestep zero to update KV cache using clean context
|
| 594 |
+
self.generator(
|
| 595 |
+
noisy_image_or_video=latents,
|
| 596 |
+
conditional_dict=conditional_dict,
|
| 597 |
+
timestep=timestep * 0,
|
| 598 |
+
kv_cache=self.kv_cache_pos,
|
| 599 |
+
crossattn_cache=self.crossattn_cache_pos,
|
| 600 |
+
current_start=current_start_frame * self.frame_seq_length,
|
| 601 |
+
cache_start=cache_start_frame * self.frame_seq_length
|
| 602 |
+
)
|
| 603 |
+
self.generator(
|
| 604 |
+
noisy_image_or_video=latents,
|
| 605 |
+
conditional_dict=unconditional_dict,
|
| 606 |
+
timestep=timestep * 0,
|
| 607 |
+
kv_cache=self.kv_cache_neg,
|
| 608 |
+
crossattn_cache=self.crossattn_cache_neg,
|
| 609 |
+
current_start=current_start_frame * self.frame_seq_length,
|
| 610 |
+
cache_start=cache_start_frame * self.frame_seq_length
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
# Step 3.4: update the start and end frame indices
|
| 614 |
+
current_start_frame += current_num_frames
|
| 615 |
+
cache_start_frame += current_num_frames
|
| 616 |
+
|
| 617 |
+
# Step 4: Decode the output
|
| 618 |
+
video = self.vae.decode_to_pixel(output)
|
| 619 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 620 |
+
|
| 621 |
+
if return_latents:
|
| 622 |
+
return video, output
|
| 623 |
+
else:
|
| 624 |
+
return video
|
| 625 |
+
|
| 626 |
+
def _initialize_kv_cache(self, batch_size, dtype, device, num_total_frames=None):
|
| 627 |
+
"""
|
| 628 |
+
Initialize a Per-GPU KV cache for the Wan model.
|
| 629 |
+
"""
|
| 630 |
+
kv_cache_pos = []
|
| 631 |
+
kv_cache_neg = []
|
| 632 |
+
if self.local_attn_size != -1:
|
| 633 |
+
# Sliding window: cache size equals the window size
|
| 634 |
+
kv_cache_size = self.local_attn_size * self.frame_seq_length
|
| 635 |
+
elif num_total_frames is not None:
|
| 636 |
+
# Full history: allocate enough for every frame in this inference call
|
| 637 |
+
kv_cache_size = num_total_frames * self.frame_seq_length
|
| 638 |
+
else:
|
| 639 |
+
kv_cache_size = 32760
|
| 640 |
+
|
| 641 |
+
# For full-history mode, extend max_attention_size on every self-attention
|
| 642 |
+
# module so the attention op actually reads the full cache (default is 32760).
|
| 643 |
+
if self.local_attn_size == -1:
|
| 644 |
+
for module in self.generator.model.modules():
|
| 645 |
+
if hasattr(module, 'max_attention_size'):
|
| 646 |
+
module.max_attention_size = kv_cache_size
|
| 647 |
+
|
| 648 |
+
for _ in range(self.num_transformer_blocks):
|
| 649 |
+
kv_cache_pos.append({
|
| 650 |
+
"k": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
|
| 651 |
+
"v": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
|
| 652 |
+
"global_end_index": torch.tensor([0], dtype=torch.long, device=device),
|
| 653 |
+
"local_end_index": torch.tensor([0], dtype=torch.long, device=device)
|
| 654 |
+
})
|
| 655 |
+
kv_cache_neg.append({
|
| 656 |
+
"k": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
|
| 657 |
+
"v": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
|
| 658 |
+
"global_end_index": torch.tensor([0], dtype=torch.long, device=device),
|
| 659 |
+
"local_end_index": torch.tensor([0], dtype=torch.long, device=device)
|
| 660 |
+
})
|
| 661 |
+
|
| 662 |
+
self.kv_cache_pos = kv_cache_pos # always store the clean cache
|
| 663 |
+
self.kv_cache_neg = kv_cache_neg # always store the clean cache
|
| 664 |
+
|
| 665 |
+
def _initialize_crossattn_cache(self, batch_size, dtype, device):
|
| 666 |
+
"""
|
| 667 |
+
Initialize a Per-GPU cross-attention cache for the Wan model.
|
| 668 |
+
"""
|
| 669 |
+
crossattn_cache_pos = []
|
| 670 |
+
crossattn_cache_neg = []
|
| 671 |
+
for _ in range(self.num_transformer_blocks):
|
| 672 |
+
crossattn_cache_pos.append({
|
| 673 |
+
"k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
|
| 674 |
+
"v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
|
| 675 |
+
"is_init": False
|
| 676 |
+
})
|
| 677 |
+
crossattn_cache_neg.append({
|
| 678 |
+
"k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
|
| 679 |
+
"v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
|
| 680 |
+
"is_init": False
|
| 681 |
+
})
|
| 682 |
+
|
| 683 |
+
self.crossattn_cache_pos = crossattn_cache_pos # always store the clean cache
|
| 684 |
+
self.crossattn_cache_neg = crossattn_cache_neg # always store the clean cache
|
| 685 |
+
|
| 686 |
+
def _initialize_sample_scheduler(self, noise):
|
| 687 |
+
if self.sample_solver == 'unipc':
|
| 688 |
+
sample_scheduler = FlowUniPCMultistepScheduler(
|
| 689 |
+
num_train_timesteps=self.num_train_timesteps,
|
| 690 |
+
shift=1,
|
| 691 |
+
use_dynamic_shifting=False)
|
| 692 |
+
sample_scheduler.set_timesteps(
|
| 693 |
+
self.sampling_steps, device=noise.device, shift=self.shift)
|
| 694 |
+
self.timesteps = sample_scheduler.timesteps
|
| 695 |
+
elif self.sample_solver == 'dpm++':
|
| 696 |
+
sample_scheduler = FlowDPMSolverMultistepScheduler(
|
| 697 |
+
num_train_timesteps=self.num_train_timesteps,
|
| 698 |
+
shift=1,
|
| 699 |
+
use_dynamic_shifting=False)
|
| 700 |
+
sampling_sigmas = get_sampling_sigmas(self.sampling_steps, self.shift)
|
| 701 |
+
self.timesteps, _ = retrieve_timesteps(
|
| 702 |
+
sample_scheduler,
|
| 703 |
+
device=noise.device,
|
| 704 |
+
sigmas=sampling_sigmas)
|
| 705 |
+
else:
|
| 706 |
+
raise NotImplementedError("Unsupported solver.")
|
| 707 |
+
return sample_scheduler
|
DecMem/pipeline/causal_inference.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
|
| 5 |
+
from demo_utils.memory import gpu, get_cuda_free_memory_gb, DynamicSwapInstaller, move_model_to_device_with_memory_preservation
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class CausalInferencePipeline(torch.nn.Module):
|
| 9 |
+
def __init__(
|
| 10 |
+
self,
|
| 11 |
+
args,
|
| 12 |
+
device,
|
| 13 |
+
generator=None,
|
| 14 |
+
text_encoder=None,
|
| 15 |
+
vae=None
|
| 16 |
+
):
|
| 17 |
+
super().__init__()
|
| 18 |
+
# Step 1: Initialize all models
|
| 19 |
+
self.generator = WanDiffusionWrapper(
|
| 20 |
+
**getattr(args, "model_kwargs", {}), is_causal=True) if generator is None else generator
|
| 21 |
+
self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
|
| 22 |
+
self.vae = WanVAEWrapper() if vae is None else vae
|
| 23 |
+
|
| 24 |
+
# Step 2: Initialize all causal hyperparmeters
|
| 25 |
+
self.scheduler = self.generator.get_scheduler()
|
| 26 |
+
self.denoising_step_list = torch.tensor(
|
| 27 |
+
args.denoising_step_list, dtype=torch.long)
|
| 28 |
+
if args.warp_denoising_step:
|
| 29 |
+
timesteps = torch.cat((self.scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32)))
|
| 30 |
+
self.denoising_step_list = timesteps[1000 - self.denoising_step_list]
|
| 31 |
+
|
| 32 |
+
self.num_transformer_blocks = 30
|
| 33 |
+
self.frame_seq_length = 1560
|
| 34 |
+
|
| 35 |
+
self.kv_cache1 = None
|
| 36 |
+
self.args = args
|
| 37 |
+
self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
|
| 38 |
+
self.independent_first_frame = args.independent_first_frame
|
| 39 |
+
self.local_attn_size = self.generator.model.local_attn_size
|
| 40 |
+
|
| 41 |
+
print(f"KV inference with {self.num_frame_per_block} frames per block")
|
| 42 |
+
|
| 43 |
+
if self.num_frame_per_block > 1:
|
| 44 |
+
self.generator.model.num_frame_per_block = self.num_frame_per_block
|
| 45 |
+
|
| 46 |
+
def inference(
|
| 47 |
+
self,
|
| 48 |
+
noise: torch.Tensor,
|
| 49 |
+
text_prompts: List[str],
|
| 50 |
+
initial_latent: Optional[torch.Tensor] = None,
|
| 51 |
+
return_latents: bool = False,
|
| 52 |
+
profile: bool = False,
|
| 53 |
+
low_memory: bool = False,
|
| 54 |
+
) -> torch.Tensor:
|
| 55 |
+
"""
|
| 56 |
+
Perform inference on the given noise and text prompts.
|
| 57 |
+
Inputs:
|
| 58 |
+
noise (torch.Tensor): The input noise tensor of shape
|
| 59 |
+
(batch_size, num_output_frames, num_channels, height, width).
|
| 60 |
+
text_prompts (List[str]): The list of text prompts.
|
| 61 |
+
initial_latent (torch.Tensor): The initial latent tensor of shape
|
| 62 |
+
(batch_size, num_input_frames, num_channels, height, width).
|
| 63 |
+
If num_input_frames is 1, perform image to video.
|
| 64 |
+
If num_input_frames is greater than 1, perform video extension.
|
| 65 |
+
return_latents (bool): Whether to return the latents.
|
| 66 |
+
Outputs:
|
| 67 |
+
video (torch.Tensor): The generated video tensor of shape
|
| 68 |
+
(batch_size, num_output_frames, num_channels, height, width).
|
| 69 |
+
It is normalized to be in the range [0, 1].
|
| 70 |
+
"""
|
| 71 |
+
batch_size, num_frames, num_channels, height, width = noise.shape
|
| 72 |
+
if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
|
| 73 |
+
# If the first frame is independent and the first frame is provided, then the number of frames in the
|
| 74 |
+
# noise should still be a multiple of num_frame_per_block
|
| 75 |
+
assert num_frames % self.num_frame_per_block == 0
|
| 76 |
+
num_blocks = num_frames // self.num_frame_per_block
|
| 77 |
+
else:
|
| 78 |
+
# Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
|
| 79 |
+
assert (num_frames - 1) % self.num_frame_per_block == 0
|
| 80 |
+
num_blocks = (num_frames - 1) // self.num_frame_per_block
|
| 81 |
+
num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
|
| 82 |
+
num_output_frames = num_frames + num_input_frames # add the initial latent frames
|
| 83 |
+
conditional_dict = self.text_encoder(
|
| 84 |
+
text_prompts=text_prompts
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
if low_memory:
|
| 88 |
+
gpu_memory_preservation = get_cuda_free_memory_gb(gpu) + 5
|
| 89 |
+
move_model_to_device_with_memory_preservation(self.text_encoder, target_device=gpu, preserved_memory_gb=gpu_memory_preservation)
|
| 90 |
+
|
| 91 |
+
output = torch.zeros(
|
| 92 |
+
[batch_size, num_output_frames, num_channels, height, width],
|
| 93 |
+
device=noise.device,
|
| 94 |
+
dtype=noise.dtype
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
# Set up profiling if requested
|
| 98 |
+
if profile:
|
| 99 |
+
init_start = torch.cuda.Event(enable_timing=True)
|
| 100 |
+
init_end = torch.cuda.Event(enable_timing=True)
|
| 101 |
+
diffusion_start = torch.cuda.Event(enable_timing=True)
|
| 102 |
+
diffusion_end = torch.cuda.Event(enable_timing=True)
|
| 103 |
+
vae_start = torch.cuda.Event(enable_timing=True)
|
| 104 |
+
vae_end = torch.cuda.Event(enable_timing=True)
|
| 105 |
+
block_times = []
|
| 106 |
+
block_start = torch.cuda.Event(enable_timing=True)
|
| 107 |
+
block_end = torch.cuda.Event(enable_timing=True)
|
| 108 |
+
init_start.record()
|
| 109 |
+
|
| 110 |
+
# Step 1: Initialize KV cache to all zeros
|
| 111 |
+
required_cache_size = (
|
| 112 |
+
num_output_frames * self.frame_seq_length
|
| 113 |
+
if self.local_attn_size == -1
|
| 114 |
+
else self.local_attn_size * self.frame_seq_length
|
| 115 |
+
)
|
| 116 |
+
need_reinit = (
|
| 117 |
+
self.kv_cache1 is None
|
| 118 |
+
or self.kv_cache1[0]["k"].shape[1] < required_cache_size
|
| 119 |
+
)
|
| 120 |
+
if need_reinit:
|
| 121 |
+
self._initialize_kv_cache(
|
| 122 |
+
batch_size=batch_size,
|
| 123 |
+
dtype=noise.dtype,
|
| 124 |
+
device=noise.device,
|
| 125 |
+
num_total_frames=num_output_frames
|
| 126 |
+
)
|
| 127 |
+
self._initialize_crossattn_cache(
|
| 128 |
+
batch_size=batch_size,
|
| 129 |
+
dtype=noise.dtype,
|
| 130 |
+
device=noise.device
|
| 131 |
+
)
|
| 132 |
+
else:
|
| 133 |
+
# reset cross attn cache
|
| 134 |
+
for block_index in range(self.num_transformer_blocks):
|
| 135 |
+
self.crossattn_cache[block_index]["is_init"] = False
|
| 136 |
+
# reset kv cache
|
| 137 |
+
for block_index in range(len(self.kv_cache1)):
|
| 138 |
+
self.kv_cache1[block_index]["global_end_index"] = torch.tensor(
|
| 139 |
+
[0], dtype=torch.long, device=noise.device)
|
| 140 |
+
self.kv_cache1[block_index]["local_end_index"] = torch.tensor(
|
| 141 |
+
[0], dtype=torch.long, device=noise.device)
|
| 142 |
+
|
| 143 |
+
# Step 2: Cache context feature
|
| 144 |
+
current_start_frame = 0
|
| 145 |
+
if initial_latent is not None:
|
| 146 |
+
timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
|
| 147 |
+
if self.independent_first_frame:
|
| 148 |
+
# Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
|
| 149 |
+
assert (num_input_frames - 1) % self.num_frame_per_block == 0
|
| 150 |
+
num_input_blocks = (num_input_frames - 1) // self.num_frame_per_block
|
| 151 |
+
output[:, :1] = initial_latent[:, :1]
|
| 152 |
+
self.generator(
|
| 153 |
+
noisy_image_or_video=initial_latent[:, :1],
|
| 154 |
+
conditional_dict=conditional_dict,
|
| 155 |
+
timestep=timestep * 0,
|
| 156 |
+
kv_cache=self.kv_cache1,
|
| 157 |
+
crossattn_cache=self.crossattn_cache,
|
| 158 |
+
current_start=current_start_frame * self.frame_seq_length,
|
| 159 |
+
)
|
| 160 |
+
current_start_frame += 1
|
| 161 |
+
else:
|
| 162 |
+
# Assume num_input_frames is self.num_frame_per_block * num_input_blocks
|
| 163 |
+
assert num_input_frames % self.num_frame_per_block == 0
|
| 164 |
+
num_input_blocks = num_input_frames // self.num_frame_per_block
|
| 165 |
+
|
| 166 |
+
for _ in range(num_input_blocks):
|
| 167 |
+
current_ref_latents = \
|
| 168 |
+
initial_latent[:, current_start_frame:current_start_frame + self.num_frame_per_block]
|
| 169 |
+
output[:, current_start_frame:current_start_frame + self.num_frame_per_block] = current_ref_latents
|
| 170 |
+
self.generator(
|
| 171 |
+
noisy_image_or_video=current_ref_latents,
|
| 172 |
+
conditional_dict=conditional_dict,
|
| 173 |
+
timestep=timestep * 0,
|
| 174 |
+
kv_cache=self.kv_cache1,
|
| 175 |
+
crossattn_cache=self.crossattn_cache,
|
| 176 |
+
current_start=current_start_frame * self.frame_seq_length,
|
| 177 |
+
)
|
| 178 |
+
current_start_frame += self.num_frame_per_block
|
| 179 |
+
|
| 180 |
+
if profile:
|
| 181 |
+
init_end.record()
|
| 182 |
+
torch.cuda.synchronize()
|
| 183 |
+
diffusion_start.record()
|
| 184 |
+
|
| 185 |
+
# Step 3: Temporal denoising loop
|
| 186 |
+
all_num_frames = [self.num_frame_per_block] * num_blocks
|
| 187 |
+
if self.independent_first_frame and initial_latent is None:
|
| 188 |
+
all_num_frames = [1] + all_num_frames
|
| 189 |
+
|
| 190 |
+
for current_num_frames in all_num_frames:
|
| 191 |
+
if profile:
|
| 192 |
+
block_start.record()
|
| 193 |
+
|
| 194 |
+
noisy_input = noise[
|
| 195 |
+
:, current_start_frame - num_input_frames:current_start_frame + current_num_frames - num_input_frames]
|
| 196 |
+
|
| 197 |
+
# Step 3.1: Spatial denoising loop
|
| 198 |
+
for index, current_timestep in enumerate(self.denoising_step_list):
|
| 199 |
+
print(f"current_timestep: {current_timestep}")
|
| 200 |
+
# set current timestep
|
| 201 |
+
timestep = torch.ones(
|
| 202 |
+
[batch_size, current_num_frames],
|
| 203 |
+
device=noise.device,
|
| 204 |
+
dtype=torch.int64) * current_timestep
|
| 205 |
+
|
| 206 |
+
if index < len(self.denoising_step_list) - 1:
|
| 207 |
+
_, denoised_pred = self.generator(
|
| 208 |
+
noisy_image_or_video=noisy_input,
|
| 209 |
+
conditional_dict=conditional_dict,
|
| 210 |
+
timestep=timestep,
|
| 211 |
+
kv_cache=self.kv_cache1,
|
| 212 |
+
crossattn_cache=self.crossattn_cache,
|
| 213 |
+
current_start=current_start_frame * self.frame_seq_length
|
| 214 |
+
)
|
| 215 |
+
next_timestep = self.denoising_step_list[index + 1]
|
| 216 |
+
noisy_input = self.scheduler.add_noise(
|
| 217 |
+
denoised_pred.flatten(0, 1),
|
| 218 |
+
torch.randn_like(denoised_pred.flatten(0, 1)),
|
| 219 |
+
next_timestep * torch.ones(
|
| 220 |
+
[batch_size * current_num_frames], device=noise.device, dtype=torch.long)
|
| 221 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 222 |
+
else:
|
| 223 |
+
# for getting real output
|
| 224 |
+
_, denoised_pred = self.generator(
|
| 225 |
+
noisy_image_or_video=noisy_input,
|
| 226 |
+
conditional_dict=conditional_dict,
|
| 227 |
+
timestep=timestep,
|
| 228 |
+
kv_cache=self.kv_cache1,
|
| 229 |
+
crossattn_cache=self.crossattn_cache,
|
| 230 |
+
current_start=current_start_frame * self.frame_seq_length
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
# Step 3.2: record the model's output
|
| 234 |
+
output[:, current_start_frame:current_start_frame + current_num_frames] = denoised_pred
|
| 235 |
+
|
| 236 |
+
# Step 3.3: rerun with timestep zero to update KV cache using clean context
|
| 237 |
+
context_timestep = torch.ones_like(timestep) * self.args.context_noise
|
| 238 |
+
self.generator(
|
| 239 |
+
noisy_image_or_video=denoised_pred,
|
| 240 |
+
conditional_dict=conditional_dict,
|
| 241 |
+
timestep=context_timestep,
|
| 242 |
+
kv_cache=self.kv_cache1,
|
| 243 |
+
crossattn_cache=self.crossattn_cache,
|
| 244 |
+
current_start=current_start_frame * self.frame_seq_length,
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
if profile:
|
| 248 |
+
block_end.record()
|
| 249 |
+
torch.cuda.synchronize()
|
| 250 |
+
block_time = block_start.elapsed_time(block_end)
|
| 251 |
+
block_times.append(block_time)
|
| 252 |
+
|
| 253 |
+
# Step 3.4: update the start and end frame indices
|
| 254 |
+
current_start_frame += current_num_frames
|
| 255 |
+
|
| 256 |
+
if profile:
|
| 257 |
+
# End diffusion timing and synchronize CUDA
|
| 258 |
+
diffusion_end.record()
|
| 259 |
+
torch.cuda.synchronize()
|
| 260 |
+
diffusion_time = diffusion_start.elapsed_time(diffusion_end)
|
| 261 |
+
init_time = init_start.elapsed_time(init_end)
|
| 262 |
+
vae_start.record()
|
| 263 |
+
|
| 264 |
+
# Step 4: Decode the output
|
| 265 |
+
video = self.vae.decode_to_pixel(output, use_cache=False)
|
| 266 |
+
video = (video * 0.5 + 0.5).clamp(0, 1)
|
| 267 |
+
|
| 268 |
+
if profile:
|
| 269 |
+
# End VAE timing and synchronize CUDA
|
| 270 |
+
vae_end.record()
|
| 271 |
+
torch.cuda.synchronize()
|
| 272 |
+
vae_time = vae_start.elapsed_time(vae_end)
|
| 273 |
+
total_time = init_time + diffusion_time + vae_time
|
| 274 |
+
|
| 275 |
+
print("Profiling results:")
|
| 276 |
+
print(f" - Initialization/caching time: {init_time:.2f} ms ({100 * init_time / total_time:.2f}%)")
|
| 277 |
+
print(f" - Diffusion generation time: {diffusion_time:.2f} ms ({100 * diffusion_time / total_time:.2f}%)")
|
| 278 |
+
for i, block_time in enumerate(block_times):
|
| 279 |
+
print(f" - Block {i} generation time: {block_time:.2f} ms ({100 * block_time / diffusion_time:.2f}% of diffusion)")
|
| 280 |
+
print(f" - VAE decoding time: {vae_time:.2f} ms ({100 * vae_time / total_time:.2f}%)")
|
| 281 |
+
print(f" - Total time: {total_time:.2f} ms")
|
| 282 |
+
|
| 283 |
+
if return_latents:
|
| 284 |
+
return video, output
|
| 285 |
+
else:
|
| 286 |
+
return video
|
| 287 |
+
|
| 288 |
+
def _initialize_kv_cache(self, batch_size, dtype, device, num_total_frames=None):
|
| 289 |
+
"""
|
| 290 |
+
Initialize a Per-GPU KV cache for the Wan model.
|
| 291 |
+
"""
|
| 292 |
+
kv_cache1 = []
|
| 293 |
+
if self.local_attn_size != -1:
|
| 294 |
+
# Sliding window: cache size equals the window size
|
| 295 |
+
kv_cache_size = self.local_attn_size * self.frame_seq_length
|
| 296 |
+
elif num_total_frames is not None:
|
| 297 |
+
# Full history: allocate enough for every frame in this inference call
|
| 298 |
+
kv_cache_size = num_total_frames * self.frame_seq_length
|
| 299 |
+
else:
|
| 300 |
+
kv_cache_size = 32760
|
| 301 |
+
|
| 302 |
+
# For full-history mode, extend max_attention_size on every self-attention
|
| 303 |
+
# module so the attention op actually reads the full cache (default is 32760).
|
| 304 |
+
if self.local_attn_size == -1:
|
| 305 |
+
for module in self.generator.model.modules():
|
| 306 |
+
if hasattr(module, 'max_attention_size'):
|
| 307 |
+
module.max_attention_size = kv_cache_size
|
| 308 |
+
|
| 309 |
+
for _ in range(self.num_transformer_blocks):
|
| 310 |
+
kv_cache1.append({
|
| 311 |
+
"k": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
|
| 312 |
+
"v": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
|
| 313 |
+
"global_end_index": torch.tensor([0], dtype=torch.long, device=device),
|
| 314 |
+
"local_end_index": torch.tensor([0], dtype=torch.long, device=device)
|
| 315 |
+
})
|
| 316 |
+
|
| 317 |
+
self.kv_cache1 = kv_cache1 # always store the clean cache
|
| 318 |
+
|
| 319 |
+
def _initialize_crossattn_cache(self, batch_size, dtype, device):
|
| 320 |
+
"""
|
| 321 |
+
Initialize a Per-GPU cross-attention cache for the Wan model.
|
| 322 |
+
"""
|
| 323 |
+
crossattn_cache = []
|
| 324 |
+
|
| 325 |
+
for _ in range(self.num_transformer_blocks):
|
| 326 |
+
crossattn_cache.append({
|
| 327 |
+
"k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
|
| 328 |
+
"v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
|
| 329 |
+
"is_init": False
|
| 330 |
+
})
|
| 331 |
+
self.crossattn_cache = crossattn_cache
|
DecMem/pipeline/self_forcing_training.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 SelfForcingTrainingPipeline:
|
| 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_cache1 = 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 inference_with_trajectory(
|
| 61 |
+
self,
|
| 62 |
+
noise: torch.Tensor,
|
| 63 |
+
initial_latent: Optional[torch.Tensor] = None,
|
| 64 |
+
return_sim_step: bool = False,
|
| 65 |
+
**conditional_dict
|
| 66 |
+
) -> torch.Tensor:
|
| 67 |
+
batch_size, num_frames, num_channels, height, width = noise.shape
|
| 68 |
+
if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
|
| 69 |
+
# If the first frame is independent and the first frame is provided, then the number of frames in the
|
| 70 |
+
# noise should still be a multiple of num_frame_per_block
|
| 71 |
+
assert num_frames % self.num_frame_per_block == 0
|
| 72 |
+
num_blocks = num_frames // self.num_frame_per_block
|
| 73 |
+
else:
|
| 74 |
+
# Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
|
| 75 |
+
assert (num_frames - 1) % self.num_frame_per_block == 0
|
| 76 |
+
num_blocks = (num_frames - 1) // self.num_frame_per_block
|
| 77 |
+
num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
|
| 78 |
+
num_output_frames = num_frames + num_input_frames # add the initial latent frames
|
| 79 |
+
output = torch.zeros(
|
| 80 |
+
[batch_size, num_output_frames, num_channels, height, width],
|
| 81 |
+
device=noise.device,
|
| 82 |
+
dtype=noise.dtype
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# Step 1: Initialize KV cache to all zeros
|
| 86 |
+
self._initialize_kv_cache(
|
| 87 |
+
batch_size=batch_size, dtype=noise.dtype, device=noise.device
|
| 88 |
+
)
|
| 89 |
+
self._initialize_crossattn_cache(
|
| 90 |
+
batch_size=batch_size, dtype=noise.dtype, device=noise.device
|
| 91 |
+
)
|
| 92 |
+
# if self.kv_cache1 is None:
|
| 93 |
+
# self._initialize_kv_cache(
|
| 94 |
+
# batch_size=batch_size,
|
| 95 |
+
# dtype=noise.dtype,
|
| 96 |
+
# device=noise.device,
|
| 97 |
+
# )
|
| 98 |
+
# self._initialize_crossattn_cache(
|
| 99 |
+
# batch_size=batch_size,
|
| 100 |
+
# dtype=noise.dtype,
|
| 101 |
+
# device=noise.device
|
| 102 |
+
# )
|
| 103 |
+
# else:
|
| 104 |
+
# # reset cross attn cache
|
| 105 |
+
# for block_index in range(self.num_transformer_blocks):
|
| 106 |
+
# self.crossattn_cache[block_index]["is_init"] = False
|
| 107 |
+
# # reset kv cache
|
| 108 |
+
# for block_index in range(len(self.kv_cache1)):
|
| 109 |
+
# self.kv_cache1[block_index]["global_end_index"] = torch.tensor(
|
| 110 |
+
# [0], dtype=torch.long, device=noise.device)
|
| 111 |
+
# self.kv_cache1[block_index]["local_end_index"] = torch.tensor(
|
| 112 |
+
# [0], dtype=torch.long, device=noise.device)
|
| 113 |
+
|
| 114 |
+
# Step 2: Cache context feature
|
| 115 |
+
current_start_frame = 0
|
| 116 |
+
if initial_latent is not None:
|
| 117 |
+
timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
|
| 118 |
+
# Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
|
| 119 |
+
output[:, :1] = initial_latent
|
| 120 |
+
with torch.no_grad():
|
| 121 |
+
self.generator(
|
| 122 |
+
noisy_image_or_video=initial_latent,
|
| 123 |
+
conditional_dict=conditional_dict,
|
| 124 |
+
timestep=timestep * 0,
|
| 125 |
+
kv_cache=self.kv_cache1,
|
| 126 |
+
crossattn_cache=self.crossattn_cache,
|
| 127 |
+
current_start=current_start_frame * self.frame_seq_length
|
| 128 |
+
)
|
| 129 |
+
current_start_frame += 1
|
| 130 |
+
|
| 131 |
+
# Step 3: Temporal denoising loop
|
| 132 |
+
all_num_frames = [self.num_frame_per_block] * num_blocks
|
| 133 |
+
if self.independent_first_frame and initial_latent is None:
|
| 134 |
+
all_num_frames = [1] + all_num_frames
|
| 135 |
+
num_denoising_steps = len(self.denoising_step_list)
|
| 136 |
+
exit_flags = self.generate_and_sync_list(len(all_num_frames), num_denoising_steps, device=noise.device)
|
| 137 |
+
start_gradient_frame_index = num_output_frames - 21
|
| 138 |
+
|
| 139 |
+
# for block_index in range(num_blocks):
|
| 140 |
+
for block_index, current_num_frames in enumerate(all_num_frames):
|
| 141 |
+
noisy_input = noise[
|
| 142 |
+
:, current_start_frame - num_input_frames:current_start_frame + current_num_frames - num_input_frames]
|
| 143 |
+
|
| 144 |
+
# Step 3.1: Spatial denoising loop
|
| 145 |
+
for index, current_timestep in enumerate(self.denoising_step_list):
|
| 146 |
+
if self.same_step_across_blocks:
|
| 147 |
+
exit_flag = (index == exit_flags[0])
|
| 148 |
+
else:
|
| 149 |
+
exit_flag = (index == exit_flags[block_index]) # Only backprop at the randomly selected timestep (consistent across all ranks)
|
| 150 |
+
timestep = torch.ones(
|
| 151 |
+
[batch_size, current_num_frames],
|
| 152 |
+
device=noise.device,
|
| 153 |
+
dtype=torch.int64) * current_timestep
|
| 154 |
+
|
| 155 |
+
if not exit_flag:
|
| 156 |
+
with torch.no_grad():
|
| 157 |
+
_, denoised_pred = self.generator(
|
| 158 |
+
noisy_image_or_video=noisy_input,
|
| 159 |
+
conditional_dict=conditional_dict,
|
| 160 |
+
timestep=timestep,
|
| 161 |
+
kv_cache=self.kv_cache1,
|
| 162 |
+
crossattn_cache=self.crossattn_cache,
|
| 163 |
+
current_start=current_start_frame * self.frame_seq_length
|
| 164 |
+
)
|
| 165 |
+
next_timestep = self.denoising_step_list[index + 1]
|
| 166 |
+
noisy_input = self.scheduler.add_noise(
|
| 167 |
+
denoised_pred.flatten(0, 1),
|
| 168 |
+
torch.randn_like(denoised_pred.flatten(0, 1)),
|
| 169 |
+
next_timestep * torch.ones(
|
| 170 |
+
[batch_size * current_num_frames], device=noise.device, dtype=torch.long)
|
| 171 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 172 |
+
else:
|
| 173 |
+
# for getting real output
|
| 174 |
+
# with torch.set_grad_enabled(current_start_frame >= start_gradient_frame_index):
|
| 175 |
+
if current_start_frame < start_gradient_frame_index:
|
| 176 |
+
with torch.no_grad():
|
| 177 |
+
_, denoised_pred = self.generator(
|
| 178 |
+
noisy_image_or_video=noisy_input,
|
| 179 |
+
conditional_dict=conditional_dict,
|
| 180 |
+
timestep=timestep,
|
| 181 |
+
kv_cache=self.kv_cache1,
|
| 182 |
+
crossattn_cache=self.crossattn_cache,
|
| 183 |
+
current_start=current_start_frame * self.frame_seq_length
|
| 184 |
+
)
|
| 185 |
+
else:
|
| 186 |
+
_, denoised_pred = self.generator(
|
| 187 |
+
noisy_image_or_video=noisy_input,
|
| 188 |
+
conditional_dict=conditional_dict,
|
| 189 |
+
timestep=timestep,
|
| 190 |
+
kv_cache=self.kv_cache1,
|
| 191 |
+
crossattn_cache=self.crossattn_cache,
|
| 192 |
+
current_start=current_start_frame * self.frame_seq_length
|
| 193 |
+
)
|
| 194 |
+
break
|
| 195 |
+
|
| 196 |
+
# Step 3.2: record the model's output
|
| 197 |
+
output[:, current_start_frame:current_start_frame + current_num_frames] = denoised_pred
|
| 198 |
+
|
| 199 |
+
# Step 3.3: rerun with timestep zero to update the cache
|
| 200 |
+
context_timestep = torch.ones_like(timestep) * self.context_noise
|
| 201 |
+
# add context noise
|
| 202 |
+
denoised_pred = self.scheduler.add_noise(
|
| 203 |
+
denoised_pred.flatten(0, 1),
|
| 204 |
+
torch.randn_like(denoised_pred.flatten(0, 1)),
|
| 205 |
+
context_timestep * torch.ones(
|
| 206 |
+
[batch_size * current_num_frames], device=noise.device, dtype=torch.long)
|
| 207 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 208 |
+
with torch.no_grad():
|
| 209 |
+
self.generator(
|
| 210 |
+
noisy_image_or_video=denoised_pred,
|
| 211 |
+
conditional_dict=conditional_dict,
|
| 212 |
+
timestep=context_timestep,
|
| 213 |
+
kv_cache=self.kv_cache1,
|
| 214 |
+
crossattn_cache=self.crossattn_cache,
|
| 215 |
+
current_start=current_start_frame * self.frame_seq_length
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
# Step 3.4: update the start and end frame indices
|
| 219 |
+
current_start_frame += current_num_frames
|
| 220 |
+
|
| 221 |
+
# Step 3.5: Return the denoised timestep
|
| 222 |
+
if not self.same_step_across_blocks:
|
| 223 |
+
denoised_timestep_from, denoised_timestep_to = None, None
|
| 224 |
+
elif exit_flags[0] == len(self.denoising_step_list) - 1:
|
| 225 |
+
denoised_timestep_to = 0
|
| 226 |
+
denoised_timestep_from = 1000 - torch.argmin(
|
| 227 |
+
(self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
|
| 228 |
+
else:
|
| 229 |
+
denoised_timestep_to = 1000 - torch.argmin(
|
| 230 |
+
(self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0] + 1].cuda()).abs(), dim=0).item()
|
| 231 |
+
denoised_timestep_from = 1000 - torch.argmin(
|
| 232 |
+
(self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
|
| 233 |
+
|
| 234 |
+
if return_sim_step:
|
| 235 |
+
return output, denoised_timestep_from, denoised_timestep_to, exit_flags[0] + 1
|
| 236 |
+
|
| 237 |
+
return output, denoised_timestep_from, denoised_timestep_to
|
| 238 |
+
|
| 239 |
+
def _initialize_kv_cache(self, batch_size, dtype, device):
|
| 240 |
+
"""
|
| 241 |
+
Initialize a Per-GPU KV cache for the Wan model.
|
| 242 |
+
"""
|
| 243 |
+
kv_cache1 = []
|
| 244 |
+
|
| 245 |
+
for _ in range(self.num_transformer_blocks):
|
| 246 |
+
kv_cache1.append({
|
| 247 |
+
"k": torch.zeros([batch_size, self.kv_cache_size, 12, 128], dtype=dtype, device=device),
|
| 248 |
+
"v": torch.zeros([batch_size, self.kv_cache_size, 12, 128], dtype=dtype, device=device),
|
| 249 |
+
"global_end_index": torch.tensor([0], dtype=torch.long, device=device),
|
| 250 |
+
"local_end_index": torch.tensor([0], dtype=torch.long, device=device)
|
| 251 |
+
})
|
| 252 |
+
|
| 253 |
+
self.kv_cache1 = kv_cache1 # always store the clean cache
|
| 254 |
+
|
| 255 |
+
def _initialize_crossattn_cache(self, batch_size, dtype, device):
|
| 256 |
+
"""
|
| 257 |
+
Initialize a Per-GPU cross-attention cache for the Wan model.
|
| 258 |
+
"""
|
| 259 |
+
crossattn_cache = []
|
| 260 |
+
|
| 261 |
+
for _ in range(self.num_transformer_blocks):
|
| 262 |
+
crossattn_cache.append({
|
| 263 |
+
"k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
|
| 264 |
+
"v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
|
| 265 |
+
"is_init": False
|
| 266 |
+
})
|
| 267 |
+
self.crossattn_cache = crossattn_cache
|
DecMem/requirements.txt
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
opencv-python>=4.9.0.80
|
| 2 |
+
diffusers==0.31.0
|
| 3 |
+
transformers>=4.49.0
|
| 4 |
+
tokenizers>=0.20.3
|
| 5 |
+
accelerate>=1.1.1
|
| 6 |
+
tqdm
|
| 7 |
+
imageio
|
| 8 |
+
easydict
|
| 9 |
+
ftfy
|
| 10 |
+
imageio-ffmpeg
|
| 11 |
+
numpy
|
| 12 |
+
wandb
|
| 13 |
+
omegaconf
|
| 14 |
+
einops
|
| 15 |
+
av==13.1.0
|
| 16 |
+
opencv-python
|
| 17 |
+
pycocotools
|
| 18 |
+
lmdb
|
| 19 |
+
matplotlib
|
| 20 |
+
pydantic==2.10.6
|
| 21 |
+
scikit-image
|
| 22 |
+
huggingface_hub[cli]
|
| 23 |
+
dominate
|
| 24 |
+
ninja
|
| 25 |
+
pytest
|
| 26 |
+
decord
|
DecMem/scripts/build_worldmem_csv.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Build WorldMem train/test CSV index files from mp4/npz pairs.
|
| 3 |
+
|
| 4 |
+
Each row: video_path, action_path (paths relative to the project cwd when possible).
|
| 5 |
+
|
| 6 |
+
When --root is a symlink under the project (e.g. ./data), CSV rows use paths like
|
| 7 |
+
data/training/.... Avoid resolving symlinks so relative paths stay valid.
|
| 8 |
+
|
| 9 |
+
Example:
|
| 10 |
+
python scripts/build_worldmem_csv.py
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import csv
|
| 17 |
+
import random
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _path_for_csv(path: Path) -> str:
|
| 22 |
+
"""Path for CSV rows, relative to cwd when possible (preserves symlinks)."""
|
| 23 |
+
if not path.is_absolute():
|
| 24 |
+
return path.as_posix()
|
| 25 |
+
try:
|
| 26 |
+
return path.relative_to(Path.cwd()).as_posix()
|
| 27 |
+
except ValueError:
|
| 28 |
+
return path.as_posix()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def collect_pairs(split_dir: Path, root: Path) -> list[tuple[str, str]]:
|
| 32 |
+
"""Collect (video_path, action_path) for every mp4 with a matching npz."""
|
| 33 |
+
if not split_dir.is_dir():
|
| 34 |
+
raise FileNotFoundError(f"Split directory not found: {split_dir}")
|
| 35 |
+
|
| 36 |
+
pairs: list[tuple[str, str]] = []
|
| 37 |
+
missing_npz: list[str] = []
|
| 38 |
+
|
| 39 |
+
for mp4 in sorted(split_dir.rglob("*.mp4")):
|
| 40 |
+
npz = mp4.with_suffix(".npz")
|
| 41 |
+
if not npz.is_file():
|
| 42 |
+
missing_npz.append(mp4.relative_to(root).as_posix())
|
| 43 |
+
continue
|
| 44 |
+
pairs.append((
|
| 45 |
+
_path_for_csv(mp4),
|
| 46 |
+
_path_for_csv(npz),
|
| 47 |
+
))
|
| 48 |
+
|
| 49 |
+
if missing_npz:
|
| 50 |
+
print(f" warning: {len(missing_npz)} mp4 files without matching npz "
|
| 51 |
+
f"(first: {missing_npz[0]})")
|
| 52 |
+
|
| 53 |
+
return pairs
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def write_csv(rows: list[tuple[str, str]], output_path: Path) -> None:
|
| 57 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 58 |
+
with output_path.open("w", newline="", encoding="utf-8") as f:
|
| 59 |
+
writer = csv.writer(f)
|
| 60 |
+
writer.writerow(["video_path", "action_path"])
|
| 61 |
+
writer.writerows(rows)
|
| 62 |
+
print(f" wrote {len(rows)} rows -> {output_path}")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def main() -> None:
|
| 66 |
+
parser = argparse.ArgumentParser(description="Build WorldMem train/test CSV metadata.")
|
| 67 |
+
parser.add_argument(
|
| 68 |
+
"--root",
|
| 69 |
+
type=str,
|
| 70 |
+
default='./data',
|
| 71 |
+
help=f"Dataset root directory",
|
| 72 |
+
)
|
| 73 |
+
parser.add_argument(
|
| 74 |
+
"--output_dir",
|
| 75 |
+
type=str,
|
| 76 |
+
default='./data',
|
| 77 |
+
help="Directory for train.csv / test.csv (default: same as --root)",
|
| 78 |
+
)
|
| 79 |
+
parser.add_argument(
|
| 80 |
+
"--seed",
|
| 81 |
+
type=int,
|
| 82 |
+
default=42,
|
| 83 |
+
help="Random seed for shuffling rows",
|
| 84 |
+
)
|
| 85 |
+
args = parser.parse_args()
|
| 86 |
+
|
| 87 |
+
root = Path(args.root)
|
| 88 |
+
output_dir = Path(args.output_dir) if args.output_dir else root
|
| 89 |
+
|
| 90 |
+
splits = {
|
| 91 |
+
"train.csv": root / "training",
|
| 92 |
+
"test.csv": root / "test",
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
print(f"Dataset root : {root} (-> {root.resolve()})")
|
| 96 |
+
print(f"Output dir : {output_dir}")
|
| 97 |
+
print(f"Shuffle seed : {args.seed}")
|
| 98 |
+
|
| 99 |
+
rng = random.Random(args.seed)
|
| 100 |
+
|
| 101 |
+
for csv_name, split_dir in splits.items():
|
| 102 |
+
print(f"\n[{csv_name}] scanning {split_dir.relative_to(root)}/")
|
| 103 |
+
pairs = collect_pairs(split_dir, root)
|
| 104 |
+
rng.shuffle(pairs)
|
| 105 |
+
write_csv(pairs, output_dir / csv_name)
|
| 106 |
+
|
| 107 |
+
print("\nDone.")
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
if __name__ == "__main__":
|
| 111 |
+
main()
|
DecMem/scripts/infer.sh
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -o pipefail
|
| 3 |
+
|
| 4 |
+
# CONFIG SETTING
|
| 5 |
+
NUM_GPUS=${NUM_GPUS:-1}
|
| 6 |
+
MASTER_PORT=${MASTER_PORT:-29110}
|
| 7 |
+
OFFLOAD=${OFFLOAD:-0} # 1 or true = mutually-exclusive GPU residency for generator/VAE
|
| 8 |
+
EXP_NAME=${EXP_NAME:-decmem}
|
| 9 |
+
STEP=${STEP:-7000}
|
| 10 |
+
printf -v STEP "%06d" "$STEP"
|
| 11 |
+
|
| 12 |
+
DATA_PATH=data/test.csv
|
| 13 |
+
CONFIG_PATH=${CONFIG_PATH:-configs/${EXP_NAME}.yaml}
|
| 14 |
+
CHECKPOINT_PATH=${CHECKPOINT_PATH:-exps/${EXP_NAME}/checkpoint_model_${STEP}/model.pt}
|
| 15 |
+
NUM_CONDITION_FRAMES=${NUM_CONDITION_FRAMES:-56}
|
| 16 |
+
NUM_INFERENCE_STEPS=${NUM_INFERENCE_STEPS:-50}
|
| 17 |
+
GUIDANCE_SCALE=${GUIDANCE_SCALE:-5}
|
| 18 |
+
SEED=${SEED:-42}
|
| 19 |
+
START_INDEX=${START_INDEX:-0}
|
| 20 |
+
N=${N:-50}
|
| 21 |
+
|
| 22 |
+
T_LAT=${T_LAT:-200} # optional; empty = use config value
|
| 23 |
+
T_TAG=${T_LAT:+_T${T_LAT}}
|
| 24 |
+
|
| 25 |
+
OUTPUT_DIR=${OUTPUT_DIR:-test/res/${EXP_NAME}_step_${STEP}${T_TAG}_con_${NUM_CONDITION_FRAMES}_inf_${NUM_INFERENCE_STEPS}_gs_${GUIDANCE_SCALE}}
|
| 26 |
+
mkdir -p "$OUTPUT_DIR"
|
| 27 |
+
RANK0_LOG="${OUTPUT_DIR}/rank0.log"
|
| 28 |
+
|
| 29 |
+
SCRIPT_ARGS=(
|
| 30 |
+
--config_path "$CONFIG_PATH"
|
| 31 |
+
--checkpoint_path "$CHECKPOINT_PATH"
|
| 32 |
+
--data_path "$DATA_PATH"
|
| 33 |
+
--output_dir "$OUTPUT_DIR"
|
| 34 |
+
--num_condition_frames "$NUM_CONDITION_FRAMES"
|
| 35 |
+
--guidance_scale "$GUIDANCE_SCALE"
|
| 36 |
+
--num_inference_steps "$NUM_INFERENCE_STEPS"
|
| 37 |
+
--seed "$SEED")
|
| 38 |
+
|
| 39 |
+
SCRIPT_ARGS+=(--start_index "$START_INDEX")
|
| 40 |
+
if [[ -n "$N" ]]; then
|
| 41 |
+
SCRIPT_ARGS+=(--num_entries "$N")
|
| 42 |
+
fi
|
| 43 |
+
|
| 44 |
+
if [[ -n "$T_LAT" ]]; then
|
| 45 |
+
SCRIPT_ARGS+=(--num_latent_frames "$T_LAT")
|
| 46 |
+
fi
|
| 47 |
+
|
| 48 |
+
if [[ "$NUM_GPUS" -gt 1 ]]; then
|
| 49 |
+
echo "Multi-GPU inference: NUM_GPUS=${NUM_GPUS} MASTER_PORT=${MASTER_PORT}"
|
| 50 |
+
torchrun \
|
| 51 |
+
--standalone \
|
| 52 |
+
--nproc_per_node="$NUM_GPUS" \
|
| 53 |
+
--master_port="$MASTER_PORT" \
|
| 54 |
+
--local-ranks-filter=0 \
|
| 55 |
+
infer.py \
|
| 56 |
+
"${SCRIPT_ARGS[@]}" \
|
| 57 |
+
2>&1 | tee "$RANK0_LOG"
|
| 58 |
+
else
|
| 59 |
+
torchrun infer.py "${SCRIPT_ARGS[@]}" 2>&1 | tee "$RANK0_LOG"
|
| 60 |
+
fi
|
DecMem/scripts/infer_example.sh
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -o pipefail
|
| 3 |
+
|
| 4 |
+
# CONFIG SETTING
|
| 5 |
+
NUM_GPUS=${NUM_GPUS:-1}
|
| 6 |
+
MASTER_PORT=${MASTER_PORT:-29110}
|
| 7 |
+
OFFLOAD=${OFFLOAD:-0} # 1 or true = mutually-exclusive GPU residency for generator/VAE
|
| 8 |
+
|
| 9 |
+
EXP_NAME=decmem
|
| 10 |
+
CONFIG_PATH=${CONFIG_PATH:-configs/${EXP_NAME}.yaml}
|
| 11 |
+
CHECKPOINT_PATH=checkpoints/decmem.pt
|
| 12 |
+
DATA_PATH=example/example.csv
|
| 13 |
+
|
| 14 |
+
NUM_CONDITION_FRAMES=${NUM_CONDITION_FRAMES:-56}
|
| 15 |
+
NUM_INFERENCE_STEPS=${NUM_INFERENCE_STEPS:-20}
|
| 16 |
+
GUIDANCE_SCALE=${GUIDANCE_SCALE:-5}
|
| 17 |
+
SEED=${SEED:-42}
|
| 18 |
+
START_INDEX=${START_INDEX:-0}
|
| 19 |
+
N=${N:-2}
|
| 20 |
+
|
| 21 |
+
T_LAT=${T_LAT:-160} # optional; empty = use config value
|
| 22 |
+
T_TAG=${T_LAT:+_T${T_LAT}}
|
| 23 |
+
|
| 24 |
+
OUTPUT_DIR=${OUTPUT_DIR:-test/res/${EXP_NAME}_${T_TAG}_con_${NUM_CONDITION_FRAMES}_inf_${NUM_INFERENCE_STEPS}_gs_${GUIDANCE_SCALE}}
|
| 25 |
+
mkdir -p "$OUTPUT_DIR"
|
| 26 |
+
RANK0_LOG="${OUTPUT_DIR}/rank0.log"
|
| 27 |
+
|
| 28 |
+
SCRIPT_ARGS=(
|
| 29 |
+
--config_path "$CONFIG_PATH"
|
| 30 |
+
--checkpoint_path "$CHECKPOINT_PATH"
|
| 31 |
+
--data_path "$DATA_PATH"
|
| 32 |
+
--output_dir "$OUTPUT_DIR"
|
| 33 |
+
--num_condition_frames "$NUM_CONDITION_FRAMES"
|
| 34 |
+
--guidance_scale "$GUIDANCE_SCALE"
|
| 35 |
+
--num_inference_steps "$NUM_INFERENCE_STEPS"
|
| 36 |
+
--seed "$SEED")
|
| 37 |
+
|
| 38 |
+
SCRIPT_ARGS+=(--start_index "$START_INDEX")
|
| 39 |
+
if [[ -n "$N" ]]; then
|
| 40 |
+
SCRIPT_ARGS+=(--num_entries "$N")
|
| 41 |
+
fi
|
| 42 |
+
|
| 43 |
+
if [[ -n "$T_LAT" ]]; then
|
| 44 |
+
SCRIPT_ARGS+=(--num_latent_frames "$T_LAT")
|
| 45 |
+
fi
|
| 46 |
+
|
| 47 |
+
if [[ "$NUM_GPUS" -gt 1 ]]; then
|
| 48 |
+
echo "Multi-GPU inference: NUM_GPUS=${NUM_GPUS} MASTER_PORT=${MASTER_PORT}"
|
| 49 |
+
torchrun \
|
| 50 |
+
--standalone \
|
| 51 |
+
--nproc_per_node="$NUM_GPUS" \
|
| 52 |
+
--master_port="$MASTER_PORT" \
|
| 53 |
+
--local-ranks-filter=0 \
|
| 54 |
+
infer.py \
|
| 55 |
+
"${SCRIPT_ARGS[@]}" \
|
| 56 |
+
2>&1 | tee "$RANK0_LOG"
|
| 57 |
+
else
|
| 58 |
+
torchrun infer.py "${SCRIPT_ARGS[@]}" 2>&1 | tee "$RANK0_LOG"
|
| 59 |
+
fi
|
DecMem/scripts/train_multinode.sh
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# WorldMem diffusion multi-node training, launched MANUALLY on each node.
|
| 3 |
+
#
|
| 4 |
+
# Run this on every node, passing that node's rank as the only argument.
|
| 5 |
+
# Node 0 must be the machine whose IP matches MASTER_ADDR.
|
| 6 |
+
#
|
| 7 |
+
# Usage (run on each node):
|
| 8 |
+
# bash scripts/train_multinode.sh <node_rank> [exp_name]
|
| 9 |
+
#
|
| 10 |
+
# Example with 2 nodes:
|
| 11 |
+
# # on master node:
|
| 12 |
+
# bash scripts/train_multinode.sh 0 decmem
|
| 13 |
+
# # on worker node:
|
| 14 |
+
# bash scripts/train_multinode.sh 1 decmem
|
| 15 |
+
#
|
| 16 |
+
# Env overrides:
|
| 17 |
+
# EXP_NAME experiment name / config stem (default: decmem)
|
| 18 |
+
# NPROC_PER_NODE GPUs per node (default 8)
|
| 19 |
+
# MASTER_PORT torchrun rendezvous port (default 29500)
|
| 20 |
+
# RESUME_CKPT path to a checkpoint to fully resume from (optional)
|
| 21 |
+
|
| 22 |
+
# ─── Topology (EDIT THESE) ───────────────────────────────────────────────────
|
| 23 |
+
NNODES=2 # TODO: total number of nodes
|
| 24 |
+
MASTER_ADDR="" # TODO: IP of node_rank=0
|
| 25 |
+
MASTER_PORT=${MASTER_PORT:-29500}
|
| 26 |
+
NPROC_PER_NODE=${NPROC_PER_NODE:-8}
|
| 27 |
+
|
| 28 |
+
# ─── Node rank / experiment from CLI ─────────────────────────────────────────
|
| 29 |
+
NODE_RANK=$1
|
| 30 |
+
EXP_NAME=${2:-${EXP_NAME:-decmem}}
|
| 31 |
+
if [ -z "$NODE_RANK" ]; then
|
| 32 |
+
echo "Error: NODE_RANK is required. Usage: bash $0 <node_rank> [exp_name]"
|
| 33 |
+
exit 1
|
| 34 |
+
fi
|
| 35 |
+
|
| 36 |
+
# ─── Paths / config ──────────────────────────────────────────────────────────
|
| 37 |
+
CONFIG=configs/${EXP_NAME}.yaml
|
| 38 |
+
LOGDIR=exps/${EXP_NAME}
|
| 39 |
+
WANDB_SAVE_DIR=${LOGDIR}/wandb
|
| 40 |
+
TORCHRUN=torchrun
|
| 41 |
+
RESUME_CKPT=${RESUME_CKPT:-}
|
| 42 |
+
|
| 43 |
+
mkdir -p "${LOGDIR}"
|
| 44 |
+
|
| 45 |
+
# Optional resume: `RESUME_CKPT=/path/to/model.pt bash <this_script> <node_rank>`
|
| 46 |
+
# Must be set on EVERY node (this script doesn't propagate env across nodes).
|
| 47 |
+
RESUME_ARGS=""
|
| 48 |
+
if [ -n "${RESUME_CKPT}" ]; then
|
| 49 |
+
RESUME_ARGS="--resume-ckpt ${RESUME_CKPT}"
|
| 50 |
+
echo "[resume] RESUME_CKPT = ${RESUME_CKPT}"
|
| 51 |
+
fi
|
| 52 |
+
|
| 53 |
+
# ─── NCCL (match train_worldmem_multinode_torchrun.sh) ───────────────────────
|
| 54 |
+
export NCCL_IB_DISABLE=0
|
| 55 |
+
export NCCL_IB_GID_INDEX=3
|
| 56 |
+
export NCCL_MIN_NCHANNELS=16
|
| 57 |
+
export NCCL_IB_HCA=mlx5
|
| 58 |
+
export NCCL_IB_QPS_PER_CONNECTION=4
|
| 59 |
+
export NCCL_IB_TIMEOUT=22
|
| 60 |
+
export NCCL_DEBUG=WARN
|
| 61 |
+
|
| 62 |
+
echo "EXP_NAME = ${EXP_NAME}"
|
| 63 |
+
echo "CONFIG = ${CONFIG}"
|
| 64 |
+
echo "LOGDIR = ${LOGDIR}"
|
| 65 |
+
echo "NNODES = ${NNODES}"
|
| 66 |
+
echo "NPROC_PER_NODE = ${NPROC_PER_NODE}"
|
| 67 |
+
echo "WORLD_SIZE = $((NNODES * NPROC_PER_NODE))"
|
| 68 |
+
echo "MASTER_ADDR = ${MASTER_ADDR}"
|
| 69 |
+
echo "MASTER_PORT = ${MASTER_PORT}"
|
| 70 |
+
echo "RESUME_CKPT = ${RESUME_CKPT:-<none>}"
|
| 71 |
+
echo "Starting node with NODE_RANK=${NODE_RANK}"
|
| 72 |
+
|
| 73 |
+
# ─── Launch torchrun ─────────────────────────────────────────────────────────
|
| 74 |
+
${TORCHRUN} \
|
| 75 |
+
--nnodes=${NNODES} \
|
| 76 |
+
--nproc_per_node=${NPROC_PER_NODE} \
|
| 77 |
+
--node_rank=${NODE_RANK} \
|
| 78 |
+
--master_addr=${MASTER_ADDR} \
|
| 79 |
+
--master_port=${MASTER_PORT} \
|
| 80 |
+
train.py \
|
| 81 |
+
--config_path ${CONFIG} \
|
| 82 |
+
--logdir ${LOGDIR} \
|
| 83 |
+
--wandb-save-dir ${WANDB_SAVE_DIR} \
|
| 84 |
+
${RESUME_ARGS} \
|
| 85 |
+
> "${LOGDIR}/node_${NODE_RANK}_$(date +%Y%m%d_%H%M%S).log" 2>&1
|
DecMem/slurm_logs/.gitkeep
ADDED
|
File without changes
|
DecMem/train.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
from omegaconf import OmegaConf
|
| 4 |
+
import wandb
|
| 5 |
+
|
| 6 |
+
from trainer import DiffusionTrainer
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main():
|
| 10 |
+
parser = argparse.ArgumentParser()
|
| 11 |
+
parser.add_argument("--config_path", type=str, required=True)
|
| 12 |
+
parser.add_argument("--no_save", action="store_true")
|
| 13 |
+
parser.add_argument("--no_visualize", action="store_true")
|
| 14 |
+
parser.add_argument("--logdir", type=str, default="", help="Path to the directory to save logs")
|
| 15 |
+
parser.add_argument("--wandb-save-dir", type=str, default="", help="Path to the directory to save wandb logs")
|
| 16 |
+
parser.add_argument("--disable-wandb", action="store_true")
|
| 17 |
+
parser.add_argument("--resume-ckpt", type=str, default=None,
|
| 18 |
+
help="Path to a checkpoint to fully resume from (weights + optimizer + step + wandb_run_id). "
|
| 19 |
+
"Overrides config.resume_ckpt if both are set.")
|
| 20 |
+
|
| 21 |
+
args = parser.parse_args()
|
| 22 |
+
|
| 23 |
+
config = OmegaConf.load(args.config_path)
|
| 24 |
+
default_config = OmegaConf.load("configs/default_config.yaml")
|
| 25 |
+
config = OmegaConf.merge(default_config, config)
|
| 26 |
+
config.no_save = args.no_save
|
| 27 |
+
config.no_visualize = args.no_visualize
|
| 28 |
+
|
| 29 |
+
# get the filename of config_path
|
| 30 |
+
config_name = os.path.basename(args.config_path).split(".")[0]
|
| 31 |
+
config.config_name = config_name
|
| 32 |
+
config.logdir = args.logdir
|
| 33 |
+
config.wandb_save_dir = args.wandb_save_dir
|
| 34 |
+
config.disable_wandb = args.disable_wandb
|
| 35 |
+
if args.resume_ckpt is not None:
|
| 36 |
+
config.resume_ckpt = args.resume_ckpt
|
| 37 |
+
|
| 38 |
+
trainer = DiffusionTrainer(config)
|
| 39 |
+
trainer.train()
|
| 40 |
+
|
| 41 |
+
wandb.finish()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
if __name__ == "__main__":
|
| 45 |
+
main()
|
DecMem/trainer/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .diffusion import Trainer as DiffusionTrainer
|
| 2 |
+
from .gan import Trainer as GANTrainer
|
| 3 |
+
|
| 4 |
+
__all__ = [
|
| 5 |
+
"DiffusionTrainer",
|
| 6 |
+
"GANTrainer",
|
| 7 |
+
]
|
DecMem/trainer/diffusion.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gc
|
| 2 |
+
import logging
|
| 3 |
+
|
| 4 |
+
from model import CausalDiffusion
|
| 5 |
+
from utils.dataset import ShardingLMDBDataset, WorldMemCSVDataset, cycle
|
| 6 |
+
from utils.pose_utils import poses_to_viewmats
|
| 7 |
+
from utils.misc import set_seed
|
| 8 |
+
import torch.distributed as dist
|
| 9 |
+
from omegaconf import OmegaConf
|
| 10 |
+
import torch
|
| 11 |
+
import wandb
|
| 12 |
+
import time
|
| 13 |
+
import os
|
| 14 |
+
|
| 15 |
+
from utils.distributed import barrier, fsdp_wrap, fsdp_state_dict, fsdp_optim_state_dict, load_fsdp_optim_state_dict, launch_distributed_job
|
| 16 |
+
# from utils.distributed import EMA_FSDP
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Trainer:
|
| 20 |
+
def __init__(self, config):
|
| 21 |
+
self.config = config
|
| 22 |
+
self.step = 0
|
| 23 |
+
|
| 24 |
+
# Step 1: Initialize the distributed training environment (rank, seed, dtype, logging etc.)
|
| 25 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 26 |
+
torch.backends.cudnn.allow_tf32 = True
|
| 27 |
+
|
| 28 |
+
launch_distributed_job()
|
| 29 |
+
global_rank = dist.get_rank()
|
| 30 |
+
|
| 31 |
+
self.dtype = torch.bfloat16 if config.mixed_precision else torch.float32
|
| 32 |
+
self.device = torch.cuda.current_device()
|
| 33 |
+
self.is_main_process = global_rank == 0
|
| 34 |
+
self.causal = config.causal
|
| 35 |
+
self.disable_wandb = config.disable_wandb
|
| 36 |
+
|
| 37 |
+
# If resume_ckpt is set, load it now so we can restore step / seed / wandb_run_id
|
| 38 |
+
# BEFORE seeding RNG and initializing wandb. The full state stays in CPU memory
|
| 39 |
+
# and is consumed (then freed) after the optimizer / EMA are constructed.
|
| 40 |
+
self._resume_state = None
|
| 41 |
+
resume_path = getattr(config, "resume_ckpt", None)
|
| 42 |
+
if resume_path:
|
| 43 |
+
if global_rank == 0:
|
| 44 |
+
print(f"Loading resume checkpoint: {resume_path}")
|
| 45 |
+
self._resume_state = torch.load(resume_path, map_location="cpu", weights_only=False)
|
| 46 |
+
self.step = int(self._resume_state.get("step", 0))
|
| 47 |
+
resumed_seed = self._resume_state.get("seed", None)
|
| 48 |
+
if resumed_seed is not None:
|
| 49 |
+
config.seed = int(resumed_seed)
|
| 50 |
+
if global_rank == 0:
|
| 51 |
+
print(f"Resumed: step={self.step}, seed={config.seed}, "
|
| 52 |
+
f"wandb_run_id={self._resume_state.get('wandb_run_id')}")
|
| 53 |
+
|
| 54 |
+
# use a random seed for the training
|
| 55 |
+
if config.seed == 0:
|
| 56 |
+
random_seed = torch.randint(0, 10000000, (1,), device=self.device)
|
| 57 |
+
dist.broadcast(random_seed, src=0)
|
| 58 |
+
config.seed = random_seed.item()
|
| 59 |
+
|
| 60 |
+
set_seed(config.seed + global_rank)
|
| 61 |
+
|
| 62 |
+
if self.is_main_process and not self.disable_wandb:
|
| 63 |
+
wandb.login(host=config.wandb_host, key=config.wandb_key)
|
| 64 |
+
wandb_init_kwargs = dict(
|
| 65 |
+
config=OmegaConf.to_container(config, resolve=True),
|
| 66 |
+
name=config.config_name,
|
| 67 |
+
mode="online",
|
| 68 |
+
entity=config.wandb_entity,
|
| 69 |
+
project=config.wandb_project,
|
| 70 |
+
dir=config.wandb_save_dir,
|
| 71 |
+
)
|
| 72 |
+
wandb_run_id = self._resume_state.get("wandb_run_id") if self._resume_state else None
|
| 73 |
+
if wandb_run_id:
|
| 74 |
+
# resume="must" → wandb errors out if the id is missing, instead of
|
| 75 |
+
# silently creating a new run that diverges from the original.
|
| 76 |
+
wandb_init_kwargs["id"] = wandb_run_id
|
| 77 |
+
wandb_init_kwargs["resume"] = "must"
|
| 78 |
+
wandb.init(**wandb_init_kwargs)
|
| 79 |
+
|
| 80 |
+
self.output_path = config.logdir
|
| 81 |
+
|
| 82 |
+
# Step 2: Initialize the model and optimizer
|
| 83 |
+
# For WorldMem training (CSV data path), skip text encoder to save GPU memory
|
| 84 |
+
data_path = config.data_path
|
| 85 |
+
self.use_worldmem_data = isinstance(data_path, str) and data_path.endswith(".csv")
|
| 86 |
+
if self.use_worldmem_data:
|
| 87 |
+
config.use_text_encoder = False
|
| 88 |
+
|
| 89 |
+
self.model = CausalDiffusion(config, device=self.device)
|
| 90 |
+
|
| 91 |
+
if global_rank == 0:
|
| 92 |
+
def _fmt(n: int) -> str:
|
| 93 |
+
for unit in ("", "K", "M", "B"):
|
| 94 |
+
if abs(n) < 1000:
|
| 95 |
+
return f"{n:.2f}{unit}"
|
| 96 |
+
n /= 1000
|
| 97 |
+
return f"{n:.2f}T"
|
| 98 |
+
total = sum(p.numel() for p in self.model.generator.parameters())
|
| 99 |
+
trainable = sum(p.numel() for p in self.model.generator.parameters() if p.requires_grad)
|
| 100 |
+
print(f"[generator] params total={_fmt(total)} ({total:,}) "
|
| 101 |
+
f"trainable={_fmt(trainable)} ({trainable:,})", flush=True)
|
| 102 |
+
if self.model.text_encoder is not None:
|
| 103 |
+
te = sum(p.numel() for p in self.model.text_encoder.parameters())
|
| 104 |
+
print(f"[text_encoder] params total={_fmt(te)} ({te:,})", flush=True)
|
| 105 |
+
vae = sum(p.numel() for p in self.model.vae.parameters())
|
| 106 |
+
print(f"[vae] params total={_fmt(vae)} ({vae:,})", flush=True)
|
| 107 |
+
|
| 108 |
+
self.model.generator = fsdp_wrap(
|
| 109 |
+
self.model.generator,
|
| 110 |
+
sharding_strategy=config.sharding_strategy,
|
| 111 |
+
mixed_precision=config.mixed_precision,
|
| 112 |
+
wrap_strategy=config.generator_fsdp_wrap_strategy
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
if self.model.text_encoder is not None:
|
| 116 |
+
self.model.text_encoder = fsdp_wrap(
|
| 117 |
+
self.model.text_encoder,
|
| 118 |
+
sharding_strategy=config.sharding_strategy,
|
| 119 |
+
mixed_precision=config.mixed_precision,
|
| 120 |
+
wrap_strategy=config.text_encoder_fsdp_wrap_strategy
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
if not config.no_visualize or config.load_raw_video:
|
| 124 |
+
self.model.vae = self.model.vae.to(
|
| 125 |
+
device=self.device, dtype=torch.bfloat16 if config.mixed_precision else torch.float32)
|
| 126 |
+
|
| 127 |
+
self.generator_optimizer = torch.optim.AdamW(
|
| 128 |
+
[param for param in self.model.generator.parameters()
|
| 129 |
+
if param.requires_grad],
|
| 130 |
+
lr=config.lr,
|
| 131 |
+
betas=(config.beta1, config.beta2),
|
| 132 |
+
weight_decay=config.weight_decay
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
# Step 3: Initialize the dataloader
|
| 136 |
+
if self.use_worldmem_data:
|
| 137 |
+
num_latent_frames = getattr(config, "num_frames", 21)
|
| 138 |
+
sample_strategy = getattr(config, "sample_strategy", "random")
|
| 139 |
+
dataset = WorldMemCSVDataset(
|
| 140 |
+
data_path,
|
| 141 |
+
num_latent_frames=num_latent_frames,
|
| 142 |
+
sample_strategy=sample_strategy,
|
| 143 |
+
)
|
| 144 |
+
else:
|
| 145 |
+
dataset = ShardingLMDBDataset(data_path, max_pair=int(1e8))
|
| 146 |
+
sampler = torch.utils.data.distributed.DistributedSampler(
|
| 147 |
+
dataset, shuffle=True, drop_last=True)
|
| 148 |
+
dataloader = torch.utils.data.DataLoader(
|
| 149 |
+
dataset,
|
| 150 |
+
batch_size=config.batch_size,
|
| 151 |
+
sampler=sampler,
|
| 152 |
+
num_workers=8,
|
| 153 |
+
pin_memory=True,
|
| 154 |
+
persistent_workers=True)
|
| 155 |
+
|
| 156 |
+
if dist.get_rank() == 0:
|
| 157 |
+
world_size = dist.get_world_size()
|
| 158 |
+
per_rank_samples = len(sampler) # honors drop_last + world_size split
|
| 159 |
+
global_batch_size = world_size * config.batch_size
|
| 160 |
+
print("=" * 80, flush=True)
|
| 161 |
+
print(f"[dataset] total samples : {len(dataset)}", flush=True)
|
| 162 |
+
print(f"[dataset] per-rank : {per_rank_samples} "
|
| 163 |
+
f"(drop_last=True, world_size={world_size})", flush=True)
|
| 164 |
+
print(f"[batch] per-rank : {config.batch_size}", flush=True)
|
| 165 |
+
print(f"[batch] global : {global_batch_size} "
|
| 166 |
+
f"(= {world_size} ranks x {config.batch_size})", flush=True)
|
| 167 |
+
print(f"[steps/epoch] : {per_rank_samples // config.batch_size}",
|
| 168 |
+
flush=True)
|
| 169 |
+
print("-" * 80, flush=True)
|
| 170 |
+
print("[config]")
|
| 171 |
+
print(OmegaConf.to_yaml(config, resolve=True), flush=True)
|
| 172 |
+
print("=" * 80, flush=True)
|
| 173 |
+
self.dataloader = cycle(dataloader)
|
| 174 |
+
|
| 175 |
+
##############################################################################################################
|
| 176 |
+
# 6. Set up EMA parameter containers
|
| 177 |
+
rename_param = (
|
| 178 |
+
lambda name: name.replace("_fsdp_wrapped_module.", "")
|
| 179 |
+
.replace("_checkpoint_wrapped_module.", "")
|
| 180 |
+
.replace("_orig_mod.", "")
|
| 181 |
+
)
|
| 182 |
+
self.name_to_trainable_params = {}
|
| 183 |
+
for n, p in self.model.generator.named_parameters():
|
| 184 |
+
if not p.requires_grad:
|
| 185 |
+
continue
|
| 186 |
+
|
| 187 |
+
renamed_n = rename_param(n)
|
| 188 |
+
self.name_to_trainable_params[renamed_n] = p
|
| 189 |
+
# ema_weight = config.ema_weight
|
| 190 |
+
# self.generator_ema = None
|
| 191 |
+
# if (ema_weight is not None) and (ema_weight > 0.0):
|
| 192 |
+
# print(f"Setting up EMA with weight {ema_weight}")
|
| 193 |
+
# self.generator_ema = EMA_FSDP(self.model.generator, decay=ema_weight)
|
| 194 |
+
|
| 195 |
+
##############################################################################################################
|
| 196 |
+
# 7. (If resuming) Load the model and optimizer, lr_scheduler statedicts
|
| 197 |
+
if self._resume_state is not None:
|
| 198 |
+
# Full resume: weights + optimizer + EMA (step already restored in __init__ top).
|
| 199 |
+
if global_rank == 0:
|
| 200 |
+
print("Restoring generator / optimizer from resume checkpoint.")
|
| 201 |
+
# Use FULL_STATE_DICT context so FSDP scatters the (CPU) full
|
| 202 |
+
# state-dict into local GPU shards instead of leaving params on CPU.
|
| 203 |
+
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
|
| 204 |
+
from torch.distributed.fsdp import StateDictType
|
| 205 |
+
with FSDP.state_dict_type(self.model.generator, StateDictType.FULL_STATE_DICT):
|
| 206 |
+
self.model.generator.load_state_dict(self._resume_state["generator"], strict=False)
|
| 207 |
+
if "optimizer" in self._resume_state:
|
| 208 |
+
load_fsdp_optim_state_dict(
|
| 209 |
+
self.model.generator,
|
| 210 |
+
self.generator_optimizer,
|
| 211 |
+
self._resume_state["optimizer"],
|
| 212 |
+
)
|
| 213 |
+
# if "generator_ema" in self._resume_state and self.generator_ema is not None:
|
| 214 |
+
# self.generator_ema.load_state_dict(self._resume_state["generator_ema"])
|
| 215 |
+
self._resume_state = None
|
| 216 |
+
torch.cuda.empty_cache()
|
| 217 |
+
elif getattr(config, "generator_ckpt", False):
|
| 218 |
+
# Warm-start: weights only, step counter and optimizer stay fresh.
|
| 219 |
+
print(f"Loading pretrained generator from {config.generator_ckpt}")
|
| 220 |
+
state_dict = torch.load(config.generator_ckpt, map_location="cpu")
|
| 221 |
+
if "generator" in state_dict:
|
| 222 |
+
state_dict = state_dict["generator"]
|
| 223 |
+
elif "model" in state_dict:
|
| 224 |
+
state_dict = state_dict["model"]
|
| 225 |
+
# strict=False: dense ckpts lack LTM-only params (e.g. mem_proj); keep init values.
|
| 226 |
+
missing, unexpected = self.model.generator.load_state_dict(
|
| 227 |
+
state_dict, strict=False
|
| 228 |
+
)
|
| 229 |
+
if missing:
|
| 230 |
+
print(
|
| 231 |
+
f" warm-start missing keys ({len(missing)}): "
|
| 232 |
+
f"{missing[:5]}{'...' if len(missing) > 5 else ''}"
|
| 233 |
+
)
|
| 234 |
+
if unexpected:
|
| 235 |
+
print(
|
| 236 |
+
f" warm-start unexpected keys ({len(unexpected)}): "
|
| 237 |
+
f"{unexpected[:5]}{'...' if len(unexpected) > 5 else ''}"
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
##############################################################################################################
|
| 241 |
+
|
| 242 |
+
# # Let's delete EMA params for early steps to save some computes at training and inference
|
| 243 |
+
# if self.step < config.ema_start_step:
|
| 244 |
+
# self.generator_ema = None
|
| 245 |
+
|
| 246 |
+
self.max_grad_norm = 10.0
|
| 247 |
+
self.previous_time = None
|
| 248 |
+
|
| 249 |
+
def save(self):
|
| 250 |
+
print("Start gathering distributed model states...")
|
| 251 |
+
# Both calls are collectives: every rank must participate, only rank 0 ends up
|
| 252 |
+
# holding the full CPU tensors (rank0_only=True inside the helpers).
|
| 253 |
+
generator_state_dict = fsdp_state_dict(self.model.generator)
|
| 254 |
+
optimizer_state_dict = fsdp_optim_state_dict(
|
| 255 |
+
self.model.generator, self.generator_optimizer)
|
| 256 |
+
|
| 257 |
+
wandb_run_id = (
|
| 258 |
+
wandb.run.id
|
| 259 |
+
if (self.is_main_process and not self.disable_wandb and wandb.run is not None)
|
| 260 |
+
else None
|
| 261 |
+
)
|
| 262 |
+
state_dict = {
|
| 263 |
+
"generator": generator_state_dict,
|
| 264 |
+
"optimizer": optimizer_state_dict,
|
| 265 |
+
"step": self.step,
|
| 266 |
+
"seed": int(self.config.seed),
|
| 267 |
+
"wandb_run_id": wandb_run_id,
|
| 268 |
+
}
|
| 269 |
+
# if self.step > self.config.ema_start_step and self.generator_ema is not None:
|
| 270 |
+
# state_dict["generator_ema"] = self.generator_ema.state_dict()
|
| 271 |
+
|
| 272 |
+
if self.is_main_process:
|
| 273 |
+
os.makedirs(os.path.join(self.output_path,
|
| 274 |
+
f"checkpoint_model_{self.step:06d}"), exist_ok=True)
|
| 275 |
+
torch.save(state_dict, os.path.join(self.output_path,
|
| 276 |
+
f"checkpoint_model_{self.step:06d}", "model.pt"))
|
| 277 |
+
print("Model saved to", os.path.join(self.output_path,
|
| 278 |
+
f"checkpoint_model_{self.step:06d}", "model.pt"))
|
| 279 |
+
|
| 280 |
+
def train_one_step(self, batch):
|
| 281 |
+
torch.cuda.synchronize()
|
| 282 |
+
t1 = time.time()
|
| 283 |
+
self.log_iters = 1
|
| 284 |
+
|
| 285 |
+
if self.step % 20 == 0:
|
| 286 |
+
torch.cuda.empty_cache()
|
| 287 |
+
|
| 288 |
+
# Step 1: Get the next batch of text prompts
|
| 289 |
+
text_prompts = batch["prompts"]
|
| 290 |
+
if not self.config.load_raw_video: # precomputed latent
|
| 291 |
+
clean_latent = batch["ode_latent"][:, -1].to(
|
| 292 |
+
device=self.device, dtype=self.dtype)
|
| 293 |
+
else: # encode raw video to latent
|
| 294 |
+
frames = batch["frames"].to(
|
| 295 |
+
device=self.device, dtype=self.dtype)
|
| 296 |
+
with torch.no_grad():
|
| 297 |
+
clean_latent = self.model.vae.encode_to_latent(
|
| 298 |
+
frames).to(device=self.device, dtype=self.dtype)
|
| 299 |
+
image_latent = clean_latent[:, 0:1, ]
|
| 300 |
+
|
| 301 |
+
batch_size = len(text_prompts)
|
| 302 |
+
image_or_video_shape = list(self.config.image_or_video_shape)
|
| 303 |
+
image_or_video_shape[0] = batch_size
|
| 304 |
+
|
| 305 |
+
# Step 2: Build conditional_dict
|
| 306 |
+
proportion_empty_prompts = getattr(self.config, "proportion_empty_prompts", 0.0)
|
| 307 |
+
|
| 308 |
+
if self.use_worldmem_data:
|
| 309 |
+
# WorldMem: no text conditioning; only actions and viewmats
|
| 310 |
+
conditional_dict = {}
|
| 311 |
+
unconditional_dict = {}
|
| 312 |
+
|
| 313 |
+
actions = batch["actions"].to(device=self.device, dtype=self.dtype) # (B, T, 25)
|
| 314 |
+
poses = batch["poses"].to(device=self.device, dtype=self.dtype) # (B, T, 5)
|
| 315 |
+
viewmats = poses_to_viewmats(poses) # (B, T, 4, 4)
|
| 316 |
+
|
| 317 |
+
# CFG: randomly drop action conditioning
|
| 318 |
+
if proportion_empty_prompts > 0.0:
|
| 319 |
+
drop_mask = torch.rand(batch_size, device=self.device) < proportion_empty_prompts
|
| 320 |
+
actions = actions.clone()
|
| 321 |
+
# viewmats = viewmats.clone()
|
| 322 |
+
actions[drop_mask] = 0.0
|
| 323 |
+
# eye = torch.eye(4, dtype=viewmats.dtype, device=viewmats.device)
|
| 324 |
+
# viewmats[drop_mask] = eye.unsqueeze(0).unsqueeze(0).expand(
|
| 325 |
+
# drop_mask.sum(), viewmats.shape[1], -1, -1)
|
| 326 |
+
|
| 327 |
+
conditional_dict["actions"] = actions
|
| 328 |
+
conditional_dict["viewmats"] = viewmats
|
| 329 |
+
else:
|
| 330 |
+
with torch.no_grad():
|
| 331 |
+
conditional_dict = self.model.text_encoder(text_prompts=text_prompts)
|
| 332 |
+
|
| 333 |
+
if not getattr(self, "unconditional_dict", None):
|
| 334 |
+
unconditional_dict = self.model.text_encoder(
|
| 335 |
+
text_prompts=[self.config.negative_prompt] * batch_size)
|
| 336 |
+
unconditional_dict = {k: v.detach()
|
| 337 |
+
for k, v in unconditional_dict.items()}
|
| 338 |
+
self.unconditional_dict = unconditional_dict
|
| 339 |
+
else:
|
| 340 |
+
unconditional_dict = self.unconditional_dict
|
| 341 |
+
|
| 342 |
+
# Step 3: Train the generator
|
| 343 |
+
generator_loss, log_dict = self.model.generator_loss(
|
| 344 |
+
image_or_video_shape=image_or_video_shape,
|
| 345 |
+
conditional_dict=conditional_dict,
|
| 346 |
+
unconditional_dict=unconditional_dict,
|
| 347 |
+
clean_latent=clean_latent,
|
| 348 |
+
initial_latent=image_latent
|
| 349 |
+
)
|
| 350 |
+
self.generator_optimizer.zero_grad()
|
| 351 |
+
generator_loss.backward()
|
| 352 |
+
generator_grad_norm = self.model.generator.clip_grad_norm_(
|
| 353 |
+
self.max_grad_norm)
|
| 354 |
+
self.generator_optimizer.step()
|
| 355 |
+
|
| 356 |
+
# Increment the step since we finished gradient update
|
| 357 |
+
self.step += 1
|
| 358 |
+
|
| 359 |
+
loss_unweighted_val = log_dict["loss_unweighted"].item()
|
| 360 |
+
wandb_loss_dict = {
|
| 361 |
+
"generator_loss": generator_loss.item(), # weighted, used for backprop
|
| 362 |
+
"generator_loss_unweighted": loss_unweighted_val, # raw MSE, comparable across runs
|
| 363 |
+
"generator_grad_norm": generator_grad_norm.item(),
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
# Per-frame unweighted loss bucketed by frame position (10 frames per bucket)
|
| 367 |
+
bucket_str = ""
|
| 368 |
+
loss_per_frame = log_dict.get("loss_per_frame", None)
|
| 369 |
+
if loss_per_frame is not None:
|
| 370 |
+
per_frame = loss_per_frame.float().mean(dim=0) # (F,)
|
| 371 |
+
num_frame = per_frame.shape[0]
|
| 372 |
+
bucket_size = 10
|
| 373 |
+
bucket_means = []
|
| 374 |
+
for start in range(0, num_frame, bucket_size):
|
| 375 |
+
end = min(start + bucket_size, num_frame)
|
| 376 |
+
v = per_frame[start:end].mean().item()
|
| 377 |
+
bucket_means.append((start, end, v))
|
| 378 |
+
wandb_loss_dict[f"loss_frames_{start:02d}-{end - 1:02d}"] = v
|
| 379 |
+
bucket_str = " " + " ".join(
|
| 380 |
+
f"f{start:02d}-{end - 1:02d}={v:.4f}" for start, end, v in bucket_means
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
# Step 4: Logging
|
| 384 |
+
torch.cuda.synchronize()
|
| 385 |
+
t2 = time.time()
|
| 386 |
+
if self.is_main_process:
|
| 387 |
+
print(
|
| 388 |
+
f"step={self.step} loss={generator_loss.item():.4f} "
|
| 389 |
+
f"loss_unweighted={loss_unweighted_val:.4f} "
|
| 390 |
+
f"grad_norm={generator_grad_norm.item():.4f}",
|
| 391 |
+
f"time={t2 - t1:.2f} seconds",
|
| 392 |
+
flush=True,
|
| 393 |
+
)
|
| 394 |
+
if not self.disable_wandb:
|
| 395 |
+
wandb.log(wandb_loss_dict, step=self.step)
|
| 396 |
+
|
| 397 |
+
if self.step % self.config.gc_interval == 0:
|
| 398 |
+
if dist.get_rank() == 0:
|
| 399 |
+
logging.info("DistGarbageCollector: Running GC.")
|
| 400 |
+
gc.collect()
|
| 401 |
+
|
| 402 |
+
def generate_video(self, pipeline, prompts, image=None):
|
| 403 |
+
batch_size = len(prompts)
|
| 404 |
+
sampled_noise = torch.randn(
|
| 405 |
+
[batch_size, 21, 16, 60, 104], device="cuda", dtype=self.dtype
|
| 406 |
+
)
|
| 407 |
+
video, _ = pipeline.inference(
|
| 408 |
+
noise=sampled_noise,
|
| 409 |
+
text_prompts=prompts,
|
| 410 |
+
return_latents=True
|
| 411 |
+
)
|
| 412 |
+
current_video = video.permute(0, 1, 3, 4, 2).cpu().numpy() * 255.0
|
| 413 |
+
return current_video
|
| 414 |
+
|
| 415 |
+
def train(self):
|
| 416 |
+
while True:
|
| 417 |
+
batch = next(self.dataloader)
|
| 418 |
+
self.train_one_step(batch)
|
| 419 |
+
if (not self.config.no_save) and self.step % self.config.log_iters == 0:
|
| 420 |
+
torch.cuda.empty_cache()
|
| 421 |
+
self.save()
|
| 422 |
+
torch.cuda.empty_cache()
|
| 423 |
+
|
| 424 |
+
barrier()
|
| 425 |
+
if self.is_main_process:
|
| 426 |
+
current_time = time.time()
|
| 427 |
+
if self.previous_time is None:
|
| 428 |
+
self.previous_time = current_time
|
| 429 |
+
else:
|
| 430 |
+
if not self.disable_wandb:
|
| 431 |
+
wandb.log({"per iteration time": current_time - self.previous_time}, step=self.step)
|
| 432 |
+
self.previous_time = current_time
|
DecMem/trainer/distillation.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gc
|
| 2 |
+
import logging
|
| 3 |
+
|
| 4 |
+
from utils.dataset import ShardingLMDBDataset, cycle
|
| 5 |
+
from utils.dataset import TextDataset
|
| 6 |
+
from utils.distributed import EMA_FSDP, fsdp_wrap, fsdp_state_dict, launch_distributed_job
|
| 7 |
+
from utils.misc import (
|
| 8 |
+
set_seed,
|
| 9 |
+
merge_dict_list
|
| 10 |
+
)
|
| 11 |
+
import torch.distributed as dist
|
| 12 |
+
from omegaconf import OmegaConf
|
| 13 |
+
from model import CausVid, DMD, SiD
|
| 14 |
+
import torch
|
| 15 |
+
import wandb
|
| 16 |
+
import time
|
| 17 |
+
import os
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class Trainer:
|
| 21 |
+
def __init__(self, config):
|
| 22 |
+
self.config = config
|
| 23 |
+
self.step = 0
|
| 24 |
+
|
| 25 |
+
# Step 1: Initialize the distributed training environment (rank, seed, dtype, logging etc.)
|
| 26 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 27 |
+
torch.backends.cudnn.allow_tf32 = True
|
| 28 |
+
|
| 29 |
+
launch_distributed_job()
|
| 30 |
+
global_rank = dist.get_rank()
|
| 31 |
+
self.world_size = dist.get_world_size()
|
| 32 |
+
|
| 33 |
+
self.dtype = torch.bfloat16 if config.mixed_precision else torch.float32
|
| 34 |
+
self.device = torch.cuda.current_device()
|
| 35 |
+
self.is_main_process = global_rank == 0
|
| 36 |
+
self.causal = config.causal
|
| 37 |
+
self.disable_wandb = config.disable_wandb
|
| 38 |
+
|
| 39 |
+
# use a random seed for the training
|
| 40 |
+
if config.seed == 0:
|
| 41 |
+
random_seed = torch.randint(0, 10000000, (1,), device=self.device)
|
| 42 |
+
dist.broadcast(random_seed, src=0)
|
| 43 |
+
config.seed = random_seed.item()
|
| 44 |
+
|
| 45 |
+
set_seed(config.seed + global_rank)
|
| 46 |
+
|
| 47 |
+
if self.is_main_process and not self.disable_wandb:
|
| 48 |
+
wandb.login(host=config.wandb_host, key=config.wandb_key)
|
| 49 |
+
wandb.init(
|
| 50 |
+
config=OmegaConf.to_container(config, resolve=True),
|
| 51 |
+
name=config.config_name,
|
| 52 |
+
mode="online",
|
| 53 |
+
entity=config.wandb_entity,
|
| 54 |
+
project=config.wandb_project,
|
| 55 |
+
dir=config.wandb_save_dir
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
self.output_path = config.logdir
|
| 59 |
+
|
| 60 |
+
# Step 2: Initialize the model and optimizer
|
| 61 |
+
if config.distribution_loss == "causvid":
|
| 62 |
+
self.model = CausVid(config, device=self.device)
|
| 63 |
+
elif config.distribution_loss == "dmd":
|
| 64 |
+
self.model = DMD(config, device=self.device)
|
| 65 |
+
elif config.distribution_loss == "sid":
|
| 66 |
+
self.model = SiD(config, device=self.device)
|
| 67 |
+
else:
|
| 68 |
+
raise ValueError("Invalid distribution matching loss")
|
| 69 |
+
|
| 70 |
+
# Save pretrained model state_dicts to CPU
|
| 71 |
+
self.fake_score_state_dict_cpu = self.model.fake_score.state_dict()
|
| 72 |
+
|
| 73 |
+
self.model.generator = fsdp_wrap(
|
| 74 |
+
self.model.generator,
|
| 75 |
+
sharding_strategy=config.sharding_strategy,
|
| 76 |
+
mixed_precision=config.mixed_precision,
|
| 77 |
+
wrap_strategy=config.generator_fsdp_wrap_strategy
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
self.model.real_score = fsdp_wrap(
|
| 81 |
+
self.model.real_score,
|
| 82 |
+
sharding_strategy=config.sharding_strategy,
|
| 83 |
+
mixed_precision=config.mixed_precision,
|
| 84 |
+
wrap_strategy=config.real_score_fsdp_wrap_strategy
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
self.model.fake_score = fsdp_wrap(
|
| 88 |
+
self.model.fake_score,
|
| 89 |
+
sharding_strategy=config.sharding_strategy,
|
| 90 |
+
mixed_precision=config.mixed_precision,
|
| 91 |
+
wrap_strategy=config.fake_score_fsdp_wrap_strategy
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
self.model.text_encoder = fsdp_wrap(
|
| 95 |
+
self.model.text_encoder,
|
| 96 |
+
sharding_strategy=config.sharding_strategy,
|
| 97 |
+
mixed_precision=config.mixed_precision,
|
| 98 |
+
wrap_strategy=config.text_encoder_fsdp_wrap_strategy,
|
| 99 |
+
cpu_offload=getattr(config, "text_encoder_cpu_offload", False)
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
if not config.no_visualize or config.load_raw_video:
|
| 103 |
+
self.model.vae = self.model.vae.to(
|
| 104 |
+
device=self.device, dtype=torch.bfloat16 if config.mixed_precision else torch.float32)
|
| 105 |
+
|
| 106 |
+
self.generator_optimizer = torch.optim.AdamW(
|
| 107 |
+
[param for param in self.model.generator.parameters()
|
| 108 |
+
if param.requires_grad],
|
| 109 |
+
lr=config.lr,
|
| 110 |
+
betas=(config.beta1, config.beta2),
|
| 111 |
+
weight_decay=config.weight_decay
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
self.critic_optimizer = torch.optim.AdamW(
|
| 115 |
+
[param for param in self.model.fake_score.parameters()
|
| 116 |
+
if param.requires_grad],
|
| 117 |
+
lr=config.lr_critic if hasattr(config, "lr_critic") else config.lr,
|
| 118 |
+
betas=(config.beta1_critic, config.beta2_critic),
|
| 119 |
+
weight_decay=config.weight_decay
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
# Step 3: Initialize the dataloader
|
| 123 |
+
if self.config.i2v:
|
| 124 |
+
dataset = ShardingLMDBDataset(config.data_path, max_pair=int(1e8))
|
| 125 |
+
else:
|
| 126 |
+
dataset = TextDataset(config.data_path)
|
| 127 |
+
sampler = torch.utils.data.distributed.DistributedSampler(
|
| 128 |
+
dataset, shuffle=True, drop_last=True)
|
| 129 |
+
dataloader = torch.utils.data.DataLoader(
|
| 130 |
+
dataset,
|
| 131 |
+
batch_size=config.batch_size,
|
| 132 |
+
sampler=sampler,
|
| 133 |
+
num_workers=8)
|
| 134 |
+
|
| 135 |
+
if dist.get_rank() == 0:
|
| 136 |
+
print("DATASET SIZE %d" % len(dataset))
|
| 137 |
+
self.dataloader = cycle(dataloader)
|
| 138 |
+
|
| 139 |
+
##############################################################################################################
|
| 140 |
+
# 6. Set up EMA parameter containers
|
| 141 |
+
rename_param = (
|
| 142 |
+
lambda name: name.replace("_fsdp_wrapped_module.", "")
|
| 143 |
+
.replace("_checkpoint_wrapped_module.", "")
|
| 144 |
+
.replace("_orig_mod.", "")
|
| 145 |
+
)
|
| 146 |
+
self.name_to_trainable_params = {}
|
| 147 |
+
for n, p in self.model.generator.named_parameters():
|
| 148 |
+
if not p.requires_grad:
|
| 149 |
+
continue
|
| 150 |
+
|
| 151 |
+
renamed_n = rename_param(n)
|
| 152 |
+
self.name_to_trainable_params[renamed_n] = p
|
| 153 |
+
ema_weight = config.ema_weight
|
| 154 |
+
self.generator_ema = None
|
| 155 |
+
# if (ema_weight is not None) and (ema_weight > 0.0):
|
| 156 |
+
# print(f"Setting up EMA with weight {ema_weight}")
|
| 157 |
+
# self.generator_ema = EMA_FSDP(self.model.generator, decay=ema_weight)
|
| 158 |
+
|
| 159 |
+
##############################################################################################################
|
| 160 |
+
# 7. (If resuming) Load the model and optimizer, lr_scheduler, ema's statedicts
|
| 161 |
+
if getattr(config, "generator_ckpt", False):
|
| 162 |
+
print(f"Loading pretrained generator from {config.generator_ckpt}")
|
| 163 |
+
state_dict = torch.load(config.generator_ckpt, map_location="cpu")
|
| 164 |
+
if "generator" in state_dict:
|
| 165 |
+
state_dict = state_dict["generator"]
|
| 166 |
+
elif "model" in state_dict:
|
| 167 |
+
state_dict = state_dict["model"]
|
| 168 |
+
self.model.generator.load_state_dict(
|
| 169 |
+
state_dict, strict=True
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
##############################################################################################################
|
| 173 |
+
|
| 174 |
+
# # Let's delete EMA params for early steps to save some computes at training and inference
|
| 175 |
+
# if self.step < config.ema_start_step:
|
| 176 |
+
# self.generator_ema = None
|
| 177 |
+
|
| 178 |
+
self.max_grad_norm_generator = getattr(config, "max_grad_norm_generator", 10.0)
|
| 179 |
+
self.max_grad_norm_critic = getattr(config, "max_grad_norm_critic", 10.0)
|
| 180 |
+
self.previous_time = None
|
| 181 |
+
|
| 182 |
+
def save(self):
|
| 183 |
+
print("Start gathering distributed model states...")
|
| 184 |
+
generator_state_dict = fsdp_state_dict(
|
| 185 |
+
self.model.generator)
|
| 186 |
+
critic_state_dict = fsdp_state_dict(
|
| 187 |
+
self.model.fake_score)
|
| 188 |
+
state_dict = {
|
| 189 |
+
"generator": generator_state_dict,
|
| 190 |
+
"critic": critic_state_dict,
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
if self.is_main_process:
|
| 194 |
+
os.makedirs(os.path.join(self.output_path,
|
| 195 |
+
f"checkpoint_model_{self.step:06d}"), exist_ok=True)
|
| 196 |
+
torch.save(state_dict, os.path.join(self.output_path,
|
| 197 |
+
f"checkpoint_model_{self.step:06d}", "model.pt"))
|
| 198 |
+
print("Model saved to", os.path.join(self.output_path,
|
| 199 |
+
f"checkpoint_model_{self.step:06d}", "model.pt"))
|
| 200 |
+
|
| 201 |
+
def fwdbwd_one_step(self, batch, train_generator):
|
| 202 |
+
self.model.eval() # prevent any randomness (e.g. dropout)
|
| 203 |
+
|
| 204 |
+
if self.step % 20 == 0:
|
| 205 |
+
torch.cuda.empty_cache()
|
| 206 |
+
|
| 207 |
+
# Step 1: Get the next batch of text prompts
|
| 208 |
+
text_prompts = batch["prompts"]
|
| 209 |
+
if self.config.i2v:
|
| 210 |
+
clean_latent = None
|
| 211 |
+
image_latent = batch["ode_latent"][:, -1][:, 0:1, ].to(
|
| 212 |
+
device=self.device, dtype=self.dtype)
|
| 213 |
+
else:
|
| 214 |
+
clean_latent = None
|
| 215 |
+
image_latent = None
|
| 216 |
+
|
| 217 |
+
batch_size = len(text_prompts)
|
| 218 |
+
image_or_video_shape = list(self.config.image_or_video_shape)
|
| 219 |
+
image_or_video_shape[0] = batch_size
|
| 220 |
+
|
| 221 |
+
# Step 2: Extract the conditional infos
|
| 222 |
+
with torch.no_grad():
|
| 223 |
+
conditional_dict = self.model.text_encoder(
|
| 224 |
+
text_prompts=text_prompts)
|
| 225 |
+
|
| 226 |
+
if not getattr(self, "unconditional_dict", None):
|
| 227 |
+
unconditional_dict = self.model.text_encoder(
|
| 228 |
+
text_prompts=[self.config.negative_prompt] * batch_size)
|
| 229 |
+
unconditional_dict = {k: v.detach()
|
| 230 |
+
for k, v in unconditional_dict.items()}
|
| 231 |
+
self.unconditional_dict = unconditional_dict # cache the unconditional_dict
|
| 232 |
+
else:
|
| 233 |
+
unconditional_dict = self.unconditional_dict
|
| 234 |
+
|
| 235 |
+
# Step 3: Store gradients for the generator (if training the generator)
|
| 236 |
+
if train_generator:
|
| 237 |
+
generator_loss, generator_log_dict = self.model.generator_loss(
|
| 238 |
+
image_or_video_shape=image_or_video_shape,
|
| 239 |
+
conditional_dict=conditional_dict,
|
| 240 |
+
unconditional_dict=unconditional_dict,
|
| 241 |
+
clean_latent=clean_latent,
|
| 242 |
+
initial_latent=image_latent if self.config.i2v else None
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
generator_loss.backward()
|
| 246 |
+
generator_grad_norm = self.model.generator.clip_grad_norm_(
|
| 247 |
+
self.max_grad_norm_generator)
|
| 248 |
+
|
| 249 |
+
generator_log_dict.update({"generator_loss": generator_loss,
|
| 250 |
+
"generator_grad_norm": generator_grad_norm})
|
| 251 |
+
|
| 252 |
+
return generator_log_dict
|
| 253 |
+
else:
|
| 254 |
+
generator_log_dict = {}
|
| 255 |
+
|
| 256 |
+
# Step 4: Store gradients for the critic (if training the critic)
|
| 257 |
+
critic_loss, critic_log_dict = self.model.critic_loss(
|
| 258 |
+
image_or_video_shape=image_or_video_shape,
|
| 259 |
+
conditional_dict=conditional_dict,
|
| 260 |
+
unconditional_dict=unconditional_dict,
|
| 261 |
+
clean_latent=clean_latent,
|
| 262 |
+
initial_latent=image_latent if self.config.i2v else None
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
critic_loss.backward()
|
| 266 |
+
critic_grad_norm = self.model.fake_score.clip_grad_norm_(
|
| 267 |
+
self.max_grad_norm_critic)
|
| 268 |
+
|
| 269 |
+
critic_log_dict.update({"critic_loss": critic_loss,
|
| 270 |
+
"critic_grad_norm": critic_grad_norm})
|
| 271 |
+
|
| 272 |
+
return critic_log_dict
|
| 273 |
+
|
| 274 |
+
def generate_video(self, pipeline, prompts, image=None):
|
| 275 |
+
batch_size = len(prompts)
|
| 276 |
+
if image is not None:
|
| 277 |
+
image = image.squeeze(0).unsqueeze(0).unsqueeze(2).to(device="cuda", dtype=torch.bfloat16)
|
| 278 |
+
|
| 279 |
+
# Encode the input image as the first latent
|
| 280 |
+
initial_latent = pipeline.vae.encode_to_latent(image).to(device="cuda", dtype=torch.bfloat16)
|
| 281 |
+
initial_latent = initial_latent.repeat(batch_size, 1, 1, 1, 1)
|
| 282 |
+
sampled_noise = torch.randn(
|
| 283 |
+
[batch_size, self.model.num_training_frames - 1, 16, 60, 104],
|
| 284 |
+
device="cuda",
|
| 285 |
+
dtype=self.dtype
|
| 286 |
+
)
|
| 287 |
+
else:
|
| 288 |
+
initial_latent = None
|
| 289 |
+
sampled_noise = torch.randn(
|
| 290 |
+
[batch_size, self.model.num_training_frames, 16, 60, 104],
|
| 291 |
+
device="cuda",
|
| 292 |
+
dtype=self.dtype
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
video, _ = pipeline.inference(
|
| 296 |
+
noise=sampled_noise,
|
| 297 |
+
text_prompts=prompts,
|
| 298 |
+
return_latents=True,
|
| 299 |
+
initial_latent=initial_latent
|
| 300 |
+
)
|
| 301 |
+
current_video = video.permute(0, 1, 3, 4, 2).cpu().numpy() * 255.0
|
| 302 |
+
return current_video
|
| 303 |
+
|
| 304 |
+
def train(self):
|
| 305 |
+
start_step = self.step
|
| 306 |
+
|
| 307 |
+
while True:
|
| 308 |
+
TRAIN_GENERATOR = self.step % self.config.dfake_gen_update_ratio == 0
|
| 309 |
+
|
| 310 |
+
# Train the generator
|
| 311 |
+
if TRAIN_GENERATOR:
|
| 312 |
+
self.generator_optimizer.zero_grad(set_to_none=True)
|
| 313 |
+
extras_list = []
|
| 314 |
+
batch = next(self.dataloader)
|
| 315 |
+
extra = self.fwdbwd_one_step(batch, True)
|
| 316 |
+
extras_list.append(extra)
|
| 317 |
+
generator_log_dict = merge_dict_list(extras_list)
|
| 318 |
+
self.generator_optimizer.step()
|
| 319 |
+
# if self.generator_ema is not None:
|
| 320 |
+
# self.generator_ema.update(self.model.generator)
|
| 321 |
+
|
| 322 |
+
# Train the critic
|
| 323 |
+
self.critic_optimizer.zero_grad(set_to_none=True)
|
| 324 |
+
extras_list = []
|
| 325 |
+
batch = next(self.dataloader)
|
| 326 |
+
extra = self.fwdbwd_one_step(batch, False)
|
| 327 |
+
extras_list.append(extra)
|
| 328 |
+
critic_log_dict = merge_dict_list(extras_list)
|
| 329 |
+
self.critic_optimizer.step()
|
| 330 |
+
|
| 331 |
+
# Increment the step since we finished gradient update
|
| 332 |
+
self.step += 1
|
| 333 |
+
|
| 334 |
+
# # Create EMA params (if not already created)
|
| 335 |
+
# if (self.step >= self.config.ema_start_step) and \
|
| 336 |
+
# (self.generator_ema is None) and (self.config.ema_weight > 0):
|
| 337 |
+
# self.generator_ema = EMA_FSDP(self.model.generator, decay=self.config.ema_weight)
|
| 338 |
+
|
| 339 |
+
# Save the model
|
| 340 |
+
if (not self.config.no_save) and (self.step - start_step) > 0 and self.step % self.config.log_iters == 0:
|
| 341 |
+
torch.cuda.empty_cache()
|
| 342 |
+
self.save()
|
| 343 |
+
torch.cuda.empty_cache()
|
| 344 |
+
|
| 345 |
+
# Logging
|
| 346 |
+
if self.is_main_process:
|
| 347 |
+
wandb_loss_dict = {}
|
| 348 |
+
if TRAIN_GENERATOR:
|
| 349 |
+
wandb_loss_dict.update(
|
| 350 |
+
{
|
| 351 |
+
"generator_loss": generator_log_dict["generator_loss"].mean().item(),
|
| 352 |
+
"generator_grad_norm": generator_log_dict["generator_grad_norm"].mean().item(),
|
| 353 |
+
"dmdtrain_gradient_norm": generator_log_dict["dmdtrain_gradient_norm"].mean().item()
|
| 354 |
+
}
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
wandb_loss_dict.update(
|
| 358 |
+
{
|
| 359 |
+
"critic_loss": critic_log_dict["critic_loss"].mean().item(),
|
| 360 |
+
"critic_grad_norm": critic_log_dict["critic_grad_norm"].mean().item()
|
| 361 |
+
}
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
if not self.disable_wandb:
|
| 365 |
+
wandb.log(wandb_loss_dict, step=self.step)
|
| 366 |
+
|
| 367 |
+
if self.step % self.config.gc_interval == 0:
|
| 368 |
+
if dist.get_rank() == 0:
|
| 369 |
+
logging.info("DistGarbageCollector: Running GC.")
|
| 370 |
+
gc.collect()
|
| 371 |
+
torch.cuda.empty_cache()
|
| 372 |
+
|
| 373 |
+
if self.is_main_process:
|
| 374 |
+
current_time = time.time()
|
| 375 |
+
if self.previous_time is None:
|
| 376 |
+
self.previous_time = current_time
|
| 377 |
+
else:
|
| 378 |
+
if not self.disable_wandb:
|
| 379 |
+
wandb.log({"per iteration time": current_time - self.previous_time}, step=self.step)
|
| 380 |
+
self.previous_time = current_time
|
DecMem/trainer/gan.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gc
|
| 2 |
+
import logging
|
| 3 |
+
|
| 4 |
+
from utils.dataset import ShardingLMDBDataset, cycle
|
| 5 |
+
from utils.distributed import EMA_FSDP, fsdp_wrap, fsdp_state_dict, launch_distributed_job
|
| 6 |
+
from utils.misc import (
|
| 7 |
+
set_seed,
|
| 8 |
+
merge_dict_list
|
| 9 |
+
)
|
| 10 |
+
import torch.distributed as dist
|
| 11 |
+
from omegaconf import OmegaConf
|
| 12 |
+
from model import GAN
|
| 13 |
+
import torch
|
| 14 |
+
import wandb
|
| 15 |
+
import time
|
| 16 |
+
import os
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Trainer:
|
| 20 |
+
def __init__(self, config):
|
| 21 |
+
self.config = config
|
| 22 |
+
self.step = 0
|
| 23 |
+
|
| 24 |
+
# Step 1: Initialize the distributed training environment (rank, seed, dtype, logging etc.)
|
| 25 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 26 |
+
torch.backends.cudnn.allow_tf32 = True
|
| 27 |
+
|
| 28 |
+
launch_distributed_job()
|
| 29 |
+
global_rank = dist.get_rank()
|
| 30 |
+
self.world_size = dist.get_world_size()
|
| 31 |
+
|
| 32 |
+
self.dtype = torch.bfloat16 if config.mixed_precision else torch.float32
|
| 33 |
+
self.device = torch.cuda.current_device()
|
| 34 |
+
self.is_main_process = global_rank == 0
|
| 35 |
+
self.causal = config.causal
|
| 36 |
+
self.disable_wandb = config.disable_wandb
|
| 37 |
+
|
| 38 |
+
# Configuration for discriminator warmup
|
| 39 |
+
self.discriminator_warmup_steps = getattr(config, "discriminator_warmup_steps", 0)
|
| 40 |
+
self.in_discriminator_warmup = self.step < self.discriminator_warmup_steps
|
| 41 |
+
if self.in_discriminator_warmup and self.is_main_process:
|
| 42 |
+
print(f"Starting with discriminator warmup for {self.discriminator_warmup_steps} steps")
|
| 43 |
+
self.loss_scale = getattr(config, "loss_scale", 1.0)
|
| 44 |
+
|
| 45 |
+
# use a random seed for the training
|
| 46 |
+
if config.seed == 0:
|
| 47 |
+
random_seed = torch.randint(0, 10000000, (1,), device=self.device)
|
| 48 |
+
dist.broadcast(random_seed, src=0)
|
| 49 |
+
config.seed = random_seed.item()
|
| 50 |
+
|
| 51 |
+
set_seed(config.seed + global_rank)
|
| 52 |
+
|
| 53 |
+
if self.is_main_process and not self.disable_wandb:
|
| 54 |
+
wandb.login(host=config.wandb_host, key=config.wandb_key)
|
| 55 |
+
wandb.init(
|
| 56 |
+
config=OmegaConf.to_container(config, resolve=True),
|
| 57 |
+
name=config.config_name,
|
| 58 |
+
mode="online",
|
| 59 |
+
entity=config.wandb_entity,
|
| 60 |
+
project=config.wandb_project,
|
| 61 |
+
dir=config.wandb_save_dir
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
self.output_path = config.logdir
|
| 65 |
+
|
| 66 |
+
# Step 2: Initialize the model and optimizer
|
| 67 |
+
self.model = GAN(config, device=self.device)
|
| 68 |
+
|
| 69 |
+
self.model.generator = fsdp_wrap(
|
| 70 |
+
self.model.generator,
|
| 71 |
+
sharding_strategy=config.sharding_strategy,
|
| 72 |
+
mixed_precision=config.mixed_precision,
|
| 73 |
+
wrap_strategy=config.generator_fsdp_wrap_strategy
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
self.model.fake_score = fsdp_wrap(
|
| 77 |
+
self.model.fake_score,
|
| 78 |
+
sharding_strategy=config.sharding_strategy,
|
| 79 |
+
mixed_precision=config.mixed_precision,
|
| 80 |
+
wrap_strategy=config.fake_score_fsdp_wrap_strategy
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
self.model.text_encoder = fsdp_wrap(
|
| 84 |
+
self.model.text_encoder,
|
| 85 |
+
sharding_strategy=config.sharding_strategy,
|
| 86 |
+
mixed_precision=config.mixed_precision,
|
| 87 |
+
wrap_strategy=config.text_encoder_fsdp_wrap_strategy,
|
| 88 |
+
cpu_offload=getattr(config, "text_encoder_cpu_offload", False)
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
if not config.no_visualize or config.load_raw_video:
|
| 92 |
+
self.model.vae = self.model.vae.to(
|
| 93 |
+
device=self.device, dtype=torch.bfloat16 if config.mixed_precision else torch.float32)
|
| 94 |
+
|
| 95 |
+
self.generator_optimizer = torch.optim.AdamW(
|
| 96 |
+
[param for param in self.model.generator.parameters()
|
| 97 |
+
if param.requires_grad],
|
| 98 |
+
lr=config.gen_lr,
|
| 99 |
+
betas=(config.beta1, config.beta2)
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# Create separate parameter groups for the fake_score network
|
| 103 |
+
# One group for parameters with "_cls_pred_branch" or "_gan_ca_blocks" in the name
|
| 104 |
+
# and another group for all other parameters
|
| 105 |
+
fake_score_params = []
|
| 106 |
+
discriminator_params = []
|
| 107 |
+
|
| 108 |
+
for name, param in self.model.fake_score.named_parameters():
|
| 109 |
+
if param.requires_grad:
|
| 110 |
+
if "_cls_pred_branch" in name or "_gan_ca_blocks" in name:
|
| 111 |
+
discriminator_params.append(param)
|
| 112 |
+
else:
|
| 113 |
+
fake_score_params.append(param)
|
| 114 |
+
|
| 115 |
+
# Use the special learning rate for the special parameter group
|
| 116 |
+
# and the default critic learning rate for other parameters
|
| 117 |
+
self.critic_param_groups = [
|
| 118 |
+
{'params': fake_score_params, 'lr': config.critic_lr},
|
| 119 |
+
{'params': discriminator_params, 'lr': config.critic_lr * config.discriminator_lr_multiplier}
|
| 120 |
+
]
|
| 121 |
+
if self.in_discriminator_warmup:
|
| 122 |
+
self.critic_optimizer = torch.optim.AdamW(
|
| 123 |
+
self.critic_param_groups,
|
| 124 |
+
betas=(0.9, config.beta2_critic)
|
| 125 |
+
)
|
| 126 |
+
else:
|
| 127 |
+
self.critic_optimizer = torch.optim.AdamW(
|
| 128 |
+
self.critic_param_groups,
|
| 129 |
+
betas=(config.beta1_critic, config.beta2_critic)
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# Step 3: Initialize the dataloader
|
| 133 |
+
self.data_path = config.data_path
|
| 134 |
+
dataset = ShardingLMDBDataset(config.data_path, max_pair=int(1e8))
|
| 135 |
+
sampler = torch.utils.data.distributed.DistributedSampler(
|
| 136 |
+
dataset, shuffle=True, drop_last=True)
|
| 137 |
+
dataloader = torch.utils.data.DataLoader(
|
| 138 |
+
dataset,
|
| 139 |
+
batch_size=config.batch_size,
|
| 140 |
+
sampler=sampler,
|
| 141 |
+
num_workers=8)
|
| 142 |
+
|
| 143 |
+
if dist.get_rank() == 0:
|
| 144 |
+
print("DATASET SIZE %d" % len(dataset))
|
| 145 |
+
|
| 146 |
+
self.dataloader = cycle(dataloader)
|
| 147 |
+
|
| 148 |
+
##############################################################################################################
|
| 149 |
+
# 6. Set up EMA parameter containers
|
| 150 |
+
rename_param = (
|
| 151 |
+
lambda name: name.replace("_fsdp_wrapped_module.", "")
|
| 152 |
+
.replace("_checkpoint_wrapped_module.", "")
|
| 153 |
+
.replace("_orig_mod.", "")
|
| 154 |
+
)
|
| 155 |
+
self.name_to_trainable_params = {}
|
| 156 |
+
for n, p in self.model.generator.named_parameters():
|
| 157 |
+
if not p.requires_grad:
|
| 158 |
+
continue
|
| 159 |
+
|
| 160 |
+
renamed_n = rename_param(n)
|
| 161 |
+
self.name_to_trainable_params[renamed_n] = p
|
| 162 |
+
ema_weight = config.ema_weight
|
| 163 |
+
self.generator_ema = None
|
| 164 |
+
if (ema_weight is not None) and (ema_weight > 0.0):
|
| 165 |
+
print(f"Setting up EMA with weight {ema_weight}")
|
| 166 |
+
self.generator_ema = EMA_FSDP(self.model.generator, decay=ema_weight)
|
| 167 |
+
|
| 168 |
+
##############################################################################################################
|
| 169 |
+
# 7. (If resuming) Load the model and optimizer, lr_scheduler, ema's statedicts
|
| 170 |
+
if getattr(config, "generator_ckpt", False):
|
| 171 |
+
print(f"Loading pretrained generator from {config.generator_ckpt}")
|
| 172 |
+
state_dict = torch.load(config.generator_ckpt, map_location="cpu")
|
| 173 |
+
if "generator" in state_dict:
|
| 174 |
+
state_dict = state_dict["generator"]
|
| 175 |
+
elif "model" in state_dict:
|
| 176 |
+
state_dict = state_dict["model"]
|
| 177 |
+
self.model.generator.load_state_dict(
|
| 178 |
+
state_dict, strict=True
|
| 179 |
+
)
|
| 180 |
+
if hasattr(config, "load"):
|
| 181 |
+
resume_ckpt_path_critic = os.path.join(config.load, "critic")
|
| 182 |
+
resume_ckpt_path_generator = os.path.join(config.load, "generator")
|
| 183 |
+
else:
|
| 184 |
+
resume_ckpt_path_critic = "none"
|
| 185 |
+
resume_ckpt_path_generator = "none"
|
| 186 |
+
|
| 187 |
+
_, _ = self.checkpointer_critic.try_best_load(
|
| 188 |
+
resume_ckpt_path=resume_ckpt_path_critic,
|
| 189 |
+
)
|
| 190 |
+
self.step, _ = self.checkpointer_generator.try_best_load(
|
| 191 |
+
resume_ckpt_path=resume_ckpt_path_generator,
|
| 192 |
+
force_start_w_ema=config.force_start_w_ema,
|
| 193 |
+
force_reset_zero_step=config.force_reset_zero_step,
|
| 194 |
+
force_reinit_ema=config.force_reinit_ema,
|
| 195 |
+
skip_optimizer_scheduler=config.skip_optimizer_scheduler,
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
##############################################################################################################
|
| 199 |
+
|
| 200 |
+
# # Let's delete EMA params for early steps to save some computes at training and inference
|
| 201 |
+
# if self.step < config.ema_start_step:
|
| 202 |
+
# self.generator_ema = None
|
| 203 |
+
|
| 204 |
+
self.max_grad_norm_generator = getattr(config, "max_grad_norm_generator", 10.0)
|
| 205 |
+
self.max_grad_norm_critic = getattr(config, "max_grad_norm_critic", 10.0)
|
| 206 |
+
self.previous_time = None
|
| 207 |
+
|
| 208 |
+
def save(self):
|
| 209 |
+
print("Start gathering distributed model states...")
|
| 210 |
+
generator_state_dict = fsdp_state_dict(
|
| 211 |
+
self.model.generator)
|
| 212 |
+
critic_state_dict = fsdp_state_dict(
|
| 213 |
+
self.model.fake_score)
|
| 214 |
+
|
| 215 |
+
state_dict = {
|
| 216 |
+
"generator": generator_state_dict,
|
| 217 |
+
"critic": critic_state_dict,
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
if self.is_main_process:
|
| 221 |
+
os.makedirs(os.path.join(self.output_path,
|
| 222 |
+
f"checkpoint_model_{self.step:06d}"), exist_ok=True)
|
| 223 |
+
torch.save(state_dict, os.path.join(self.output_path,
|
| 224 |
+
f"checkpoint_model_{self.step:06d}", "model.pt"))
|
| 225 |
+
print("Model saved to", os.path.join(self.output_path,
|
| 226 |
+
f"checkpoint_model_{self.step:06d}", "model.pt"))
|
| 227 |
+
|
| 228 |
+
def fwdbwd_one_step(self, batch, train_generator):
|
| 229 |
+
self.model.eval() # prevent any randomness (e.g. dropout)
|
| 230 |
+
|
| 231 |
+
if self.step % 20 == 0:
|
| 232 |
+
torch.cuda.empty_cache()
|
| 233 |
+
|
| 234 |
+
# Step 1: Get the next batch of text prompts
|
| 235 |
+
text_prompts = batch["prompts"] # next(self.dataloader)
|
| 236 |
+
if "ode_latent" in batch:
|
| 237 |
+
clean_latent = batch["ode_latent"][:, -1].to(device=self.device, dtype=self.dtype)
|
| 238 |
+
else:
|
| 239 |
+
frames = batch["frames"].to(device=self.device, dtype=self.dtype)
|
| 240 |
+
with torch.no_grad():
|
| 241 |
+
clean_latent = self.model.vae.encode_to_latent(
|
| 242 |
+
frames).to(device=self.device, dtype=self.dtype)
|
| 243 |
+
|
| 244 |
+
image_latent = clean_latent[:, 0:1, ]
|
| 245 |
+
|
| 246 |
+
batch_size = len(text_prompts)
|
| 247 |
+
image_or_video_shape = list(self.config.image_or_video_shape)
|
| 248 |
+
image_or_video_shape[0] = batch_size
|
| 249 |
+
|
| 250 |
+
# Step 2: Extract the conditional infos
|
| 251 |
+
with torch.no_grad():
|
| 252 |
+
conditional_dict = self.model.text_encoder(
|
| 253 |
+
text_prompts=text_prompts)
|
| 254 |
+
|
| 255 |
+
if not getattr(self, "unconditional_dict", None):
|
| 256 |
+
unconditional_dict = self.model.text_encoder(
|
| 257 |
+
text_prompts=[self.config.negative_prompt] * batch_size)
|
| 258 |
+
unconditional_dict = {k: v.detach()
|
| 259 |
+
for k, v in unconditional_dict.items()}
|
| 260 |
+
self.unconditional_dict = unconditional_dict # cache the unconditional_dict
|
| 261 |
+
else:
|
| 262 |
+
unconditional_dict = self.unconditional_dict
|
| 263 |
+
|
| 264 |
+
mini_bs, full_bs = (
|
| 265 |
+
batch["mini_bs"],
|
| 266 |
+
batch["full_bs"],
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
# Step 3: Store gradients for the generator (if training the generator)
|
| 270 |
+
if train_generator:
|
| 271 |
+
gan_G_loss = self.model.generator_loss(
|
| 272 |
+
image_or_video_shape=image_or_video_shape,
|
| 273 |
+
conditional_dict=conditional_dict,
|
| 274 |
+
unconditional_dict=unconditional_dict,
|
| 275 |
+
clean_latent=clean_latent,
|
| 276 |
+
initial_latent=image_latent if self.config.i2v else None
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
loss_ratio = mini_bs * self.world_size / full_bs
|
| 280 |
+
total_loss = gan_G_loss * loss_ratio * self.loss_scale
|
| 281 |
+
|
| 282 |
+
total_loss.backward()
|
| 283 |
+
generator_grad_norm = self.model.generator.clip_grad_norm_(
|
| 284 |
+
self.max_grad_norm_generator)
|
| 285 |
+
|
| 286 |
+
generator_log_dict = {"generator_grad_norm": generator_grad_norm,
|
| 287 |
+
"gan_G_loss": gan_G_loss}
|
| 288 |
+
|
| 289 |
+
return generator_log_dict
|
| 290 |
+
else:
|
| 291 |
+
generator_log_dict = {}
|
| 292 |
+
|
| 293 |
+
# Step 4: Store gradients for the critic (if training the critic)
|
| 294 |
+
(gan_D_loss, r1_loss, r2_loss), critic_log_dict = self.model.critic_loss(
|
| 295 |
+
image_or_video_shape=image_or_video_shape,
|
| 296 |
+
conditional_dict=conditional_dict,
|
| 297 |
+
unconditional_dict=unconditional_dict,
|
| 298 |
+
clean_latent=clean_latent,
|
| 299 |
+
real_image_or_video=clean_latent,
|
| 300 |
+
initial_latent=image_latent if self.config.i2v else None
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
loss_ratio = mini_bs * dist.get_world_size() / full_bs
|
| 304 |
+
total_loss = (gan_D_loss + 0.5 * (r1_loss + r2_loss)) * loss_ratio * self.loss_scale
|
| 305 |
+
|
| 306 |
+
total_loss.backward()
|
| 307 |
+
critic_grad_norm = self.model.fake_score.clip_grad_norm_(
|
| 308 |
+
self.max_grad_norm_critic)
|
| 309 |
+
|
| 310 |
+
critic_log_dict.update({"critic_grad_norm": critic_grad_norm,
|
| 311 |
+
"gan_D_loss": gan_D_loss,
|
| 312 |
+
"r1_loss": r1_loss,
|
| 313 |
+
"r2_loss": r2_loss})
|
| 314 |
+
|
| 315 |
+
return critic_log_dict
|
| 316 |
+
|
| 317 |
+
def generate_video(self, pipeline, prompts, image=None):
|
| 318 |
+
batch_size = len(prompts)
|
| 319 |
+
sampled_noise = torch.randn(
|
| 320 |
+
[batch_size, 21, 16, 60, 104], device="cuda", dtype=self.dtype
|
| 321 |
+
)
|
| 322 |
+
video, _ = pipeline.inference(
|
| 323 |
+
noise=sampled_noise,
|
| 324 |
+
text_prompts=prompts,
|
| 325 |
+
return_latents=True
|
| 326 |
+
)
|
| 327 |
+
current_video = video.permute(0, 1, 3, 4, 2).cpu().numpy() * 255.0
|
| 328 |
+
return current_video
|
| 329 |
+
|
| 330 |
+
def train(self):
|
| 331 |
+
start_step = self.step
|
| 332 |
+
|
| 333 |
+
while True:
|
| 334 |
+
if self.step == self.discriminator_warmup_steps and self.discriminator_warmup_steps != 0:
|
| 335 |
+
print("Resetting critic optimizer")
|
| 336 |
+
del self.critic_optimizer
|
| 337 |
+
torch.cuda.empty_cache()
|
| 338 |
+
# Create new optimizers
|
| 339 |
+
self.critic_optimizer = torch.optim.AdamW(
|
| 340 |
+
self.critic_param_groups,
|
| 341 |
+
betas=(self.config.beta1_critic, self.config.beta2_critic)
|
| 342 |
+
)
|
| 343 |
+
# Update checkpointer references
|
| 344 |
+
self.checkpointer_critic.optimizer = self.critic_optimizer
|
| 345 |
+
# Check if we're in the discriminator warmup phase
|
| 346 |
+
self.in_discriminator_warmup = self.step < self.discriminator_warmup_steps
|
| 347 |
+
|
| 348 |
+
# Only update generator and critic outside the warmup phase
|
| 349 |
+
TRAIN_GENERATOR = not self.in_discriminator_warmup and self.step % self.config.dfake_gen_update_ratio == 0
|
| 350 |
+
|
| 351 |
+
# Train the generator (only outside warmup phase)
|
| 352 |
+
if TRAIN_GENERATOR:
|
| 353 |
+
self.model.fake_score.requires_grad_(False)
|
| 354 |
+
self.model.generator.requires_grad_(True)
|
| 355 |
+
self.generator_optimizer.zero_grad(set_to_none=True)
|
| 356 |
+
extras_list = []
|
| 357 |
+
for ii, mini_batch in enumerate(self.dataloader.next()):
|
| 358 |
+
extra = self.fwdbwd_one_step(mini_batch, True)
|
| 359 |
+
extras_list.append(extra)
|
| 360 |
+
generator_log_dict = merge_dict_list(extras_list)
|
| 361 |
+
self.generator_optimizer.step()
|
| 362 |
+
# if self.generator_ema is not None:
|
| 363 |
+
# self.generator_ema.update(self.model.generator)
|
| 364 |
+
else:
|
| 365 |
+
generator_log_dict = {}
|
| 366 |
+
|
| 367 |
+
# Train the critic/discriminator
|
| 368 |
+
if self.in_discriminator_warmup:
|
| 369 |
+
# During warmup, only allow gradient for discriminator params
|
| 370 |
+
self.model.generator.requires_grad_(False)
|
| 371 |
+
self.model.fake_score.requires_grad_(False)
|
| 372 |
+
|
| 373 |
+
# Enable gradient only for discriminator params
|
| 374 |
+
for name, param in self.model.fake_score.named_parameters():
|
| 375 |
+
if "_cls_pred_branch" in name or "_gan_ca_blocks" in name:
|
| 376 |
+
param.requires_grad_(True)
|
| 377 |
+
else:
|
| 378 |
+
# Normal training mode
|
| 379 |
+
self.model.generator.requires_grad_(False)
|
| 380 |
+
self.model.fake_score.requires_grad_(True)
|
| 381 |
+
|
| 382 |
+
self.critic_optimizer.zero_grad(set_to_none=True)
|
| 383 |
+
extras_list = []
|
| 384 |
+
batch = next(self.dataloader)
|
| 385 |
+
extra = self.fwdbwd_one_step(batch, False)
|
| 386 |
+
extras_list.append(extra)
|
| 387 |
+
critic_log_dict = merge_dict_list(extras_list)
|
| 388 |
+
self.critic_optimizer.step()
|
| 389 |
+
|
| 390 |
+
# Increment the step since we finished gradient update
|
| 391 |
+
self.step += 1
|
| 392 |
+
|
| 393 |
+
# If we just finished warmup, print a message
|
| 394 |
+
if self.is_main_process and self.step == self.discriminator_warmup_steps:
|
| 395 |
+
print(f"Finished discriminator warmup after {self.discriminator_warmup_steps} steps")
|
| 396 |
+
|
| 397 |
+
# Save the model
|
| 398 |
+
if (not self.config.no_save) and (self.step - start_step) > 0 and self.step % self.config.log_iters == 0:
|
| 399 |
+
torch.cuda.empty_cache()
|
| 400 |
+
self.save()
|
| 401 |
+
torch.cuda.empty_cache()
|
| 402 |
+
|
| 403 |
+
# Logging
|
| 404 |
+
wandb_loss_dict = {
|
| 405 |
+
"generator_grad_norm": generator_log_dict["generator_grad_norm"],
|
| 406 |
+
"critic_grad_norm": critic_log_dict["critic_grad_norm"],
|
| 407 |
+
"real_logit": critic_log_dict["noisy_real_logit"],
|
| 408 |
+
"fake_logit": critic_log_dict["noisy_fake_logit"],
|
| 409 |
+
"r1_loss": critic_log_dict["r1_loss"],
|
| 410 |
+
"r2_loss": critic_log_dict["r2_loss"],
|
| 411 |
+
}
|
| 412 |
+
if TRAIN_GENERATOR:
|
| 413 |
+
wandb_loss_dict.update({
|
| 414 |
+
"generator_grad_norm": generator_log_dict["generator_grad_norm"],
|
| 415 |
+
})
|
| 416 |
+
self.all_gather_dict(wandb_loss_dict)
|
| 417 |
+
wandb_loss_dict["diff_logit"] = wandb_loss_dict["real_logit"] - wandb_loss_dict["fake_logit"]
|
| 418 |
+
wandb_loss_dict["reg_loss"] = 0.5 * (wandb_loss_dict["r1_loss"] + wandb_loss_dict["r2_loss"])
|
| 419 |
+
|
| 420 |
+
if self.is_main_process:
|
| 421 |
+
if self.in_discriminator_warmup:
|
| 422 |
+
warmup_status = f"[WARMUP {self.step}/{self.discriminator_warmup_steps}] Training only discriminator params"
|
| 423 |
+
print(warmup_status)
|
| 424 |
+
if not self.disable_wandb:
|
| 425 |
+
wandb_loss_dict.update({"warmup_status": 1.0})
|
| 426 |
+
|
| 427 |
+
if not self.disable_wandb:
|
| 428 |
+
wandb.log(wandb_loss_dict, step=self.step)
|
| 429 |
+
|
| 430 |
+
if self.step % self.config.gc_interval == 0:
|
| 431 |
+
if dist.get_rank() == 0:
|
| 432 |
+
logging.info("DistGarbageCollector: Running GC.")
|
| 433 |
+
gc.collect()
|
| 434 |
+
torch.cuda.empty_cache()
|
| 435 |
+
|
| 436 |
+
if self.is_main_process:
|
| 437 |
+
current_time = time.time()
|
| 438 |
+
if self.previous_time is None:
|
| 439 |
+
self.previous_time = current_time
|
| 440 |
+
else:
|
| 441 |
+
if not self.disable_wandb:
|
| 442 |
+
wandb.log({"per iteration time": current_time - self.previous_time}, step=self.step)
|
| 443 |
+
self.previous_time = current_time
|
| 444 |
+
|
| 445 |
+
def all_gather_dict(self, target_dict):
|
| 446 |
+
for key, value in target_dict.items():
|
| 447 |
+
gathered_value = torch.zeros(
|
| 448 |
+
[self.world_size, *value.shape],
|
| 449 |
+
dtype=value.dtype, device=self.device)
|
| 450 |
+
dist.all_gather_into_tensor(gathered_value, value)
|
| 451 |
+
avg_value = gathered_value.mean().item()
|
| 452 |
+
target_dict[key] = avg_value
|
DecMem/trainer/ode.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gc
|
| 2 |
+
import logging
|
| 3 |
+
from utils.dataset import ODERegressionLMDBDataset, cycle
|
| 4 |
+
from model import ODERegression
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
from utils.misc import (
|
| 7 |
+
set_seed
|
| 8 |
+
)
|
| 9 |
+
import torch.distributed as dist
|
| 10 |
+
from omegaconf import OmegaConf
|
| 11 |
+
import torch
|
| 12 |
+
import wandb
|
| 13 |
+
import time
|
| 14 |
+
import os
|
| 15 |
+
|
| 16 |
+
from utils.distributed import barrier, fsdp_wrap, fsdp_state_dict, launch_distributed_job
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Trainer:
|
| 20 |
+
def __init__(self, config):
|
| 21 |
+
self.config = config
|
| 22 |
+
self.step = 0
|
| 23 |
+
|
| 24 |
+
# Step 1: Initialize the distributed training environment (rank, seed, dtype, logging etc.)
|
| 25 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 26 |
+
torch.backends.cudnn.allow_tf32 = True
|
| 27 |
+
|
| 28 |
+
launch_distributed_job()
|
| 29 |
+
global_rank = dist.get_rank()
|
| 30 |
+
self.world_size = dist.get_world_size()
|
| 31 |
+
|
| 32 |
+
self.dtype = torch.bfloat16 if config.mixed_precision else torch.float32
|
| 33 |
+
self.device = torch.cuda.current_device()
|
| 34 |
+
self.is_main_process = global_rank == 0
|
| 35 |
+
self.disable_wandb = config.disable_wandb
|
| 36 |
+
|
| 37 |
+
# use a random seed for the training
|
| 38 |
+
if config.seed == 0:
|
| 39 |
+
random_seed = torch.randint(0, 10000000, (1,), device=self.device)
|
| 40 |
+
dist.broadcast(random_seed, src=0)
|
| 41 |
+
config.seed = random_seed.item()
|
| 42 |
+
|
| 43 |
+
set_seed(config.seed + global_rank)
|
| 44 |
+
|
| 45 |
+
if self.is_main_process and not self.disable_wandb:
|
| 46 |
+
wandb.login(host=config.wandb_host, key=config.wandb_key)
|
| 47 |
+
wandb.init(
|
| 48 |
+
config=OmegaConf.to_container(config, resolve=True),
|
| 49 |
+
name=config.config_name,
|
| 50 |
+
mode="online",
|
| 51 |
+
entity=config.wandb_entity,
|
| 52 |
+
project=config.wandb_project,
|
| 53 |
+
dir=config.wandb_save_dir
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
self.output_path = config.logdir
|
| 57 |
+
|
| 58 |
+
# Step 2: Initialize the model and optimizer
|
| 59 |
+
|
| 60 |
+
assert config.distribution_loss == "ode", "Only ODE loss is supported for ODE training"
|
| 61 |
+
self.model = ODERegression(config, device=self.device)
|
| 62 |
+
|
| 63 |
+
self.model.generator = fsdp_wrap(
|
| 64 |
+
self.model.generator,
|
| 65 |
+
sharding_strategy=config.sharding_strategy,
|
| 66 |
+
mixed_precision=config.mixed_precision,
|
| 67 |
+
wrap_strategy=config.generator_fsdp_wrap_strategy
|
| 68 |
+
)
|
| 69 |
+
self.model.text_encoder = fsdp_wrap(
|
| 70 |
+
self.model.text_encoder,
|
| 71 |
+
sharding_strategy=config.sharding_strategy,
|
| 72 |
+
mixed_precision=config.mixed_precision,
|
| 73 |
+
wrap_strategy=config.text_encoder_fsdp_wrap_strategy,
|
| 74 |
+
cpu_offload=getattr(config, "text_encoder_cpu_offload", False)
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
if not config.no_visualize or config.load_raw_video:
|
| 78 |
+
self.model.vae = self.model.vae.to(
|
| 79 |
+
device=self.device, dtype=torch.bfloat16 if config.mixed_precision else torch.float32)
|
| 80 |
+
|
| 81 |
+
self.generator_optimizer = torch.optim.AdamW(
|
| 82 |
+
[param for param in self.model.generator.parameters()
|
| 83 |
+
if param.requires_grad],
|
| 84 |
+
lr=config.lr,
|
| 85 |
+
betas=(config.beta1, config.beta2),
|
| 86 |
+
weight_decay=config.weight_decay
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
# Step 3: Initialize the dataloader
|
| 90 |
+
dataset = ODERegressionLMDBDataset(
|
| 91 |
+
config.data_path, max_pair=getattr(config, "max_pair", int(1e8)))
|
| 92 |
+
sampler = torch.utils.data.distributed.DistributedSampler(
|
| 93 |
+
dataset, shuffle=True, drop_last=True)
|
| 94 |
+
dataloader = torch.utils.data.DataLoader(
|
| 95 |
+
dataset, batch_size=config.batch_size, sampler=sampler, num_workers=8)
|
| 96 |
+
total_batch_size = getattr(config, "total_batch_size", None)
|
| 97 |
+
if total_batch_size is not None:
|
| 98 |
+
assert total_batch_size == config.batch_size * self.world_size, "Gradient accumulation is not supported for ODE training"
|
| 99 |
+
self.dataloader = cycle(dataloader)
|
| 100 |
+
|
| 101 |
+
self.step = 0
|
| 102 |
+
|
| 103 |
+
##############################################################################################################
|
| 104 |
+
# 7. (If resuming) Load the model and optimizer, lr_scheduler, ema's statedicts
|
| 105 |
+
if getattr(config, "generator_ckpt", False):
|
| 106 |
+
print(f"Loading pretrained generator from {config.generator_ckpt}")
|
| 107 |
+
state_dict = torch.load(config.generator_ckpt, map_location="cpu")[
|
| 108 |
+
'generator']
|
| 109 |
+
self.model.generator.load_state_dict(
|
| 110 |
+
state_dict, strict=True
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
##############################################################################################################
|
| 114 |
+
|
| 115 |
+
self.max_grad_norm = 10.0
|
| 116 |
+
self.previous_time = None
|
| 117 |
+
|
| 118 |
+
def save(self):
|
| 119 |
+
print("Start gathering distributed model states...")
|
| 120 |
+
generator_state_dict = fsdp_state_dict(
|
| 121 |
+
self.model.generator)
|
| 122 |
+
state_dict = {
|
| 123 |
+
"generator": generator_state_dict
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
if self.is_main_process:
|
| 127 |
+
os.makedirs(os.path.join(self.output_path,
|
| 128 |
+
f"checkpoint_model_{self.step:06d}"), exist_ok=True)
|
| 129 |
+
torch.save(state_dict, os.path.join(self.output_path,
|
| 130 |
+
f"checkpoint_model_{self.step:06d}", "model.pt"))
|
| 131 |
+
print("Model saved to", os.path.join(self.output_path,
|
| 132 |
+
f"checkpoint_model_{self.step:06d}", "model.pt"))
|
| 133 |
+
|
| 134 |
+
def train_one_step(self):
|
| 135 |
+
VISUALIZE = self.step % 100 == 0
|
| 136 |
+
self.model.eval() # prevent any randomness (e.g. dropout)
|
| 137 |
+
|
| 138 |
+
# Step 1: Get the next batch of text prompts
|
| 139 |
+
batch = next(self.dataloader)
|
| 140 |
+
text_prompts = batch["prompts"]
|
| 141 |
+
ode_latent = batch["ode_latent"].to(
|
| 142 |
+
device=self.device, dtype=self.dtype)
|
| 143 |
+
|
| 144 |
+
# Step 2: Extract the conditional infos
|
| 145 |
+
with torch.no_grad():
|
| 146 |
+
conditional_dict = self.model.text_encoder(
|
| 147 |
+
text_prompts=text_prompts)
|
| 148 |
+
|
| 149 |
+
# Step 3: Train the generator
|
| 150 |
+
generator_loss, log_dict = self.model.generator_loss(
|
| 151 |
+
ode_latent=ode_latent,
|
| 152 |
+
conditional_dict=conditional_dict
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
unnormalized_loss = log_dict["unnormalized_loss"]
|
| 156 |
+
timestep = log_dict["timestep"]
|
| 157 |
+
|
| 158 |
+
if self.world_size > 1:
|
| 159 |
+
gathered_unnormalized_loss = torch.zeros(
|
| 160 |
+
[self.world_size, *unnormalized_loss.shape],
|
| 161 |
+
dtype=unnormalized_loss.dtype, device=self.device)
|
| 162 |
+
gathered_timestep = torch.zeros(
|
| 163 |
+
[self.world_size, *timestep.shape],
|
| 164 |
+
dtype=timestep.dtype, device=self.device)
|
| 165 |
+
|
| 166 |
+
dist.all_gather_into_tensor(
|
| 167 |
+
gathered_unnormalized_loss, unnormalized_loss)
|
| 168 |
+
dist.all_gather_into_tensor(gathered_timestep, timestep)
|
| 169 |
+
else:
|
| 170 |
+
gathered_unnormalized_loss = unnormalized_loss
|
| 171 |
+
gathered_timestep = timestep
|
| 172 |
+
|
| 173 |
+
loss_breakdown = defaultdict(list)
|
| 174 |
+
stats = {}
|
| 175 |
+
|
| 176 |
+
for index, t in enumerate(timestep):
|
| 177 |
+
loss_breakdown[str(int(t.item()) // 250 * 250)].append(
|
| 178 |
+
unnormalized_loss[index].item())
|
| 179 |
+
|
| 180 |
+
for key_t in loss_breakdown.keys():
|
| 181 |
+
stats["loss_at_time_" + key_t] = sum(loss_breakdown[key_t]) / \
|
| 182 |
+
len(loss_breakdown[key_t])
|
| 183 |
+
|
| 184 |
+
self.generator_optimizer.zero_grad()
|
| 185 |
+
generator_loss.backward()
|
| 186 |
+
generator_grad_norm = self.model.generator.clip_grad_norm_(
|
| 187 |
+
self.max_grad_norm)
|
| 188 |
+
self.generator_optimizer.step()
|
| 189 |
+
|
| 190 |
+
# Step 4: Visualization
|
| 191 |
+
if VISUALIZE and not self.config.no_visualize and not self.config.disable_wandb and self.is_main_process:
|
| 192 |
+
# Visualize the input, output, and ground truth
|
| 193 |
+
input = log_dict["input"]
|
| 194 |
+
output = log_dict["output"]
|
| 195 |
+
ground_truth = ode_latent[:, -1]
|
| 196 |
+
|
| 197 |
+
input_video = self.model.vae.decode_to_pixel(input)
|
| 198 |
+
output_video = self.model.vae.decode_to_pixel(output)
|
| 199 |
+
ground_truth_video = self.model.vae.decode_to_pixel(ground_truth)
|
| 200 |
+
input_video = 255.0 * (input_video.cpu().numpy() * 0.5 + 0.5)
|
| 201 |
+
output_video = 255.0 * (output_video.cpu().numpy() * 0.5 + 0.5)
|
| 202 |
+
ground_truth_video = 255.0 * (ground_truth_video.cpu().numpy() * 0.5 + 0.5)
|
| 203 |
+
|
| 204 |
+
# Visualize the input, output, and ground truth
|
| 205 |
+
wandb.log({
|
| 206 |
+
"input": wandb.Video(input_video, caption="Input", fps=16, format="mp4"),
|
| 207 |
+
"output": wandb.Video(output_video, caption="Output", fps=16, format="mp4"),
|
| 208 |
+
"ground_truth": wandb.Video(ground_truth_video, caption="Ground Truth", fps=16, format="mp4"),
|
| 209 |
+
}, step=self.step)
|
| 210 |
+
|
| 211 |
+
# Step 5: Logging
|
| 212 |
+
if self.is_main_process and not self.disable_wandb:
|
| 213 |
+
wandb_loss_dict = {
|
| 214 |
+
"generator_loss": generator_loss.item(),
|
| 215 |
+
"generator_grad_norm": generator_grad_norm.item(),
|
| 216 |
+
**stats
|
| 217 |
+
}
|
| 218 |
+
wandb.log(wandb_loss_dict, step=self.step)
|
| 219 |
+
|
| 220 |
+
if self.step % self.config.gc_interval == 0:
|
| 221 |
+
if dist.get_rank() == 0:
|
| 222 |
+
logging.info("DistGarbageCollector: Running GC.")
|
| 223 |
+
gc.collect()
|
| 224 |
+
|
| 225 |
+
def train(self):
|
| 226 |
+
while True:
|
| 227 |
+
self.train_one_step()
|
| 228 |
+
if (not self.config.no_save) and self.step % self.config.log_iters == 0:
|
| 229 |
+
self.save()
|
| 230 |
+
torch.cuda.empty_cache()
|
| 231 |
+
|
| 232 |
+
barrier()
|
| 233 |
+
if self.is_main_process:
|
| 234 |
+
current_time = time.time()
|
| 235 |
+
if self.previous_time is None:
|
| 236 |
+
self.previous_time = current_time
|
| 237 |
+
else:
|
| 238 |
+
if not self.disable_wandb:
|
| 239 |
+
wandb.log({"per iteration time": current_time - self.previous_time}, step=self.step)
|
| 240 |
+
self.previous_time = current_time
|
| 241 |
+
|
| 242 |
+
self.step += 1
|
DecMem/utils/action_encoder.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from einops import rearrange
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ActionEncoder(nn.Module):
|
| 7 |
+
"""Encode raw per-video-frame action vectors and project to latent spatial dim.
|
| 8 |
+
|
| 9 |
+
Input: (B, T_vid, action_dim) where T_vid = (T_lat - 1) * 4 + 1
|
| 10 |
+
Output: (B, T_lat, C, 1, 1) broadcastable latent-space embeddings.
|
| 11 |
+
|
| 12 |
+
Packing strategy:
|
| 13 |
+
- Frame 0 (first latent frame): the single raw action is repeated 4 times
|
| 14 |
+
so it has the same packed dimension as grouped frames.
|
| 15 |
+
- Frames 1..(T_vid-1): every 4 consecutive raw actions are flattened into
|
| 16 |
+
one latent frame, giving T_lat - 1 grouped frames.
|
| 17 |
+
The linear layer therefore receives 4 * action_dim features per latent frame.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def __init__(self, action_dim: int = 25, hidden_dim: int = 64, latent_channels: int = 16):
|
| 21 |
+
super().__init__()
|
| 22 |
+
self.latent_channels = latent_channels
|
| 23 |
+
# self.net = nn.Sequential(
|
| 24 |
+
# nn.Linear(action_dim * 4, hidden_dim),
|
| 25 |
+
# nn.SiLU(),
|
| 26 |
+
# nn.Linear(hidden_dim, latent_channels),
|
| 27 |
+
# )
|
| 28 |
+
self.net = nn.Linear(action_dim * 4, latent_channels, bias=False)
|
| 29 |
+
|
| 30 |
+
def forward(self, actions: torch.Tensor) -> torch.Tensor:
|
| 31 |
+
"""
|
| 32 |
+
Args:
|
| 33 |
+
actions: (B, T_vid, action_dim) T_vid = (T_lat - 1) * 4 + 1
|
| 34 |
+
Returns:
|
| 35 |
+
(B, T_lat, C, 1, 1)
|
| 36 |
+
"""
|
| 37 |
+
B, T_vid, D = actions.shape
|
| 38 |
+
# First frame: repeat raw action 4 times to match packed dim of grouped frames
|
| 39 |
+
action_first = actions[:, :1].unsqueeze(2).repeat(1, 1, 4, 1) # (B, 1, 4, D)
|
| 40 |
+
# Remaining frames: group every 4 raw frames into one latent frame
|
| 41 |
+
actions_rest = actions[:, 1:].reshape(B, (T_vid - 1) // 4, 4, D) # (B, T_lat-1, 4, D)
|
| 42 |
+
# Concatenate along the latent-frame dimension
|
| 43 |
+
actions = torch.cat([action_first, actions_rest], dim=1) # (B, T_lat, 4, D)
|
| 44 |
+
# Flatten 4 raw actions into the feature dim
|
| 45 |
+
actions = rearrange(actions, "b f p d -> b f (p d)") # (B, T_lat, 4*D)
|
| 46 |
+
emb = self.net(actions) # (B, T_lat, C)
|
| 47 |
+
return emb.unsqueeze(-1).unsqueeze(-1) # (B, T_lat, C, 1, 1)
|
DecMem/utils/dataset.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from utils.lmdb import get_array_shape_from_lmdb, retrieve_row_from_lmdb
|
| 2 |
+
from utils.pose_utils import extract_pose_action_from_npz
|
| 3 |
+
from torch.utils.data import Dataset
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
import lmdb
|
| 7 |
+
import json
|
| 8 |
+
import csv
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from PIL import Image
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TextDataset(Dataset):
|
| 15 |
+
def __init__(self, prompt_path, extended_prompt_path=None):
|
| 16 |
+
with open(prompt_path, encoding="utf-8") as f:
|
| 17 |
+
self.prompt_list = [line.rstrip() for line in f]
|
| 18 |
+
|
| 19 |
+
if extended_prompt_path is not None:
|
| 20 |
+
with open(extended_prompt_path, encoding="utf-8") as f:
|
| 21 |
+
self.extended_prompt_list = [line.rstrip() for line in f]
|
| 22 |
+
assert len(self.extended_prompt_list) == len(self.prompt_list)
|
| 23 |
+
else:
|
| 24 |
+
self.extended_prompt_list = None
|
| 25 |
+
|
| 26 |
+
def __len__(self):
|
| 27 |
+
return len(self.prompt_list)
|
| 28 |
+
|
| 29 |
+
def __getitem__(self, idx):
|
| 30 |
+
batch = {
|
| 31 |
+
"prompts": self.prompt_list[idx],
|
| 32 |
+
"idx": idx,
|
| 33 |
+
}
|
| 34 |
+
if self.extended_prompt_list is not None:
|
| 35 |
+
batch["extended_prompts"] = self.extended_prompt_list[idx]
|
| 36 |
+
return batch
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ODERegressionLMDBDataset(Dataset):
|
| 40 |
+
def __init__(self, data_path: str, max_pair: int = int(1e8)):
|
| 41 |
+
self.env = lmdb.open(data_path, readonly=True,
|
| 42 |
+
lock=False, readahead=False, meminit=False)
|
| 43 |
+
|
| 44 |
+
self.latents_shape = get_array_shape_from_lmdb(self.env, 'latents')
|
| 45 |
+
self.max_pair = max_pair
|
| 46 |
+
|
| 47 |
+
def __len__(self):
|
| 48 |
+
return min(self.latents_shape[0], self.max_pair)
|
| 49 |
+
|
| 50 |
+
def __getitem__(self, idx):
|
| 51 |
+
"""
|
| 52 |
+
Outputs:
|
| 53 |
+
- prompts: List of Strings
|
| 54 |
+
- latents: Tensor of shape (num_denoising_steps, num_frames, num_channels, height, width). It is ordered from pure noise to clean image.
|
| 55 |
+
"""
|
| 56 |
+
latents = retrieve_row_from_lmdb(
|
| 57 |
+
self.env,
|
| 58 |
+
"latents", np.float16, idx, shape=self.latents_shape[1:]
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
if len(latents.shape) == 4:
|
| 62 |
+
latents = latents[None, ...]
|
| 63 |
+
|
| 64 |
+
prompts = retrieve_row_from_lmdb(
|
| 65 |
+
self.env,
|
| 66 |
+
"prompts", str, idx
|
| 67 |
+
)
|
| 68 |
+
return {
|
| 69 |
+
"prompts": prompts,
|
| 70 |
+
"ode_latent": torch.tensor(latents, dtype=torch.float32)
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class ShardingLMDBDataset(Dataset):
|
| 75 |
+
def __init__(self, data_path: str, max_pair: int = int(1e8)):
|
| 76 |
+
self.envs = []
|
| 77 |
+
self.index = []
|
| 78 |
+
|
| 79 |
+
for fname in sorted(os.listdir(data_path)):
|
| 80 |
+
path = os.path.join(data_path, fname)
|
| 81 |
+
env = lmdb.open(path,
|
| 82 |
+
readonly=True,
|
| 83 |
+
lock=False,
|
| 84 |
+
readahead=False,
|
| 85 |
+
meminit=False)
|
| 86 |
+
self.envs.append(env)
|
| 87 |
+
|
| 88 |
+
self.latents_shape = [None] * len(self.envs)
|
| 89 |
+
for shard_id, env in enumerate(self.envs):
|
| 90 |
+
self.latents_shape[shard_id] = get_array_shape_from_lmdb(env, 'latents')
|
| 91 |
+
for local_i in range(self.latents_shape[shard_id][0]):
|
| 92 |
+
self.index.append((shard_id, local_i))
|
| 93 |
+
|
| 94 |
+
# print("shard_id ", shard_id, " local_i ", local_i)
|
| 95 |
+
|
| 96 |
+
self.max_pair = max_pair
|
| 97 |
+
|
| 98 |
+
def __len__(self):
|
| 99 |
+
return len(self.index)
|
| 100 |
+
|
| 101 |
+
def __getitem__(self, idx):
|
| 102 |
+
"""
|
| 103 |
+
Outputs:
|
| 104 |
+
- prompts: List of Strings
|
| 105 |
+
- latents: Tensor of shape (num_denoising_steps, num_frames, num_channels, height, width). It is ordered from pure noise to clean image.
|
| 106 |
+
"""
|
| 107 |
+
shard_id, local_idx = self.index[idx]
|
| 108 |
+
|
| 109 |
+
latents = retrieve_row_from_lmdb(
|
| 110 |
+
self.envs[shard_id],
|
| 111 |
+
"latents", np.float16, local_idx,
|
| 112 |
+
shape=self.latents_shape[shard_id][1:]
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
if len(latents.shape) == 4:
|
| 116 |
+
latents = latents[None, ...]
|
| 117 |
+
|
| 118 |
+
prompts = retrieve_row_from_lmdb(
|
| 119 |
+
self.envs[shard_id],
|
| 120 |
+
"prompts", str, local_idx
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
return {
|
| 124 |
+
"prompts": prompts,
|
| 125 |
+
"ode_latent": torch.tensor(latents, dtype=torch.float32)
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
class TextImagePairDataset(Dataset):
|
| 130 |
+
def __init__(
|
| 131 |
+
self,
|
| 132 |
+
data_dir,
|
| 133 |
+
transform=None,
|
| 134 |
+
eval_first_n=-1,
|
| 135 |
+
pad_to_multiple_of=None
|
| 136 |
+
):
|
| 137 |
+
"""
|
| 138 |
+
Args:
|
| 139 |
+
data_dir (str): Path to the directory containing:
|
| 140 |
+
- target_crop_info_*.json (metadata file)
|
| 141 |
+
- */ (subdirectory containing images with matching aspect ratio)
|
| 142 |
+
transform (callable, optional): Optional transform to be applied on the image
|
| 143 |
+
"""
|
| 144 |
+
self.transform = transform
|
| 145 |
+
data_dir = Path(data_dir)
|
| 146 |
+
|
| 147 |
+
# Find the metadata JSON file
|
| 148 |
+
metadata_files = list(data_dir.glob('target_crop_info_*.json'))
|
| 149 |
+
if not metadata_files:
|
| 150 |
+
raise FileNotFoundError(f"No metadata file found in {data_dir}")
|
| 151 |
+
if len(metadata_files) > 1:
|
| 152 |
+
raise ValueError(f"Multiple metadata files found in {data_dir}")
|
| 153 |
+
|
| 154 |
+
metadata_path = metadata_files[0]
|
| 155 |
+
# Extract aspect ratio from metadata filename (e.g. target_crop_info_26-15.json -> 26-15)
|
| 156 |
+
aspect_ratio = metadata_path.stem.split('_')[-1]
|
| 157 |
+
|
| 158 |
+
# Use aspect ratio subfolder for images
|
| 159 |
+
self.image_dir = data_dir / aspect_ratio
|
| 160 |
+
if not self.image_dir.exists():
|
| 161 |
+
raise FileNotFoundError(f"Image directory not found: {self.image_dir}")
|
| 162 |
+
|
| 163 |
+
# Load metadata
|
| 164 |
+
with open(metadata_path, 'r') as f:
|
| 165 |
+
self.metadata = json.load(f)
|
| 166 |
+
|
| 167 |
+
eval_first_n = eval_first_n if eval_first_n != -1 else len(self.metadata)
|
| 168 |
+
self.metadata = self.metadata[:eval_first_n]
|
| 169 |
+
|
| 170 |
+
# Verify all images exist
|
| 171 |
+
for item in self.metadata:
|
| 172 |
+
image_path = self.image_dir / item['file_name']
|
| 173 |
+
if not image_path.exists():
|
| 174 |
+
raise FileNotFoundError(f"Image not found: {image_path}")
|
| 175 |
+
|
| 176 |
+
self.dummy_prompt = "DUMMY PROMPT"
|
| 177 |
+
self.pre_pad_len = len(self.metadata)
|
| 178 |
+
if pad_to_multiple_of is not None and len(self.metadata) % pad_to_multiple_of != 0:
|
| 179 |
+
# Duplicate the last entry
|
| 180 |
+
self.metadata += [self.metadata[-1]] * (
|
| 181 |
+
pad_to_multiple_of - len(self.metadata) % pad_to_multiple_of
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
def __len__(self):
|
| 185 |
+
return len(self.metadata)
|
| 186 |
+
|
| 187 |
+
def __getitem__(self, idx):
|
| 188 |
+
"""
|
| 189 |
+
Returns:
|
| 190 |
+
dict: A dictionary containing:
|
| 191 |
+
- image: PIL Image
|
| 192 |
+
- caption: str
|
| 193 |
+
- target_bbox: list of int [x1, y1, x2, y2]
|
| 194 |
+
- target_ratio: str
|
| 195 |
+
- type: str
|
| 196 |
+
- origin_size: tuple of int (width, height)
|
| 197 |
+
"""
|
| 198 |
+
item = self.metadata[idx]
|
| 199 |
+
|
| 200 |
+
# Load image
|
| 201 |
+
image_path = self.image_dir / item['file_name']
|
| 202 |
+
image = Image.open(image_path).convert('RGB')
|
| 203 |
+
|
| 204 |
+
# Apply transform if specified
|
| 205 |
+
if self.transform:
|
| 206 |
+
image = self.transform(image)
|
| 207 |
+
|
| 208 |
+
return {
|
| 209 |
+
'image': image,
|
| 210 |
+
'prompts': item['caption'],
|
| 211 |
+
'target_bbox': item['target_crop']['target_bbox'],
|
| 212 |
+
'target_ratio': item['target_crop']['target_ratio'],
|
| 213 |
+
'type': item['type'],
|
| 214 |
+
'origin_size': (item['origin_width'], item['origin_height']),
|
| 215 |
+
'idx': idx
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class WorldMemCSVDataset(Dataset):
|
| 220 |
+
"""Dataset for WorldMem-format data: CSV index + NPZ action/pose files + raw MP4 videos.
|
| 221 |
+
|
| 222 |
+
CSV columns (as produced by WorldMem):
|
| 223 |
+
video_path — path to MP4 video (640×360 @10fps Minecraft footage)
|
| 224 |
+
action_path — path to NPZ file with 'actions' (N, 8) and 'poses' (N, 5)
|
| 225 |
+
|
| 226 |
+
Loads (num_latent_frames - 1) * 4 + 1 = 81 raw video frames per sample and returns
|
| 227 |
+
them as (C, T_vid, H, W). The trainer encodes with the WAN VAE to obtain
|
| 228 |
+
num_latent_frames=21 latent frames (first frame kept, rest 4× compressed).
|
| 229 |
+
Actions/poses are subsampled to match latent frame count (one per latent frame).
|
| 230 |
+
Resolution 352×640 avoids the odd latent-height issue (360/8=45 odd).
|
| 231 |
+
"""
|
| 232 |
+
|
| 233 |
+
VIDEO_H = 352
|
| 234 |
+
VIDEO_W = 640
|
| 235 |
+
SKIP_FRAMES = 0 # skip unstable early frames
|
| 236 |
+
|
| 237 |
+
def __init__(self, csv_path: str, num_latent_frames: int = 21, sample_strategy: str = "random"):
|
| 238 |
+
assert sample_strategy in ("random", "first"), \
|
| 239 |
+
f"sample_strategy must be 'random' or 'first', got {sample_strategy!r}"
|
| 240 |
+
self.sample_strategy = sample_strategy
|
| 241 |
+
self.num_latent_frames = num_latent_frames
|
| 242 |
+
# WAN VAE: T_vid = (T_lat - 1) * 4 + 1
|
| 243 |
+
self.num_video_frames = (num_latent_frames - 1) * 4 + 1
|
| 244 |
+
# Pose subsample indices: frame 0, then first frame of each 4-frame group
|
| 245 |
+
self.pose_indices = [k * 4 for k in range(num_latent_frames)]
|
| 246 |
+
self.rows = []
|
| 247 |
+
with open(csv_path, newline="", encoding="utf-8") as f:
|
| 248 |
+
reader = csv.DictReader(f)
|
| 249 |
+
for row in reader:
|
| 250 |
+
self.rows.append(row)
|
| 251 |
+
|
| 252 |
+
def __len__(self):
|
| 253 |
+
return len(self.rows)
|
| 254 |
+
|
| 255 |
+
def _load_video_frames(self, video_path: str, start_frame: int) -> torch.Tensor:
|
| 256 |
+
"""Load num_video_frames frames from an MP4, returns (C, T, H, W) float32 in [-1,1]."""
|
| 257 |
+
import decord
|
| 258 |
+
decord.bridge.set_bridge("torch")
|
| 259 |
+
reader = decord.VideoReader(
|
| 260 |
+
video_path,
|
| 261 |
+
ctx=decord.cpu(0),
|
| 262 |
+
height=self.VIDEO_H,
|
| 263 |
+
width=self.VIDEO_W,
|
| 264 |
+
)
|
| 265 |
+
total = len(reader)
|
| 266 |
+
end_frame = min(start_frame + self.num_video_frames, total)
|
| 267 |
+
frame_indices = list(range(start_frame, end_frame))
|
| 268 |
+
frames = reader.get_batch(frame_indices) # (T, H, W, C) uint8, torch.Tensor
|
| 269 |
+
# (T, H, W, C) uint8 -> (C, T, H, W) float32 in [-1, 1]
|
| 270 |
+
return frames.permute(3, 0, 1, 2).to(torch.float32).div(127.5).sub(1.0).contiguous()
|
| 271 |
+
|
| 272 |
+
def __getitem__(self, idx):
|
| 273 |
+
import decord
|
| 274 |
+
row = self.rows[idx]
|
| 275 |
+
video_path = row["video_path"]
|
| 276 |
+
action_path = row["action_path"]
|
| 277 |
+
|
| 278 |
+
reader = decord.VideoReader(video_path, ctx=decord.cpu(0))
|
| 279 |
+
total = len(reader)
|
| 280 |
+
if self.sample_strategy == "first":
|
| 281 |
+
start_frame = self.SKIP_FRAMES
|
| 282 |
+
else:
|
| 283 |
+
max_start = max(self.SKIP_FRAMES, total - self.num_video_frames)
|
| 284 |
+
start_frame = int(np.random.randint(self.SKIP_FRAMES, max_start + 1))
|
| 285 |
+
|
| 286 |
+
frames = self._load_video_frames(video_path, start_frame) # (C, T_vid, H, W)
|
| 287 |
+
|
| 288 |
+
actions_full, poses_full = extract_pose_action_from_npz(
|
| 289 |
+
action_path, start_frame, self.num_video_frames
|
| 290 |
+
) # (T_vid, 25), (T_vid, 5)
|
| 291 |
+
|
| 292 |
+
actions = actions_full # (T_vid, 25) — all raw frames
|
| 293 |
+
poses = poses_full[self.pose_indices] # (T_lat, 5) — one per latent frame
|
| 294 |
+
|
| 295 |
+
return {
|
| 296 |
+
"prompts": "", # unused for WorldMem
|
| 297 |
+
"frames": frames, # (C, T_vid, H, W) float32 in [-1, 1]
|
| 298 |
+
"actions": actions, # (T_vid, 25)
|
| 299 |
+
"poses": poses, # (T_lat, 5)
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def cycle(dl):
|
| 304 |
+
while True:
|
| 305 |
+
for data in dl:
|
| 306 |
+
yield data
|
DecMem/utils/distributed.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import timedelta
|
| 2 |
+
from functools import partial
|
| 3 |
+
import os
|
| 4 |
+
import torch
|
| 5 |
+
import torch.distributed as dist
|
| 6 |
+
from torch.distributed.fsdp import FullOptimStateDictConfig, FullStateDictConfig, FullyShardedDataParallel as FSDP, MixedPrecision, ShardingStrategy, StateDictType
|
| 7 |
+
from torch.distributed.fsdp.api import CPUOffload
|
| 8 |
+
from torch.distributed.fsdp.wrap import size_based_auto_wrap_policy, transformer_auto_wrap_policy
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def fsdp_state_dict(model):
|
| 12 |
+
fsdp_fullstate_save_policy = FullStateDictConfig(
|
| 13 |
+
offload_to_cpu=True, rank0_only=True
|
| 14 |
+
)
|
| 15 |
+
with FSDP.state_dict_type(
|
| 16 |
+
model, StateDictType.FULL_STATE_DICT, fsdp_fullstate_save_policy
|
| 17 |
+
):
|
| 18 |
+
checkpoint = model.state_dict()
|
| 19 |
+
|
| 20 |
+
return checkpoint
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def fsdp_optim_state_dict(model, optimizer):
|
| 24 |
+
"""Gather the full optimizer state on rank 0 (CPU). Other ranks return an empty dict."""
|
| 25 |
+
state_dict_config = FullStateDictConfig(offload_to_cpu=True, rank0_only=True)
|
| 26 |
+
optim_state_dict_config = FullOptimStateDictConfig(offload_to_cpu=True, rank0_only=True)
|
| 27 |
+
with FSDP.state_dict_type(
|
| 28 |
+
model,
|
| 29 |
+
StateDictType.FULL_STATE_DICT,
|
| 30 |
+
state_dict_config=state_dict_config,
|
| 31 |
+
optim_state_dict_config=optim_state_dict_config,
|
| 32 |
+
):
|
| 33 |
+
return FSDP.optim_state_dict(model, optimizer)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def load_fsdp_optim_state_dict(model, optimizer, full_optim_state_dict):
|
| 37 |
+
"""Scatter a full optimizer state (valid on rank 0) into the local FSDP shards."""
|
| 38 |
+
with FSDP.state_dict_type(model, StateDictType.FULL_STATE_DICT):
|
| 39 |
+
sharded = FSDP.optim_state_dict_to_load(
|
| 40 |
+
model=model,
|
| 41 |
+
optim=optimizer,
|
| 42 |
+
optim_state_dict=full_optim_state_dict,
|
| 43 |
+
)
|
| 44 |
+
optimizer.load_state_dict(sharded)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def fsdp_wrap(module, sharding_strategy="full", mixed_precision=False, wrap_strategy="size", min_num_params=int(5e7), transformer_module=None, cpu_offload=False):
|
| 48 |
+
if mixed_precision:
|
| 49 |
+
mixed_precision_policy = MixedPrecision(
|
| 50 |
+
param_dtype=torch.bfloat16,
|
| 51 |
+
reduce_dtype=torch.float32,
|
| 52 |
+
buffer_dtype=torch.float32,
|
| 53 |
+
cast_forward_inputs=False
|
| 54 |
+
)
|
| 55 |
+
else:
|
| 56 |
+
mixed_precision_policy = None
|
| 57 |
+
|
| 58 |
+
if wrap_strategy == "transformer":
|
| 59 |
+
auto_wrap_policy = partial(
|
| 60 |
+
transformer_auto_wrap_policy,
|
| 61 |
+
transformer_layer_cls=transformer_module
|
| 62 |
+
)
|
| 63 |
+
elif wrap_strategy == "size":
|
| 64 |
+
auto_wrap_policy = partial(
|
| 65 |
+
size_based_auto_wrap_policy,
|
| 66 |
+
min_num_params=min_num_params
|
| 67 |
+
)
|
| 68 |
+
else:
|
| 69 |
+
raise ValueError(f"Invalid wrap strategy: {wrap_strategy}")
|
| 70 |
+
|
| 71 |
+
os.environ["NCCL_CROSS_NIC"] = "1"
|
| 72 |
+
|
| 73 |
+
sharding_strategy = {
|
| 74 |
+
"full": ShardingStrategy.FULL_SHARD,
|
| 75 |
+
"hybrid_full": ShardingStrategy.HYBRID_SHARD,
|
| 76 |
+
"hybrid_zero2": ShardingStrategy._HYBRID_SHARD_ZERO2,
|
| 77 |
+
"no_shard": ShardingStrategy.NO_SHARD,
|
| 78 |
+
}[sharding_strategy]
|
| 79 |
+
|
| 80 |
+
module = FSDP(
|
| 81 |
+
module,
|
| 82 |
+
auto_wrap_policy=auto_wrap_policy,
|
| 83 |
+
sharding_strategy=sharding_strategy,
|
| 84 |
+
mixed_precision=mixed_precision_policy,
|
| 85 |
+
device_id=torch.cuda.current_device(),
|
| 86 |
+
limit_all_gathers=True,
|
| 87 |
+
use_orig_params=True,
|
| 88 |
+
cpu_offload=CPUOffload(offload_params=cpu_offload),
|
| 89 |
+
sync_module_states=False # Load ckpt on rank 0 and sync to other ranks
|
| 90 |
+
)
|
| 91 |
+
return module
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def barrier():
|
| 95 |
+
if dist.is_initialized():
|
| 96 |
+
dist.barrier()
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def launch_distributed_job(backend: str = "nccl"):
|
| 100 |
+
rank = int(os.environ["RANK"])
|
| 101 |
+
local_rank = int(os.environ["LOCAL_RANK"])
|
| 102 |
+
world_size = int(os.environ["WORLD_SIZE"])
|
| 103 |
+
host = os.environ["MASTER_ADDR"]
|
| 104 |
+
port = int(os.environ["MASTER_PORT"])
|
| 105 |
+
|
| 106 |
+
if ":" in host: # IPv6
|
| 107 |
+
init_method = f"tcp://[{host}]:{port}"
|
| 108 |
+
else: # IPv4
|
| 109 |
+
init_method = f"tcp://{host}:{port}"
|
| 110 |
+
dist.init_process_group(rank=rank, world_size=world_size, backend=backend,
|
| 111 |
+
init_method=init_method, timeout=timedelta(minutes=30))
|
| 112 |
+
torch.cuda.set_device(local_rank)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class EMA_FSDP:
|
| 116 |
+
def __init__(self, fsdp_module: torch.nn.Module, decay: float = 0.999):
|
| 117 |
+
self.decay = decay
|
| 118 |
+
self.shadow = {}
|
| 119 |
+
self._init_shadow(fsdp_module)
|
| 120 |
+
|
| 121 |
+
@torch.no_grad()
|
| 122 |
+
def _init_shadow(self, fsdp_module):
|
| 123 |
+
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
|
| 124 |
+
# Anchor shadow to the local CUDA device. summon_full_params can return
|
| 125 |
+
# CPU views of params under some FSDP/resume code paths, so don't
|
| 126 |
+
# implicitly inherit p's device.
|
| 127 |
+
target_device = torch.device("cuda", torch.cuda.current_device())
|
| 128 |
+
with FSDP.summon_full_params(fsdp_module, writeback=False):
|
| 129 |
+
for n, p in fsdp_module.module.named_parameters():
|
| 130 |
+
self.shadow[n] = p.detach().to(target_device).float().clone()
|
| 131 |
+
|
| 132 |
+
@torch.no_grad()
|
| 133 |
+
def update(self, fsdp_module):
|
| 134 |
+
d = self.decay
|
| 135 |
+
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
|
| 136 |
+
with FSDP.summon_full_params(fsdp_module, writeback=False):
|
| 137 |
+
for n, p in fsdp_module.module.named_parameters():
|
| 138 |
+
shadow = self.shadow[n]
|
| 139 |
+
# Always align the source tensor to shadow's device to avoid
|
| 140 |
+
# cuda/cpu mismatches when FSDP exposes params on CPU.
|
| 141 |
+
shadow.mul_(d).add_(
|
| 142 |
+
p.detach().to(shadow.device).float(), alpha=1. - d
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# Optional helpers ---------------------------------------------------
|
| 146 |
+
def state_dict(self):
|
| 147 |
+
return self.shadow # picklable
|
| 148 |
+
|
| 149 |
+
def load_state_dict(self, sd):
|
| 150 |
+
self.shadow = {k: v.clone() for k, v in sd.items()}
|
| 151 |
+
|
| 152 |
+
def copy_to(self, fsdp_module):
|
| 153 |
+
# load EMA weights into an (unwrapped) copy of the generator
|
| 154 |
+
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
|
| 155 |
+
with FSDP.summon_full_params(fsdp_module, writeback=True):
|
| 156 |
+
for n, p in fsdp_module.module.named_parameters():
|
| 157 |
+
if n in self.shadow:
|
| 158 |
+
p.data.copy_(self.shadow[n].to(p.dtype, device=p.device))
|
DecMem/utils/lmdb.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def get_array_shape_from_lmdb(env, array_name):
|
| 5 |
+
with env.begin() as txn:
|
| 6 |
+
image_shape = txn.get(f"{array_name}_shape".encode()).decode()
|
| 7 |
+
image_shape = tuple(map(int, image_shape.split()))
|
| 8 |
+
return image_shape
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def store_arrays_to_lmdb(env, arrays_dict, start_index=0):
|
| 12 |
+
"""
|
| 13 |
+
Store rows of multiple numpy arrays in a single LMDB.
|
| 14 |
+
Each row is stored separately with a naming convention.
|
| 15 |
+
"""
|
| 16 |
+
with env.begin(write=True) as txn:
|
| 17 |
+
for array_name, array in arrays_dict.items():
|
| 18 |
+
for i, row in enumerate(array):
|
| 19 |
+
# Convert row to bytes
|
| 20 |
+
if isinstance(row, str):
|
| 21 |
+
row_bytes = row.encode()
|
| 22 |
+
else:
|
| 23 |
+
row_bytes = row.tobytes()
|
| 24 |
+
|
| 25 |
+
data_key = f'{array_name}_{start_index + i}_data'.encode()
|
| 26 |
+
|
| 27 |
+
txn.put(data_key, row_bytes)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def process_data_dict(data_dict, seen_prompts):
|
| 31 |
+
output_dict = {}
|
| 32 |
+
|
| 33 |
+
all_videos = []
|
| 34 |
+
all_prompts = []
|
| 35 |
+
for prompt, video in data_dict.items():
|
| 36 |
+
if prompt in seen_prompts:
|
| 37 |
+
continue
|
| 38 |
+
else:
|
| 39 |
+
seen_prompts.add(prompt)
|
| 40 |
+
|
| 41 |
+
video = video.half().numpy()
|
| 42 |
+
all_videos.append(video)
|
| 43 |
+
all_prompts.append(prompt)
|
| 44 |
+
|
| 45 |
+
if len(all_videos) == 0:
|
| 46 |
+
return {"latents": np.array([]), "prompts": np.array([])}
|
| 47 |
+
|
| 48 |
+
all_videos = np.concatenate(all_videos, axis=0)
|
| 49 |
+
|
| 50 |
+
output_dict['latents'] = all_videos
|
| 51 |
+
output_dict['prompts'] = np.array(all_prompts)
|
| 52 |
+
|
| 53 |
+
return output_dict
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def retrieve_row_from_lmdb(lmdb_env, array_name, dtype, row_index, shape=None):
|
| 57 |
+
"""
|
| 58 |
+
Retrieve a specific row from a specific array in the LMDB.
|
| 59 |
+
"""
|
| 60 |
+
data_key = f'{array_name}_{row_index}_data'.encode()
|
| 61 |
+
|
| 62 |
+
with lmdb_env.begin() as txn:
|
| 63 |
+
row_bytes = txn.get(data_key)
|
| 64 |
+
|
| 65 |
+
if dtype == str:
|
| 66 |
+
array = row_bytes.decode()
|
| 67 |
+
else:
|
| 68 |
+
array = np.frombuffer(row_bytes, dtype=dtype)
|
| 69 |
+
|
| 70 |
+
if shape is not None and len(shape) > 0:
|
| 71 |
+
array = array.reshape(shape)
|
| 72 |
+
return array
|
DecMem/utils/loss.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import ABC, abstractmethod
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class DenoisingLoss(ABC):
|
| 6 |
+
@abstractmethod
|
| 7 |
+
def __call__(
|
| 8 |
+
self, x: torch.Tensor, x_pred: torch.Tensor,
|
| 9 |
+
noise: torch.Tensor, noise_pred: torch.Tensor,
|
| 10 |
+
alphas_cumprod: torch.Tensor,
|
| 11 |
+
timestep: torch.Tensor,
|
| 12 |
+
**kwargs
|
| 13 |
+
) -> torch.Tensor:
|
| 14 |
+
"""
|
| 15 |
+
Base class for denoising loss.
|
| 16 |
+
Input:
|
| 17 |
+
- x: the clean data with shape [B, F, C, H, W]
|
| 18 |
+
- x_pred: the predicted clean data with shape [B, F, C, H, W]
|
| 19 |
+
- noise: the noise with shape [B, F, C, H, W]
|
| 20 |
+
- noise_pred: the predicted noise with shape [B, F, C, H, W]
|
| 21 |
+
- alphas_cumprod: the cumulative product of alphas (defining the noise schedule) with shape [T]
|
| 22 |
+
- timestep: the current timestep with shape [B, F]
|
| 23 |
+
"""
|
| 24 |
+
pass
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class X0PredLoss(DenoisingLoss):
|
| 28 |
+
def __call__(
|
| 29 |
+
self, x: torch.Tensor, x_pred: torch.Tensor,
|
| 30 |
+
noise: torch.Tensor, noise_pred: torch.Tensor,
|
| 31 |
+
alphas_cumprod: torch.Tensor,
|
| 32 |
+
timestep: torch.Tensor,
|
| 33 |
+
**kwargs
|
| 34 |
+
) -> torch.Tensor:
|
| 35 |
+
return torch.mean((x - x_pred) ** 2)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class VPredLoss(DenoisingLoss):
|
| 39 |
+
def __call__(
|
| 40 |
+
self, x: torch.Tensor, x_pred: torch.Tensor,
|
| 41 |
+
noise: torch.Tensor, noise_pred: torch.Tensor,
|
| 42 |
+
alphas_cumprod: torch.Tensor,
|
| 43 |
+
timestep: torch.Tensor,
|
| 44 |
+
**kwargs
|
| 45 |
+
) -> torch.Tensor:
|
| 46 |
+
weights = 1 / (1 - alphas_cumprod[timestep].reshape(*timestep.shape, 1, 1, 1))
|
| 47 |
+
return torch.mean(weights * (x - x_pred) ** 2)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class NoisePredLoss(DenoisingLoss):
|
| 51 |
+
def __call__(
|
| 52 |
+
self, x: torch.Tensor, x_pred: torch.Tensor,
|
| 53 |
+
noise: torch.Tensor, noise_pred: torch.Tensor,
|
| 54 |
+
alphas_cumprod: torch.Tensor,
|
| 55 |
+
timestep: torch.Tensor,
|
| 56 |
+
**kwargs
|
| 57 |
+
) -> torch.Tensor:
|
| 58 |
+
return torch.mean((noise - noise_pred) ** 2)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class FlowPredLoss(DenoisingLoss):
|
| 62 |
+
def __call__(
|
| 63 |
+
self, x: torch.Tensor, x_pred: torch.Tensor,
|
| 64 |
+
noise: torch.Tensor, noise_pred: torch.Tensor,
|
| 65 |
+
alphas_cumprod: torch.Tensor,
|
| 66 |
+
timestep: torch.Tensor,
|
| 67 |
+
**kwargs
|
| 68 |
+
) -> torch.Tensor:
|
| 69 |
+
return torch.mean((kwargs["flow_pred"] - (noise - x)) ** 2)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
NAME_TO_CLASS = {
|
| 73 |
+
"x0": X0PredLoss,
|
| 74 |
+
"v": VPredLoss,
|
| 75 |
+
"noise": NoisePredLoss,
|
| 76 |
+
"flow": FlowPredLoss
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def get_denoising_loss(loss_type: str) -> DenoisingLoss:
|
| 81 |
+
return NAME_TO_CLASS[loss_type]
|
DecMem/utils/misc.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import random
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def set_seed(seed: int, deterministic: bool = False):
|
| 7 |
+
"""
|
| 8 |
+
Helper function for reproducible behavior to set the seed in `random`, `numpy`, `torch`.
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
seed (`int`):
|
| 12 |
+
The seed to set.
|
| 13 |
+
deterministic (`bool`, *optional*, defaults to `False`):
|
| 14 |
+
Whether to use deterministic algorithms where available. Can slow down training.
|
| 15 |
+
"""
|
| 16 |
+
random.seed(seed)
|
| 17 |
+
np.random.seed(seed)
|
| 18 |
+
torch.manual_seed(seed)
|
| 19 |
+
torch.cuda.manual_seed_all(seed)
|
| 20 |
+
|
| 21 |
+
if deterministic:
|
| 22 |
+
torch.use_deterministic_algorithms(True)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def merge_dict_list(dict_list):
|
| 26 |
+
if len(dict_list) == 1:
|
| 27 |
+
return dict_list[0]
|
| 28 |
+
|
| 29 |
+
merged_dict = {}
|
| 30 |
+
for k, v in dict_list[0].items():
|
| 31 |
+
if isinstance(v, torch.Tensor):
|
| 32 |
+
if v.ndim == 0:
|
| 33 |
+
merged_dict[k] = torch.stack([d[k] for d in dict_list], dim=0)
|
| 34 |
+
else:
|
| 35 |
+
merged_dict[k] = torch.cat([d[k] for d in dict_list], dim=0)
|
| 36 |
+
else:
|
| 37 |
+
# for non-tensor values, we just copy the value from the first item
|
| 38 |
+
merged_dict[k] = v
|
| 39 |
+
return merged_dict
|
DecMem/utils/pose_utils.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pose and action utility functions adapted from WorldMem/framear.
|
| 3 |
+
"""
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
from einops import rearrange
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
ACTION_KEYS = [
|
| 10 |
+
"inventory", # 0
|
| 11 |
+
"ESC", # 1
|
| 12 |
+
"hotbar.1", # 2
|
| 13 |
+
"hotbar.2", # 3
|
| 14 |
+
"hotbar.3", # 4
|
| 15 |
+
"hotbar.4", # 5
|
| 16 |
+
"hotbar.5", # 6
|
| 17 |
+
"hotbar.6", # 7
|
| 18 |
+
"hotbar.7", # 8
|
| 19 |
+
"hotbar.8", # 9
|
| 20 |
+
"hotbar.9", # 10
|
| 21 |
+
"forward", # 11
|
| 22 |
+
"back", # 12
|
| 23 |
+
"left", # 13
|
| 24 |
+
"right", # 14
|
| 25 |
+
"cameraY", # 15
|
| 26 |
+
"cameraX", # 16
|
| 27 |
+
"jump", # 17
|
| 28 |
+
"sneak", # 18
|
| 29 |
+
"sprint", # 19
|
| 30 |
+
"swapHands", # 20
|
| 31 |
+
"attack", # 21
|
| 32 |
+
"use", # 22
|
| 33 |
+
"pickItem", # 23
|
| 34 |
+
"drop", # 24
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def convert_action_space(actions: np.ndarray) -> torch.Tensor:
|
| 39 |
+
"""Convert raw WorldMem action array to 25-dim one-hot vector.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
actions: np.ndarray of shape (T, 8+), raw action indices from NPZ.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
Tensor of shape (T, 25) with float values.
|
| 46 |
+
"""
|
| 47 |
+
vec_25 = torch.zeros(len(actions), len(ACTION_KEYS))
|
| 48 |
+
vec_25[actions[:, 0] == 1, 11] = 1 # forward
|
| 49 |
+
vec_25[actions[:, 0] == 2, 12] = 1 # backward
|
| 50 |
+
vec_25[actions[:, 4] == 11, 16] = -1 # camera X left
|
| 51 |
+
vec_25[actions[:, 4] == 13, 16] = 1 # camera X right
|
| 52 |
+
vec_25[actions[:, 3] == 11, 15] = -1 # camera Y up
|
| 53 |
+
vec_25[actions[:, 3] == 13, 15] = 1 # camera Y down
|
| 54 |
+
vec_25[actions[:, 5] == 6, 24] = 1 # place box
|
| 55 |
+
vec_25[actions[:, 5] == 1, 24] = 1 # place box
|
| 56 |
+
vec_25[actions[:, 1] == 1, 13] = 1 # left
|
| 57 |
+
vec_25[actions[:, 1] == 2, 14] = 1 # right
|
| 58 |
+
vec_25[actions[:, 7] == 1, 2] = 1 # hotbar 1
|
| 59 |
+
return vec_25
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def euler_to_rotation_matrix(pitch: torch.Tensor, yaw: torch.Tensor) -> torch.Tensor:
|
| 63 |
+
"""Convert pitch and yaw angles (radians) to 3x3 rotation matrix.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
pitch: Tensor of pitch angles, shape (...,).
|
| 67 |
+
yaw: Tensor of yaw angles, shape (...,).
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
Rotation matrix of shape (..., 3, 3).
|
| 71 |
+
"""
|
| 72 |
+
cos_pitch, sin_pitch = torch.cos(pitch), torch.sin(pitch)
|
| 73 |
+
cos_yaw, sin_yaw = torch.cos(yaw), torch.sin(yaw)
|
| 74 |
+
|
| 75 |
+
R_pitch = torch.stack([
|
| 76 |
+
torch.ones_like(pitch), torch.zeros_like(pitch), torch.zeros_like(pitch),
|
| 77 |
+
torch.zeros_like(pitch), cos_pitch, -sin_pitch,
|
| 78 |
+
torch.zeros_like(pitch), sin_pitch, cos_pitch
|
| 79 |
+
], dim=-1).reshape(-1, 3, 3)
|
| 80 |
+
|
| 81 |
+
R_yaw = torch.stack([
|
| 82 |
+
cos_yaw, torch.zeros_like(yaw), sin_yaw,
|
| 83 |
+
torch.zeros_like(yaw), torch.ones_like(yaw), torch.zeros_like(yaw),
|
| 84 |
+
-sin_yaw, torch.zeros_like(yaw), cos_yaw
|
| 85 |
+
], dim=-1).reshape(-1, 3, 3)
|
| 86 |
+
|
| 87 |
+
return torch.matmul(R_yaw, R_pitch)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def euler_to_camera_to_world_matrix(pose: torch.Tensor) -> torch.Tensor:
|
| 91 |
+
"""Convert (x, y, z, pitch, yaw) pose vector to 4x4 camera-to-world matrix.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
pose: Tensor of shape (5,), (f, 5), or (f, b, 5).
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
Tensor of shape (4,4), (f, 4, 4), or (f, b, 4, 4).
|
| 98 |
+
"""
|
| 99 |
+
origin_dim = pose.ndim
|
| 100 |
+
if origin_dim == 1:
|
| 101 |
+
pose = pose.unsqueeze(0).unsqueeze(0)
|
| 102 |
+
elif origin_dim == 2:
|
| 103 |
+
pose = pose.unsqueeze(0)
|
| 104 |
+
|
| 105 |
+
x, y, z, pitch, yaw = (
|
| 106 |
+
pose[..., 0], pose[..., 1], pose[..., 2],
|
| 107 |
+
pose[..., 3], pose[..., 4]
|
| 108 |
+
)
|
| 109 |
+
pitch = torch.deg2rad(pitch)
|
| 110 |
+
yaw = torch.deg2rad(yaw)
|
| 111 |
+
|
| 112 |
+
R = euler_to_rotation_matrix(pitch, yaw)
|
| 113 |
+
|
| 114 |
+
eye = torch.eye(4, dtype=pose.dtype, device=pose.device)
|
| 115 |
+
c2w = eye.repeat(R.shape[0], 1, 1)
|
| 116 |
+
c2w[:, :3, :3] = R
|
| 117 |
+
c2w[:, :3, 3] = torch.stack([x.reshape(-1), y.reshape(-1), z.reshape(-1)], dim=-1)
|
| 118 |
+
|
| 119 |
+
if origin_dim == 3:
|
| 120 |
+
return c2w.view(pose.shape[0], pose.shape[1], 4, 4)
|
| 121 |
+
elif origin_dim == 2:
|
| 122 |
+
return c2w.view(pose.shape[0], 4, 4)
|
| 123 |
+
else:
|
| 124 |
+
return c2w.squeeze(0).squeeze(0)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def c2w_to_w2c(c2w: torch.Tensor) -> torch.Tensor:
|
| 128 |
+
"""Invert camera-to-world to world-to-camera matrix.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
c2w: Tensor of shape (..., 4, 4).
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
w2c: Tensor of shape (..., 4, 4).
|
| 135 |
+
"""
|
| 136 |
+
R = c2w[..., :3, :3]
|
| 137 |
+
t = c2w[..., :3, 3]
|
| 138 |
+
Rt = R.transpose(-1, -2)
|
| 139 |
+
t_new = -Rt @ t.unsqueeze(-1)
|
| 140 |
+
w2c = torch.zeros_like(c2w)
|
| 141 |
+
w2c[..., :3, :3] = Rt
|
| 142 |
+
w2c[..., :3, 3] = t_new.squeeze(-1)
|
| 143 |
+
w2c[..., 3, 3] = 1.0
|
| 144 |
+
return w2c
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def extract_pose_action_from_npz(
|
| 148 |
+
data_path: str,
|
| 149 |
+
start_frame: int,
|
| 150 |
+
num_frames: int,
|
| 151 |
+
dtype=torch.float32
|
| 152 |
+
) -> tuple:
|
| 153 |
+
"""Load action and pose from WorldMem NPZ file.
|
| 154 |
+
|
| 155 |
+
Args:
|
| 156 |
+
data_path: Path to .npz file containing 'actions' and 'poses'.
|
| 157 |
+
start_frame: Starting frame index.
|
| 158 |
+
num_frames: Number of frames to load.
|
| 159 |
+
dtype: Output tensor dtype.
|
| 160 |
+
|
| 161 |
+
Returns:
|
| 162 |
+
actions: Tensor (num_frames, 25) - encoded action vectors.
|
| 163 |
+
poses: Tensor (num_frames, 5) - raw pose vectors (x,y,z,pitch,yaw).
|
| 164 |
+
"""
|
| 165 |
+
data = np.load(data_path)
|
| 166 |
+
actions_raw = data["actions"] # (N, 8+)
|
| 167 |
+
poses_pool = data["poses"].copy() # (N, 5)
|
| 168 |
+
|
| 169 |
+
# Fix potentially corrupted height in first frame
|
| 170 |
+
poses_pool[0, 1] = poses_pool[1, 1]
|
| 171 |
+
|
| 172 |
+
# Pad poses if shorter than actions
|
| 173 |
+
if len(poses_pool) < len(actions_raw):
|
| 174 |
+
poses_pool = np.pad(poses_pool, ((1, 0), (0, 0)))
|
| 175 |
+
|
| 176 |
+
actions_raw = actions_raw[start_frame: start_frame + num_frames]
|
| 177 |
+
poses = poses_pool[start_frame: start_frame + num_frames]
|
| 178 |
+
|
| 179 |
+
# Normalize pose: translate relative to first frame, negate yaw
|
| 180 |
+
poses[:, :3] -= poses[:1, :3]
|
| 181 |
+
poses[:, -1] = -poses[:, -1]
|
| 182 |
+
poses[:, 3:] %= 360
|
| 183 |
+
|
| 184 |
+
actions = convert_action_space(actions_raw) # (T, 25)
|
| 185 |
+
return actions.to(dtype), torch.tensor(poses, dtype=dtype)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def poses_to_viewmats(poses: torch.Tensor) -> torch.Tensor:
|
| 189 |
+
"""Convert (B, T, 5) pose vectors to (B, T, 4, 4) world-to-camera matrices.
|
| 190 |
+
|
| 191 |
+
Args:
|
| 192 |
+
poses: Tensor of shape (B, T, 5).
|
| 193 |
+
|
| 194 |
+
Returns:
|
| 195 |
+
viewmats: Tensor of shape (B, T, 4, 4).
|
| 196 |
+
"""
|
| 197 |
+
B, T, _ = poses.shape
|
| 198 |
+
# (B, T, 5) -> (T, B, 5) for euler_to_camera_to_world_matrix
|
| 199 |
+
pose_tbd = rearrange(poses, "b t d -> t b d")
|
| 200 |
+
c2w = euler_to_camera_to_world_matrix(pose_tbd) # (T, B, 4, 4)
|
| 201 |
+
w2c = c2w_to_w2c(c2w) # (T, B, 4, 4)
|
| 202 |
+
viewmats = rearrange(w2c, "t b p q -> b t p q") # (B, T, 4, 4)
|
| 203 |
+
return viewmats
|