BonanDing commited on
Commit
59630ba
·
verified ·
1 Parent(s): e0ce2cb

Add isolated Minecraft and RE10K baseline evaluation suite

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +16 -0
  2. DecMem/.gitignore +25 -0
  3. DecMem/LICENSE +201 -0
  4. DecMem/README.md +318 -0
  5. DecMem/configs/decmem.yaml +74 -0
  6. DecMem/configs/default_config.yaml +20 -0
  7. DecMem/configs/dense.yaml +68 -0
  8. DecMem/configs/dense_short.yaml +67 -0
  9. DecMem/configs/worldmem_diffusion.yaml +69 -0
  10. DecMem/demo_utils/constant.py +41 -0
  11. DecMem/demo_utils/memory.py +135 -0
  12. DecMem/demo_utils/taehv.py +313 -0
  13. DecMem/demo_utils/utils.py +616 -0
  14. DecMem/demo_utils/vae.py +390 -0
  15. DecMem/demo_utils/vae_block3.py +291 -0
  16. DecMem/demo_utils/vae_torch2trt.py +308 -0
  17. DecMem/evaluation/method_manifest.json +91 -0
  18. DecMem/evaluation/worldmem_adapter.py +188 -0
  19. DecMem/example/example.csv +3 -0
  20. DecMem/infer.py +338 -0
  21. DecMem/model/__init__.py +9 -0
  22. DecMem/model/base.py +222 -0
  23. DecMem/model/causvid.py +391 -0
  24. DecMem/model/diffusion.py +155 -0
  25. DecMem/model/gan.py +295 -0
  26. DecMem/pipeline/__init__.py +17 -0
  27. DecMem/pipeline/bidirectional_diffusion_inference.py +110 -0
  28. DecMem/pipeline/bidirectional_inference.py +71 -0
  29. DecMem/pipeline/causal_diffusion_inference.py +707 -0
  30. DecMem/pipeline/causal_inference.py +331 -0
  31. DecMem/pipeline/self_forcing_training.py +267 -0
  32. DecMem/requirements.txt +26 -0
  33. DecMem/scripts/build_worldmem_csv.py +111 -0
  34. DecMem/scripts/infer.sh +60 -0
  35. DecMem/scripts/infer_example.sh +59 -0
  36. DecMem/scripts/train_multinode.sh +85 -0
  37. DecMem/slurm_logs/.gitkeep +0 -0
  38. DecMem/train.py +45 -0
  39. DecMem/trainer/__init__.py +7 -0
  40. DecMem/trainer/diffusion.py +432 -0
  41. DecMem/trainer/distillation.py +380 -0
  42. DecMem/trainer/gan.py +452 -0
  43. DecMem/trainer/ode.py +242 -0
  44. DecMem/utils/action_encoder.py +47 -0
  45. DecMem/utils/dataset.py +306 -0
  46. DecMem/utils/distributed.py +158 -0
  47. DecMem/utils/lmdb.py +72 -0
  48. DecMem/utils/loss.py +81 -0
  49. DecMem/utils/misc.py +39 -0
  50. 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 &nbsp;&nbsp;
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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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
+ [![Watch the video](https://img.youtube.com/vi/f6jBmFXxgzg/maxresdefault.jpg)](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