diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..2d8981f7cd0209927e8cc50c92a532b92af79ad4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+envs/
+cache/
+checkpoints/
+artifacts/
+shared/metric_vendor/
+**/wan_models/
+**/outputs/
+**/build/
+**/dist/
+**/__pycache__/
+**/.pytest_cache/
+**/*.pyc
+**/*.so
+**/*.egg-info/
+**/slurm_logs/*
+!**/slurm_logs/.gitkeep
diff --git a/DecMem/.gitignore b/DecMem/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..6b8e84a498a09aa0efc9736d6d9ae99b49c00cf9
--- /dev/null
+++ b/DecMem/.gitignore
@@ -0,0 +1,25 @@
+__pycache__
+*.egg-info
+
+wan_models
+checkpoints
+
+test/*
+
+videos/*
+
+exps
+
+.vscode/
+
+configs/*_overfit.yaml
+
+data
+
+video_sparse_attn/build
+
+scripts/train_worldmem.sh
+
+scripts/train_local.sh
+
+scripts/train_worldmem_multinode_torchrun.sh
\ No newline at end of file
diff --git a/DecMem/LICENSE b/DecMem/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64
--- /dev/null
+++ b/DecMem/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/DecMem/README.md b/DecMem/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..88a0f8924ff09f7fb27230d9622d6c5bd19743d7
--- /dev/null
+++ b/DecMem/README.md
@@ -0,0 +1,318 @@
+
+
DecMem: Towards Minute-Long Consistent World Generation with Decoupled Memory
+ 1The University of Hong Kong
+ 2Kling Team, Kuaishou Technology
+ †Corresponding author
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## 📋 Table of Contents
+
+- [📋 Table of Contents](#-table-of-contents)
+- [🔥 Updates](#-updates)
+- [📷 Introduction](#-introduction)
+- [⚙️ Code: DecMem + Wan2.1-T2V-1.3B](#️-code-decmem--wan21-t2v-13b)
+ - [Requirements](#requirements)
+ - [Installation](#installation)
+ - [Download Checkpoints](#download-checkpoints)
+ - [Quick Start](#quick-start)
+ - [Data](#data)
+ - [Train](#train)
+ - [Inference](#inference)
+- [🤗 Acknowledgement](#-acknowledgement)
+- [🌟 Citation](#-citation)
+
+**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.
+
+## 🔥 Updates
+
+- **[2026.06.01]**: Release the [Training and Inference Code](https://github.com/KlingAIResearch/DecMem) and the [Checkpoints](https://huggingface.co/KlingTeam/DecMem).
+- **[2026.06.01]**: Release the [Project Page](https://jeffreyyzh.github.io/DecMem-Page/) and the [Arxiv](https://arxiv.org/abs/2605.31336) version.
+
+## 📷 Introduction
+
+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.
+
+**(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.
+
+**(b) Anchored Local Memory (ALM):** Keeps short-term transition smooth and supports stable, high-quality extrapolation by anchoring generation on recent local context.
+
+**(c) Capability:** DecMem enables **minute-level** controllable long video generation with **high fidelity**, **consistency** and **efficiency**.
+
+
+
+[](https://www.youtube.com/watch?v=f6jBmFXxgzg)
+
+
+
+## ⚙️ Code: DecMem + Wan2.1-T2V-1.3B
+
+### Requirements
+
+We tested this repo on the following setup:
+
+- **GPU:** NVIDIA GPU with at least 24 GB VRAM.
+ - **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.
+ - 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.
+- **RAM:** 64 GB recommended.
+- **CUDA:** 12.x toolkit matching the PyTorch build.
+- **Compiler:** **gcc / g++ ≥ 11** (C++20 required by the VSA kernel).
+
+### Installation
+
+#### 1. Core environment
+
+Create a conda environment and install dependencies:
+
+```bash
+git clone https://github.com/KlingAIResearch/DecMem.git
+cd DecMem
+conda create -n decmem python=3.10 -y
+conda activate decmem
+pip install torch==2.5.1 torchvision==0.20.1 --index-url https://download.pytorch.org/whl/cu121
+pip install -r requirements.txt
+pip install flash-attn --no-build-isolation
+```
+
+#### 2. Video Sparse Attention (VSA) kernel
+
+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:
+
+```bash
+cd video_sparse_attn
+python setup.py install
+```
+
+Verify:
+
+```bash
+python -c "
+import torch, vsa
+from vsa.block_sparse_wrapper import block_sparse_attn_SM90
+print('device cap:', torch.cuda.get_device_capability(0))
+print('SM90 op:', block_sparse_attn_SM90)
+"
+```
+
+### Checkpoints
+
+Download the Wan2.1 backbone (VAE + tokenizer weights used by the pipeline):
+
+```bash
+huggingface-cli download Wan-AI/Wan2.1-T2V-1.3B \
+ --local-dir-use-symlinks False \
+ --local-dir wan_models/Wan2.1-T2V-1.3B
+```
+
+Download DecMem trained checkpoints from HuggingFace:
+
+```bash
+huggingface-cli download KlingTeam/DecMem --local-dir checkpoints
+```
+
+Checkpoint layout expected by training / inference scripts:
+
+```
+checkpoints/
+└── decmem.pt # released weights
+```
+
+### Quick start
+
+We provide the example video-pose pairs for quick inference. The inference is Block-by-block causal denoising manner with KV cache.
+
+```bash
+bash scripts/infer_example.sh
+```
+
+### Data
+
+DecMem trains on **WorldMem** gameplay data: paired MP4 videos and NPZ action/pose files indexed by a CSV.
+
+#### Download dataset
+
+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)).
+
+Place the dataset in the following directory structure:
+
+```
+data/
+├── training/
+├── validation/
+└── test/
+```
+
+Each clip is an MP4 file with a matching NPZ file (same basename) in the same directory.
+
+#### Build metadata CSV
+
+After download, run the helper script to scan MP4/NPZ pairs and generate index files:
+
+```bash
+python scripts/build_worldmem_csv.py
+```
+
+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.
+
+#### Directory layout
+
+After download and indexing:
+
+```
+data/
+├── train.csv # training index (generated)
+├── test.csv # evaluation index (generated)
+├── training/
+│ └── *.mp4 + *.npz # paired clips
+├── validation/
+│ └── *.mp4 + *.npz
+└── test/
+ └── *.mp4 + *.npz
+```
+
+#### CSV format
+
+Each row has two columns (`utils/dataset.py` → `WorldMemCSVDataset`):
+
+
+| Column | Description |
+| ------------- | ------------------------------ |
+| `video_path` | Path to an MP4 video clip. |
+| `action_path` | Path to the matching NPZ file. |
+
+
+Example (`data/minecraft/train.csv`):
+
+```csv
+video_path,action_path
+data/training/000001.mp4,data/training/000001.npz
+data/training/000002.mp4,data/training/000002.npz
+```
+
+### Train
+
+> **Note:**
+>
+> 1. Update `MASTER_ADDR` (and `NNODES` for multi-node) in the launch scripts before distributed training.
+> 2. Dense training (`use_ltm_attn: false`) can run on A100; LTM training requires H100/H800/H200 with the block-sparse kernel installed.
+> 3. Set `WANDB_KEY` / `wandb_entity` in the config or disable wandb with `--disable-wandb`.
+> 4. We train DecMem in three stages in this opensource version and load the checkpoints in the previous stage for resume training.
+
+#### Stage 1: Dense causal diffusion (Short)
+
+```bash
+# Edit NNODES, MASTER_ADDR, NPROC_PER_NODE in the script first.
+# On node 0:
+bash scripts/train_worldmem_multinode_manual.sh 0 dense_short
+# On node 1:
+bash scripts/train_worldmem_multinode_manual.sh 1 dense_short
+# On node 2:...
+```
+
+#### Stage 2: Dense causal diffusion (Long)
+
+Fine-tune from a short-sequence training dense checkpoint ; set `generator_ckpt` in the config:
+
+```bash
+# Edit NNODES, MASTER_ADDR, NPROC_PER_NODE in the script first.
+# On node 0:
+bash scripts/train_worldmem_multinode_manual.sh 0 dense
+# On node 1:
+bash scripts/train_worldmem_multinode_manual.sh 1 dense
+# On node 2:...
+```
+
+#### Stage 3: Decoupled memory (STM + LTM)
+
+Fine-tune from a long-sequence training dense checkpoint; set `generator_ckpt` in the config:
+
+```bash
+# Edit NNODES, MASTER_ADDR, NPROC_PER_NODE in the script first.
+# On node 0:
+bash scripts/train_worldmem_multinode_manual.sh 0 decmem
+# On node 1:
+bash scripts/train_worldmem_multinode_manual.sh 1 decmem
+# On node 2:...
+```
+
+You can set `RESUME_CKPT` in training scripts to resume from a full training checkpoint (weights + optimizer + step) .
+
+### Inference
+
+Once you finish the training in each stage, you can load the correponding checkpoints and configs for inference.
+
+Common overrides:
+
+```bash
+EXP_NAME=decmem \
+CHECKPOINT_PATH=checkpoints/decmem.pt \
+T_LAT=160 \
+NUM_CONDITION_FRAMES=56 \
+GUIDANCE_SCALE=5.0 \
+NUM_INFERENCE_STEPS=50 \
+bash scripts/infer.sh
+
+# Multi-GPU inference
+NUM_GPUS=8 bash scripts/infer.sh
+```
+
+
+| Argument | Description |
+| ---------------------- | --------------------------------------------------------------------- |
+| `NUM_CONDITION_FRAMES` | Number of clean prefix **latent** frames (conditioning video prefix). |
+| `T_LAT` | Number of clean prefix **latent** frames (conditioning video prefix). |
+| `GUIDANCE_SCALE` | CFG scale on actions;`≤ 1.0` disables CFG. |
+| `N` | Number of inference samples |
+| `NUM_INFERENCE_STEPS` | Number of denoising step |
+
+
+## 🤗 Acknowledgement
+
+This codebase is built on top of:
+
+- [WorldMem](https://github.com/xizaoqu/WorldMem) for action-pose data format and pose utilities.
+- [PRoPE](https://arxiv.org/pdf/2507.10496) for camera-relative positional encoding.
+- [Video Sparse Attention](https://github.com/hao-ai-lab/FastVideo) for the LTM sparse attention kernel.
+- [Self Forcing](https://github.com/gdhe17/Self-Forcing) and [CausVid](https://github.com/tianweiy/CausVid) for causal diffusion training and teacher forcing codebase.
+- [Wan2.1](https://github.com/Wan-Video/Wan2.1) as the base video diffusion backbone.
+
+## 🌟 Citation
+
+Please leave us a star 🌟 and cite our paper if you find our work helpful.
+
+```bibtex
+@misc{yang2026decmemminutelongconsistentworld,
+ title={DecMem: Towards Minute-Long Consistent World Generation with Decoupled Memory},
+ 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},
+ year={2026},
+ eprint={2605.31336},
+ archivePrefix={arXiv},
+ primaryClass={cs.CV},
+ url={https://arxiv.org/abs/2605.31336},
+}
+```
\ No newline at end of file
diff --git a/DecMem/configs/decmem.yaml b/DecMem/configs/decmem.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6f9b9ac210343653e3c352603fad1cd5c113c019
--- /dev/null
+++ b/DecMem/configs/decmem.yaml
@@ -0,0 +1,74 @@
+# WorldMem diffusion training config with Long-Term Memory (LTM)
+# Replaces the flex_attention self-attn path with STM (sliding-window dense via
+# flex_attention) + LTM (sparse block attention on H100 SM90), gated by a
+# zero-init mem_proj so initial step output matches the prior dense behaviour.
+
+trainer: diffusion
+causal: true
+mixed_precision: true
+seed: 0
+
+# Data
+# CSV columns: video_path, action_path (WorldMem format)
+data_path: data/train.csv
+num_frames: 60 # latent frame count; dataset loads (21-1)*4+1=81 video frames
+sample_strategy: random
+load_raw_video: true
+
+# CFG: probability of dropping action conditioning (zeros actions + identity viewmats)
+proportion_empty_prompts: 0.1
+
+# Model
+image_or_video_shape:
+- 1
+- 60
+- 16
+- 44
+- 80
+
+model_kwargs:
+ timestep_shift: 5.0
+ local_attn_size: -1
+ sink_size: 0
+ use_action_encoder: true
+ action_dim: 25
+ latent_channels: 16
+ # ── LTM ──
+ use_ltm_attn: true
+ ltm_tile_size: [1, 8, 8]
+ ltm_topk: 80 # top-k key blocks per query block (matches ar_mem default)
+ stm_sliding_window: 8 # STM window = 2 * num_frame_per_block (= 2 * 4 frames)
+
+# Training hyperparams
+num_train_timestep: 1000
+timestep_shift: 5.0
+guidance_scale: 1.0
+teacher_forcing: true
+noise_augmentation_max_timestep: 0
+generator_ckpt: exps/dense/checkpoint_model_010000/model.pt
+
+lr: 2.0e-5
+beta1: 0.9
+beta2: 0.999
+weight_decay: 0.01
+batch_size: 1
+gradient_checkpointing: true
+num_frame_per_block: 4
+sharding_strategy: hybrid_full
+
+# Logging / saving
+no_save: false
+no_visualize: true
+log_iters: 500
+# ema_weight: 0.999
+# ema_start_step: 500
+gc_interval: 100
+wandb_host: "https://api.wandb.ai"
+wandb_key: ""
+wandb_entity: ""
+wandb_project: ""
+negative_prompt: ""
+
+# FSDP wrap strategy
+generator_fsdp_wrap_strategy: size
+text_encoder_fsdp_wrap_strategy: size
diff --git a/DecMem/configs/default_config.yaml b/DecMem/configs/default_config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..3ca7ae3f11d8250f2ad8134767acc7e5ba37fded
--- /dev/null
+++ b/DecMem/configs/default_config.yaml
@@ -0,0 +1,20 @@
+independent_first_frame: false
+warp_denoising_step: false
+weight_decay: 0.01
+same_step_across_blocks: true
+discriminator_lr_multiplier: 1.0
+last_step_only: false
+i2v: false
+num_training_frames: 21
+gc_interval: 100
+context_noise: 0
+causal: true
+
+ckpt_step: 0
+prompt_name: MovieGenVideoBench
+prompt_path: prompts/MovieGenVideoBench.txt
+eval_first_n: 64
+num_samples: 1
+height: 480
+width: 832
+num_frames: 81
\ No newline at end of file
diff --git a/DecMem/configs/dense.yaml b/DecMem/configs/dense.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7d58eb178b62dfabfae5fa3138714eb2f3f96cdd
--- /dev/null
+++ b/DecMem/configs/dense.yaml
@@ -0,0 +1,68 @@
+# WorldMem diffusion training config
+# Action + PRoPE conditioning, no text conditioning, CFG on actions only
+
+trainer: diffusion
+causal: true
+mixed_precision: true
+seed: 0
+
+# Data
+# CSV columns: video_path, action_path (WorldMem format)
+data_path: data/train_wm_gamecraft/v2/worldmem_train_v2_shuffle.csv
+num_frames: 60 # latent frame count; dataset loads (21-1)*4+1=81 video frames
+load_raw_video: true
+sample_strategy: random # overfit: always start from SKIP_FRAMES; use "random" for normal training
+
+# CFG: probability of dropping action conditioning (zeros actions + identity viewmats)
+proportion_empty_prompts: 0.1
+
+# Model
+# 81 video frames @ 640x352 -> 21 latent frames @ 80x44 (WAN 4x temporal, 8x spatial)
+image_or_video_shape:
+- 1 # batch_size placeholder (overridden at runtime)
+- 60 # latent frames
+- 16 # latent channels
+- 44 # latent height (352/8)
+- 80 # latent width (640/8)
+
+model_kwargs:
+ timestep_shift: 5.0
+ local_attn_size: -1
+ sink_size: 0
+ use_action_encoder: true
+ action_dim: 25
+ latent_channels: 16
+
+# Training hyperparams
+num_train_timestep: 1000
+timestep_shift: 5.0
+guidance_scale: 1.0
+teacher_forcing: true
+noise_augmentation_max_timestep: 0
+generator_ckpt: exps/dense_short/checkpoint_model_022000/model.pt
+
+lr: 2.0e-5
+beta1: 0.9
+beta2: 0.999
+weight_decay: 0.01
+batch_size: 1
+gradient_checkpointing: true
+num_frame_per_block: 4
+sharding_strategy: hybrid_full
+
+# Logging / saving
+no_save: false
+no_visualize: true
+log_iters: 500
+# ema_weight: 0.999
+# ema_start_step: 500
+gc_interval: 100
+wandb_host: "https://api.wandb.ai"
+wandb_key:
+wandb_entity: ""
+wandb_project: ""
+negative_prompt: ""
+
+# FSDP wrap strategy
+generator_fsdp_wrap_strategy: size
+text_encoder_fsdp_wrap_strategy: size
\ No newline at end of file
diff --git a/DecMem/configs/dense_short.yaml b/DecMem/configs/dense_short.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..061f59ddad41c288676a0c9b80daa60e71a31d02
--- /dev/null
+++ b/DecMem/configs/dense_short.yaml
@@ -0,0 +1,67 @@
+# WorldMem diffusion training config
+# Action + PRoPE conditioning, no text conditioning, CFG on actions only
+
+trainer: diffusion
+causal: true
+mixed_precision: true
+seed: 0
+
+# Data
+# CSV columns: video_path, action_path (WorldMem format)
+data_path: data/train_wm_gamecraft/v2/worldmem_train_v2_shuffle.csv
+num_frames: 20 # latent frame count; dataset loads (21-1)*4+1=81 video frames
+load_raw_video: true
+sample_strategy: random # overfit: always start from SKIP_FRAMES; use "random" for normal training
+
+# CFG: probability of dropping action conditioning (zeros actions + identity viewmats)
+proportion_empty_prompts: 0.1
+
+# Model
+# 81 video frames @ 640x352 -> 21 latent frames @ 80x44 (WAN 4x temporal, 8x spatial)
+image_or_video_shape:
+- 1 # batch_size placeholder (overridden at runtime)
+- 20 # latent frames
+- 16 # latent channels
+- 44 # latent height (352/8)
+- 80 # latent width (640/8)
+
+model_kwargs:
+ timestep_shift: 5.0
+ local_attn_size: -1
+ sink_size: 0
+ use_action_encoder: true
+ action_dim: 25
+ latent_channels: 16
+
+# Training hyperparams
+num_train_timestep: 1000
+timestep_shift: 5.0
+guidance_scale: 1.0
+teacher_forcing: true
+noise_augmentation_max_timestep: 0
+
+lr: 1.0e-4
+beta1: 0.9
+beta2: 0.999
+weight_decay: 0.01
+batch_size: 1
+gradient_checkpointing: true
+num_frame_per_block: 4
+sharding_strategy: hybrid_full
+
+# Logging / saving
+no_save: false
+no_visualize: true
+log_iters: 500
+# ema_weight: 0.0
+# ema_start_step: 200
+gc_interval: 100
+wandb_host: "https://api.wandb.ai"
+wandb_key:
+wandb_entity: ""
+wandb_project: ""
+negative_prompt: ""
+
+# FSDP wrap strategy
+generator_fsdp_wrap_strategy: size
+text_encoder_fsdp_wrap_strategy: size
\ No newline at end of file
diff --git a/DecMem/configs/worldmem_diffusion.yaml b/DecMem/configs/worldmem_diffusion.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..114a8f5548e4798610e314a8caf070b6840280b9
--- /dev/null
+++ b/DecMem/configs/worldmem_diffusion.yaml
@@ -0,0 +1,69 @@
+# WorldMem diffusion training config
+# Action + PRoPE conditioning, no text conditioning, CFG on actions only
+
+trainer: diffusion
+causal: true
+mixed_precision: true
+seed: 0
+
+# Data
+# CSV columns: video_path, action_path (WorldMem format)
+data_path: data/train_wm_gamecraft/v2/worldmem_train_v2_shuffle.csv
+num_frames: 28 # latent frame count; dataset loads (21-1)*4+1=81 video frames
+load_raw_video: true
+sample_strategy: first # overfit: always start from SKIP_FRAMES; use "random" for normal training
+
+# CFG: probability of dropping action conditioning (zeros actions + identity viewmats)
+proportion_empty_prompts: 0.1
+
+# Model
+# 81 video frames @ 640x352 -> 21 latent frames @ 80x44 (WAN 4x temporal, 8x spatial)
+image_or_video_shape:
+- 1 # batch_size placeholder (overridden at runtime)
+- 28 # latent frames
+- 16 # latent channels
+- 44 # latent height (352/8)
+- 80 # latent width (640/8)
+
+model_kwargs:
+ timestep_shift: 5.0
+ local_attn_size: -1
+ sink_size: 0
+ use_action_encoder: true
+ action_dim: 25
+ action_hidden_dim: 128
+ latent_channels: 16
+
+# Training hyperparams
+num_train_timestep: 1000
+timestep_shift: 5.0
+guidance_scale: 1.0
+teacher_forcing: true
+# noise_augmentation_max_timestep: 100
+noise_augmentation_max_timestep: 0
+
+lr: 1.0e-4
+beta1: 0.9
+beta2: 0.999
+weight_decay: 0.01
+batch_size: 1
+gradient_checkpointing: true
+num_frame_per_block: 4
+sharding_strategy: hybrid_full
+
+# Logging / saving
+no_save: false
+no_visualize: true
+log_iters: 300
+ema_weight: 0.999
+ema_start_step: 200
+gc_interval: 100
+wandb_host: "https://api.wandb.ai"
+wandb_key: WANDB_KEY
+wandb_entity: "zhenhao-yang11-the-university-of-hong-kong"
+wandb_project: worldmem_diffusion
+negative_prompt: ""
+
+# FSDP wrap strategy
+generator_fsdp_wrap_strategy: size
+text_encoder_fsdp_wrap_strategy: size
\ No newline at end of file
diff --git a/DecMem/demo_utils/constant.py b/DecMem/demo_utils/constant.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ef237829ca620a5b0b4b00ef7a795fb36a0ca94
--- /dev/null
+++ b/DecMem/demo_utils/constant.py
@@ -0,0 +1,41 @@
+
+import torch
+
+
+ZERO_VAE_CACHE = [
+ torch.zeros(1, 16, 2, 60, 104),
+ torch.zeros(1, 384, 2, 60, 104),
+ torch.zeros(1, 384, 2, 60, 104),
+ torch.zeros(1, 384, 2, 60, 104),
+ torch.zeros(1, 384, 2, 60, 104),
+ torch.zeros(1, 384, 2, 60, 104),
+ torch.zeros(1, 384, 2, 60, 104),
+ torch.zeros(1, 384, 2, 60, 104),
+ torch.zeros(1, 384, 2, 60, 104),
+ torch.zeros(1, 384, 2, 60, 104),
+ torch.zeros(1, 384, 2, 60, 104),
+ torch.zeros(1, 384, 2, 60, 104),
+ torch.zeros(1, 192, 2, 120, 208),
+ torch.zeros(1, 384, 2, 120, 208),
+ torch.zeros(1, 384, 2, 120, 208),
+ torch.zeros(1, 384, 2, 120, 208),
+ torch.zeros(1, 384, 2, 120, 208),
+ torch.zeros(1, 384, 2, 120, 208),
+ torch.zeros(1, 384, 2, 120, 208),
+ torch.zeros(1, 192, 2, 240, 416),
+ torch.zeros(1, 192, 2, 240, 416),
+ torch.zeros(1, 192, 2, 240, 416),
+ torch.zeros(1, 192, 2, 240, 416),
+ torch.zeros(1, 192, 2, 240, 416),
+ torch.zeros(1, 192, 2, 240, 416),
+ torch.zeros(1, 96, 2, 480, 832),
+ torch.zeros(1, 96, 2, 480, 832),
+ torch.zeros(1, 96, 2, 480, 832),
+ torch.zeros(1, 96, 2, 480, 832),
+ torch.zeros(1, 96, 2, 480, 832),
+ torch.zeros(1, 96, 2, 480, 832),
+ torch.zeros(1, 96, 2, 480, 832)
+]
+
+feat_names = [f"vae_cache_{i}" for i in range(len(ZERO_VAE_CACHE))]
+ALL_INPUTS_NAMES = ["z", "use_cache"] + feat_names
diff --git a/DecMem/demo_utils/memory.py b/DecMem/demo_utils/memory.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9758df6dffaed70887f2ec3bf14d9fa49181c32
--- /dev/null
+++ b/DecMem/demo_utils/memory.py
@@ -0,0 +1,135 @@
+# Copied from https://github.com/lllyasviel/FramePack/tree/main/demo_utils
+# Apache-2.0 License
+# By lllyasviel
+
+import torch
+
+
+cpu = torch.device('cpu')
+gpu = torch.device(f'cuda:{torch.cuda.current_device()}')
+gpu_complete_modules = []
+
+
+class DynamicSwapInstaller:
+ @staticmethod
+ def _install_module(module: torch.nn.Module, **kwargs):
+ original_class = module.__class__
+ module.__dict__['forge_backup_original_class'] = original_class
+
+ def hacked_get_attr(self, name: str):
+ if '_parameters' in self.__dict__:
+ _parameters = self.__dict__['_parameters']
+ if name in _parameters:
+ p = _parameters[name]
+ if p is None:
+ return None
+ if p.__class__ == torch.nn.Parameter:
+ return torch.nn.Parameter(p.to(**kwargs), requires_grad=p.requires_grad)
+ else:
+ return p.to(**kwargs)
+ if '_buffers' in self.__dict__:
+ _buffers = self.__dict__['_buffers']
+ if name in _buffers:
+ return _buffers[name].to(**kwargs)
+ return super(original_class, self).__getattr__(name)
+
+ module.__class__ = type('DynamicSwap_' + original_class.__name__, (original_class,), {
+ '__getattr__': hacked_get_attr,
+ })
+
+ return
+
+ @staticmethod
+ def _uninstall_module(module: torch.nn.Module):
+ if 'forge_backup_original_class' in module.__dict__:
+ module.__class__ = module.__dict__.pop('forge_backup_original_class')
+ return
+
+ @staticmethod
+ def install_model(model: torch.nn.Module, **kwargs):
+ for m in model.modules():
+ DynamicSwapInstaller._install_module(m, **kwargs)
+ return
+
+ @staticmethod
+ def uninstall_model(model: torch.nn.Module):
+ for m in model.modules():
+ DynamicSwapInstaller._uninstall_module(m)
+ return
+
+
+def fake_diffusers_current_device(model: torch.nn.Module, target_device: torch.device):
+ if hasattr(model, 'scale_shift_table'):
+ model.scale_shift_table.data = model.scale_shift_table.data.to(target_device)
+ return
+
+ for k, p in model.named_modules():
+ if hasattr(p, 'weight'):
+ p.to(target_device)
+ return
+
+
+def get_cuda_free_memory_gb(device=None):
+ if device is None:
+ device = gpu
+
+ memory_stats = torch.cuda.memory_stats(device)
+ bytes_active = memory_stats['active_bytes.all.current']
+ bytes_reserved = memory_stats['reserved_bytes.all.current']
+ bytes_free_cuda, _ = torch.cuda.mem_get_info(device)
+ bytes_inactive_reserved = bytes_reserved - bytes_active
+ bytes_total_available = bytes_free_cuda + bytes_inactive_reserved
+ return bytes_total_available / (1024 ** 3)
+
+
+def move_model_to_device_with_memory_preservation(model, target_device, preserved_memory_gb=0):
+ print(f'Moving {model.__class__.__name__} to {target_device} with preserved memory: {preserved_memory_gb} GB')
+
+ for m in model.modules():
+ if get_cuda_free_memory_gb(target_device) <= preserved_memory_gb:
+ torch.cuda.empty_cache()
+ return
+
+ if hasattr(m, 'weight'):
+ m.to(device=target_device)
+
+ model.to(device=target_device)
+ torch.cuda.empty_cache()
+ return
+
+
+def offload_model_from_device_for_memory_preservation(model, target_device, preserved_memory_gb=0):
+ print(f'Offloading {model.__class__.__name__} from {target_device} to preserve memory: {preserved_memory_gb} GB')
+
+ for m in model.modules():
+ if get_cuda_free_memory_gb(target_device) >= preserved_memory_gb:
+ torch.cuda.empty_cache()
+ return
+
+ if hasattr(m, 'weight'):
+ m.to(device=cpu)
+
+ model.to(device=cpu)
+ torch.cuda.empty_cache()
+ return
+
+
+def unload_complete_models(*args):
+ for m in gpu_complete_modules + list(args):
+ m.to(device=cpu)
+ print(f'Unloaded {m.__class__.__name__} as complete.')
+
+ gpu_complete_modules.clear()
+ torch.cuda.empty_cache()
+ return
+
+
+def load_model_as_complete(model, target_device, unload=True):
+ if unload:
+ unload_complete_models()
+
+ model.to(device=target_device)
+ print(f'Loaded {model.__class__.__name__} to {target_device} as complete.')
+
+ gpu_complete_modules.append(model)
+ return
diff --git a/DecMem/demo_utils/taehv.py b/DecMem/demo_utils/taehv.py
new file mode 100644
index 0000000000000000000000000000000000000000..8531563e7d8da9cf5b1f93f46fda8215f50ed769
--- /dev/null
+++ b/DecMem/demo_utils/taehv.py
@@ -0,0 +1,313 @@
+#!/usr/bin/env python3
+"""
+Tiny AutoEncoder for Hunyuan Video
+(DNN for encoding / decoding videos to Hunyuan Video's latent space)
+"""
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from tqdm.auto import tqdm
+from collections import namedtuple
+
+DecoderResult = namedtuple("DecoderResult", ("frame", "memory"))
+TWorkItem = namedtuple("TWorkItem", ("input_tensor", "block_index"))
+
+
+def conv(n_in, n_out, **kwargs):
+ return nn.Conv2d(n_in, n_out, 3, padding=1, **kwargs)
+
+
+class Clamp(nn.Module):
+ def forward(self, x):
+ return torch.tanh(x / 3) * 3
+
+
+class MemBlock(nn.Module):
+ def __init__(self, n_in, n_out):
+ super().__init__()
+ self.conv = nn.Sequential(conv(n_in * 2, n_out), nn.ReLU(inplace=True),
+ conv(n_out, n_out), nn.ReLU(inplace=True), conv(n_out, n_out))
+ self.skip = nn.Conv2d(n_in, n_out, 1, bias=False) if n_in != n_out else nn.Identity()
+ self.act = nn.ReLU(inplace=True)
+
+ def forward(self, x, past):
+ return self.act(self.conv(torch.cat([x, past], 1)) + self.skip(x))
+
+
+class TPool(nn.Module):
+ def __init__(self, n_f, stride):
+ super().__init__()
+ self.stride = stride
+ self.conv = nn.Conv2d(n_f * stride, n_f, 1, bias=False)
+
+ def forward(self, x):
+ _NT, C, H, W = x.shape
+ return self.conv(x.reshape(-1, self.stride * C, H, W))
+
+
+class TGrow(nn.Module):
+ def __init__(self, n_f, stride):
+ super().__init__()
+ self.stride = stride
+ self.conv = nn.Conv2d(n_f, n_f * stride, 1, bias=False)
+
+ def forward(self, x):
+ _NT, C, H, W = x.shape
+ x = self.conv(x)
+ return x.reshape(-1, C, H, W)
+
+
+def apply_model_with_memblocks(model, x, parallel, show_progress_bar):
+ """
+ Apply a sequential model with memblocks to the given input.
+ Args:
+ - model: nn.Sequential of blocks to apply
+ - x: input data, of dimensions NTCHW
+ - parallel: if True, parallelize over timesteps (fast but uses O(T) memory)
+ if False, each timestep will be processed sequentially (slow but uses O(1) memory)
+ - show_progress_bar: if True, enables tqdm progressbar display
+
+ Returns NTCHW tensor of output data.
+ """
+ assert x.ndim == 5, f"TAEHV operates on NTCHW tensors, but got {x.ndim}-dim tensor"
+ N, T, C, H, W = x.shape
+ if parallel:
+ x = x.reshape(N * T, C, H, W)
+ # parallel over input timesteps, iterate over blocks
+ for b in tqdm(model, disable=not show_progress_bar):
+ if isinstance(b, MemBlock):
+ NT, C, H, W = x.shape
+ T = NT // N
+ _x = x.reshape(N, T, C, H, W)
+ mem = F.pad(_x, (0, 0, 0, 0, 0, 0, 1, 0), value=0)[:, :T].reshape(x.shape)
+ x = b(x, mem)
+ else:
+ x = b(x)
+ NT, C, H, W = x.shape
+ T = NT // N
+ x = x.view(N, T, C, H, W)
+ else:
+ # TODO(oboerbohan): at least on macos this still gradually uses more memory during decode...
+ # need to fix :(
+ out = []
+ # iterate over input timesteps and also iterate over blocks.
+ # because of the cursed TPool/TGrow blocks, this is not a nested loop,
+ # it's actually a ***graph traversal*** problem! so let's make a queue
+ work_queue = [TWorkItem(xt, 0) for t, xt in enumerate(x.reshape(N, T * C, H, W).chunk(T, dim=1))]
+ # in addition to manually managing our queue, we also need to manually manage our progressbar.
+ # we'll update it for every source node that we consume.
+ progress_bar = tqdm(range(T), disable=not show_progress_bar)
+ # we'll also need a separate addressable memory per node as well
+ mem = [None] * len(model)
+ while work_queue:
+ xt, i = work_queue.pop(0)
+ if i == 0:
+ # new source node consumed
+ progress_bar.update(1)
+ if i == len(model):
+ # reached end of the graph, append result to output list
+ out.append(xt)
+ else:
+ # fetch the block to process
+ b = model[i]
+ if isinstance(b, MemBlock):
+ # mem blocks are simple since we're visiting the graph in causal order
+ if mem[i] is None:
+ xt_new = b(xt, xt * 0)
+ mem[i] = xt
+ else:
+ xt_new = b(xt, mem[i])
+ mem[i].copy_(xt) # inplace might reduce mysterious pytorch memory allocations? doesn't help though
+ # add successor to work queue
+ work_queue.insert(0, TWorkItem(xt_new, i + 1))
+ elif isinstance(b, TPool):
+ # pool blocks are miserable
+ if mem[i] is None:
+ mem[i] = [] # pool memory is itself a queue of inputs to pool
+ mem[i].append(xt)
+ if len(mem[i]) > b.stride:
+ # pool mem is in invalid state, we should have pooled before this
+ raise ValueError("???")
+ elif len(mem[i]) < b.stride:
+ # pool mem is not yet full, go back to processing the work queue
+ pass
+ else:
+ # pool mem is ready, run the pool block
+ N, C, H, W = xt.shape
+ xt = b(torch.cat(mem[i], 1).view(N * b.stride, C, H, W))
+ # reset the pool mem
+ mem[i] = []
+ # add successor to work queue
+ work_queue.insert(0, TWorkItem(xt, i + 1))
+ elif isinstance(b, TGrow):
+ xt = b(xt)
+ NT, C, H, W = xt.shape
+ # each tgrow has multiple successor nodes
+ for xt_next in reversed(xt.view(N, b.stride * C, H, W).chunk(b.stride, 1)):
+ # add successor to work queue
+ work_queue.insert(0, TWorkItem(xt_next, i + 1))
+ else:
+ # normal block with no funny business
+ xt = b(xt)
+ # add successor to work queue
+ work_queue.insert(0, TWorkItem(xt, i + 1))
+ progress_bar.close()
+ x = torch.stack(out, 1)
+ return x
+
+
+class TAEHV(nn.Module):
+ latent_channels = 16
+ image_channels = 3
+
+ def __init__(self, checkpoint_path="taehv.pth", decoder_time_upscale=(True, True), decoder_space_upscale=(True, True, True)):
+ """Initialize pretrained TAEHV from the given checkpoint.
+
+ Arg:
+ checkpoint_path: path to weight file to load. taehv.pth for Hunyuan, taew2_1.pth for Wan 2.1.
+ decoder_time_upscale: whether temporal upsampling is enabled for each block. upsampling can be disabled for a cheaper preview.
+ decoder_space_upscale: whether spatial upsampling is enabled for each block. upsampling can be disabled for a cheaper preview.
+ """
+ super().__init__()
+ self.encoder = nn.Sequential(
+ conv(TAEHV.image_channels, 64), nn.ReLU(inplace=True),
+ TPool(64, 2), conv(64, 64, stride=2, bias=False), MemBlock(64, 64), MemBlock(64, 64), MemBlock(64, 64),
+ TPool(64, 2), conv(64, 64, stride=2, bias=False), MemBlock(64, 64), MemBlock(64, 64), MemBlock(64, 64),
+ TPool(64, 1), conv(64, 64, stride=2, bias=False), MemBlock(64, 64), MemBlock(64, 64), MemBlock(64, 64),
+ conv(64, TAEHV.latent_channels),
+ )
+ n_f = [256, 128, 64, 64]
+ self.frames_to_trim = 2**sum(decoder_time_upscale) - 1
+ self.decoder = nn.Sequential(
+ Clamp(), conv(TAEHV.latent_channels, n_f[0]), nn.ReLU(inplace=True),
+ MemBlock(n_f[0], n_f[0]), MemBlock(n_f[0], n_f[0]), MemBlock(n_f[0], n_f[0]), nn.Upsample(
+ scale_factor=2 if decoder_space_upscale[0] else 1), TGrow(n_f[0], 1), conv(n_f[0], n_f[1], bias=False),
+ MemBlock(n_f[1], n_f[1]), MemBlock(n_f[1], n_f[1]), MemBlock(n_f[1], n_f[1]), nn.Upsample(
+ 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),
+ MemBlock(n_f[2], n_f[2]), MemBlock(n_f[2], n_f[2]), MemBlock(n_f[2], n_f[2]), nn.Upsample(
+ 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),
+ nn.ReLU(inplace=True), conv(n_f[3], TAEHV.image_channels),
+ )
+ if checkpoint_path is not None:
+ self.load_state_dict(self.patch_tgrow_layers(torch.load(
+ checkpoint_path, map_location="cpu", weights_only=True)))
+
+ def patch_tgrow_layers(self, sd):
+ """Patch TGrow layers to use a smaller kernel if needed.
+
+ Args:
+ sd: state dict to patch
+ """
+ new_sd = self.state_dict()
+ for i, layer in enumerate(self.decoder):
+ if isinstance(layer, TGrow):
+ key = f"decoder.{i}.conv.weight"
+ if sd[key].shape[0] > new_sd[key].shape[0]:
+ # take the last-timestep output channels
+ sd[key] = sd[key][-new_sd[key].shape[0]:]
+ return sd
+
+ def encode_video(self, x, parallel=True, show_progress_bar=True):
+ """Encode a sequence of frames.
+
+ Args:
+ x: input NTCHW RGB (C=3) tensor with values in [0, 1].
+ parallel: if True, all frames will be processed at once.
+ (this is faster but may require more memory).
+ if False, frames will be processed sequentially.
+ Returns NTCHW latent tensor with ~Gaussian values.
+ """
+ return apply_model_with_memblocks(self.encoder, x, parallel, show_progress_bar)
+
+ def decode_video(self, x, parallel=True, show_progress_bar=False):
+ """Decode a sequence of frames.
+
+ Args:
+ x: input NTCHW latent (C=12) tensor with ~Gaussian values.
+ parallel: if True, all frames will be processed at once.
+ (this is faster but may require more memory).
+ if False, frames will be processed sequentially.
+ Returns NTCHW RGB tensor with ~[0, 1] values.
+ """
+ x = apply_model_with_memblocks(self.decoder, x, parallel, show_progress_bar)
+ # return x[:, self.frames_to_trim:]
+ return x
+
+ def forward(self, x):
+ return self.c(x)
+
+
+@torch.no_grad()
+def main():
+ """Run TAEHV roundtrip reconstruction on the given video paths."""
+ import os
+ import sys
+ import cv2 # no highly esteemed deed is commemorated here
+
+ class VideoTensorReader:
+ def __init__(self, video_file_path):
+ self.cap = cv2.VideoCapture(video_file_path)
+ assert self.cap.isOpened(), f"Could not load {video_file_path}"
+ self.fps = self.cap.get(cv2.CAP_PROP_FPS)
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ ret, frame = self.cap.read()
+ if not ret:
+ self.cap.release()
+ raise StopIteration # End of video or error
+ return torch.from_numpy(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)).permute(2, 0, 1) # BGR HWC -> RGB CHW
+
+ class VideoTensorWriter:
+ def __init__(self, video_file_path, width_height, fps=30):
+ self.writer = cv2.VideoWriter(video_file_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, width_height)
+ assert self.writer.isOpened(), f"Could not create writer for {video_file_path}"
+
+ def write(self, frame_tensor):
+ assert frame_tensor.ndim == 3 and frame_tensor.shape[0] == 3, f"{frame_tensor.shape}??"
+ self.writer.write(cv2.cvtColor(frame_tensor.permute(1, 2, 0).numpy(),
+ cv2.COLOR_RGB2BGR)) # RGB CHW -> BGR HWC
+
+ def __del__(self):
+ if hasattr(self, 'writer'):
+ self.writer.release()
+
+ dev = torch.device("cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu")
+ dtype = torch.float16
+ checkpoint_path = os.getenv("TAEHV_CHECKPOINT_PATH", "taehv.pth")
+ checkpoint_name = os.path.splitext(os.path.basename(checkpoint_path))[0]
+ print(
+ f"Using device \033[31m{dev}\033[0m, dtype \033[32m{dtype}\033[0m, checkpoint \033[34m{checkpoint_name}\033[0m ({checkpoint_path})")
+ taehv = TAEHV(checkpoint_path=checkpoint_path).to(dev, dtype)
+ for video_path in sys.argv[1:]:
+ print(f"Processing {video_path}...")
+ video_in = VideoTensorReader(video_path)
+ video = torch.stack(list(video_in), 0)[None]
+ vid_dev = video.to(dev, dtype).div_(255.0)
+ # convert to device tensor
+ if video.numel() < 100_000_000:
+ print(f" {video_path} seems small enough, will process all frames in parallel")
+ # convert to device tensor
+ vid_enc = taehv.encode_video(vid_dev)
+ print(f" Encoded {video_path} -> {vid_enc.shape}. Decoding...")
+ vid_dec = taehv.decode_video(vid_enc)
+ print(f" Decoded {video_path} -> {vid_dec.shape}")
+ else:
+ print(f" {video_path} seems large, will process each frame sequentially")
+ # convert to device tensor
+ vid_enc = taehv.encode_video(vid_dev, parallel=False)
+ print(f" Encoded {video_path} -> {vid_enc.shape}. Decoding...")
+ vid_dec = taehv.decode_video(vid_enc, parallel=False)
+ print(f" Decoded {video_path} -> {vid_dec.shape}")
+ video_out_path = video_path + f".reconstructed_by_{checkpoint_name}.mp4"
+ video_out = VideoTensorWriter(
+ video_out_path, (vid_dec.shape[-1], vid_dec.shape[-2]), fps=int(round(video_in.fps)))
+ for frame in vid_dec.clamp_(0, 1).mul_(255).round_().byte().cpu()[0]:
+ video_out.write(frame)
+ print(f" Saved to {video_out_path}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/DecMem/demo_utils/utils.py b/DecMem/demo_utils/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..a24aefb4d8074acdce8fe3d32f6e37c07c4d6baa
--- /dev/null
+++ b/DecMem/demo_utils/utils.py
@@ -0,0 +1,616 @@
+# Copied from https://github.com/lllyasviel/FramePack/tree/main/demo_utils
+# Apache-2.0 License
+# By lllyasviel
+
+import os
+import cv2
+import json
+import random
+import glob
+import torch
+import einops
+import numpy as np
+import datetime
+import torchvision
+
+from PIL import Image
+
+
+def min_resize(x, m):
+ if x.shape[0] < x.shape[1]:
+ s0 = m
+ s1 = int(float(m) / float(x.shape[0]) * float(x.shape[1]))
+ else:
+ s0 = int(float(m) / float(x.shape[1]) * float(x.shape[0]))
+ s1 = m
+ new_max = max(s1, s0)
+ raw_max = max(x.shape[0], x.shape[1])
+ if new_max < raw_max:
+ interpolation = cv2.INTER_AREA
+ else:
+ interpolation = cv2.INTER_LANCZOS4
+ y = cv2.resize(x, (s1, s0), interpolation=interpolation)
+ return y
+
+
+def d_resize(x, y):
+ H, W, C = y.shape
+ new_min = min(H, W)
+ raw_min = min(x.shape[0], x.shape[1])
+ if new_min < raw_min:
+ interpolation = cv2.INTER_AREA
+ else:
+ interpolation = cv2.INTER_LANCZOS4
+ y = cv2.resize(x, (W, H), interpolation=interpolation)
+ return y
+
+
+def resize_and_center_crop(image, target_width, target_height):
+ if target_height == image.shape[0] and target_width == image.shape[1]:
+ return image
+
+ pil_image = Image.fromarray(image)
+ original_width, original_height = pil_image.size
+ scale_factor = max(target_width / original_width, target_height / original_height)
+ resized_width = int(round(original_width * scale_factor))
+ resized_height = int(round(original_height * scale_factor))
+ resized_image = pil_image.resize((resized_width, resized_height), Image.LANCZOS)
+ left = (resized_width - target_width) / 2
+ top = (resized_height - target_height) / 2
+ right = (resized_width + target_width) / 2
+ bottom = (resized_height + target_height) / 2
+ cropped_image = resized_image.crop((left, top, right, bottom))
+ return np.array(cropped_image)
+
+
+def resize_and_center_crop_pytorch(image, target_width, target_height):
+ B, C, H, W = image.shape
+
+ if H == target_height and W == target_width:
+ return image
+
+ scale_factor = max(target_width / W, target_height / H)
+ resized_width = int(round(W * scale_factor))
+ resized_height = int(round(H * scale_factor))
+
+ resized = torch.nn.functional.interpolate(image, size=(resized_height, resized_width), mode='bilinear', align_corners=False)
+
+ top = (resized_height - target_height) // 2
+ left = (resized_width - target_width) // 2
+ cropped = resized[:, :, top:top + target_height, left:left + target_width]
+
+ return cropped
+
+
+def resize_without_crop(image, target_width, target_height):
+ if target_height == image.shape[0] and target_width == image.shape[1]:
+ return image
+
+ pil_image = Image.fromarray(image)
+ resized_image = pil_image.resize((target_width, target_height), Image.LANCZOS)
+ return np.array(resized_image)
+
+
+def just_crop(image, w, h):
+ if h == image.shape[0] and w == image.shape[1]:
+ return image
+
+ original_height, original_width = image.shape[:2]
+ k = min(original_height / h, original_width / w)
+ new_width = int(round(w * k))
+ new_height = int(round(h * k))
+ x_start = (original_width - new_width) // 2
+ y_start = (original_height - new_height) // 2
+ cropped_image = image[y_start:y_start + new_height, x_start:x_start + new_width]
+ return cropped_image
+
+
+def write_to_json(data, file_path):
+ temp_file_path = file_path + ".tmp"
+ with open(temp_file_path, 'wt', encoding='utf-8') as temp_file:
+ json.dump(data, temp_file, indent=4)
+ os.replace(temp_file_path, file_path)
+ return
+
+
+def read_from_json(file_path):
+ with open(file_path, 'rt', encoding='utf-8') as file:
+ data = json.load(file)
+ return data
+
+
+def get_active_parameters(m):
+ return {k: v for k, v in m.named_parameters() if v.requires_grad}
+
+
+def cast_training_params(m, dtype=torch.float32):
+ result = {}
+ for n, param in m.named_parameters():
+ if param.requires_grad:
+ param.data = param.to(dtype)
+ result[n] = param
+ return result
+
+
+def separate_lora_AB(parameters, B_patterns=None):
+ parameters_normal = {}
+ parameters_B = {}
+
+ if B_patterns is None:
+ B_patterns = ['.lora_B.', '__zero__']
+
+ for k, v in parameters.items():
+ if any(B_pattern in k for B_pattern in B_patterns):
+ parameters_B[k] = v
+ else:
+ parameters_normal[k] = v
+
+ return parameters_normal, parameters_B
+
+
+def set_attr_recursive(obj, attr, value):
+ attrs = attr.split(".")
+ for name in attrs[:-1]:
+ obj = getattr(obj, name)
+ setattr(obj, attrs[-1], value)
+ return
+
+
+def print_tensor_list_size(tensors):
+ total_size = 0
+ total_elements = 0
+
+ if isinstance(tensors, dict):
+ tensors = tensors.values()
+
+ for tensor in tensors:
+ total_size += tensor.nelement() * tensor.element_size()
+ total_elements += tensor.nelement()
+
+ total_size_MB = total_size / (1024 ** 2)
+ total_elements_B = total_elements / 1e9
+
+ print(f"Total number of tensors: {len(tensors)}")
+ print(f"Total size of tensors: {total_size_MB:.2f} MB")
+ print(f"Total number of parameters: {total_elements_B:.3f} billion")
+ return
+
+
+@torch.no_grad()
+def batch_mixture(a, b=None, probability_a=0.5, mask_a=None):
+ batch_size = a.size(0)
+
+ if b is None:
+ b = torch.zeros_like(a)
+
+ if mask_a is None:
+ mask_a = torch.rand(batch_size) < probability_a
+
+ mask_a = mask_a.to(a.device)
+ mask_a = mask_a.reshape((batch_size,) + (1,) * (a.dim() - 1))
+ result = torch.where(mask_a, a, b)
+ return result
+
+
+@torch.no_grad()
+def zero_module(module):
+ for p in module.parameters():
+ p.detach().zero_()
+ return module
+
+
+@torch.no_grad()
+def supress_lower_channels(m, k, alpha=0.01):
+ data = m.weight.data.clone()
+
+ assert int(data.shape[1]) >= k
+
+ data[:, :k] = data[:, :k] * alpha
+ m.weight.data = data.contiguous().clone()
+ return m
+
+
+def freeze_module(m):
+ if not hasattr(m, '_forward_inside_frozen_module'):
+ m._forward_inside_frozen_module = m.forward
+ m.requires_grad_(False)
+ m.forward = torch.no_grad()(m.forward)
+ return m
+
+
+def get_latest_safetensors(folder_path):
+ safetensors_files = glob.glob(os.path.join(folder_path, '*.safetensors'))
+
+ if not safetensors_files:
+ raise ValueError('No file to resume!')
+
+ latest_file = max(safetensors_files, key=os.path.getmtime)
+ latest_file = os.path.abspath(os.path.realpath(latest_file))
+ return latest_file
+
+
+def generate_random_prompt_from_tags(tags_str, min_length=3, max_length=32):
+ tags = tags_str.split(', ')
+ tags = random.sample(tags, k=min(random.randint(min_length, max_length), len(tags)))
+ prompt = ', '.join(tags)
+ return prompt
+
+
+def interpolate_numbers(a, b, n, round_to_int=False, gamma=1.0):
+ numbers = a + (b - a) * (np.linspace(0, 1, n) ** gamma)
+ if round_to_int:
+ numbers = np.round(numbers).astype(int)
+ return numbers.tolist()
+
+
+def uniform_random_by_intervals(inclusive, exclusive, n, round_to_int=False):
+ edges = np.linspace(0, 1, n + 1)
+ points = np.random.uniform(edges[:-1], edges[1:])
+ numbers = inclusive + (exclusive - inclusive) * points
+ if round_to_int:
+ numbers = np.round(numbers).astype(int)
+ return numbers.tolist()
+
+
+def soft_append_bcthw(history, current, overlap=0):
+ if overlap <= 0:
+ return torch.cat([history, current], dim=2)
+
+ assert history.shape[2] >= overlap, f"History length ({history.shape[2]}) must be >= overlap ({overlap})"
+ assert current.shape[2] >= overlap, f"Current length ({current.shape[2]}) must be >= overlap ({overlap})"
+
+ weights = torch.linspace(1, 0, overlap, dtype=history.dtype, device=history.device).view(1, 1, -1, 1, 1)
+ blended = weights * history[:, :, -overlap:] + (1 - weights) * current[:, :, :overlap]
+ output = torch.cat([history[:, :, :-overlap], blended, current[:, :, overlap:]], dim=2)
+
+ return output.to(history)
+
+
+def save_bcthw_as_mp4(x, output_filename, fps=10, crf=0):
+ b, c, t, h, w = x.shape
+
+ per_row = b
+ for p in [6, 5, 4, 3, 2]:
+ if b % p == 0:
+ per_row = p
+ break
+
+ os.makedirs(os.path.dirname(os.path.abspath(os.path.realpath(output_filename))), exist_ok=True)
+ x = torch.clamp(x.float(), -1., 1.) * 127.5 + 127.5
+ x = x.detach().cpu().to(torch.uint8)
+ x = einops.rearrange(x, '(m n) c t h w -> t (m h) (n w) c', n=per_row)
+ torchvision.io.write_video(output_filename, x, fps=fps, video_codec='libx264', options={'crf': str(int(crf))})
+ return x
+
+
+def save_bcthw_as_png(x, output_filename):
+ os.makedirs(os.path.dirname(os.path.abspath(os.path.realpath(output_filename))), exist_ok=True)
+ x = torch.clamp(x.float(), -1., 1.) * 127.5 + 127.5
+ x = x.detach().cpu().to(torch.uint8)
+ x = einops.rearrange(x, 'b c t h w -> c (b h) (t w)')
+ torchvision.io.write_png(x, output_filename)
+ return output_filename
+
+
+def save_bchw_as_png(x, output_filename):
+ os.makedirs(os.path.dirname(os.path.abspath(os.path.realpath(output_filename))), exist_ok=True)
+ x = torch.clamp(x.float(), -1., 1.) * 127.5 + 127.5
+ x = x.detach().cpu().to(torch.uint8)
+ x = einops.rearrange(x, 'b c h w -> c h (b w)')
+ torchvision.io.write_png(x, output_filename)
+ return output_filename
+
+
+def add_tensors_with_padding(tensor1, tensor2):
+ if tensor1.shape == tensor2.shape:
+ return tensor1 + tensor2
+
+ shape1 = tensor1.shape
+ shape2 = tensor2.shape
+
+ new_shape = tuple(max(s1, s2) for s1, s2 in zip(shape1, shape2))
+
+ padded_tensor1 = torch.zeros(new_shape)
+ padded_tensor2 = torch.zeros(new_shape)
+
+ padded_tensor1[tuple(slice(0, s) for s in shape1)] = tensor1
+ padded_tensor2[tuple(slice(0, s) for s in shape2)] = tensor2
+
+ result = padded_tensor1 + padded_tensor2
+ return result
+
+
+def print_free_mem():
+ torch.cuda.empty_cache()
+ free_mem, total_mem = torch.cuda.mem_get_info(0)
+ free_mem_mb = free_mem / (1024 ** 2)
+ total_mem_mb = total_mem / (1024 ** 2)
+ print(f"Free memory: {free_mem_mb:.2f} MB")
+ print(f"Total memory: {total_mem_mb:.2f} MB")
+ return
+
+
+def print_gpu_parameters(device, state_dict, log_count=1):
+ summary = {"device": device, "keys_count": len(state_dict)}
+
+ logged_params = {}
+ for i, (key, tensor) in enumerate(state_dict.items()):
+ if i >= log_count:
+ break
+ logged_params[key] = tensor.flatten()[:3].tolist()
+
+ summary["params"] = logged_params
+
+ print(str(summary))
+ return
+
+
+def visualize_txt_as_img(width, height, text, font_path='font/DejaVuSans.ttf', size=18):
+ from PIL import Image, ImageDraw, ImageFont
+
+ txt = Image.new("RGB", (width, height), color="white")
+ draw = ImageDraw.Draw(txt)
+ font = ImageFont.truetype(font_path, size=size)
+
+ if text == '':
+ return np.array(txt)
+
+ # Split text into lines that fit within the image width
+ lines = []
+ words = text.split()
+ current_line = words[0]
+
+ for word in words[1:]:
+ line_with_word = f"{current_line} {word}"
+ if draw.textbbox((0, 0), line_with_word, font=font)[2] <= width:
+ current_line = line_with_word
+ else:
+ lines.append(current_line)
+ current_line = word
+
+ lines.append(current_line)
+
+ # Draw the text line by line
+ y = 0
+ line_height = draw.textbbox((0, 0), "A", font=font)[3]
+
+ for line in lines:
+ if y + line_height > height:
+ break # stop drawing if the next line will be outside the image
+ draw.text((0, y), line, fill="black", font=font)
+ y += line_height
+
+ return np.array(txt)
+
+
+def blue_mark(x):
+ x = x.copy()
+ c = x[:, :, 2]
+ b = cv2.blur(c, (9, 9))
+ x[:, :, 2] = ((c - b) * 16.0 + b).clip(-1, 1)
+ return x
+
+
+def green_mark(x):
+ x = x.copy()
+ x[:, :, 2] = -1
+ x[:, :, 0] = -1
+ return x
+
+
+def frame_mark(x):
+ x = x.copy()
+ x[:64] = -1
+ x[-64:] = -1
+ x[:, :8] = 1
+ x[:, -8:] = 1
+ return x
+
+
+@torch.inference_mode()
+def pytorch2numpy(imgs):
+ results = []
+ for x in imgs:
+ y = x.movedim(0, -1)
+ y = y * 127.5 + 127.5
+ y = y.detach().float().cpu().numpy().clip(0, 255).astype(np.uint8)
+ results.append(y)
+ return results
+
+
+@torch.inference_mode()
+def numpy2pytorch(imgs):
+ h = torch.from_numpy(np.stack(imgs, axis=0)).float() / 127.5 - 1.0
+ h = h.movedim(-1, 1)
+ return h
+
+
+@torch.no_grad()
+def duplicate_prefix_to_suffix(x, count, zero_out=False):
+ if zero_out:
+ return torch.cat([x, torch.zeros_like(x[:count])], dim=0)
+ else:
+ return torch.cat([x, x[:count]], dim=0)
+
+
+def weighted_mse(a, b, weight):
+ return torch.mean(weight.float() * (a.float() - b.float()) ** 2)
+
+
+def clamped_linear_interpolation(x, x_min, y_min, x_max, y_max, sigma=1.0):
+ x = (x - x_min) / (x_max - x_min)
+ x = max(0.0, min(x, 1.0))
+ x = x ** sigma
+ return y_min + x * (y_max - y_min)
+
+
+def expand_to_dims(x, target_dims):
+ return x.view(*x.shape, *([1] * max(0, target_dims - x.dim())))
+
+
+def repeat_to_batch_size(tensor: torch.Tensor, batch_size: int):
+ if tensor is None:
+ return None
+
+ first_dim = tensor.shape[0]
+
+ if first_dim == batch_size:
+ return tensor
+
+ if batch_size % first_dim != 0:
+ raise ValueError(f"Cannot evenly repeat first dim {first_dim} to match batch_size {batch_size}.")
+
+ repeat_times = batch_size // first_dim
+
+ return tensor.repeat(repeat_times, *[1] * (tensor.dim() - 1))
+
+
+def dim5(x):
+ return expand_to_dims(x, 5)
+
+
+def dim4(x):
+ return expand_to_dims(x, 4)
+
+
+def dim3(x):
+ return expand_to_dims(x, 3)
+
+
+def crop_or_pad_yield_mask(x, length):
+ B, F, C = x.shape
+ device = x.device
+ dtype = x.dtype
+
+ if F < length:
+ y = torch.zeros((B, length, C), dtype=dtype, device=device)
+ mask = torch.zeros((B, length), dtype=torch.bool, device=device)
+ y[:, :F, :] = x
+ mask[:, :F] = True
+ return y, mask
+
+ return x[:, :length, :], torch.ones((B, length), dtype=torch.bool, device=device)
+
+
+def extend_dim(x, dim, minimal_length, zero_pad=False):
+ original_length = int(x.shape[dim])
+
+ if original_length >= minimal_length:
+ return x
+
+ if zero_pad:
+ padding_shape = list(x.shape)
+ padding_shape[dim] = minimal_length - original_length
+ padding = torch.zeros(padding_shape, dtype=x.dtype, device=x.device)
+ else:
+ idx = (slice(None),) * dim + (slice(-1, None),) + (slice(None),) * (len(x.shape) - dim - 1)
+ last_element = x[idx]
+ padding = last_element.repeat_interleave(minimal_length - original_length, dim=dim)
+
+ return torch.cat([x, padding], dim=dim)
+
+
+def lazy_positional_encoding(t, repeats=None):
+ if not isinstance(t, list):
+ t = [t]
+
+ from diffusers.models.embeddings import get_timestep_embedding
+
+ te = torch.tensor(t)
+ te = get_timestep_embedding(timesteps=te, embedding_dim=256, flip_sin_to_cos=True, downscale_freq_shift=0.0, scale=1.0)
+
+ if repeats is None:
+ return te
+
+ te = te[:, None, :].expand(-1, repeats, -1)
+
+ return te
+
+
+def state_dict_offset_merge(A, B, C=None):
+ result = {}
+ keys = A.keys()
+
+ for key in keys:
+ A_value = A[key]
+ B_value = B[key].to(A_value)
+
+ if C is None:
+ result[key] = A_value + B_value
+ else:
+ C_value = C[key].to(A_value)
+ result[key] = A_value + B_value - C_value
+
+ return result
+
+
+def state_dict_weighted_merge(state_dicts, weights):
+ if len(state_dicts) != len(weights):
+ raise ValueError("Number of state dictionaries must match number of weights")
+
+ if not state_dicts:
+ return {}
+
+ total_weight = sum(weights)
+
+ if total_weight == 0:
+ raise ValueError("Sum of weights cannot be zero")
+
+ normalized_weights = [w / total_weight for w in weights]
+
+ keys = state_dicts[0].keys()
+ result = {}
+
+ for key in keys:
+ result[key] = state_dicts[0][key] * normalized_weights[0]
+
+ for i in range(1, len(state_dicts)):
+ state_dict_value = state_dicts[i][key].to(result[key])
+ result[key] += state_dict_value * normalized_weights[i]
+
+ return result
+
+
+def group_files_by_folder(all_files):
+ grouped_files = {}
+
+ for file in all_files:
+ folder_name = os.path.basename(os.path.dirname(file))
+ if folder_name not in grouped_files:
+ grouped_files[folder_name] = []
+ grouped_files[folder_name].append(file)
+
+ list_of_lists = list(grouped_files.values())
+ return list_of_lists
+
+
+def generate_timestamp():
+ now = datetime.datetime.now()
+ timestamp = now.strftime('%y%m%d_%H%M%S')
+ milliseconds = f"{int(now.microsecond / 1000):03d}"
+ random_number = random.randint(0, 9999)
+ return f"{timestamp}_{milliseconds}_{random_number}"
+
+
+def write_PIL_image_with_png_info(image, metadata, path):
+ from PIL.PngImagePlugin import PngInfo
+
+ png_info = PngInfo()
+ for key, value in metadata.items():
+ png_info.add_text(key, value)
+
+ image.save(path, "PNG", pnginfo=png_info)
+ return image
+
+
+def torch_safe_save(content, path):
+ torch.save(content, path + '_tmp')
+ os.replace(path + '_tmp', path)
+ return path
+
+
+def move_optimizer_to_device(optimizer, device):
+ for state in optimizer.state.values():
+ for k, v in state.items():
+ if isinstance(v, torch.Tensor):
+ state[k] = v.to(device)
diff --git a/DecMem/demo_utils/vae.py b/DecMem/demo_utils/vae.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c80925194127bc6f96759b1893a08c3d440e0bc
--- /dev/null
+++ b/DecMem/demo_utils/vae.py
@@ -0,0 +1,390 @@
+from typing import List
+from einops import rearrange
+import tensorrt as trt
+import torch
+import torch.nn as nn
+
+from demo_utils.constant import ALL_INPUTS_NAMES, ZERO_VAE_CACHE
+from wan.modules.vae import AttentionBlock, CausalConv3d, RMS_norm, Upsample
+
+CACHE_T = 2
+
+
+class ResidualBlock(nn.Module):
+
+ def __init__(self, in_dim, out_dim, dropout=0.0):
+ super().__init__()
+ self.in_dim = in_dim
+ self.out_dim = out_dim
+
+ # layers
+ self.residual = nn.Sequential(
+ RMS_norm(in_dim, images=False), nn.SiLU(),
+ CausalConv3d(in_dim, out_dim, 3, padding=1),
+ RMS_norm(out_dim, images=False), nn.SiLU(), nn.Dropout(dropout),
+ CausalConv3d(out_dim, out_dim, 3, padding=1))
+ self.shortcut = CausalConv3d(in_dim, out_dim, 1) \
+ if in_dim != out_dim else nn.Identity()
+
+ def forward(self, x, feat_cache_1, feat_cache_2):
+ h = self.shortcut(x)
+ feat_cache = feat_cache_1
+ out_feat_cache = []
+ for layer in self.residual:
+ if isinstance(layer, CausalConv3d):
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
+ if cache_x.shape[2] < 2 and feat_cache is not None:
+ # cache last frame of last two chunk
+ cache_x = torch.cat([
+ feat_cache[:, :, -1, :, :].unsqueeze(2).to(
+ cache_x.device), cache_x
+ ],
+ dim=2)
+ x = layer(x, feat_cache)
+ out_feat_cache.append(cache_x)
+ feat_cache = feat_cache_2
+ else:
+ x = layer(x)
+ return x + h, *out_feat_cache
+
+
+class Resample(nn.Module):
+
+ def __init__(self, dim, mode):
+ assert mode in ('none', 'upsample2d', 'upsample3d')
+ super().__init__()
+ self.dim = dim
+ self.mode = mode
+
+ # layers
+ if mode == 'upsample2d':
+ self.resample = nn.Sequential(
+ Upsample(scale_factor=(2., 2.), mode='nearest'),
+ nn.Conv2d(dim, dim // 2, 3, padding=1))
+ elif mode == 'upsample3d':
+ self.resample = nn.Sequential(
+ Upsample(scale_factor=(2., 2.), mode='nearest'),
+ nn.Conv2d(dim, dim // 2, 3, padding=1))
+ self.time_conv = CausalConv3d(
+ dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
+ else:
+ self.resample = nn.Identity()
+
+ def forward(self, x, is_first_frame, feat_cache):
+ if self.mode == 'upsample3d':
+ b, c, t, h, w = x.size()
+ # x, out_feat_cache = torch.cond(
+ # is_first_frame,
+ # lambda: (torch.cat([torch.zeros_like(x), x], dim=2), feat_cache.clone()),
+ # lambda: self.temporal_conv(x, feat_cache),
+ # )
+ # x, out_feat_cache = torch.cond(
+ # is_first_frame,
+ # lambda: (torch.cat([torch.zeros_like(x), x], dim=2), feat_cache.clone()),
+ # lambda: self.temporal_conv(x, feat_cache),
+ # )
+ x, out_feat_cache = self.temporal_conv(x, is_first_frame, feat_cache)
+ out_feat_cache = torch.cond(
+ is_first_frame,
+ lambda: feat_cache.clone().contiguous(),
+ lambda: out_feat_cache.clone().contiguous(),
+ )
+ # if is_first_frame:
+ # x = torch.cat([torch.zeros_like(x), x], dim=2)
+ # out_feat_cache = feat_cache.clone()
+ # else:
+ # x, out_feat_cache = self.temporal_conv(x, feat_cache)
+ else:
+ out_feat_cache = None
+ t = x.shape[2]
+ x = rearrange(x, 'b c t h w -> (b t) c h w')
+ x = self.resample(x)
+ x = rearrange(x, '(b t) c h w -> b c t h w', t=t)
+ return x, out_feat_cache
+
+ def temporal_conv(self, x, is_first_frame, feat_cache):
+ b, c, t, h, w = x.size()
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
+ if cache_x.shape[2] < 2 and feat_cache is not None:
+ cache_x = torch.cat([
+ torch.zeros_like(cache_x),
+ cache_x
+ ], dim=2)
+ x = torch.cond(
+ is_first_frame,
+ lambda: torch.cat([torch.zeros_like(x), x], dim=1).contiguous(),
+ lambda: self.time_conv(x, feat_cache).contiguous(),
+ )
+ # x = self.time_conv(x, feat_cache)
+ out_feat_cache = cache_x
+
+ x = x.reshape(b, 2, c, t, h, w)
+ x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]),
+ 3)
+ x = x.reshape(b, c, t * 2, h, w)
+ return x.contiguous(), out_feat_cache.contiguous()
+
+ def init_weight(self, conv):
+ conv_weight = conv.weight
+ nn.init.zeros_(conv_weight)
+ c1, c2, t, h, w = conv_weight.size()
+ one_matrix = torch.eye(c1, c2)
+ init_matrix = one_matrix
+ nn.init.zeros_(conv_weight)
+ # conv_weight.data[:,:,-1,1,1] = init_matrix * 0.5
+ conv_weight.data[:, :, 1, 0, 0] = init_matrix # * 0.5
+ conv.weight.data.copy_(conv_weight)
+ nn.init.zeros_(conv.bias.data)
+
+ def init_weight2(self, conv):
+ conv_weight = conv.weight.data
+ nn.init.zeros_(conv_weight)
+ c1, c2, t, h, w = conv_weight.size()
+ init_matrix = torch.eye(c1 // 2, c2)
+ # init_matrix = repeat(init_matrix, 'o ... -> (o 2) ...').permute(1,0,2).contiguous().reshape(c1,c2)
+ conv_weight[:c1 // 2, :, -1, 0, 0] = init_matrix
+ conv_weight[c1 // 2:, :, -1, 0, 0] = init_matrix
+ conv.weight.data.copy_(conv_weight)
+ nn.init.zeros_(conv.bias.data)
+
+
+class VAEDecoderWrapperSingle(nn.Module):
+ def __init__(self):
+ super().__init__()
+ self.decoder = VAEDecoder3d()
+ mean = [
+ -0.7571, -0.7089, -0.9113, 0.1075, -0.1745, 0.9653, -0.1517, 1.5508,
+ 0.4134, -0.0715, 0.5517, -0.3632, -0.1922, -0.9497, 0.2503, -0.2921
+ ]
+ std = [
+ 2.8184, 1.4541, 2.3275, 2.6558, 1.2196, 1.7708, 2.6052, 2.0743,
+ 3.2687, 2.1526, 2.8652, 1.5579, 1.6382, 1.1253, 2.8251, 1.9160
+ ]
+ self.mean = torch.tensor(mean, dtype=torch.float32)
+ self.std = torch.tensor(std, dtype=torch.float32)
+ self.z_dim = 16
+ self.conv2 = CausalConv3d(self.z_dim, self.z_dim, 1)
+
+ def forward(
+ self,
+ z: torch.Tensor,
+ is_first_frame: torch.Tensor,
+ *feat_cache: List[torch.Tensor]
+ ):
+ # from [batch_size, num_frames, num_channels, height, width]
+ # to [batch_size, num_channels, num_frames, height, width]
+ z = z.permute(0, 2, 1, 3, 4)
+ assert z.shape[2] == 1
+ feat_cache = list(feat_cache)
+ is_first_frame = is_first_frame.bool()
+
+ device, dtype = z.device, z.dtype
+ scale = [self.mean.to(device=device, dtype=dtype),
+ 1.0 / self.std.to(device=device, dtype=dtype)]
+
+ if isinstance(scale[0], torch.Tensor):
+ z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
+ 1, self.z_dim, 1, 1, 1)
+ else:
+ z = z / scale[1] + scale[0]
+ x = self.conv2(z)
+ out, feat_cache = self.decoder(x, is_first_frame, feat_cache=feat_cache)
+ out = out.clamp_(-1, 1)
+ # from [batch_size, num_channels, num_frames, height, width]
+ # to [batch_size, num_frames, num_channels, height, width]
+ out = out.permute(0, 2, 1, 3, 4)
+ return out, feat_cache
+
+
+class VAEDecoder3d(nn.Module):
+ def __init__(self,
+ dim=96,
+ z_dim=16,
+ dim_mult=[1, 2, 4, 4],
+ num_res_blocks=2,
+ attn_scales=[],
+ temperal_upsample=[True, True, False],
+ dropout=0.0):
+ super().__init__()
+ self.dim = dim
+ self.z_dim = z_dim
+ self.dim_mult = dim_mult
+ self.num_res_blocks = num_res_blocks
+ self.attn_scales = attn_scales
+ self.temperal_upsample = temperal_upsample
+ self.cache_t = 2
+ self.decoder_conv_num = 32
+
+ # dimensions
+ dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
+ scale = 1.0 / 2**(len(dim_mult) - 2)
+
+ # init block
+ self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
+
+ # middle blocks
+ self.middle = nn.Sequential(
+ ResidualBlock(dims[0], dims[0], dropout), AttentionBlock(dims[0]),
+ ResidualBlock(dims[0], dims[0], dropout))
+
+ # upsample blocks
+ upsamples = []
+ for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
+ # residual (+attention) blocks
+ if i == 1 or i == 2 or i == 3:
+ in_dim = in_dim // 2
+ for _ in range(num_res_blocks + 1):
+ upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
+ if scale in attn_scales:
+ upsamples.append(AttentionBlock(out_dim))
+ in_dim = out_dim
+
+ # upsample block
+ if i != len(dim_mult) - 1:
+ mode = 'upsample3d' if temperal_upsample[i] else 'upsample2d'
+ upsamples.append(Resample(out_dim, mode=mode))
+ scale *= 2.0
+ self.upsamples = nn.Sequential(*upsamples)
+
+ # output blocks
+ self.head = nn.Sequential(
+ RMS_norm(out_dim, images=False), nn.SiLU(),
+ CausalConv3d(out_dim, 3, 3, padding=1))
+
+ def forward(
+ self,
+ x: torch.Tensor,
+ is_first_frame: torch.Tensor,
+ feat_cache: List[torch.Tensor]
+ ):
+ idx = 0
+ out_feat_cache = []
+
+ # conv1
+ cache_x = x[:, :, -self.cache_t:, :, :].clone()
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
+ # cache last frame of last two chunk
+ cache_x = torch.cat([
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
+ cache_x.device), cache_x
+ ],
+ dim=2)
+ x = self.conv1(x, feat_cache[idx])
+ out_feat_cache.append(cache_x)
+ idx += 1
+
+ # middle
+ for layer in self.middle:
+ if isinstance(layer, ResidualBlock) and feat_cache is not None:
+ x, out_feat_cache_1, out_feat_cache_2 = layer(x, feat_cache[idx], feat_cache[idx + 1])
+ idx += 2
+ out_feat_cache.append(out_feat_cache_1)
+ out_feat_cache.append(out_feat_cache_2)
+ else:
+ x = layer(x)
+
+ # upsamples
+ for layer in self.upsamples:
+ if isinstance(layer, Resample):
+ x, cache_x = layer(x, is_first_frame, feat_cache[idx])
+ if cache_x is not None:
+ out_feat_cache.append(cache_x)
+ idx += 1
+ else:
+ x, out_feat_cache_1, out_feat_cache_2 = layer(x, feat_cache[idx], feat_cache[idx + 1])
+ idx += 2
+ out_feat_cache.append(out_feat_cache_1)
+ out_feat_cache.append(out_feat_cache_2)
+
+ # head
+ for layer in self.head:
+ if isinstance(layer, CausalConv3d) and feat_cache is not None:
+ cache_x = x[:, :, -self.cache_t:, :, :].clone()
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
+ # cache last frame of last two chunk
+ cache_x = torch.cat([
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
+ cache_x.device), cache_x
+ ],
+ dim=2)
+ x = layer(x, feat_cache[idx])
+ out_feat_cache.append(cache_x)
+ idx += 1
+ else:
+ x = layer(x)
+ return x, out_feat_cache
+
+
+class VAETRTWrapper():
+ def __init__(self):
+ TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
+ with open("checkpoints/vae_decoder_int8.trt", "rb") as f, trt.Runtime(TRT_LOGGER) as rt:
+ self.engine: trt.ICudaEngine = rt.deserialize_cuda_engine(f.read())
+
+ self.context: trt.IExecutionContext = self.engine.create_execution_context()
+ self.stream = torch.cuda.current_stream().cuda_stream
+
+ # ──────────────────────────────
+ # 2️⃣ Feed the engine with tensors
+ # (name-based API in TRT ≥10)
+ # ──────────────────────────────
+ self.dtype_map = {
+ trt.float32: torch.float32,
+ trt.float16: torch.float16,
+ trt.int8: torch.int8,
+ trt.int32: torch.int32,
+ }
+ test_input = torch.zeros(1, 16, 1, 60, 104).cuda().half()
+ is_first_frame = torch.tensor(1.0).cuda().half()
+ test_cache_inputs = [c.cuda().half() for c in ZERO_VAE_CACHE]
+ test_inputs = [test_input, is_first_frame] + test_cache_inputs
+
+ # keep references so buffers stay alive
+ self.device_buffers, self.outputs = {}, []
+
+ # ---- inputs ----
+ for i, name in enumerate(ALL_INPUTS_NAMES):
+ tensor, scale = test_inputs[i], 1 / 127
+ tensor = self.quantize_if_needed(tensor, self.engine.get_tensor_dtype(name), scale)
+
+ # dynamic shapes
+ if -1 in self.engine.get_tensor_shape(name):
+ # new API :contentReference[oaicite:0]{index=0}
+ self.context.set_input_shape(name, tuple(tensor.shape))
+
+ # replaces bindings[] :contentReference[oaicite:1]{index=1}
+ self.context.set_tensor_address(name, int(tensor.data_ptr()))
+ self.device_buffers[name] = tensor # keep pointer alive
+
+ # ---- (after all input shapes are known) infer output shapes ----
+ # propagates shapes :contentReference[oaicite:2]{index=2}
+ self.context.infer_shapes()
+
+ for i in range(self.engine.num_io_tensors):
+ name = self.engine.get_tensor_name(i)
+ # replaces binding_is_input :contentReference[oaicite:3]{index=3}
+ if self.engine.get_tensor_mode(name) == trt.TensorIOMode.OUTPUT:
+ shape = tuple(self.context.get_tensor_shape(name))
+ dtype = self.dtype_map[self.engine.get_tensor_dtype(name)]
+ out = torch.empty(shape, dtype=dtype, device="cuda").contiguous()
+
+ self.context.set_tensor_address(name, int(out.data_ptr()))
+ self.outputs.append(out)
+ self.device_buffers[name] = out
+
+ # helper to quant-convert on the fly
+ def quantize_if_needed(self, t, expected_dtype, scale):
+ if expected_dtype == trt.int8 and t.dtype != torch.int8:
+ t = torch.clamp((t / scale).round(), -128, 127).to(torch.int8).contiguous()
+ return t # keep pointer alive
+
+ def forward(self, *test_inputs):
+ for i, name in enumerate(ALL_INPUTS_NAMES):
+ tensor, scale = test_inputs[i], 1 / 127
+ tensor = self.quantize_if_needed(tensor, self.engine.get_tensor_dtype(name), scale)
+ self.context.set_tensor_address(name, int(tensor.data_ptr()))
+ self.device_buffers[name] = tensor
+
+ self.context.execute_async_v3(stream_handle=self.stream)
+ torch.cuda.current_stream().synchronize()
+ return self.outputs
diff --git a/DecMem/demo_utils/vae_block3.py b/DecMem/demo_utils/vae_block3.py
new file mode 100644
index 0000000000000000000000000000000000000000..a5b80a025f7b4b81356b1be93cc1260e10a841fc
--- /dev/null
+++ b/DecMem/demo_utils/vae_block3.py
@@ -0,0 +1,291 @@
+from typing import List
+from einops import rearrange
+import torch
+import torch.nn as nn
+
+from wan.modules.vae import AttentionBlock, CausalConv3d, RMS_norm, ResidualBlock, Upsample
+
+
+class Resample(nn.Module):
+
+ def __init__(self, dim, mode):
+ assert mode in ('none', 'upsample2d', 'upsample3d', 'downsample2d',
+ 'downsample3d')
+ super().__init__()
+ self.dim = dim
+ self.mode = mode
+ self.cache_t = 2
+
+ # layers
+ if mode == 'upsample2d':
+ self.resample = nn.Sequential(
+ Upsample(scale_factor=(2., 2.), mode='nearest'),
+ nn.Conv2d(dim, dim // 2, 3, padding=1))
+ elif mode == 'upsample3d':
+ self.resample = nn.Sequential(
+ Upsample(scale_factor=(2., 2.), mode='nearest'),
+ nn.Conv2d(dim, dim // 2, 3, padding=1))
+ self.time_conv = CausalConv3d(
+ dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
+
+ elif mode == 'downsample2d':
+ self.resample = nn.Sequential(
+ nn.ZeroPad2d((0, 1, 0, 1)),
+ nn.Conv2d(dim, dim, 3, stride=(2, 2)))
+ elif mode == 'downsample3d':
+ self.resample = nn.Sequential(
+ nn.ZeroPad2d((0, 1, 0, 1)),
+ nn.Conv2d(dim, dim, 3, stride=(2, 2)))
+ self.time_conv = CausalConv3d(
+ dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0))
+
+ else:
+ self.resample = nn.Identity()
+
+ def forward(self, x, feat_cache=None, feat_idx=[0]):
+ b, c, t, h, w = x.size()
+ if self.mode == 'upsample3d':
+ if feat_cache is not None:
+ idx = feat_idx[0]
+ if feat_cache[idx] is None:
+ feat_cache[idx] = 'Rep'
+ feat_idx[0] += 1
+ else:
+
+ cache_x = x[:, :, -self.cache_t:, :, :].clone()
+ if cache_x.shape[2] < 2 and feat_cache[
+ idx] is not None and feat_cache[idx] != 'Rep':
+ # cache last frame of last two chunk
+ cache_x = torch.cat([
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
+ cache_x.device), cache_x
+ ],
+ dim=2)
+ if cache_x.shape[2] < 2 and feat_cache[
+ idx] is not None and feat_cache[idx] == 'Rep':
+ cache_x = torch.cat([
+ torch.zeros_like(cache_x).to(cache_x.device),
+ cache_x
+ ],
+ dim=2)
+ if feat_cache[idx] == 'Rep':
+ x = self.time_conv(x)
+ else:
+ x = self.time_conv(x, feat_cache[idx])
+ feat_cache[idx] = cache_x
+ feat_idx[0] += 1
+
+ x = x.reshape(b, 2, c, t, h, w)
+ x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]),
+ 3)
+ x = x.reshape(b, c, t * 2, h, w)
+ t = x.shape[2]
+ x = rearrange(x, 'b c t h w -> (b t) c h w')
+ x = self.resample(x)
+ x = rearrange(x, '(b t) c h w -> b c t h w', t=t)
+
+ if self.mode == 'downsample3d':
+ if feat_cache is not None:
+ idx = feat_idx[0]
+ if feat_cache[idx] is None:
+ feat_cache[idx] = x.clone()
+ feat_idx[0] += 1
+ else:
+
+ cache_x = x[:, :, -1:, :, :].clone()
+ # if cache_x.shape[2] < 2 and feat_cache[idx] is not None and feat_cache[idx]!='Rep':
+ # # cache last frame of last two chunk
+ # cache_x = torch.cat([feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device), cache_x], dim=2)
+
+ x = self.time_conv(
+ torch.cat([feat_cache[idx][:, :, -1:, :, :], x], 2))
+ feat_cache[idx] = cache_x
+ feat_idx[0] += 1
+ return x
+
+ def init_weight(self, conv):
+ conv_weight = conv.weight
+ nn.init.zeros_(conv_weight)
+ c1, c2, t, h, w = conv_weight.size()
+ one_matrix = torch.eye(c1, c2)
+ init_matrix = one_matrix
+ nn.init.zeros_(conv_weight)
+ # conv_weight.data[:,:,-1,1,1] = init_matrix * 0.5
+ conv_weight.data[:, :, 1, 0, 0] = init_matrix # * 0.5
+ conv.weight.data.copy_(conv_weight)
+ nn.init.zeros_(conv.bias.data)
+
+ def init_weight2(self, conv):
+ conv_weight = conv.weight.data
+ nn.init.zeros_(conv_weight)
+ c1, c2, t, h, w = conv_weight.size()
+ init_matrix = torch.eye(c1 // 2, c2)
+ # init_matrix = repeat(init_matrix, 'o ... -> (o 2) ...').permute(1,0,2).contiguous().reshape(c1,c2)
+ conv_weight[:c1 // 2, :, -1, 0, 0] = init_matrix
+ conv_weight[c1 // 2:, :, -1, 0, 0] = init_matrix
+ conv.weight.data.copy_(conv_weight)
+ nn.init.zeros_(conv.bias.data)
+
+
+class VAEDecoderWrapper(nn.Module):
+ def __init__(self):
+ super().__init__()
+ self.decoder = VAEDecoder3d()
+ mean = [
+ -0.7571, -0.7089, -0.9113, 0.1075, -0.1745, 0.9653, -0.1517, 1.5508,
+ 0.4134, -0.0715, 0.5517, -0.3632, -0.1922, -0.9497, 0.2503, -0.2921
+ ]
+ std = [
+ 2.8184, 1.4541, 2.3275, 2.6558, 1.2196, 1.7708, 2.6052, 2.0743,
+ 3.2687, 2.1526, 2.8652, 1.5579, 1.6382, 1.1253, 2.8251, 1.9160
+ ]
+ self.mean = torch.tensor(mean, dtype=torch.float32)
+ self.std = torch.tensor(std, dtype=torch.float32)
+ self.z_dim = 16
+ self.conv2 = CausalConv3d(self.z_dim, self.z_dim, 1)
+
+ def forward(
+ self,
+ z: torch.Tensor,
+ *feat_cache: List[torch.Tensor]
+ ):
+ # from [batch_size, num_frames, num_channels, height, width]
+ # to [batch_size, num_channels, num_frames, height, width]
+ z = z.permute(0, 2, 1, 3, 4)
+ feat_cache = list(feat_cache)
+ print("Length of feat_cache: ", len(feat_cache))
+
+ device, dtype = z.device, z.dtype
+ scale = [self.mean.to(device=device, dtype=dtype),
+ 1.0 / self.std.to(device=device, dtype=dtype)]
+
+ if isinstance(scale[0], torch.Tensor):
+ z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
+ 1, self.z_dim, 1, 1, 1)
+ else:
+ z = z / scale[1] + scale[0]
+ iter_ = z.shape[2]
+ x = self.conv2(z)
+ for i in range(iter_):
+ if i == 0:
+ out, feat_cache = self.decoder(
+ x[:, :, i:i + 1, :, :],
+ feat_cache=feat_cache)
+ else:
+ out_, feat_cache = self.decoder(
+ x[:, :, i:i + 1, :, :],
+ feat_cache=feat_cache)
+ out = torch.cat([out, out_], 2)
+
+ out = out.float().clamp_(-1, 1)
+ # from [batch_size, num_channels, num_frames, height, width]
+ # to [batch_size, num_frames, num_channels, height, width]
+ out = out.permute(0, 2, 1, 3, 4)
+ return out, feat_cache
+
+
+class VAEDecoder3d(nn.Module):
+ def __init__(self,
+ dim=96,
+ z_dim=16,
+ dim_mult=[1, 2, 4, 4],
+ num_res_blocks=2,
+ attn_scales=[],
+ temperal_upsample=[True, True, False],
+ dropout=0.0):
+ super().__init__()
+ self.dim = dim
+ self.z_dim = z_dim
+ self.dim_mult = dim_mult
+ self.num_res_blocks = num_res_blocks
+ self.attn_scales = attn_scales
+ self.temperal_upsample = temperal_upsample
+ self.cache_t = 2
+ self.decoder_conv_num = 32
+
+ # dimensions
+ dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
+ scale = 1.0 / 2**(len(dim_mult) - 2)
+
+ # init block
+ self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
+
+ # middle blocks
+ self.middle = nn.Sequential(
+ ResidualBlock(dims[0], dims[0], dropout), AttentionBlock(dims[0]),
+ ResidualBlock(dims[0], dims[0], dropout))
+
+ # upsample blocks
+ upsamples = []
+ for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
+ # residual (+attention) blocks
+ if i == 1 or i == 2 or i == 3:
+ in_dim = in_dim // 2
+ for _ in range(num_res_blocks + 1):
+ upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
+ if scale in attn_scales:
+ upsamples.append(AttentionBlock(out_dim))
+ in_dim = out_dim
+
+ # upsample block
+ if i != len(dim_mult) - 1:
+ mode = 'upsample3d' if temperal_upsample[i] else 'upsample2d'
+ upsamples.append(Resample(out_dim, mode=mode))
+ scale *= 2.0
+ self.upsamples = nn.Sequential(*upsamples)
+
+ # output blocks
+ self.head = nn.Sequential(
+ RMS_norm(out_dim, images=False), nn.SiLU(),
+ CausalConv3d(out_dim, 3, 3, padding=1))
+
+ def forward(
+ self,
+ x: torch.Tensor,
+ feat_cache: List[torch.Tensor]
+ ):
+ feat_idx = [0]
+
+ # conv1
+ idx = feat_idx[0]
+ cache_x = x[:, :, -self.cache_t:, :, :].clone()
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
+ # cache last frame of last two chunk
+ cache_x = torch.cat([
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
+ cache_x.device), cache_x
+ ],
+ dim=2)
+ x = self.conv1(x, feat_cache[idx])
+ feat_cache[idx] = cache_x
+ feat_idx[0] += 1
+
+ # middle
+ for layer in self.middle:
+ if isinstance(layer, ResidualBlock) and feat_cache is not None:
+ x = layer(x, feat_cache, feat_idx)
+ else:
+ x = layer(x)
+
+ # upsamples
+ for layer in self.upsamples:
+ x = layer(x, feat_cache, feat_idx)
+
+ # head
+ for layer in self.head:
+ if isinstance(layer, CausalConv3d) and feat_cache is not None:
+ idx = feat_idx[0]
+ cache_x = x[:, :, -self.cache_t:, :, :].clone()
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
+ # cache last frame of last two chunk
+ cache_x = torch.cat([
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
+ cache_x.device), cache_x
+ ],
+ dim=2)
+ x = layer(x, feat_cache[idx])
+ feat_cache[idx] = cache_x
+ feat_idx[0] += 1
+ else:
+ x = layer(x)
+ return x, feat_cache
diff --git a/DecMem/demo_utils/vae_torch2trt.py b/DecMem/demo_utils/vae_torch2trt.py
new file mode 100644
index 0000000000000000000000000000000000000000..c0e08f258a199cbfb6eaf0b3a0782f29f2d8faf3
--- /dev/null
+++ b/DecMem/demo_utils/vae_torch2trt.py
@@ -0,0 +1,308 @@
+# ---- INT8 (optional) ----
+from demo_utils.vae import (
+ VAEDecoderWrapperSingle, # main nn.Module
+ ZERO_VAE_CACHE # helper constants shipped with your code base
+)
+import pycuda.driver as cuda # ← add
+import pycuda.autoinit # noqa
+
+import sys
+from pathlib import Path
+
+import torch
+import tensorrt as trt
+
+from utils.dataset import ShardingLMDBDataset
+
+data_path = "/mnt/localssd/wanx_14B_shift-3.0_cfg-5.0_lmdb_oneshard"
+dataset = ShardingLMDBDataset(data_path, max_pair=int(1e8))
+dataloader = torch.utils.data.DataLoader(
+ dataset,
+ batch_size=1,
+ num_workers=0
+)
+
+# ─────────────────────────────────────────────────────────
+# 1️⃣ Bring the PyTorch model into scope
+# (all code you pasted lives in `vae_decoder.py`)
+# ─────────────────────────────────────────────────────────
+
+# --- dummy tensors (exact shapes you posted) ---
+dummy_input = torch.randn(1, 1, 16, 60, 104).half().cuda()
+is_first_frame = torch.tensor([1.0], device="cuda", dtype=torch.float16)
+dummy_cache_input = [
+ torch.randn(*s.shape).half().cuda() if isinstance(s, torch.Tensor) else s
+ for s in ZERO_VAE_CACHE # keep exactly the same ordering
+]
+inputs = [dummy_input, is_first_frame, *dummy_cache_input]
+
+# ─────────────────────────────────────────────────────────
+# 2️⃣ Export → ONNX
+# ─────────────────────────────────────────────────────────
+model = VAEDecoderWrapperSingle().half().cuda().eval()
+
+vae_state_dict = torch.load('wan_models/Wan2.1-T2V-1.3B/Wan2.1_VAE.pth', map_location="cpu")
+decoder_state_dict = {}
+for key, value in vae_state_dict.items():
+ if 'decoder.' in key or 'conv2' in key:
+ decoder_state_dict[key] = value
+model.load_state_dict(decoder_state_dict)
+model = model.half().cuda().eval() # only batch dim dynamic
+
+onnx_path = Path("vae_decoder.onnx")
+feat_names = [f"vae_cache_{i}" for i in range(len(dummy_cache_input))]
+all_inputs_names = ["z", "use_cache"] + feat_names
+
+with torch.inference_mode():
+ torch.onnx.export(
+ model,
+ tuple(inputs), # must be a tuple
+ onnx_path.as_posix(),
+ input_names=all_inputs_names,
+ output_names=["rgb_out", "cache_out"],
+ opset_version=17,
+ do_constant_folding=True,
+ dynamo=True
+ )
+print(f"✅ ONNX graph saved to {onnx_path.resolve()}")
+
+# (Optional) quick sanity-check with ONNX-Runtime
+try:
+ import onnxruntime as ort
+ sess = ort.InferenceSession(onnx_path.as_posix(),
+ providers=["CUDAExecutionProvider"])
+ ort_inputs = {n: t.cpu().numpy() for n, t in zip(all_inputs_names, inputs)}
+ _ = sess.run(None, ort_inputs)
+ print("✅ ONNX graph is executable")
+except Exception as e:
+ print("⚠️ ONNX check failed:", e)
+
+# ─────────────────────────────────────────────────────────
+# 3️⃣ Build the TensorRT engine
+# ─────────────────────────────────────────────────────────
+TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
+builder = trt.Builder(TRT_LOGGER)
+network = builder.create_network(
+ 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
+parser = trt.OnnxParser(network, TRT_LOGGER)
+
+with open(onnx_path, "rb") as f:
+ if not parser.parse(f.read()):
+ for i in range(parser.num_errors):
+ print(parser.get_error(i))
+ sys.exit("❌ ONNX → TRT parsing failed")
+
+config = builder.create_builder_config()
+
+
+def set_workspace(config, bytes_):
+ """Version-agnostic workspace limit."""
+ if hasattr(config, "max_workspace_size"): # TRT 8 / 9
+ config.max_workspace_size = bytes_
+ else: # TRT 10+
+ config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, bytes_)
+
+
+# …
+config = builder.create_builder_config()
+set_workspace(config, 4 << 30) # 4 GB
+# 4 GB
+
+if builder.platform_has_fast_fp16:
+ config.set_flag(trt.BuilderFlag.FP16)
+
+# ---- INT8 (optional) ----
+# provide a calibrator if you need an INT8 engine; comment this
+# block if you only care about FP16.
+# ─────────────────────────────────────────────────────────
+# helper: version-agnostic workspace limit
+# ─────────────────────────────────────────────────────────
+
+
+def set_workspace(config: trt.IBuilderConfig, bytes_: int = 4 << 30):
+ """
+ TRT < 10.x → config.max_workspace_size
+ TRT ≥ 10.x → config.set_memory_pool_limit(...)
+ """
+ if hasattr(config, "max_workspace_size"): # TRT 8 / 9
+ config.max_workspace_size = bytes_
+ else: # TRT 10+
+ config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE,
+ bytes_)
+
+# ─────────────────────────────────────────────────────────
+# (optional) INT-8 calibrator
+# ─────────────────────────────────────────────────────────
+# ‼ Only keep this block if you really need INT-8 ‼ # gracefully skip if PyCUDA not present
+
+
+class VAECalibrator(trt.IInt8EntropyCalibrator2):
+ def __init__(self, loader, cache="calibration.cache", max_batches=10):
+ super().__init__()
+ self.loader = iter(loader)
+ self.batch_size = loader.batch_size or 1
+ self.max_batches = max_batches
+ self.count = 0
+ self.cache_file = cache
+ self.stream = cuda.Stream()
+ self.dev_ptrs = {}
+
+ # --- TRT 10 needs BOTH spellings ---
+ def get_batch_size(self):
+ return self.batch_size
+
+ def getBatchSize(self):
+ return self.batch_size
+
+ def get_batch(self, names):
+ if self.count >= self.max_batches:
+ return None
+
+ # Randomly sample a number from 1 to 10
+ import random
+ vae_idx = random.randint(0, 10)
+ data = next(self.loader)
+
+ latent = data['ode_latent'][0][:, :1]
+ is_first_frame = torch.tensor([1.0], device="cuda", dtype=torch.float16)
+ feat_cache = ZERO_VAE_CACHE
+ for i in range(vae_idx):
+ inputs = [latent, is_first_frame, *feat_cache]
+ with torch.inference_mode():
+ outputs = model(*inputs)
+ latent = data['ode_latent'][0][:, i + 1:i + 2]
+ is_first_frame = torch.tensor([0.0], device="cuda", dtype=torch.float16)
+ feat_cache = outputs[1:]
+
+ # -------- ensure context is current --------
+ z_np = latent.cpu().numpy().astype('float32')
+
+ ptrs = [] # list[int] – one entry per name
+ for name in names: # <-- match TRT's binding order
+ if name == "z":
+ arr = z_np
+ elif name == "use_cache":
+ arr = is_first_frame.cpu().numpy().astype('float32')
+ else:
+ idx = int(name.split('_')[-1]) # "vae_cache_17" -> 17
+ arr = feat_cache[idx].cpu().numpy().astype('float32')
+
+ if name not in self.dev_ptrs:
+ self.dev_ptrs[name] = cuda.mem_alloc(arr.nbytes)
+
+ cuda.memcpy_htod_async(self.dev_ptrs[name], arr, self.stream)
+ ptrs.append(int(self.dev_ptrs[name])) # ***int() is required***
+
+ self.stream.synchronize()
+ self.count += 1
+ print(f"Calibration batch {self.count}/{self.max_batches}")
+ return ptrs
+
+ # --- calibration-cache helpers (both spellings) ---
+ def read_calibration_cache(self):
+ try:
+ with open(self.cache_file, "rb") as f:
+ return f.read()
+ except FileNotFoundError:
+ return None
+
+ def readCalibrationCache(self):
+ return self.read_calibration_cache()
+
+ def write_calibration_cache(self, cache):
+ with open(self.cache_file, "wb") as f:
+ f.write(cache)
+
+ def writeCalibrationCache(self, cache):
+ self.write_calibration_cache(cache)
+
+
+# ─────────────────────────────────────────────────────────
+# Builder-config + optimisation profile
+# ─────────────────────────────────────────────────────────
+config = builder.create_builder_config()
+set_workspace(config, 4 << 30) # 4 GB
+
+# ► enable FP16 if possible
+if builder.platform_has_fast_fp16:
+ config.set_flag(trt.BuilderFlag.FP16)
+
+# ► enable INT-8 (delete this block if you don’t need it)
+if cuda is not None:
+ config.set_flag(trt.BuilderFlag.INT8)
+ # supply any representative batch you like – here we reuse the latent z
+ calib = VAECalibrator(dataloader)
+ # TRT-10 renamed the setter:
+ if hasattr(config, "set_int8_calibrator"): # TRT 10+
+ config.set_int8_calibrator(calib)
+ else: # TRT ≤ 9
+ config.int8_calibrator = calib
+
+# ---- optimisation profile ----
+profile = builder.create_optimization_profile()
+profile.set_shape(all_inputs_names[0], # latent z
+ min=(1, 1, 16, 60, 104),
+ opt=(1, 1, 16, 60, 104),
+ max=(1, 1, 16, 60, 104))
+profile.set_shape("use_cache", # scalar flag
+ min=(1,), opt=(1,), max=(1,))
+for name, tensor in zip(all_inputs_names[2:], dummy_cache_input):
+ profile.set_shape(name, tensor.shape, tensor.shape, tensor.shape)
+
+config.add_optimization_profile(profile)
+
+# ─────────────────────────────────────────────────────────
+# Build the engine (API changed in TRT-10)
+# ─────────────────────────────────────────────────────────
+print("⚙️ Building engine … (can take a minute)")
+
+if hasattr(builder, "build_serialized_network"): # TRT 10+
+ serialized_engine = builder.build_serialized_network(network, config)
+ assert serialized_engine is not None, "build_serialized_network() failed"
+ plan_path = Path("checkpoints/vae_decoder_int8.trt")
+ plan_path.write_bytes(serialized_engine)
+ engine_bytes = serialized_engine # keep for smoke-test
+else: # TRT ≤ 9
+ engine = builder.build_engine(network, config)
+ assert engine is not None, "build_engine() returned None"
+ plan_path = Path("checkpoints/vae_decoder_int8.trt")
+ plan_path.write_bytes(engine.serialize())
+ engine_bytes = engine.serialize()
+
+print(f"✅ TensorRT engine written to {plan_path.resolve()}")
+
+# ─────────────────────────────────────────────────────────
+# 4️⃣ Quick smoke-test with the brand-new engine
+# ─────────────────────────────────────────────────────────
+with trt.Runtime(TRT_LOGGER) as rt:
+ engine = rt.deserialize_cuda_engine(engine_bytes)
+ context = engine.create_execution_context()
+ stream = torch.cuda.current_stream().cuda_stream
+
+ # pre-allocate device buffers once
+ device_buffers, outputs = {}, []
+ dtype_map = {trt.float32: torch.float32,
+ trt.float16: torch.float16,
+ trt.int8: torch.int8,
+ trt.int32: torch.int32}
+
+ for name, tensor in zip(all_inputs_names, inputs):
+ if -1 in engine.get_tensor_shape(name): # dynamic input
+ context.set_input_shape(name, tensor.shape)
+ context.set_tensor_address(name, int(tensor.data_ptr()))
+ device_buffers[name] = tensor
+
+ context.infer_shapes() # propagate ⇢ outputs
+ for i in range(engine.num_io_tensors):
+ name = engine.get_tensor_name(i)
+ if engine.get_tensor_mode(name) == trt.TensorIOMode.OUTPUT:
+ shape = tuple(context.get_tensor_shape(name))
+ dtype = dtype_map[engine.get_tensor_dtype(name)]
+ out = torch.empty(shape, dtype=dtype, device="cuda")
+ context.set_tensor_address(name, int(out.data_ptr()))
+ outputs.append(out)
+ print(f"output {name} shape: {shape}")
+
+ context.execute_async_v3(stream_handle=stream)
+ torch.cuda.current_stream().synchronize()
+ print("✅ TRT execution OK – first output shape:", outputs[0].shape)
diff --git a/DecMem/evaluation/method_manifest.json b/DecMem/evaluation/method_manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..8868e1e3fbfd09754d2d38e3607b1c37ba062fde
--- /dev/null
+++ b/DecMem/evaluation/method_manifest.json
@@ -0,0 +1,91 @@
+{
+ "method": "DecMem official public Wan2.1-T2V-1.3B reference",
+ "upstream": "https://github.com/KlingAIResearch/DecMem",
+ "upstream_revision": "f355a1809444a9cc6746ff99d5255614348b20ac",
+ "checkpoint": "KlingTeam/DecMem/decmem.pt generator state",
+ "checkpoint_tensor_count": 492,
+ "checkpoint_parameter_count": 1197639296,
+ "checkpoint_weight_policy": "released generator; no EMA substitution",
+ "model": {
+ "trainable_parameters": 0,
+ "optimizer_groups": [],
+ "lr_stages": [],
+ "lora": "none",
+ "action_encoder": "official linear100to16",
+ "memory_projection_layers": 30,
+ "use_ltm_attn": true,
+ "ltm_topk": 80,
+ "ltm_tile_size": [
+ 1,
+ 8,
+ 8
+ ],
+ "stm_sliding_window": 8,
+ "local_attn_size": -1,
+ "block_latents": 4,
+ "activation_checkpointing": false
+ },
+ "sampling": {
+ "profile": "scripts/infer_example.sh released example defaults",
+ "solver": "UniPC",
+ "steps": 20,
+ "cfg": 5.0,
+ "timestep_shift": 5.0,
+ "dtype": "bfloat16",
+ "batch_size": 1,
+ "cfg_effective_batch": 2
+ },
+ "protocol": {
+ "manifest": "evaluation/manifests/minecraft_test_300.csv",
+ "observe_source": [
+ 100,
+ 700
+ ],
+ "score_source": [
+ 700,
+ 1200
+ ],
+ "stride": 1,
+ "unique_observed": 600,
+ "left_duplicates": 1,
+ "observed_rgb_with_padding": 601,
+ "observed_latents": 151,
+ "total_latents": 276,
+ "generated_latents": 125,
+ "generated_rgb": 500,
+ "score_rgb": 500,
+ "full_history": "allobserved latents warm SGM+ALM; partial3cleanlatentsinfirstgeneratedblock preserved"
+ },
+ "controls": {
+ "actions": "unchangedcanonicalWorldMem25channels; prependonezeroaction",
+ "poses": "xyz-firstxyz, negateyaw, wrapanglesmod360; sampled[::4]afterleftduplicate",
+ "pose_construction": "float32 world-to-camera then castbfloat16",
+ "first_generated_latent_pose_source": 703,
+ "last_pose_source": 1199,
+ "prope_max_time": 60,
+ "prope_sequence_semantics": "preserve native temporal indices, dynamicallyextendcoefficienttable; no60framecap"
+ },
+ "image": {
+ "native_resolution": [
+ 352,
+ 640
+ ],
+ "input": "commonRGB01, fullframebilinearresize antialias, then[-1,1]",
+ "encode": "observed601framesonly; futureinitiallatentszero",
+ "decode": "entire observed+generated1101timelineusingnativecausalVAECPUstreaming;discardfirst601",
+ "score_targets": "sharedfixedOasisVAEreconstruction, neverDecMemVAEreconstruction"
+ },
+ "verification": {
+ "syntax": "passed",
+ "actual_first_clip_actions": "matchesofficialextract_pose_action_from_npz",
+ "actual_first_clip_poses": "matchesofficialnormalizationand[::4]",
+ "timeline_600_500": "151context,276total,500generated;firstscored700,last1199",
+ "timeline_600_20": "151context,156total,20generated",
+ "native_model_gpu_import": "pendingavailableSlurmallocation",
+ "strict_checkpoint_model_compatibility": "performed in GPU constructor, pending",
+ "vsa_compile": "passed; CUDA12.9 explicit compute90a/sm90a",
+ "vsa_binary_import": "passed on CPU; execution still awaits GPU"
+ },
+ "full_ready": false,
+ "blocker": "GPU native VSA import and actualgeneration smoke not completed"
+}
diff --git a/DecMem/evaluation/worldmem_adapter.py b/DecMem/evaluation/worldmem_adapter.py
new file mode 100644
index 0000000000000000000000000000000000000000..7177c016fe0e3baed7eaf55370a25a47caf4e701
--- /dev/null
+++ b/DecMem/evaluation/worldmem_adapter.py
@@ -0,0 +1,188 @@
+"""Run the released DecMem memory model on the fixed WorldMem Minecraft split."""
+
+import argparse
+import json
+import math
+import os
+from pathlib import Path
+import sys
+
+import torch
+import torch.nn.functional as F
+from omegaconf import OmegaConf
+
+REPO = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(REPO))
+sys.path.insert(0, str(REPO.parent / "shared"))
+
+from protocol import case_seed, load_minecraft_case, save_case, select_cases
+from utils.pose_utils import poses_to_viewmats
+
+
+def timeline(history_frames=600, target_frames=500):
+ prefix_padding = (1 - history_frames) % 4
+ context_latents = (history_frames + prefix_padding - 1) // 4 + 1
+ total_latents = math.ceil((context_latents + math.ceil(target_frames / 4)) / 4) * 4
+ generated_frames = (total_latents - context_latents) * 4
+ return context_latents, prefix_padding, total_latents, generated_frames
+
+
+def prepare_controls(case, target_frames):
+ history = case["history"]
+ n_cond, padding, total_latents, generated_frames = timeline(history, target_frames)
+ poses = case["poses"][:history + target_frames].clone().float()
+ # Match extract_pose_action_from_npz: translate xyz, negate yaw, then wrap angles.
+ poses[:, :3] -= poses[:1, :3].clone()
+ poses[:, -1].neg_()
+ poses[:, 3:].remainder_(360)
+ actions = case["actions"][:history + target_frames].float()
+ tail = generated_frames - target_frames
+ poses = torch.cat([poses[:1].expand(padding, -1), poses, poses[-1:].expand(tail, -1)])
+ actions = torch.cat([actions.new_zeros((padding, 25)), actions, actions.new_zeros((tail, 25))])
+ source_indices = case["source_indices"][:history + target_frames]
+ source_indices = torch.cat([source_indices[:1].expand(padding), source_indices, source_indices[-1:].expand(tail)])
+ return actions, poses[::4], source_indices[::4], (n_cond, padding, total_latents, generated_frames)
+
+
+class DecMemAdapter:
+ def __init__(self, checkpoint, device="cuda"):
+ self.checkpoint = str(Path(checkpoint).resolve())
+ # Upstream model and VAE loading use paths relative to the DecMem checkout.
+ os.chdir(REPO)
+ from pipeline import WorldMemCausalDiffusionInferencePipeline
+
+ self.device = torch.device(device)
+ self.dtype = torch.bfloat16
+ self.config = OmegaConf.merge(
+ OmegaConf.load(REPO / "configs/default_config.yaml"),
+ OmegaConf.load(REPO / "configs/decmem.yaml"),
+ )
+ self.config.use_text_encoder = False
+ self.config.image_or_video_shape[1] = timeline()[2]
+ self.config.num_frames = self.config.image_or_video_shape[1]
+ self.config.prope_max_time = 60
+ self.config.rope_scale_pattern = "rope"
+ self.pipeline = WorldMemCausalDiffusionInferencePipeline(self.config, device=self.device)
+ checkpoint_state = torch.load(self.checkpoint, map_location="cpu", weights_only=True, mmap=True)
+ self.pipeline.generator.load_state_dict(checkpoint_state["generator"], strict=True)
+ self.pipeline.to(device=self.device, dtype=self.dtype).eval().requires_grad_(False)
+ self.parameter_counts = {
+ "generator_total": sum(p.numel() for p in self.pipeline.generator.parameters()),
+ "generator_trainable": sum(p.numel() for p in self.pipeline.generator.parameters() if p.requires_grad),
+ "action_encoder": sum(p.numel() for p in self.pipeline.generator.action_encoder.parameters()),
+ "memory_projection": sum(p.numel() for n, p in self.pipeline.generator.named_parameters() if ".mem_proj." in n),
+ "vae_total": sum(p.numel() for p in self.pipeline.vae.parameters()),
+ "vae_trainable": sum(p.numel() for p in self.pipeline.vae.parameters() if p.requires_grad),
+ }
+ print(json.dumps({"parameter_counts": self.parameter_counts, "checkpoint_strict_load": "passed"}), flush=True)
+
+ @torch.inference_mode()
+ def generate(self, case, target_frames=500, seed=0):
+ torch.manual_seed(seed)
+ torch.cuda.manual_seed_all(seed)
+ history = case["history"]
+ actions, poses, pose_indices, layout = prepare_controls(case, target_frames)
+ n_cond, padding, total_latents, generated_frames = layout
+ # Future RGB never reaches the VAE; only the complete observed prefix is encoded.
+ observed = case["rgb"][:history].to(self.device)
+ observed = F.interpolate(observed, (352, 640), mode="bilinear", align_corners=False, antialias=True)
+ observed = torch.cat([observed[:1].expand(padding, -1, -1, -1), observed])
+ observed = observed.transpose(0, 1).unsqueeze(0).mul(2).sub(1).to(self.dtype)
+ context_latent = self.pipeline.vae.encode_to_latent(observed).to(self.device, self.dtype)
+ del observed
+
+ initial_latent = context_latent.new_zeros((1, total_latents, 16, 44, 80))
+ initial_latent[:, :n_cond] = context_latent
+ del context_latent
+ actions = actions.unsqueeze(0).to(self.device, self.dtype)
+ viewmats = poses_to_viewmats(poses.unsqueeze(0).to(self.device)).to(self.dtype)
+ noise = torch.randn(initial_latent.shape, device=self.device, dtype=self.dtype)
+ print(json.dumps({"observed_frames": history, "prefix_padding": padding, "condition_latents": n_cond, "total_latents": total_latents}), flush=True)
+ _, output_latents = self.pipeline.inference(
+ noise=noise,
+ actions=actions,
+ viewmats=viewmats,
+ initial_latent=initial_latent,
+ num_condition_frames=n_cond,
+ guidance_scale=5.0,
+ sampling_steps=20,
+ return_latents=True,
+ skip_vae_decode=True,
+ )
+ # Denoising is finished. Release global KV before decoding the complete causal timeline.
+ self.pipeline.kv_cache = None
+ self.pipeline.crossattn_cache = None
+ torch.cuda.empty_cache()
+ video = self.pipeline.vae.decode_to_pixel(output_latents)
+ prediction = video[0, history + padding:history + padding + target_frames].float().mul(0.5).add(0.5).clamp(0, 1).cpu()
+ metadata = {
+ "method": "decmem",
+ "implementation": "official public Wan2.1-T2V-1.3B reference",
+ "checkpoint": self.checkpoint,
+ "weight_source": "released generator state; no EMA substitution",
+ "parameter_counts": self.parameter_counts,
+ "dtype": "bfloat16",
+ "batch_size": 1,
+ "cfg_batch_size": 2,
+ "sampling": {"solver": "UniPC", "steps": 20, "guidance_scale": 5.0, "timestep_shift": 5.0, "source": "scripts/infer_example.sh"},
+ "memory": {"use_ltm_attn": True, "ltm_topk": 80, "ltm_tile_size": [1, 8, 8], "stm_sliding_window": 8, "num_frame_per_block": 4},
+ "history_frames": history,
+ "history_source_indices": case["source_indices"][:history].tolist(),
+ "history_left_padding": padding,
+ "condition_latents": n_cond,
+ "total_latents": total_latents,
+ "generated_frames": generated_frames,
+ "scored_frames": target_frames,
+ "future_padding": generated_frames - target_frames,
+ "action_mapping": "official WorldMem 25 channels, no scale changes; prepend one noop",
+ "pose_mapping": "translate xyz relative to source100; negate yaw; angles modulo360; padded source indices[::4]",
+ "latent_pose_source_indices": pose_indices.tolist(),
+ "pose_matrix_precision": "float32 construction, then bfloat16 model input",
+ "prope_max_time": 60,
+ "prope_time_semantics": "native absolute latent indices;60 is not a sequence length cap",
+ "native_resolution": [352, 640],
+ "preprocessing": "full-frame bilinear resize with antialias; no crop",
+ "target_contract": "shared fixed Oasis VAE reconstruction; no method VAE target substitution",
+ "seed": seed,
+ }
+ return prediction, metadata
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--manifest", required=True)
+ parser.add_argument("--data-root", required=True)
+ parser.add_argument("--checkpoint", default=str(REPO.parent / "checkpoints/decmem/decmem.pt"))
+ parser.add_argument("--output", required=True)
+ parser.add_argument("--limit", type=int)
+ parser.add_argument("--max-generated", type=int, default=500)
+ parser.add_argument("--seed", type=int, default=0)
+ parser.add_argument("--rank", type=int, default=int(os.environ.get("SLURM_PROCID", 0)))
+ parser.add_argument("--world-size", type=int, default=int(os.environ.get("SLURM_NTASKS", 1)))
+ parser.add_argument("--preflight-only", action="store_true")
+ args = parser.parse_args()
+ if not 1 <= args.max_generated <= 500:
+ parser.error("--max-generated must be between 1 and the fixed 500-frame evaluation horizon")
+ torch.set_num_threads(4)
+ rows = select_cases(args.manifest, rank=args.rank, world_size=args.world_size, limit=args.limit)
+ if args.preflight_only:
+ case = load_minecraft_case(rows[0], args.data_root)
+ actions, poses, indices, layout = prepare_controls(case, args.max_generated)
+ 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"}
+ output = Path(args.output)
+ output.mkdir(parents=True, exist_ok=True)
+ (output / f"preflight_rank{args.rank}.json").write_text(json.dumps(report, indent=2) + "\n")
+ print(json.dumps(report), flush=True)
+ return
+ model = DecMemAdapter(args.checkpoint)
+ revision = "f355a1809444a9cc6746ff99d5255614348b20ac"
+ for row in rows:
+ case = load_minecraft_case(row, args.data_root)
+ prediction, metadata = model.generate(case, args.max_generated, case_seed(row, args.seed))
+ metadata.update({"git_revision": revision, "arguments": vars(args)})
+ directory = save_case(args.output, row, prediction, metadata)
+ print(f"Saved {tuple(prediction.shape)} to {directory}", flush=True)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/DecMem/example/example.csv b/DecMem/example/example.csv
new file mode 100644
index 0000000000000000000000000000000000000000..45a3160800ef8bc83e37b4303c9bf60ca7a4b7e1
--- /dev/null
+++ b/DecMem/example/example.csv
@@ -0,0 +1,3 @@
+video_path,action_path
+example/000045.mp4,example/000045.npz
+example/000046.mp4,example/000046.npz
\ No newline at end of file
diff --git a/DecMem/infer.py b/DecMem/infer.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f3eab7742b04e466c92cfb96d2989d1af422f5a
--- /dev/null
+++ b/DecMem/infer.py
@@ -0,0 +1,338 @@
+"""
+WorldMem CFG inference — block-by-block causal denoising with KV cache + PRoPE.
+
+Delegates to WorldMemCausalDiffusionInferencePipeline, which:
+ - Writes any fully-clean prefix block to the KV cache at timestep=0 first.
+ - Denoises one block at a time via UniPC, each step a single forward with
+ kv_cache + PRoPE recomputed for that block's viewmats.
+ - Commits each block's clean K/V to cache before moving to the next, so
+ later blocks attend against clean history (matching teacher-forcing
+ training distribution).
+ - CFG is one batched forward with batch_size = 2*B; the KV cache is also
+ allocated with batch_size = 2*B. cond/uncond chunked back after each step.
+"""
+
+import argparse
+import gc
+import os
+import sys
+
+import torch
+import torch.distributed as dist
+import numpy as np
+from omegaconf import OmegaConf
+from torchvision.io import write_video
+
+os.chdir(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, ".")
+
+from pipeline import WorldMemCausalDiffusionInferencePipeline
+from utils.dataset import WorldMemCSVDataset
+from utils.pose_utils import poses_to_viewmats
+from utils.misc import set_seed
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--config_path", default="configs/worldmem_diffusion_overfit.yaml")
+parser.add_argument("--output_dir", default="test/res/infer_worldmem_cfg")
+parser.add_argument("--checkpoint_path", default=None,
+ help="Optional generator checkpoint (model.pt)")
+parser.add_argument("--data_path", default=None,
+ help="Override config.data_path (CSV) for inference test set.")
+parser.add_argument("--num_samples", type=int, default=1,
+ help="Number of noise resamples per dataset entry.")
+parser.add_argument("--num_inference_steps", type=int, default=50)
+parser.add_argument("--guidance_scale", type=float, default=3.0,
+ help="CFG scale. <=1.0 disables CFG (single forward, B=1 cache).")
+parser.add_argument("--seed", type=int, default=42)
+parser.add_argument("--start_index", type=int, default=0,
+ help="First dataset row (env: START_INDEX). "
+ "Ignored if --sample_indices is provided.")
+parser.add_argument("--num_entries", type=int, default=None,
+ help="Number of consecutive dataset rows from --start_index (env: N). "
+ "Default: all rows from start_index to dataset end. "
+ "Ignored if --sample_indices is provided.")
+parser.add_argument("--sample_indices", type=str, default=None,
+ help="Explicit dataset indices, overrides --start_index/--num_entries. "
+ "Accepts comma-separated ints and inclusive ranges, e.g. "
+ "'0,3,5' or '0-9,15,20-22', or 'all' for the full dataset.")
+parser.add_argument("--num_condition_frames", type=int, default=1,
+ help="Number of clean prefix latent frames (1=first image, "
+ "3=first block when num_frame_per_block=3).")
+parser.add_argument("--num_latent_frames", type=int, default=None,
+ help="Override config.image_or_video_shape[1] (latent T). "
+ "Must be a multiple of num_frame_per_block.")
+parser.add_argument("--tag", type=str, default="",
+ help="Filename tag appended to outputs.")
+parser.add_argument("--offload", action="store_true",
+ help="Mutually-exclusive GPU residency for generator and VAE: "
+ "during denoising the VAE lives on CPU; for VAE encode/decode "
+ "the generator lives on CPU. Trades extra H2D/D2H copies for "
+ "lower peak VRAM.")
+args = parser.parse_args()
+
+# ─── Distributed (optional, via torchrun) ────────────────────────────────────
+if "LOCAL_RANK" in os.environ:
+ dist.init_process_group(backend="nccl")
+ local_rank = int(os.environ["LOCAL_RANK"])
+ world_size = dist.get_world_size()
+ torch.cuda.set_device(local_rank)
+ device = torch.device(f"cuda:{local_rank}")
+else:
+ local_rank = 0
+ world_size = 1
+ device = torch.device("cuda")
+
+set_seed(args.seed + local_rank)
+dtype = torch.bfloat16
+
+# ─── Config ──────────────────────────────────────────────────────────────────
+config = OmegaConf.load(args.config_path)
+default_config = OmegaConf.load("configs/default_config.yaml")
+config = OmegaConf.merge(default_config, config)
+config.use_text_encoder = False
+if args.data_path is not None:
+ print(f"Overriding config.data_path: {config.data_path} -> {args.data_path}")
+ config.data_path = args.data_path
+
+if args.num_latent_frames is not None:
+ nfb = int(config.num_frame_per_block)
+ assert args.num_latent_frames % nfb == 0, (
+ f"--num_latent_frames={args.num_latent_frames} must be a multiple of "
+ f"num_frame_per_block={nfb}")
+ print(f"Overriding latent T: {config.image_or_video_shape[1]} -> {args.num_latent_frames}")
+ config.image_or_video_shape[1] = args.num_latent_frames
+ config.num_frames = args.num_latent_frames
+
+num_latent_frames = config.image_or_video_shape[1]
+latent_C, latent_H, latent_W = config.image_or_video_shape[2:]
+n_cond = int(args.num_condition_frames)
+assert 1 <= n_cond < num_latent_frames, \
+ f"num_condition_frames must be in [1, {num_latent_frames - 1}], got {n_cond}"
+
+guidance_scale = float(args.guidance_scale)
+use_cfg = guidance_scale > 1.0
+if local_rank == 0:
+ os.makedirs(args.output_dir, exist_ok=True)
+if dist.is_initialized():
+ dist.barrier()
+print(f"Config: T={num_latent_frames} latent {latent_H}x{latent_W} "
+ f"nfb={config.num_frame_per_block} "
+ f"guidance_scale={guidance_scale} (cfg={use_cfg}) "
+ f"n_cond={n_cond}")
+
+# ─── Pipeline ────────────────────────────────────────────────────────────────
+torch.set_grad_enabled(False)
+pipeline = WorldMemCausalDiffusionInferencePipeline(config, device=device)
+pipeline.generator = pipeline.generator.to(device=device, dtype=dtype)
+pipeline.generator.eval()
+pipeline.vae = pipeline.vae.to(device=device, dtype=dtype)
+
+
+# ─── Optional CPU/GPU swap for generator/VAE ────────────────────────────────
+# When --offload is set, only one of {generator, VAE} lives on the GPU at any
+# time. The "owner" variable below tracks which model currently holds GPU
+# residency so we can no-op redundant swaps. Idle model is parked on CPU.
+_GPU_OWNER = "both" # "generator" | "vae" | "both" | "none"
+
+
+def _swap_to(target: str):
+ """Make ``target`` the sole GPU resident; park the other on CPU.
+
+ target ∈ {"generator", "vae"}. No-op when --offload is disabled.
+ Frees pipeline.kv_cache before parking the generator and runs an
+ explicit gc + empty_cache so VRAM is reclaimed before the next load.
+ """
+ global _GPU_OWNER
+ if not args.offload:
+ return
+ if _GPU_OWNER == target:
+ return
+ if target == "vae":
+ # Park generator on CPU. Drop transient GPU state (kv_cache) first
+ # so the move actually frees memory rather than fragmenting it.
+ if pipeline.kv_cache is not None:
+ pipeline.kv_cache = None
+ if pipeline.crossattn_cache is not None:
+ pipeline.crossattn_cache = None
+ pipeline.generator.to("cpu")
+ gc.collect()
+ torch.cuda.empty_cache()
+ pipeline.vae.to(device=device, dtype=dtype)
+ elif target == "generator":
+ pipeline.vae.to("cpu")
+ gc.collect()
+ torch.cuda.empty_cache()
+ pipeline.generator.to(device=device, dtype=dtype)
+ else:
+ raise ValueError(f"Unknown swap target: {target}")
+ _GPU_OWNER = target
+
+
+if args.offload:
+ # Initial state: VAE on GPU (first thing we do per-sample is encode).
+ pipeline.generator.to("cpu")
+ gc.collect()
+ torch.cuda.empty_cache()
+ _GPU_OWNER = "vae"
+ print("Offload enabled: generator/VAE are mutually exclusive on GPU.")
+
+if args.checkpoint_path:
+ weight_tag = "generator"
+ print(f"Loading checkpoint: {args.checkpoint_path} ({weight_tag})")
+ ckpt = torch.load(args.checkpoint_path, map_location="cpu", weights_only=False)
+ sd = ckpt.get("generator", ckpt.get("model", ckpt))
+ sample_key = next(iter(sd.keys()))
+ target = pipeline.generator if sample_key.startswith("model.") else pipeline.generator.model
+ missing, unexpected = target.load_state_dict(sd, strict=False)
+ if missing:
+ print(f" missing keys ({len(missing)}): {missing[:5]}{'...' if len(missing) > 5 else ''}")
+ if unexpected:
+ print(f" unexpected keys ({len(unexpected)}): {unexpected[:5]}{'...' if len(unexpected) > 5 else ''}")
+else:
+ print("No checkpoint — using pretrained WAN backbone + random action encoder.")
+
+# ─── Dataset ─────────────────────────────────────────────────────────────────
+dataset = WorldMemCSVDataset(
+ config.data_path,
+ num_latent_frames=num_latent_frames,
+ sample_strategy="first",
+)
+print(f"Dataset size: {len(dataset)}")
+
+def _parse_sample_indices(spec: str, dataset_size: int) -> list:
+ """Parse '0,3,5' / '0-9,15,20-22' / 'all' into a deduplicated sorted index list."""
+ if spec.strip().lower() == "all":
+ return list(range(dataset_size))
+ out = set()
+ for token in spec.split(","):
+ token = token.strip()
+ if not token:
+ continue
+ if "-" in token:
+ lo, hi = token.split("-", 1)
+ lo, hi = int(lo), int(hi)
+ assert lo <= hi, f"bad range '{token}': lo > hi"
+ out.update(range(lo, hi + 1))
+ else:
+ out.add(int(token))
+ indices = sorted(out)
+ for i in indices:
+ assert 0 <= i < dataset_size, (
+ f"index {i} out of range [0, {dataset_size}) for --sample_indices='{spec}'")
+ return indices
+
+
+if args.sample_indices is not None:
+ sample_indices = _parse_sample_indices(args.sample_indices, len(dataset))
+ assert sample_indices, f"--sample_indices='{args.sample_indices}' parsed to empty list"
+ print(f"Using explicit --sample_indices ({len(sample_indices)}): {sample_indices}")
+else:
+ assert args.start_index >= 0, f"--start_index must be >= 0, got {args.start_index}"
+ if args.num_entries is not None:
+ assert args.num_entries > 0, f"--num_entries must be > 0, got {args.num_entries}"
+ end_index = args.start_index + args.num_entries
+ sample_indices = list(range(args.start_index, min(end_index, len(dataset))))
+ if end_index > len(dataset):
+ print(f"Warning: start_index={args.start_index} + num_entries={args.num_entries} "
+ f"exceeds dataset size {len(dataset)}; using indices "
+ f"{args.start_index}..{len(dataset) - 1}")
+ else:
+ sample_indices = list(range(args.start_index, len(dataset)))
+
+if world_size > 1:
+ sample_indices = sample_indices[local_rank::world_size]
+ print(f"[rank {local_rank}/{world_size}] shard indices: {sample_indices}")
+elif sample_indices:
+ print(f"Will run on dataset indices: {sample_indices}")
+
+def run_one_sample(ds_idx: int):
+ print(f"\n=== Dataset index {ds_idx} ===")
+ rng_state = np.random.get_state()
+ np.random.seed(args.seed)
+ sample = dataset[ds_idx]
+ np.random.set_state(rng_state)
+
+ frames_raw = sample["frames"].unsqueeze(0).to(device=device, dtype=dtype) # (1,3,T_vid,H,W)
+ actions = sample["actions"].unsqueeze(0).to(device=device, dtype=dtype) # (1,T_vid,25)
+ poses = sample["poses"].unsqueeze(0).to(device=device, dtype=dtype) # (1,T_lat,5)
+
+ # VAE encode: needs VAE on GPU.
+ _swap_to("vae")
+ gt_latent = pipeline.vae.encode_to_latent(frames_raw).to(device=device, dtype=dtype)
+ viewmats = poses_to_viewmats(poses) # (1,T,4,4)
+
+ for sample_idx in range(args.num_samples):
+ print(f" noise sample {sample_idx + 1}/{args.num_samples} (prefix={n_cond}) ...")
+ torch.manual_seed(args.seed + sample_idx)
+
+ noise = torch.randn(1, num_latent_frames, latent_C, latent_H, latent_W,
+ device=device, dtype=dtype)
+ # Denoising: needs generator on GPU; VAE parked on CPU.
+ _swap_to("generator")
+ with torch.no_grad():
+ video, latents = pipeline.inference(
+ noise=noise,
+ actions=actions,
+ viewmats=viewmats,
+ initial_latent=gt_latent,
+ num_condition_frames=n_cond,
+ guidance_scale=guidance_scale,
+ sampling_steps=args.num_inference_steps,
+ return_latents=True,
+ skip_vae_decode=args.offload,
+ )
+
+ # Decode: park generator, bring VAE back to GPU.
+ # In offload mode, `video` is None — we decode externally now that the
+ # VAE owns the GPU. In non-offload mode the decode already happened.
+ _swap_to("vae")
+ if args.offload:
+ video = pipeline.vae.decode_to_pixel(latents.to(dtype=dtype))
+ video = (video * 0.5 + 0.5).clamp(0, 1)
+
+ gt_video = pipeline.vae.decode_to_pixel(gt_latent.to(dtype=dtype))
+ gt_video = (gt_video * 0.5 + 0.5).clamp(0, 1)
+
+ gen_uint8 = (video[0].permute(0, 2, 3, 1).cpu().float() * 255).byte()
+ gt_uint8 = (gt_video[0].permute(0, 2, 3, 1).cpu().float() * 255).byte()
+ side_by_side = torch.cat([gt_uint8, gen_uint8], dim=2)
+
+ tag = f"_{args.tag}" if args.tag else ""
+ out_path = os.path.join(
+ args.output_dir,
+ f"worldmem_s{ds_idx}_n{sample_idx}_cond{n_cond}_cfg{guidance_scale}{tag}.mp4")
+ write_video(out_path, side_by_side, fps=10)
+
+ out_dir = os.path.join(args.output_dir, 'pred')
+ os.makedirs(out_dir, exist_ok=True)
+ out_gen_path = os.path.join(
+ out_dir,
+ f"worldmem_s{ds_idx}_n{sample_idx}_cond{n_cond}_cfg{guidance_scale}{tag}.mp4")
+ write_video(out_gen_path, gen_uint8, fps=10)
+
+ out_dir = os.path.join(args.output_dir, 'gt')
+ os.makedirs(out_dir, exist_ok=True)
+ out_gt_path = os.path.join(
+ out_dir,
+ f"worldmem_s{ds_idx}_n{sample_idx}_cond{n_cond}_cfg{guidance_scale}{tag}.mp4")
+ write_video(out_gt_path, gt_uint8, fps=10)
+
+ print(f" Saved {out_path} (left=GT | right=generated)")
+
+ # Release per-sample GPU residents and defrag the allocator before the
+ # next iteration re-allocates the KV cache inside pipeline.inference().
+ del video, latents, gt_video, gen_uint8, gt_uint8, side_by_side
+ gc.collect()
+ torch.cuda.empty_cache()
+
+
+if not sample_indices:
+ print(f"[rank {local_rank}] no samples in this shard, skipping.")
+else:
+ for ds_idx in sample_indices:
+ run_one_sample(ds_idx)
+
+if dist.is_initialized():
+ dist.barrier()
+if local_rank == 0:
+ print("\nDone.")
\ No newline at end of file
diff --git a/DecMem/model/__init__.py b/DecMem/model/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f858161d1f9118fb2ddb1b33205fe5f09bfcec7
--- /dev/null
+++ b/DecMem/model/__init__.py
@@ -0,0 +1,9 @@
+from .diffusion import CausalDiffusion
+from .causvid import CausVid
+from .gan import GAN
+
+__all__ = [
+ "CausalDiffusion",
+ "CausVid",
+ "GAN",
+]
diff --git a/DecMem/model/base.py b/DecMem/model/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..2714042bea30c10a50640b447ffafb2962ea4656
--- /dev/null
+++ b/DecMem/model/base.py
@@ -0,0 +1,222 @@
+from typing import Tuple
+from einops import rearrange
+from torch import nn
+import torch.distributed as dist
+import torch
+
+from pipeline import SelfForcingTrainingPipeline
+from utils.loss import get_denoising_loss
+from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
+
+
+class BaseModel(nn.Module):
+ def __init__(self, args, device):
+ super().__init__()
+ self._initialize_models(args, device)
+
+ self.device = device
+ self.args = args
+ self.dtype = torch.bfloat16 if args.mixed_precision else torch.float32
+ if hasattr(args, "denoising_step_list"):
+ self.denoising_step_list = torch.tensor(args.denoising_step_list, dtype=torch.long)
+ if args.warp_denoising_step:
+ timesteps = torch.cat((self.scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32)))
+ self.denoising_step_list = timesteps[1000 - self.denoising_step_list]
+
+ def _initialize_models(self, args, device):
+ self.real_model_name = getattr(args, "real_name", "Wan2.1-T2V-1.3B")
+ self.fake_model_name = getattr(args, "fake_name", "Wan2.1-T2V-1.3B")
+
+ self.generator = WanDiffusionWrapper(**getattr(args, "model_kwargs", {}), is_causal=True)
+ self.generator.model.requires_grad_(True)
+
+ self.real_score = WanDiffusionWrapper(model_name=self.real_model_name, is_causal=False)
+ self.real_score.model.requires_grad_(False)
+
+ self.fake_score = WanDiffusionWrapper(model_name=self.fake_model_name, is_causal=False)
+ self.fake_score.model.requires_grad_(True)
+
+ self.text_encoder = WanTextEncoder()
+ self.text_encoder.requires_grad_(False)
+
+ self.vae = WanVAEWrapper()
+ self.vae.requires_grad_(False)
+
+ self.scheduler = self.generator.get_scheduler()
+ self.scheduler.timesteps = self.scheduler.timesteps.to(device)
+
+ def _get_timestep(
+ self,
+ min_timestep: int,
+ max_timestep: int,
+ batch_size: int,
+ num_frame: int,
+ num_frame_per_block: int,
+ uniform_timestep: bool = False
+ ) -> torch.Tensor:
+ """
+ Randomly generate a timestep tensor based on the generator's task type. It uniformly samples a timestep
+ from the range [min_timestep, max_timestep], and returns a tensor of shape [batch_size, num_frame].
+ - If uniform_timestep, it will use the same timestep for all frames.
+ - If not uniform_timestep, it will use a different timestep for each block.
+ """
+ if uniform_timestep:
+ timestep = torch.randint(
+ min_timestep,
+ max_timestep,
+ [batch_size, 1],
+ device=self.device,
+ dtype=torch.long
+ ).repeat(1, num_frame)
+ return timestep
+ else:
+ timestep = torch.randint(
+ min_timestep,
+ max_timestep,
+ [batch_size, num_frame],
+ device=self.device,
+ dtype=torch.long
+ )
+ # make the noise level the same within every block
+ if self.independent_first_frame:
+ # the first frame is always kept the same
+ timestep_from_second = timestep[:, 1:]
+ timestep_from_second = timestep_from_second.reshape(
+ timestep_from_second.shape[0], -1, num_frame_per_block)
+ timestep_from_second[:, :, 1:] = timestep_from_second[:, :, 0:1]
+ timestep_from_second = timestep_from_second.reshape(
+ timestep_from_second.shape[0], -1)
+ timestep = torch.cat([timestep[:, 0:1], timestep_from_second], dim=1)
+ else:
+ timestep = timestep.reshape(
+ timestep.shape[0], -1, num_frame_per_block)
+ timestep[:, :, 1:] = timestep[:, :, 0:1]
+ timestep = timestep.reshape(timestep.shape[0], -1)
+ return timestep
+
+
+class SelfForcingModel(BaseModel):
+ def __init__(self, args, device):
+ super().__init__(args, device)
+ self.denoising_loss_func = get_denoising_loss(args.denoising_loss_type)()
+
+ def _run_generator(
+ self,
+ image_or_video_shape,
+ conditional_dict: dict,
+ initial_latent: torch.tensor = None
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Optionally simulate the generator's input from noise using backward simulation
+ and then run the generator for one-step.
+ Input:
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
+ - initial_latent: a tensor containing the initial latents [B, F, C, H, W].
+ Output:
+ - pred_image: a tensor with shape [B, F, C, H, W].
+ - denoised_timestep: an integer
+ """
+ # Step 1: Sample noise and backward simulate the generator's input
+ assert getattr(self.args, "backward_simulation", True), "Backward simulation needs to be enabled"
+ if initial_latent is not None:
+ conditional_dict["initial_latent"] = initial_latent
+ if self.args.i2v:
+ noise_shape = [image_or_video_shape[0], image_or_video_shape[1] - 1, *image_or_video_shape[2:]]
+ else:
+ noise_shape = image_or_video_shape.copy()
+
+ # During training, the number of generated frames should be uniformly sampled from
+ # [21, self.num_training_frames], but still being a multiple of self.num_frame_per_block
+ min_num_frames = 20 if self.args.independent_first_frame else 21
+ max_num_frames = self.num_training_frames - 1 if self.args.independent_first_frame else self.num_training_frames
+ assert max_num_frames % self.num_frame_per_block == 0
+ assert min_num_frames % self.num_frame_per_block == 0
+ max_num_blocks = max_num_frames // self.num_frame_per_block
+ min_num_blocks = min_num_frames // self.num_frame_per_block
+ num_generated_blocks = torch.randint(min_num_blocks, max_num_blocks + 1, (1,), device=self.device)
+ dist.broadcast(num_generated_blocks, src=0)
+ num_generated_blocks = num_generated_blocks.item()
+ num_generated_frames = num_generated_blocks * self.num_frame_per_block
+ if self.args.independent_first_frame and initial_latent is None:
+ num_generated_frames += 1
+ min_num_frames += 1
+ # Sync num_generated_frames across all processes
+ noise_shape[1] = num_generated_frames
+
+ pred_image_or_video, denoised_timestep_from, denoised_timestep_to = self._consistency_backward_simulation(
+ noise=torch.randn(noise_shape,
+ device=self.device, dtype=self.dtype),
+ **conditional_dict,
+ )
+ # Slice last 21 frames
+ if pred_image_or_video.shape[1] > 21:
+ with torch.no_grad():
+ # Reencode to get image latent
+ latent_to_decode = pred_image_or_video[:, :-20, ...]
+ # Deccode to video
+ pixels = self.vae.decode_to_pixel(latent_to_decode)
+ frame = pixels[:, -1:, ...].to(self.dtype)
+ frame = rearrange(frame, "b t c h w -> b c t h w")
+ # Encode frame to get image latent
+ image_latent = self.vae.encode_to_latent(frame).to(self.dtype)
+ pred_image_or_video_last_21 = torch.cat([image_latent, pred_image_or_video[:, -20:, ...]], dim=1)
+ else:
+ pred_image_or_video_last_21 = pred_image_or_video
+
+ if num_generated_frames != min_num_frames:
+ # Currently, we do not use gradient for the first chunk, since it contains image latents
+ gradient_mask = torch.ones_like(pred_image_or_video_last_21, dtype=torch.bool)
+ if self.args.independent_first_frame:
+ gradient_mask[:, :1] = False
+ else:
+ gradient_mask[:, :self.num_frame_per_block] = False
+ else:
+ gradient_mask = None
+
+ pred_image_or_video_last_21 = pred_image_or_video_last_21.to(self.dtype)
+ return pred_image_or_video_last_21, gradient_mask, denoised_timestep_from, denoised_timestep_to
+
+ def _consistency_backward_simulation(
+ self,
+ noise: torch.Tensor,
+ **conditional_dict: dict
+ ) -> torch.Tensor:
+ """
+ Simulate the generator's input from noise to avoid training/inference mismatch.
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
+ Here we use the consistency sampler (https://arxiv.org/abs/2303.01469)
+ Input:
+ - noise: a tensor sampled from N(0, 1) with shape [B, F, C, H, W] where the number of frame is 1 for images.
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
+ Output:
+ - output: a tensor with shape [B, T, F, C, H, W].
+ T is the total number of timesteps. output[0] is a pure noise and output[i] and i>0
+ represents the x0 prediction at each timestep.
+ """
+ if self.inference_pipeline is None:
+ self._initialize_inference_pipeline()
+
+ return self.inference_pipeline.inference_with_trajectory(
+ noise=noise, **conditional_dict
+ )
+
+ def _initialize_inference_pipeline(self):
+ """
+ Lazy initialize the inference pipeline during the first backward simulation run.
+ Here we encapsulate the inference code with a model-dependent outside function.
+ We pass our FSDP-wrapped modules into the pipeline to save memory.
+ """
+ self.inference_pipeline = SelfForcingTrainingPipeline(
+ denoising_step_list=self.denoising_step_list,
+ scheduler=self.scheduler,
+ generator=self.generator,
+ num_frame_per_block=self.num_frame_per_block,
+ independent_first_frame=self.args.independent_first_frame,
+ same_step_across_blocks=self.args.same_step_across_blocks,
+ last_step_only=self.args.last_step_only,
+ num_max_frames=self.num_training_frames,
+ context_noise=self.args.context_noise
+ )
diff --git a/DecMem/model/causvid.py b/DecMem/model/causvid.py
new file mode 100644
index 0000000000000000000000000000000000000000..3abdac190f3fb596cf5e8391687d185f47661e48
--- /dev/null
+++ b/DecMem/model/causvid.py
@@ -0,0 +1,391 @@
+import torch.nn.functional as F
+from typing import Tuple
+import torch
+
+from model.base import BaseModel
+
+
+class CausVid(BaseModel):
+ def __init__(self, args, device):
+ """
+ Initialize the DMD (Distribution Matching Distillation) module.
+ This class is self-contained and compute generator and fake score losses
+ in the forward pass.
+ """
+ super().__init__(args, device)
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
+ self.num_training_frames = getattr(args, "num_training_frames", 21)
+
+ if self.num_frame_per_block > 1:
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
+
+ self.independent_first_frame = getattr(args, "independent_first_frame", False)
+ if self.independent_first_frame:
+ self.generator.model.independent_first_frame = True
+ if args.gradient_checkpointing:
+ self.generator.enable_gradient_checkpointing()
+ self.fake_score.enable_gradient_checkpointing()
+
+ # Step 2: Initialize all dmd hyperparameters
+ self.num_train_timestep = args.num_train_timestep
+ self.min_step = int(0.02 * self.num_train_timestep)
+ self.max_step = int(0.98 * self.num_train_timestep)
+ if hasattr(args, "real_guidance_scale"):
+ self.real_guidance_scale = args.real_guidance_scale
+ self.fake_guidance_scale = args.fake_guidance_scale
+ else:
+ self.real_guidance_scale = args.guidance_scale
+ self.fake_guidance_scale = 0.0
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
+ self.teacher_forcing = getattr(args, "teacher_forcing", False)
+
+ if getattr(self.scheduler, "alphas_cumprod", None) is not None:
+ self.scheduler.alphas_cumprod = self.scheduler.alphas_cumprod.to(device)
+ else:
+ self.scheduler.alphas_cumprod = None
+
+ def _compute_kl_grad(
+ self, noisy_image_or_video: torch.Tensor,
+ estimated_clean_image_or_video: torch.Tensor,
+ timestep: torch.Tensor,
+ conditional_dict: dict, unconditional_dict: dict,
+ normalization: bool = True
+ ) -> Tuple[torch.Tensor, dict]:
+ """
+ Compute the KL grad (eq 7 in https://arxiv.org/abs/2311.18828).
+ Input:
+ - noisy_image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
+ - estimated_clean_image_or_video: a tensor with shape [B, F, C, H, W] representing the estimated clean image or video.
+ - timestep: a tensor with shape [B, F] containing the randomly generated timestep.
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
+ - normalization: a boolean indicating whether to normalize the gradient.
+ Output:
+ - kl_grad: a tensor representing the KL grad.
+ - kl_log_dict: a dictionary containing the intermediate tensors for logging.
+ """
+ # Step 1: Compute the fake score
+ _, pred_fake_image_cond = self.fake_score(
+ noisy_image_or_video=noisy_image_or_video,
+ conditional_dict=conditional_dict,
+ timestep=timestep
+ )
+
+ if self.fake_guidance_scale != 0.0:
+ _, pred_fake_image_uncond = self.fake_score(
+ noisy_image_or_video=noisy_image_or_video,
+ conditional_dict=unconditional_dict,
+ timestep=timestep
+ )
+ pred_fake_image = pred_fake_image_cond + (
+ pred_fake_image_cond - pred_fake_image_uncond
+ ) * self.fake_guidance_scale
+ else:
+ pred_fake_image = pred_fake_image_cond
+
+ # Step 2: Compute the real score
+ # We compute the conditional and unconditional prediction
+ # and add them together to achieve cfg (https://arxiv.org/abs/2207.12598)
+ _, pred_real_image_cond = self.real_score(
+ noisy_image_or_video=noisy_image_or_video,
+ conditional_dict=conditional_dict,
+ timestep=timestep
+ )
+
+ _, pred_real_image_uncond = self.real_score(
+ noisy_image_or_video=noisy_image_or_video,
+ conditional_dict=unconditional_dict,
+ timestep=timestep
+ )
+
+ pred_real_image = pred_real_image_cond + (
+ pred_real_image_cond - pred_real_image_uncond
+ ) * self.real_guidance_scale
+
+ # Step 3: Compute the DMD gradient (DMD paper eq. 7).
+ grad = (pred_fake_image - pred_real_image)
+
+ # TODO: Change the normalizer for causal teacher
+ if normalization:
+ # Step 4: Gradient normalization (DMD paper eq. 8).
+ p_real = (estimated_clean_image_or_video - pred_real_image)
+ normalizer = torch.abs(p_real).mean(dim=[1, 2, 3, 4], keepdim=True)
+ grad = grad / normalizer
+ grad = torch.nan_to_num(grad)
+
+ return grad, {
+ "dmdtrain_gradient_norm": torch.mean(torch.abs(grad)).detach(),
+ "timestep": timestep.detach()
+ }
+
+ def compute_distribution_matching_loss(
+ self,
+ image_or_video: torch.Tensor,
+ conditional_dict: dict,
+ unconditional_dict: dict,
+ gradient_mask: torch.Tensor = None,
+ ) -> Tuple[torch.Tensor, dict]:
+ """
+ Compute the DMD loss (eq 7 in https://arxiv.org/abs/2311.18828).
+ Input:
+ - image_or_video: a tensor with shape [B, F, C, H, W] where the number of frame is 1 for images.
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
+ - gradient_mask: a boolean tensor with the same shape as image_or_video indicating which pixels to compute loss .
+ Output:
+ - dmd_loss: a scalar tensor representing the DMD loss.
+ - dmd_log_dict: a dictionary containing the intermediate tensors for logging.
+ """
+ original_latent = image_or_video
+
+ batch_size, num_frame = image_or_video.shape[:2]
+
+ with torch.no_grad():
+ # Step 1: Randomly sample timestep based on the given schedule and corresponding noise
+ timestep = self._get_timestep(
+ 0,
+ self.num_train_timestep,
+ batch_size,
+ num_frame,
+ self.num_frame_per_block,
+ uniform_timestep=True
+ )
+
+ if self.timestep_shift > 1:
+ timestep = self.timestep_shift * \
+ (timestep / 1000) / \
+ (1 + (self.timestep_shift - 1) * (timestep / 1000)) * 1000
+ timestep = timestep.clamp(self.min_step, self.max_step)
+
+ noise = torch.randn_like(image_or_video)
+ noisy_latent = self.scheduler.add_noise(
+ image_or_video.flatten(0, 1),
+ noise.flatten(0, 1),
+ timestep.flatten(0, 1)
+ ).detach().unflatten(0, (batch_size, num_frame))
+
+ # Step 2: Compute the KL grad
+ grad, dmd_log_dict = self._compute_kl_grad(
+ noisy_image_or_video=noisy_latent,
+ estimated_clean_image_or_video=original_latent,
+ timestep=timestep,
+ conditional_dict=conditional_dict,
+ unconditional_dict=unconditional_dict
+ )
+
+ if gradient_mask is not None:
+ dmd_loss = 0.5 * F.mse_loss(original_latent.double(
+ )[gradient_mask], (original_latent.double() - grad.double()).detach()[gradient_mask], reduction="mean")
+ else:
+ dmd_loss = 0.5 * F.mse_loss(original_latent.double(
+ ), (original_latent.double() - grad.double()).detach(), reduction="mean")
+ return dmd_loss, dmd_log_dict
+
+ def _run_generator(
+ self,
+ image_or_video_shape,
+ conditional_dict: dict,
+ clean_latent: torch.tensor
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Optionally simulate the generator's input from noise using backward simulation
+ and then run the generator for one-step.
+ Input:
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
+ - initial_latent: a tensor containing the initial latents [B, F, C, H, W].
+ Output:
+ - pred_image: a tensor with shape [B, F, C, H, W].
+ """
+ simulated_noisy_input = []
+ for timestep in self.denoising_step_list:
+ noise = torch.randn(
+ image_or_video_shape, device=self.device, dtype=self.dtype)
+
+ noisy_timestep = timestep * torch.ones(
+ image_or_video_shape[:2], device=self.device, dtype=torch.long)
+
+ if timestep != 0:
+ noisy_image = self.scheduler.add_noise(
+ clean_latent.flatten(0, 1),
+ noise.flatten(0, 1),
+ noisy_timestep.flatten(0, 1)
+ ).unflatten(0, image_or_video_shape[:2])
+ else:
+ noisy_image = clean_latent
+
+ simulated_noisy_input.append(noisy_image)
+
+ simulated_noisy_input = torch.stack(simulated_noisy_input, dim=1)
+
+ # Step 2: Randomly sample a timestep and pick the corresponding input
+ index = self._get_timestep(
+ 0,
+ len(self.denoising_step_list),
+ image_or_video_shape[0],
+ image_or_video_shape[1],
+ self.num_frame_per_block,
+ uniform_timestep=False
+ )
+
+ # select the corresponding timestep's noisy input from the stacked tensor [B, T, F, C, H, W]
+ noisy_input = torch.gather(
+ simulated_noisy_input, dim=1,
+ index=index.reshape(index.shape[0], 1, index.shape[1], 1, 1, 1).expand(
+ -1, -1, -1, *image_or_video_shape[2:]).to(self.device)
+ ).squeeze(1)
+
+ timestep = self.denoising_step_list[index].to(self.device)
+
+ _, pred_image_or_video = self.generator(
+ noisy_image_or_video=noisy_input,
+ conditional_dict=conditional_dict,
+ timestep=timestep,
+ clean_x=clean_latent if self.teacher_forcing else None,
+ )
+
+ gradient_mask = None # timestep != 0
+
+ pred_image_or_video = pred_image_or_video.type_as(noisy_input)
+
+ return pred_image_or_video, gradient_mask
+
+ def generator_loss(
+ self,
+ image_or_video_shape,
+ conditional_dict: dict,
+ unconditional_dict: dict,
+ clean_latent: torch.Tensor,
+ initial_latent: torch.Tensor = None
+ ) -> Tuple[torch.Tensor, dict]:
+ """
+ Generate image/videos from noise and compute the DMD loss.
+ The noisy input to the generator is backward simulated.
+ This removes the need of any datasets during distillation.
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
+ Input:
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
+ Output:
+ - loss: a scalar tensor representing the generator loss.
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
+ """
+ # Step 1: Run generator on backward simulated noisy input
+ pred_image, gradient_mask = self._run_generator(
+ image_or_video_shape=image_or_video_shape,
+ conditional_dict=conditional_dict,
+ clean_latent=clean_latent
+ )
+
+ # Step 2: Compute the DMD loss
+ dmd_loss, dmd_log_dict = self.compute_distribution_matching_loss(
+ image_or_video=pred_image,
+ conditional_dict=conditional_dict,
+ unconditional_dict=unconditional_dict,
+ gradient_mask=gradient_mask
+ )
+
+ # Step 3: TODO: Implement the GAN loss
+
+ return dmd_loss, dmd_log_dict
+
+ def critic_loss(
+ self,
+ image_or_video_shape,
+ conditional_dict: dict,
+ unconditional_dict: dict,
+ clean_latent: torch.Tensor,
+ initial_latent: torch.Tensor = None
+ ) -> Tuple[torch.Tensor, dict]:
+ """
+ Generate image/videos from noise and train the critic with generated samples.
+ The noisy input to the generator is backward simulated.
+ This removes the need of any datasets during distillation.
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
+ Input:
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
+ Output:
+ - loss: a scalar tensor representing the generator loss.
+ - critic_log_dict: a dictionary containing the intermediate tensors for logging.
+ """
+
+ # Step 1: Run generator on backward simulated noisy input
+ with torch.no_grad():
+ generated_image, _ = self._run_generator(
+ image_or_video_shape=image_or_video_shape,
+ conditional_dict=conditional_dict,
+ clean_latent=clean_latent
+ )
+
+ # Step 2: Compute the fake prediction
+ critic_timestep = self._get_timestep(
+ 0,
+ self.num_train_timestep,
+ image_or_video_shape[0],
+ image_or_video_shape[1],
+ self.num_frame_per_block,
+ uniform_timestep=True
+ )
+
+ if self.timestep_shift > 1:
+ critic_timestep = self.timestep_shift * \
+ (critic_timestep / 1000) / (1 + (self.timestep_shift - 1) * (critic_timestep / 1000)) * 1000
+
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
+
+ critic_noise = torch.randn_like(generated_image)
+ noisy_generated_image = self.scheduler.add_noise(
+ generated_image.flatten(0, 1),
+ critic_noise.flatten(0, 1),
+ critic_timestep.flatten(0, 1)
+ ).unflatten(0, image_or_video_shape[:2])
+
+ _, pred_fake_image = self.fake_score(
+ noisy_image_or_video=noisy_generated_image,
+ conditional_dict=conditional_dict,
+ timestep=critic_timestep
+ )
+
+ # Step 3: Compute the denoising loss for the fake critic
+ if self.args.denoising_loss_type == "flow":
+ from utils.wan_wrapper import WanDiffusionWrapper
+ flow_pred = WanDiffusionWrapper._convert_x0_to_flow_pred(
+ scheduler=self.scheduler,
+ x0_pred=pred_fake_image.flatten(0, 1),
+ xt=noisy_generated_image.flatten(0, 1),
+ timestep=critic_timestep.flatten(0, 1)
+ )
+ pred_fake_noise = None
+ else:
+ flow_pred = None
+ pred_fake_noise = self.scheduler.convert_x0_to_noise(
+ x0=pred_fake_image.flatten(0, 1),
+ xt=noisy_generated_image.flatten(0, 1),
+ timestep=critic_timestep.flatten(0, 1)
+ ).unflatten(0, image_or_video_shape[:2])
+
+ denoising_loss = self.denoising_loss_func(
+ x=generated_image.flatten(0, 1),
+ x_pred=pred_fake_image.flatten(0, 1),
+ noise=critic_noise.flatten(0, 1),
+ noise_pred=pred_fake_noise,
+ alphas_cumprod=self.scheduler.alphas_cumprod,
+ timestep=critic_timestep.flatten(0, 1),
+ flow_pred=flow_pred
+ )
+
+ # Step 4: TODO: Compute the GAN loss
+
+ # Step 5: Debugging Log
+ critic_log_dict = {
+ "critic_timestep": critic_timestep.detach()
+ }
+
+ return denoising_loss, critic_log_dict
diff --git a/DecMem/model/diffusion.py b/DecMem/model/diffusion.py
new file mode 100644
index 0000000000000000000000000000000000000000..e1a00fba235e9b2f6e22bcce2fdfe57f0a7f15ff
--- /dev/null
+++ b/DecMem/model/diffusion.py
@@ -0,0 +1,155 @@
+from typing import Tuple
+import torch
+
+from model.base import BaseModel
+from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
+
+
+class CausalDiffusion(BaseModel):
+ def __init__(self, args, device):
+ """
+ Initialize the Diffusion loss module.
+ """
+ super().__init__(args, device)
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
+ if self.num_frame_per_block > 1:
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
+ self.independent_first_frame = getattr(args, "independent_first_frame", False)
+ if self.independent_first_frame:
+ self.generator.model.independent_first_frame = True
+
+ if args.gradient_checkpointing:
+ self.generator.enable_gradient_checkpointing()
+
+ # Step 2: Initialize all hyperparameters
+ self.num_train_timestep = args.num_train_timestep
+ self.min_step = int(0.02 * self.num_train_timestep)
+ self.max_step = int(0.98 * self.num_train_timestep)
+ self.guidance_scale = args.guidance_scale
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
+ self.teacher_forcing = getattr(args, "teacher_forcing", False)
+ # Noise augmentation in teacher forcing, we add small noise to clean context latents
+ self.noise_augmentation_max_timestep = getattr(args, "noise_augmentation_max_timestep", 0)
+
+ def _initialize_models(self, args, device=None):
+ self.generator = WanDiffusionWrapper(**getattr(args, "model_kwargs", {}), is_causal=True)
+ self.generator.model.requires_grad_(True)
+
+ # Text encoder is optional; skip when training without text conditioning (e.g. WorldMem)
+ if getattr(args, "use_text_encoder", True):
+ self.text_encoder = WanTextEncoder()
+ self.text_encoder.requires_grad_(False)
+ else:
+ self.text_encoder = None
+
+ self.vae = WanVAEWrapper()
+ self.vae.requires_grad_(False)
+
+ self.scheduler = self.generator.get_scheduler()
+ if device is not None:
+ self.scheduler.timesteps = self.scheduler.timesteps.to(device)
+
+ # Set up PRoPE for all attention layers when action encoder is enabled
+ model_kwargs = getattr(args, "model_kwargs", {})
+ if model_kwargs.get("use_action_encoder", False):
+ image_or_video_shape = getattr(args, "image_or_video_shape", [1, 21, 16, 60, 104])
+ num_frames = image_or_video_shape[1]
+ latent_h = image_or_video_shape[3]
+ latent_w = image_or_video_shape[4]
+ # patch_size for WAN is (1, 2, 2)
+ patches_y = latent_h // 2
+ patches_x = latent_w // 2
+ self.generator.model.setup_prope(
+ patches_x=patches_x,
+ patches_y=patches_y,
+ image_width=latent_w,
+ image_height=latent_h,
+ cameras=num_frames,
+ prope_max_time=getattr(args, "prope_max_time", 60),
+ rope_scale_pattern=getattr(args, "rope_scale_pattern", "rope"),
+ )
+
+ def generator_loss(
+ self,
+ image_or_video_shape,
+ conditional_dict: dict,
+ unconditional_dict: dict,
+ clean_latent: torch.Tensor,
+ initial_latent: torch.Tensor = None
+ ) -> Tuple[torch.Tensor, dict]:
+ """
+ Generate image/videos from noise and compute the DMD loss.
+ The noisy input to the generator is backward simulated.
+ This removes the need of any datasets during distillation.
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
+ Input:
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
+ Output:
+ - loss: a scalar tensor representing the generator loss.
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
+ """
+ noise = torch.randn_like(clean_latent)
+ batch_size, num_frame = image_or_video_shape[:2]
+
+ # Step 2: Randomly sample a timestep and add noise to denoiser inputs
+ index = self._get_timestep(
+ 0,
+ self.scheduler.num_train_timesteps,
+ image_or_video_shape[0],
+ image_or_video_shape[1],
+ self.num_frame_per_block,
+ uniform_timestep=False
+ )
+ timestep = self.scheduler.timesteps[index].to(dtype=self.dtype, device=self.device)
+ noisy_latents = self.scheduler.add_noise(
+ clean_latent.flatten(0, 1),
+ noise.flatten(0, 1),
+ timestep.flatten(0, 1)
+ ).unflatten(0, (batch_size, num_frame))
+ training_target = self.scheduler.training_target(clean_latent, noise, timestep)
+
+ # Step 3: Noise augmentation, also add small noise to clean context latents
+ if self.noise_augmentation_max_timestep > 0:
+ index_clean_aug = self._get_timestep(
+ 0,
+ self.noise_augmentation_max_timestep,
+ image_or_video_shape[0],
+ image_or_video_shape[1],
+ self.num_frame_per_block,
+ uniform_timestep=False
+ )
+ timestep_clean_aug = self.scheduler.timesteps[index_clean_aug].to(dtype=self.dtype, device=self.device)
+ clean_latent_aug = self.scheduler.add_noise(
+ clean_latent.flatten(0, 1),
+ noise.flatten(0, 1),
+ timestep_clean_aug.flatten(0, 1)
+ ).unflatten(0, (batch_size, num_frame))
+ else:
+ clean_latent_aug = clean_latent
+ timestep_clean_aug = None
+
+ # Compute loss
+ flow_pred, x0_pred = self.generator(
+ noisy_image_or_video=noisy_latents,
+ conditional_dict=conditional_dict,
+ timestep=timestep,
+ clean_x=clean_latent_aug if self.teacher_forcing else None,
+ aug_t=timestep_clean_aug if self.teacher_forcing else None
+ )
+ # loss = torch.nn.functional.mse_loss(flow_pred.float(), training_target.float())
+ loss_unweighted = torch.nn.functional.mse_loss(
+ flow_pred.float(), training_target.float(), reduction='none'
+ ).mean(dim=(2, 3, 4)) # (B, F) unweighted per-frame loss
+ weight = self.scheduler.training_weight(timestep).unflatten(0, (batch_size, num_frame))
+ loss = (loss_unweighted * weight).mean() # scalar; used for backprop
+
+ log_dict = {
+ "x0": clean_latent.detach(),
+ "x0_pred": x0_pred.detach(),
+ "loss_per_frame": loss_unweighted.detach(), # (B, F) unweighted
+ "loss_unweighted": loss_unweighted.detach().mean(), # scalar unweighted
+ }
+ return loss, log_dict
diff --git a/DecMem/model/gan.py b/DecMem/model/gan.py
new file mode 100644
index 0000000000000000000000000000000000000000..92367d1835728133a66e20999786f96d4b640397
--- /dev/null
+++ b/DecMem/model/gan.py
@@ -0,0 +1,295 @@
+import copy
+from pipeline import SelfForcingTrainingPipeline
+import torch.nn.functional as F
+from typing import Tuple
+import torch
+
+from model.base import SelfForcingModel
+
+
+class GAN(SelfForcingModel):
+ def __init__(self, args, device):
+ """
+ Initialize the GAN module.
+ This class is self-contained and compute generator and fake score losses
+ in the forward pass.
+ """
+ super().__init__(args, device)
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
+ self.same_step_across_blocks = getattr(args, "same_step_across_blocks", True)
+ self.concat_time_embeddings = getattr(args, "concat_time_embeddings", False)
+ self.num_class = args.num_class
+ self.relativistic_discriminator = getattr(args, "relativistic_discriminator", False)
+
+ if self.num_frame_per_block > 1:
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
+
+ self.fake_score.adding_cls_branch(
+ atten_dim=1536, num_class=args.num_class, time_embed_dim=1536 if self.concat_time_embeddings else 0)
+ self.fake_score.model.requires_grad_(True)
+
+ self.independent_first_frame = getattr(args, "independent_first_frame", False)
+ if self.independent_first_frame:
+ self.generator.model.independent_first_frame = True
+ if args.gradient_checkpointing:
+ self.generator.enable_gradient_checkpointing()
+ self.fake_score.enable_gradient_checkpointing()
+
+ # this will be init later with fsdp-wrapped modules
+ self.inference_pipeline: SelfForcingTrainingPipeline = None
+
+ # Step 2: Initialize all dmd hyperparameters
+ self.num_train_timestep = args.num_train_timestep
+ self.min_step = int(0.02 * self.num_train_timestep)
+ self.max_step = int(0.98 * self.num_train_timestep)
+ if hasattr(args, "real_guidance_scale"):
+ self.real_guidance_scale = args.real_guidance_scale
+ self.fake_guidance_scale = args.fake_guidance_scale
+ else:
+ self.real_guidance_scale = args.guidance_scale
+ self.fake_guidance_scale = 0.0
+ self.timestep_shift = getattr(args, "timestep_shift", 1.0)
+ self.critic_timestep_shift = getattr(args, "critic_timestep_shift", self.timestep_shift)
+ self.ts_schedule = getattr(args, "ts_schedule", True)
+ self.ts_schedule_max = getattr(args, "ts_schedule_max", False)
+ self.min_score_timestep = getattr(args, "min_score_timestep", 0)
+
+ self.gan_g_weight = getattr(args, "gan_g_weight", 1e-2)
+ self.gan_d_weight = getattr(args, "gan_d_weight", 1e-2)
+ self.r1_weight = getattr(args, "r1_weight", 0.0)
+ self.r2_weight = getattr(args, "r2_weight", 0.0)
+ self.r1_sigma = getattr(args, "r1_sigma", 0.01)
+ self.r2_sigma = getattr(args, "r2_sigma", 0.01)
+
+ if getattr(self.scheduler, "alphas_cumprod", None) is not None:
+ self.scheduler.alphas_cumprod = self.scheduler.alphas_cumprod.to(device)
+ else:
+ self.scheduler.alphas_cumprod = None
+
+ def _run_cls_pred_branch(self,
+ noisy_image_or_video: torch.Tensor,
+ conditional_dict: dict,
+ timestep: torch.Tensor) -> torch.Tensor:
+ """
+ Run the classifier prediction branch on the generated image or video.
+ Input:
+ - image_or_video: a tensor with shape [B, F, C, H, W].
+ Output:
+ - cls_pred: a tensor with shape [B, 1, 1, 1, 1] representing the feature map for classification.
+ """
+ _, _, noisy_logit = self.fake_score(
+ noisy_image_or_video=noisy_image_or_video,
+ conditional_dict=conditional_dict,
+ timestep=timestep,
+ classify_mode=True,
+ concat_time_embeddings=self.concat_time_embeddings
+ )
+
+ return noisy_logit
+
+ def generator_loss(
+ self,
+ image_or_video_shape,
+ conditional_dict: dict,
+ unconditional_dict: dict,
+ clean_latent: torch.Tensor,
+ initial_latent: torch.Tensor = None
+ ) -> Tuple[torch.Tensor, dict]:
+ """
+ Generate image/videos from noise and compute the DMD loss.
+ The noisy input to the generator is backward simulated.
+ This removes the need of any datasets during distillation.
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
+ Input:
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
+ Output:
+ - loss: a scalar tensor representing the generator loss.
+ - generator_log_dict: a dictionary containing the intermediate tensors for logging.
+ """
+ # Step 1: Unroll generator to obtain fake videos
+ pred_image, gradient_mask, denoised_timestep_from, denoised_timestep_to = self._run_generator(
+ image_or_video_shape=image_or_video_shape,
+ conditional_dict=conditional_dict,
+ initial_latent=initial_latent
+ )
+
+ # Step 2: Get timestep and add noise to generated/real latents
+ min_timestep = denoised_timestep_to if self.ts_schedule and denoised_timestep_to is not None else self.min_score_timestep
+ max_timestep = denoised_timestep_from if self.ts_schedule_max and denoised_timestep_from is not None else self.num_train_timestep
+ critic_timestep = self._get_timestep(
+ min_timestep,
+ max_timestep,
+ image_or_video_shape[0],
+ image_or_video_shape[1],
+ self.num_frame_per_block,
+ uniform_timestep=True
+ )
+
+ if self.critic_timestep_shift > 1:
+ critic_timestep = self.critic_timestep_shift * \
+ (critic_timestep / 1000) / (1 + (self.critic_timestep_shift - 1) * (critic_timestep / 1000)) * 1000
+
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
+
+ critic_noise = torch.randn_like(pred_image)
+ noisy_fake_latent = self.scheduler.add_noise(
+ pred_image.flatten(0, 1),
+ critic_noise.flatten(0, 1),
+ critic_timestep.flatten(0, 1)
+ ).unflatten(0, image_or_video_shape[:2])
+
+ # Step 4: Compute the real GAN discriminator loss
+ real_image_or_video = clean_latent.clone()
+ critic_noise = torch.randn_like(real_image_or_video)
+ noisy_real_latent = self.scheduler.add_noise(
+ real_image_or_video.flatten(0, 1),
+ critic_noise.flatten(0, 1),
+ critic_timestep.flatten(0, 1)
+ ).unflatten(0, image_or_video_shape[:2])
+
+ conditional_dict["prompt_embeds"] = torch.concatenate(
+ (conditional_dict["prompt_embeds"], conditional_dict["prompt_embeds"]), dim=0)
+ critic_timestep = torch.concatenate((critic_timestep, critic_timestep), dim=0)
+ noisy_latent = torch.concatenate((noisy_fake_latent, noisy_real_latent), dim=0)
+ _, _, noisy_logit = self.fake_score(
+ noisy_image_or_video=noisy_latent,
+ conditional_dict=conditional_dict,
+ timestep=critic_timestep,
+ classify_mode=True,
+ concat_time_embeddings=self.concat_time_embeddings
+ )
+ noisy_fake_logit, noisy_real_logit = noisy_logit.chunk(2, dim=0)
+
+ if not self.relativistic_discriminator:
+ gan_G_loss = F.softplus(-noisy_fake_logit.float()).mean() * self.gan_g_weight
+ else:
+ relative_fake_logit = noisy_fake_logit - noisy_real_logit
+ gan_G_loss = F.softplus(-relative_fake_logit.float()).mean() * self.gan_g_weight
+
+ return gan_G_loss
+
+ def critic_loss(
+ self,
+ image_or_video_shape,
+ conditional_dict: dict,
+ unconditional_dict: dict,
+ clean_latent: torch.Tensor,
+ real_image_or_video: torch.Tensor,
+ initial_latent: torch.Tensor = None
+ ) -> Tuple[torch.Tensor, dict]:
+ """
+ Generate image/videos from noise and train the critic with generated samples.
+ The noisy input to the generator is backward simulated.
+ This removes the need of any datasets during distillation.
+ See Sec 4.5 of the DMD2 paper (https://arxiv.org/abs/2405.14867) for details.
+ Input:
+ - image_or_video_shape: a list containing the shape of the image or video [B, F, C, H, W].
+ - conditional_dict: a dictionary containing the conditional information (e.g. text embeddings, image embeddings).
+ - unconditional_dict: a dictionary containing the unconditional information (e.g. null/negative text embeddings, null/negative image embeddings).
+ - clean_latent: a tensor containing the clean latents [B, F, C, H, W]. Need to be passed when no backward simulation is used.
+ Output:
+ - loss: a scalar tensor representing the generator loss.
+ - critic_log_dict: a dictionary containing the intermediate tensors for logging.
+ """
+
+ # Step 1: Run generator on backward simulated noisy input
+ with torch.no_grad():
+ generated_image, _, denoised_timestep_from, denoised_timestep_to, num_sim_steps = self._run_generator(
+ image_or_video_shape=image_or_video_shape,
+ conditional_dict=conditional_dict,
+ initial_latent=initial_latent
+ )
+
+ # Step 2: Get timestep and add noise to generated/real latents
+ min_timestep = denoised_timestep_to if self.ts_schedule and denoised_timestep_to is not None else self.min_score_timestep
+ max_timestep = denoised_timestep_from if self.ts_schedule_max and denoised_timestep_from is not None else self.num_train_timestep
+ critic_timestep = self._get_timestep(
+ min_timestep,
+ max_timestep,
+ image_or_video_shape[0],
+ image_or_video_shape[1],
+ self.num_frame_per_block,
+ uniform_timestep=True
+ )
+
+ if self.critic_timestep_shift > 1:
+ critic_timestep = self.critic_timestep_shift * \
+ (critic_timestep / 1000) / (1 + (self.critic_timestep_shift - 1) * (critic_timestep / 1000)) * 1000
+
+ critic_timestep = critic_timestep.clamp(self.min_step, self.max_step)
+
+ critic_noise = torch.randn_like(generated_image)
+ noisy_fake_latent = self.scheduler.add_noise(
+ generated_image.flatten(0, 1),
+ critic_noise.flatten(0, 1),
+ critic_timestep.flatten(0, 1)
+ ).unflatten(0, image_or_video_shape[:2])
+
+ # Step 4: Compute the real GAN discriminator loss
+ noisy_real_latent = self.scheduler.add_noise(
+ real_image_or_video.flatten(0, 1),
+ critic_noise.flatten(0, 1),
+ critic_timestep.flatten(0, 1)
+ ).unflatten(0, image_or_video_shape[:2])
+
+ conditional_dict_cloned = copy.deepcopy(conditional_dict)
+ conditional_dict_cloned["prompt_embeds"] = torch.concatenate(
+ (conditional_dict_cloned["prompt_embeds"], conditional_dict_cloned["prompt_embeds"]), dim=0)
+ _, _, noisy_logit = self.fake_score(
+ noisy_image_or_video=torch.concatenate((noisy_fake_latent, noisy_real_latent), dim=0),
+ conditional_dict=conditional_dict_cloned,
+ timestep=torch.concatenate((critic_timestep, critic_timestep), dim=0),
+ classify_mode=True,
+ concat_time_embeddings=self.concat_time_embeddings
+ )
+ noisy_fake_logit, noisy_real_logit = noisy_logit.chunk(2, dim=0)
+
+ if not self.relativistic_discriminator:
+ gan_D_loss = F.softplus(-noisy_real_logit.float()).mean() + F.softplus(noisy_fake_logit.float()).mean()
+ else:
+ relative_real_logit = noisy_real_logit - noisy_fake_logit
+ gan_D_loss = F.softplus(-relative_real_logit.float()).mean()
+ gan_D_loss = gan_D_loss * self.gan_d_weight
+
+ # R1 regularization
+ if self.r1_weight > 0.:
+ noisy_real_latent_perturbed = noisy_real_latent.clone()
+ epison_real = self.r1_sigma * torch.randn_like(noisy_real_latent_perturbed)
+ noisy_real_latent_perturbed = noisy_real_latent_perturbed + epison_real
+ noisy_real_logit_perturbed = self._run_cls_pred_branch(
+ noisy_image_or_video=noisy_real_latent_perturbed,
+ conditional_dict=conditional_dict,
+ timestep=critic_timestep
+ )
+
+ r1_grad = (noisy_real_logit_perturbed - noisy_real_logit) / self.r1_sigma
+ r1_loss = self.r1_weight * torch.mean((r1_grad)**2)
+ else:
+ r1_loss = torch.zeros_like(gan_D_loss)
+
+ # R2 regularization
+ if self.r2_weight > 0.:
+ noisy_fake_latent_perturbed = noisy_fake_latent.clone()
+ epison_generated = self.r2_sigma * torch.randn_like(noisy_fake_latent_perturbed)
+ noisy_fake_latent_perturbed = noisy_fake_latent_perturbed + epison_generated
+ noisy_fake_logit_perturbed = self._run_cls_pred_branch(
+ noisy_image_or_video=noisy_fake_latent_perturbed,
+ conditional_dict=conditional_dict,
+ timestep=critic_timestep
+ )
+
+ r2_grad = (noisy_fake_logit_perturbed - noisy_fake_logit) / self.r2_sigma
+ r2_loss = self.r2_weight * torch.mean((r2_grad)**2)
+ else:
+ r2_loss = torch.zeros_like(r2_loss)
+
+ critic_log_dict = {
+ "critic_timestep": critic_timestep.detach(),
+ 'noisy_real_logit': noisy_real_logit.detach(),
+ 'noisy_fake_logit': noisy_fake_logit.detach(),
+ }
+
+ return (gan_D_loss, r1_loss, r2_loss), critic_log_dict
diff --git a/DecMem/pipeline/__init__.py b/DecMem/pipeline/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..ad80477f22b133c1bac1d08bf1ec6fa17f36ff62
--- /dev/null
+++ b/DecMem/pipeline/__init__.py
@@ -0,0 +1,17 @@
+from .bidirectional_diffusion_inference import BidirectionalDiffusionInferencePipeline
+from .bidirectional_inference import BidirectionalInferencePipeline
+from .causal_diffusion_inference import (
+ CausalDiffusionInferencePipeline,
+ WorldMemCausalDiffusionInferencePipeline,
+)
+from .causal_inference import CausalInferencePipeline
+from .self_forcing_training import SelfForcingTrainingPipeline
+
+__all__ = [
+ "BidirectionalDiffusionInferencePipeline",
+ "BidirectionalInferencePipeline",
+ "CausalDiffusionInferencePipeline",
+ "CausalInferencePipeline",
+ "SelfForcingTrainingPipeline",
+ "WorldMemCausalDiffusionInferencePipeline",
+]
diff --git a/DecMem/pipeline/bidirectional_diffusion_inference.py b/DecMem/pipeline/bidirectional_diffusion_inference.py
new file mode 100644
index 0000000000000000000000000000000000000000..31cce4d1553d88261442936171df1ac8a0cf4f2c
--- /dev/null
+++ b/DecMem/pipeline/bidirectional_diffusion_inference.py
@@ -0,0 +1,110 @@
+from tqdm import tqdm
+from typing import List
+import torch
+
+from wan.utils.fm_solvers import FlowDPMSolverMultistepScheduler, get_sampling_sigmas, retrieve_timesteps
+from wan.utils.fm_solvers_unipc import FlowUniPCMultistepScheduler
+from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
+
+
+class BidirectionalDiffusionInferencePipeline(torch.nn.Module):
+ def __init__(
+ self,
+ args,
+ device,
+ generator=None,
+ text_encoder=None,
+ vae=None
+ ):
+ super().__init__()
+ # Step 1: Initialize all models
+ self.generator = WanDiffusionWrapper(
+ **getattr(args, "model_kwargs", {}), is_causal=False) if generator is None else generator
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
+ self.vae = WanVAEWrapper() if vae is None else vae
+
+ # Step 2: Initialize scheduler
+ self.num_train_timesteps = args.num_train_timestep
+ self.sampling_steps = 50
+ self.sample_solver = 'unipc'
+ self.shift = 8.0
+
+ self.args = args
+
+ def inference(
+ self,
+ noise: torch.Tensor,
+ text_prompts: List[str],
+ return_latents=False
+ ) -> torch.Tensor:
+ """
+ Perform inference on the given noise and text prompts.
+ Inputs:
+ noise (torch.Tensor): The input noise tensor of shape
+ (batch_size, num_frames, num_channels, height, width).
+ text_prompts (List[str]): The list of text prompts.
+ Outputs:
+ video (torch.Tensor): The generated video tensor of shape
+ (batch_size, num_frames, num_channels, height, width). It is normalized to be in the range [0, 1].
+ """
+
+ conditional_dict = self.text_encoder(
+ text_prompts=text_prompts
+ )
+ unconditional_dict = self.text_encoder(
+ text_prompts=[self.args.negative_prompt] * len(text_prompts)
+ )
+
+ latents = noise
+
+ sample_scheduler = self._initialize_sample_scheduler(noise)
+ for _, t in enumerate(tqdm(sample_scheduler.timesteps)):
+ latent_model_input = latents
+ timestep = t * torch.ones([latents.shape[0], 21], device=noise.device, dtype=torch.float32)
+
+ flow_pred_cond, _ = self.generator(latent_model_input, conditional_dict, timestep)
+ flow_pred_uncond, _ = self.generator(latent_model_input, unconditional_dict, timestep)
+
+ flow_pred = flow_pred_uncond + self.args.guidance_scale * (
+ flow_pred_cond - flow_pred_uncond)
+
+ temp_x0 = sample_scheduler.step(
+ flow_pred.unsqueeze(0),
+ t,
+ latents.unsqueeze(0),
+ return_dict=False)[0]
+ latents = temp_x0.squeeze(0)
+
+ x0 = latents
+ video = self.vae.decode_to_pixel(x0)
+ video = (video * 0.5 + 0.5).clamp(0, 1)
+
+ del sample_scheduler
+
+ if return_latents:
+ return video, latents
+ else:
+ return video
+
+ def _initialize_sample_scheduler(self, noise):
+ if self.sample_solver == 'unipc':
+ sample_scheduler = FlowUniPCMultistepScheduler(
+ num_train_timesteps=self.num_train_timesteps,
+ shift=1,
+ use_dynamic_shifting=False)
+ sample_scheduler.set_timesteps(
+ self.sampling_steps, device=noise.device, shift=self.shift)
+ self.timesteps = sample_scheduler.timesteps
+ elif self.sample_solver == 'dpm++':
+ sample_scheduler = FlowDPMSolverMultistepScheduler(
+ num_train_timesteps=self.num_train_timesteps,
+ shift=1,
+ use_dynamic_shifting=False)
+ sampling_sigmas = get_sampling_sigmas(self.sampling_steps, self.shift)
+ self.timesteps, _ = retrieve_timesteps(
+ sample_scheduler,
+ device=noise.device,
+ sigmas=sampling_sigmas)
+ else:
+ raise NotImplementedError("Unsupported solver.")
+ return sample_scheduler
diff --git a/DecMem/pipeline/bidirectional_inference.py b/DecMem/pipeline/bidirectional_inference.py
new file mode 100644
index 0000000000000000000000000000000000000000..b523ec0d0ac0082daf310b159aed6f286e0b0efb
--- /dev/null
+++ b/DecMem/pipeline/bidirectional_inference.py
@@ -0,0 +1,71 @@
+from typing import List
+import torch
+
+from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
+
+
+class BidirectionalInferencePipeline(torch.nn.Module):
+ def __init__(
+ self,
+ args,
+ device,
+ generator=None,
+ text_encoder=None,
+ vae=None
+ ):
+ super().__init__()
+ # Step 1: Initialize all models
+ self.generator = WanDiffusionWrapper(
+ **getattr(args, "model_kwargs", {}), is_causal=False) if generator is None else generator
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
+ self.vae = WanVAEWrapper() if vae is None else vae
+
+ # Step 2: Initialize all bidirectional wan hyperparmeters
+ self.scheduler = self.generator.get_scheduler()
+ self.denoising_step_list = torch.tensor(
+ args.denoising_step_list, dtype=torch.long, device=device)
+ if self.denoising_step_list[-1] == 0:
+ self.denoising_step_list = self.denoising_step_list[:-1] # remove the zero timestep for inference
+ if args.warp_denoising_step:
+ timesteps = torch.cat((self.scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32)))
+ self.denoising_step_list = timesteps[1000 - self.denoising_step_list]
+
+ def inference(self, noise: torch.Tensor, text_prompts: List[str]) -> torch.Tensor:
+ """
+ Perform inference on the given noise and text prompts.
+ Inputs:
+ noise (torch.Tensor): The input noise tensor of shape
+ (batch_size, num_frames, num_channels, height, width).
+ text_prompts (List[str]): The list of text prompts.
+ Outputs:
+ video (torch.Tensor): The generated video tensor of shape
+ (batch_size, num_frames, num_channels, height, width). It is normalized to be in the range [0, 1].
+ """
+ conditional_dict = self.text_encoder(
+ text_prompts=text_prompts
+ )
+
+ # initial point
+ noisy_image_or_video = noise
+
+ # use the last n-1 timesteps to simulate the generator's input
+ for index, current_timestep in enumerate(self.denoising_step_list[:-1]):
+ _, pred_image_or_video = self.generator(
+ noisy_image_or_video=noisy_image_or_video,
+ conditional_dict=conditional_dict,
+ timestep=torch.ones(
+ noise.shape[:2], dtype=torch.long, device=noise.device) * current_timestep
+ ) # [B, F, C, H, W]
+
+ next_timestep = self.denoising_step_list[index + 1] * torch.ones(
+ noise.shape[:2], dtype=torch.long, device=noise.device)
+
+ noisy_image_or_video = self.scheduler.add_noise(
+ pred_image_or_video.flatten(0, 1),
+ torch.randn_like(pred_image_or_video.flatten(0, 1)),
+ next_timestep.flatten(0, 1)
+ ).unflatten(0, noise.shape[:2])
+
+ video = self.vae.decode_to_pixel(pred_image_or_video)
+ video = (video * 0.5 + 0.5).clamp(0, 1)
+ return video
diff --git a/DecMem/pipeline/causal_diffusion_inference.py b/DecMem/pipeline/causal_diffusion_inference.py
new file mode 100644
index 0000000000000000000000000000000000000000..15d56ae2d8beabf75dd1605da6eb40d02e3b88b6
--- /dev/null
+++ b/DecMem/pipeline/causal_diffusion_inference.py
@@ -0,0 +1,707 @@
+from tqdm import tqdm
+from typing import List, Optional
+import torch
+
+from wan.utils.fm_solvers import FlowDPMSolverMultistepScheduler, get_sampling_sigmas, retrieve_timesteps
+from wan.utils.fm_solvers_unipc import FlowUniPCMultistepScheduler
+from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
+class WorldMemCausalDiffusionInferencePipeline(torch.nn.Module):
+ """Block-by-block causal inference for WorldMem (action + PRoPE, no text).
+
+ Key differences vs. CausalDiffusionInferencePipeline:
+ * No text encoder; conditioning comes from per-frame actions + viewmats.
+ * CFG is batched into a single KV cache with batch_size = 2 * B; cond
+ and uncond are stacked along dim 0 and chunked back after each forward.
+ * PRoPE is precomputed PER BLOCK (with the block's viewmats slice and
+ absolute `time_ids=blk_start`). The cache stores PRoPE-rotated K/V,
+ so subsequent blocks attend against features in their correct
+ rotated frames (relative-position attention falls out naturally).
+
+ KV-cache schedule for one inference call:
+ 1. PREFIX: forward each fully-clean block at timestep=0 to write its
+ PRoPE-rotated K/V to the cache.
+ 2. DENOISE: for each remaining block, run `sampling_steps` of UniPC,
+ each step a forward with kv_cache + PRoPE for that block.
+ Frames already pinned clean (partial-prefix case) keep
+ timestep=0 within the block.
+ 3. COMMIT: after each block converges, re-forward at timestep=0 with
+ the now-clean latents to overwrite that block's cache slot.
+ """
+
+ def __init__(self, args, device, generator=None, vae=None):
+ super().__init__()
+ self.args = args
+ self.device = device
+
+ self.generator = WanDiffusionWrapper(
+ **getattr(args, "model_kwargs", {}), is_causal=True
+ ) if generator is None else generator
+ self.vae = WanVAEWrapper() if vae is None else vae
+
+ self.num_train_timesteps = args.num_train_timestep
+ self.sampling_steps = 50
+ self.sample_solver = 'unipc'
+ self.shift = args.timestep_shift
+
+ self.num_transformer_blocks = len(self.generator.model.blocks)
+ # frame_seq_length = patches_y * patches_x for WAN patch_size=(1,2,2)
+ latent_h = args.image_or_video_shape[3]
+ latent_w = args.image_or_video_shape[4]
+ self.frame_seq_length = (latent_h // 2) * (latent_w // 2)
+
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
+ if self.num_frame_per_block > 1:
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
+
+ self.local_attn_size = self.generator.model.local_attn_size
+
+ # Setup PRoPE (mirrors CausalDiffusion._initialize_models).
+ model_kwargs = getattr(args, "model_kwargs", {})
+ if model_kwargs.get("use_action_encoder", False):
+ num_frames = args.image_or_video_shape[1]
+ patches_y = latent_h // 2
+ patches_x = latent_w // 2
+ self.generator.model.setup_prope(
+ patches_x=patches_x,
+ patches_y=patches_y,
+ image_width=latent_w,
+ image_height=latent_h,
+ cameras=num_frames,
+ prope_max_time=getattr(args, "prope_max_time", 60),
+ rope_scale_pattern=getattr(args, "rope_scale_pattern", "rope"),
+ )
+
+ self.kv_cache = None
+ self.crossattn_cache = None
+
+ # ─── Cache lifecycle ─────────────────────────────────────────────────────
+ def _initialize_kv_cache(self, batch_size, dtype, device, num_total_frames):
+ self.kv_cache = None
+ torch.cuda.empty_cache()
+ kv_cache = []
+ if self.local_attn_size != -1:
+ kv_cache_size = self.local_attn_size * self.frame_seq_length
+ else:
+ kv_cache_size = num_total_frames * self.frame_seq_length
+
+ # Extend max_attention_size so attention reads the full cache range.
+ if self.local_attn_size == -1:
+ for module in self.generator.model.modules():
+ if hasattr(module, 'max_attention_size'):
+ module.max_attention_size = kv_cache_size
+
+ head_dim = self.generator.model.dim // self.generator.model.num_heads
+ num_heads = self.generator.model.num_heads
+ use_ltm = getattr(self.generator.model, "use_ltm_attn", False)
+ for _ in range(self.num_transformer_blocks):
+ entry = {
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device),
+ }
+ if use_ltm:
+ # LTM: untiled k/v + incremental tiled/compressed KV (per block).
+ # entry["num_frames"] = 0
+ entry["k"] = None
+ entry["v"] = None
+ entry["k_tiled"] = None
+ entry["v_tiled"] = None
+ entry["k_compress"] = None
+ entry["v_compress"] = None
+ entry["variable_block_sizes"] = None
+ else:
+ entry["k"] = torch.zeros(
+ [batch_size, kv_cache_size, num_heads, head_dim], dtype=dtype, device=device)
+ entry["v"] = torch.zeros(
+ [batch_size, kv_cache_size, num_heads, head_dim], dtype=dtype, device=device)
+ kv_cache.append(entry)
+ self.kv_cache = kv_cache
+
+ def _initialize_crossattn_cache(self, batch_size, dtype, device):
+ # Cross-attention is disabled for WorldMem (no text); allocate stubs
+ # so the model's block forward can still index `crossattn_cache[i]`
+ # without surprises.
+ crossattn_cache = []
+ head_dim = self.generator.model.dim // self.generator.model.num_heads
+ num_heads = self.generator.model.num_heads
+ for _ in range(self.num_transformer_blocks):
+ crossattn_cache.append({
+ "k": torch.zeros([batch_size, 512, num_heads, head_dim],
+ dtype=dtype, device=device),
+ "v": torch.zeros([batch_size, 512, num_heads, head_dim],
+ dtype=dtype, device=device),
+ "is_init": False,
+ })
+ self.crossattn_cache = crossattn_cache
+
+ def _init_scheduler(self, device):
+ if self.sample_solver == 'unipc':
+ scheduler = FlowUniPCMultistepScheduler(
+ num_train_timesteps=self.num_train_timesteps,
+ shift=1, use_dynamic_shifting=False)
+ scheduler.set_timesteps(self.sampling_steps, device=device, shift=self.shift)
+ elif self.sample_solver == 'dpm++':
+ scheduler = FlowDPMSolverMultistepScheduler(
+ num_train_timesteps=self.num_train_timesteps,
+ shift=1, use_dynamic_shifting=False)
+ sigmas = get_sampling_sigmas(self.sampling_steps, self.shift)
+ _, _ = retrieve_timesteps(scheduler, device=device, sigmas=sigmas)
+ else:
+ raise NotImplementedError(self.sample_solver)
+ return scheduler
+
+ # ─── Inference ───────────────────────────────────────────────────────────
+ def _precompute_prope_for_block(self, viewmats_in, blk_start_frame, num_frame_per_block):
+ """Call PRoPE precompute once for this block's viewmats slice.
+
+ Must be invoked before any `_forward_block(..., blk_start_frame)` call
+ that targets the same block. Reused across all denoising steps within
+ the block (viewmats don't change per step) and the clean-commit pass.
+ """
+ viewmats_blk = viewmats_in[:, blk_start_frame:blk_start_frame + num_frame_per_block]
+ self.generator.model.precompute_prope(
+ viewmats_blk,
+ time_ids=blk_start_frame,
+ pre_rope=True,
+ )
+ return viewmats_blk
+
+ def _forward_block(self, blk_latents, blk_timestep, viewmats_blk,
+ full_action_emb, blk_start_frame, kv_batch, use_cfg,
+ write_cache=True):
+ """Run a single forward for one block, assuming PRoPE has already been
+ precomputed for this block (via `_precompute_prope_for_block`).
+
+ blk_latents: (kv_batch, num_frame_per_block, C, H, W)
+ blk_timestep: (kv_batch, num_frame_per_block)
+ viewmats_blk: (kv_batch, num_frame_per_block, 4, 4)
+ full_action_emb: (kv_batch, T_total, C, 1, 1) or None
+ write_cache: if False, k/v are read-only (denoising steps).
+ True for prefix / clean commit forwards (t=0).
+ """
+ num_frame_per_block = blk_latents.shape[1]
+ for cache in self.kv_cache:
+ cache["write_cache"] = write_cache
+ # Build a per-block conditional_dict: action_emb slice + block viewmats.
+ # (We omit "actions" so WanDiffusionWrapper takes the "action_emb" path.)
+ conditional_dict = {"viewmats": viewmats_blk}
+ if full_action_emb is not None:
+ conditional_dict["action_emb"] = full_action_emb[
+ :, blk_start_frame:blk_start_frame + num_frame_per_block]
+
+ flow_both, _ = self.generator(
+ noisy_image_or_video=blk_latents,
+ conditional_dict=conditional_dict,
+ timestep=blk_timestep,
+ kv_cache=self.kv_cache,
+ crossattn_cache=self.crossattn_cache,
+ current_start=blk_start_frame * self.frame_seq_length,
+ cache_start=blk_start_frame * self.frame_seq_length,
+ )
+ return flow_both
+
+ def inference(
+ self,
+ noise: torch.Tensor, # (B, T, C, H, W) pure noise
+ actions: torch.Tensor, # (B, T_vid, action_dim)
+ viewmats: torch.Tensor, # (B, T, 4, 4)
+ initial_latent: torch.Tensor, # (B, T, C, H, W) — clean latent for frames [:n_cond]
+ num_condition_frames: int = 1,
+ guidance_scale: float = 3.0,
+ sampling_steps: int = 50,
+ return_latents: bool = True,
+ skip_vae_decode: bool = False,
+ ):
+ """Block-by-block causal inference for WorldMem."""
+ if sampling_steps is not None:
+ self.sampling_steps = sampling_steps
+
+ B, T, C, H, W = noise.shape
+ device = noise.device
+ dtype = noise.dtype
+ num_frame_per_block = self.num_frame_per_block
+ n_cond = int(num_condition_frames)
+ assert 1 <= n_cond < T
+ use_cfg = guidance_scale > 1.0
+ kv_batch = 2 * B if use_cfg else B
+
+ # ─── Build cond / uncond (batched along dim 0) ───────────────────────
+ if use_cfg:
+ actions_in = torch.cat([actions, torch.zeros_like(actions)], dim=0)
+ viewmats_in = torch.cat([viewmats, viewmats], dim=0)
+ else:
+ actions_in = actions
+ viewmats_in = viewmats
+
+ # Pre-encode the full action embedding once (B, T, C, 1, 1). The block
+ # forwards pass a per-block slice via conditional_dict["action_emb"],
+ # bypassing the action encoder's "first-frame is special" packing rule
+ # which only holds when the slice starts at frame 0.
+ if getattr(self.generator, "use_action_encoder", False):
+ with torch.no_grad():
+ full_action_emb = self.generator.action_encoder(
+ actions_in.to(dtype)) # (kv_batch, T, C, 1, 1)
+ else:
+ full_action_emb = None
+
+ # ─── Allocate KV/crossattn caches sized for kv_batch ─────────────────
+ self._initialize_kv_cache(kv_batch, dtype=dtype, device=device, num_total_frames=T)
+ # self._initialize_crossattn_cache(kv_batch, dtype=dtype, device=device)
+
+ # ─── Output buffer (B-batch, not kv_batch) ───────────────────────────
+ output = torch.zeros_like(noise)
+ output[:, :n_cond] = initial_latent[:, :n_cond]
+
+ # ─── Block layout ────────────────────────────────────────────────────
+ # Fully-clean blocks at the front are committed once at t=0. The first
+ # block that contains any noisy frames enters the denoise loop; if
+ # n_cond is not block-aligned, that block's leading `partial_clean`
+ # frames stay pinned to t=0 while the rest are denoised.
+ cleaned_blocks = n_cond // num_frame_per_block
+ partial_clean = n_cond % num_frame_per_block
+ num_total_blocks = T // num_frame_per_block
+ assert T % num_frame_per_block == 0, f"T={T} must be a multiple of num_frame_per_block={num_frame_per_block}"
+
+ # Step 1 — commit fully-clean prefix blocks at t=0.
+ # PRoPE precomputed once per block (viewmats are block-local).
+ for blk_idx in range(cleaned_blocks):
+ st = blk_idx * num_frame_per_block
+ viewmats_blk = self._precompute_prope_for_block(
+ viewmats_in, st, num_frame_per_block)
+ blk_lat = initial_latent[:, st:st + num_frame_per_block]
+ if use_cfg:
+ blk_lat = torch.cat([blk_lat, blk_lat], dim=0)
+ t0 = torch.zeros(kv_batch, num_frame_per_block, dtype=torch.float32, device=device)
+ self._forward_block(blk_lat, t0, viewmats_blk,
+ full_action_emb, st, kv_batch, use_cfg)
+
+ # Step 2 — denoise remaining blocks one at a time, then commit each.
+ # PRoPE is precomputed ONCE per block at the top, reused across all
+ # `sampling_steps` denoising forwards AND the clean-commit pass.
+ first_noisy_block = cleaned_blocks
+ for blk_idx in tqdm(range(first_noisy_block, num_total_blocks), desc="blocks"):
+ st = blk_idx * num_frame_per_block
+ ed = st + num_frame_per_block
+
+ viewmats_blk = self._precompute_prope_for_block(
+ viewmats_in, st, num_frame_per_block)
+
+ # Initialise this block's latents.
+ blk_lat = noise[:, st:ed].clone()
+ local_partial_clean = partial_clean if blk_idx == cleaned_blocks else 0
+ if local_partial_clean > 0:
+ blk_lat[:, :local_partial_clean] = initial_latent[:, st:st + local_partial_clean]
+
+ sched = self._init_scheduler(device)
+ for step_idx, t in enumerate(sched.timesteps):
+ # Per-frame timestep within this block.
+ ts = torch.full((B, num_frame_per_block), float(t.item()),
+ dtype=torch.float32, device=device)
+ if local_partial_clean > 0:
+ ts[:, :local_partial_clean] = 0.0
+
+ if use_cfg:
+ blk_lat_in = torch.cat([blk_lat, blk_lat], dim=0)
+ ts_in = torch.cat([ts, ts], dim=0)
+ else:
+ blk_lat_in = blk_lat
+ ts_in = ts
+
+ flow_both = self._forward_block(
+ blk_lat_in, ts_in, viewmats_blk,
+ full_action_emb, st, kv_batch, use_cfg,
+ write_cache=False)
+
+ if use_cfg:
+ flow_cond, flow_uncond = flow_both.chunk(2, dim=0)
+ flow_pred = flow_uncond + guidance_scale * (flow_cond - flow_uncond)
+ else:
+ flow_pred = flow_both
+
+ # Scheduler step on the noisy portion only.
+ noisy_flow = flow_pred[:, local_partial_clean:]
+ noisy_lat = blk_lat[:, local_partial_clean:]
+ new_noisy = sched.step(noisy_flow, t, noisy_lat, return_dict=False)[0]
+ blk_lat = blk_lat.clone()
+ blk_lat[:, local_partial_clean:] = new_noisy
+
+ output[:, st:ed] = blk_lat
+
+ # Commit clean K/V for this block to cache (overwrites the last
+ # denoise step's writes). Reuses the PRoPE precompute from above.
+ blk_lat_clean = blk_lat
+ if use_cfg:
+ blk_lat_clean = torch.cat([blk_lat_clean, blk_lat_clean], dim=0)
+ t0 = torch.zeros(kv_batch, num_frame_per_block, dtype=torch.float32, device=device)
+ self._forward_block(blk_lat_clean, t0, viewmats_blk,
+ full_action_emb, st, kv_batch, use_cfg)
+
+ # ─── Decode ──────────────────────────────────────────────────────────
+ # When `skip_vae_decode=True`, return only the latent so the caller can
+ # control where/when the VAE runs (used by the GPU/CPU offload path
+ # that swaps generator and VAE in/out of GPU memory).
+ if skip_vae_decode:
+ if return_latents:
+ return None, output
+ return None
+
+ video = self.vae.decode_to_pixel(output.to(dtype=dtype))
+ video = (video * 0.5 + 0.5).clamp(0, 1)
+ if return_latents:
+ return video, output
+ return video
+
+
+class CausalDiffusionInferencePipeline(torch.nn.Module):
+ def __init__(
+ self,
+ args,
+ device,
+ generator=None,
+ text_encoder=None,
+ vae=None
+ ):
+ super().__init__()
+ # Step 1: Initialize all models
+ self.generator = WanDiffusionWrapper(
+ **getattr(args, "model_kwargs", {}), is_causal=True) if generator is None else generator
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
+ self.vae = WanVAEWrapper() if vae is None else vae
+
+ # Step 2: Initialize scheduler
+ self.num_train_timesteps = args.num_train_timestep
+ self.sampling_steps = 50
+ self.sample_solver = 'unipc'
+ self.shift = args.timestep_shift
+
+ self.num_transformer_blocks = 30
+ self.frame_seq_length = 1560
+
+ self.kv_cache_pos = None
+ self.kv_cache_neg = None
+ self.crossattn_cache_pos = None
+ self.crossattn_cache_neg = None
+ self.args = args
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
+ self.independent_first_frame = args.independent_first_frame
+ self.local_attn_size = self.generator.model.local_attn_size
+
+ print(f"KV inference with {self.num_frame_per_block} frames per block")
+
+ if self.num_frame_per_block > 1:
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
+
+ def inference(
+ self,
+ noise: torch.Tensor,
+ text_prompts: List[str],
+ initial_latent: Optional[torch.Tensor] = None,
+ return_latents: bool = False,
+ start_frame_index: Optional[int] = 0,
+ ) -> torch.Tensor:
+ """
+ Perform inference on the given noise and text prompts.
+ Inputs:
+ noise (torch.Tensor): The input noise tensor of shape
+ (batch_size, num_output_frames, num_channels, height, width).
+ text_prompts (List[str]): The list of text prompts.
+ initial_latent (torch.Tensor): The initial latent tensor of shape
+ (batch_size, num_input_frames, num_channels, height, width).
+ If num_input_frames is 1, perform image to video.
+ If num_input_frames is greater than 1, perform video extension.
+ return_latents (bool): Whether to return the latents.
+ start_frame_index (int): In long video generation, where does the current window start?
+ Outputs:
+ video (torch.Tensor): The generated video tensor of shape
+ (batch_size, num_frames, num_channels, height, width). It is normalized to be in the range [0, 1].
+ """
+ batch_size, num_frames, num_channels, height, width = noise.shape
+ if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
+ # noise should still be a multiple of num_frame_per_block
+ assert num_frames % self.num_frame_per_block == 0
+ num_blocks = num_frames // self.num_frame_per_block
+ elif self.independent_first_frame and initial_latent is None:
+ # Using a [1, 4, 4, 4, 4, 4] model to generate a video without image conditioning
+ assert (num_frames - 1) % self.num_frame_per_block == 0
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
+ conditional_dict = self.text_encoder(
+ text_prompts=text_prompts
+ )
+ unconditional_dict = self.text_encoder(
+ text_prompts=[self.args.negative_prompt] * len(text_prompts)
+ )
+
+ output = torch.zeros(
+ [batch_size, num_output_frames, num_channels, height, width],
+ device=noise.device,
+ dtype=noise.dtype
+ )
+
+ # Step 1: Initialize KV cache to all zeros
+ required_cache_size = (
+ num_output_frames * self.frame_seq_length
+ if self.local_attn_size == -1
+ else self.local_attn_size * self.frame_seq_length
+ )
+ need_reinit = (
+ self.kv_cache_pos is None
+ or self.kv_cache_pos[0]["k"].shape[1] < required_cache_size
+ )
+ if need_reinit:
+ self._initialize_kv_cache(
+ batch_size=batch_size,
+ dtype=noise.dtype,
+ device=noise.device,
+ num_total_frames=num_output_frames
+ )
+ self._initialize_crossattn_cache(
+ batch_size=batch_size,
+ dtype=noise.dtype,
+ device=noise.device
+ )
+ else:
+ # reset cross attn cache
+ for block_index in range(self.num_transformer_blocks):
+ self.crossattn_cache_pos[block_index]["is_init"] = False
+ self.crossattn_cache_neg[block_index]["is_init"] = False
+ # reset kv cache
+ for block_index in range(len(self.kv_cache_pos)):
+ self.kv_cache_pos[block_index]["global_end_index"] = torch.tensor(
+ [0], dtype=torch.long, device=noise.device)
+ self.kv_cache_pos[block_index]["local_end_index"] = torch.tensor(
+ [0], dtype=torch.long, device=noise.device)
+ self.kv_cache_neg[block_index]["global_end_index"] = torch.tensor(
+ [0], dtype=torch.long, device=noise.device)
+ self.kv_cache_neg[block_index]["local_end_index"] = torch.tensor(
+ [0], dtype=torch.long, device=noise.device)
+
+ # Step 2: Cache context feature
+ current_start_frame = start_frame_index
+ cache_start_frame = 0
+ if initial_latent is not None:
+ timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
+ if self.independent_first_frame:
+ # Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
+ assert (num_input_frames - 1) % self.num_frame_per_block == 0
+ num_input_blocks = (num_input_frames - 1) // self.num_frame_per_block
+ output[:, :1] = initial_latent[:, :1]
+ self.generator(
+ noisy_image_or_video=initial_latent[:, :1],
+ conditional_dict=conditional_dict,
+ timestep=timestep * 0,
+ kv_cache=self.kv_cache_pos,
+ crossattn_cache=self.crossattn_cache_pos,
+ current_start=current_start_frame * self.frame_seq_length,
+ cache_start=cache_start_frame * self.frame_seq_length
+ )
+ self.generator(
+ noisy_image_or_video=initial_latent[:, :1],
+ conditional_dict=unconditional_dict,
+ timestep=timestep * 0,
+ kv_cache=self.kv_cache_neg,
+ crossattn_cache=self.crossattn_cache_neg,
+ current_start=current_start_frame * self.frame_seq_length,
+ cache_start=cache_start_frame * self.frame_seq_length
+ )
+ current_start_frame += 1
+ cache_start_frame += 1
+ else:
+ # Assume num_input_frames is self.num_frame_per_block * num_input_blocks
+ assert num_input_frames % self.num_frame_per_block == 0
+ num_input_blocks = num_input_frames // self.num_frame_per_block
+
+ for block_index in range(num_input_blocks):
+ current_ref_latents = \
+ initial_latent[:, cache_start_frame:cache_start_frame + self.num_frame_per_block]
+ output[:, cache_start_frame:cache_start_frame + self.num_frame_per_block] = current_ref_latents
+ self.generator(
+ noisy_image_or_video=current_ref_latents,
+ conditional_dict=conditional_dict,
+ timestep=timestep * 0,
+ kv_cache=self.kv_cache_pos,
+ crossattn_cache=self.crossattn_cache_pos,
+ current_start=current_start_frame * self.frame_seq_length,
+ cache_start=cache_start_frame * self.frame_seq_length
+ )
+ self.generator(
+ noisy_image_or_video=current_ref_latents,
+ conditional_dict=unconditional_dict,
+ timestep=timestep * 0,
+ kv_cache=self.kv_cache_neg,
+ crossattn_cache=self.crossattn_cache_neg,
+ current_start=current_start_frame * self.frame_seq_length,
+ cache_start=cache_start_frame * self.frame_seq_length
+ )
+ current_start_frame += self.num_frame_per_block
+ cache_start_frame += self.num_frame_per_block
+
+ # Step 3: Temporal denoising loop
+ all_num_frames = [self.num_frame_per_block] * num_blocks
+ if self.independent_first_frame and initial_latent is None:
+ all_num_frames = [1] + all_num_frames
+
+ for current_num_frames in all_num_frames:
+ noisy_input = noise[
+ :, cache_start_frame - num_input_frames:cache_start_frame + current_num_frames - num_input_frames]
+ latents = noisy_input
+
+ # Step 3.1: Spatial denoising loop
+ sample_scheduler = self._initialize_sample_scheduler(noise)
+ for step_idx, t in enumerate(tqdm(sample_scheduler.timesteps)):
+ latent_model_input = latents
+ timestep = t * torch.ones(
+ [batch_size, current_num_frames], device=noise.device, dtype=torch.float32
+ )
+
+ flow_pred_cond, _ = self.generator(
+ noisy_image_or_video=latent_model_input,
+ conditional_dict=conditional_dict,
+ timestep=timestep,
+ kv_cache=self.kv_cache_pos,
+ crossattn_cache=self.crossattn_cache_pos,
+ current_start=current_start_frame * self.frame_seq_length,
+ cache_start=cache_start_frame * self.frame_seq_length
+ )
+
+ flow_pred_uncond, _ = self.generator(
+ noisy_image_or_video=latent_model_input,
+ conditional_dict=unconditional_dict,
+ timestep=timestep,
+ kv_cache=self.kv_cache_neg,
+ crossattn_cache=self.crossattn_cache_neg,
+ current_start=current_start_frame * self.frame_seq_length,
+ cache_start=cache_start_frame * self.frame_seq_length
+ )
+
+ flow_pred = flow_pred_uncond + self.args.guidance_scale * (
+ flow_pred_cond - flow_pred_uncond)
+
+ temp_x0 = sample_scheduler.step(
+ flow_pred,
+ t,
+ latents,
+ return_dict=False)[0]
+ latents = temp_x0
+ print(f"kv_cache['local_end_index']: {self.kv_cache_pos[0]['local_end_index']}")
+ print(f"kv_cache['global_end_index']: {self.kv_cache_pos[0]['global_end_index']}")
+
+ # Step 3.2: record the model's output
+ output[:, cache_start_frame:cache_start_frame + current_num_frames] = latents
+
+ # Step 3.3: rerun with timestep zero to update KV cache using clean context
+ self.generator(
+ noisy_image_or_video=latents,
+ conditional_dict=conditional_dict,
+ timestep=timestep * 0,
+ kv_cache=self.kv_cache_pos,
+ crossattn_cache=self.crossattn_cache_pos,
+ current_start=current_start_frame * self.frame_seq_length,
+ cache_start=cache_start_frame * self.frame_seq_length
+ )
+ self.generator(
+ noisy_image_or_video=latents,
+ conditional_dict=unconditional_dict,
+ timestep=timestep * 0,
+ kv_cache=self.kv_cache_neg,
+ crossattn_cache=self.crossattn_cache_neg,
+ current_start=current_start_frame * self.frame_seq_length,
+ cache_start=cache_start_frame * self.frame_seq_length
+ )
+
+ # Step 3.4: update the start and end frame indices
+ current_start_frame += current_num_frames
+ cache_start_frame += current_num_frames
+
+ # Step 4: Decode the output
+ video = self.vae.decode_to_pixel(output)
+ video = (video * 0.5 + 0.5).clamp(0, 1)
+
+ if return_latents:
+ return video, output
+ else:
+ return video
+
+ def _initialize_kv_cache(self, batch_size, dtype, device, num_total_frames=None):
+ """
+ Initialize a Per-GPU KV cache for the Wan model.
+ """
+ kv_cache_pos = []
+ kv_cache_neg = []
+ if self.local_attn_size != -1:
+ # Sliding window: cache size equals the window size
+ kv_cache_size = self.local_attn_size * self.frame_seq_length
+ elif num_total_frames is not None:
+ # Full history: allocate enough for every frame in this inference call
+ kv_cache_size = num_total_frames * self.frame_seq_length
+ else:
+ kv_cache_size = 32760
+
+ # For full-history mode, extend max_attention_size on every self-attention
+ # module so the attention op actually reads the full cache (default is 32760).
+ if self.local_attn_size == -1:
+ for module in self.generator.model.modules():
+ if hasattr(module, 'max_attention_size'):
+ module.max_attention_size = kv_cache_size
+
+ for _ in range(self.num_transformer_blocks):
+ kv_cache_pos.append({
+ "k": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
+ "v": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
+ })
+ kv_cache_neg.append({
+ "k": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
+ "v": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
+ })
+
+ self.kv_cache_pos = kv_cache_pos # always store the clean cache
+ self.kv_cache_neg = kv_cache_neg # always store the clean cache
+
+ def _initialize_crossattn_cache(self, batch_size, dtype, device):
+ """
+ Initialize a Per-GPU cross-attention cache for the Wan model.
+ """
+ crossattn_cache_pos = []
+ crossattn_cache_neg = []
+ for _ in range(self.num_transformer_blocks):
+ crossattn_cache_pos.append({
+ "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
+ "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
+ "is_init": False
+ })
+ crossattn_cache_neg.append({
+ "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
+ "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
+ "is_init": False
+ })
+
+ self.crossattn_cache_pos = crossattn_cache_pos # always store the clean cache
+ self.crossattn_cache_neg = crossattn_cache_neg # always store the clean cache
+
+ def _initialize_sample_scheduler(self, noise):
+ if self.sample_solver == 'unipc':
+ sample_scheduler = FlowUniPCMultistepScheduler(
+ num_train_timesteps=self.num_train_timesteps,
+ shift=1,
+ use_dynamic_shifting=False)
+ sample_scheduler.set_timesteps(
+ self.sampling_steps, device=noise.device, shift=self.shift)
+ self.timesteps = sample_scheduler.timesteps
+ elif self.sample_solver == 'dpm++':
+ sample_scheduler = FlowDPMSolverMultistepScheduler(
+ num_train_timesteps=self.num_train_timesteps,
+ shift=1,
+ use_dynamic_shifting=False)
+ sampling_sigmas = get_sampling_sigmas(self.sampling_steps, self.shift)
+ self.timesteps, _ = retrieve_timesteps(
+ sample_scheduler,
+ device=noise.device,
+ sigmas=sampling_sigmas)
+ else:
+ raise NotImplementedError("Unsupported solver.")
+ return sample_scheduler
diff --git a/DecMem/pipeline/causal_inference.py b/DecMem/pipeline/causal_inference.py
new file mode 100644
index 0000000000000000000000000000000000000000..8ffbf6214d78c2377204d4f6871eca650a659e51
--- /dev/null
+++ b/DecMem/pipeline/causal_inference.py
@@ -0,0 +1,331 @@
+from typing import List, Optional
+import torch
+
+from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
+from demo_utils.memory import gpu, get_cuda_free_memory_gb, DynamicSwapInstaller, move_model_to_device_with_memory_preservation
+
+
+class CausalInferencePipeline(torch.nn.Module):
+ def __init__(
+ self,
+ args,
+ device,
+ generator=None,
+ text_encoder=None,
+ vae=None
+ ):
+ super().__init__()
+ # Step 1: Initialize all models
+ self.generator = WanDiffusionWrapper(
+ **getattr(args, "model_kwargs", {}), is_causal=True) if generator is None else generator
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
+ self.vae = WanVAEWrapper() if vae is None else vae
+
+ # Step 2: Initialize all causal hyperparmeters
+ self.scheduler = self.generator.get_scheduler()
+ self.denoising_step_list = torch.tensor(
+ args.denoising_step_list, dtype=torch.long)
+ if args.warp_denoising_step:
+ timesteps = torch.cat((self.scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32)))
+ self.denoising_step_list = timesteps[1000 - self.denoising_step_list]
+
+ self.num_transformer_blocks = 30
+ self.frame_seq_length = 1560
+
+ self.kv_cache1 = None
+ self.args = args
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
+ self.independent_first_frame = args.independent_first_frame
+ self.local_attn_size = self.generator.model.local_attn_size
+
+ print(f"KV inference with {self.num_frame_per_block} frames per block")
+
+ if self.num_frame_per_block > 1:
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
+
+ def inference(
+ self,
+ noise: torch.Tensor,
+ text_prompts: List[str],
+ initial_latent: Optional[torch.Tensor] = None,
+ return_latents: bool = False,
+ profile: bool = False,
+ low_memory: bool = False,
+ ) -> torch.Tensor:
+ """
+ Perform inference on the given noise and text prompts.
+ Inputs:
+ noise (torch.Tensor): The input noise tensor of shape
+ (batch_size, num_output_frames, num_channels, height, width).
+ text_prompts (List[str]): The list of text prompts.
+ initial_latent (torch.Tensor): The initial latent tensor of shape
+ (batch_size, num_input_frames, num_channels, height, width).
+ If num_input_frames is 1, perform image to video.
+ If num_input_frames is greater than 1, perform video extension.
+ return_latents (bool): Whether to return the latents.
+ Outputs:
+ video (torch.Tensor): The generated video tensor of shape
+ (batch_size, num_output_frames, num_channels, height, width).
+ It is normalized to be in the range [0, 1].
+ """
+ batch_size, num_frames, num_channels, height, width = noise.shape
+ if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
+ # noise should still be a multiple of num_frame_per_block
+ assert num_frames % self.num_frame_per_block == 0
+ num_blocks = num_frames // self.num_frame_per_block
+ else:
+ # Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
+ assert (num_frames - 1) % self.num_frame_per_block == 0
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
+ conditional_dict = self.text_encoder(
+ text_prompts=text_prompts
+ )
+
+ if low_memory:
+ gpu_memory_preservation = get_cuda_free_memory_gb(gpu) + 5
+ move_model_to_device_with_memory_preservation(self.text_encoder, target_device=gpu, preserved_memory_gb=gpu_memory_preservation)
+
+ output = torch.zeros(
+ [batch_size, num_output_frames, num_channels, height, width],
+ device=noise.device,
+ dtype=noise.dtype
+ )
+
+ # Set up profiling if requested
+ if profile:
+ init_start = torch.cuda.Event(enable_timing=True)
+ init_end = torch.cuda.Event(enable_timing=True)
+ diffusion_start = torch.cuda.Event(enable_timing=True)
+ diffusion_end = torch.cuda.Event(enable_timing=True)
+ vae_start = torch.cuda.Event(enable_timing=True)
+ vae_end = torch.cuda.Event(enable_timing=True)
+ block_times = []
+ block_start = torch.cuda.Event(enable_timing=True)
+ block_end = torch.cuda.Event(enable_timing=True)
+ init_start.record()
+
+ # Step 1: Initialize KV cache to all zeros
+ required_cache_size = (
+ num_output_frames * self.frame_seq_length
+ if self.local_attn_size == -1
+ else self.local_attn_size * self.frame_seq_length
+ )
+ need_reinit = (
+ self.kv_cache1 is None
+ or self.kv_cache1[0]["k"].shape[1] < required_cache_size
+ )
+ if need_reinit:
+ self._initialize_kv_cache(
+ batch_size=batch_size,
+ dtype=noise.dtype,
+ device=noise.device,
+ num_total_frames=num_output_frames
+ )
+ self._initialize_crossattn_cache(
+ batch_size=batch_size,
+ dtype=noise.dtype,
+ device=noise.device
+ )
+ else:
+ # reset cross attn cache
+ for block_index in range(self.num_transformer_blocks):
+ self.crossattn_cache[block_index]["is_init"] = False
+ # reset kv cache
+ for block_index in range(len(self.kv_cache1)):
+ self.kv_cache1[block_index]["global_end_index"] = torch.tensor(
+ [0], dtype=torch.long, device=noise.device)
+ self.kv_cache1[block_index]["local_end_index"] = torch.tensor(
+ [0], dtype=torch.long, device=noise.device)
+
+ # Step 2: Cache context feature
+ current_start_frame = 0
+ if initial_latent is not None:
+ timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
+ if self.independent_first_frame:
+ # Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
+ assert (num_input_frames - 1) % self.num_frame_per_block == 0
+ num_input_blocks = (num_input_frames - 1) // self.num_frame_per_block
+ output[:, :1] = initial_latent[:, :1]
+ self.generator(
+ noisy_image_or_video=initial_latent[:, :1],
+ conditional_dict=conditional_dict,
+ timestep=timestep * 0,
+ kv_cache=self.kv_cache1,
+ crossattn_cache=self.crossattn_cache,
+ current_start=current_start_frame * self.frame_seq_length,
+ )
+ current_start_frame += 1
+ else:
+ # Assume num_input_frames is self.num_frame_per_block * num_input_blocks
+ assert num_input_frames % self.num_frame_per_block == 0
+ num_input_blocks = num_input_frames // self.num_frame_per_block
+
+ for _ in range(num_input_blocks):
+ current_ref_latents = \
+ initial_latent[:, current_start_frame:current_start_frame + self.num_frame_per_block]
+ output[:, current_start_frame:current_start_frame + self.num_frame_per_block] = current_ref_latents
+ self.generator(
+ noisy_image_or_video=current_ref_latents,
+ conditional_dict=conditional_dict,
+ timestep=timestep * 0,
+ kv_cache=self.kv_cache1,
+ crossattn_cache=self.crossattn_cache,
+ current_start=current_start_frame * self.frame_seq_length,
+ )
+ current_start_frame += self.num_frame_per_block
+
+ if profile:
+ init_end.record()
+ torch.cuda.synchronize()
+ diffusion_start.record()
+
+ # Step 3: Temporal denoising loop
+ all_num_frames = [self.num_frame_per_block] * num_blocks
+ if self.independent_first_frame and initial_latent is None:
+ all_num_frames = [1] + all_num_frames
+
+ for current_num_frames in all_num_frames:
+ if profile:
+ block_start.record()
+
+ noisy_input = noise[
+ :, current_start_frame - num_input_frames:current_start_frame + current_num_frames - num_input_frames]
+
+ # Step 3.1: Spatial denoising loop
+ for index, current_timestep in enumerate(self.denoising_step_list):
+ print(f"current_timestep: {current_timestep}")
+ # set current timestep
+ timestep = torch.ones(
+ [batch_size, current_num_frames],
+ device=noise.device,
+ dtype=torch.int64) * current_timestep
+
+ if index < len(self.denoising_step_list) - 1:
+ _, denoised_pred = self.generator(
+ noisy_image_or_video=noisy_input,
+ conditional_dict=conditional_dict,
+ timestep=timestep,
+ kv_cache=self.kv_cache1,
+ crossattn_cache=self.crossattn_cache,
+ current_start=current_start_frame * self.frame_seq_length
+ )
+ next_timestep = self.denoising_step_list[index + 1]
+ noisy_input = self.scheduler.add_noise(
+ denoised_pred.flatten(0, 1),
+ torch.randn_like(denoised_pred.flatten(0, 1)),
+ next_timestep * torch.ones(
+ [batch_size * current_num_frames], device=noise.device, dtype=torch.long)
+ ).unflatten(0, denoised_pred.shape[:2])
+ else:
+ # for getting real output
+ _, denoised_pred = self.generator(
+ noisy_image_or_video=noisy_input,
+ conditional_dict=conditional_dict,
+ timestep=timestep,
+ kv_cache=self.kv_cache1,
+ crossattn_cache=self.crossattn_cache,
+ current_start=current_start_frame * self.frame_seq_length
+ )
+
+ # Step 3.2: record the model's output
+ output[:, current_start_frame:current_start_frame + current_num_frames] = denoised_pred
+
+ # Step 3.3: rerun with timestep zero to update KV cache using clean context
+ context_timestep = torch.ones_like(timestep) * self.args.context_noise
+ self.generator(
+ noisy_image_or_video=denoised_pred,
+ conditional_dict=conditional_dict,
+ timestep=context_timestep,
+ kv_cache=self.kv_cache1,
+ crossattn_cache=self.crossattn_cache,
+ current_start=current_start_frame * self.frame_seq_length,
+ )
+
+ if profile:
+ block_end.record()
+ torch.cuda.synchronize()
+ block_time = block_start.elapsed_time(block_end)
+ block_times.append(block_time)
+
+ # Step 3.4: update the start and end frame indices
+ current_start_frame += current_num_frames
+
+ if profile:
+ # End diffusion timing and synchronize CUDA
+ diffusion_end.record()
+ torch.cuda.synchronize()
+ diffusion_time = diffusion_start.elapsed_time(diffusion_end)
+ init_time = init_start.elapsed_time(init_end)
+ vae_start.record()
+
+ # Step 4: Decode the output
+ video = self.vae.decode_to_pixel(output, use_cache=False)
+ video = (video * 0.5 + 0.5).clamp(0, 1)
+
+ if profile:
+ # End VAE timing and synchronize CUDA
+ vae_end.record()
+ torch.cuda.synchronize()
+ vae_time = vae_start.elapsed_time(vae_end)
+ total_time = init_time + diffusion_time + vae_time
+
+ print("Profiling results:")
+ print(f" - Initialization/caching time: {init_time:.2f} ms ({100 * init_time / total_time:.2f}%)")
+ print(f" - Diffusion generation time: {diffusion_time:.2f} ms ({100 * diffusion_time / total_time:.2f}%)")
+ for i, block_time in enumerate(block_times):
+ print(f" - Block {i} generation time: {block_time:.2f} ms ({100 * block_time / diffusion_time:.2f}% of diffusion)")
+ print(f" - VAE decoding time: {vae_time:.2f} ms ({100 * vae_time / total_time:.2f}%)")
+ print(f" - Total time: {total_time:.2f} ms")
+
+ if return_latents:
+ return video, output
+ else:
+ return video
+
+ def _initialize_kv_cache(self, batch_size, dtype, device, num_total_frames=None):
+ """
+ Initialize a Per-GPU KV cache for the Wan model.
+ """
+ kv_cache1 = []
+ if self.local_attn_size != -1:
+ # Sliding window: cache size equals the window size
+ kv_cache_size = self.local_attn_size * self.frame_seq_length
+ elif num_total_frames is not None:
+ # Full history: allocate enough for every frame in this inference call
+ kv_cache_size = num_total_frames * self.frame_seq_length
+ else:
+ kv_cache_size = 32760
+
+ # For full-history mode, extend max_attention_size on every self-attention
+ # module so the attention op actually reads the full cache (default is 32760).
+ if self.local_attn_size == -1:
+ for module in self.generator.model.modules():
+ if hasattr(module, 'max_attention_size'):
+ module.max_attention_size = kv_cache_size
+
+ for _ in range(self.num_transformer_blocks):
+ kv_cache1.append({
+ "k": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
+ "v": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
+ })
+
+ self.kv_cache1 = kv_cache1 # always store the clean cache
+
+ def _initialize_crossattn_cache(self, batch_size, dtype, device):
+ """
+ Initialize a Per-GPU cross-attention cache for the Wan model.
+ """
+ crossattn_cache = []
+
+ for _ in range(self.num_transformer_blocks):
+ crossattn_cache.append({
+ "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
+ "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
+ "is_init": False
+ })
+ self.crossattn_cache = crossattn_cache
diff --git a/DecMem/pipeline/self_forcing_training.py b/DecMem/pipeline/self_forcing_training.py
new file mode 100644
index 0000000000000000000000000000000000000000..ab989fe620b2441f2b01c5bd656628e34687db85
--- /dev/null
+++ b/DecMem/pipeline/self_forcing_training.py
@@ -0,0 +1,267 @@
+from utils.wan_wrapper import WanDiffusionWrapper
+from utils.scheduler import SchedulerInterface
+from typing import List, Optional
+import torch
+import torch.distributed as dist
+
+
+class SelfForcingTrainingPipeline:
+ def __init__(self,
+ denoising_step_list: List[int],
+ scheduler: SchedulerInterface,
+ generator: WanDiffusionWrapper,
+ num_frame_per_block=3,
+ independent_first_frame: bool = False,
+ same_step_across_blocks: bool = False,
+ last_step_only: bool = False,
+ num_max_frames: int = 21,
+ context_noise: int = 0,
+ **kwargs):
+ super().__init__()
+ self.scheduler = scheduler
+ self.generator = generator
+ self.denoising_step_list = denoising_step_list
+ if self.denoising_step_list[-1] == 0:
+ self.denoising_step_list = self.denoising_step_list[:-1] # remove the zero timestep for inference
+
+ # Wan specific hyperparameters
+ self.num_transformer_blocks = 30
+ self.frame_seq_length = 1560
+ self.num_frame_per_block = num_frame_per_block
+ self.context_noise = context_noise
+ self.i2v = False
+
+ self.kv_cache1 = None
+ self.kv_cache2 = None
+ self.independent_first_frame = independent_first_frame
+ self.same_step_across_blocks = same_step_across_blocks
+ self.last_step_only = last_step_only
+ self.kv_cache_size = num_max_frames * self.frame_seq_length
+
+ def generate_and_sync_list(self, num_blocks, num_denoising_steps, device):
+ rank = dist.get_rank() if dist.is_initialized() else 0
+
+ if rank == 0:
+ # Generate random indices
+ indices = torch.randint(
+ low=0,
+ high=num_denoising_steps,
+ size=(num_blocks,),
+ device=device
+ )
+ if self.last_step_only:
+ indices = torch.ones_like(indices) * (num_denoising_steps - 1)
+ else:
+ indices = torch.empty(num_blocks, dtype=torch.long, device=device)
+
+ dist.broadcast(indices, src=0) # Broadcast the random indices to all ranks
+ return indices.tolist()
+
+ def inference_with_trajectory(
+ self,
+ noise: torch.Tensor,
+ initial_latent: Optional[torch.Tensor] = None,
+ return_sim_step: bool = False,
+ **conditional_dict
+ ) -> torch.Tensor:
+ batch_size, num_frames, num_channels, height, width = noise.shape
+ if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
+ # noise should still be a multiple of num_frame_per_block
+ assert num_frames % self.num_frame_per_block == 0
+ num_blocks = num_frames // self.num_frame_per_block
+ else:
+ # Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
+ assert (num_frames - 1) % self.num_frame_per_block == 0
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
+ output = torch.zeros(
+ [batch_size, num_output_frames, num_channels, height, width],
+ device=noise.device,
+ dtype=noise.dtype
+ )
+
+ # Step 1: Initialize KV cache to all zeros
+ self._initialize_kv_cache(
+ batch_size=batch_size, dtype=noise.dtype, device=noise.device
+ )
+ self._initialize_crossattn_cache(
+ batch_size=batch_size, dtype=noise.dtype, device=noise.device
+ )
+ # if self.kv_cache1 is None:
+ # self._initialize_kv_cache(
+ # batch_size=batch_size,
+ # dtype=noise.dtype,
+ # device=noise.device,
+ # )
+ # self._initialize_crossattn_cache(
+ # batch_size=batch_size,
+ # dtype=noise.dtype,
+ # device=noise.device
+ # )
+ # else:
+ # # reset cross attn cache
+ # for block_index in range(self.num_transformer_blocks):
+ # self.crossattn_cache[block_index]["is_init"] = False
+ # # reset kv cache
+ # for block_index in range(len(self.kv_cache1)):
+ # self.kv_cache1[block_index]["global_end_index"] = torch.tensor(
+ # [0], dtype=torch.long, device=noise.device)
+ # self.kv_cache1[block_index]["local_end_index"] = torch.tensor(
+ # [0], dtype=torch.long, device=noise.device)
+
+ # Step 2: Cache context feature
+ current_start_frame = 0
+ if initial_latent is not None:
+ timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
+ # Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
+ output[:, :1] = initial_latent
+ with torch.no_grad():
+ self.generator(
+ noisy_image_or_video=initial_latent,
+ conditional_dict=conditional_dict,
+ timestep=timestep * 0,
+ kv_cache=self.kv_cache1,
+ crossattn_cache=self.crossattn_cache,
+ current_start=current_start_frame * self.frame_seq_length
+ )
+ current_start_frame += 1
+
+ # Step 3: Temporal denoising loop
+ all_num_frames = [self.num_frame_per_block] * num_blocks
+ if self.independent_first_frame and initial_latent is None:
+ all_num_frames = [1] + all_num_frames
+ num_denoising_steps = len(self.denoising_step_list)
+ exit_flags = self.generate_and_sync_list(len(all_num_frames), num_denoising_steps, device=noise.device)
+ start_gradient_frame_index = num_output_frames - 21
+
+ # for block_index in range(num_blocks):
+ for block_index, current_num_frames in enumerate(all_num_frames):
+ noisy_input = noise[
+ :, current_start_frame - num_input_frames:current_start_frame + current_num_frames - num_input_frames]
+
+ # Step 3.1: Spatial denoising loop
+ for index, current_timestep in enumerate(self.denoising_step_list):
+ if self.same_step_across_blocks:
+ exit_flag = (index == exit_flags[0])
+ else:
+ exit_flag = (index == exit_flags[block_index]) # Only backprop at the randomly selected timestep (consistent across all ranks)
+ timestep = torch.ones(
+ [batch_size, current_num_frames],
+ device=noise.device,
+ dtype=torch.int64) * current_timestep
+
+ if not exit_flag:
+ with torch.no_grad():
+ _, denoised_pred = self.generator(
+ noisy_image_or_video=noisy_input,
+ conditional_dict=conditional_dict,
+ timestep=timestep,
+ kv_cache=self.kv_cache1,
+ crossattn_cache=self.crossattn_cache,
+ current_start=current_start_frame * self.frame_seq_length
+ )
+ next_timestep = self.denoising_step_list[index + 1]
+ noisy_input = self.scheduler.add_noise(
+ denoised_pred.flatten(0, 1),
+ torch.randn_like(denoised_pred.flatten(0, 1)),
+ next_timestep * torch.ones(
+ [batch_size * current_num_frames], device=noise.device, dtype=torch.long)
+ ).unflatten(0, denoised_pred.shape[:2])
+ else:
+ # for getting real output
+ # with torch.set_grad_enabled(current_start_frame >= start_gradient_frame_index):
+ if current_start_frame < start_gradient_frame_index:
+ with torch.no_grad():
+ _, denoised_pred = self.generator(
+ noisy_image_or_video=noisy_input,
+ conditional_dict=conditional_dict,
+ timestep=timestep,
+ kv_cache=self.kv_cache1,
+ crossattn_cache=self.crossattn_cache,
+ current_start=current_start_frame * self.frame_seq_length
+ )
+ else:
+ _, denoised_pred = self.generator(
+ noisy_image_or_video=noisy_input,
+ conditional_dict=conditional_dict,
+ timestep=timestep,
+ kv_cache=self.kv_cache1,
+ crossattn_cache=self.crossattn_cache,
+ current_start=current_start_frame * self.frame_seq_length
+ )
+ break
+
+ # Step 3.2: record the model's output
+ output[:, current_start_frame:current_start_frame + current_num_frames] = denoised_pred
+
+ # Step 3.3: rerun with timestep zero to update the cache
+ context_timestep = torch.ones_like(timestep) * self.context_noise
+ # add context noise
+ denoised_pred = self.scheduler.add_noise(
+ denoised_pred.flatten(0, 1),
+ torch.randn_like(denoised_pred.flatten(0, 1)),
+ context_timestep * torch.ones(
+ [batch_size * current_num_frames], device=noise.device, dtype=torch.long)
+ ).unflatten(0, denoised_pred.shape[:2])
+ with torch.no_grad():
+ self.generator(
+ noisy_image_or_video=denoised_pred,
+ conditional_dict=conditional_dict,
+ timestep=context_timestep,
+ kv_cache=self.kv_cache1,
+ crossattn_cache=self.crossattn_cache,
+ current_start=current_start_frame * self.frame_seq_length
+ )
+
+ # Step 3.4: update the start and end frame indices
+ current_start_frame += current_num_frames
+
+ # Step 3.5: Return the denoised timestep
+ if not self.same_step_across_blocks:
+ denoised_timestep_from, denoised_timestep_to = None, None
+ elif exit_flags[0] == len(self.denoising_step_list) - 1:
+ denoised_timestep_to = 0
+ denoised_timestep_from = 1000 - torch.argmin(
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
+ else:
+ denoised_timestep_to = 1000 - torch.argmin(
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0] + 1].cuda()).abs(), dim=0).item()
+ denoised_timestep_from = 1000 - torch.argmin(
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
+
+ if return_sim_step:
+ return output, denoised_timestep_from, denoised_timestep_to, exit_flags[0] + 1
+
+ return output, denoised_timestep_from, denoised_timestep_to
+
+ def _initialize_kv_cache(self, batch_size, dtype, device):
+ """
+ Initialize a Per-GPU KV cache for the Wan model.
+ """
+ kv_cache1 = []
+
+ for _ in range(self.num_transformer_blocks):
+ kv_cache1.append({
+ "k": torch.zeros([batch_size, self.kv_cache_size, 12, 128], dtype=dtype, device=device),
+ "v": torch.zeros([batch_size, self.kv_cache_size, 12, 128], dtype=dtype, device=device),
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
+ })
+
+ self.kv_cache1 = kv_cache1 # always store the clean cache
+
+ def _initialize_crossattn_cache(self, batch_size, dtype, device):
+ """
+ Initialize a Per-GPU cross-attention cache for the Wan model.
+ """
+ crossattn_cache = []
+
+ for _ in range(self.num_transformer_blocks):
+ crossattn_cache.append({
+ "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
+ "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device),
+ "is_init": False
+ })
+ self.crossattn_cache = crossattn_cache
diff --git a/DecMem/requirements.txt b/DecMem/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ac932081f345511ad4fd53b1d417922ebcab3f2f
--- /dev/null
+++ b/DecMem/requirements.txt
@@ -0,0 +1,26 @@
+opencv-python>=4.9.0.80
+diffusers==0.31.0
+transformers>=4.49.0
+tokenizers>=0.20.3
+accelerate>=1.1.1
+tqdm
+imageio
+easydict
+ftfy
+imageio-ffmpeg
+numpy
+wandb
+omegaconf
+einops
+av==13.1.0
+opencv-python
+pycocotools
+lmdb
+matplotlib
+pydantic==2.10.6
+scikit-image
+huggingface_hub[cli]
+dominate
+ninja
+pytest
+decord
\ No newline at end of file
diff --git a/DecMem/scripts/build_worldmem_csv.py b/DecMem/scripts/build_worldmem_csv.py
new file mode 100644
index 0000000000000000000000000000000000000000..9bb2eab8835c450583f6f1b6042399f61fb11443
--- /dev/null
+++ b/DecMem/scripts/build_worldmem_csv.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+"""Build WorldMem train/test CSV index files from mp4/npz pairs.
+
+Each row: video_path, action_path (paths relative to the project cwd when possible).
+
+When --root is a symlink under the project (e.g. ./data), CSV rows use paths like
+data/training/.... Avoid resolving symlinks so relative paths stay valid.
+
+Example:
+ python scripts/build_worldmem_csv.py
+"""
+
+from __future__ import annotations
+
+import argparse
+import csv
+import random
+from pathlib import Path
+
+
+def _path_for_csv(path: Path) -> str:
+ """Path for CSV rows, relative to cwd when possible (preserves symlinks)."""
+ if not path.is_absolute():
+ return path.as_posix()
+ try:
+ return path.relative_to(Path.cwd()).as_posix()
+ except ValueError:
+ return path.as_posix()
+
+
+def collect_pairs(split_dir: Path, root: Path) -> list[tuple[str, str]]:
+ """Collect (video_path, action_path) for every mp4 with a matching npz."""
+ if not split_dir.is_dir():
+ raise FileNotFoundError(f"Split directory not found: {split_dir}")
+
+ pairs: list[tuple[str, str]] = []
+ missing_npz: list[str] = []
+
+ for mp4 in sorted(split_dir.rglob("*.mp4")):
+ npz = mp4.with_suffix(".npz")
+ if not npz.is_file():
+ missing_npz.append(mp4.relative_to(root).as_posix())
+ continue
+ pairs.append((
+ _path_for_csv(mp4),
+ _path_for_csv(npz),
+ ))
+
+ if missing_npz:
+ print(f" warning: {len(missing_npz)} mp4 files without matching npz "
+ f"(first: {missing_npz[0]})")
+
+ return pairs
+
+
+def write_csv(rows: list[tuple[str, str]], output_path: Path) -> None:
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ with output_path.open("w", newline="", encoding="utf-8") as f:
+ writer = csv.writer(f)
+ writer.writerow(["video_path", "action_path"])
+ writer.writerows(rows)
+ print(f" wrote {len(rows)} rows -> {output_path}")
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Build WorldMem train/test CSV metadata.")
+ parser.add_argument(
+ "--root",
+ type=str,
+ default='./data',
+ help=f"Dataset root directory",
+ )
+ parser.add_argument(
+ "--output_dir",
+ type=str,
+ default='./data',
+ help="Directory for train.csv / test.csv (default: same as --root)",
+ )
+ parser.add_argument(
+ "--seed",
+ type=int,
+ default=42,
+ help="Random seed for shuffling rows",
+ )
+ args = parser.parse_args()
+
+ root = Path(args.root)
+ output_dir = Path(args.output_dir) if args.output_dir else root
+
+ splits = {
+ "train.csv": root / "training",
+ "test.csv": root / "test",
+ }
+
+ print(f"Dataset root : {root} (-> {root.resolve()})")
+ print(f"Output dir : {output_dir}")
+ print(f"Shuffle seed : {args.seed}")
+
+ rng = random.Random(args.seed)
+
+ for csv_name, split_dir in splits.items():
+ print(f"\n[{csv_name}] scanning {split_dir.relative_to(root)}/")
+ pairs = collect_pairs(split_dir, root)
+ rng.shuffle(pairs)
+ write_csv(pairs, output_dir / csv_name)
+
+ print("\nDone.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/DecMem/scripts/infer.sh b/DecMem/scripts/infer.sh
new file mode 100644
index 0000000000000000000000000000000000000000..aa7c2537fbe0155ecd5b5ddb3e76ec4590dbb11f
--- /dev/null
+++ b/DecMem/scripts/infer.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+set -o pipefail
+
+# CONFIG SETTING
+NUM_GPUS=${NUM_GPUS:-1}
+MASTER_PORT=${MASTER_PORT:-29110}
+OFFLOAD=${OFFLOAD:-0} # 1 or true = mutually-exclusive GPU residency for generator/VAE
+EXP_NAME=${EXP_NAME:-decmem}
+STEP=${STEP:-7000}
+printf -v STEP "%06d" "$STEP"
+
+DATA_PATH=data/test.csv
+CONFIG_PATH=${CONFIG_PATH:-configs/${EXP_NAME}.yaml}
+CHECKPOINT_PATH=${CHECKPOINT_PATH:-exps/${EXP_NAME}/checkpoint_model_${STEP}/model.pt}
+NUM_CONDITION_FRAMES=${NUM_CONDITION_FRAMES:-56}
+NUM_INFERENCE_STEPS=${NUM_INFERENCE_STEPS:-50}
+GUIDANCE_SCALE=${GUIDANCE_SCALE:-5}
+SEED=${SEED:-42}
+START_INDEX=${START_INDEX:-0}
+N=${N:-50}
+
+T_LAT=${T_LAT:-200} # optional; empty = use config value
+T_TAG=${T_LAT:+_T${T_LAT}}
+
+OUTPUT_DIR=${OUTPUT_DIR:-test/res/${EXP_NAME}_step_${STEP}${T_TAG}_con_${NUM_CONDITION_FRAMES}_inf_${NUM_INFERENCE_STEPS}_gs_${GUIDANCE_SCALE}}
+mkdir -p "$OUTPUT_DIR"
+RANK0_LOG="${OUTPUT_DIR}/rank0.log"
+
+SCRIPT_ARGS=(
+ --config_path "$CONFIG_PATH"
+ --checkpoint_path "$CHECKPOINT_PATH"
+ --data_path "$DATA_PATH"
+ --output_dir "$OUTPUT_DIR"
+ --num_condition_frames "$NUM_CONDITION_FRAMES"
+ --guidance_scale "$GUIDANCE_SCALE"
+ --num_inference_steps "$NUM_INFERENCE_STEPS"
+ --seed "$SEED")
+
+SCRIPT_ARGS+=(--start_index "$START_INDEX")
+if [[ -n "$N" ]]; then
+ SCRIPT_ARGS+=(--num_entries "$N")
+fi
+
+if [[ -n "$T_LAT" ]]; then
+ SCRIPT_ARGS+=(--num_latent_frames "$T_LAT")
+fi
+
+if [[ "$NUM_GPUS" -gt 1 ]]; then
+ echo "Multi-GPU inference: NUM_GPUS=${NUM_GPUS} MASTER_PORT=${MASTER_PORT}"
+ torchrun \
+ --standalone \
+ --nproc_per_node="$NUM_GPUS" \
+ --master_port="$MASTER_PORT" \
+ --local-ranks-filter=0 \
+ infer.py \
+ "${SCRIPT_ARGS[@]}" \
+ 2>&1 | tee "$RANK0_LOG"
+else
+ torchrun infer.py "${SCRIPT_ARGS[@]}" 2>&1 | tee "$RANK0_LOG"
+fi
\ No newline at end of file
diff --git a/DecMem/scripts/infer_example.sh b/DecMem/scripts/infer_example.sh
new file mode 100644
index 0000000000000000000000000000000000000000..491724664f9234a76e57b5ff5fa5b8f202d6f8ec
--- /dev/null
+++ b/DecMem/scripts/infer_example.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+set -o pipefail
+
+# CONFIG SETTING
+NUM_GPUS=${NUM_GPUS:-1}
+MASTER_PORT=${MASTER_PORT:-29110}
+OFFLOAD=${OFFLOAD:-0} # 1 or true = mutually-exclusive GPU residency for generator/VAE
+
+EXP_NAME=decmem
+CONFIG_PATH=${CONFIG_PATH:-configs/${EXP_NAME}.yaml}
+CHECKPOINT_PATH=checkpoints/decmem.pt
+DATA_PATH=example/example.csv
+
+NUM_CONDITION_FRAMES=${NUM_CONDITION_FRAMES:-56}
+NUM_INFERENCE_STEPS=${NUM_INFERENCE_STEPS:-20}
+GUIDANCE_SCALE=${GUIDANCE_SCALE:-5}
+SEED=${SEED:-42}
+START_INDEX=${START_INDEX:-0}
+N=${N:-2}
+
+T_LAT=${T_LAT:-160} # optional; empty = use config value
+T_TAG=${T_LAT:+_T${T_LAT}}
+
+OUTPUT_DIR=${OUTPUT_DIR:-test/res/${EXP_NAME}_${T_TAG}_con_${NUM_CONDITION_FRAMES}_inf_${NUM_INFERENCE_STEPS}_gs_${GUIDANCE_SCALE}}
+mkdir -p "$OUTPUT_DIR"
+RANK0_LOG="${OUTPUT_DIR}/rank0.log"
+
+SCRIPT_ARGS=(
+ --config_path "$CONFIG_PATH"
+ --checkpoint_path "$CHECKPOINT_PATH"
+ --data_path "$DATA_PATH"
+ --output_dir "$OUTPUT_DIR"
+ --num_condition_frames "$NUM_CONDITION_FRAMES"
+ --guidance_scale "$GUIDANCE_SCALE"
+ --num_inference_steps "$NUM_INFERENCE_STEPS"
+ --seed "$SEED")
+
+SCRIPT_ARGS+=(--start_index "$START_INDEX")
+if [[ -n "$N" ]]; then
+ SCRIPT_ARGS+=(--num_entries "$N")
+fi
+
+if [[ -n "$T_LAT" ]]; then
+ SCRIPT_ARGS+=(--num_latent_frames "$T_LAT")
+fi
+
+if [[ "$NUM_GPUS" -gt 1 ]]; then
+ echo "Multi-GPU inference: NUM_GPUS=${NUM_GPUS} MASTER_PORT=${MASTER_PORT}"
+ torchrun \
+ --standalone \
+ --nproc_per_node="$NUM_GPUS" \
+ --master_port="$MASTER_PORT" \
+ --local-ranks-filter=0 \
+ infer.py \
+ "${SCRIPT_ARGS[@]}" \
+ 2>&1 | tee "$RANK0_LOG"
+else
+ torchrun infer.py "${SCRIPT_ARGS[@]}" 2>&1 | tee "$RANK0_LOG"
+fi
\ No newline at end of file
diff --git a/DecMem/scripts/train_multinode.sh b/DecMem/scripts/train_multinode.sh
new file mode 100644
index 0000000000000000000000000000000000000000..4b6d035fc15ff602ed5fc3d347e2a4d1df2ae0a9
--- /dev/null
+++ b/DecMem/scripts/train_multinode.sh
@@ -0,0 +1,85 @@
+#!/bin/bash
+# WorldMem diffusion multi-node training, launched MANUALLY on each node.
+#
+# Run this on every node, passing that node's rank as the only argument.
+# Node 0 must be the machine whose IP matches MASTER_ADDR.
+#
+# Usage (run on each node):
+# bash scripts/train_multinode.sh [exp_name]
+#
+# Example with 2 nodes:
+# # on master node:
+# bash scripts/train_multinode.sh 0 decmem
+# # on worker node:
+# bash scripts/train_multinode.sh 1 decmem
+#
+# Env overrides:
+# EXP_NAME experiment name / config stem (default: decmem)
+# NPROC_PER_NODE GPUs per node (default 8)
+# MASTER_PORT torchrun rendezvous port (default 29500)
+# RESUME_CKPT path to a checkpoint to fully resume from (optional)
+
+# ─── Topology (EDIT THESE) ───────────────────────────────────────────────────
+NNODES=2 # TODO: total number of nodes
+MASTER_ADDR="" # TODO: IP of node_rank=0
+MASTER_PORT=${MASTER_PORT:-29500}
+NPROC_PER_NODE=${NPROC_PER_NODE:-8}
+
+# ─── Node rank / experiment from CLI ─────────────────────────────────────────
+NODE_RANK=$1
+EXP_NAME=${2:-${EXP_NAME:-decmem}}
+if [ -z "$NODE_RANK" ]; then
+ echo "Error: NODE_RANK is required. Usage: bash $0 [exp_name]"
+ exit 1
+fi
+
+# ─── Paths / config ──────────────────────────────────────────────────────────
+CONFIG=configs/${EXP_NAME}.yaml
+LOGDIR=exps/${EXP_NAME}
+WANDB_SAVE_DIR=${LOGDIR}/wandb
+TORCHRUN=torchrun
+RESUME_CKPT=${RESUME_CKPT:-}
+
+mkdir -p "${LOGDIR}"
+
+# Optional resume: `RESUME_CKPT=/path/to/model.pt bash `
+# Must be set on EVERY node (this script doesn't propagate env across nodes).
+RESUME_ARGS=""
+if [ -n "${RESUME_CKPT}" ]; then
+ RESUME_ARGS="--resume-ckpt ${RESUME_CKPT}"
+ echo "[resume] RESUME_CKPT = ${RESUME_CKPT}"
+fi
+
+# ─── NCCL (match train_worldmem_multinode_torchrun.sh) ───────────────────────
+export NCCL_IB_DISABLE=0
+export NCCL_IB_GID_INDEX=3
+export NCCL_MIN_NCHANNELS=16
+export NCCL_IB_HCA=mlx5
+export NCCL_IB_QPS_PER_CONNECTION=4
+export NCCL_IB_TIMEOUT=22
+export NCCL_DEBUG=WARN
+
+echo "EXP_NAME = ${EXP_NAME}"
+echo "CONFIG = ${CONFIG}"
+echo "LOGDIR = ${LOGDIR}"
+echo "NNODES = ${NNODES}"
+echo "NPROC_PER_NODE = ${NPROC_PER_NODE}"
+echo "WORLD_SIZE = $((NNODES * NPROC_PER_NODE))"
+echo "MASTER_ADDR = ${MASTER_ADDR}"
+echo "MASTER_PORT = ${MASTER_PORT}"
+echo "RESUME_CKPT = ${RESUME_CKPT:-}"
+echo "Starting node with NODE_RANK=${NODE_RANK}"
+
+# ─── Launch torchrun ─────────────────────────────────────────────────────────
+${TORCHRUN} \
+ --nnodes=${NNODES} \
+ --nproc_per_node=${NPROC_PER_NODE} \
+ --node_rank=${NODE_RANK} \
+ --master_addr=${MASTER_ADDR} \
+ --master_port=${MASTER_PORT} \
+ train.py \
+ --config_path ${CONFIG} \
+ --logdir ${LOGDIR} \
+ --wandb-save-dir ${WANDB_SAVE_DIR} \
+ ${RESUME_ARGS} \
+ > "${LOGDIR}/node_${NODE_RANK}_$(date +%Y%m%d_%H%M%S).log" 2>&1
diff --git a/DecMem/slurm_logs/.gitkeep b/DecMem/slurm_logs/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/DecMem/train.py b/DecMem/train.py
new file mode 100644
index 0000000000000000000000000000000000000000..ad81aeacba05457844931045087d2f984aa983c5
--- /dev/null
+++ b/DecMem/train.py
@@ -0,0 +1,45 @@
+import argparse
+import os
+from omegaconf import OmegaConf
+import wandb
+
+from trainer import DiffusionTrainer
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--config_path", type=str, required=True)
+ parser.add_argument("--no_save", action="store_true")
+ parser.add_argument("--no_visualize", action="store_true")
+ parser.add_argument("--logdir", type=str, default="", help="Path to the directory to save logs")
+ parser.add_argument("--wandb-save-dir", type=str, default="", help="Path to the directory to save wandb logs")
+ parser.add_argument("--disable-wandb", action="store_true")
+ parser.add_argument("--resume-ckpt", type=str, default=None,
+ help="Path to a checkpoint to fully resume from (weights + optimizer + step + wandb_run_id). "
+ "Overrides config.resume_ckpt if both are set.")
+
+ args = parser.parse_args()
+
+ config = OmegaConf.load(args.config_path)
+ default_config = OmegaConf.load("configs/default_config.yaml")
+ config = OmegaConf.merge(default_config, config)
+ config.no_save = args.no_save
+ config.no_visualize = args.no_visualize
+
+ # get the filename of config_path
+ config_name = os.path.basename(args.config_path).split(".")[0]
+ config.config_name = config_name
+ config.logdir = args.logdir
+ config.wandb_save_dir = args.wandb_save_dir
+ config.disable_wandb = args.disable_wandb
+ if args.resume_ckpt is not None:
+ config.resume_ckpt = args.resume_ckpt
+
+ trainer = DiffusionTrainer(config)
+ trainer.train()
+
+ wandb.finish()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/DecMem/trainer/__init__.py b/DecMem/trainer/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d9c8e57220f85ee810d902e184ad419329986a5
--- /dev/null
+++ b/DecMem/trainer/__init__.py
@@ -0,0 +1,7 @@
+from .diffusion import Trainer as DiffusionTrainer
+from .gan import Trainer as GANTrainer
+
+__all__ = [
+ "DiffusionTrainer",
+ "GANTrainer",
+]
diff --git a/DecMem/trainer/diffusion.py b/DecMem/trainer/diffusion.py
new file mode 100644
index 0000000000000000000000000000000000000000..48b11e6be3831567d548a8e56287fb87798d9b51
--- /dev/null
+++ b/DecMem/trainer/diffusion.py
@@ -0,0 +1,432 @@
+import gc
+import logging
+
+from model import CausalDiffusion
+from utils.dataset import ShardingLMDBDataset, WorldMemCSVDataset, cycle
+from utils.pose_utils import poses_to_viewmats
+from utils.misc import set_seed
+import torch.distributed as dist
+from omegaconf import OmegaConf
+import torch
+import wandb
+import time
+import os
+
+from utils.distributed import barrier, fsdp_wrap, fsdp_state_dict, fsdp_optim_state_dict, load_fsdp_optim_state_dict, launch_distributed_job
+# from utils.distributed import EMA_FSDP
+
+
+class Trainer:
+ def __init__(self, config):
+ self.config = config
+ self.step = 0
+
+ # Step 1: Initialize the distributed training environment (rank, seed, dtype, logging etc.)
+ torch.backends.cuda.matmul.allow_tf32 = True
+ torch.backends.cudnn.allow_tf32 = True
+
+ launch_distributed_job()
+ global_rank = dist.get_rank()
+
+ self.dtype = torch.bfloat16 if config.mixed_precision else torch.float32
+ self.device = torch.cuda.current_device()
+ self.is_main_process = global_rank == 0
+ self.causal = config.causal
+ self.disable_wandb = config.disable_wandb
+
+ # If resume_ckpt is set, load it now so we can restore step / seed / wandb_run_id
+ # BEFORE seeding RNG and initializing wandb. The full state stays in CPU memory
+ # and is consumed (then freed) after the optimizer / EMA are constructed.
+ self._resume_state = None
+ resume_path = getattr(config, "resume_ckpt", None)
+ if resume_path:
+ if global_rank == 0:
+ print(f"Loading resume checkpoint: {resume_path}")
+ self._resume_state = torch.load(resume_path, map_location="cpu", weights_only=False)
+ self.step = int(self._resume_state.get("step", 0))
+ resumed_seed = self._resume_state.get("seed", None)
+ if resumed_seed is not None:
+ config.seed = int(resumed_seed)
+ if global_rank == 0:
+ print(f"Resumed: step={self.step}, seed={config.seed}, "
+ f"wandb_run_id={self._resume_state.get('wandb_run_id')}")
+
+ # use a random seed for the training
+ if config.seed == 0:
+ random_seed = torch.randint(0, 10000000, (1,), device=self.device)
+ dist.broadcast(random_seed, src=0)
+ config.seed = random_seed.item()
+
+ set_seed(config.seed + global_rank)
+
+ if self.is_main_process and not self.disable_wandb:
+ wandb.login(host=config.wandb_host, key=config.wandb_key)
+ wandb_init_kwargs = dict(
+ config=OmegaConf.to_container(config, resolve=True),
+ name=config.config_name,
+ mode="online",
+ entity=config.wandb_entity,
+ project=config.wandb_project,
+ dir=config.wandb_save_dir,
+ )
+ wandb_run_id = self._resume_state.get("wandb_run_id") if self._resume_state else None
+ if wandb_run_id:
+ # resume="must" → wandb errors out if the id is missing, instead of
+ # silently creating a new run that diverges from the original.
+ wandb_init_kwargs["id"] = wandb_run_id
+ wandb_init_kwargs["resume"] = "must"
+ wandb.init(**wandb_init_kwargs)
+
+ self.output_path = config.logdir
+
+ # Step 2: Initialize the model and optimizer
+ # For WorldMem training (CSV data path), skip text encoder to save GPU memory
+ data_path = config.data_path
+ self.use_worldmem_data = isinstance(data_path, str) and data_path.endswith(".csv")
+ if self.use_worldmem_data:
+ config.use_text_encoder = False
+
+ self.model = CausalDiffusion(config, device=self.device)
+
+ if global_rank == 0:
+ def _fmt(n: int) -> str:
+ for unit in ("", "K", "M", "B"):
+ if abs(n) < 1000:
+ return f"{n:.2f}{unit}"
+ n /= 1000
+ return f"{n:.2f}T"
+ total = sum(p.numel() for p in self.model.generator.parameters())
+ trainable = sum(p.numel() for p in self.model.generator.parameters() if p.requires_grad)
+ print(f"[generator] params total={_fmt(total)} ({total:,}) "
+ f"trainable={_fmt(trainable)} ({trainable:,})", flush=True)
+ if self.model.text_encoder is not None:
+ te = sum(p.numel() for p in self.model.text_encoder.parameters())
+ print(f"[text_encoder] params total={_fmt(te)} ({te:,})", flush=True)
+ vae = sum(p.numel() for p in self.model.vae.parameters())
+ print(f"[vae] params total={_fmt(vae)} ({vae:,})", flush=True)
+
+ self.model.generator = fsdp_wrap(
+ self.model.generator,
+ sharding_strategy=config.sharding_strategy,
+ mixed_precision=config.mixed_precision,
+ wrap_strategy=config.generator_fsdp_wrap_strategy
+ )
+
+ if self.model.text_encoder is not None:
+ self.model.text_encoder = fsdp_wrap(
+ self.model.text_encoder,
+ sharding_strategy=config.sharding_strategy,
+ mixed_precision=config.mixed_precision,
+ wrap_strategy=config.text_encoder_fsdp_wrap_strategy
+ )
+
+ if not config.no_visualize or config.load_raw_video:
+ self.model.vae = self.model.vae.to(
+ device=self.device, dtype=torch.bfloat16 if config.mixed_precision else torch.float32)
+
+ self.generator_optimizer = torch.optim.AdamW(
+ [param for param in self.model.generator.parameters()
+ if param.requires_grad],
+ lr=config.lr,
+ betas=(config.beta1, config.beta2),
+ weight_decay=config.weight_decay
+ )
+
+ # Step 3: Initialize the dataloader
+ if self.use_worldmem_data:
+ num_latent_frames = getattr(config, "num_frames", 21)
+ sample_strategy = getattr(config, "sample_strategy", "random")
+ dataset = WorldMemCSVDataset(
+ data_path,
+ num_latent_frames=num_latent_frames,
+ sample_strategy=sample_strategy,
+ )
+ else:
+ dataset = ShardingLMDBDataset(data_path, max_pair=int(1e8))
+ sampler = torch.utils.data.distributed.DistributedSampler(
+ dataset, shuffle=True, drop_last=True)
+ dataloader = torch.utils.data.DataLoader(
+ dataset,
+ batch_size=config.batch_size,
+ sampler=sampler,
+ num_workers=8,
+ pin_memory=True,
+ persistent_workers=True)
+
+ if dist.get_rank() == 0:
+ world_size = dist.get_world_size()
+ per_rank_samples = len(sampler) # honors drop_last + world_size split
+ global_batch_size = world_size * config.batch_size
+ print("=" * 80, flush=True)
+ print(f"[dataset] total samples : {len(dataset)}", flush=True)
+ print(f"[dataset] per-rank : {per_rank_samples} "
+ f"(drop_last=True, world_size={world_size})", flush=True)
+ print(f"[batch] per-rank : {config.batch_size}", flush=True)
+ print(f"[batch] global : {global_batch_size} "
+ f"(= {world_size} ranks x {config.batch_size})", flush=True)
+ print(f"[steps/epoch] : {per_rank_samples // config.batch_size}",
+ flush=True)
+ print("-" * 80, flush=True)
+ print("[config]")
+ print(OmegaConf.to_yaml(config, resolve=True), flush=True)
+ print("=" * 80, flush=True)
+ self.dataloader = cycle(dataloader)
+
+ ##############################################################################################################
+ # 6. Set up EMA parameter containers
+ rename_param = (
+ lambda name: name.replace("_fsdp_wrapped_module.", "")
+ .replace("_checkpoint_wrapped_module.", "")
+ .replace("_orig_mod.", "")
+ )
+ self.name_to_trainable_params = {}
+ for n, p in self.model.generator.named_parameters():
+ if not p.requires_grad:
+ continue
+
+ renamed_n = rename_param(n)
+ self.name_to_trainable_params[renamed_n] = p
+ # ema_weight = config.ema_weight
+ # self.generator_ema = None
+ # if (ema_weight is not None) and (ema_weight > 0.0):
+ # print(f"Setting up EMA with weight {ema_weight}")
+ # self.generator_ema = EMA_FSDP(self.model.generator, decay=ema_weight)
+
+ ##############################################################################################################
+ # 7. (If resuming) Load the model and optimizer, lr_scheduler statedicts
+ if self._resume_state is not None:
+ # Full resume: weights + optimizer + EMA (step already restored in __init__ top).
+ if global_rank == 0:
+ print("Restoring generator / optimizer from resume checkpoint.")
+ # Use FULL_STATE_DICT context so FSDP scatters the (CPU) full
+ # state-dict into local GPU shards instead of leaving params on CPU.
+ from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
+ from torch.distributed.fsdp import StateDictType
+ with FSDP.state_dict_type(self.model.generator, StateDictType.FULL_STATE_DICT):
+ self.model.generator.load_state_dict(self._resume_state["generator"], strict=False)
+ if "optimizer" in self._resume_state:
+ load_fsdp_optim_state_dict(
+ self.model.generator,
+ self.generator_optimizer,
+ self._resume_state["optimizer"],
+ )
+ # if "generator_ema" in self._resume_state and self.generator_ema is not None:
+ # self.generator_ema.load_state_dict(self._resume_state["generator_ema"])
+ self._resume_state = None
+ torch.cuda.empty_cache()
+ elif getattr(config, "generator_ckpt", False):
+ # Warm-start: weights only, step counter and optimizer stay fresh.
+ print(f"Loading pretrained generator from {config.generator_ckpt}")
+ state_dict = torch.load(config.generator_ckpt, map_location="cpu")
+ if "generator" in state_dict:
+ state_dict = state_dict["generator"]
+ elif "model" in state_dict:
+ state_dict = state_dict["model"]
+ # strict=False: dense ckpts lack LTM-only params (e.g. mem_proj); keep init values.
+ missing, unexpected = self.model.generator.load_state_dict(
+ state_dict, strict=False
+ )
+ if missing:
+ print(
+ f" warm-start missing keys ({len(missing)}): "
+ f"{missing[:5]}{'...' if len(missing) > 5 else ''}"
+ )
+ if unexpected:
+ print(
+ f" warm-start unexpected keys ({len(unexpected)}): "
+ f"{unexpected[:5]}{'...' if len(unexpected) > 5 else ''}"
+ )
+
+ ##############################################################################################################
+
+ # # Let's delete EMA params for early steps to save some computes at training and inference
+ # if self.step < config.ema_start_step:
+ # self.generator_ema = None
+
+ self.max_grad_norm = 10.0
+ self.previous_time = None
+
+ def save(self):
+ print("Start gathering distributed model states...")
+ # Both calls are collectives: every rank must participate, only rank 0 ends up
+ # holding the full CPU tensors (rank0_only=True inside the helpers).
+ generator_state_dict = fsdp_state_dict(self.model.generator)
+ optimizer_state_dict = fsdp_optim_state_dict(
+ self.model.generator, self.generator_optimizer)
+
+ wandb_run_id = (
+ wandb.run.id
+ if (self.is_main_process and not self.disable_wandb and wandb.run is not None)
+ else None
+ )
+ state_dict = {
+ "generator": generator_state_dict,
+ "optimizer": optimizer_state_dict,
+ "step": self.step,
+ "seed": int(self.config.seed),
+ "wandb_run_id": wandb_run_id,
+ }
+ # if self.step > self.config.ema_start_step and self.generator_ema is not None:
+ # state_dict["generator_ema"] = self.generator_ema.state_dict()
+
+ if self.is_main_process:
+ os.makedirs(os.path.join(self.output_path,
+ f"checkpoint_model_{self.step:06d}"), exist_ok=True)
+ torch.save(state_dict, os.path.join(self.output_path,
+ f"checkpoint_model_{self.step:06d}", "model.pt"))
+ print("Model saved to", os.path.join(self.output_path,
+ f"checkpoint_model_{self.step:06d}", "model.pt"))
+
+ def train_one_step(self, batch):
+ torch.cuda.synchronize()
+ t1 = time.time()
+ self.log_iters = 1
+
+ if self.step % 20 == 0:
+ torch.cuda.empty_cache()
+
+ # Step 1: Get the next batch of text prompts
+ text_prompts = batch["prompts"]
+ if not self.config.load_raw_video: # precomputed latent
+ clean_latent = batch["ode_latent"][:, -1].to(
+ device=self.device, dtype=self.dtype)
+ else: # encode raw video to latent
+ frames = batch["frames"].to(
+ device=self.device, dtype=self.dtype)
+ with torch.no_grad():
+ clean_latent = self.model.vae.encode_to_latent(
+ frames).to(device=self.device, dtype=self.dtype)
+ image_latent = clean_latent[:, 0:1, ]
+
+ batch_size = len(text_prompts)
+ image_or_video_shape = list(self.config.image_or_video_shape)
+ image_or_video_shape[0] = batch_size
+
+ # Step 2: Build conditional_dict
+ proportion_empty_prompts = getattr(self.config, "proportion_empty_prompts", 0.0)
+
+ if self.use_worldmem_data:
+ # WorldMem: no text conditioning; only actions and viewmats
+ conditional_dict = {}
+ unconditional_dict = {}
+
+ actions = batch["actions"].to(device=self.device, dtype=self.dtype) # (B, T, 25)
+ poses = batch["poses"].to(device=self.device, dtype=self.dtype) # (B, T, 5)
+ viewmats = poses_to_viewmats(poses) # (B, T, 4, 4)
+
+ # CFG: randomly drop action conditioning
+ if proportion_empty_prompts > 0.0:
+ drop_mask = torch.rand(batch_size, device=self.device) < proportion_empty_prompts
+ actions = actions.clone()
+ # viewmats = viewmats.clone()
+ actions[drop_mask] = 0.0
+ # eye = torch.eye(4, dtype=viewmats.dtype, device=viewmats.device)
+ # viewmats[drop_mask] = eye.unsqueeze(0).unsqueeze(0).expand(
+ # drop_mask.sum(), viewmats.shape[1], -1, -1)
+
+ conditional_dict["actions"] = actions
+ conditional_dict["viewmats"] = viewmats
+ else:
+ with torch.no_grad():
+ conditional_dict = self.model.text_encoder(text_prompts=text_prompts)
+
+ if not getattr(self, "unconditional_dict", None):
+ unconditional_dict = self.model.text_encoder(
+ text_prompts=[self.config.negative_prompt] * batch_size)
+ unconditional_dict = {k: v.detach()
+ for k, v in unconditional_dict.items()}
+ self.unconditional_dict = unconditional_dict
+ else:
+ unconditional_dict = self.unconditional_dict
+
+ # Step 3: Train the generator
+ generator_loss, log_dict = self.model.generator_loss(
+ image_or_video_shape=image_or_video_shape,
+ conditional_dict=conditional_dict,
+ unconditional_dict=unconditional_dict,
+ clean_latent=clean_latent,
+ initial_latent=image_latent
+ )
+ self.generator_optimizer.zero_grad()
+ generator_loss.backward()
+ generator_grad_norm = self.model.generator.clip_grad_norm_(
+ self.max_grad_norm)
+ self.generator_optimizer.step()
+
+ # Increment the step since we finished gradient update
+ self.step += 1
+
+ loss_unweighted_val = log_dict["loss_unweighted"].item()
+ wandb_loss_dict = {
+ "generator_loss": generator_loss.item(), # weighted, used for backprop
+ "generator_loss_unweighted": loss_unweighted_val, # raw MSE, comparable across runs
+ "generator_grad_norm": generator_grad_norm.item(),
+ }
+
+ # Per-frame unweighted loss bucketed by frame position (10 frames per bucket)
+ bucket_str = ""
+ loss_per_frame = log_dict.get("loss_per_frame", None)
+ if loss_per_frame is not None:
+ per_frame = loss_per_frame.float().mean(dim=0) # (F,)
+ num_frame = per_frame.shape[0]
+ bucket_size = 10
+ bucket_means = []
+ for start in range(0, num_frame, bucket_size):
+ end = min(start + bucket_size, num_frame)
+ v = per_frame[start:end].mean().item()
+ bucket_means.append((start, end, v))
+ wandb_loss_dict[f"loss_frames_{start:02d}-{end - 1:02d}"] = v
+ bucket_str = " " + " ".join(
+ f"f{start:02d}-{end - 1:02d}={v:.4f}" for start, end, v in bucket_means
+ )
+
+ # Step 4: Logging
+ torch.cuda.synchronize()
+ t2 = time.time()
+ if self.is_main_process:
+ print(
+ f"step={self.step} loss={generator_loss.item():.4f} "
+ f"loss_unweighted={loss_unweighted_val:.4f} "
+ f"grad_norm={generator_grad_norm.item():.4f}",
+ f"time={t2 - t1:.2f} seconds",
+ flush=True,
+ )
+ if not self.disable_wandb:
+ wandb.log(wandb_loss_dict, step=self.step)
+
+ if self.step % self.config.gc_interval == 0:
+ if dist.get_rank() == 0:
+ logging.info("DistGarbageCollector: Running GC.")
+ gc.collect()
+
+ def generate_video(self, pipeline, prompts, image=None):
+ batch_size = len(prompts)
+ sampled_noise = torch.randn(
+ [batch_size, 21, 16, 60, 104], device="cuda", dtype=self.dtype
+ )
+ video, _ = pipeline.inference(
+ noise=sampled_noise,
+ text_prompts=prompts,
+ return_latents=True
+ )
+ current_video = video.permute(0, 1, 3, 4, 2).cpu().numpy() * 255.0
+ return current_video
+
+ def train(self):
+ while True:
+ batch = next(self.dataloader)
+ self.train_one_step(batch)
+ if (not self.config.no_save) and self.step % self.config.log_iters == 0:
+ torch.cuda.empty_cache()
+ self.save()
+ torch.cuda.empty_cache()
+
+ barrier()
+ if self.is_main_process:
+ current_time = time.time()
+ if self.previous_time is None:
+ self.previous_time = current_time
+ else:
+ if not self.disable_wandb:
+ wandb.log({"per iteration time": current_time - self.previous_time}, step=self.step)
+ self.previous_time = current_time
diff --git a/DecMem/trainer/distillation.py b/DecMem/trainer/distillation.py
new file mode 100644
index 0000000000000000000000000000000000000000..7478c452e9e911b21820ac6d2d2da920d2e259b0
--- /dev/null
+++ b/DecMem/trainer/distillation.py
@@ -0,0 +1,380 @@
+import gc
+import logging
+
+from utils.dataset import ShardingLMDBDataset, cycle
+from utils.dataset import TextDataset
+from utils.distributed import EMA_FSDP, fsdp_wrap, fsdp_state_dict, launch_distributed_job
+from utils.misc import (
+ set_seed,
+ merge_dict_list
+)
+import torch.distributed as dist
+from omegaconf import OmegaConf
+from model import CausVid, DMD, SiD
+import torch
+import wandb
+import time
+import os
+
+
+class Trainer:
+ def __init__(self, config):
+ self.config = config
+ self.step = 0
+
+ # Step 1: Initialize the distributed training environment (rank, seed, dtype, logging etc.)
+ torch.backends.cuda.matmul.allow_tf32 = True
+ torch.backends.cudnn.allow_tf32 = True
+
+ launch_distributed_job()
+ global_rank = dist.get_rank()
+ self.world_size = dist.get_world_size()
+
+ self.dtype = torch.bfloat16 if config.mixed_precision else torch.float32
+ self.device = torch.cuda.current_device()
+ self.is_main_process = global_rank == 0
+ self.causal = config.causal
+ self.disable_wandb = config.disable_wandb
+
+ # use a random seed for the training
+ if config.seed == 0:
+ random_seed = torch.randint(0, 10000000, (1,), device=self.device)
+ dist.broadcast(random_seed, src=0)
+ config.seed = random_seed.item()
+
+ set_seed(config.seed + global_rank)
+
+ if self.is_main_process and not self.disable_wandb:
+ wandb.login(host=config.wandb_host, key=config.wandb_key)
+ wandb.init(
+ config=OmegaConf.to_container(config, resolve=True),
+ name=config.config_name,
+ mode="online",
+ entity=config.wandb_entity,
+ project=config.wandb_project,
+ dir=config.wandb_save_dir
+ )
+
+ self.output_path = config.logdir
+
+ # Step 2: Initialize the model and optimizer
+ if config.distribution_loss == "causvid":
+ self.model = CausVid(config, device=self.device)
+ elif config.distribution_loss == "dmd":
+ self.model = DMD(config, device=self.device)
+ elif config.distribution_loss == "sid":
+ self.model = SiD(config, device=self.device)
+ else:
+ raise ValueError("Invalid distribution matching loss")
+
+ # Save pretrained model state_dicts to CPU
+ self.fake_score_state_dict_cpu = self.model.fake_score.state_dict()
+
+ self.model.generator = fsdp_wrap(
+ self.model.generator,
+ sharding_strategy=config.sharding_strategy,
+ mixed_precision=config.mixed_precision,
+ wrap_strategy=config.generator_fsdp_wrap_strategy
+ )
+
+ self.model.real_score = fsdp_wrap(
+ self.model.real_score,
+ sharding_strategy=config.sharding_strategy,
+ mixed_precision=config.mixed_precision,
+ wrap_strategy=config.real_score_fsdp_wrap_strategy
+ )
+
+ self.model.fake_score = fsdp_wrap(
+ self.model.fake_score,
+ sharding_strategy=config.sharding_strategy,
+ mixed_precision=config.mixed_precision,
+ wrap_strategy=config.fake_score_fsdp_wrap_strategy
+ )
+
+ self.model.text_encoder = fsdp_wrap(
+ self.model.text_encoder,
+ sharding_strategy=config.sharding_strategy,
+ mixed_precision=config.mixed_precision,
+ wrap_strategy=config.text_encoder_fsdp_wrap_strategy,
+ cpu_offload=getattr(config, "text_encoder_cpu_offload", False)
+ )
+
+ if not config.no_visualize or config.load_raw_video:
+ self.model.vae = self.model.vae.to(
+ device=self.device, dtype=torch.bfloat16 if config.mixed_precision else torch.float32)
+
+ self.generator_optimizer = torch.optim.AdamW(
+ [param for param in self.model.generator.parameters()
+ if param.requires_grad],
+ lr=config.lr,
+ betas=(config.beta1, config.beta2),
+ weight_decay=config.weight_decay
+ )
+
+ self.critic_optimizer = torch.optim.AdamW(
+ [param for param in self.model.fake_score.parameters()
+ if param.requires_grad],
+ lr=config.lr_critic if hasattr(config, "lr_critic") else config.lr,
+ betas=(config.beta1_critic, config.beta2_critic),
+ weight_decay=config.weight_decay
+ )
+
+ # Step 3: Initialize the dataloader
+ if self.config.i2v:
+ dataset = ShardingLMDBDataset(config.data_path, max_pair=int(1e8))
+ else:
+ dataset = TextDataset(config.data_path)
+ sampler = torch.utils.data.distributed.DistributedSampler(
+ dataset, shuffle=True, drop_last=True)
+ dataloader = torch.utils.data.DataLoader(
+ dataset,
+ batch_size=config.batch_size,
+ sampler=sampler,
+ num_workers=8)
+
+ if dist.get_rank() == 0:
+ print("DATASET SIZE %d" % len(dataset))
+ self.dataloader = cycle(dataloader)
+
+ ##############################################################################################################
+ # 6. Set up EMA parameter containers
+ rename_param = (
+ lambda name: name.replace("_fsdp_wrapped_module.", "")
+ .replace("_checkpoint_wrapped_module.", "")
+ .replace("_orig_mod.", "")
+ )
+ self.name_to_trainable_params = {}
+ for n, p in self.model.generator.named_parameters():
+ if not p.requires_grad:
+ continue
+
+ renamed_n = rename_param(n)
+ self.name_to_trainable_params[renamed_n] = p
+ ema_weight = config.ema_weight
+ self.generator_ema = None
+ # if (ema_weight is not None) and (ema_weight > 0.0):
+ # print(f"Setting up EMA with weight {ema_weight}")
+ # self.generator_ema = EMA_FSDP(self.model.generator, decay=ema_weight)
+
+ ##############################################################################################################
+ # 7. (If resuming) Load the model and optimizer, lr_scheduler, ema's statedicts
+ if getattr(config, "generator_ckpt", False):
+ print(f"Loading pretrained generator from {config.generator_ckpt}")
+ state_dict = torch.load(config.generator_ckpt, map_location="cpu")
+ if "generator" in state_dict:
+ state_dict = state_dict["generator"]
+ elif "model" in state_dict:
+ state_dict = state_dict["model"]
+ self.model.generator.load_state_dict(
+ state_dict, strict=True
+ )
+
+ ##############################################################################################################
+
+ # # Let's delete EMA params for early steps to save some computes at training and inference
+ # if self.step < config.ema_start_step:
+ # self.generator_ema = None
+
+ self.max_grad_norm_generator = getattr(config, "max_grad_norm_generator", 10.0)
+ self.max_grad_norm_critic = getattr(config, "max_grad_norm_critic", 10.0)
+ self.previous_time = None
+
+ def save(self):
+ print("Start gathering distributed model states...")
+ generator_state_dict = fsdp_state_dict(
+ self.model.generator)
+ critic_state_dict = fsdp_state_dict(
+ self.model.fake_score)
+ state_dict = {
+ "generator": generator_state_dict,
+ "critic": critic_state_dict,
+ }
+
+ if self.is_main_process:
+ os.makedirs(os.path.join(self.output_path,
+ f"checkpoint_model_{self.step:06d}"), exist_ok=True)
+ torch.save(state_dict, os.path.join(self.output_path,
+ f"checkpoint_model_{self.step:06d}", "model.pt"))
+ print("Model saved to", os.path.join(self.output_path,
+ f"checkpoint_model_{self.step:06d}", "model.pt"))
+
+ def fwdbwd_one_step(self, batch, train_generator):
+ self.model.eval() # prevent any randomness (e.g. dropout)
+
+ if self.step % 20 == 0:
+ torch.cuda.empty_cache()
+
+ # Step 1: Get the next batch of text prompts
+ text_prompts = batch["prompts"]
+ if self.config.i2v:
+ clean_latent = None
+ image_latent = batch["ode_latent"][:, -1][:, 0:1, ].to(
+ device=self.device, dtype=self.dtype)
+ else:
+ clean_latent = None
+ image_latent = None
+
+ batch_size = len(text_prompts)
+ image_or_video_shape = list(self.config.image_or_video_shape)
+ image_or_video_shape[0] = batch_size
+
+ # Step 2: Extract the conditional infos
+ with torch.no_grad():
+ conditional_dict = self.model.text_encoder(
+ text_prompts=text_prompts)
+
+ if not getattr(self, "unconditional_dict", None):
+ unconditional_dict = self.model.text_encoder(
+ text_prompts=[self.config.negative_prompt] * batch_size)
+ unconditional_dict = {k: v.detach()
+ for k, v in unconditional_dict.items()}
+ self.unconditional_dict = unconditional_dict # cache the unconditional_dict
+ else:
+ unconditional_dict = self.unconditional_dict
+
+ # Step 3: Store gradients for the generator (if training the generator)
+ if train_generator:
+ generator_loss, generator_log_dict = self.model.generator_loss(
+ image_or_video_shape=image_or_video_shape,
+ conditional_dict=conditional_dict,
+ unconditional_dict=unconditional_dict,
+ clean_latent=clean_latent,
+ initial_latent=image_latent if self.config.i2v else None
+ )
+
+ generator_loss.backward()
+ generator_grad_norm = self.model.generator.clip_grad_norm_(
+ self.max_grad_norm_generator)
+
+ generator_log_dict.update({"generator_loss": generator_loss,
+ "generator_grad_norm": generator_grad_norm})
+
+ return generator_log_dict
+ else:
+ generator_log_dict = {}
+
+ # Step 4: Store gradients for the critic (if training the critic)
+ critic_loss, critic_log_dict = self.model.critic_loss(
+ image_or_video_shape=image_or_video_shape,
+ conditional_dict=conditional_dict,
+ unconditional_dict=unconditional_dict,
+ clean_latent=clean_latent,
+ initial_latent=image_latent if self.config.i2v else None
+ )
+
+ critic_loss.backward()
+ critic_grad_norm = self.model.fake_score.clip_grad_norm_(
+ self.max_grad_norm_critic)
+
+ critic_log_dict.update({"critic_loss": critic_loss,
+ "critic_grad_norm": critic_grad_norm})
+
+ return critic_log_dict
+
+ def generate_video(self, pipeline, prompts, image=None):
+ batch_size = len(prompts)
+ if image is not None:
+ image = image.squeeze(0).unsqueeze(0).unsqueeze(2).to(device="cuda", dtype=torch.bfloat16)
+
+ # Encode the input image as the first latent
+ initial_latent = pipeline.vae.encode_to_latent(image).to(device="cuda", dtype=torch.bfloat16)
+ initial_latent = initial_latent.repeat(batch_size, 1, 1, 1, 1)
+ sampled_noise = torch.randn(
+ [batch_size, self.model.num_training_frames - 1, 16, 60, 104],
+ device="cuda",
+ dtype=self.dtype
+ )
+ else:
+ initial_latent = None
+ sampled_noise = torch.randn(
+ [batch_size, self.model.num_training_frames, 16, 60, 104],
+ device="cuda",
+ dtype=self.dtype
+ )
+
+ video, _ = pipeline.inference(
+ noise=sampled_noise,
+ text_prompts=prompts,
+ return_latents=True,
+ initial_latent=initial_latent
+ )
+ current_video = video.permute(0, 1, 3, 4, 2).cpu().numpy() * 255.0
+ return current_video
+
+ def train(self):
+ start_step = self.step
+
+ while True:
+ TRAIN_GENERATOR = self.step % self.config.dfake_gen_update_ratio == 0
+
+ # Train the generator
+ if TRAIN_GENERATOR:
+ self.generator_optimizer.zero_grad(set_to_none=True)
+ extras_list = []
+ batch = next(self.dataloader)
+ extra = self.fwdbwd_one_step(batch, True)
+ extras_list.append(extra)
+ generator_log_dict = merge_dict_list(extras_list)
+ self.generator_optimizer.step()
+ # if self.generator_ema is not None:
+ # self.generator_ema.update(self.model.generator)
+
+ # Train the critic
+ self.critic_optimizer.zero_grad(set_to_none=True)
+ extras_list = []
+ batch = next(self.dataloader)
+ extra = self.fwdbwd_one_step(batch, False)
+ extras_list.append(extra)
+ critic_log_dict = merge_dict_list(extras_list)
+ self.critic_optimizer.step()
+
+ # Increment the step since we finished gradient update
+ self.step += 1
+
+ # # Create EMA params (if not already created)
+ # if (self.step >= self.config.ema_start_step) and \
+ # (self.generator_ema is None) and (self.config.ema_weight > 0):
+ # self.generator_ema = EMA_FSDP(self.model.generator, decay=self.config.ema_weight)
+
+ # Save the model
+ if (not self.config.no_save) and (self.step - start_step) > 0 and self.step % self.config.log_iters == 0:
+ torch.cuda.empty_cache()
+ self.save()
+ torch.cuda.empty_cache()
+
+ # Logging
+ if self.is_main_process:
+ wandb_loss_dict = {}
+ if TRAIN_GENERATOR:
+ wandb_loss_dict.update(
+ {
+ "generator_loss": generator_log_dict["generator_loss"].mean().item(),
+ "generator_grad_norm": generator_log_dict["generator_grad_norm"].mean().item(),
+ "dmdtrain_gradient_norm": generator_log_dict["dmdtrain_gradient_norm"].mean().item()
+ }
+ )
+
+ wandb_loss_dict.update(
+ {
+ "critic_loss": critic_log_dict["critic_loss"].mean().item(),
+ "critic_grad_norm": critic_log_dict["critic_grad_norm"].mean().item()
+ }
+ )
+
+ if not self.disable_wandb:
+ wandb.log(wandb_loss_dict, step=self.step)
+
+ if self.step % self.config.gc_interval == 0:
+ if dist.get_rank() == 0:
+ logging.info("DistGarbageCollector: Running GC.")
+ gc.collect()
+ torch.cuda.empty_cache()
+
+ if self.is_main_process:
+ current_time = time.time()
+ if self.previous_time is None:
+ self.previous_time = current_time
+ else:
+ if not self.disable_wandb:
+ wandb.log({"per iteration time": current_time - self.previous_time}, step=self.step)
+ self.previous_time = current_time
\ No newline at end of file
diff --git a/DecMem/trainer/gan.py b/DecMem/trainer/gan.py
new file mode 100644
index 0000000000000000000000000000000000000000..0f36d1a7f82b12b23da2f80a59b6153b6333384e
--- /dev/null
+++ b/DecMem/trainer/gan.py
@@ -0,0 +1,452 @@
+import gc
+import logging
+
+from utils.dataset import ShardingLMDBDataset, cycle
+from utils.distributed import EMA_FSDP, fsdp_wrap, fsdp_state_dict, launch_distributed_job
+from utils.misc import (
+ set_seed,
+ merge_dict_list
+)
+import torch.distributed as dist
+from omegaconf import OmegaConf
+from model import GAN
+import torch
+import wandb
+import time
+import os
+
+
+class Trainer:
+ def __init__(self, config):
+ self.config = config
+ self.step = 0
+
+ # Step 1: Initialize the distributed training environment (rank, seed, dtype, logging etc.)
+ torch.backends.cuda.matmul.allow_tf32 = True
+ torch.backends.cudnn.allow_tf32 = True
+
+ launch_distributed_job()
+ global_rank = dist.get_rank()
+ self.world_size = dist.get_world_size()
+
+ self.dtype = torch.bfloat16 if config.mixed_precision else torch.float32
+ self.device = torch.cuda.current_device()
+ self.is_main_process = global_rank == 0
+ self.causal = config.causal
+ self.disable_wandb = config.disable_wandb
+
+ # Configuration for discriminator warmup
+ self.discriminator_warmup_steps = getattr(config, "discriminator_warmup_steps", 0)
+ self.in_discriminator_warmup = self.step < self.discriminator_warmup_steps
+ if self.in_discriminator_warmup and self.is_main_process:
+ print(f"Starting with discriminator warmup for {self.discriminator_warmup_steps} steps")
+ self.loss_scale = getattr(config, "loss_scale", 1.0)
+
+ # use a random seed for the training
+ if config.seed == 0:
+ random_seed = torch.randint(0, 10000000, (1,), device=self.device)
+ dist.broadcast(random_seed, src=0)
+ config.seed = random_seed.item()
+
+ set_seed(config.seed + global_rank)
+
+ if self.is_main_process and not self.disable_wandb:
+ wandb.login(host=config.wandb_host, key=config.wandb_key)
+ wandb.init(
+ config=OmegaConf.to_container(config, resolve=True),
+ name=config.config_name,
+ mode="online",
+ entity=config.wandb_entity,
+ project=config.wandb_project,
+ dir=config.wandb_save_dir
+ )
+
+ self.output_path = config.logdir
+
+ # Step 2: Initialize the model and optimizer
+ self.model = GAN(config, device=self.device)
+
+ self.model.generator = fsdp_wrap(
+ self.model.generator,
+ sharding_strategy=config.sharding_strategy,
+ mixed_precision=config.mixed_precision,
+ wrap_strategy=config.generator_fsdp_wrap_strategy
+ )
+
+ self.model.fake_score = fsdp_wrap(
+ self.model.fake_score,
+ sharding_strategy=config.sharding_strategy,
+ mixed_precision=config.mixed_precision,
+ wrap_strategy=config.fake_score_fsdp_wrap_strategy
+ )
+
+ self.model.text_encoder = fsdp_wrap(
+ self.model.text_encoder,
+ sharding_strategy=config.sharding_strategy,
+ mixed_precision=config.mixed_precision,
+ wrap_strategy=config.text_encoder_fsdp_wrap_strategy,
+ cpu_offload=getattr(config, "text_encoder_cpu_offload", False)
+ )
+
+ if not config.no_visualize or config.load_raw_video:
+ self.model.vae = self.model.vae.to(
+ device=self.device, dtype=torch.bfloat16 if config.mixed_precision else torch.float32)
+
+ self.generator_optimizer = torch.optim.AdamW(
+ [param for param in self.model.generator.parameters()
+ if param.requires_grad],
+ lr=config.gen_lr,
+ betas=(config.beta1, config.beta2)
+ )
+
+ # Create separate parameter groups for the fake_score network
+ # One group for parameters with "_cls_pred_branch" or "_gan_ca_blocks" in the name
+ # and another group for all other parameters
+ fake_score_params = []
+ discriminator_params = []
+
+ for name, param in self.model.fake_score.named_parameters():
+ if param.requires_grad:
+ if "_cls_pred_branch" in name or "_gan_ca_blocks" in name:
+ discriminator_params.append(param)
+ else:
+ fake_score_params.append(param)
+
+ # Use the special learning rate for the special parameter group
+ # and the default critic learning rate for other parameters
+ self.critic_param_groups = [
+ {'params': fake_score_params, 'lr': config.critic_lr},
+ {'params': discriminator_params, 'lr': config.critic_lr * config.discriminator_lr_multiplier}
+ ]
+ if self.in_discriminator_warmup:
+ self.critic_optimizer = torch.optim.AdamW(
+ self.critic_param_groups,
+ betas=(0.9, config.beta2_critic)
+ )
+ else:
+ self.critic_optimizer = torch.optim.AdamW(
+ self.critic_param_groups,
+ betas=(config.beta1_critic, config.beta2_critic)
+ )
+
+ # Step 3: Initialize the dataloader
+ self.data_path = config.data_path
+ dataset = ShardingLMDBDataset(config.data_path, max_pair=int(1e8))
+ sampler = torch.utils.data.distributed.DistributedSampler(
+ dataset, shuffle=True, drop_last=True)
+ dataloader = torch.utils.data.DataLoader(
+ dataset,
+ batch_size=config.batch_size,
+ sampler=sampler,
+ num_workers=8)
+
+ if dist.get_rank() == 0:
+ print("DATASET SIZE %d" % len(dataset))
+
+ self.dataloader = cycle(dataloader)
+
+ ##############################################################################################################
+ # 6. Set up EMA parameter containers
+ rename_param = (
+ lambda name: name.replace("_fsdp_wrapped_module.", "")
+ .replace("_checkpoint_wrapped_module.", "")
+ .replace("_orig_mod.", "")
+ )
+ self.name_to_trainable_params = {}
+ for n, p in self.model.generator.named_parameters():
+ if not p.requires_grad:
+ continue
+
+ renamed_n = rename_param(n)
+ self.name_to_trainable_params[renamed_n] = p
+ ema_weight = config.ema_weight
+ self.generator_ema = None
+ if (ema_weight is not None) and (ema_weight > 0.0):
+ print(f"Setting up EMA with weight {ema_weight}")
+ self.generator_ema = EMA_FSDP(self.model.generator, decay=ema_weight)
+
+ ##############################################################################################################
+ # 7. (If resuming) Load the model and optimizer, lr_scheduler, ema's statedicts
+ if getattr(config, "generator_ckpt", False):
+ print(f"Loading pretrained generator from {config.generator_ckpt}")
+ state_dict = torch.load(config.generator_ckpt, map_location="cpu")
+ if "generator" in state_dict:
+ state_dict = state_dict["generator"]
+ elif "model" in state_dict:
+ state_dict = state_dict["model"]
+ self.model.generator.load_state_dict(
+ state_dict, strict=True
+ )
+ if hasattr(config, "load"):
+ resume_ckpt_path_critic = os.path.join(config.load, "critic")
+ resume_ckpt_path_generator = os.path.join(config.load, "generator")
+ else:
+ resume_ckpt_path_critic = "none"
+ resume_ckpt_path_generator = "none"
+
+ _, _ = self.checkpointer_critic.try_best_load(
+ resume_ckpt_path=resume_ckpt_path_critic,
+ )
+ self.step, _ = self.checkpointer_generator.try_best_load(
+ resume_ckpt_path=resume_ckpt_path_generator,
+ force_start_w_ema=config.force_start_w_ema,
+ force_reset_zero_step=config.force_reset_zero_step,
+ force_reinit_ema=config.force_reinit_ema,
+ skip_optimizer_scheduler=config.skip_optimizer_scheduler,
+ )
+
+ ##############################################################################################################
+
+ # # Let's delete EMA params for early steps to save some computes at training and inference
+ # if self.step < config.ema_start_step:
+ # self.generator_ema = None
+
+ self.max_grad_norm_generator = getattr(config, "max_grad_norm_generator", 10.0)
+ self.max_grad_norm_critic = getattr(config, "max_grad_norm_critic", 10.0)
+ self.previous_time = None
+
+ def save(self):
+ print("Start gathering distributed model states...")
+ generator_state_dict = fsdp_state_dict(
+ self.model.generator)
+ critic_state_dict = fsdp_state_dict(
+ self.model.fake_score)
+
+ state_dict = {
+ "generator": generator_state_dict,
+ "critic": critic_state_dict,
+ }
+
+ if self.is_main_process:
+ os.makedirs(os.path.join(self.output_path,
+ f"checkpoint_model_{self.step:06d}"), exist_ok=True)
+ torch.save(state_dict, os.path.join(self.output_path,
+ f"checkpoint_model_{self.step:06d}", "model.pt"))
+ print("Model saved to", os.path.join(self.output_path,
+ f"checkpoint_model_{self.step:06d}", "model.pt"))
+
+ def fwdbwd_one_step(self, batch, train_generator):
+ self.model.eval() # prevent any randomness (e.g. dropout)
+
+ if self.step % 20 == 0:
+ torch.cuda.empty_cache()
+
+ # Step 1: Get the next batch of text prompts
+ text_prompts = batch["prompts"] # next(self.dataloader)
+ if "ode_latent" in batch:
+ clean_latent = batch["ode_latent"][:, -1].to(device=self.device, dtype=self.dtype)
+ else:
+ frames = batch["frames"].to(device=self.device, dtype=self.dtype)
+ with torch.no_grad():
+ clean_latent = self.model.vae.encode_to_latent(
+ frames).to(device=self.device, dtype=self.dtype)
+
+ image_latent = clean_latent[:, 0:1, ]
+
+ batch_size = len(text_prompts)
+ image_or_video_shape = list(self.config.image_or_video_shape)
+ image_or_video_shape[0] = batch_size
+
+ # Step 2: Extract the conditional infos
+ with torch.no_grad():
+ conditional_dict = self.model.text_encoder(
+ text_prompts=text_prompts)
+
+ if not getattr(self, "unconditional_dict", None):
+ unconditional_dict = self.model.text_encoder(
+ text_prompts=[self.config.negative_prompt] * batch_size)
+ unconditional_dict = {k: v.detach()
+ for k, v in unconditional_dict.items()}
+ self.unconditional_dict = unconditional_dict # cache the unconditional_dict
+ else:
+ unconditional_dict = self.unconditional_dict
+
+ mini_bs, full_bs = (
+ batch["mini_bs"],
+ batch["full_bs"],
+ )
+
+ # Step 3: Store gradients for the generator (if training the generator)
+ if train_generator:
+ gan_G_loss = self.model.generator_loss(
+ image_or_video_shape=image_or_video_shape,
+ conditional_dict=conditional_dict,
+ unconditional_dict=unconditional_dict,
+ clean_latent=clean_latent,
+ initial_latent=image_latent if self.config.i2v else None
+ )
+
+ loss_ratio = mini_bs * self.world_size / full_bs
+ total_loss = gan_G_loss * loss_ratio * self.loss_scale
+
+ total_loss.backward()
+ generator_grad_norm = self.model.generator.clip_grad_norm_(
+ self.max_grad_norm_generator)
+
+ generator_log_dict = {"generator_grad_norm": generator_grad_norm,
+ "gan_G_loss": gan_G_loss}
+
+ return generator_log_dict
+ else:
+ generator_log_dict = {}
+
+ # Step 4: Store gradients for the critic (if training the critic)
+ (gan_D_loss, r1_loss, r2_loss), critic_log_dict = self.model.critic_loss(
+ image_or_video_shape=image_or_video_shape,
+ conditional_dict=conditional_dict,
+ unconditional_dict=unconditional_dict,
+ clean_latent=clean_latent,
+ real_image_or_video=clean_latent,
+ initial_latent=image_latent if self.config.i2v else None
+ )
+
+ loss_ratio = mini_bs * dist.get_world_size() / full_bs
+ total_loss = (gan_D_loss + 0.5 * (r1_loss + r2_loss)) * loss_ratio * self.loss_scale
+
+ total_loss.backward()
+ critic_grad_norm = self.model.fake_score.clip_grad_norm_(
+ self.max_grad_norm_critic)
+
+ critic_log_dict.update({"critic_grad_norm": critic_grad_norm,
+ "gan_D_loss": gan_D_loss,
+ "r1_loss": r1_loss,
+ "r2_loss": r2_loss})
+
+ return critic_log_dict
+
+ def generate_video(self, pipeline, prompts, image=None):
+ batch_size = len(prompts)
+ sampled_noise = torch.randn(
+ [batch_size, 21, 16, 60, 104], device="cuda", dtype=self.dtype
+ )
+ video, _ = pipeline.inference(
+ noise=sampled_noise,
+ text_prompts=prompts,
+ return_latents=True
+ )
+ current_video = video.permute(0, 1, 3, 4, 2).cpu().numpy() * 255.0
+ return current_video
+
+ def train(self):
+ start_step = self.step
+
+ while True:
+ if self.step == self.discriminator_warmup_steps and self.discriminator_warmup_steps != 0:
+ print("Resetting critic optimizer")
+ del self.critic_optimizer
+ torch.cuda.empty_cache()
+ # Create new optimizers
+ self.critic_optimizer = torch.optim.AdamW(
+ self.critic_param_groups,
+ betas=(self.config.beta1_critic, self.config.beta2_critic)
+ )
+ # Update checkpointer references
+ self.checkpointer_critic.optimizer = self.critic_optimizer
+ # Check if we're in the discriminator warmup phase
+ self.in_discriminator_warmup = self.step < self.discriminator_warmup_steps
+
+ # Only update generator and critic outside the warmup phase
+ TRAIN_GENERATOR = not self.in_discriminator_warmup and self.step % self.config.dfake_gen_update_ratio == 0
+
+ # Train the generator (only outside warmup phase)
+ if TRAIN_GENERATOR:
+ self.model.fake_score.requires_grad_(False)
+ self.model.generator.requires_grad_(True)
+ self.generator_optimizer.zero_grad(set_to_none=True)
+ extras_list = []
+ for ii, mini_batch in enumerate(self.dataloader.next()):
+ extra = self.fwdbwd_one_step(mini_batch, True)
+ extras_list.append(extra)
+ generator_log_dict = merge_dict_list(extras_list)
+ self.generator_optimizer.step()
+ # if self.generator_ema is not None:
+ # self.generator_ema.update(self.model.generator)
+ else:
+ generator_log_dict = {}
+
+ # Train the critic/discriminator
+ if self.in_discriminator_warmup:
+ # During warmup, only allow gradient for discriminator params
+ self.model.generator.requires_grad_(False)
+ self.model.fake_score.requires_grad_(False)
+
+ # Enable gradient only for discriminator params
+ for name, param in self.model.fake_score.named_parameters():
+ if "_cls_pred_branch" in name or "_gan_ca_blocks" in name:
+ param.requires_grad_(True)
+ else:
+ # Normal training mode
+ self.model.generator.requires_grad_(False)
+ self.model.fake_score.requires_grad_(True)
+
+ self.critic_optimizer.zero_grad(set_to_none=True)
+ extras_list = []
+ batch = next(self.dataloader)
+ extra = self.fwdbwd_one_step(batch, False)
+ extras_list.append(extra)
+ critic_log_dict = merge_dict_list(extras_list)
+ self.critic_optimizer.step()
+
+ # Increment the step since we finished gradient update
+ self.step += 1
+
+ # If we just finished warmup, print a message
+ if self.is_main_process and self.step == self.discriminator_warmup_steps:
+ print(f"Finished discriminator warmup after {self.discriminator_warmup_steps} steps")
+
+ # Save the model
+ if (not self.config.no_save) and (self.step - start_step) > 0 and self.step % self.config.log_iters == 0:
+ torch.cuda.empty_cache()
+ self.save()
+ torch.cuda.empty_cache()
+
+ # Logging
+ wandb_loss_dict = {
+ "generator_grad_norm": generator_log_dict["generator_grad_norm"],
+ "critic_grad_norm": critic_log_dict["critic_grad_norm"],
+ "real_logit": critic_log_dict["noisy_real_logit"],
+ "fake_logit": critic_log_dict["noisy_fake_logit"],
+ "r1_loss": critic_log_dict["r1_loss"],
+ "r2_loss": critic_log_dict["r2_loss"],
+ }
+ if TRAIN_GENERATOR:
+ wandb_loss_dict.update({
+ "generator_grad_norm": generator_log_dict["generator_grad_norm"],
+ })
+ self.all_gather_dict(wandb_loss_dict)
+ wandb_loss_dict["diff_logit"] = wandb_loss_dict["real_logit"] - wandb_loss_dict["fake_logit"]
+ wandb_loss_dict["reg_loss"] = 0.5 * (wandb_loss_dict["r1_loss"] + wandb_loss_dict["r2_loss"])
+
+ if self.is_main_process:
+ if self.in_discriminator_warmup:
+ warmup_status = f"[WARMUP {self.step}/{self.discriminator_warmup_steps}] Training only discriminator params"
+ print(warmup_status)
+ if not self.disable_wandb:
+ wandb_loss_dict.update({"warmup_status": 1.0})
+
+ if not self.disable_wandb:
+ wandb.log(wandb_loss_dict, step=self.step)
+
+ if self.step % self.config.gc_interval == 0:
+ if dist.get_rank() == 0:
+ logging.info("DistGarbageCollector: Running GC.")
+ gc.collect()
+ torch.cuda.empty_cache()
+
+ if self.is_main_process:
+ current_time = time.time()
+ if self.previous_time is None:
+ self.previous_time = current_time
+ else:
+ if not self.disable_wandb:
+ wandb.log({"per iteration time": current_time - self.previous_time}, step=self.step)
+ self.previous_time = current_time
+
+ def all_gather_dict(self, target_dict):
+ for key, value in target_dict.items():
+ gathered_value = torch.zeros(
+ [self.world_size, *value.shape],
+ dtype=value.dtype, device=self.device)
+ dist.all_gather_into_tensor(gathered_value, value)
+ avg_value = gathered_value.mean().item()
+ target_dict[key] = avg_value
diff --git a/DecMem/trainer/ode.py b/DecMem/trainer/ode.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b48830d057e9bde8876264c6a846950f786806a
--- /dev/null
+++ b/DecMem/trainer/ode.py
@@ -0,0 +1,242 @@
+import gc
+import logging
+from utils.dataset import ODERegressionLMDBDataset, cycle
+from model import ODERegression
+from collections import defaultdict
+from utils.misc import (
+ set_seed
+)
+import torch.distributed as dist
+from omegaconf import OmegaConf
+import torch
+import wandb
+import time
+import os
+
+from utils.distributed import barrier, fsdp_wrap, fsdp_state_dict, launch_distributed_job
+
+
+class Trainer:
+ def __init__(self, config):
+ self.config = config
+ self.step = 0
+
+ # Step 1: Initialize the distributed training environment (rank, seed, dtype, logging etc.)
+ torch.backends.cuda.matmul.allow_tf32 = True
+ torch.backends.cudnn.allow_tf32 = True
+
+ launch_distributed_job()
+ global_rank = dist.get_rank()
+ self.world_size = dist.get_world_size()
+
+ self.dtype = torch.bfloat16 if config.mixed_precision else torch.float32
+ self.device = torch.cuda.current_device()
+ self.is_main_process = global_rank == 0
+ self.disable_wandb = config.disable_wandb
+
+ # use a random seed for the training
+ if config.seed == 0:
+ random_seed = torch.randint(0, 10000000, (1,), device=self.device)
+ dist.broadcast(random_seed, src=0)
+ config.seed = random_seed.item()
+
+ set_seed(config.seed + global_rank)
+
+ if self.is_main_process and not self.disable_wandb:
+ wandb.login(host=config.wandb_host, key=config.wandb_key)
+ wandb.init(
+ config=OmegaConf.to_container(config, resolve=True),
+ name=config.config_name,
+ mode="online",
+ entity=config.wandb_entity,
+ project=config.wandb_project,
+ dir=config.wandb_save_dir
+ )
+
+ self.output_path = config.logdir
+
+ # Step 2: Initialize the model and optimizer
+
+ assert config.distribution_loss == "ode", "Only ODE loss is supported for ODE training"
+ self.model = ODERegression(config, device=self.device)
+
+ self.model.generator = fsdp_wrap(
+ self.model.generator,
+ sharding_strategy=config.sharding_strategy,
+ mixed_precision=config.mixed_precision,
+ wrap_strategy=config.generator_fsdp_wrap_strategy
+ )
+ self.model.text_encoder = fsdp_wrap(
+ self.model.text_encoder,
+ sharding_strategy=config.sharding_strategy,
+ mixed_precision=config.mixed_precision,
+ wrap_strategy=config.text_encoder_fsdp_wrap_strategy,
+ cpu_offload=getattr(config, "text_encoder_cpu_offload", False)
+ )
+
+ if not config.no_visualize or config.load_raw_video:
+ self.model.vae = self.model.vae.to(
+ device=self.device, dtype=torch.bfloat16 if config.mixed_precision else torch.float32)
+
+ self.generator_optimizer = torch.optim.AdamW(
+ [param for param in self.model.generator.parameters()
+ if param.requires_grad],
+ lr=config.lr,
+ betas=(config.beta1, config.beta2),
+ weight_decay=config.weight_decay
+ )
+
+ # Step 3: Initialize the dataloader
+ dataset = ODERegressionLMDBDataset(
+ config.data_path, max_pair=getattr(config, "max_pair", int(1e8)))
+ sampler = torch.utils.data.distributed.DistributedSampler(
+ dataset, shuffle=True, drop_last=True)
+ dataloader = torch.utils.data.DataLoader(
+ dataset, batch_size=config.batch_size, sampler=sampler, num_workers=8)
+ total_batch_size = getattr(config, "total_batch_size", None)
+ if total_batch_size is not None:
+ assert total_batch_size == config.batch_size * self.world_size, "Gradient accumulation is not supported for ODE training"
+ self.dataloader = cycle(dataloader)
+
+ self.step = 0
+
+ ##############################################################################################################
+ # 7. (If resuming) Load the model and optimizer, lr_scheduler, ema's statedicts
+ if getattr(config, "generator_ckpt", False):
+ print(f"Loading pretrained generator from {config.generator_ckpt}")
+ state_dict = torch.load(config.generator_ckpt, map_location="cpu")[
+ 'generator']
+ self.model.generator.load_state_dict(
+ state_dict, strict=True
+ )
+
+ ##############################################################################################################
+
+ self.max_grad_norm = 10.0
+ self.previous_time = None
+
+ def save(self):
+ print("Start gathering distributed model states...")
+ generator_state_dict = fsdp_state_dict(
+ self.model.generator)
+ state_dict = {
+ "generator": generator_state_dict
+ }
+
+ if self.is_main_process:
+ os.makedirs(os.path.join(self.output_path,
+ f"checkpoint_model_{self.step:06d}"), exist_ok=True)
+ torch.save(state_dict, os.path.join(self.output_path,
+ f"checkpoint_model_{self.step:06d}", "model.pt"))
+ print("Model saved to", os.path.join(self.output_path,
+ f"checkpoint_model_{self.step:06d}", "model.pt"))
+
+ def train_one_step(self):
+ VISUALIZE = self.step % 100 == 0
+ self.model.eval() # prevent any randomness (e.g. dropout)
+
+ # Step 1: Get the next batch of text prompts
+ batch = next(self.dataloader)
+ text_prompts = batch["prompts"]
+ ode_latent = batch["ode_latent"].to(
+ device=self.device, dtype=self.dtype)
+
+ # Step 2: Extract the conditional infos
+ with torch.no_grad():
+ conditional_dict = self.model.text_encoder(
+ text_prompts=text_prompts)
+
+ # Step 3: Train the generator
+ generator_loss, log_dict = self.model.generator_loss(
+ ode_latent=ode_latent,
+ conditional_dict=conditional_dict
+ )
+
+ unnormalized_loss = log_dict["unnormalized_loss"]
+ timestep = log_dict["timestep"]
+
+ if self.world_size > 1:
+ gathered_unnormalized_loss = torch.zeros(
+ [self.world_size, *unnormalized_loss.shape],
+ dtype=unnormalized_loss.dtype, device=self.device)
+ gathered_timestep = torch.zeros(
+ [self.world_size, *timestep.shape],
+ dtype=timestep.dtype, device=self.device)
+
+ dist.all_gather_into_tensor(
+ gathered_unnormalized_loss, unnormalized_loss)
+ dist.all_gather_into_tensor(gathered_timestep, timestep)
+ else:
+ gathered_unnormalized_loss = unnormalized_loss
+ gathered_timestep = timestep
+
+ loss_breakdown = defaultdict(list)
+ stats = {}
+
+ for index, t in enumerate(timestep):
+ loss_breakdown[str(int(t.item()) // 250 * 250)].append(
+ unnormalized_loss[index].item())
+
+ for key_t in loss_breakdown.keys():
+ stats["loss_at_time_" + key_t] = sum(loss_breakdown[key_t]) / \
+ len(loss_breakdown[key_t])
+
+ self.generator_optimizer.zero_grad()
+ generator_loss.backward()
+ generator_grad_norm = self.model.generator.clip_grad_norm_(
+ self.max_grad_norm)
+ self.generator_optimizer.step()
+
+ # Step 4: Visualization
+ if VISUALIZE and not self.config.no_visualize and not self.config.disable_wandb and self.is_main_process:
+ # Visualize the input, output, and ground truth
+ input = log_dict["input"]
+ output = log_dict["output"]
+ ground_truth = ode_latent[:, -1]
+
+ input_video = self.model.vae.decode_to_pixel(input)
+ output_video = self.model.vae.decode_to_pixel(output)
+ ground_truth_video = self.model.vae.decode_to_pixel(ground_truth)
+ input_video = 255.0 * (input_video.cpu().numpy() * 0.5 + 0.5)
+ output_video = 255.0 * (output_video.cpu().numpy() * 0.5 + 0.5)
+ ground_truth_video = 255.0 * (ground_truth_video.cpu().numpy() * 0.5 + 0.5)
+
+ # Visualize the input, output, and ground truth
+ wandb.log({
+ "input": wandb.Video(input_video, caption="Input", fps=16, format="mp4"),
+ "output": wandb.Video(output_video, caption="Output", fps=16, format="mp4"),
+ "ground_truth": wandb.Video(ground_truth_video, caption="Ground Truth", fps=16, format="mp4"),
+ }, step=self.step)
+
+ # Step 5: Logging
+ if self.is_main_process and not self.disable_wandb:
+ wandb_loss_dict = {
+ "generator_loss": generator_loss.item(),
+ "generator_grad_norm": generator_grad_norm.item(),
+ **stats
+ }
+ wandb.log(wandb_loss_dict, step=self.step)
+
+ if self.step % self.config.gc_interval == 0:
+ if dist.get_rank() == 0:
+ logging.info("DistGarbageCollector: Running GC.")
+ gc.collect()
+
+ def train(self):
+ while True:
+ self.train_one_step()
+ if (not self.config.no_save) and self.step % self.config.log_iters == 0:
+ self.save()
+ torch.cuda.empty_cache()
+
+ barrier()
+ if self.is_main_process:
+ current_time = time.time()
+ if self.previous_time is None:
+ self.previous_time = current_time
+ else:
+ if not self.disable_wandb:
+ wandb.log({"per iteration time": current_time - self.previous_time}, step=self.step)
+ self.previous_time = current_time
+
+ self.step += 1
diff --git a/DecMem/utils/action_encoder.py b/DecMem/utils/action_encoder.py
new file mode 100644
index 0000000000000000000000000000000000000000..6dbf845ab08154e5b472343c97bf2a0d42bb61b2
--- /dev/null
+++ b/DecMem/utils/action_encoder.py
@@ -0,0 +1,47 @@
+import torch
+import torch.nn as nn
+from einops import rearrange
+
+
+class ActionEncoder(nn.Module):
+ """Encode raw per-video-frame action vectors and project to latent spatial dim.
+
+ Input: (B, T_vid, action_dim) where T_vid = (T_lat - 1) * 4 + 1
+ Output: (B, T_lat, C, 1, 1) broadcastable latent-space embeddings.
+
+ Packing strategy:
+ - Frame 0 (first latent frame): the single raw action is repeated 4 times
+ so it has the same packed dimension as grouped frames.
+ - Frames 1..(T_vid-1): every 4 consecutive raw actions are flattened into
+ one latent frame, giving T_lat - 1 grouped frames.
+ The linear layer therefore receives 4 * action_dim features per latent frame.
+ """
+
+ def __init__(self, action_dim: int = 25, hidden_dim: int = 64, latent_channels: int = 16):
+ super().__init__()
+ self.latent_channels = latent_channels
+ # self.net = nn.Sequential(
+ # nn.Linear(action_dim * 4, hidden_dim),
+ # nn.SiLU(),
+ # nn.Linear(hidden_dim, latent_channels),
+ # )
+ self.net = nn.Linear(action_dim * 4, latent_channels, bias=False)
+
+ def forward(self, actions: torch.Tensor) -> torch.Tensor:
+ """
+ Args:
+ actions: (B, T_vid, action_dim) T_vid = (T_lat - 1) * 4 + 1
+ Returns:
+ (B, T_lat, C, 1, 1)
+ """
+ B, T_vid, D = actions.shape
+ # First frame: repeat raw action 4 times to match packed dim of grouped frames
+ action_first = actions[:, :1].unsqueeze(2).repeat(1, 1, 4, 1) # (B, 1, 4, D)
+ # Remaining frames: group every 4 raw frames into one latent frame
+ actions_rest = actions[:, 1:].reshape(B, (T_vid - 1) // 4, 4, D) # (B, T_lat-1, 4, D)
+ # Concatenate along the latent-frame dimension
+ actions = torch.cat([action_first, actions_rest], dim=1) # (B, T_lat, 4, D)
+ # Flatten 4 raw actions into the feature dim
+ actions = rearrange(actions, "b f p d -> b f (p d)") # (B, T_lat, 4*D)
+ emb = self.net(actions) # (B, T_lat, C)
+ return emb.unsqueeze(-1).unsqueeze(-1) # (B, T_lat, C, 1, 1)
diff --git a/DecMem/utils/dataset.py b/DecMem/utils/dataset.py
new file mode 100644
index 0000000000000000000000000000000000000000..66af96e10c91c9d831d05c11b325e7b2c32775f5
--- /dev/null
+++ b/DecMem/utils/dataset.py
@@ -0,0 +1,306 @@
+from utils.lmdb import get_array_shape_from_lmdb, retrieve_row_from_lmdb
+from utils.pose_utils import extract_pose_action_from_npz
+from torch.utils.data import Dataset
+import numpy as np
+import torch
+import lmdb
+import json
+import csv
+from pathlib import Path
+from PIL import Image
+import os
+
+
+class TextDataset(Dataset):
+ def __init__(self, prompt_path, extended_prompt_path=None):
+ with open(prompt_path, encoding="utf-8") as f:
+ self.prompt_list = [line.rstrip() for line in f]
+
+ if extended_prompt_path is not None:
+ with open(extended_prompt_path, encoding="utf-8") as f:
+ self.extended_prompt_list = [line.rstrip() for line in f]
+ assert len(self.extended_prompt_list) == len(self.prompt_list)
+ else:
+ self.extended_prompt_list = None
+
+ def __len__(self):
+ return len(self.prompt_list)
+
+ def __getitem__(self, idx):
+ batch = {
+ "prompts": self.prompt_list[idx],
+ "idx": idx,
+ }
+ if self.extended_prompt_list is not None:
+ batch["extended_prompts"] = self.extended_prompt_list[idx]
+ return batch
+
+
+class ODERegressionLMDBDataset(Dataset):
+ def __init__(self, data_path: str, max_pair: int = int(1e8)):
+ self.env = lmdb.open(data_path, readonly=True,
+ lock=False, readahead=False, meminit=False)
+
+ self.latents_shape = get_array_shape_from_lmdb(self.env, 'latents')
+ self.max_pair = max_pair
+
+ def __len__(self):
+ return min(self.latents_shape[0], self.max_pair)
+
+ def __getitem__(self, idx):
+ """
+ Outputs:
+ - prompts: List of Strings
+ - latents: Tensor of shape (num_denoising_steps, num_frames, num_channels, height, width). It is ordered from pure noise to clean image.
+ """
+ latents = retrieve_row_from_lmdb(
+ self.env,
+ "latents", np.float16, idx, shape=self.latents_shape[1:]
+ )
+
+ if len(latents.shape) == 4:
+ latents = latents[None, ...]
+
+ prompts = retrieve_row_from_lmdb(
+ self.env,
+ "prompts", str, idx
+ )
+ return {
+ "prompts": prompts,
+ "ode_latent": torch.tensor(latents, dtype=torch.float32)
+ }
+
+
+class ShardingLMDBDataset(Dataset):
+ def __init__(self, data_path: str, max_pair: int = int(1e8)):
+ self.envs = []
+ self.index = []
+
+ for fname in sorted(os.listdir(data_path)):
+ path = os.path.join(data_path, fname)
+ env = lmdb.open(path,
+ readonly=True,
+ lock=False,
+ readahead=False,
+ meminit=False)
+ self.envs.append(env)
+
+ self.latents_shape = [None] * len(self.envs)
+ for shard_id, env in enumerate(self.envs):
+ self.latents_shape[shard_id] = get_array_shape_from_lmdb(env, 'latents')
+ for local_i in range(self.latents_shape[shard_id][0]):
+ self.index.append((shard_id, local_i))
+
+ # print("shard_id ", shard_id, " local_i ", local_i)
+
+ self.max_pair = max_pair
+
+ def __len__(self):
+ return len(self.index)
+
+ def __getitem__(self, idx):
+ """
+ Outputs:
+ - prompts: List of Strings
+ - latents: Tensor of shape (num_denoising_steps, num_frames, num_channels, height, width). It is ordered from pure noise to clean image.
+ """
+ shard_id, local_idx = self.index[idx]
+
+ latents = retrieve_row_from_lmdb(
+ self.envs[shard_id],
+ "latents", np.float16, local_idx,
+ shape=self.latents_shape[shard_id][1:]
+ )
+
+ if len(latents.shape) == 4:
+ latents = latents[None, ...]
+
+ prompts = retrieve_row_from_lmdb(
+ self.envs[shard_id],
+ "prompts", str, local_idx
+ )
+
+ return {
+ "prompts": prompts,
+ "ode_latent": torch.tensor(latents, dtype=torch.float32)
+ }
+
+
+class TextImagePairDataset(Dataset):
+ def __init__(
+ self,
+ data_dir,
+ transform=None,
+ eval_first_n=-1,
+ pad_to_multiple_of=None
+ ):
+ """
+ Args:
+ data_dir (str): Path to the directory containing:
+ - target_crop_info_*.json (metadata file)
+ - */ (subdirectory containing images with matching aspect ratio)
+ transform (callable, optional): Optional transform to be applied on the image
+ """
+ self.transform = transform
+ data_dir = Path(data_dir)
+
+ # Find the metadata JSON file
+ metadata_files = list(data_dir.glob('target_crop_info_*.json'))
+ if not metadata_files:
+ raise FileNotFoundError(f"No metadata file found in {data_dir}")
+ if len(metadata_files) > 1:
+ raise ValueError(f"Multiple metadata files found in {data_dir}")
+
+ metadata_path = metadata_files[0]
+ # Extract aspect ratio from metadata filename (e.g. target_crop_info_26-15.json -> 26-15)
+ aspect_ratio = metadata_path.stem.split('_')[-1]
+
+ # Use aspect ratio subfolder for images
+ self.image_dir = data_dir / aspect_ratio
+ if not self.image_dir.exists():
+ raise FileNotFoundError(f"Image directory not found: {self.image_dir}")
+
+ # Load metadata
+ with open(metadata_path, 'r') as f:
+ self.metadata = json.load(f)
+
+ eval_first_n = eval_first_n if eval_first_n != -1 else len(self.metadata)
+ self.metadata = self.metadata[:eval_first_n]
+
+ # Verify all images exist
+ for item in self.metadata:
+ image_path = self.image_dir / item['file_name']
+ if not image_path.exists():
+ raise FileNotFoundError(f"Image not found: {image_path}")
+
+ self.dummy_prompt = "DUMMY PROMPT"
+ self.pre_pad_len = len(self.metadata)
+ if pad_to_multiple_of is not None and len(self.metadata) % pad_to_multiple_of != 0:
+ # Duplicate the last entry
+ self.metadata += [self.metadata[-1]] * (
+ pad_to_multiple_of - len(self.metadata) % pad_to_multiple_of
+ )
+
+ def __len__(self):
+ return len(self.metadata)
+
+ def __getitem__(self, idx):
+ """
+ Returns:
+ dict: A dictionary containing:
+ - image: PIL Image
+ - caption: str
+ - target_bbox: list of int [x1, y1, x2, y2]
+ - target_ratio: str
+ - type: str
+ - origin_size: tuple of int (width, height)
+ """
+ item = self.metadata[idx]
+
+ # Load image
+ image_path = self.image_dir / item['file_name']
+ image = Image.open(image_path).convert('RGB')
+
+ # Apply transform if specified
+ if self.transform:
+ image = self.transform(image)
+
+ return {
+ 'image': image,
+ 'prompts': item['caption'],
+ 'target_bbox': item['target_crop']['target_bbox'],
+ 'target_ratio': item['target_crop']['target_ratio'],
+ 'type': item['type'],
+ 'origin_size': (item['origin_width'], item['origin_height']),
+ 'idx': idx
+ }
+
+
+class WorldMemCSVDataset(Dataset):
+ """Dataset for WorldMem-format data: CSV index + NPZ action/pose files + raw MP4 videos.
+
+ CSV columns (as produced by WorldMem):
+ video_path — path to MP4 video (640×360 @10fps Minecraft footage)
+ action_path — path to NPZ file with 'actions' (N, 8) and 'poses' (N, 5)
+
+ Loads (num_latent_frames - 1) * 4 + 1 = 81 raw video frames per sample and returns
+ them as (C, T_vid, H, W). The trainer encodes with the WAN VAE to obtain
+ num_latent_frames=21 latent frames (first frame kept, rest 4× compressed).
+ Actions/poses are subsampled to match latent frame count (one per latent frame).
+ Resolution 352×640 avoids the odd latent-height issue (360/8=45 odd).
+ """
+
+ VIDEO_H = 352
+ VIDEO_W = 640
+ SKIP_FRAMES = 0 # skip unstable early frames
+
+ def __init__(self, csv_path: str, num_latent_frames: int = 21, sample_strategy: str = "random"):
+ assert sample_strategy in ("random", "first"), \
+ f"sample_strategy must be 'random' or 'first', got {sample_strategy!r}"
+ self.sample_strategy = sample_strategy
+ self.num_latent_frames = num_latent_frames
+ # WAN VAE: T_vid = (T_lat - 1) * 4 + 1
+ self.num_video_frames = (num_latent_frames - 1) * 4 + 1
+ # Pose subsample indices: frame 0, then first frame of each 4-frame group
+ self.pose_indices = [k * 4 for k in range(num_latent_frames)]
+ self.rows = []
+ with open(csv_path, newline="", encoding="utf-8") as f:
+ reader = csv.DictReader(f)
+ for row in reader:
+ self.rows.append(row)
+
+ def __len__(self):
+ return len(self.rows)
+
+ def _load_video_frames(self, video_path: str, start_frame: int) -> torch.Tensor:
+ """Load num_video_frames frames from an MP4, returns (C, T, H, W) float32 in [-1,1]."""
+ import decord
+ decord.bridge.set_bridge("torch")
+ reader = decord.VideoReader(
+ video_path,
+ ctx=decord.cpu(0),
+ height=self.VIDEO_H,
+ width=self.VIDEO_W,
+ )
+ total = len(reader)
+ end_frame = min(start_frame + self.num_video_frames, total)
+ frame_indices = list(range(start_frame, end_frame))
+ frames = reader.get_batch(frame_indices) # (T, H, W, C) uint8, torch.Tensor
+ # (T, H, W, C) uint8 -> (C, T, H, W) float32 in [-1, 1]
+ return frames.permute(3, 0, 1, 2).to(torch.float32).div(127.5).sub(1.0).contiguous()
+
+ def __getitem__(self, idx):
+ import decord
+ row = self.rows[idx]
+ video_path = row["video_path"]
+ action_path = row["action_path"]
+
+ reader = decord.VideoReader(video_path, ctx=decord.cpu(0))
+ total = len(reader)
+ if self.sample_strategy == "first":
+ start_frame = self.SKIP_FRAMES
+ else:
+ max_start = max(self.SKIP_FRAMES, total - self.num_video_frames)
+ start_frame = int(np.random.randint(self.SKIP_FRAMES, max_start + 1))
+
+ frames = self._load_video_frames(video_path, start_frame) # (C, T_vid, H, W)
+
+ actions_full, poses_full = extract_pose_action_from_npz(
+ action_path, start_frame, self.num_video_frames
+ ) # (T_vid, 25), (T_vid, 5)
+
+ actions = actions_full # (T_vid, 25) — all raw frames
+ poses = poses_full[self.pose_indices] # (T_lat, 5) — one per latent frame
+
+ return {
+ "prompts": "", # unused for WorldMem
+ "frames": frames, # (C, T_vid, H, W) float32 in [-1, 1]
+ "actions": actions, # (T_vid, 25)
+ "poses": poses, # (T_lat, 5)
+ }
+
+
+def cycle(dl):
+ while True:
+ for data in dl:
+ yield data
diff --git a/DecMem/utils/distributed.py b/DecMem/utils/distributed.py
new file mode 100644
index 0000000000000000000000000000000000000000..735c053cb1ee4c777667bff94b1a90e63254c1a6
--- /dev/null
+++ b/DecMem/utils/distributed.py
@@ -0,0 +1,158 @@
+from datetime import timedelta
+from functools import partial
+import os
+import torch
+import torch.distributed as dist
+from torch.distributed.fsdp import FullOptimStateDictConfig, FullStateDictConfig, FullyShardedDataParallel as FSDP, MixedPrecision, ShardingStrategy, StateDictType
+from torch.distributed.fsdp.api import CPUOffload
+from torch.distributed.fsdp.wrap import size_based_auto_wrap_policy, transformer_auto_wrap_policy
+
+
+def fsdp_state_dict(model):
+ fsdp_fullstate_save_policy = FullStateDictConfig(
+ offload_to_cpu=True, rank0_only=True
+ )
+ with FSDP.state_dict_type(
+ model, StateDictType.FULL_STATE_DICT, fsdp_fullstate_save_policy
+ ):
+ checkpoint = model.state_dict()
+
+ return checkpoint
+
+
+def fsdp_optim_state_dict(model, optimizer):
+ """Gather the full optimizer state on rank 0 (CPU). Other ranks return an empty dict."""
+ state_dict_config = FullStateDictConfig(offload_to_cpu=True, rank0_only=True)
+ optim_state_dict_config = FullOptimStateDictConfig(offload_to_cpu=True, rank0_only=True)
+ with FSDP.state_dict_type(
+ model,
+ StateDictType.FULL_STATE_DICT,
+ state_dict_config=state_dict_config,
+ optim_state_dict_config=optim_state_dict_config,
+ ):
+ return FSDP.optim_state_dict(model, optimizer)
+
+
+def load_fsdp_optim_state_dict(model, optimizer, full_optim_state_dict):
+ """Scatter a full optimizer state (valid on rank 0) into the local FSDP shards."""
+ with FSDP.state_dict_type(model, StateDictType.FULL_STATE_DICT):
+ sharded = FSDP.optim_state_dict_to_load(
+ model=model,
+ optim=optimizer,
+ optim_state_dict=full_optim_state_dict,
+ )
+ optimizer.load_state_dict(sharded)
+
+
+def fsdp_wrap(module, sharding_strategy="full", mixed_precision=False, wrap_strategy="size", min_num_params=int(5e7), transformer_module=None, cpu_offload=False):
+ if mixed_precision:
+ mixed_precision_policy = MixedPrecision(
+ param_dtype=torch.bfloat16,
+ reduce_dtype=torch.float32,
+ buffer_dtype=torch.float32,
+ cast_forward_inputs=False
+ )
+ else:
+ mixed_precision_policy = None
+
+ if wrap_strategy == "transformer":
+ auto_wrap_policy = partial(
+ transformer_auto_wrap_policy,
+ transformer_layer_cls=transformer_module
+ )
+ elif wrap_strategy == "size":
+ auto_wrap_policy = partial(
+ size_based_auto_wrap_policy,
+ min_num_params=min_num_params
+ )
+ else:
+ raise ValueError(f"Invalid wrap strategy: {wrap_strategy}")
+
+ os.environ["NCCL_CROSS_NIC"] = "1"
+
+ sharding_strategy = {
+ "full": ShardingStrategy.FULL_SHARD,
+ "hybrid_full": ShardingStrategy.HYBRID_SHARD,
+ "hybrid_zero2": ShardingStrategy._HYBRID_SHARD_ZERO2,
+ "no_shard": ShardingStrategy.NO_SHARD,
+ }[sharding_strategy]
+
+ module = FSDP(
+ module,
+ auto_wrap_policy=auto_wrap_policy,
+ sharding_strategy=sharding_strategy,
+ mixed_precision=mixed_precision_policy,
+ device_id=torch.cuda.current_device(),
+ limit_all_gathers=True,
+ use_orig_params=True,
+ cpu_offload=CPUOffload(offload_params=cpu_offload),
+ sync_module_states=False # Load ckpt on rank 0 and sync to other ranks
+ )
+ return module
+
+
+def barrier():
+ if dist.is_initialized():
+ dist.barrier()
+
+
+def launch_distributed_job(backend: str = "nccl"):
+ rank = int(os.environ["RANK"])
+ local_rank = int(os.environ["LOCAL_RANK"])
+ world_size = int(os.environ["WORLD_SIZE"])
+ host = os.environ["MASTER_ADDR"]
+ port = int(os.environ["MASTER_PORT"])
+
+ if ":" in host: # IPv6
+ init_method = f"tcp://[{host}]:{port}"
+ else: # IPv4
+ init_method = f"tcp://{host}:{port}"
+ dist.init_process_group(rank=rank, world_size=world_size, backend=backend,
+ init_method=init_method, timeout=timedelta(minutes=30))
+ torch.cuda.set_device(local_rank)
+
+
+class EMA_FSDP:
+ def __init__(self, fsdp_module: torch.nn.Module, decay: float = 0.999):
+ self.decay = decay
+ self.shadow = {}
+ self._init_shadow(fsdp_module)
+
+ @torch.no_grad()
+ def _init_shadow(self, fsdp_module):
+ from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
+ # Anchor shadow to the local CUDA device. summon_full_params can return
+ # CPU views of params under some FSDP/resume code paths, so don't
+ # implicitly inherit p's device.
+ target_device = torch.device("cuda", torch.cuda.current_device())
+ with FSDP.summon_full_params(fsdp_module, writeback=False):
+ for n, p in fsdp_module.module.named_parameters():
+ self.shadow[n] = p.detach().to(target_device).float().clone()
+
+ @torch.no_grad()
+ def update(self, fsdp_module):
+ d = self.decay
+ from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
+ with FSDP.summon_full_params(fsdp_module, writeback=False):
+ for n, p in fsdp_module.module.named_parameters():
+ shadow = self.shadow[n]
+ # Always align the source tensor to shadow's device to avoid
+ # cuda/cpu mismatches when FSDP exposes params on CPU.
+ shadow.mul_(d).add_(
+ p.detach().to(shadow.device).float(), alpha=1. - d
+ )
+
+ # Optional helpers ---------------------------------------------------
+ def state_dict(self):
+ return self.shadow # picklable
+
+ def load_state_dict(self, sd):
+ self.shadow = {k: v.clone() for k, v in sd.items()}
+
+ def copy_to(self, fsdp_module):
+ # load EMA weights into an (unwrapped) copy of the generator
+ from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
+ with FSDP.summon_full_params(fsdp_module, writeback=True):
+ for n, p in fsdp_module.module.named_parameters():
+ if n in self.shadow:
+ p.data.copy_(self.shadow[n].to(p.dtype, device=p.device))
diff --git a/DecMem/utils/lmdb.py b/DecMem/utils/lmdb.py
new file mode 100644
index 0000000000000000000000000000000000000000..2171d54cd3b1b1963590c5c1a633aac7b9fc287e
--- /dev/null
+++ b/DecMem/utils/lmdb.py
@@ -0,0 +1,72 @@
+import numpy as np
+
+
+def get_array_shape_from_lmdb(env, array_name):
+ with env.begin() as txn:
+ image_shape = txn.get(f"{array_name}_shape".encode()).decode()
+ image_shape = tuple(map(int, image_shape.split()))
+ return image_shape
+
+
+def store_arrays_to_lmdb(env, arrays_dict, start_index=0):
+ """
+ Store rows of multiple numpy arrays in a single LMDB.
+ Each row is stored separately with a naming convention.
+ """
+ with env.begin(write=True) as txn:
+ for array_name, array in arrays_dict.items():
+ for i, row in enumerate(array):
+ # Convert row to bytes
+ if isinstance(row, str):
+ row_bytes = row.encode()
+ else:
+ row_bytes = row.tobytes()
+
+ data_key = f'{array_name}_{start_index + i}_data'.encode()
+
+ txn.put(data_key, row_bytes)
+
+
+def process_data_dict(data_dict, seen_prompts):
+ output_dict = {}
+
+ all_videos = []
+ all_prompts = []
+ for prompt, video in data_dict.items():
+ if prompt in seen_prompts:
+ continue
+ else:
+ seen_prompts.add(prompt)
+
+ video = video.half().numpy()
+ all_videos.append(video)
+ all_prompts.append(prompt)
+
+ if len(all_videos) == 0:
+ return {"latents": np.array([]), "prompts": np.array([])}
+
+ all_videos = np.concatenate(all_videos, axis=0)
+
+ output_dict['latents'] = all_videos
+ output_dict['prompts'] = np.array(all_prompts)
+
+ return output_dict
+
+
+def retrieve_row_from_lmdb(lmdb_env, array_name, dtype, row_index, shape=None):
+ """
+ Retrieve a specific row from a specific array in the LMDB.
+ """
+ data_key = f'{array_name}_{row_index}_data'.encode()
+
+ with lmdb_env.begin() as txn:
+ row_bytes = txn.get(data_key)
+
+ if dtype == str:
+ array = row_bytes.decode()
+ else:
+ array = np.frombuffer(row_bytes, dtype=dtype)
+
+ if shape is not None and len(shape) > 0:
+ array = array.reshape(shape)
+ return array
diff --git a/DecMem/utils/loss.py b/DecMem/utils/loss.py
new file mode 100644
index 0000000000000000000000000000000000000000..c420466d641d5fe2012eb2c970cba18b66d45826
--- /dev/null
+++ b/DecMem/utils/loss.py
@@ -0,0 +1,81 @@
+from abc import ABC, abstractmethod
+import torch
+
+
+class DenoisingLoss(ABC):
+ @abstractmethod
+ def __call__(
+ self, x: torch.Tensor, x_pred: torch.Tensor,
+ noise: torch.Tensor, noise_pred: torch.Tensor,
+ alphas_cumprod: torch.Tensor,
+ timestep: torch.Tensor,
+ **kwargs
+ ) -> torch.Tensor:
+ """
+ Base class for denoising loss.
+ Input:
+ - x: the clean data with shape [B, F, C, H, W]
+ - x_pred: the predicted clean data with shape [B, F, C, H, W]
+ - noise: the noise with shape [B, F, C, H, W]
+ - noise_pred: the predicted noise with shape [B, F, C, H, W]
+ - alphas_cumprod: the cumulative product of alphas (defining the noise schedule) with shape [T]
+ - timestep: the current timestep with shape [B, F]
+ """
+ pass
+
+
+class X0PredLoss(DenoisingLoss):
+ def __call__(
+ self, x: torch.Tensor, x_pred: torch.Tensor,
+ noise: torch.Tensor, noise_pred: torch.Tensor,
+ alphas_cumprod: torch.Tensor,
+ timestep: torch.Tensor,
+ **kwargs
+ ) -> torch.Tensor:
+ return torch.mean((x - x_pred) ** 2)
+
+
+class VPredLoss(DenoisingLoss):
+ def __call__(
+ self, x: torch.Tensor, x_pred: torch.Tensor,
+ noise: torch.Tensor, noise_pred: torch.Tensor,
+ alphas_cumprod: torch.Tensor,
+ timestep: torch.Tensor,
+ **kwargs
+ ) -> torch.Tensor:
+ weights = 1 / (1 - alphas_cumprod[timestep].reshape(*timestep.shape, 1, 1, 1))
+ return torch.mean(weights * (x - x_pred) ** 2)
+
+
+class NoisePredLoss(DenoisingLoss):
+ def __call__(
+ self, x: torch.Tensor, x_pred: torch.Tensor,
+ noise: torch.Tensor, noise_pred: torch.Tensor,
+ alphas_cumprod: torch.Tensor,
+ timestep: torch.Tensor,
+ **kwargs
+ ) -> torch.Tensor:
+ return torch.mean((noise - noise_pred) ** 2)
+
+
+class FlowPredLoss(DenoisingLoss):
+ def __call__(
+ self, x: torch.Tensor, x_pred: torch.Tensor,
+ noise: torch.Tensor, noise_pred: torch.Tensor,
+ alphas_cumprod: torch.Tensor,
+ timestep: torch.Tensor,
+ **kwargs
+ ) -> torch.Tensor:
+ return torch.mean((kwargs["flow_pred"] - (noise - x)) ** 2)
+
+
+NAME_TO_CLASS = {
+ "x0": X0PredLoss,
+ "v": VPredLoss,
+ "noise": NoisePredLoss,
+ "flow": FlowPredLoss
+}
+
+
+def get_denoising_loss(loss_type: str) -> DenoisingLoss:
+ return NAME_TO_CLASS[loss_type]
diff --git a/DecMem/utils/misc.py b/DecMem/utils/misc.py
new file mode 100644
index 0000000000000000000000000000000000000000..94cf29feb244eeac4f65b113f7a0c16f59d6442f
--- /dev/null
+++ b/DecMem/utils/misc.py
@@ -0,0 +1,39 @@
+import numpy as np
+import random
+import torch
+
+
+def set_seed(seed: int, deterministic: bool = False):
+ """
+ Helper function for reproducible behavior to set the seed in `random`, `numpy`, `torch`.
+
+ Args:
+ seed (`int`):
+ The seed to set.
+ deterministic (`bool`, *optional*, defaults to `False`):
+ Whether to use deterministic algorithms where available. Can slow down training.
+ """
+ random.seed(seed)
+ np.random.seed(seed)
+ torch.manual_seed(seed)
+ torch.cuda.manual_seed_all(seed)
+
+ if deterministic:
+ torch.use_deterministic_algorithms(True)
+
+
+def merge_dict_list(dict_list):
+ if len(dict_list) == 1:
+ return dict_list[0]
+
+ merged_dict = {}
+ for k, v in dict_list[0].items():
+ if isinstance(v, torch.Tensor):
+ if v.ndim == 0:
+ merged_dict[k] = torch.stack([d[k] for d in dict_list], dim=0)
+ else:
+ merged_dict[k] = torch.cat([d[k] for d in dict_list], dim=0)
+ else:
+ # for non-tensor values, we just copy the value from the first item
+ merged_dict[k] = v
+ return merged_dict
diff --git a/DecMem/utils/pose_utils.py b/DecMem/utils/pose_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..40c8b2b983cac8e15d343c0855f6798477b4e183
--- /dev/null
+++ b/DecMem/utils/pose_utils.py
@@ -0,0 +1,203 @@
+"""
+Pose and action utility functions adapted from WorldMem/framear.
+"""
+import numpy as np
+import torch
+from einops import rearrange
+
+
+ACTION_KEYS = [
+ "inventory", # 0
+ "ESC", # 1
+ "hotbar.1", # 2
+ "hotbar.2", # 3
+ "hotbar.3", # 4
+ "hotbar.4", # 5
+ "hotbar.5", # 6
+ "hotbar.6", # 7
+ "hotbar.7", # 8
+ "hotbar.8", # 9
+ "hotbar.9", # 10
+ "forward", # 11
+ "back", # 12
+ "left", # 13
+ "right", # 14
+ "cameraY", # 15
+ "cameraX", # 16
+ "jump", # 17
+ "sneak", # 18
+ "sprint", # 19
+ "swapHands", # 20
+ "attack", # 21
+ "use", # 22
+ "pickItem", # 23
+ "drop", # 24
+]
+
+
+def convert_action_space(actions: np.ndarray) -> torch.Tensor:
+ """Convert raw WorldMem action array to 25-dim one-hot vector.
+
+ Args:
+ actions: np.ndarray of shape (T, 8+), raw action indices from NPZ.
+
+ Returns:
+ Tensor of shape (T, 25) with float values.
+ """
+ vec_25 = torch.zeros(len(actions), len(ACTION_KEYS))
+ vec_25[actions[:, 0] == 1, 11] = 1 # forward
+ vec_25[actions[:, 0] == 2, 12] = 1 # backward
+ vec_25[actions[:, 4] == 11, 16] = -1 # camera X left
+ vec_25[actions[:, 4] == 13, 16] = 1 # camera X right
+ vec_25[actions[:, 3] == 11, 15] = -1 # camera Y up
+ vec_25[actions[:, 3] == 13, 15] = 1 # camera Y down
+ vec_25[actions[:, 5] == 6, 24] = 1 # place box
+ vec_25[actions[:, 5] == 1, 24] = 1 # place box
+ vec_25[actions[:, 1] == 1, 13] = 1 # left
+ vec_25[actions[:, 1] == 2, 14] = 1 # right
+ vec_25[actions[:, 7] == 1, 2] = 1 # hotbar 1
+ return vec_25
+
+
+def euler_to_rotation_matrix(pitch: torch.Tensor, yaw: torch.Tensor) -> torch.Tensor:
+ """Convert pitch and yaw angles (radians) to 3x3 rotation matrix.
+
+ Args:
+ pitch: Tensor of pitch angles, shape (...,).
+ yaw: Tensor of yaw angles, shape (...,).
+
+ Returns:
+ Rotation matrix of shape (..., 3, 3).
+ """
+ cos_pitch, sin_pitch = torch.cos(pitch), torch.sin(pitch)
+ cos_yaw, sin_yaw = torch.cos(yaw), torch.sin(yaw)
+
+ R_pitch = torch.stack([
+ torch.ones_like(pitch), torch.zeros_like(pitch), torch.zeros_like(pitch),
+ torch.zeros_like(pitch), cos_pitch, -sin_pitch,
+ torch.zeros_like(pitch), sin_pitch, cos_pitch
+ ], dim=-1).reshape(-1, 3, 3)
+
+ R_yaw = torch.stack([
+ cos_yaw, torch.zeros_like(yaw), sin_yaw,
+ torch.zeros_like(yaw), torch.ones_like(yaw), torch.zeros_like(yaw),
+ -sin_yaw, torch.zeros_like(yaw), cos_yaw
+ ], dim=-1).reshape(-1, 3, 3)
+
+ return torch.matmul(R_yaw, R_pitch)
+
+
+def euler_to_camera_to_world_matrix(pose: torch.Tensor) -> torch.Tensor:
+ """Convert (x, y, z, pitch, yaw) pose vector to 4x4 camera-to-world matrix.
+
+ Args:
+ pose: Tensor of shape (5,), (f, 5), or (f, b, 5).
+
+ Returns:
+ Tensor of shape (4,4), (f, 4, 4), or (f, b, 4, 4).
+ """
+ origin_dim = pose.ndim
+ if origin_dim == 1:
+ pose = pose.unsqueeze(0).unsqueeze(0)
+ elif origin_dim == 2:
+ pose = pose.unsqueeze(0)
+
+ x, y, z, pitch, yaw = (
+ pose[..., 0], pose[..., 1], pose[..., 2],
+ pose[..., 3], pose[..., 4]
+ )
+ pitch = torch.deg2rad(pitch)
+ yaw = torch.deg2rad(yaw)
+
+ R = euler_to_rotation_matrix(pitch, yaw)
+
+ eye = torch.eye(4, dtype=pose.dtype, device=pose.device)
+ c2w = eye.repeat(R.shape[0], 1, 1)
+ c2w[:, :3, :3] = R
+ c2w[:, :3, 3] = torch.stack([x.reshape(-1), y.reshape(-1), z.reshape(-1)], dim=-1)
+
+ if origin_dim == 3:
+ return c2w.view(pose.shape[0], pose.shape[1], 4, 4)
+ elif origin_dim == 2:
+ return c2w.view(pose.shape[0], 4, 4)
+ else:
+ return c2w.squeeze(0).squeeze(0)
+
+
+def c2w_to_w2c(c2w: torch.Tensor) -> torch.Tensor:
+ """Invert camera-to-world to world-to-camera matrix.
+
+ Args:
+ c2w: Tensor of shape (..., 4, 4).
+
+ Returns:
+ w2c: Tensor of shape (..., 4, 4).
+ """
+ R = c2w[..., :3, :3]
+ t = c2w[..., :3, 3]
+ Rt = R.transpose(-1, -2)
+ t_new = -Rt @ t.unsqueeze(-1)
+ w2c = torch.zeros_like(c2w)
+ w2c[..., :3, :3] = Rt
+ w2c[..., :3, 3] = t_new.squeeze(-1)
+ w2c[..., 3, 3] = 1.0
+ return w2c
+
+
+def extract_pose_action_from_npz(
+ data_path: str,
+ start_frame: int,
+ num_frames: int,
+ dtype=torch.float32
+) -> tuple:
+ """Load action and pose from WorldMem NPZ file.
+
+ Args:
+ data_path: Path to .npz file containing 'actions' and 'poses'.
+ start_frame: Starting frame index.
+ num_frames: Number of frames to load.
+ dtype: Output tensor dtype.
+
+ Returns:
+ actions: Tensor (num_frames, 25) - encoded action vectors.
+ poses: Tensor (num_frames, 5) - raw pose vectors (x,y,z,pitch,yaw).
+ """
+ data = np.load(data_path)
+ actions_raw = data["actions"] # (N, 8+)
+ poses_pool = data["poses"].copy() # (N, 5)
+
+ # Fix potentially corrupted height in first frame
+ poses_pool[0, 1] = poses_pool[1, 1]
+
+ # Pad poses if shorter than actions
+ if len(poses_pool) < len(actions_raw):
+ poses_pool = np.pad(poses_pool, ((1, 0), (0, 0)))
+
+ actions_raw = actions_raw[start_frame: start_frame + num_frames]
+ poses = poses_pool[start_frame: start_frame + num_frames]
+
+ # Normalize pose: translate relative to first frame, negate yaw
+ poses[:, :3] -= poses[:1, :3]
+ poses[:, -1] = -poses[:, -1]
+ poses[:, 3:] %= 360
+
+ actions = convert_action_space(actions_raw) # (T, 25)
+ return actions.to(dtype), torch.tensor(poses, dtype=dtype)
+
+
+def poses_to_viewmats(poses: torch.Tensor) -> torch.Tensor:
+ """Convert (B, T, 5) pose vectors to (B, T, 4, 4) world-to-camera matrices.
+
+ Args:
+ poses: Tensor of shape (B, T, 5).
+
+ Returns:
+ viewmats: Tensor of shape (B, T, 4, 4).
+ """
+ B, T, _ = poses.shape
+ # (B, T, 5) -> (T, B, 5) for euler_to_camera_to_world_matrix
+ pose_tbd = rearrange(poses, "b t d -> t b d")
+ c2w = euler_to_camera_to_world_matrix(pose_tbd) # (T, B, 4, 4)
+ w2c = c2w_to_w2c(c2w) # (T, B, 4, 4)
+ viewmats = rearrange(w2c, "t b p q -> b t p q") # (B, T, 4, 4)
+ return viewmats
diff --git a/DecMem/utils/prope.py b/DecMem/utils/prope.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a80bd0c2f9a4ebb2903d1b95f3579376b93c246
--- /dev/null
+++ b/DecMem/utils/prope.py
@@ -0,0 +1,900 @@
+# MIT License
+#
+# Copyright (c) Authors of
+# "Cameras as Relative Positional Encoding" https://arxiv.org/pdf/2507.10496
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+from functools import partial
+from typing import Callable, Optional, Tuple, List
+
+import torch
+import torch.nn.functional as F
+import math
+
+class PropeDotProductAttention(torch.nn.Module):
+ """PRoPE attention with precomputed RoPE coefficients."""
+
+ coeffs_x_0: torch.Tensor
+ coeffs_x_1: torch.Tensor
+ coeffs_y_0: torch.Tensor
+ coeffs_y_1: torch.Tensor
+ coeffs_t_0: torch.Tensor
+ coeffs_t_1: torch.Tensor
+
+ def __init__(
+ self,
+ head_dim: int,
+ patches_x: int,
+ patches_y: int,
+ image_width: int,
+ image_height: int,
+ Ks: torch.Tensor = None,
+ freq_base: float = 100.0,
+ freq_scale: float = 1.0,
+ freq_base_tem: float = 10000.0,
+ cameras: int = -1,
+ prope_max_time: int = 2000,
+ rope_scale_pattern: str = None,
+ tem_dim_scale: int = 1
+ ):
+ super().__init__()
+ self.head_dim = head_dim
+ self.patches_x = patches_x
+ self.patches_y = patches_y
+ self.image_width = image_width
+ self.image_height = image_height
+ self.freq_base_tem = freq_base_tem
+ self.freq_base = freq_base
+
+ if cameras != -1: # has tem rope
+ # Divide head_dim equally among: projmat | x-RoPE | y-RoPE | temporal-RoPE (4 parts)
+ # head_dim=128 -> feat_dim=32; each part is 32, total 32*4=128 (all even)
+ feat_dim = head_dim // 4
+ self.feat_dim = feat_dim
+ self.freq_scale = freq_scale
+ self.max_frames = cameras
+ self.prope_max_time = prope_max_time
+ self.tem_dim_scale = tem_dim_scale
+ self.rope_scale_pattern = rope_scale_pattern
+
+ self.attn_scale = None
+
+ coeffs_x: Tuple[torch.Tensor, torch.Tensor] = _rope_precompute_coeffs(
+ torch.tile(torch.arange(patches_x), (patches_y,)),
+ freq_base=freq_base,
+ freq_scale=freq_scale,
+ feat_dim=feat_dim,
+ )
+ coeffs_y: Tuple[torch.Tensor, torch.Tensor] = _rope_precompute_coeffs(
+ torch.repeat_interleave(torch.arange(patches_y), patches_x),
+ freq_base=freq_base,
+ freq_scale=freq_scale,
+ feat_dim=feat_dim,
+ )
+
+ if cameras != -1:
+ coeffs_t: Tuple[torch.Tensor, torch.Tensor] = _rope_precompute_coeffs(
+ torch.repeat_interleave(torch.arange(cameras), patches_x * patches_y),
+ freq_base=freq_base_tem, freq_scale=freq_scale, feat_dim=feat_dim * self.tem_dim_scale,
+ )
+ self.register_buffer("coeffs_t_0", coeffs_t[0], persistent=False)
+ self.register_buffer("coeffs_t_1", coeffs_t[1], persistent=False)
+
+ # Do not save coeffs to checkpoint as `cameras` might change during testing.
+ self.register_buffer("coeffs_x_0", coeffs_x[0], persistent=False)
+ self.register_buffer("coeffs_x_1", coeffs_x[1], persistent=False)
+ self.register_buffer("coeffs_y_0", coeffs_y[0], persistent=False)
+ self.register_buffer("coeffs_y_1", coeffs_y[1], persistent=False)
+
+ # override load_state_dict to not load coeffs if they exist (for backward compatibility)
+ def load_state_dict(self, state_dict, strict=True):
+ # remove coeffs from state_dict
+ state_dict.pop("coeffs_x_0", None)
+ state_dict.pop("coeffs_x_1", None)
+ state_dict.pop("coeffs_y_0", None)
+ state_dict.pop("coeffs_y_1", None)
+ if 'coeffs_t_0' in state_dict:
+ state_dict.pop("coeffs_t_0", None)
+ state_dict.pop("coeffs_t_1", None)
+ super().load_state_dict(state_dict, strict)
+
+ def forward(
+ self,
+ q: torch.Tensor, # (batch, num_heads, seqlen, head_dim)
+ k: torch.Tensor, # (batch, num_heads, seqlen, head_dim)
+ v: torch.Tensor, # (batch, num_heads, seqlen, head_dim)
+ viewmats: torch.Tensor, # (batch, cameras, 4, 4)
+ Ks: Optional[torch.Tensor], # (batch, cameras, 3, 3)
+ **kwargs,
+ ) -> torch.Tensor:
+ Ks = torch.tensor(self.Ks, device=q.device, dtype=q.dtype).unsqueeze(0).unsqueeze(0).repeat(viewmats.shape[0], viewmats.shape[1], 1, 1)
+ return prope_dot_product_attention(
+ q,
+ k,
+ v,
+ viewmats=viewmats,
+ Ks=Ks,
+ patches_x=self.patches_x,
+ patches_y=self.patches_y,
+ image_width=self.image_width,
+ image_height=self.image_height,
+ coeffs_x=(self.coeffs_x_0, self.coeffs_x_1),
+ coeffs_y=(self.coeffs_y_0, self.coeffs_y_1),
+ **kwargs,
+ )
+
+ def _precompute_and_cache_apply_fns(
+ self, viewmats: torch.Tensor, Ks: Optional[torch.Tensor] = None
+ ):
+ (batch, cameras, _, _) = viewmats.shape
+ assert viewmats.shape == (batch, cameras, 4, 4)
+ assert Ks is None or Ks.shape == (batch, cameras, 3, 3)
+ self.cameras = cameras
+
+ if len(self.Ks.shape) != len(viewmats.shape):
+ Ks = self.Ks.unsqueeze(0).unsqueeze(0).repeat(viewmats.shape[0], viewmats.shape[1], 1, 1)
+
+ self.apply_fn_q, self.apply_fn_kv, self.apply_fn_o = _prepare_apply_fns(
+ head_dim=self.head_dim,
+ viewmats=viewmats,
+ Ks=Ks.type_as(viewmats).to(viewmats) if Ks is not None else None,
+ patches_x=self.patches_x,
+ patches_y=self.patches_y,
+ image_width=self.image_width,
+ image_height=self.image_height,
+ coeffs_x=(self.coeffs_x_0, self.coeffs_x_1),
+ coeffs_y=(self.coeffs_y_0, self.coeffs_y_1),
+ )
+
+ def _precompute_and_cache_apply_fns_with_tem_rope(
+ self, viewmats: torch.Tensor, Ks: Optional[torch.Tensor] = None,
+ time_ids: int = 0, pre_rope: bool = True,
+ tem_rope_scaling_factor: float = 1.0, training: bool = False,
+ ):
+ (batch, cameras, _, _) = viewmats.shape
+ assert viewmats.shape == (batch, cameras, 4, 4)
+ assert Ks is None or Ks.shape == (3, 3) or Ks.shape == (batch, cameras, 3, 3)
+ self.cameras = cameras
+ if Ks is not None and len(Ks.shape) == 2:
+ Ks = Ks.unsqueeze(0).unsqueeze(0).repeat(batch, cameras, 1, 1)
+
+ if pre_rope:
+ cur_time_max = time_ids + cameras
+ else:
+ cur_time_max = cameras
+
+ feat_dim = self.feat_dim * self.tem_dim_scale
+ freq_base_tem = self.freq_base_tem
+ freq_scale = self.freq_scale
+ coeffs_t = None
+ if cur_time_max > self.max_frames:
+ time_indices = torch.repeat_interleave(torch.arange(cur_time_max, device=viewmats.device), self.patches_x * self.patches_y)
+ coeffs_t: Tuple[torch.Tensor, torch.Tensor] = _rope_precompute_coeffs(time_indices, freq_base=freq_base_tem, freq_scale=freq_scale, feat_dim=feat_dim)
+
+ if coeffs_t is None:
+ coeffs_t = (self.coeffs_t_0, self.coeffs_t_1)
+
+ start = time_ids * (self.patches_x * self.patches_y) if pre_rope else 0
+ end = cur_time_max * (self.patches_x * self.patches_y)
+ if training:
+ coeffs_t_0, coeffs_t_1 = coeffs_t[0][:, :, start:end], coeffs_t[1][:, :, start:end]
+ else:
+ coeffs_t_0 = coeffs_t[0][:, :, start:end].type_as(self.coeffs_t_0)
+ coeffs_t_1 = coeffs_t[1][:, :, start:end].type_as(self.coeffs_t_0)
+
+ if time_ids == 0 or pre_rope: # cache rope embedding
+ self.apply_fn_q, self.apply_fn_k, self.apply_fn_v, self.apply_fn_o = _prepare_apply_fns_with_tem_rope_v3(
+ head_dim=self.head_dim,
+ viewmats=viewmats,
+ Ks=Ks.type_as(viewmats).to(viewmats) if Ks is not None else None,
+ patches_x=self.patches_x,
+ patches_y=self.patches_y,
+ image_width=self.image_width,
+ image_height=self.image_height,
+ coeffs_x=(self.coeffs_x_0, self.coeffs_x_1),
+ coeffs_y=(self.coeffs_y_0, self.coeffs_y_1),
+ coeffs_t=(coeffs_t_0, coeffs_t_1),
+ tem_dim_scale=self.tem_dim_scale,
+ )
+ elif not pre_rope:
+ _, self.apply_fn_k, self.apply_fn_v, _, = _prepare_apply_fns_with_tem_rope_v3(
+ head_dim=self.head_dim,
+ viewmats=viewmats,
+ Ks=Ks.type_as(viewmats).to(viewmats) if Ks is not None else None,
+ patches_x=self.patches_x,
+ patches_y=self.patches_y,
+ image_width=self.image_width,
+ image_height=self.image_height,
+ coeffs_x=(self.coeffs_x_0, self.coeffs_x_1),
+ coeffs_y=(self.coeffs_y_0, self.coeffs_y_1),
+ coeffs_t=(coeffs_t_0, coeffs_t_1),
+ tem_dim_scale=self.tem_dim_scale,
+ )
+ self.apply_fn_q, _, _, self.apply_fn_o = _prepare_apply_fns_with_tem_rope_v3(
+ head_dim=self.head_dim,
+ viewmats=viewmats[:, -4:],
+ Ks=Ks.type_as(viewmats).to(viewmats)[:, -4:] if Ks is not None else None,
+ patches_x=self.patches_x,
+ patches_y=self.patches_y,
+ image_width=self.image_width,
+ image_height=self.image_height,
+ coeffs_x=(self.coeffs_x_0, self.coeffs_x_1),
+ coeffs_y=(self.coeffs_y_0, self.coeffs_y_1),
+ coeffs_t=(coeffs_t_0[:, :, -4 * self.patches_x * self.patches_y:], coeffs_t_1[:, :, -4 * self.patches_x * self.patches_y:]),
+ tem_dim_scale=self.tem_dim_scale,
+ )
+
+ def _apply_to_q(self, q: torch.Tensor) -> torch.Tensor:
+ (batch, num_heads, seqlen, head_dim) = q.shape
+ assert head_dim == self.head_dim
+ assert q.shape == (batch, num_heads, seqlen, head_dim)
+ assert self.apply_fn_q is not None
+ return self.apply_fn_q(q)
+
+ def _apply_to_kv(self, kv: torch.Tensor) -> torch.Tensor:
+ (batch, num_heads, seqlen, head_dim) = kv.shape
+ assert seqlen == self.cameras * self.patches_x * self.patches_y
+ assert head_dim == self.head_dim
+ assert kv.shape == (batch, num_heads, seqlen, head_dim)
+ assert self.apply_fn_kv is not None
+ return self.apply_fn_kv(kv)
+
+ def _apply_to_qk_tem_rope(self, qk: torch.Tensor) -> torch.Tensor:
+ (batch, num_heads, seqlen, head_dim) = qk.shape
+ assert seqlen == self.cameras * self.patches_x * self.patches_y
+ assert head_dim == self.head_dim
+ assert qk.shape == (batch, num_heads, seqlen, head_dim)
+ assert self.apply_fn_qk_tem_rope is not None
+ return self.apply_fn_qk_tem_rope(qk)
+
+ def _apply_to_k(self, k: torch.Tensor) -> torch.Tensor:
+ (batch, num_heads, seqlen, head_dim) = k.shape
+ assert head_dim == self.head_dim
+ assert k.shape == (batch, num_heads, seqlen, head_dim)
+ assert self.apply_fn_k is not None
+ return self.apply_fn_k(k)
+
+ def _apply_to_v(self, v: torch.Tensor) -> torch.Tensor:
+ (batch, num_heads, seqlen, head_dim) = v.shape
+ assert head_dim == self.head_dim
+ assert v.shape == (batch, num_heads, seqlen, head_dim)
+ assert self.apply_fn_v is not None
+ return self.apply_fn_v(v)
+
+ def _apply_to_o(self, o: torch.Tensor) -> torch.Tensor:
+ (batch, num_heads, seqlen, head_dim) = o.shape
+ assert head_dim == self.head_dim
+ assert o.shape == (batch, num_heads, seqlen, head_dim)
+ assert self.apply_fn_o is not None
+ return self.apply_fn_o(o)
+
+def prope_dot_product_attention(
+ q: torch.Tensor, # (batch, num_heads, seqlen, head_dim)
+ k: torch.Tensor, # (batch, num_heads, seqlen, head_dim)
+ v: torch.Tensor, # (batch, num_heads, seqlen, head_dim)
+ *,
+ viewmats: torch.Tensor, # (batch, cameras, 4, 4)
+ Ks: Optional[torch.Tensor], # (batch, cameras, 3, 3)
+ patches_x: int,
+ patches_y: int,
+ image_width: int,
+ image_height: int,
+ coeffs_x: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
+ coeffs_y: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
+ **kwargs,
+) -> torch.Tensor:
+ (batch, num_heads, seqlen, head_dim) = q.shape
+ cameras = viewmats.shape[1]
+ assert q.shape == k.shape == v.shape
+ assert viewmats.shape == (batch, cameras, 4, 4)
+ assert Ks is None or Ks.shape == (batch, cameras, 3, 3)
+ assert seqlen == cameras * patches_x * patches_y
+
+ apply_fn_q, apply_fn_kv, apply_fn_o = _prepare_apply_fns(
+ head_dim=head_dim,
+ viewmats=viewmats,
+ Ks=Ks,
+ patches_x=patches_x,
+ patches_y=patches_y,
+ image_width=image_width,
+ image_height=image_height,
+ coeffs_x=coeffs_x,
+ coeffs_y=coeffs_y,
+ )
+
+ out = F.scaled_dot_product_attention(
+ query=apply_fn_q(q),
+ key=apply_fn_kv(k),
+ value=apply_fn_kv(v),
+ **kwargs,
+ )
+ out = apply_fn_o(out)
+ assert out.shape == (batch, num_heads, seqlen, head_dim)
+ return out
+
+def _prepare_apply_fns_with_tem_rope(
+ head_dim: int,
+ viewmats: torch.Tensor,
+ Ks: Optional[torch.Tensor],
+ patches_x: int,
+ patches_y: int,
+ image_width: int,
+ image_height: int,
+ coeffs_x: Optional[torch.Tensor] = None,
+ coeffs_y: Optional[torch.Tensor] = None,
+ coeffs_t: Optional[torch.Tensor] = None,
+ time_ids: Optional[torch.Tensor] = None,
+) -> Tuple[Callable, Callable, Callable]:
+
+ device = viewmats.device
+ (batch, cameras, _, _) = viewmats.shape
+
+ if Ks is not None:
+ Ks_norm = torch.zeros_like(Ks)
+ Ks_norm[..., 0, 0] = Ks[..., 0, 0] / image_width
+ Ks_norm[..., 1, 1] = Ks[..., 1, 1] / image_height
+ Ks_norm[..., 0, 2] = Ks[..., 0, 2] / image_width - 0.5
+ Ks_norm[..., 1, 2] = Ks[..., 1, 2] / image_height - 0.5
+ Ks_norm[..., 2, 2] = 1.0
+ del Ks
+
+ P = torch.einsum("...ij,...jk->...ik", _lift_K(Ks_norm), viewmats)
+ P_T = P.transpose(-1, -2)
+ P_inv = torch.einsum(
+ "...ij,...jk->...ik",
+ _invert_SE3(viewmats),
+ _lift_K(_invert_K(Ks_norm)),
+ )
+
+ else:
+ P = viewmats
+ P_T = P.transpose(-1, -2)
+ P_inv = _invert_SE3(viewmats)
+
+ assert P.shape == P_inv.shape == (batch, cameras, 4, 4)
+
+ feat_dim_part = head_dim // 4
+
+ if coeffs_x is None:
+ coeffs_x = _rope_precompute_coeffs(
+ torch.tile(torch.arange(patches_x, device=device), (patches_y * cameras,)),
+ freq_base=100.0, freq_scale=1.0, feat_dim=feat_dim_part,
+ )
+ if coeffs_y is None:
+ coeffs_y = _rope_precompute_coeffs(
+ torch.tile(torch.repeat_interleave(torch.arange(patches_y, device=device), patches_x), (cameras,)),
+ freq_base=100.0, freq_scale=1.0, feat_dim=feat_dim_part,
+ )
+
+ if coeffs_t is None:
+ coeffs_t = _rope_precompute_coeffs(
+ torch.repeat_interleave(torch.arange(cameras, device=device), patches_x * patches_y),
+ freq_base=10000.0, freq_scale=1.0, feat_dim=feat_dim_part,
+ )
+
+ transforms_q = [
+ (partial(_apply_tiled_projmat, matrix=P_T), head_dim // 2),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_x), feat_dim_part),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_y), feat_dim_part),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_t), feat_dim_part),
+ ]
+ transforms_kv = [
+ (partial(_apply_tiled_projmat, matrix=P_inv), head_dim // 2),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_x), feat_dim_part),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_y), feat_dim_part),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_t), feat_dim_part),
+ ]
+ transforms_o = [
+ (partial(_apply_tiled_projmat, matrix=P), head_dim // 2),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_x, inverse=True), feat_dim_part),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_y, inverse=True), feat_dim_part),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_t, inverse=True), feat_dim_part),
+ ]
+
+ apply_fn_q = partial(_apply_block_diagonal, func_size_pairs=transforms_q)
+ apply_fn_kv = partial(_apply_block_diagonal, func_size_pairs=transforms_kv)
+ apply_fn_o = partial(_apply_block_diagonal, func_size_pairs=transforms_o)
+
+ return apply_fn_q, apply_fn_kv, apply_fn_o
+
+def _prepare_apply_fns_with_tem_rope_v3(
+ head_dim: int,
+ viewmats: torch.Tensor,
+ Ks: Optional[torch.Tensor],
+ patches_x: int,
+ patches_y: int,
+ image_width: int,
+ image_height: int,
+ coeffs_x: Optional[torch.Tensor] = None,
+ coeffs_y: Optional[torch.Tensor] = None,
+ coeffs_t: Optional[torch.Tensor] = None,
+ time_ids: Optional[torch.Tensor] = None,
+ tem_dim_scale: int = 1,
+) -> Tuple[Callable, Callable, Callable, Callable]:
+ device = viewmats.device
+ (batch, cameras, _, _) = viewmats.shape
+
+ if Ks is not None:
+ Ks_norm = torch.zeros_like(Ks)
+ Ks_norm[..., 0, 0] = Ks[..., 0, 0] / image_width
+ Ks_norm[..., 1, 1] = Ks[..., 1, 1] / image_height
+ Ks_norm[..., 0, 2] = Ks[..., 0, 2] / image_width - 0.5
+ Ks_norm[..., 1, 2] = Ks[..., 1, 2] / image_height - 0.5
+ Ks_norm[..., 2, 2] = 1.0
+ del Ks
+ P = torch.einsum("...ij,...jk->...ik", _lift_K(Ks_norm), viewmats)
+ P_T = P.transpose(-1, -2)
+ P_inv = torch.einsum("...ij,...jk->...ik", _invert_SE3(viewmats), _lift_K(_invert_K(Ks_norm)))
+ else:
+ P = viewmats
+ P_T = P.transpose(-1, -2)
+ P_inv = _invert_SE3(viewmats)
+
+ assert P.shape == P_inv.shape == (batch, cameras, 4, 4)
+ # Divide head_dim into 4 equal even parts: projmat | x-RoPE | y-RoPE | temporal-RoPE
+ feat_dim_part = head_dim // 4
+
+ def identity_func(x): return x
+
+ if coeffs_x is None:
+ coeffs_x = _rope_precompute_coeffs(
+ torch.tile(torch.arange(patches_x, device=device), (patches_y * cameras,)),
+ freq_base=100.0, freq_scale=1.0, feat_dim=feat_dim_part,
+ )
+ if coeffs_y is None:
+ coeffs_y = _rope_precompute_coeffs(
+ torch.tile(torch.repeat_interleave(torch.arange(patches_y, device=device), patches_x), (cameras,)),
+ freq_base=100.0, freq_scale=1.0, feat_dim=feat_dim_part,
+ )
+
+ if coeffs_t is None:
+ coeffs_t = _rope_precompute_coeffs(
+ torch.repeat_interleave(torch.arange(cameras, device=device), patches_x * patches_y),
+ freq_base=10000.0, freq_scale=1.0, feat_dim=feat_dim_part * tem_dim_scale,
+ )
+ qk_temporal_transform = (partial(_rope_apply_coeffs, coeffs=coeffs_t), feat_dim_part * tem_dim_scale)
+
+ transforms_q = [
+ (partial(_apply_tiled_projmat, matrix=P_T), head_dim - feat_dim_part*(tem_dim_scale+2)),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_x), feat_dim_part),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_y), feat_dim_part),
+ qk_temporal_transform,
+ ]
+
+ transforms_k = [
+ (partial(_apply_tiled_projmat, matrix=P_inv), head_dim - feat_dim_part*(tem_dim_scale+2)),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_x), feat_dim_part),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_y), feat_dim_part),
+ qk_temporal_transform,
+ ]
+
+ transforms_v = [
+ (partial(_apply_tiled_projmat, matrix=P_inv), head_dim - feat_dim_part*(tem_dim_scale+2)),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_x), feat_dim_part),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_y), feat_dim_part),
+ (identity_func, feat_dim_part * tem_dim_scale),
+ ]
+
+ transforms_o = [
+ (partial(_apply_tiled_projmat, matrix=P), head_dim - feat_dim_part*(tem_dim_scale+2)),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_x, inverse=True), feat_dim_part),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_y, inverse=True), feat_dim_part),
+ (identity_func, feat_dim_part * tem_dim_scale),
+ ]
+
+ apply_fn_q = partial(_apply_block_diagonal, func_size_pairs=transforms_q)
+ apply_fn_k = partial(_apply_block_diagonal, func_size_pairs=transforms_k)
+ apply_fn_v = partial(_apply_block_diagonal, func_size_pairs=transforms_v)
+ apply_fn_o = partial(_apply_block_diagonal, func_size_pairs=transforms_o)
+
+ return apply_fn_q, apply_fn_k, apply_fn_v, apply_fn_o
+
+def _prepare_apply_fns(
+ head_dim: int,
+ viewmats: torch.Tensor,
+ Ks: Optional[torch.Tensor],
+ patches_x: int,
+ patches_y: int,
+ image_width: int,
+ image_height: int,
+ coeffs_x: Optional[torch.Tensor] = None,
+ coeffs_y: Optional[torch.Tensor] = None,
+) -> Tuple[
+ Callable[[torch.Tensor], torch.Tensor],
+ Callable[[torch.Tensor], torch.Tensor],
+ Callable[[torch.Tensor], torch.Tensor],
+]:
+ device = viewmats.device
+ (batch, cameras, _, _) = viewmats.shape
+
+ if Ks is not None:
+ Ks_norm = torch.zeros_like(Ks)
+ Ks_norm[..., 0, 0] = Ks[..., 0, 0] / image_width
+ Ks_norm[..., 1, 1] = Ks[..., 1, 1] / image_height
+ Ks_norm[..., 0, 2] = Ks[..., 0, 2] / image_width - 0.5
+ Ks_norm[..., 1, 2] = Ks[..., 1, 2] / image_height - 0.5
+ Ks_norm[..., 2, 2] = 1.0
+ del Ks
+
+ P = torch.einsum("...ij,...jk->...ik", _lift_K(Ks_norm), viewmats)
+ P_T = P.transpose(-1, -2)
+ P_inv = torch.einsum(
+ "...ij,...jk->...ik",
+ _invert_SE3(viewmats),
+ _lift_K(_invert_K(Ks_norm)),
+ )
+
+ else:
+ P = viewmats
+ P_T = P.transpose(-1, -2)
+ P_inv = _invert_SE3(viewmats)
+
+ assert P.shape == P_inv.shape == (batch, cameras, 4, 4)
+
+ if coeffs_x is None:
+ coeffs_x = _rope_precompute_coeffs(
+ torch.tile(torch.arange(patches_x, device=device), (patches_y * cameras,)),
+ freq_base=100.0,
+ freq_scale=1.0,
+ feat_dim=head_dim // 4,
+ )
+ if coeffs_y is None:
+ coeffs_y = _rope_precompute_coeffs(
+ torch.tile(
+ torch.repeat_interleave(
+ torch.arange(patches_y, device=device), patches_x
+ ),
+ (cameras,),
+ ),
+ freq_base=100.0,
+ freq_scale=1.0,
+ feat_dim=head_dim // 4,
+ )
+
+ assert head_dim % 4 == 0
+ transforms_q = [
+ (partial(_apply_tiled_projmat, matrix=P_T), head_dim // 2),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_x), head_dim // 4),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_y), head_dim // 4),
+ ]
+ transforms_kv = [
+ (partial(_apply_tiled_projmat, matrix=P_inv), head_dim // 2),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_x), head_dim // 4),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_y), head_dim // 4),
+ ]
+ transforms_o = [
+ (partial(_apply_tiled_projmat, matrix=P), head_dim // 2),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_x, inverse=True), head_dim // 4),
+ (partial(_rope_apply_coeffs, coeffs=coeffs_y, inverse=True), head_dim // 4),
+ ]
+
+ apply_fn_q = partial(_apply_block_diagonal, func_size_pairs=transforms_q)
+ apply_fn_kv = partial(_apply_block_diagonal, func_size_pairs=transforms_kv)
+ apply_fn_o = partial(_apply_block_diagonal, func_size_pairs=transforms_o)
+ return apply_fn_q, apply_fn_kv, apply_fn_o
+
+def _apply_tiled_projmat(
+ feats: torch.Tensor, # (batch, num_heads, seqlen, feat_dim)
+ matrix: torch.Tensor, # (batch, cameras, D, D)
+) -> torch.Tensor:
+ (batch, num_heads, seqlen, feat_dim) = feats.shape
+ cameras = matrix.shape[1]
+ assert seqlen >= cameras and seqlen % cameras == 0
+ D = matrix.shape[-1]
+ assert matrix.shape == (batch, cameras, D, D)
+ assert feat_dim % D == 0
+ return torch.einsum(
+ "bcij,bncpkj->bncpki",
+ matrix.to(dtype=feats.dtype),
+ feats.reshape((batch, num_heads, cameras, -1, feat_dim // D, D)),
+ ).reshape(feats.shape)
+
+
+def _rope_precompute_coeffs(
+ positions: torch.Tensor, # (seqlen,)
+ freq_base: float,
+ freq_scale: float,
+ feat_dim: int,
+ scaled_positions: torch.Tensor = None, # (seqlen,)
+ scale_dim: int = None,
+ gamma: torch.Tensor = None, # (seqlen,)
+) -> Tuple[torch.Tensor, torch.Tensor]:
+ assert len(positions.shape) == 1
+ assert feat_dim % 2 == 0
+ num_freqs = feat_dim // 2
+ freqs = freq_scale * (
+ freq_base
+ ** (
+ -torch.arange(num_freqs, device=positions.device)[None, None, None, :]
+ / num_freqs
+ )
+ )
+
+ if scaled_positions is not None and scale_dim is not None:
+ angles = torch.cat([positions[None, None, :, None].repeat(1,1,1,num_freqs-scale_dim), \
+ scaled_positions[None, None, :, None].repeat(1,1,1,scale_dim)], dim=-1) * freqs
+ elif gamma is not None:
+ base_angles = (positions[None, None, :1, None] - 1) * freqs * gamma
+ positions = positions - min(positions) + 1
+ delta_angles = positions[None, None, :, None] * freqs
+ angles = base_angles + delta_angles
+ else:
+ angles = positions[None, None, :, None] * freqs
+
+ assert angles.shape == (1, 1, positions.shape[0], num_freqs)
+ return torch.cos(angles), torch.sin(angles)
+
+def _rope_precompute_coeffs_dynamic_ntk(positions, freq_base, freq_scale, feat_dim,
+ max_train_len, current_len):
+ num_freqs = feat_dim // 2
+
+ if current_len <= max_train_len:
+ effective_base = freq_base
+ else:
+ ratio = current_len / max_train_len
+ effective_base = freq_base * (ratio ** (feat_dim / (feat_dim - 2)))
+
+ inv_freq = freq_scale * effective_base ** (
+ -torch.arange(0, num_freqs, device=positions.device)[None, None, None, :] / num_freqs
+ )
+ angles = positions[None, None, :, None] * inv_freq
+ return torch.cos(angles), torch.sin(angles)
+
+def _rope_precompute_coeffs_yarn(
+ positions: torch.Tensor,
+ freq_base: float,
+ freq_scale: float,
+ feat_dim: int,
+ scaling_factor: float,
+ original_max_position: int = 2048,
+ alpha: float = 1.0,
+ beta: float = 32.0,
+ delta: float = None,
+ red_ratio: float = 0.2,
+ ntk_by_parts: bool = False,
+ redundancy_ratio: float = None,
+) -> Tuple[torch.Tensor, torch.Tensor, float]:
+
+ num_freqs = feat_dim // 2
+ inv_freq = freq_base ** (
+ -torch.arange(0, num_freqs, device=positions.device, dtype=torch.float32)
+ / num_freqs
+ )
+
+ wavelengths = 2 * torch.pi / inv_freq
+ ratios = original_max_position / wavelengths
+
+ if delta is not None:
+ ada_alpha = alpha*(1 + delta * positions.max().item() / 211)
+ gamma = torch.clamp((ratios - ada_alpha) / (beta - ada_alpha), 0.0, 1.0)
+ elif redundancy_ratio is not None:
+ ada_beta = beta * (1 + red_ratio * (redundancy_ratio-0.5))
+ gamma = torch.clamp((ratios - alpha) / (ada_beta - alpha), 0.0, 1.0)
+ else:
+ gamma = torch.clamp((ratios - alpha) / (beta - alpha), 0.0, 1.0)
+
+ inv_freq_interpolated = inv_freq / scaling_factor
+ inv_freq_yarn = (1 - gamma) * inv_freq_interpolated + gamma * inv_freq
+
+ freqs = freq_scale * inv_freq_yarn[None, None, None, :]
+ angles = positions[None, None, :, None] * freqs
+
+ attn_scale = 0.1 * math.log(scaling_factor) + 1.0
+
+ assert angles.shape == (1, 1, positions.shape[0], num_freqs)
+ if ntk_by_parts:
+ return (torch.cos(angles), torch.sin(angles)), (1-gamma)/scaling_factor + gamma
+ else:
+ return (torch.cos(angles), torch.sin(angles)), attn_scale
+
+
+def _rope_apply_coeffs(
+ feats: torch.Tensor, # (batch, num_heads, seqlen, feat_dim)
+ coeffs: Tuple[torch.Tensor, torch.Tensor],
+ inverse: bool = False,
+) -> torch.Tensor:
+ cos, sin = coeffs
+ cos = cos.to(dtype=feats.dtype)
+ sin = sin.to(dtype=feats.dtype)
+ if cos.shape[2] != feats.shape[2]:
+ n_repeats = feats.shape[2] // cos.shape[2]
+ cos = cos.repeat(1, 1, n_repeats, 1)
+ sin = sin.repeat(1, 1, n_repeats, 1)
+ assert len(feats.shape) == len(cos.shape) == len(sin.shape) == 4
+ assert cos.shape[-1] == sin.shape[-1] == feats.shape[-1] // 2
+ x_in = feats[..., : feats.shape[-1] // 2]
+ y_in = feats[..., feats.shape[-1] // 2 :]
+ return torch.cat(
+ (
+ [cos * x_in + sin * y_in, -sin * x_in + cos * y_in]
+ if not inverse
+ else [cos * x_in - sin * y_in, sin * x_in + cos * y_in]
+ ),
+ dim=-1,
+ )
+
+def _apply_block_diagonal(
+ feats: torch.Tensor, # (..., dim)
+ func_size_pairs: List[Tuple[Callable[[torch.Tensor], torch.Tensor], int]],
+) -> torch.Tensor:
+ funcs, block_sizes = zip(*func_size_pairs)
+ assert feats.shape[-1] == sum(block_sizes)
+ x_blocks = torch.split(feats, block_sizes, dim=-1)
+ out = torch.cat(
+ [f(x_block) for f, x_block in zip(funcs, x_blocks)],
+ dim=-1,
+ )
+ assert out.shape == feats.shape, "Input/output shapes should match."
+ return out
+
+
+def _invert_SE3(transforms: torch.Tensor) -> torch.Tensor:
+ assert transforms.shape[-2:] == (4, 4)
+ Rinv = transforms[..., :3, :3].transpose(-1, -2)
+ out = torch.zeros_like(transforms, device=transforms.device, dtype=transforms.dtype)
+ out[..., :3, :3] = Rinv
+ out[..., :3, 3] = -torch.einsum("...ij,...j->...i", Rinv, transforms[..., :3, 3])
+ out[..., 3, 3] = 1.0
+ return out
+
+
+def _lift_K(Ks: torch.Tensor) -> torch.Tensor:
+ assert Ks.shape[-2:] == (3, 3)
+ out = torch.zeros(Ks.shape[:-2] + (4, 4), device=Ks.device, dtype=Ks.dtype)
+ out[..., :3, :3] = Ks
+ out[..., 3, 3] = 1.0
+ return out
+
+def _invert_K(Ks: torch.Tensor) -> torch.Tensor:
+ assert Ks.shape[-2:] == (3, 3)
+ out = torch.zeros_like(Ks, device=Ks.device, dtype=Ks.dtype)
+ out[..., 0, 0] = 1.0 / Ks[..., 0, 0]
+ out[..., 1, 1] = 1.0 / Ks[..., 1, 1]
+ out[..., 0, 2] = -Ks[..., 0, 2] / Ks[..., 0, 0]
+ out[..., 1, 2] = -Ks[..., 1, 2] / Ks[..., 1, 1]
+ out[..., 2, 2] = 1.0
+ return out
+
+
+def rescale_rope_by_scale(
+ n: int,
+ L: int,
+ W_right: Optional[int] = None,
+ scale = None,
+ is_history = False
+) -> torch.Tensor:
+
+ if W_right is None:
+ W_right = 0
+
+ cur_assigned_max_time = min(n, L)
+ count = n - W_right
+ budget = cur_assigned_max_time - W_right - 1
+
+ t = torch.linspace(0, 1, count)
+ scale = torch.cat([torch.zeros_like(scale[:1], device=scale.device, dtype=scale.dtype), scale])
+ left_mapped = torch.cumsum(scale, dim=0) * budget
+ right_mapped = torch.arange(W_right, dtype=torch.float32, device=left_mapped.device) + (cur_assigned_max_time - W_right)
+
+ result = torch.cat([left_mapped, right_mapped])
+ result = torch.clamp(result, 0, L - 1)
+ return result
+
+
+def rescale_rope_sin_dir_power(
+ n: int,
+ L: int,
+ W_right: Optional[int] = None,
+ smooth_transition: int = 0,
+ alpha: float = None,
+ **kwargs,
+) -> torch.Tensor:
+ if n <= L:
+ return torch.arange(n, dtype=torch.float32)
+
+ assert W_right is not None, "W_left and W_right must be denoted"
+
+ count = n - W_right
+ budget = L - W_right - 1
+
+ t = torch.linspace(0, 1, count)
+ if alpha is None:
+ compression_ratio = n / L
+ alpha = 1.0 + torch.log(torch.tensor(max(compression_ratio, 1.01))) * 0.5
+
+ left_mapped = torch.pow(t, alpha) * budget
+ right_mapped = torch.arange(W_right, dtype=torch.float32) + (L - W_right)
+
+ result = torch.cat([left_mapped, right_mapped])
+ result = torch.clamp(result, 0, L - 1)
+ return result
+
+def rescale_rope(
+ n: int,
+ L: int,
+ W_left: Optional[int] = None,
+ W_right: Optional[int] = None,
+ smooth_transition: int = 0,
+ **kwargs,
+) -> torch.Tensor:
+ if n <= L:
+ return torch.arange(n, dtype=torch.float32)
+
+ if W_left is None:
+ W_left = L // 4
+ if W_right is None:
+ W_right = L // 4
+
+ middle_count = n - W_left - W_right
+ middle_budget = L - W_left - W_right
+
+ if middle_count <= 0 or middle_budget <= 0:
+ return torch.linspace(0, L - 1, n)
+
+ left_mapped = torch.arange(W_left, dtype=torch.float32)
+
+ t = torch.linspace(0, 1, middle_count)
+ middle_mapped = _compress_power(t, n=n, L=L, W_left=W_left, W_right=W_right, **kwargs) * middle_budget + W_left
+
+ right_mapped = torch.arange(W_right, dtype=torch.float32) + (L - W_right)
+
+ result = torch.cat([left_mapped, middle_mapped, right_mapped])
+
+ result = torch.maximum(result[0], torch.cummax(result, dim=0)[0])
+ result = torch.clamp(result, 0, L - 1)
+ return result
+
+
+def _smooth_boundary(result, boundary_idx, T):
+ total = len(result)
+ start = max(0, boundary_idx - T)
+ end = min(total, boundary_idx + T)
+
+ original_spacing = result[1:] - result[:-1]
+
+ for i in range(start, min(end - 1, total - 1)):
+ local_t = (i - start) / (end - 1 - start)
+ blend = 0.5 * (1 - torch.cos(torch.tensor(3.14159265359 * local_t)))
+ target_spacing = (1 - blend) * original_spacing[i] + blend * 1.0
+ result[i + 1] = result[i] + target_spacing
+
+ return result
+
+
+def _compress_power(t, alpha=None, **kw):
+ if alpha is None:
+ n, L = kw.get("n", 100), kw.get("L", 50)
+ compression_ratio = n / L
+ alpha = 1.0 + torch.log(torch.tensor(max(compression_ratio, 1.01))) * 0.5
+
+ mask_first_half = t <= 0.5
+ mask_second_half = t > 0.5
+
+ result = torch.zeros_like(t)
+
+ if mask_first_half.any():
+ t_first = t[mask_first_half] * 2
+ result[mask_first_half] = torch.pow(t_first, alpha) * 0.5
+
+ if mask_second_half.any():
+ t_second = (t[mask_second_half] - 0.5) * 2
+ result[mask_second_half] = 0.5 + (1 - torch.pow(1 - t_second, alpha)) * 0.5
+
+ return result
diff --git a/DecMem/utils/scheduler.py b/DecMem/utils/scheduler.py
new file mode 100644
index 0000000000000000000000000000000000000000..cde3f85c8046b2d5e697b827f4531a3410c20e9a
--- /dev/null
+++ b/DecMem/utils/scheduler.py
@@ -0,0 +1,194 @@
+from abc import abstractmethod, ABC
+import torch
+
+
+class SchedulerInterface(ABC):
+ """
+ Base class for diffusion noise schedule.
+ """
+ alphas_cumprod: torch.Tensor # [T], alphas for defining the noise schedule
+
+ @abstractmethod
+ def add_noise(
+ self, clean_latent: torch.Tensor,
+ noise: torch.Tensor, timestep: torch.Tensor
+ ):
+ """
+ Diffusion forward corruption process.
+ Input:
+ - clean_latent: the clean latent with shape [B, C, H, W]
+ - noise: the noise with shape [B, C, H, W]
+ - timestep: the timestep with shape [B]
+ Output: the corrupted latent with shape [B, C, H, W]
+ """
+ pass
+
+ def convert_x0_to_noise(
+ self, x0: torch.Tensor, xt: torch.Tensor,
+ timestep: torch.Tensor
+ ) -> torch.Tensor:
+ """
+ Convert the diffusion network's x0 prediction to noise predidction.
+ x0: the predicted clean data with shape [B, C, H, W]
+ xt: the input noisy data with shape [B, C, H, W]
+ timestep: the timestep with shape [B]
+
+ noise = (xt-sqrt(alpha_t)*x0) / sqrt(beta_t) (eq 11 in https://arxiv.org/abs/2311.18828)
+ """
+ # use higher precision for calculations
+ original_dtype = x0.dtype
+ x0, xt, alphas_cumprod = map(
+ lambda x: x.double().to(x0.device), [x0, xt,
+ self.alphas_cumprod]
+ )
+
+ alpha_prod_t = alphas_cumprod[timestep].reshape(-1, 1, 1, 1)
+ beta_prod_t = 1 - alpha_prod_t
+
+ noise_pred = (xt - alpha_prod_t **
+ (0.5) * x0) / beta_prod_t ** (0.5)
+ return noise_pred.to(original_dtype)
+
+ def convert_noise_to_x0(
+ self, noise: torch.Tensor, xt: torch.Tensor,
+ timestep: torch.Tensor
+ ) -> torch.Tensor:
+ """
+ Convert the diffusion network's noise prediction to x0 predidction.
+ noise: the predicted noise with shape [B, C, H, W]
+ xt: the input noisy data with shape [B, C, H, W]
+ timestep: the timestep with shape [B]
+
+ x0 = (x_t - sqrt(beta_t) * noise) / sqrt(alpha_t) (eq 11 in https://arxiv.org/abs/2311.18828)
+ """
+ # use higher precision for calculations
+ original_dtype = noise.dtype
+ noise, xt, alphas_cumprod = map(
+ lambda x: x.double().to(noise.device), [noise, xt,
+ self.alphas_cumprod]
+ )
+ alpha_prod_t = alphas_cumprod[timestep].reshape(-1, 1, 1, 1)
+ beta_prod_t = 1 - alpha_prod_t
+
+ x0_pred = (xt - beta_prod_t **
+ (0.5) * noise) / alpha_prod_t ** (0.5)
+ return x0_pred.to(original_dtype)
+
+ def convert_velocity_to_x0(
+ self, velocity: torch.Tensor, xt: torch.Tensor,
+ timestep: torch.Tensor
+ ) -> torch.Tensor:
+ """
+ Convert the diffusion network's velocity prediction to x0 predidction.
+ velocity: the predicted noise with shape [B, C, H, W]
+ xt: the input noisy data with shape [B, C, H, W]
+ timestep: the timestep with shape [B]
+
+ v = sqrt(alpha_t) * noise - sqrt(beta_t) x0
+ noise = (xt-sqrt(alpha_t)*x0) / sqrt(beta_t)
+ given v, x_t, we have
+ x0 = sqrt(alpha_t) * x_t - sqrt(beta_t) * v
+ see derivations https://chatgpt.com/share/679fb6c8-3a30-8008-9b0e-d1ae892dac56
+ """
+ # use higher precision for calculations
+ original_dtype = velocity.dtype
+ velocity, xt, alphas_cumprod = map(
+ lambda x: x.double().to(velocity.device), [velocity, xt,
+ self.alphas_cumprod]
+ )
+ alpha_prod_t = alphas_cumprod[timestep].reshape(-1, 1, 1, 1)
+ beta_prod_t = 1 - alpha_prod_t
+
+ x0_pred = (alpha_prod_t ** 0.5) * xt - (beta_prod_t ** 0.5) * velocity
+ return x0_pred.to(original_dtype)
+
+
+class FlowMatchScheduler():
+
+ def __init__(self, num_inference_steps=100, num_train_timesteps=1000, shift=3.0, sigma_max=1.0, sigma_min=0.003 / 1.002, inverse_timesteps=False, extra_one_step=False, reverse_sigmas=False):
+ self.num_train_timesteps = num_train_timesteps
+ self.shift = shift
+ self.sigma_max = sigma_max
+ self.sigma_min = sigma_min
+ self.inverse_timesteps = inverse_timesteps
+ self.extra_one_step = extra_one_step
+ self.reverse_sigmas = reverse_sigmas
+ self.set_timesteps(num_inference_steps)
+
+ def set_timesteps(self, num_inference_steps=100, denoising_strength=1.0, training=False):
+ sigma_start = self.sigma_min + \
+ (self.sigma_max - self.sigma_min) * denoising_strength
+ if self.extra_one_step:
+ self.sigmas = torch.linspace(
+ sigma_start, self.sigma_min, num_inference_steps + 1)[:-1]
+ else:
+ self.sigmas = torch.linspace(
+ sigma_start, self.sigma_min, num_inference_steps)
+ if self.inverse_timesteps:
+ self.sigmas = torch.flip(self.sigmas, dims=[0])
+ self.sigmas = self.shift * self.sigmas / \
+ (1 + (self.shift - 1) * self.sigmas)
+ if self.reverse_sigmas:
+ self.sigmas = 1 - self.sigmas
+ self.timesteps = self.sigmas * self.num_train_timesteps
+ if training:
+ x = self.timesteps
+ y = torch.exp(-2 * ((x - num_inference_steps / 2) /
+ num_inference_steps) ** 2)
+ y_shifted = y - y.min()
+ bsmntw_weighing = y_shifted * \
+ (num_inference_steps / y_shifted.sum())
+ self.linear_timesteps_weights = bsmntw_weighing
+
+ def step(self, model_output, timestep, sample, to_final=False):
+ if timestep.ndim == 2:
+ timestep = timestep.flatten(0, 1)
+ self.sigmas = self.sigmas.to(model_output.device)
+ self.timesteps = self.timesteps.to(model_output.device)
+ timestep_id = torch.argmin(
+ (self.timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1)
+ sigma = self.sigmas[timestep_id].reshape(-1, 1, 1, 1)
+ if to_final or (timestep_id + 1 >= len(self.timesteps)).any():
+ sigma_ = 1 if (
+ self.inverse_timesteps or self.reverse_sigmas) else 0
+ else:
+ sigma_ = self.sigmas[timestep_id + 1].reshape(-1, 1, 1, 1)
+ prev_sample = sample + model_output * (sigma_ - sigma)
+ return prev_sample
+
+ def add_noise(self, original_samples, noise, timestep):
+ """
+ Diffusion forward corruption process.
+ Input:
+ - clean_latent: the clean latent with shape [B*T, C, H, W]
+ - noise: the noise with shape [B*T, C, H, W]
+ - timestep: the timestep with shape [B*T]
+ Output: the corrupted latent with shape [B*T, C, H, W]
+ """
+ if timestep.ndim == 2:
+ timestep = timestep.flatten(0, 1)
+ self.sigmas = self.sigmas.to(noise.device)
+ self.timesteps = self.timesteps.to(noise.device)
+ timestep_id = torch.argmin(
+ (self.timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1)
+ sigma = self.sigmas[timestep_id].reshape(-1, 1, 1, 1)
+ sample = (1 - sigma) * original_samples + sigma * noise
+ return sample.type_as(noise)
+
+ def training_target(self, sample, noise, timestep):
+ target = noise - sample
+ return target
+
+ def training_weight(self, timestep):
+ """
+ Input:
+ - timestep: the timestep with shape [B*T]
+ Output: the corresponding weighting [B*T]
+ """
+ if timestep.ndim == 2:
+ timestep = timestep.flatten(0, 1)
+ self.linear_timesteps_weights = self.linear_timesteps_weights.to(timestep.device)
+ timestep_id = torch.argmin(
+ (self.timesteps.unsqueeze(1) - timestep.unsqueeze(0)).abs(), dim=0)
+ weights = self.linear_timesteps_weights[timestep_id]
+ return weights
diff --git a/DecMem/utils/wan_wrapper.py b/DecMem/utils/wan_wrapper.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed6ffd99f48c345ec41f709fb018368cc6695e65
--- /dev/null
+++ b/DecMem/utils/wan_wrapper.py
@@ -0,0 +1,362 @@
+import types
+from typing import List, Optional
+import torch
+from torch import nn
+
+from utils.scheduler import SchedulerInterface, FlowMatchScheduler
+from utils.action_encoder import ActionEncoder
+from wan.modules.tokenizers import HuggingfaceTokenizer
+from wan.modules.model import WanModel, RegisterTokens, GanAttentionBlock
+from wan.modules.vae import _video_vae
+from wan.modules.t5 import umt5_xxl
+from wan.modules.causal_model import CausalWanModel
+
+
+class WanTextEncoder(torch.nn.Module):
+ def __init__(self) -> None:
+ super().__init__()
+
+ self.text_encoder = umt5_xxl(
+ encoder_only=True,
+ return_tokenizer=False,
+ dtype=torch.float32,
+ device=torch.device('cpu')
+ ).eval().requires_grad_(False)
+ self.text_encoder.load_state_dict(
+ torch.load("wan_models/Wan2.1-T2V-1.3B/models_t5_umt5-xxl-enc-bf16.pth",
+ map_location='cpu', weights_only=False)
+ )
+
+ self.tokenizer = HuggingfaceTokenizer(
+ name="wan_models/Wan2.1-T2V-1.3B/google/umt5-xxl/", seq_len=512, clean='whitespace')
+
+ @property
+ def device(self):
+ # Assume we are always on GPU
+ return torch.cuda.current_device()
+
+ def forward(self, text_prompts: List[str]) -> dict:
+ ids, mask = self.tokenizer(
+ text_prompts, return_mask=True, add_special_tokens=True)
+ ids = ids.to(self.device)
+ mask = mask.to(self.device)
+ seq_lens = mask.gt(0).sum(dim=1).long()
+ context = self.text_encoder(ids, mask)
+
+ for u, v in zip(context, seq_lens):
+ u[v:] = 0.0 # set padding to 0.0
+
+ return {
+ "prompt_embeds": context
+ }
+
+
+class WanVAEWrapper(torch.nn.Module):
+ def __init__(self):
+ super().__init__()
+ mean = [
+ -0.7571, -0.7089, -0.9113, 0.1075, -0.1745, 0.9653, -0.1517, 1.5508,
+ 0.4134, -0.0715, 0.5517, -0.3632, -0.1922, -0.9497, 0.2503, -0.2921
+ ]
+ std = [
+ 2.8184, 1.4541, 2.3275, 2.6558, 1.2196, 1.7708, 2.6052, 2.0743,
+ 3.2687, 2.1526, 2.8652, 1.5579, 1.6382, 1.1253, 2.8251, 1.9160
+ ]
+ self.mean = torch.tensor(mean, dtype=torch.float32)
+ self.std = torch.tensor(std, dtype=torch.float32)
+
+ # init model
+ self.model = _video_vae(
+ pretrained_path="wan_models/Wan2.1-T2V-1.3B/Wan2.1_VAE.pth",
+ z_dim=16,
+ ).eval().requires_grad_(False)
+
+ def encode_to_latent(self, pixel: torch.Tensor) -> torch.Tensor:
+ # pixel: [batch_size, num_channels, num_frames, height, width]
+ device, dtype = pixel.device, pixel.dtype
+ scale = [self.mean.to(device=device, dtype=dtype),
+ 1.0 / self.std.to(device=device, dtype=dtype)]
+
+ output = [
+ self.model.encode(u.unsqueeze(0), scale).float().squeeze(0)
+ for u in pixel
+ ]
+ output = torch.stack(output, dim=0)
+ # from [batch_size, num_channels, num_frames, height, width]
+ # to [batch_size, num_frames, num_channels, height, width]
+ output = output.permute(0, 2, 1, 3, 4)
+ return output
+
+ def decode_to_pixel(self, latent: torch.Tensor, use_cache: bool = False) -> torch.Tensor:
+ # from [batch_size, num_frames, num_channels, height, width]
+ # to [batch_size, num_channels, num_frames, height, width]
+ zs = latent.permute(0, 2, 1, 3, 4)
+ if use_cache:
+ assert latent.shape[0] == 1, "Batch size must be 1 when using cache"
+
+ device, dtype = latent.device, latent.dtype
+ scale = [self.mean.to(device=device, dtype=dtype),
+ 1.0 / self.std.to(device=device, dtype=dtype)]
+
+ if use_cache:
+ decode_function = self.model.cached_decode
+ else:
+ decode_function = self.model.decode
+
+ output = []
+ for u in zs:
+ output.append(decode_function(u.unsqueeze(0), scale).float().clamp_(-1, 1).squeeze(0))
+ output = torch.stack(output, dim=0)
+ # from [batch_size, num_channels, num_frames, height, width]
+ # to [batch_size, num_frames, num_channels, height, width]
+ output = output.permute(0, 2, 1, 3, 4)
+ return output
+
+
+class WanDiffusionWrapper(torch.nn.Module):
+ def __init__(
+ self,
+ model_name="Wan2.1-T2V-1.3B",
+ timestep_shift=8.0,
+ is_causal=False,
+ local_attn_size=-1,
+ sink_size=0,
+ use_action_encoder=False,
+ action_dim=25,
+ action_hidden_dim=64,
+ latent_channels=16,
+ use_ltm_attn=False,
+ ltm_tile_size=(1, 8, 8),
+ ltm_topk=-1,
+ stm_sliding_window=-1,
+ ):
+ super().__init__()
+
+ if is_causal:
+ # When use_ltm_attn=True the model gains mem_proj layers that the
+ # pretrained Wan ckpt doesn't have; disable low_cpu_mem_usage /
+ # device_map so diffusers lets us randomly initialise them.
+ extra_kwargs = (
+ {"low_cpu_mem_usage": False, "device_map": None}
+ if use_ltm_attn else {}
+ )
+ self.model = CausalWanModel.from_pretrained(
+ f"wan_models/{model_name}/",
+ local_attn_size=local_attn_size,
+ sink_size=sink_size,
+ use_ltm_attn=use_ltm_attn,
+ ltm_tile_size=tuple(ltm_tile_size),
+ ltm_topk=ltm_topk,
+ stm_sliding_window=stm_sliding_window,
+ **extra_kwargs,
+ )
+ else:
+ self.model = WanModel.from_pretrained(f"wan_models/{model_name}/")
+ self.model.eval()
+
+ # For non-causal diffusion, all frames share the same timestep
+ self.uniform_timestep = not is_causal
+
+ # Optional action encoder: encodes (B, T, 25) → (B, T, C, 1, 1) and adds to latents
+ self.use_action_encoder = use_action_encoder
+ if use_action_encoder:
+ self.action_encoder = ActionEncoder(action_dim, action_hidden_dim, latent_channels)
+
+ self.scheduler = FlowMatchScheduler(
+ shift=timestep_shift, sigma_min=0.0, extra_one_step=True
+ )
+ self.scheduler.set_timesteps(1000, training=True)
+
+ self.seq_len = 32760 # [1, 21, 16, 60, 104]
+ self.post_init()
+
+ def enable_gradient_checkpointing(self) -> None:
+ self.model.enable_gradient_checkpointing()
+
+ def adding_cls_branch(self, atten_dim=1536, num_class=4, time_embed_dim=0) -> None:
+ # NOTE: This is hard coded for WAN2.1-T2V-1.3B for now!!!!!!!!!!!!!!!!!!!!
+ self._cls_pred_branch = nn.Sequential(
+ # Input: [B, 384, 21, 60, 104]
+ nn.LayerNorm(atten_dim * 3 + time_embed_dim),
+ nn.Linear(atten_dim * 3 + time_embed_dim, 1536),
+ nn.SiLU(),
+ nn.Linear(atten_dim, num_class)
+ )
+ self._cls_pred_branch.requires_grad_(True)
+ num_registers = 3
+ self._register_tokens = RegisterTokens(num_registers=num_registers, dim=atten_dim)
+ self._register_tokens.requires_grad_(True)
+
+ gan_ca_blocks = []
+ for _ in range(num_registers):
+ block = GanAttentionBlock()
+ gan_ca_blocks.append(block)
+ self._gan_ca_blocks = nn.ModuleList(gan_ca_blocks)
+ self._gan_ca_blocks.requires_grad_(True)
+ # self.has_cls_branch = True
+
+ def _convert_flow_pred_to_x0(self, flow_pred: torch.Tensor, xt: torch.Tensor, timestep: torch.Tensor) -> torch.Tensor:
+ """
+ Convert flow matching's prediction to x0 prediction.
+ flow_pred: the prediction with shape [B, C, H, W]
+ xt: the input noisy data with shape [B, C, H, W]
+ timestep: the timestep with shape [B]
+
+ pred = noise - x0
+ x_t = (1-sigma_t) * x0 + sigma_t * noise
+ we have x0 = x_t - sigma_t * pred
+ see derivations https://chatgpt.com/share/67bf8589-3d04-8008-bc6e-4cf1a24e2d0e
+ """
+ # use higher precision for calculations
+ original_dtype = flow_pred.dtype
+ flow_pred, xt, sigmas, timesteps = map(
+ lambda x: x.double().to(flow_pred.device), [flow_pred, xt,
+ self.scheduler.sigmas,
+ self.scheduler.timesteps]
+ )
+
+ timestep_id = torch.argmin(
+ (timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1)
+ sigma_t = sigmas[timestep_id].reshape(-1, 1, 1, 1)
+ x0_pred = xt - sigma_t * flow_pred
+ return x0_pred.to(original_dtype)
+
+ @staticmethod
+ def _convert_x0_to_flow_pred(scheduler, x0_pred: torch.Tensor, xt: torch.Tensor, timestep: torch.Tensor) -> torch.Tensor:
+ """
+ Convert x0 prediction to flow matching's prediction.
+ x0_pred: the x0 prediction with shape [B, C, H, W]
+ xt: the input noisy data with shape [B, C, H, W]
+ timestep: the timestep with shape [B]
+
+ pred = (x_t - x_0) / sigma_t
+ """
+ # use higher precision for calculations
+ original_dtype = x0_pred.dtype
+ x0_pred, xt, sigmas, timesteps = map(
+ lambda x: x.double().to(x0_pred.device), [x0_pred, xt,
+ scheduler.sigmas,
+ scheduler.timesteps]
+ )
+ timestep_id = torch.argmin(
+ (timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1)
+ sigma_t = sigmas[timestep_id].reshape(-1, 1, 1, 1)
+ flow_pred = (xt - x0_pred) / sigma_t
+ return flow_pred.to(original_dtype)
+
+ def forward(
+ self,
+ noisy_image_or_video: torch.Tensor, conditional_dict: dict,
+ timestep: torch.Tensor, kv_cache: Optional[List[dict]] = None,
+ crossattn_cache: Optional[List[dict]] = None,
+ current_start: Optional[int] = None,
+ classify_mode: Optional[bool] = False,
+ concat_time_embeddings: Optional[bool] = False,
+ clean_x: Optional[torch.Tensor] = None,
+ aug_t: Optional[torch.Tensor] = None,
+ cache_start: Optional[int] = None
+ ) -> torch.Tensor:
+ # prompt_embeds is optional; when None the model skips text cross-attention
+ prompt_embeds = conditional_dict.get("prompt_embeds", None)
+
+ # Action injection: add action embeddings to noisy latents before transformer.
+ # Block-wise inference passes a pre-sliced "action_emb" (already encoded for
+ # the current chunk) to avoid re-encoding the full sequence each forward and
+ # to keep the latent T dim consistent with the slice.
+ if self.use_action_encoder and "action_emb" in conditional_dict:
+ action_emb = conditional_dict["action_emb"].to(noisy_image_or_video.dtype)
+ noisy_image_or_video = noisy_image_or_video + action_emb
+ if clean_x is not None:
+ clean_x = clean_x + action_emb
+ elif self.use_action_encoder and "actions" in conditional_dict:
+ actions = conditional_dict["actions"] # (B, T_vid, 25)
+ action_emb = self.action_encoder(actions.to(noisy_image_or_video.dtype)) # (B, T_lat, C, 1, 1)
+ noisy_image_or_video = noisy_image_or_video + action_emb
+ if clean_x is not None:
+ clean_x = clean_x + action_emb
+
+ # Extract viewmats for PRoPE (if provided)
+ viewmats = conditional_dict.get("viewmats", None)
+
+ # [B, F] -> [B]
+ if self.uniform_timestep:
+ input_timestep = timestep[:, 0]
+ else:
+ input_timestep = timestep
+
+ logits = None
+ # X0 prediction
+ if kv_cache is not None:
+ flow_pred = self.model(
+ noisy_image_or_video.permute(0, 2, 1, 3, 4),
+ t=input_timestep, context=prompt_embeds,
+ seq_len=self.seq_len,
+ kv_cache=kv_cache,
+ crossattn_cache=crossattn_cache,
+ current_start=current_start,
+ cache_start=cache_start
+ ).permute(0, 2, 1, 3, 4)
+ else:
+ if clean_x is not None:
+ # teacher forcing
+ flow_pred = self.model(
+ noisy_image_or_video.permute(0, 2, 1, 3, 4),
+ t=input_timestep, context=prompt_embeds,
+ seq_len=self.seq_len,
+ clean_x=clean_x.permute(0, 2, 1, 3, 4),
+ aug_t=aug_t,
+ viewmats=viewmats,
+ ).permute(0, 2, 1, 3, 4)
+ else:
+ if classify_mode:
+ flow_pred, logits = self.model(
+ noisy_image_or_video.permute(0, 2, 1, 3, 4),
+ t=input_timestep, context=prompt_embeds,
+ seq_len=self.seq_len,
+ classify_mode=True,
+ register_tokens=self._register_tokens,
+ cls_pred_branch=self._cls_pred_branch,
+ gan_ca_blocks=self._gan_ca_blocks,
+ concat_time_embeddings=concat_time_embeddings
+ )
+ flow_pred = flow_pred.permute(0, 2, 1, 3, 4)
+ else:
+ flow_pred = self.model(
+ noisy_image_or_video.permute(0, 2, 1, 3, 4),
+ t=input_timestep, context=prompt_embeds,
+ seq_len=self.seq_len,
+ viewmats=viewmats,
+ ).permute(0, 2, 1, 3, 4)
+
+ pred_x0 = self._convert_flow_pred_to_x0(
+ flow_pred=flow_pred.flatten(0, 1),
+ xt=noisy_image_or_video.flatten(0, 1),
+ timestep=timestep.flatten(0, 1)
+ ).unflatten(0, flow_pred.shape[:2])
+
+ if logits is not None:
+ return flow_pred, pred_x0, logits
+
+ return flow_pred, pred_x0
+
+ def get_scheduler(self) -> SchedulerInterface:
+ """
+ Update the current scheduler with the interface's static method
+ """
+ scheduler = self.scheduler
+ scheduler.convert_x0_to_noise = types.MethodType(
+ SchedulerInterface.convert_x0_to_noise, scheduler)
+ scheduler.convert_noise_to_x0 = types.MethodType(
+ SchedulerInterface.convert_noise_to_x0, scheduler)
+ scheduler.convert_velocity_to_x0 = types.MethodType(
+ SchedulerInterface.convert_velocity_to_x0, scheduler)
+ self.scheduler = scheduler
+ return scheduler
+
+ def post_init(self):
+ """
+ A few custom initialization steps that should be called after the object is created.
+ Currently, the only one we have is to bind a few methods to scheduler.
+ We can gradually add more methods here if needed.
+ """
+ self.get_scheduler()
diff --git a/DecMem/video_sparse_attn/MANIFEST.in b/DecMem/video_sparse_attn/MANIFEST.in
new file mode 100644
index 0000000000000000000000000000000000000000..de701fcb9c84347c40826028a1f1c0a754d5f107
--- /dev/null
+++ b/DecMem/video_sparse_attn/MANIFEST.in
@@ -0,0 +1,2 @@
+recursive-include tk *
+include config_vsa.py
diff --git a/DecMem/video_sparse_attn/README.md b/DecMem/video_sparse_attn/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..c4a027592dcf8b733f845d8e72622e601fab6bd7
--- /dev/null
+++ b/DecMem/video_sparse_attn/README.md
@@ -0,0 +1,61 @@
+
+
+# Attention Kernel Used in FastVideo
+
+## Video Sparse Attention (VSA)
+
+### Installation
+We support H100 (via TK) and any other GPU (via triton) for VSA.
+
+```bash
+pip install vsa
+```
+
+Install from source:
+
+```bash
+git submodule update --init --recursive
+python setup.py install
+```
+
+
+If you encounter error during installation, try below:
+Install C++20 for ThunderKittens:
+```bash
+sudo apt update
+sudo apt install gcc-11 g++-11
+
+sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 --slave /usr/bin/g++ g++ /usr/bin/g++-11
+
+sudo apt update
+sudo apt install clang-11
+```
+(If you use CUDA12.8)
+```bash
+export CUDA_HOME=/usr/local/cuda-12.8
+export PATH=${CUDA_HOME}/bin:${PATH}
+export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:$LD_LIBRARY_PATH
+```
+
+### Verify if you have successfully installed
+
+```bash
+# test numerical
+python ../tests/test_vsa.py
+# (For H100) test speed
+python ../benchmarks/bench_vsa_hopper.py
+```
+
+bench_vsa_hopper.py should print something like this:
+
+```bash
+Using topk=76 kv blocks per q block (out of 768 total kv blocks)
+
+=== BLOCK SPARSE ATTENTION BENCHMARK ===
+Block Sparse Forward - TFLOPS: 5622.26
+Block Sparse Backward - TFLOPS: 3865.68
+```
+
+## Acknowledgement
+
+We learned or reuse code from FlexAtteniton, NATEN, and ThunderKittens.
diff --git a/DecMem/video_sparse_attn/config_vsa.py b/DecMem/video_sparse_attn/config_vsa.py
new file mode 100644
index 0000000000000000000000000000000000000000..40e725426a8828ec97f94e43fd85c4bced8af913
--- /dev/null
+++ b/DecMem/video_sparse_attn/config_vsa.py
@@ -0,0 +1,15 @@
+### ADD TO THIS TO REGISTER NEW KERNELS
+sources = {
+ 'block_sparse': {
+ 'source_files': {
+ 'h100': 'vsa/block_sparse_h100.cu'
+ }
+ }
+}
+
+### WHICH KERNELS DO WE WANT TO BUILD?
+# (oftentimes during development work you don't need to redefine them all.)
+kernels = ['block_sparse']
+
+### WHICH GPU TARGET DO WE WANT TO BUILD FOR?
+target = 'h100'
diff --git a/DecMem/video_sparse_attn/setup.py b/DecMem/video_sparse_attn/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..55b5093149dd14622772aa1005b6c7950e141633
--- /dev/null
+++ b/DecMem/video_sparse_attn/setup.py
@@ -0,0 +1,88 @@
+import os
+import subprocess
+
+from config_vsa import kernels, sources, target
+from setuptools import find_packages, setup
+from torch.utils.cpp_extension import BuildExtension, CUDAExtension
+
+target = target.lower()
+
+# Package metadata
+PACKAGE_NAME = "vsa"
+VERSION = "0.0.3"
+AUTHOR = "Hao AI Lab"
+DESCRIPTION = "Video Sparse Attention Kernel Used in FastVideo"
+URL = "https://github.com/hao-ai-lab/FastVideo/tree/main/csrc/attn/video_sparse_attn"
+
+# Set environment variables
+tk_root = os.getenv('THUNDERKITTENS_ROOT', os.path.abspath(os.path.join(os.getcwd(), 'tk/')))
+python_include = subprocess.check_output(['python', '-c',
+ "import sysconfig; print(sysconfig.get_path('include'))"]).decode().strip()
+torch_include = subprocess.check_output([
+ 'python', '-c',
+ "import torch; from torch.utils.cpp_extension import include_paths; print(' '.join(['-I' + p for p in include_paths()]))"
+]).decode().strip()
+print('vsa root:', tk_root)
+print('Python include:', python_include)
+print('Torch include directories:', torch_include)
+
+# CUDA flags
+cuda_flags = [
+ '-DNDEBUG', '-Xcompiler=-Wno-psabi', '-Xcompiler=-fno-strict-aliasing', '--expt-extended-lambda',
+ '--expt-relaxed-constexpr', '-forward-unknown-to-host-compiler', '--use_fast_math', '-std=c++20', '-O3',
+ '-Xnvlink=--verbose', '-Xptxas=--verbose', '-Xptxas=--warn-on-spills', f'-I{tk_root}/include',
+ f'-I{tk_root}/prototype', f'-I{python_include}', '-DTORCH_COMPILE'
+] + torch_include.split()
+cpp_flags = ['-std=c++20', '-O3']
+# cuda_flags = [
+# '-DNDEBUG', '-Xcompiler=-Wno-psabi', '-Xcompiler=-fno-strict-aliasing', '--expt-extended-lambda',
+# '--expt-relaxed-constexpr', '-forward-unknown-to-host-compiler', '--use_fast_math', '-std=c++17', '-O3',
+# '-Xnvlink=--verbose', '-Xptxas=--verbose', '-Xptxas=--warn-on-spills', f'-I{tk_root}/include',
+# f'-I{tk_root}/prototype', f'-I{python_include}', '-DTORCH_COMPILE'
+# ] + torch_include.split()
+# cpp_flags = ['-std=c++17', '-O3']
+
+if target == 'h100':
+ cuda_flags.append('-DKITTENS_HOPPER')
+ # CUDA 12.9's -arch shorthand also emits generic sm_90 PTX, which cannot use WGMMA.
+ cuda_flags.append('-gencode=arch=compute_90a,code=sm_90a')
+else:
+ raise ValueError(f'Target {target} not supported')
+
+source_files = ['vsa.cpp']
+for k in kernels:
+ if target not in sources[k]['source_files']:
+ raise KeyError(f'Target {target} not found in source files for kernel {k}')
+ if isinstance(sources[k]['source_files'][target], list):
+ source_files.extend(sources[k]['source_files'][target])
+ else:
+ source_files.append(sources[k]['source_files'][target])
+ cpp_flags.append(f'-DTK_COMPILE_{k.replace(" ", "_").upper()}')
+
+
+ext_modules = [
+ CUDAExtension('vsa_cuda',
+ sources=source_files,
+ extra_compile_args={
+ 'cxx': cpp_flags,
+ 'nvcc': cuda_flags
+ },
+ libraries=['cuda'])
+]
+
+setup(name=PACKAGE_NAME,
+ version=VERSION,
+ author=AUTHOR,
+ description=DESCRIPTION,
+ url=URL,
+ packages=find_packages(),
+ ext_modules=ext_modules,
+ cmdclass={'build_ext': BuildExtension},
+ classifiers=[
+ "Programming Language :: Python :: 3",
+ "Environment :: GPU :: NVIDIA CUDA :: 12",
+ "License :: OSI Approved :: Apache Software License",
+ ],
+ python_requires='>=3.10',
+ install_requires=["torch>=2.5.0"]
+ )
diff --git a/DecMem/video_sparse_attn/tk/.gitignore b/DecMem/video_sparse_attn/tk/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..d944fa50c365326dffb24e3d92428b6dcee2ff30
--- /dev/null
+++ b/DecMem/video_sparse_attn/tk/.gitignore
@@ -0,0 +1,11 @@
+*.i
+*.ii
+*.gpu
+*.ptx
+*.cubin
+*.fatbin
+*.txt
+*.d
+*.ncu-rep
+__pycache__/
+*/printouts/*
diff --git a/DecMem/video_sparse_attn/tk/Doxyfile b/DecMem/video_sparse_attn/tk/Doxyfile
new file mode 100644
index 0000000000000000000000000000000000000000..78de736d50079846213ecadfa2c171b3f6d2c03d
--- /dev/null
+++ b/DecMem/video_sparse_attn/tk/Doxyfile
@@ -0,0 +1,2658 @@
+# Doxyfile 1.9.1
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the configuration
+# file that follow. The default is UTF-8 which is also the encoding used for all
+# text before the first occurrence of this tag. Doxygen uses libiconv (or the
+# iconv built into libc) for the transcoding. See
+# https://www.gnu.org/software/libiconv/ for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME = "ThunderKittens"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER = 0.0.1
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF = "Tile primitives for speedy kernels"
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO =
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY = "docs/"
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS = NO
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE = English
+
+# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all generated output in the proper direction.
+# Possible values are: None, LTR, RTL and Context.
+# The default value is: None.
+
+OUTPUT_TEXT_DIRECTION = None
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF = "The $name class" \
+ "The $name widget" \
+ "The $name file" \
+ is \
+ provides \
+ specifies \
+ contains \
+ represents \
+ a \
+ an \
+ the
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF = NO
+
+# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line
+# such as
+# /***************
+# as being the beginning of a Javadoc-style comment "banner". If set to NO, the
+# Javadoc-style will behave just like regular comments and it will not be
+# interpreted by doxygen.
+# The default value is: NO.
+
+JAVADOC_BANNER = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# By default Python docstrings are displayed as preformatted text and doxygen's
+# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the
+# doxygen's special commands can be used and the contents of the docstring
+# documentation blocks is shown as doxygen documentation.
+# The default value is: YES.
+
+PYTHON_DOCSTRING = YES
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE = 4
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines (in the resulting output). You can put ^^ in the value part of an
+# alias to insert a newline as if a physical newline was in the original file.
+# When you need a literal { or } or , in the value part of an alias you have to
+# escape them by means of a backslash (\), this can lead to conflicts with the
+# commands \{ and \} for these it is advised to use the version @{ and @} or use
+# a double escape (\\{ and \\})
+
+ALIASES =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL = NO
+
+# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice
+# sources only. Doxygen will then generate output that is more tailored for that
+# language. For instance, namespaces will be presented as modules, types will be
+# separated into more groups, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_SLICE = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
+# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL,
+# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
+# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
+# tries to guess whether the code is fixed or free formatted code, this is the
+# default for Fortran type files). For instance to make doxygen treat .inc files
+# as Fortran files (default is PHP), and .f files as C (default is Fortran),
+# use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen. When specifying no_extension you should add
+# * to the FILE_PATTERNS.
+#
+# Note see also the list of default file extension mappings.
+
+EXTENSION_MAPPING = cuh=c++ cu=c++
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See https://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT = YES
+
+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
+# to that level are automatically included in the table of contents, even if
+# they do not have an id attribute.
+# Note: This feature currently applies only to Markdown headings.
+# Minimum value: 0, maximum value: 99, default value: 5.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+TOC_INCLUDE_HEADINGS = 5
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC = NO
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE = 0
+
+# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use
+# during processing. When set to 0 doxygen will based this on the number of
+# cores available in the system. You can set it explicitly to a value larger
+# than 0 to get more control over the balance between CPU load and processing
+# speed. At this moment only the input processing can be done using multiple
+# threads. Since this is still an experimental feature the default is set to 1,
+# which efficively disables parallel processing. Please report any issues you
+# encounter. Generating dot graphs in parallel is controlled by the
+# DOT_NUM_THREADS setting.
+# Minimum value: 0, maximum value: 32, default value: 1.
+
+NUM_PROC_THREADS = 1
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE = NO
+
+# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual
+# methods of a class will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIV_VIRTUAL = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE = NO
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC = YES
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES = NO
+
+# If this flag is set to YES, the name of an unnamed parameter in a declaration
+# will be determined by the corresponding definition. By default unnamed
+# parameters remain unnamed in the output.
+# The default value is: YES.
+
+RESOLVE_UNNAMED_PARAMS = YES
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# declarations. If set to NO, these declarations will be included in the
+# documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS = NO
+
+# With the correct setting of option CASE_SENSE_NAMES doxygen will better be
+# able to match the capabilities of the underlying filesystem. In case the
+# filesystem is case sensitive (i.e. it supports files in the same directory
+# whose names only differ in casing), the option must be set to YES to properly
+# deal with such files in case they appear in the input. For filesystems that
+# are not case sensitive the option should be be set to NO to properly deal with
+# output files written for symbols that only differ in casing, such as for two
+# classes, one named CLASS and the other named Class, and to also support
+# references to files without having to specify the exact matching casing. On
+# Windows (including Cygwin) and MacOS, users should typically set this option
+# to NO, whereas on Linux or other Unix flavors it should typically be set to
+# YES.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES = YES
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if ... \endif and \cond
+# ... \endcond blocks.
+
+ENABLED_SECTIONS =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation. If
+# EXTRACT_ALL is set to YES then this flag will automatically be disabled.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC = NO
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
+# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
+# at the end of the doxygen process doxygen will return with a non-zero status.
+# Possible values are: NO, YES and FAIL_ON_WARNINGS.
+# The default value is: NO.
+
+WARN_AS_ERROR = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT = src/
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see:
+# https://www.gnu.org/software/libiconv/) for the list of possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# Note the list of default checked file patterns might differ from the list of
+# default file extension mappings.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
+# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment),
+# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl,
+# *.ucf, *.qsf and *.ice.
+
+FILE_PATTERNS = *.c \
+ *.cc \
+ *.cxx \
+ *.cpp \
+ *.c++ \
+ *.java \
+ *.ii \
+ *.ixx \
+ *.ipp \
+ *.i++ \
+ *.inl \
+ *.idl \
+ *.ddl \
+ *.odl \
+ *.h \
+ *.hh \
+ *.hxx \
+ *.hpp \
+ *.h++ \
+ *.cs \
+ *.d \
+ *.php \
+ *.php4 \
+ *.php5 \
+ *.phtml \
+ *.inc \
+ *.m \
+ *.markdown \
+ *.md \
+ *.mm \
+ *.dox \
+ *.cuh \
+ *.cu \
+ *.f90 \
+ *.f95 \
+ *.f03 \
+ *.f08 \
+ *.f18 \
+ *.f \
+ *.for \
+ *.vhd \
+ *.vhdl \
+ *.ucf \
+ *.qsf \
+ *.ice
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS = *
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+#
+#
+# where is the value of the INPUT_FILTER tag, and is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# entity all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see https://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS = YES
+
+# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the
+# clang parser (see:
+# http://clang.llvm.org/) for more accurate parsing at the cost of reduced
+# performance. This can be particularly helpful with template rich C++ code for
+# which doxygen's built-in parser lacks the necessary type information.
+# Note: The availability of this option depends on whether or not doxygen was
+# generated with the -Duse_libclang=ON option for CMake.
+# The default value is: NO.
+
+CLANG_ASSISTED_PARSING = NO
+
+# If clang assisted parsing is enabled and the CLANG_ADD_INC_PATHS tag is set to
+# YES then doxygen will add the directory of each input to the include path.
+# The default value is: YES.
+
+CLANG_ADD_INC_PATHS = YES
+
+# If clang assisted parsing is enabled you can provide the compiler with command
+# line options that you would normally use when invoking the compiler. Note that
+# the include paths will already be set by doxygen for the files and directories
+# specified with INPUT and INCLUDE_PATH.
+# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.
+
+CLANG_OPTIONS =
+
+# If clang assisted parsing is enabled you can provide the clang parser with the
+# path to the directory containing a file called compile_commands.json. This
+# file is the compilation database (see:
+# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the
+# options used when the source files were built. This is equivalent to
+# specifying the -p option to a clang tool, such as clang-check. These options
+# will then be passed to the parser. Any options specified with CLANG_OPTIONS
+# will be added as well.
+# Note: The availability of this option depends on whether or not doxygen was
+# generated with the -Duse_libclang=ON option for CMake.
+
+CLANG_DATABASE_PATH =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX = YES
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP = NO
+
+# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
+# documentation will contain a main index with vertical navigation menus that
+# are dynamically created via JavaScript. If disabled, the navigation index will
+# consists of multiple levels of tabs that are statically embedded in every HTML
+# page. Disable this option to support browsers that do not have JavaScript,
+# like the Qt help browser.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_MENUS = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see:
+# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To
+# create a documentation set, doxygen will generate a Makefile in the HTML
+# output directory. Running make will produce the docset in that directory and
+# running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy
+# genXcode/_index.html for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see:
+# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the main .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS =
+
+# The QHG_LOCATION tag can be used to specify the location (absolute path
+# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to
+# run qhelpgenerator on the generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW = NO
+
+# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
+# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
+# https://inkscape.org) to generate formulas as SVG images instead of PNGs for
+# the HTML output. These images will generally look nicer at scaled resolutions.
+# Possible values are: png (the default) and svg (looks nicer but requires the
+# pdf2svg or inkscape tool).
+# The default value is: png.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FORMULA_FORMAT = png
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE = 10
+
+# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT = YES
+
+# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
+# to create new LaTeX commands to be used in formulas as building blocks. See
+# the section "Including formulas" for details.
+
+FORMULA_MACROFILE =
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# https://www.mathjax.org) which uses client side JavaScript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from https://www.mathjax.org before deployment.
+# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see:
+# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use + S
+# (what the is depends on the OS and browser, but it is typically
+# , /