Spaces:
Running on Zero
Running on Zero
Commit ·
fcfea15
0
Parent(s):
Initial commit with Xet-tracked image assets
Browse files- .gitattributes +38 -0
- .gitignore +7 -0
- LICENSE +190 -0
- README.md +280 -0
- app.py +206 -0
- assets/architecture.png +3 -0
- assets/capability-radar.png +3 -0
- assets/spatial-editing-showcase.png +3 -0
- assets/spatial-reasoning-showcase.png +3 -0
- assets/text-rendering-showcase.png +3 -0
- demo_release.py +0 -0
- images/example_1.jpg +3 -0
- images/example_2.png +3 -0
- images/example_3.jpg +3 -0
- images/example_4.png +3 -0
- inference.py +126 -0
- inference_und.py +183 -0
- pyproject.toml +31 -0
- requirements.txt +17 -0
- src/infer_runtime/__init__.py +1 -0
- src/infer_runtime/checkpoints.py +63 -0
- src/infer_runtime/infer_config.py +53 -0
- src/infer_runtime/model.py +139 -0
- src/infer_runtime/prompt_rewrite.py +107 -0
- src/infer_runtime/settings.py +70 -0
- src/modules/__init__.py +0 -0
- src/modules/models/__init__.py +131 -0
- src/modules/models/attention.py +119 -0
- src/modules/models/bucket.py +108 -0
- src/modules/models/mmdit/dit/__init__.py +4 -0
- src/modules/models/mmdit/dit/models.py +539 -0
- src/modules/models/mmdit/dit/modulate_layers.py +80 -0
- src/modules/models/mmdit/dit/posemb_layers.py +320 -0
- src/modules/models/mmdit/text_encoder/__init__.py +23 -0
- src/modules/models/mmdit/vae/__init__.py +3 -0
- src/modules/models/mmdit/vae/wanvae.py +697 -0
- src/modules/models/pipeline.py +971 -0
- src/modules/models/scheduler.py +265 -0
- src/modules/utils/__init__.py +65 -0
- src/modules/utils/constants.py +7 -0
- src/modules/utils/fsdp_load.py +208 -0
- src/modules/utils/logging.py +38 -0
- src/modules/utils/utils.py +27 -0
- test_images/test_1.jpg +3 -0
- test_images/test_2.jpg +3 -0
- test_images/test_3.png +3 -0
- 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 |
+
[](https://joyai-image.s3.cn-north-1.jdcloud-oss.com/JoyAI-Image.pdf)
|
| 18 |
+
[](https://github.com/jd-opensource/JoyAI-Image)
|
| 19 |
+
[](https://huggingface.co/jdopensource/JoyAI-Image-Edit) 
|
| 20 |
+
[](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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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
|
assets/capability-radar.png
ADDED
|
Git LFS Details
|
assets/spatial-editing-showcase.png
ADDED
|
Git LFS Details
|
assets/spatial-reasoning-showcase.png
ADDED
|
Git LFS Details
|
assets/text-rendering-showcase.png
ADDED
|
Git LFS Details
|
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
|
images/example_2.png
ADDED
|
Git LFS Details
|
images/example_3.jpg
ADDED
|
Git LFS Details
|
images/example_4.png
ADDED
|
Git LFS Details
|
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
|
test_images/test_2.jpg
ADDED
|
Git LFS Details
|
test_images/test_3.png
ADDED
|
Git LFS Details
|
three.min.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|