stevengrove commited on
Commit
fcfea15
·
0 Parent(s):

Initial commit with Xet-tracked image assets

Browse files
Files changed (47) hide show
  1. .gitattributes +38 -0
  2. .gitignore +7 -0
  3. LICENSE +190 -0
  4. README.md +280 -0
  5. app.py +206 -0
  6. assets/architecture.png +3 -0
  7. assets/capability-radar.png +3 -0
  8. assets/spatial-editing-showcase.png +3 -0
  9. assets/spatial-reasoning-showcase.png +3 -0
  10. assets/text-rendering-showcase.png +3 -0
  11. demo_release.py +0 -0
  12. images/example_1.jpg +3 -0
  13. images/example_2.png +3 -0
  14. images/example_3.jpg +3 -0
  15. images/example_4.png +3 -0
  16. inference.py +126 -0
  17. inference_und.py +183 -0
  18. pyproject.toml +31 -0
  19. requirements.txt +17 -0
  20. src/infer_runtime/__init__.py +1 -0
  21. src/infer_runtime/checkpoints.py +63 -0
  22. src/infer_runtime/infer_config.py +53 -0
  23. src/infer_runtime/model.py +139 -0
  24. src/infer_runtime/prompt_rewrite.py +107 -0
  25. src/infer_runtime/settings.py +70 -0
  26. src/modules/__init__.py +0 -0
  27. src/modules/models/__init__.py +131 -0
  28. src/modules/models/attention.py +119 -0
  29. src/modules/models/bucket.py +108 -0
  30. src/modules/models/mmdit/dit/__init__.py +4 -0
  31. src/modules/models/mmdit/dit/models.py +539 -0
  32. src/modules/models/mmdit/dit/modulate_layers.py +80 -0
  33. src/modules/models/mmdit/dit/posemb_layers.py +320 -0
  34. src/modules/models/mmdit/text_encoder/__init__.py +23 -0
  35. src/modules/models/mmdit/vae/__init__.py +3 -0
  36. src/modules/models/mmdit/vae/wanvae.py +697 -0
  37. src/modules/models/pipeline.py +971 -0
  38. src/modules/models/scheduler.py +265 -0
  39. src/modules/utils/__init__.py +65 -0
  40. src/modules/utils/constants.py +7 -0
  41. src/modules/utils/fsdp_load.py +208 -0
  42. src/modules/utils/logging.py +38 -0
  43. src/modules/utils/utils.py +27 -0
  44. test_images/test_1.jpg +3 -0
  45. test_images/test_2.jpg +3 -0
  46. test_images/test_3.png +3 -0
  47. three.min.js +0 -0
.gitattributes ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.png filter=lfs diff=lfs merge=lfs -text
37
+ *.jpg filter=lfs diff=lfs merge=lfs -text
38
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+
4
+ .pytest_cache/
5
+
6
+ test_outputs/
7
+ outputs/
LICENSE ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to the Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by the Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding any notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ Copyright 2025 JD.com, Inc.
179
+
180
+ Licensed under the Apache License, Version 2.0 (the "License");
181
+ you may not use this file except in compliance with the License.
182
+ You may obtain a copy of the License at
183
+
184
+ http://www.apache.org/licenses/LICENSE-2.0
185
+
186
+ Unless required by applicable law or agreed to in writing, software
187
+ distributed under the License is distributed on an "AS IS" BASIS,
188
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189
+ See the License for the specific language governing permissions and
190
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: JoyAI-Image-Edit-Space
3
+ emoji: 🚀
4
+ colorFrom: indigo
5
+ colorTo: pink
6
+ sdk: gradio
7
+ sdk_version: "6.5.1"
8
+ python_version: "3.10"
9
+ app_file: app.py
10
+ pinned: false
11
+ ---
12
+
13
+ <h1 align="center">JoyAI-Image<br><sub><sup>Awakening Spatial Intelligence in Unified Multimodal Understanding and Generation</sup></sub></h1>
14
+
15
+ <div align="center">
16
+
17
+ [![Report PDF](https://img.shields.io/badge/Report-PDF-red)](https://joyai-image.s3.cn-north-1.jdcloud-oss.com/JoyAI-Image.pdf)
18
+ [![Project](https://img.shields.io/badge/Project-JoyAI--Image-333399)](https://github.com/jd-opensource/JoyAI-Image)
19
+ [![Hugging Face](https://img.shields.io/badge/%F0%9F%A4%97%20Checkpoint-JoyAI--Image--Edit-yellow)](https://huggingface.co/jdopensource/JoyAI-Image-Edit)&#160;
20
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
21
+
22
+ </div>
23
+
24
+ ## 🔥🔥🔥 News!!
25
+ * 2026.04.02: 🎉 We release the JoyAI-Image-Edit weights. Please Check at [Huggingface](https://huggingface.co/jdopensource/JoyAI-Image-Edit).
26
+
27
+
28
+ ## 🐶 JoyAI-Image
29
+
30
+ JoyAI-Image is a **unified multimodal foundation model** for image understanding, text-to-image generation, and instruction-guided image editing. It combines an 8B Multimodal Large Language Model (MLLM) with a 16B Multimodal Diffusion Transformer (MMDiT). A central principle of JoyAI-Image is the **closed-loop collaboration between understanding, generation, and editing**. Stronger spatial understanding improves grounded generation and contrallable editing through better scene parsing, relational grounding, and instruction decomposition, while generative transformations such as viewpoint changes provide complementary evidence for spatial reasoning.
31
+
32
+
33
+ ## 📦 Model Zoo
34
+
35
+ | Models | Task | Description | Download Link |
36
+ |----------------------|--------------------------|-----------------------------------------------------------------------------|-----------------|
37
+ | JoyAI-Image-Und | Multimodal Understanding | A text–image understanding backbone that enables high-fidelity spatial reasoning and editing-aware perception. | 🤗[Hugging Face](https://huggingface.co/jdopensource/JoyAI-Image-Edit/tree/main/JoyAI-Image-Und) |
38
+ | JoyAI-Image-Edit | Image Editing | An instruction-guided image editing model with precise and controllable spatial manipulation. | 🤗[Hugging Face](https://huggingface.co/jdopensource/JoyAI-Image-Edit) |
39
+ | JoyAI-Image-Edit-Plus | Multi-Image Editing | An instruction-guided model that supports multi-image editing, enabling cross-image composition, consistency, and joint manipulation. | To be released |
40
+ | JoyAI-Image | Text-to-Image | A high-quality text-to-image generation model with strong multi-view consistency. | To be released |
41
+
42
+
43
+ ![JoyAI-Image Architecture](assets/architecture.png)
44
+
45
+ ## 💎 Highlights
46
+
47
+ - **Unified multimodal foundation**: one model family for understanding, generation, and editing through a shared MLLM-MMDiT interface.
48
+ - **Practical data and training recipe**: a scalable pipeline with spatial understanding data ([OpenSpatial](https://github.com/VINHYU/OpenSpatial)), long-text rendering data, editing data ([SpatialEdit](https://github.com/EasonXiao-888/SpatialEdit)), and multi-stage optimization strategies.
49
+ - **Awakened spatial intelligence**: stronger spatial understanding, controllable spatial editing, and novel-view-assisted reasoning through a bidirectional loop between understanding and generation.
50
+ - **Advanced visual generation**: strong long-text typography, layout fidelity, multi-view generation, and controllable editing with better preservation of scene structure.
51
+
52
+ ## 🔍 Visual Overview
53
+
54
+ ### Capability Profile
55
+
56
+ JoyAI-Image demonstrates broad multimodal performance across understanding, synthesis, and editing, with particular strengths in spatial reasoning, long-text rendering, multi-view generation, and controllable editing.
57
+
58
+ ![JoyAI-Image Capability Radar](assets/capability-radar.png)
59
+
60
+ ### Advanced Text Rendering Showcase
61
+
62
+ JoyAI-Image is optimized for challenging text-heavy scenarios, including multi-panel comics, dense multi-line text, multilingual typography, long-form layouts, real-world scene text, and handwritten styles.
63
+
64
+ ![JoyAI-Image Text Rendering Showcase](assets/text-rendering-showcase.png)
65
+
66
+ ### Multi-view Generation and Spatial Editing Showcase
67
+
68
+ JoyAI-Image showcases a spatially grounded generation and editing pipeline that supports multi-view generation, geometry-aware transformations, camera control, object rotation, and precise location-specific object editing. Across these settings, it preserves scene content, structure, and visual consistency while following viewpoint-sensitive instructions more accurately.
69
+
70
+ ![JoyAI-Image Multi-view Generation and Spatial Editing Showcase Showcase](assets/spatial-editing-showcase.png)
71
+
72
+ ### Spatial Editing for Spatial Reasoning Showcase
73
+
74
+ JoyAI-Image poses high-fidelity spatial editing, serving as a powerful catalyst for enhancing spatial reasoning. Compared with Qwen-Image-Edit and Nano Banana Pro, JoyAI-Image-Edit synthesizes the most diagnostic viewpoints by faithfully executing camera motions. These high-fidelity novel views effectively disambiguate complex spatial relations, providing clearer visual evidence for downstream reasoning.
75
+
76
+ ![JoyAI-Image Spatial Editing for Spatial Reasoning Showcase](assets/spatial-reasoning-showcase.png)
77
+
78
+
79
+ ## 🚀 Quick Start
80
+
81
+ ### 1. Environment Setup
82
+
83
+ **Requirements**: Python >= 3.10, CUDA-capable GPU
84
+
85
+ Create a virtual environment and install:
86
+
87
+ ```bash
88
+ conda create -n joyai python=3.10 -y
89
+ conda activate joyai
90
+
91
+ pip install -e .
92
+ ```
93
+
94
+ > **Note on Flash Attention**: `flash-attn` is optional and is intentionally **not** installed in Hugging Face Spaces builds by default, because pip build isolation frequently breaks its installation there. The Space will fall back to PyTorch SDPA automatically.
95
+
96
+ #### Core Dependencies
97
+
98
+ | Package | Version | Purpose |
99
+ |---------|---------|---------|
100
+ | `torch` | >= 2.8 | PyTorch |
101
+ | `transformers` | >= 4.57.0, < 4.58.0 | Text encoder |
102
+ | `diffusers` | >= 0.34.0 | Pipeline utilities |
103
+ | `flash-attn` | optional | Fast attention kernel (local install only) |
104
+
105
+
106
+ ### 2. Inference
107
+
108
+ #### 2.1 Image Understanding
109
+
110
+ ```bash
111
+ python inference_und.py \
112
+ --ckpt-root /path/to/ckpts_infer \
113
+ --image "test_images/test_1.jpg,test_images/test3.png" \
114
+ --prompt "Compare these two images." \
115
+ --max-new-tokens 1024
116
+ ```
117
+
118
+ #### CLI Reference (`inference_und.py`)
119
+
120
+ | Argument | Type | Default | Description |
121
+ |----------|------|---------|-------------|
122
+ | `--ckpt-root` | str | *required* | Checkpoint root containing `text_encoder/` |
123
+ | `--image` | str | *required* | Input image path, or comma-separated paths for multiple images |
124
+ | `--prompt` | str | `"Describe this image in detail."` | User question or instruction. When omitted, defaults to image captioning |
125
+ | `--max-new-tokens` | int | 2048 | Maximum number of tokens to generate |
126
+ | `--temperature` | float | 0.7 | Sampling temperature. Use `0` for greedy decoding |
127
+ | `--top-p` | float | 0.8 | Top-p (nucleus) sampling threshold |
128
+ | `--top-k` | int | 50 | Top-k sampling threshold |
129
+ | `--output` | str | None | Optional output file to save the response text |
130
+
131
+ #### 2.2 Image Editing
132
+
133
+ ```bash
134
+ python inference.py \
135
+ --ckpt-root /path/to/ckpts_infer \
136
+ --prompt "Turn the plate blue" \
137
+ --image test_images/test_1.jpg \
138
+ --output outputs/result.png \
139
+ --seed 123 \
140
+ --steps 30 \
141
+ --guidance-scale 5.0 \
142
+ --basesize 1024
143
+ ```
144
+
145
+ #### CLI Reference (`inference.py`)
146
+
147
+ | Argument | Type | Default | Description |
148
+ |----------|------|---------|-------------|
149
+ | `--ckpt-root` | str | *required* | Checkpoint root |
150
+ | `--prompt` | str | *required* | Edit instruction or T2I prompt |
151
+ | `--image` | str | None | Input image path (required for editing, omit for T2I) |
152
+ | `--output` | str | `example.png` | Output image path |
153
+ | `--steps` | int | 50 | Denoising steps |
154
+ | `--guidance-scale` | float | 5.0 | Classifier-free guidance scale |
155
+ | `--seed` | int | 42 | Random seed for reproducibility |
156
+ | `--neg-prompt` | str | `""` | Negative prompt |
157
+ | `--basesize` | int | 1024 | Bucket base size for input image resizing (256/512/768/1024) |
158
+ | `--config` | str | auto | Config path; defaults to `<ckpt-root>/infer_config.py` |
159
+ | `--rewrite-prompt` | flag | off | Enable LLM-based prompt rewriting |
160
+ | `--rewrite-model` | str | `gpt-5` | Model name for prompt rewriting |
161
+ | `--hsdp-shard-dim` | int | 1 | FSDP shard dimension for multi-GPU (set to GPU count) |
162
+
163
+
164
+ ### 3. Spatial Editing Reference
165
+
166
+ JoyAI-Image supports three spatial editing prompt patterns: **Object Move**, **Object Rotation**, and **Camera Control**. For the most stable behavior, we recommend following the prompt templates below as closely as possible.
167
+
168
+ #### 3.1 Object Move
169
+
170
+ Use this pattern when you want to move a target object into a specified region.
171
+
172
+ **Prompt template:**
173
+
174
+ ```text
175
+ Move the <object> into the red box and finally remove the red box.
176
+ ```
177
+
178
+ **Rules:**
179
+
180
+ * Replace `<object>` with a clear description of the target object to be moved.
181
+ * The **red box** indicates the target destination in the image.
182
+ * The phrase **"finally remove the red box"** means the guidance box should not appear in the final edited result.
183
+
184
+ **Example:**
185
+
186
+ ```text
187
+ Move the apple into the red box and finally remove the red box.
188
+ ```
189
+
190
+ #### 3.2 Object Rotation
191
+
192
+ Use this pattern when you want to rotate an object to a specific canonical view.
193
+
194
+ **Prompt template:**
195
+
196
+ ```text
197
+ Rotate the <object> to show the <view> side view.
198
+ ```
199
+
200
+ **Supported `<view>` values:**
201
+
202
+ * `front`
203
+ * `right`
204
+ * `left`
205
+ * `rear`
206
+ * `front right`
207
+ * `front left`
208
+ * `rear right`
209
+ * `rear left`
210
+
211
+ **Rules:**
212
+
213
+ * Replace `<object>` with a clear description of the object to rotate.
214
+ * Replace `<view>` with one of the supported directions above.
215
+ * This instruction is intended to change the **object orientation**, while keeping the object identity and surrounding scene as consistent as possible.
216
+
217
+ **Examples:**
218
+
219
+ ```text
220
+ Rotate the chair to show the front side view.
221
+ Rotate the car to show the rear left side view.
222
+ ```
223
+
224
+ #### 3.3 Camera Control
225
+
226
+ Use this pattern when you want to change only the camera viewpoint while keeping the 3D scene itself unchanged.
227
+
228
+ **Prompt template:**
229
+
230
+ ```text
231
+ Move the camera.
232
+ - Camera rotation: Yaw {y_rotation}°, Pitch {p_rotation}°.
233
+ - Camera zoom: in/out/unchanged.
234
+ - Keep the 3D scene static; only change the viewpoint.
235
+ ```
236
+
237
+ **Rules:**
238
+
239
+ * `{y_rotation}` specifies the yaw rotation angle in degrees.
240
+ * `{p_rotation}` specifies the pitch rotation angle in degrees.
241
+ * `Camera zoom` must be one of:
242
+
243
+ * `in`
244
+ * `out`
245
+ * `unchanged`
246
+ * The last line is important: it explicitly tells the model to preserve the 3D scene content and geometry, and only adjust the camera viewpoint.
247
+
248
+ **Examples:**
249
+
250
+ ```text
251
+ Move the camera.
252
+ - Camera rotation: Yaw 45°, Pitch 0°.
253
+ - Camera zoom: in.
254
+ - Keep the 3D scene static; only change the viewpoint.
255
+ ```
256
+
257
+ ```text
258
+ Move the camera.
259
+ - Camera rotation: Yaw -90°, Pitch 20°.
260
+ - Camera zoom: unchanged.
261
+ - Keep the 3D scene static; only change the viewpoint.
262
+ ```
263
+
264
+
265
+ ## ⚖️ License Agreement
266
+
267
+ JoyAI-Image is licensed under Apache 2.0.
268
+
269
+ ## ☎️ We're Hiring!
270
+ We are actively hiring Research Scientists, Engineers, and Interns to join us in building next-generation generative foundation models and bringing them into real-world applications. If you’re interested, please send your resume to: huanghaoyang.ocean@jd.com
271
+
272
+
273
+ ## Space Runtime Note
274
+
275
+ This Space should run with client-side rendering enabled. On Hugging Face Spaces, Gradio SSR is enabled by default for newer Gradio versions, so `app.py` explicitly launches with `ssr_mode=False` to avoid SSR routing issues with the custom HTML/JS controls.
276
+
277
+
278
+ ## ZeroGPU / Stateless GPU note
279
+
280
+ This Space now defaults to `MODEL_LOAD_MODE=cpu_preload`. On Hugging Face ZeroGPU, CUDA still cannot be initialized in the main process, so the Space preloads the model globally on CPU during startup and only migrates it to GPU inside a `@spaces.GPU` inference call. By default, the model is moved back to CPU after each inference to stay compatible with stateless GPU workers.
app.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import random
4
+ import torch
5
+ import spaces
6
+
7
+ import os
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ from huggingface_hub import snapshot_download
12
+
13
+ from demo_release import (
14
+ EditorApp,
15
+ build_demo_examples_from_config,
16
+ create_demo,
17
+ is_rank0,
18
+ read_local_js_inline,
19
+ resolve_local_three_js,
20
+ )
21
+ from modules.models.attention import describe_attention_backend
22
+ from modules.utils import clean_dist_env, maybe_init_distributed
23
+
24
+
25
+ ROOT_DIR = Path(__file__).resolve().parent
26
+ TRUE_VALUES = {"1", "true", "yes", "y"}
27
+ GPU_PRELOAD_MODES = {
28
+ "startup",
29
+ "startup_preload",
30
+ "boot",
31
+ "auto",
32
+ "eager",
33
+ "preload",
34
+ "gpu",
35
+ "gpu_preload",
36
+ "cuda",
37
+ "global_cuda",
38
+ }
39
+ CPU_PRELOAD_MODES = {"cpu_preload", "cpu", "cpu_only", "cpu_global_preload"}
40
+
41
+
42
+ def env_flag(name: str, default: str = "0") -> bool:
43
+ return os.getenv(name, default).strip().lower() in TRUE_VALUES
44
+
45
+
46
+ def env_optional_int(name: str) -> int | None:
47
+ value = os.getenv(name, "").strip()
48
+ return int(value) if value else None
49
+
50
+
51
+ def env_optional_str(name: str) -> str | None:
52
+ value = os.getenv(name, "").strip()
53
+ return value or None
54
+
55
+
56
+ def resolve_ckpt_root(model_repo_id: str, explicit_ckpt_root: str | None, hf_token: str | None) -> str:
57
+ if explicit_ckpt_root:
58
+ return explicit_ckpt_root
59
+ return snapshot_download(repo_id=model_repo_id, token=hf_token)
60
+
61
+
62
+ def build_app() -> tuple[EditorApp, str, str | None, str | None, bool, bool, bool, int, bool, str]:
63
+ model_repo_id = os.getenv("MODEL_REPO_ID", "jdopensource/JoyAI-Image-Edit")
64
+ ckpt_root_env = env_optional_str("CKPT_ROOT")
65
+ config_path = env_optional_str("CONFIG_PATH")
66
+ rewrite_prompt = env_flag("REWRITE_PROMPT")
67
+ rewrite_model = os.getenv("REWRITE_MODEL", "gpt-5")
68
+ basesize = int(os.getenv("BASESIZE", "1024"))
69
+ hide_advanced_options = env_flag("HIDE_ADVANCED_OPTIONS")
70
+ auto_pe = env_flag("AUTO_PE")
71
+ default_save_dir = os.getenv("DEFAULT_SAVE_DIR", "")
72
+ hsdp_shard_dim = env_optional_int("HSDP_SHARD_DIM")
73
+ model_load_mode = os.getenv("MODEL_LOAD_MODE", "startup_preload").strip().lower()
74
+ hf_token = env_optional_str("HF_TOKEN") or env_optional_str("HUGGING_FACE_HUB_TOKEN")
75
+
76
+ ckpt_root = resolve_ckpt_root(model_repo_id, ckpt_root_env, hf_token)
77
+ app = EditorApp(
78
+ ckpt_root=ckpt_root,
79
+ config_path=config_path,
80
+ rewrite_model=rewrite_model,
81
+ hsdp_shard_dim=hsdp_shard_dim,
82
+ enable_prompt_rewrite=rewrite_prompt,
83
+ basesize=basesize,
84
+ device=None,
85
+ model_load_mode=model_load_mode,
86
+ )
87
+ return (
88
+ app,
89
+ model_repo_id,
90
+ ckpt_root,
91
+ config_path,
92
+ rewrite_prompt,
93
+ rewrite_model,
94
+ hide_advanced_options,
95
+ basesize,
96
+ auto_pe,
97
+ default_save_dir,
98
+ )
99
+
100
+
101
+ def print_startup_info(
102
+ *,
103
+ model_repo_id: str,
104
+ ckpt_root: str,
105
+ config_path: str | None,
106
+ rewrite_prompt: bool,
107
+ rewrite_model: str,
108
+ basesize: int,
109
+ auto_pe: bool,
110
+ hide_advanced_options: bool,
111
+ three_js_file: str | None,
112
+ ) -> None:
113
+ if not is_rank0():
114
+ return
115
+
116
+ print("[Info] Direct GPU startup preload is enabled by default; the app will try to build the model globally on CUDA during startup.")
117
+ print(f"[Info] Attention backend: {describe_attention_backend()}")
118
+ print(f"[Info] MODEL_REPO_ID: {model_repo_id}")
119
+ print(f"[Info] CKPT_ROOT: {ckpt_root}")
120
+ print(f"[Info] CONFIG_PATH: {config_path or '(auto)'}")
121
+ print(f"[Info] REWRITE_PROMPT: {rewrite_prompt}")
122
+ print(f"[Info] REWRITE_MODEL: {rewrite_model}")
123
+ print(f"[Info] BASESIZE: {basesize}")
124
+ print(f"[Info] AUTO_PE: {auto_pe}")
125
+ print(f"[Info] HIDE_ADVANCED_OPTIONS: {hide_advanced_options}")
126
+ if three_js_file:
127
+ print(f"[Info] Using local three.js: {three_js_file}")
128
+ else:
129
+ print("[Info] No local three.min.js found. Falling back to slider-only mode.")
130
+
131
+
132
+ def maybe_preload(app: EditorApp) -> None:
133
+ mode = (app.model_load_mode or "").strip().lower()
134
+ if mode in GPU_PRELOAD_MODES:
135
+ print("[Model] Using direct global GPU preload mode.")
136
+ app.maybe_preload_model()
137
+ return
138
+ if mode in CPU_PRELOAD_MODES:
139
+ print("[Model] Using CPU preload mode.")
140
+ app.maybe_preload_model()
141
+ return
142
+ print(f"[Model] Using runtime loading mode: {mode}")
143
+
144
+
145
+ def build_demo(app: EditorApp, hide_advanced_options: bool, auto_pe: bool, default_save_dir: str):
146
+ examples_table, examples_full = build_demo_examples_from_config()
147
+ three_js_path = os.getenv("THREE_JS_PATH", str(ROOT_DIR / "three.min.js"))
148
+ three_js_file = resolve_local_three_js(three_js_path if Path(three_js_path).exists() else None)
149
+ inline_js = read_local_js_inline(three_js_file)
150
+
151
+ demo, _, page_css = create_demo(
152
+ app,
153
+ three_available=three_js_file is not None,
154
+ hide_advanced_options=hide_advanced_options,
155
+ examples_table=examples_table,
156
+ examples_full=examples_full,
157
+ auto_pe=auto_pe,
158
+ default_save_dir=default_save_dir,
159
+ )
160
+ launch_css = page_css + "\n.fillable{max-width: 1400px !important}"
161
+ allowed_paths = [
162
+ str(Path(tempfile.gettempdir()).resolve()),
163
+ str((ROOT_DIR / "images").resolve()),
164
+ ]
165
+ return demo, inline_js, launch_css, allowed_paths, three_js_file
166
+
167
+
168
+ def main() -> None:
169
+ dist_initialized = maybe_init_distributed()
170
+ app, model_repo_id, ckpt_root, config_path, rewrite_prompt, rewrite_model, hide_advanced_options, basesize, auto_pe, default_save_dir = build_app()
171
+ demo, inline_js, launch_css, allowed_paths, three_js_file = build_demo(
172
+ app,
173
+ hide_advanced_options=hide_advanced_options,
174
+ auto_pe=auto_pe,
175
+ default_save_dir=default_save_dir,
176
+ )
177
+
178
+ print_startup_info(
179
+ model_repo_id=model_repo_id,
180
+ ckpt_root=ckpt_root,
181
+ config_path=config_path,
182
+ rewrite_prompt=rewrite_prompt,
183
+ rewrite_model=rewrite_model,
184
+ basesize=basesize,
185
+ auto_pe=auto_pe,
186
+ hide_advanced_options=hide_advanced_options,
187
+ three_js_file=three_js_file,
188
+ )
189
+ maybe_preload(app)
190
+
191
+ try:
192
+ demo.queue(default_concurrency_limit=1, max_size=20).launch(
193
+ server_name="0.0.0.0",
194
+ server_port=int(os.getenv("PORT", "7860")),
195
+ ssr_mode=False,
196
+ head=inline_js,
197
+ css=launch_css,
198
+ allowed_paths=allowed_paths,
199
+ )
200
+ finally:
201
+ if dist_initialized:
202
+ clean_dist_env()
203
+
204
+
205
+ if __name__ == "__main__":
206
+ main()
assets/architecture.png ADDED

Git LFS Details

  • SHA256: b43cbec49a816e30c37dfc5e216d04391c7e66f0302eca404c0f644294cc0abf
  • Pointer size: 132 Bytes
  • Size of remote file: 1.2 MB
assets/capability-radar.png ADDED

Git LFS Details

  • SHA256: 012f7bdeda44d6fcb43f67350f56e6972b800c25c63629a44bf3c015b6b43c5c
  • Pointer size: 131 Bytes
  • Size of remote file: 697 kB
assets/spatial-editing-showcase.png ADDED

Git LFS Details

  • SHA256: 37d4434dcd40c72d6331f68d57f6a3e7af27496e034f738b062abcc9083370b2
  • Pointer size: 132 Bytes
  • Size of remote file: 4.35 MB
assets/spatial-reasoning-showcase.png ADDED

Git LFS Details

  • SHA256: 5862074e22618f6e4c6322128b8aac051ad2b9111fcef2a30f5138c581fdaddf
  • Pointer size: 131 Bytes
  • Size of remote file: 550 kB
assets/text-rendering-showcase.png ADDED

Git LFS Details

  • SHA256: 3adede7528e137f0e3cd07509a0c43efea8c576f8069f339e27a128549026fde
  • Pointer size: 133 Bytes
  • Size of remote file: 10.1 MB
demo_release.py ADDED
The diff for this file is too large to render. See raw diff
 
images/example_1.jpg ADDED

Git LFS Details

  • SHA256: 622b9dd6c815b8d0bcb90476d0d8004ceb047958b6528e2a80ce9eb6c46c5e9e
  • Pointer size: 131 Bytes
  • Size of remote file: 189 kB
images/example_2.png ADDED

Git LFS Details

  • SHA256: f11e8b9d939ad39dd4a89075577b15d31c79135f1e9c4b03cbf8a216823a976e
  • Pointer size: 132 Bytes
  • Size of remote file: 1.5 MB
images/example_3.jpg ADDED

Git LFS Details

  • SHA256: 223ea753b4d90ee85770ca75fbc85aa609ad56f5f6d0a37fef5ab7d23a4f21e8
  • Pointer size: 131 Bytes
  • Size of remote file: 287 kB
images/example_4.png ADDED

Git LFS Details

  • SHA256: 76bc8aabe6417e9992f6e25f70b343774053e3eb1f588895d7f97750d17a7439
  • Pointer size: 132 Bytes
  • Size of remote file: 5.17 MB
inference.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local inference entrypoint for the clean JoyAI-Image release."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ import time
9
+ import warnings
10
+ from pathlib import Path
11
+
12
+ import torch
13
+ from PIL import Image
14
+
15
+
16
+ ROOT_DIR = Path(__file__).resolve().parent
17
+ SRC_DIR = ROOT_DIR / 'src'
18
+ if str(SRC_DIR) not in sys.path:
19
+ sys.path.insert(0, str(SRC_DIR))
20
+
21
+ warnings.filterwarnings('ignore')
22
+
23
+
24
+ def parse_args() -> argparse.Namespace:
25
+ parser = argparse.ArgumentParser(description='Run local inference without FastAPI.')
26
+ parser.add_argument('--ckpt-root', required=True, help='Checkpoint root.')
27
+ parser.add_argument('--prompt', required=True, help='Edit prompt or T2I prompt.')
28
+ parser.add_argument('--image', help='Optional input image path for image editing.')
29
+ parser.add_argument('--output', default='example.png', help='Output image path.')
30
+ parser.add_argument('--height', type=int, default=1024, help='Only used for text-to-image inference.')
31
+ parser.add_argument('--width', type=int, default=1024, help='Only used for text-to-image inference.')
32
+ parser.add_argument('--steps', type=int, default=50)
33
+ parser.add_argument('--guidance-scale', type=float, default=5.0)
34
+ parser.add_argument('--seed', type=int, default=42)
35
+ parser.add_argument('--neg-prompt', default='')
36
+ parser.add_argument('--basesize', type=int, default=1024, help='Resize bucket base size for image editing inputs.')
37
+ parser.add_argument('--rewrite-prompt', action='store_true')
38
+ parser.add_argument('--config', help='Optional config path. Defaults to <ckpt-root>/infer_config.py.')
39
+ parser.add_argument('--rewrite-model', default='gpt-5')
40
+ parser.add_argument('--hsdp-shard-dim', type=int, help='Override config hsdp_shard_dim for multi-GPU FSDP inference.')
41
+ return parser.parse_args()
42
+
43
+
44
+ def load_input_image(image_path: str | None) -> Image.Image | None:
45
+ if not image_path:
46
+ return None
47
+ return Image.open(image_path).convert('RGB')
48
+
49
+
50
+ def is_rank0() -> bool:
51
+ return int(os.environ.get('RANK', '0')) == 0
52
+
53
+
54
+ def resolve_device() -> torch.device:
55
+ if not torch.cuda.is_available():
56
+ return torch.device('cpu')
57
+ local_rank = int(os.environ.get('LOCAL_RANK', '0'))
58
+ torch.cuda.set_device(local_rank)
59
+ return torch.device(f'cuda:{local_rank}')
60
+
61
+
62
+ def main() -> None:
63
+ args = parse_args()
64
+
65
+ from infer_runtime.model import InferenceParams, build_model
66
+ from infer_runtime.settings import load_settings
67
+ from modules.utils import maybe_init_distributed, clean_dist_env
68
+ from modules.models.attention import describe_attention_backend
69
+
70
+ dist_initialized = False
71
+ try:
72
+ settings = load_settings(
73
+ ckpt_root=args.ckpt_root,
74
+ config_path=args.config,
75
+ rewrite_model=args.rewrite_model,
76
+ default_seed=args.seed,
77
+ )
78
+ device = resolve_device()
79
+ dist_initialized = maybe_init_distributed()
80
+
81
+ if is_rank0():
82
+ print(f'Chosen device: {device}')
83
+ print(f'Attention backend: {describe_attention_backend()}')
84
+ print(f'Config path: {settings.config_path}')
85
+ print(f'Checkpoint path: {settings.ckpt_path}')
86
+ if args.hsdp_shard_dim is not None:
87
+ print(f'Override hsdp_shard_dim: {args.hsdp_shard_dim}')
88
+
89
+ model = build_model(
90
+ settings,
91
+ device=device,
92
+ hsdp_shard_dim_override=args.hsdp_shard_dim,
93
+ )
94
+ input_image = load_input_image(args.image)
95
+ effective_prompt = model.maybe_rewrite_prompt(args.prompt, input_image, args.rewrite_prompt)
96
+
97
+ start_time = time.time()
98
+ output_image = model.infer(
99
+ InferenceParams(
100
+ prompt=effective_prompt,
101
+ image=input_image,
102
+ height=args.height,
103
+ width=args.width,
104
+ steps=args.steps,
105
+ guidance_scale=args.guidance_scale,
106
+ seed=args.seed,
107
+ neg_prompt=args.neg_prompt,
108
+ basesize=args.basesize,
109
+ )
110
+ )
111
+ elapsed = time.time() - start_time
112
+
113
+ if is_rank0():
114
+ output_path = Path(args.output)
115
+ output_path.parent.mkdir(parents=True, exist_ok=True)
116
+ output_image.save(output_path)
117
+ print(f'Prompt used: {effective_prompt}')
118
+ print(f'Saved output: {output_path}')
119
+ print(f'Time taken: {elapsed:.2f} seconds')
120
+ finally:
121
+ if dist_initialized:
122
+ clean_dist_env()
123
+
124
+
125
+ if __name__ == '__main__':
126
+ main()
inference_und.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local inference entrypoint for the image understanding capability of JoyAI-Image."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ import time
8
+ import warnings
9
+ from pathlib import Path
10
+
11
+ import torch
12
+
13
+
14
+ ROOT_DIR = Path(__file__).resolve().parent
15
+ SRC_DIR = ROOT_DIR / "src"
16
+ if str(SRC_DIR) not in sys.path:
17
+ sys.path.insert(0, str(SRC_DIR))
18
+
19
+ from PIL import Image
20
+
21
+ warnings.filterwarnings("ignore")
22
+
23
+
24
+ def parse_args() -> argparse.Namespace:
25
+ parser = argparse.ArgumentParser(
26
+ description="Run image understanding inference with the JoyAI-Image MLLM.",
27
+ )
28
+ parser.add_argument(
29
+ "--ckpt-root", required=True,
30
+ help="Checkpoint root containing text_encoder/ directory.",
31
+ )
32
+ parser.add_argument(
33
+ "--image", required=True,
34
+ help="Input image path (or comma-separated paths for multiple images).",
35
+ )
36
+ parser.add_argument(
37
+ "--prompt", default=None,
38
+ help="User prompt / question about the image. "
39
+ "Defaults to a detailed description request if not provided.",
40
+ )
41
+ parser.add_argument(
42
+ "--max-new-tokens", type=int, default=2048,
43
+ help="Maximum number of tokens to generate.",
44
+ )
45
+ parser.add_argument(
46
+ "--temperature", type=float, default=0.7,
47
+ help="Sampling temperature. Use 0 for greedy decoding.",
48
+ )
49
+ parser.add_argument(
50
+ "--top-p", type=float, default=0.8,
51
+ help="Top-p (nucleus) sampling threshold.",
52
+ )
53
+ parser.add_argument(
54
+ "--top-k", type=int, default=50,
55
+ help="Top-k sampling threshold.",
56
+ )
57
+ parser.add_argument(
58
+ "--output", default=None,
59
+ help="Optional output file to save the response text.",
60
+ )
61
+ return parser.parse_args()
62
+
63
+
64
+ def load_images(image_arg: str) -> list[Image.Image]:
65
+ paths = [p.strip() for p in image_arg.split(",")]
66
+ images = []
67
+ for p in paths:
68
+ if not Path(p).is_file():
69
+ raise FileNotFoundError(f"Image not found: {p}")
70
+ images.append(Image.open(p).convert("RGB"))
71
+ return images
72
+
73
+
74
+ def resolve_text_encoder_path(ckpt_root: str) -> Path:
75
+ root = Path(ckpt_root).expanduser().resolve()
76
+ text_encoder_dir = root / "JoyAI-Image-Und"
77
+ if not text_encoder_dir.is_dir():
78
+ raise FileNotFoundError(
79
+ f"Expected text_encoder/ directory inside checkpoint root: {root}"
80
+ )
81
+ return text_encoder_dir
82
+
83
+
84
+ def build_conversation(
85
+ images: list[Image.Image],
86
+ prompt: str | None,
87
+ ) -> list[dict]:
88
+ SYS_PROMPT = "You are a helpful assistant."
89
+
90
+ default_prompt = "Describe this image in detail."
91
+ user_text = prompt if prompt is not None else default_prompt
92
+
93
+ image_content = [{"type": "image", "image": img} for img in images]
94
+ user_content = image_content + [{"type": "text", "text": user_text}]
95
+
96
+ messages = [
97
+ {"role": "system", "content": SYS_PROMPT},
98
+ {"role": "user", "content": user_content},
99
+ ]
100
+ return messages
101
+
102
+
103
+ def main() -> None:
104
+ args = parse_args()
105
+
106
+ from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
107
+
108
+ text_encoder_path = resolve_text_encoder_path(args.ckpt_root)
109
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
110
+ dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32
111
+
112
+ print(f"Loading MLLM from: {text_encoder_path}")
113
+ print(f"Device: {device}, dtype: {dtype}")
114
+
115
+ model = Qwen3VLForConditionalGeneration.from_pretrained(
116
+ str(text_encoder_path),
117
+ torch_dtype=dtype,
118
+ local_files_only=True,
119
+ trust_remote_code=True,
120
+ ).to(device).eval()
121
+
122
+ processor = AutoProcessor.from_pretrained(
123
+ str(text_encoder_path),
124
+ local_files_only=True,
125
+ trust_remote_code=True,
126
+ )
127
+
128
+ images = load_images(args.image)
129
+ messages = build_conversation(images, args.prompt)
130
+
131
+ text_input = processor.apply_chat_template(
132
+ messages, tokenize=False, add_generation_prompt=True,
133
+ )
134
+ inputs = processor(
135
+ text=[text_input],
136
+ images=images,
137
+ padding=True,
138
+ return_tensors="pt",
139
+ ).to(device)
140
+
141
+ print(f"Input tokens: {inputs['input_ids'].shape[1]}")
142
+ print("Generating...")
143
+
144
+ start_time = time.time()
145
+
146
+ generate_kwargs = dict(
147
+ max_new_tokens=args.max_new_tokens,
148
+ )
149
+ if args.temperature == 0:
150
+ generate_kwargs["do_sample"] = False
151
+ else:
152
+ generate_kwargs["do_sample"] = True
153
+ generate_kwargs["temperature"] = args.temperature
154
+ generate_kwargs["top_p"] = args.top_p
155
+ generate_kwargs["top_k"] = args.top_k
156
+
157
+ with torch.no_grad():
158
+ output_ids = model.generate(**inputs, **generate_kwargs)
159
+
160
+ # Strip the input tokens from the output
161
+ generated_ids = output_ids[:, inputs["input_ids"].shape[1]:]
162
+ response = processor.batch_decode(
163
+ generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False,
164
+ )[0]
165
+
166
+ elapsed = time.time() - start_time
167
+ num_output_tokens = generated_ids.shape[1]
168
+
169
+ print(f"\n{'=' * 60}")
170
+ print(f"Response:\n{response}")
171
+ print(f"{'=' * 60}")
172
+ print(f"Output tokens: {num_output_tokens}")
173
+ print(f"Time: {elapsed:.2f}s ({num_output_tokens / elapsed:.1f} tok/s)")
174
+
175
+ if args.output:
176
+ output_path = Path(args.output)
177
+ output_path.parent.mkdir(parents=True, exist_ok=True)
178
+ output_path.write_text(response, encoding="utf-8")
179
+ print(f"Saved response to: {output_path}")
180
+
181
+
182
+ if __name__ == "__main__":
183
+ main()
pyproject.toml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "joyai-image-release"
7
+ version = "0.1.0"
8
+ description = "Package for JoyAI-Image image inference."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "Apache-2.0"}
12
+ dependencies = [
13
+ "accelerate",
14
+ "diffusers>=0.34.0",
15
+ "einops",
16
+ "loguru",
17
+ "packaging",
18
+ "pillow",
19
+ "safetensors",
20
+ "sentencepiece",
21
+ "torch",
22
+ "torchvision",
23
+ "transformers>=4.57.0,<4.58.0",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ flash-attn = ["flash-attn>=2.8.0"]
28
+ rewrite = ["openai"]
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ accelerate
2
+ diffusers>=0.34.0
3
+ einops
4
+ fastapi
5
+ loguru
6
+ openai
7
+ packaging
8
+ pillow
9
+ pydantic>=2
10
+ requests
11
+ safetensors
12
+ sentencepiece
13
+ torch
14
+ torchvision
15
+ transformers>=4.57.0,<4.58.0
16
+ uvicorn
17
+
src/infer_runtime/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __all__: list[str] = []
src/infer_runtime/checkpoints.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ from pathlib import Path
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class CheckpointLayout:
10
+ root: Path
11
+ transformer_ckpt: Path
12
+ vae_ckpt: Path
13
+ text_encoder_ckpt: Path
14
+
15
+
16
+ def _must_exist(path: Path, kind: str) -> Path:
17
+ if not (path.exists() or path.is_symlink()):
18
+ raise FileNotFoundError(f"Missing {kind}: {path}")
19
+ return path
20
+
21
+
22
+ def _find_single_entry(directory: Path, kind: str, *, expect_dir: bool) -> Path:
23
+ _must_exist(directory, f"{kind} directory")
24
+ entries = sorted(p for p in directory.iterdir() if not p.name.startswith("."))
25
+ if len(entries) != 1:
26
+ raise FileNotFoundError(f"Expected exactly one entry in {directory} for {kind}, found {len(entries)}")
27
+ entry = entries[0]
28
+ if expect_dir:
29
+ if not (entry.is_dir() or entry.is_symlink()):
30
+ raise FileNotFoundError(f"Expected directory-like entry for {kind}: {entry}")
31
+ else:
32
+ if not (entry.is_file() or entry.is_symlink()):
33
+ raise FileNotFoundError(f"Expected file-like entry for {kind}: {entry}")
34
+ return entry
35
+
36
+
37
+ def resolve_checkpoint_layout(root: str | Path) -> CheckpointLayout:
38
+ root_path = Path(root).expanduser().resolve()
39
+ transformer_ckpt = str(root_path / "transformer" / "transformer.pth")
40
+ vae_ckpt = _find_single_entry(root_path / "vae", "vae checkpoint", expect_dir=False)
41
+ text_encoder_ckpt = _must_exist(root_path / "JoyAI-Image-Und", "text encoder checkpoint directory")
42
+ if not text_encoder_ckpt.is_dir():
43
+ raise FileNotFoundError(f"Expected text encoder checkpoint directory: {text_encoder_ckpt}")
44
+ return CheckpointLayout(
45
+ root=root_path,
46
+ transformer_ckpt=transformer_ckpt,
47
+ vae_ckpt=vae_ckpt,
48
+ text_encoder_ckpt=text_encoder_ckpt,
49
+ )
50
+
51
+ def build_manifest(layout: CheckpointLayout) -> dict[str, str]:
52
+ return {
53
+ "root": str(layout.root),
54
+ "transformer_ckpt": str(layout.transformer_ckpt),
55
+ "vae_ckpt": str(layout.vae_ckpt),
56
+ "text_encoder_ckpt": str(layout.text_encoder_ckpt),
57
+ }
58
+
59
+
60
+ def write_manifest(layout: CheckpointLayout, output_path: str | Path) -> Path:
61
+ output = Path(output_path)
62
+ output.write_text(json.dumps(build_manifest(layout), indent=2) + "\n", encoding="utf-8")
63
+ return output
src/infer_runtime/infer_config.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import importlib.util
5
+ import inspect
6
+ from pathlib import Path
7
+ from typing import Any, Type
8
+
9
+
10
+ @dataclass
11
+ class InferConfig:
12
+ dit_ckpt: str | None = None
13
+ dit_ckpt_type: str = "pt"
14
+ dit_arch_config: dict[str, Any] | None = None
15
+ dit_precision: str = "bf16"
16
+
17
+ vae_arch_config: dict[str, Any] | None = None
18
+ vae_precision: str = "bf16"
19
+
20
+ text_encoder_arch_config: dict[str, Any] | None = None
21
+ text_encoder_precision: str = "bf16"
22
+ text_token_max_length: int = 2048
23
+
24
+ scheduler_arch_config: dict[str, Any] | None = None
25
+
26
+ training_mode: bool = False
27
+ hsdp_shard_dim: int = 1
28
+ reshard_after_forward: bool = False
29
+ use_fsdp_inference: bool = False
30
+ cpu_offload: bool = False
31
+ pin_cpu_memory: bool = False
32
+
33
+
34
+ def load_infer_config_class_from_pyfile(file_path: str) -> Type[Any]:
35
+ path = Path(file_path)
36
+ if not path.is_file():
37
+ raise FileNotFoundError(f"Configuration file not found: {file_path}")
38
+
39
+ module_name = path.stem
40
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
41
+ if spec is None or spec.loader is None:
42
+ raise ImportError(f"Could not create module spec for '{file_path}'.")
43
+
44
+ module = importlib.util.module_from_spec(spec)
45
+ spec.loader.exec_module(module)
46
+
47
+ for _, obj in inspect.getmembers(module, inspect.isclass):
48
+ if obj is InferConfig:
49
+ continue
50
+ if issubclass(obj, InferConfig):
51
+ return obj
52
+
53
+ raise ValueError(f"No class inheriting from 'InferConfig' was found in '{file_path}'.")
src/infer_runtime/model.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+ import os
6
+
7
+ from PIL import Image
8
+ import torch
9
+
10
+ from infer_runtime.infer_config import InferConfig, load_infer_config_class_from_pyfile
11
+ from infer_runtime.prompt_rewrite import rewrite_prompt
12
+ from infer_runtime.settings import InferSettings
13
+ from modules.models import load_dit, load_pipeline
14
+ from modules.utils import _dynamic_resize_from_bucket, seed_everything
15
+
16
+
17
+ @dataclass
18
+ class InferenceParams:
19
+ prompt: str
20
+ image: Optional[Image.Image]
21
+ height: int
22
+ width: int
23
+ steps: int
24
+ guidance_scale: float
25
+ seed: int
26
+ neg_prompt: str
27
+ basesize: int
28
+
29
+
30
+ class EditModel:
31
+ def __init__(
32
+ self,
33
+ settings: InferSettings,
34
+ device: torch.device,
35
+ hsdp_shard_dim_override: int | None = None,
36
+ ):
37
+ self.settings = settings
38
+ self.device = device
39
+ self._rewrite_cache: dict[str, str] = {}
40
+
41
+ config_class = load_infer_config_class_from_pyfile(settings.config_path)
42
+ self.cfg: InferConfig = config_class()
43
+ self.cfg.dit_ckpt = settings.ckpt_path
44
+ self.cfg.training_mode = False
45
+ if hsdp_shard_dim_override is not None:
46
+ self.cfg.hsdp_shard_dim = hsdp_shard_dim_override
47
+ if int(os.environ.get('WORLD_SIZE', '1')) > 1 and self.cfg.hsdp_shard_dim > 1:
48
+ self.cfg.use_fsdp_inference = True
49
+
50
+ self.dit = load_dit(self.cfg, device=self.device)
51
+ self.dit.requires_grad_(False)
52
+ self.dit.eval()
53
+ self.pipeline = load_pipeline(self.cfg, self.dit, self.device)
54
+
55
+ def current_device(self) -> torch.device:
56
+ return self.device
57
+
58
+ def move_to_device(self, device: torch.device) -> torch.device:
59
+ target = torch.device(device)
60
+ if self.device == target:
61
+ return self.device
62
+
63
+ self.dit = self.dit.to(device=target)
64
+ self.pipeline = self.pipeline.to(target)
65
+ self.device = target
66
+ return self.device
67
+
68
+ def move_to_cpu(self) -> torch.device:
69
+ return self.move_to_device(torch.device('cpu'))
70
+
71
+ def move_to_gpu(self, device: torch.device | None = None) -> torch.device:
72
+ target = torch.device(device) if device is not None else torch.device('cuda')
73
+ return self.move_to_device(target)
74
+
75
+ def maybe_rewrite_prompt(self, prompt: str, image: Optional[Image.Image], enabled: bool) -> str:
76
+ if not enabled:
77
+ return str(prompt or '')
78
+ cache_key = f"prompt={prompt.strip()}"
79
+ if image is not None:
80
+ cache_key += f"|image={image.size[0]}x{image.size[1]}"
81
+ if cache_key not in self._rewrite_cache:
82
+ self._rewrite_cache[cache_key] = rewrite_prompt(
83
+ prompt,
84
+ image,
85
+ model=self.settings.rewrite_model,
86
+ api_key=self.settings.openai_api_key,
87
+ base_url=self.settings.openai_base_url,
88
+ )
89
+ return self._rewrite_cache[cache_key]
90
+
91
+ @torch.no_grad()
92
+ def infer(self, params: InferenceParams) -> Image.Image:
93
+ if params.image is None:
94
+ prompts = [params.prompt]
95
+ negative_prompt = [params.neg_prompt]
96
+ images = None
97
+ height = params.height
98
+ width = params.width
99
+ else:
100
+ processed = _dynamic_resize_from_bucket(params.image, basesize=params.basesize)
101
+ width, height = processed.size
102
+ image_tokens = '<image>\n'
103
+ prompts = [f"<|im_start|>user\n{image_tokens}{params.prompt}<|im_end|>\n"]
104
+ negative_prompt = [f"<|im_start|>user\n{image_tokens}{params.neg_prompt}<|im_end|>\n"]
105
+ images = [processed]
106
+
107
+ generator_device = 'cuda' if self.device.type == 'cuda' else 'cpu'
108
+ generator = torch.Generator(device=generator_device).manual_seed(int(params.seed))
109
+ output = self.pipeline(
110
+ prompt=prompts,
111
+ negative_prompt=negative_prompt,
112
+ images=images,
113
+ height=height,
114
+ width=width,
115
+ num_frames=1,
116
+ num_inference_steps=params.steps,
117
+ guidance_scale=params.guidance_scale,
118
+ generator=generator,
119
+ num_videos_per_prompt=1,
120
+ output_type='pt',
121
+ return_dict=False,
122
+ )
123
+ image_tensor = (output[0, -1, 0] * 255).to(torch.uint8).cpu()
124
+ return Image.fromarray(image_tensor.permute(1, 2, 0).numpy())
125
+
126
+
127
+ def build_model(
128
+ settings: InferSettings,
129
+ device: torch.device | None = None,
130
+ hsdp_shard_dim_override: int | None = None,
131
+ ) -> EditModel:
132
+ seed_everything(settings.default_seed)
133
+ if device is None:
134
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
135
+ return EditModel(
136
+ settings=settings,
137
+ device=device,
138
+ hsdp_shard_dim_override=hsdp_shard_dim_override,
139
+ )
src/infer_runtime/prompt_rewrite.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import io
5
+ import json
6
+ import time
7
+ from typing import Optional
8
+
9
+ from PIL import Image
10
+
11
+
12
+ SYSTEM_PROMPT = r"""
13
+ # Edit Prompt Enhancer
14
+ You are a professional edit prompt enhancer. Your task is to generate a direct and specific edit prompt based on the user-provided instruction and the image input conditions.
15
+ Please strictly follow the enhancing rules below:
16
+ ## 1. General Principles
17
+ - Keep the enhanced prompt direct and specific.
18
+ - If the instruction is contradictory, vague, or unachievable, prioritize reasonable inference and correction, and supplement details when necessary.
19
+ - Keep the core intention of the original instruction unchanged, only enhancing its clarity, rationality, and visual feasibility.
20
+ - All added objects or modifications must align with the logic and style of the edited input image's overall scene.
21
+ ## 2. Task-Type Handling Rules
22
+ ### 1. Add, Delete, Replace Tasks
23
+ - If the instruction is clear, preserve the original intent and only refine the grammar.
24
+ - If the description is vague, supplement with minimal but sufficient details.
25
+ ### 2. Text Editing Tasks
26
+ - All text content must be enclosed in English double quotes.
27
+ ### 3. Human (ID) Editing Tasks
28
+ - Emphasize maintaining the person's core visual consistency.
29
+ - For expression changes or beauty changes, they must be natural and subtle.
30
+ ### 4. Style Conversion or Enhancement Tasks
31
+ - Colorization tasks must use: "Restore and colorize the photo."
32
+ ### 5. Content Filling Tasks
33
+ - Inpainting tasks must use: "Perform inpainting on this image. The original caption is: "
34
+ - Outpainting tasks must use: "Extend the image beyond its boundaries using outpainting. The original caption is: "
35
+ Output a JSON object: {"Rewritten": "..."}
36
+ """
37
+
38
+
39
+ def encode_image_base64_png(image: Image.Image) -> str:
40
+ buffer = io.BytesIO()
41
+ image.save(buffer, format="PNG")
42
+ return base64.b64encode(buffer.getvalue()).decode("utf-8")
43
+
44
+
45
+ def extract_rewritten(content: str) -> str:
46
+ text = (content or "").strip().replace("```json", "").replace("```", "").strip()
47
+ payload = json.loads(text)
48
+ return (payload.get("Rewritten") or "").strip().replace("\n", " ")
49
+
50
+
51
+ def rewrite_prompt(
52
+ prompt: str,
53
+ image: Optional[Image.Image],
54
+ *,
55
+ model: str,
56
+ api_key: str | None,
57
+ base_url: str | None,
58
+ max_retries: int = 3,
59
+ ) -> str:
60
+ prompt = str(prompt or "").strip()
61
+ if not prompt:
62
+ return prompt
63
+ if not api_key:
64
+ return prompt
65
+
66
+ from openai import OpenAI
67
+
68
+ client = OpenAI(api_key=api_key, base_url=base_url) if base_url else OpenAI(api_key=api_key)
69
+ user_content: list[dict[str, object]] = []
70
+ if image is not None:
71
+ user_content.append(
72
+ {
73
+ "type": "image_url",
74
+ "image_url": {"url": f"data:image/png;base64,{encode_image_base64_png(image.convert('RGB'))}"},
75
+ }
76
+ )
77
+ user_content.append({"type": "text", "text": f"User Input: {prompt}\n\nRewritten Prompt:"})
78
+ messages = [
79
+ {"role": "system", "content": SYSTEM_PROMPT},
80
+ {"role": "user", "content": user_content if image is not None else f"{SYSTEM_PROMPT}\n\nUser Input: {prompt}\n\nRewritten Prompt:"},
81
+ ]
82
+
83
+ temperature = 1.0 if "gpt-5" in model.lower() else 0.0
84
+ last_error = None
85
+ for attempt in range(max_retries):
86
+ try:
87
+ try:
88
+ response = client.chat.completions.create(
89
+ model=model,
90
+ messages=messages,
91
+ temperature=temperature,
92
+ response_format={"type": "json_object"},
93
+ )
94
+ except Exception:
95
+ response = client.chat.completions.create(
96
+ model=model,
97
+ messages=messages,
98
+ temperature=temperature,
99
+ )
100
+ rewritten = extract_rewritten(response.choices[0].message.content or "")
101
+ return rewritten or prompt
102
+ except Exception as exc:
103
+ last_error = exc
104
+ time.sleep(0.5 * (2 ** attempt))
105
+
106
+ print(f"[PromptRewrite] failed after {max_retries} retries: {last_error}")
107
+ return prompt
src/infer_runtime/settings.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from infer_runtime.checkpoints import resolve_checkpoint_layout
8
+
9
+
10
+ @dataclass
11
+ class InferSettings:
12
+ config_path: str
13
+ ckpt_path: str
14
+ rewrite_model: str
15
+ openai_api_key: str | None
16
+ openai_base_url: str | None
17
+ default_seed: int
18
+
19
+
20
+ def _materialize_config_if_needed(config_path: Path, ckpt_root: Path) -> Path:
21
+ """Make a real config file inside ckpt_root when the source config is a symlink.
22
+
23
+ Some Hugging Face cache layouts expose snapshot files as symlinks into a shared
24
+ blobs directory. The upstream infer_config.py uses Path(__file__).resolve().parent
25
+ to locate the checkpoint root, so loading the symlink directly makes it resolve to
26
+ the blobs directory instead of the snapshot root. Materializing a real file inside
27
+ the checkpoint root preserves the intended relative layout.
28
+ """
29
+ if not config_path.exists():
30
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
31
+
32
+ if not config_path.is_symlink():
33
+ return config_path
34
+
35
+ materialized = ckpt_root / '_space_runtime_infer_config.py'
36
+ print(f"[Model] Materializing symlinked config for Spaces: {config_path} -> {materialized}")
37
+ source_text = config_path.read_text(encoding='utf-8')
38
+ current_text = materialized.read_text(encoding='utf-8') if materialized.exists() else None
39
+ if current_text != source_text:
40
+ tmp_path = materialized.with_suffix('.tmp')
41
+ tmp_path.write_text(source_text, encoding='utf-8')
42
+ tmp_path.replace(materialized)
43
+ return materialized
44
+
45
+
46
+ def load_settings(
47
+ *,
48
+ ckpt_root: str,
49
+ config_path: str | None = None,
50
+ rewrite_model: str | None = None,
51
+ default_seed: int = 42,
52
+ ) -> InferSettings:
53
+ layout = resolve_checkpoint_layout(ckpt_root)
54
+ default_config = layout.root / 'infer_config.py'
55
+ if config_path is None and not default_config.exists():
56
+ raise FileNotFoundError(
57
+ f"Missing inference config: {default_config}. Pass --config explicitly to choose a config file."
58
+ )
59
+
60
+ chosen_config = Path(config_path).expanduser() if config_path is not None else default_config
61
+ chosen_config = _materialize_config_if_needed(chosen_config, layout.root)
62
+
63
+ return InferSettings(
64
+ config_path=str(chosen_config),
65
+ ckpt_path=str(layout.transformer_ckpt),
66
+ rewrite_model=rewrite_model or 'gpt-5',
67
+ openai_api_key=os.environ.get('OPENAI_API_KEY'),
68
+ openai_base_url=os.environ.get('OPENAI_BASE_URL'),
69
+ default_seed=default_seed,
70
+ )
src/modules/__init__.py ADDED
File without changes
src/modules/models/__init__.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import glob
3
+ import torch
4
+ import torch.distributed as dist
5
+
6
+ from modules.models.bucket import BucketGroup
7
+ from modules.models.mmdit.dit import Transformer3DModel
8
+ from modules.models.mmdit.text_encoder import load_text_encoder
9
+ from modules.models.mmdit.vae import WanxVAE
10
+ from modules.models.pipeline import Pipeline
11
+ from modules.models.scheduler import FlowMatchDiscreteScheduler
12
+ from modules.utils.fsdp_load import maybe_load_fsdp_model, pt_weights_iterator, safetensors_weights_iterator
13
+ from modules.utils.logging import get_logger
14
+ from modules.utils.constants import PRECISION_TO_TYPE
15
+ from modules.utils.utils import build_from_config
16
+
17
+
18
+ def load_pipeline(cfg, dit, device: torch.device):
19
+ # vae
20
+ factory_kwargs = {
21
+ 'torch_dtype': PRECISION_TO_TYPE[cfg.vae_precision], "device": device}
22
+ vae = build_from_config(cfg.vae_arch_config, **factory_kwargs)
23
+ if getattr(cfg.vae_arch_config, "enable_feature_caching", False):
24
+ vae.enable_feature_caching()
25
+
26
+ # text_encoder
27
+ factory_kwargs = {
28
+ 'torch_dtype': PRECISION_TO_TYPE[cfg.text_encoder_precision], "device": device}
29
+ tokenizer, text_encoder = build_from_config(
30
+ cfg.text_encoder_arch_config, **factory_kwargs)
31
+
32
+ # scheduler
33
+ scheduler = build_from_config(cfg.scheduler_arch_config)
34
+
35
+ pipeline = Pipeline(
36
+ vae=vae,
37
+ tokenizer=tokenizer,
38
+ text_encoder=text_encoder,
39
+ transformer=dit,
40
+ scheduler=scheduler,
41
+ args=cfg,
42
+ )
43
+
44
+ pipeline = pipeline.to(device)
45
+ return pipeline
46
+
47
+
48
+ def load_dit(cfg, device: torch.device) -> torch.nn.Module:
49
+ """Load DiT model with FSDP support."""
50
+ logger = get_logger()
51
+
52
+ state_dict = None
53
+ if cfg.dit_ckpt is not None:
54
+ logger.info(
55
+ f"Loading model from: {cfg.dit_ckpt}, type: {cfg.dit_ckpt_type}")
56
+
57
+ if cfg.dit_ckpt_type == "safetensor":
58
+ # Find all safetensors files
59
+ safetensors_files = glob.glob(
60
+ os.path.join(str(cfg.dit_ckpt), "*.safetensors"))
61
+ if not safetensors_files:
62
+ raise ValueError(
63
+ f"No safetensors files found in {cfg.dit_ckpt}")
64
+ state_dict = dict(
65
+ safetensors_weights_iterator(safetensors_files))
66
+ elif cfg.dit_ckpt_type == "pt":
67
+ pt_files = [cfg.dit_ckpt]
68
+ state_dict = dict(pt_weights_iterator(pt_files))
69
+ if "model" in state_dict:
70
+ state_dict = state_dict["model"]
71
+ else:
72
+ raise ValueError(
73
+ f"Unknown dit_ckpt_type: {cfg.dit_ckpt_type}, must be 'safetensor' or 'pt'")
74
+
75
+ dtype = PRECISION_TO_TYPE[cfg.dit_precision]
76
+ model_kwargs = {'dtype': dtype, 'device': device, 'args': cfg}
77
+ model = build_from_config(cfg.dit_arch_config, **model_kwargs)
78
+ if not dist.is_initialized() or dist.get_world_size() == 1:
79
+ # Debug mode
80
+ model.to(device=device)
81
+
82
+ if state_dict is not None:
83
+ # filter unused params
84
+ load_state_dict = {}
85
+ for k, v in state_dict.items():
86
+
87
+ if k == "img_in.weight" and model.img_in.weight.shape != v.shape:
88
+ logger.info(
89
+ f"Inflate {k} from {v.shape} to {model.img_in.weight.shape}")
90
+ v_new = v.new_zeros(model.img_in.weight.shape)
91
+ v_new[:, :v.shape[1], :, :, :] = v
92
+ v = v_new
93
+
94
+ load_state_dict[k] = v
95
+ model.load_state_dict(load_state_dict, strict=True)
96
+
97
+ model = maybe_load_fsdp_model(
98
+ model=model,
99
+ hsdp_shard_dim=cfg.hsdp_shard_dim,
100
+ reshard_after_forward=cfg.reshard_after_forward,
101
+ param_dtype=dtype,
102
+ reduce_dtype=torch.float32,
103
+ output_dtype=None,
104
+ cpu_offload=cfg.cpu_offload,
105
+ fsdp_inference=cfg.use_fsdp_inference,
106
+ training_mode=cfg.training_mode,
107
+ pin_cpu_memory=cfg.pin_cpu_memory,
108
+ )
109
+
110
+ # Log model info
111
+ total_params = sum(p.numel() for p in model.parameters())
112
+ logger.info(f"Instantiate model with {total_params / 1e9:.2f}B parameters")
113
+
114
+ # Ensure consistent dtype
115
+ param_dtypes = {param.dtype for param in model.parameters()}
116
+ if len(param_dtypes) > 1:
117
+ logger.warning(
118
+ f"Model has mixed dtypes: {param_dtypes}. Converting to {dtype}")
119
+ model = model.to(dtype)
120
+
121
+ return model.eval()
122
+
123
+ __all__ = [
124
+ "BucketGroup",
125
+ "FlowMatchDiscreteScheduler",
126
+ "Pipeline",
127
+ "Transformer3DModel",
128
+ "WanxVAE",
129
+ "load_pipeline",
130
+ "load_text_encoder",
131
+ ]
src/modules/models/attention.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Adapted from https://github.com/hao-ai-lab/FastVideo/tree/main/fastvideo/attention
2
+
3
+ import os
4
+ import sys
5
+ import torch
6
+ from einops import rearrange
7
+
8
+ _FLASH_ATTN_IMPORT_ERROR = None
9
+
10
+ try:
11
+ # Check for Flash Attention 3 installation path
12
+ flash_attn3_path = os.getenv("FLASH_ATTN3_PATH")
13
+ if flash_attn3_path:
14
+ print(f"Using Flash Attention 3 from: {flash_attn3_path}")
15
+ sys.path.insert(0, flash_attn3_path)
16
+ from flash_attn_interface import flash_attn_varlen_func
17
+ else:
18
+ from flash_attn.flash_attn_interface import flash_attn_varlen_func
19
+ except ImportError as exc:
20
+ flash_attn_varlen_func = None
21
+ _FLASH_ATTN_IMPORT_ERROR = exc
22
+
23
+
24
+
25
+ def is_flash_attn_available() -> bool:
26
+ return flash_attn_varlen_func is not None
27
+
28
+
29
+ def get_preferred_attention_backend() -> str:
30
+ return "flash_attn" if is_flash_attn_available() else "torch_spda"
31
+
32
+
33
+ def describe_attention_backend() -> str:
34
+ backend = get_preferred_attention_backend()
35
+ if backend == "flash_attn":
36
+ return "flash_attn"
37
+ if _FLASH_ATTN_IMPORT_ERROR is None:
38
+ return "torch_spda"
39
+ return f"torch_spda (flash_attn unavailable: {_FLASH_ATTN_IMPORT_ERROR})"
40
+
41
+
42
+ def get_cu_seqlens(text_mask, img_len):
43
+ """Calculate cu_seqlens_q, cu_seqlens_kv using text_mask and img_len
44
+
45
+ Args:
46
+ text_mask (torch.Tensor): the mask of text
47
+ img_len (int): the length of image
48
+
49
+ Returns:
50
+ torch.Tensor: the calculated cu_seqlens for flash attention
51
+ """
52
+ batch_size = text_mask.shape[0]
53
+ text_len = text_mask.sum(dim=1)
54
+ max_len = text_mask.shape[1] + img_len
55
+
56
+ cu_seqlens = torch.zeros([2 * batch_size + 1],
57
+ dtype=torch.int32, device="cuda")
58
+
59
+ for i in range(batch_size):
60
+ s = text_len[i] + img_len
61
+ s1 = i * max_len + s
62
+ s2 = (i + 1) * max_len
63
+ cu_seqlens[2 * i + 1] = s1
64
+ cu_seqlens[2 * i + 2] = s2
65
+
66
+ return cu_seqlens
67
+
68
+
69
+ def attention(
70
+ q: torch.Tensor,
71
+ k: torch.Tensor,
72
+ v: torch.Tensor,
73
+ backend: str = "flash_attn",
74
+ *,
75
+ causal: bool = False,
76
+ softmax_scale: float = None,
77
+ attn_kwargs: dict = None,
78
+ ):
79
+ """
80
+ Args:
81
+ q (torch.Tensor): Query tensor of shape [batch_size, seq_len, num_heads, head_dim]
82
+ k (torch.Tensor): Key tensor of shape [batch_size, seq_len, num_heads, head_dim]
83
+ v (torch.Tensor): Value tensor of shape [batch_size, seq_len, num_heads
84
+ """
85
+ if backend == "auto":
86
+ backend = get_preferred_attention_backend()
87
+ # Fall back to torch_spda when flash_attn was requested but unavailable
88
+ if backend == "flash_attn" and flash_attn_varlen_func is None:
89
+ backend = "torch_spda"
90
+ assert backend in [
91
+ "torch_spda", "flash_attn"], f"Unsupported attention backend: {backend}"
92
+ assert q.dim() == 4 and k.dim() == 4 and v.dim() == 4, "Input tensors must be 4D"
93
+ batch_size = q.shape[0]
94
+ if backend == "torch_spda":
95
+ q = rearrange(q, "b l h c -> b h l c")
96
+ k = rearrange(k, "b l h c -> b h l c")
97
+ v = rearrange(v, "b l h c -> b h l c")
98
+ output = torch.nn.functional.scaled_dot_product_attention(
99
+ q, k, v, is_causal=causal, scale=softmax_scale)
100
+ output = rearrange(output, "b h l c -> b l h c")
101
+ elif backend == "flash_attn":
102
+ cu_seqlens_q = attn_kwargs['cu_seqlens_q']
103
+ cu_seqlens_kv = attn_kwargs['cu_seqlens_kv']
104
+ max_seqlen_q = attn_kwargs['max_seqlen_q']
105
+ max_seqlen_kv = attn_kwargs['max_seqlen_kv']
106
+ x = flash_attn_varlen_func(
107
+ q.view(q.shape[0] * q.shape[1], *q.shape[2:]),
108
+ k.view(k.shape[0] * k.shape[1], *k.shape[2:]),
109
+ v.view(v.shape[0] * v.shape[1], *v.shape[2:]),
110
+ cu_seqlens_q,
111
+ cu_seqlens_kv,
112
+ max_seqlen_q,
113
+ max_seqlen_kv,
114
+ )
115
+ output = x.view(
116
+ batch_size, max_seqlen_q, x.shape[-2], x.shape[-1]
117
+ )
118
+
119
+ return output
src/modules/models/bucket.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class BucketGroup:
2
+ """Manages dynamic batch grouping buckets for image inference."""
3
+
4
+ def __init__(
5
+ self,
6
+ bucket_configs: list[tuple[int, int, int, int, int]],
7
+ prioritize_frame_matching: bool = True,
8
+ ):
9
+ """
10
+ Initialize bucket group with predefined configurations.
11
+
12
+ Args:
13
+ bucket_configs: List of (batch_size, num_items, num_frames, height, width) tuples
14
+ prioritize_frame_matching: Unused, kept for API compatibility.
15
+ """
16
+ self.bucket_configs = [tuple(b) for b in bucket_configs]
17
+
18
+ def find_best_bucket(self, media_shape: tuple[int, int, int, int]) -> tuple[int, int, int, int, int]:
19
+ """
20
+ Find the best matching bucket for given media dimensions.
21
+
22
+ Args:
23
+ media_shape: (num_items, num_frames, height, width) of input media
24
+
25
+ Returns:
26
+ Best matching bucket as (batch_size, num_items, num_frames, height, width)
27
+ """
28
+ num_items, num_frames, height, width = media_shape
29
+ target_aspect_ratio = height / width
30
+
31
+ if num_frames != 1:
32
+ raise ValueError(
33
+ f"Only image inference (num_frames=1) is supported, got num_frames={num_frames}")
34
+
35
+ valid_buckets = [
36
+ b for b in self.bucket_configs
37
+ if b[1] == num_items and b[2] == 1
38
+ ]
39
+ if not valid_buckets:
40
+ raise ValueError(
41
+ f"No image buckets found for shape {media_shape}")
42
+
43
+ return min(
44
+ valid_buckets,
45
+ key=lambda bucket: abs(
46
+ (bucket[3] / bucket[4]) - target_aspect_ratio)
47
+ )
48
+
49
+ def __repr__(self) -> str:
50
+ return (
51
+ f"BucketGroup("
52
+ f"total_buckets={len(self.bucket_configs)}, "
53
+ f"configs={self.bucket_configs})"
54
+ )
55
+
56
+
57
+ def _generate_hw_buckets(base_height=256, base_width=256, step_width=16, step_height=16, max_ratio=4.0) -> list[tuple[int, int, int, int, int]]:
58
+ """Generate dimension buckets based on aspect ratios."""
59
+ buckets = []
60
+ target_pixels = base_height * base_width
61
+
62
+ height = target_pixels // step_width
63
+ width = step_width
64
+
65
+ while height >= step_height:
66
+ if max(height, width) / min(height, width) <= max_ratio:
67
+ buckets.append((1, 1, 1, height, width))
68
+ if height * (width + step_width) <= target_pixels:
69
+ width += step_width
70
+ else:
71
+ height -= step_height
72
+
73
+ return buckets
74
+
75
+
76
+ def generate_video_image_bucket(basesize=256, min_temporal=65, max_temporal=129, bs_img=8, bs_vid=1, bs_mimg=4, min_items=1, max_items=1):
77
+ """Generate bucket configs for image inference.
78
+
79
+ Returns:
80
+ List of (batch_size, num_items, num_frames, height, width) tuples.
81
+ """
82
+ assert basesize in [
83
+ 256, 512, 768, 1024], f"[generate_video_image_bucket] wrong basesize {basesize}"
84
+ bucket_list = []
85
+
86
+ base_bucket_list = _generate_hw_buckets()
87
+ # image
88
+ for _bucket in base_bucket_list:
89
+ bucket = list(_bucket)
90
+ bucket[0] = bs_img
91
+ bucket_list.append(bucket)
92
+ # multiple images
93
+ for num_items in range(min_items, max_items + 1):
94
+ for _bucket in base_bucket_list:
95
+ bucket = list(_bucket)
96
+ bucket[0] = bs_mimg
97
+ bucket[1] = num_items
98
+ bucket_list.append(bucket)
99
+ # spatial resize
100
+ if basesize > 256:
101
+ ratio = basesize // 256
102
+
103
+ def resize(bucket, r):
104
+ bucket[-2] *= r
105
+ bucket[-1] *= r
106
+ return bucket
107
+ bucket_list = [resize(bucket, ratio) for bucket in bucket_list]
108
+ return bucket_list
src/modules/models/mmdit/dit/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .models import Transformer3DModel
2
+
3
+
4
+ __all__ = ["Transformer3DModel"]
src/modules/models/mmdit/dit/models.py ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, List, Tuple, Optional, Union, Dict
2
+ from einops import rearrange
3
+ import math
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+
9
+ from diffusers.models import ModelMixin
10
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
11
+ from diffusers.models.attention import FeedForward
12
+ from diffusers.models.embeddings import PixArtAlphaTextProjection, TimestepEmbedding, Timesteps
13
+
14
+ from modules.models.attention import attention, get_cu_seqlens, get_preferred_attention_backend
15
+
16
+ from .posemb_layers import apply_rotary_emb, get_nd_rotary_pos_embed
17
+ from .modulate_layers import load_modulation, modulate, apply_gate
18
+
19
+
20
+ class RMSNorm(nn.Module):
21
+ def __init__(
22
+ self,
23
+ dim: int,
24
+ elementwise_affine=True,
25
+ eps: float = 1e-6,
26
+ device=None,
27
+ dtype=None,
28
+ ):
29
+ """
30
+ Initialize the RMSNorm normalization layer.
31
+
32
+ Args:
33
+ dim (int): The dimension of the input tensor.
34
+ eps (float, optional): A small value added to the denominator for numerical stability. Default is 1e-6.
35
+
36
+ Attributes:
37
+ eps (float): A small value added to the denominator for numerical stability.
38
+ weight (nn.Parameter): Learnable scaling parameter.
39
+
40
+ """
41
+ factory_kwargs = {"device": device, "dtype": dtype}
42
+ super().__init__()
43
+ self.eps = eps
44
+ if elementwise_affine:
45
+ self.weight = nn.Parameter(torch.ones(dim, **factory_kwargs))
46
+
47
+ def _norm(self, x):
48
+ """
49
+ Apply the RMSNorm normalization to the input tensor.
50
+
51
+ Args:
52
+ x (torch.Tensor): The input tensor.
53
+
54
+ Returns:
55
+ torch.Tensor: The normalized tensor.
56
+
57
+ """
58
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
59
+
60
+ def forward(self, x):
61
+ """
62
+ Forward pass through the RMSNorm layer.
63
+
64
+ Args:
65
+ x (torch.Tensor): The input tensor.
66
+
67
+ Returns:
68
+ torch.Tensor: The output tensor after applying RMSNorm.
69
+
70
+ """
71
+ output = self._norm(x.float()).type_as(x)
72
+ if hasattr(self, "weight"):
73
+ output = output * self.weight
74
+ return output
75
+
76
+
77
+ class MMDoubleStreamBlock(nn.Module):
78
+ """
79
+ A multimodal dit block with seperate modulation for
80
+ text and image/video, see more details (SD3): https://arxiv.org/abs/2403.03206
81
+ (Flux.1): https://github.com/black-forest-labs/flux
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ hidden_size: int,
87
+ heads_num: int,
88
+ mlp_width_ratio: float,
89
+ mlp_act_type: str = "gelu_tanh",
90
+ dtype: Optional[torch.dtype] = None,
91
+ device: Optional[torch.device] = None,
92
+ dit_modulation_type: Optional[str] = "wanx",
93
+ attn_backend: str = 'auto',
94
+ ):
95
+ factory_kwargs = {"device": device, "dtype": dtype}
96
+ super().__init__()
97
+ self.attn_backend = get_preferred_attention_backend() if attn_backend == 'auto' else attn_backend
98
+ self.dit_modulation_type = dit_modulation_type
99
+ self.heads_num = heads_num
100
+ head_dim = hidden_size // heads_num
101
+ mlp_hidden_dim = int(hidden_size * mlp_width_ratio)
102
+
103
+ self.img_mod = load_modulation(
104
+ modulate_type=self.dit_modulation_type,
105
+ hidden_size=hidden_size,
106
+ factor=6,
107
+ **factory_kwargs,
108
+ )
109
+ self.img_norm1 = nn.LayerNorm(
110
+ hidden_size, elementwise_affine=False, eps=1e-6, **factory_kwargs
111
+ )
112
+
113
+ self.img_attn_qkv = nn.Linear(
114
+ hidden_size, hidden_size * 3, bias=True, **factory_kwargs
115
+ )
116
+ self.img_attn_q_norm = RMSNorm(head_dim, elementwise_affine=True,
117
+ eps=1e-6, **factory_kwargs)
118
+ self.img_attn_k_norm = RMSNorm(head_dim, elementwise_affine=True,
119
+ eps=1e-6, **factory_kwargs)
120
+ self.img_attn_proj = nn.Linear(
121
+ hidden_size, hidden_size, bias=True, **factory_kwargs
122
+ )
123
+
124
+ self.img_norm2 = nn.LayerNorm(
125
+ hidden_size, elementwise_affine=False, eps=1e-6, **factory_kwargs
126
+ )
127
+ # There is no dtype fpr FeedForward, because FSDP2 casts the dtype for all parameters.
128
+ # You may need to give the dtype when no autocast and fsdp !!!
129
+ self.img_mlp = FeedForward(hidden_size, inner_dim=mlp_hidden_dim,
130
+ activation_fn="gelu-approximate")
131
+
132
+ self.txt_mod = load_modulation(
133
+ modulate_type=self.dit_modulation_type,
134
+ hidden_size=hidden_size,
135
+ factor=6,
136
+ **factory_kwargs,
137
+ )
138
+ self.txt_norm1 = nn.LayerNorm(
139
+ hidden_size, elementwise_affine=False, eps=1e-6, **factory_kwargs
140
+ )
141
+
142
+ self.txt_attn_qkv = nn.Linear(
143
+ hidden_size, hidden_size * 3, bias=True, **factory_kwargs
144
+ )
145
+ self.txt_attn_q_norm = RMSNorm(head_dim, elementwise_affine=True,
146
+ eps=1e-6, **factory_kwargs)
147
+ self.txt_attn_k_norm = RMSNorm(head_dim, elementwise_affine=True,
148
+ eps=1e-6, **factory_kwargs)
149
+ self.txt_attn_proj = nn.Linear(
150
+ hidden_size, hidden_size, bias=True, **factory_kwargs
151
+ )
152
+
153
+ self.txt_norm2 = nn.LayerNorm(
154
+ hidden_size, elementwise_affine=False, eps=1e-6, **factory_kwargs
155
+ )
156
+ self.txt_mlp = FeedForward(hidden_size, inner_dim=mlp_hidden_dim,
157
+ activation_fn="gelu-approximate")
158
+
159
+ def forward(
160
+ self,
161
+ img: torch.Tensor,
162
+ txt: torch.Tensor,
163
+ vec: torch.Tensor,
164
+ vis_freqs_cis: tuple = None,
165
+ txt_freqs_cis: tuple = None,
166
+ attn_kwargs: Optional[dict] = {},
167
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
168
+ tt, th, tw = attn_kwargs['thw']
169
+ (
170
+ img_mod1_shift,
171
+ img_mod1_scale,
172
+ img_mod1_gate,
173
+ img_mod2_shift,
174
+ img_mod2_scale,
175
+ img_mod2_gate,
176
+ ) = self.img_mod(vec)
177
+ (
178
+ txt_mod1_shift,
179
+ txt_mod1_scale,
180
+ txt_mod1_gate,
181
+ txt_mod2_shift,
182
+ txt_mod2_scale,
183
+ txt_mod2_gate,
184
+ ) = self.txt_mod(vec)
185
+
186
+ # Prepare image for attention.
187
+ img_modulated = self.img_norm1(img)
188
+ img_modulated = modulate(
189
+ img_modulated, shift=img_mod1_shift, scale=img_mod1_scale
190
+ )
191
+ img_qkv = self.img_attn_qkv(img_modulated)
192
+ img_q, img_k, img_v = rearrange(
193
+ img_qkv, "B L (K H D) -> K B L H D", K=3, H=self.heads_num
194
+ )
195
+ # Apply QK-Norm if needed
196
+ img_q = self.img_attn_q_norm(img_q).to(img_v)
197
+ img_k = self.img_attn_k_norm(img_k).to(img_v)
198
+
199
+ # Apply RoPE if needed.
200
+ if vis_freqs_cis is not None:
201
+ img_qq, img_kk = apply_rotary_emb(
202
+ img_q, img_k, vis_freqs_cis, head_first=False)
203
+ assert (
204
+ img_qq.shape == img_q.shape and img_kk.shape == img_k.shape
205
+ ), f"img_kk: {img_qq.shape}, img_q: {img_q.shape}, img_kk: {img_kk.shape}, img_k: {img_k.shape}"
206
+ img_q, img_k = img_qq, img_kk
207
+
208
+ # Prepare txt for attention.
209
+ txt_modulated = self.txt_norm1(txt)
210
+ txt_modulated = modulate(
211
+ txt_modulated, shift=txt_mod1_shift, scale=txt_mod1_scale
212
+ )
213
+ txt_qkv = self.txt_attn_qkv(txt_modulated)
214
+ txt_q, txt_k, txt_v = rearrange(
215
+ txt_qkv, "B L (K H D) -> K B L H D", K=3, H=self.heads_num
216
+ )
217
+ # Apply QK-Norm if needed.
218
+ txt_q = self.txt_attn_q_norm(txt_q).to(txt_v)
219
+ txt_k = self.txt_attn_k_norm(txt_k).to(txt_v)
220
+
221
+ if txt_freqs_cis is not None:
222
+ raise NotImplementedError("RoPE text is not supported for inference")
223
+ txt_qq, txt_kk = apply_rotary_emb(
224
+ txt_q, txt_k, txt_freqs_cis, head_first=False)
225
+ assert (
226
+ txt_qq.shape == txt_q.shape and txt_kk.shape == txt_k.shape
227
+ ), f"txt_kk: {txt_qq.shape}, txt_q: {txt_q.shape}, txt_kk: {txt_kk.shape}, txt_k: {txt_k.shape}"
228
+ txt_q, txt_k = txt_qq, txt_kk
229
+
230
+ # attention computation start
231
+
232
+ q = torch.cat((img_q, txt_q), dim=1)
233
+ k = torch.cat((img_k, txt_k), dim=1)
234
+ v = torch.cat((img_v, txt_v), dim=1)
235
+ attn = attention(
236
+ q, k, v,
237
+ backend=self.attn_backend,
238
+ attn_kwargs=attn_kwargs,
239
+ )
240
+ attn = attn.flatten(2, 3)
241
+ # attention computation end
242
+ img_attn, txt_attn = attn[:,
243
+ : img.shape[1]], attn[:, img.shape[1]:]
244
+
245
+ # Calculate the img bloks.
246
+ img = img + apply_gate(self.img_attn_proj(img_attn),
247
+ gate=img_mod1_gate)
248
+ img = img + apply_gate(
249
+ self.img_mlp(
250
+ modulate(
251
+ self.img_norm2(img), shift=img_mod2_shift, scale=img_mod2_scale
252
+ )
253
+ ),
254
+ gate=img_mod2_gate,
255
+ )
256
+
257
+ # Calculate the txt bloks.
258
+ txt = txt + apply_gate(self.txt_attn_proj(txt_attn),
259
+ gate=txt_mod1_gate)
260
+ txt = txt + apply_gate(
261
+ self.txt_mlp(
262
+ modulate(
263
+ self.txt_norm2(txt), shift=txt_mod2_shift, scale=txt_mod2_scale
264
+ )
265
+ ),
266
+ gate=txt_mod2_gate,
267
+ )
268
+
269
+ return img, txt
270
+
271
+
272
+ class WanTimeTextImageEmbedding(nn.Module):
273
+ def __init__(
274
+ self,
275
+ dim: int,
276
+ time_freq_dim: int,
277
+ time_proj_dim: int,
278
+ text_embed_dim: int,
279
+ image_embed_dim: Optional[int] = None,
280
+ pos_embed_seq_len: Optional[int] = None,
281
+ ):
282
+ super().__init__()
283
+
284
+ self.timesteps_proj = Timesteps(
285
+ num_channels=time_freq_dim, flip_sin_to_cos=True, downscale_freq_shift=0)
286
+ self.time_embedder = TimestepEmbedding(
287
+ in_channels=time_freq_dim, time_embed_dim=dim)
288
+ self.act_fn = nn.SiLU()
289
+ self.time_proj = nn.Linear(dim, time_proj_dim)
290
+ self.text_embedder = PixArtAlphaTextProjection(
291
+ text_embed_dim, dim, act_fn="gelu_tanh")
292
+
293
+ def forward(
294
+ self,
295
+ timestep: torch.Tensor,
296
+ encoder_hidden_states: torch.Tensor,
297
+ ):
298
+ timestep = self.timesteps_proj(timestep)
299
+
300
+ time_embedder_dtype = next(iter(self.time_embedder.parameters())).dtype
301
+ if timestep.dtype != time_embedder_dtype and time_embedder_dtype != torch.int8:
302
+ timestep = timestep.to(time_embedder_dtype)
303
+ temb = self.time_embedder(timestep).type_as(encoder_hidden_states)
304
+ timestep_proj = self.time_proj(self.act_fn(temb))
305
+
306
+ encoder_hidden_states = self.text_embedder(encoder_hidden_states)
307
+
308
+ return temb, timestep_proj, encoder_hidden_states
309
+
310
+
311
+ class Transformer3DModel(ModelMixin, ConfigMixin):
312
+ _fsdp_shard_conditions: list = [
313
+ lambda name, module: isinstance(module, (MMDoubleStreamBlock))]
314
+ _supports_gradient_checkpointing = True
315
+
316
+ @register_to_config
317
+ def __init__(
318
+ self,
319
+ args: Any,
320
+ patch_size: list = [1, 2, 2],
321
+ in_channels: int = 4, # Should be VAE.config.latent_channels.
322
+ out_channels: int = None,
323
+ hidden_size: int = 3072,
324
+ heads_num: int = 24,
325
+ text_states_dim: int = 4096,
326
+ mlp_width_ratio: float = 4.0,
327
+ mm_double_blocks_depth: int = 20,
328
+ rope_dim_list: List[int] = [16, 56, 56],
329
+ rope_type: str = 'rope',
330
+ dtype: Optional[torch.dtype] = None,
331
+ device: Optional[torch.device] = None,
332
+ dit_modulation_type: str = "wanx",
333
+ attn_backend: str = 'auto',
334
+ theta: int = 256,
335
+ ):
336
+ self.args = args
337
+ self.out_channels = out_channels or in_channels
338
+ self.patch_size = patch_size
339
+ self.hidden_size = hidden_size
340
+ self.heads_num = heads_num
341
+ self.rope_dim_list = rope_dim_list
342
+ self.dit_modulation_type = dit_modulation_type
343
+ self.mm_double_blocks_depth = mm_double_blocks_depth
344
+ self.attn_backend = get_preferred_attention_backend() if attn_backend == 'auto' else attn_backend
345
+ self.rope_type = rope_type
346
+ self.theta = theta
347
+
348
+ factory_kwargs = {"device": device, "dtype": dtype}
349
+ super().__init__()
350
+ self.hidden_size = hidden_size
351
+ if hidden_size % heads_num != 0:
352
+ raise ValueError(
353
+ f"Hidden size {hidden_size} must be divisible by heads_num {heads_num}"
354
+ )
355
+
356
+ # image projection
357
+ self.img_in = nn.Conv3d(
358
+ in_channels, hidden_size, kernel_size=patch_size, stride=patch_size)
359
+
360
+ # condition embedding
361
+ self.condition_embedder = WanTimeTextImageEmbedding(
362
+ dim=hidden_size,
363
+ time_freq_dim=256,
364
+ time_proj_dim=hidden_size * 6,
365
+ text_embed_dim=text_states_dim,
366
+ )
367
+
368
+ # double blocks
369
+ self.double_blocks = nn.ModuleList(
370
+ [
371
+ MMDoubleStreamBlock(
372
+ self.hidden_size,
373
+ self.heads_num,
374
+ mlp_width_ratio=mlp_width_ratio,
375
+ dit_modulation_type=self.dit_modulation_type,
376
+ attn_backend=attn_backend,
377
+ **factory_kwargs,
378
+ )
379
+ for _ in range(mm_double_blocks_depth)
380
+ ]
381
+ )
382
+
383
+ # Output norm & projection
384
+ self.norm_out = nn.LayerNorm(
385
+ hidden_size, elementwise_affine=False, eps=1e-6
386
+ )
387
+ self.proj_out = nn.Linear(
388
+ hidden_size, out_channels * math.prod(patch_size),
389
+ **factory_kwargs)
390
+
391
+
392
+ def get_rotary_pos_embed(self, vis_rope_size, txt_rope_size=None):
393
+ target_ndim = 3
394
+ ndim = 5 - 2
395
+
396
+ if len(vis_rope_size) != target_ndim:
397
+ vis_rope_size = [1] * (target_ndim - len(vis_rope_size)
398
+ ) + vis_rope_size # time axis
399
+ head_dim = self.hidden_size // self.heads_num
400
+ rope_dim_list = self.rope_dim_list
401
+ if rope_dim_list is None:
402
+ rope_dim_list = [head_dim //
403
+ target_ndim for _ in range(target_ndim)]
404
+ assert (
405
+ sum(rope_dim_list) == head_dim
406
+ ), "sum(rope_dim_list) should equal to head_dim of attention layer"
407
+ vis_freqs, txt_freqs = get_nd_rotary_pos_embed(
408
+ rope_dim_list,
409
+ vis_rope_size,
410
+ txt_rope_size=txt_rope_size,
411
+ theta=self.theta,
412
+ use_real=True,
413
+ theta_rescale_factor=1,
414
+ )
415
+ return vis_freqs, txt_freqs
416
+
417
+ def forward(
418
+ self,
419
+ hidden_states: torch.Tensor,
420
+ timestep: torch.Tensor, # Should be in range(0, 1000).
421
+ encoder_hidden_states: torch.Tensor = None,
422
+ encoder_hidden_states_mask: torch.Tensor = None,
423
+ return_dict: bool = True,
424
+ ) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
425
+ # For Multi-item Input: hidden_states: (b, n, c, t, h, w)
426
+ # Permute the items into the temporal dimension
427
+ is_multi_item = (len(hidden_states.shape) == 6)
428
+ num_items = 0
429
+ if is_multi_item:
430
+ num_items = hidden_states.shape[1]
431
+ if num_items > 1:
432
+ assert self.patch_size[0] == 1, "For multi-item input, patch_size[0] must be 1"
433
+ # Move the last item to the first position
434
+ hidden_states = torch.cat(
435
+ [
436
+ hidden_states[:, -1:],
437
+ hidden_states[:, :-1]
438
+ ],
439
+ dim=1
440
+ )
441
+ hidden_states = rearrange(
442
+ hidden_states, 'b n c t h w -> b c (n t) h w')
443
+
444
+ out = {}
445
+ batch_size, _, ot, oh, ow = hidden_states.shape
446
+ tt, th, tw = (
447
+ ot // self.patch_size[0],
448
+ oh // self.patch_size[1],
449
+ ow // self.patch_size[2],
450
+ )
451
+ # Text Mask
452
+ if encoder_hidden_states_mask == None:
453
+ encoder_hidden_states_mask = torch.ones(
454
+ (encoder_hidden_states.shape[0], encoder_hidden_states.shape[1]), dtype=torch.bool).to(encoder_hidden_states.device)
455
+
456
+ # Prepare img, txt, vec.
457
+ img = self.img_in(hidden_states).flatten(2).transpose(1, 2)
458
+ temb, vec, txt = self.condition_embedder(
459
+ timestep, encoder_hidden_states)
460
+ if vec.shape[-1] > self.hidden_size:
461
+ vec = vec.unflatten(1, (6, -1))
462
+
463
+ txt_seq_len = txt.shape[1]
464
+ img_seq_len = img.shape[1]
465
+
466
+ # rope
467
+ vis_freqs_cis, txt_freqs_cis = self.get_rotary_pos_embed(vis_rope_size=(
468
+ tt, th, tw), txt_rope_size=txt_seq_len if self.rope_type == 'mrope' else None)
469
+
470
+ # Compute attn_kwargs
471
+ attn_kwargs = {'thw': [tt, th, tw], 'txt_len': txt_seq_len}
472
+ if self.attn_backend == 'flash_attn':
473
+ cu_seqlens_q = get_cu_seqlens(
474
+ encoder_hidden_states_mask, img_seq_len)
475
+ cu_seqlens_kv = cu_seqlens_q
476
+ max_seqlen_q = img_seq_len + txt_seq_len
477
+ max_seqlen_kv = max_seqlen_q
478
+
479
+ attn_kwargs.update({
480
+ 'cu_seqlens_q': cu_seqlens_q,
481
+ 'cu_seqlens_kv': cu_seqlens_kv,
482
+ 'max_seqlen_q': max_seqlen_q,
483
+ 'max_seqlen_kv': max_seqlen_kv,
484
+ })
485
+
486
+ # --------------------- Pass through DiT blocks ------------------------
487
+ for _, block in enumerate(self.double_blocks):
488
+ double_block_args = [
489
+ img,
490
+ txt,
491
+ vec,
492
+ vis_freqs_cis,
493
+ txt_freqs_cis,
494
+ attn_kwargs
495
+ ]
496
+
497
+ img, txt = block(*double_block_args)
498
+
499
+ img_len = img.shape[1]
500
+ x = torch.cat((img, txt), 1)
501
+ img = x[:, :img_len, ...]
502
+
503
+ # ---------------------------- Final layer ------------------------------
504
+ img = self.proj_out(self.norm_out(img))
505
+
506
+ img = self.unpatchify(img, tt, th, tw)
507
+
508
+
509
+ # Reshape back to multiple items
510
+ if is_multi_item:
511
+ img = rearrange(
512
+ img, 'b c (n t) h w -> b n c t h w', n=num_items)
513
+ if num_items > 1:
514
+ # Move the first item back to the last position
515
+ img = torch.cat(
516
+ [
517
+ img[:, 1:],
518
+ img[:, :1]
519
+ ],
520
+ dim=1
521
+ )
522
+
523
+ return (img, txt)
524
+
525
+ def unpatchify(self, x, t, h, w):
526
+ """
527
+ x: (N, T, patch_size**2 * C)
528
+ imgs: (N, H, W, C)
529
+ """
530
+ c = self.out_channels
531
+ pt, ph, pw = self.patch_size
532
+ assert t * h * w == x.shape[1]
533
+
534
+ x = x.reshape(shape=(x.shape[0], t, h, w, pt, ph, pw, c))
535
+ x = torch.einsum("nthwopqc->nctohpwq", x)
536
+
537
+ imgs = x.reshape(shape=(x.shape[0], c, t * pt, h * ph, w * pw))
538
+
539
+ return imgs
src/modules/models/mmdit/dit/modulate_layers.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+
5
+ def load_modulation(
6
+ modulate_type: str,
7
+ hidden_size: int,
8
+ factor: int,
9
+ act_layer=nn.SiLU,
10
+ dtype=None,
11
+ device=None):
12
+ factory_kwargs = {"dtype": dtype, "device": device}
13
+ if modulate_type == 'wanx':
14
+ return ModulateWan(hidden_size, factor, **factory_kwargs)
15
+ raise ValueError(
16
+ f"Unknown modulation type: {modulate_type}. Only 'wanx' is supported.")
17
+
18
+
19
+ class ModulateWan(nn.Module):
20
+ """Modulation layer for WanX."""
21
+
22
+ def __init__(
23
+ self,
24
+ hidden_size: int,
25
+ factor: int,
26
+ dtype=None,
27
+ device=None,
28
+ ):
29
+ super().__init__()
30
+ self.factor = factor
31
+ self.modulate_table = nn.Parameter(
32
+ torch.zeros(1, factor, hidden_size,
33
+ dtype=dtype, device=device) / hidden_size**0.5,
34
+ requires_grad=True
35
+ )
36
+
37
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
38
+ if len(x.shape) != 3:
39
+ x = x.unsqueeze(1)
40
+ return [o.squeeze(1) for o in (self.modulate_table + x).chunk(self.factor, dim=1)]
41
+
42
+
43
+ def modulate(x, shift=None, scale=None):
44
+ """modulate by shift and scale
45
+
46
+ Args:
47
+ x (torch.Tensor): input tensor.
48
+ shift (torch.Tensor, optional): shift tensor. Defaults to None.
49
+ scale (torch.Tensor, optional): scale tensor. Defaults to None.
50
+
51
+ Returns:
52
+ torch.Tensor: the output tensor after modulate.
53
+ """
54
+ if scale is None and shift is None:
55
+ return x
56
+ elif shift is None:
57
+ return x * (1 + scale.unsqueeze(1))
58
+ elif scale is None:
59
+ return x + shift.unsqueeze(1)
60
+ else:
61
+ return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
62
+
63
+
64
+ def apply_gate(x, gate=None, tanh=False):
65
+ """Apply gating to tensor.
66
+
67
+ Args:
68
+ x (torch.Tensor): input tensor.
69
+ gate (torch.Tensor, optional): gate tensor. Defaults to None.
70
+ tanh (bool, optional): whether to use tanh function. Defaults to False.
71
+
72
+ Returns:
73
+ torch.Tensor: the output tensor after apply gate.
74
+ """
75
+ if gate is None:
76
+ return x
77
+ if tanh:
78
+ return x * gate.unsqueeze(1).tanh()
79
+ else:
80
+ return x * gate.unsqueeze(1)
src/modules/models/mmdit/dit/posemb_layers.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from typing import Union, Tuple, List
3
+
4
+
5
+ def _to_tuple(x, dim=2):
6
+ if isinstance(x, int):
7
+ return (x,) * dim
8
+ elif len(x) == dim:
9
+ return x
10
+ else:
11
+ raise ValueError(f"Expected length {dim} or int, but got {x}")
12
+
13
+
14
+ def get_meshgrid_nd(start, *args, dim=2):
15
+ """
16
+ Get n-D meshgrid with start, stop and num.
17
+
18
+ Args:
19
+ start (int or tuple): If len(args) == 0, start is num; If len(args) == 1, start is start, args[0] is stop,
20
+ step is 1; If len(args) == 2, start is start, args[0] is stop, args[1] is num. For n-dim, start/stop/num
21
+ should be int or n-tuple. If n-tuple is provided, the meshgrid will be stacked following the dim order in
22
+ n-tuples.
23
+ *args: See above.
24
+ dim (int): Dimension of the meshgrid. Defaults to 2.
25
+
26
+ Returns:
27
+ grid (np.ndarray): [dim, ...]
28
+ """
29
+ if len(args) == 0:
30
+ # start is grid_size
31
+ num = _to_tuple(start, dim=dim)
32
+ start = (0,) * dim
33
+ stop = num
34
+ elif len(args) == 1:
35
+ # start is start, args[0] is stop, step is 1
36
+ start = _to_tuple(start, dim=dim)
37
+ stop = _to_tuple(args[0], dim=dim)
38
+ num = [stop[i] - start[i] for i in range(dim)]
39
+ elif len(args) == 2:
40
+ # start is start, args[0] is stop, args[1] is num
41
+ start = _to_tuple(start, dim=dim) # Left-Top eg: 12,0
42
+ stop = _to_tuple(args[0], dim=dim) # Right-Bottom eg: 20,32
43
+ num = _to_tuple(args[1], dim=dim) # Target Size eg: 32,124
44
+ else:
45
+ raise ValueError(f"len(args) should be 0, 1 or 2, but got {len(args)}")
46
+
47
+ # PyTorch implement of np.linspace(start[i], stop[i], num[i], endpoint=False)
48
+ axis_grid = []
49
+ for i in range(dim):
50
+ a, b, n = start[i], stop[i], num[i]
51
+ g = torch.linspace(a, b, n + 1, dtype=torch.float32)[:n]
52
+ axis_grid.append(g)
53
+ grid = torch.meshgrid(*axis_grid, indexing="ij") # dim x [W, H, D]
54
+ grid = torch.stack(grid, dim=0) # [dim, W, H, D]
55
+
56
+ return grid
57
+
58
+
59
+ #################################################################################
60
+ # Rotary Positional Embedding Functions #
61
+ #################################################################################
62
+ # https://github.com/meta-llama/llama/blob/be327c427cc5e89cc1d3ab3d3fec4484df771245/llama/model.py#L80
63
+
64
+
65
+ def reshape_for_broadcast(
66
+ freqs_cis: Union[torch.Tensor, Tuple[torch.Tensor]],
67
+ x: torch.Tensor,
68
+ head_first=False,
69
+ ):
70
+ """
71
+ Reshape frequency tensor for broadcasting it with another tensor.
72
+
73
+ This function reshapes the frequency tensor to have the same shape as the target tensor 'x'
74
+ for the purpose of broadcasting the frequency tensor during element-wise operations.
75
+
76
+ Notes:
77
+ When using FlashMHAModified, head_first should be False.
78
+ When using Attention, head_first should be True.
79
+
80
+ Args:
81
+ freqs_cis (Union[torch.Tensor, Tuple[torch.Tensor]]): Frequency tensor to be reshaped.
82
+ x (torch.Tensor): Target tensor for broadcasting compatibility.
83
+ head_first (bool): head dimension first (except batch dim) or not.
84
+
85
+ Returns:
86
+ torch.Tensor: Reshaped frequency tensor.
87
+
88
+ Raises:
89
+ AssertionError: If the frequency tensor doesn't match the expected shape.
90
+ AssertionError: If the target tensor 'x' doesn't have the expected number of dimensions.
91
+ """
92
+ ndim = x.ndim
93
+ assert 0 <= 1 < ndim
94
+
95
+ if isinstance(freqs_cis, tuple):
96
+ # freqs_cis: (cos, sin) in real space
97
+ if head_first:
98
+ assert freqs_cis[0].shape == (
99
+ x.shape[-2],
100
+ x.shape[-1],
101
+ ), f"freqs_cis shape {freqs_cis[0].shape} does not match x shape {x.shape}"
102
+ shape = [
103
+ d if i == ndim - 2 or i == ndim - 1 else 1
104
+ for i, d in enumerate(x.shape)
105
+ ]
106
+ else:
107
+ assert freqs_cis[0].shape == (
108
+ x.shape[1],
109
+ x.shape[-1],
110
+ ), f"freqs_cis shape {freqs_cis[0].shape} does not match x shape {x.shape}"
111
+ shape = [d if i == 1 or i == ndim -
112
+ 1 else 1 for i, d in enumerate(x.shape)]
113
+ return freqs_cis[0].view(*shape), freqs_cis[1].view(*shape)
114
+ else:
115
+ # freqs_cis: values in complex space
116
+ if head_first:
117
+ assert freqs_cis.shape == (
118
+ x.shape[-2],
119
+ x.shape[-1],
120
+ ), f"freqs_cis shape {freqs_cis.shape} does not match x shape {x.shape}"
121
+ shape = [
122
+ d if i == ndim - 2 or i == ndim - 1 else 1
123
+ for i, d in enumerate(x.shape)
124
+ ]
125
+ else:
126
+ assert freqs_cis.shape == (
127
+ x.shape[1],
128
+ x.shape[-1],
129
+ ), f"freqs_cis shape {freqs_cis.shape} does not match x shape {x.shape}"
130
+ shape = [d if i == 1 or i == ndim -
131
+ 1 else 1 for i, d in enumerate(x.shape)]
132
+ return freqs_cis.view(*shape)
133
+
134
+
135
+ def rotate_half(x):
136
+ x_real, x_imag = (
137
+ x.float().reshape(*x.shape[:-1], -1, 2).unbind(-1)
138
+ ) # [B, S, H, D//2]
139
+ return torch.stack([-x_imag, x_real], dim=-1).flatten(3)
140
+
141
+
142
+ def apply_rotary_emb(
143
+ xq: torch.Tensor,
144
+ xk: torch.Tensor,
145
+ freqs_cis: Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]],
146
+ head_first: bool = False,
147
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
148
+ """
149
+ Apply rotary embeddings to input tensors using the given frequency tensor.
150
+
151
+ This function applies rotary embeddings to the given query 'xq' and key 'xk' tensors using the provided
152
+ frequency tensor 'freqs_cis'. The input tensors are reshaped as complex numbers, and the frequency tensor
153
+ is reshaped for broadcasting compatibility. The resulting tensors contain rotary embeddings and are
154
+ returned as real tensors.
155
+
156
+ Args:
157
+ xq (torch.Tensor): Query tensor to apply rotary embeddings. [B, S, H, D]
158
+ xk (torch.Tensor): Key tensor to apply rotary embeddings. [B, S, H, D]
159
+ freqs_cis (torch.Tensor or tuple): Precomputed frequency tensor for complex exponential.
160
+ head_first (bool): head dimension first (except batch dim) or not.
161
+
162
+ Returns:
163
+ Tuple[torch.Tensor, torch.Tensor]: Tuple of modified query tensor and key tensor with rotary embeddings.
164
+
165
+ """
166
+ xk_out = None
167
+ cos, sin = reshape_for_broadcast(freqs_cis, xq, head_first) # [S, D]
168
+ cos, sin = cos.to(xq.device), sin.to(xq.device)
169
+ # real * cos - imag * sin
170
+ # imag * cos + real * sin
171
+ xq_out = (xq.float() * cos + rotate_half(xq.float()) * sin).type_as(xq)
172
+ xk_out = (xk.float() * cos + rotate_half(xk.float()) * sin).type_as(xk)
173
+
174
+ return xq_out, xk_out
175
+
176
+
177
+ def get_nd_rotary_pos_embed(
178
+ rope_dim_list,
179
+ start,
180
+ *args,
181
+ theta=10000.0,
182
+ use_real=False,
183
+ txt_rope_size = None,
184
+ theta_rescale_factor: Union[float, List[float]] = 1.0,
185
+ interpolation_factor: Union[float, List[float]] = 1.0,
186
+ ):
187
+ """
188
+ This is a n-d version of precompute_freqs_cis, which is a RoPE for tokens with n-d structure.
189
+
190
+ Args:
191
+ rope_dim_list (list of int): Dimension of each rope. len(rope_dim_list) should equal to n.
192
+ sum(rope_dim_list) should equal to head_dim of attention layer.
193
+ start (int | tuple of int | list of int): If len(args) == 0, start is num; If len(args) == 1, start is start,
194
+ args[0] is stop, step is 1; If len(args) == 2, start is start, args[0] is stop, args[1] is num.
195
+ *args: See above.
196
+ theta (float): Scaling factor for frequency computation. Defaults to 10000.0.
197
+ use_real (bool): If True, return real part and imaginary part separately. Otherwise, return complex numbers.
198
+ Some libraries such as TensorRT does not support complex64 data type. So it is useful to provide a real
199
+ part and an imaginary part separately.
200
+ theta_rescale_factor (float): Rescale factor for theta. Defaults to 1.0.
201
+
202
+ Returns:
203
+ pos_embed (torch.Tensor): [HW, D/2]
204
+ """
205
+
206
+ grid = get_meshgrid_nd(
207
+ start, *args, dim=len(rope_dim_list)
208
+ ) # [3, W, H, D] / [2, W, H]
209
+
210
+ if isinstance(theta_rescale_factor, int) or isinstance(theta_rescale_factor, float):
211
+ theta_rescale_factor = [theta_rescale_factor] * len(rope_dim_list)
212
+ elif isinstance(theta_rescale_factor, list) and len(theta_rescale_factor) == 1:
213
+ theta_rescale_factor = [theta_rescale_factor[0]] * len(rope_dim_list)
214
+ assert len(theta_rescale_factor) == len(
215
+ rope_dim_list
216
+ ), "len(theta_rescale_factor) should equal to len(rope_dim_list)"
217
+
218
+ if isinstance(interpolation_factor, int) or isinstance(interpolation_factor, float):
219
+ interpolation_factor = [interpolation_factor] * len(rope_dim_list)
220
+ elif isinstance(interpolation_factor, list) and len(interpolation_factor) == 1:
221
+ interpolation_factor = [interpolation_factor[0]] * len(rope_dim_list)
222
+ assert len(interpolation_factor) == len(
223
+ rope_dim_list
224
+ ), "len(interpolation_factor) should equal to len(rope_dim_list)"
225
+
226
+ # use 1/ndim of dimensions to encode grid_axis
227
+ embs = []
228
+ for i in range(len(rope_dim_list)):
229
+ emb = get_1d_rotary_pos_embed(
230
+ rope_dim_list[i],
231
+ grid[i].reshape(-1),
232
+ theta,
233
+ use_real=use_real,
234
+ theta_rescale_factor=theta_rescale_factor[i],
235
+ interpolation_factor=interpolation_factor[i],
236
+ ) # 2 x [WHD, rope_dim_list[i]]
237
+ embs.append(emb)
238
+
239
+ if use_real:
240
+ cos = torch.cat([emb[0] for emb in embs], dim=1) # (WHD, D/2)
241
+ sin = torch.cat([emb[1] for emb in embs], dim=1) # (WHD, D/2)
242
+ vis_emb = (cos, sin)
243
+ else:
244
+ vis_emb = torch.cat(embs, dim=1) # (WHD, D/2)
245
+ # add text rope
246
+ if txt_rope_size is not None:
247
+ embs_txt = []
248
+ vis_max_ids = grid.view(-1).max().item()
249
+ grid_txt = torch.arange(txt_rope_size) + vis_max_ids + 1
250
+ for i in range(len(rope_dim_list)):
251
+ emb = get_1d_rotary_pos_embed(
252
+ rope_dim_list[i],
253
+ grid_txt,
254
+ theta,
255
+ use_real=use_real,
256
+ theta_rescale_factor=theta_rescale_factor[i],
257
+ interpolation_factor=interpolation_factor[i],
258
+ )
259
+ embs_txt.append(emb)
260
+ if use_real:
261
+ cos = torch.cat([emb[0] for emb in embs_txt], dim=1) # (WHD, D/2)
262
+ sin = torch.cat([emb[1] for emb in embs_txt], dim=1) # (WHD, D/2)
263
+ txt_emb = (cos, sin)
264
+ else:
265
+ txt_emb = torch.cat(embs_txt, dim=1) # (WHD, D/2)
266
+ else:
267
+ txt_emb = None
268
+ return vis_emb, txt_emb
269
+
270
+
271
+ def get_1d_rotary_pos_embed(
272
+ dim: int,
273
+ pos: Union[torch.FloatTensor, int],
274
+ theta: float = 10000.0,
275
+ use_real: bool = False,
276
+ theta_rescale_factor: float = 1.0,
277
+ interpolation_factor: float = 1.0,
278
+ ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
279
+ """
280
+ Precompute the frequency tensor for complex exponential (cis) with given dimensions.
281
+ (Note: `cis` means `cos + i * sin`, where i is the imaginary unit.)
282
+
283
+ This function calculates a frequency tensor with complex exponential using the given dimension 'dim'
284
+ and the end index 'end'. The 'theta' parameter scales the frequencies.
285
+ The returned tensor contains complex values in complex64 data type.
286
+
287
+ Args:
288
+ dim (int): Dimension of the frequency tensor.
289
+ pos (int or torch.FloatTensor): Position indices for the frequency tensor. [S] or scalar
290
+ theta (float, optional): Scaling factor for frequency computation. Defaults to 10000.0.
291
+ use_real (bool, optional): If True, return real part and imaginary part separately.
292
+ Otherwise, return complex numbers.
293
+ theta_rescale_factor (float, optional): Rescale factor for theta. Defaults to 1.0.
294
+
295
+ Returns:
296
+ freqs_cis: Precomputed frequency tensor with complex exponential. [S, D/2]
297
+ freqs_cos, freqs_sin: Precomputed frequency tensor with real and imaginary parts separately. [S, D]
298
+ """
299
+ if isinstance(pos, int):
300
+ pos = torch.arange(pos).float()
301
+
302
+ # proposed by reddit user bloc97, to rescale rotary embeddings to longer sequence length without fine-tuning
303
+ # has some connection to NTK literature
304
+ if theta_rescale_factor != 1.0:
305
+ theta *= theta_rescale_factor ** (dim / (dim - 2))
306
+
307
+ freqs = 1.0 / (
308
+ theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)
309
+ ) # [D/2]
310
+ # assert interpolation_factor == 1.0, f"interpolation_factor: {interpolation_factor}"
311
+ freqs = torch.outer(pos * interpolation_factor, freqs) # [S, D/2]
312
+ if use_real:
313
+ freqs_cos = freqs.cos().repeat_interleave(2, dim=1) # [S, D]
314
+ freqs_sin = freqs.sin().repeat_interleave(2, dim=1) # [S, D]
315
+ return freqs_cos, freqs_sin
316
+ else:
317
+ freqs_cis = torch.polar(
318
+ torch.ones_like(freqs), freqs
319
+ ) # complex64 # [S, D/2]
320
+ return freqs_cis
src/modules/models/mmdit/text_encoder/__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from transformers import Qwen3VLForConditionalGeneration, AutoTokenizer
4
+
5
+
6
+ def load_text_encoder(
7
+ text_encoder_ckpt: str,
8
+ device: torch.device = torch.device("cpu"),
9
+ torch_dtype: torch.dtype = torch.bfloat16,
10
+ ):
11
+ loader = Qwen3VLForConditionalGeneration #or AutoModelForVision2Seq
12
+ model = loader.from_pretrained(
13
+ text_encoder_ckpt,
14
+ torch_dtype=torch_dtype,
15
+ local_files_only=True,
16
+ trust_remote_code=True,
17
+ ).to(device).eval()
18
+ tokenizer = AutoTokenizer.from_pretrained(
19
+ text_encoder_ckpt,
20
+ local_files_only=True,
21
+ trust_remote_code=True,
22
+ )
23
+ return tokenizer, model
src/modules/models/mmdit/vae/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from modules.models.mmdit.vae.wanvae import WanxVAE
2
+
3
+ __all__ = ["WanxVAE"]
src/modules/models/mmdit/vae/wanvae.py ADDED
@@ -0,0 +1,697 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
2
+ import logging
3
+
4
+ import torch
5
+ import torch.cuda.amp as amp
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+ from einops import rearrange
9
+
10
+ __all__ = [
11
+ 'WanVAE',
12
+ ]
13
+
14
+ CACHE_T = 2
15
+
16
+
17
+ class CausalConv3d(nn.Conv3d):
18
+ """
19
+ Causal 3d convolusion.
20
+ """
21
+
22
+ def __init__(self, *args, **kwargs):
23
+ super().__init__(*args, **kwargs)
24
+ self._padding = (self.padding[2], self.padding[2], self.padding[1],
25
+ self.padding[1], 2 * self.padding[0], 0)
26
+ self.padding = (0, 0, 0)
27
+
28
+ def forward(self, x, cache_x=None):
29
+ padding = list(self._padding)
30
+ if cache_x is not None and self._padding[4] > 0:
31
+ cache_x = cache_x.to(x.device)
32
+ x = torch.cat([cache_x, x], dim=2)
33
+ padding[4] -= cache_x.shape[2]
34
+ x = F.pad(x, padding)
35
+
36
+ return super().forward(x)
37
+
38
+
39
+ class RMS_norm(nn.Module):
40
+
41
+ def __init__(self, dim, channel_first=True, images=True, bias=False):
42
+ super().__init__()
43
+ broadcastable_dims = (1, 1, 1) if not images else (1, 1)
44
+ shape = (dim, *broadcastable_dims) if channel_first else (dim,)
45
+
46
+ self.channel_first = channel_first
47
+ self.scale = dim**0.5
48
+ self.gamma = nn.Parameter(torch.ones(shape))
49
+ self.bias = nn.Parameter(torch.zeros(shape)) if bias else 0.
50
+
51
+ def forward(self, x):
52
+ return F.normalize(
53
+ x, dim=(1 if self.channel_first else
54
+ -1)) * self.scale * self.gamma + self.bias
55
+
56
+
57
+ class Upsample(nn.Upsample):
58
+
59
+ def forward(self, x):
60
+ """
61
+ Fix bfloat16 support for nearest neighbor interpolation.
62
+ """
63
+ return super().forward(x.float()).type_as(x)
64
+
65
+
66
+ class Resample(nn.Module):
67
+
68
+ def __init__(self, dim, mode):
69
+ assert mode in ('none', 'upsample2d', 'upsample3d', 'downsample2d',
70
+ 'downsample3d')
71
+ super().__init__()
72
+ self.dim = dim
73
+ self.mode = mode
74
+
75
+ # layers
76
+ if mode == 'upsample2d':
77
+ self.resample = nn.Sequential(
78
+ Upsample(scale_factor=(2., 2.), mode='nearest-exact'),
79
+ nn.Conv2d(dim, dim // 2, 3, padding=1))
80
+ elif mode == 'upsample3d':
81
+ self.resample = nn.Sequential(
82
+ Upsample(scale_factor=(2., 2.), mode='nearest-exact'),
83
+ nn.Conv2d(dim, dim // 2, 3, padding=1))
84
+ self.time_conv = CausalConv3d(
85
+ dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
86
+
87
+ elif mode == 'downsample2d':
88
+ self.resample = nn.Sequential(
89
+ nn.ZeroPad2d((0, 1, 0, 1)),
90
+ nn.Conv2d(dim, dim, 3, stride=(2, 2)))
91
+ elif mode == 'downsample3d':
92
+ self.resample = nn.Sequential(
93
+ nn.ZeroPad2d((0, 1, 0, 1)),
94
+ nn.Conv2d(dim, dim, 3, stride=(2, 2)))
95
+ self.time_conv = CausalConv3d(
96
+ dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0))
97
+
98
+ else:
99
+ self.resample = nn.Identity()
100
+
101
+ def forward(self, x, feat_cache=None, feat_idx=[0]):
102
+ b, c, t, h, w = x.size()
103
+ if self.mode == 'upsample3d':
104
+ if feat_cache is not None:
105
+ idx = feat_idx[0]
106
+ if feat_cache[idx] is None:
107
+ feat_cache[idx] = 'Rep'
108
+ feat_idx[0] += 1
109
+ else:
110
+
111
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
112
+ if cache_x.shape[2] < 2 and feat_cache[
113
+ idx] is not None and feat_cache[idx] != 'Rep':
114
+ # cache last frame of last two chunk
115
+ cache_x = torch.cat([
116
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
117
+ cache_x.device), cache_x
118
+ ],
119
+ dim=2)
120
+ if cache_x.shape[2] < 2 and feat_cache[
121
+ idx] is not None and feat_cache[idx] == 'Rep':
122
+ cache_x = torch.cat([
123
+ torch.zeros_like(cache_x).to(cache_x.device),
124
+ cache_x
125
+ ],
126
+ dim=2)
127
+ if feat_cache[idx] == 'Rep':
128
+ x = self.time_conv(x)
129
+ else:
130
+ x = self.time_conv(x, feat_cache[idx])
131
+ feat_cache[idx] = cache_x
132
+ feat_idx[0] += 1
133
+
134
+ x = x.reshape(b, 2, c, t, h, w)
135
+ x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]),
136
+ 3)
137
+ x = x.reshape(b, c, t * 2, h, w)
138
+ t = x.shape[2]
139
+ x = rearrange(x, 'b c t h w -> (b t) c h w')
140
+ x = self.resample(x)
141
+ x = rearrange(x, '(b t) c h w -> b c t h w', t=t)
142
+
143
+ if self.mode == 'downsample3d':
144
+ if feat_cache is not None:
145
+ idx = feat_idx[0]
146
+ if feat_cache[idx] is None:
147
+ feat_cache[idx] = x.clone()
148
+ feat_idx[0] += 1
149
+ else:
150
+
151
+ cache_x = x[:, :, -1:, :, :].clone()
152
+ # if cache_x.shape[2] < 2 and feat_cache[idx] is not None and feat_cache[idx]!='Rep':
153
+ # # cache last frame of last two chunk
154
+ # cache_x = torch.cat([feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device), cache_x], dim=2)
155
+
156
+ x = self.time_conv(
157
+ torch.cat([feat_cache[idx][:, :, -1:, :, :], x], 2))
158
+ feat_cache[idx] = cache_x
159
+ feat_idx[0] += 1
160
+ return x
161
+
162
+ def init_weight(self, conv):
163
+ conv_weight = conv.weight
164
+ nn.init.zeros_(conv_weight)
165
+ c1, c2, t, h, w = conv_weight.size()
166
+ one_matrix = torch.eye(c1, c2)
167
+ init_matrix = one_matrix
168
+ nn.init.zeros_(conv_weight)
169
+ #conv_weight.data[:,:,-1,1,1] = init_matrix * 0.5
170
+ conv_weight.data[:, :, 1, 0, 0] = init_matrix #* 0.5
171
+ conv.weight.data.copy_(conv_weight)
172
+ nn.init.zeros_(conv.bias.data)
173
+
174
+ def init_weight2(self, conv):
175
+ conv_weight = conv.weight.data
176
+ nn.init.zeros_(conv_weight)
177
+ c1, c2, t, h, w = conv_weight.size()
178
+ init_matrix = torch.eye(c1 // 2, c2)
179
+ #init_matrix = repeat(init_matrix, 'o ... -> (o 2) ...').permute(1,0,2).contiguous().reshape(c1,c2)
180
+ conv_weight[:c1 // 2, :, -1, 0, 0] = init_matrix
181
+ conv_weight[c1 // 2:, :, -1, 0, 0] = init_matrix
182
+ conv.weight.data.copy_(conv_weight)
183
+ nn.init.zeros_(conv.bias.data)
184
+
185
+
186
+ class ResidualBlock(nn.Module):
187
+
188
+ def __init__(self, in_dim, out_dim, dropout=0.0):
189
+ super().__init__()
190
+ self.in_dim = in_dim
191
+ self.out_dim = out_dim
192
+
193
+ # layers
194
+ self.residual = nn.Sequential(
195
+ RMS_norm(in_dim, images=False), nn.SiLU(),
196
+ CausalConv3d(in_dim, out_dim, 3, padding=1),
197
+ RMS_norm(out_dim, images=False), nn.SiLU(), nn.Dropout(dropout),
198
+ CausalConv3d(out_dim, out_dim, 3, padding=1))
199
+ self.shortcut = CausalConv3d(in_dim, out_dim, 1) \
200
+ if in_dim != out_dim else nn.Identity()
201
+
202
+ def forward(self, x, feat_cache=None, feat_idx=[0]):
203
+ h = self.shortcut(x)
204
+ for layer in self.residual:
205
+ if isinstance(layer, CausalConv3d) and feat_cache is not None:
206
+ idx = feat_idx[0]
207
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
208
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
209
+ # cache last frame of last two chunk
210
+ cache_x = torch.cat([
211
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
212
+ cache_x.device), cache_x
213
+ ],
214
+ dim=2)
215
+ x = layer(x, feat_cache[idx])
216
+ feat_cache[idx] = cache_x
217
+ feat_idx[0] += 1
218
+ else:
219
+ x = layer(x)
220
+ return x + h
221
+
222
+
223
+ class AttentionBlock(nn.Module):
224
+ """
225
+ Causal self-attention with a single head.
226
+ """
227
+
228
+ def __init__(self, dim):
229
+ super().__init__()
230
+ self.dim = dim
231
+
232
+ # layers
233
+ self.norm = RMS_norm(dim)
234
+ self.to_qkv = nn.Conv2d(dim, dim * 3, 1)
235
+ self.proj = nn.Conv2d(dim, dim, 1)
236
+
237
+ # zero out the last layer params
238
+ nn.init.zeros_(self.proj.weight)
239
+
240
+ def forward(self, x):
241
+ identity = x
242
+ b, c, t, h, w = x.size()
243
+ x = rearrange(x, 'b c t h w -> (b t) c h w')
244
+ x = self.norm(x)
245
+ # compute query, key, value
246
+ q, k, v = self.to_qkv(x).reshape(b * t, 1, c * 3,
247
+ -1).permute(0, 1, 3,
248
+ 2).contiguous().chunk(
249
+ 3, dim=-1)
250
+
251
+ # apply attention
252
+ x = F.scaled_dot_product_attention(
253
+ q,
254
+ k,
255
+ v,
256
+ )
257
+ x = x.squeeze(1).permute(0, 2, 1).reshape(b * t, c, h, w)
258
+
259
+ # output
260
+ x = self.proj(x)
261
+ x = rearrange(x, '(b t) c h w-> b c t h w', t=t)
262
+ return x + identity
263
+
264
+
265
+ class Encoder3d(nn.Module):
266
+
267
+ def __init__(self,
268
+ dim=128,
269
+ z_dim=4,
270
+ dim_mult=[1, 2, 4, 4],
271
+ num_res_blocks=2,
272
+ attn_scales=[],
273
+ temperal_downsample=[True, True, False],
274
+ dropout=0.0):
275
+ super().__init__()
276
+ self.dim = dim
277
+ self.z_dim = z_dim
278
+ self.dim_mult = dim_mult
279
+ self.num_res_blocks = num_res_blocks
280
+ self.attn_scales = attn_scales
281
+ self.temperal_downsample = temperal_downsample
282
+
283
+ # dimensions
284
+ dims = [dim * u for u in [1] + dim_mult]
285
+ scale = 1.0
286
+
287
+ # init block
288
+ self.conv1 = CausalConv3d(3, dims[0], 3, padding=1)
289
+
290
+ # downsample blocks
291
+ downsamples = []
292
+ for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
293
+ # residual (+attention) blocks
294
+ for _ in range(num_res_blocks):
295
+ downsamples.append(ResidualBlock(in_dim, out_dim, dropout))
296
+ if scale in attn_scales:
297
+ downsamples.append(AttentionBlock(out_dim))
298
+ in_dim = out_dim
299
+
300
+ # downsample block
301
+ if i != len(dim_mult) - 1:
302
+ mode = 'downsample3d' if temperal_downsample[
303
+ i] else 'downsample2d'
304
+ downsamples.append(Resample(out_dim, mode=mode))
305
+ scale /= 2.0
306
+ self.downsamples = nn.Sequential(*downsamples)
307
+
308
+ # middle blocks
309
+ self.middle = nn.Sequential(
310
+ ResidualBlock(out_dim, out_dim, dropout), AttentionBlock(out_dim),
311
+ ResidualBlock(out_dim, out_dim, dropout))
312
+
313
+ # output blocks
314
+ self.head = nn.Sequential(
315
+ RMS_norm(out_dim, images=False), nn.SiLU(),
316
+ CausalConv3d(out_dim, z_dim, 3, padding=1))
317
+
318
+ def forward(self, x, feat_cache=None, feat_idx=[0]):
319
+ if feat_cache is not None:
320
+ idx = feat_idx[0]
321
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
322
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
323
+ # cache last frame of last two chunk
324
+ cache_x = torch.cat([
325
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
326
+ cache_x.device), cache_x
327
+ ],
328
+ dim=2)
329
+ x = self.conv1(x, feat_cache[idx])
330
+ feat_cache[idx] = cache_x
331
+ feat_idx[0] += 1
332
+ else:
333
+ x = self.conv1(x)
334
+
335
+ ## downsamples
336
+ for layer in self.downsamples:
337
+ if feat_cache is not None:
338
+ x = layer(x, feat_cache, feat_idx)
339
+ else:
340
+ x = layer(x)
341
+
342
+ ## middle
343
+ for layer in self.middle:
344
+ if isinstance(layer, ResidualBlock) and feat_cache is not None:
345
+ x = layer(x, feat_cache, feat_idx)
346
+ else:
347
+ x = layer(x)
348
+
349
+ ## head
350
+ for layer in self.head:
351
+ if isinstance(layer, CausalConv3d) and feat_cache is not None:
352
+ idx = feat_idx[0]
353
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
354
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
355
+ # cache last frame of last two chunk
356
+ cache_x = torch.cat([
357
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
358
+ cache_x.device), cache_x
359
+ ],
360
+ dim=2)
361
+ x = layer(x, feat_cache[idx])
362
+ feat_cache[idx] = cache_x
363
+ feat_idx[0] += 1
364
+ else:
365
+ x = layer(x)
366
+ return x
367
+
368
+
369
+ class Decoder3d(nn.Module):
370
+
371
+ def __init__(self,
372
+ dim=128,
373
+ z_dim=4,
374
+ dim_mult=[1, 2, 4, 4],
375
+ num_res_blocks=2,
376
+ attn_scales=[],
377
+ temperal_upsample=[False, True, True],
378
+ dropout=0.0):
379
+ super().__init__()
380
+ self.dim = dim
381
+ self.z_dim = z_dim
382
+ self.dim_mult = dim_mult
383
+ self.num_res_blocks = num_res_blocks
384
+ self.attn_scales = attn_scales
385
+ self.temperal_upsample = temperal_upsample
386
+
387
+ # dimensions
388
+ dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
389
+ scale = 1.0 / 2**(len(dim_mult) - 2)
390
+
391
+ # init block
392
+ self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
393
+
394
+ # middle blocks
395
+ self.middle = nn.Sequential(
396
+ ResidualBlock(dims[0], dims[0], dropout), AttentionBlock(dims[0]),
397
+ ResidualBlock(dims[0], dims[0], dropout))
398
+
399
+ # upsample blocks
400
+ upsamples = []
401
+ for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
402
+ # residual (+attention) blocks
403
+ if i == 1 or i == 2 or i == 3:
404
+ in_dim = in_dim // 2
405
+ for _ in range(num_res_blocks + 1):
406
+ upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
407
+ if scale in attn_scales:
408
+ upsamples.append(AttentionBlock(out_dim))
409
+ in_dim = out_dim
410
+
411
+ # upsample block
412
+ if i != len(dim_mult) - 1:
413
+ mode = 'upsample3d' if temperal_upsample[i] else 'upsample2d'
414
+ upsamples.append(Resample(out_dim, mode=mode))
415
+ scale *= 2.0
416
+ self.upsamples = nn.Sequential(*upsamples)
417
+
418
+ # output blocks
419
+ self.head = nn.Sequential(
420
+ RMS_norm(out_dim, images=False), nn.SiLU(),
421
+ CausalConv3d(out_dim, 3, 3, padding=1))
422
+
423
+ def forward(self, x, feat_cache=None, feat_idx=[0]):
424
+ ## conv1
425
+ if feat_cache is not None:
426
+ idx = feat_idx[0]
427
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
428
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
429
+ # cache last frame of last two chunk
430
+ cache_x = torch.cat([
431
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
432
+ cache_x.device), cache_x
433
+ ],
434
+ dim=2)
435
+ x = self.conv1(x, feat_cache[idx])
436
+ feat_cache[idx] = cache_x
437
+ feat_idx[0] += 1
438
+ else:
439
+ x = self.conv1(x)
440
+
441
+ ## middle
442
+ for layer in self.middle:
443
+ if isinstance(layer, ResidualBlock) and feat_cache is not None:
444
+ x = layer(x, feat_cache, feat_idx)
445
+ else:
446
+ x = layer(x)
447
+
448
+ ## upsamples
449
+ for layer in self.upsamples:
450
+ if feat_cache is not None:
451
+ x = layer(x, feat_cache, feat_idx)
452
+ else:
453
+ x = layer(x)
454
+
455
+ ## head
456
+ for layer in self.head:
457
+ if isinstance(layer, CausalConv3d) and feat_cache is not None:
458
+ idx = feat_idx[0]
459
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
460
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
461
+ # cache last frame of last two chunk
462
+ cache_x = torch.cat([
463
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
464
+ cache_x.device), cache_x
465
+ ],
466
+ dim=2)
467
+ x = layer(x, feat_cache[idx])
468
+ feat_cache[idx] = cache_x
469
+ feat_idx[0] += 1
470
+ else:
471
+ x = layer(x)
472
+ return x
473
+
474
+
475
+ def count_conv3d(model):
476
+ count = 0
477
+ for m in model.modules():
478
+ if isinstance(m, CausalConv3d):
479
+ count += 1
480
+ return count
481
+
482
+
483
+ class WanVAE_(nn.Module):
484
+
485
+ def __init__(self,
486
+ dim=128,
487
+ z_dim=4,
488
+ dim_mult=[1, 2, 4, 4],
489
+ num_res_blocks=2,
490
+ attn_scales=[],
491
+ temperal_downsample=[True, True, False],
492
+ dropout=0.0):
493
+ super().__init__()
494
+ self.dim = dim
495
+ self.z_dim = z_dim
496
+ self.dim_mult = dim_mult
497
+ self.num_res_blocks = num_res_blocks
498
+ self.attn_scales = attn_scales
499
+ self.temperal_downsample = temperal_downsample
500
+ self.temperal_upsample = temperal_downsample[::-1]
501
+
502
+ # modules
503
+ self.encoder = Encoder3d(dim, z_dim * 2, dim_mult, num_res_blocks,
504
+ attn_scales, self.temperal_downsample, dropout)
505
+ self.conv1 = CausalConv3d(z_dim * 2, z_dim * 2, 1)
506
+ self.conv2 = CausalConv3d(z_dim, z_dim, 1)
507
+ self.decoder = Decoder3d(dim, z_dim, dim_mult, num_res_blocks,
508
+ attn_scales, self.temperal_upsample, dropout)
509
+
510
+ def forward(self, x):
511
+ mu, log_var = self.encode(x)
512
+ z = self.reparameterize(mu, log_var)
513
+ x_recon = self.decode(z)
514
+ return x_recon, mu, log_var
515
+
516
+ def encode(self, x, scale=None, return_posterior=False):
517
+ self.clear_cache()
518
+ ## cache
519
+ t = x.shape[2]
520
+ iter_ = 1 + (t - 1) // 4
521
+ ## 对encode输入的x,按时间拆分为1、4、4、4....
522
+ for i in range(iter_):
523
+ self._enc_conv_idx = [0]
524
+ if i == 0:
525
+ out = self.encoder(
526
+ x[:, :, :1, :, :],
527
+ feat_cache=self._enc_feat_map,
528
+ feat_idx=self._enc_conv_idx)
529
+ else:
530
+ out_ = self.encoder(
531
+ x[:, :, 1 + 4 * (i - 1):1 + 4 * i, :, :],
532
+ feat_cache=self._enc_feat_map,
533
+ feat_idx=self._enc_conv_idx)
534
+ out = torch.cat([out, out_], 2)
535
+ mu, log_var = self.conv1(out).chunk(2, dim=1)
536
+ if scale is None or return_posterior:
537
+ return mu, log_var
538
+
539
+ mu = self.reparameterize(mu, log_var)
540
+ if isinstance(scale[0], torch.Tensor):
541
+ mu = (mu - scale[0].view(1, self.z_dim, 1, 1, 1)) * scale[1].view(
542
+ 1, self.z_dim, 1, 1, 1)
543
+ else:
544
+ mu = (mu - scale[0]) * scale[1]
545
+ self.clear_cache()
546
+ return mu
547
+
548
+ def decode(self, z, scale=None):
549
+ self.clear_cache()
550
+ # z: [b,c,t,h,w]
551
+ if scale is not None:
552
+ if isinstance(scale[0], torch.Tensor):
553
+ z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
554
+ 1, self.z_dim, 1, 1, 1)
555
+ else:
556
+ z = z / scale[1] + scale[0]
557
+ iter_ = z.shape[2]
558
+ x = self.conv2(z)
559
+ for i in range(iter_):
560
+ self._conv_idx = [0]
561
+ if i == 0:
562
+ out = self.decoder(
563
+ x[:, :, i:i + 1, :, :],
564
+ feat_cache=self._feat_map,
565
+ feat_idx=self._conv_idx)
566
+ else:
567
+ out_ = self.decoder(
568
+ x[:, :, i:i + 1, :, :],
569
+ feat_cache=self._feat_map,
570
+ feat_idx=self._conv_idx)
571
+ out = torch.cat([out, out_], 2)
572
+ self.clear_cache()
573
+ return out
574
+
575
+ def reparameterize(self, mu, log_var):
576
+ std = torch.exp(0.5 * log_var)
577
+ eps = torch.randn_like(std)
578
+ return eps * std + mu
579
+
580
+ def sample(self, imgs, deterministic=False, scale=None):
581
+ mu, log_var = self.encode(imgs)
582
+ if deterministic:
583
+ return mu
584
+ std = torch.exp(0.5 * log_var.clamp(-30.0, 20.0))
585
+ mu = mu + std * torch.randn_like(std)
586
+ if isinstance(scale[0], torch.Tensor):
587
+ mu = (mu - scale[0].view(1, self.z_dim, 1, 1, 1)) * scale[1].view(
588
+ 1, self.z_dim, 1, 1, 1)
589
+ else:
590
+ mu = (mu - scale[0]) * scale[1]
591
+ self.clear_cache()
592
+ return mu
593
+
594
+ def clear_cache(self):
595
+ self._conv_num = count_conv3d(self.decoder)
596
+ self._conv_idx = [0]
597
+ self._feat_map = [None] * self._conv_num
598
+ #cache encode
599
+ self._enc_conv_num = count_conv3d(self.encoder)
600
+ self._enc_conv_idx = [0]
601
+ self._enc_feat_map = [None] * self._enc_conv_num
602
+
603
+
604
+ def _video_vae(pretrained_path=None, z_dim=None, device='cpu', **kwargs):
605
+ """
606
+ Autoencoder3d adapted from Stable Diffusion 1.x, 2.x and XL.
607
+ """
608
+ # params
609
+ cfg = dict(
610
+ dim=96,
611
+ z_dim=z_dim,
612
+ dim_mult=[1, 2, 4, 4],
613
+ num_res_blocks=2,
614
+ attn_scales=[],
615
+ temperal_downsample=[False, True, True],
616
+ dropout=0.0)
617
+ cfg.update(**kwargs)
618
+
619
+ # init model
620
+ with torch.device('meta'):
621
+ model = WanVAE_(**cfg)
622
+
623
+ # load checkpoint
624
+ logging.info(f'loading {pretrained_path}')
625
+
626
+ if pretrained_path.endswith('.safetensors'):
627
+ from safetensors.torch import load_file
628
+ pretrained_state_dict = load_file(pretrained_path, device='cpu')
629
+ else:
630
+ pretrained_state_dict = torch.load(pretrained_path, map_location='cpu')
631
+
632
+ model.load_state_dict(pretrained_state_dict, assign=True)
633
+
634
+ return model
635
+
636
+
637
+
638
+ class WanxVAE(nn.Module):
639
+ # @register_to_config
640
+ def __init__(self,
641
+ pretrained='',
642
+ torch_dtype=torch.float32,
643
+ device='cuda'
644
+ ):
645
+ super().__init__()
646
+
647
+ self.dtype = torch_dtype
648
+ self.device = device
649
+
650
+ mean = [
651
+ -0.7571, -0.7089, -0.9113, 0.1075, -0.1745, 0.9653, -0.1517, 1.5508,
652
+ 0.4134, -0.0715, 0.5517, -0.3632, -0.1922, -0.9497, 0.2503, -0.2921
653
+ ]
654
+ std = [
655
+ 2.8184, 1.4541, 2.3275, 2.6558, 1.2196, 1.7708, 2.6052, 2.0743,
656
+ 3.2687, 2.1526, 2.8652, 1.5579, 1.6382, 1.1253, 2.8251, 1.9160
657
+ ]
658
+ self.mean = torch.tensor(mean, dtype=self.dtype, device=device)
659
+ self.std = torch.tensor(std, dtype=self.dtype, device=device)
660
+ self.scale = [self.mean, 1.0 / self.std]
661
+
662
+ self.config = lambda: None
663
+ self.config.latents_mean = self.mean
664
+ self.config.latents_std = self.std
665
+ self.ffactor_spatial = 8
666
+ self.ffactor_temporal = 4
667
+ self.config.latent_channels = 16
668
+
669
+ # init model
670
+ self.model = _video_vae(
671
+ pretrained_path=pretrained,
672
+ z_dim=16,
673
+ ).eval().requires_grad_(False)
674
+ self.model = self.model.to(device=device, dtype=torch_dtype)
675
+
676
+ def encode(self, videos, return_posterior=False, **kwargs):
677
+ """
678
+ videos: A list of videos each with shape [C, T, H, W].
679
+ """
680
+ with amp.autocast(dtype=torch.float):
681
+ if return_posterior:
682
+ mus, log_vars = self.model.encode(
683
+ videos, scale=self.scale, return_posterior=True)
684
+ return mus, log_vars
685
+ else:
686
+ latents = self.model.encode(videos, scale=self.scale)
687
+ return latents
688
+
689
+ def decode(self, zs, **kwargs):
690
+ with amp.autocast(dtype=torch.float):
691
+ videos = [
692
+ self.model.decode(u.unsqueeze(0), scale=self.scale).clamp_(-1, 1).squeeze(0)
693
+ for u in zs
694
+ ]
695
+ videos = torch.stack(videos, dim=0)
696
+ return (videos, )
697
+
src/modules/models/pipeline.py ADDED
@@ -0,0 +1,971 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ #
16
+ # Modified from diffusers==0.29.2
17
+ #
18
+ # ==============================================================================
19
+ import inspect
20
+ from typing import Any, Callable, Dict, List, Optional, Union, Tuple
21
+ import torch
22
+ import torch.distributed as dist
23
+ import numpy as np
24
+ from dataclasses import dataclass
25
+ from packaging import version
26
+ from einops import rearrange
27
+
28
+ from typing import Any
29
+
30
+ from transformers import AutoProcessor
31
+
32
+ from diffusers.callbacks import MultiPipelineCallbacks, PipelineCallback
33
+ from diffusers.image_processor import VaeImageProcessor
34
+ from diffusers.models import AutoencoderKL
35
+ from diffusers.schedulers import KarrasDiffusionSchedulers
36
+ from diffusers.utils import (
37
+ logging,
38
+ replace_example_docstring,
39
+ )
40
+ from diffusers.utils.torch_utils import randn_tensor
41
+ from diffusers.pipelines.pipeline_utils import DiffusionPipeline
42
+ from diffusers.utils import BaseOutput
43
+
44
+ from modules.models.mmdit.dit import Transformer3DModel
45
+
46
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
47
+
48
+ EXAMPLE_DOC_STRING = """"""
49
+
50
+ PRECISION_TO_TYPE = {
51
+ 'fp32': torch.float32,
52
+ 'fp16': torch.float16,
53
+ 'bf16': torch.bfloat16,
54
+ }
55
+
56
+
57
+ def retrieve_timesteps(
58
+ scheduler,
59
+ num_inference_steps: Optional[int] = None,
60
+ device: Optional[Union[str, torch.device]] = None,
61
+ timesteps: Optional[List[int]] = None,
62
+ sigmas: Optional[List[float]] = None,
63
+ **kwargs,
64
+ ):
65
+ """
66
+ Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
67
+ custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
68
+
69
+ Args:
70
+ scheduler (`SchedulerMixin`):
71
+ The scheduler to get timesteps from.
72
+ num_inference_steps (`int`):
73
+ The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
74
+ must be `None`.
75
+ device (`str` or `torch.device`, *optional*):
76
+ The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
77
+ timesteps (`List[int]`, *optional*):
78
+ Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
79
+ `num_inference_steps` and `sigmas` must be `None`.
80
+ sigmas (`List[float]`, *optional*):
81
+ Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
82
+ `num_inference_steps` and `timesteps` must be `None`.
83
+
84
+ Returns:
85
+ `Tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
86
+ second element is the number of inference steps.
87
+ """
88
+ if timesteps is not None and sigmas is not None:
89
+ raise ValueError(
90
+ "Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values"
91
+ )
92
+ if timesteps is not None:
93
+ accepts_timesteps = "timesteps" in set(
94
+ inspect.signature(scheduler.set_timesteps).parameters.keys()
95
+ )
96
+ if not accepts_timesteps:
97
+ raise ValueError(
98
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
99
+ f" timestep schedules. Please check whether you are using the correct scheduler."
100
+ )
101
+ scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
102
+ timesteps = scheduler.timesteps
103
+ num_inference_steps = len(timesteps)
104
+ elif sigmas is not None:
105
+ accept_sigmas = "sigmas" in set(
106
+ inspect.signature(scheduler.set_timesteps).parameters.keys()
107
+ )
108
+ if not accept_sigmas:
109
+ raise ValueError(
110
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
111
+ f" sigmas schedules. Please check whether you are using the correct scheduler."
112
+ )
113
+ scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
114
+ timesteps = scheduler.timesteps
115
+ num_inference_steps = len(timesteps)
116
+ else:
117
+ scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
118
+ timesteps = scheduler.timesteps
119
+ return timesteps, num_inference_steps
120
+
121
+
122
+ @dataclass
123
+ class PipelineOutput(BaseOutput):
124
+ videos: Union[torch.Tensor, np.ndarray]
125
+
126
+
127
+ class Pipeline(DiffusionPipeline):
128
+ model_cpu_offload_seq = "text_encoder->transformer->vae"
129
+ _callback_tensor_inputs = ["latents", "prompt_embeds"]
130
+
131
+ def __init__(
132
+ self,
133
+ vae: AutoencoderKL,
134
+ text_encoder: Any,
135
+ tokenizer: Any,
136
+ transformer: Transformer3DModel,
137
+ scheduler: KarrasDiffusionSchedulers,
138
+ args=None,
139
+ ):
140
+ super().__init__()
141
+ self.args = args
142
+ self.register_modules(
143
+ vae=vae,
144
+ text_encoder=text_encoder,
145
+ tokenizer=tokenizer,
146
+ transformer=transformer,
147
+ scheduler=scheduler,
148
+ )
149
+ self.enable_multi_task = getattr(
150
+ self.args, "enable_multi_task_training", False)
151
+ if hasattr(self.vae, "ffactor_spatial"):
152
+ self.vae_scale_factor = self.vae.ffactor_spatial
153
+ self.vae_scale_factor_temporal = self.vae.ffactor_temporal
154
+ else:
155
+ self.vae_scale_factor = 2 ** (
156
+ len(self.vae.config.block_out_channels) - 1)
157
+ self.vae_scale_factor_temporal = 4 # hard code for HunyuanVideoVAE
158
+
159
+ self.image_processor = VaeImageProcessor(
160
+ vae_scale_factor=self.vae_scale_factor)
161
+
162
+ text_encoder_ckpt = dict(args.text_encoder_arch_config.get("params", {}))[
163
+ 'text_encoder_ckpt']
164
+ self.qwen_processor = AutoProcessor.from_pretrained(text_encoder_ckpt)
165
+
166
+ self.text_token_max_length = self.args.text_token_max_length
167
+ self.prompt_template_encode = {
168
+ 'image': "<|im_start|>system\n \\nDescribe the image by detailing the color, shape, size, texture, quantity, text, spatial relationships of the objects and background:<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n",
169
+ 'multiple_images': "<|im_start|>system\n \\nDescribe the image by detailing the color, shape, size, texture, quantity, text, spatial relationships of the objects and background:<|im_end|>\n{}<|im_start|>assistant\n",
170
+ 'video': "<|im_start|>system\n \\nDescribe the video by detailing the following aspects:\n1. The main content and theme of the video.\n2. The color, shape, size, texture, quantity, text, and spatial relationships of the objects.\n3. Actions, events, behaviors temporal relationships, physical movement changes of the objects.\n4. background environment, light, style and atmosphere.\n5. camera angles, movements, and transitions used in the video:<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
171
+ }
172
+ # [36:-4]
173
+ # [36:-4]
174
+ # [93:-4]
175
+ self.prompt_template_encode_start_idx = {
176
+ 'image': 34,
177
+ 'multiple_images': 34,
178
+ 'video': 91,
179
+ }
180
+
181
+ def _extract_masked_hidden(self, hidden_states: torch.Tensor, mask: torch.Tensor):
182
+ bool_mask = mask.bool()
183
+ valid_lengths = bool_mask.sum(dim=1)
184
+ selected = hidden_states[bool_mask]
185
+ split_result = torch.split(selected, valid_lengths.tolist(), dim=0)
186
+
187
+ return split_result
188
+
189
+ def _get_qwen_prompt_embeds(
190
+ self,
191
+ prompt: Union[str, List[str]] = None,
192
+ template_type: str = 'image',
193
+ device: Optional[torch.device] = None,
194
+ dtype: Optional[torch.dtype] = None,
195
+ ):
196
+ device = device or self._execution_device
197
+ dtype = dtype or self.text_encoder.dtype
198
+
199
+ prompt = [prompt] if isinstance(prompt, str) else prompt
200
+
201
+ template = self.prompt_template_encode[template_type]
202
+ drop_idx = self.prompt_template_encode_start_idx[template_type]
203
+ txt = [template.format(e) for e in prompt]
204
+ txt_tokens = self.tokenizer(
205
+ txt, max_length=self.text_token_max_length + drop_idx, padding=True, truncation=True, return_tensors="pt"
206
+ ).to(device)
207
+ encoder_hidden_states = self.text_encoder(
208
+ input_ids=txt_tokens.input_ids,
209
+ attention_mask=txt_tokens.attention_mask,
210
+ output_hidden_states=True,
211
+ )
212
+ hidden_states = encoder_hidden_states.hidden_states[-1]
213
+ split_hidden_states = self._extract_masked_hidden(
214
+ hidden_states, txt_tokens.attention_mask)
215
+ split_hidden_states = [e[drop_idx:] for e in split_hidden_states]
216
+ attn_mask_list = [torch.ones(
217
+ e.size(0), dtype=torch.long, device=e.device) for e in split_hidden_states]
218
+ max_seq_len = min([
219
+ self.text_token_max_length,
220
+ max([u.size(0) for u in split_hidden_states]),
221
+ max([u.size(0) for u in attn_mask_list])])
222
+ prompt_embeds = torch.stack(
223
+ [torch.cat([u, u.new_zeros(max_seq_len - u.size(0), u.size(1))])
224
+ for u in split_hidden_states]
225
+ )
226
+ encoder_attention_mask = torch.stack(
227
+ [torch.cat([u, u.new_zeros(max_seq_len - u.size(0))])
228
+ for u in attn_mask_list]
229
+ )
230
+
231
+ prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
232
+
233
+ return prompt_embeds, encoder_attention_mask
234
+
235
+
236
+ def encode_prompt_multiple_images(
237
+ self,
238
+ prompt: Union[str, List[str]],
239
+ device: Optional[torch.device] = None,
240
+ images: Optional[torch.Tensor] = None,
241
+ template_type: Optional[str] = 'multiple_images',
242
+ max_sequence_length: Optional[int] = None,
243
+ drop_vit_feature: Optional[float] = False,
244
+ ):
245
+ assert template_type == 'multiple_images', "template_type must be 'multiple_images'"
246
+ device = device or self._execution_device
247
+ template = self.prompt_template_encode[template_type]
248
+ drop_idx = self.prompt_template_encode_start_idx[template_type]
249
+ prompt = [p.replace(
250
+ '<image>\n', '<|vision_start|><|image_pad|><|vision_end|>') for p in prompt]
251
+ prompt = [template.format(p) for p in prompt]
252
+
253
+ inputs = self.qwen_processor(
254
+ text=prompt,
255
+ images=images,
256
+ padding=True,
257
+ return_tensors="pt",
258
+ ).to(device)
259
+ encoder_hidden_states = self.text_encoder(
260
+ **inputs,
261
+ output_hidden_states=True,
262
+ )
263
+ last_hidden_states = encoder_hidden_states.hidden_states[-1]
264
+ if drop_vit_feature:
265
+ input_ids = inputs['input_ids']
266
+ vlm_image_end_idx = torch.where(input_ids[0] == 151653)[0][-1]
267
+ drop_idx = vlm_image_end_idx + 1
268
+ prompt_embeds = last_hidden_states[:, drop_idx:]
269
+ prompt_embeds_mask = inputs['attention_mask'][:, drop_idx:]
270
+ if max_sequence_length is not None and prompt_embeds.shape[1] > max_sequence_length:
271
+ prompt_embeds = prompt_embeds[:, -max_sequence_length:, :]
272
+ prompt_embeds_mask = prompt_embeds_mask[:, -max_sequence_length:]
273
+ return prompt_embeds, prompt_embeds_mask
274
+
275
+ def encode_prompt_images(
276
+ self,
277
+ prompt: Union[str, List[str]],
278
+ device: Optional[torch.device] = None,
279
+ images: Optional[torch.Tensor] = None,
280
+ max_sequence_length: int = 1024,
281
+ ):
282
+ r"""
283
+
284
+ Args:
285
+ prompt (`str` or `List[str]`, *optional*):
286
+ prompt to be encoded
287
+ device: (`torch.device`):
288
+ torch device
289
+ """
290
+ device = device or self._execution_device
291
+ bs = images.shape[0]
292
+ if images.shape[2] == 1:
293
+ template = self.prompt_template_encode['image']
294
+ drop_idx = self.prompt_template_encode_start_idx['image']
295
+ prompt = template.replace(
296
+ '{}', '<|vision_start|><|image_pad|><|vision_end|>')
297
+ prompt = [prompt] * bs
298
+ inputs = self.qwen_processor(
299
+ text=prompt,
300
+ images=images.squeeze(2),
301
+ padding=True,
302
+ return_tensors="pt",
303
+ ).to(device)
304
+ output_tensor = self.text_encoder(
305
+ **inputs,
306
+ output_hidden_states=True,
307
+ )
308
+ last_hidden_states = output_tensor.hidden_states[-1]
309
+ vis_hidden_states = last_hidden_states[:, drop_idx+3:-6]
310
+ patchify_size = self.qwen_processor.image_processor.merge_size
311
+ image_grid_thw = inputs['image_grid_thw']
312
+ vis_hidden_states = vis_hidden_states.view(
313
+ bs, image_grid_thw[0, 1]//patchify_size, image_grid_thw[0, 2]//patchify_size, -1)
314
+ return vis_hidden_states
315
+
316
+ def encode_prompt(
317
+ self,
318
+ prompt: Union[str, List[str]],
319
+ images: Optional[torch.Tensor] = None,
320
+ device: Optional[torch.device] = None,
321
+ num_videos_per_prompt: int = 1,
322
+ prompt_embeds: Optional[torch.Tensor] = None,
323
+ prompt_embeds_mask: Optional[torch.Tensor] = None,
324
+ max_sequence_length: int = 1024,
325
+ template_type: str = 'image',
326
+ drop_vit_feature: bool = False,
327
+ ):
328
+ r"""
329
+
330
+ Args:
331
+ prompt (`str` or `List[str]`, *optional*):
332
+ prompt to be encoded
333
+ device: (`torch.device`):
334
+ torch device
335
+ num_videos_per_prompt (`int`):
336
+ number of videos that should be generated per prompt
337
+ prompt_embeds (`torch.Tensor`, *optional*):
338
+ Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
339
+ provided, text embeddings will be generated from `prompt` input argument.
340
+ """
341
+ if images is not None:
342
+ ##################################################
343
+ # from PIL import Image
344
+ # images = [img.resize((512, 512), Image.LANCZOS) for img in images]
345
+ ##################################################
346
+ return self.encode_prompt_multiple_images(
347
+ prompt=prompt,
348
+ images=images,
349
+ device=device,
350
+ max_sequence_length=max_sequence_length,
351
+ drop_vit_feature=drop_vit_feature,
352
+ )
353
+ device = device or self._execution_device
354
+
355
+ prompt = [prompt] if isinstance(prompt, str) else prompt
356
+ batch_size = len(
357
+ prompt) if prompt_embeds is None else prompt_embeds.shape[0]
358
+
359
+ if prompt_embeds is None:
360
+ prompt_embeds, prompt_embeds_mask = self._get_qwen_prompt_embeds(
361
+ prompt, template_type, device)
362
+
363
+ prompt_embeds = prompt_embeds[:, :max_sequence_length]
364
+ prompt_embeds_mask = prompt_embeds_mask[:, :max_sequence_length]
365
+
366
+ _, seq_len, _ = prompt_embeds.shape
367
+ prompt_embeds = prompt_embeds.repeat(1, num_videos_per_prompt, 1)
368
+ prompt_embeds = prompt_embeds.view(
369
+ batch_size * num_videos_per_prompt, seq_len, -1)
370
+ prompt_embeds_mask = prompt_embeds_mask.repeat(
371
+ 1, num_videos_per_prompt, 1)
372
+ prompt_embeds_mask = prompt_embeds_mask.view(
373
+ batch_size * num_videos_per_prompt, seq_len)
374
+
375
+ return prompt_embeds, prompt_embeds_mask
376
+
377
+
378
+ def check_inputs(
379
+ self,
380
+ prompt,
381
+ height,
382
+ width,
383
+ images=None,
384
+ negative_prompt=None,
385
+ prompt_embeds=None,
386
+ negative_prompt_embeds=None,
387
+ prompt_embeds_mask=None,
388
+ negative_prompt_embeds_mask=None,
389
+ callback_on_step_end_tensor_inputs=None,
390
+ ):
391
+ # if height % (self.vae_scale_factor * 2) != 0 or width % (self.vae_scale_factor * 2) != 0:
392
+ # logger.warning(
393
+ # f"`height` and `width` have to be divisible by {self.vae_scale_factor * 2} but are {height} and {width}. Dimensions will be resized accordingly"
394
+ # )
395
+
396
+ if callback_on_step_end_tensor_inputs is not None and not all(
397
+ k in self._callback_tensor_inputs for k in callback_on_step_end_tensor_inputs
398
+ ):
399
+ raise ValueError(
400
+ f"`callback_on_step_end_tensor_inputs` has to be in {self._callback_tensor_inputs}, but found {[k for k in callback_on_step_end_tensor_inputs if k not in self._callback_tensor_inputs]}"
401
+ )
402
+
403
+ if prompt is not None and prompt_embeds is not None:
404
+ raise ValueError(
405
+ f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to"
406
+ " only forward one of the two."
407
+ )
408
+ elif prompt is None and prompt_embeds is None:
409
+ raise ValueError(
410
+ "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined."
411
+ )
412
+ elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
413
+ raise ValueError(
414
+ f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
415
+
416
+ if negative_prompt is not None and negative_prompt_embeds is not None:
417
+ raise ValueError(
418
+ f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:"
419
+ f" {negative_prompt_embeds}. Please make sure to only forward one of the two."
420
+ )
421
+
422
+ if prompt_embeds is not None and prompt_embeds_mask is None:
423
+ raise ValueError(
424
+ "If `prompt_embeds` are provided, `prompt_embeds_mask` also have to be passed. Make sure to generate `prompt_embeds_mask` from the same text encoder that was used to generate `prompt_embeds`."
425
+ )
426
+ if negative_prompt_embeds is not None and negative_prompt_embeds_mask is None:
427
+ raise ValueError(
428
+ "If `negative_prompt_embeds` are provided, `negative_prompt_embeds_mask` also have to be passed. Make sure to generate `negative_prompt_embeds_mask` from the same text encoder that was used to generate `negative_prompt_embeds`."
429
+ )
430
+
431
+ def prepare_conditions(self, latents: torch.Tensor, image=None, last_image=None):
432
+ """
433
+ Prepare conditional inputs for video generation.
434
+
435
+ Args:
436
+ latents: Generated latent tensor with shape (B, N, C, T, latent_H, latent_W)
437
+ image: First frame condition, shape (B, N, 3, 1, H, W)
438
+ last_image: Last frame condition, shape (B, N, 3, 1, H, W)
439
+
440
+ Returns:
441
+ Combined condition tensor with shape (B, N, C+1, T, H, W)
442
+ """
443
+ device, dtype = latents.device, latents.dtype
444
+ batch_size, num_items, latent_channels, latent_frames, latent_h, latent_w = latents.shape
445
+
446
+ # If no conditions provided, return zero condition
447
+ if image is None and last_image is None:
448
+ return torch.zeros(
449
+ batch_size, num_items, latent_channels + 1, latent_frames, latent_h, latent_w,
450
+ device=device, dtype=dtype
451
+ )
452
+
453
+ num_frame = (latent_frames - 1) * self.vae_scale_factor_temporal + 1
454
+ height = latent_h * self.vae_scale_factor
455
+ width = latent_w * self.vae_scale_factor
456
+
457
+ # Initialize mask
458
+ mask = torch.zeros(batch_size, num_items, 1, latent_frames,
459
+ latent_h, latent_w, device=device, dtype=dtype)
460
+
461
+ # Build video condition
462
+ if image is not None and last_image is not None:
463
+ # Both first and last frame conditions
464
+ image = image.to(device=device, dtype=dtype)
465
+ last_image = last_image.to(device=device, dtype=dtype)
466
+
467
+ middle_frames = torch.zeros(
468
+ batch_size, num_items, image.shape[2], num_frame -
469
+ 2, height, width,
470
+ device=device, dtype=dtype
471
+ )
472
+ video_condition = torch.cat(
473
+ [image, middle_frames, last_image], dim=3)
474
+ mask[:, :, :, 0] = 1 # Mark first frame as conditional
475
+ mask[:, :, :, -1] = 1 # Mark last frame as conditional
476
+
477
+ elif image is not None:
478
+ # Only first frame condition
479
+ image = image.to(device=device, dtype=dtype)
480
+ remaining_frames = torch.zeros(
481
+ batch_size, num_items, image.shape[2], num_frame -
482
+ 1, height, width,
483
+ device=device, dtype=dtype
484
+ )
485
+ video_condition = torch.cat([image, remaining_frames], dim=3)
486
+ mask[:, :, :, 0] = 1 # Mark first frame as conditional
487
+ else:
488
+ raise NotImplementedError
489
+
490
+ # VAE encode the video condition
491
+ video_condition = rearrange(
492
+ video_condition, "b n c t h w -> (b n) c t h w")
493
+ latent_condition = self.vae.encode(
494
+ video_condition).latent_dist.sample()
495
+
496
+ # Normalize
497
+ latent_condition = self.normalize_latents(latent_condition)
498
+
499
+ # Reshape back to (B, N, C, T, H, W)
500
+ latent_condition = rearrange(
501
+ latent_condition, "(b n) c t h w -> b n c t h w", b=batch_size)
502
+
503
+ # Concat
504
+ return torch.cat([latent_condition, mask], dim=2)
505
+
506
+ def prepare_latents(
507
+ self,
508
+ batch_size,
509
+ num_items,
510
+ num_channels_latents,
511
+ height,
512
+ width,
513
+ video_length,
514
+ dtype,
515
+ device,
516
+ generator,
517
+ latents=None,
518
+ reference_images=None,
519
+ image=None,
520
+ last_image=None,
521
+ ):
522
+ shape = (
523
+ batch_size,
524
+ num_items,
525
+ num_channels_latents,
526
+ (video_length - 1) // self.vae_scale_factor_temporal + 1,
527
+ int(height) // self.vae_scale_factor,
528
+ int(width) // self.vae_scale_factor,
529
+ )
530
+ if isinstance(generator, list) and len(generator) != batch_size:
531
+ raise ValueError(
532
+ f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
533
+ f" size of {batch_size}. Make sure the batch size matches the length of the generators."
534
+ )
535
+
536
+ if latents is None:
537
+ if reference_images is not None:
538
+ ref_img = [torch.from_numpy(
539
+ np.array(x.convert("RGB"))) for x in reference_images]
540
+ ref_img = torch.stack(ref_img).to(device=device, dtype=dtype)
541
+ ref_img = ref_img / 127.5 - 1.0
542
+ ref_img = rearrange(ref_img, "x h w c -> x c 1 h w")
543
+ ref_vae = self.vae.encode(ref_img)
544
+
545
+ ref_vae = rearrange(
546
+ ref_vae, "(b n) c 1 h w -> b n c 1 h w", n=(num_items - 1))
547
+ noise = randn_tensor(
548
+ (shape[0], 1, *shape[2:]),
549
+ generator=generator, device=device, dtype=dtype
550
+ )
551
+ latents = torch.cat([ref_vae, noise], dim=1)
552
+ else:
553
+ latents = randn_tensor(
554
+ shape, generator=generator, device=device, dtype=dtype
555
+ )
556
+ else:
557
+ latents = latents.to(device)
558
+
559
+ if not self.enable_multi_task:
560
+ return latents, None
561
+
562
+ # image: (b, n, c, 1, h, w), last_image: (b, n, c, 1, h, w)
563
+ condition = self.prepare_conditions(latents, image, last_image)
564
+
565
+ return latents, condition
566
+
567
+ @property
568
+ def guidance_scale(self):
569
+ return self._guidance_scale
570
+
571
+ # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2)
572
+ # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1`
573
+ # corresponds to doing no classifier free guidance.
574
+ @property
575
+ def do_classifier_free_guidance(self):
576
+ # return self._guidance_scale > 1 and self.transformer.config.time_cond_proj_dim is None
577
+ return self._guidance_scale > 1
578
+
579
+ @property
580
+ def num_timesteps(self):
581
+ return self._num_timesteps
582
+
583
+ @property
584
+ def interrupt(self):
585
+ return self._interrupt
586
+
587
+ def pad_sequence(self, x: torch.Tensor, target_length: int):
588
+ current_length = x.shape[1]
589
+ if current_length >= target_length:
590
+ return x[:, -target_length:]
591
+ padding_length = target_length - current_length
592
+ if x.ndim >= 3:
593
+ padding = torch.zeros(
594
+ (x.shape[0], padding_length, *x.shape[2:]),
595
+ dtype=x.dtype, device=x.device
596
+ )
597
+ else:
598
+ padding = torch.zeros(
599
+ (x.shape[0], padding_length),
600
+ dtype=x.dtype, device=x.device
601
+ )
602
+ return torch.cat([x, padding], dim=1)
603
+
604
+ @torch.no_grad()
605
+ @replace_example_docstring(EXAMPLE_DOC_STRING)
606
+ def __call__(
607
+ self,
608
+ prompt: Union[str, List[str]],
609
+ height: int,
610
+ width: int,
611
+ num_frames: int,
612
+ images: Optional[torch.Tensor] = None,
613
+ image_condition: torch.Tensor | None = None,
614
+ last_image_condition: torch.Tensor | None = None,
615
+ data_type: str = "video",
616
+ num_inference_steps: int = 50,
617
+ timesteps: List[int] = None,
618
+ sigmas: List[float] = None,
619
+ guidance_scale: float = 7.5,
620
+ negative_prompt: Optional[Union[str, List[str]]] = None,
621
+ num_videos_per_prompt: Optional[int] = 1,
622
+ eta: float = 0.0,
623
+ generator: Optional[Union[torch.Generator,
624
+ List[torch.Generator]]] = None,
625
+ latents: Optional[torch.Tensor] = None,
626
+ prompt_embeds: Optional[torch.Tensor] = None,
627
+ prompt_embeds_mask: Optional[torch.Tensor] = None,
628
+ negative_prompt_embeds: Optional[torch.Tensor] = None,
629
+ negative_prompt_embeds_mask: Optional[torch.Tensor] = None,
630
+ output_type: Optional[str] = "pil",
631
+ return_dict: bool = True,
632
+ cross_attention_kwargs: Optional[Dict[str, Any]] = None,
633
+ guidance_rescale: float = 0.0,
634
+ clip_skip: Optional[int] = None,
635
+ callback_on_step_end: Optional[
636
+ Union[
637
+ Callable[[int, int, Dict], None],
638
+ PipelineCallback,
639
+ MultiPipelineCallbacks,
640
+ ]
641
+ ] = None,
642
+ callback_on_step_end_tensor_inputs: List[str] = ["latents"],
643
+ enable_tiling: bool = False,
644
+ max_sequence_length: int = 4096,
645
+ drop_vit_feature: bool = False,
646
+ **kwargs,
647
+ ):
648
+ r"""
649
+ The call function to the pipeline for generation.
650
+
651
+ Args:
652
+ prompt (`str` or `List[str]`):
653
+ The prompt or prompts to guide image generation. If not defined, you need to pass `prompt_embeds`.
654
+ height (`int`):
655
+ The height in pixels of the generated image.
656
+ width (`int`):
657
+ The width in pixels of the generated image.
658
+ video_length (`int`):
659
+ The number of frames in the generated video.
660
+ num_inference_steps (`int`, *optional*, defaults to 50):
661
+ The number of denoising steps. More denoising steps usually lead to a higher quality image at the
662
+ expense of slower inference.
663
+ timesteps (`List[int]`, *optional*):
664
+ Custom timesteps to use for the denoising process with schedulers which support a `timesteps` argument
665
+ in their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is
666
+ passed will be used. Must be in descending order.
667
+ sigmas (`List[float]`, *optional*):
668
+ Custom sigmas to use for the denoising process with schedulers which support a `sigmas` argument in
669
+ their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is passed
670
+ will be used.
671
+ guidance_scale (`float`, *optional*, defaults to 7.5):
672
+ A higher guidance scale value encourages the model to generate images closely linked to the text
673
+ `prompt` at the expense of lower image quality. Guidance scale is enabled when `guidance_scale > 1`.
674
+ negative_prompt (`str` or `List[str]`, *optional*):
675
+ The prompt or prompts to guide what to not include in image generation. If not defined, you need to
676
+ pass `negative_prompt_embeds` instead. Ignored when not using guidance (`guidance_scale < 1`).
677
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
678
+ The number of images to generate per prompt.
679
+ eta (`float`, *optional*, defaults to 0.0):
680
+ Corresponds to parameter eta (η) from the [DDIM](https://arxiv.org/abs/2010.02502) paper. Only applies
681
+ to the [`~schedulers.DDIMScheduler`], and is ignored in other schedulers.
682
+ generator (`torch.Generator` or `List[torch.Generator]`, *optional*):
683
+ A [`torch.Generator`](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make
684
+ generation deterministic.
685
+ latents (`torch.Tensor`, *optional*):
686
+ Pre-generated noisy latents sampled from a Gaussian distribution, to be used as inputs for image
687
+ generation. Can be used to tweak the same generation with different prompts. If not provided, a latents
688
+ tensor is generated by sampling using the supplied random `generator`.
689
+ prompt_embeds (`torch.Tensor`, *optional*):
690
+ Pre-generated text embeddings. Can be used to easily tweak text inputs (prompt weighting). If not
691
+ provided, text embeddings are generated from the `prompt` input argument.
692
+ negative_prompt_embeds (`torch.Tensor`, *optional*):
693
+ Pre-generated negative text embeddings. Can be used to easily tweak text inputs (prompt weighting). If
694
+ not provided, `negative_prompt_embeds` are generated from the `negative_prompt` input argument.
695
+
696
+ output_type (`str`, *optional*, defaults to `"pil"`):
697
+ The output format of the generated image. Choose between `PIL.Image` or `np.array`.
698
+ return_dict (`bool`, *optional*, defaults to `True`):
699
+ Whether or not to return a [`HunyuanVideoPipelineOutput`] instead of a
700
+ plain tuple.
701
+ cross_attention_kwargs (`dict`, *optional*):
702
+ A kwargs dictionary that if specified is passed along to the [`AttentionProcessor`] as defined in
703
+ [`self.processor`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
704
+ guidance_rescale (`float`, *optional*, defaults to 0.0):
705
+ Guidance rescale factor from [Common Diffusion Noise Schedules and Sample Steps are
706
+ Flawed](https://arxiv.org/pdf/2305.08891.pdf). Guidance rescale factor should fix overexposure when
707
+ using zero terminal SNR.
708
+ clip_skip (`int`, *optional*):
709
+ Number of layers to be skipped from CLIP while computing the prompt embeddings. A value of 1 means that
710
+ the output of the pre-final layer will be used for computing the prompt embeddings.
711
+ callback_on_step_end (`Callable`, `PipelineCallback`, `MultiPipelineCallbacks`, *optional*):
712
+ A function or a subclass of `PipelineCallback` or `MultiPipelineCallbacks` that is called at the end of
713
+ each denoising step during the inference. with the following arguments: `callback_on_step_end(self:
714
+ DiffusionPipeline, step: int, timestep: int, callback_kwargs: Dict)`. `callback_kwargs` will include a
715
+ list of all tensors as specified by `callback_on_step_end_tensor_inputs`.
716
+ callback_on_step_end_tensor_inputs (`List`, *optional*):
717
+ The list of tensor inputs for the `callback_on_step_end` function. The tensors specified in the list
718
+ will be passed as `callback_kwargs` argument. You will only be able to include variables listed in the
719
+ `._callback_tensor_inputs` attribute of your pipeline class.
720
+
721
+ Examples:
722
+
723
+ Returns:
724
+ [`~HunyuanVideoPipelineOutput`] or `tuple`:
725
+ If `return_dict` is `True`, [`HunyuanVideoPipelineOutput`] is returned,
726
+ otherwise a `tuple` is returned where the first element is a list with the generated images and the
727
+ second element is a list of `bool`s indicating whether the corresponding generated image contains
728
+ "not-safe-for-work" (nsfw) content.
729
+ """
730
+ # 1. Check inputs. Raise error if not correct
731
+ self.check_inputs(
732
+ prompt,
733
+ height,
734
+ width,
735
+ images=images,
736
+ negative_prompt=negative_prompt,
737
+ prompt_embeds=prompt_embeds,
738
+ negative_prompt_embeds=negative_prompt_embeds,
739
+ prompt_embeds_mask=prompt_embeds_mask,
740
+ negative_prompt_embeds_mask=negative_prompt_embeds_mask,
741
+ callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs,
742
+ )
743
+
744
+ self._guidance_scale = guidance_scale
745
+ self._interrupt = False
746
+
747
+ # 2. Define call parameters
748
+ if prompt is not None and isinstance(prompt, str):
749
+ batch_size = 1
750
+ elif prompt is not None and isinstance(prompt, list):
751
+ batch_size = len(prompt)
752
+ else:
753
+ batch_size = prompt_embeds.shape[0]
754
+
755
+ device = self.transformer.device
756
+
757
+ # 3. Encode input prompt
758
+ template_type = 'image' if num_frames == 1 else "video"
759
+ num_items = 1 if images is None or len(
760
+ images) == 0 else 1 + len(images)
761
+ prompt_embeds, prompt_embeds_mask = self.encode_prompt(
762
+ prompt=prompt,
763
+ prompt_embeds=prompt_embeds,
764
+ prompt_embeds_mask=prompt_embeds_mask,
765
+ images=images,
766
+ device=device,
767
+ num_videos_per_prompt=num_videos_per_prompt,
768
+ max_sequence_length=max_sequence_length,
769
+ template_type=template_type,
770
+ drop_vit_feature=drop_vit_feature,
771
+ )
772
+ # For classifier free guidance, we need to do two forward passes.
773
+ # Here we concatenate the unconditional and text embeddings into a single batch
774
+ # to avoid doing two forward passes
775
+ if self.do_classifier_free_guidance:
776
+ if negative_prompt is None and negative_prompt_embeds is None:
777
+ # default_negative_prompt = 'low quality, jpeg artifacts, ugly, duplicate, morbid, mutilated, extra fingers, mutated hands, poorly drawn hands, poorly drawn face, mutation, deformed, blurry, dehydrated, bad anatomy, bad proportions, extra limbs, cloned face, disfigured, gross proportions, malformed limbs, missing arms, missing legs, extra arms, extra legs, fused fingers, too many fingers.' # noqa
778
+ default_negative_prompt = ""
779
+ if num_items <= 1:
780
+ negative_prompt = [
781
+ f"<|im_start|>user\n{default_negative_prompt}<|im_end|>\n"] * batch_size
782
+ else:
783
+ image_tokens = "<image>\n" * (num_items - 1)
784
+ negative_prompt = [
785
+ f"<|im_start|>user\n{image_tokens}{default_negative_prompt}<|im_end|>\n"] * batch_size
786
+
787
+ negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt(
788
+ prompt=negative_prompt,
789
+ prompt_embeds=negative_prompt_embeds,
790
+ prompt_embeds_mask=negative_prompt_embeds_mask,
791
+ images=images,
792
+ device=device,
793
+ num_videos_per_prompt=num_videos_per_prompt,
794
+ max_sequence_length=max_sequence_length,
795
+ template_type=template_type,
796
+ )
797
+
798
+ max_seq_len = max(
799
+ prompt_embeds.shape[1], negative_prompt_embeds.shape[1])
800
+ prompt_embeds = torch.cat([
801
+ self.pad_sequence(negative_prompt_embeds, max_seq_len),
802
+ self.pad_sequence(prompt_embeds, max_seq_len)])
803
+ if prompt_embeds_mask is not None:
804
+ prompt_embeds_mask = torch.cat([
805
+ self.pad_sequence(
806
+ negative_prompt_embeds_mask, max_seq_len),
807
+ self.pad_sequence(prompt_embeds_mask, max_seq_len)])
808
+
809
+ # 4. Prepare timesteps
810
+ timesteps, num_inference_steps = retrieve_timesteps(
811
+ self.scheduler,
812
+ num_inference_steps,
813
+ device,
814
+ timesteps,
815
+ sigmas,
816
+ )
817
+
818
+ # 5. Prepare latent variables
819
+ num_channels_latents = self.vae.config.latent_channels
820
+ latents, condition = self.prepare_latents(
821
+ batch_size * num_videos_per_prompt,
822
+ num_items,
823
+ num_channels_latents,
824
+ height,
825
+ width,
826
+ num_frames,
827
+ prompt_embeds.dtype,
828
+ device,
829
+ generator,
830
+ latents,
831
+ reference_images=images,
832
+ image=image_condition,
833
+ last_image=last_image_condition,
834
+ )
835
+
836
+ target_dtype = PRECISION_TO_TYPE[self.args.dit_precision]
837
+ autocast_enabled = (
838
+ target_dtype != torch.float32
839
+ )
840
+ vae_dtype = PRECISION_TO_TYPE[self.args.vae_precision]
841
+ vae_autocast_enabled = (
842
+ vae_dtype != torch.float32
843
+ )
844
+
845
+ # 6. Denoising loop
846
+ num_warmup_steps = len(timesteps) - \
847
+ num_inference_steps * self.scheduler.order
848
+ self._num_timesteps = len(timesteps)
849
+
850
+ if num_items > 1:
851
+ ref_latents = latents[:, :(num_items - 1)].clone()
852
+
853
+ # if is_progress_bar:
854
+ with self.progress_bar(total=num_inference_steps) as progress_bar:
855
+ for i, t in enumerate(timesteps):
856
+ if self.interrupt:
857
+ continue
858
+
859
+ # copy reference latents
860
+ if num_items > 1:
861
+ latents[:, :(num_items - 1)] = ref_latents.clone()
862
+
863
+ # concat condition if enable multi-task
864
+ if condition is not None:
865
+ latents_ = torch.cat([latents, condition], dim=2)
866
+ else:
867
+ latents_ = latents
868
+
869
+ # expand the latents if we are doing classifier free guidance
870
+ latent_model_input = (
871
+ torch.cat([latents_] * 2)
872
+ if self.do_classifier_free_guidance
873
+ else latents_
874
+ )
875
+
876
+ t_expand = t.repeat(latent_model_input.shape[0])
877
+
878
+ # predict the noise residual
879
+ with torch.autocast(
880
+ device_type="cuda", dtype=target_dtype, enabled=autocast_enabled
881
+ ):
882
+ noise_pred = self.transformer( # For an input image (129, 192, 336) (1, 256, 256)
883
+ # [2, 16, 33, 24, 42]
884
+ hidden_states=latent_model_input,
885
+ timestep=t_expand, # [2]
886
+ encoder_hidden_states=prompt_embeds, # [2, 256, 4096]
887
+ # [2, 256]
888
+ encoder_hidden_states_mask=prompt_embeds_mask,
889
+ return_dict=False,
890
+ )[0]
891
+ if (noise_pred.isnan()).any() or (noise_pred.isinf()).any():
892
+ print("handle with nan/inf data")
893
+
894
+ # perform guidance
895
+ if self.do_classifier_free_guidance:
896
+ noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
897
+ noise_pred = noise_pred_uncond + self.guidance_scale * (
898
+ noise_pred_text - noise_pred_uncond
899
+ )
900
+ cond_norm = torch.norm(
901
+ noise_pred_text, dim=2, keepdim=True)
902
+ noise_norm = torch.norm(noise_pred, dim=2, keepdim=True)
903
+ noise_pred = noise_pred * (cond_norm / noise_norm)
904
+
905
+ # compute the previous noisy sample x_t -> x_t-1
906
+ latents = self.scheduler.step(
907
+ noise_pred, t, latents, return_dict=False)[0]
908
+
909
+ if callback_on_step_end is not None:
910
+ callback_kwargs = {}
911
+ for k in callback_on_step_end_tensor_inputs:
912
+ callback_kwargs[k] = locals()[k]
913
+ callback_outputs = callback_on_step_end(
914
+ self, i, t, callback_kwargs)
915
+
916
+ latents = callback_outputs.pop("latents", latents)
917
+ prompt_embeds = callback_outputs.pop(
918
+ "prompt_embeds", prompt_embeds)
919
+ negative_prompt_embeds = callback_outputs.pop(
920
+ "negative_prompt_embeds", negative_prompt_embeds
921
+ )
922
+
923
+ # call the callback, if provided
924
+ if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):
925
+ if progress_bar is not None:
926
+ progress_bar.update()
927
+
928
+ if not output_type == "latent":
929
+ if not (len(latents.shape) == 5 or len(latents.shape) == 6):
930
+ raise ValueError(
931
+ f"Only support latents with shape (b, n, c, h, w) or (b, n, c, f, h, w), but got {latents.shape}."
932
+ )
933
+
934
+ if (latents.isnan()).any() or (latents.isinf()).any():
935
+ print("handle with nan/inf data")
936
+
937
+ # Decode latents (VAE handles denormalization internally via scale)
938
+ latents = rearrange(
939
+ latents, "b n c f h w -> (b n) c f h w")
940
+
941
+ with torch.autocast(
942
+ device_type="cuda", dtype=vae_dtype, enabled=vae_autocast_enabled
943
+ ):
944
+ image = self.vae.decode(
945
+ latents, return_dict=False
946
+ )[0]
947
+ image = rearrange(
948
+ image, "(b n) c f h w -> b n c f h w", b=batch_size)
949
+
950
+ # Replace the first frame with the image condition
951
+ if image_condition is not None:
952
+ image[:, :, :, :1] = image_condition
953
+
954
+ # Replace the last frame with the last image condition
955
+ if last_image_condition is not None:
956
+ image[:, :, :, -1:] = last_image_condition
957
+
958
+ else:
959
+ image = latents
960
+
961
+ image = (image / 2 + 0.5).clamp(0, 1)
962
+ # we always cast to float32 as this does not cause significant overhead and is compatible with bfloa16
963
+ image = image.cpu().float().permute(0, 1, 3, 2, 4, 5)
964
+
965
+ # Offload all models
966
+ self.maybe_free_model_hooks()
967
+
968
+ if not return_dict:
969
+ return image
970
+
971
+ return PipelineOutput(videos=image)
src/modules/models/scheduler.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Stability AI, Katherine Crowson and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ #
16
+ # Modified from diffusers==0.29.2
17
+ #
18
+ # ==============================================================================
19
+
20
+ from dataclasses import dataclass
21
+ from typing import Optional, Tuple, Union
22
+
23
+ import numpy as np
24
+ import torch
25
+
26
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
27
+ from diffusers.utils import BaseOutput, logging
28
+ from diffusers.schedulers.scheduling_utils import SchedulerMixin
29
+
30
+
31
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
32
+
33
+
34
+ @dataclass
35
+ class FlowMatchDiscreteSchedulerOutput(BaseOutput):
36
+ """
37
+ Output class for the scheduler's `step` function output.
38
+
39
+ Args:
40
+ prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images):
41
+ Computed sample `(x_{t-1})` of previous timestep. `prev_sample` should be used as next model input in the
42
+ denoising loop.
43
+ """
44
+
45
+ prev_sample: torch.FloatTensor
46
+
47
+
48
+ class FlowMatchDiscreteScheduler(SchedulerMixin, ConfigMixin):
49
+ """
50
+ Euler scheduler.
51
+
52
+ This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic
53
+ methods the library implements for all schedulers such as loading and saving.
54
+
55
+ Args:
56
+ num_train_timesteps (`int`, defaults to 1000):
57
+ The number of diffusion steps to train the model.
58
+ timestep_spacing (`str`, defaults to `"linspace"`):
59
+ The way the timesteps should be scaled. Refer to Table 2 of the [Common Diffusion Noise Schedules and
60
+ Sample Steps are Flawed](https://huggingface.co/papers/2305.08891) for more information.
61
+ shift (`float`, defaults to 1.0):
62
+ The shift value for the timestep schedule.
63
+ reverse (`bool`, defaults to `True`):
64
+ Whether to reverse the timestep schedule.
65
+ """
66
+
67
+ _compatibles = []
68
+ order = 1
69
+
70
+ @register_to_config
71
+ def __init__(
72
+ self,
73
+ num_train_timesteps: int = 1000,
74
+ shift: float = 1.0,
75
+ reverse: bool = True,
76
+ solver: str = "euler",
77
+ n_tokens: Optional[int] = None,
78
+ ):
79
+ sigmas = torch.linspace(1, 0, num_train_timesteps + 1)
80
+
81
+ if not reverse:
82
+ sigmas = sigmas.flip(0)
83
+
84
+ self.sigmas = sigmas
85
+ # the value fed to model
86
+ self.timesteps = (sigmas[:-1] * num_train_timesteps).to(dtype=torch.float32)
87
+
88
+
89
+ self._step_index = None
90
+ self._begin_index = None
91
+
92
+ self.supported_solver = ["euler"]
93
+ if solver not in self.supported_solver:
94
+ raise ValueError(
95
+ f"Solver {solver} not supported. Supported solvers: {self.supported_solver}"
96
+ )
97
+
98
+ @property
99
+ def step_index(self):
100
+ """
101
+ The index counter for current timestep. It will increase 1 after each scheduler step.
102
+ """
103
+ return self._step_index
104
+
105
+ @property
106
+ def begin_index(self):
107
+ """
108
+ The index for the first timestep. It should be set from pipeline with `set_begin_index` method.
109
+ """
110
+ return self._begin_index
111
+
112
+ # Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.set_begin_index
113
+ def set_begin_index(self, begin_index: int = 0):
114
+ """
115
+ Sets the begin index for the scheduler. This function should be run from pipeline before the inference.
116
+
117
+ Args:
118
+ begin_index (`int`):
119
+ The begin index for the scheduler.
120
+ """
121
+ self._begin_index = begin_index
122
+
123
+ def _sigma_to_t(self, sigma):
124
+ return sigma * self.config.num_train_timesteps
125
+
126
+ def set_timesteps(
127
+ self,
128
+ num_inference_steps: int,
129
+ device: Union[str, torch.device] = None,
130
+ n_tokens: int = None,
131
+ ):
132
+ """
133
+ Sets the discrete timesteps used for the diffusion chain (to be run before inference).
134
+
135
+ Args:
136
+ num_inference_steps (`int`):
137
+ The number of diffusion steps used when generating samples with a pre-trained model.
138
+ device (`str` or `torch.device`, *optional*):
139
+ The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
140
+ n_tokens (`int`, *optional*):
141
+ Number of tokens in the input sequence.
142
+ """
143
+ self.num_inference_steps = num_inference_steps
144
+
145
+ sigmas = torch.linspace(1, 0, num_inference_steps + 1)
146
+
147
+ sigmas = self.sd3_time_shift(sigmas)
148
+
149
+ if not self.config.reverse:
150
+ sigmas = 1 - sigmas
151
+
152
+ self.sigmas = sigmas
153
+ self.timesteps = (sigmas[:-1] * self.config.num_train_timesteps).to(
154
+ dtype=torch.float32, device=device
155
+ )
156
+
157
+ # Reset step index
158
+ self._step_index = None
159
+
160
+ def index_for_timestep(self, timestep, schedule_timesteps=None):
161
+ if schedule_timesteps is None:
162
+ schedule_timesteps = self.timesteps
163
+
164
+ indices = (schedule_timesteps == timestep).nonzero()
165
+
166
+ # The sigma index that is taken for the **very** first `step`
167
+ # is always the second index (or the last index if there is only 1)
168
+ # This way we can ensure we don't accidentally skip a sigma in
169
+ # case we start in the middle of the denoising schedule (e.g. for image-to-image)
170
+ pos = 1 if len(indices) > 1 else 0
171
+
172
+ return indices[pos].item()
173
+
174
+ def _init_step_index(self, timestep):
175
+ if self.begin_index is None:
176
+ if isinstance(timestep, torch.Tensor):
177
+ timestep = timestep.to(self.timesteps.device)
178
+ self._step_index = self.index_for_timestep(timestep)
179
+ else:
180
+ self._step_index = self._begin_index
181
+
182
+ def scale_model_input(
183
+ self, sample: torch.Tensor, timestep: Optional[int] = None
184
+ ) -> torch.Tensor:
185
+ return sample
186
+
187
+ def sd3_time_shift(self, t: torch.Tensor):
188
+ # print("sd3:self.config.shift",self.config.shift)
189
+ return (self.config.shift * t) / (1 + (self.config.shift - 1) * t)
190
+
191
+ def flux_time_shift(self, t: torch.Tensor):
192
+ # print("flux2:self.config.shift",self.config.shift)
193
+ return (self.config.shift * t) / (1 + (self.config.shift - 1) * t)
194
+ pass
195
+
196
+ def step(
197
+ self,
198
+ model_output: torch.FloatTensor,
199
+ timestep: Union[float, torch.FloatTensor],
200
+ sample: torch.FloatTensor,
201
+ return_dict: bool = True,
202
+ ) -> Union[FlowMatchDiscreteSchedulerOutput, Tuple]:
203
+ """
204
+ Predict the sample from the previous timestep by reversing the SDE. This function propagates the diffusion
205
+ process from the learned model outputs (most often the predicted noise).
206
+
207
+ Args:
208
+ model_output (`torch.FloatTensor`):
209
+ The direct output from learned diffusion model.
210
+ timestep (`float`):
211
+ The current discrete timestep in the diffusion chain.
212
+ sample (`torch.FloatTensor`):
213
+ A current instance of a sample created by the diffusion process.
214
+ generator (`torch.Generator`, *optional*):
215
+ A random number generator.
216
+ n_tokens (`int`, *optional*):
217
+ Number of tokens in the input sequence.
218
+ return_dict (`bool`):
219
+ Whether or not to return a [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] or
220
+ tuple.
221
+
222
+ Returns:
223
+ [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] or `tuple`:
224
+ If return_dict is `True`, [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] is
225
+ returned, otherwise a tuple is returned where the first element is the sample tensor.
226
+ """
227
+
228
+ if (
229
+ isinstance(timestep, int)
230
+ or isinstance(timestep, torch.IntTensor)
231
+ or isinstance(timestep, torch.LongTensor)
232
+ ):
233
+ raise ValueError(
234
+ (
235
+ "Passing integer indices (e.g. from `enumerate(timesteps)`) as timesteps to"
236
+ " `EulerDiscreteScheduler.step()` is not supported. Make sure to pass"
237
+ " one of the `scheduler.timesteps` as a timestep."
238
+ ),
239
+ )
240
+
241
+ if self.step_index is None:
242
+ self._init_step_index(timestep)
243
+
244
+ # Upcast to avoid precision issues when computing prev_sample
245
+ sample = sample.to(torch.float32)
246
+
247
+ dt = self.sigmas[self.step_index + 1] - self.sigmas[self.step_index]
248
+
249
+ if self.config.solver == "euler":
250
+ prev_sample = sample + model_output.to(torch.float32) * dt
251
+ else:
252
+ raise ValueError(
253
+ f"Solver {self.config.solver} not supported. Supported solvers: {self.supported_solver}"
254
+ )
255
+
256
+ # upon completion increase step index by one
257
+ self._step_index += 1
258
+
259
+ if not return_dict:
260
+ return (prev_sample,)
261
+
262
+ return FlowMatchDiscreteSchedulerOutput(prev_sample=prev_sample)
263
+
264
+ def __len__(self):
265
+ return self.config.num_train_timesteps
src/modules/utils/__init__.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ import re
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ from PIL import Image
8
+ import torch
9
+ import torch.distributed as dist
10
+
11
+
12
+ def seed_everything(seed: int | None = None) -> None:
13
+ if seed is not None:
14
+ random.seed(seed)
15
+ np.random.seed(seed)
16
+ torch.manual_seed(seed)
17
+ torch.cuda.manual_seed_all(seed)
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Distributed helpers (replaces modules.distributed.parallel_states)
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def maybe_init_distributed() -> bool:
25
+ """Initialize torch distributed if WORLD_SIZE > 1. Returns True if initialized."""
26
+ world_size = int(os.environ.get('WORLD_SIZE', '1'))
27
+ if world_size <= 1:
28
+ return False
29
+ rank = int(os.environ.get('RANK', '0'))
30
+ dist.init_process_group(backend='nccl', world_size=world_size, rank=rank)
31
+ return True
32
+
33
+
34
+ def clean_dist_env() -> None:
35
+ """Destroy the distributed process group if it was initialized."""
36
+ if dist.is_initialized():
37
+ dist.destroy_process_group()
38
+
39
+
40
+ def _dynamic_resize_from_bucket(image: Image, basesize: int = 512):
41
+ from modules.models.bucket import BucketGroup, generate_video_image_bucket
42
+ from typing import Tuple
43
+ import math
44
+ import torchvision.transforms.functional as TF
45
+
46
+ def resize_center_crop(img: Image.Image, target_size: Tuple[int, int]) -> Image.Image:
47
+ """等比缩放到 >= 目标尺寸,再中心裁剪到目标尺寸。(PIL输入/输出)"""
48
+ w, h = img.size # PIL: (width, height)
49
+ bh, bw = target_size
50
+ scale = max(bh / h, bw / w)
51
+ resize_h, resize_w = math.ceil(h * scale), math.ceil(w * scale)
52
+ img = TF.resize(img, (resize_h, resize_w),
53
+ interpolation=TF.InterpolationMode.BILINEAR, antialias=True)
54
+ img = TF.center_crop(img, target_size)
55
+ return img
56
+
57
+ bucket_config = generate_video_image_bucket(
58
+ basesize=basesize, min_temporal=56, max_temporal=56, bs_img=4, bs_vid=4, bs_mimg=8, min_items=2, max_items=2
59
+ )
60
+ bucket_group = BucketGroup(bucket_config)
61
+ img_w, img_h = image.size
62
+ bucket = bucket_group.find_best_bucket((1, 1, img_h, img_w))
63
+ target_height, target_width = bucket[-2], bucket[-1] # (height, width)
64
+ img_proc = resize_center_crop(image, (target_height, target_width))
65
+ return img_proc
src/modules/utils/constants.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ PRECISION_TO_TYPE = {
4
+ "fp32": torch.float32,
5
+ "fp16": torch.float16,
6
+ "bf16": torch.bfloat16,
7
+ }
src/modules/utils/fsdp_load.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Adapted from https://github.com/hao-ai-lab/FastVideo/blob/main/fastvideo/models/loader/
2
+
3
+ from typing import Generator
4
+ import os
5
+ import contextlib
6
+ from collections.abc import Generator, Callable
7
+
8
+ from tqdm import tqdm
9
+ import torch
10
+ from torch import nn
11
+ from torch.distributed import init_device_mesh, DeviceMesh
12
+ from torch.distributed.checkpoint.state_dict import set_model_state_dict, get_model_state_dict, StateDictOptions
13
+ from torch.distributed.fsdp import CPUOffloadPolicy, MixedPrecisionPolicy, fully_shard
14
+ from safetensors.torch import safe_open
15
+ from modules.utils.logging import get_logger
16
+
17
+
18
+ # TODO(PY): move this to utils elsewhere
19
+ @contextlib.contextmanager
20
+ def set_default_dtype(dtype: torch.dtype) -> Generator[None, None, None]:
21
+ """
22
+ Context manager to set torch's default dtype.
23
+
24
+ Args:
25
+ dtype (torch.dtype): The desired default dtype inside the context manager.
26
+
27
+ Returns:
28
+ ContextManager: context manager for setting default dtype.
29
+
30
+ Example:
31
+ >>> with set_default_dtype(torch.bfloat16):
32
+ >>> x = torch.tensor([1, 2, 3])
33
+ >>> x.dtype
34
+ torch.bfloat16
35
+
36
+
37
+ """
38
+ old_dtype = torch.get_default_dtype()
39
+ torch.set_default_dtype(dtype)
40
+ try:
41
+ yield
42
+ finally:
43
+ torch.set_default_dtype(old_dtype)
44
+
45
+
46
+ # explicitly use pure text format, with a newline at the end
47
+ # this makes it impossible to see the animation in the progress bar
48
+ # but will avoid messing up with ray or multiprocessing, which wraps
49
+ # each line of output with some prefix.
50
+ _BAR_FORMAT = "{desc}: {percentage:3.0f}% Completed | {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]\n" # noqa: E501
51
+
52
+
53
+ def safetensors_weights_iterator(hf_weights_files: list[str]) -> Generator[tuple[str, torch.Tensor], None, None]:
54
+ """Iterate over the weights in the model safetensor files."""
55
+ enable_tqdm = not torch.distributed.is_initialized(
56
+ ) or torch.distributed.get_rank() == 0
57
+ device = "cpu"
58
+ for st_file in tqdm(
59
+ hf_weights_files,
60
+ desc="Loading safetensors checkpoint shards",
61
+ disable=not enable_tqdm,
62
+ bar_format=_BAR_FORMAT,
63
+ ):
64
+ with safe_open(st_file, framework="pt", device=device) as f:
65
+ for name in f.keys(): # noqa: SIM118
66
+ param = f.get_tensor(name)
67
+ yield name, param
68
+
69
+
70
+ def pt_weights_iterator(hf_weights_files: list[str]) -> Generator[tuple[str, torch.Tensor], None, None]:
71
+ """Iterate over the weights in the model bin/pt files."""
72
+ device = "cpu"
73
+ enable_tqdm = not torch.distributed.is_initialized(
74
+ ) or torch.distributed.get_rank() == 0
75
+ for bin_file in tqdm(
76
+ hf_weights_files,
77
+ desc="Loading pt checkpoint shards",
78
+ disable=not enable_tqdm,
79
+ bar_format=_BAR_FORMAT,
80
+ ):
81
+ state = torch.load(bin_file, map_location=device, weights_only=True)
82
+ yield from state.items()
83
+ del state
84
+
85
+
86
+ def maybe_load_fsdp_model(
87
+ model: nn.Module,
88
+ hsdp_shard_dim: int,
89
+ reshard_after_forward: bool,
90
+ param_dtype: torch.dtype,
91
+ reduce_dtype: torch.dtype,
92
+ cpu_offload: bool = False,
93
+ fsdp_inference: bool = False,
94
+ output_dtype: torch.dtype | None = None,
95
+ training_mode: bool = True,
96
+ pin_cpu_memory: bool = True,
97
+ ) -> torch.nn.Module:
98
+ """
99
+ Load the model with FSDP if is training, else load the model without FSDP.
100
+ """
101
+ logger = get_logger()
102
+ mp_policy = MixedPrecisionPolicy(param_dtype,
103
+ reduce_dtype,
104
+ output_dtype,
105
+ cast_forward_inputs=False)
106
+
107
+ # Check if we should use FSDP
108
+ world_size = int(os.getenv("WORLD_SIZE", "1"))
109
+ assert world_size % hsdp_shard_dim == 0, f"world_size {world_size} must be divisible by hsdp_shard_dim {hsdp_shard_dim}"
110
+ hsdp_replicate_dim = world_size // hsdp_shard_dim
111
+
112
+ use_fsdp = training_mode or fsdp_inference
113
+ if hsdp_shard_dim * hsdp_replicate_dim <= 1:
114
+ use_fsdp = False
115
+ logger.warning(
116
+ f"hsdp_replicate_dim * hsdp_shard_dim = {hsdp_replicate_dim}x{hsdp_shard_dim} <= 1, not using FSDP.")
117
+
118
+ if use_fsdp:
119
+ device_mesh = init_device_mesh(
120
+ "cuda",
121
+ # (Replicate(), Shard(dim=0))
122
+ mesh_shape=(hsdp_replicate_dim, hsdp_shard_dim),
123
+ mesh_dim_names=("replicate", "shard"),
124
+ )
125
+ shard_model(model,
126
+ cpu_offload=cpu_offload,
127
+ reshard_after_forward=reshard_after_forward,
128
+ mp_policy=mp_policy,
129
+ mesh=device_mesh,
130
+ fsdp_shard_conditions=model._fsdp_shard_conditions,
131
+ pin_cpu_memory=pin_cpu_memory)
132
+
133
+ return model
134
+
135
+
136
+ def shard_model(
137
+ model,
138
+ *,
139
+ cpu_offload: bool,
140
+ reshard_after_forward: bool = True,
141
+ mp_policy: MixedPrecisionPolicy | None = MixedPrecisionPolicy(), # noqa
142
+ mesh: DeviceMesh | None = None,
143
+ fsdp_shard_conditions: list[Callable[[str, nn.Module], bool]] = [], # noqa
144
+ pin_cpu_memory: bool = True,
145
+ ) -> None:
146
+ """
147
+ Utility to shard a model with FSDP using the PyTorch Distributed fully_shard API.
148
+
149
+ This method will over the model's named modules from the bottom-up and apply shard modules
150
+ based on whether they meet any of the criteria from shard_conditions.
151
+
152
+ Args:
153
+ model (TransformerDecoder): Model to shard with FSDP.
154
+ shard_conditions (List[Callable[[str, nn.Module], bool]]): A list of functions to determine
155
+ which modules to shard with FSDP. Each function should take module name (relative to root)
156
+ and the module itself, returning True if FSDP should shard the module and False otherwise.
157
+ If any of shard_conditions return True for a given module, it will be sharded by FSDP.
158
+ cpu_offload (bool): If set to True, FSDP will offload parameters, gradients, and optimizer
159
+ states to CPU.
160
+ reshard_after_forward (bool): Whether to reshard parameters and buffers after
161
+ the forward pass. Setting this to True corresponds to the FULL_SHARD sharding strategy
162
+ from FSDP1, while setting it to False corresponds to the SHARD_GRAD_OP sharding strategy.
163
+ mesh (Optional[DeviceMesh]): Device mesh to use for FSDP sharding under multiple parallelism.
164
+ Default to None.
165
+ fsdp_shard_conditions (List[Callable[[str, nn.Module], bool]]): A list of functions to determine
166
+ which modules to shard with FSDP.
167
+ pin_cpu_memory (bool): If set to True, FSDP will pin the CPU memory of the offloaded parameters.
168
+
169
+ Raises:
170
+ ValueError: If no layer modules were sharded, indicating that no shard_condition was triggered.
171
+ """
172
+
173
+ if fsdp_shard_conditions is None or len(fsdp_shard_conditions) == 0:
174
+ logger = get_logger()
175
+ logger.warning(
176
+ "The FSDP shard condition list is empty or None. No modules will be sharded in %s",
177
+ type(model).__name__)
178
+ return
179
+
180
+ fsdp_kwargs = {
181
+ "reshard_after_forward": reshard_after_forward,
182
+ "mesh": mesh,
183
+ "mp_policy": mp_policy,
184
+ }
185
+ if cpu_offload:
186
+ fsdp_kwargs["offload_policy"] = CPUOffloadPolicy(
187
+ pin_memory=pin_cpu_memory)
188
+
189
+ # iterating in reverse to start with
190
+ # lowest-level modules first
191
+ num_layers_sharded = 0
192
+ # TODO(will): don't reshard after forward for the last layer to save on the
193
+ # all-gather that will immediately happen Shard the model with FSDP,
194
+ for n, m in reversed(list(model.named_modules())):
195
+ if any([
196
+ shard_condition(n, m)
197
+ for shard_condition in fsdp_shard_conditions
198
+ ]):
199
+ fully_shard(m, **fsdp_kwargs)
200
+ num_layers_sharded += 1
201
+
202
+ if num_layers_sharded == 0:
203
+ raise ValueError(
204
+ "No layer modules were sharded. Please check if shard conditions are working as expected."
205
+ )
206
+
207
+ # Finally shard the entire model to account for any stragglers
208
+ fully_shard(model, **fsdp_kwargs)
src/modules/utils/logging.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import loguru
3
+
4
+ _logger = loguru.logger
5
+
6
+
7
+ class NullLogger:
8
+ def __getattr__(self, name):
9
+ return lambda *args, **kwargs: None
10
+
11
+ def bind(self, **kwargs):
12
+ return self
13
+
14
+
15
+ def setup_logger(exp_dir: str):
16
+ global _logger
17
+
18
+ if int(os.getenv("RANK", 0)) <= 0:
19
+ _logger.add(
20
+ os.path.join(exp_dir, "train.log"),
21
+ level="DEBUG",
22
+ colorize=False,
23
+ backtrace=True,
24
+ diagnose=True,
25
+ encoding="utf-8",
26
+ )
27
+ else:
28
+ _logger = NullLogger()
29
+
30
+ _logger.info(f"Experiment directory created at: {exp_dir}")
31
+ return _logger
32
+
33
+
34
+ def get_logger():
35
+ return _logger
36
+
37
+
38
+ __all__ = ["setup_logger", "get_logger"]
src/modules/utils/utils.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+
3
+
4
+ def get_obj_from_str(string, reload=False):
5
+ module, cls = string.rsplit(".", 1)
6
+ if reload:
7
+ module_imp = importlib.import_module(module)
8
+ importlib.reload(module_imp)
9
+ return getattr(importlib.import_module(module, package=None), cls)
10
+
11
+
12
+ def build_from_config(config, **kwargs):
13
+ if "target" not in config:
14
+ if config in ("__is_first_stage__", "__is_unconditional__"):
15
+ return None
16
+ raise KeyError("Expected key `target` to instantiate.")
17
+
18
+ cls = get_obj_from_str(config["target"])
19
+ params = dict(config.get("params", {}))
20
+ params.update(kwargs)
21
+
22
+ pretrained_path = config.get("pretrained", None)
23
+ if pretrained_path is not None and hasattr(cls, "from_pretrained"):
24
+ return cls.from_pretrained(pretrained_path, **params)
25
+
26
+ obj = cls(**params)
27
+ return obj
test_images/test_1.jpg ADDED

Git LFS Details

  • SHA256: caecc697176da307be0ba889fc7178f935811baf088b3293b233162ce82cd88d
  • Pointer size: 131 Bytes
  • Size of remote file: 103 kB
test_images/test_2.jpg ADDED

Git LFS Details

  • SHA256: cb46250291a7a1ea1ecfcc7a96ce18cae6d6db4a56c7d89ba616be22cdb0a933
  • Pointer size: 131 Bytes
  • Size of remote file: 307 kB
test_images/test_3.png ADDED

Git LFS Details

  • SHA256: d95218cdbc04e3fa1d21c99eaf1d622a8e937671cf16bd0e2aefbcbbe2da4780
  • Pointer size: 131 Bytes
  • Size of remote file: 518 kB
three.min.js ADDED
The diff for this file is too large to render. See raw diff